Added sanity check baseline made by claude.
Also changed the location to a panel in VIEW_3D instead of menu.
This commit is contained in:
1 parent
71b1a61b5c
commit
4617c47ad0
33 files changed
+2771
-52
No files matched your search
@@ -0,0 +1,110 @@
|
||||
"""Character reference: topology and part names exported from a master character.
|
||||
|
||||
The reference JSON lets every character file be compared with the master
|
||||
character without opening it:
|
||||
|
||||
- ``parts``: inner mouth / eye object names, with the character name replaced
|
||||
by ``{asset}`` (e.g. ``GEO_{asset}_Eye_L``).
|
||||
- ``body``: vertex/edge/face counts and the face vertex indices of the body
|
||||
mesh, used to compare vertex order.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import zlib
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
from .. import config
|
||||
from .mesh import polygon_arrays
|
||||
|
||||
FORMAT_VERSION = 1
|
||||
|
||||
|
||||
def templated(name, asset):
|
||||
"""Replace the ``asset`` token of an underscore separated name by ``{asset}``."""
|
||||
return "_".join("{asset}" if token == asset else token for token in name.split("_"))
|
||||
|
||||
|
||||
def part_objects(objects):
|
||||
keywords = [k.lower() for k in config.CHAR_PART_KEYWORDS]
|
||||
return [ob for ob in objects if any(k in ob.name.lower() for k in keywords)]
|
||||
|
||||
|
||||
def body_name(asset):
|
||||
return config.CHAR_BODY_NAME.format(asset=asset)
|
||||
|
||||
|
||||
def _encode(array):
|
||||
return base64.b64encode(zlib.compress(np.ascontiguousarray(array, dtype=np.int32).tobytes())).decode("ascii")
|
||||
|
||||
|
||||
def _decode(text):
|
||||
return np.frombuffer(zlib.decompress(base64.b64decode(text)), dtype=np.int32)
|
||||
|
||||
|
||||
def topology_signature(mesh):
|
||||
_, totals, loop_verts = polygon_arrays(mesh)
|
||||
digest = hashlib.sha1(totals.tobytes() + loop_verts.tobytes()).hexdigest()
|
||||
return {
|
||||
"vertices": len(mesh.vertices),
|
||||
"edges": len(mesh.edges),
|
||||
"faces": len(mesh.polygons),
|
||||
"hash": digest,
|
||||
"loop_totals": _encode(totals),
|
||||
"loop_vertices": _encode(loop_verts),
|
||||
}
|
||||
|
||||
|
||||
def decode_topology(body):
|
||||
return _decode(body["loop_totals"]), _decode(body["loop_vertices"])
|
||||
|
||||
|
||||
def reference_path(prefs=None):
|
||||
if prefs is not None and getattr(prefs, "char_reference_path", ""):
|
||||
return bpy.path.abspath(prefs.char_reference_path)
|
||||
return os.environ.get("WTD_CHAR_REFERENCE", "") or config.CHAR_REFERENCE_PATH
|
||||
|
||||
|
||||
_cache: dict[str, tuple[float, dict]] = {}
|
||||
|
||||
|
||||
def load_reference(path):
|
||||
"""Load a reference JSON, cached by modification time. None if missing."""
|
||||
if not path or not os.path.isfile(path):
|
||||
return None
|
||||
mtime = os.path.getmtime(path)
|
||||
cached = _cache.get(path)
|
||||
if cached and cached[0] == mtime:
|
||||
return cached[1]
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
_cache[path] = (mtime, data)
|
||||
return data
|
||||
|
||||
|
||||
def export_reference(scene, asset, path):
|
||||
"""Write the reference of the character ``asset`` found in ``scene``."""
|
||||
body = scene.objects.get(body_name(asset))
|
||||
if body is None or body.type != "MESH":
|
||||
raise ValueError(f"Body mesh '{body_name(asset)}' not found in the scene")
|
||||
if body.mode == "EDIT":
|
||||
body.update_from_editmode()
|
||||
|
||||
parts = sorted({templated(ob.name, asset) for ob in part_objects(scene.objects)})
|
||||
data = {
|
||||
"format_version": FORMAT_VERSION,
|
||||
"source_asset": asset,
|
||||
"source_file": bpy.data.filepath,
|
||||
"parts": parts,
|
||||
"body": topology_signature(body.data),
|
||||
}
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump(data, fh, indent=2)
|
||||
_cache.pop(path, None)
|
||||
return data
|
||||
Reference in new issue
Block a user