134 lines
4.7 KiB
Python
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)
|