213 lines
7.0 KiB
C#

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";
/// <summary>
/// JsonPath を 再帰的に列挙する
/// object[] の中身は int(array index) or string(object key)
/// </summary>
/// <param name="node"></param>
/// <param name="path"></param>
/// <returns></returns>
public static IEnumerable<object[]> TraverseJsonPath(JsonNode node, List<object> path)
{
if (path == null)
{
path = new List<object>();
}
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}\"";
}
/// <summary>
/// jsonPath が
///
/// [..., "extensions", "EXTENSION_NAME"]
///
/// で有る場合に EXTENSION_NAME を返す。
/// </summary>
/// <param name="jsonPath"></param>
/// <param name="extensionName"></param>
/// <returns></returns>
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<string> 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);
}
}
/// <summary>
/// https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/schema/glTF.schema.json
///
/// extensionUsed の更新を各拡張自身にやらせるのは無駄だし、手動でコントロールするのも間違いの元である。
/// 完成品の JSON から後付けで作ることにした。
///
/// * Exporter しか使わない処理なので、GC, 処理速度は気にしてない
///
/// </summary>
/// <param name="src"></param>
/// <returns></returns>
public static string FindUsedExtensionsAndUpdateJson(string src)
{
var parsed = src.ParseAsJson();
// unique な extension 名を収集
var used = new HashSet<string>();
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);
}
}
}