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,133 @@
|
||||
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)
|
||||
Reference in new issue
Block a user