Skip to content

Part connection ontology + deep solid merge for module-instantiated parts#99

Draft
krazyjakee wants to merge 3 commits into
masterfrom
part-connection-ontology
Draft

Part connection ontology + deep solid merge for module-instantiated parts#99
krazyjakee wants to merge 3 commits into
masterfrom
part-connection-ontology

Conversation

@krazyjakee

Copy link
Copy Markdown
Owner

What & why

Groundwork for a blocky, grid-based part system where parts are authored as mogen modules with typed, oriented connectors so an algorithm can snap them into models. The connection vocabulary is mined from Lego (specifically the LDCad shadow-library snapping metas) — not its geometry, so there's no copyright entanglement — and the parts use real Lego measurements.

Along the way this fixes a concrete blocker: the export merge couldn't collapse parts built from modules, which defeats the whole "minimum geometry" premise.

Changes

1. Ontology + seed parts (36da4fa)

  • docs/part-connection-ontology.md — the design: connection taxonomy grounded in the real LDCad metas (SNAP_CYL/CLP/FGR/SPH/GEN; studs/pins/axles collapse into one parametric cylinder, mapping onto Connector's pos+quat+tag+radius), the tag scheme kind:profile:gender[:key], the mate predicate, the Lego 5:2 grid, and the two-layer semantic/geometry split.
  • examples/parts/lego_bricks.mogbrick_1x1 / plate_1x1 / tile_1x1 / brick_2x1 with connectors at real Lego dimensions (stud pitch 8mm, plate 3.2mm, brick 9.6mm), plus a demo build. Validates clean, builds to GLB.

2. Deep solid merge (dd8fd69)

  • solid { … } previously only merged direct leaf-mesh children. A use always wraps its geometry in a group (module body + use's implicit transform group), so module-instantiated parts were never direct children and never merged.
  • merge_solid_groups now does a deep merge: when a solid's entire subtree is mergeable leaves, it collapses all same-material descendant leaves into one CSG-unioned mesh per material, baking each leaf's solid-relative transform and flattening the intermediate groups. Any keeper (skin / collider / slot / non-manifold mesh / cast_shadow=false opt-out / skin-or-clip-referenced node) disqualifies the solid, which falls back to the existing shallow direct-child merge.
  • Watertight by construction — same try_union_manyclean_csg_output path as in-DSL CSG.

Verification

  • Demo GLB: 30 nodes / 5 meshes → 8 nodes / 3 meshes (each solid course collapses to one merged_<material> leaf; the standalone tile, not in a solid, stays separate as intended).
  • All 25 mogen-export tests pass, including two new deep-merge tests (nested-group flatten+merge; keeper triggers safe fallback). Clippy clean on the new code.

Reviewer notes

  • The mogen build summary line reports pre-merge scene countsinspect the GLB to see the merged result. (This tripped me up mid-development; worth knowing.)
  • Behaviour change is scoped to solid-tagged subtrees; the global merge_sibling_meshes pass and the shallow path are untouched.
  • Known follow-up (documented, not in scope here): CSG union drops per-vertex UVs, so greedy meshing is the future UV-preserving win for large flat same-material fields.

🤖 Generated with Claude Code

krazyjakee and others added 3 commits July 9, 2026 16:06
Design note and seed part library for a blocky, grid-based part system whose
parts are mogen modules with typed oriented connectors. The connection
vocabulary is grounded in the real LDCad shadow-library snapping metas
(SNAP_CYL/CLP/FGR/SPH/GEN); studs/pins/axles collapse into one parametric
cylinder, mapping onto Connector's pos+quat+tag+radius.

- docs/part-connection-ontology.md: tag scheme (kind:profile:gender[:key]),
  mate predicate, Lego 5:2 grid, two-layer semantic/geometry split, and the
  verified solid-merge behaviour + module-nesting gap.
- examples/parts/lego_bricks.mog: brick_1x1 / plate_1x1 / tile_1x1 /
  brick_2x1 with connectors at real Lego measurements; compiles and builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`solid { … }` previously only merged direct leaf-mesh children, so parts
authored as modules (always group-wrapped by `use`) never merged — a build
made of `use`d parts kept every node separate.

Add a deep merge to `merge_solid_groups`: when a solid's entire subtree is
mergeable leaves (no skins/colliders/slots/protected nodes/non-manifold
meshes/cast_shadow=false opt-outs), collapse ALL same-material *descendant*
leaves into one CSG-unioned mesh per material, baking each leaf's transform
relative to the solid and flattening the intermediate groups. Any keeper in
the subtree disqualifies the solid and falls back to the existing shallow
direct-child merge. Watertight via the same try_union_many/clean_csg_output
path as in-DSL CSG.

examples/parts/lego_bricks.mog now exports 8 nodes / 3 meshes (was 30 / 5).

Adds two tests (nested-group flatten+merge; keeper triggers safe fallback)
and updates docs/part-connection-ontology.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
collect_deep_leaves() only recorded mergeable leaves and recursed into
non-mergeable nodes with children. A non-mergeable, childless node (e.g.
a named anchor/locator with no mesh) fell through both branches and was
silently discarded, even though solid_qualifies_deep() lets such nodes
qualify a solid for the deep merge (they're not a keeper mesh, skin,
collider, or slot).

Track these as "keepers" and re-emit each one baked into solid-space
before the merged material groups, alongside regression tests for the
fix and for grouping deep-merged leaves by material.

Copy link
Copy Markdown
Owner Author

Review findings

Reviewed crates/mogen-export/src/merge.rs (deep solid merge), docs/part-connection-ontology.md, and examples/parts/lego_bricks.mog, plus a visual pass on the demo build. Pushed a fix + tests in 34616ce.

1. Correctness (High) — deep merge silently dropped meshless marker nodes — fixed

collect_deep_leaves only ever did two things with a non-mergeable node: recurse into its children, or (implicitly) drop it. A childless, mesh-free node — e.g. a named anchor/locator, or any future POI-style marker nested inside a solid — falls into neither branch: it's not is_mergeable (no mesh) and has no children to recurse into, so it vanished from the exported GLB with no warning. solid_qualifies_deep doesn't guard against this case either, since such a node is neither a keeper mesh nor a skin/collider/slot.

Confirmed with a reproduction: a solid containing one mergeable box plus a tagged, childless group "spawn_point" node exported only the merged box — the marker was gone.

Fix: collect_deep_leaves now routes non-mergeable, childless nodes into a keepers list; deep_merge_solid_children re-emits each one baked into solid-space (via Mat4::to_scale_rotation_translation) before the merged material groups, preserving its name/tags/connectors. Added deep_solid_merge_preserves_meshless_marker_nodes as a regression test.

2. Test coverage (Medium) — no test for multi-material deep merge — fixed

All existing deep-merge tests used a single material. Added deep_solid_merge_groups_by_material, which nests two materials under one flattened group and asserts the deep merge produces one merged leaf per material (merged_oak + merged_pine) rather than conflating or dropping one.

3. Visual review (informational, out of scope for this PR)

Built examples/parts/lego_bricks.mog → GLB and rendered it headlessly (Xvfb + mogen thumbnail). Confirmed the merge itself is correct: mogen build reports 30 nodes/13 meshes pre-merge, the emitted GLB has 8 nodes/3 meshes post-merge (merged_red, merged_yellow, plus the un-merged tile_1x1/blue cap, matching the PR description exactly). Zoomed into the render: red base course, yellow offset course, and blue tile cap all stack correctly with no visible cracks or CSG artifacts.

One separate observation: the demo renders as a tiny dot in the 800×800 thumbnail, whereas a meter-scale example (examples/furniture/chair.mog) fills the frame under the same mogen thumbnail defaults. mogen-render's auto-fit (radius * 2.8 in headless.rs) should be scale-invariant, so this smells like a units mismatch somewhere in that crate's radius/AABB computation for mm-authored scenes — but mogen-render isn't touched by this PR, so I left it as a follow-up rather than fixing it here.

Other checks

  • File size: merge.rs is 936 lines post-fix, well under the 1000-line split threshold.
  • cargo clippy -p mogen-export --all-targets: no new warnings from this PR's code (the only clippy warnings in the workspace are pre-existing, in mogen-dsl).
  • cargo test -p mogen-export: all 15 merge:: tests pass (13 original/PR + 2 new).
  • Docs and example .mog read cleanly; brick_2x1 is defined in the part library but unused in the demo scene, which is fine for a seed catalog.

Pushed to part-connection-ontology in 34616ce.


Generated by Claude Code

@krazyjakee
krazyjakee marked this pull request as draft July 10, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants