`reach character {logo, strip-glb, qa, qa-analyze}` replaces make_logo.py,
glb_strip_utility_nodes.py, analyze_captures.py and the run-garment-qa bash
driver. The QA configs and method doc move beside the domain (qa_configs/,
GARMENT_QA.md), and `qa` takes a config name (`reach character qa hoodie_modern`)
or a path.
Parity, from baselines taken before anything moved:
- the logo PNG is byte-identical
- a synthetic GLB with three real utility nodes strips to identical bytes
(the committed bodies strip 0 nodes, so they proved nothing)
- re-analyzing a cached capture set gives a byte-identical report.json and
summary
run-garment-qa is rewritten, not wrapped (D-263). Its decisions — which config,
which Godot ($GODOT, then ~/bin/godot4, then PATH), and whether xvfb-run is
needed — are capture_plan(), pinned by tooling/test_character.py without
launching Godot. The bash exit codes are kept: 2 for a missing config, 3 for
no Godot.
The T-1271 domain map was wrong about this domain. Six of its ten files import
bpy: convert_outfit, inspect_glb, check_hair_symmetry, check_icosphere,
render_quaternius_test and test_quaternius_raw. They are Blender payloads and
joined the carve-out as blender_* (41 payloads now). The 22 existing payloads'
docstrings still cited tooling/garment-fit/ from before T-1273; fixed.
Archived, with reasons in tooling/archive/README.md:
- setup_clothing_metadata.py wrote coverage data for five garments that no
longer exist in the 24-garment wardrobe
- wipe-bodies.sh ran raw DELETEs on systems.db
segment_reference_distribution.md moved to docs/assets/visual/.
Behaviour changes:
- The QA analyzer exited 0 whatever it found, though its own README says
clip-through "is the real defect and it gates". qa and qa-analyze now exit 1
on clip-through, and the remedy names --min-pixels (Wave 1/2 were accepted
at 150). The cached peasant set has 33 failures at the default 8 px.
- glb strip re-reported the same nodes as stripped on every re-run and
rewrote an unchanged file: it left them as orphans and then found them
again. Only nodes still linked into the graph count now, and a first pass
writes the same bytes as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
1.8 KiB
Python
54 lines
1.8 KiB
Python
"""Convert a Quaternius outfit gltf to glb with optional solidify."""
|
|
import sys
|
|
import bpy
|
|
|
|
CLOTHING_THICKNESS = 0.025 # 25mm outward solidify
|
|
|
|
if __name__ == "__main__":
|
|
argv = sys.argv
|
|
if "--" not in argv:
|
|
print("Usage: reach blender run blender_convert_outfit <input.gltf> <output.glb>")
|
|
sys.exit(1)
|
|
args = argv[argv.index("--") + 1:]
|
|
input_path = args[0]
|
|
output_path = args[1]
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
bpy.ops.import_scene.gltf(filepath=input_path)
|
|
|
|
# Solidify clothing meshes for thickness
|
|
meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH']
|
|
for m in meshes:
|
|
if m.name.startswith("Icosphere") or len(m.data.vertices) < 50:
|
|
continue
|
|
bpy.context.view_layer.objects.active = m
|
|
m.select_set(True)
|
|
# Recalculate normals for consistent outward direction
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
bpy.ops.mesh.normals_make_consistent(inside=False)
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
# Solidify
|
|
sol = m.modifiers.new(name="Solidify", type='SOLIDIFY')
|
|
sol.thickness = CLOTHING_THICKNESS
|
|
sol.offset = 1.0 # grow outward only
|
|
sol.use_rim = True
|
|
bpy.ops.object.modifier_apply(modifier=sol.name)
|
|
m.select_set(False)
|
|
print(f" Solidified: {m.name} ({CLOTHING_THICKNESS*1000:.0f}mm)")
|
|
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
|
|
bpy.ops.export_scene.gltf(
|
|
filepath=output_path,
|
|
use_selection=True,
|
|
export_format='GLB',
|
|
export_animations=False,
|
|
export_yup=True,
|
|
export_texcoords=True,
|
|
export_normals=True,
|
|
export_skins=True,
|
|
export_materials='EXPORT',
|
|
)
|
|
print(f"Exported: {output_path}")
|