using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using UniJSON; using UnityEngine; namespace UniGLTF { public static class GltfJsonUtil { const string EXTENSION_USED_KEY = "extensionUsed"; /// /// JsonPath を 再帰的に列挙する /// object[] の中身は int(array index) or string(object key) /// /// /// /// public static IEnumerable TraverseJsonPath(JsonNode node, List path) { if (path == null) { path = new List(); } yield return path.ToArray(); if (node.IsArray()) { int i = 0; foreach (var child in node.ArrayItems()) { path.Add(i); foreach (var x in TraverseJsonPath(child, path)) { yield return x; } path.RemoveAt(path.Count - 1); ++i; } } else if (node.IsMap()) { foreach (var kv in node.ObjectItems()) { path.Add(kv.Key.GetString()); foreach (var x in TraverseJsonPath(kv.Value, path)) { yield return x; } path.RemoveAt(path.Count - 1); } } } static string DoubleQuote(string src) { return $"\"{src}\""; } /// /// jsonPath が /// /// [..., "extensions", "EXTENSION_NAME"] /// /// で有る場合に EXTENSION_NAME を返す。 /// /// /// /// static bool TryGetExtensionName(object[] path, out string extensionName) { if (path.Length >= 2) { if (path[path.Length - 2] is string x) { if (x == "extensions") { if (path[path.Length - 1] is string y) { extensionName = y; return true; } else { // ありえない。はず var join = string.Join(", ", path); Debug.LogWarning($"invalid json path: {join}"); } } } } extensionName = default; return false; } static void CopyJson(IReadOnlyList extensionUsed, JsonFormatter dst, JsonNode src, int level) { if (src.IsArray()) { dst.BeginList(); foreach (var v in src.ArrayItems()) { CopyJson(extensionUsed, dst, v, level + 1); } dst.EndList(); } else if (src.IsMap()) { if (level == 0) { // 最上層だけ extensionsUsed の処理をする var done = false; dst.BeginMap(); foreach (var kv in src.ObjectItems()) { var key = kv.Key.GetString(); if (key == EXTENSION_USED_KEY) { if (extensionUsed.Count == 0) { // skip } else { dst.Key(key); // replace dst.BeginList(); foreach (var ex in extensionUsed) { dst.Value(ex); } dst.EndList(); // 処理済 } done = true; } else { dst.Key(key); CopyJson(extensionUsed, dst, kv.Value, level + 1); } } if (!done && level == 0 && extensionUsed.Count > 0) { // add dst.Key(EXTENSION_USED_KEY); dst.BeginList(); foreach (var ex in extensionUsed) { dst.Value(ex); } dst.EndList(); } dst.EndMap(); } else { dst.BeginMap(); foreach (var kv in src.ObjectItems()) { dst.Key(kv.Key.GetUtf8String()); CopyJson(extensionUsed, dst, kv.Value, level + 1); } dst.EndMap(); } } else { // leaf dst.Value(src); } } /// /// https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/schema/glTF.schema.json /// /// extensionUsed の更新を各拡張自身にやらせるのは無駄だし、手動でコントロールするのも間違いの元である。 /// 完成品の JSON から後付けで作ることにした。 /// /// * Exporter しか使わない処理なので、GC, 処理速度は気にしてない /// /// /// /// public static string FindUsedExtensionsAndUpdateJson(string src) { var parsed = src.ParseAsJson(); // unique な extension 名を収集 var used = new HashSet(); foreach (var path in TraverseJsonPath(parsed, null)) { if (TryGetExtensionName(path, out string extensionName)) { used.Add(extensionName); } } // json 加工 var f = new JsonFormatter(); CopyJson(used.ToArray(), f, parsed, 0); // bom無しutf8 var bytes = f.GetStoreBytes(); var utf8 = new UTF8Encoding(false); return utf8.GetString(bytes.Array, bytes.Offset, bytes.Count); } } }