diff --git a/docs/research/mesh-probe-run2-shippable/RESULT.md b/docs/research/mesh-probe-run2-shippable/RESULT.md new file mode 100644 index 0000000..9902ad2 --- /dev/null +++ b/docs/research/mesh-probe-run2-shippable/RESULT.md @@ -0,0 +1,226 @@ +# Mesh probe run 2: is the lowered geometry shippable? + +Agent `probe-shippable-lowering`, session `self-2026-10-10-ag2`, 2026-10-10. Disposable scratch; no repo edited. +Root: this directory (`...\scratchpad\mesh-probe\run2-shippable\`). + +Operator question: "Sure, it can fit geometry, especially with 'real OpenSCAD', but is it good geometry? Do we +end up with something we'll want to ship, if we go this way? Because I'm not sure we wouldn't be better off +building with, say, Blender's geonodes." + +Her bar (brush-tree:ruling:shippable-part-bar): clean, unbroken topology flow; UVs that keep the parts of one +object together, with minimal distortion; hard edges sharpened by a one-segment bevel shaded with face-weighted +normals. + +Input: `docs/research/mesh-probe-run1/dexter-quiet-compact.scad` from the brush-tree branch, copied here as +`A.scad`. It is run 1's round 3: superellipse stations, n = 3. All three lowerings use its numbers: +- planform, 17 points; +- wing plane z = a + b|x| + c·y, with wing_t 0.045; +- trailing scallops; +- 12 tail and body stations; +- pods as hulls of two spheres; +- the chin fin. + +Python copy of the numbers: `tools/desc.py`. + +## Contact sheets (rows top to bottom: Tripo target, A, B, C; orange squares = row number) + +- **`sheet-main.png`**. Columns: 3/4 front, side and 3/4 rear in metallic grey under studio light, then a + wireframe overlay, then the UV layout. In the UV column, colour marks the island and red marks overlap. The + target has no UVs, so its UV tile is black. This is the one to judge at a glance. +- **`sheet-closeup.png`**. Columns: close-up of the wing edge and wing-body junction (shaded, then wireframe), + and a top view with wireframe. The bevel highlights and the faceting are visible here. +- **`sheet-edit.png`**. Rows: A, A_edit, B, B_edit, C, C_edit. The edit is nose_k 1.0 to 1.2 plus a new + wing_sweep of 0.1. +- `sheet-top.png`: top views, shaded and wireframe. +- Per-lowering frames are in `out/.v0-4.png`, `out/.w0,w2,w4.png` and `out/.uv.png`. Metrics are in + `out/.metrics.json`, scores in `out/.score.txt`, and the .blend files in `out/`. + +Renders are Eevee (Blender 5.2.2). The look: +- Principled metallic 1.0, roughness 0.3, grey; +- Blender's bundled `studio.exr` world plus one big soft area key; +- orthographic camera, dark ground composited. + +This approximates her test render; it is not her scene. + +## The three lowerings + +- **A, OpenSCAD lowering with the best automatic cleanup** (`tools/lower_a.py`): + - OpenSCAD 2026.09.23 with Manifold renders the file (2 s, 2836 triangles, genus 0). + - Blender then runs: merge by distance, tris-to-quads, limited dissolve at 2°, Bevel (1 segment, by angle + 30°, width 0.004), Smart UV Project (66°) and Weighted Normal (face area, keep sharp). + - A2 also tried QuadriFlow, see "Could not test". +- **B, Blender built by script** (`tools/lower_b.py`, 174 lines): + - Body: tail and body stations as one quad loft, 36 verts per ring, with 2 Catmull-Rom rings inserted per + span. The ends get Coons-patch quad caps. + - Wing: one quad sheet across the whole span, with columns at the planform breakpoints and at the + trailing-edge kink, 16 chord rows, the scallop as the trailing edge, and a closed side band. + - Exact Boolean only for the wing into the body. + - Pods are quad capsules and the chin fin is a prism. Both are left as intersecting shells, not + booleaned. + - Seams go on edges over 30°, the body side lines and the cap rings. Then unwrap (angle based), average the + island scale, pack, Bevel (1 segment, 30°), Weighted Normal. +- **C, Geometry Nodes generated by Python** (`tools/lower_c.py`, 172 lines). It is the same construction as + one node group, `DexterQuiet`, with 199 nodes and 271 links. Its 16 parameters are modifier inputs: + - nose_root, nose_k, wing_t, wing_a/b/c, wing_sweep, scallop_r/cx/cy, sect_n, ring_n, loft_res, + wing_cuts, chord_rows, seam_angle. + - Station and planform data live in Index Switch nodes. + - Body: Points, then Points to Curves (Catmull-Rom), then Curve to Mesh with a superellipse profile, then + Set Position from the hw/zc/hh attributes. + - Wing: a Grid whose columns sample the leading-edge curve, then Extrude Mesh, joined with a flipped copy. + - Mesh Boolean (Manifold solver) for the wing into the body. + - Pods are UV spheres reshaped into capsules; the fin is a convex hull. + - UVs come from the UV Unwrap node, with the seam field = edge angle > 30° or a `cut_e` edge flag, then + Pack UV Islands. + - Modifier stack: GN, then Bevel (1 segment, 30°), then Weighted Normal. + +## Measurements + +| | A (OpenSCAD + cleanup) | B (bmesh script) | C (geonodes) | +|---|---|---|---| +| Faces: quads / tris / ngons | 1146 / 125 / 168 | 3039 / 49 / 215 | 3979 / 127 / 229 | +| Largest ngon | **59** (wing top/bottom dissolved to single faces) | 11 (boolean seam) | 36 (loft end caps; GN has no grid fill) | +| Poles (valence ≠ 4) per 1000 verts | **411.7** | 100.1 | 98.0 | +| Poles of valence 6 or more | 10 | 0 | 4 (UV-sphere pod tips) | +| Longest edge loop (edges) | **8** | 86 | 35 | +| Closed loops ≥ 20 edges | 0 | 41 | 57 | +| Share of edges in loops ≥ 20 edges | **0.013** | 0.528 | 0.605 | +| Non-manifold edges / shells | 0 / 1 | 0 / 4 (pods and fin intersect) | 0 / 4 | +| Exported triangles | 3392 | 6914 | 9022 | +| Sliver tris (< 10°) after export triangulation | 2082 (61%) | 2112 (31%) | 2537 (28%) | +| …excluding faces of bevel width | 491 | 336 | 157 | +| Dihedral > 60° / > 80° left (unbevelled hard edges) | 86 / 15 | 83 / 7 | 77 / 9 | +| UV islands: total / ≥ 1% of area / < 4 faces | 51 / 13 / 15 | 23 / 11 / 2 | 21 / 11 / 2 | +| UV area distortion, mean \|log2\|, area-weighted | 0.120 | 0.113 | 0.118 | +| UV area outside 0.5-2x | 0.1% | 0.7% | 1.1% | +| UV angle error, mean | 2.74° | **1.04°** | 1.14° | +| UV space wasted | 54% | 47% | 61% | +| Chamfer % diag / F@2% / mean sil-IoU (run-1 `mp score`) | 0.852 / 0.901 / 0.919 | 0.841 / 0.899 / 0.920 | 0.840 / 0.900 / 0.920 | + +Raw OpenSCAD output scores 0.852 / 0.901 / 0.919. Cleanup lost no shape, and B and C lose none against A. + +Reading the numbers: +- **Fidelity is the description's, not the lowering's.** All three are within 0.012 chamfer. This is the + same 0.852 that run 1's round 3 scored. No lowering makes the description closer to the target. +- **A fails the topology bar, and cleanup cannot fix it.** Hull-of-disks lofting plus CSG emits long fan + triangles. Join-triangles and limited dissolve turn them into a patchwork with no loops longer than 8 + edges. The wing top and bottom become single ngons of up to 59 sides. 61% of the exported triangles are + slivers. On the close-up, the body hull's facets show through the weighted normals as flat shading + patches. +- **A's UVs look fine on paper** (area 0.12), because Smart UV cuts the surface into 51 islands, 15 of them + crumbs under 4 faces. That breaks "parts of one object kept together". See its UV tile. +- **B and C meet the topology-flow bar on the loft and the wing.** Body rings are closed 36-edge loops from + stinger to beak, and the wing is a quad grid. +- **Where B and C miss the topology bar:** + - The wing-body boolean seam leaves 215-229 ngons and most of the poles. That is the one place a hand + modeller would retopologise. + - The pods intersect the body as separate shells instead of being merged. + - C's loft caps are 36-gons and its pod tips are pole fans. Geometry Nodes has no grid-fill or Coons + patch, so B's quad caps have no direct node equivalent. +- **The remaining slivers in B and C are mostly the bevel strips themselves.** A 0.004 strip against + 0.025 edges is inherently thin when triangulated; her bevel style has the same property. Excluding faces + of bevel width, C has 157 and B 336, from the thin rings near the stinger and beak tips. +- **The bevel bar is met on the outline in all three:** one segment, a bright highlight on the wing edge + band, weighted normals on. +- **It is not met at the wing-body junction**, where the crease is under 30° over much of its length, so + it gets no bevel. That is a hard intersection line, not a fillet. The edge counts above 60° (77-86) are + similar in all three, so the leftover hard edges are in the description (fin, pod ends, crease), not in + the lowering. +- **The normal-deviation metric** (mean angle between custom and face normals, 3.6-4.4° in all three) is + not diagnostic. The largest faces here sit on the curved body, which is legitimately curved. I have + dropped it as evidence. +- **The UV bar is met by B and C.** Each has about 11 islands of 1% of area or more: + - body top, body bottom; + - wing top and wing bottom per side; + - the wing edge band; + - pods, caps, fin. + + Angle error is about 1° and area distortion about 0.11. Packing wastes 47-61%, which is plain + pack_islands; no trim or texel-density decisions were made. + +## Editability (nose_k 1.0 to 1.2, plus a wing_sweep 0.1 that the description lacked) + +| | What the edit is | Rerun | Topology after the edit | +|---|---|---|---| +| A | 2 `.scad` lines (one value, plus a `multmatrix` shear on the planform inside `wing_side`) | OpenSCAD 2 s + cleanup script ~3 s | It changes. Faces 1439 to 1434, ngons 168 to 173, islands 51 to 48. Every edit re-rolls the patchwork, and any hand fix-up in Blender is lost. | +| B | `DESC_OVERRIDES` or `desc.py` values; sweep is 1 line in the wing loop | Script ~3.6 s, produces a new .blend | Stable. Loops 86 to 86, islands 23 to 23, +4 faces at the boolean seam. Hand work is lost on rerun. | +| C | Two modifier input values. Sweep was one exposed input plus 2 math nodes. | Live in the modifier panel. Script build ~3 s. | Stable. Loops 35 to 35, islands 21 to 21, +2 faces. Hand work can only be stacked as later modifiers; editing the result means applying the modifier. | + +`sheet-edit.png` shows the edits. They are small on screen: the nose grows from 0.52 to 0.552 and the wing +tips sweep back about 0.05. + +## B and C converge + +Same fidelity (0.841 / 0.840). Same UV quality (angle error 1.04° / 1.14°, area 0.113 / 0.118). Same island +structure, and the same misses at the boolean seam. The differences are resolution choices and two Geometry +Nodes gaps: +- the caps are ngons, and the pods are UV spheres with pole fans; +- there is no bevel node, so the bevel is a modifier after the tree, and the unwrap happens before the + bevel. The bevel faces inherit UVs from the neighbouring face, and that measured fine. + +**Geonodes is a practical lowering target the model can drive.** One generator script, about 170 lines, +produced a working parametric tree. I hit four Blender 5.2 API traps before it ran: +1. The Index Switch has a virtual extend socket. +2. Several enum properties became menu sockets that take UI labels: `'Angle Based'`, not `'ANGLE_BASED'`. +3. Modifier inputs are set through `mod.properties.inputs[id]["value"]`. `mod[id]` is gone. +4. **The GN Mesh Boolean with the Exact solver corrupted named attributes.** Wing vertices came out with + `cut` and `endcap` = 1, values the wing never had, even after storing explicit zeros. The Manifold + solver propagates them correctly. + +Point-attribute seams also broke where the boolean split an edge, which merged the body's top and bottom UV +islands. The fix was carrying the seam as an edge attribute, `cut_e`. I checked it with +`tools/c_opunwrap.py`: the same geometry unwrapped by the operator gave the identical bad result, so this +was a seam-definition issue, not the UV node. + +**Readability:** the tree's node positions are auto-placed with no frames, so as a canvas it is spaghetti. +The readable artefact is the Python generator. A tree she would want to open needs frames per part +(body / wing / pods / UV) and a layout pass, which I did not do. + +## Conclusion on her question + +**OpenSCAD as the lowering does not give geometry she would ship, and automatic cleanup cannot rescue +it.** Its output has already lost the construction intent: which vertices are a station ring, which faces +are the wing sheet. The topology numbers above are the evidence: 412 poles per 1000 vertices, a longest loop +of 8 edges, 59-gons, 61% slivers, and 51 UV crumbs. + +**Blender lowering (B or C) from the same description meets her bar where the description has clean +construction:** the body loft and the wing sheet. Both misses come from the construction, not the lowering +tool: +- the boolean seam where the wing enters the body (ngons and poles); +- intersecting pods and fin. + +Neither B nor C is a finished part yet. The description itself is crude, so the part is too: a +constant-thickness slab wing, no crease fillet, blob pods, 0.84% chamfer. + +**Recommendation: the hybrid.** The model writes the description, in OpenSCAD or anything else that carries +the parameters and the construction intent: stations, planform, plane, part list. Blender lowers it by +construction, not by CSG output. +- **Geonodes (C)** is the better target for her. It is equal in quality to B and live-editable from the + modifier panel, which is her tool. +- **B's bmesh script** is the fallback for constructions nodes cannot express, such as quad caps or + grid fill. +- **"Real OpenSCAD" can stay as the authoring and fitting surface** only if the lowering reads its + parameters and not its mesh. Lowering the CSG tree itself reproduces A. + +Confidence: moderate for "A cannot meet the bar". That follows from what the output loses, and the numbers +are large. Low to moderate for "C is shippable with work": +- one hull, one description; +- construction and seam choices are mine; +- no human retopologised the seam for comparison; +- the eye on the renders was mine, not hers. + +## Could not test / caveats + +- **QuadriFlow, A2's best automatic retopology**, refused in background mode. It reported "needs to be + manifold" even on bmesh-manifold input, and again after a voxel remesh. A voxel remesh alone (0.004) + gives 114k faces and 380 poles per 1000 vertices, and destroys the hard edges that bevel-by-angle needs. + I did not try QuadriFlow in the GUI. `tools/lower_a2.py` and `out/A2.*` hold the failed attempt. +- Her studio scene, texture baking and decals, export into the game engine, and LOD were not tested. + Packing efficiency was not tuned. +- Interpenetrating pods were a choice. Booleaning them in would add more seam ngons of the kind the wing + produced. +- The A cleanup parameters (dissolve 2°, join 10°/40°, Smart UV 66°) were set once, not swept. +- Tools: `tools/run.sh measure|render|target|score `, `measure.py`, `render.py` and `sheet.py`. The + scorer is run 1's `mp`, rebuilt into `tools/mp-target`; that 156 MB build directory has been deleted, so + rebuild it before rescoring. `dbg*.py` are throwaway diagnostics. +- No processes of mine are running. The operator's GUI Blender (PID 36324) was never touched. I waited out + another agent's background Blender smoke and a cargo build before my first jobs.