Skip to content

Commit 7e93b92

Browse files
committed
feat: resolve embedded GLB meshes
1 parent fff0e9b commit 7e93b92

12 files changed

Lines changed: 812 additions & 110 deletions

File tree

CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,24 @@ if(MESH_CRAFT_BUILD_TESTING AND UNIX AND NOT ANDROID AND NOT EMSCRIPTEN)
16141614
set_tests_properties(svg_inline_texture_viewport_test PROPERTIES TIMEOUT 30 LABELS "render")
16151615
endif()
16161616

1617+
# SYS-W14-05: <mesh src="embed:id"> must resolve the document's
1618+
# external GLB in the live CNA viewport as well as in mc3togltf. The
1619+
# exporter test builds the small asset and additionally checks inline
1620+
# base64; this render-labelled invocation proves the shared loader is
1621+
# actually wired into SceneRenderer rather than export-only.
1622+
if(TARGET mc3togltf AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/mc3togltf/test/embed_mesh_source_test.py")
1623+
add_test(
1624+
NAME embed_mesh_viewport_test
1625+
COMMAND "${PYTHON3_EXEC}"
1626+
"${CMAKE_CURRENT_SOURCE_DIR}/mc3togltf/test/embed_mesh_source_test.py"
1627+
$<TARGET_FILE:mc3togltf>
1628+
"${CMAKE_CURRENT_SOURCE_DIR}/test/embed_mesh_source.mc3.xml"
1629+
$<TARGET_FILE:MeshCraft>
1630+
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
1631+
)
1632+
set_tests_properties(embed_mesh_viewport_test PROPERTIES TIMEOUT 30 LABELS "render")
1633+
endif()
1634+
16171635
# STAB-0524: confirms the equirectangular skybox shader actually
16181636
# renders a document-driven skybox_texture.
16191637
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/skybox_texture.mc3.xml")

MC3_FORMAT.md

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -217,17 +217,14 @@ are covered by this same skip-set (fixed in STAB-0091 — the writer's
217217
SVG-texture loop was previously missing this check and silently
218218
re-inlined included SVG textures on every save).
219219

220-
**Known limitation — `<embeds>` is not include-aware (STAB-0092):**
221-
unlike definitions/materials/textures, an included file's own `<embeds>`
222-
section is **never merged** — only the main document's own top-level
223-
`<embeds>` is parsed. If a `<definition>` merged from an included file
224-
references `<mesh src="embed:xyz"/>` where `xyz` is declared in *that
225-
same included file's* `<embeds>` section (rather than the main
226-
document's), the reference silently fails to resolve. This is a narrow,
227-
accepted limitation (embedding glTF data specifically inside a
228-
shared/included asset library), not implemented — would need an
229-
`includedEmbeds` tracking set plus an `<embeds>` merge block in
230-
`mergeInclude()`, mirroring the existing definitions-merge pattern.
220+
**`<embeds>` are include-aware (STAB-0092):** embeds declared by an included
221+
file are merged alongside its definitions. Their external `src` paths are
222+
rebased from the included file's directory to the main document's directory,
223+
and `includedEmbeds` ensures an unchanged included embed is not inlined when
224+
the main document is saved. A local edit makes it local content, matching the
225+
existing definitions/materials/textures ownership rule. Duplicate ids use the
226+
same documented last-write-wins rule as other included resources and emit a
227+
named warning.
231228

232229
---
233230

@@ -974,8 +971,8 @@ curve in a short time window could in principle be under-sampled.
974971
| Per-object `metadata` (`<metadata><property name="..." value="..."/></metadata>`) | ✅ (`AUD-029`) — serialized into `node.extras.metadata`, alongside the pre-existing `tags`/`collision` extras |
975972
| `--stats` "Warnings" count | ✅ truthful (`AUD-026`) — every `"Warning:"` print site in `GltfExporter.cpp` increments the shared counter (verified by grep, not spot-checked); previously several paths (unknown material, SVG-slot warnings, ambient-light drop, duplicate node name, image-format detection, missing embed texture, action warnings) printed a warning without counting it |
976973
| `TANGENT` accessor (for `normal_texture`-mapped meshes) | ✅ (STAB-0664) — computed per-vertex (standard per-triangle-then-averaged-then-Gram-Schmidt-orthogonalized algorithm, not a full MikkTSpace port), only when a mesh has both `NORMAL`/`TEXCOORD_0` and its material sets `normal_texture`; meshes without a normal map get no `TANGENT` (not needed) |
977-
| SVG textures (N1, `<textures><texture>` with an SVG source) | `svgTextures` is a separate map in `Mc3Document` that `GltfExporter` never rasterizes (rasterization isn't implemented anywhere yet, editor viewport included); since STAB-0440, a material referencing an SVG texture prints a warning naming the material/slot/texture id instead of dropping it silently, but the texture itself is still omitted from the export |
978-
| Embedded glTF (N2, `<mesh src="embed:id"/>`) | ❌ — treated as a literal OBJ file path, which fails to parse; the export doesn't crash but continues with **no mesh on that node** (`Warning: OBJ load failed (...)` on stderr, `stats.warnings` incremented). See STAB-0194 for the tracked automated test of this exact behavior |
974+
| SVG textures (N1, `<textures><texture>` with an SVG source) | external and inline SVG are rasterized into bounded PNG pixels for both glTF export and the live viewport. See the Textures section for the 2048px safety cap and cache/sampler details. |
975+
| Embedded GLB (N2, `<mesh src="embed:id"/>`) | ✅ (`SYS-W14-05`) — external self-contained `.glb` files and inline base64 GLB are decoded, their default-scene node transforms are flattened, and triangle geometry reaches both `mc3togltf` and the live viewport. The MC3 object's material remains authoritative: source GLB materials/textures, skins, morph targets, animations, non-triangle primitives, loose `.gltf` companion-file assets, singular transforms, and geometry beyond 64 MiB/300,000 triangles are deliberately rejected with a named warning rather than partially or unsafely imported. |
979976
| Scripts, Sounds, Music, Triggers, Scene States, Meta (N3-N7) | ❌ (no glTF equivalent — these are MCB/XML-only data, round-tripped but not translated to any glTF concept; see [Scripts (N3)](#scripts-n3) etc. above) |
980977

981978
### Export scalability (STAB-0699)

NEXT.md

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -663,15 +663,16 @@ a stale-build false positive rather than hidden behind a longer timeout.
663663

664664
The P1 alternate-backend qualification (`SYS-W8-05`) remains blocked by CNA's
665665
missing cross-backend custom-effect contract and a first-frame Vulkan backend
666-
crash outside this repository. The only deferred audit row is Android
667-
(`AUD-042`): this workspace has no Android NDK, and selecting a real Android
668-
graphics path would require a CNA ownership decision.
666+
crash outside this repository. Broad backend qualification is now deliberately
667+
postponed by user priority. Android (`AUD-042`) is an intended supported
668+
platform, but this workspace has no Android NDK and a real GLES/EASYGL editor
669+
path still requires CNA/backend validation.
669670

670671
## 5. Known bugs and limitations
671672

672673
- **Web/Windows build status: needs re-verification**, not checked this
673674
session — see §2's caveat and README.md's own platform table.
674-
- **`SYS-W3-01` (in progress, not a bug):** `MeshCraftApplication` god
675+
- **`SYS-W3-01` (deferred by user priority, not a bug):** `MeshCraftApplication` god
675676
object, 12 subsystems extracted so far (`KeybindingManager`,
676677
`Preferences`, `MacroRecorder`, `UndoManager`, animation-override
677678
computation, `WalkController`, `AudioPreview`, `CameraBookmarks`). File
@@ -856,7 +857,8 @@ git stash pop && cmake --build b-release -j4 --target <affected-target>
856857
## 8. Next smallest tasks
857858

858859
No actionable follow-up audit task remains: `AUD-089` through `AUD-092` are
859-
complete, while Android (`AUD-042`) is environment/owner deferred.
860+
complete. Android (`AUD-042`) remains environment-blocked, but is now an
861+
explicit supported-platform goal rather than a possible rejection path.
860862
`SYS-W3-01` has 12 completed subsystem phases and an active Phase 13 for
861863
application/UI ownership. The authorized Camera Bookmarks, Camera Preset Overlay, Gizmo Drag Overlay, Stats Overlay, Measurement Overlay, Status Bar, Walk Mode, View
862864
Bloom/SSAO, Help, Add/CSG, Edit-history, Edit-clipboard, and Edit-object-actions
@@ -1135,15 +1137,23 @@ light lookup. Splitting any one without a new, broader ownership design would
11351137
create a god-context or relocate application state into UI. The next work should
11361138
be a separately scoped subsystem, not another mechanical `Overlays.cpp` slice.
11371139

1140+
### Current authorized work
1141+
1142+
- **SYS-W14-05:** embedded external/inline GLB support is implemented and
1143+
under final validation. It safely accepts self-contained triangle GLBs only,
1144+
with a 64 MiB / 300,000-triangle ceiling; MC3 materials remain authoritative.
1145+
- **SYS-W14-06:** improved CSG normals/UVs/materials is the next authorized
1146+
task once the `SYS-W14-05` validation/commit is complete.
1147+
11381148
### Tracked work that is not implementation-ready
11391149

1140-
- **SYS-W8-05:** alternate-backend runtime qualification needs the appropriate
1141-
backend environment and owner coordination.
1142-
- **AUD-042:** Android remains deferred until an Android-capable toolchain and
1143-
a viable graphics-backend path are available.
1144-
- **Deferred, decision-dependent work:** `SYS-W5-03`, `SYS-W14-05`,
1145-
`SYS-W14-06`, and `SYS-W14-14` require the documented human/owner decision;
1146-
they are not automatic follow-ups to Phase 13.
1150+
- **SYS-W8-05:** broad alternate-backend support is intentionally postponed
1151+
while the current W14 feature work has priority; it still needs the
1152+
appropriate backend environment and CNA owner coordination.
1153+
- **AUD-042:** Android is a supported product target but remains blocked until
1154+
an Android-capable toolchain and viable graphics-backend path are available.
1155+
- **Deferred, decision-dependent work:** `SYS-W5-03` and `SYS-W14-14` retain
1156+
their documented human decisions. `SYS-W14-05`/`06` are no longer deferred.
11471157

11481158
## 9. Do not do yet
11491159

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,11 @@ reference.
279279
- Headless screenshot: a `.png` path writes a real PNG (`stbi_write_png`); every other extension (e.g. `.ppm`) writes raw PPM (P6) bytes regardless of what the extension actually says
280280
- MCB: as of `SYS-W14-25` (2026-07-20), compression is implemented — `MeshCraft::Mcb::saveToBinary`/`saveToFile` take an opt-in `compress` parameter (default `false`, unchanged output) that zlib-deflates the document payload; `loadFromBinary`/`loadFromFile` transparently detect and decompress it. Requires this build to have been compiled with zlib available (system package, optional — see `THIRD_PARTY.md`); degrades to a clear "requires zlib"/"compiled without zlib support" error rather than misparsing a compressed file or silently ignoring the `compress` request. No editor UI toggle — see `MC3_FORMAT.md`'s MCB section for the full header layout
281281
- CSG export to glTF: evaluated by Manifold (union/difference/intersection). Unsupported child types inside a CSG node (Plane, Disk, Grid, Mesh, Extrude) cause the export to fail with a clear error in default mode. Pass `--allow-approximate-csg` (CLI) or enable "Allow approximate CSG export" (editor) to bypass Manifold and export children separately (debug fallback, geometrically incorrect). CSG result mesh has flat normals; child materials are not preserved (CSG root material is used)
282-
- No full end-to-end UI-interaction-simulation harness (nothing scripts a sequence of real user clicks/drags through the live application window) — but this understates real coverage: 34 `render`-labeled tests include real pixel-sampling checks against headless `--screenshot` output (fog, light/camera gizmos, look-through-camera, background/skybox textures, LOD, CSG preview cache, etc. — see `TESTING.md`), and a handful of `unit`-labeled tests drive real ImGui widget frames (drag/click gestures, undo-snapshot timing, scene-hierarchy drag-drop) directly against production widget code with no GL context needed
282+
- No full end-to-end UI-interaction-simulation harness (nothing scripts a sequence of real user clicks/drags through the live application window) — but this understates real coverage: 36 `render`-labeled tests include real pixel-sampling checks against headless `--screenshot` output (fog, light/camera gizmos, look-through-camera, embedded GLB, background/skybox textures, LOD, CSG preview cache, etc. — see `TESTING.md`), and a handful of `unit`-labeled tests drive real ImGui widget frames (drag/click gestures, undo-snapshot timing, scene-hierarchy drag-drop) directly against production widget code with no GL context needed
283283
- AI Assistant: not available on Emscripten or Android builds (`cpp-httplib`/OpenSSL are only fetched/linked when `NOT EMSCRIPTEN AND NOT ANDROID`). The API key is **never persisted to disk** — it lives only in the in-memory UI text buffer for the session, pre-filled from the `ANTHROPIC_API_KEY` environment variable if set, and is not part of the saved preferences file. The **Model** field (default `claude-sonnet-5`) and **Max tokens** (4096–64000, default 32000) are both user-editable in the AI panel and control the outgoing API request directly — there is no fixed/hardcoded model. **Scope** selects what's sent as context: "Full scene" serializes the entire document; "Selection only" serializes just the currently-selected objects (plus all materials/definitions, so references still resolve) and is disabled when nothing is selected. Applying a response that would drastically shrink the scene's object count (more than half, on a scene of 10+ objects) requires an explicit second "Confirm Replace" click rather than applying immediately. Response validation is structural, not semantic: it checks the XML is well-formed, has a `<mc3>` root, isn't empty, and conforms to `mc3.xsd` (element order, attribute types/patterns, ID/IDREF cross-references) — but `mc3.xsd` has no numeric range constraints (no `minInclusive`/`minExclusive` anywhere), so a geometrically nonsensical response (e.g. negative `size`/`radius`) passes validation and applies to the scene as-is
284284
- Model Registry: no thumbnail column (design placeholder only, never implemented — see `m1m2m3.md`); requires the system SQLite3 library on desktop builds (stubbed out, feature disabled, on Emscripten/Android); no sync between multiple registry database files — it's a single local SQLite file at `~/.meshcraft/modelregistry.sqlite3`, not backed up or shared automatically
285285
- SVG textures (`<texture type="svg">`): external `.svg` files and inline CDATA markup are rasterized by NanoSVG (maximum output dimension 2048px) for both the live editor viewport and glTF export. The live cache has compact content-hash keys and automatically re-rasterizes an external SVG when its file changes; malformed input is warned once per unchanged source. `wrap_u`, `wrap_v`, and `filter` round-trip and affect both the viewport sampler and glTF sampler; `mip_maps` affects glTF, while the live CNA texture remains level-zero because its available API cannot generate a mip chain. `.gltf` exports write a generated PNG beside the document; `.glb` embeds it. Unsupported or malformed SVG is skipped with a named warning rather than dropping the material silently.
286-
- Embedded glTF (`<mesh src="embed:id"/>`): parsed and serialized, but `GltfExporter` treats `embed:id` as a literal OBJ file path, which fails to parse — the export doesn't crash, but the node exports with no mesh (see `MC3_FORMAT.md`'s export support matrix)
286+
- Embedded GLB (`<mesh src="embed:id"/>`): external self-contained `.glb` files and inline base64 GLB both resolve in `mc3togltf` and in the live viewport. The asset's default-scene transforms are flattened into the Mesh object's local geometry; MC3 retains authority over the material. The loader rejects loose companion-file `.gltf`, non-triangle primitives, animation/skin/morph data, malformed paths/data, and assets over the documented 64 MiB/300,000-triangle limits with a named warning instead of importing an unsafe partial asset.
287287
- N3–N7 scene data (scripts, sounds, music, triggers, scene states, meta): fully round-tripped (XML/MCB/XSD) and editable. As of `SYS-W14-18`/`SYS-W14-19`/`SYS-W14-20` (2026-07-20), scripts (`type="lua"`) actually run — a real sandboxed Lua interpreter (`LuaScriptRunner`) with `def:place()`/`place_at()`/`has_socket()` (compose-time socket placement) and `scene:find()` (read/write any object's position/rotation/scale/visible/material) — triggers actually fire (an explicit "Fire" action executes `play-action`/`play-sound`/`play-music`/`run-script` steps for real), and scene states actually apply (an explicit "Apply State" action writes a state's overrides onto the matching live objects). What's still data-model-only: there is no in-scene EVENT system (collision/click/timer) that fires a trigger or switches a scene state automatically — only the explicit manual actions above do. See `MC3_FORMAT.md`'s per-section status notes and `plan.md`'s `SYS-W14-##` rows.
288288
- `<library>`/`<imports>` (R101/R110, `Mc3ImportResolver`): as of `SYS-W14-21` (2026-07-20), the editor actually resolves `<imports>` against the loaded document's own directory and merges the imported libraries' definitions into `document_.definitions`, so `<instance definition="namespace:id">` referencing an imported library's definition renders instead of silently resolving to nothing — this now runs automatically right after every load (initial launch, Open dialog, Open Recent, autosave recovery) plus an explicit "Resolve Imports" button in the Imports tab for re-resolving after editing the rows without a full reload. A resolution failure (missing library file, content-hash mismatch, an import cycle/depth-limit) doesn't fail the whole document load — it's reported via the status bar and the affected imports simply stay unresolved.
289289
- `<texture mip_maps="...">`: as of `SYS-W14-22` (2026-07-20), honored by `mc3togltf``false` makes the exporter emit a plain (non-mipmap) glTF sampler `minFilter` instead of unconditionally requesting a mipmapped one. **Not honored by the live editor viewport** — CNA's `Texture2D` asset-loading path has no mipmap-generation option (confirmed in CNA's own OpenGL backend: it explicitly does not generate mipmaps by default for the filter that path uses), and closing that would need a CNA-side API change, out of scope per this repo's CNA boundary. See `MC3_FORMAT.md`'s Textures section for the full writeup.

0 commit comments

Comments
 (0)