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:
Nero Frame 13 committed 2026-10-13 16:33:03 +02:00
1 parent 71b1a61b5c
commit 4617c47ad0
33 files changed
+2771 -52

No files matched your search

+110
View File
@@ -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