refactor(kit): SM_ asset naming and topology validation per the modeling checklist

- Kit pieces export as SM_Kit_* (Unreal static-mesh convention) while the Blender
  objects keep their readable names; the kit loader strips the prefix so layout
  still addresses pieces by short name.
- facade_kit_build.py validates each piece before handoff (non-manifold edges,
  loose vertices, triangle count) and records the result in the part report:
  16 pieces, all manifold, no loose geometry, 12 tris each.
- ue_import_facade_kit.py removes kit assets the current build no longer produces,
  so a rename cannot leave two generations of the same piece behind.
- Kit re-imported and re-applied (625 pieces); packaged QA still PASS.
This commit is contained in:
zwitschi committed 2026-11-08 19:38:29 +01:00
1 parent 32dff056c3
commit 1b069188a2
60 files changed
+154 -97

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14
View File
@@ -612,3 +612,17 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
- Lesson: after any blockout regeneration, probe the staged buildings' world X spans
and compare them with `Geo/verification/2026-11-06-el-capitan-block.md` before
trusting a render. A shift of this size is invisible in a screenshot.
## 2026-11-09 — kit naming + topology validation (blender-modeler checklist)
- Kit assets now follow the Unreal convention: the Blender object keeps its readable
name (`Kit_Pilaster`), the exported asset and file are `SM_Kit_Pilaster`. The
kit loader strips the `SM_` prefix so the layout code still addresses pieces by
short name.
- `facade_kit_build.py::validate()` runs the blender-modeler cleanup checklist on
every piece before handoff — non-manifold edges, loose vertices, triangle count —
and records the result in the part report. Current kit: 16 pieces, **all
manifold, no loose geometry, 12 tris each**.
- `ue_import_facade_kit.py` now deletes kit assets that the current build no longer
produces, so a rename cannot leave two generations of the same piece in the
folder (the rename from `Kit_*` to `SM_Kit_*` did exactly that until it did).
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+31 -1
View File
@@ -76,11 +76,39 @@ def make_material(name, value):
return mat
def validate(obj, tri_budget=4000):
"""Topology check before handoff (blender-modeler cleanup checklist).
Reports non-manifold edges, loose geometry and the triangle count. Kit pieces
are primitive solids, so any non-manifold edge means a modelling slip rather
than an intentional hole.
"""
import bmesh
me = obj.data
bm = bmesh.new()
bm.from_mesh(me)
bm.edges.ensure_lookup_table()
non_manifold = [e for e in bm.edges if len(e.link_faces) != 2]
loose_verts = [v for v in bm.verts if not v.link_edges]
tri_count = sum(len(f.verts) - 2 for f in bm.faces)
bm.free()
if non_manifold or loose_verts:
print(f"[kit] WARNING {obj.name}: {len(non_manifold)} non-manifold edge(s), "
f"{len(loose_verts)} loose vertex/vertices")
if tri_count > tri_budget:
print(f"[kit] WARNING {obj.name}: {tri_count} tris exceeds the {tri_budget} budget")
return {"manifold": not non_manifold, "loose_verts": len(loose_verts),
"tris": tri_count}
def note(obj, note_text):
PARTS.append({
"object": obj.name,
"asset": "SM_" + obj.name,
"dims_m": [round(d, 3) for d in obj.dimensions],
"location_m": [round(c, 3) for c in obj.location],
"topology": validate(obj),
"evidence": note_text,
})
print(f"[kit] {obj.name:22} {obj.dimensions.x:.2f} x {obj.dimensions.y:.2f} "
@@ -235,7 +263,9 @@ def export_group(objects, out_dir):
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
path = os.path.join(out_dir, obj.name + ".fbx")
# Unreal naming convention for static meshes is SM_<Name>; the Blender
# object keeps its readable name, the asset the importable one.
path = os.path.join(out_dir, "SM_" + obj.name + ".fbx")
fbx_preset.export_static(path, selection=True)
exported.append(path)
return exported
+4 -1
View File
@@ -53,7 +53,10 @@ def load_kit() -> dict:
for data in registry.get_assets(filt):
mesh = unreal.EditorAssetLibrary.load_asset(str(data.package_name))
if mesh:
kit[mesh.get_name()] = mesh
# Assets are SM_-prefixed (Unreal convention); the layout code refers to
# pieces by their short name, so key on the name without the prefix.
name = mesh.get_name()
kit[name[3:] if name.startswith("SM_") else name] = mesh
return kit
+10
View File
@@ -42,6 +42,16 @@ def main() -> None:
log(f"no FBX files in {SRC}")
return
# Drop kit assets that the current build no longer produces (the pieces were
# renamed to follow the SM_ convention), so the folder cannot accumulate two
# generations of the same piece.
expected = {os.path.splitext(f)[0] for f in files}
for existing in unreal.EditorAssetLibrary.list_assets(DEST, recursive=False):
name = existing.split("/")[-1].split(".")[0]
if name not in expected:
unreal.EditorAssetLibrary.delete_asset(f"{DEST}/{name}")
log(f"removed stale kit asset {name}")
for filename in files:
asset_name = os.path.splitext(filename)[0]
task = unreal.AssetImportTask()