Files
Nero Frame 13 4617c47ad0 Added sanity check baseline made by claude.
Also changed the location to a panel in VIEW_3D instead of menu.
2026-10-13 16:33:03 +02:00

134 lines
4.7 KiB
Python

import difflib
import numpy as np
from ... import config
from ...core import Check, Issue, SkipCheck, contexts
from ...core import char_reference as ref
from ...core.mesh import polygon_arrays
def _require_asset(ctx):
if not ctx.asset_name:
raise SkipCheck("Unknown character name: set it in the panel or follow the file naming convention")
return ctx.asset_name
def _require_reference(ctx):
path = ref.reference_path(ctx.prefs)
data = ref.load_reference(path)
if data is None:
raise SkipCheck(
"No character reference: export one from the master character "
"and set its path in the add-on preferences"
+ (f" (not found: {path})" if path else "")
)
return data
def _body(ctx, asset):
name = ref.body_name(asset)
ob = ctx.scene.objects.get(name)
if ob is None or ob.type != "MESH":
return None, Issue(f"Body mesh '{name}' not found")
return ob, None
class CharPartsNamingCheck(Check):
id = "char_parts_naming"
label = "Inner mouth & eyes naming"
description = (
"Inner mouth and eye objects carry exactly the names of the reference character "
"(with this character's name)."
)
category = "Character"
contexts = frozenset({contexts.CHAR_MOD})
editable_names = True
def run(self, ctx):
asset = _require_asset(ctx)
reference = _require_reference(ctx)
expected = {name.format(asset=asset) for name in reference.get("parts", [])}
found = {ob.name for ob in ref.part_objects(ctx.objects())}
for name in sorted(expected - found):
yield Issue(f"Missing '{name}'")
for name in sorted(found - expected):
close = difflib.get_close_matches(name, sorted(expected - found) or sorted(expected), n=1)
hint = f" (did you mean '{close[0]}'?)" if close else ""
yield Issue.for_id(f"Unexpected name '{name}'{hint}", ctx.scene.objects[name])
class BodyTopologyCheck(Check):
id = "body_topology"
label = "Body vertex order"
description = "The body mesh has the same vertex count and vertex order as the reference character."
category = "Character"
contexts = frozenset({contexts.CHAR_MOD})
def run(self, ctx):
asset = _require_asset(ctx)
reference = _require_reference(ctx)
body, issue = _body(ctx, asset)
if issue:
yield issue
return
expected = reference["body"]
current = ref.topology_signature(body.data)
if current["hash"] == expected["hash"]:
return
counts = [
f"{key} {current[key]} vs {expected[key]}"
for key in ("vertices", "edges", "faces")
if current[key] != expected[key]
]
if counts:
yield Issue.for_id(f"{body.name}: different topology ({', '.join(counts)})", body)
return
# Same counts: find the faces whose vertex indices differ.
ref_totals, ref_loops = ref.decode_topology(expected)
_, totals, loops = polygon_arrays(body.data)
if not np.array_equal(totals, ref_totals):
bad = np.flatnonzero(totals != ref_totals)
else:
poly_of_loop = np.repeat(np.arange(len(totals)), totals)
bad = np.unique(poly_of_loop[loops != ref_loops])
yield Issue.for_id(
f"{body.name}: vertex order differs from '{reference.get('source_asset', 'reference')}' "
f"on {bad.size} face(s)",
body,
elements=bad.tolist(),
element_type="FACE",
)
class BodyVertexGroupsCheck(Check):
id = "body_vertex_groups"
label = "Body vertex groups"
description = "The body mesh has the vertex groups required by production (config CHAR_BODY_VERTEX_GROUPS)."
category = "Character"
contexts = frozenset({contexts.CHAR_MOD})
fix_description = "Create the missing (empty) vertex groups"
def run(self, ctx):
if not config.CHAR_BODY_VERTEX_GROUPS:
raise SkipCheck("No required vertex group defined in config CHAR_BODY_VERTEX_GROUPS")
asset = _require_asset(ctx)
body, issue = _body(ctx, asset)
if issue:
yield issue
return
missing = [name for name in config.CHAR_BODY_VERTEX_GROUPS if name not in body.vertex_groups]
if missing:
yield Issue.for_id(f"{body.name}: missing {', '.join(missing)}", body, data={"missing": missing})
def fix(self, ctx, issues):
for issue in issues:
body = issue.get_id()
if body is None:
continue
for name in issue.data.get("missing", []):
body.vertex_groups.new(name=name)