You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
**`projection` (`SYS-W14-24`, 2026-07-20):**`"box"` and `"sphere"` are
731
-
implemented by `mc3togltf` (editor-viewport parity is intentionally out
732
-
of scope — the exported glTF is the ground truth for appearance).
730
+
**`projection` (`SYS-W14-24`, `SYS-W14-06`):**`"box"` and `"sphere"` are
731
+
implemented by `mc3togltf`. Ordinary primitive viewport projection remains
732
+
export-only, but CSG roots use the same generated projection in both the
733
+
exporter and live CSG preview.
733
734
`"box"`/triplanar picks, per vertex, the dominant axis of that vertex's
734
735
normal (or its direction from the mesh's local bounding-box center if
735
736
normals are absent) and projects onto the other two axes using **raw,
@@ -741,11 +742,12 @@ mesh's local bounding-box center, normalized to `[0, 1]` on both axes.
741
742
`scale_u`/`scale_v`/`offset_u`/`offset_v`/`rotation` are still applied
742
743
on top of either regenerated projection, same as for `"planar"`.
743
744
744
-
Per-object UV mapping is ignored on CSG boolean output (see
745
-
[CSG operations](#csg-operations)'s "UV coordinates are not real"
746
-
limitation) — it only affects primitives whose geometry is generated
747
-
directly, not the Manifold-evaluated result of a `<union>`/`<difference>`/
748
-
`<intersection>`.
745
+
For a CSG root, `uv_mapping` is applied **after** the Manifold boolean to the
746
+
generated result: `"planar"` projects local X/Z, `"box"` uses the dominant
747
+
normal axis, and `"sphere"` is the documented equirectangular projection.
748
+
Without it, a CSG result defaults to box projection. This is generated mapping,
749
+
not a retained authored unwrap from the operands; see [CSG operations](#csg-operations)
750
+
for that remaining limitation.
749
751
750
752
### `<mesh>` — external OBJ file
751
753
@@ -832,12 +834,22 @@ Pass `--allow-approximate-csg` (CLI) or enable the "Allow approximate CSG export
832
834
833
835
**Strict mode is the default** (`allowApproximateCSG = false` in `GltfExporter`; the `mc3togltf_csg_strict` test asserts this): a CSG node containing an unsupported child type fails the whole export with an error rather than silently producing wrong geometry. There is no separate "strict" flag to set — it's simply what happens unless `--allow-approximate-csg` is explicitly passed.
834
836
835
-
**Limitations of CSG output** (the Manifold-evaluated result mesh, not the approximate-fallback path), per `CsgEvaluator.cpp`'s `manifoldToMeshData()`:
836
-
-**UV coordinates are not real** — a texcoord channel is present (same vertex count as positions/normals), but every value is a hardcoded `(0, 0)` placeholder, not an actual UV unwrap. Any material with texture slots (base color, normal, etc.) samples the same texel everywhere on a CSG result.
837
-
-**Normals are not preserved from the child geometry** — flat per-face normals are recomputed from each triangle's winding via cross product; there is no smooth-shading / vertex-normal-interpolation option for CSG output.
838
-
- Child material assignments are not preserved — the CSG root's material is used for the entire merged mesh, regardless of what materials the children had.
839
-
840
-
**Investigated: could UV be preserved (STAB-0225)?** Not without a real feature addition. Every `Manifold` fed into a boolean op in `CsgEvaluator.cpp` is built either from Manifold's own built-in primitive generators (`Manifold::Cube`/`Sphere`/`Cylinder`, which carry no UV data at all) or from a `MeshGL` with `numProp = 3` (position-only) for the Torus/Capsule/IcoSphere path — no UV channel is ever fed in for Manifold to carry through the boolean op in the first place. Manifold v3's `MeshGL` *does* support extra per-vertex properties beyond position (and interpolates them across new cut edges during boolean ops), so preserving UVs is technically possible — but it would require rebuilding every CSG-eligible primitive with a UV-carrying `MeshGL` (including writing new UV-aware constructors for the cases currently using Manifold's built-in generators) and handling the interpolated-but-unwrapped seams that boolean cuts create. That's a real, non-trivial feature, out of scope for this stabilization effort — documented as an explicit limitation above rather than attempted.
Copy file name to clipboardExpand all lines: README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -278,7 +278,7 @@ reference.
278
278
- Preferences dialog: auto-save interval, snap (translate/rotate/scale), grid spacing, and theme are all persisted (`savePrefsAlg`/`loadPrefsAlg`)
279
279
- 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
280
280
- 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
281
-
- 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 (CSGroot material is used)
281
+
- 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). Real CSG produces smooth normals with sharp creases, generated box/default or root-selected UV projection, and separate glTF primitives for child materials when no CSG-root material overrides them; operand UV unwraps themselves are not preserved through cuts
282
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
283
283
- 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
284
284
- 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
Copy file name to clipboardExpand all lines: TESTING.md
+4-4Lines changed: 4 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
_Last updated: 2026-07-26. Counts below were verified against a freshly configured Release build after `SYS-W14-05` embedded-GLB support. This document is derived from the actual `CMakeLists.txt` test registrations and test source files — if it drifts from a freshly configured build's `ctest -N` output, trust `ctest -N`, not this file's claimed count._
4
4
5
-
MeshCraft's tests run through **CTest** — **183 tests registered** (`ctest --print-labels` label breakdown after the new target: `ai` 1, `commands` 1, `export`71, `format` 37, `lint` 4, `perf` 2, `registry` 1, `render` 36, `unit` 32), mixing C++ assertion-based binaries and Python/bash subprocess-driven checks against the `mc3togltf`/`mc3tomcb` CLIs and the `MeshCraft` editor binary itself (headless `--screenshot` real-pixel-sampling tests, plus 3 tests that drive real headless Blender for GLB-import verification). `render_display_preflight` verifies an actual `xvfb-run` + `xdpyinfo` X client before the render subset; if unavailable, it reports a CTest skip and CMake disables only the other render-labelled tests. **Known gap:** the "CLI-driving Python tests" table below documents the most significant/representative tests in each category but is not exhaustively 1:1 with all registrations — `ctest -N` and `ctest --print-labels` are authoritative for the complete list.
5
+
MeshCraft's tests run through **CTest** — **184 tests registered** (`ctest --print-labels` label breakdown after the new target: `ai` 1, `commands` 1, `export`72, `format` 37, `lint` 4, `perf` 2, `registry` 1, `render` 36, `unit` 32), mixing C++ assertion-based binaries and Python/bash subprocess-driven checks against the `mc3togltf`/`mc3tomcb` CLIs and the `MeshCraft` editor binary itself (headless `--screenshot` real-pixel-sampling tests, plus 3 tests that drive real headless Blender for GLB-import verification). `render_display_preflight` verifies an actual `xvfb-run` + `xdpyinfo` X client before the render subset; if unavailable, it reports a CTest skip and CMake disables only the other render-labelled tests. **Known gap:** the "CLI-driving Python tests" table below documents the most significant/representative tests in each category but is not exhaustively 1:1 with all registrations — `ctest -N` and `ctest --print-labels` are authoritative for the complete list.
6
6
7
7
---
8
8
@@ -21,7 +21,7 @@ ctest -N
21
21
22
22
# Run one test by name (regex match)
23
23
ctest -R mc3_commands --output-on-failure
24
-
ctest -R mc3togltf_csg --output-on-failure # matches all 4 CSG tests
24
+
ctest -R mc3togltf_csg --output-on-failure # matches all CSG export tests
25
25
26
26
# Run one group by label (format/export/render/registry/ai/commands/lint/perf/unit)
27
27
ctest -L export --output-on-failure
@@ -31,7 +31,7 @@ ctest --print-labels # list all labels
31
31
ctest --rerun-failed --output-on-failure
32
32
```
33
33
34
-
Expected result: every registered test passes. On 2026-07-26 the fresh Release build reported 183 registrations and passed all of them as two disjoint host runs: 147/147 with `-LE render` and 36/36 with `-L render` under Xvfb. The focused suites are run with at most `-j4` after each change. A failing test prints its assertion/subprocess output inline with `--output-on-failure`; without that flag, CTest only shows pass/fail per test name.
34
+
Expected result: every registered test passes. On 2026-07-26 the fresh Release build reported 184 registrations and passed all of them as two disjoint host runs: 148/148 with `-LE render` and 36/36 with `-L render` under Xvfb. The focused suites are run with at most `-j4` after each change. A failing test prints its assertion/subprocess output inline with `--output-on-failure`; without that flag, CTest only shows pass/fail per test name.
35
35
36
36
Each C++ test binary can also be run directly (bypassing CTest) for faster iteration:
37
37
@@ -105,7 +105,7 @@ These spawn the built `mc3togltf`/`mc3tomcb` binaries as subprocesses and assert
105
105
|`mc3togltf_csg_export`|`mc3togltf/test/*.py`| Union/difference/intersection evaluated by Manifold and exported as real merged geometry | Exported mesh has expected triangle count / bounds |
106
106
|`mc3togltf_csg_unsupported`|`mc3togltf/test/*.py`| An unsupported CSG child type (Plane, Disk, Grid, Mesh, Extrude) is detected and reported | Error names the unsupported type |
|`mc3togltf_csg_semantics` / `mc3togltf_csg_mesh_child` / `mc3togltf_csg_stress`|`mc3togltf/test/*.py`|`isCutter`/world-transform/empty-result/material-on-node CSG semantics; a `<mesh>` child inside a CSG node; a deep/many-child CSG stress case| Calibrated vertex-count / structural assertions |
108
+
|`mc3togltf_csg_semantics` / `mc3togltf_csg_mesh_child` / `mc3togltf_csg_stress`/ `mc3togltf_csg_shading_materials`|`mc3togltf/test/*.py`|`isCutter`/world-transform/empty-result/material-on-node CSG semantics; a `<mesh>` child inside a CSG node; a deep/many-child CSG stress case; generated CSG UVs, smooth normals, and child-material glTF primitives | Calibrated geometry / GLB-buffer / structural assertions |
109
109
|`mc3togltf_instance_deform_cache`|`mc3togltf/test/*.py`| Instances of the same definition with different `<deform>` produce separate cached meshes (not incorrectly shared) | Distinct mesh indices per distinct deform |
110
110
|`mc3togltf_float_cache_key` / `mc3togltf_geom_cache_key`|`mc3togltf/test/*.py`| Two primitives with close-but-not-equal float dimensions produce 2 distinct meshes (cache key doesn't collide on float rounding); the geometry cache key's full construction is exercised directly |`len(meshes) == 2`; cache-key assertions |
111
111
|`mc3togltf_obj_robustness`|`mc3togltf/test/obj_robustness_test.py`| Untrusted OBJ input: out-of-range negative vertex index and an infinite (`1e400`-overflow) coordinate must not crash the exporter | Exit 0, a `Warning:`/`non-finite` message per malformed file, valid mesh still exports geometry, no `null` (non-finite) values in any accessor `min`/`max`|
0 commit comments