Compare commits

..
Author SHA1 Message Date
Meta Vens
1aa1ee2e83 Research: truck B-rep kernel evaluation (facts, torture harness, OCCT STEP checks)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:58:39 +02:00
Meta Vens
df6a1cc96f Research: Aetheris B-rep kernel evaluation (facts, torture harness, OCCT STEP checks)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:02:38 +02:00
150 changed files with 6412 additions and 85531 deletions

View file

@ -1,32 +0,0 @@
# Brush tree: map (model page: identity, lifecycle and authority)
One row for each persistent or crossing kind. A cell this draft cannot settle
names the question that settles it (questions in the mind, campaign brush-tree). No cut is mapped until
those questions are answered.
| Kind | What names it | What happens to it over time | Who decides |
|---|---|---|---|
| **Brush-tree document** (CultCache `.cc`, Geometry.Csg schema) | One document per ship hull. The key is the hull's asset id (store location and granularity: Q `tree-store`). | Created by the first commit. Each accepted commit replaces it and bumps a monotonic `revision`. It is never edited in place outside a commit. It is deleted only by the operator. | The commit primitive (I3) decides whether a change lands, by revision check. The operator and the model propose changes; neither writes around the commit. |
| **Branch node** (union, difference, intersection, and grouping `module` instances if kept) | Its name path within the tree, siblings unique (Q `brush-identity`). | Lives and dies with tree revisions. Ops apply to its children only (I6). | The tree commit. |
| **Parametric layer** (top-level parameters, module definitions, symmetry and relations) | Parameter name and module name, tree-scoped. | Whether it persists in the tree or is flattened at parse is Q `openscad-surface`. If kept, evaluation to brushes is derived per revision. | The tree commit. Fits propose values; the commit accepts them. |
| **Brush** (leaf: primitive, op, transform, params) | Q `brush-identity` (recommended: a stable minted id carried in the text as a named argument, plus a human name). | Created, edited and deleted only by tree commits. `generation` bumps on each edit, for dirty-frontier rebuilds. | The tree commit. A fitter or a Blender gizmo only proposes. |
| **Primitive vocabulary** (the set of primitive kinds and ops) | Kind name in the schema, matched to an OpenSCAD built-in where one exists. | Grows by schema version. A kind enters the authoring surface only when the polygon kernel and the field both honour it (I5). Which kinds: Q `vocabulary-growth`. | Geometry.Csg, through Hands cuts; the operator rules the set. |
| **Brush surface** (material, UV parameterisation, seam, smoothing, bevel or blend radius) | `(brush id, surface slot)`. Slots are fixed per primitive kind, e.g. cylinder side, top cap, bottom cap. | Exists while its brush exists, edited with it. A cut face takes one explicit policy (Q `cut-face-surface`). Seam placement per curved kind: Q `seam-policy`. UV channel meaning: Q `uv-channels`. | The brush (I7). Lowerings only evaluate it. |
| **Blend or bevel radius** | A surface or op argument on the brush (Q `bevel-operator`). | Edited with the brush. | The brush. Operator family and placement: Q `bevel-operator`. |
| **Authoring text** (OpenSCAD-shaped) | Not persisted as truth. A printed text carries a header naming the tree key and its base revision. | Printed on demand from a revision. Parsed back only through the commit primitive. A stale base is refused. It may be written to scratch for the model to edit, then discarded. | The printer derives it. The commit decides whether a parse lands. |
| **Lowered mesh** (triangles, normals, UVs, per-triangle brush and surface ids) | `(tree key, revision, LOD)`. | A cache, regenerated whenever the revision changes. Never edited and never read back (I8). | Geometry.Csg's lowering derives it. |
| **Field and bevel geometry** (SDF bound, smooth-blended cut strips) | Same as the lowered mesh. | Derived per revision. | Geometry.Csg's field (I4, I9). How bevels reach the mesh: Q `bevel-to-mesh`. |
| **Fit request and result** (primitive, parameters, residual, inlier set) | Not persisted. The `brokkr-ctl` call returns it; the receipt id is its only handle. | Ephemeral. Accepting a fit is a tree commit; the rejected ones vanish. | Fitter math computes it with I4's distance (location: Q `fitting-home`). The model and the operator decide acceptance. |
| **Selection read** (Edit Mode vertices and faces with world coordinates) | The receipt of the `selection` command. | Read live per call, never cached as truth. Written as a scratch file the receipt points to (format: Q `blender-transport`). | Blender owns selection. The bridge reports it. |
| **Viewport image** (`look`) | A file path printed by `brokkr-ctl`, named by command id. | Written per call to a Brokkr scratch directory, pruned by count, never kept as state. | Blender renders it. The bridge writes it. The retention cap is a proposed Defaulted ruling. |
| **Deviation heatmap** | `(tree key, revision, source mesh)`. | Derived per revision. Shown in Blender as a colour attribute on a projection copy, never on the source. | Geometry.Csg's distance (I4) computes it. The bridge displays it. |
| **Blender command intent** (`brokkr.blender.command_intent.v0`, extended: look, selection, fit, run, apply-tree) | Single-use `commandId` minted per act (Unity F6 carried over). | Written once by a caller, executed at most once, never rewritten. | Callers write it. The bridge alone decides execution, after admission. |
| **Blender command receipt** (`brokkr.blender.command_receipt.v0`) | Keyed by `commandId`. | Written once by the bridge. Pruned by the cap rule Unity already uses. | The bridge alone. The `brokkr-ctl` exit code derives from its status. |
| **Run script** (`brokkr-ctl blender run <script>`) | The script's content hash, recorded in the intent and the receipt. | Executed at most once as one undo step. The script is not stored by Brokkr. | Admission (allowed-action list, sink enabled) decides. Blender executes. Default admission: proposed Defaulted ruling. |
| **Agent-command enable flag (Blender)** | One per Blender project or file, echoed in the snapshot. | Off by default. Toggled only by the operator. | The operator only (Unity F5 carried over). |
| **Transport and auth material** | Q `blender-transport`. | Q `blender-transport`. | Q `blender-transport`. |
| **Blender projection objects** (lowered-mesh object, brush proxies) | A custom property holding tree key plus brush id. | Rebuilt from the revision. An operator edit to a proxy, such as a gizmo move, becomes a proposed tree commit through the bridge. Edits to the lowered-mesh object are never read back. | The bridge projects them. The tree commit decides. |
| **Tripo source mesh** | Its Blender object in the `Source` collection. | Read-only for the loop (I14). | The operator, and Aetheria's add-on. |
| **Shipped hull geometry** (Aetheria ship `.cc`) | Aetheria's existing package keys. | Written by Package from the Blender object (I15). | Aetheria's Package. Whether the lowered mesh enters Blender first: proposed Defaulted ruling. |
| **Impostor atlas** (far LOD) | `(tree key, revision)`. | Derived from the lowered mesh and materials, not from SDF marching. Whether built here: Q `impostor-scope`. | Whichever campaign builds it. Not this one, if recommended. |
| **In-game rebuild** | Q `bake-or-runtime`. | Q `bake-or-runtime`. | Q `bake-or-runtime`. |

View file

@ -1,229 +0,0 @@
# Brush tree: target (r3)
Campaign `brush-tree`, owned by the Aetheria/Ghostlight Self. Drafted by Imagination
(`imagination-brush-tree-target-r3`, session `self-2026-10-11-ag2`) on 2026-10-12.
It supersedes r2 (`a7879dad`). This page holds the ends, not the means: no cut plan. The typed
`brush-tree:target:r3` holds the invariants, the not-in-scope list and the canonical implementations;
this page holds why, the authority map, and what is still unknown.
- **Repos:** `GameCult/GameCult.Geometry.Csg` (the tree, the cage evaluator, the kernel, booleans and the
weld, the scorer), `GameCult/Brokkr` (the agent's hands in Blender), `GameCult/Aetheria` (the first
consumer).
- **Working branch:** `eureka/brush-tree`.
## The rulings this target rests on
Read them in the mind by id; their operator quotes are the authority, not this page.
- `brush-tree:ruling:hand-cages-enter-the-tree` (Operator, 2026-10-12): a continuous hull's primary
form is an artist's cage, entered as a tree primitive with its distance defined once in Geometry.Csg;
detail is cut into its evaluated surface. Automatic cage generation is research and gates nothing.
- `brush-tree:ruling:boolean-cleanup-tighter-curve-wins` (Operator): detail is boolean cuts into the
evaluated hull, and the weld cleanup is automated by her rule. Her words: "the surface with the
tighter curve defines the silhouette of the cut, and the spare vertices that come from the flatter
surface all get merged away."
- `brush-tree:ruling:panel-lines-by-split-boolean-r2` (Operator): a panel line is a through-cut that
keeps both sides; the stack's one-segment Bevel and Weighted Normal turn the split into a seam. The weld
rule applies to its seams too. It is the only panel-line method: the operator chose geometry over
parallax decals (`aetheria-release:ruling:panel-lines-are-geometry`), so hull panel lines are in this
campaign's scope.
- `brush-tree:ruling:primary-form-is-the-hand-cage` (Defaulted): research generators are scored against
the hand cage's limit surface, not against a masked or smoothed scan.
- `brush-tree:ruling:representation-per-hull`: cages for continuous bodies like Dexter Quiet, primitives
for mechanical hulls like the squatter mech ship.
- `brush-tree:ruling:shippable-part-bar`, `:no-unownable-tools-in-loop`, `:openscad-target`,
`:brokkr-beats-blender-mcp`, `:cli-not-mcp`, and the defaulted rulings on the Blender sink, `run`, docs
lookup, look files, the per-ship tree `.cc`, persistence through cultcache-rs, the Package path,
Studio and impostors: unchanged from r2.
- `brush-tree:ruling:pursue-sdf-composition-remeshing` and `:spike-sdf-smin-to-cage`: still in force,
but narrowed by `hand-cages-enter-the-tree`. SDF composition with automated remeshing is one research
generator among the cage-connectivity candidates, not the production path.
## What changed from r2, in one paragraph
r2 made the SDF composition the authority for a continuous hull's shape and derived a fitted cage from
it, with the operator's hand cage as the measuring stick. Two findings ended that. The
cage-connectivity pass (`cage-connectivity-prior-art.md` @ 492f83a) found no open method that reaches
a coarse quad cage at the hand cage's quality. Every method starts from a dense mesh, and only
Lavoue 2009 reports output at about 200 faces, with n-gons and high valence accepted. The operator then
ruled her own cages into the tree, and named her detail practice: boolean cuts into the subdivided form,
then a weld she does by a rule simple enough to automate. So in r3 the cage is authored, not derived.
The field leaves the production path for continuous hulls. The open engineering problem moves from
generating connectivity to cleaning booleans on a curved evaluated surface.
## What the pipeline turns a brush tree into
**The tree** is one typed document per ship (`tree-store-per-ship-cc`). Every part, meaning a top-level
branch, declares its representation, `cage` or `kernel`. The declaration is authored, never inferred.
Each part carries a detail list of cutters, each tagged `cut` (remove) or `split` (keep both sides).
**A `cage` part** (continuous body, Dexter Quiet):
1. **Primary form: the cage primitive.** The tree holds control points, faces (quads and triangles),
edge crease weights and the mirror plane. She authors it in Blender. A `brokkr-ctl` commit reads the
cage object and commits it as a new revision of the part against its base revision. After that the
Blender cage object is a projection, and an edit to it is a proposed commit, as for any brush proxy.
A commit is refused when a crease chain is open and does not end on the mirror plane (her finding:
open crease paths give "horrible geometry" in Blender's Catmull-Clark).
2. **Evaluated mesh, derived.** Mirror, then Catmull-Clark to the limit surface at a declared level,
with Blender's crease rules (`one-subdivision-rule`). Who runs this evaluation is question
`cage-evaluator`.
3. **Detail, derived from cutters.** Each cutter is a kernel brush, lowered alone to a closed mesh.
- `cut`: the evaluated mesh minus the cutter (gun ports, vents, intakes).
- `split`: the evaluated mesh minus the cutter, plus the evaluated mesh intersected with the cutter,
kept as two pieces that share one seam loop. This is how every panel line is made; there is no
decal path.
Every intersection loop is then welded by the rule below, and its edges get the bevel weight.
Detail never moves the cage. Outside a band around each loop, the output's vertices are the evaluated
mesh's vertices, unchanged.
4. **Shipped stack on the output object:** Bevel (one segment, limited by weight: the crease loops
plus every cut and split loop), then Weighted Normal. Seams for UVs lie on closed crease loops, part
boundaries and split loops, so every panel is its own island.
**The weld rule**, as specified for the spike (`boolean-cleanup-prior-art.md` @ 34cccf3 found it
nowhere under any name):
- Along each intersection loop, compare the two surfaces' normal curvature across the loop. The across
direction is the surface tangent perpendicular to the loop tangent.
- The surface with the larger magnitude owns the loop. The loop's vertices are sampled at the owner's
resolution and lie on the owner's surface.
- The yielding surface's vertices within one ring of the loop, plus any of its loop vertices that are
not owner vertices, collapse onto the nearest owner loop vertex.
- A collapse is refused if it flips a triangle or drops the worst angle below a floor. This is the
Manifold `CollapseEdge` guard (same file, s.1).
- In a `split`, the two pieces weld their shared loop identically, so the seam stays closed vertex for
vertex.
Undefined so far, and named as the spike's failure cases: equal curvature, saddles where the across
direction changes sign, a loop crossing a crease, and an ownership switch partway along one loop.
**A `kernel` part** (mechanical, the squatter mech ship): unchanged from r2. The polygon kernel builds
exact planar faces with scoped booleans, a one-segment bevel on hard edges, face-weighted normals and
UVs from each brush's declared charts. Its cutters use the same boolean and weld as a cage part's
detail.
**Both** reach Aetheria the same way (`lowered-hull-through-package`): an object in the ship file,
exported by the existing Package. Nothing reads an evaluated mesh, cut mesh or render back as truth.
### Distance for a cage primitive, stated honestly
`one-distance-per-primitive` holds for the cage. Its distance is defined once in Geometry.Csg as the
unsigned distance to the limit surface, signed by winding number. The cost is the honest part.
- The limit surface has exact evaluation: Stam 1998, OpenSubdiv `LimitStencilTable`, and run 3's
`X = S c`, linear in the cage. A closest point on it comes from a Newton search
(Marinov and Kobbelt 2005). So the distance is computable (`subd-cages-mixed-hulls-and-open-kernels.md`
@ 390db63, s.1).
- No source gives a **cheap distance bound** away from the surface, that is, a Lipschitz-safe lower
bound usable for sphere tracing, Newton projection from afar, or smooth-min composition with other
primitives. The r1 field assumed one for every primitive.
- So the cage primitive is refused inside any field composition (smooth union, hard `max` crease) and in
sphere tracing (`no-silent-skips`). Its distance serves measurement only: heatmaps, scores and fit
residuals, evaluated near the surface from a dense limit sample. Nothing in the production path needs
more. A bound, if one is ever needed, is research.
### Where SDF composition still has a role
- **As a research generator.** An SDF program with smooth-min, remeshed and refit to a cage (runs 4-7),
is one candidate source of cage connectivity. It is scored like any other candidate.
- **As the kernel brushes' distance.** `field.rs` stays the one distance for kernel primitives, for
heatmaps and scores.
- **Not as a part representation.** No production part is a `field` part, and smooth union leaves the
production vocabulary. A generator's output, if it ever meets the bar, enters as a cage, and so does
every continuous hull.
### Authority map (r2 to r3)
- **Owner of a continuous hull's primary form:** the cage primitive in the tree. The field is no longer
an owner. The cage is no longer derived from the field; it is authored and committed.
- **Inputs:** the operator's cage object in Blender (through the commit primitive only), the part's
cutter brushes.
- **Outputs:** the lowered part object (evaluated, cut, split and welded mesh, with bevel weights), and
scores.
- **Derived state:** the evaluated mesh, the cutter meshes, the cut mesh, UV islands, heatmaps, renders.
These are cache only, regenerated per revision.
- **Forbidden writers:** the SDF program, the cage fitter and any remesher writing a production cage.
They write research artifacts only, and a research cage enters only by a commit like any other. Also
forbidden: Blender's Boolean modifier, or hand edits to the lowered object, deciding the shipped
detail.
- **Shared paths:** the operator's cage edit, a model's script edit and a research generator's
candidate all enter through the one commit primitive. Cut-in and split use one boolean and one weld.
Kernel parts' cutters use the same boolean.
- **Deletion line:** r2's "cage connectivity generator" and "cage fitter" leave the production owner
table and become research ports. `field` leaves the per-part representation enum. Smooth union is not
added to the tree vocabulary.
### Owners, inputs, outputs, mock points
| Owner | Inputs | Outputs | Mock point |
|---|---|---|---|
| Tree (Geometry.Csg) | text or a cage object, through the one commit primitive | typed tree revision | in-memory store |
| Cage evaluator | cage primitive, level | evaluated mesh (mirror applied) | trait; Blender's output as the oracle (`cage-evaluator`) |
| Cutter lowering | cutter brush | closed cutter mesh | the kernel, per brush |
| Boolean | evaluated mesh, cutter mesh, `cut` or `split` | pieces, intersection loops with both sides' faces | trait; kernel chosen by the spike |
| Weld | pieces, loops, the two surfaces' curvature | welded pieces, loop edges with bevel weight | pure mesh math |
| Kernel | kernel part | mesh with per-face brush and surface ids | trait |
| Scorer | two meshes | the bar's numbers | pure mesh math, no Blender |
| Research generators | a reference (scan or SDF program) | candidate cage | port; never a production writer |
| Brokkr bridge | intents from `brokkr-ctl` | receipts, cage reads, objects in the ship file | directory store or loopback, per `blender-transport` |
## The hand cage
**What it is** (`docs/research/dexter-quiet-cage/` @ d426f86, note @ 4b55c58): half hull of 29 vertices
and 25 faces (19 quads, 6 triangles); about 50 faces mirrored. Nine creases form one chain that the
mirror closes into a loop round the wing rim. At level 3 it evaluates to 3,154 vertices and 3,152 quads,
with 31 irregular vertices and no UVs. Against the scan (vertex to nearest vertex, % of the 1.492
diagonal): cage to scan mean 0.88, p95 2.04, max 3.11; scan to cage mean 1.29, p95 4.12, max 6.03.
Those numbers mix her declared liberty with detail the cage omits by design. They describe the
reference; they are not a bar any production output is held to.
**It is the production primary form, not only the yardstick.** Production fidelity is by construction:
away from detail loops, the shipped part is the cage's limit surface.
**Research generators are scored against it** (`primary-form-is-the-hand-cage`). A candidate's
evaluated limit surface is compared with the hand cage's (Mirror, limit, no Bevel) vertex to nearest
vertex both ways, at half to double 3,154 vertices, and by three-axis silhouette IoU. Every measurement
reports the candidate's source against its reference and the candidate cage against its own source
separately, besides the end-to-end number; run 7 lost a spike to an unattributed number. The scan is the
reference only for the hand cage's own recorded liberty. A candidate is interesting when it is no worse
than the hand cage's own spread and meets the cage bar: at most about 200 faces (4x), quads and
triangles only with at most 24 % triangles, closed creases, and a watertight level-3 limit mesh with at
most 62 irregular vertices. Meeting the bar makes a candidate committable as a cage. It gates nothing
else.
## Not yet known (owed before or during the next cuts)
- **The weld rule's behaviour on real geometry.** It has never been run. The spike brief is
`spike-boolean-weld.md` (Self's scratch); its measures and failure cases decide whether the rule ships
as stated, needs a tiebreak, or needs the cutter tessellated to the hull's density.
- **Which boolean kernel:** Manifold through `manifold-csg` (Apache-2.0, C++ underneath, with provenance
and its own degenerate cleanup), boolmesh (pure Rust, MPL-2.0, released 2026-10-11, 0 % doc coverage),
or an extension of the crate's own kernel. The spike measures the first two; the crate's kernel is
planar-convex today.
- **Who evaluates the cage** for the lowering: question `cage-evaluator`.
- **Split pieces' interior caps:** a through-cut of a closed hull leaves cross-section caps inside both
pieces. Whether they are kept (closed pieces, hidden triangles) or dropped (open pieces) depends on how
Bevel and Weighted Normal treat the open boundary. The spike probes it.
- **How a `cage` part and a `kernel` part meet** on one hull. Under r3 a union with the same boolean and
weld is the natural candidate, but nothing has measured it.
- **Text for a cage part.** It prints as a polyhedron-like `subdivision_cage(points, faces, creases,
mirror)`. OpenSCAD would render the cage's control polyhedron, not its limit surface, so cage parts sit
outside the "OpenSCAD renders it the same" half of `text-round-trips`, as smooth-min parts did in r2.
## Research it rests on
All in GameCult.Geometry.Csg `docs/research/` on `eureka/brush-tree`:
- `dexter-quiet-cage/` @ 4b55c58: the reference cage, the primary-form note, her screenshots (cage files
@ d426f86).
- `cage-connectivity-prior-art.md` @ 492f83a: no open method reaches a coarse quad cage at this quality.
- `boolean-cleanup-prior-art.md` @ 34cccf3: no named precedent for the weld rule; Manifold's collapse
guard; Adaptive Mesh Booleans' failure modes; ownable kernels.
- `detail-on-subd-prior-art.md` @ c513992: cut into the evaluated mesh is the only option that keeps the
cage independent and regenerable; support loops grow the cage.
- `sdf-authoring-text-prior-art.md` @ 6a575d2: no SDF tool round-trips a typed tree through text.
- `subd-cages-mixed-hulls-and-open-kernels.md` @ 390db63: limit evaluation, closest point, no cheap
bound.
- `shippable-bar/`, `mesh-probe-run1/` to `mesh-probe-run7-im-field/`,
`openscad-evaluators-and-csg-export.md`, `primitive-seams-and-sdf-bevel-meshing-prior-art.md`,
`brush-surfaces-and-sdf-bricks-prior-art.md`, `octahedral-impostors-prior-art.md`: as in r2.

View file

@ -0,0 +1,145 @@
# Aetheris evaluation (2026-10-14)
Serves the operator's request to evaluate https://github.com/yuechen-li-dev/Aetheris (a C# AGPL-3.0 B-rep kernel) for capability and ideas, campaign brush-tree, session self-2026-10-14-ag. Facts only, measured at Aetheris 335dfb07df on Yggdrasil; Self's reading is in the conversation, not here. The torture harness is in `harness/`.
# Aetheris evaluation facts
Measured at Aetheris commit 335dfb07df97295c541ab3fcf50e37b9f97b1846 (2026-10-13, "Claude Perf optimization M3"), cloned to
`<scratch>\eyes-aetheris\repo` (read only on Starfire; every build/run on Yggdrasil via ygg-verify.sh, image mcr.microsoft.com/dotnet/sdk:10.0, Linux).
Tags: [V] measured or read this pass; [S] stated by the repo's own docs only, not verified; [M] the author's marketing claim.
Job logs: `<scratch>\eyes-aetheris\jobs\job1..job9.log` (job2 = tests, job6 = torture+canonical, job7/8/9 = OCCT detail probes).
Harness (mine, not Aetheris's): `harness\Program.cs`, `harness\cases.json`, `jobs\ocp_check.py`, `jobs\inspect.py`.
## Q1 Shape of the codebase
- [V] History: 2002 commits, all by one author (Yuechen Li), first 2026-02-25, last 2026-10-13. GitHub: 47 stars, AGPL-3.0 (gh api). 5119 files, 1545 .cs files, 293,876 lines of C#.
- [V] Lines of C# (git ls-files, `.cs`): Kernel.Firmament 66,280; Kernel.Core 57,373; Kernel.Firmament.Tests 40,718; Kernel.Core.Tests 25,513; CLI 12,119; CLI.Tests 10,172; Continuum 8,511; Firmament.FrictionLab 8,244; Surfacing 6,217; SheetMetal 5,749; StandardLibrary 3,831; the rest each under 3.7k. ~50 projects in `Aetheris.slnx`.
- [V] Test attribute counts ([Fact]/[Theory] lines): Firmament.Tests 1886, Core.Tests 909, CLI.Tests 379, FrictionLab.Tests 225, Continuum.Tests 135, Modules.Tests 94, SheetMetal.Tests 85, others smaller. 15 xunit projects.
- [V] CI: one workflow `.github/workflows/dotnet.yml`; the main job is `ubuntu-latest`: restore/build server and kernel tests, `dotnet test Aetheris.Kernel.Core.Tests --filter "Category!=SlowCorpus"`, then build and test the whole solution (`-m:1`). A Windows job (`public-library-build`, `workflow_dispatch` only) packs NuGet libraries. It also checks out a second repo, `yuechen-li-dev/Copeland` at a pinned commit (`scripts/managed-wgsl-commit-pin.txt`), packs it into a local NuGet feed (`.github/actions/prepare-managed-wgsl`).
- [V] Dependencies: Kernel.Core has one NuGet package, `Machina.Typography.OpenFont` (csproj). Kernel.Firmament adds `Copeland.TS.Backend.Wgsl 0.1.0-preview.1-a3.16c0ce996358af80`, which is NOT on nuget.org (restore error NU1101 `Copeland.TS`, job1.log) and has to be built from the Copeland repo. Copeland's licence is GPL-3.0 (gh api repos/yuechen-li-dev/Copeland/license). Other packages: Rhino3dm (3dm import), EF Core Sqlite, Roslyn, Swashbuckle, WebView2, DocumentFormat.OpenXml, PDFtoImage (each one project).
- [V] No OCCT or other native geometry kernel: no P/Invoke except one DllImport in `Aetheris.CLI/CadAssistantInspection.cs`; grep for opencascade/occt in Kernel.Core and Kernel.Firmament .cs finds nothing. OCCT appears only as an interop/validation reference in docs and tests. Geometry code is all C#.
- [V] Surface types (`Aetheris.Kernel.Core/Geometry/Surfaces/`): PlaneSurface, CylinderSurface, ConeSurface, SphereSurface, TorusSurface, LinearExtrusionSurface, SurfaceOfRevolutionSurface, BSplineSurfaceWithKnots (+ BSplineSurfaceRationalReduction), HelicalRibSurfaceRealizer. Curves (`Geometry/Curves/`): Line3, Circle3, Ellipse3, Hyperbola3, BSpline3Curve, CylindricalHelix3, CylinderCylinderIntersectionCurve, plus pcurves.
- [V] So "NURBS genuinely absent" is false as stated: B-spline curve and surface types exist in the kernel, and the STEP outputs of the Perforation/Presentation fixtures contain `B_SPLINE_CURVE_WITH_KNOTS` (job7 ZERO probe). Docs say products are "rational-free"/non-rational (docs/public/reference/supported-features.md, README.md:"exact analytical geometry as its foundation").
- [V] Booleans are not a general intersection engine. `Brep/Boolean/BrepBoolean.cs` (2158 lines) plus ~20 sibling files (8,346 lines total) classify the pair into hard-coded recognition families (`BooleanExecutionClass { PlanarOnly, PlanarWithAnalyticHole, UnsupportedGeneralCase }`, BrepBoolean.cs:36-41; `BooleanTopLevelCase` enum at :74-84): axis-aligned box vs box (orthogonal cells), box with a world-Z cylinder/cone/sphere hole ("safe composition"), cylinder-root centre bore / keyway, prismatic through-cuts. Selection is by `JudgmentEngine` utility scoring (Kernel.Core/Judgment). Everything else returns an Error diagnostic `NotImplemented`. Tolerances: `ToleranceContext.Default` = linear 1e-6, angular 1e-9, relative 1e-12 (Numerics/ToleranceContext.cs:36); the family validators compare against `tolerance.Linear`.
- [V] No exact surface-surface intersection curve solver is used by the boolean entry point; the intersection-curve classes (CylinderCylinderIntersectionCurve, TransverseConePlaneIntersection) are used by feature builders, not by arbitrary boolean pairs (observed behaviour in Q3: Steinmetz refused).
- [V] Firmament does not expose CSG: docs/public/firmament/geometry.md:136 states "public Union / Subtract / Intersect, and general CSG trees are not Preview 3 Firmament features"; `scripts/Test-CanonicalFixtures.ps1` lists `Union|Subtract|Intersect` as a forbidden "legacy arbitrary Boolean authoring" pattern in canonical fixtures. Firmament authors feature-based models (Base, Boss, Pocket, Hole, Slot, Revolve, Loft, SectionChain...). Pocket is documented as "intentionally not arbitrary solid subtraction" (geometry.md:90).
- [V] Firmament -> kernel for CSG-ish work goes through `Aetheris.Kernel.Firmament/Materializer/CirBrepMaterializer.cs`: a registry of exactly three strategies, `subtract(box,cylinder)`, `subtract(box,box)`, `subtract(box,torus)` (the last one an "Unsupported" strategy), chosen by JudgmentEngine, from an SDF graph (`Aetheris.Continuum.Backends.Sdf`) (CirBrepMaterializer.cs:11-18).
- [S] README claims Aetheris is "a solved problem" relative to Parasolid/ACIS/OpenCascade; supported-features.md labels nearly everything "Bounded"/"Experimental" and says "general blend networks ... remain unsupported".
## Q2 Build and tests on Linux (Yggdrasil container)
- [V] Builds clean on Linux: `dotnet build` of Kernel.Core.Tests, Kernel.Firmament.Tests and Aetheris.CLI, Release, 0 errors, 0 warnings in the shown tails (job1/job2), after packing Copeland at the pin by hand. SDK 10.0 image; global.json asks 10.0.203 with latestFeature rollForward.
- [V] `Aetheris.Kernel.Core.Tests` (filter `Category!=SlowCorpus`): Passed 1063, Failed 0, Skipped 0, 27 s (job1.log).
- [V] `Aetheris.Kernel.Firmament.Tests`: Passed 2131, Failed 0, Skipped 0, 4 m 35 s (job2.log).
- [V] SlowCorpus lane (`Category=SlowCorpus`, 52 occurrences in Core.Tests): the run produced no output lines in my tail (job2.log "CORE-SLOW" empty); not established whether it ran zero tests or lacks the NIST corpus files. Unmeasured.
- [-] The other 13 test projects (CLI.Tests, Continuum.Tests, SheetMetal.Tests, Server.Tests, etc.) were not run; the web/TypeScript parts (Telos, Cadmata) were not built.
- [S] README/known-issues: "Release binaries are qualified for Windows x64 only. Framework tests on other platforms are not a binary-support promise." CI nonetheless runs the kernel and full solution tests on ubuntu-latest.
## Q3 Boolean torture (35 cases through `BrepBoolean.Union/Subtract/Intersect` via the public Core API; plus 11 Firmament models; plus 320 canonical fixtures)
Method: `harness/Program.cs` is a console app compiled as assembly `Aetheris.Kernel.Core.Tests` (to reuse the kernel's InternalsVisibleTo, same as the repo's own tests) that builds `BrepPrimitives.CreateBox/Cylinder/Sphere/Torus`, places them with a copy of the repo tests' `TransformBody` helper (extended to Sphere and Torus; plane/cylinder/line/circle only otherwise), runs the boolean, then `Step242Exporter.ExportBody`. Cases in `harness/cases.json`. Primitives are origin-centred; translation/rotation only. Caveat: the test helper does not transform cone surfaces or ellipse curves, which the cases do not use.
Results (job6.log; "vol" is Aetheris's own mass-property status):
| # | case | outcome |
|---|---|---|
| 01 | coplanar face union, box+box face contact | OK, exported |
| 02 | flush slab subtract (coplanar top and sides) | OK |
| 03 | coplanar top pocket | OK |
| 04 | coincident-edge union (edge-only contact) | REFUSED "edge-only/point-only unions are non-manifold" |
| 05 | coincident-edge subtract | REFUSED "rejects tangent/zero-thickness subtract contact" |
| 06 | tangent cylinders union | REFUSED "only supports recognized axis-aligned boxes" |
| 07/08/09 | Steinmetz: cyl r5 x cyl r5 intersect / union / subtract | all REFUSED (07, 08 "only supports ... boxes"; 09 "supports only world-Z aligned root and center-bore cylinders") |
| 10 | subtract leaving two 1e-7 side walls | REFUSED, with the misleading message "subtraction fully removes the left box" |
| 11 | pocket leaving a 1e-7 floor | OK, exported (no sliver refusal at kernel level) |
| 12 | pocket leaving a 1e-4 floor | OK |
| 13/14/15/16 | spheres: tangent / 1e-9 / 1e-4 overlap / 6-apart union | all REFUSED "only supports ... boxes" |
| 17 | box minus sphere centred on top face | OK, exported (see Q4: wrong volume) |
| 18 | torus half cut by a box | REFUSED "only supports ... boxes" |
| 19/20 | torus union cyl / torus minus cyl | REFUSED |
| 21 | 10x10 hole grid, 100 sequential cylinder subtracts | OK, 100/100 steps, 954 ms |
| 22 | two tangent holes | REFUSED "tangent safe-hole composition is rejected to avoid zero-thickness geometry" |
| 23 | hole tangent to side wall | REFUSED with a long explanatory message ("would leave a wall of zero thickness along a line") |
| 24 | two holes with a 1e-7 web | REFUSED (same "tangent" message: 1e-7 < tolerance 1e-6 treated as tangent) |
| 25 | perpendicular cylinder through cylinder, union | REFUSED |
| 26 | coaxial tube (cyl minus smaller coaxial cyl) | OK, exported (see Q4: OCCT segfaults reading it) |
| 27 | blind hole from top | OK |
| 28 | oblique (30 deg) through hole | OK (Aetheris mass-property call threw ArgumentOutOfRangeException; STEP exported) |
| 29 | box intersect sphere | REFUSED |
| 30 | box union box overlapping at an offset corner | OK |
| 31 | blind pocket leaving a 1e-7 side wall | REFUSED "bounded orthogonal pocket family only supports a single mouth or one opposite-face through-slot" (refused for family reasons, not sliver) |
| 32 | union of boxes overlapping by a 1e-7 slab | OK |
| 33 | union with partial face contact | OK |
| 34 | sphere fully inside box subtract (void) | OK, exported as BREP_WITH_VOIDS (see Q4) |
| 35 | cylinder minus enclosed sphere | REFUSED "does not support analytic tool surface kind 'Sphere'" |
- [V] Score: 35 cases: 14 succeeded, 21 refused (typed `NotImplemented` errors, no exceptions), 0 crashed, 0 timeouts. Every refusal message names a missing family rather than the geometry's true difficulty; only 04, 05, 22, 23, 24 are refusals of degeneracy (tangent/edge contact/zero-thickness).
- [V] All general curved-curved cases (tangent cylinders, Steinmetz x3, spheres x4, torus x3, perpendicular cylinders) are refused; none produced a result. The Steinmetz case is not computed.
- [V] A kernel-level sliver of thickness 1e-7 is NOT refused when it is a pocket floor (11) or a union overlap slab (32); the refusal claim holds for hole-hole and hole-wall tangency (22-24) and for pocket floor policy at the Firmament layer (below).
- [V] Firmament models (`harness/fa/*.firmament`, job6.log "FIRMAMENT-TORTURE"): 11 models via `aetheris build`: 4 built (fa05 hole poking through the edge, fa06 two holes with ample web, fa07 10x10 hole grid, fa10 pocket with a 3 mm floor), 7 refused with named diagnostics:
- fa01 hole tangent to side face: `FirmamentV2.SemanticHoleBuild: rejected: Hole 'Base.H0' ... is exactly tangent to the +X face ... zero thickness along a line` (Materializer/AirHoleFootprint.cs:55)
- fa02 tangent pair and fa03 1e-7 web: `firmament-v2-semantic-hole-overlap ... would be tangent to previously accepted hole` (AirHoleCompositeMaterializer.cs:80; Core BrepBooleanSafeCompositionGraphValidator.cs:229)
- fa04 overlapping holes: `overlaps previously accepted hole ... overlapping safe-hole composition is not supported`
- fa11 pocket with floor 9.9999999 mm deep in 10 mm host, required floor 1e-7: `firmament-pocket-through-depth ... remainingFloor=9.99999993922529E-08mm requiredFloor=1E-07mm` (float rounding put the floor just under the required value)
- fa12 required floor 0 mm: `firmament-pocket-minimum-floor-policy-invalid`
- fa13 floor 2 mm < required 3 mm: `firmament-pocket-minimum-floor-thickness` (PrismaticProfileComposition.cs:404)
- [V] Canonical fixtures (`fixtures/Canonical`, 320 `.firmament` files) through `aetheris build` (job6.log): 249 exit 0, 71 exit non-zero. The 71 are almost all not buildable by the `build` verb: garments (11, "garment-step-export-unsupported ... use garment build"), scenes (8, "scene-step-export-unsupported"), FEA/Analysis files (7, "firmament-v2-canonical-declaration-unknown:Analysis"), gear pairs (7, "gear-build-body-count-invalid ... use Assembly"), BodyState (6, "section-chain-section-count-invalid:minimum=2"), assembly components and Guitar/Atlas sub-files (~30, parse errors for partial files or "AssemblyIR must be successfully materialized"), 1 `Pipeline/profile-with-inner-loop` ("profile-segment-identity-collision"). I did not confirm each as expected-failure; they are reported as the CLI printed them.
## Q4 STEP validity checked independently with OpenCASCADE
Method: cadquery-ocp (pip, Ubuntu 24.04, Python 3.12) `STEPControl_Reader` default settings (`ReadFile`, `TransferRoots`, `OneShape`), `BRepCheck_Analyzer.IsValid`, `TopExp_Explorer` counts, `BRepGProp.VolumeProperties`; volume reference from an OCCT boolean (`BRepAlgoAPI_Fuse/Cut/Common`) on identical OCCT primitives and from analytic formulas. (job7.log OCP-CASES, job8.log INSPECT.)
- [V] Harness cases that produced STEP (14): BRepCheck_Analyzer.IsValid = True for the 12 that OCCT could read. Detail:
| case | OCCT reads as | volume Aetheris STEP | OCCT reference |
|---|---|---|---|
| 01 face union | 1 solid, 1 shell, 6 faces | 2000 | 2000 |
| 02 flush slab | 1 solid, 6 faces | 500 | 500 |
| 03 top pocket | compound, 0 solids, 4 shells, 14 faces | 920 | 920 |
| 11 floor 1e-7 | compound, 0 solids, 6 shells, 16 faces | 839.999992 | 840.000001 |
| 12 floor 1e-4 | compound, 0 solids, 4 shells, 14 faces | 840.0016 | 840.0016 |
| 17 box minus sphere (dimple) | 1 solid, 7 faces | **8261.799** | **7738.201** (= 8000 - 261.8); Aetheris's result is 8000 + 261.8, a bump, not a dimple |
| 21 hole grid 10x10 | 1 solid, 1 shell, 106 faces | 87433.629386 | 87433.629386 (analytic 87433.6294) |
| 27 blind hole | 1 solid, 8 faces | 3858.628331 | 3858.628331 |
| 28 oblique hole | 1 solid, 7 faces | 3673.516114 | 3673.516114 |
| 30 offset corner union | compound, 0 solids, 3 shells, 18 faces | 1875 | 1875 |
| 32 1e-7 slab union | 1 solid, 6 faces | 1999.99999 | 1999.999993 |
| 33 partial face contact | single SHELL, 0 solids, 10 faces | 1500 | 1500 |
| 26 coaxial tube | OCCT **segfaults** inside `TransferRoots` (python faulthandler: job9.log SEGV-26) | n/a | 904.1 analytic |
| 34 enclosed sphere void | OCCT **segfaults** inside `TransferRoots` (BREP_WITH_VOIDS + ORIENTED_CLOSED_SHELL .F., job8.log STEP-HEAD-34) | n/a | 7886.9 analytic |
- [V] Every STEP file contains one `MANIFOLD_SOLID_BREP` over one `CLOSED_SHELL` (job8 INSPECT), but for cases 03, 11, 12, 30, 33 OCCT returns no SOLID. Inspecting the case-03 file (job9.log STEPFILE-03, saved as `jobs/case03.step`): 38 `EDGE_CURVE`s, 14 faces, 20 vertices; of the 56 `ORIENTED_EDGE` uses, 18 edges are used twice and 20 edges are used once. A closed shell requires every edge used twice, so this "CLOSED_SHELL" has 20 unmatched edges: faces are fragmented at coplanar/face-contact boundaries without edge sharing. OCCT's volume still sums to the right number; BRepCheck calls the result valid; downstream tooling expecting one solid gets a compound of shells.
- [V] Case 26's file (first 70 non-point lines, `jobs/case26.step`): two sets of three faces, circles r=5 and r=3, each end face a full disc with a single outer loop, no annulus; i.e. the "tube" is expressed as two cylinder shells, not a through-bore. Remaining entities were not inspected.
- [V] Case 17 (box minus sphere): both Aetheris's tessellated volume (8263.2) and OCCT's volume of Aetheris's STEP (8261.8) say the exported body is box + hemisphere. Cause not isolated (could be face orientation of the sphere face in the STEP, `ADVANCED_FACE(...,#119 SPHERICAL_SURFACE, .T.)`, or the harness's translated sphere frame).
- [V] Aetheris's own `BrepMassProperties` returned status `Unavailable` (volume 0) for cases 03, 11, 12, 26, 30, 33, and threw ArgumentOutOfRangeException for 28; `NumericalWithBound` (tessellation based, "not authoritative") for the others.
- [V] Firmament outputs (job6.log OCP-FA): fa05, fa06, fa07, fa10 all read as 1 solid, 1 shell, BRepCheck valid; fa07 (10x10 holes) 106 faces, volume 87433.629386 equals the analytic value; fa10 volume 13648.0 equals 16000-2352 exactly; fa05 8326.43, fa06 8434.51.
- [V] Canonical STEP outputs (253 files from job6, job6.log OCP-CANON, `jobs/canon_dir.txt`): all 253 read by OCCT, BRepCheck valid 253/253. Solid counts: 199 files with 1 solid; 48 multi-solid (assemblies and scenes, 2 to 1366 solids); 6 with 0 solids: SectionChain/open-chain (1 shell, 4 faces, intended open) and 5 where OCCT reported 1 root transferred but 0 solids/shells/faces and volume 0 (Perforation cylindrical-grid-8, -24, -ordning-blank, -staggered-32, Presentation ordning-utensil-holder-blended; files of 0.86 MB to 18 MB with many `B_SPLINE_CURVE_WITH_KNOTS`, `PCURVE`, `EDGE_CURVE`; a rebuild confirmed `success: true` from the CLI, job7 ZBUILD). Why those read as empty was not determined.
- [S] The "no healing needed" claim: the measurement above uses OCCT's default reader (which applies its own shape processing); I did not disable it.
## Q5 GPU renderer ("Three Telos")
- [V] Location `Aetheris.Web.Runtime/telos/` (TypeScript package `@aetheris/three-telos`, AGPL-3.0-only): 3,169 lines of TS/WGSL (host.ts 1067, device.ts 284, temporal.ts 302...). First commit touching it 2026-10-05; 6 commits, latest 2026-10-07.
- [V] It is a WebGPU renderer (`navigator.gpu`, `@webgpu/types`), not CUDA/Vulkan/native. It owns device, attachments, camera, ray queries, mesh/field/line submission; Three.js 0.183.2 is used only for math, input adapters and CPU triangle picks ("Three supplies math and input adapters, never a GPU renderer", telos/README.md).
- [V] Two draw paths: (a) rasterising tessellated triangle meshes (`src/shaders/mesh.wgsl`, `line.wgsl`, `background.wgsl`, 4x MSAA, experimental temporal AA); (b) "field" draws: a per-occurrence quad whose fragment shader sphere-traces a signed distance function with a fixed 512-step budget, normal by central differences, writes `frag_depth` (`Aetheris.Kernel.Firmament/Display/telos-field.v.ts`; docs/release/THREE-TELOS-X0.md). The WGSL is compiled from typed shader source by the Copeland compiler (GPL-3.0 repo), browsers receive WGSL only. The field path is documented only for the cylinder/sphere smoke shaders; "Full CIR-DISPLAY-X1B packet/admission/field default qualification is not claimed" (X0.md), and docs/release/CIR-DISPLAY-X1.md is an "Honest stop" (Cadmata/Helios cannot yet use the field path by default).
- [V] It does not ray trace the B-rep exactly. The B-rep is tessellated by `Brep/Tessellation/*` for the mesh path; picking is CPU (Three Raycaster/server BRep query).
- [V] Headless: the only automation found is Playwright with `chromium.launch({executablePath: msedge.exe, headless: true})` hard-coded to a Windows Edge path (`scripts/display-qa-x1-playwright.mjs:12,51`) and a `tests/browser-witness.ts`; unit tests run with `node --test` on camera/inspection/overlay/temporal logic. Not run in the container; no WebGPU device was tried.
## Q6 Mechanisms
- Degeneracy refusal [V]: it is not a type-system or compile-time proof. It is a layered set of runtime checks that return Error diagnostics before geometry is emitted:
1. Kernel family validators compare geometry to `ToleranceContext.Linear` (1e-6): tangent hole-hole (`BrepBooleanSafeCompositionGraphValidator.cs:224-233`, `:827`), hole-wall tangency (`BrepBooleanCylinderRecognition.cs:452`), box-box edge/point-only contact ("non-manifold"), tangent spherical cavity (test `Subtract_BoxSphereTangentBoundary_ReturnsTangentContactDiagnostic`, BrepBooleanTests.cs:1555).
2. Firmament semantic layer: `AirHoleFootprint.cs:55`, `AirHoleCompositeMaterializer.cs:80`, `PrismaticProfileComposition.cs:404` (pocket `MinimumFloorThickness`), profile-level checks (explicit polygons reject degenerate area and self-intersection, boundaries.md:62), template `Require` clauses.
3. Anything outside the recognised families is refused wholesale (Q3), which also blocks most degenerate cases by construction.
Observed gap: 1e-7 pocket floors and 1e-7 overlap slabs pass the kernel API (cases 11, 32) and only the Firmament Pocket policy refuses (fa11-fa13).
- Firmament -> kernel [V]: V2 frontend in `Kernel.Firmament/FirmamentV2` (parse, static authoring, Concepts/Templates, validation, lowering), features lowered to "section-stack" operations and `PrismaticProfileComposition`/`AirHole*` materializers, then `BrepPrimitives`/`BrepBoolean`/`Step242Exporter`. V1 TOON/JSON path is kept as frozen compatibility (Kernel.Firmament/README.md). Ownership audit at `docs/development/architecture/system/artifacts/archaeology-m1/`.
- "Constraints erased at compile time" [V]: Firmament has no 2D constraint solver; layout is explicit coordinates in a `Concept Struct Layout On XY { Rect2 Stock { Center; Size } }` block and profiles are built by naming edges with `|>` (fixtures/Canonical/Integration/machined-mounting-block.firmament). Static Records/arrays/Tables, `with` derivation, `Template<...>` specialisation and `Pattern ... Over` are expanded and discarded before geometry (docs/public/firmament/overview.md; templates.md:100-108: `Require Name => BooleanExpression` is finite comparisons "not a general constraint solver", failure stops specialisation before materialisation). Units are part of value types and checked by the binder.
- Forge Host [V]: `Aetheris.Forge.Host` (1.4k lines) is a process protocol "Forge Host Protocol v1": commands `list`, `describe <stable-id>`, `invoke <id> --request - --out <dir>` with a JSON envelope (`protocolVersion`, `arguments` with unit strings like "120 mm", `artifacts`), response JSON with `success`; `describe` returns signature, units, defaults, enum cases, nested Record fields and named `Require` constraints. Clients in Python, Go, Rust, TypeScript shipped in `samples/forge-interop-x1/`; released as NativeAOT exe (Windows-qualified). Doc: docs/public/forge/interop.md; code `Aetheris.Forge.Host/ForgeProtocolHost.cs`, `Aetheris.Forge.KernelSDK/KernelSdk.cs`.
- LLM authoring [V]: repo-level `AGENTS.md` (use the CLI as ground truth, convergence rule: Success / Meaningful progression / Honest stop, fixture-location policy); a skill `docs/public/skills/firmament-modeling-strategy/SKILL.md` (+ `agents/openai.yaml`) on planning shape spines, separating measured vs estimated dimensions, and reporting compiler gaps with reproducible evidence; canonical cookbook `fixtures/Canonical/README.md`; `docs/development` has language audits and grammar inventory. README credits models (GPT-5/6, Claude 4/5, Gemini 3) for both the code and the showcase parts.
- GPU/CIR [V]: see Q5; the shared SDF "CIR" graph (`Aetheris.Continuum`, 8.5k lines) feeds both the field shader and the materializer strategies in Q1.
## Q7 Licence
- [V] `LICENSE` is the 661-line unmodified GNU AGPL v3 text; every csproj inherits `PackageLicenseExpression AGPL-3.0-only` from `Directory.Build.props`; the Telos package.json says AGPL-3.0-only.
- [V] README (section "License and acknowledgments"): "Aetheris code is licensed under the GNU Affero General Public License v3.0. Alternative licensing is available on request. Third-party assets retain their own terms and provenance; see THIRD_PARTY_NOTICES.md." The same line appears in docs/release/*README*.md and the preview-3 notes.
- [V] CONTRIBUTING.md ("Contributor license agreement status"): intended CLA lets contributors keep copyright and grants the owner "non-exclusive rights to distribute contributions under AGPL-3.0 and alternative licenses", with a commitment to continued AGPL availability; `docs/legal/CLA-CANDIDATE.md` is "candidate only; pending review by qualified human legal counsel", no acceptance mechanism is defined, maintainers "should not represent a contribution as having accepted that draft". Companion `CLA-COUNSEL-QUESTIONS.md`.
- [V] All 2002 commits are by the one author, so no third-party contributions exist yet to cover. NIST STEP models and the Stanford Bunny are noted as not under the Aetheris licence (README, THIRD_PARTY_NOTICES.md; earcut adapted from Mapbox, ISC).
- [V] Kernel.Firmament depends at build time on the Copeland repo, licensed GPL-3.0 (gh api), same author.

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>Aetheris.Kernel.Core.Tests</AssemblyName>
<RootNamespace>Harness</RootNamespace>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="/src/Aetheris.Kernel.Core/Aetheris.Kernel.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,161 @@
using System.Diagnostics;
using System.Text.Json;
using Aetheris.Kernel.Core.Brep;
using Aetheris.Kernel.Core.Brep.Boolean;
using Aetheris.Kernel.Core.Brep.Verification;
using Aetheris.Kernel.Core.Geometry;
using Aetheris.Kernel.Core.Geometry.Curves;
using Aetheris.Kernel.Core.Geometry.Surfaces;
using Aetheris.Kernel.Core.Math;
using Aetheris.Kernel.Core.Results;
using Aetheris.Kernel.Core.Step242;
using Aetheris.Kernel.Core.Topology;
using Aetheris.Kernel.Core.Diagnostics;
static class P
{
static (double c, double s) CS(double deg)
{
var d = ((deg % 360) + 360) % 360;
if (d == 0) return (1, 0);
if (d == 90) return (0, 1);
if (d == 180) return (-1, 0);
if (d == 270) return (0, -1);
var r = deg * Math.PI / 180;
return (Math.Cos(r), Math.Sin(r));
}
static Transform3D Make(double rxDeg, double ryDeg, double[] p)
{
var (cx, sx) = CS(rxDeg);
var (cy, sy) = CS(ryDeg);
var rx = Transform3D.FromRowMajor(new double[] { 1, 0, 0, 0, 0, cx, sx, 0, 0, -sx, cx, 0, 0, 0, 0, 1 });
var ry = Transform3D.FromRowMajor(new double[] { cy, 0, -sy, 0, 0, 1, 0, 0, sy, 0, cy, 0, 0, 0, 0, 1 });
var t = Transform3D.CreateTranslation(new Vector3D(p[0], p[1], p[2]));
return rx * ry * t;
}
static BrepBody Tr(BrepBody body, Transform3D t)
{
var g = new BrepGeometryStore();
foreach (var c in body.Geometry.Curves)
{
g.AddCurve(c.Key, c.Value.Kind switch
{
CurveGeometryKind.Line3 => CurveGeometry.FromLine(new Line3Curve(t.Apply(c.Value.Line3!.Value.Origin), t.Apply(c.Value.Line3.Value.Direction))),
CurveGeometryKind.Circle3 => CurveGeometry.FromCircle(new Circle3Curve(t.Apply(c.Value.Circle3!.Value.Center), t.Apply(c.Value.Circle3.Value.Normal), c.Value.Circle3.Value.Radius, t.Apply(c.Value.Circle3.Value.XAxis))),
_ => c.Value
});
}
foreach (var s in body.Geometry.Surfaces)
{
g.AddSurface(s.Key, s.Value.Kind switch
{
SurfaceGeometryKind.Plane => SurfaceGeometry.FromPlane(new PlaneSurface(t.Apply(s.Value.Plane!.Value.Origin), t.Apply(s.Value.Plane.Value.Normal), t.Apply(s.Value.Plane.Value.UAxis))),
SurfaceGeometryKind.Cylinder => SurfaceGeometry.FromCylinder(new CylinderSurface(t.Apply(s.Value.Cylinder!.Value.Origin), t.Apply(s.Value.Cylinder.Value.Axis), s.Value.Cylinder.Value.Radius, t.Apply(s.Value.Cylinder.Value.XAxis))),
SurfaceGeometryKind.Sphere => SurfaceGeometry.FromSphere(new SphereSurface(t.Apply(s.Value.Sphere!.Value.Center), t.Apply(s.Value.Sphere.Value.Axis), s.Value.Sphere.Value.Radius, t.Apply(s.Value.Sphere.Value.XAxis))),
SurfaceGeometryKind.Torus => SurfaceGeometry.FromTorus(new TorusSurface(t.Apply(s.Value.Torus!.Value.Center), t.Apply(s.Value.Torus.Value.Axis), s.Value.Torus.Value.MajorRadius, s.Value.Torus.Value.MinorRadius, t.Apply(s.Value.Torus.Value.XAxis))),
_ => s.Value
});
}
var vp = new Dictionary<VertexId, Point3D>();
foreach (var v in body.Topology.Vertices)
if (body.TryGetVertexPoint(v.Id, out var pt)) vp[v.Id] = t.Apply(pt);
return new BrepBody(body.Topology, g, body.Bindings, vp, body.SafeBooleanComposition);
}
static KernelResult<BrepBody> Prim(JsonElement e)
{
var k = e.GetProperty("k").GetString()!;
var d = e.GetProperty("d").EnumerateArray().Select(x => x.GetDouble()).ToArray();
var p = e.GetProperty("p").EnumerateArray().Select(x => x.GetDouble()).ToArray();
var rx = e.GetProperty("rx").GetDouble();
var ry = e.GetProperty("ry").GetDouble();
KernelResult<BrepBody> r;
if (k == "box") r = BrepPrimitives.CreateBox(d[0], d[1], d[2]);
else if (k == "cyl") r = BrepPrimitives.CreateCylinder(d[0], d[1]);
else if (k == "sph") r = BrepPrimitives.CreateSphere(d[0]);
else if (k == "tor") r = BrepPrimitives.CreateTorus(d[0], d[1]);
else throw new Exception("kind " + k);
if (!r.IsSuccess) return r;
if (rx == 0 && ry == 0 && p.All(x => x == 0)) return r;
return KernelResult<BrepBody>.Success(Tr(r.Value, Make(rx, ry, p)));
}
static string Short(IEnumerable<KernelDiagnostic> ds, int n = 3)
=> string.Join(" || ", ds.Take(n).Select(d => "[" + d.Severity + "/" + d.Code + "] " + d.Message)).Replace("\n", " ");
static int Main(string[] a)
{
var cases = JsonDocument.Parse(File.ReadAllText(a[0])).RootElement;
var outDir = a[1];
Directory.CreateDirectory(outDir);
var only = a.Length > 2 ? a[2] : null;
foreach (var c in cases.EnumerateArray())
{
var name = c.GetProperty("name").GetString()!;
if (only != null && !name.StartsWith(only)) continue;
var line = new Dictionary<string, object?> { ["name"] = name };
var sw = Stopwatch.StartNew();
var cc = c;
var task = Task.Run(() =>
{
var cur = Prim(cc.GetProperty("a"));
if (!cur.IsSuccess) throw new Exception("PRIM(a): " + Short(cur.Diagnostics));
var op = cc.GetProperty("op").GetString();
int i = 0;
int n = cc.GetProperty("bs").GetArrayLength();
foreach (var b in cc.GetProperty("bs").EnumerateArray())
{
var rb = Prim(b);
if (!rb.IsSuccess) throw new Exception("PRIM(b" + i + "): " + Short(rb.Diagnostics));
if (op == "union") cur = BrepBoolean.Union(cur.Value, rb.Value);
else if (op == "subtract") cur = BrepBoolean.Subtract(cur.Value, rb.Value);
else cur = BrepBoolean.Intersect(cur.Value, rb.Value);
i++;
if (!cur.IsSuccess) { line["failedAtStep"] = i + "/" + n; return cur; }
}
line["stepsDone"] = i + "/" + n;
return cur;
});
try
{
if (!task.Wait(TimeSpan.FromSeconds(180))) { line["status"] = "TIMEOUT"; }
else
{
var r = task.Result;
if (!r.IsSuccess) { line["status"] = "REFUSED"; line["msg"] = Short(r.Diagnostics); }
else
{
line["status"] = "OK";
line["warn"] = Short(r.Diagnostics.Where(d => d.Severity != KernelDiagnosticSeverity.Info), 2);
try
{
var m = BrepMassProperties.Evaluate(r.Value);
line["vol"] = m.Status + ":" + m.AbsoluteVolume.ToString("R") + ":" + m.EvaluationMethod;
}
catch (Exception ex) { line["vol"] = "EX " + ex.GetType().Name; }
try
{
var s = Step242Exporter.ExportBody(r.Value);
if (s.IsSuccess) { File.WriteAllText(Path.Combine(outDir, name + ".step"), s.Value); line["step"] = "exported"; }
else line["step"] = "EXPORT_REFUSED " + Short(s.Diagnostics);
}
catch (Exception ex) { line["step"] = "EXPORT_CRASH " + ex.GetType().Name + ": " + ex.Message; }
}
}
}
catch (AggregateException ex)
{
var m = ex.InnerException?.Message ?? ex.Message;
bool prim = m.StartsWith("PRIM");
line["status"] = prim ? "HARNESS_PRIM_FAIL" : "CRASH";
line["msg"] = (ex.InnerException?.GetType().Name + ": " + m).Replace("\n", " ");
if (!prim) line["stack"] = (ex.InnerException?.StackTrace ?? "").Split('\n').Take(3).Select(s => s.Trim()).ToArray();
}
line["ms"] = sw.ElapsedMilliseconds;
Console.WriteLine("RESULT " + JsonSerializer.Serialize(line));
}
return 0;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
import sys, re, collections
for f in sys.argv[1:]:
t = open(f).read()
ec = set(re.findall(r"#(\d+)=EDGE_CURVE", t))
uses = collections.Counter()
for m in re.finditer(r"ORIENTED_EDGE\([^,]*,\*,\*,#(\d+),", t):
uses[m.group(1)] += 1
hist = collections.Counter(uses.values())
print("EDGES", f.split("/")[-1], "edge_curves", len(ec), "oriented_edge_uses_per_edge_histogram", dict(hist), "edges_never_used", len(ec - set(uses)))

View file

@ -0,0 +1,29 @@
import sys, re, collections, math
from OCP.STEPControl import STEPControl_Reader
from OCP.TopAbs import TopAbs_SOLID, TopAbs_SHELL, TopAbs_FACE, TopAbs_COMPOUND
from OCP.TopExp import TopExp_Explorer
from OCP.BRepCheck import BRepCheck_Analyzer
from OCP.GProp import GProp_GProps
from OCP.BRepGProp import BRepGProp
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds
from OCP.TopoDS import TopoDS
def vol(s):
p = GProp_GProps(); BRepGProp.VolumeProperties_s(s, p); return round(p.Mass(), 6)
KEYS = ["MANIFOLD_SOLID_BREP","BREP_WITH_VOIDS","CLOSED_SHELL","OPEN_SHELL","ORIENTED_CLOSED_SHELL","SHELL_BASED_SURFACE_MODEL","ADVANCED_FACE","SPHERICAL_SURFACE","CYLINDRICAL_SURFACE","PLANE","FACE_BOUND","FACE_OUTER_BOUND"]
for f in sys.argv[1:]:
t = open(f).read()
ents = collections.Counter(re.findall(r"=\s*([A-Z_0-9]+)\s*\(", t))
print("INSPECT", f.split("/")[-1], {k: ents[k] for k in KEYS if ents[k]})
rd = STEPControl_Reader(); rd.ReadFile(f); rd.TransferRoots(); s = rd.OneShape()
print(" OCCT shape type", s.ShapeType(), "valid", BRepCheck_Analyzer(s).IsValid(), "vol", vol(s))
for kind, nm in ((TopAbs_SOLID, "solid"), (TopAbs_SHELL, "shell")):
ex = TopExp_Explorer(s, kind); i = 0
while ex.More():
sh = ex.Current()
try:
fb = ShapeAnalysis_FreeBounds(sh)
nopen = fb.GetOpenWires().NbChildren() if hasattr(fb.GetOpenWires(), "NbChildren") else "?"
except Exception as e:
nopen = "err"
print(" ", nm, i, "vol", vol(sh), "valid", BRepCheck_Analyzer(sh).IsValid(), "freeOpenWires", nopen)
i += 1; ex.Next()

View file

@ -1,95 +0,0 @@
# Boolean cleanup near the intersection curve: prior art
Serves ruling `brush-tree:ruling:boolean-cleanup-tighter-curve-wins` (campaign brush-tree, Operator, 2026-10-12): along a boolean cut into the hull's evaluated surface, the surface with the higher curvature owns the cut's silhouette, and the vertices the flatter surface contributes near the cut are merged into it. Prior art first, then a spike on the Dexter Quiet cage with a cutter.
Pass: Eyes, 2026-10-12, agent `eyes-bt-boolean-cleanup`. Facts with evidence pointers; "(inferred)" marks inference; "(recall, unverified)" marks things stated from memory that were not fetched this pass. No recommendation. Builds on `detail-on-subd-prior-art.md` @ c513992 (section 1 records that practitioners weld and clean ragged vertices after a boolean cut).
Tool caveat: the page fetcher summarises with a small model; Blender manual pages returned only navigation, so Blender operator behaviour below comes from search snippets, not full pages.
## Headline findings
1. No source found that names or specifies "the tighter-curved surface defines the cut, merge the flatter surface's vertices onto it" as an algorithm. Nearest relatives are in section 2.
2. Every mainstream tool found treats post-boolean cleanup as a distance or length threshold operation (merge-by-distance, collapse edges shorter than epsilon, aspect-ratio sliver removal), not a curvature-ownership rule. The one curvature-driven item found (Hu et al., SPIE 2022) sets target edge length from curvature before the boolean, not merge direction after it.
3. Manifold runs a post-boolean degenerate-edge collapse internally with a triangle-flip guard; CGAL returns the intersection curve as a constrained-edge map that its isotropic remesher can protect; Houdini outputs seam edge groups and PolyReduce has hard/soft edges. These are the "intersection loop as protected feature" mechanisms found.
## 1. Research and tools that clean booleans near the intersection
### Libraries and kernels
- **Manifold** (Apache-2.0, C++). After assembling a boolean result, `boolean_result.cpp` calls `outR.RemoveDegenerates(nPv + nQv)` then `RemoveUnreferencedVerts()` (https://github.com/elalish/manifold/blob/master/src/boolean_result.cpp). In `edge_op.cpp` (https://raw.githubusercontent.com/elalish/manifold/master/src/edge_op.cpp; fetcher summary, function names as reported):
- `RemoveDegenerates`: `CleanupTopology`, collapse edges shorter than `epsilon_`, `SwapDegenerateEdges`, collapse colinear edges, recompute vertex normals.
- `CheckEdge`/`CollapseEdge`: short edges collapse to the midpoint; longer edges get a fitted position in the plane of the edge and averaged normal, clamped to [-0.5, 0.5] along the edge, with cost in squared-length units. A collapse is rejected if a surrounding triangle would flip (cross product against face normal); a Delaunay-style worst-angle threshold (0.5 or 0.99) also gates it.
- `Colinear`: an edge may collapse only if each rotated neighbour stays within epsilon of the face plane and does not invert.
- `SwapDegenerateEdges`: swaps the longest edge of a degenerate triangle only if that reduces degenerates; per-edge cap `kMaxSwapsPerEdge = 255`.
- `Decimate(tolerance)`: collapse/swap in cost-sorted rounds, `maxCost = tolerance^2`, tolerance floored at epsilon.
- Epsilon is a global length tolerance, not curvature based; its value is set outside these files. The README (https://github.com/elalish/manifold) does not mention epsilon or short-edge collapse; it states reliability and guaranteed-manifold output as the goal. Whether `Decimate` is on the public API was not confirmed.
- **CGAL Polygon Mesh Processing** (GPL v3+, some core parts LGPL; commercial licences from GeometryFactory). Corefinement booleans `corefine_and_compute_union/intersection/difference`; with a constrained-edge map supplied, edges on the intersection of the inputs are marked constrained, and sub-edges stay marked when split (search result quoting https://doc.cgal.org/latest/PMP_Boolean_operations/group__PMP__boolop__grp.html). Exact predicates needed for correct topology; exact constructions recommended when chaining. As of CGAL 6.2 remeshing lives in "Meshing and Remeshing of Polygon Meshes"; `isotropic_remeshing` with `protect_constraints(true)` and `face_patch_map` limits remeshing to a region and keeps constraint edges (https://doc.cgal.org/latest/PMP_Remeshing/PMP_Remeshing_2isotropic_remeshing_of_patch_example_8cpp-example.html). Curvature-adaptive sizing in that remesher: not confirmed in fetched pages (recall, unverified: a sizing-field parameter exists in recent versions).
- **libigl**: README says "MPL-2.0-licensed", repo also carries LICENSE.GPL (https://github.com/libigl/libigl). `igl::copyleft::cgal::mesh_boolean` implements Mesh Arrangements for Solid Geometry (Zhou, Grinspun, Zorin, Jacobson, SIGGRAPH 2016; https://alecjacobson.com/weblog/4573.html) under a `copyleft/cgal` path; an old mirror header shows an MPL-2.0 notice and a CGAL dependency, so the effective licence is bounded by CGAL's (inferred). Related functions named there: `mesh_boolean_cork`, `intersect_other`, `remesh_self_intersections`. C++; no Rust bindings found.
- **Cherchi et al., Fast and Robust Mesh Arrangements using Floating-point Arithmetic** (TOG 2020): https://github.com/gcherchi/FastAndRobustMeshArrangements, MIT, header-only (C++ presumed), four `solveIntersections` variants; no remeshing or cleanup mentioned on the page.
- **Cherchi, Pellacini, Attene, Livesu, Interactive and Robust Mesh Booleans** (2022; https://ar5iv.arxiv.org/html/2205.14151): output triangles are portions of input triangles; the only cleanup stated is per-frame "degeneracy removal and rebuilding of adjacencies"; no simplification or coplanar merging described; C++ with CinoLib, Indirect Predicates, Abseil, TBB; code "will be released", no repo given in the text read.
- **Exact predicates, exact constructions and combinatorics for mesh CSG** (arXiv 2405.12949; fetch failed on size, search snippet only): says results keep "scars" of intersection from co-refinement that the final mesh does not need; finds coplanar regions by traversal and simplifies them; notes poorly triangulated planar input areas are worth remeshing.
- **Adaptive Mesh Booleans**, Schmidt and Brochu, Autodesk Research (https://arxiv.org/abs/1605.01760; read via https://ar5iv.arxiv.org/html/1605.01760). Steps: refine locally around the intersection (split, collapse, flip, smooth), delete intersecting triangles, zipper the two boundary loops by iterative closest-point evolution, reproject loop vertices onto the input surfaces with sharp-edge constraints (3.5, 3.6), simplify the intersection region as post-process (3.7). Density: constant max edge length `l_max`, min `0.4*l_max`; the paper "does not adapt density to surface curvature", adapting to local feature size is future work (3.4). Failure modes listed: loop-count mismatch giving holes or floating patches; 5-iteration refinement cap; slow convergence near coplanar; loops of very different shape give self-intersections or non-manifold seams; sharp-edge constraints can leave triangular corner holes (3.6.1). Shipped in Meshmixer (since 2011) without the sharp-edge enhancements; no source release stated. Closest published "both sides conform to one loop" method found.
- **Hu et al., adaptive remeshing from local information for mesh booleans** (SPIE vol. 12331, 123314S; title and full text not obtained, search summary only, https://spiedigitallibrary.org/conference-proceedings-of-spie/12331/123314S/An-adaptive-remeshing-method-based-on-local-information-for-mesh/10.1117/12.2652878.full): edge length from local edge length and curvature; reported average edge length 55% shorter in high-curvature areas, 78% longer in low-curvature areas versus uniform remeshing. Verify before citing.
### Applications and add-ons
- **Houdini Boolean SOP** (https://www.sidefx.com/docs/houdini/nodes/sop/boolean.html): "Collapse tiny seam-adjacent edges" fuses microscopic edges from misaligned inputs with an Edge length threshold; Detriangulate (All / Only Unchanged / None) merges triangles back into original polygons; "Assume seam polygons are flat" drops the triangulation-crease point; "Unique Points Along Seams"; Output Edge Groups for A-A, B-B, A-B seams. Arbitrary precision internally, snapped to float on output, which can produce microscopic self-intersections when chained.
- **Houdini PolyReduce SOP** (https://www.sidefx.com/docs/houdini/nodes/sop/polyreduce.html): hard points/edges never move, soft ones carry a weight, Boundaries weight, vertex-attribute seam weight, "Only use original point positions", normal-deviation limit, density retention by attribute or by view silhouette, Equalize lengths. Fetcher found no mention of quadric error on the page.
- **Blender** (search snippets of https://docs.blender.org/manual/en/dev/modeling/meshes/editing/mesh/cleanup.html; bodies not read): Merge by Distance (can merge selected into unselected; newer manual preserves sharp edges when custom split normals exist), Degenerate Dissolve (collapses edges shorter than a length), Limited Dissolve (only linked in snippets), Weld modifier = non-destructive Merge by Distance, Decimate modifier Collapse mode = non-destructive Degenerate Dissolve. Boolean modifier Exact solver: overlap and self-intersection options, only manifold inputs guaranteed (https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/booleans.html, via search). Nothing found saying Exact minimises vertices along the cut.
- **Hard Ops / Boxcutter / Bool Tool / Fluent**: Hard Ops manual boolean page (https://hardops-manual.readthedocs.io/en/latest/boolean/) names no dissolve or weld options in the text fetched. The Blender Bros bundle page lists a "Boolean Cleanup" feature without detail (search snippet). A Blender Nation Bool Tool tutorial summary cleans up with the native Weld modifier, not a Bool Tool option. Add-on source not inspected (`F:\Projects\blender-tooling-surface-prior-art.md` line 71 says the same).
- **Hardsurface Doctor** (https://superhivemarket.com/products/hardsurface-doctor/docs, snippet): "Safe Repair" merges doubles, removes zero-area and loose geometry, dissolves multi-face non-manifold edges. **Boolmend** (https://blenderartists.org/t/boolmend-one-click-post-boolean-cleanup-topology-repair-shading-fix-15-early-access/1650088): early-access add-on claiming one-click post-boolean cleanup and sliver-edge removal; vendor claims, unverified.
- **Meshmixer**: Boolean adds triangles around the intersection, deletes strips to form clean boundary loops on each side, applies Reduce as post-process; on failure remeshes denser and retries (search snippets of https://help.autodesk.com/cloudhelp/2019/ENU/MSHMXR/files/GUID-355503DE-889B-4168-8398-084F9BBD508B.htm).
- **Altair HyperMesh** `fix 2d mesh` (https://help.altair.com/hwdesktop/hwd/topics/reference/hm/_fix_2d_mesh.htm, snippet): sliver removal by minimum aspect ratio, intersection removal that avoids sub-threshold triangles, bitmask to disable edge swaps, collapses, node moves.
- **COMSOL, Intersect with Line** (https://doc.comsol.com/6.3/doc/com.comsol.help.comsol/comsol_ref_mesh.24.70.html, snippet): vertices on intersected edges are snapped onto the intersection line, bounded by a repair tolerance. Closest found to "move one side's vertices onto the other's curve", but FE meshing and tolerance-driven.
- **ZBrush**: DynaMesh with Groups, Polish by Features after remesh (Novedge tip, snippet). Not an ownership rule.
### Simplification with locked features
- **meshoptimizer** (MIT, C++; Rust `meshopt` crate; https://github.com/zeux/meshoptimizer): `meshopt_simplifyWithAttributes`, per-vertex `vertex_lock` array (Lock, Protect, Priority), `meshopt_SimplifyLockBorder`, absolute or relative error bound. Quadric basis not stated in the fetched text (recall, unverified: it is quadric-based).
- Garland-Heckbert QEM with boundary constraint planes as the standard way to protect a loop in constrained collapse (recall, unverified; not fetched).
## 2. "Tighter-curved surface owns the cut; flatter surface's vertices merge onto it"
### Name or precedent
- No source found with this rule under any name. Searches for it returned Adaptive Mesh Booleans (symmetric: both sides conform to one loop, no side ownership), COMSOL snapping (tolerance based), Hu et al. (pre-boolean curvature sizing).
- CAD trimmed-surface tessellation: the common boundary edge is discretised once and both faces reuse those vertices (patent text US 9,836,884 / 9,875,577, mental ray "Edge Merging" doc, a BUAA paper on trimmed-surface meshing using distance and angle tolerances; all search snippets, not read in full). Shared-edge-first has precedent, but sampling follows the edge's own chord and angle tolerance, not which adjoining surface is more curved.
- Meshmixer deletes strips from each object and leaves loops on both sides, then zips: the loop is a joint product (inferred).
- Practitioner side (`detail-on-subd-prior-art.md` section 1 @ c513992): booleans leave extra vertices and ragged tris/n-gons along the cut, which practitioners weld and clean by hand. The operator's rule describes what that manual weld converges to; no tool found automates it as stated.
### What goes wrong: sourced
- Triangle flips on collapse: Manifold `CollapseEdge` rejects flips and gates on worst angle (edge_op.cpp summary).
- Collapse position bounded to [-0.5, 0.5] along the edge, epsilon-scale moves (same).
- Loop mismatch when the two sides differ in feature size or shape: Adaptive Mesh Booleans 3.2, 3.4.
- Sharp-edge constraints leaving corner holes: same paper, 3.6.1.
- Chained booleans accumulate microscopic self-intersections after float snapping: Houdini Boolean page.
- Merging across sharp edges loses them unless guarded: Blender Merge by Distance now marks edges to keep sharp edges when custom split normals exist (snippet).
- Density near the cut scales with subdivision level, so the flatter surface's contribution count depends on level: `detail-on-subd-prior-art.md` section 1 (inferred there).
### What goes wrong: inferred, no source either way
(inferred) Cases the rule leaves undefined and no source addresses: both surfaces comparably curved (ownership ambiguous); saddle intersections where one surface has opposite-sign principal curvatures, so "tighter" depends on direction along versus across the curve; the cut crossing a crease or hard edge of either surface (local curvature unbounded; the stack's one-segment Bevel and Weighted Normal introduce creases); ownership switching along one loop and what happens at the switch vertex; a flatter-surface vertex moved onto the tighter surface's curve changing the flat surface's silhouette or flipping its triangles when the displacement is large relative to local edge length; curvature measured on a tessellated polyline being noisy at sliver density; moved vertices leaving the cage's Subsurf limit surface because the cut is on the evaluated mesh.
## 3. Implementations we could own
| Item | Licence | Language | Rust | Does |
|---|---|---|---|---|
| Manifold (elalish) | Apache-2.0 | C++ | `manifold-csg` safe bindings over the C API (docs.rs lists Apache-2.0 OR MIT for the crate; `manifold3d` re-exports it, v0.3.3 seen; f64 via MeshGL64). README also lists a native Rust port `manifold-rust` with no C++ dependency (not verified further). Other bindings: C, JS/WASM, Python, C# (`ManifoldNET`, external) | boolean with epsilon degenerate cleanup; decimate (public status unconfirmed) |
| boolmesh | MPL-2.0 | pure Rust, `glam`, optional `rayon`/`bevy` | native | from-scratch boolean "inspired by Manifold"; v0.2.1 released 2026-10-11 (0.2.0 yanked); input must be manifold; per-corner attribute carry; docs.rs doc coverage 0%; whether it ports `RemoveDegenerates` not stated (https://docs.rs/crate/boolmesh/latest) |
| meshoptimizer | MIT | C++ | `meshopt` crate | simplify with vertex locks and attribute weights |
| CGAL PMP | GPL v3+ (LGPL parts), commercial available | C++ | none found | corefinement with constrained-edge output; isotropic remeshing with protected constraints |
| libigl | MPL-2.0 core, GPL file present; copyleft/cgal boolean tied to CGAL | C++ | none found | Mesh Arrangements boolean |
| FastAndRobustMeshArrangements | MIT | C++ header-only | none found | arrangement via indirect predicates, no cleanup |
| Interactive and Robust Mesh Booleans | unstated | C++ | none found | per-frame degeneracy removal only |
| csgrs | not checked | Rust | native | appeared in search results; not inspected |
Local: `F:\Projects\GameCult.Geometry.Csg` checkout is on branch main with `docs/research` holding brush-surfaces-and-sdf-bricks and octahedral-impostors notes; the eureka/brush-tree branch and the boolean kernel the crate uses were not inspected this pass.
## Not resolved in budget
- Blender Limited Dissolve semantics; Hard Ops / Boxcutter cleanup option names.
- Whether Manifold's `Decimate`/`Simplify` is on the public C API and the Rust crate.
- Full text of arXiv 2405.12949 and of Hu et al.
- CGAL adaptive sizing field and curvature-adaptive isotropic remeshing parameters.
- Garland-Heckbert boundary-constraint details and meshoptimizer's quadric basis (recall only).

View file

@ -1,43 +0,0 @@
# Cage connectivity prior art
Serves the operator fork: how the connectivity of a coarse Catmull-Clark control cage is generated automatically for brush-tree (invariants cage-is-fitted, cage-bar, creases-declared/closed in `docs/brush-tree-target.md`). Prior-art pass per Eureka SKILL step 2. Facts with evidence pointers; "(inferred)" marks the only non-sourced remarks. Written 2026-10-12 by eyes-bt-cage-connectivity.
Read depth is stated per row. Full texts read: Hoppe 1994, Marinov & Kobbelt 2004 conference version, Lavoue & Dupont 2009, Ma et al. 2015 (author manuscript), Campen et al. 2012 (preprint), Viertel/Osting/Staten 2019 (grep-level on method and limitations). Abstract or repo README only: QuadWild paper (PDF hosts refused or blocked), Lavoue 2007 CGF, Fitmaps 2011, Ma 2004, Instant Meshes paper, Kloiber 2020 (not found).
## Headline facts
1. Every method here that hits a coarse cage obtains connectivity by one of four routes: (a) decimate a dense mesh (QEM or Hoppe edge collapse/swap/split) then optimise positions; (b) cluster/segment the target into regions and link boundary control points (Lavoue); (c) build a coarse quad layout from a cross field (Dual Loops, QuadWild, Viertel); (d) connect feature points (ridges/umbilics) by distance transform (Ma 2015). No source generates connectivity from the fitted limit surface alone; all start from a dense target mesh or point cloud.
2. Only route (a) with Hoppe's energy and route (b) with Lavoue declare creases as part of the output. Hoppe and Ma use Loop (triangles); Lavoue uses the Stam-Loop hybrid quad/triangle scheme; Fitmaps and the layout methods yield quads. None of the read papers targets Catmull-Clark creases on a quad-only cage at 200 faces.
3. The coarsest figures reported: Lavoue 2009 control meshes of 119 to 187 vertices (200 to 260 faces) for 5K to 15K-vertex scans; Ma 2015 239 to 383 vertices; Hoppe 1994 down to 87 vertices (a swept/NURBS source); Dual Loops 20 to 275 patches (layouts, not cages).
4. No read source treats thin wing tips or saddles as a named case. Failure modes recorded below are the ones the authors state.
## Table
| Method | Produces | How coarse | Creases | Open implementation, licence | Failure modes (as stated by authors) | Evidence |
|---|---|---|---|---|---|---|
| Hoppe, DeRose et al. 1994, Piecewise smooth surface reconstruction (SIGGRAPH) | Tagged Loop control mesh (triangles) by 3 phases: surface from points, mesh optimisation, then phase 3 optimising the subdivision control mesh | Table 3: 87 to 656 vertices from 4K to 31K points; Table 2 teapot/knot cases 207 and 363 vertices. Controlled by `crep` penalty on vertex count | Yes, first-class. Energy `E = Edist + crep*m + csharp*e`; edge-tag move toggles an edge in sharp set L; creases, corners, darts classified by sharp-edge count (Sec 3, 4.1). `csharp = crep/5` worked on all examples | Original code not found. Algorithm is public in paper (Sec 4.2.2: random descent over edge collapse, swap, split, tag, with local approximate E). Hoppe 1993 mesh optimisation is the phase-2 ancestor | Local random descent only reaches a local minimum; paper omits the spring energy in phase 3 as unnecessary; times 0.9 to 7.7 hours on 1994 hardware (Table 3); Loop only, not Catmull-Clark; topology fixed (K same type as phase-2 mesh) | https://grail.cs.washington.edu/wp-content/uploads/2015/08/hoppe-1994-pss.pdf Sec 1 phases, 3.1, 4.1, 4.2.2, Tables 2-3 |
| Marinov & Kobbelt 2004/2005, Optimization techniques for approximation with subdivision surfaces (SM'04; Graphical Models 67(5)) | Loop control mesh. Initial connectivity = QEM decimation of the dense mesh to a fixed vertex count (Sec 4.1); then L2 fitting with closest-point parameter correction and periodic L-inf phase that inserts vertices where error exceeds tolerance, removes under-determined vertices by half-edge collapse, and flips edges to regularise valence (R(M) metric, Sec 3.1 to 3.3) | Initial 1% to 5% of dense vertex count; finals 4.5K to 18K vertices at 0.03% to 0.05% tolerance (Table 1); coarsest 612 vertices at 0.63% error (Fig 7b) | No. "Full support of piecewise-smooth subdivision surfaces [ZK02] ... pending" (Sec 6) | Authors' implementation not found | Local flipping of the parameter mapping when the initial control mesh is "extremely coarse"; remedy given is more degrees of freedom from the start (Sec 6). Initial mesh must share the dense mesh's topology; valence regularisation is a greedy local minimum | https://www.graphics.rwth-aachen.de/media/papers/otass1.pdf Secs 3, 4.1, 5, 6, Table 1 |
| Lavoue & Dupont 2009, Semi-sharp subdivision surface fitting based on feature lines approximation (Computers & Graphics 33); precursor Lavoue, Dupont, Baskurt 2007 CGF 26(1) | Mixed quad/triangle control mesh (Stam-Loop scheme). Pipeline: VSA segmentation + region merging by anisotropy (Sec 4.1, Eq 1), smoothed region-boundary network = feature lines (4.2), subdivision-curve approximation of each boundary gives the only control points (5.1), connectivity inside each region by edge scoring against minimum-curvature direction (5.2, Fig 8), local meshes glued, boundary edges tagged sharp (5.3); then integer semi-sharpness per edge (0 to 3) chosen by curvature-radius match (Sec 6.1) and geometry optimisation, 3 iterations (6.2) | Table 1: 119 to 187 control vertices, 200 to 260 faces for 2.5K to 15K-vertex models; abstract states "less than 200 vertices"; authors say objective is "coarsest possible" | Yes, integer semi-sharp creases per edge, closed boundary loops from segmentation. Weakness stated: boundary edges between patches are sharp, "can produce unpleasant discontinuities for smooth objects" (5.3) | Author code not found; method fully described; Stam-Loop scheme is published. Related: same group 2005 3DIM and 2007 CGF PDFs at https://perso.liris.cnrs.fr/fdupont/publications/ | Does not aim at quads or regular valence: "can possibly have high degree facets or high valence vertices" (Sec 5.3 end). Maximal errors larger than mean "particularly tiny details" because coarse mesh lacks degrees of freedom (Sec 7); fix suggested is local enrichment or finer segmentation. Threshold for region merging "hard to find a priori" (4.1) | https://perso.liris.cnrs.fr/fdupont/publications/2009-CAG-Lavoue-Dupont.pdf Secs 3, 4.1, 5.1 to 5.3, 6, 7, Tables 1-2; 2007 abstract https://diglib.eg.org/handle/10.2312/CGF.v26i1pp001-014 (abstract only) |
| Ma, Keates, Jiang, Kosinka 2015, Subdivision surface fitting to a dense mesh using ridges and umbilics (CAGD 32) | Loop control mesh whose connectivity joins umbilics and ridge-crossings (feature points) by grass-fire distance transform on the dense mesh with a Hausdorff+curvature-vector metric (Secs 3 to 4), then ridge-colour-matching edge flips; control points by solving limit-point system | Rocker arm 239 vertices (from 10,044); bunny 381 (from 35,947); Igea 383 (from 8,268); a 254-vertex car-type case (Sec 5) | No dedicated crease mechanism: "To fit extreme explicit sharp features, some special subdivision rules would need to be used" (Sec 5, cites Lavoue 2009). Feature filtering needed on planes/cylinders where every point is a ridge point (3.4) | Code not found; manuscript CC BY-NC-ND https://gala.gre.ac.uk/id/eprint/14421/ | Accuracy depends on position and number of filtered umbilics and ridges; "some non-regular vertices" remain, post-process for valence suggested (conclusion). Related-work section (pp. 1 to 2) lists simplification/parametrisation/clustering/feature-based families and says typical features "may not align to salient edges" | gala.gre.ac.uk manuscript PDF, Secs 1, 3.3 to 3.4, 5, 6 |
| Panozzo, Puppo, Tarini, Pietroni, Cignoni 2011, Automatic construction of quad-based subdivision surfaces using fitmaps (IEEE TVCG) | Catmull-Clark control mesh, "coarse and adaptive", by progressive simplification of a fine quad mesh with local operators guided by fitmaps | Not read (abstract only) | Not stated in abstract | Code not located; Tarini et al. 2010 quad simplification paper says implementation "will be available in MeshLab" (future tense; shipping not verified) | Not read | http://vcg.isti.cnr.it/publication/2011/PPTPC11/ (abstract); https://vcg.isti.cnr.it/Publications/2010/TPCPP10 |
| Ma et al. 2004 (CAD 36(6) 525-536), Ma & Zhao 2002 | Subdivision fitting by simplification that tags vertex/edge types to keep sharp features (per Ma 2015 related work); Ma & Zhao 2002 uses an interactively defined topological model with Catmull-Clark and linear least squares | Not read | Sharp features preserved during simplification (per secondary description only) | Not located | Not read; Ma 2015 says interactive topology definition suits "simple topology" | Secondary: Ma 2015 manuscript p. 2; citation https://dblp.dagstuhl.de/rec/conf/gmp/MaMTP02.html |
| Campen, Bommes, Kobbelt 2012, Dual Loops Meshing (SIGGRAPH / TOG 31(4)) | All-quad coarse layout (base complex) built in the dual: curvature-guided crossing loops chosen greedily to separate cross-field singularities, region valence tied to total curvature (Sec 4, Thm 4.1/4.2), dualised to patches (5.3); cross-field singularity index decides irregular node valence | Table in Sec 7: 20 to 275 patches (e.g. FERTILITY 20 to 76, ROCKERARM, LEVER 83 singularities); "a few seconds to a few minutes" on 20K to 122K faces | Feature curves enter as directional constraints in the cross field and as lifted curves loops may not cross, so arcs align to them (Sec 6). "Complexity of the layout is thus predetermined to a degree" by the feature network. Only dihedral-angle sharp edges tested (JOINT, LEVER) | Original code not found in this pass. Related open code: NSchertler/GeneralizedMotorcycleGraph (Schertler et al. 2018; CMake, optional Gurobi; licence not read) | Greedy loop choice, not global optimum; geometric fidelity only indirectly controlled; "narrow patches" break the initial embedding and need a domain-interpolation workaround (Sec 5.3); irregular nodes may have very high or low valence (Sec 4); manual layouts still simpler because professionals place tactical irregular nodes (Sec 7.1) | https://graphics.rwth-aachen.de/publication/3/campen_2012_sg_preprint1.pdf Secs 2 to 7.1 |
| Viertel, Osting, Staten 2019, Coarse quad layouts through robust simplification of cross field separatrix partitions (arXiv 1905.09097) | Coarse quad layout: trace cross field separatrices, then collapse thinnest chords while keeping T-junction constraints | Tested on 100 objects; coarser than naive tracing (counts not extracted) | Not extracted | Code not located | States that naive tracing yields "small regions and limit cycles" and thin regions near singularities; chord collapse is the remedy; collapsing is limited by conditions that guarantee valid layouts | https://arxiv.org/pdf/1905.09097 abstract and Secs 1, chord-collapse section |
| QuadWild, Pietroni, Nuvoli, Alderighi, Cignoni, Tarini 2021, Reliable feature-line driven quad-remeshing (SIGGRAPH / TOG) | Quad mesh from a cross field aligned to feature lines, via a coarse patch layout in which every feature line is a patch boundary; non-quad patches and T-junctions allowed in the layout so long as each patch admits a pure-quad tessellation (abstract) | Output size is a scale parameter (`scaleFact`), not a face target; repo exposes `alpha` (regularity vs isometry; near 1 inserts more singularities) and `sharp_feature_thr` dihedral threshold or a `.sharp` file. Layout patches can be coarse but the output is a dense-ish quad mesh | Sharp features are patch boundaries; border edges treated as sharp by default | https://github.com/nicopietroni/quadwild: GPL-3.0, C++/Qt, needs Boost and Gurobi; 245 commits, 22 open issues, README says results may differ from paper. Fork https://github.com/cgg-bern/quadwild-bimdf: GPL-3.0, CMake, Bi-MDF solver, Gurobi optional, 308 commits | Authors report under 0.5% failure on Thingi10K. A later paper (NeurCross, arXiv 2405.13745) says patching creates singularities at patch junctions and malformed quads on free-form models. Third-party note: face count approximate. Paper PDF not read | abstract via https://vcg.isti.cnr.it/publication/2021/PNACT21; READMEs as listed |
| Instant Meshes, Jakob, Tarini, Panozzo, Sorkine-Hornung 2015 | Isotropic tri or quad-dominant remesh from an orientation/position field at a target vertex count | Target vertex count; used in earlier brush-tree runs (mesh-probe runs 3 to 7) | Snaps to sharp features per abstract; crease options not verified from source | https://github.com/wjakob/instant-meshes; licence BSD-3-Clause per FreeBSD port listing (packager, unverified against upstream LICENSE) | Paper limitations section not read; local runs in this repo are the evidence | https://rgl.epfl.ch/publications/Jakob2015Instant |
| Tarini et al. 2010, Practical quad mesh simplification (Eurographics CGF 29(2)) and Daniels et al. 2008 quad simplification | Quad-preserving local simplification operators | Not read | Not read | MeshLab implementation promised in text; unverified | Viertel 2019 cites that collapse-the-chords style does not generalise to closed surfaces without extra machinery | https://vcg.isti.cnr.it/Publications/2010/TPCPP10 (abstract) |
| Kloiber et al. 2020 | Not found by search under that title or authors | n/a | n/a | n/a | n/a | WebSearch returned only fitmaps, Peters 2000, tree-structure papers |
## Notes by question
Creases and closed loops. Lavoue's boundary network is closed by construction (each region is bounded by a closed cycle of control polygons, Sec 5.2) and tags every inter-region edge sharp, with semi-sharp relaxation afterward. Hoppe leaves crease placement to a discrete edge-tag search with a per-edge penalty. Dual Loops and QuadWild take feature curves as input and force the layout to follow them; neither chooses which curves are creases.
Irregular vertex count. Marinov regularises valence by greedy edge flips on R(M) (Sec 3.3) and says the result is a local minimum. Lavoue explicitly accepts high valence and n-gons. Dual Loops ties irregular nodes to cross-field singularities and total curvature and reports node counts per model (Sec 7 table). Ma 2015 leaves non-regular vertices to a possible post-process.
Thin wings and saddles. No source above names them. Stated adjacent hazards: narrow patches (Campen Sec 5.3), thin regions and limit cycles near singularities (Viertel abstract and Sec 1), coarse-mesh lack of degrees of freedom for "tiny details" and larger maximal error (Lavoue Sec 7), and parameter-mapping flips at extremely coarse starting meshes (Marinov Sec 6). Ma 2015 notes planes and cylinders make every point a ridge point.
Bar match (inferred). Against the cage-bar (at most about 200 faces, at most 24% triangles, at most 62 irregular vertices): only Lavoue's published outputs are at that count (200 to 260 triangle-or-mixed faces, vertex counts 119 to 187), and his method states it does not optimise for quads or valence. No read source reports an irregular-vertex count for its control mesh; Dual Loops reports singularities per layout (16 to 83), which counts the irregular nodes of the layout.
## Not verified in this pass
QuadWild paper internals (patch count, limitations); Instant Meshes licence and limitations; Fitmaps method detail and creases; Ma 2004 and Lavoue 2007 internals; licence of GeneralizedMotorcycleGraph; Kloiber 2020 existence; whether any source reports behaviour on thin wing-like features. PDFs from air.unimi.it, vcg-legacy.isti.cnr.it, dl.acm.org and diglib.eg.org returned 403 or refused connection from this host.

View file

@ -1,63 +0,0 @@
# Detail on a subdivision hull: prior art
Fork: brush-tree, detail layer. How does hard-surface detail (canopy, vents, intakes, gun ports, panel insets) get onto a smooth subdivision hull without moving the primary-form cage? Options floated: boolean into the evaluated mesh; inset/support loops in the cage; a separate interpenetrating (floating) piece.
Provenance: Eyes pass, 2026-10-12, agent `eyes-bt-detail-layer`. Facts carry sources; "(inferred)" marks inference; "(unsourced)" marks recall not checked against a page. Not read: `docs/brush-tree-target.md` at a7879dad (the commit did not resolve in the CultLib object store, so invariant wording is from the brief only).
Operator workflow (verbatim, 2026-10-12): "expressing the form elegantly with as few control points as possible, then I go and start cutting into it for all the hard surface details and bits that interrupt the idealized form like those gun ports". The detail layer in her practice is subtractive cuts into the subdivided form.
## 1. Cut into the evaluated mesh (boolean after subdivision)
- Blender modifiers run top to bottom, each taking the output above; order decides which operator sees which geometry. Boolean below Bevel means bevel acts on the clean base first; boolean above Bevel makes bevel work on boolean-generated geometry and can narrow it ([stack order](https://steveyang.readthedocs.io/Modeling/Non-destructive/ND-Beginning/Modifier_Stack_Order/), [cgcookie thread](https://cgcookie.com/community/9780-change-to-modifier-stack-changes-bevel-width)). Subdivision Surface docs: stack position "significantly affects the result" ([Blender 4.2 manual](https://docs.blender.org/manual/en/4.2/modeling/modifiers/generate/subdivision_surface.html)).
- Blender Boolean modifier: Exact solver handles overlap and self-intersection, slow; Fast does not handle overlapping geometry; only manifold inputs are guaranteed correct ([manual](https://docs.blender.org/manual/en/4.2/modeling/modifiers/generate/booleans.html)).
- Regenerable: yes. Cutter list plus cage are the inputs and the stack re-runs. Cage independence: full, cutters never touch cage vertices. Triangles: dense, because the cut runs on the subdivided mesh, scaling with subdivision level (inferred). Practitioners report booleans leave extra vertices and ragged n-gons/triangles along the cut, which they weld and clean ([Blender Artists](https://blenderartists.org/t/how-to-create-this-kind-of-side-cut-in-hard-surface-modeling-with-good-topology/1482837), [Blender Artists](https://blenderartists.org/t/questions-regarding-hard-surface-modeling/1207286)).
- Shading: the cut wall meets the hull at a hard crease; the practitioner fix is a bevel/chamfer on the cut edges ([same thread](https://blenderartists.org/t/how-to-create-this-kind-of-side-cut-in-hard-surface-modeling-with-good-topology/1482837)). Weighted Normal rewrites custom normals by corner angle/face area; Keep Sharp preserves sharp edges; Face Influence uses per-face strength set by Set Strength or the Bevel modifier's Set Strength mode, so bevel faces drive the result ([manual 4.2](https://docs.blender.org/manual/fi/4.2/modeling/modifiers/normals/weighted_normal.html)). One add-on doc says at least a one-step bevel matters for good weighted-normal shading ([page](https://superhivemarket.com/products/shadingmod)). That matches the one-segment-bevel plus weighted-normals shippable stack.
- Tooling precedent: Hard Ops cSharpen applies booleans by default (to keep cutting) but keeps bevel modifiers live and never applies bevels; default 3 segments, 0.7 profile, weight-limited; WN and Harden Normals are optional toggles ([manual](https://hardops-manual.readthedocs.io/en/latest/csharpen/)). Another add-on says it re-establishes non-destructive Bevel and Weighted Normal finishing layers after apply ([Arcane Rifter](https://superhivemarket.com/products/arcane-rifter)). These tools bake cuts into the mesh, so they are not regenerable; only a stack that never applies is.
- No source found describing "boolean after Subsurf" as a deliberate standard modifier order. The sourced practice cuts the cage first (section 2); the usual finishing order Boolean, Bevel, Weighted Normal, Subsurf last is unsourced recall.
## 2. Cut the cage with support loops (subd modelling)
- Practitioners: after a boolean cut and subdivision "everything falls apart"; the fix is supporting loops around the cut, clean and weld verts, then chamfer/bevel the cut edges so it stays sharp after smoothing ([Blender Artists](https://blenderartists.org/t/how-to-create-this-kind-of-side-cut-in-hard-surface-modeling-with-good-topology/1482837)). Artifacts come from too few segments for the curvature; abrupt loop routing causes smoothing artifacts; carrying loops across the surface smooths more consistently ([Blender Artists](https://blenderartists.org/t/questions-regarding-hard-surface-modeling/1207286)).
- Game-art counterpoint: some artists use bevel-first low poly instead of subd because a subdivided hull bakes less cleanly (bevel widens, wavy artifact, extra supports did not fix it) ([CG Cookie](https://cgcookie.com/community/9686-why-not-use-subdivision-surface-modifier-on-this-weapon), [polycount](https://polycount.com/discussion/comment/2573659)). Others hold that subd with support loops remains best for normal-mapped hard surface ([polycount](https://polycount.com/discussion/comment/2573688)).
- Regenerable: only if loop insertion is itself generated from the tree. Cage independence: lost by construction, since loops and cut-ring vertices are cage vertices and the cage grows with every detail. The "detail never moves the cage" invariant excludes this option (inferred).
- Semi-sharp creases are the in-cage alternative to extra loops. OpenSubdiv: per-edge sharpness, 10 or more is infinite, Chaikin rule propagates sharpness; infinite creases are tangent discontinuities where normal displacement can tear ([OpenSubdiv](https://graphics.pixar.com/opensubdiv/docs_3x_alpha/subdivision_surfaces.html)). Blender: weighted edge crease, Boundary Smooth, Use Limit Surface and Quality ([manual](https://docs.blender.org/manual/en/4.2/modeling/modifiers/generate/subdivision_surface.html)). Creases mark edges already in the cage, so a cut still needs cage edges where it falls (inferred).
- OpenSubdiv feature-adaptive refinement clusters subdivision near creases, extraordinary points, boundaries, corners ([release info](https://graphics.pixar.com/opensubdiv/release_info.html)). Hierarchical edits (override values at any refinement level, localized detail without specifying the whole surface) were removed in OpenSubdiv 3.0 ([hedits](https://graphics.pixar.com/opensubdiv/docs/hedits.html)).
## 3. Boolean then retopo
- Common practice: model the high poly, copy, strip support loops and subdivisions that do not affect silhouette to a low poly, bake ([polycount](https://polycount.com/discussion/comment/2573659)). Retopo output is a new mesh with its own topology: not regenerable from a tree, and cage-independent only if the retopo output is itself the cage. No source found automating retopo from a CSG tree (gap).
## 4. Floating geometry / mid-poly (separate interpenetrating pieces)
- Mid-poly workflow, attributed to Star Citizen and Alien: Isolation by one tutorial: face-weighted normals, high-poly detail in normal maps rather than geometry ([80.lv](https://80.lv/articles/how-to-create-a-detailed-large-scale-scene-with-rich-props-assets-in-unreal-engine/)).
- A polycount developer describes tri-planar local-space decals that modify only normals in targeted areas, usable for panel lines, "but sometimes it's worth more to cut that into the topology" ([polycount](https://polycount.com/discussion/comment/2333210)). Baked bevels in normal maps are cheap but resolution-dependent; the alternative cited is real bevels plus adjusted vertex normals ([polycount](https://polycount.com/discussion/comment/2333606)).
- Regenerable: yes, pieces are tree outputs. Cage independence: full. Triangles: each piece adds its own, including hidden interior geometry; the hull is not cut. Shading: the intersection seam is a hard visual line unless a normal bake or bevel masks it. Floaters rely on a bake, which this campaign does not have (inferred; no source on floaters without a bake).
## 5. NURBS / CAD closed prior art
- Plasticity: pure NURBS, booleans advertised as watertight, fillets adapting to edge conditions, direct-edit with no history tree, meshes only on export (OBJ, STEP, IGES); no subdivision-surface mention found ([DigitalProduction](https://digitalproduction.com/2023/10/04/plasticity-3d-cad-for-artists/), [Plasticity](https://plasticity.xyz/)). Its boolean-then-fillet works on exact surfaces and tessellates afterwards, not on a subdivided mesh (inferred).
## 6. Displacement and detail transfer on subdivision surfaces
- Displaced Subdivision Surfaces (Lee, Moreton, Hoppe, SIGGRAPH 2000): scalar displacement over a smooth subdivision domain surface in one framework; the hard part is finding a simple domain surface; the project page does not discuss overhangs, holes or hard creases ([Hoppe](https://hhoppe.com/proj/dss/)). A scalar over a surface cannot represent undercuts or cut-through (inferred).
- Kobbelt, Campagna, Vorsatz, Seidel, Interactive Multi-Resolution Modeling on Arbitrary Meshes (SIGGRAPH 1998): detail coefficients coded in local frames over a decimation hierarchy for detail-preserving edits ([RWTH](https://www.graphics.rwth-aachen.de/publication/03156/)). Frame definition not read.
- Biermann, Martin, Bernardini, Zorin, Cut-and-paste editing of multiresolution surfaces (SIGGRAPH 2002): split source and target into base and detail, parameterize bases to a common domain, transfer detail offsets ([SIGGRAPH history](https://history.siggraph.org/learning/cut-and-paste-editing-of-multiresolution-surfaces-by-biermann-martin-bernardini-and-zorin/)). Same group, Approximate Boolean Operations on Free-Form Solids (Biermann, Kristjansson, Zorin, SIGGRAPH 2001), is cited as related; contents not read.
- All three keep the base fixed and put detail in coefficients on top, which is the two-layer invariant in academic form. None addresses hard-edged cut-through features with bevels (inferred). Multires (sculpt subsurf) versus plain subsurf with runtime GPU subdivision instead of baked displacement is noted in a forum answer ([Blender Artists](https://blenderartists.org/t/difference-between-subdivision-surface-and-multiresolution/1277968)).
## Summary table
| Approach | Regenerable from tree | Cage independent | Triangles | Shading under 1-seg bevel + WN |
|---|---|---|---|---|
| Boolean on evaluated mesh, bevel + WN live | yes | yes | high, ragged along cuts | works if bevel sits below boolean and WN last; ragged cut tris need cleanup |
| Support loops in cage | only if loop insertion is generated | no, cage grows | cage low, subdivided high | well behaved; artifact-prone with loop routing |
| Boolean then retopo | no | only if retopo is the cage | controlled | controlled, bake needed |
| Floaters / mid-poly | yes | yes | adds interior tris | seam visible without bake |
| Displacement / detail transfer | yes | yes | displaced mesh resolution | no hard cut-through; bevel not in model |
## Gaps
- `brush-tree-target.md` unread (commit not resolvable locally).
- Blender Bevel (Harden Normals, Set Strength) and Weighted Normal read via search summaries only; raw manual RST URLs 404 and the 4.2 pages returned navigation text first.
- No primary source for Star Citizen, Sea of Thieves, or CDPR pipelines; mid-poly claim rests on one tutorial.
- No source on boolean-after-subdivision as a named deliberate workflow with documented shading outcome.
- OpenSubdiv limit-evaluation pages not reached; Support Loop Pro behaviour not read.

View file

@ -1,28 +0,0 @@
# Dexter Quiet: the operator's reference cage (2026-10-12)
The reference brush-tree target r2 is measured against (ruling brush-tree:ruling:pursue-sdf-composition-remeshing:
the operator rebuilds Dexter Quiet by hand as the reference cage).
Source: `Asset Sources/Ships.blend` in Aetheria, object `Dexter Quiet Cage` (collection Quiet), saved 2026-10-12 17:31.
Exported from a copy by `export.py` (Blender, background mode). Files:
- `cage_raw.obj`: the cage as authored, half a hull: 29 vertices, 25 faces, 9 edges creased at 1.0 (`cage_info.txt`).
- `cage_eval.obj`: the evaluated stack: Mirror (merge on) -> Subsurf level 3 (render 2, limit surface on) ->
Bevel (0.001, 1 segment, weight-limited) -> Weighted Normal. 3,154 vertices, 3,152 faces.
- The scanned original `Dexter Quiet` (4,832 vertices, 5,600 faces) is not copied here; it stays in Ships.blend.
Fidelity of the hand cage to the scan (vertex-to-nearest-vertex, an upper bound on surface distance; hull diagonal 1.492):
cage->scan mean 0.88% p95 2.04% max 3.11% of the diagonal; scan->cage mean 1.29% p95 4.12% max 6.03%.
That spread mixes two things and is NOT yet a fidelity bar: the cage captures the primary form only (silhouette and main
curves); the canopy, vents, intakes and nose details of the scan are absent by design and belong to a detail layer above
the cage. A bar must be measured on primary form (smoothed scan, masked detail, or silhouettes), not raw vertices.
Operator, 2026-10-12, with the screenshots here (cage-limit-*.png beside scan-*.png): "Note that I'm specifically trying
to be faithful here to the curves of the original, and my control points can go to unexpected places to achieve that.
The cage controls the surface, the cage is not the surface." Producing such a cage is subdivision-surface fitting
(control points off the surface, solved so the limit surface fits), not remeshing.
Operator, 2026-10-12: "I'm afraid I had to take a bit of artistic liberty to get the cage to behave, mostly because
of how creases work in Blender (generates horrible geometry if the crease path isn't closed)". Body fact for the
remesher: crease chains in the cage are closed loops; an open crease path ending inside a face region produces
bad subdivision geometry in Blender's Catmull-Clark (OpenSubdiv) evaluation, so a generated cage must close its creases.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because it is too large Load diff

View file

@ -1,4 +0,0 @@
[('cage_raw', (29, 25)), ('cage_limit_eval', (3154, 3152)), ('dexter_ref', (4832, 5600))]
creases=[(1, 3, 1.0), (5, 1, 1.0), (17, 11, 1.0), (3, 17, 1.0), (13, 16, 1.0), (24, 22, 1.0), (25, 16, 1.0), (13, 11, 1.0), (24, 25, 1.0)]
mods=[('MIRROR', {'use_clip': False, 'use_mirror_merge': True}), ('SUBSURF', {'levels': 3, 'render_levels': 2, 'use_limit_surface': True}), ('BEVEL', {'width': 0.0010000001639127731, 'segments': 1, 'limit_method': 'WEIGHT'}), ('WEIGHTED_NORMAL', {})]
cage_dims=(0.9702155590057373, 1.0190552473068237, 0.3775596618652344) ref_dims=(0.9811705946922302, 1.0477101802825928, 0.407174676656723)

View file

@ -1,54 +0,0 @@
v -0.050183 -0.515158 0.120381
v -0.050183 -0.502594 0.165733
v -0.066373 -0.290093 0.034410
v -0.066373 -0.279099 0.154038
v -0.000000 -0.521785 0.077995
v -0.000000 -0.495967 0.196439
v 0.000000 -0.257172 0.011836
v 0.000000 -0.079878 0.194280
v -0.085456 0.296420 0.036890
v 0.000000 0.264006 -0.120438
v -0.000000 0.324338 0.095861
v -0.532850 -0.332750 0.201392
v -0.000000 0.083429 0.170193
v -0.445757 -0.036017 0.098265
v -0.069995 0.072662 0.125021
v -0.305911 -0.163824 0.119325
v -0.219219 0.256351 -0.032856
v -0.271429 -0.083411 0.118972
v -0.071629 0.154301 -0.071808
v 0.000000 0.096312 -0.125795
v -0.230102 0.060001 0.075233
v -0.231539 0.092497 -0.020192
v 0.002669 0.513952 -0.201831
v 0.002669 0.519207 -0.172940
v -0.010721 0.515629 -0.180240
v -0.106624 0.345361 -0.096021
v -0.041564 0.406965 -0.070340
v 0.001707 0.450794 -0.082077
v 0.001374 0.391670 -0.173090
f 1 2 4 3
f 15 13 11 9
f 5 6 2 1
f 3 7 5 1
f 8 4 2 6
f 22 16 12 14
f 20 19 10
f 10 19 22 17
f 16 18 12
f 21 17 14
f 15 9 17 21
f 3 4 18 16
f 19 3 16 22
f 4 15 21 18
f 18 21 14 12
f 7 3 19 20
f 17 22 14
f 4 8 13 15
f 24 23 25
f 26 27 25
f 9 11 28 27
f 29 26 25 23
f 10 17 26 29
f 17 9 27 26
f 27 28 24 25

View file

@ -1,20 +0,0 @@
import bpy, sys, bmesh
d=sys.argv[-1]
dg=bpy.context.evaluated_depsgraph_get()
cage=bpy.data.objects['Dexter Quiet Cage']; ref=bpy.data.objects['Dexter Quiet']
def dump(o, ev, path):
src=o.evaluated_get(dg) if ev else o
me=src.to_mesh()
with open(path,'w') as f:
for v in me.vertices:
p=o.matrix_world @ v.co; f.write(f"v {p.x:.6f} {p.y:.6f} {p.z:.6f}\n")
for poly in me.polygons: f.write("f "+" ".join(str(i+1) for i in poly.vertices)+"\n")
n=(len(me.vertices),len(me.polygons)); src.to_mesh_clear(); return n
info=[]
info.append(("cage_raw",dump(cage,False,d+"/cage_raw.obj")))
info.append(("cage_limit_eval",dump(cage,True,d+"/cage_eval.obj")))
info.append(("dexter_ref",dump(ref,False,d+"/dexter_ref.obj")))
me=cage.data; a=me.attributes.get('crease_edge')
cr=[(e.vertices[0],e.vertices[1],round(a.data[e.index].value,3)) for e in me.edges if a and a.data[e.index].value>0]
mods=[(m.type,{k:getattr(m,k) for k in ('levels','render_levels','use_limit_surface','width','segments','limit_method','use_clip','use_mirror_merge') if hasattr(m,k)}) for m in cage.modifiers]
open(d+"/cage_info.txt","w").write(f"{info}\ncreases={cr}\nmods={mods}\ncage_dims={tuple(cage.dimensions)} ref_dims={tuple(ref.dimensions)}\n")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

View file

@ -1,256 +0,0 @@
# Mesh probe run 1: parametric rebuilds of Tripo hulls, with tools
Agent `probe-mesh-rebuild-1`, session `self-2026-10-10-ag2`, 2026-10-10. Disposable scratch; nothing
committed, no repo edited. Root: this directory (`...\scratchpad\mesh-probe\run1\`).
Governing operator words: "I'm not interested in measuring Opus's spatial reasoning performance, in
interested in exploiting it. Give it access, let it build and use tools." The probe ran the F2(b) arm
(full tool access) with the plan's targets, metrics, pass bars and stopping rules.
## Headline
| Target | Vocabulary | Best chamfer (% diag) | F@1% / F@2% | Mean sil-IoU | Vol-IoU | B0 chamfer | Round-0 chamfer | Loop gain (best / r0) | Edit test | Bars |
|---|---|---|---|---|---|---|---|---|---|---|
| Control | Geometry.Csg (12 brushes, truth 12) | **0.015** | 0.996 / 1.000 | 0.994 | 0.996 | 5.796 | 0.085 | 0.18x | 4 lines; 0.070 (4.7x best) | pass; edit fails the 1.5x bound |
| Control | OpenSCAD | **0.029** | 0.990 / 1.000 | 0.985 | 0.992 | 5.796 | 0.090 | 0.32x | 1 line; 0.030 (1.03x) | pass |
| Dexter Quiet run 1 | OpenSCAD (table of offsets) | **0.266** | 0.954 / 0.973 | 0.973 | n/a | 10.565 | 0.278 | 0.96x | 1 line; 0.266 (1.0x) * | pass; loop effect fails |
| Dexter Quiet run 1 | Geometry.Csg (29 brushes) | **0.463** | 0.888 / 0.985 | 0.911 | n/a | 10.565 | 1.226 | 0.38x | 7 lines; 0.461 (1.0x) * | pass; edit > 5 lines |
| Dexter Quiet run 2 | OpenSCAD (compact) | **0.682** | 0.788 / 0.947 | 0.924 | n/a | 10.565 | 0.803 | 0.85x | 1 line; 0.678 (1.0x) * | pass; loop effect in between |
| Headliner | OpenSCAD | **1.121** | 0.563 / 0.821 | 0.913 | n/a | 8.078 | 1.134 | 0.99x | 1 line; 1.087 * | pass (narrowly); loop effect fails |
Quiet pass bars: chamfer <= 2%, F@2% >= 0.80, mean sil-IoU >= 0.90, chamfer <= 0.33 x B0 (3.49 on Quiet,
2.67 on Headliner). Control pass bars: chamfer <= 1%, sil >= 0.95, vol >= 0.90, brushes <= 1.5x truth.
Headliner has no bar of its own in the plan, so Quiet's bars were applied.
\* On Quiet and Headliner the edit test does not discriminate: the **unedited** model scores almost the
same chamfer on the stretched target (Quiet r1 0.276, Quiet Csg 0.469, Quiet run 2 0.672, Headliner
1.088). The nose is a few percent of the surface. Only side-view sil-IoU moves (Headliner 0.927 to
0.948 with the edit; Quiet run 1 0.963 to 0.980). The control is the only target where the edit test
measures anything.
Full scope (every part) equals main-body scope within 0.001 on every best round: the dropped parts are
8 tiny floaters on Quiet and a few dozen specks on Headliner.
**Outcome letter: A by the table (control passes, Quiet passes, both vocabularies), qualified twice.**
1. The loop effect (outcome D) fails for OpenSCAD on Quiet run 1 and Headliner. Render-compare bought
little there, because round 0 was already built from measurement tools. It passes for Geometry.Csg
(0.38x) and on the control. In both of those cases the gain came mostly from tools: numeric refine,
plane fits, and the winding-number labels. Render critique contributed less.
2. The edit readout says the Geometry.Csg description is a fit, not a description: 7 lines on Quiet,
and 4.7x on the control. OpenSCAD is one line everywhere, because it has parameters and affine
transforms. However, the edit test only discriminates on the control.
## What actually produced the numbers (read before trusting the table)
- **Control contamination.** No Hands harness existed, so I wrote the control generator
(`tools/gen_control.py`). It used a random seed, and the truth (`sealed/truth.csg`) was never read.
I still knew its *structure* from writing the code: core, nose cuts, sponsons, wings, engines,
bridge and trench. The control therefore tests numeric recovery of a known structure. It does not
test discovering structure.
- **Quiet run 1 OpenSCAD is fit-heavy.** The skin is a table of offsets: 21 wing-body stations and 8
tail stations, each with a half-span and 7 [z_bottom, z_top] pairs, emitted by `mp prof` and
`tools/table.py` from the target. The model chose the decomposition: one wing-body skin, a separate
tail skin, two scallop cylinders, and the nose_k parameter. Run 2 is the compact description: a
17-point planform, one wing plane per side, 8 elliptical body stations, 4 tail stations, pods and a
chin fin. That is about 110 numbers against about 460. It is 2.5x worse in chamfer and still passes.
- **Run 2 is not independent.** It is the same agent with run 1 in context. It is a different
*approach*, not a second sample.
- **Nose direction.** The plan says nose -Y. On this mesh the beak and canopy end is at **+Y**: the
operator's schematic has the canopy arc at the pointed end, and the stinger is at -Y. I used +Y
for "the nose" in the edit test.
- **Metrics differ from the plan's harness.** Everything was computed by my own `mp` tool. Chamfer is
point-to-triangle with 30k area-weighted samples each way. Silhouette IoU uses 5 orthographic
rasters at 384 px: front, side, top, and two obliques. Opposite axis views are identical in
orthographic projection, so 3 axes and 2 obliques stand in for the plan's 6 + 2 Workbench renders.
Vol-IoU is on the control only, from 128^3 ray parity. No Blender Workbench renders were made.
- **Geometry.Csg scoring.** The brush list is lowered through the crate's own `Assembler::distance`
(src/field.rs). Surface nets at 220 cells on the long axis mesh the field, and that mesh is
scored. The control target is itself a field mesh at the same resolution, which slightly favours
Csg candidates on the control. Kernel (polygon) meshes were also scored as a side readout; see the
gaps below.
- **Blender.** One background export job ran on a *copy* of `Desktop\Quiet.blend`
(`targets/Quiet-copy.blend` to `targets/src_*.obj`). An operator GUI Blender (PID 36324, open on
`Aetheria\Asset Sources\Ships.blend` since 2026-09-27) was present the whole time. I did not wait
on it, because it is an interactive session, not a smoke run. A second Blender (PID 59588, not
mine) appeared near the end, after my last Blender use. Nothing of mine is running.
- **Not done.** Headliner was rebuilt in OpenSCAD only, not in Geometry.Csg. Quiet run 2 was
OpenSCAD only, which the brief allows. The Quiet run 2 loop stopped after one regression (r3),
not after two sub-2% rounds, to stay inside the call budget.
## Per-round logs
Scores are main-body chamfer % / mean sil-IoU. "ref" means `mp refine` was run on the round before.
**Control, Csg.** r0 0.085 / 0.977, built from planes, sections and depth readouts. r1 (refine, 12
sweeps, 12 s) 0.015 / 0.994. The tools exposed what the renders did not: the trench length was 0.33,
not 0.24, and the refine also moved the wing roll and the nose_top pitch. The refine also drifted
nuisance parameters, for example a cutter roll of 13 deg that does nothing, which is a fit-not-
description smell.
**Control, OpenSCAD.** r0 0.090 / 0.972, a direct translation. r1 0.029 / 0.985 uses the refined
numbers, restructured with `nose_root`, `nose_len` and a `nose_frame()` that scales the nose cutters.
**Quiet run 1, OpenSCAD.** r0 0.278 / 0.970 (15 + 8 stations). The first render was NotManifold:
the cap winding was wrong in my 10-line skin module, and the mesh tool caught it because Manifold
warned. r1 0.266 / 0.973 adds denser stations at the beak. The residual is mostly the target's
interior shells: cockpit internals and pods, visible as red loops *inside* the outline on
`quiet1/r1.sec.png`. No solid can match those.
**Quiet run 1, Geometry.Csg.**
| Round | Chamfer / sil | Change |
|---|---|---|
| r0 | 1.226 / 0.827 | Hand-estimated wing plane, step boxes, nose wedge, tail block. |
| r0ref | 0.800 / 0.857 | Refine. |
| r1 | 0.971 / 0.873 | Wing plane re-fitted by least squares (`tools/wingfit.py`). The fit showed my hand estimate was wrong: dihedral slope 0.44, where the fit gave 0.17 with 3.5 mm rms. |
| r1ref | 0.875 / 0.841 | Refine. Sil got worse. |
| r2 | 1.025 / 0.860 | Bounded leading-edge and beak cuts for concave outline corners; keel. |
| r2ref | 0.828 / 0.827 | Refine with ray-parity inside labels. |
| r2ref2 | 0.644 / 0.874 | Same refine with **generalized-winding-number labels** (Jacobson 2013). 1454 of 14000 parity labels (10%) on the non-manifold target were wrong; the parity refine had been trading silhouette away. |
| r3 | 0.630 / 0.887 | Inner and outer wing panels on their own fitted planes. |
| r3ref | 0.555 / 0.891 | Refine. |
| r4 | 0.541 / 0.908 | Dorsal spine and nose floor cut, from a centreline profile diff (`mp prof` target vs candidate). |
| r4ref | **0.463 / 0.911** | Refine. |
**Quiet run 2, OpenSCAD.**
- r0 0.803 / 0.919.
- r1 0.729 / 0.926: joined the tail to the body (a gap was visible in the side view), added
under-body pods and a chin fin.
- r2 **0.682** / 0.924: a station-wise thickness diff showed the body 45-55 mm too thin at
x = 0.13-0.25, so the body was widened.
- r3 0.852 and r3b 0.775: superellipse sections (n = 3 and 2.5). These are regressions: the target's
wing-root concavity punishes fuller sections.
**Headliner, OpenSCAD.**
- r0 1.134 / 0.902: fuselage, blade hulls, pods and inner engines as superellipse lofts and
cylinders, measured with `mp island`.
- r1 1.188 / 0.910: deck and blade fins. Sil went up, chamfer went down: the deck buried target
side walls.
- r2 **1.121** / 0.913: thinner, higher deck.
## Renders (target | candidate | silhouette diff; red = target only, blue = candidate only)
- Control: `control/best-csg.sheet.png` and `control/best-scad.sheet.png`. Round 0 is
`control/r0.sheet.png`, the OpenSCAD r0, which ran last under that basename.
- Quiet run 1 OpenSCAD: `quiet1/best-scad.sheet.png` and `quiet1/best-scad.sec.png`. Round 0 is
`quiet1/r0-scad.sheet.png`.
- Quiet run 1 Csg: `quiet1/best-csg.sheet.png` and `quiet1/r4ref.sec.png`. Round 0 is
`quiet1/r0-csg.sheet.png`.
- Caveat: `rN.sheet.png` files in `control/` and `quiet1/` hold whichever vocabulary ran last under
that basename, because .csg and .scad rounds share basenames. Use the `best-*` and `r0-*` files.
- Quiet run 2: `quiet2/r2.sheet.png`, `quiet2/r0.sheet.png`.
- Headliner: `headliner/r2.sheet.png`, `headliner/r0.sheet.png`, target sections `headliner/t_secy.png`.
- B0 baselines: `quiet1/b0.sheet.png`, `headliner/b0.sheet.png`, `control/b0.sheet.png`.
Best programs: `control/r1.csg`, `control/r1.scad`, `quiet1/r1.scad` + `quiet1/tables1.scad`,
`quiet1/r4ref.csg`, `quiet2/r2.scad`, `headliner/r2.scad`. Edits: `*_edit.*` beside each.
## Tools built (candidates for brokkr-ctl commands)
All are in `tools/`. `mp` is one Rust binary, about 650 lines, that depends on GameCult.Geometry.Csg
by path. It was built once locally and offline, in 1 min 10 s; incremental rebuilds took 7-9 s. The
target directory is 156 MB and is safe to delete. Every command runs in well under a second, except
`refine` (12-40 s).
| Tool | What it does | Earned its keep? |
|---|---|---|
| `mp score` | Chamfer (both directions), F@1/2%, 5-view sil-IoU, vol-IoU | Yes, every round. Seeing the t2c/c2t split told me whether a surface was missing or extra. |
| `mp sheet` | Contact sheet (4 views x target, candidate, diff) | Yes. The diff column found every missing part: tail gap, pods, deck, blade fins. |
| `mp sections` | Station slices, overlaid, with numeric extents | Yes. Control round 0 numbers. It showed Quiet's residual is interior shells. |
| `mp prof` | Table of offsets: per station, half-span and z-range at fixed span fractions (with an x window) | **Biggest single lever.** It built Quiet OpenSCAD round 0 at 0.278 directly, and its target-vs-candidate diff drove Quiet csg r4 and run 2 r2. |
| `mp island` | Station extents inside an x/z window (off-centre parts) | Yes on Headliner: engines, blades, pods, deck. |
| `mp depth` | ASCII depth map from an axis | Moderately. Control engines and trench; the Quiet scallop arc (circle fitted by hand from it). |
| `mp planes` | Iterated 1-point RANSAC plus a least-squares plane refit | Yes on the control: every face angle (chines 60.5/124.3 deg, bridge 11.1 deg, wing roll 9.6/8.2 deg). Unused on Quiet, which has few planes. |
| `tools/wingfit.py` | Least-squares plane over a region of the offsets table | Yes. It corrected a wrong hand estimate (dihedral 0.44 to 0.17). |
| `mp sym` | Best mirror plane x0 and residual | Yes. It showed Quiet is exactly mirrored (0.000%) and Headliner 0.089%, so mirror pairs are safe. |
| `mp parts` | Welded connected components, main-body scope, part table | Yes. It set the scope. |
| `mp csg` | Kernel build (warnings, tri count, kernel STL) plus field mesh | Yes. It surfaced SubtractIgnoredForNonBox and the kernel-vs-field divergence. |
| `mp refine` | Coordinate descent on brush parameters. Loss is mean surface \|SDF\| plus volume misclassification. Mirror-tied `_p`/`_s`, symmetry-locked centred brushes, winding-number labels. | **Yes for Csg** (control 0.085 to 0.015; Quiet about -25% per application). It drifts nuisance parameters and is blind to silhouette when its labels are wrong. |
| winding-number labels (in refine) | Robust inside test for non-manifold targets | Yes. It replaced ray parity, which was 10% wrong on Quiet. |
| `tools/warp.py`, `tools/warp_obox.py` | Stretch target forward of y_n; re-fit an obox under that stretch | Needed only because Csg has no scale transform. warp_obox even picked the wrong face for mirrored cutters (beak*_s). |
| `tools/table.py`, `tools/ev.sh`, `tools/gen_control.py`, `tools/export.py` | Glue | Glue. |
As brokkr-ctl commands, the clear candidates are `score`, `sheet`, `sections` / `prof` (as one
"stations" command), `planes` / `fit plane|circle`, `sym`, `parts` and `refine`. Each one is pure mesh
math over exported geometry: no Blender API, millisecond to second latency, files in, text and PNG
out.
## OpenSCAD constructs used and wanted (card fork: real OpenSCAD vs a subset)
Used:
- **Language:** variables and vectors, `function` (the `ny` nose warp, a section generator, a
superellipse, `mirrored`), list comprehensions with `each`, conditional expressions,
`include <tables.scad>` for data.
- **Geometry:** `polyhedron` (a hand-written 10-line `skin` loft), `hull()` (pairwise loft chains,
pods, fins), `multmatrix` (shear: a planar wing slab on z = a + b|x| + c y), `scale` inside a
module with `children()` (stretching nose cutters, so the edit is one line), `linear_extrude`,
`polygon`, `circle`, `intersection` (planform with wing slab), `difference`, `union`, `rotate`,
`translate`, `cube`, `sphere`, `cylinder`, `$fn`, mirroring via `for (s = [1,-1])`.
Not used: `minkowski`, `offset`, `rotate_extrude`, `import`, `surface`.
Wanted and absent even from OpenSCAD: a built-in loft or skin (BOSL2 has `skin()`; I wrote one) and a
superellipse primitive.
**Evidence for the fork.** Every best OpenSCAD result leaned on functions, list comprehensions,
`polyhedron`, `include`d data tables, `multmatrix` and `hull`. A subset limited to CSG-tree syntax
(cube, cylinder, sphere, translate, rotate, the booleans) could not have expressed the Quiet run 1 or
run 2 bodies. It also could not have made the edit a one-line parameter change.
I recommend real OpenSCAD files evaluated by OpenSCAD (or a full evaluator) as the authoring
surface, with the lowering consuming its CSG tree. If a subset is chosen, it must keep `function`,
list comprehensions, `polyhedron` (or a loft), `hull`, `multmatrix` and `scale`. Confidence:
moderate. This is one agent on three hulls.
## Geometry.Csg vocabulary gaps hit (counts)
- **Mirror construct.** On Quiet, 18 of 29 lines are mirror twins (9 pairs); 4 of the 7 edit lines
are twins. On the control, 2 pairs, and the generator's own engines and wings were asymmetric. The
`_p`/`_s` tie had to live in my refine tool.
- **Non-box cutters skipped by the polygon kernel.** 2 scallop `cylz` subtracts on Quiet produced
`SubtractIgnoredForNonBox` x2. The kernel mesh then diverges from the field for the same list:
Quiet r3ref kernel chamfer 1.170 / sil 0.809, against field 0.555 / 0.891. The concave trailing
scallop is not expressible as a box cut.
- **Kernel interior faces even with boxes only.** Control r1 kernel mesh chamfer 0.504 (c2t 0.996)
against field 0.015. The silhouette is identical, so this is internal faces from overlapping Adds,
not geometry error.
- **No affine or scale transform, no parameters.** The control edit needed 4 hand-recomputed oboxes
and lost fidelity (4.7x). The Quiet edit needed 7 lines.
- **No curved sweep, loft or ellipsoid.**
- The body is 4 step boxes plus a keel plus 2 spine blocks.
- The wing is 2 planar slabs per side.
- Best Csg 0.463 against OpenSCAD 0.266 on Quiet; the gap is curvature.
- Headliner (y-axis engines, ellipsoidal pods) was not attempted in Csg.
- **No arbitrary-axis cylinder.** It was needed for Headliner's 4 engines and 2 pods, and on Quiet for
the pods (skipped in Csg).
- **Global intersect and subtract need ordering tricks.**
- The wing trims (6 cutters) had to come before the body Adds, so the body survived.
- The nose cuts had to come after.
- Concave outlines needed **bounded** cutters, 4 per side (lead0/lead1, beak1/beak2), each with a
hand-computed extent.
- A scoped intersect (planform ∩ wing plane, which OpenSCAD run 2 used directly) would have
replaced all of these.
- **FloretArm and DomeCapZ went unused.** FloretArm has constant thickness and lifts only along its
arm. DomeCapZ has only a circular base. Both were considered for the wing, tail and canopy, and
rejected.
## Card fork: where the fitting logic should live
Every fitting step was pure geometry over exported meshes, in a standalone CLI, with no Blender
dependency:
- station profiles;
- plane, RANSAC and least-squares fits;
- symmetry;
- winding-number inside tests;
- coordinate-descent refine;
- scoring.
Blender was needed once, to export. The evidence favours fitting in the CLI process, or a library it
links, operating on a selection or mesh dump. The Blender bridge should only supply geometry
(selection, export) and renders. Confidence: moderate. The probe never exercised a live Edit Mode
selection.
## Continuation
Finished within budget. Not done: Headliner in Geometry.Csg; a genuinely independent Quiet run 2 (fresh
agent); an eyes-only arm for comparison; Workbench renders as the plan specified.

View file

@ -1,43 +0,0 @@
// Dexter Quiet, run 2, round 3: a compact description instead of a table of offsets.
// Manta: beak +Y, stinger tail -Y, starboard -X. Parts: wing sheet, body, tail.
nose_root = 0.36; nose_k = 1.0; // beak starts at nose_root; nose_k stretches it
function ny(y) = y > nose_root ? nose_root + nose_k*(y - nose_root) : y;
// Wing sheet: the planform (top view, +x half, beak tip to wingtip) cut by the trailing scallops,
// lying on one plane per side, z = a + b|x| + c y, of thickness wing_t.
planform = [[0,0.52],[0.011,0.51],[0.033,0.461],[0.062,0.412],[0.146,0.363],[0.184,0.265],[0.238,0.216],
[0.289,0.167],[0.332,0.118],[0.369,0.069],[0.402,0.02],[0.431,-0.03],[0.46,-0.086],[0.475,-0.135],
[0.49,-0.19],[0.49,-0.245],[0,-0.245]];
wing_plane = [0.0146, 0.1735, -0.3128]; wing_t = 0.045;
scallop_r = 0.2904; scallop_c = [0.3018, -0.4593];
// Body and tail: elliptical stations [y, z_bottom, z_top, half_width], skinned by pairwise hull.
body = [[-0.185, 0.005, 0.178, 0.12], [-0.08, -0.075, 0.175, 0.17], [0.02, -0.10, 0.158, 0.19], [0.118, -0.127, 0.131, 0.21],
[0.216, -0.13, 0.101, 0.19], [0.314, -0.165, 0.047, 0.13], [0.40, -0.16, -0.03, 0.065], [0.51, -0.189, -0.176, 0.012]];
tail = [[-0.525, 0.146, 0.191, 0.024], [-0.42, 0.065, 0.187, 0.05], [-0.33, 0.03, 0.184, 0.077], [-0.24, 0.02, 0.18, 0.12]];
sect_n = 3; // station cross-section is a superellipse of this exponent (2 = ellipse)
function se(t) = sign(t)*pow(abs(t), 2/sect_n);
module station(s) translate([0, ny(s[0]), (s[1] + s[2])/2]) rotate([90, 0, 0]) linear_extrude(0.001, center = true)
polygon([for (a = [0:10:359]) [s[3]*se(cos(a)), (s[2] - s[1])/2*se(sin(a))]]);
module loft(st) for (i = [0:len(st)-2]) hull() { station(st[i]); station(st[i+1]); }
module wing_side(s) intersection() { // s = +1 port, -1 starboard
multmatrix([[1,0,0,0],[0,1,0,0],[s*wing_plane[1], wing_plane[2], 1, wing_plane[0]],[0,0,0,1]])
translate([s > 0 ? 0 : -1, -1, -wing_t/2]) cube([1, 2, wing_t]);
linear_extrude(height = 2, center = true) planform2d();
}
module planform2d() difference() {
polygon(concat([for (p = planform) [p[0], ny(p[1])]], [for (i = [len(planform)-1:-1:0]) [-planform[i][0], ny(planform[i][1])]]));
for (s = [1, -1]) translate([s*scallop_c[0], scallop_c[1]]) circle(scallop_r, $fn = 96);
}
union() {
for (s = [1, -1]) wing_side(s);
loft(body);
loft(concat(tail, [body[0]])); // tail runs into the body
for (s = [1, -1]) pod(s);
chin_fin();
}
// Under-body pods either side of the keel, and the chin fin under the beak root.
pod_a = [0.13, 0.22, -0.085, 0.03]; pod_b = [0.135, 0.38, -0.10, 0.035]; // [x, y, z, r] at each end
module pod(s) hull() for (p = [pod_a, pod_b]) translate([s*p[0], ny(p[1]), p[2]]) sphere(p[3], $fn = 24);
module chin_fin() hull() for (p = [[0.30, -0.15], [0.363, -0.215], [0.40, -0.16]]) translate([0, ny(p[0]), p[1]]) cube([0.012, 0.01, 0.01], center = true);

View file

@ -1,367 +0,0 @@
# Probe plan: can Opus describe a messy Tripo hull as a brush list?
Imagination, agent `imagination-brokkr-mesh-probe`, session `self-2026-10-10-ag2`, 2026-10-10.
Ruling in force: `aetheria-release:ruling:brokkr-mesh-probe-ag-self`. Operator: "What if the model
had some trash geo and its goal is to describe it parametrically?"
This is a probe plan, not a campaign. Nothing here is admitted. The probe is disposable scratch;
only its result document is kept.
## 1. The question, narrowed
Given a messy hull mesh, can Opus write a GameCult.Geometry.Csg brush list that matches it? How
close does it get, and does a render-and-compare loop improve the match?
The probe separates two things that VibeGeometry never separated:
- **Model reasoning.** Can the model place, size and cut brushes to match a shape it can only see?
This is tested on a **control hull**: a synthetic target that is itself a known brush list, so the
vocabulary can express it exactly.
- **Vocabulary and noise.** Can the vocabulary describe a real Tripo hull, with its floaters,
non-manifold edges and soft forms? This is tested on **Dexter Quiet**.
The pairing decides the next step. If the control passes and Quiet fails, the bottleneck is the
vocabulary or the noise. If the control fails, the bottleneck is the model with this harness.
## 2. What exists today (facts the plan rests on)
**Brokkr cannot run this loop today.** The Blender adapter (`F:\Projects\Brokkr\surfaces\blender\brokkr_bridge`)
admits eight commands: createObject, deleteObject, setObjectTransform, setObjectVisibility,
setObjectParent, setObjectCustomProperty, selectObject and assignMaterial. It also captures a scene
snapshot. It cannot render, import or export meshes, read mesh data, or run code. It needs GUI
Blender with the add-on and CultLib's Python packages loaded. `docs/research/blender-mcp-capability-bar.md`
records that rendering, code execution and mesh reads are exactly what the Blender MCP servers have
and Brokkr lacks.
The probe therefore runs as plain `blender --background` scripts. It answers the same question,
and its harness doubles as the requirements list for a Brokkr Blender surface, if the probe earns one.
**Blender.** Blender 5.2.2 LTS is at `C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe`.
This is the binary the Aetheria release map's probes used (M8, M16). It bundles numpy 2.3.4 but not
scipy. `mathutils.bvhtree` provides nearest-point and ray queries. The probe needs no other Python
packages, since system Python has no numpy, trimesh or open3d. Two other installs exist and are not
used: `D:\Steam\SteamApps\common\Blender` (5.0), and 2.77 and 2.78 under `D:\Applications`.
**GameCult.Geometry.Csg (0.1.0, Rust 2024, deps bevy_math 0.17, rmp-serde, serde, sha2).**
- `Primitive` has five variants: Box{bounds}, OrientedBox{center,size,rotation}, CylinderZ,
DomeCapZ and FloretArm. The ops are Add, Subtract and Intersect, applied as one ordered stream.
- Only Box and OrientedBox take part in the polygon CSG (`convex_cutter`, `assembler.rs:677`).
Each of the other three is appended as its own mesh. A Subtract or Intersect brush of one of those
types is skipped, and the build records `SubtractIgnoredForNonBox` or `IntersectIgnoredForNonBox`
(`assembler.rs:803,836`).
- CylinderZ is fixed to the Z axis. `Brush.rotation` is not read when CylinderZ is compiled.
- Intersect is global: it clips every solid accumulated before it. `CsgTree` branches exist in
`tree.rs`, but the assembler stream does not consume them.
- `Assembler::distance(p)` and `stream_distance` honour every op on every primitive. For boxes,
oriented boxes and CylinderZ, the field is the exact signed distance.
- OBJ output lives in the examples (`examples/dream_machine_level.rs:355`), not the library. No
primitive derives `Deserialize`.
**Hulls.** `C:\Users\Meta\Desktop\Quiet.blend` is the operator's file. Treat it as read-only and use
it only through a copy. Measurements from M8 and M16 of `docs/aetheria-release-map.md` on
Aetheria origin/master:
| Hull | Tris | Parts | Largest part | Parts < 2% length | Non-manifold edges | Size (Blender units) |
|---|---|---|---|---|---|---|
| Dexter Quiet | 9,614 | 12 | 87% | 8 | 524 | 0.98 x 1.05 x 0.41 |
| Sinister Quiet | 10,880 | 12 | 88% | 7 | 356 | 1.07 x 1.05 x 0.41 |
| Headliner | 50,008 | 189 | 33% | 23 | 6,145 | 0.68 x 1.00 x 0.35 |
Schematics are on the Desktop beside the .blend: `DexterQuietSchematic.png`, `SinisterQuietSchematic.png`
and `HeadlinerSchematic.png`. The nose points along Blender -Y, and starboard is -X. The add-on keeps
the Tripo mesh in the `Source` child collection (`tools/blender/aetheria_ships/__init__.py:30`), which
is never exported. The release map already makes the Tripo mesh a bake source for the operator's
rebuild (M16). A brush hull would be that rebuild's geometry.
## 3. Candidate targets, in increasing difficulty
1. **Control hull (synthetic).** Hands writes 12-20 Box and OrientedBox brushes in Quiet's
proportions: a core hull, a wedge nose made by subtracting angled boxes, wings or sponsons,
engine blocks and a bridge. It is meshed by the same evaluator as candidates. The truth file is
never shown to the probe agent.
2. **Dexter Quiet.** One dominant part (87%), a moderate number of non-manifold edges, and an
operator schematic. This is the real test.
3. **Headliner.** 189 parts, of which the largest is only 33%. Stretch only. Run it only if Quiet
reaches at least the partial band (section 8).
Sinister Quiet is held back. It is a near-sibling of Dexter and adds little until variance is known.
## 4. Representation the model writes
The model writes the Geometry.Csg brush stream as-is, in a thin line form, with one brush per line.
The fields map one-to-one onto `Primitive`:
```
# op shape name center x y z size x y z rot_deg x y z (obox only)
add box hull_core 0.00 0.02 0.00 0.34 0.80 0.16
sub obox nose_chine_top 0.00 -0.46 0.10 0.50 0.30 0.12 -28 0 0
add obox sponson_port 0.30 0.10 -0.02 0.14 0.55 0.10 0 0 4
int box clip_envelope ...
```
- `box` becomes `Primitive::Box { bounds: Aabb::from_center_size(..) }`.
- `obox` becomes `Primitive::OrientedBox` with `Quat::from_euler(EulerRot::XYZ, ..)` in degrees.
- Ops are add, sub and int, in stream order. Material is fixed at `MaterialId(0)`.
- The vocabulary is Box and OrientedBox only (fork F3). These are the only primitives the kernel
actually runs CSG on. A curved primitive cannot be cut, so offering one invites silent skips.
Why this form and not Rust `LevelDsl` source:
- A Rust source form needs a compile on every round.
- Rust source invites helper loops and abstractions. VibeGeometry's own failure record names "valid
Python, … high node counts, … parameter sliders" among its seductive proxies.
- Euler degrees in plain columns are easier to read and edit than quaternions. BenchCAD records
single-literal errors as a failure mode, and a flat table makes each literal one visible edit.
- No new DSL is introduced. A line is a Brush with its fields written out.
What Geometry.Csg cannot express for hard-surface hulls. Each gap is a probe readout to tally, not
something to build:
- Cylinders on any axis other than Z. Engines along Y need these.
- Boolean cuts by curved primitives.
- Bevels and chamfers, except as subtracted angled boxes.
- Mirror and array constructs.
- Scoped intersect, meaning an intersect that clips one part instead of the whole stream.
The probe counts how often the model wanted each one: brush pairs that duplicate each other as
mirror images, faceted stand-ins for a cylinder, and many small chamfer cuts. Those counts are the
evidence for any later vocabulary cut.
## 5. The verifier
Two disposable tools live in the probe scratch directory. Neither is committed to any repo.
**(a) `csgeval`.** A scratch Rust binary with a path dependency on `F:\Projects\GameCult.Geometry.Csg`,
and `CARGO_TARGET_DIR` set inside scratch. It parses the line form (about 80 lines), assembles it,
and writes:
- an OBJ (using the example's `obj_text`);
- the build warnings;
- about 20k surface samples, area-weighted, kept only where `|stream_distance| <= 1e-4 x target
diagonal`.
The sample filter exists because overlapping Add brushes can leave internal faces in the mesh. Only
points on the field's zero set count as surface, so the kernel's own field decides what the surface is.
The binary is built once; each round only runs it.
**(b) `eval.py`.** A Blender 5.2.2 background script, run as
`blender --background --factory-startup --python eval.py -- <cand.obj> <cand_samples> <target.obj> <out>`.
It uses only bundled numpy and mathutils, and computes:
| Metric | How | Role | Prior art |
|---|---|---|---|
| Chamfer, symmetric mean | 20k area-weighted target samples to the candidate BVH via `find_nearest`, plus candidate samples to the target BVH. Reported as % of the target bbox diagonal. | Headline | BlenderGym geometry metric; CAD-Recode and MeshCoder rank by it |
| F-score at 1% and 2% of the diagonal | From the same nearest distances (precision and recall at threshold tau) | Headline. Chamfer hides outliers and F-score does not. | Tatarchenko et al., CVPR 2019 (KNOWLEDGE) |
| Silhouette IoU per view | Workbench flat renders at 384 px. Masks for 6 axis views and 2 obliques. IoU per view and the mean. | Robust to the Tripo target's non-manifold edges and floaters | ShapeCraft and SEIG render loops; multi-view beat single-view (prior-art section 4) |
| Volumetric IoU | 64^3 voxels. Inside test by majority vote over ray parity on 3 axes. | Control hull only. Too unreliable on the non-manifold Tripo target to report. | MeshCoder and CAD-Recode report IoU |
| Brush count; mirror-pair count; kernel warnings | From the brush file and `csgeval` | Compactness and parametric quality | ShapeMOD, which minimises calls and free parameters |
Two target scopes are fixed in advance:
- **Main body:** face-connected parts at least 2% of hull length. This is the headline scope.
- **Full:** every part. Reported alongside.
The pre-registration is the point: the brush list should not be scored on the 8 greeble floaters,
and choosing the scope after seeing the numbers would invite rationalisation.
**Reference band.** BlenderGym's procedural-geometry task scored Chamfer 0.334 for humans against
1.120 for the best VLM (GPT-4-Turbo), a 3.4x gap. Those numbers are in BlenderGym's own units and
from 2024-25 models, so only the ratio carries over. The probe's own band:
- **Floor (B0):** one oriented bounding box, scored by the same verifier. It is computed once per hull.
- **Ceiling:** the control's truth list, which scores 0 by construction. On Quiet, the floor is the
self-Chamfer between the full and main-body scopes.
- **Human band:** optional (fork F4).
## 6. What the model sees each round
The model receives two contact sheets of about 1536x1152 px, roughly 2.4k image tokens each, plus
one text block.
1. **Views sheet.** Four rows (front -Y, side +X, top +Z, iso) by three columns: target shaded,
candidate shaded, and a silhouette diff. In the diff, target-only pixels are red, candidate-only
pixels are blue, and overlap is grey.
- Prior art: SceneCraft and SEIG render-critique-revise. ShapeCraft bbox and global views.
Multi-view conditioning beat single-view.
- The diff column is there because BlenderGym's verifier agreement was low (Claude 3.5 Sonnet
0.66 against human 0.79), so the model should not have to judge "close" unaided.
2. **Stations sheet.** Seven cross-sections at fixed stations along Y. Each is a front orthographic
slab render with clip start and end 1% of length apart, with target and candidate outlines
overlaid.
- This is naval architecture's own hull description, the station lines.
- It also gives the model the interior profile that silhouettes hide. Text-to-B-Rep and BenchCAD
name spatial misalignment and dropped features as the dominant failures.
3. **Text block.**
- The metrics table for this round and the best round so far.
- Per-view IoU.
- Brush count and kernel warnings.
- Round 0 adds the target bbox and the per-station width and height extents as numbers.
Per prior art, numeric feedback alone peaked at round 1 (2607.05573), while render critique kept
improving through rounds 2-3. Both are given. BenchCAD found the image "at best a clarifier" for
thinking models, so the numeric extents carry the measurements and the images carry the shape.
## 7. The loop
**Session.** One fresh Opus agent session per hull run. The brief contains:
- the operator's quote;
- the line-form grammar and its semantics: stream order, global intersect, obox Euler XYZ in degrees;
- the axis convention (nose -Y, starboard -X);
- the brush budget;
- the single evaluation command.
**Sight.** Eyes-only (fork F2). The agent may write `cand.txt`, run `eval.cmd`, which chains
`csgeval` and `eval.py`, and read the sheets and text the command emits. It may not open the target
mesh, the control truth file or the schematics. This restriction is a brief instruction, not
something enforced, so the transcript is audited afterwards for any read of target paths.
**Rounds.**
- Round 0 writes a first list from the target pack.
- Rounds 1 to 7 each revise the list, so there are at most 8 evaluations per run.
- The best round is the one with the lowest main-body Chamfer. A regression is kept in the log but
never replaces the best, as in CAD-Recode's min-Chamfer selection.
**Stop** at the first of these:
- the round cap is reached;
- main-body Chamfer improves less than 2% (relative) for two consecutive rounds;
- every success threshold is met.
**Held fixed across rounds and runs:**
- the camera rig, resolution and sample counts, with a fixed seed;
- the target scopes;
- the vocabulary;
- the brief text;
- the model, Opus 5.5;
- the brush budget: 40 for Quiet, 30 for the control, 80 for Headliner.
The budget forces description rather than voxelisation. A list of 500 tiny boxes is not parametric.
**Edit readout (one extra turn after the best round).** The model is asked to "lengthen the nose 20%
and widen the sponsons 10%". The readout is how many lines it touches and whether Chamfer to a
correspondingly scaled target stays within 1.5x of its best. That measures whether the description
is parametric in the operator's sense, editable intent, or only a fit.
**Runs.**
- Control x1.
- Dexter Quiet x2, because one run of a stochastic model is an anecdote.
- Headliner x1, only if Quiet reaches the partial band or better.
All runs go on Starfire, one at a time. Blender 5.2.2 is installed only there, and each evaluation is
a small background Workbench job.
## 8. Success and failure, fixed before running
The thresholds are judgment calls, written down now so the result cannot move them.
| Target | Pass | Partial (blockout-grade) | Fail |
|---|---|---|---|
| Control | Chamfer <= 1% diag, mean sil-IoU >= 0.95, vol-IoU >= 0.90, brushes <= 1.5x truth | Chamfer <= 3%, sil-IoU >= 0.85 | Worse than partial |
| Dexter Quiet (main body, best of 8, both runs) | Chamfer <= 2% diag, F@2% >= 0.80, mean sil-IoU >= 0.90, Chamfer <= 0.33 x B0 | sil-IoU 0.80-0.90 and Chamfer <= 0.5 x B0 | sil-IoU < 0.80, or Chamfer > 0.5 x B0 |
| Loop effect (any target) | Best-round Chamfer <= 0.7 x round-0 Chamfer | | Best is round 0 or within 10% of it |
What each outcome means for the operator's plan:
- **A. Control passes, Quiet passes.** The idea earns a campaign. The scope would be:
- Brokkr's Blender surface gains the harness's capabilities as typed commands: import mesh,
render views, evaluate against a target, apply a brush list.
- Geometry.Csg gains only the vocabulary gaps the tallies prove.
- The hull rebuild in M16 starts from the model's brush list, and the Tripo mesh stays the bake
source.
- **B. Control passes, Quiet partial or fails.** The model reasons well enough and the vocabulary or
the noise is the limit. The next probe arm is either one vocabulary extension from the tallies, or a
classical front end that hands the model primitive hints. RANSAC planes (Schnabel 2007) and the
discretisation step from InverseCSG are the candidates. Neither is built before this result.
- **C. Control fails.** Even in-vocabulary placement fails with this harness. Do not build Brokkr
features for this. Two alternatives remain:
- fitting without the model: InverseCSG-style search, or differentiable CSG refinement (UCSG-Net,
the fuzzy-Boolean paper);
- keeping the operator as the modeller, with the model editing her blockout instead of writing one.
- **D. Loop effect fails, whatever the absolute scores.** Render-compare is not buying anything. One
shot plus human editing is the shape, and a Brokkr render loop is not justified by this use.
- **Edit readout.** If the edit touches more than 5 lines or blows the 1.5x bound, the output is a
fit, not a description, even under outcome A. The campaign would then need structure the
line form lacks (named parts, mirror), and that must be decided before any build.
## 9. Cost
- **Harness (Hands, one bounded batch, cap of about 60 calls).**
- Copy `Quiet.blend` to scratch and export Dexter, Headliner and both main-body scopes to OBJ.
- Write the control truth list.
- Write `csgeval` (about 120 lines) and `eval.py` (about 250 lines) and `eval.cmd`.
- Compute B0 for each hull.
- Self-check: the control truth scores Chamfer about 0 and IoU 1; the OBB scores below truth.
- One cargo build of a small dependency set (bevy_math, serde, rmp-serde, sha2) into scratch. That
is a few hundred MB, deleted afterwards. This is the only compile.
- **Probe runs.** About 30 calls per run (setup, then about 3 per round over 8 rounds, plus the edit
readout).
- Four runs make about 120 calls. Headliner is conditional, so three runs make about 90.
- Each round adds roughly 5k image tokens, so context peaks around 60-80k tokens per run. Total input
is about 1.5-2M tokens per run, mostly cache reads.
- **Blender.** About 35 evaluations across all runs plus about 5 harness checks. Each is 15-40 s of
background Workbench work. They run serially on Starfire, never two at once and never alongside
another heavy job, for about 20 minutes of Blender time in all.
- **Total.** About 150-190 agent calls and one session-day of wall clock. Self reads the results;
Soul is not needed for a probe that admits nothing.
## 10. What not to build
- No Brokkr changes: no new commands, no render surface, no MCP server install. The Blender Lab MCP
is GPL-3.0 and unauthenticated, and background scripts answer the question without it.
- No service, daemon, Eve surface or CultCache persistence. The probe's state is files in scratch.
- No new crate or package in any repo, and no commits to Geometry.Csg. `csgeval` is a scratch binary
with a path dependency.
- No grammar beyond the line form: no mirror, array or bevel constructs. Their absence is measured,
not fixed.
- No RANSAC front end, differentiable optimiser or point-cloud encoder in v1.
- No writes to `C:\Users\Meta\Desktop\Quiet.blend` or anything under `F:\Projects\Aetheria`.
- The VibeGeometry scar applies directly. That project "dived almost immediately into CSG grammar,
parity fixtures, native bridge probing, mesh generation, and performance work" before measuring
whether the authoring loop worked. This probe measures first. Infrastructure is earned by outcome A
and scoped by the tallies.
Kept afterwards: one result document. It holds the metrics table per run and round, the best sheets,
the brush lists, the vocabulary tallies, the transcript-audit line, and which outcome A-D held. Its
home is suggested below.
## 11. Forks for the operator (questions, with recommendations; not admitted)
**F1. Evaluator: Geometry.Csg's own kernel, or Blender's exact booleans?**
- (a) Scratch `csgeval` on the real kernel. This needs one small cargo build.
- (b) `eval.py` builds the boxes in Blender with exact boolean modifiers. No Rust at all.
Recommend (a), confidence moderate. The question is whether the Geometry.Csg brush list can be the
durable design surface, and only its own kernel and field prove that the list meshes. Kernel faults
that turn up are findings, not noise. (b) answers "can Opus box-model this", which is a weaker
question.
**F2. What the probe agent may see.**
- (a) Eyes-only: renders, sections and scores.
- (b) Full tool access, so the agent may script numeric analysis of the target mesh: sections, plane
fits, its own optimiser.
Recommend (a) first, confidence high, because the operator asked about Opus's spatial reasoning.
(b) answers "can Opus write a fitter", which is a different and probably easier question. It is a
cheap second arm if (a) lands in B or C.
**F3. Vocabulary: boxes and oriented boxes only, or include CylinderZ, DomeCapZ and FloretArm?**
- (a) Boxes only.
- (b) All five, with the curved ones additive-only, since the kernel skips cuts by them.
Recommend (a), confidence moderate. In (b), a cut by a curved primitive is silently skipped, which
corrupts the comparison. The cylinder demand is tallied instead.
**F4. Human reference band.** Should the operator spend about 30 minutes box-modelling Dexter Quiet
by hand in Blender, scored by the same verifier?
Recommend no for v1. Ask only if Quiet lands on the partial and pass boundary, where a human number
would decide the outcome.
**F5. Brokkr in the path now?**
- (a) No Brokkr. Plain background scripts.
- (b) First give Brokkr's Blender adapter render, import and evaluate commands, then probe through it.
Recommend (a), confidence high. Brokkr lacks every capability the loop needs, and building them first
repeats VibeGeometry's order. Outcome A makes the harness the spec for those commands.
**F6. Targets and runs.** Control x1, Dexter Quiet x2, Headliner x1 only on a partial or better.
Alternative: Quiet x1 to halve the cost.
Recommend x2. With n = 1, a pass or fail is indistinguishable from sampling luck.
Suggested home for the result document (Self decides): `F:\Projects\Brokkr\docs\research\parametric-hull-probe.md`,
beside the capability bar. If outcome A holds, the result steers Brokkr's Blender surface. If B or C
holds, it steers Aetheria's hull rebuild instead, and a pointer belongs in the release map.

View file

@ -1,226 +0,0 @@
# 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/<X>.v0-4.png`, `out/<X>.w0,w2,w4.png` and `out/<X>.uv.png`. Metrics are in
`out/<X>.metrics.json`, scores in `out/<X>.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 <name>`, `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.

View file

@ -1,248 +0,0 @@
# Mesh probe run 3: Dexter Quiet as one Catmull-Clark body
Agent `probe-subd-cage-quiet`, session `self-2026-10-10-ag2`, 2026-10-11. Disposable scratch; no repo was edited.
Root: this directory (`...\scratchpad\mesh-probe\run3-subd\`). Blender 5.2.2, `--background` only, bundled numpy
and mathutils only. No downloads, no pip, no closed tools.
Serves brush-tree:ruling:representation-per-hull, :no-unownable-tools-in-loop and :shippable-part-bar.
## What I built
**One closed Catmull-Clark cage** (`tools/build.py`, about 330 lines), mirror-symmetric, fitted to the run-1
target (`run1/targets/dexter_main.obj`). It has three parts.
1. **Designed topology. This is the part no prior art automates.** It is a "pillow" of a top sheet and a
bottom sheet, joined by a rim band two faces tall. Each sheet has:
- A **spine strip** along the centreline, 2 x 16 quads.
- **5 concentric rings** from the spine out to the outline. Their loops run parallel to the manta outline:
beak side, leading edge, wing tip, trailing scallop, stinger.
- **Spokes** (the radial edges) from the spine out to the outline. These carry the wing sweep into the
body.
The outline is 18 edges per half: beak 2, leading edge 8, tip 1, scallop 4, stinger 3. It comes from run
1's planform numbers plus the scallop circle.
The only poles in the cage are the 4 strip corners per sheet plus the inset corners. The rim band is
creased 1.0 along the wing stretch only; the beak and stinger stay smooth.
2. **The fused mechanical sweep: a canopy frame.**
- The region is chosen by topology, not by position: spine rows 11-15 plus rings 1-2 beside them.
- It is inset twice, with creases of 0.5 and 0.6.
- The frame loop is raised 6 mm after the fit.
- It carries a 20x local shape regulariser, so the fit cannot crumple it.
The belly keel and side lobes come out of the fit on the bottom sheet; I modelled no separate parts for
them. I did not model the chin pods, dorsal vents, wing strakes or the tail nozzle (see "Where it falls
short").
3. **Least-squares fit of cage positions.** This is my own, simpler version of the Marinov and Kobbelt
approach:
- The level-2 limit-surface vertices are linear in the cage, `X = S c`. I extract `S` exactly by probing
Blender's Subdivision modifier with unit vectors, 2 s.
- Each iteration: point-to-plane from the surface to the target, with a 0.15 point-to-point term; a
coverage term from the target to the surface with normal-compatible correspondences; a fairness term
‖Lc‖²; and a shape term ‖L(c − c₀)‖² that keeps the designed layout.
- The solve is in a mirror-reduced space: 843 unknowns for a 542-vertex cage. 30 iterations take about
55 s.
**Lowering** (same script):
- Seams go on the cage: the rim's middle loop (top / bottom) and the canopy glass loop. The seam flags
propagate through the subdivision.
- Apply subdivision at the ship level, then Minimum Stretch (SLIM) unwrap, average island scale, pack.
- Bevel, 1 segment, angle 30°, width 0.004, then Weighted Normal (face area, keep sharp). The stack is the
same as run 2's B and C.
**Final variant `v7`:** ship level 2. `v7L1` is the same cage at ship level 1. `v7e` is the edit test.
## Contact sheets (orange squares = row number)
- **`sheet-main.png`**, the best one to judge at a glance. Rows: 1 Tripo target, 2 run 2's B (bmesh loft +
boolean), 3 v7 (level 2), 4 v7L1 (level 1). Columns: 3/4 front, side, 3/4 rear, wireframe overlay, UV
layout. The target has no UVs, so its tile is black.
- `sheet-closeup.png`: the wing edge and wing-body junction (shaded, then wireframe), then a top view with
wireframe. Rows: T, B, v7, v7L1.
- **`sheet-cage.png`**: the cage I designed (orange) over its limit surface. Views: 3/4, top, side, bottom.
- `sheet-edit.png`: v7 against v7e (wider sweep plus longer beak). Columns: 3/4, top, top with cage, side.
- `sheet-under.png`: bottom and 3/4 front-high. Rows: T, v7.
- Per-variant frames are in `out/<name>.v0-6.png`, `.w0/.w2/.w4.png` and `.c0/.c1/.c2/.c5.png` (cage).
Metrics are in `.metrics.json`, scores in `.score.txt`, build info in `.build.json` and the .blend files
in `out/`.
- Renders use run 2's look. An earlier render pass shaded the live cage limit surface over the hull (it
z-fought); that is fixed, and the sheets above are the clean re-renders.
## Measurements (same measure.py as run 2)
| | B (run 2) | **v7: subd, level 2** | v7L1: subd, level 1 |
|---|---|---|---|
| Faces: quads / tris / ngons | 3039 / 49 / 215 | 9297 / 196 / 180 | 2533 / 158 / 130 |
| Largest ngon | 11 (boolean seam) | 6 (bevel corners) | 6 |
| Shells | 4 | **1** | 1 |
| Poles per 1000 verts | 100.1 | **47.1** | 108.7 |
| Poles of valence 6 or more | 0 | 0 | 0 |
| Longest loop (edges) | 86, open | **148, closed** | 62 |
| Closed loops ≥ 20 edges | 41 | 30 | 3 |
| Share of edges in loops ≥ 20 | 0.528 | **0.755** | 0.417 |
| Exported triangles | 6914 | 19340 | **5624** |
| Sliver tris < 10° (share) | 31% | 24% | 33% |
| …excluding faces of bevel width | 336 | 254 | 340 |
| Dihedral > 60° / > 80° left after bevel | **83 / 7** | 546 / 316 | 352 / 223 |
| UV islands: total / ≥ 1% of area / crumbs | 23 / 11 / 2 | **3 / 3 / 0** | 3 / 3 / 0 |
| UV area distortion, mean \|log2\| | 0.113 | **0.076** | 0.078 |
| UV area outside 0.5-2x | 0.7% | 0.8% | 0.9% |
| UV angle error, mean | **1.04°** | 2.61° | 3.04° |
| UV space wasted | 47% | 59% | 60% |
| Chamfer % diag / F@2% / sil-IoU | 0.852 / 0.898 / 0.920 | **0.228 / 0.990 / 0.967** | 0.233 / 0.990 / 0.965 |
- **Scorer.** Run 1's `mp` binary was deleted, and rebuilding it means a cargo build against
GameCult.Geometry.Csg. I ported `mp score` to numpy instead (`tools/score.py`, same sample counts, views
and τ). It scores B at 0.852 / 0.898 / 0.920; `mp` gave 0.841 / 0.899 / 0.920. The B column above is
rescored with the port, so the comparison is like for like.
- **Cage.** 542 vertices, 540 faces, 118 creased edges. The free parameters are 843 coordinates from 301
half-cage vertices; the fitter owns those numbers.
- **What a designer touches.**
- Topology knobs: L, K, the 5 outline segment counts, strip width and extent, canopy rows, rings and front.
- Look knobs: wing crease, 2 canopy creases, 2 inset widths, frame height, bevel width and angle, ship level.
- Fit weights: lam, lam_s, canopy_reg.
That is about 20 numbers, plus the 25-point outline polyline.
- **Overfitting is real.** The first fit, v1, had weak regularisation. It scored chamfer **0.128** / IoU
0.979 but tangled the canopy and rippled the wings (`out/v1.v0.png`). v7 trades about 0.1% chamfer for
clean surfaces. Fidelity is cheap with this method; cleanliness is what costs.
## Against her bar
- **Topology flow: met, and better than B or C, on the body and wings.**
- It is one shell; nothing is booleaned, so there is no boolean seam.
- Loops run unbroken around the whole outline (closed loops of 148 edges).
- Pole density is half of B's at level 2.
- Missed in three places:
- The concentric rings crowd into the narrow beak and stinger, because a ring cannot end without a pole.
- The stinger's tall walls have only the two-face rim band, so they fold.
- The thin wing edge is thinner than its band, so the band folds there too.
- Those three account for most of the 316 edges over 80°. The breakdown is from `tools/dbg_edges.py`:
stinger about 100, wing outline about 160, canopy about 36, beak and tip about 20. A bevel can't fix a
fold of nearly 180°.
- **UVs: met.**
- 3 islands: top (with the canopy frame), bottom, canopy glass. The parts stay together and there are no
crumbs.
- Area distortion 0.076 is better than B's.
- Angle error is 2.6° against B's 1.0°. That is the SLIM trade-off: angle-based unwrap of the same seams
gave area 0.63, which is unusable.
- Packing is plain and wastes 59%.
- **Hard edges (one-segment bevel + face-weighted normals):** the stack is in place, but partly met.
- The creased wing band comes out as a soft rounded edge with a highlight. It does not read as the crisp
single-face bevel in her test render (closeup row 3).
- Where the crease meets the folds, the bevel can't resolve the angle.
- Her look needs a deliberate edge design: a band of chosen thickness with support loops, or a knife edge
with the crease on the middle loop. I did not get there.
- **Triangle budget.** Level 2 exports 19.3k triangles, 2.8x B. Level 1 exports 5.6k, under B, at almost the
same fidelity (0.233), but its flow metrics fall to B's level. A per-region level, or level 1 plus
normal-map baking, is the real shipping question.
## Editability (v7 to v7e: wing sweep 0.06 plus beak 0.05)
- **The edit is 2 numbers.** Each is a smooth falloff field applied to the fitted cage.
- **It moves 315 of the 542 cage vertices.** No hand-dragging is needed.
- **The cage topology is identical, and the lowered topology stays stable:**
- faces 9673 to 9686 (the angle-limited bevel picks up 13 more);
- islands 3 to 3;
- poles 456 to 452;
- longest loop 148 to 202, because the stretched wing loop now crosses the loop-length cut-off differently.
- **Rerun cost.** About 60 s with the refit, then lowering. An edit-only rerun without the refit would take a
few seconds; I did not time one separately.
- **The limit.** An edit is a deformation of fitted numbers. Moving a feature (a canopy further back, a new
strake) is a topology edit: change knobs, rebuild and refit.
## What worked
- Exact `S` by probing the modifier (2 s, error 1.5e-7). It turns Blender's OpenSubdiv into an owned,
linear fitting kernel, with creases included.
- Concentric pillow topology for a manta. One coherent body with no booleans, which B and C could not
achieve.
- Selecting feature regions by topology index instead of by position. The position-picked canopy came out
jagged and crumpled (v1-v4); the topology-picked one is clean (v5+).
- The shape regulariser ‖L(c − c₀)‖², weighted up locally on the mechanical feature. It keeps designed
geometry designed while the rest of the cage fits.
- Seams authored on the cage propagate through the subdivision. Those seams plus SLIM give 3 clean islands.
## Where the cage design was hard
- **Concentric rings carry a constant loop count everywhere.** The wing tip gets about 9 cm spacing; the beak
and stinger get 1-2 cm. A proper cage terminates rings (3-to-1 or 2-to-1 poles) where the body narrows,
or uses a separate tail-boom tube. I tried neither.
- **Feature insets on a fitted surface.**
- `bmesh.ops.inset_region` with even offset fired spikes off the crowded beak (v0 to v2).
- Thin insets crumple under the fit unless they are regularised.
- The canopy reads as a recessed slot with a crisp rear arc, not the target's raised bubble with a framed
rim. Getting there needs a glass-crown height knob and support loops, and more iteration than I had.
- **Fidelity against fairness.** The target is Tripo output with lumpy panel detail. A coarse cage that
chases it ripples. The fairness weights are the most consequential numbers in the file, and nobody
designs them.
- **Mechanical detail at this scale** (pods, vents, strakes, nozzle) does not belong in this cage. It
belongs in either support-loop sub-features (more cage design) or separate bevelled primitives and decals
(the mixed-hull path from the prior-art doc). I did neither.
- **Pitfalls I hit:**
- A Python late-binding closure bug.
- Dense numpy matmuls in Blender's numpy were orders of magnitude slow. I worked around it with a sparse
bincount assembly; I never checked whether the bundled numpy has an optimised BLAS.
- Twice I started a job while another agent's Blender (PIDs 58156, 34064) was running. After that,
`run.sh` waits for other Blender jobs.
## What a future brokkr-ctl command would have saved
- **`cage new --pillow --outline <poly> --spine L --rings K --segments …`.** Generates the concentric pillow
cage with its index maps (spine, ring, spoke, rim), so regions are addressed by name. About 40% of my
effort.
- **`cage region <name> --rows a..b --rings 1..k` plus `cage inset/crease/seam`.** Topology-addressed
features, which never fold the way position-picked ones do.
- **`cage fit --target X --level 2 --fair λ --keep λs --lock <region>`.** The `S` probe, the sparse solve
and the mirror reduction, with per-region locks. Iterations would drop from minutes of script surgery to
one flag.
- **`cage terminate-ring` / `cage reroute`.** Pole-placing ring terminations, for the beak and stinger
crowding. This is the operation I most needed and did not have.
- **`lower --level N --bevel … --uv slim --measure --score --sheet`.** Run 2 and run 3's measure, score,
render and sheet scripts as one call, with the cage hidden in shaded renders by default.
## Verdict
**Can this path produce a hull she would ship? Yes, I think so, but this cage is not that hull yet.** Of the
three paths probed, subdivision is the first whose structure matches her bar:
- one continuous body;
- loops that follow the manta's curves;
- 3 part-coherent islands with low area stretch;
- and, from the same fit, fidelity 3.7x closer to the target than run 2's lowerings (chamfer 0.23 against
0.85).
**What is missing is cage design**, which is exactly where prior art is silent:
- ring terminations at the beak and stinger;
- a deliberately designed wing edge, so her one-segment bevel reads crisp instead of folding;
- a canopy with crown and frame support loops;
- the pods, strakes and nozzle as support-loop features or separate bevelled parts.
Each is a known modeller's move. None is automated, and each needs her eye in the loop.
Confidence:
- **Moderate** that the representation can meet her bar.
- **Low** that an agent reaches her standard on this hull without the cage tooling listed above, or without
her reviewing cage topology at the sheet level.
The renders were judged by my eye, not hers.
## Caveats
- **One hull, one target, one topology family.** I did not try a tube-plus-wing cage, a quad-remesher seed
or ring terminations.
- **No retopology by a human** for comparison. Her studio scene, texture baking, decals and engine export
were not tested.
- **Scores come from the numpy port** of `mp score`; it matches `mp` to within 0.011 on B.
- **Fit distances in `.build.json` are proxies**: level-2 vertex distances, not exact limit-surface
closest points.
- **Processes.** No processes of mine are running. The operator's GUI Blender (PID 36324) was never touched.
Nothing outside this scratch directory was written. `F:\Projects\Aetheria` and the brush-tree worktree
were only read, and only the research docs.
- **Tools** (all in `tools/`):
- `build.py`: cage, fit, edit, lowering. Use `CAGE_OVERRIDES` for knobs.
- `run.sh build|post|render <name> [json]`: one Blender job at a time; waits for other Blender jobs.
- `measure.py`, `render.py`, `sheet.py`, `desc.py`: copied from run 2. `render.py` gained bottom and
front-high views, cage views, and the cage hidden in shaded renders.
- `score.py`: the `mp` port.
- `study.py`: target height fields and samples, written to `out/target.npz`.
- `dbg_edges.py`: the location of the leftover hard edges.

View file

@ -1,175 +0,0 @@
# Mesh probe run 4: SDF with smooth-min for the shape, a field-aligned cage for the topology
Agent `spike-sdf-smin-cage`, session `self-2026-10-10-ag2`, 2026-10-11. Disposable scratch; no repo edited.
Root: this directory (`...\scratchpad\mesh-probe\run4-sdf-cage\`). Ruling: `brush-tree:ruling:spike-sdf-smin-to-cage`.
## Verdict
- **SDF for the shape works, and fusion costs nothing.** One program (`tools/sdf_quiet.py`, about 130 lines, 63 fitted
numbers) fuses the body loft, wing, pods and chin fin with named smin radii. Fitted, it scores chamfer 0.520 % / F@2 % 0.949 / sil-IoU 0.895.
- Chamfer is better than run 2's description (0.84).
- Silhouette is worse than the unfitted start (0.934).
- It is not at run 3's hand cage (0.19-0.23 / 0.967).
- **Automatic field-aligned topology did not reach a cage.** Here is how far each step got:
- The cross field is good. It uses analytic curvature, the program's exact crease and Ohtake-style ridges.
- My numpy Instant-Meshes position field and extraction break. Result: 463 faces, of which 102 are quads, with 189 boundary edges (holes).
- Blender's QuadriFlow refuses to run headless on every input, including a verified-manifold voxel remesh.
- **The Catmull-Clark refit of a cage to the field works** (limit |f| mean 0.0016, chamfer 0.599 to 0.552). It is the
reusable piece. But it needs a cage with sane topology, and this spike could not make one automatically.
- **The shippable lowering fails her bar.** The cause is upstream: the cage is broken. Measured anyway, see below.
- Plain answer: "SDF for the shape, fitted cage for the topology" is viable only if the cage comes from a designed layout
(run 3's approach) or from a real global parametrisation (MIQ or the full Instant Meshes hierarchy and extraction). The local
Jacobi version I could build in the budget does not do it.
## 1. The SDF program and its fidelity
`tools/sdf_quiet.py`:
- `hull = smin(smin(smin(body, wing, k_wing), pods, k_pod), chin_fin, k_fin)`.
- body: superellipse stations with Catmull-Rom between them.
- wing: a slab on z = a + b|x| + c·y, tapered, cut by the planform polygon minus the scallops (IQ sdPolygon).
- The wing edge is `max(slab, planform)` with `k_edge = 0`, a deliberate hard edge.
- Start values: run 2's description numbers. The fit is Levenberg-Marquardt over the named parameters (`tools/fit.py`):
- residuals: f(p) at 9.7k outer-shell target points, from ray first-hits in 96 directions, so cockpit internals are ignored;
- a hinge on 15k ray-derived "outside" labels;
- a small prior;
- box bounds.
Scores use `tools/common.py`, a numpy/BVH port of run 1's `mp score` (the mp build dir was deleted). It is calibrated on run 2's B.stl: 0.852 / 0.898 / 0.920 here against mp's 0.841 / 0.899 / 0.920.
| Program state | Chamfer % diag | F@2% | Mean sil-IoU | Note |
|---|---|---|---|---|
| p0: run 2's numbers as an SDF (k_wing 0.06) | 0.897 | 0.910 | 0.934 | Smooth fusion alone already beats run 2's silhouette (0.920) |
| LM, unbounded (`out/fit.log`) | 1.348 | 0.840 | 0.798 | Degenerate: self-crossing planform (x = -1.99), negative pod radii, taper -5.5. The fit exploits the program |
| **LM, bounded + prior (`out/params_fit.json`)** | **0.520** | **0.949** | **0.895** | t2c 0.586, c2t 0.454 |
The bounded fit presses on its bounds:
- wing_t is at its 0.08 cap and k_wing at its 0.12 cap;
- a pod coordinate is at its bound;
- the beak station came out inverted (z_bottom above z_top, half-width 0.084), which leaves a flat cap at the nose.
The program lacks structure the target has: canopy, ridges, a wing thicker at the root. The optimiser compensates with blend radius and thickness. The program is still readable, but the fitted values are partly a fit, not a description (run 1's finding again). Zero-set render: row 2 of `sheet-main.png`. The wing edge shows surface-nets staircase, because a hard `max()` edge is not feature-preserved by surface nets. The nose is ragged where the inverted station sits.
## 2. Cross field and quad meshing: how far it got
Pipeline (`tools/quad.py`, `tools/qmesh.py`):
1. Surface nets at h = 0.008, 27,660 vertices, Newton-projected onto f = 0. It has 64 non-manifold edges where the wing is under 3 cells thick.
2. Principal directions and magnitudes come from the program's Hessian, by central differences of the analytic field, not mesh estimates.
3. Ridges: the derivative of |kmax| along dmax, a third-order quantity, crosses zero at strong |kmax| (Ohtake et al. 2004). This found 1,678 vertices.
4. The exact crease comes from the program's structure: the tangent is cross(∇slab, ∇planform) where both are about 0 on the wing part. This found 912 vertices.
5. Flat and umbilic rule: alignment weight = (anisotropy / p90)², so flat or umbilic regions get no curvature constraint and are filled by smoothing from features and curved regions. Ridges and creases are near-hard constraints (weight 5).
6. 4-RoSy smoothing.
- Representation: power vectors z = e^{4iθ}, with Rodrigues (minimal-rotation) transport between tangent planes. This is the Palacios-Zhang / Ray et al. / Knöppel et al. representation.
- Method: Jacobi.
- Why not IM matching: Instant Meshes' per-edge 4-way matching under Jacobi stalled at 1,500 singular faces on the coarse test. Plain projection transport left 404 false singularities on the wing edge; Rodrigues fixed that.
7. Position field: the Instant Meshes 4-PoSy in numpy.
- Lattice origins start random, because p = V is a harmonic phase ramp that Jacobi never removes.
- Each edge uses the minimal lattice image.
- A voxel-cluster hierarchy of 2-3 levels, capped at s/2.
8. Extraction: union-find collapse of zero-offset edges, unit-offset edges become lattice edges, planar face tracing.
9. Catmull-Clark refit (`tools/cc.py`): CC is built as an explicit linear operator. Cage positions are least-squares fitted so the limit points equal their own Newton projections (8 rounds).
Higher-order constraints against plain Instant-Meshes-style smoothing (same mesh and solver, alignment term off: `quad.py ... plain`):
| | Field (curvature + ridge + crease) | Plain IM smoothing |
|---|---|---|
| Misalignment to principal dir (aniso conf > 0.5), median / p90 | **0.3° / 2.1°** | 27.8° / 40.8° |
| Misalignment to the wing-edge crease, median / p90 | **0.1° / 0.7°** | 17.9° / 33.0° |
| Singular faces (±1 / ±2 index) | 220 (196 / 24) | 185 (164 / 21) |
| Extraction at s = 0.035: faces / quads / tris / untraced cycles / boundary edges | 463 / 102 / 342 / 129 / 189 | 444 / 107 / 315 / 159 / 252 |
| Same mesh as a CC cage after refit: chamfer / sil | 0.552 / 0.883 | 0.574 / 0.873 |
| Extraction at s = 0.07 (cage density): faces / quads | 59 / 17 | 47 / 9 |
What the higher-order information bought:
- An exactly aligned field on every feature and curved region.
- 35 more singularities, most of them clustered at the ragged nose, where the fit is bad and the constraints conflict.
- Downstream, slightly fewer holes and slightly better refit fidelity. That gain is swamped, because the position field and extraction break either way.
Pictures:
- `out/X.top.png`, `out/X.q34.png` and `out/X.close.png`: field mode. Orange crosses, red singular faces, blue lines for the IM lattice edges that survived.
- `out/Xplain.*.png`: plain mode.
Where it broke:
- **The position field does not converge globally.** On a flat 60x60 grid test, the lattice phase still drifts (a smooth residual ramp, mean minimal offset 0.025 s). The extraction then splits or merges lattice nodes and produces tris and holes: 40 faces, of which 12 are quads, where about 49 perfect quads were expected. In the paper the hierarchy and its Gauss-Seidel solver are what make the solve global. My voxel-cluster hierarchy is shallow: coarser than s/2 makes the minimal image ambiguous. The paper's extraction also has a cleanup stage I did not build.
- **The zero set has a hard wing edge thinner than the lattice.** Wing thickness is 0.03-0.08 at s = 0.035-0.07, so the top and bottom lattices of the wing interfere.
- **QuadriFlow, Blender's built-in, does not run headless.** It returned `CANCELLED` with "needs to be manifold and have face normals that point in a consistent direction" on:
- the surface-nets mesh (64 non-manifold edges);
- a voxel remesh (0 non-manifold edges and 0 non-manifold verts by bmesh, normals made consistent, loose geometry deleted, degenerates dissolved), with and without Newton projection.
Run 2 hit the same refusal. I did not try the GUI.
- **Not done:** an anisotropic target edge length (finer where |k| is high). The position field uses one global s.
## 3-4. Shippable lowering, measured as run 2 did
There was no automatic cage worth lowering, so I lowered the best automatic output to show where it lands: the field-mode dense IM mesh after CC refit (`out/qm_dense_refit.obj`).
- 154 cage edges with dihedral over 50° were creased.
- Then Subdivision Surface (2 levels, applied) and a Newton snap onto f = 0.
- Seams at the program's part-label boundaries (body / wing / pods / fin from argmin of the part fields), at hard edges and at the mirror plane.
- Angle-based unwrap, averaged scale, pack.
- Bevel 1 segment at 30°, width 0.004, then Weighted Normal (face area, keep sharp).
- Script: `tools/lower.py`. Measured with run 2's `measure.py`, unchanged.
| | **L (run 4: SDF + IM cage)** | run 2 B (bmesh) | run 2 C (geonodes) | run 3 v6 (hand cage, interim, no RESULT yet) |
|---|---|---|---|---|
| Faces quads / tris / ngons | 6662 / 176 / 231 | 3039 / 49 / 215 | 3979 / 127 / 229 | 9395 / 210 / 182 |
| Poles per 1000 verts / valence ≥ 6 | **140.3 / 60** | 100.1 / 0 | 98.0 / 4 | 49.3 / n/a |
| Longest loop / closed loops ≥ 20 / share of edges in loops ≥ 20 | 124 (open) / 7 / 0.243 | 86 / 41 / 0.528 | 35 / 57 / 0.605 | 148 / n/a / 0.753 |
| Boundary edges / shells | **846 / 4 (holes)** | 0 / 4 | 0 / 4 | n/a / 1 |
| Exported tris / sliver fraction / slivers excluding bevel strips | 14236 / 17.6 % / 205 | 6914 / 31 % / 336 | 9022 / 28 % / 157 | 19558 / 25 % / 256 |
| Dihedral > 60° left | 751 | 83 | 77 | n/a |
| UV islands total / ≥ 1 % area / under 4 faces | **182 / 16 / 83** | 23 / 11 / 2 | 21 / 11 / 2 | 3 / 3 / n/a |
| UV area distortion (mean \|log2\|) / angle error | 0.189 / 0.74° | 0.113 / 1.04° | 0.118 / 1.14° | 0.631 / 0.97° |
| UV wasted | 51 % | 47 % | 61 % | n/a |
| Chamfer / F@2% / sil-IoU | 0.531 / 0.950 / 0.875 | 0.841 / 0.899 / 0.920 | 0.840 / 0.900 / 0.920 | 0.229 / 0.989 / 0.967 |
Against her bar, L fails every topology and UV criterion:
- holes and torn shards (visible in row 3 of the sheet);
- 140 poles per 1000 verts;
- 182 UV islands, 83 of them crumbs.
Its high quad share is only Catmull-Clark turning every face into quads; the poles and holes show the real topology. Its chamfer (0.531) beats run 2 because the SDF fit does. Silhouette does not. Run 3's interim hand cage beats everything here on every axis except the UV distortion figure.
The bevel and normals stack runs, but on this geometry the 154 creased cage edges include spurious ones from the wrong-shaped faces. That is why 751 edges over 60° are left.
## Contact sheets (run 2's layout)
- **`sheet-main.png`**. Rows: Tripo target, **SDF zero set (fitted program)**, L, run 2 B, run 2 C. Columns: 3/4 front, side, 3/4 rear, wireframe, UV layout.
- `sheet-closeup.png`. Columns: wing-edge and junction close-up, its wire, and the top wire. Same rows.
- Cross field: **`out/X.q34.png`** (best single view of the field), `out/X.top.png`, `out/X.close.png`; plain: `out/Xplain.*`.
- Per-row frames: `out/<Z|L>.v0-4.png` and `out/<Z|L>.w*.png`; `out/L.uv.png`; `out/L.metrics.json`; `out/L.score.txt`; `out/L.blend`.
## Sources relied on
All of these were used from my own knowledge of the papers and public formulas. Nothing was downloaded, and no third-party code was copied; every algorithm here was re-implemented in numpy. The licences are as I recall them; verify before relying on them.
- **Instant Meshes.** W. Jakob, M. Tarini, D. Panozzo, O. Sorkine-Hornung, "Instant Field-Aligned Meshes", ACM TOG 34(6), SIGGRAPH Asia 2015.
- Used: the extrinsic orientation and position fields, random lattice initialisation, hierarchy, and extraction by edge-offset classification.
- Reference code: github.com/wjakob/instant-meshes, BSD 3-clause.
- **QuadriFlow.** J. Huang, Y. Zhou, M. Niessner, J. R. Shewchuk, L. J. Guibas, "QuadriFlow: A Scalable and Robust Method for Quadrangulation", Computer Graphics Forum 37(5), SGP 2018.
- Code: github.com/hjwdzh/QuadriFlow, BSD-style; built into Blender (GPL) as `object.quadriflow_remesh`. It was tested and refused headless.
- **N-RoSy power-vector representation.**
- J. Palacios, E. Zhang, "Rotational Symmetry Field Design on Surfaces", SIGGRAPH 2007.
- N. Ray, B. Vallet, W.-C. Li, B. Lévy, "N-Symmetry Direction Field Design", ACM TOG 2008.
- F. Knöppel, K. Crane, U. Pinkall, P. Schröder, "Globally Optimal Direction Fields", SIGGRAPH 2013. That paper's global eigen-solve was not done; I used Jacobi.
- **Ridges.** Y. Ohtake, A. Belyaev, H.-P. Seidel, "Ridge-valley Lines on Meshes via Implicit Surface Fitting", SIGGRAPH 2004.
- **Not implemented, the named next step.** D. Bommes, H. Zimmer, L. Kobbelt, "Mixed-Integer Quadrangulation", SIGGRAPH 2009. Open implementation: libigl `igl::copyleft::comiso::miq` (libigl is MPL2; CoMISo is GPL, hence "copyleft").
- **Surface nets.** S. Gibson, "Constrained Elastic Surface Nets", MICCAI 1998.
- **Feature-preserving alternative, not used.** T. Ju, F. Losasso, S. Schaefer, J. Warren, "Dual Contouring of Hermite Data", SIGGRAPH 2002.
- **Subdivision.** E. Catmull, J. Clark, "Recursively generated B-spline surfaces on arbitrary topological meshes", CAD 1978.
- **SDF primitives and smooth-min.** Inigo Quilez, iquilezles.org articles "distfunctions", "smin" and "distfunctions2d" (sdPolygon). These are published formulas, re-implemented.
## What the next spike would need
1. **A global integer-grid parametrisation** instead of local Jacobi. The options are MIQ (libigl, MPL2 plus GPL CoMISo) or a faithful port of Instant Meshes' Gauss-Seidel hierarchy and its extraction cleanup (BSD). Either is open code we could own. The cross field from this spike (analytic curvature, ridges, exact crease) is the input it needs, and that part works.
2. **Feature-aware treatment of the hard wing edge.** Either give the program `k_edge > 0` and crease it later, or extract with dual contouring. A sub-lattice-thin wing also needs the parametrisation to treat top and bottom as separate charts cut at the crease.
3. **A seeded cage.** Run 3's evidence plus this one suggests the practical hybrid: a designed cage layout (run 3) whose vertex positions this spike's CC-to-field least-squares refit sets automatically. That keeps her topology and gets the field's fidelity. It is one small spike, and `tools/cc.py` and the `refit` in `tools/quad.py` / `tools/qf.py` are ready for it.
4. **A better-structured program.** Add a canopy, a root-thickened wing, and a nose that is not a station clamp, so the fit stops pressing on its bounds.
## Process notes
- Blender jobs ran one at a time behind a wait loop on other agents' Blender PIDs. There were three overlaps:
- My first fit started before probe-subd-cage's job appeared.
- A 3-second QuadriFlow retry ran while another agent's Blender (PID 58336) was up.
- A numpy test via Blender's bundled python overlapped my own fit job; I killed that test (PID 40264).
- One job of mine (PID 55532) ran away to 14 GB, because it built a dense Catmull-Clark operator for a 27k-vertex mesh after QuadriFlow returned its input unchanged. I killed it within minutes. `qf.py` now refits only meshes under 2,500 vertices.
- No processes of mine are running. The operator GUI Blender (PID 36324) was never touched.
- Tools: `tools/` (common, sdf_quiet, fit, sn, qmesh, cc, quad, qf, lower, viz, score, chain.sh; measure/render/sheet copied from run 2). Logs: `out/*.log`.

View file

@ -1,46 +0,0 @@
# Run 5: Blender 5.2.2 QuadriFlow, headless, on Dexter Quiet's SDF zero set
## Why it "refused"
Not headless, not context. The operator's exec path runs blocking when called from Python (`OP_IS_INVOKE` unset), poll needs only an active editable
mesh object in object mode, and no window/area is used. An ico sphere and a UV sphere remesh fine with `--background --factory-startup` and no
temp_override (`tools/diag.py`: both FINISHED).
The refusal text comes from `mesh_is_manifold_consistent` (object_remesh.cc). Besides edge-with-3-faces, flipped winding and wire edges, it
rejects **any edge shorter than 1e-4 (compare_v3v3)**. Run 2/4's voxel remesh (voxel 0.005, Newton-projected) had 20 edges of 1e-5..1e-4 although
bmesh called it manifold and consistent. The message is the same for all four failure causes, which is why it looked like a headless problem.
## Recipe (works)
Voxel remesh -> project onto f=0 -> `bmesh.ops.remove_doubles` on the verts of edges < 1.2e-4 (dist 2e-4) -> dissolve_degenerate ->
recalc_face_normals -> (optional) mark edges with dihedral > 30 deg as sharp (`e.smooth=False`) ->
`bpy.ops.object.quadriflow_remesh(target_faces=N, use_mesh_symmetry=False, use_preserve_sharp=..., use_preserve_boundary=False, seed=0)`.
Active object, object mode, no override. Script `tools/qf5.py`; measure/score/render via `tools/one.py` + `tools/chain.sh` (run 4's measure/score/render unchanged).
Input 69,911 faces; each run 6-30 s. One case hung: sharp-preserve at target 15000 ran >25 min, I killed my PID 55924 (no output).
Guidance: QuadriFlow takes no cross field or user-supplied orientation; it computes its own from the (sharp-flagged) mesh. Only knobs:
target faces/edge length, sharp, boundary, symmetry (mesh symmetry axes), seed, curvature (compiled out). Run 4's field cannot be fed in.
## Measurements (run 2/4 scorer; out/*.metrics.json, *.score.txt)
| | QF 1796 f | QF 5032 f | QF 11534 f | QF sharp 1831 f | QF sharp 5581 f | run 4 L | run 3 v7 |
|---|---|---|---|---|---|---|---|
| tris / ngons | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 176 / 231 | 196 / 180 |
| poles per 1000 v | 39.0 | 20.3 | 12.8 | 36.5 | 18.6 | 140.3 | 47.1 |
| boundary edges (holes) | 6 | 6 | 0 | 12 | 20 | 846 | 0 |
| slivers <10 deg (share) | 0.3% | 0.03% | 0.08% | 3.0% | 0.5% | 17.6% | 24% |
| longest loop (edges) | 346 open | 823 open | 1389 open | 561 | 800 | 124 | 148 closed |
| closed loops >= 20 edges | 0 | 0 | 0 | 0 | 6 | 7 | 30 |
| chamfer % diag | 0.494 | 0.495 | 0.501 | 0.502 | 0.501 | 0.531 | **0.228** |
| F@2% / sil-IoU | 0.955 / 0.908 | 0.955 / 0.906 | 0.954 / 0.901 | 0.953 / 0.900 | 0.954 / 0.903 | 0.950 / 0.875 | **0.990 / 0.967** |
(Targets 2000/6000/15000/sharp 2000/6000; QF returns about 0.75-0.9x the target. "Loops" are open and meander: no designed edge flow.)
## Reading
- Topology is clean: pure quads, low poles (13-39/1000 v, falling with density), almost no slivers, near-watertight. Better than run 4 on every count
and on poles/slivers better than v7. But loops are not aligned to design features and no loop closes.
- Fidelity does NOT improve with face count and sits at 0.49-0.50, close to run 4 and 2x worse than v7. The sheet shows why: the thin wing
(0.03-0.08 thick) is below QuadriFlow's lattice, so the trailing/leading rim is eaten into a ragged saw-edge and the wing-tip/tail go jagged; the
body and dorsal detail (canopy ridge, pods, strakes) smooth away. Sharp preservation (dihedral > 30 flags, 3648 edges) did not fix the rim; it
added boundary holes (12-20) and slivers. Unprojected output; I did not re-project QF verts onto f=0 (would help a little, not the rim).
- So: QuadriFlow is a viable automatic quad-dominant retopo for the thick body, a poor one for a knife-edge wing. It does not replace run 3's designed
cage; at best it could cover body parts with the wing handled separately.
## Files
sheet-quadriflow.png (rows: target, SDF zero set, run 4 L, run 3 v7, QF 1796, QF 5032, QF-sharp 5581; cols 3/4, top-ish, side, wire 3/4, wire top).
out/qf_s{0,1}_{N}.obj, out/*.metrics.json, out/chain.log, tools/diag.py qf5.py one.py chain.sh.
Processes: all mine ended; operator GUI Blender PID 36324 untouched.

View file

@ -1,62 +0,0 @@
# Mesh probe run 6: Instant Meshes (Jakob et al. 2015, BSD-3) built and run headless on Dexter Quiet
Agent `probe-instant-meshes`, session `self-2026-10-10-ag2`, 2026-10-11. Scratch only; no repo edited. Root: this directory.
## Build recipe
- Source `C:\Tools\instant-meshes-src` at 7b3160864a2e1025af498c84cfed91cbfb613698 (with submodules). Output `C:\Tools\instant-meshes-build\Release\Instant Meshes.exe`.
- No option disables the GUI: nanogui/glfw/GL are linked into the one exe (batch mode is just `-o`, which never opens a window). Built whole.
- Toolchain: VS 2022 BuildTools (MSVC 14.44), CMake 4.3.2, `tools/build.sh`: `cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_POLICY_VERSION_MINIMUM=3.5`, `_CL_=-MP4` (note: `/MP4` gets path-mangled by git-bash), `cmake --build . --config Release -- -m:2`. It waits for other agents' blender/cargo/node first. The VS "18" toolset was not needed.
- **Source changes (one):** `ext/nanogui/ext/glfw/CMakeLists.txt` line 10, `cmake_policy(SET CMP0042 OLD)` to `NEW` (CMake 4 rejects OLD; the policy only affects macOS install names). Nothing in the C++ needed to change.
## Flags (`tools/runim.sh`)
`-d -r 4 -p 4 -c 40 -S 2 -f N` (deterministic, 4-RoSy/4-PoSy, crease 40 deg, 2 reprojection steps). Variants at -f 2000 (zero set): no crease, `-i` (intrinsic), `-D` (quad-dominant), `-S 8`, `-c 20`. `-b` is moot: the inputs are closed or nearly so. Inputs: `in/zeroset.obj` (run 4's `sdf_fit.stl`, welded: 49k v / 98.6k tris) and `in/tripo.obj` (run 1 `dexter_main.obj`, 4.8k v / 9.5k tris).
- Fast: 0.4-3.5 s per run on 1 job. Singularities in the orientation field: 304 (zero set), 483-700 (Tripo; its noisy surface makes more).
- **`-f N` is not the output face count.** The pure-quad output has about 4x N faces (N=40 gives 105-144, 100 gives 330-342, 400 gives 1.3-1.6k, 2000 gives 7.4-8k, 8000 gives 31k). Read the real count from the table.
- Guidance accepted: target size (`-f/-v/-s`), RoSy/PoSy, crease angle (hard feature edges found by dihedral threshold), boundary alignment, intrinsic/extrinsic, smoothing, dominance. **No input for an orientation/cross field, no per-region density, no feature curves.**
## Measurements (run 2/4 measure.py, score.py unchanged; raw = IM output, no subdivision/UV; L_ = cage refit to run 4's SDF, CC level 2, run 4's lower.py)
| mesh | faces (q/t/n) | poles/1000v | boundary / nonmanifold edges | shells | slivers <10deg | longest loop | closed loops>=20 | chamfer % | F@2% | sil-IoU |
|---|---|---|---|---|---|---|---|---|---|---|
| zeroset_f40 | 144 (144/0/0) | 161.1 | 8 / 8 | 3 | 0.0 | 20 | 0 | 0.743 | 0.924 | 0.828 |
| zeroset_f100 | 342 (342/0/0) | 114.6 | 8 / 8 | 5 | 0.0088 | 45 | 5 | 0.607 | 0.945 | 0.853 |
| zeroset_f400 | 1596 (1596/0/0) | 77.7 | 28 / 28 | 2 | 0.0003 | 96 | 0 | 0.477 | 0.959 | 0.904 |
| zeroset_f2000 | 8027 (8027/0/0) | 36.8 | 50 / 50 | 1 | 0.0001 | 284 | 0 | 0.498 | 0.954 | 0.900 |
| zeroset_f8000 | 31967 (31967/0/0) | 19.8 | 98 / 98 | 3 | 0.0001 | 386 | 3 | 0.505 | 0.953 | 0.899 |
| tripo_f40 | 105 (105/0/0) | 168.1 | 14 / 14 | 1 | 0.0 | 14 | 0 | 1.562 | 0.689 | 0.665 |
| tripo_f100 | 330 (330/0/0) | 151.1 | 0 / 0 | 1 | 0.0 | 42 | 0 | 0.619 | 0.953 | 0.859 |
| tripo_f400 | 1349 (1349/0/0) | 127.2 | 70 / 70 | 6 | 0.0019 | 38 | 0 | 0.199 | 0.994 | 0.949 |
| tripo_f2000 | 7430 (7430/0/0) | 77.3 | 240 / 240 | 10 | 0.0009 | 120 | 0 | 0.053 | 1.000 | 0.986 |
| tripo_f8000 | 30707 (30707/0/0) | 43.1 | 810 / 810 | 25 | 0.0004 | 314 | 4 | 0.017 | 1.000 | 0.994 |
| zeroset_f2000_nocrease | 7984 (7984/0/0) | 37.6 | 48 / 48 | 2 | 0.0006 | 210 | 3 | 0.492 | 0.956 | 0.905 |
| zeroset_f2000_intr | 7975 (7975/0/0) | 26.1 | 92 / 92 | 4 | 0.0 | 272 | 0 | 0.491 | 0.956 | 0.904 |
| zeroset_f2000_dom | 2017 (1888/90/39) | 78.8 | 35 / 35 | 1 | 0.0 | 86 | 0 | 0.489 | 0.956 | 0.901 |
| zeroset_f2000_S8 | 8022 (8022/0/0) | 36.3 | 54 / 54 | 2 | 0.0001 | 284 | 0 | 0.499 | 0.954 | 0.901 |
| zeroset_f2000_c20 | 7891 (7891/0/0) | 38.0 | 64 / 64 | 6 | 0.0003 | 244 | 1 | 0.498 | 0.954 | 0.902 |
| L_zeroset_f40 | 2980 (2699/146/135) | 118.9 | 49 / 50 | 1 | 0.2136 | 73 | 0 | 0.589 | 0.933 | 0.866 |
| L_zeroset_f100 | 6981 (6426/308/247) | 103.5 | 68 / 68 | 2 | 0.2163 | 101 closed | 9 | 0.527 | 0.947 | 0.881 |
| L_tripo_f40 | 2730 (2395/223/112) | 215.9 | 65 / 65 | 1 | 0.4027 | 64 | 0 | 0.690 | 0.916 | 0.854 |
| L_tripo_f100 | 6750 (6211/310/229) | 123.4 | 4 / 4 | 1 | 0.2336 | 153 | 0 | 0.508 | 0.950 | 0.904 |
Reference rows (from run 3/4/5 RESULTs): run 4 L: poles 140/1000v, 846 boundary edges, chamfer 0.531 / F@2% 0.950 / sil 0.875. run 3 v7 (designed cage): poles 47.1, 1 shell, longest loop 148 closed, 0.228 / 0.990 / 0.967. QuadriFlow (run 5), 1.8k-11.5k faces: poles 13-39, slivers <=0.3%, longest loop open, 0.494-0.501 / 0.954 / 0.90.
Cage refit (limit |f| and score before refit, then after; `out/chain1.log`, `chain2.log`):
| cage (IM faces) | before refit: chamfer / sil | after refit: chamfer / sil |
|---|---|---|
| zero set, 144 faces | 0.871 / 0.754 | 0.669 / 0.837 |
| zero set, 342 faces | 0.646 / 0.814 | 0.555 / 0.868 |
| Tripo, 105 faces | 1.806 / 0.604 | 0.970 / 0.770 |
| Tripo, 330 faces | 0.746 / 0.800 | 0.553 / 0.889 |
Lowered through run 4's stack (rows L_ above): the 342-face cage gives 0.527 / 0.947 / 0.881 but with 21 % sliver tris (bevel strips), 103 poles/1000v, 68 boundary edges and 160+ crumbs of UV islands; none reaches run 3 (0.228). The 1.5k-face (f400) refits were abandoned: `refit.py`'s dense Catmull-Clark operator ran over 30 min; I killed my PIDs.
## Reading
- Raw IM output is pure quads, almost sliver-free, loops long (284 edges at 8k faces), poles fall with density (37/1000v at 8k, 20 at 32k): same class as QuadriFlow, far better than run 4's numpy attempt. Open boundaries/non-manifold edges (8-98) come from the zero set's own thin-wing holes.
- Fidelity is the same wall as QuadriFlow: chamfer 0.48-0.50 on the zero set at every density from 1.6k to 32k faces, sil 0.90. The thin wing and the sharp rim are below its lattice; `-c`, `-i`, `-S 8` do not move it (0.49-0.50). The crease flag only changes the field (222 vs 304 singularities), not the score.
- Fed the Tripo scan it reproduces the scan (0.05 % at 7k faces), meaning it is just a retopo of whatever noise is in the input (10-25 shells, up to 810 open edges).
- A cage-like count (144-342 faces) is blocky: loops are not designed (longest 20-45 edges), poles are 115-170/1000v. Refit helps (0.87 to 0.67) but not to run 3's level.
- It does not replace run 3's designed cage. As a body retopo it is as good as QuadriFlow, and it is BSD-3 source we can own and patch (QuadriFlow in Blender is closed to us).
## Injecting an external cross field: feasible, modest
Source reading (`src/batch.cpp`, `field.cpp`, `hierarchy.cpp`): the batch path already has the hook. `align_to_boundaries` writes per-vertex constraints `mRes.CQ().col(i)` (direction), `CQw()[i]` (weight), `CO/COw` (position pin) and calls `mRes.propagateConstraints(rosy, posy)`; `Optimizer::optimizeOrientations` (field.cpp ~632-660) blends CQ by CQw. So run 4's exact-Hessian field goes in as: read a per-vertex tangent direction + weight file, set CQ/CQw (and optionally CO/COw on creases for position pinning), propagate, optimize. Alternatively set `mRes.Q(0)` directly and skip the orientation solve. Work: roughly 50-100 lines in batch.cpp plus a reader, around half a day to a day, no change to the solver. Gotchas: `subdivide()` may add vertices before the constraints (pre-subdivide the input so indices match, or evaluate the analytic field at `mRes.V()`), constraints must lie in the vertex tangent plane, and the weight scale is empirical. Whether it fixes the thin-wing fidelity wall is untested (the wall is lattice size vs. wing thickness, not orientation).
## Files
`sheet-main.png`, `sheet-closeup.png` (rows: Tripo target, SDF zero set, run 3 v7, run 4 L, QuadriFlow 6000, IM zero-set f40/f100/f400/f2000, IM Tripo f400, L_zeroset_f100, L_tripo_f100; columns as run 2). `out/*.obj` (IM outputs), `out/*.metrics.json`, `*.score.txt`, `out/table.md`, `tools/` (build.sh, runim.sh, chain.sh, refit.py, rawmeasure.py, table.py, stl2obj.py). Processes: all mine ended; operator GUI Blender PID 36324 untouched.

View file

@ -1,169 +0,0 @@
# Run 7: an Instant Meshes fork driven by the SDF (cross field, hard rim crease, per-vertex size)
Agent `spike-im-field-features`, session `self-2026-10-10-ag2`, 2026-10-11. Ruling `brush-tree:ruling:spike-instant-meshes-field-features`, bar `brush-tree:ruling:shippable-part-bar`. Scratch only; no repo touched; operator GUI Blender (PID 36324) untouched; all my processes ended.
## Verdict
1. **The diagnosis was wrong.** The ~0.49 % chamfer wall is not lattice size. It is how well run 4's SDF program fits the Tripo target.
- Measured against the SDF itself, every Instant Meshes output, plain included, sits at 0.01-0.07 % chamfer and 0.99+ top silhouette IoU.
- Scored against Tripo, every variant lands at 0.48-0.55 % whatever the field, constraints or density.
- The program alone scores 0.520 (run 4). No remesher can beat that against Tripo.
2. **The real thin-feature defect was in the program.** Its fitted beak was a pinched neck (half-width 0.006) and an inverted station. That gave:
- 119 non-manifold input edges;
- ~300 extra orientation singularities (374 against 62);
- every hole in the IM outputs (16-122 open edges).
Reverting those two stations to run 2's description values (`in/params7.json`) leaves every output with 0 holes, 0 non-manifold edges and 1 shell.
3. **Hard rim crease (step 2) is what bought rim fidelity.** It works through position and orientation constraints on the exact crease plus Newton projection: at 8k quads the rim p95 error falls 10x, from 0.20 % to 0.02 % of the diagonal, and the rim renders crisp with a one-segment bevel. It needs the lattice finer than the rim wall (0.035-0.08 high). At 2k quads it only halves the error.
4. **The exact-Hessian cross field (step 1) buys alignment and costs poles.** Median misalignment to the principal directions drops from 13° to 3.4°, but poles rise from 31 to 52 per 1000 v.
5. **The size field (1b) made topology worse.** The isotropic per-vertex scale cost 3-4x poles and up to 1074 open edges for no fidelity gain at equal density. Anisotropic scale is too deep to patch (see 1b).
6. **Step 3 was not run.** Fidelity at the rim was already met, and the wing joins the body through a smooth blend (k_wing 0.12), not a crease, so there is no curve to stitch along.
7. **Her bar is half met.**
- **Met:** watertight, 1 shell, pure-quad base, crisp beveled rim with face-weighted normals.
- **Not met:**
- Flow: loops are not designed, none closes, and poles are 50-69 per 1000 v against v7's 47.
- UVs: 10 islands with no crumbs, but area distortion of 0.67-0.76 against v7's 0.076. The automatic seams join wing top and bottom wherever the snapped rim has gaps.
- Automated remeshing of an SDF composition is now a fidelity solution, not yet a topology-and-UV solution.
## The fork
`C:\Tools\instant-meshes-fork` is a copy of `instant-meshes-src` at 7b31608, built out of tree in `C:\Tools\instant-meshes-fork-build` with `/m:4`. Patch: `instant-meshes-field.patch` (100 lines added, 10 removed, plus run 6's glfw CMP0042 line).
- `--field <file>`: a text file of records `x y z qx qy qz qw ox oy oz ow s`.
- Each solver vertex takes the nearest record through a uniform-grid lookup, so subdivision does not break indexing.
- q and qw feed `CQ/CQw`. o and ow feed `CO/COw`, the same constraint path `-b` uses. Then `propagateConstraints`.
- `s > 0` gives a per-vertex target edge length.
- Per-vertex scale: `MultiResolutionHierarchy::mS`, one vector per level, built from `toUpper` averages.
- `optimize_positions_impl` uses the edge scale s_ij = (s_i + s_j)/2 in the compat functor and s_i in the rounding.
- `extract_graph` uses s_ij for its edge classification and s_i for its cluster weights.
- The degenerate-triangle cleanup uses min(s).
- Upstream bug fixed: `propagateConstraints` projected `cq - v` where it meant `co - v` (`hierarchy.cpp`).
- Runtime is 2.4-5 s per run.
## Inputs
`tools/prep.py` and `prep2.py` produce them:
- Surface nets at h 0.005, Newton-projected: 68k vertices for the repaired program, 71k for the fitted one.
- The field is run 4's: exact-Hessian principal directions, anisotropy confidence, Ohtake ridges, and the program's crease.
- New in `tools/feat.py`: the exact crease. Gauss-Newton on {slab = 0, planform = 0}, kept where the wing is the active surface.
Field variants:
- **field2:** curvature weight 0.5·cw where cw > 0.3, else free; ridges 0.5; crease 1.
- **creaseonly:** constraints only on the rim (2,865 vertices, orientation along the crease and position on it).
- **crease2:** field2 plus the rim constraints.
Post-process (`tools/post.py`): output vertices within 0.35·s of the crease are snapped onto the exact crease, and the rest are Newton-projected onto f = 0 (the `_p` meshes).
## Measurements
All rows below are on the repaired program, flags `-d -r 4 -p 4 -c 40 -S 2`; `out/post3.log`.
Fidelity is in % of the bbox diagonal:
- "SDF ch." is chamfer against the dense SDF zero set.
- "rim p95" is the 95th-percentile distance from 2,202 exact crease samples to the mesh, raw then projected.
- "top IoU" is the top-view silhouette against the SDF.
- Tripo is run 4's scorer.
| mesh (IM -f) | quads | poles/1000v | open/nonmanifold | misalign to principal dir, med / p90 | rim edge misalign med / p90 | SDF ch. raw → proj | rim p95 raw → proj | top IoU proj | Tripo ch. / sil |
|---|---|---|---|---|---|---|---|---|---|
| plain f500 | 2020 | 51.4 | 0/0 | 14.5° / 35.9° | 2.9° / 9.1° | 0.046 → 0.033 | 0.644 → 0.541 | 0.9962 | 0.547 / 0.905 |
| field2 f500 | 2010 | 79.0 | 0/0 | 4.9° / 21.3° | 2.4° / 6.7° | 0.043 → 0.031 | 0.552 → 0.513 | 0.9967 | 0.547 / 0.903 |
| creaseonly f500 | 2038 | 67.2 | 0/0 | 17.3° / 37.0° | 2.3° / 6.4° | 0.036 → 0.030 | 0.334 → 0.151 | 0.9961 | 0.545 / 0.904 |
| crease2 f500 | 2086 | 92.4 | 0/0 | 4.9° / 25.0° | 2.0° / 7.2° | 0.038 → 0.030 | 0.459 → 0.302 | 0.9966 | 0.546 / 0.904 |
| plain f2000 | 7978 | **30.7** | 0/0 | 13.2° / 34.7° | 2.2° / 6.2° | 0.011 → 0.008 | 0.257 → 0.202 | 0.9990 | 0.555 / 0.902 |
| field2 f2000 | 8132 | 51.5 | 0/0 | **3.4° / 16.1°** | 2.1° / 5.3° | 0.011 → 0.008 | 0.274 → 0.201 | 0.9991 | 0.554 / 0.903 |
| creaseonly f2000 | 8052 | 41.1 | 0/0 | 15.6° / 37.6° | 1.2° / 4.1° | 0.010 → 0.008 | 0.123 → **0.020** | 0.9990 | 0.554 / 0.903 |
| crease2 f2000 | 8242 | 57.6 | 0/0 | 3.7° / 15.9° | **1.1° / 4.0°** | 0.010 → 0.008 | 0.161 → 0.045 | 0.9991 | 0.553 / 0.904 |
### On the fitted program (beak defect present; `out/post1.log`, `post2.log`)
The same pattern holds, plus holes:
- plain f2000: 39.8 poles, 70 open edges, rim p95 0.262 after projection.
- creaseonly f2000: 48.3 poles, rim p95 0.023.
The open edges sit mostly on the body at the beak, for example 57 body / 28 wing / 12 pods.
### Step 1b, size field
Per vertex, s = min(0.35/|kmax|, inscribed radius, s0), clamped to [0.3, 1]·s0, with 0.6·s0 on the crease and a gradation limit of 0.3·|edge| (`fieldS`, `creaseS`). Fitted program:
| mesh | quads | poles/1000v | open edges | SDF ch. proj | rim p95 proj |
|---|---|---|---|---|---|
| fieldS f500 | 13009 | 115.2 | 134 | 0.010 | 0.235 |
| creaseS f500 | 13229 | 120.7 | 178 | 0.010 | **0.014** |
| plain f3300 (equal density) | 13024 | **33.7** | 104 | 0.010 | 0.198 |
| creaseS f2000 | 26381 | 121.6 | **1074** | 0.009 | 0.214 |
| creaseG f2000 (gentle: ≥ 0.6·s0, gradation 0.15) | 15203 | 78.6 | 250 | 0.009 | 0.006 |
- Why the size field fails: grading the lattice spacing breaks the integer-offset consistency between neighbours, which shows up as poles and holes. Fidelity at equal density is unchanged.
- Thickness never limits the wing body: it is 0.035-0.08 thick against s = 0.025 at 8k quads. The only sub-lattice thing on the wing is the rim wall at 2k quads, and the crease constraint already handles it at 8k.
- Anisotropic scale was not attempted, because the patch would have to go deep:
- The functions are `position_round_3/4`, `position_floor_3/4`, `compat_position_{extrinsic,intrinsic}_{3,4}` and their `_index` variants (field.cpp ~290-610), `optimize_positions_impl`, `extract_graph`'s classify_edges, and `extract_faces`' scale thresholds.
- Every one rounds q and n×q by a single scale.
- A (s_u, s_v) pair must also swap when the 4-RoSy matching rotates q by 90°, but `compat_orientation_*` returns rotated vectors, not the rotation index. So the pair cannot follow without changing that interface across the solver and the extractor.
- The anisotropic follow-up for that is Panozzo et al. 2014 frame fields with a non-orthogonal parametrisation. That is a different solver, not a patch.
### Lowered and measured as run 2 did
`tools/lower7.py` is run 4's `lower.py` with these changes:
- no Catmull-Clark and no re-projection;
- holes filled (none left on the repaired program);
- seams at part-label boundaries, where the labels are majority-smoothed and split at the mirror;
- seams on edges over 60° and on the exact crease.
It keeps the one-segment bevel at 30° with width 0.004, then Weighted Normal by face area. Measured with run 4's `measure.py`; Tripo score from `score.py`; `out/chain7n.log`, `chain7m.log`.
| | L7nplain (plain f2000, unprojected) | **L7ncreaseM (creaseonly f2000)** | L7nfieldM (crease2 f2000) | L7nfield500M (crease2 f500) | run 3 v7 (designed cage) | run 6 plain IM f2000 raw |
|---|---|---|---|---|---|---|
| faces q/t/n, max ngon | 8382/89/150, 6 | 8468/97/155, 6 | 8635/114/178, 6 | 2311/82/116, 6 | 9297/196/180, 6 | 8027/0/0 |
| shells / open edges | 1 / 0 | 1 / 0 | 1 / 0 | 1 / 0 | 1 / 0 | 1 / 50 |
| poles/1000v / valence ≥ 6 | **38.4** / 0 | 50.2 / 4 | 69.0 / 5 | 114.1 / 7 | 47.1 / 0 | 36.8 |
| longest loop / closed ≥ 20 / share in loops ≥ 20 | 253 open / 2 / **0.764** | 274 open / 1 / 0.707 | 105 open / 1 / 0.593 | 94 open / 0 / 0.363 | 148 closed / 30 / 0.755 | 284 open / 0 |
| exported sliver share / excluding bevel strips | 0.1 % / 0 | 1.8 % / 0 | 6.9 % / 1 | 15.7 % / 2 | 24 % / 254 | n/a |
| dihedral > 60° / > 80° left | 28 / 9 | 16 / 3 | 11 / 4 | 22 / 4 | 546 / 316 | n/a |
| UV islands / ≥ 1 % / crumbs | 11 / 6 / 1 | 10 / 6 / 0 | 10 / 6 / 0 | 11 / 6 / 1 | 3 / 3 / 0 | n/a |
| UV area distortion mean \|log2\| / outside 0.5-2x | 1.074 / 25 % | 0.764 / 21 % | 0.668 / 17 % | 0.473 / 7 % | **0.076 / 0.8 %** | n/a |
| UV angle error / wasted | 2.07° / 67 % | 1.50° / 60 % | 1.88° / 70 % | 2.91° / 63 % | 2.61° / 59 % | n/a |
| Tripo chamfer / F@2% / sil | 0.552 / 0.950 / 0.907 | 0.553 / 0.950 / 0.905 | 0.552 / 0.951 / 0.904 | 0.546 / 0.953 / 0.904 | **0.228 / 0.990 / 0.967** | 0.498 / 0.954 / 0.900 |
- **The rim is the visible win.** In L7nplain, IM rounds the rim off the surface-nets staircase. Few edges pass 30°, so almost nothing is beveled, and it shades faceted (sheet row 5). The crease rows (6-8) have a straight, crisp beveled rim that shades clean with face-weighted normals. The rim wall is 1-2 quads high, with some poles on it.
- **Topology.** Pure-quad bases, watertight, near-zero slivers outside the bevel strips.
- **Flow is generic field flow.** Loops are long but open and not designed.
- **The cross field's poles survive lowering.** L7nfieldM has 69 poles per 1000 v against 50 for crease-only and 38 for plain.
- **UVs are the clear failure.**
- The part-label, mirror and hard-edge seams leave each wing half as one chart, top and bottom joined through gaps in the snapped crease: only ~300 of the rim vertices land on the exact crease.
- Area distortion is 6-10x v7's. A cut along the exact crease did not change the seams.
- Fixing this needs either IM vertices placed on the whole crease (so it becomes a closed seam loop) or a distortion-driven seam method. Examples: OptCuts (Li, Kaufman, Kim, Sorkine-Hornung 2018) or Autocuts (Poranne et al. 2017); neither was tried.
- **Tripo fidelity of every lowered row is the program's.** The 0.55 is the repaired beak; it was 0.51 with the fitted beak. v7 wins on Tripo because its cage was fitted to Tripo, not to this SDF.
## What each step bought
| step | bought | cost | where it broke |
|---|---|---|---|
| 1 cross field (field2) | misalignment 13° → 3.4° median; rim edges 2.2° → 1.1° | poles +68 % (31 → 52 per 1000 v); singularities 62 → 120 | no fidelity gain. The unfiltered field (`field`, every vertex weighted) doubled poles. |
| 1b size field | nothing at equal density | poles 3-4x, holes | integer lattice offsets break under spacing gradients |
| 2 rim crease + projection | rim p95 0.20 → 0.02 % at 8k quads; crisp bevelable rim | poles +34 % (31 → 41) | below s ≈ rim wall height (2k quads) it only halves the error. Snapped coverage of the crease is partial (~300 vertices). |
| beak repair (program fix) | 0 holes, 0 non-manifold edges, singularities 374 → 62 | Tripo chamfer +0.05 | degenerate primitives in the program are the real "thin feature" hazard |
| 3 wing as own patch | not run | | no crease between wing and body to stitch along (smooth blend). Fidelity was already met. |
**Next, if pursued:**
1. Make the crease constraint cover the whole curve, so the rim becomes a closed seam loop. Seed lattice vertices along the curve at s spacing as hard COw rather than nearest-vertex hints.
2. Add a validator that rejects degenerate SDF primitives: inverted or near-zero stations.
3. Use a seam method for UVs.
4. To get designed flow, refit run 3's designed cage to the SDF (run 4's refit) rather than an IM field. On this evidence that is still the only path to closed loops and v7-class UVs.
## Files
Root `...\scratchpad\mesh-probe\run7-im-field\`.
- **Sheets:** `sheet-main.png` and `sheet-closeup.png`.
- Main sheet columns: 3/4 front, side, 3/4 rear, wire, UV.
- Close-up columns: rim close-up, rim wire, top wire.
- Rows (in both): 1 Tripo target, 2 SDF zero set (repaired), 3 run 3 v7, 4 run 6 plain IM f2000, 5 L7nplain, 6 L7ncreaseM, 7 L7nfieldM, 8 L7nfield500M.
- **Patch:** `instant-meshes-field.patch`.
- **Tools** (`tools/`):
- `prep.py`, `prep2.py`, `feat.py`, `post.py`, `lower7.py`;
- `runim.sh`, `chain7.sh`, `bl.sh` (wait-first Blender wrapper), `build.sh`.
- **Logs:** `out/*.log`; `post1-3.log` hold the per-mesh JSON.
- **Meshes and renders:**
- `out/<variant>.obj` and the projected `_p.obj`/`.stl`;
- `out/L7*.blend`, `.metrics.json`, `.score.txt` and renders.
- **Inputs:** `in/`, for the repaired program (`params7.json`); `in_fit/` holds the fitted-program inputs.

View file

@ -1,275 +0,0 @@
Submodule ext/nanogui contains modified content
Submodule ext/glfw contains modified content
diff --git a/ext/nanogui/ext/glfw/CMakeLists.txt b/ext/nanogui/ext/glfw/CMakeLists.txt
index 8b861820..be54a918 100644
--- a/ext/nanogui/ext/glfw/CMakeLists.txt
+++ b/ext/nanogui/ext/glfw/CMakeLists.txt
@@ -7,7 +7,7 @@ cmake_minimum_required(VERSION 2.8.11.2)
if (NOT CMAKE_VERSION VERSION_LESS "3.0")
# Until all major package systems have moved to CMake 3,
# we stick with the older INSTALL_NAME_DIR mechanism
- cmake_policy(SET CMP0042 OLD)
+ cmake_policy(SET CMP0042 NEW)
endif()
if(POLICY CMP0022)
diff --git a/src/batch.cpp b/src/batch.cpp
index b35769d..d5d8e36 100644
--- a/src/batch.cpp
+++ b/src/batch.cpp
@@ -21,12 +21,79 @@
#include "normal.h"
#include "extract.h"
#include "bvh.h"
+#include <fstream>
+#include <unordered_map>
+
+/* Fork: external constraint field. Text file: a count n, then n records
+ x y z qx qy qz qw ox oy oz ow s
+ (q: tangent direction + weight in [0,1] blended into the orientation solve;
+ o: a point the lattice line along q must pass through + weight, as Instant Meshes' boundary
+ constraints use it; s: target edge length at the point, <= 0 for the global scale).
+ Each solver vertex takes the nearest record (uniform-grid lookup), so the records need not
+ match the vertex indexing after subdivision. */
+static void apply_field_file(MultiResolutionHierarchy &mRes, const std::string &path, int rosy, int posy) {
+ std::ifstream is(path);
+ if (!is) throw std::runtime_error("Could not open field file " + path);
+ size_t n; is >> n;
+ std::vector<Float> R(n * 12);
+ for (size_t k = 0; k < n * 12; ++k) is >> R[k];
+ if (!is) throw std::runtime_error("Field file is truncated: " + path);
+ Vector3f lo = Vector3f::Constant(1e30f), hi = Vector3f::Constant(-1e30f);
+ for (size_t r = 0; r < n; ++r) for (int k = 0; k < 3; ++k) { lo[k] = std::min(lo[k], R[r*12+k]); hi[k] = std::max(hi[k], R[r*12+k]); }
+ Float cell = std::max((hi - lo).maxCoeff() / std::cbrt((Float) n) * 1.5f, (Float) 1e-6f);
+ auto key = [&](const Vector3f &p, int dx, int dy, int dz) {
+ int64_t x = (int64_t) std::floor((p.x() - lo.x()) / cell) + dx, y = (int64_t) std::floor((p.y() - lo.y()) / cell) + dy,
+ z = (int64_t) std::floor((p.z() - lo.z()) / cell) + dz;
+ return (x * 73856093) ^ (y * 19349663) ^ (z * 83492791);
+ };
+ std::unordered_multimap<int64_t, uint32_t> grid;
+ for (size_t r = 0; r < n; ++r) grid.emplace(key(Vector3f(R[r*12], R[r*12+1], R[r*12+2]), 0, 0, 0), (uint32_t) r);
+ mRes.clearConstraints();
+ const MatrixXf &V = mRes.V(), &N = mRes.N();
+ VectorXf S0(V.cols()); bool anyS = false;
+ uint32_t nq = 0, no = 0;
+ for (uint32_t i = 0; i < V.cols(); ++i) {
+ Vector3f p = V.col(i); Float best = 1e30f; int64_t br = -1;
+ for (int ring = 1; ring <= 3 && br < 0; ++ring)
+ for (int dx = -ring; dx <= ring; ++dx) for (int dy = -ring; dy <= ring; ++dy) for (int dz = -ring; dz <= ring; ++dz) {
+ auto range = grid.equal_range(key(p, dx, dy, dz));
+ for (auto it = range.first; it != range.second; ++it) {
+ const Float *q = &R[it->second * 12];
+ Float d = (Vector3f(q[0], q[1], q[2]) - p).squaredNorm();
+ if (d < best) { best = d; br = it->second; }
+ }
+ }
+ if (br < 0) { S0[i] = mRes.scale(); continue; }
+ const Float *q = &R[br * 12];
+ Vector3f n_i = N.col(i), cq(q[3], q[4], q[5]);
+ cq -= n_i * n_i.dot(cq);
+ if (q[6] > 0 && cq.squaredNorm() > 1e-12f) {
+ mRes.CQ().col(i) = cq.normalized(); mRes.CQw()[i] = std::min(q[6], (Float) 1); ++nq;
+ if (q[10] > 0) { mRes.CO().col(i) = Vector3f(q[7], q[8], q[9]); mRes.COw()[i] = std::min(q[10], (Float) 1); ++no; }
+ }
+ S0[i] = q[11] > 0 ? q[11] : mRes.scale(); anyS |= q[11] > 0;
+ }
+ mRes.propagateConstraints(rosy, posy);
+ cout << "Field file: " << n << " records, " << nq << " orientation and " << no << " position constraints";
+ if (anyS) {
+ std::vector<VectorXf> &S = mRes.Slevels(); S.clear(); S.push_back(S0);
+ for (int l = 0; l + 1 < mRes.levels(); ++l) {
+ const MatrixXu &up = mRes.toUpper(l); VectorXf next(up.cols());
+ for (uint32_t i = 0; i < up.cols(); ++i)
+ next[i] = up(1, i) == INVALID ? S[l][up(0, i)] : 0.5f * (S[l][up(0, i)] + S[l][up(1, i)]);
+ S.push_back(next);
+ }
+ cout << ", per-vertex scale " << S0.minCoeff() << " .. " << S0.maxCoeff();
+ }
+ cout << endl;
+}
void batch_process(const std::string &input, const std::string &output,
int rosy, int posy, Float scale, int face_count,
int vertex_count, Float creaseAngle, bool extrinsic,
bool align_to_boundaries, int smooth_iter, int knn_points,
- bool pure_quad, bool deterministic) {
+ bool pure_quad, bool deterministic,
+ const std::string &field_file) {
cout << endl;
cout << "Running in batch mode:" << endl;
cout << " Input file = " << input << endl;
@@ -157,6 +224,9 @@ void batch_process(const std::string &input, const std::string &output,
mRes.propagateConstraints(rosy, posy);
}
+ if (!field_file.empty() && !pointcloud)
+ apply_field_file(mRes, field_file, rosy, posy);
+
if (bvh) {
bvh->setData(&mRes.F(), &mRes.V(), &mRes.N());
} else if (smooth_iter > 0) {
diff --git a/src/batch.h b/src/batch.h
index e9d7baa..298ea5c 100644
--- a/src/batch.h
+++ b/src/batch.h
@@ -19,4 +19,5 @@ extern void batch_process(const std::string &input, const std::string &output,
int rosy, int posy, Float scale, int face_count,
int vertex_count, Float creaseAngle, bool extrinsic,
bool align_to_boundaries, int smooth_iter,
- int knn_points, bool dominant, bool deterministic);
+ int knn_points, bool dominant, bool deterministic,
+ const std::string &field_file = "");
diff --git a/src/extract.cpp b/src/extract.cpp
index 5a38653..34ea42f 100644
--- a/src/extract.cpp
+++ b/src/extract.cpp
@@ -44,6 +44,8 @@ extract_graph(const MultiResolutionHierarchy &mRes, bool extrinsic, int rosy, in
const MatrixXf &Q = mRes.Q(), &O = mRes.O(), &N = mRes.N(), &V = mRes.V();
const VectorXf &COw = mRes.COw();
+ const VectorXf &S = mRes.S();
+ const bool varS = S.size() == V.cols();
const AdjacencyMatrix &adj = mRes.adj();
Timer<> timer;
@@ -75,7 +77,8 @@ extract_graph(const MultiResolutionHierarchy &mRes, bool extrinsic, int rosy, in
std::pair<Vector2i, Vector2i> shift = compat_position(
V.col(i), N.col(i), Q_rot.first, O.col(i),
V.col(j), N.col(j), Q_rot.second, O.col(j),
- scale, inv_scale, &error);
+ varS ? 0.5f * (S[i] + S[j]) : scale,
+ varS ? 2.0f / (S[i] + S[j]) : inv_scale, &error);
Vector2i absDiff = (shift.first-shift.second).cwiseAbs();
@@ -280,7 +283,8 @@ extract_graph(const MultiResolutionHierarchy &mRes, bool extrinsic, int rosy, in
continue;
uint32_t j = it->second;
- Float weight = std::exp(-(O.col(i)-V.col(i)).squaredNorm() * inv_scale * inv_scale * 9);
+ const Float is_i = varS ? 1.0f / S[i] : inv_scale;
+ Float weight = std::exp(-(O.col(i)-V.col(i)).squaredNorm() * is_i * is_i * 9);
if (COw.size() != 0 && COw[i] != 0) {
tbb::spin_mutex::scoped_lock lock(mutex);
crease_out.insert(j);
@@ -325,7 +329,7 @@ extract_graph(const MultiResolutionHierarchy &mRes, bool extrinsic, int rosy, in
bool changed_inner;
do {
changed_inner = false;
- Float thresh = 0.3f * scale;
+ Float thresh = 0.3f * (varS ? S.minCoeff() : scale); /* fork: smallest local spacing */
std::vector<std::tuple<Float, uint32_t, uint32_t, uint32_t>> candidates;
for (uint32_t i_id=0; i_id<adj_new.size(); ++i_id) {
diff --git a/src/field.cpp b/src/field.cpp
index 4d986aa..e07ce29 100644
--- a/src/field.cpp
+++ b/src/field.cpp
@@ -901,6 +901,8 @@ template <typename CompatFunctor, typename RoundFunctor> static inline Float opt
const AdjacencyMatrix &adj = mRes.adj(level);
const MatrixXf &N = mRes.N(level), &Q = mRes.Q(level), &V = mRes.V(level);
const Float scale = mRes.scale(), inv_scale = 1.0f / scale;
+ const VectorXf &S = mRes.S(level);
+ const bool varS = S.size() == V.cols();
const std::vector<uint32_t> *phase = nullptr;
const MatrixXf &CQ = mRes.CQ(level);
const MatrixXf &CO = mRes.CO(level);
@@ -933,8 +935,9 @@ template <typename CompatFunctor, typename RoundFunctor> static inline Float opt
q_j.normalize();
#endif
+ const Float s_ij = varS ? 0.5f * (S[i] + S[j]) : scale;
std::pair<Vector3f, Vector3f> value = compat_functor(
- v_i, n_i, q_i, sum, v_j, n_j, q_j, o_j, scale, inv_scale);
+ v_i, n_i, q_i, sum, v_j, n_j, q_j, o_j, s_ij, 1.0f / s_ij);
sum = value.first*weight_sum + value.second*weight;
weight_sum += weight;
@@ -955,7 +958,8 @@ template <typename CompatFunctor, typename RoundFunctor> static inline Float opt
}
if (weight_sum > 0)
- O.col(i) = round_functor(sum, q_i, n_i, v_i, scale, inv_scale);
+ O.col(i) = varS ? round_functor(sum, q_i, n_i, v_i, S[i], 1.0f / S[i])
+ : round_functor(sum, q_i, n_i, v_i, scale, inv_scale);
}
};
diff --git a/src/hierarchy.cpp b/src/hierarchy.cpp
index 2882a67..7bca4ea 100644
--- a/src/hierarchy.cpp
+++ b/src/hierarchy.cpp
@@ -813,7 +813,7 @@ void MultiResolutionHierarchy::propagateConstraints(int rosy, int posy) {
}
if (co != Vector3f::Zero()) {
Vector3f n = N_next.col(i), v = V_next.col(i);
- co -= n.dot(cq - v) * n;
+ co -= n.dot(co - v) * n; /* fork: upstream projected cq - v */
}
#if 0
cqw *= 0.5f;
diff --git a/src/hierarchy.h b/src/hierarchy.h
index 2b3c337..ee36928 100644
--- a/src/hierarchy.h
+++ b/src/hierarchy.h
@@ -76,6 +76,9 @@ public:
inline uint32_t size(int level = 0) const { return mV[level].cols(); }
inline Float scale() const { return mScale; }
+ /* Fork: optional per-vertex target edge length (one vector per level; empty = global scale) */
+ inline const VectorXf &S(int level = 0) const { static const VectorXf empty; return level < (int) mS.size() ? mS[level] : empty; }
+ inline std::vector<VectorXf> &Slevels() { return mS; }
inline void setScale(Float scale) { mScale = scale; }
inline int iterationsQ() const { return mIterationsQ; }
inline void setIterationsQ(int iterationsQ) { mIterationsQ = iterationsQ; }
@@ -125,6 +128,7 @@ public:
bool mFrozenQ, mFrozenO;
ordered_lock mMutex;
Float mScale;
+ std::vector<VectorXf> mS;
int mIterationsQ;
int mIterationsO;
uint32_t mTotalSize;
diff --git a/src/main.cpp b/src/main.cpp
index a2f6f11..439b11f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -29,7 +29,7 @@ int main(int argc, char **argv) {
int rosy = 4, posy = 4, face_count = -1, vertex_count = -1;
uint32_t knn_points = 10, smooth_iter = 2;
Float crease_angle = -1, scale = -1;
- std::string batchOutput;
+ std::string batchOutput, fieldFile;
#if defined(__APPLE__)
bool launched_from_finder = false;
#endif
@@ -44,6 +44,12 @@ int main(int argc, char **argv) {
deterministic = true;
} else if (strcmp("--intrinsic", argv[i]) == 0 || strcmp("-i", argv[i]) == 0) {
extrinsic = false;
+ } else if (strcmp("--field", argv[i]) == 0) {
+ if (++i >= argc) {
+ cerr << "Missing field file argument!" << endl;
+ return -1;
+ }
+ fieldFile = argv[i];
} else if (strcmp("--boundaries", argv[i]) == 0 || strcmp("-b", argv[i]) == 0) {
align_to_boundaries = true;
} else if (strcmp("--threads", argv[i]) == 0 || strcmp("-t", argv[i]) == 0) {
@@ -154,6 +160,7 @@ int main(int argc, char **argv) {
cout << " -S, --smooth <iter> Number of smoothing & ray tracing reprojection steps (default: 2)" << endl;
cout << " -D, --dominant Generate a tri/quad dominant mesh instead of a pure tri/quad mesh" << endl;
cout << " -i, --intrinsic Intrinsic mode (extrinsic is the default)" << endl;
+ cout << " --field <file> Per-point constraints: x y z qx qy qz qw ox oy oz ow s (see batch.cpp)" << endl;
cout << " -b, --boundaries Align to boundaries (only applies when the mesh is not closed)" << endl;
cout << " -r, --rosy <number> Specifies the orientation symmetry type (2, 4, or 6)" << endl;
cout << " -p, --posy <number> Specifies the position symmetry type (4 or 6)" << endl;
@@ -177,7 +184,7 @@ int main(int argc, char **argv) {
batch_process(args[0], batchOutput, rosy, posy, scale, face_count,
vertex_count, crease_angle, extrinsic,
align_to_boundaries, smooth_iter, knn_points,
- !dominant, deterministic);
+ !dominant, deterministic, fieldFile);
return 0;
} catch (const std::exception &e) {
cerr << "Caught runtime error : " << e.what() << endl;

View file

@ -1,63 +0,0 @@
<!-- Serves brush-tree:question:openscad-surface. Eyes pass eyes-openscad-evaluators, 2026-10-10. -->
# Facts: OpenSCAD evaluators, CSG export, round-trip precedent
Serves: brush-tree:question:openscad-surface (InForce, recommended c; Eyes pass owed before costing options a/b/c; unverified items: Rust evaluator existence, CSG export node set). Eyes, 2026-10-10. Facts only. Tags: FETCHED (page read this session), SEARCH (search-result snippet only), KNOWLEDGE (memory, unverified), LOCAL (ran on installed nightly).
Scratch dir: C:\Users\Meta\AppData\Local\Temp\claude\F--Projects-CultLib\1b994b80-e27e-42a8-8b75-ab8cb1dd9c54\scratchpad\eyes-openscad-evaluators\ (t.scad, t2.scad, out.csg, o2.csg, out.ast, t.nef3, t.param).
## 1. Evaluators / parsers outside OpenSCAD
| Name | What it is | Coverage | State | Licence | Tag |
|---|---|---|---|---|---|
| openscad-rs (github.com/ierror/openscad-rs, docs.rs/openscad-rs) | Rust parser to typed AST. No evaluator, no include resolution, no geometry. | literals, precedence, vectors, ranges, list comprehensions, functions, modules, children, modifiers, include/use (parsed only). Numbers as exact hyperreal::Real. Tested against vendored upstream OpenSCAD fixture corpus (regression floor, not full acceptance); cargo-fuzz. | v0.1.0, release 14 Sep 2026; 15 commits; 2 stars | Apache-2.0 OR MIT (repo page); docs.rs page said GPL-3.0-or-later (the two disagree) | FETCHED |
| scad-rs (github.com/Michael-F-Bryan/scad-rs) | "A Rust implementation of the OpenSCAD virtual machine"; topics bytecode-VM, compiler, parser; scad.ungram grammar; openscad as submodule for compat tests | README gives no feature or maturity detail | 104 commits, 31 stars, not archived; last-commit date not shown; crates.io listing not confirmed | Apache-2.0 / MIT | FETCHED (README), maturity UNKNOWN |
| scad, scadman, scad_tree, flowscad (crates.io) | Generators (Rust emits .scad). Not parsers/evaluators. scad 1.2.2 last updated 2019; scadman 0.2.2 Dec 2025 | n/a | | | SEARCH |
| tree-sitter-openscad (github.com/openscad/tree-sitter-openscad) | Incremental tree-sitter grammar; npm @openscad/tree-sitter-openscad; Rust crate tree-sitter-openscad-ng; pyproject/setup.py present | syntax only | 154 commits, 11 stars; last-commit date not shown | MIT | FETCHED |
| openscad_parser (github.com/BelfrySCAD/openscad_parser, PyPI) | Python parser (Arpeggio PEG; also an LALR variant openscad_lalr_parser feeds the evaluator) to AST | Claims "Full OpenSCAD language support": functions, modules, let/echo/assert, function literals, list comprehensions (for, C-style, if, let, each), modifiers, use/include; strict_commas() mimics 2021.01 | 202 commits; PyPI mirror snippet showed 2.4.7 (SEARCH) | MIT | FETCHED (README), version SEARCH |
| openscad_evaluator / PyPI openscad-evaluator (github.com/BelfrySCAD/openscad_evaluator) | Python AST evaluator producing Manifold geometry from the openscad_lalr_parser AST. v1.8.1 (26 Sep 2026); 1.0.0 was 20 Jul 2026; Python 3.11+; 214 commits | docs/evaluator.md (read first 100k of 127k chars): user functions, closures, default params, tail calls; let (expr+stmt); list comprehensions incl. C-style for, let, intersection_for; modules, children() (index, separate=true extension), $children, parent_module(); include and use with namespace isolation; hull (accepts open meshes); minkowski (+ minkowski_difference extension, 3D); multmatrix (3D only; 2D children pass through unchanged); polyhedron (four input forms; open mesh dropped by booleans). README: full built-in coverage of primitives, transforms, booleans, 2D, text(), surface(), DXF/3MF import, roof(). Noted limitation: same-name double binding in one let "unhandled edge case". | | MIT | FETCHED |
| (same) CSG tree exposure | evaluate() builds persistent `self.csg_tree` (list of CSGNode) in a resolve pass before any Manifold work; generate_tree() fills bodies; flatten_csg_tree(); id_to_node and id_to_call_chain map bodies back to source | | | | FETCHED |
| openscad-wasm (github.com/openscad/openscad-wasm) | Full OpenSCAD port to WASM; ES module; `await OpenSCAD({noInitialRun:true})`, FS.writeFile, `callMain([...cli args])`, FS.readFile. 428 stars, 99 commits; no releases listed; no npm package named on page | same as desktop (it is the C++ core) | | GPL-2.0 | FETCHED |
| openscad-playground (github.com/openscad/openscad-playground; npm openscad-playground "React component") | Browser app on a headless WASM OpenSCAD; README: "Licenses: see LICENSES" | | | not retrieved | SEARCH |
| SolidPython | Generator (Python emits .scad), not an evaluator | | | | KNOWLEDGE (not re-checked) |
| csgrs, synaps-cad | Named as related projects on openscad-rs README (geometry / interactive CAD) | not inspected | | | FETCHED (names only) |
No Rust crate found that evaluates OpenSCAD to a tree or mesh (SEARCH x2; scad-rs is the only candidate, maturity unknown). The only found evaluator is Python (openscad_evaluator).
## 2. OpenSCAD's own export (installed nightly)
LOCAL: `C:\Tools\OpenSCAD-2026.09.23-x86-64\openscad.com --version` -> `OpenSCAD version 2026.09.23` (git 28fe66bc). Default backend Manifold 3.5.2 (CGAL 6.1 available via --backend). `--info` Features list: roof, input-driver-dbus, lazy-union, vertex-object-renderers-indexing, textmetrics, import-function, object-function, predictible-output, vector-swizzle, discretization-by-error, ai-features, unicode-identifiers.
`--help` `-o` extension list: stl, off, wrl, 3mf, csg, dxf, svg, pdf, png, echo, ast, term, nef3, nefdbg, param, pov. `--export-format arg` overrides the format. `-o x.json` -> "Invalid suffix json" (no JSON export). `--summary-file` writes render stats as JSON (not a tree). `--help-export` lists -O settings (grep for csg/ast/json matched nothing). No library mode in help; `--enable` and `-D` exist. KNOWLEDGE: no libopenscad ships; wasm build drives it through callMain (see above).
Test file t.scad (10 lines): function sq, module m with for + translate, `let(a=3) m(a);`, hull{sphere,translate cube}, multmatrix+polyhedron, minkowski, linear_extrude(twist), rotate_extrude of translate+circle.
**.ast export** (out.ast, 558 B): pretty-printed source of the parsed (unevaluated) program. Keeps function/module definitions, `for`, `let`, calls, operators as text, with parentheses inserted: `function sq(x) = (x * x);` `for(i = [0 : (n - 1)]) translate(...)`. Nothing evaluated; named-argument order preserved. Text, not JSON.
**.csg export** (out.csg, 1267 B), evaluated tree as OpenSCAD text, all literals:
- functions, module definitions, `let`, for loops are gone. The for loop becomes `group() { group() { group() { multmatrix(...) { cube } ... } } }` with i*sq(2) folded into translation matrices (4, 8).
- `translate`, `rotate`, `scale`, `mirror` all become `multmatrix([[..4x4..]])` (translate([5,0,0]) -> matrix with 5 in column 4; mirror gives `-0` entries: `[-1, -0, -0, 0]`). No translate/rotate/scale/mirror survive.
- Kept as nodes with literal params: `hull() { sphere($fn=8,$fa=12,$fs=2,r=1); multmatrix(..){cube(size=[2,2,2],center=false);} }`, `polyhedron(points=[..], faces=[..], convexity=1)`, `minkowski(convexity=0){cube; sphere}`, `linear_extrude(height=2, twist=10, $fn=0,$fa=12,$fs=2){square(size=[3,3],center=false);}`, `rotate_extrude(angle=360,start=180,convexity=2,$fn=12,...){multmatrix{circle(...)}}`, `difference(){cube; multmatrix{cube}}`, `polygon(points=..., paths=undef, convexity=1)`, `import(file="nonexist.stl", origin, scale, center, convexity, $fn,$fa,$fs, timestamp=0)` (written even though the file is missing).
- Primitives are fully defaulted: `cube(size=[1,1,1], center=false)`, `cylinder($fn=5,$fa=12,$fs=2,h=1,r1=1,r2=0.5,center=false)`; `sphere(...)` gets `$fn,$fa,$fs,r`.
- Variables/list comprehensions: table `tbl=[for...]` consumed at evaluation; only resolved values appear. `children()` in module `wrap` expanded inline (`multmatrix` for the translate then `group(){...}`).
- Redundant `group()` wrappers appear with default settings (3 nested for the for-loop, extra for children).
- `--enable=all` changes the output (diff vs default): group wrappers collapse (lazy-union) and `$fe = 0` is added to sphere/linear_extrude/rotate_extrude/circle calls. Default and enable=all are different texts for the same program.
- Re-reading: `openscad o2.csg -o o2b.csg` succeeded, byte-identical size (1568 B both); emitted `WARNING: variable "timestamp" not specified as parameter in file o2.csg, line 37` for the import() line.
- No source line numbers, instance paths, names or ids appear in the .csg.
- `.param` export produced 13 bytes (customizer params); `.nef3` 256 KB (CGAL Nef dump of the geometry, not a tree).
## 3. Round-trip precedent: program + edits
- FreeCAD OpenSCAD workbench. FETCHED via search snippets only (wiki page itself returned an Anubis block): contains a CSG importer for OpenSCAD .csg and an exporter of a CSG-based tree; OpenSCAD itself must be installed to convert .scad to .csg first; non-CSG geometry is exported as a mesh; Ply required to parse .csg since 0.19. Sources do not say edits flow back to OpenSCAD variables (UNVERIFIED). https://wiki.freecad.org/OpenSCAD_Workbench
- CadQuery / CQ-editor: SEARCH. CQ-editor edits the script, with debugger and export. CQGI discovers user parameters through script metadata and the host supplies new values at build(). Sketch API has tag()/select(tags); a roadmap page (test mirror) lists tag propagation as planned. No built-in direct-edit-to-override found. https://cadquery-ja.readthedocs.io/ja/latest/cqgi.html , https://cadquery.readthedocs.io/en/latest/selectors.html
- build123d: SEARCH. Python CAD with builder/algebra/direct APIs; no override mechanism found. https://build123d.readthedocs.io/en/latest/
- Fusion 360: SEARCH (2014 Autodesk announcement). Timeline is a history of commands; direct operations such as edit form and patch are not captured in the timeline. Current behaviour not checked. https://www.autodesk.com/products/fusion-360/blog/january-2014-update-is-available/
- Onshape: SEARCH. Direct edits on imported geometry are captured as features in the feature list; a FeatureScript custom feature executes at regeneration and cannot modify the feature list (employee forum answer). https://www.onshape.com/en/blog/parametric-editing-vs-direct-editing-cad-systems , https://forum.onshape.com/discussion/20673/how-to-edit-the-code-of-a-shape
- Blender Geometry Nodes: SEARCH returned nothing on apply-vs-override; the manual states per-modifier input values on a shared group (https://docs.blender.org/manual/es/3.0/modeling/modifiers/generate/geometry_nodes.html). KNOWLEDGE (unverified): the modifier is non-destructive and edit-mode changes to the evaluated mesh are not stored back; "Apply" bakes the result; Blender 4.x adds a Bake node.
## Gaps
- scad-rs maturity/last commit and crates.io status unchecked; openscad-rs licence conflict (Apache/MIT vs GPL-3 on docs.rs) unresolved; openscad_evaluator last 27k chars of evaluator.md and TODO.md unread; no hull/minkowski/multmatrix correctness comparison run against OpenSCAD.
- The Python evaluator's CSGNode shape was not inspected; whether it keeps pre-flattened structure (names, call chains) beyond id_to_call_chain unknown.
- CSG export of `use`/`include`, `children(i)`, `$`-special vars, `text()`, `surface()`, `render`, `intersection_for`, modifiers (#, %, !) not tested.
- FreeCAD importer's per-node mapping (hull, polyhedron, minkowski) not retrieved. Fusion/Onshape/Blender current behaviour not verified from vendor docs.
- openscad-playground and openscad-wasm npm publication/licence text (LICENSES) not retrieved. SolidPython not re-checked.

View file

@ -1,100 +0,0 @@
<!-- Serves brush-tree forks seam-policy, uv-channels, bevel-operator, bevel-to-mesh and brush identity on faces. Eyes pass eyes-brush-tree-seams-bevels, 2026-10-10. -->
# Prior art: UV seams/parameterisation, SDF microbevels to mesh, Manifold ID/property tracking (Eyes, 2026-10-10)
Serves forks (campaign brush-tree, target brush-tree:target:r1; drafts in scratchpad\brush-tree-target\questions.md): `seam-policy`, `uv-channels`, `bevel-operator`, `bevel-to-mesh`, and `brush-identity` on faces (Manifold ID tracking, section C).
Extends GameCult.Geometry.Csg docs/research/brush-surfaces-and-sdf-bricks-prior-art.md (df89568) ([BS]); does not repeat it. Facts only; no conclusions.
Tags: FETCHED = page body read this pass (WebFetch returns a small-model extraction of the page, so quotes are extraction-mediated); SEARCH = search-result snippet/summary only; KNOWLEDGE = memory, unverified.
Fetch failures: iquilezles.org/articles/smin (404 again, also via http redirect), developer.valvesoftware.com Valve_Map_Format (403), projects.blender.org issue 47478 (403), Blender manual bevel page body (extraction returned navigation only), raw manifold.h/common.h (MeshGL struct not in them), Houdini tube/sphere pages (first 100k chars, no UV text), cgal.geids.org (dns).
## A. UV seams and parameterisation
### A1. Blender primitive UVs (source: bmesh operators bmo_primitive.cc, raw GitHub main) FETCHED
- Cylinder and cone are one path, `bmo_create_cone_exec`; UVs from `BM_mesh_calc_uvs_cone(bm, mat, rad2, rad1, segs, cap_ends, FACE_MARK, cd_loop_uv_offset)`.
- Cap layout: circles packed in lower half of the unit square when capped: `uv_center_y = cap_ends ? 0.25f : 0.5f`; top cap centre x 0.25, bottom 0.75; `uv_radius = cap_ends ? 0.24f : 0.5f`. Cap scale = `uv_radius / radius_top` (own radius per cap), measured in local space (via inverse matrix); top/bottom chosen by `dot(f->no, local_up) > 0`.
- Side strip: upper half when capped (`uv_height = cap_ends ? 0.5f : 1.0f`); U steps `1/segments` per face starting at `x = 1.0` and decreasing. No explicit seam marker in this code; seam is the start/end of the strip, and the extraction notes it depends on face iteration order.
- UV sphere (`bmo_create_uvsphere_exec`): poles at (0,0,+-rad); duplicate pole vertices merged with `remove_doubles`; pole fan triangles take U from atan2 of the average XY of the three vertices (`f->len == 3 && |x|,|y| < 0.0001`); `luv[0] = 0.5 + theta/(2pi)`, `luv[1] = 1 - phi/pi` (equirectangular; poles at V=1 and V=0). Seam at the atan2 branch cut; borderline coordinates "shifted to the left"; straddling faces get +1.0 on far vertices when U gap > 0.5; final pass subtracts global min U.
- UVs computed in local unrotated space before the matrix is applied.
- Torus: not in this file. SEARCH: Blender issue #47478 (new default UVs on some primitives "poor/missing") proposed equirectangular UV sphere and noted pole rings are triangles; torus UVs tracked separately in #47489 (not read, 403). SEARCH: helper functions declared in bmesh_operators.h: `BM_mesh_calc_uvs_grid/_sphere/_circle/_cone/_cube`; grid-seam fixes T52066 (2017), T67523 (2019, wrap threshold slightly below 1.0 so last column does not overlap first); T51180 (2017) made the UV-layer offset a parameter.
- Blender Geometry Nodes Cylinder node page: extraction returned only navigation (no UV text). Not read.
### A2. TrenchBroom (manual, trenchbroom.github.io/manual/latest, 256k chars; read chars 0-200000) FETCHED
- Format support: Quake standard and Valve 220. "Alignment lock in the Shear tool only works in Valve 220 format maps." Face attributes: plane, material, X/Y offset and scale, rotation angle.
- Projection: Paraxial = projection axis is the coordinate axis closest to the face normal (can shear slanted faces; "perfect alignment locking is impossible"). Parallel = projects along the face normal, offered only for Valve 220 maps. Shearing needs parallel projection.
- UV Lock (separate preference): when enabled, "will attempt to keep vertex UV coordinates the same" during vertex editing/face moves; alignment lock is "precise ... for all brush editing operations".
- Scale step default 0.1 per step (0.25 or 0.01 with modifiers); offset step = grid size; angle step 15 deg.
- CSG material rule: for CSG operations TrenchBroom "looks for an input face on the same plane and copies its material and attributes", otherwise applies the current material (paraphrase of extraction; no statement on cutter's-vs-target's surface for cut faces beyond this).
- Primitive creation (cylinder, cone, UV sphere, arch; scalable circles 12/24/48/96 sides): no text on how they are textured in the part read. Chars 200000-256434 not read.
- Valve 220 line syntax and default scale: page 403; remains KNOWLEDGE (see [BS] §A).
### A3. Unity ProBuilder (docs.unity3d.com/Packages/com.unity.probuilder, SEARCH snippets)
- Two UV modes: Auto (default; projects texture, Tiling scale value, "World Space" checkbox to align UVs to the world so same-angle faces line up, UVs "do not stay put when you move the object") and Manual (per vertex/edge). Auto UVs editable only per face; editing an edge/vertex converts to Manual; manual edits do not transfer back.
- No ProBuilder text found on texel density, tile-to-metre units, or cylinder/sphere-specific UVs. Pages not opened (workflow-uvs.html, auto-uvs-actions).
### A4. RealtimeCSG (Unity) SEARCH
- Asset Store text: "edit your texturing by dragging, rotating or scaling your material over the brushes". No documentation found on automatic UV method or cut-face UVs. Source-level behaviour of the successor Chisel is already in [BS] §A (per-surface 2x4 UV matrix, evaluated from final vertex positions; cut face = cutter's own base polygon surface, inferred). SabreCSG (comparison) lists per-face UV edit tools; not read.
### A5. Houdini FETCHED (negative) + SEARCH
- Tube and Sphere SOP pages (first 100k chars): no UV text. Sphere SOP: mesh-type sphere meridians meet at the poles; "Unique Points per Pole" controls whether meridians share one pole point or keep their own; "Triangular Poles" turns the degenerate pole quads into triangles.
- Houdini Boolean SOP UV/attribute transfer: see [BS] §A (SEARCH only); not extended.
### A6. OpenCASCADE / CadQuery (BRep parameter spaces) SEARCH
- `Geom_CylindricalSurface`: S(U,V) = Loc + R cos(U) X + R sin(U) Y + V Z; U in [0,2pi], periodic; V unbounded; XAxis gives U origin; a seam edge lies at U=0/2pi. (Location of the seam for `BRepPrimAPI_MakeCylinder` is the snippet author's inference from the parametrisation, not stated by a source.)
- `Geom_SphericalSurface`: U rotation about main axis, X axis gives U origin; V rotation about YDirection; poles at V limits; topology uses degenerate edges at both poles plus a seam edge joining them (tutorial, Chinese-language aliyun article, snippet). Toroidal: V increases about main axis, U chosen so D1U x D1V points outward; U/V ranges not returned.
- CADEX mesher option (SDK docs, snippet): stores UVs "of mesh nodes in parametric space domain of each B-Rep surface"; OCC forum: face-triangulation UVs can fall outside 0-1 and have very large values, fix is to normalise before export.
- Onshape/Fusion export (SEARCH): Onshape export presets (Coarse/Medium/Fine/Custom with distance/angular tolerance, max chord length) for glTF/OBJ/3MF; user reports of no `vt` in OBJ export, glTF UVs present but "no consistency in proportions"; Onshape emits a sub-mesh per face. Fusion reports conflict (one says cube exports with UVs, an older one says no texture info). No source for how planar/cylindrical faces are mapped.
### A7. Property/ID propagation through booleans (cut-face attributes)
- Manifold: section C.
- libigl `mesh_boolean` (SEARCH, header comments of a mirror at git.inf-cv.uni-jena.de/3DFaces/libigl_Martin): output `J` = "#FC list of indices into [Flist[0];Flist[1];...;Flist[k]] revealing 'birth' facet" (two-mesh overload: indices into [FA;FB]). PyMesh (SEARCH): per-face attribute `source` (0 = first operand) and `source_face` (combined input face index). Property interpolation across new vertices: not described in the snippets. Mirror is old; current upstream unconfirmed.
- CGAL Polygon_mesh_processing corefinement (SEARCH): `PMPCorefinementVisitor` concept with face hooks `before_face_copy` / `after_face_copy` (Boolean-specific, 5.1 docs), `before_subface_creations` / `after_subface_created` (face split during corefinement); edge hooks added from 5.3. Default is `Corefinement::Default_visitor`. No official example found propagating a property map through a Boolean.
- DarkRadiant subtract and Hammer texture lock: [BS] §A.
### A8. Not found (curved-primitive questions)
- Texel-density keeping across primitives: no source found in any tool above (ProBuilder gives Tiling/World Space only; Blender cap circle scaled to unit-square cell by its own radius, the side strip fixed to 1/segments per face). Cube-sphere/octahedral/torus/sweep sources: none fetched this pass. Blender torus UV code, Valve 220 spec, Blender Geometry Nodes primitive UV text, Houdini UV attribute on primitives: not read.
## B. Turning SDF microbevels into mesh
### B1. hg_sdf operators (mercury.sexy/hg_sdf/hg_sdf.glsl) FETCHED
- Header: "Stay Lipschitz continuous. That means: don't have any distance gradient larger than 1." "Underestimating distances will happen. That's why calling it a 'distance bound' is more correct." "The invariant is: each fSomething() function returns a correct distance bound."
- Operator section: the ops "are designed to actually produce correct distances or distance bounds, unlike popular 'smooth minimum' operators, on the condition that the gradients of the two SDFs are at right angles." "When they are off by more than 30 degrees or so, the Lipschitz condition will no longer hold (i.e. you might get artifacts)." Most take a radius `r`; some check `if ((-a < r) && (-b < r))` to limit influence and cost to a region.
- fOpUnionChamfer: `min(min(a,b), (a - r + b)*sqrt(0.5))`, "45-degree chamfered edge (the diagonal of a square of size r)". fOpUnionRound: `u = max(vec2(r-a, r-b), 0); return max(r, min(a,b)) - length(u)`, "quarter-circle". Correction term is zero when a and b are both >= r. fOpUnionSoft: "Similar to fOpUnionRound, but more lipschitz-y at acute angles (and less so at 90 degrees)"; `e = max(r - abs(a-b), 0); min(a,b) - e*e*0.25/r` (same as Dreams soft_min in [BS] §C); deviates from min only where abs(a-b) < r. fOpPipe `length(vec2(a,b)) - r`, "not a boolean operator". fOpEngrave `max(a, (a + r - abs(b))*sqrt(0.5))`. fOpUnionColumns: differs from min only where both a,b < r. fOpUnionStairs: "much less stupid version by paniq". Source states no bound property for Chamfer/Round/Engrave/Columns/Stairs individually; the only stated condition is the right-angle gradient clause above.
### B2. Smooth-min (Quilez) SEARCH, article body not read
- Search summary of iquilezles.org/articles/smin: exponential smin gives the same result regardless of chaining order (smin(a,smin(b,c)) == smin(b,smin(a,c))); polynomial and root variants do not; exponential generalises to n distances. Article's bound/exactness statements: not obtained. [BS] §C already holds the polynomial formula and a Shadertoy comment that smooth-min is not SDF-preserving.
- Sphere tracing and smooth union (SEARCH): a source says max and the smooth operators return a lower bound on true distance (never larger, sometimes smaller); a smaller step costs iterations rather than causing overshoot; overshoot arises when gradient exceeds 1. Segment-tracing paper (Galin et al., "Segment Tracing Using Local Lipschitz Bounds", CGF 2020) sets a global Lipschitz bound of 1.45 "to accommodate field compression"; blog (digitalfreepen.com) reports stepping up to ~50% faster than the derived limit before visible artefacts (~3x with Perlin noise added). A shader demo (gamedev.net shaderlab 28) describes polynomial smooth-min as leaving the surface slightly outside the intersection and adding a quadratic correction; gradient magnitude not analysed. Whether polynomial smin of two 1-Lipschitz fields can exceed gradient 1: not found in sources (KNOWLEDGE only).
### B3. Meshing implicit fields with sharp features
- Dual Contouring (Ju, Losasso, Schaefer, Warren, SIGGRAPH 2002) SEARCH: contours a signed grid with Hermite data (exact edge intersections and normals); avoids a separate feature-detection step; vertices at QEF minimiser per cell, made numerically stable; QEF also drives octree simplification. Schaefer-Warren follow-up: "mass points" for rank-deficient QEFs, handling of non-manifold sign configurations. Manifold DC (Schaefer, Ju, Warren): states DC "may generate non-manifold surfaces" (edge shared by >2 polygons; vertex neighbourhood not a disk). Thin features (sheet thinner than a cell): none of the retrieved sources discuss it. PDFs located but not read: cs.wustl.edu/~taoju/research/dualContour.pdf, people.engr.tamu.edu/schaefer/research/dualcontour.pdf, dualsimp_tvcg.pdf, dmc.pdf.
- "Dual Contouring of Signed Distance Data" (arXiv 2604.00157, 2026) FETCHED (html): reconstructs from SDF grid values without arbitrary function access or gradients; init: sign-change edge intersections by linear interpolation, normals from trilinear-interpolant gradients, cell vertex at Hermite-point centroid; outer loop re-assigns SDF samples to cells by closest mesh point, optimises vertex with distance term + DC Hermite term, refits Hermite data by PCA; inner loop linearised regularised quadratic; vertex may leave its cell. Reported failure modes: self-intersections and face flips when vertices leave cells; sensitivity to noise (chamfer error worse than some baselines at noise 0.01); isolated dents along feature curves; high Hermite weight unstable (spikes); manifoldness not guaranteed (future work). Fillets and thin features not discussed. Newton projection not used or mentioned.
- libfive (SEARCH, secondhand Curv mailing-list post, groups.google.com/g/curv/c/7hMmGQFav7w): libfive's mesher is "mostly manifold dual contouring, which snaps to sharp features and is watertight and manifold, but may contain self-intersections"; uses partial derivatives to place vertices on features, so integrations must supply derivatives. libfive's own docs/Keeter's paper not fetched this pass. Fidget/MPR details (JIT, tapes, autodiff normals) are in [BS] §D.
- nTop (docs.ntop.com, SEARCH): "Mesh from Implicit Body" tolerance step-down (start at 30-50% of minimum feature size); sharpening option, 3 iterations recommended start, `Sharpen Extents` to limit region; simplification and remesh options (growth rate 1.2-1.5 for fidelity; span angle and chord height controls); export target closed, edge-manifold, oriented, not self-intersecting; field health checked in a Field Viewer (parallel evenly spaced contours = healthy SDF, "Show Defects" for discontinuities; common field defects from edge-to-edge booleans, "fake zeros" from coincident surfaces, distorted fields from custom equations); booleans of distant bodies share one voxel grid so sharp edges may round off. No fillet/blend-field-specific meshing text found.
### B4. Manifold smoothing and fillet-like options (manifoldcad.org jsuser Manifold.html + wiki Manifold-Library) FETCHED
- `smoothOut(minSharpAngle=52.5, minSmoothness=0)`: edges sharper than minSharpAngle stay sharp; others smoothed to G1; flat faces of >=3 triangles stay flat; "minSmoothness ... The default gives a hard edge, while values > 0 will give a small fillet on these sharp edges"; 1 equals minSharpAngle 180.
- `smoothByNormals(normalIdx=0)`: fills halfedgeTangent vectors; geometry unchanged until `refine`/`refineToLength` interpolates; vertices where multiple disagreeing normals exist become sharp edges.
- `refine(n)`: splits every edge into n; new vertices coplanar unless halfedgeTangents are present, then moved to the interpolated surface by barycentric coordinates. `refineToLength`, `refineToTolerance` ("All vertices are exactly on the surface, within rounding error").
- Wiki: smoothing aims at G1 (G2 explicit non-goal); each edge gets two weighted cubic Bezier control points; sharpening takes half-edges with smoothness 0..1, "a crease is only formed at a smoothness of zero"; "The Boolean works only on flat triangles and ignores control points, so refine before booleaning curved meshes."
- `calculateNormals(normalIdx=0, minSharpAngle=52.5)`: fills normal properties; "Flat faces composed of three or more triangles will remain flat"; edges beyond the angle get different normals on each side; 0 gives faceted. Non-zero normalIdx "will not be supported in a future release".
### B5. Mesh-boolean-then-bevel practice
- Blender Bevel modifier (4.1 manual snippets, SEARCH; 5.2 page body not extracted): Harden Normals adjusts bevel-face vertex normals to match surrounding faces so flat neighbours stay flat and the bevel shades smoothly, needs custom split normals (auto-enabled; 2.8x required Auto Smooth); Mark Seam / Mark Sharp keep seam/sharp propagation consistent when bevelling edges that cross; Face Strength (None/New/Affected, older: All) sets strengths (new edge faces Medium, vertex faces Weak, Affected adds Strong on adjacent) for a following Weighted Normals modifier with Face Influence. UV handling of bevel faces: not found.
- Hard-surface game workflow (polycount threads and vendor guide, SEARCH; practitioner opinion): one smoothing group + bevels + weighted normals instead of hard-edge splits (fewer split vertices, cleaner normal-map gradients when a normal map is used); one poster found chamfer/support loops on the low poly bake less finely than a high-poly bevel and cost more polygons than hard edges when silhouette is kept; wavy reflections on fake bevels at low texel density; ordering: fix topology and bevel width, then hard/soft boundaries, then weighted normals last; "resolve Boolean intersections before beveling" (duplicate faces give unstable highlights); triangulate late and inspect long diagonals on non-planar faces; custom normals can be lost on FBX import; Blender GSoC 2018 thread: Weighted Normal modifier last in the stack; weighted-normal gradients baked into a low poly can cause a compensating gradient when baking from high poly. Hard Ops/Boxcutter: [BS] §C.
- CGAL/other "bevel on a mesh" operators: not searched.
### B6. Not found
- Newton projection onto f=0 for strip vertices as an established mesh-extraction technique: no source surfaced. Edge-strip emission at CSG cut edges from a field: none. Failure modes for overestimating bounds specifically in meshing: only the sphere-tracing material in B2 (rendering, not meshing).
## C. Manifold: properties and original-ID tracking through booleans
- Properties (wiki Manifold-Library, manifoldcad.org docs) FETCHED/SEARCH: arbitrary number of float channels per vertex ("merely float values indexed by channel number"); user decides meaning (colour, UV, normal); linearly interpolated across each triangle; vertices and property verts are not one-to-one: "along a Boolean intersection curve each vertex will have different properties on each side"; the design allows "sudden property changes, e.g. at Boolean intersections, without sacrificing manifoldness". Properties of separate input objects "are kept separate" so channel meaning need not agree across inputs. The pages do not state the interpolation rule at the new vertices of a boolean beyond this (FETCHED, negative).
- MeshGL fields (manifoldcad.org C++ docs, structmanifold_1_1_mesh_g_l_p.html) FETCHED: `numProp` ">= 3", `vertProperties` flat interleaved, first three always position; `triVerts` CCW from outside; `mergeFromVert`/`mergeToVert` optional (vertices to merge to reconstruct the manifold; same position, possibly different other properties); `runIndex` ("runs of triangles that correspond to a particular input mesh instance", start index divisible by 3); `runOriginalID` ("The OriginalID of the mesh this triangle run came from", several runs may share an ID); `runTransform` (3x4 column-major per run); `faceID` ("Optional: Length NumTri, contains the source face ID this triangle comes from"); `halfedgeTangent` (weighted XYZW for smooth Refine; empty = faceted); `tolerance` ("for mesh simplification"; edges shorter may be collapsed); `Merge()`.
- IDs (wiki + jsuser) FETCHED: each Manifold built from scratch gets a unique sequential `originalID`; after booleans each triangle refers back via `originalID` and `faceID`; `faceID` is an input triangle index, and for coplanar triangles "the index of the largest area triangle of that face is used as the faceID for all of those triangles"; each input instance appears as its own triangle run, sorted by originalID. `reserveIDs(n)`: "Returns the first of n sequential new unique mesh IDs for marking sets of triangles that can be looked up after further operations. Assign to Mesh.runOriginalID vector." `asOriginal()`: resets IDs so descendants reference this mesh; "also condenses all coplanar faces in the relation, and collapses those edges." `originalID()` returns the ID for an original, -1 for a product. JS guide for multi-material input: call `reserveIDs()` to give each material a unique originalID and sort materials into triangle runs.
- Boolean precision (wiki): no tolerance parameter; "does not account for any tolerance", uses symbolic perturbation; manifold carries internal epsilon; `MeshGL.Merge` takes a `precision` argument.
- Header manifold.h (raw, master) FETCHED: confirms `SetProperties(numProp, fn)`, `CalculateNormals`, `Refine*`, `SmoothByNormals`, `OriginalID`, `AsOriginal`, `ReserveIDs`; no behaviour text in the header. Error enum includes `RunIndexWrongLength`, `FaceIDWrongLength`, `PropertiesWrongLength`, `InvalidTangents`.
- OpenSCAD: Manifold is the default backend (announcement quoted in SEARCH result; CGAL via `--backend=cgal`); 3MF coloured export reported working on the Manifold backend (Fabbaloo/mailing-list snippets). How OpenSCAD maps colours onto originalID/runOriginalID: not found (OpenSCAD source not read).
- Local note: the mapping from a brush to a Manifold originalID would be a user choice of `reserveIDs` per input; nothing in the pages read assigns IDs per brush automatically.
## Gaps for a follow-up
Quilez smin article body (bound/exactness per variant); Valve 220 spec text; Blender torus/cube UV code and Geometry Nodes primitive UV text; Blender bevel manual body and bevel UV behaviour; DC paper body (thin features, QEF limits); libfive docs; sphere/cube-sphere/octahedral/torus/sweep UV sources; texel-density-across-primitives sources; Manifold interpolation rule for properties at new boolean vertices (source code `src/boolean3` / `boolean_result.cpp` unread); OpenSCAD colour-to-originalID mapping; libigl upstream `J`; CGAL property propagation example; Newton-projection-onto-isosurface literature.

View file

@ -1,92 +0,0 @@
# SDF authoring text: prior art
Fork: campaign `brush-tree`, question `openscad-surface` (map rows "Parametric layer" and "Authoring text", `docs/brush-tree-map.md`; invariants `text-round-trips` and `text-is-a-surface`, `docs/brush-tree-target.md`; GameCult.Geometry.Csg branch `eureka/brush-tree`, commit a7879dad). Gathered by Eyes agent `eyes-bt-sdf-text`, session `self-2026-10-11-ag2`, 2026-10-12. Facts with pointers; lines marked "(inferred)" are not facts from a source.
Method limits: web pages were read through a summarising fetcher, not raw. Several raw-file fetches returned 404, so items under "Not verified" (section 5) were not read. The mind query for `brush-tree:question:openscad-surface` returned the whole store with no match for that id, so the question text itself was not read; its content is taken from the map and target docs. The commit is in `F:\Projects\GameCult.Geometry.Csg`, not in the CultLib checkout.
## 0. The local side (what the text must express)
- `src/field.rs` @ a7879dad: `Add` = `min`, `Subtract` = `d.max(-b)`, `Intersect` = `d.max(b)` (`stream_distance`), all f32 (bevy_math). Primitive distances: oriented box exact; cylinder exact; ellipsoid is a bound; floret arm a max-of-planes bound. No smooth union exists in the file at this commit.
- Target needs (`docs/brush-tree-target.md`): primitives, transforms, scoped booleans, smooth unions with a named radius, creases as hard `max`, per-part `field`/`kernel` declaration. Map row Brush: identity "carried in the text as a named argument, plus a human name".
- Map row "Authoring text (OpenSCAD-shaped)": not persisted as truth; header names tree key and base revision; parsed back only through the commit primitive; stale base refused.
- OpenSCAD has no smooth union. Searched: no native OpenSCAD smooth-union feature or PR was found; mailing-list threads (lists.openscad.org) point to `minkowski()` and hull-based fillets; BOSL2 offers `round_corners`, `offset_sweep`, `rounded_prism` (rounding of paths and prisms, not a field blend; github.com/BelfrySCAD/BOSL2 wiki rounding.scad).
## 1. Survey
### libfive
- Syntax: Scheme (Guile) `(union a b)`, `(blend-expt a b m)`; Python bindings from the same stdlib; C API; Studio GUI (OpenSCAD-like live coding).
- Round trip: shapes evaluate to an expression `Tree`; no documented text print/parse of the tree. The Scheme source is the text.
- Smooth: `blend_expt(a,b,m)` ("using exponents"), `blend_expt_unit`, `blend_rough` ("may not preserve gradients"), `blend_difference(a,b,m,o)`. Hard: `union`/`intersection`/`difference` (variadic; difference = intersection with inverse of the union of the rest). Formulas not read (header comments only).
- Licence: libfive, stdlib, Python bindings MPL-2.0; Guile bindings and Studio GPL-2.0+. README copyright 2015-2021, no status statement; Guile bindings Linux/macOS only.
- Rust: community bindings listed in README, not checked. Vocabulary: large stdlib.
- Source: github.com/libfive/libfive README; stdlib Guile csg.scm; libfive_stdlib.h.
### Fidget (Matthew Keeter)
- Syntax: Rust library; Rhai scripts via `fidget-rhai` 0.5.1: `circle(#{ center: [1, 2], radius: 3 }).move(#{ offset: [1, 1] })`, `difference(a, b)`, `union(a, b, c, ...)`, `axes()` returns x,y,z; operators on trees build nodes rather than compute. `fidgetpy` (Python) and a third-party Koto fork exist (README).
- Round trip: `Context::from_text` parses a flat node-per-line form (address, op name, operand addresses; `var-x`, `const 1`, `square <addr>`); docs.rs: "loosely defined and only intended for use in quick experiments". `Context` ops: add sub mul div modulo min max atan2 mix compare and or, neg recip abs sqrt square sin cos tan asin acos atan exp ln floor ceil round not rand, `if_nonzero_else`, `deriv`. Canonical tape bytecode exists (README). A Rhai script is a general program (loops, conditionals) evaluated once; no printer from tree back to script is documented.
- Smooth: no smooth-min builtin seen in `Context`; the docs.rs rhai page lists only `union`/`difference` and does not mention min/max/blend. Smooth forms would be composed from the math ops. Crease = `max`.
- Licence MPL-2.0. Maintenance: sole maintainer, "personal-scale experimental project"; README recommends pinning 0.4.1 to avoid LLM-influenced code; unlikely to merge changes with substantial maintenance burden.
- Rust: native (`fidget` on crates.io; JIT aarch64/x86_64; Manifold Dual Contouring meshing). Rhai vocabulary on docs.rs: circle, sphere, move, union, difference, vec2/vec3, sin/cos, constants (partial list).
- Source: github.com/mkeeter/fidget; docs.rs/fidget-rhai; docs.rs/fidget Context.
### Curv
- Own pure functional language; shapes are first-class values; GPU render, mesh export STL/OBJ/X3D. `lib.blend`: "14+ blending kernels" (Codeberg README); names not read.
- Round trip: not documented. Licence Apache-2.0 (GitHub sidebar). GitHub repo archived 2023-09-27, development moved to Codeberg (codeberg.org/doug-moen/curv, maintained by Tim Ayres; most recent commit shown 2026-09-26). No Rust.
### fogleman/sdf (Python)
- Syntax: `sphere(1) & box(1.5)`, `a | b.k(0.25)`, `union(a, b, k=0.25)`, `.translate((1,2,3))`; `@sdf3` / `@op3` decorators for custom ones.
- Round trip: none; the Python expression is the text; no serialisation mentioned.
- Smooth (dn.py): union `h=clip(0.5+0.5*(d2-d1)/K,0,1); m=d2+(d1-d2)*h; d=m-K*h*(1-h)`; difference `h=clip(0.5-0.5*(d2+d1)/K,0,1); m=d1+(-d2-d1)*h; d=m+K*h*(1-h)`; intersection `h=clip(0.5-0.5*(d2-d1)/K,0,1); m=d2+(d1-d2)*h; d=m+K*h*(1-h)`. Hard: np.minimum / np.maximum / `max(d1,-d2)`. `K = k or operand._k`, so `k=0` is falsy and falls to the operand's `_k` or the hard form. Separate `blend` is linear `K*d2+(1-K)*d1`, default 0.5.
- Vocabulary (about 60 names): 3D primitives sphere box rounded_box wireframe_box torus capsule capped_cylinder rounded_cylinder capped_cone rounded_cone ellipsoid pyramid; 4 Platonics; plane slab cylinder; text image; translate scale rotate orient; union difference intersection and smooth versions; repeat circular_array; blend dilate erode shell elongate twist bend bend_linear bend_radial transition_linear transition_radial wrap_around; extrude extrude_to revolve slice; 2D set.
- Licence MIT. 209 commits, 28 open issues, 8 open PRs; last-commit date not shown. No Rust.
### hg_sdf (Mercury)
- GLSL library: primitives, domain folds (`pMod1`, `pModPolar`, ...), booleans in rounded, chamfered, columned and stairs variants: `fOpUnionRound`, `fOpUnionChamfer`, `fOpUnionStairs`, `fOpPipe`, `fOpEngrave`. `fOpUnionSoft` not on the fetched page.
- Not a language; no tree. Licence "MIT OR CC-BY-NC-4.0" (page, 2021-07-28 entry). No Rust.
### Inigo Quilez (distfunctions, smin)
- GLSL snippets. From the smin article (via search summary): exponential, power, root, polynomial families; polynomial `h=max(k-|a-b|,0)/k; min(a,b)-h*h*k/4`, equivalent to the clamped-`h` form. "opSmoothUnion" is a library name (Houdini HDK, pyssv); pyssv uses a cubic `h^3*k/6`. Smooth intersection is the negated smin of negations. Code licence not found; distfunctions page fetch returned 404.
### Shadertoy convention
- `float map(vec3 p)` returning `min(...)` or `smin(a,b,k)` copied from IQ; crease by `max`. Hand-written code; no round trip; per-shader licence. (From general knowledge, not fetched.)
### OpenSCAD and BOSL2
- Tree-structured CSG text; OpenSCAD's own `.csg` export prints a CSG tree (general knowledge, not fetched). No smooth operation; see section 0.
### ImplicitCAD (Haskell)
- Haskell DSL plus `extopenscad`, an OpenSCAD dialect with extensions (README): rounded union `union(r=14){...}`; `linear_extrude` accepts `r`; curried functions, `map`. Haskell API: `unionR`, `differenceR`, `intersectR :: ℝ -> [obj] -> obj` (radius first); internal constructors `UnionR`, `DifferenceR`, `IntersectR` in 0.4.1.0 source. README shows rounded union only; rounding formula not read.
- Licence AGPL-3.0+ (source headers on Hackage). README status dated 2020-01-26, "very much a work in progress"; hull and minkowski unfinished. No Rust.
### sdfx (Go)
- Go code: `Union3D(sdf ...SDF3)`, `Difference3D(s0, s1)`, `Intersect3D`, `Transform3D(sdf, M44)`; `SetMin(MinFunc)` / `SetMax(MaxFunc)` replace the min and max a boolean uses (default `math.Min`/`math.Max`). Smooth-min type names not found in the visible file. Licence MIT; 850 commits, 12 open PRs; latest commit date not shown. No tree text. No Rust.
### Blender geometry nodes SDF
- SDF Grid Boolean (Intersect, Union, Difference), SDF Fillet, SDF Grid Offset, Mean, Median, Laplacian, Mean Curvature, Mesh to SDF Grid, Grid to Mesh (Volume Nodes manual, 5.1; 5.3 adds Deactivate Voxels and Grid Topology Boolean). Node graphs live in .blend datablocks, scriptable by `bpy`; operate on voxel grids, not closed-form distance. Licence GPL. No smooth-union parameter read on the Boolean node.
### MagicaCSG, Womp
- Closed GUI products; not fetched. Nothing verified.
## 2. Findings that bear on the fork
1. No surveyed tool documents a typed field tree printed to a text surface and parsed back as its canonical form. Nearest: Fidget `Context::from_text`, a flat math-node DAG (min, max, sqrt, ...), self-described as experiment-only; it carries no primitives, transforms or named radii.
2. libfive, Fidget-Rhai, fogleman, Curv and sdfx authoring surfaces are general programming languages evaluated to a value. A printed typed tree would be a restricted subset of such a language (inferred); none documents a canonical printer.
3. Smooth union is a binary operator with a scalar width everywhere: fogleman `a | b.k(k)` / `union(a,b,k=)`, ImplicitCAD `union(r=)` / `unionR r [..]`, hg_sdf `fOpUnionRound(a,b,r)`, libfive `blend_expt(a,b,m)`, IQ `smin(a,b,k)`.
4. The blend function differs per tool, so a radius number is not interchangeable: quadratic `k*h*(1-h)` (fogleman, IQ polynomial), cubic `h^3*k/6` (pyssv), exponent-based (libfive), round/chamfer/stairs (hg_sdf). "Named radius" means whatever `src/field.rs` implements.
5. Crease is hard `max`/`min` in all of them; no surveyed text language has a separate crease token.
6. Precedent for an OpenSCAD-syntax dialect carrying rounded union: ImplicitCAD `extopenscad` `union(r=...)`. README shows no rounded difference or intersection there; Haskell AGPL; 2020 status note.
7. None of the surveyed surfaces has a first-class stable-id slot for nodes; named arguments are available in OpenSCAD, Python and Rhai (`#{k: v}` maps) (inferred carrier).
## 3. Evaluating exactly our distance function (invariant one-distance-per-primitive)
- Libraries with their own primitives (libfive, Fidget stdlib, fogleman, hg_sdf, sdfx, Curv) compute their own formulas. Our ellipsoid and floret are explicit bounds in `field.rs`; library ellipsoids are different formulas (inferred; fogleman's ellipsoid not read).
- Fidget could evaluate our distance only if each primitive is written out in its math nodes (it has sqrt, min, max, abs, mul, compare, if_nonzero_else, exp, ln). Whether its interpreter and JIT backends give bit-equal f32 results to our `bevy_math` code was not examined (inferred risk).
- A text front end that only parses into our typed tree and leaves evaluation to `src/field.rs` has one distance by construction (inferred); the other systems then serve as syntax prior art only.
- fogleman's smooth forms are plain arithmetic and can be compared term by term with a Rust implementation of the same quadratic form.
## 4. Rust availability
- Fidget and `fidget-rhai`: yes (crates.io, MPL-2.0, file-level copyleft). Rhai is itself a Rust embedded scripting engine (used by fidget-rhai). libfive: C++, community Rust bindings listed, unchecked. Others: none. Rust OpenSCAD parser crates were not surveyed.
## 5. Not verified
- libfive `blend_expt`/`blend_rough` formulas; Curv `lib.blend` kernel names; sdfx smooth-min type names; IQ code licence; hg_sdf `fOpUnionSoft`; ImplicitCAD rounded difference/intersection syntax and blend formula; full Fidget Rhai function list; whether `fidgetpy` exposes smooth ops; MagicaCSG and Womp text forms; BOSL2 and OpenSCAD licences as fetched; OpenSCAD `.csg` export behaviour; Shadertoy conventions (from general knowledge); the text of `brush-tree:question:openscad-surface`.

View file

@ -1,20 +0,0 @@
# What a finished hull part is (operator, 2026-10-10)
The bar the brush-tree output is judged against. Operator's words:
> A part is done for me when its topo flow is reasonably clean and unbroken, and I try to unwrap such
> that parts of the same object stay together with minimal distortion. Bevels are used to sharpen edges
> (sharp shading requires duplicating geo anyway, while a single faced bevel shades flawlessly with
> face-weighted normals. See this test render showing a simple bevel, face weighted simple bevel, and
> expensive perfect bevel next to each other, and a little test box showing off that and decals for that
> Star Citizen effect)
`operator-bevel-test.webp`, left to right: single-segment bevel with plain normals; the same bevel with
face-weighted normals (the target look); a multi-segment "perfect" bevel (too expensive); a test box
with face-weighted bevels and decals (decals are the separate panel-decals campaign).
Measurable reading of the bar:
- Topology: clean, unbroken edge flow (quad-dominant loops, few poles, no triangle fans or slivers).
- UVs: islands follow the object's parts, kept together, with minimal stretch.
- Edges: one-segment bevels on hard edges, shaded with face-weighted normals; no duplicated split
geometry for sharpness, no multi-segment bevels.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View file

@ -1,668 +0,0 @@
# Spike: boolean cut and split on the Dexter Quiet hull, welded by the operator's rule
Campaign brush-tree, target r3 (invariants weld-rule, detail-is-boolean, two-layers, shippable-stack). Serves rulings
`boolean-cleanup-tighter-curve-wins` and `panel-lines-by-split-boolean-r2`. Plan: Imagination's `spike-boolean-weld.md`.
Agent `spike-bt-boolean-weld`, 2026-10-12. A spike, not a cut: the code under `spikes/boolean-weld/` is scratch.
Her words, the spec of the rule: "the surface with the tighter curve defines the silhouette of the cut, and the spare
vertices that come from the flatter surface all get merged away."
## Round 4: Self's calls (a) to (f) under target r4
Agent `hands-bt-weld-spike-r4`, session `self-2026-10-13-ag`, 2026-10-13. Target r4 (`brush-tree:target:r4`, doc
@ 40faab6) is in force; rulings `weld-owner-local-corners-kept`, `brush-modes-add-subtract-split`,
`weld-runs-as-live-result-object`, `blender-evaluates-cages`, `paint-after-commit`; follow-up
`aetheria-release:follow_up:weld-result-uv0-ownership`.
How it was run (all on Starfire, Windows; one job at a time):
- **Input:** Blender 5.2.2's evaluated mesh without the Bevel, as before (`fixtures/hull_nobevel.obj`). For UVs,
`py/export_uv.py` gives the hand cage a stand-in UV0 (Smart UV Project on the cage, 66 deg, mirrored half offset
by 1 in U; she has no unwrap yet: the cage's `UVMap` layer was there but unused) and exports the evaluated mesh with
per-corner UVs (`fixtures/hull_uv.obj`). Its positions equal the fixture's to 2e-7 (the OBJ's 7 decimals): 3,010
vertices, 3,008 quads, 11 UV islands. Nothing was saved to any .blend.
- **Kernel:** boolmesh 0.2.1 through the spike driver, now with `add` (union).
- **CDT:** the driver's new `cdt` subcommand over **spade 2.15.1**. Its crate metadata reads
`license = "MIT OR Apache-2.0"` (`Cargo.toml` of the downloaded crate; `cargo info spade` agrees). Settings:
angle limit 20 deg, `keep_constraint_edges()` (no constraint edge is split), `exclude_outer_faces(true)`. The driver
asserts that spade keeps the input vertices in order and unmoved. 177 calls, all `refinement_complete`, 1,051
Steiner points. Triangle is gone from the spike: `py/round3.py` is deleted and `py/round4.py` replaces it.
- **Weld:** `py/round4.py` with guard floor 5 deg; `py/owner_weld.py` without the `snap` kind (b).
- **Finishing and renders:** `py/render_blender.py` headless, the cage's own Bevel and Weighted Normal settings
(0.001, one segment, clamp overlap on), Workbench. Loop edges and hull crease edges carry bevel weight 1; brush
edges carry none in these renders (see the add-mode note).
- **Tables:** `spikes/boolean-weld/results/round4/` (`table4.md`, `measures4.json`, `bevel_uv.json`).
### (a) Slide onto the seam: flat split before and after
Look at these first: same pipeline (short seam merge, flips, spade cap CDT), without and with the slide.
![flat split, before and after the slide](spike-boolean-weld/r4_split_r4.jpg)
![flat split wide, before and after](spike-boolean-weld/r4_splitwide_r4.jpg)
![flat split wire, before and after](spike-boolean-weld/r4_splitwire_r4.jpg)
![crease box, before and after](spike-boolean-weld/r4_crease_r4.jpg)
What the slide does, as built:
- A hull vertex H, never a crease vertex, a corner or a UV0 seam vertex, whose own edges cross the seam at
consecutive seam vertices C(H), slides inside its own surface to the nearest point M of the seam between them,
if that is closer than the bevel width. C(H) merge into it, so the seam passes through H's new position.
- M lies on the intersection, so the seam stays on the plane (off-plane 0 in every row). In the other piece of the
split (where H is absent) the first C(H) vertex moves to M and the rest merge into it. The two seams stay identical
(crack 0.0).
- Every operation is judged on its end state over both pieces: no touched face inverts, and the worst angle over
the touched faces (deleted ones included) does not end below 5 deg if it got worse. Round 3's per-step guard
refused all 7 candidates on the split, because the first step (moving H) flattens a face that the merges then
delete. The short seam merge now uses the same guard.
- Round 3's diagnosis was half wrong. The split's 0.68 deg needle is two boolean-inserted seam vertices 0.00034
apart (crossings of two edges of one hull vertex), not a hull vertex on a seam vertex. That hull vertex lies on the
removed side, in the other piece. The short merge (round 3's call, now with the end-state guard) or the slide
removes it.
| flat split | variant | hull slivers | hull worst deg | hull p5 | cap slivers | cap worst | seam-adjacent edges < bevel width | shortest seam-adjacent edge | crack |
|---|---|---|---|---|---|---|---|---|---|
| panel split, subtract piece | naive | 37 | 0.68 | 3.9 | 60 | 0.058 | 15 | 0.00026 | 0 |
| | before (no slide) | 23 | 1.37 | 5.7 | 6 | 7.6 | 6 | 0.00030 | 0 |
| | after (slide) | 19 | 1.37 | 6.8 | 8 | 6.8 | 4 | 0.00030 | 0 |
| panel split, intersect piece | before | 16 | 2.51 | 6.7 | 6 | 7.6 | 5 | 0.00079 | 0 |
| | after | 18 | 2.51 | 5.6 | 8 | 6.8 | 5 | 0.00079 | 0 |
| crease box, subtract piece | naive | 22 | 0.38 | 3.7 | 22 | 0.033 | 12 | 1.5e-5 | 0 |
| | before | 12 | 2.36 | 6.8 | 0 | 6.9 | 6 | 1.3e-4 | 0 |
| | after | 9 | 2.51 | 7.3 | 0 | 7.4 | 1 | 9.9e-4 | 0 |
| crease box, intersect piece | before | 10 | 1.40 | 7.1 | 0 | 6.9 | 5 | 1.5e-4 | 0 |
| | after | 7 | 2.08 | 7.3 | 0 | 7.4 | 0 | 1.0e-3 | 0 |
- **Moves:** split 4 slides (10 seam vertices merged), crease box 3 (6 merged). The largest move is 0.00087 on the
split and 0.00084 on the crease box, below the 0.001 bevel width. Each moved vertex stays on its own hull edges.
- **Without UV0 seam protection** the split took 5 slides and the subtract piece reached worst 2.1 deg, 2
short edges. The extra slide sat on a UV0 seam and tore the UVs (stretch 820x), so the protected run is the one
reported. Geometry and UV seams compete at that one spot.
- **Render:** the slide removes the dark smear at the lower end of the seam. A light wedge remains in the middle.
It sits where seam-adjacent edges stay under the bevel width and the overlap clamp narrows the groove.
- **Her reading to confirm:** the moved vertices are the hull's own (the owner's) on a flat split. They keep their
identity and leave their position by under the bevel width. Her words were "so they survive". The pair above is
for her eye.
### (b) No snapping on any stretch
`owner_weld.py` no longer has the `snap` kind or its knobs. Along-loop merges (`loop`) and, where the cutter
owns, `ring` merges remain.
| case | variant | hull slivers | hull worst | silhouette dev max % | corners kept |
|---|---|---|---|---|---|
| ownership switch r 0.08 | round 3 (snap on hull stretches) | 53 | 1.2e-6 | 0.323 | 4 / 4 |
| | owner-r4 (no snap) | 44 | 1.2e-6 | 0.0196 | 4 / 4 |
| | r4 (+ slide, flips, facet CDT) | 23 | 0.144 | 0.0196 | 4 / 4 |
| gun port 16 | owner-r4 | 5 | 5.09 | 0.0198 | |
| | r4 | 4 | 5.40 | 0.0198 | |
- The switch probe's loop no longer moves on hull-owned stretches: 0.32 % to 0.0196 %. The 0.0196 % is the
cutter-owned `loop` merges, as in round 1.
- The switch keeps a needle on the owner side (0.14 deg after flips). The port is unchanged.
![port](spike-boolean-weld/r4_port_r4.jpg)
![switch](spike-boolean-weld/r4_switch_r4.jpg)
### (c) spade replaces Triangle
- spade caps match or beat round 3's Triangle caps:
- split: cap worst 0.058 to 6.8 to 7.6 deg (round 3, Triangle: 5.5), cap slivers 4 to 8;
- crease box: cap slivers 0, cap worst 6.9 to 7.4 deg (round 3: 3 slivers, 0.71 deg).
- No seam vertex was moved or split (driver assertion; seam off-plane 0; crack 0).
- Licence: MIT OR Apache-2.0, quoted above. It satisfies `no-unownable-tools-in-loop`.
### (d) Planar brush facets re-triangulated
Each planar brush facet the hull trimmed (one holding a loop edge) is re-triangulated in its own plane by spade.
Its boundary edges (loop and brush edges) are constraints, with interior Steiner points only. No boundary edge
was lost in any case (`constraint_edges_missing` 0), so loop and brush edge weights survive.
| case | variant | brush slivers | brush worst | brush p5 | seam-adjacent edges < bw |
|---|---|---|---|---|---|
| box across ridge | naive | 48 | 0.070 | 0.41 | 4 |
| | r4 (facet CDT, Steiner inside only) | 48 | 0.19 | 0.47 | 3 |
| | r4 + split brush edges (variant) | 5 | 4.91 | 18.1 | 5 |
| box in flank | naive / r4 / split variant | 1 / 0 / 0 | 0.29 / 6.9 / 13.7 | | 0 |
| add box | naive / r4 / split variant | 4 / 1 / 0 | 3.0 / 5.0 / 17.8 | | 0 |
| switch (cylinder walls) | owner-r4 / r4 / split variant | 11 / 16 / 36 | 0.06 / 0.30 / 0.30 | | 16 / 22 / 28 |
- **As called, (d) does not fix the ridge box.** Its 48 slivers are fans from a dense trimmed loop edge to the two
ends of one long brush edge. Under `keep_constraint_edges` no Steiner point may land on that edge, so spade added
only 3 points.
- **The variant** puts evenly spaced collinear points on long brush edges (never loop edges), shared by both facets
on that edge. It removes them: 48 to 5 slivers, worst 4.9 deg. The edge stays where it is and both halves keep its
weight. The neighbouring untrimmed facet is re-triangulated too.
- **On the curved cutters' planar wall quads it makes things worse** (switch 11 to 16 to 36 slivers), and more
seam-adjacent edges fall under the bevel width. Those quads are narrow strips, and the sliver test flags any
triangulation of them.
![box across ridge: naive, r4, split brush edges](spike-boolean-weld/r4_boxridge_r4.jpg)
![box across ridge, wire](spike-boolean-weld/r4_boxridgewire_r4.jpg)
![box in flank](spike-boolean-weld/r4_boxflank_r4.jpg)
### (e) Add mode
Cases: an oriented box raised panel (half-width 0.025, 0.02 proud) at the flank box's placement, and a 16-segment
cylinder boss (r 0.015, 0.02 proud) at the gun port.
| case | variant | hull slivers | hull worst | brush slivers | brush worst | seam-adjacent edges < bw | corners kept |
|---|---|---|---|---|---|---|---|
| add box | naive | 10 | 0.44 | 4 | 3.0 | 0 | |
| | owner-r4 | 10 | 0.44 | 2 | 3.0 | 0 | 4 / 4 |
| | r4 | 4 | 2.37 | 1 | 5.0 | 0 | 4 / 4 |
| add cylinder | naive | 19 | 0.11 | 16 | 0.15 | 6 | |
| | owner-r4 | 4 | 3.92 | 0 | 7.6 | 0 | |
| | r4 | 4 | 5.47 | 0 | 7.8 | 0 | |
- **The same rule holds on the union loop.** The hull owns every stretch of the box (28 hull-owned loop vertices,
its 4 corners protected) and the cylinder owns its loop (38 / 38), exactly as for subtract.
- **The concave loop's bevel:** 25 strips on the box loop and 28 on the cylinder loop, none collapsed, no edge
under the bevel width, so the clamp does not narrow it.
- **The render's box walls are smooth-shaded into the floor**, a dark gradient. The brush's own edges carry no
weight in this render, and Weighted Normal smooths across the 90 deg box edges. Production gives brush edges
their row weight (`bevel-weights-carry`). This is a render-setup gap, not a weld result.
![add box: naive, r4, loop owners](spike-boolean-weld/r4_addbox_r4.jpg)
![add cylinder](spike-boolean-weld/r4_addcyl_r4.jpg)
### (f) UV0 carry-through
The operator asked: "Isn't every face present in either mesh operand?"
- **Yes, out of the boolean.** Every one of every naive result's faces (4,404 to 6,238 per mesh, 11 meshes) is a
fragment of one operand face: a hull quad or a planar brush facet. Each takes its UVs exactly by barycentric
interpolation in that face. boolmesh carries only one per-corner scalar, so the spike rebuilds UVs from face
provenance plus position. Manifold would carry UVs as properties.
- **After the weld, some faces are no longer single-face fragments.** Merges, slides and flips join corners from
two neighbouring operand faces: 5 to 88 hull faces per mesh (`not_fragments_hull`). Each corner still lies on the
hull surface and takes its UV from the operand face it lies on.
- With UV0 seams protected, their stretch stays within 0.25x to 3.9x of the hull's density on the panel split, box
cut-ins and adds. The crease box's intersect piece has one face nearly collapsed in UV (sigma_min 3e-5). Before protection the split reached 820x.
- The switch (88x) and the crease notch (109x) still show it: cutter-owned `loop` and `ring` merges there pull a
corner across a UV seam that no protected vertex marks.
- **Hull UV0 islands:** unchanged in count on the gun port, box cut-ins, adds and the switch (11 of 11). Hull-side UV
flips are 0 everywhere except the crease box's intersect piece (1).
- The crease box's pieces gain islands: 11 source islands appear as 14 and 4 in place of 11 and 3. Some welded
faces there no longer share UVs with their neighbours along an edge.
- Not solved: the protection covers seam crossings and seam vertices, not every corner that a merge pulls into
another island's face.
- **Brush-side UV flips** (58 to 433) are subtract and split caps whose faces are reversed relative to the operand
face. Their UVs are mirrored relative to the face. A brush chart for a cut wall needs a mirrored placement or a
flip at assignment.
- **Split caps:** both pieces' caps are fragments of the same cutter face, so carried UVs coincide exactly (max
difference 1e-15) and the two caps overlap in UV0. `uv-bar` wants unique UV0, so one cap's chart must be placed
apart. That is a placement rule, and the follow-up's question.
- **Bevel strips** (`bmesh.ops.bevel`, the modifier's own BM bevel, with its settings, on a copy; loop strips only):
- Every strip on every mesh takes the hull side's UVs (spans: hull 100 %, including hull-to-cap strips on
splits). That holds wherever this spike looked, though BMesh documents no rule for which side a strip takes.
- Stretch, relative to the hull's density: the narrow direction is compressed (sigma_min median 0.46 to 0.81;
p5 0.22 to 0.67). The long direction is near 1 (sigma_max median 0.95 to 1.22; p95 1.05 to 2.9).
- Outliers: 19.8 on the split's intersect piece, 96 to 156 on the crease box, at strips beside the welded faces
above. No strip collapsed except 1 on the crease box.
- So strips need no UV placement of their own. They inherit a slightly compressed copy of the hull chart, which a
livery mask sampling UV0 will show as the hull's paint running into the groove.
- The one measure that failed: the switch's owner-r4 mesh returned zero density (`collapsed` 80 of 80). It is a
measuring fault on that mesh; its r4 row is normal.
![UV grid across the split seam](spike-boolean-weld/r4_splituv_r4.jpg)
![UV grid on the adds](spike-boolean-weld/r4_adduv_r4.jpg)
### Did each call hold
- (a) Built and measured. The split's needle goes (0.68 to 1.37 deg; edges under the bevel width 15 to 4) and
the crease box's short edges go (12 to 1 and 0). It is not uniform yet: one slide is refused to keep UV0 whole, and
the light wedge remains. Her eye decides the reading.
- (b) Held. The loop no longer moves on hull stretches.
- (c) Held. spade (MIT OR Apache-2.0) replaces Triangle and does as well or better.
- (d) Held as specified but does not fix the ridge box. It needs brush edges split collinearly (variant measured:
48 to 5 slivers).
- (e) Held. The rule and corners work on the union loop, and its concave bevel is clean.
- (f) Answered yes for the boolean. The weld must not move UVs across UV0 seams: partly solved here.
### Open questions for Self (round 4), with a recommendation each
1. **Brush edges split for the CDT.** May the facet CDT put collinear points on a brush edge (never a loop or crease
edge), shared by both facets, each half keeping the weight? Recommendation: yes for planar brush facets only,
with a minimum spacing near the loop's. It is the only measured fix for the ridge box, and it moves no surface. Do
not apply it to curved-cutter wall quads; it worsened them.
2. **UV0 seams as protected features.** Should a loop vertex on a UV0 seam, and a hull UV0 seam vertex, be
protected like corners for every weld step (merges, slide, flips)? Recommendation: yes, and extend it to
cutter-owned `loop` and `ring` merges, which still tear UVs on the switch and the notch. Her unwrap is authored
state, and the weld must not cut through it. This costs one slide on the split.
3. **Split caps and reversed cut walls in UV0.** Recommendation: give each brush a UV chart (`brushes-own-surfaces`)
and place the intersect piece's cap as a mirrored copy in its own tile. Then a carried cap UV is never shared, and
a cut wall's mirrored winding is fixed at placement, not by the weld. That answers the follow-up's "who owns UV0"
for created faces: the brush's chart. The hull's UV0 rides through by interpolation, so no unwrap after the weld
is needed.
4. **The guard.** Round 4 judges compound operations by their end state, with a local worst-angle rule (never
ending below 5 deg if it got worse). A per-step guard refuses operations whose intermediate state is degenerate.
Recommendation: adopt the end-state guard in the weld's spec. It is target r4's own wording, "drops the band's
worst angle below the floor".
5. **Her reading of "so they survive".** The split pair at the top of this section shows owner (hull) vertices
moved by under the bevel width. Recommendation: ask her with that image. If she says no, the slide applies only to
the yielding side, and the short merge alone takes the needle (the before image).
What remains unmeasured:
- Manifold's property carry (UVs through the kernel).
- A real unwrap of hers in place of the stand-in.
- The light wedge's exact clamp edges on the split.
## Round 3: Self's calls and ruling weld-owner-local-corners-kept
Inputs to this round:
- Self's calls: a flat split keeps its seam exactly on the cutter's plane; hull vertices survive; hull-side slivers
along a flat seam are repaired by edge flips; the cap faces a cut leaves are ours and get a constrained Delaunay
(CDT); worst-angle guard 5 deg.
- Ruling `weld-owner-local-corners-kept` (operator): ownership is local along the loop, and a hard-edged cutter's
corners always survive.
How it was run:
- **Code:** `py/round3.py` for the flat seams; `py/owner_weld.py` with `NOSNAP_FLAT=1 MIN_ANGLE=5` for the curved
cutters.
- **Ownership:** per loop vertex, by curvature along the loop. Nothing in the code takes a loop-wide vote.
- **Protected corners:** loop vertices on a sharp cutter edge (dihedral > 30 deg) or on a hull crease edge. They are
never a merge source and never moved.
- **Edge flips:** greedy. A flip must raise the pair's worst angle and keep both normals within 20 deg. Seam and
crease edges are constrained.
- **CDT:** Shewchuk's Triangle through the `triangle` package (Python wrapper LGPL-3.0; the Triangle core's licence
bars commercial use, so it is fine for this spike and not shippable). Variants:
- `cdt`: plain CDT, loop as the boundary.
- `cdt-q`: CDT with a 20 deg quality bound, interior Steiner points only (`pq20Y`), so the seam is never split.
- **Short seam merge (`short`):** consecutive seam vertices closer than the 0.001 bevel width merge, jointly on both
pieces. Both endpoints lie on the plane, so the seam stays on it. Only boolean-inserted vertices are merged away.
- **New probes:** a box cut into the curved flank (`probe_box_flank`, half-width 0.02) and a box across the dorsal
ridge (`probe_box_ridge`, half-width 0.06). Each is rotated off the tessellation, with its sharp vertical edges on
the loop.
Tables: `spikes/boolean-weld/results/round3/`.
### Flat seams (seam on the plane; hull side and cap reported separately)
| case | variant | hull slivers | hull worst deg | hull p5 deg | cap slivers | cap worst deg | flips | seam off plane | seam crack | seam-adjacent edges < bevel width |
|---|---|---|---|---|---|---|---|---|---|---|
| panel split, subtract piece | naive | 37 | 0.676 | 3.86 | 85 / 122 | 0.058 | 0 | 3e-16 | 0 | 15 |
| | flip + cdt | 28 | 0.676 | 4.69 | 80 / 122 | 0.122 | 0 | 3e-16 | 0 | 15 |
| | flip + cdt-q | 28 | 0.676 | 4.69 | 0 / 332 | 6.76 | 0 | 3e-16 | 0 | 23 |
| | short + flip + cdt-q | 26 | 0.676 | 5.30 | 1 / 313 | 5.50 | 0 | 3e-16 | 0 | 16 |
| panel split, intersect piece | naive | 32 | 0.539 | 4.36 | 85 / 122 | 0.056 | 0 | 3e-16 | 0 | 14 |
| | short + flip + cdt-q | 17 | 0.539 | 6.69 | 1 / 313 | 5.50 | 0 | 3e-16 | 0 | 16 |
| box across the crease, subtract piece | naive | 22 | 0.377 | 3.73 | 22 / 82 | 0.033 | 0 | 7e-16 | 0 | 12 |
| | short + flip + cdt-q | 12 | 2.41 | 6.64 | 3 / 147 | 0.714 | 0 | 7e-16 | 0 | 6 |
| box across the crease, intersect piece | naive | 25 | 0.0405 | 5.61 | 22 / 82 | 0.033 | 0 | 7e-16 | 0 | 11 |
| | short + flip + cdt-q | 10 | 0.0405 | 7.14 | 3 / 147 | 0.714 | 0 | 7e-16 | 0 | 5 |
- **Flips applied:** 138 and 99 on the split; 65 and 79 on the crease box.
- **Short seam merges:** 3 on the split, 4 refused by the 5 deg guard; 5 on the crease box, 1 refused. They move the
seam polyline by at most 0.0069 % and 0.0040 % of the diagonal, within the plane.
- **The seam stays on the plane to float** (3e-16), the two pieces' seams coincide (crack 0), and nothing flips.
- **The cap CDT with interior refinement removes the cap's slivers.** Cap worst angle 0.058 to 5.5 deg (6.8 deg
without the short merge); the crease box's cap reaches only 0.71 deg. Plain CDT cannot: the cap is a long thin
polygon, and with no interior points its worst angle only reaches 0.12 deg.
- **The hull side's worst angle does not move** (0.676, 0.539 and 0.0405 deg). It is a triangle whose two vertices
are a boolean-inserted seam vertex and a hull vertex almost on top of it. Flips cannot remove the edge between
them: two nearly coincident points stay connected in any Delaunay-like triangulation. With both points pinned (the
seam on the plane, hull vertices surviving), that angle stays. The seam-adjacent edges shorter than the bevel width
remain for the same reason (15 to 16).
- **Render** (`r3_longinus_vs_split.jpg`, `r3_r3split.jpg`): the round-3 seam is straight, crisp and continuous for
most of its length. That is better than round 2's kinks, and its seam no longer shows round 1's sliver highlight.
Where the short seam-adjacent edges sit, the bevel narrows and a light wedge appears: Bevel's overlap clamp (on in
the cage's own modifier) caps the width at the shortest adjacent edge. Turning the clamp off (`r3_r3clamp0.jpg`)
gives a constant-width groove but overlapping bevel geometry: a spike on the round-3 seam, sawtooth on the naive
one. So her uniform Longinus groove is not reached yet. Reaching it needs those near-coincident seam/hull vertex
pairs resolved. Either the hull vertex moves onto the seam within the hull's own surface (it survives, displaced
by less than the bevel width), or Self's no-move call is relaxed for vertices within the bevel width.
### Curved and hard-edged cutters (owner rule, local ownership, corners kept, 5 deg)
| case | variant | slivers | worst deg | p5 deg | flips | silhouette dev max | corners kept / total |
|---|---|---|---|---|---|---|---|
| gun port 16 (regression) | naive | 32 | 0.007 | 1.14 | 0 | 0.0198 % | 0 / 0 |
| | owner r3 | 5 | 5.09 | 6.68 | 0 | 0.0198 % | 0 / 0 |
| ownership switch r 0.08 | naive | 112 | 1.2e-6 | 0.62 | 0 | 0.0128 % | |
| | owner r3 | 53 | 1.2e-6 | 2.65 | 0 | 0.323 % | 4 / 4 |
| crease notch 16 | owner r3 | 10 | 4.31 | 6.83 | 0 | 0.061 % | 2 / 2 |
| box in flank (corners on loop) | naive | 3 | 0.285 | 6.62 | 0 | 0 | |
| | owner r3 | 2 | 0.285 | 9.61 | 0 | 0.0108 % | 4 / 4 |
| | hull-side flips (24) | hull 0 (was 2) | hull 6.0 (was 0.95) | hull 12.1 | 0 | 0 | 4 / 4 |
| box across ridge | owner r3 | 66 | 0.070 | 1.41 | 0 | 0.0014 % | 6 / 6 |
| | hull-side flips (118) | hull 7 (was 19) | hull 1.75 (was 0.72) | hull 7.24 | 0 | 0 | 6 / 6 |
- **Corners:** no corner was moved or lost in any case. `corners_kept_in_place` equals `corners` everywhere, and the
guard turned away 4 merges on the flank box and 6 on the ridge box (`protected_corner`).
- On the ownership-switch probe, the 4 corners are where the r 0.08 cylinder's floor rim breaks through the ridge.
- On the crease notch they are the 2 rim-crease crossings; on the flat splits, the 4 crease crossings.
- **Ownership on the box cut-ins:** the hull owns every stretch (box faces have zero curvature along the loop), no
vertex snaps (flat), and the box contributes its corners. That matches her "keep the first's corners".
- **The ridge box's pocket walls keep 48 slivers.** They are the cutter's own faces, trimmed by the hull. They are
not a planar cap, so this round did not re-triangulate them.
- **The port regression is unchanged** from round 2: slivers 5, worst 5.09 deg, silhouette unchanged.
- **The ownership switch** keeps its owner-side 1.2e-6 deg needle and round 2's 0.32 % loop movement on the
hull-owned stretches, where curved-cutter snapping is still on.
### Renders, round 3
![Longinus vs split, round 3](spike-boolean-weld/r3_longinus_vs_split.jpg)
![split seam](spike-boolean-weld/r3_r3split.jpg)
![split wide](spike-boolean-weld/r3_r3splitwide.jpg)
![clamp off](spike-boolean-weld/r3_r3clamp0.jpg)
![crease box](spike-boolean-weld/r3_r3crease.jpg)
![box in flank](spike-boolean-weld/r3_r3boxflank.jpg)
![box across ridge](spike-boolean-weld/r3_r3boxridge.jpg)
![port regression](spike-boolean-weld/r3_r3port16.jpg)
![switch](spike-boolean-weld/r3_r3switch.jpg)
### Questions for Self (round 3, text only)
1. A flat seam's residual worst angle and short bevel-clamped edges come from a boolean-inserted seam vertex lying
almost on a hull vertex. Under "seam on the plane, hull vertices survive" both are pinned. Should a hull vertex
within the bevel width of the seam be allowed to slide onto the seam inside the hull's surface? It still survives,
moved by less than 0.001.
2. Curved-cutter snapping on hull-owned stretches still moves the loop (switch probe 0.32 %). Should the round-3
no-snap apply to every hull-owned stretch, curved cutter or not?
3. The CDT here uses Triangle, whose licence bars commercial use. A shippable cap CDT needs an owned implementation
or a permissive crate (for example `spade`, a Rust CDT; not evaluated here).
## Round 2: ruling weld-owner-defines-silhouette
The operator answered round 1's question (ruling `weld-owner-defines-silhouette`, superseding
`boolean-cleanup-tighter-curve-wins`):
> "When i cut a panel line with a flat cutter, it's the hull's vertices that define the silhouette, so they survive,
> this is what the tighter curve rule is for. When I cut a small thing out of a bigger thing like your first example
> (which looks like a sphere cutting into a flat surface to make a dimple), the bigger thing is contributing fewer
> (none, in your example) vertices to the silhouette, so its vertices get merged away"
Implemented in `spikes/boolean-weld/py/owner_weld.py`, run by `py/run2.py`, with boolmesh output (Manifold's is
identical, round 1).
- **Owner:** curvature along the loop.
- **Spare vertices merged:**
- `loop`: a loop vertex made only by the yielding side's edges merges into the nearest owner-made loop vertex.
- `ring`: where the cutter owns, hull vertices one ring off the loop merge onto it.
- `snap`: where the hull owns, a loop vertex the boolean inserted on a hull edge, within SNAP local edge lengths
of an existing hull vertex, merges into that hull vertex. This is Self's specification: the hull vertex survives
and the loop goes through it.
- **Splits:** both pieces are welded jointly, and every operation must pass both pieces' guards or it is refused.
In the piece that lacks the surviving hull vertex, the loop vertex moves to it.
- **Guard thresholds:** worst angle 10 and 5 deg, both reported; SNAP 0.25 and 0.5 h.
- **New probe:** a vertical cylinder of r 0.03, 16 and 48 segments, cut through the wing-rim crease (a notch).
Full tables: `spikes/boolean-weld/results/round2/table2-*.md`. Tags: `a10s25` = 10 deg and SNAP 0.25 h; `a5s50` =
5 deg and SNAP 0.5 h; `r*` = the reverse snap direction.
### Answer, per case family
- **Small curved cutter in a larger surface (gun ports, dimple probes): the rule works.** The cutter owns the loop
and the hull's spare vertices merge away.
- Port, 16 segments, at 10 deg: slivers 32 to 5, worst angle 0.007 to 5.09 deg, p5 1.14 to 6.68 deg, silhouette
unchanged (0.0198 %), 0 flips, 0 vertices moved outside the band. Edges under the bevel width touching the loop
drop from 6 to 0. The 5 deg guard gives the same numbers.
- At 48 segments the hull-side fans to the dense cutter remain: slivers 227 to 211 (10 deg) or 207 (5 deg). The
cutter's tessellation should stay near the hull's density.
- **Flat cutter splitting the hull (panel line): the rule as specified does not fix the seam, and it bends it.**
- At 10 deg and 0.25 h, both pieces accepted 16 of 78 snaps; 35 were refused on angle, 25 on flips and 2 on the
link condition.
- Slivers 97 to 85 on the subtract piece and 92 to 82 on the intersect piece.
- The worst angle is unchanged: 0.058 deg and 0.056 deg.
- Seam crack 0. The joint guard holds, so both pieces' loops stay identical.
- At 5 deg / 0.25 h: 22 snaps applied, slivers 83 / 79. At 5 deg / 0.5 h: 54 applied, slivers 71 / 64.
- Where they are: hull-side slivers barely move (37 to 35 and 32 to 32). The worst angle and most slivers (61 per
piece) are in the planar cap's triangulation, which no vertex merge reaches.
- The cost: the seam leaves the cutter's plane, by up to 0.40 % of the diagonal (0.23 h) at 0.25 h and 0.95 %
(0.56 h) at 0.5 h. After Bevel and Weighted Normal this shows as visible kinks and a step in the panel line
(`r2_splitclose.jpg`). The naive seam stays straight apart from one bright sliver highlight. Her Longinus seams
are straight bevelled grooves (`r2_longinus_vs_split.jpg`).
- The reverse direction does no better. Sliding the hull vertex onto the loop keeps the seam on the plane, but the
guards refuse nearly all of it (1 of 78 applied at 10 deg / 0.25 h; 11 of 156 at 5 deg / 0.5 h), and it moves
hull vertices up to 0.38 h off the hull surface.
- **Equal curvature, saddle, ownership switch: the rule cleans up but does not reach the needles.**
- Slivers: equal-k 87 to 35 (10 deg) or 31 (5 deg); saddle 50 to 25 or 26; switch 112 to 54 or 53.
- Worst angle: the 1e-6 to 3e-3 deg needles survive at 10 deg. At 5 deg equal-k reaches 0.71 deg.
- The hull-owned stretches now snap, so the loop moves where ownership flips to the hull: equal-k 0.056 to 0.098 %
(0.13 h), switch 0.013 to 0.32 % (0.16 h).
- The saddle's 1 "flipped" near-vertical sliver remains at 10 deg and is gone at 5 deg.
- **Crease: the rule works.**
- Curved cutter (the notch): the cutter owns the arc and the hull owns at the 2 crease crossings (6 of 41 loop
vertices at 16 segments, 4 and 6 owner switches). Both crease vertices survive, no collapse uses a crease vertex
as its source, and the hull's crease bevel weights are untouched (144 weighted edges before and after).
- Notch results at 10 deg: 16 segments, slivers 27 to 8, worst angle 0.21 to 3.91 deg (4.31 at 5 deg), edges under
the bevel width 4 to 0, silhouette 0.043 to 0.061 %; 48 segments, slivers 64 to 27, worst angle 0.10 to 0.79 deg.
- Box across the crease: like the panel split, 40 of 44 slivers remain, the worst angle is unchanged, the seam
crack is 0 and the seam moves 0.26 h.
### Renders, round 2
![Longinus vs split](spike-boolean-weld/r2_longinus_vs_split.jpg)
![split seam close](spike-boolean-weld/r2_splitclose.jpg)
![split](spike-boolean-weld/r2_split2.jpg)
![notch 16](spike-boolean-weld/r2_creasecyl16.jpg)
![notch 48](spike-boolean-weld/r2_creasecyl48.jpg)
![notch side](spike-boolean-weld/r2_creasecylside.jpg)
### Questions for Self (round 2, text only)
1. On a panel split, the snap makes the hull's vertices the seam's path as well as its silhouette, and the seam
kinks. Should the hull vertices survive while the seam stays on the cutter's plane? If so, the seam's cleanup is
not a vertex merge but a retriangulation of the cap and of the hull faces next to the loop. That is untested;
both pieces would get the same loop, so no crack.
2. The cap's triangulation holds the worst angle and most slivers of every planar split, and the cap is kept for the
bevel. Is the cap ours to remesh? It is interior, planar and invisible except through the Bevel and Weighted
Normal it feeds.
3. Angle bar: 5 deg lets more merges through (split 22 vs 16 at 0.25 h; equal-k worst angle 0.71 vs 1.7e-6) and
never produced a flip. 10 deg is safer on the saddle. Both are reported; Self decides.
## Round 1
### Answer
- **As the plan wrote it (owner by curvature across the loop, `u = n x t`), the rule fails on the gun port.** A
cylinder has zero curvature across its own cut loop (that direction is its axis), so the hull owns 34 of 36 loop
vertices and the weld merges away the cylinder's circle samples. The port's silhouette error doubles (max 0.0198 %
to 0.045 % of the diagonal) and the worst angle does not improve (0.007 deg).
- **Measured along the loop (`k = II(t, t)` on each surface), the rule works for the gun port.** The cylinder owns
every loop vertex, the 10 hull-made loop vertices merge onto it, and none is refused. Slivers in the band drop from 32 to 5. The minimum
angle goes from 0.007 to 5.1 deg and the 5th percentile from 1.1 to 6.7 deg. The silhouette is unchanged, nothing
flips and no vertex moves outside the band. Edges shorter than the bevel width touching the loop drop from 6 to 0,
so the shipped Bevel's clamp no longer chokes the seam: the render shows a continuous bevel highlight where the
naive cut has almost none. This reading, the curvature that bends the cut curve, matches her sentence ("defines
the silhouette of the cut"). That is inference, and a question for Self below.
- **For the panel split the rule is a no-op.** A planar cutter contributes no loop vertices, since every one of the
124 loop vertices is a hull edge crossing the plane. The hull owns all of them in every direction mode, and nothing
merges. The 97 slivers in the band (37 hull-side, 60 in the cap; worst angle 0.058 deg) are the owner's own, and
her rule does not speak to them.
- **Kernel: boolmesh 0.2.1.** It gives the same output as Manifold to the bit (identical vertex sets and triangle
counts on every case), builds as pure Rust in 14 s on Windows, and carries provenance as a per-corner value.
Manifold (manifold-csg 0.4.1 over Manifold v3.5.3) builds in 89 s on Linux and fetches upstream C++ source at
build time.
### Inputs
- Hull: `Dexter Quiet Cage` re-exported from a copy of Ships.blend with Bevel and Weighted Normal off (Mirror, then
Subsurf level 3 with limit surface): 3,010 vertices, 6,016 triangles, 0 non-manifold edges, 0 misoriented, Euler 2
(watertight, genus 0), diagonal 1.4584, mean edge 0.0256 (p5 0.0067, p95 0.048). The 144 evaluated crease edges
equal the cage's bevel-weight edges (midpoint match 8e-8). Fixtures: `spikes/boolean-weld/fixtures/`.
`cage_eval.obj` (3,154 vertices, Bevel baked in) was not used.
- Curvature: a per-vertex quadric fit over the 2-ring of the evaluated mesh, giving a 3x3 second fundamental form;
sign positive for convex. This is not the limit-surface `X = S c` (S not extracted). Hull crease edges count as
infinite curvature, so the hull always owns there. Cutter curvature is analytic: cylinder `e_theta e_theta^T / r`,
box faces 0.
- Cutters (`cases.py`). Gun port: cylinder r 0.015 on the upper flank at (0.17, 0.0), axis on the face normal,
sunk 0.02, 16 and 48 segments. Split: box over y > 0.22, a transverse break round the forward body; both pieces
kept. Probes:
- equal-k: cylinder r 0.0199 = 1/|k|max at hull vertex 1799.
- saddle: cylinder r 0.015 at vertex 538, k1 = -7.0, k2 = 55.2. The hull is 0.0143 thick there, so the port is sunk
0.0086, not 0.02.
- crease: box x > 0.33, a chordwise split across the wing-rim crease.
- switch: cylinder r 0.08 across the dorsal ridge.
- True-curve reference: the same cylinder at 1,024 segments. For planes the boolean loop is exact.
### Weld pass (`py/weld.py`)
1. Loop = edges between a hull-provenance face and a cutter-provenance face, traced into cycles. Neither kernel
returns the loop or each loop vertex's source edge, so origin is a geometric test: whether the vertex lies on an
original hull edge (H), a cutter edge (C) or both (1.5e-6 tolerance).
2. Owner per loop vertex = larger |k|, ties to the hull. The direction modes are `across` (the plan), `along` and
`mean`.
3. Collapses: a loop vertex of yielding-side origin merges into the nearest owner-origin loop vertex along the loop.
The `literal` scope also merges the yielding surface's one-ring onto the loop; `looponly` does not.
4. Guard: refuse a collapse on a face flip (old·new normal <= 0), on a worst angle below 10 deg when it got worse,
on a missing edge, or on a failed link condition. Refusals are counted, never forced.
5. Split: collapses from the subtract piece are replayed by position on the intersect piece.
6. Candidate change tested (`-snap`), not her rule: the owner's own vertices within 0.25 local edge lengths of a loop
vertex, and loop edges shorter than the 0.001 bevel width, also collapse, with the same guard.
### Measures (boolmesh; Manifold's numbers are identical)
Band = triangles whose centroid is within 2 local edge lengths of the loop. Sliver = longest edge squared over twice
the area > 10, or area < 1e-4 of the mean. Dev = welded loop polyline to the true intersection curve, in % of the
diagonal and in local edge lengths h. Out = hull vertices moved outside the band.
| case | variant | collapses | band tris | slivers | min deg | p5 deg | flips | dev max % | dev max /h | out |
|---|---|---|---|---|---|---|---|---|---|---|
| port s16 | naive | | 157 | 32 | 0.007 | 1.14 | 0 | 0.0198 | 0.0096 | 0 |
| port s16 | across (plan) | 12 | 133 | 12 | 0.007 | 2.83 | 0 | 0.045 | 0.022 | 0 |
| port s16 | **along** | 10 | 137 | **5** | **5.09** | **6.68** | 0 | 0.0198 | 0.0096 | 0 |
| port s48 | naive | | 339 | 227 | 0.016 | 0.92 | 0 | 0.0023 | 0.0011 | 0 |
| port s48 | across | 19 | 301 | 173 | 0.016 | 1.47 | 0 | 0.0064 | 0.0031 | 0 |
| port s48 | along | 8 | 323 | 211 | 0.47 | 1.57 | 0 | 0.0023 | 0.0011 | 0 |
| port s48 | along-snap | 40 | 259 | 119 | 0.93 | 2.25 | 0 | 0.0055 | 0.0027 | 0 |
| split sub | naive = every rule variant | 0 | 507 | 97 | 0.058 | 1.46 | 0 | 0 | 0 | 0 |
| split sub | along-snap | 7 | 496 | 85 | 0.058 | 2.07 | 0 | 0.0087 | 0.0051 | 0 |
| equal-k s16 | naive | | 457 | 87 | 1.5e-6 | 1.65 | 0 | 0.056 | 0.072 | 0 |
| equal-k s16 | along | 38 | 359 | 38 | 1.7e-6 | 3.67 | 0 | 0.055 | 0.073 | 0 |
| equal-k s48 | along | 53 | 506 | 73 | 0.157 | 2.98 | 0 | 0.0064 | 0.0082 | 0 |
| saddle s16 | naive | | 209 | 50 | 2.8e-6 | 0.61 | 0 | 0.094 | 0.067 | 0 |
| saddle s16 | along | 21 | 167 | 25 | 0.003 | 0.94 | **1** | 0.094 | 0.067 | 0 |
| saddle s48 | along | 28 | 309 | 83 | 0.003 | 0.85 | 0 | 0.012 | 0.0088 | 0 |
| crease sub | naive = every rule variant | 0 | 342 | 44 | 0.033 | 2.41 | 0 | 0 | 0 | 0 |
| switch s48 | naive | | 488 | 112 | 1.2e-6 | 0.62 | 0 | 0.0128 | 0.0062 | 0 |
| switch s48 | along | 48 | 392 | 57 | 1.2e-6 | 2.12 | 0 | 0.0196 | 0.0095 | 0 |
The full table (every variant, both kernels' rows, refusal counts) is in `spikes/boolean-weld/results/`.
Ownership per mode, as owner counts (hull / cutter) and owner switches per loop:
| case | across | along | mean |
|---|---|---|---|
| port s16 | 34 / 2, 4 switches | 0 / 36, 0 | 0 / 36, 0 |
| split | 124 / 0, 0 | 124 / 0, 0 | 124 / 0, 0 |
| equal-k s16 | 50 / 41, 10 | 21 / 70, 8 (11 within 10 % of a tie) | 16 / 75, 2 |
| equal-k s48 | 82 / 54, 10 | 24 / 112, 6 (15 near-tie) | 21 / 115, 2 |
| saddle s16 | 36 / 21, 18 | 9 / 48, 14 | 13 / 44, 10 |
| switch s48 | 79 / 53, 12 | 24 / 108, 4 | 22 / 110, 4 |
Shortest edge touching the loop, which bounds the bevel through Bevel's overlap clamp; the count is edges under the
0.001 bevel width:
| case | naive | across | along |
|---|---|---|---|
| port s16 | 5.3e-5 (6) | 5.3e-4 (4) | 2.3e-3 (0) |
| port s48 | 5.6e-5 (52) | 5.6e-5 (32) | 2.2e-4 (40) |
| saddle s16 | 1.4e-6 (19) | 1.4e-6 (3) | 1.4e-6 (9) |
| split | 2.6e-4 (15) | unchanged | unchanged |
### Per case
1. **Gun port, 16 segments.** With `along` the rule passes, apart from a 5.1 deg worst angle against a 10 deg guard.
With `across` it fails (see Answer).
2. **Gun port, 48 segments (probe 5).** The rule keeps the cutter's dense samples, which is the right silhouette, but
leaves 86 hull-side fans: hull vertices about 0.025 away fanned to cutter samples 0.002 apart. Another 125
"slivers" are the cylinder's own wall quads, 0.05 tall by 0.002 wide, which are inherent to the cutter's
tessellation and not caused by the boolean. The 16-segment cutter (spacing 0.0059, about 0.23 h) welds clean; at
48 (about 0.08 h) only the snap variant helps much. The rule needs the cutter's spacing no finer than about a
quarter of the hull's local edge length; finer than that, fans remain that no merge can remove.
3. **Panel split.** No-op, as above. The two pieces' loops coincide vertex for vertex (gap 0). Caps: with both caps
kept, the bevelled seam reads as a panel line. With both caps dropped, nothing shows, because Bevel on an open
boundary edge makes no seam. So the caps must stay for her panel-line method. The capped render shows a light
streak partway along the seam, at the cap's sliver triangles (cutter side, worst 0.058 deg). Replaying the snap
variant's collapses onto the second piece split the loops apart: the second piece refused 2 and could not match 1
by position, opening a 6.8e-4 gap, a crack in the panel. A split's collapses must be accepted by both pieces'
guards or by neither.
4. **Equal curvature (probe 1).** Ownership flickers: 6 to 10 owner switches per loop, and 11 to 15 loop vertices
within 10 % of a tie in `along` mode. It produced no flips and no silhouette step (along dev 0.055 % against
naive 0.056 %). When the two |k| are close, either owner gives about the same curve. The 1.5e-6 deg needle
survives every variant: it belongs to the owner, and the rule never touches it.
5. **Saddle (probe 2).** The owner depends on the direction: across 36 / 21, along 9 / 48, mean 13 / 44, with 10 to 18
switches. The hull's normal curvature there ranges from -7.0 to 55.2 with direction. Mean curvature gives fewer
switches than along, but it answers a different question: how curved the surface is, not how much it bends the
cut. After every weld variant one hull triangle in the band counts as "flipped" (normal · hull normal = -0.013,
0.76 h from the rim crease): a near-vertical sliver whose normal is noise. It fails the zero-flip measure, and
the cut here also reaches the crease (2 crease loop vertices).
6. **Cut across a crease (probe 3).** Passes, trivially. The 2 crease loop vertices are hull-owned (infinite k), no
collapse happens near them, both crease vertices survive, and every crease edge keeps its bevel weight on both
pieces (144 weighted edges become 126 + 20: the 2 crossed crease edges each split into weighted halves). Nothing
merges across the crease. The plane contributes nothing to merge, so this probe does not stress the rule. A
curved cutter across a crease would; it was not run.
7. **Ownership switch (probe 4).** Along the r 0.08 loop the owner switches 4 times. At the switch vertices: no
flips, slivers 112 to 57, and a silhouette that moves from 0.0128 to 0.0196 % max (0.0095 h). The steps are
below a tenth of a local edge. The worst angle stays 1.2e-6 deg, another owner-side needle. The render shows
radial shading streaks round this cut in every variant, naive and welded: Weighted Normal over long hull-side
fan triangles.
Locality held everywhere (invariant two-layers): 0 hull vertices moved outside the band in every case and variant.
### Renders
Each sheet shows the naive result, the ownership along the loop (blue = hull owns, orange = cutter owns) under
`across` and under `along`, and the welded results. Every result is shown after the shipped stack: Bevel 0.001,
1 segment, weight-limited, plus Weighted Normal, with settings copied from the cage's own modifiers, rendered
headless with Workbench. Loop edges carry bevel weight 1 and hull crease edges keep theirs.
![port s16](spike-boolean-weld/port_flank_s16.jpg)
![port s48](spike-boolean-weld/port_flank_s48.jpg)
![split](spike-boolean-weld/split_fwd.jpg)
![equal-k](spike-boolean-weld/probe_equalk_s16.jpg)
![saddle](spike-boolean-weld/probe_saddle_s16.jpg)
![switch](spike-boolean-weld/probe_switch_s48.jpg)
![crease](spike-boolean-weld/probe_crease.jpg)
The split sheet shows four panels: both pieces with caps, both pieces without caps, the subtract piece alone, and the
loop. The crease sheet's framing is poor; its numbers carry the result. Her Longinus panel-line screenshot was not
available to this agent, so no side-by-side was made with it.
### Kernels
| | boolmesh 0.2.1 | manifold-csg 0.4.1 (Manifold v3.5.3) |
|---|---|---|
| licence | MPL-2.0 | Apache-2.0 OR MIT bindings over Apache-2.0 |
| build | pure Rust, 14 s release on Starfire (Windows) | C++ through the -sys build script, 89 s on Yggdrasil (Linux container, `eureka-verify-rust`); fetches Manifold source by git at build time |
| time per op | 8 to 25 ms | 13 to 20 ms |
| output | identical vertex sets (max difference 0.0) and identical triangle counts on all 11 cases | same |
| provenance | per-corner value carried through (0 hull, 1 cutter) | `run_original_id` with `run_index` after `as_original` |
| intersection loop | not returned; derived from provenance | not returned; derived from provenance |
| output before internal cleanup | not exposed (`simplify_topology` runs inside `compute_boolean`) | not exposed (`RemoveDegenerates` runs inside the op) |
The "naive" rows are therefore already after each kernel's own degenerate cleanup. It leaves worst angles down to
1e-6 deg, because epsilon-length collapse does not remove slivers with long edges.
### Smallest changes, in order
1. **Measure curvature along the loop, not across it.** This is the fix. Without it the rule fails the canonical
gun port; with it the 16-segment port passes.
2. **Tie the cutter's tessellation to the hull's local density** (segment length about 0.2 to 0.3 h, never far
finer). The rule keeps the tighter surface's samples; when those are much denser than the flatter surface's,
fans remain that no merge can remove.
3. **Split seams need a cleanup the rule does not cover.** Two sources of slivers remain. One is the owner's own
samples near the loop. The other is the planar cap's triangulation, which matters because the caps must be kept
for the bevelled seam. Candidates, not tested: retriangulate the cap (it is planar and interior, so free to
remesh), and collapse owner edges shorter than the bevel width. The `-snap` variant got only part of the way:
slivers 97 to 85, worst angle unchanged at 0.058 deg.
4. **In a split, apply a collapse only if both pieces' guards accept it.** Replaying one piece's collapses opened a
crack.
### Questions for Self, round 1 (answered: ruling weld-owner-defines-silhouette)
1. Does "the surface with the tighter curve" mean curvature along the cut, so that the circle comes from the
cylinder? The plan's `u = n x t` formulation gives the opposite owner for every cylinder cut. The operator should
confirm which reading she means; this spike's renders and numbers show the two.
2. For panel splits her rule never fires, since a plane is never tighter than the hull. Is the cleanup she does by
hand on panel seams this same rule, or a different one (merging the hull's own near-loop vertices)? If it is
different, the weld rule needs a second clause, and that is an operator call.
3. Is a 5 deg worst angle in the band acceptable for the shippable bar, or does the bar want the 10 deg the guard
uses?
4. The path to the Longinus panel-line screenshot, for the side-by-side the plan asks for.
5. Should the curvature come from the cage's limit surface (`X = S c`) rather than the 2-ring quadric fit? The fit is
noisy near creases and at the saddle, but no result here turned on it.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View file

@ -1,95 +0,0 @@
<!-- Serves brush-tree:ruling:representation-per-hull and :no-unownable-tools-in-loop. Eyes pass eyes-subd-hulls, 2026-10-10. -->
# Eyes facts: subdivision-cage hulls and mixed subd/primitive assets (prior art)
Serves: brush-tree:ruling:representation-per-hull (InForce, operator, 2026-10-10; supersedes hulls-are-subd-limit-surfaces). Operator words: "No, not every hull, that hull in particular. Any attempt to build it needs to model that subtle interplay of curves in 3D, you can't do that by bashing primitives together. That squatter mech ship, though, I'd build that mostly out of primitives. One size does not fit all." Dexter Quiet = one continuous body with a few mechanical sweeps fused into the center (subd limit surface); squatter mech ship = mostly primitives (CSG). Authoring and lowering must support both paths, per hull or per part.
Also serves: brush-tree:ruling:shippable-part-bar (clean unbroken topo flow; part-coherent low-distortion UVs; hard edges by single-segment bevel with face-weighted normals, not split geometry, not multi-segment).
Agent: eyes-subd-hulls, session self-2026-10-10-ag2, 2026-10-10. Facts only; no recommendations.
Tags: FETCHED = page body read this session (WebFetch returns a small-model summary of the page, not the page itself); SEARCH = from search-result summary only; KNOWLEDGE = memory, unverified.
## 0. Mixing a subd/organic body with primitive/CSG mechanical parts in one asset (added item)
- Blender (practitioner pipeline, assembled from forum/add-on sources, no official doc): cutters kept in own collection, scale applied, Boolean modifiers live until export; Bevel modifier placed after Boolean (order changes result); bevel weight only on the boolean's edge loop, not the loops in between; guide edges added to steer the boolean solve on curved surfaces; boolean on curved surfaces yields stretched faces and shading streaks; a profile-1 bevel with a spacer loop is used to stop subd shrinking the form; Weighted Normal modifier (Keep Sharp) last in stack; apply booleans/modifiers only once the form is settled; UVs planned after panel layout, seams following panel breaks. [SEARCH https://blenderartists.org/t/boolean-and-bevel-on-curved-surface/1308394 ; https://hardops-manual.readthedocs.io/en/latest/subdivision/ ; https://garagefarm.net/blog/hard-surface-modeling-in-blender]
- Blender Bevel modifier has Face Strength + Harden Normals options intended to feed a following Weighted Normals modifier (Face Influence). [FETCHED https://projects.blender.org/blender/blender-manual/raw/branch/main/manual/modeling/modifiers/generate/bevel.rst]
- Polycount/ZBrushCentral threads: boolean blockout then rebuild clean topology over it; or cage-first with support loops; booleans only for blockout; boolean+bevel without subd for blocky parts; ZRemesh reported poor on heavy boolean cutouts; match segment counts where round shapes intersect; align operand edges so main-shape support loops are partly built. Jan van den Hemel video "boolean holes to quad topology" converts boolean output to all-quad topology for subd (details not on the page). [SEARCH https://polycount.com/discussion/comment/2729205 ; https://www.zbrushcentral.com/t/booleans-and-retopology-on-hard-surface-in-zbrush/398107 ; FETCHED https://www.blendernation.com/2023/12/14/boolean-holes-to-quad-topology/]
- Plasticity (NURBS CAD): no native subdivision modeling found; mesh enters via PolySplines (mesh to editable G2 NURBS, added in 2026.1); xNURBS patching in Studio; Indie tier exports low-res OBJ/STL only; bridges to Blender and Cinema 4D. A Modo plugin (Power SubD-NURBS) exports subd to NURBS CAD formats, "one-to-one" (planar faces) or "smooth" (combined curved patches). No source found describing Plasticity mixing subd + NURBS solids in one document. [SEARCH https://digitalproduction.com/2026/04/21/plasticity-2026-1/ ; https://cgchannel.com/2026/04/plasticity-2026-1-is-out/ ; https://www.plasticity.xyz/studio]. MoI: not searched (gap).
- Fusion (T-splines): a Form (T-spline) body must be closed to be combined as a solid; open form needs patch/stitch or Thicken; then Solid > Modify > Combine (Join / Keep Tools); Form tools Weld Vertices, Merge Edge join T-spline bodies at seams. Not confirmed whether Form joins a solid directly without conversion. [SEARCH https://autodesk.com/support/technical/article/caas/sfdcarticles/sfdcarticles/How-to-join-or-combine-bodies-in-Fusion-360.html ; https://forums.autodesk.com/t5/fusion-support-forum/how-to-join-bodies-and-forms-into-one-body/m-p/9148777/highlight/true]
- Houdini: Subdivide SOP offers Mantra Catmull-Clark or OpenSubdiv Catmull-Clark; creases via crease attributes; material property "Render polygons as subdivision" subdivides only surfaces with that material (so organic and mechanical parts of one object can differ); SideFX staff: build sharp edges by PolyBevel/PolyExtrude/PolySplit when rendering with subd; nothing found on booleans feeding subd. [SEARCH https://sidefx.com/docs/houdini/render/subdivision.html ; https://www.sidefx.com/forum/post/198241]
- ZBrush: Crease PG / polygroup-border creases with per-crease level (crisp for N levels then soften). [SEARCH https://novedge.com/blogs/design-news/zbrush-tip-crease-polygroup-borders-for-predictable-hard-surface-subdivision]
- Seam cleanliness for shading/UV between a subd body and primitive inserts: no source found stating a canonical method beyond the practitioner list above (bevel at the junction, weighted normals / harden normals, UV seams on panel breaks, apply late). No source found for two-material/two-mesh split (subd body mesh + separate bevelled primitive meshes) as a convention. [SEARCH, absence]
- Local prior art with related content: F:\Projects\aetheria-hull-materials-prior-art.md lines 145-150 (retopo tools, weighted normals + bevel references).
## 1. Fitting a subd surface to a dense/messy mesh
- Hoppe, DeRose, Duchamp, Halstead, Jin, McDonald, Schweitzer, Stuetzle, "Piecewise Smooth Surface Reconstruction", SIGGRAPH 1994 pp.295-302. Piecewise smooth subdivision-based surfaces fit to scattered range data; arbitrary topology; recovers creases and corners; unconstrained optimization; three phases (topological type estimation, mesh optimization, piecewise-smooth optimization). [FETCHED abstract https://hhoppe.com/proj/psrecon/ ; phases SEARCH]. PDFs listed, not read: https://graphics.pixar.com/people/derose/publications/PiecewiseSmooth ; https://grail.cs.washington.edu/wp-content/uploads/2015/08/hoppe-1994-pss.pdf
- Marinov & Kobbelt, "Optimization methods for scattered data approximation with subdivision surfaces", Graphical Models 67(5) 2005 pp.452-473: fit against the exact limit surface (smooth basis functions per control vertex), closest-point parameterization on the limit (Newton + nonlinear minimization), parameter correction for L2; Solid Modeling 2004 version adds adaptive control-mesh restructuring to bound L-infinity. Also "Automatic Generation of Structure Preserving Multiresolution Models" (CGF 2005) and "A Robust Two-Step Procedure for Quad-Dominant Remeshing" (EG 2006; output coarse enough as Catmull-Clark control mesh). [SEARCH https://www.graphics.rwth-aachen.de/publication/0384/ ; https://diglib.eg.org/handle/10.2312/sm20041382]
- Typical recipe in related work: dense mesh -> QEM simplification to coarse base control mesh -> optimize control vertices against distance. [SEARCH]
- Ling, Wang, Yan, "Fitting Sharp Features with Loop Subdivision Surfaces", SGP/CGF 27(5) 2008: creases, darts, corners; QEM-simplified control mesh; nonlinear least squares; exact evaluation for Loop with all sharp-feature types; boundary treatment for mechanical objects. Loop (triangles), not Catmull-Clark. [SEARCH https://diglib.eg.org/handle/10.2312/CGF.v27i5pp1383-1391 ; PDF https://i.cs.hku.hk/GraphicsGroup/publications/pdf/sgp08ling.pdf unread]
- Lavoue & Dupont, "A framework for quad/triangle subdivision surface fitting: application to mechanical objects", CGF 2007 [SEARCH titles; PDFs https://perso.liris.cnrs.fr/fdupont/publications/2007-CGF.pdf unread; fetch of the 2007-SIGGRAPH PDF returned binary, saved at C:\Users\Meta\.claude\projects\F--Projects-CultLib\1b994b80-e27e-42a8-8b75-ab8cb1dd9c54\tool-results\webfetch-1791668880080-fid2qh.pdf, unread].
- Ma, Keates, Jiang, Kosinka: Loop subd fit to dense mesh, control-mesh connectivity from ridge-joined umbilics/ridge crossings; venue/year not confirmed. A 2017 CVM paper adds continuous per-edge sharpness to Loop and fits semi-sharp creases. [SEARCH https://link.springer.com/doi/10.1007/s41095-017-0088-2]
- Kloiber & Augsdorfer, "Creating Curvature Adapted Subdivision Control Meshes from Scan Data", GRAPP 2020 pp.149-159: modified quad meshing gives curvature-varying quad-dominant mesh within an error bound, converted to control mesh whose limit approximates the scan. [SEARCH https://www.scitepress.org/Papers/2020/89896 ; PDF unread]
- Patent US 12086939: cross field -> quad layout as cage -> optimize vertices to approximate input triangle mesh. [SEARCH https://patents.justia.com/patent/12086939]. UTS "fitmaps" Catmull-Clark construction (2011, closed). [SEARCH]
- Neural Subdivision (Liu, Kim, Chaudhuri, Aigerman, Jacobson, SIGGRAPH 2020): learned vertex placement over Loop topology; refines a mesh, does not produce a cage. [SEARCH https://www.dgp.toronto.edu/projects/neural-subdivision]
- ASDGen (Eurographics STAG 2025): generates quad meshes by CAD-like operations on a seed mesh, usable as Catmull-Clark control meshes. [SEARCH https://diglib7.eg.org/handle/10.2312/stag20251334 ; direct fetch 403]
- Limit evaluation: Stam 1998 exact Catmull-Clark evaluation via eigenbasis, cost near bicubic spline [SEARCH https://courses.cs.umbc.edu/graduate/635/spring13/papers/stam98.pdf]; OpenSubdiv Far::LimitStencilTable evaluates points/derivatives on the limit, recomputable when base vertices change, construction can be costly [SEARCH https://graphics.pixar.com/opensubdiv/docs/far_tutorial_5_3.html]. Rust crate subdiv-kernels 0.1.0 has limit_eval with OpenSubdiv oracle tests [SEARCH https://docs.rs/subdiv-kernels], not examined.
- No 2023-2025 paper found that learns Catmull-Clark cage vertices+connectivity end to end. [SEARCH, absence in one query]
## 2. Quad-dominant remeshing as cage seed
- Instant Meshes (Jakob, Tarini, Panozzo, Sorkine-Hornung, SIGGRAPH Asia 2015): orientation field then position field; GUI with brush guidance; page does not state output type or CLI. [FETCHED https://github.com/wjakob/instant-meshes]
- QuadriFlow (Huang, Zhou, Niessner, Shewchuk, Guibas, SGP 2018 Best Paper): builds on Instant Meshes with network-flow + SAT constraints; ~4x fewer singularities; <10 s/model; 17,791 ShapeNet-subset objects; quad output; abstract cites Catmull-Clark rendering as a motivating application. [FETCHED https://stanford.edu/~jingweih/papers/quadriflow/]. CLI `./quadriflow -i in.obj -o out.obj -f N`, manifold triangle input, pure-quad output [SEARCH https://github.com/aschier/QuadriFlow]. Blender bundles extern/quadriflow.
- Blender QuadriFlow bug reports: fails on disconnected triangles (T70608); crash on messy mesh, fixed with manifold/consistent-normals message (T70095); crashes ~40K-900K faces (#105953); scripted loop only works for first and last object (T71871); crash after UI-state script (T92355). [SEARCH https://developer.blender.org/T70608 ; T70095 ; T71871 ; T92355 ; https://projects.blender.org/blender/blender/issues/105953]. NOT FOUND: any source that quadriflow_remesh fails or needs UI under `--background`. One MCP project comment: bpy.ops "can be flaky in background" (informal) [SEARCH https://glama.ai/mcp/servers/@ShirshovDIM/retopoflow_blender_mcp/blob/78358fedc1d6d47808d380d331e176370b479c59/src/blender_handlers/remeshing.py]. bpy.ops.object.quadriflow_remesh signature not captured (API page fetch truncated).
- Learned cross fields: NeurCross (arXiv 2405.13745), CrossGen (arXiv 2506.07020). [SEARCH]
- No measurement found of any remesher's output as a hard-surface subd cage. Community: Quad Remesher cleaner flow than Instant Meshes, tuned organic, hard-surface varies (secondary; local memo F:\Projects\aetheria-hull-materials-prior-art.md lines 145-149).
## 3. Hard surface in subd: creases, holding loops, vs one-segment bevel + face-weighted normals
- Semi-sharp creases: DeRose, Kass, Truong, "Subdivision Surfaces in Character Animation", SIGGRAPH 1998 pp.85-94: sharp rules for a finite number of levels then smooth; creases without extra control vertices; Pixar patent US6489960. [SEARCH https://graphics.pixar.com/library/Geri/paper.pdf, unread]. OpenSubdiv Sdc::Crease: scheme-independent sharpness; semi-sharp decremented 1.0 per level; fractional blend when sharpness decays; Chaikin variant. [SEARCH https://graphics.pixar.com/opensubdiv/docs/doxy_html/a01312.html]. OpenSubdiv prose doc not retrievable (raw github 404; pixar.com redirect).
- Blender Subdivision Surface modifier: Levels Viewport/Render; Use Limit Surface (Quality affects crease accuracy); Use Creases (edge weights, Shift-E); UV Smooth (None, Keep Corners, Keep Corners/Junctions, +Concave, Keep Boundaries, All); Boundary Smooth; Apply bakes subdivided geometry. [FETCHED https://projects.blender.org/blender/blender-manual/raw/branch/main/manual/modeling/modifiers/generate/subdivision_surface.rst]
- Practitioner: Bevel (Limit Weight) then Weighted Normal (Corner Angle + Face Influence; bevel Face Strength Affected); apply before export since engines import baked custom normals; Polycount guide: thin one-segment bevel, silhouette bevels unweighted; one 80.lv artist uses Bevel+Weighted Normals first, subd modelling as slow fallback; counter-views on LOD cost of support geometry. [SEARCH https://origin.80.lv/articles/hard-surface-modeling-tips-tricks-for-creating-a-vintage-quirky-looking-gun ; https://polycount.com/discussion/234851/trim-bevels-on-mesh-with-weighted-normals]. Blender Weighted Normal manual not retrieved.
- Shipping level: ZBrush-style workflows keep detail on top subd level, bake normal map to the lowest-level mesh; quads, support loops at hard features, triangulate at final export. [SEARCH https://novedge.com/blogs/design-news/zbrush-tip-baking-and-exporting-normal-maps-in-zbrush-a-step-by-step-guide]. No source found on a convention of shipping limit-surface-at-level-N with weighted normals versus baking. [SEARCH, absence]. Offline/renderer subdivision from the cage via OpenSubdiv/USD [KNOWLEDGE].
## 4. Models/tools authoring subd cages
- MeshGPT (Siddiqui 2023), MeshAnything (arXiv 2406.10163), PolyGen (Nash 2020): autoregressive transformers, triangle meshes. [SEARCH https://arxiv.org/pdf/2406.10163]
- QuadGPT (arXiv 2509.21420, ICLR 2026): end-to-end autoregressive quad/quad-dominant meshes from point cloud; 1.1B hourglass transformer; tDPO. QuadLink (arXiv 2605.16813); PolyFlow (arXiv 2606.30673); MeshMosaic (2025). None found emitting subd cages or crease tags. [SEARCH https://arxiv.org/pdf/2509.21420]
- LLM to Blender Python: LL3M (arXiv 2508.08228; multi-agent, BlenderRAG; abstract names BMesh, modifiers, shader nodes; no mention of subd) [FETCHED https://arxiv.org/abs/2508.08228]; BlenderLLM (Qwen2.5-Coder-7B, 12k BlendNet pairs, arXiv 2412.14203); CADBench 2026 (arXiv 2605.10873): specialized mesh-to-CAD beat code-generating VLMs; LLaMA-Mesh emits vertices/faces as text. [SEARCH]
- Blender Python cages: BMesh edge crease via `bm.edges.layers.crease.verify()`; vertex crease also a layer; edge crease now an attribute (`crease_edge` per search summary, unverified); issue #101401 "Setting edge crease via PyAPI doesn't work" (resolution unread). [SEARCH https://blenderartists.org/t/edge-crease-toggle/611970 ; https://projects.blender.org/blender/blender/issues/101401]. Geometry Nodes Subdivision Surface node (5.2 manual): Edge Crease, Vertex Crease, Limit Surface, UV Smooth, Boundary Smooth; older manuals one Crease input; T103304 edge-crease bug fixed Jan 2023. [SEARCH https://docs.blender.org/manual/en/latest/modeling/geometry_nodes/mesh/operations/subdivision_surface.html]
## 5. Mechanical sweeps fused into a subd body
- See section 0 and Polycount items: boolean-then-retopo vs cage-first with support loops. No source found on procedurally merging path sweeps into a Catmull-Clark body as parametric cage operations; ASDGen CAD-ops on a quad seed is nearest. [SEARCH, absence]
## 6. Plasticity (own section, per operator: "Plasticity is actually really cool")
- Kernel: Parasolid (booleans, fillets and other modeling ops); file translation via HOOPS Exchange; page says Parasolid also powers SolidWorks, NX, Shapr3D. Developer Nick Kallen. [FETCHED https://www.plasticity.xyz/product ; SEARCH https://en.wikipedia.org/wiki/Plasticity_(software)]
- Modeling: NURBS surfaces with CV editing; page claims robust filleting on complex geometry with many aesthetic options; page does not explicitly say "NURBS solids" (Parasolid B-rep solids implied by kernel, not stated). Studio-only: xNURBS surface patching/blending G0/G1/G2 (Windows and Mac, not Linux), PolySplines mesh-to-NURBS G2 conversion, Class-A tools (Square, Align, Explicit Control). [FETCHED product + https://www.plasticity.xyz/studio]
- Mesh export (OBJ): topology option Tris / Quads (quads and triangles) / Ngons; Density (single value that auto-sets others); Min width, Max width; Surface plane tolerance (max deviation in cm; docs suggest 0.01 low-poly, 0.0001 high-poly); Surface angle tolerance (radians). The topology setting is a tessellation of the B-rep, i.e. a quad-dominant (quads + triangles) option, not a quad-remesh; quality of that quad output for subd or game use not evidenced. [SEARCH https://doc.plasticity.xyz/plasticity-essentials/export-obj ; page body not fetched]. Free trial exports "low resolution" OBJ/STL only. [FETCHED product]
- Import/export formats (per product-page summary, internally inconsistent between its table and prose): trial import OBJ, STL, 3MF, FBX, STEP, PNG/JPG, JSON, SVG; Indie adds X_T, X_B (import and export), STEP, 3MF, SVG export; Studio adds SAT, IGES, 3DM (Rhino) import, DWG/DXF import (Windows). Native `.plasticity` extension; a third-party guide says it holds Parasolid-based solids/surfaces/curves/organization and no parametric history; no official spec found. [FETCHED product ; SEARCH https://www.renderhub.com/guides/3d-file-formats/plasticity (third party)]
- Scripting/automation: none found. No API, plugin SDK, scripting language or command-line/batch mode appears in product page, studio page, Wikipedia summary or search. The only programmatic surface found is the Blender bridge's local server (below). [FETCHED/SEARCH, absence]. Plasticity Studio doc pages for any API not fetched (gap).
- Blender Bridge add-on: requires Blender 4.1+ (Windows/macOS/Linux) and the Plasticity server enabled (Preferences > Server > Enabled); streams mesh data, no temp files or export dialogs; Live Link toggle pushes every Plasticity edit to Blender; Plasticity groups become Blender collections (scoped by document name); modifiers survive refresh; tolerance, edge flow and polygon density adjustable from a Plasticity panel in Blender; "Merge UV Seams" utility on imported geometry; docs do not state transport protocol, normals/UV/material transfer, or quad-only output. Free with every license; PolySplines (Blender subd to NURBS) is Studio only. Early tests: ~25 s initial load on a complex assembly, 1-3 s live updates. Introduced in Plasticity 1.3 (Oct 2023), then experimental. [FETCHED https://doc.plasticity.xyz/blender/introduction-blender-bridge ; SEARCH https://www.cgchannel.com/2023/10/nick-kallen-ships-plasticity-1-3/ ; https://80.lv/articles/plasticity-1-3-s-blender-bridge-demonstrated/]. Add-on source repo/licence: not found.
- Licence and price (facts as stated 2026 on product page): free 30-day trial, 1 machine, no commercial use; Indie $175 plus tax, 1 person, commercial use allowed, up to 2 machines, 12 months updates, perpetual; Studio $299 plus tax, up to 4 machines, 12 months updates and beta access, companies over 10 employees must use Studio (wording ambiguous); Enterprise custom with air-gapped licences; Academic free non-commercial (Indie features). Studio page itself lists no prices and says node-locked, up to 4 machines. [FETCHED product + studio]. Platforms: Windows, macOS, Linux. Plasticity 2026.1 (April 2026) added hidden lines and PolySplines. [SEARCH https://cgchannel.com/2026/04/plasticity-2026-1-is-out/]
- Plasticity docs: https://doc.plasticity.xyz/ (only export-obj and Blender bridge pages touched).
## 6b. Plasticity, short form (operator: "Expensive though. I don't want to rely on code we can't own" -> prior art only)
- Does well (per section 6): Parasolid B-rep solids with robust fillets, G2 blends (Studio), a quads/tris/ngons OBJ tessellation option with deflection controls, free Blender live-link. Closed source, commercial licence, no scripting/API found. [FETCHED/SEARCH as in section 6]
## 7. Open alternatives an agent could drive (solids, fillets, sweeps)
- OpenCASCADE Technology (OCCT): LGPL-2.1 with the "Open CASCADE Exception 1.0" (distributing object code that uses OCCT headers under own terms with prominent notice) for 6.7.0+; earlier 6.6.0- under OCCT-PL; commercial licence also offered. [SEARCH https://spdx.org/licenses/OCCT-exception-1.0.html ; https://dev.opencascade.org/content/open-cascade-technology-changes-its-license-gnu-lgpl]. Kernel with B-rep booleans, fillets, sweeps, lofts [KNOWLEDGE]. Meshing: BRepMesh_IncrementalMesh with linear and angular deflection (plus interior variants); tracker statements that it does not guarantee tolerance, and a maintainer saying it is designed for visualization facets and is "definitely not a good choice" for regular tessellation; a 7.8.0 regression report of meshing failure on a cone (deflections 0.01/0.5). Example presets coarse 0.5/1.0, medium 0.1/0.5, fine 0.02/0.2. Output triangles only. [SEARCH https://dev.opencascade.org/doc/overview/html/occt_user_guides__mesh.html ; https://dev.opencascade.org/content/mesh-triangulation-quality ; https://cad.pyvista.org/examples/06_advanced/custom_tessellation.html ; https://tracker.dev.opencascade.org/view.php?id=33691]. Scripting: C++ API; Python via OCP wrapper (used by CadQuery and build123d) [SEARCH].
- CadQuery: Apache-2.0; OCCT-based; lofts, sweeps, fillets; exports STEP/STL/AMF/3MF; glTF export only for assemblies (single solid must be wrapped); tessellation tolerance (linear 0.1, angular 0.1 default) shown on AMF/3MF exporters, unconfirmed for glTF. Python scripting API (Algebra/fluent). [SEARCH https://cadquery.readthedocs.io/en/stable/importexport.html ; https://pypi.org/project/cadquery/0.5.1]
- build123d: Apache-2.0; built on Open Cascade, derived from portions of CadQuery; fillet and chamfer in README examples; extrude/sweep/loft/revolve/boolean per docs; multi-section sweep may fail, loft suggested as alternative; docs advise fillets late in design, 2D fillets fail less than 3D; export_step, export_stl, import_svg on README (glTF/3MF not on README page); Algebra Mode and Builder Mode Python API, Python 3.11-3.14, typed; ~3.4k stars, 3,314 commits, "under active development". [FETCHED https://github.com/gumyr/build123d ; SEARCH https://build123d.readthedocs.io/en/stable/tips.html]
- truck (ricosjp, Rust): Apache-2.0; B-rep with NURBS; truck-shapeops booleans; truck-meshalgo tessellation; truck-stepio crate; truck-js WASM wrapper; wgpu rendering; fillets not mentioned on the page; docs reference v0.6.x; no latest-commit date on captured page. ~1.6k stars, 35 open issues. [FETCHED https://github.com/ricosjp/truck]
- Fornjot (Rust): 0BSD; README says "This project has been shut down. Its goals were never reached."; repo archived June 19, 2026; "early-stage and experimental", unsuited for real-world use; no fillet/sweep/boolean listed; fj-export names 3MF. [FETCHED https://github.com/hannobraun/fornjot]
- Manifold (elalish): Apache-2.0; guaranteed-manifold mesh booleans; bindings C++, C, Python (manifold3d), JS/TS (WASM, manifold-3d), Rust (manifold-csg external; native manifold-rust port), C#, others external; arbitrary vertex properties and original-ID tracking; README recommends 3MF, or glTF with EXT_mesh_manifold, over STL; refinement functions for smooth interpolation keeping flat faces flat with sharp or small-radius edges. No fillet feature. [FETCHED https://github.com/elalish/manifold]. API header (3.0): SmoothOut(minSharpAngle=52.5, minSmoothness=0) (older docs default 60), Refine, RefineToLength, RefineToTolerance, SmoothByNormals; Smooth(MeshGL, sharpenedEdges) with per-halfedge Smoothness 0 (sharp) to 1 (smooth), halfedge index = 3*triangle + local; without tangents refined triangles stay coplanar. Constructors include extrude/revolve/CrossSection per ManifoldCAD docs [KNOWLEDGE, not on fetched README; unverified]. [SEARCH https://manifoldcad.org/docs/html/manifold_8h_source.html ; https://manifoldcad.org/docs/html/group___structs.html]. Python keyword names not confirmed.
- libfive: library, C API and Python bindings MPL-2.0; Guile bindings and Studio GPL-2.0+; implicit f-rep modeling; meshing described as watertight, manifold, hierarchical, feature-preserving triangle meshes (algorithm not named on page); fillets/blends not mentioned on page (blend/smooth-union functions in stdlib [KNOWLEDGE, unverified]); copyright 2015-2021, 103 open issues, third-party Rust/Unity/Lisp bindings. [FETCHED https://github.com/libfive/libfive]
- Blender (GPL, scripts carry GPL notices) tools: Bevel modifier and bmesh.ops.bevel (bevel tool, modifier and bmesh operator refactored to matching parameters; modifier API: width, angle_limit, limit_method, affect, segments; per-edge weights); Curve to Mesh node (profile swept along a curve) in Geometry Nodes; Subdivision Surface modifier/node with creases (section 3, 4); Boolean modifier; Skin and Solidify modifiers (Skin does not work through Geometry Nodes per a forum thread; Solidify has a reported bug on curve-derived geometry). Headless: `blender --background [file] --python script.py`, `--factory-startup`, `--` passes args. Output is polygon meshes with quads/ngons possible, custom normals, UVs; this is the only item here producing game-ready UV/normal data natively. [SEARCH https://docs.blender.org/api/5.3/bpy.types.BevelModifier.html ; https://projects.blender.org/archive/blender-archive/src/commit/24b47c00eaabe12b5262b59a185da154a477bd98/release/scripts/templates_py/background_job.py ; https://blenderartists.org/t/geometry-nodes-convert-curve-to-mesh-to-work-with-skin-modifier/1605796]. bmesh.ops.bevel argument names not fetched.
- Cross-cutting: none of the OCCT front ends or Manifold yields quads from a B-rep or UVs/face-weighted normals directly on the sources read; UV/normal handling for OCCT meshes not found. [SEARCH, absence]
## Gaps
- Unread full texts: Hoppe 1994, Ling 2008, Lavoue-Dupont 2007, Kloiber 2020, DeRose 1998, QuadriFlow body, QuadGPT body, ASDGen (403).
- OpenSubdiv prose docs and Blender Weighted Normal manual not retrieved. MoI3D not searched. No Plasticity subd+NURBS single-document workflow found.
- Blender quadriflow_remesh signature and background-mode failure: no corroboration; no local Blender run made.
- No evidence on quad remesher quality as hard-surface cage; none on limit-surface-at-level-N shipping convention; none on a standard seam method between subd body and primitive inserts.
- Search-summary titles/venues (esp. Ma et al.) need confirming before citation.

View file

@ -0,0 +1,122 @@
# truck evaluation (2026-10-14)
Serves the operator's request to evaluate truck (https://github.com/ricosjp/truck), a Rust B-rep kernel, after the Aetheris evaluation (`../aetheris-evaluation/`), same method and cases. Campaign brush-tree, session self-2026-10-14-ag. Facts only, measured on Yggdrasil at truck 88ed0052.
Serves the operator's request to evaluate https://github.com/ricosjp/truck (Rust B-rep CAD kernel), campaign brush-tree, session self-2026-10-14-ag. Same method and cases as `F:\Projects\GameCult.Geometry.Csg\docs\research\aetheris-evaluation\README.md`. Facts only; no recommendation.
Measured at truck commit `88ed005249e5e3a6b07f62425399435905cd3ab6` (master, 2026-09-28, "Merge branch 'cargo-upgrade-20260928'"), cloned read-only to `<scratch>\eyes-truck\repo`. Nothing from truck was built or run on Starfire; every build/test/run was on Yggdrasil through `ygg-verify.sh` (image eureka-verify-rust, rustc/cargo 1.95.0, Debian 12, 4 CPUs, 6 GiB).
`<scratch>` = `C:\Users\Meta\AppData\Local\Temp\claude\F--Projects-CultLib\5ea0b87f-66d0-4b24-8d36-45015e4cedc1\scratchpad`.
Tags: [V] measured or read this pass; [S] stated by the repo's own docs only; [M] marketing claim.
Logs: `eyes-truck\jobs\job1.log` (truck's own tests), `job2.log` (first harness run, tol 0.05), `job4.log` (harness: 35 cases x tol 0.05/0.01/0.2, OCCT checks, truck reading OCCT STEP), `job5.log` (licences, wasm, determinism), `job6.log` (STEP entity census, case-25 stage timing, tolerance sweep). `jobs\analyze.py` digests job4.
Harness (mine, not truck's): `eyes-truck\harness\` = local git repo (`src\main.rs`, `check.py`, `cases.json` copied unchanged from the Aetheris harness, `job.sh`, `job5.sh`, `job6.sh`); depends on truck by git rev 88ed0052. Built `--release`.
## Q1 Shape
- [V] Repo: GitHub + GitLab (CI is `.gitlab-ci.yml`); 2872 commits since 2020-05-18; 273 `.rs` files, 71,555 lines of Rust (git ls-files). GitHub: 1621 stars, 41 open issues, not archived, Apache-2.0 (GitHub API). README: "Truck is a RUst Cad Kernel"; concept "Rust and WebGPU", "re-implementing the B-rep with NURBS".
- [V] Maintainers: one organisation's staff. Commit authors (all history): Yoshinori Tanimura 2482 + alias "Tanimura Yoshinori" 110, "truck" 244, then Moritz Moeller 9, Greg Rauhoeft 5, Jinlei Li 3, others 1-3. Since 2025-10-14: 253 commits, by Tanimura (127+72), "truck" 52, thayashi-tech 1. Commits per calendar year: 2022 298, 2023 393, 2024 389, 2025 325, 2026 (to 09-28) 189. Many recent commits are titled "cargo upgrade" (git log -- truck-shapeops: the last five are all "cargo upgrade", 2026-05-25..2026-09-28).
- [V] Releases: 73 git tags; newest tag/crates.io release is 2024-09-20 (truck-modeling 0.6.0, shapeops 0.4.0, stepio 0.3.0, meshalgo 0.4.0, geometry 0.5.0...; crates.io API). master is two years of unreleased work: CHANGELOG "Unreleased" lists EdgeBlendSurface, Offset geometry, fillet for a single edge, truck-drafting (2D crate), truck-assembly (assembly read/write in STEP), higher-order derivatives, wgpu v26. The git-rev dependency I measured is this unreleased master, not the crates.io 0.4.0 shapeops. crates.io downloads: truck-geometry 130k, truck-modeling 127k, truck-stepio 103k, truck-meshalgo 60k, truck-shapeops 13k.
- [V] Crates (version, Rust lines): truck-base 0.5.0 (3,207), truck-geotrait 0.4.0 (2,211), truck-geometry 0.5.0 (15,200: B-spline/NURBS, knot vectors, decorators), truck-topology 0.6.0 (5,956: vertex/edge/wire/face/shell/solid), truck-polymesh 0.6.0 (2,901: mesh structs, OBJ/STL), truck-meshalgo 0.4.0 (6,617: tessellation via `spade` CDT, mesh filters/analyzers, VTK), truck-modeling 0.6.0 (3,878: builder, tsweep/rsweep/homotopy), truck-shapeops 0.4.0 (8,531: booleans, fillet, healing), truck-stepio 0.3.0 (10,537: STEP in/out via `ruststep`), truck-assembly 0.1.0 (1,740), truck-drafting 0.1.0 (1,883, 2D), truck-derivers 0.1.0 (1,344, derive macros), truck-platform 0.6.0 (2,981, wgpu), truck-rendimpl 0.6.0 (3,797, wgpu rendering), truck-js 0.2.0 (592, wasm-bindgen wrapper). Plus example-pages-generator and readme-generator in the workspace.
- [V] Licence: repo `LICENSE` is Apache-2.0; 13 crate manifests say `license = "Apache-2.0"`; `truck-assembly` and `truck-drafting` manifests carry no `license` field (cargo metadata: truck-assembly licence null). Transitive dependency tree of the harness (truck-modeling+shapeops+meshalgo+stepio, 135 packages, cargo metadata, job5.log): MIT OR Apache-2.0 85, Apache-2.0 15, MIT 14, Apache-2.0 OR MIT 5, MIT/Apache-2.0 4, Unlicense OR MIT 3, Zlib OR Apache-2.0 OR MIT 2, BSD-2-Clause 1, Zlib 1, Unlicense/MIT 1, MIT OR Apache-2.0 OR LGPL-2.1-or-later 1 (r-efi, an OR), no licence field 2 (truck-assembly, my harness). No GPL/AGPL. Dependencies that matter: ruststep 0.4.0 (Apache-2.0, last published 2024-09-20), spade 2.15.1 (MIT OR Apache-2.0), cgmath (via truck-base), rayon, vtkio, nom 3.2.1 and quick-xml 0.22.0 (cargo prints a "future-incompat" warning for both, job4.log).
- [V] MSRV: no `rust-version` in any Cargo.toml and no MSRV text anywhere (grep). Editions: 2021 for most crates, 2024 for truck-assembly and truck-drafting (so those need rustc >= 1.85). Builds on 1.95.0.
- [V] CI: GitLab (`.gitlab-ci.yml`) jobs `cpu-test` (docker image, `cargo make ci-cpu-test`), `cpu-nightly-test`, `meshalgo-features-buildcheck`, `gpu-test` (artifacts: images), `wasm-test` (deno lint + `cargo make wasm-test`), `page-build`, deploy of example pages. `Makefile.toml` cpu-test = `cargo test -p truck-base -p truck-geotrait -p truck-geometry -p truck-topology -p truck-polymesh -p truck-meshalgo -p truck-modeling -p truck-shapeops -p truck-stepio --features derive --features polynomial`. Tests that use sample data need the git submodule `resources` (github.com/ricosjp/resources4truck).
- [V] Surface/curve types. truck-geometry: Plane, Sphere, Torus (specifieds/), Circle, Line, Hyperbola, Parabola; BSplineCurve/Surface, NurbsCurve/Surface; decorators RevolutedCurve, ExtrudedCurve, Processor (matrix-transformed), PCurve, IntersectionCurve, TrimmedCurve, AfSurface, EdgeBlend, RbfSurface, Offset, homotopy. The ready-made enums in truck-modeling are narrower: `Surface` = {Plane, BSplineSurface, NurbsSurface, RevolutedCurve(Processor<RevolutedCurve<Curve>, Matrix4>)} (truck-modeling/src/geometry.rs:126-135); `Curve` = {Line, BSplineCurve, NurbsCurve, IntersectionCurve} (:24-32). So a cylinder, sphere or torus built with the modeling builder is a surface of revolution (Line or NURBS circle swept), not an analytic Cylinder/Sphere/Torus surface type. NURBS is first-class.
- [V] How booleans work (truck-shapeops/src/transversal/integrate/mod.rs; entry points `and(solid0, solid1, tol)` and `or(...)` returning `Option<Solid>`; there is no subtract, the examples invert the second solid with `solid.not()` and call `and`): both shells are triangulated at `tol` (`shell.triangulation(tol)`); triangle-triangle intersections give polylines (`polyline_construction`); polylines are re-fitted onto the two true surfaces by Newton iteration (`IntersectionCurve`, `search_triple(..., 100)`, intersection_curve/mod.rs); faces are divided by the loops, classified and/or by crossing a ray against the other mesh; new edges are replaced by a quadratic B-spline approximation of the intersection curve within `tol` (`BSplineCurve::quadratic_approximation(ic, range, tol, 100)`, integrate/mod.rs:`altshell_to_shell`). So it is a general surface-surface path through tessellation, not a table of special cases. `tol` is both the tessellation tolerance and the intersection-curve approximation tolerance; truck-base `TOLERANCE` = 1e-6 (tolerance.rs:6) is the geometric coincidence tolerance (`near`). A non-positive tol hits `nonpositive_tolerance!`. Failure is `None` with no message; some failures are panics (Q3).
- [S] Documented limits (truck-shapeops/src/lib.rs:4-9): "Boolean operations are currently supported only for shapes where faces intersect transversally. Cases where faces are tangent to each other are not yet supported. ... performance optimization using BSP ... remains a future task." Fillet: "single edge whose end vertices are each adjacent to exactly three faces. Continuous edges are currently unsupported." truck-stepio lib.rs:1-6 states "Shapes created by set operations cannot be output yet" (contradicted by my measurement in Q4: 8 boolean results exported and read as valid by OCCT).
- [V] Also shipped in shapeops: `healing` (SplitClosedEdgesAndFaces, RobustSplitClosedEdgesAndFaces) and `fillet` (`simple_fillet`, `fillet_with_side`).
## Q2 Build and own tests on Linux
- [V] Job1 (job1.log), image rust 1.95.0, after `git submodule update --init resources` (fetched resources4truck at 9bf9de14): `cargo test --no-fail-fast -p truck-base -p truck-geotrait -p truck-geometry -p truck-topology -p truck-polymesh -p truck-meshalgo -p truck-modeling -p truck-shapeops -p truck-stepio -p truck-assembly -p truck-drafting -p truck-derivers --features derive --features polynomial` (debug profile): exit 0, 745 passed, 0 failed, 6 ignored (unit + integration + doc-tests).
- [V] Per crate (cargo prints crates alphabetically; the crate attribution below is inferred from that order and the test file names, passed counts unit+integration / doc-tests): assembly 2 / 39; base 28 / 55; derivers 0 / 0; drafting 20 / 25; geometry 82 / 139; geotrait 14 / 0; meshalgo 42 / 21; modeling 24 / 25; polymesh 13 / 15; shapeops 24 / 0 (21 unit + 3 in tests/fillet.rs); stepio 53 / 5 (3 unit + 39 tests/input + 3 tests/io + 8 tests/output); topology 7 / 112.
- [V] Not run: truck-platform, truck-rendimpl, truck-js tests (GPU/wasm CI jobs), nightly lane. `large-solid-torus` test is `#[ignore]`d (1 ignored in topology).
- [V] Compile times (4 CPUs, cold cache): my harness release build 1 m 06 s (job4.log); `truck-js` wasm release build 1 m 18 s (job5.log).
- [V] `cargo build --release --target wasm32-unknown-unknown -p truck-js` succeeds after `rustup target add wasm32-unknown-unknown`: `truck_js.wasm` 3,928,263 bytes (workspace sets opt-level "z" for truck-js). Not run in a browser. truck-js exports 12 builder fns, 20 shape, 5 polygon, 4 shapeops (and/or/not), 4 step_in wasm_bindgen items (grep counts).
## Q3 Boolean torture (the Aetheris cases.json: 35 cases, same ops/geometry/expected volumes)
Method: `harness\src\main.rs` builds primitives with truck-modeling (box = 3 tsweeps then scaled; cylinder = rsweep circle + tsweep; sphere = rsweep semicircle + `cone`; torus = two rsweeps), centred as in the Aetheris harness, rotates about X by rx, translates, flips any primitive whose tessellated volume is negative with `Solid::not()`. union = `or`, intersect = `and`, subtract = `and(a, not(b))`, sequential for multi-step cases. Each case is its own process with a 150 s timeout. "refused" = `None` (no message exists in the API). Release build. Tolerance `tol` is truck's only knob, so I ran three: 0.05, 0.01, 0.2 (model sizes are 10-100 units). Reference volumes: the cases' analytic `exp` plus OCCT booleans on identical primitives (`check.py ref`).
Per-case outcome (S = succeeded, R = refused/None, P = panic; boolean time at tol 0.05 / 0.01 / 0.2 in ms; steps for case 21 in parentheses):
| # | case | tol 0.05 | tol 0.01 | tol 0.2 |
|---|---|---|---|---|
| 01 | coplanar face union | R 2 | R 2 | R 2 |
| 02 | flush slab subtract | R 3 | R 2 | R 2 |
| 03 | coplanar top pocket | P 3 | P 4 | P 3 |
| 04 | coincident-edge union | R 2 | R 2 | R 2 |
| 05 | coincident-edge subtract | R 4 | R 2 | R 2 |
| 06 | tangent cylinders union | R 21 | R 34 | R 18 |
| 07/08/09 | Steinmetz intersect / union / subtract | R 23/30/26 | R 41/40/42 | R 16/16/18 |
| 10 | sliver wall 1e-7 | R 4 | R 2 | R 2 |
| 11 | pocket leaving 1e-7 floor | S 2 | S 2 | S 2 |
| 12 | pocket leaving 1e-4 floor | S 2 | S 2 | S 2 |
| 13 | spheres exactly tangent, union | S 17 | S 90 | S 13 |
| 14 | spheres 1e-9 apart/overlap, union | S 17 | S 90 | S 15 |
| 15 | spheres overlap 1e-4 | R 59 | R 152 | R 44 |
| 16 | spheres overlap (6 apart) | R 306 | R 1044 | R 159 |
| 17 | box minus sphere on top face | R 13 | R 65 | R 11 |
| 18 | torus half cut by box | P 1311 | P 2320 | P 757 |
| 19 | torus union cylinder | R 105 | R 163 | R 52 |
| 20 | torus minus cylinder (tangent) | R 96 | R 151 | R 51 |
| 21 | 10x10 hole grid (100 steps) | P at step 3 (2940 ms) | R at step 1 | R at step 2 |
| 22 | two tangent holes | R 743 | R 1114 | R 193 |
| 23 | hole tangent to side wall | R 26 | R 61 | R 19 |
| 24 | two holes 1e-7 web | P (step 2) 3667 | R (step 2) 1039 | P (step 2) 913 |
| 25 | perpendicular cylinders union | S 12-14 s (see below) | R 7799 | S 1587 |
| 26 | coaxial tube (cyl minus coaxial cyl) | S 3384 | R 8089 | S 333 |
| 27 | blind hole from top | P 144 | R 178 | R 49 |
| 28 | oblique 30 deg through hole | S 3706 | R 3096 | S 299 |
| 29 | box intersect sphere | R 1711 | R 7098 | R 837 |
| 30 | offset corner union | S 2 | S 2 | S 2 |
| 31 | pocket leaving 1e-7 side wall | R 2 | R 2 | R 2 |
| 32 | union overlap by 1e-7 slab | R 4 | R 2 | R 5 |
| 33 | partial face contact union | R 2 | R 2 | R 2 |
| 34 | sphere fully inside box (void) | P 11 | P 15 | P 10 |
| 35 | cylinder minus enclosed sphere | P 28 | P 52 | P 14 |
- [V] Score per tolerance, 35 cases, succeeded / refused / panicked / timed out: tol 0.05: 8 / 20 / 7 / 0 (case 25 at 0.05: boolean returned S in ~12-14 s in 6 of 8 runs; in 2 of 8 runs the harness process hit the 150 s timeout later, see below); tol 0.01: 5 / 26 / 4 / 0; tol 0.2: 8 / 22 / 5 / 0. Union over the three tolerances: 8 distinct cases ever succeed (11, 12, 13, 14, 25, 26, 28, 30); 7 cases panic at least once (03, 18, 21, 24, 27, 34, 35); the other 20 are refused at every tol. Invalid STEP among the successes: 0 of 21 exported files (Q4).
- [V] Panic causes (message captured with a backtrace filter, job4.log): "This shell is not oriented and closed." at truck-topology/src/lib.rs:187 reached from `truck_shapeops::transversal::integrate::and` (frame 6) for cases 03, 18, 24, 27, 34, 35: the boolean assembled a shell that fails `Solid::new`'s closedness check. And `called Option::unwrap() on a None value` at truck-geometry/src/decorators/intersection_curve.rs:220:67 (`IntersectionCurve::subs` -> `search_triple(t,100).unwrap()`) for case 21 at step 3 (tol 0.05) and in tessellation of results from cases 25/26/28 (below). Cases 34 and 35 (a solid wholly inside the other: no intersection curves) panic at every tol.
- [V] Outcome depends on tol non-monotonically: case 25 succeeds at 0.5, 0.2, 0.1, 0.05 and is refused at 0.02, 0.01, 0.005; case 26 succeeds at 0.5, 0.2, 0.1, 0.05, 0.02 and is refused at 0.01 and 0.005; case 28 succeeds at 0.2 and 0.05, refused at 0.01 (job6.log sweep, job4.log). Case 27 is R at 0.5, P at 0.1/0.05/0.02, R at 0.2/0.01/0.005. Hole grid (21): never gets past 2 of 100 holes at any of 0.5, 0.2, 0.1, 0.05, 0.02, 0.01, 0.005 (P at 0.1 and 0.05, R otherwise). Coplanar union (01): R at 0.5, 0.1, 0.02, 0.005 as well. Box-minus-sphere dimple (17): R at 0.5, 0.1, 0.02, 0.005 as well.
- [V] Time per op (release, tol 0.05, boolean call only): planar box cases 2-4 ms; sphere/sphere 17 ms (13 at 0.2, 90 at 0.01); coaxial tube 3.4 s (0.33 s at 0.2, 4.2 s at 0.02); perpendicular cylinders 12-14 s (1.6 s at 0.2, 0.28 s at 0.5, 5.8 s at 0.1); oblique through hole 3.7 s (0.3 s at 0.2); box/sphere refusal 1.7 s; torus/box panic 1.3 s; 10x10 grid first two holes 2.9 s total. Cost grows as tol shrinks for the curved cases.
- [V] Determinism: three repeats at tol 0.05 of cases 11, 13, 26, 28, 30 gave identical face counts and mesh volumes to ~1e-12 (job5.log). Case 25 at 0.05: 2 of 8 runs ended at the 150 s timeout (job4.log rc=124, job5.log run 2) with the stderr of the job4 run holding ruststep "Lookup failed for #219 ..." lines (emitted when truck reads STEP); in 3 runs with stage timing (job6.log) boolean 13.3-14.6 s, meshes +1.0 s, STEP write, re-read +20 ms, no hang. Which stage hung in the 2 timeouts was not captured.
- [V] Against the Aetheris result on the same 35 cases (14 succeeded / 21 refused / 0 crashed): both succeed on 11, 12, 26, 28, 30. truck succeeds where Aetheris refused: 13, 14 (spheres), 25 (perpendicular cylinders, coarse tol only). Aetheris succeeded where truck refused or panicked at every tol: 01, 02, 03, 17, 21, 27, 32, 33, 34. Steinmetz (07-09), tangent cylinders (06), torus (18-20), 15, 16, 22-24, 29, 31, 35 fail in both.
## Q4 STEP: export checked with OpenCASCADE; STEP import
Method: truck-stepio `out::StepDisplay` of `solid.compress()`; checked with cadquery-ocp (pip, Debian 12, Python 3.11, installed in the container) `STEPControl_Reader` + `BRepCheck_Analyzer.IsValid` + `TopExp_Explorer` counts + `BRepGProp.VolumeProperties`; each file in its own subprocess (job4.log "== check truck output"; reference volumes from OCCT booleans on identical primitives, "== ref").
- [V] Files: 21 STEP files exported (cases 11, 12, 13, 14, 30 at 3 tolerances; 25, 26, 28 at 0.05 and 0.2 where they succeeded). OCCT read 21/21, `IsValid` True for 21/21, no crashes. Header `FILE_SCHEMA(('ISO-10303-203'))`.
- [V] Volumes (OCCT of truck's STEP vs OCCT boolean reference / analytic): 11: 840.0000016 vs 840.000001; 12: 840.0016 vs 840.0016; 13 and 14: 2 solids, 2 shells, 24 faces, 1047.19755 vs 1047.19755; 30: 1 solid, 12 faces, 1875.0 vs 1875.0; 25 (perpendicular cylinders): 10 faces, 1865.747 at tol 0.05 (-0.064% vs 1866.935), 1870.690 at 0.2 (+0.20%); 26 (tube): 6 faces, 502.303 at 0.05 (-0.07% vs 502.655), 504.539 at 0.2 (+0.38%); 28 (oblique hole): 8 faces, 3673.266 at 0.05 (-0.007% vs 3673.516), 3680.588 at 0.2 (+0.19%). So exact for planar/analytic-intersection results and tolerance-bound for results that need new intersection curves.
- [V] Entity census of the exports (job6.log): faces are `FACE_SURFACE` over `PLANE` or `SURFACE_OF_REVOLUTION` (spheres: 24 SURFACE_OF_REVOLUTION + AXIS1_PLACEMENT); new edges are `INTERSECTION_CURVE` + `B_SPLINE_CURVE_WITH_KNOTS` (case 11, a planar pocket with 1e-7 floor, still carries 4 INTERSECTION_CURVE + 4 B_SPLINE_CURVE_WITH_KNOTS; case 30 carries 6 of each). Solid is `MANIFOLD_SOLID_BREP`; case 13/14 (two disjoint shells in one Solid) is written as `BREP_WITH_VOIDS` + `ORIENTED_CLOSED_SHELL`, which OCCT reads as 2 solids and volume 1047.2 (a union of two spheres), so the second shell is not subtracted as a void.
- [V] Truck's STEP writer supports (truck-stepio/src/out/geometry.rs): PLANE (:505), SPHERICAL_SURFACE (:537), TOROIDAL_SURFACE (:575), B_SPLINE_SURFACE_WITH_KNOTS (:621), rational B-spline (:677-680), SURFACE_OF_REVOLUTION (:741); no CYLINDRICAL_SURFACE writer line was found by that grep (cylinders from the builder are SURFACE_OF_REVOLUTION).
- [V] Truck reads STEP: yes (`truck_stepio::r#in`, `Table::from_step`, `step_assy`, `to_compressed_shell`; tests read OCCT-written cube, cone, cylinder, sphere, torus and four `abc-*` files from the `resources` submodule, tests/input/tessellate_shape.rs:5-15; input handles CylindricalSurface, SphericalSurface, ToroidalSurface, BSpline, assemblies). My probe: I had OCCT write all 35 reference results (`ref_NN.step`) and read each with truck (`Table::from_step`, every shell `to_compressed_shell`, `triangulation(0.01)`, `put_together_same_attrs`, volume; job4.log "== truck reads OCCT-written STEP"): no panic on any of 35 files; for 16 of them the tessellated shells are all `Closed` with summed volume within 2% of OCCT's (01-05, 11, 12, 13, 17, 21, 26, 27, 30-33); for the rest the shell is Irregular/Regular/Oriented, or 0 triangles, or a wrong volume (06: 783+431 vs 1570.8; 07: 9.6 vs 666.7; 08: 1937 vs 2475; 09: 2 x 481.9 vs 904.1; 14, 16: 0 triangles; 25: 1370 vs 1867; 28: 3892.8 vs 3673.5); cases 18-20, 22-24 and 29 read with volumes within 0.4% but shells not Closed. Reading ref_21 (the 100-hole plate) took 25.8 s in release. These are reads of OCCT trimmed surfaces and intersection-curve edges, tessellated at the repo-test pattern; I did not separate reader faults from tessellation faults.
- [V] Truck re-reading its own exported booleans (same probe, `step_reread` in job4.log): most results fail to come back as a closed mesh (11 and 12: shells "Oriented" with 2-10 triangles and volumes 0 / 750; 13 and 14: Irregular, negative volumes -1563.8 and -1648.7; 30 at 0.01: Oriented, 4 triangles, volume 10; 30 at 0.05 and 0.2: Closed with 0 triangles). Results vary between tolerance runs for the same geometry (case 30: 0 / 4 / 0 triangles at 0.05 / 0.01 / 0.2).
## Q5 Tessellation (truck-meshalgo) of boolean results
Method: `solid.triangulation(tol).to_polygon()` then `put_together_same_attrs(TOLERANCE*50).remove_degenerate_faces()`; `shell_condition()` and `volume()` from meshalgo; plus an independent check of the written OBJ (`check.py obj`: weld at 1e-6, every directed edge must pair with its reverse; signed volume).
- [V] Planar results (cases 11, 12, 30): 28 / 28 / 36 triangles, identical at tol 0.1, 0.01, 0.001, 0.5-1 ms, Closed, independent OBJ check watertight and oriented, volumes exact (840.0000016, 840.0016, 1875.0).
- [V] Spheres (cases 13/14, two spheres r=5): tol 0.1: 992 triangles, 6 ms, Closed, volume 1015.59; tol 0.01: 12,656-12,684 triangles, 111-153 ms, volume 1044.54 (analytic 1047.20); tol 0.001: 78,928 triangles, 1.1-1.8 s, Closed, volume 1046.76. Case 14 is watertight by the independent check at 0.01; case 13 at 0.01 is `Regular` by truck and by the independent check has 9 unmatched directed edges and 9 multi-used edges.
- [V] Results with new intersection curves: case 26 (tube, boolean tol 0.05): mesh tol 0.1: 1,102 triangles, 33-39 ms, Closed, volume 500.06; tol 0.01: 8,219 triangles, 186-214 ms, `Irregular` (independent: 17 unmatched directed edges, 6 multi-used edges), volume 501.79; tol 0.001: panic (intersection_curve.rs:220 unwrap). Case 25 (boolean tol 0.2): tol 0.1: 1,218 triangles, 57 ms Closed; tol 0.01: 11,172 triangles, 272 ms `Regular` (independent: 7 unmatched, 7 multi); tol 0.001 panic. Case 28 (oblique hole, boolean tol 0.05): tol 0.1: 376 triangles, 25-30 ms `Irregular`; tol 0.01 and 0.001: panic; the same case at boolean tol 0.2: tol 0.1 gives 318 triangles `Irregular`, 0.01 and 0.001 panic. Case 26 at boolean tol 0.2 panics at all three mesh tolerances.
- [V] Tessellating the bare primitives (tol 0.01): box 1000.0 exact; cylinder r5 h10 783.98 (analytic 785.40, -0.18%); sphere r5 522.28 (523.60, -0.25%); torus 10/3 1771.57 (1776.53, -0.28%) (job2.log prims).
- [V] The docs say the mesh of a Solid is not necessarily closed and recommend `put_together_same_attrs` (tessellation/mod.rs docs on `MeshableShape::triangulation`); `RobustMeshableShape` "does not close the mesh of a solid".
- [V] Output API: `PolygonMesh` (positions/normals/uv + tri/quad/other faces), OBJ and STL writers in truck-polymesh, VTK in meshalgo. Triangles counted with `faces().triangle_iter()`.
## Q6 Other things a consumer gets
- [V] WASM: `truck-js` (wasm-bindgen) builds for wasm32-unknown-unknown, 3.9 MB; meshalgo has a `cfg(target_arch = "wasm32")` path without rayon; CI runs wasm tests with deno (`.gitlab-ci.yml` job wasm-test). Python bindings: none found (grep pyo3/python in manifests and READMEs). C ABI: none found.
- [V] Rendering: `truck-platform` (graphic utility on wgpu) and `truck-rendimpl` (shape and polygon-mesh visualisation) with examples app.rs, simple-shape-viewer.rs, simple-obj-viewer.rs, textured-cube.rs, rotate-objects.rs, bsp-animation.rs; CI has a `gpu-test` job. Not run here (no GPU in the container).
- [V] Modelling API (truck-modeling/examples/cylinder.rs, whole body of the cylinder):
```rust
let vertex = builder::vertex(Point3::new(0.0, -height / 2.0, radius));
let circle = builder::rsweep(&vertex, Point3::origin(), Vector3::unit_y(), Rad(7.0), 2);
let disk = builder::try_attach_plane(&[circle]).unwrap();
builder::tsweep(&disk, Vector3::new(0.0, height, 0.0))
```
Box = `vertex` -> `tsweep` x/y/z. Punched cube (truck-shapeops/examples/punched-cube-shapeops.rs): build `cylinder` the same way, `cylinder.not();` then `truck_shapeops::and(&cube, &cylinder, 0.05).unwrap()`. Other builder fns: `line`, `circle_arc`, `bezier`, `homotopy`, `try_wire_homotopy`, `try_attach_plane`, `translated`/`rotated`/`scaled`, `rsweep`, `cone`. The examples pass `Rad(7.0)` (a little over 2 pi) for a full revolution.
- [V] Fillet: not reachable from `truck_modeling::Surface` (no fillet variant). The fillet test (truck-shapeops/tests/fillet.rs:1-76) defines its own `Curve` and `Surface` enums with seven derive macros and `ToSameGeometry` impls, builds two NURBS faces by hand, and calls `simple_fillet(&face0, &face1, shared_edge_id, 0.3, 0.001)`; 3 such tests pass (job1.log). Limits per docs: a single edge whose end vertices each touch exactly three faces. I did not build "filleted box with a hole": it would need that custom enum pair plus a boolean on the result, and the measured boolean cannot subtract a blind hole from a box with a coplanar top (cases 03 and 27 panic).
- [V] Serialisation: `Solid`/`Shell` are serde types (examples write `serde_json::to_vec_pretty(&solid)`); `CompressedSolid` for compact form (`solid.compress()`).
- [V] Parallelism: rayon in meshalgo (non-wasm) and `Send + Sync` bounds on `ShapeOpsSurface`/`ShapeOpsCurve`.
- [V] Panics are part of the surface: `Solid::new`, `builder` calls and tessellation panic on bad input (Q3), and there is no `Result` with a reason on `and`/`or`.

View file

@ -0,0 +1,2 @@
target
Cargo.lock

View file

@ -0,0 +1,11 @@
[package]
name = "trucktorture"
version = "0.0.0"
edition = "2021"
[dependencies]
truck-modeling = { git = "https://github.com/ricosjp/truck", rev = "88ed005249e5e3a6b07f62425399435905cd3ab6" }
truck-shapeops = { git = "https://github.com/ricosjp/truck", rev = "88ed005249e5e3a6b07f62425399435905cd3ab6" }
truck-meshalgo = { git = "https://github.com/ricosjp/truck", rev = "88ed005249e5e3a6b07f62425399435905cd3ab6" }
truck-stepio = { git = "https://github.com/ricosjp/truck", rev = "88ed005249e5e3a6b07f62425399435905cd3ab6" }
serde_json = "1"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,151 @@
"""Independent checks of truck output with OpenCASCADE (cadquery-ocp) and plain-python OBJ analysis.
modes:
ref <cases.json> <outdir> OCCT reference boolean per case: volume + writes ref_NN.step
one <file.step> BRepCheck / counts / volume of one STEP (own process; OCCT may segfault)
obj <file.obj> watertightness and volume of an OBJ mesh
all <dir> run `one` per *.step and `obj` per *.obj in subprocesses
"""
import sys, json, subprocess, math, os, glob
def vol_of(shape):
from OCP.GProp import GProp_GProps
from OCP.BRepGProp import BRepGProp
p = GProp_GProps()
BRepGProp.VolumeProperties_s(shape, p)
return p.Mass()
def count(shape, kind):
from OCP.TopExp import TopExp_Explorer
e = TopExp_Explorer(shape, kind)
n = 0
while e.More():
n += 1
e.Next()
return n
def one(path):
from OCP.STEPControl import STEPControl_Reader
from OCP.IFSelect import IFSelect_RetDone
from OCP.BRepCheck import BRepCheck_Analyzer
from OCP.TopAbs import TopAbs_SOLID, TopAbs_SHELL, TopAbs_FACE
r = STEPControl_Reader()
st = r.ReadFile(path)
out = {"file": os.path.basename(path), "read_ok": st == IFSelect_RetDone}
if st != IFSelect_RetDone:
print("RES " + json.dumps(out)); return
nroots = r.TransferRoots()
s = r.OneShape()
out.update({"roots": nroots, "null": s.IsNull()})
if not s.IsNull():
out.update({"valid": BRepCheck_Analyzer(s).IsValid(), "solids": count(s, TopAbs_SOLID),
"shells": count(s, TopAbs_SHELL), "faces": count(s, TopAbs_FACE), "volume": vol_of(s)})
print("RES " + json.dumps(out))
def objcheck(path):
pts, tris = [], []
for line in open(path):
t = line.split()
if not t:
continue
if t[0] == "v":
pts.append((float(t[1]), float(t[2]), float(t[3])))
elif t[0] == "f":
idx = [int(x.split("/")[0]) - 1 for x in t[1:]]
for i in range(1, len(idx) - 1):
tris.append((idx[0], idx[i], idx[i + 1]))
key = {}
remap = []
for p in pts:
k = (round(p[0] / 1e-6), round(p[1] / 1e-6), round(p[2] / 1e-6))
remap.append(key.setdefault(k, len(key)))
edges = {}
vol = 0.0
degenerate = 0
for a, b, c in tris:
pa, pb, pc = pts[a], pts[b], pts[c]
vol += (pa[0] * (pb[1] * pc[2] - pb[2] * pc[1]) - pa[1] * (pb[0] * pc[2] - pb[2] * pc[0]) + pa[2] * (pb[0] * pc[1] - pb[1] * pc[0])) / 6.0
ra, rb, rc = remap[a], remap[b], remap[c]
if len({ra, rb, rc}) < 3:
degenerate += 1
continue
for u, v in ((ra, rb), (rb, rc), (rc, ra)):
edges[(u, v)] = edges.get((u, v), 0) + 1
unmatched = sum(1 for (u, v), n in edges.items() if edges.get((v, u), 0) != n)
multi = sum(1 for n in edges.values() if n > 1)
print("RES " + json.dumps({"file": os.path.basename(path), "tris": len(tris), "welded_vertices": len(key),
"degenerate_after_weld": degenerate, "unmatched_directed_edges": unmatched,
"multi_edges": multi, "watertight_oriented": unmatched == 0 and multi == 0,
"volume": vol}))
def build_ref(cases_path, outdir):
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder, BRepPrimAPI_MakeSphere, BRepPrimAPI_MakeTorus
from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse, BRepAlgoAPI_Cut, BRepAlgoAPI_Common
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
from OCP.gp import gp_Trsf, gp_Ax1, gp_Ax2, gp_Pnt, gp_Dir, gp_Vec
from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs
from OCP.BRepCheck import BRepCheck_Analyzer
cases = json.load(open(cases_path))
o = gp_Pnt(0, 0, 0)
z = gp_Dir(0, 0, 1)
def prim(e):
d, k = e["d"], e["k"]
if k == "box":
s = BRepPrimAPI_MakeBox(gp_Pnt(-d[0] / 2, -d[1] / 2, -d[2] / 2), d[0], d[1], d[2]).Shape()
elif k == "cyl":
s = BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -d[1] / 2), z), d[0], d[1]).Shape()
elif k == "sph":
s = BRepPrimAPI_MakeSphere(o, d[0]).Shape()
else:
s = BRepPrimAPI_MakeTorus(gp_Ax2(o, z), d[0], d[1]).Shape()
for axis, key in ((gp_Dir(1, 0, 0), "rx"), (gp_Dir(0, 1, 0), "ry")):
if e[key]:
t = gp_Trsf(); t.SetRotation(gp_Ax1(o, axis), math.radians(e[key]))
s = BRepBuilderAPI_Transform(s, t, True).Shape()
t = gp_Trsf(); t.SetTranslation(gp_Vec(*e["p"]))
return BRepBuilderAPI_Transform(s, t, True).Shape()
for i, c in enumerate(cases):
rec = {"case": c["name"], "idx": i, "exp": c["exp"]}
try:
cur = prim(c["a"])
for b in c["bs"]:
bb = prim(b)
alg = {"union": BRepAlgoAPI_Fuse, "subtract": BRepAlgoAPI_Cut, "intersect": BRepAlgoAPI_Common}[c["op"]](cur, bb)
cur = alg.Shape()
rec["occt_volume"] = vol_of(cur)
rec["occt_valid"] = BRepCheck_Analyzer(cur).IsValid()
w = STEPControl_Writer(); w.Transfer(cur, STEPControl_AsIs)
w.Write(os.path.join(outdir, "ref_%02d.step" % (i + 1)))
except Exception as ex:
rec["error"] = str(ex)[:200]
print("RES " + json.dumps(rec))
if __name__ == "__main__":
m = sys.argv[1]
if m == "one":
one(sys.argv[2])
elif m == "obj":
objcheck(sys.argv[2])
elif m == "ref":
build_ref(sys.argv[2], sys.argv[3])
elif m == "all":
for f in sorted(glob.glob(sys.argv[2] + "/*.step")) + sorted(glob.glob(sys.argv[2] + "/*.obj")):
if os.path.basename(f).startswith("ref_"):
continue
mode = "one" if f.endswith(".step") else "obj"
try:
r = subprocess.run([sys.executable, "-I", __file__, mode, f], capture_output=True, text=True, timeout=180)
lines = [l for l in r.stdout.splitlines() if l.startswith("RES ")]
if r.returncode != 0 or not lines:
print("RES " + json.dumps({"file": os.path.basename(f), "rc": r.returncode, "stderr_tail": r.stderr[-300:]}))
else:
print(lines[0])
except subprocess.TimeoutExpired:
print("RES " + json.dumps({"file": os.path.basename(f), "timeout": True}))

View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
# usage: job.sh <tol> (runs inside the stopgap rust container, cwd = repo root)
TOLS="${*:-0.05}"
set -u
OUT=/tmp/out; mkdir -p $OUT
echo "== build"
cargo build --release 2>&1 | tail -30
BIN=target/release/trucktorture
[ -x $BIN ] || { echo BUILD-FAILED; exit 1; }
echo "== prims"
$BIN prims 2>&1 | tail -8
N=$(grep -c '"name"' cases.json)
echo "cases=$N"
for TOL in $TOLS; do
echo "== cases tol=$TOL"
for i in $(seq 0 $((N-1))); do
s=$(date +%s.%N)
timeout ${CASE_TIMEOUT:-150} $BIN run cases.json $i $TOL $OUT > /tmp/case_$i.out 2> /tmp/case_$i.err
rc=$?
e=$(date +%s.%N)
echo "CASE idx=$i rc=$rc wall=$(echo "$e - $s" | bc 2>/dev/null || echo ?)"
grep -h '^RES' /tmp/case_$i.out | cut -c1-1500
if [ $rc -ne 0 ]; then echo "STDERR-TAIL idx=$i: $(tail -c 400 /tmp/case_$i.err | tr '\n' ' ')"; fi
done
done
echo "== python/ocp"
apt-get update -qq 2>&1 | tail -2
apt-get install -y -qq python3 python3-pip libgl1 libglu1-mesa libxrender1 2>&1 | tail -3
PY=python3
$PY -m pip install -q --break-system-packages cadquery-ocp 2>&1 | tail -3
$PY -c "import OCP; print('OCP ok')" 2>&1 | tail -2
echo "== ref (OCCT reference booleans)"
$PY check.py ref cases.json $OUT 2>&1 | cut -c1-400
echo "== check truck output (STEP via OCCT, OBJ independent)"
$PY check.py all $OUT 2>&1 | cut -c1-500
echo "== truck reads OCCT-written STEP"
for f in $OUT/ref_*.step; do timeout 120 $BIN readstep $f 2>&1 | grep -h '^RES' | cut -c1-500 || echo "READSTEP-FAIL $f"; done
echo "== done"

View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
# second probe batch: STEP entity census, determinism, tolerance sweep, licences, wasm
set -u
echo "== build"; cargo build --release 2>&1 | tail -2
BIN=target/release/trucktorture
OUT=/tmp/out5; mkdir -p $OUT
apt-get update -qq 2>&1 | tail -1; apt-get install -y -qq python3 2>&1 | tail -1
echo "== step census"
for spec in "25 0.2" "12 0.05" "29 0.05" "27 0.2"; do set -- $spec
timeout 150 $BIN run cases.json $1 $2 $OUT > /tmp/o.txt 2>/dev/null
f=$(ls $OUT/$(printf %02d $(($1+1)))_t$2.step 2>/dev/null)
echo "-- case idx=$1 tol=$2 file=$f"
[ -n "$f" ] && { grep -ho '^#[0-9]*= *[A-Z_0-9]*' $f | sed 's/#[0-9]*= *//' | sort | uniq -c | sort -rn | head -22 | tr '\n' ';'; echo; grep -m3 -n 'FILE_SCHEMA\|FILE_NAME\|MANIFOLD_SOLID_BREP\|CLOSED_SHELL' $f | cut -c1-120; }
done
echo "== determinism (3 runs each, tol 0.05)"
for idx in 10 12 25 27 29; do for r in 1 2 3; do
timeout 150 $BIN run cases.json $idx 0.05 $OUT 2>/dev/null | grep '^RES' | grep -o '"status":"[a-z_]*"\|"faces":[0-9]*\|"vol":[-0-9.e]*' | head -4 | tr '\n' ' '; echo " idx=$idx run=$r"
done; done
echo "== tolerance sweep"
for idx in 0 20 24 25 26 16; do for t in 0.5 0.1 0.02 0.005; do
/usr/bin/time -f "wall=%es rss=%MKB" timeout 200 $BIN run cases.json $idx $t $OUT > /tmp/o.txt 2> /tmp/e.txt
echo "idx=$idx tol=$t rc=$? $(grep '^RES' /tmp/o.txt | grep -o '"status":"[a-z_]*"\|"steps_done":[0-9]*\|"boolean_ms_total":[0-9.]*' | tr '\n' ' ') $(tail -1 /tmp/e.txt)"
done; done
echo "== licences (transitive deps of harness)"
cargo metadata --format-version 1 > /tmp/md.json 2>/dev/null
python3 -c "
import json,collections
d=json.load(open('/tmp/md.json'));c=collections.Counter();odd=[]
for p in d['packages']:
c[p.get('license') or 'NONE']+=1
if not p.get('license') or 'GPL' in (p.get('license') or '') or 'MPL' in (p.get('license') or ''): odd.append((p['name'],p.get('license')))
print('n_packages',len(d['packages']));print(c.most_common(12));print('odd',odd)
"
echo "== wasm build of truck-js"
rustup target add wasm32-unknown-unknown 2>&1 | tail -1
D=$(ls -d /usr/local/cargo/git/checkouts/truck-*/88ed005 | head -1); echo "checkout=$D"
cd $D && (time cargo build --release --target wasm32-unknown-unknown -p truck-js 2>&1 | tail -4)
ls -la target/wasm32-unknown-unknown/release/*.wasm 2>&1 | head -3
cd $D && grep -c "wasm_bindgen" truck-js/src/*.rs | tr '\n' ' '
echo "== done"

View file

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# third probe batch: STEP entity census, case-25 stage timing, tolerance sweep
set -u
echo "== build"; cargo build --release 2>&1 | tail -2
BIN=target/release/trucktorture
OUT=/tmp/out6; mkdir -p $OUT
echo "== step census"
for spec in "25 0.2" "12 0.05" "29 0.05" "27 0.2" "10 0.05"; do
set -- $spec
timeout 150 $BIN run cases.json $1 $2 $OUT > /tmp/o.txt 2>/dev/null
f=$(ls $OUT/$(printf %02d $(($1+1)))_t$2.step 2>/dev/null)
echo "-- case idx=$1 tol=$2 file=$f"
if [ -n "$f" ]; then
grep -ho '^#[0-9]* *= *[A-Z_0-9]*' $f | sed 's/#[0-9]* *= *//' | sort | uniq -c | sort -rn | head -24 | tr '\n' ';'
echo
fi
done
echo "== case 25 stage timing x3"
for r in 1 2 3; do
timeout 200 $BIN run cases.json 24 0.05 $OUT 2>&1 >/dev/null | grep STAGE | tr '\n' ' '
echo " run=$r"
done
echo "== tolerance sweep"
for idx in 0 20 24 25 26 16; do
for t in 0.5 0.1 0.02 0.005; do
s0=$(date +%s%N)
timeout 200 $BIN run cases.json $idx $t $OUT > /tmp/o.txt 2> /tmp/e.txt
rc=$?
echo "idx=$idx tol=$t rc=$rc wall=$(( ($(date +%s%N)-s0)/1000000 ))ms $(grep '^RES' /tmp/o.txt | grep -o '"status":"[a-z_]*"\|"steps_done":[0-9]*\|"boolean_ms_total":[0-9.]*' | tr '\n' ' ')"
done
done
echo "== done"

View file

@ -0,0 +1,263 @@
// Torture harness for truck (ricosjp/truck), mirrors the Aetheris harness cases (cases.json).
use serde_json::{json, Value};
use std::f64::consts::PI;
use std::io::BufWriter;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Mutex;
use std::time::Instant;
use truck_meshalgo::prelude::*;
use truck_modeling::*;
use truck_stepio::out;
static PANIC: Mutex<String> = Mutex::new(String::new());
fn take_panic() -> String {
let mut g = PANIC.lock().unwrap();
let s = g.clone();
g.clear();
s.chars().take(300).collect()
}
fn box_(d: &[f64]) -> Solid {
let v = builder::vertex(Point3::origin());
let e = builder::tsweep(&v, Vector3::unit_x());
let f = builder::tsweep(&e, Vector3::unit_y());
let c: Solid = builder::tsweep(&f, Vector3::unit_z());
let c = builder::scaled(&c, Point3::origin(), Vector3::new(d[0], d[1], d[2]));
builder::translated(&c, Vector3::new(-d[0] / 2.0, -d[1] / 2.0, -d[2] / 2.0))
}
fn cyl(r: f64, h: f64) -> Solid {
let v = builder::vertex(Point3::new(r, 0.0, -h / 2.0));
let circle = builder::rsweep(&v, Point3::new(0.0, 0.0, -h / 2.0), Vector3::unit_z(), Rad(7.0), 2);
let disk = builder::try_attach_plane(&[circle]).unwrap();
builder::tsweep(&disk, Vector3::new(0.0, 0.0, h))
}
fn sph(r: f64) -> Solid {
let v0 = builder::vertex(Point3::new(0.0, r, 0.0));
let wire: Wire = builder::rsweep(&v0, Point3::origin(), Vector3::unit_x(), Rad(PI), 3);
let shell = builder::cone(&wire, Vector3::unit_y(), Rad(7.0), 4);
Solid::new(vec![shell])
}
fn tor(big: f64, small: f64) -> Solid {
let v = builder::vertex(Point3::new(big, 0.0, small));
let w = builder::rsweep(&v, Point3::new(big, 0.0, 0.0), Vector3::unit_y(), Rad(7.0), 2);
let shell = builder::rsweep(&w, Point3::origin(), Vector3::unit_z(), Rad(7.0), 2);
Solid::new(vec![shell])
}
fn mesh_of(s: &Solid, tol: f64) -> PolygonMesh {
let mut m = s.triangulation(tol).to_polygon();
m.put_together_same_attrs(TOLERANCE * 50.0).remove_degenerate_faces();
m
}
fn orient(mut s: Solid) -> Solid {
if mesh_of(&s, 0.05).volume() < 0.0 {
s.not();
}
s
}
fn nums(v: &Value) -> Vec<f64> {
v.as_array().unwrap().iter().map(|x| x.as_f64().unwrap()).collect()
}
fn prim(e: &Value) -> Solid {
let d = nums(&e["d"]);
let s = match e["k"].as_str().unwrap() {
"box" => box_(&d),
"cyl" => cyl(d[0], d[1]),
"sph" => sph(d[0]),
"tor" => tor(d[0], d[1]),
k => panic!("kind {k}"),
};
let mut s = orient(s);
let rx = e["rx"].as_f64().unwrap();
let ry = e["ry"].as_f64().unwrap();
if rx != 0.0 {
s = builder::rotated(&s, Point3::origin(), Vector3::unit_x(), Rad(rx.to_radians()));
}
if ry != 0.0 {
s = builder::rotated(&s, Point3::origin(), Vector3::unit_y(), Rad(ry.to_radians()));
}
let p = nums(&e["p"]);
builder::translated(&s, Vector3::new(p[0], p[1], p[2]))
}
fn apply(op: &str, a: &Solid, b: &Solid, tol: f64) -> Option<Solid> {
match op {
"union" => truck_shapeops::or(a, b, tol),
"intersect" => truck_shapeops::and(a, b, tol),
"subtract" => {
let mut nb = b.clone();
nb.not();
truck_shapeops::and(a, &nb, tol)
}
o => panic!("op {o}"),
}
}
fn mesh_stats(s: &Solid, tol: f64, objpath: Option<&str>) -> Value {
let t = Instant::now();
let r = catch_unwind(AssertUnwindSafe(|| mesh_of(s, tol)));
let ms = t.elapsed().as_secs_f64() * 1e3;
match r {
Ok(m) => {
if let Some(p) = objpath {
let f = std::fs::File::create(p).unwrap();
let _ = truck_meshalgo::rexport_polymesh::obj::write(&m, BufWriter::new(f));
}
json!({"tol": tol, "ms": ms, "tris": m.faces().triangle_iter().count(), "faces": m.faces().len(),
"closed": format!("{:?}", m.shell_condition()), "vol": m.volume()})
}
Err(_) => json!({"tol": tol, "panic": take_panic()}),
}
}
fn step_string(s: &Solid) -> String {
let c = s.compress();
let repr = out::StepDesign::from_model(out::StepModel::from(&c));
out::StepDisplay::new(
out::StepHeaderDescriptor { organization_system: "trucktorture".to_owned(), ..Default::default() },
repr,
)
.to_string()
}
fn reread(text: &str) -> Value {
use truck_stepio::r#in::*;
let r = catch_unwind(AssertUnwindSafe(|| {
let table = Table::from_step(text).ok_or("parse failed")?;
let mut shells = vec![];
for (_, sh) in table.shell.iter() {
match table.to_compressed_shell(sh) {
Ok(cs) => {
let mut m = cs.triangulation(0.01).to_polygon();
m.put_together_same_attrs(TOLERANCE * 50.0).remove_degenerate_faces();
shells.push(json!({"closed": format!("{:?}", m.shell_condition()), "vol": m.volume(),
"tris": m.faces().triangle_iter().count()}));
}
Err(e) => shells.push(json!({"err": format!("{e:?}").chars().take(200).collect::<String>()})),
}
}
Ok::<_, &str>(json!({"shells": shells}))
}));
match r {
Ok(Ok(v)) => v,
Ok(Err(e)) => json!({"err": e}),
Err(_) => json!({"panic": take_panic()}),
}
}
fn run_case(cases: &Value, idx: usize, tol: f64, out_dir: &str) {
let c = &cases[idx];
let name = c["name"].as_str().unwrap();
let op = c["op"].as_str().unwrap();
let bs = c["bs"].as_array().unwrap();
let t0 = Instant::now();
let mut step_ms: Vec<f64> = vec![];
let r = catch_unwind(AssertUnwindSafe(|| {
let mut cur = prim(&c["a"]);
for b in bs {
let bb = prim(b);
let t = Instant::now();
let n = apply(op, &cur, &bb, tol);
step_ms.push(t.elapsed().as_secs_f64() * 1e3);
match n {
Some(n) => cur = n,
None => return None,
}
}
Some(cur)
}));
let total = t0.elapsed().as_secs_f64() * 1e3;
eprintln!("STAGE boolean done {:.0}ms", total);
let maxstep = step_ms.iter().cloned().fold(0.0, f64::max);
let mut res = json!({"case": name, "idx": idx, "tol": tol, "steps_done": step_ms.len(), "steps_total": bs.len(),
"boolean_ms_total": total, "boolean_ms_max_step": maxstep, "exp": c["exp"]});
match r {
Err(_) => {
res["status"] = json!("panic");
res["panic"] = json!(take_panic());
}
Ok(None) => res["status"] = json!("refused_none"),
Ok(Some(s)) => {
res["status"] = json!("ok");
res["shells"] = json!(s.boundaries().len());
res["faces"] = json!(s.boundaries().iter().map(|b| b.len()).sum::<usize>());
let tag = format!("{}/{:02}_t{}", out_dir, idx + 1, tol);
res["mesh"] = json!([
mesh_stats(&s, 0.1, None),
mesh_stats(&s, 0.01, Some(&format!("{tag}.obj"))),
mesh_stats(&s, 0.001, None)
]);
eprintln!("STAGE meshes done {:.0}ms", t0.elapsed().as_secs_f64() * 1e3);
let t = Instant::now();
let sr = catch_unwind(AssertUnwindSafe(|| step_string(&s)));
match sr {
Ok(text) => {
res["step_ms"] = json!(t.elapsed().as_secs_f64() * 1e3);
res["step_bytes"] = json!(text.len());
std::fs::write(format!("{tag}.step"), &text).unwrap();
eprintln!("STAGE step written {:.0}ms", t0.elapsed().as_secs_f64() * 1e3);
res["step_reread"] = reread(&text);
eprintln!("STAGE reread done {:.0}ms", t0.elapsed().as_secs_f64() * 1e3);
}
Err(_) => res["step_panic"] = json!(take_panic()),
}
}
}
println!("RES {}", res);
}
fn read_step(path: &str) {
let text = std::fs::read_to_string(path).unwrap();
let t = Instant::now();
let mut v = reread(&text);
v["file"] = json!(path);
v["ms"] = json!(t.elapsed().as_secs_f64() * 1e3);
println!("RES {}", v);
}
fn prims() {
for (n, s, exp) in [
("box 10x10x10", box_(&[10.0, 10.0, 10.0]), 1000.0),
("cyl r5 h10", cyl(5.0, 10.0), PI * 25.0 * 10.0),
("sph r5", sph(5.0), 4.0 / 3.0 * PI * 125.0),
("tor 10,3", tor(10.0, 3.0), 2.0 * PI * PI * 10.0 * 9.0),
] {
let raw = mesh_of(&s, 0.01).volume();
let o = orient(s.clone());
println!(
"RES {}",
json!({"prim": n, "analytic": exp, "mesh_vol_raw_orientation": raw,
"mesh_vol_after_orient": mesh_of(&o, 0.01).volume()})
);
}
}
fn main() {
std::panic::set_hook(Box::new(|i| {
let bt = std::backtrace::Backtrace::force_capture().to_string();
let frames: Vec<String> = bt
.lines()
.filter(|l| l.contains("truck_") && !l.trim_start().starts_with("at "))
.map(|l| l.trim().chars().take(70).collect::<String>())
.take(6)
.collect();
*PANIC.lock().unwrap() = format!("{i}").replace('\n', " | ") + " || " + &frames.join(" ; ");
}));
let a: Vec<String> = std::env::args().collect();
match a[1].as_str() {
"run" => {
let cases: Value = serde_json::from_str(&std::fs::read_to_string(&a[2]).unwrap()).unwrap();
run_case(&cases, a[3].parse().unwrap(), a[4].parse().unwrap(), &a[5]);
}
"readstep" => read_step(&a[2]),
"prims" => prims(),
_ => panic!("mode"),
}
}

View file

@ -1 +0,0 @@
target/

View file

@ -1,207 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "boolmesh"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6504723206a532f9cace8eb63d339c14a17ad9c2b06d7c35b1c3127f7b945f"
dependencies = [
"glam",
]
[[package]]
name = "cc"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "glam"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0"
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "log"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "manifold-csg"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ba4dd58c980405d5a23657a59106e2844a36d3c25b745ee7a0a3e0524e6bc4d"
dependencies = [
"log",
"manifold-csg-sys",
"thiserror",
]
[[package]]
name = "manifold-csg-sys"
version = "3.5.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58f034b8af43be927e7651da0c18a0205e8942acf275550acc4a8300042dee51"
dependencies = [
"cmake",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "robust"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839"
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "smallvec"
version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
[[package]]
name = "spade"
version = "2.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa"
dependencies = [
"hashbrown",
"num-traits",
"robust",
"smallvec",
]
[[package]]
name = "spike-boolean-weld"
version = "0.0.0"
dependencies = [
"boolmesh",
"manifold-csg",
"spade",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"

View file

@ -1,21 +0,0 @@
[package]
name = "spike-boolean-weld"
version = "0.0.0"
edition = "2021"
publish = false
# Scratch spike crate (brush-tree spike-boolean-weld). Not a workspace member of the main crate.
[workspace]
[features]
default = []
manifold = ["dep:manifold-csg"]
[dependencies]
manifold-csg = { version = "=0.4.1", optional = true }
boolmesh = "=0.2.1"
# Round 4: constrained Delaunay for created faces (MIT OR Apache-2.0), replacing Shewchuk's Triangle.
spade = "=2.15.1"
[profile.release]
debug = false

View file

@ -1,20 +0,0 @@
v 0.201069627 -0.000045536 0.078752425
v 0.207746278 0.008569293 0.117238877
v 0.172253320 -0.038568992 0.092374606
v 0.178929972 -0.029954163 0.130861059
v 0.161070028 0.029954163 0.078976428
v 0.167746680 0.038568992 0.117462880
v 0.132253722 -0.008569293 0.092598609
v 0.138930373 0.000045536 0.131085062
f 1 2 4
f 5 7 8
f 1 5 6
f 3 4 8
f 1 3 7
f 2 6 8
f 1 4 3
f 5 8 6
f 1 6 2
f 3 8 7
f 1 7 5
f 2 8 4

File diff suppressed because it is too large Load diff

View file

@ -1,98 +0,0 @@
v 0.166661674 0.010330357 0.082398990
v 0.161001952 0.009425415 0.083583404
v 0.156203872 0.006429774 0.085086324
v 0.152997897 0.001799493 0.086678943
v 0.151872108 -0.003760509 0.088118800
v 0.152997897 -0.009403773 0.089186688
v 0.156203872 -0.014271162 0.089720033
v 0.161001952 -0.017621661 0.089637637
v 0.166661674 -0.018945187 0.088952044
v 0.172321396 -0.018040245 0.087767630
v 0.177119477 -0.015044603 0.086264710
v 0.180325452 -0.010414323 0.084672091
v 0.181451240 -0.004854321 0.083232234
v 0.180325452 0.000788943 0.082164346
v 0.177119477 0.005656332 0.081631001
v 0.172321396 0.009006831 0.081713397
v 0.173338326 0.018945187 0.120885442
v 0.167678604 0.018040245 0.122069857
v 0.162880523 0.015044603 0.123572777
v 0.159674548 0.010414323 0.125165396
v 0.158548760 0.004854321 0.126605252
v 0.159674548 -0.000788943 0.127673141
v 0.162880523 -0.005656332 0.128206486
v 0.167678604 -0.009006831 0.128124090
v 0.173338326 -0.010330357 0.127438497
v 0.178998048 -0.009425415 0.126254082
v 0.183796128 -0.006429774 0.124751163
v 0.187002103 -0.001799493 0.123158544
v 0.188127892 0.003760509 0.121718687
v 0.187002103 0.009403773 0.120650798
v 0.183796128 0.014271162 0.120117453
v 0.178998048 0.017621661 0.120199849
v 0.166661674 -0.004307415 0.085675517
v 0.173338326 0.004307415 0.124161970
f 1 2 18
f 1 18 17
f 33 2 1
f 34 17 18
f 2 3 19
f 2 19 18
f 33 3 2
f 34 18 19
f 3 4 20
f 3 20 19
f 33 4 3
f 34 19 20
f 4 5 21
f 4 21 20
f 33 5 4
f 34 20 21
f 5 6 22
f 5 22 21
f 33 6 5
f 34 21 22
f 6 7 23
f 6 23 22
f 33 7 6
f 34 22 23
f 7 8 24
f 7 24 23
f 33 8 7
f 34 23 24
f 8 9 25
f 8 25 24
f 33 9 8
f 34 24 25
f 9 10 26
f 9 26 25
f 33 10 9
f 34 25 26
f 10 11 27
f 10 27 26
f 33 11 10
f 34 26 27
f 11 12 28
f 11 28 27
f 33 12 11
f 34 27 28
f 12 13 29
f 12 29 28
f 33 13 12
f 34 28 29
f 13 14 30
f 13 30 29
f 33 14 13
f 34 29 30
f 14 15 31
f 14 31 30
f 33 15 14
f 34 30 31
f 15 16 32
f 15 32 31
f 33 16 15
f 34 31 32
f 16 1 17
f 16 17 32
f 33 1 16
f 34 32 17

View file

@ -1,409 +0,0 @@
[
{
"name": "port_flank_s16",
"kind": "cyl",
"op": "sub",
"cutter": "cutters/port_flank_s16.obj",
"p": [
0.17,
0.0,
0.10491874328928341
],
"n": [
0.16691628816232543,
0.21537074072406956,
0.962161315365608
],
"r": 0.015,
"seg": 16,
"thick": 0.10439623157800804,
"depth": 0.02,
"note": "flank gun port"
},
{
"name": "port_flank_s48",
"kind": "cyl",
"op": "sub",
"cutter": "cutters/port_flank_s48.obj",
"p": [
0.17,
0.0,
0.10491874328928341
],
"n": [
0.16691628816232543,
0.21537074072406956,
0.962161315365608
],
"r": 0.015,
"seg": 48,
"thick": 0.10439623157800804,
"depth": 0.02,
"note": "flank gun port"
},
{
"name": "split_fwd_sub",
"kind": "box",
"op": "sub",
"cutter": "cutters/split_fwd.obj",
"lo": [
-0.8,
0.22,
-0.6
],
"hi": [
0.8,
0.8,
0.6
],
"note": "transverse split, forward body (crosses the rim crease twice)"
},
{
"name": "split_fwd_int",
"kind": "box",
"op": "int",
"cutter": "cutters/split_fwd.obj",
"lo": [
-0.8,
0.22,
-0.6
],
"hi": [
0.8,
0.8,
0.6
],
"note": "transverse split, forward body (crosses the rim crease twice)"
},
{
"name": "probe_equalk_s16",
"kind": "cyl",
"op": "sub",
"cutter": "cutters/probe_equalk_s16.obj",
"p": [
-0.0051448,
0.4898214,
-0.1458136999999997
],
"n": [
-0.3623529208037491,
0.7424111177624207,
0.5634945368038141
],
"r": 0.01992831921910964,
"seg": 16,
"thick": 0.0369133824014618,
"depth": 0.02,
"note": "vertex 1799, r=1/|k|max=0.0199, |k|=50.18"
},
{
"name": "probe_equalk_s48",
"kind": "cyl",
"op": "sub",
"cutter": "cutters/probe_equalk_s48.obj",
"p": [
-0.0051448,
0.4898214,
-0.1458136999999997
],
"n": [
-0.3623529208037491,
0.7424111177624207,
0.5634945368038141
],
"r": 0.01992831921910964,
"seg": 48,
"thick": 0.0369133824014618,
"depth": 0.02,
"note": "vertex 1799, r=1/|k|max=0.0199, |k|=50.18"
},
{
"name": "probe_saddle_s16",
"kind": "cyl",
"op": "sub",
"cutter": "cutters/probe_saddle_s16.obj",
"p": [
0.2801811,
-0.1680136,
0.1273491000000001
],
"n": [
0.005070735374261266,
-0.9190006203256085,
0.39422347403967595
],
"r": 0.015,
"seg": 16,
"thick": 0.014325938653540371,
"depth": 0.008595563192124222,
"note": "saddle vertex 538 k1=-7.00 k2=55.16"
},
{
"name": "probe_saddle_s48",
"kind": "cyl",
"op": "sub",
"cutter": "cutters/probe_saddle_s48.obj",
"p": [
0.2801811,
-0.1680136,
0.1273491000000001
],
"n": [
0.005070735374261266,
-0.9190006203256085,
0.39422347403967595
],
"r": 0.015,
"seg": 48,
"thick": 0.014325938653540371,
"depth": 0.008595563192124222,
"note": "saddle vertex 538 k1=-7.00 k2=55.16"
},
{
"name": "probe_crease_sub",
"kind": "box",
"op": "sub",
"cutter": "cutters/probe_crease.obj",
"lo": [
0.33,
-0.8,
-0.6
],
"hi": [
0.9,
0.8,
0.6
],
"note": "chordwise split x>0.33 crosses the rim crease"
},
{
"name": "probe_crease_int",
"kind": "box",
"op": "int",
"cutter": "cutters/probe_crease.obj",
"lo": [
0.33,
-0.8,
-0.6
],
"hi": [
0.9,
0.8,
0.6
],
"note": "chordwise split x>0.33 crosses the rim crease"
},
{
"name": "probe_switch_s48",
"kind": "cyl",
"op": "sub",
"cutter": "cutters/probe_switch_s48.obj",
"p": [
0.0,
-0.05,
0.1680719189037999
],
"n": [
-0.09574350308538446,
0.11061947876461269,
0.9892403714642788
],
"r": 0.08,
"seg": 48,
"thick": 0.2200342785460706,
"depth": 0.02,
"note": "r=0.08 across the dorsal ridge"
},
{
"name": "probe_crease_cyl_s16",
"kind": "cyl",
"op": "sub",
"cutter": "cutters/probe_crease_cyl_s16.obj",
"p": [
0.4225163,
-0.0367445,
0.0935993
],
"n": [
0,
0,
1.0
],
"r": 0.03,
"seg": 16,
"depth": 0.15,
"note": "vertical cylinder r 0.03 through rim crease vertex 35"
},
{
"name": "probe_crease_cyl_s48",
"kind": "cyl",
"op": "sub",
"cutter": "cutters/probe_crease_cyl_s48.obj",
"p": [
0.4225163,
-0.0367445,
0.0935993
],
"n": [
0,
0,
1.0
],
"r": 0.03,
"seg": 48,
"depth": 0.15,
"note": "vertical cylinder r 0.03 through rim crease vertex 35"
},
{
"name": "probe_box_flank",
"kind": "obox",
"op": "sub",
"cutter": "cutters/probe_box_flank.obj",
"p": [
0.17,
0.0,
0.10491874328928341
],
"n": [
0.16691628816232545,
0.2153707407240696,
0.9621613153656081
],
"R": [
[
-0.7999919715756821,
-0.5763261213588748,
0.16691628816232545
],
[
0.5999939786817615,
-0.770469123057891,
0.2153707407240696
],
[
0.004480062508814215,
0.27244363133286853,
0.9621613153656081
]
],
"lo": [
-0.02,
-0.02,
-0.02
],
"hi": [
0.02,
0.02,
0.03
],
"note": "oriented box half-width 0.02"
},
{
"name": "probe_box_ridge",
"kind": "obox",
"op": "sub",
"cutter": "cutters/probe_box_ridge.obj",
"p": [
0.0,
-0.05,
0.1680719189037999
],
"n": [
-0.09574350308538447,
0.1106194787646127,
0.9892403714642789
],
"R": [
[
-0.7917740521194535,
-0.6032636504939445,
-0.09574350308538447
],
[
0.5938305390895902,
-0.7969495729106153,
0.1106194787646127
],
[
-0.14303545446814095,
0.03073021689327954,
0.9892403714642789
]
],
"lo": [
-0.06,
-0.06,
-0.02
],
"hi": [
0.06,
0.06,
0.03
],
"note": "oriented box half-width 0.06"
},
{
"name": "add_box_flank",
"kind": "obox",
"op": "add",
"cutter": "cutters/add_box_flank.obj",
"p": [
0.17,
0.0,
0.10491874328928341
],
"n": [
0.16691628816232545,
0.2153707407240696,
0.9621613153656081
],
"R": [
[
-0.7999919715756821,
-0.5763261213588748,
0.16691628816232545
],
[
0.5999939786817615,
-0.770469123057891,
0.2153707407240696
],
[
0.004480062508814215,
0.27244363133286853,
0.9621613153656081
]
],
"lo": [
-0.025,
-0.025,
-0.02
],
"hi": [
0.025,
0.025,
0.02
],
"note": "add: oriented box raised panel, half-width 0.025, 0.02 proud"
},
{
"name": "add_cyl_flank_s16",
"kind": "cyl",
"op": "add",
"cutter": "cutters/add_cyl_flank_s16.obj",
"p": [
0.17,
0.0,
0.10491874328928341
],
"n": [
0.16691628816232545,
0.2153707407240696,
0.9621613153656081
],
"r": 0.015,
"seg": 16,
"depth": 0.02,
"note": "add: 16-segment cylinder boss r 0.015, 0.02 proud, at the gun port"
}
]

File diff suppressed because it is too large Load diff

View file

@ -1,98 +0,0 @@
v 0.166661674 0.010330357 0.082398990
v 0.161001952 0.009425415 0.083583404
v 0.156203872 0.006429774 0.085086324
v 0.152997897 0.001799493 0.086678943
v 0.151872108 -0.003760509 0.088118800
v 0.152997897 -0.009403773 0.089186688
v 0.156203872 -0.014271162 0.089720033
v 0.161001952 -0.017621661 0.089637637
v 0.166661674 -0.018945187 0.088952044
v 0.172321396 -0.018040245 0.087767630
v 0.177119477 -0.015044603 0.086264710
v 0.180325452 -0.010414323 0.084672091
v 0.181451240 -0.004854321 0.083232234
v 0.180325452 0.000788943 0.082164346
v 0.177119477 0.005656332 0.081631001
v 0.172321396 0.009006831 0.081713397
v 0.175007489 0.021098894 0.130507055
v 0.169347767 0.020193952 0.131691470
v 0.164549686 0.017198311 0.133194390
v 0.161343711 0.012568030 0.134787009
v 0.160217923 0.007008028 0.136226865
v 0.161343711 0.001364764 0.137294754
v 0.164549686 -0.003502625 0.137828099
v 0.169347767 -0.006853124 0.137745703
v 0.175007489 -0.008176650 0.137060110
v 0.180667211 -0.007271708 0.135875695
v 0.185465291 -0.004276066 0.134372776
v 0.188671266 0.000354214 0.132780157
v 0.189797055 0.005914216 0.131340300
v 0.188671266 0.011557480 0.130272411
v 0.185465291 0.016424869 0.129739066
v 0.180667211 0.019775368 0.129821462
v 0.166661674 -0.004307415 0.085675517
v 0.175007489 0.006461122 0.133783583
f 1 2 18
f 1 18 17
f 33 2 1
f 34 17 18
f 2 3 19
f 2 19 18
f 33 3 2
f 34 18 19
f 3 4 20
f 3 20 19
f 33 4 3
f 34 19 20
f 4 5 21
f 4 21 20
f 33 5 4
f 34 20 21
f 5 6 22
f 5 22 21
f 33 6 5
f 34 21 22
f 6 7 23
f 6 23 22
f 33 7 6
f 34 22 23
f 7 8 24
f 7 24 23
f 33 8 7
f 34 23 24
f 8 9 25
f 8 25 24
f 33 9 8
f 34 24 25
f 9 10 26
f 9 26 25
f 33 10 9
f 34 25 26
f 10 11 27
f 10 27 26
f 33 11 10
f 34 26 27
f 11 12 28
f 11 28 27
f 33 12 11
f 34 27 28
f 12 13 29
f 12 29 28
f 33 13 12
f 34 28 29
f 13 14 30
f 13 30 29
f 33 14 13
f 34 29 30
f 14 15 31
f 14 31 30
f 33 15 14
f 34 30 31
f 15 16 32
f 15 32 31
f 33 16 15
f 34 31 32
f 16 1 17
f 16 17 32
f 33 1 16
f 34 32 17

View file

@ -1,290 +0,0 @@
v 0.166661674 0.010330357 0.082398990
v 0.164731248 0.010276515 0.082745933
v 0.162833853 0.009973137 0.083143003
v 0.161001952 0.009425415 0.083583404
v 0.159266891 0.008642720 0.084059602
v 0.157658357 0.007638446 0.084563449
v 0.156203872 0.006429774 0.085086324
v 0.154928323 0.005037386 0.085619280
v 0.153853534 0.003485105 0.086153198
v 0.152997897 0.001799493 0.086678943
v 0.152376050 0.000009390 0.087187519
v 0.151998635 -0.001854575 0.087670224
v 0.151872108 -0.003760509 0.088118800
v 0.151998635 -0.005675801 0.088525570
v 0.152376050 -0.007567679 0.088883574
v 0.152997897 -0.009403773 0.089186688
v 0.153853534 -0.011152667 0.089429725
v 0.154928323 -0.012784436 0.089608527
v 0.156203872 -0.014271162 0.089720033
v 0.157658357 -0.015587405 0.089762337
v 0.159266891 -0.016710644 0.089734714
v 0.161001952 -0.017621661 0.089637637
v 0.162833853 -0.018304867 0.089472767
v 0.164731248 -0.018748573 0.089242925
v 0.166661674 -0.018945187 0.088952044
v 0.168592100 -0.018891344 0.088605101
v 0.170489496 -0.018587966 0.088208031
v 0.172321396 -0.018040245 0.087767630
v 0.174056457 -0.017257550 0.087291432
v 0.175664992 -0.016253275 0.086787585
v 0.177119477 -0.015044603 0.086264710
v 0.178395026 -0.013652215 0.085731754
v 0.179469814 -0.012099935 0.085197836
v 0.180325452 -0.010414323 0.084672091
v 0.180947298 -0.008624219 0.084163515
v 0.181324714 -0.006760254 0.083680810
v 0.181451240 -0.004854321 0.083232234
v 0.181324714 -0.002939029 0.082825464
v 0.180947298 -0.001047151 0.082467460
v 0.180325452 0.000788943 0.082164346
v 0.179469814 0.002537837 0.081921309
v 0.178395026 0.004169607 0.081742507
v 0.177119477 0.005656332 0.081631001
v 0.175664992 0.006972575 0.081588697
v 0.174056457 0.008095815 0.081616320
v 0.172321396 0.009006831 0.081713397
v 0.170489496 0.009690038 0.081878267
v 0.168592100 0.010133743 0.082108108
v 0.175007489 0.021098894 0.130507055
v 0.173077063 0.021045052 0.130853999
v 0.171179667 0.020741674 0.131251068
v 0.169347767 0.020193952 0.131691470
v 0.167612706 0.019411258 0.132167668
v 0.166004171 0.018406983 0.132671515
v 0.164549686 0.017198311 0.133194390
v 0.163274137 0.015805923 0.133727346
v 0.162199349 0.014253643 0.134261264
v 0.161343711 0.012568030 0.134787009
v 0.160721865 0.010777927 0.135295585
v 0.160344449 0.008913962 0.135778290
v 0.160217923 0.007008028 0.136226865
v 0.160344449 0.005092736 0.136633635
v 0.160721865 0.003200858 0.136991640
v 0.161343711 0.001364764 0.137294754
v 0.162199349 -0.000384129 0.137537791
v 0.163274137 -0.002015899 0.137716593
v 0.164549686 -0.003502625 0.137828099
v 0.166004171 -0.004818868 0.137870403
v 0.167612706 -0.005942107 0.137842780
v 0.169347767 -0.006853124 0.137745703
v 0.171179667 -0.007536330 0.137580833
v 0.173077063 -0.007980036 0.137350991
v 0.175007489 -0.008176650 0.137060110
v 0.176937914 -0.008122807 0.136713167
v 0.178835310 -0.007819429 0.136316097
v 0.180667211 -0.007271708 0.135875695
v 0.182402272 -0.006489013 0.135399497
v 0.184010806 -0.005484738 0.134895650
v 0.185465291 -0.004276066 0.134372776
v 0.186740840 -0.002883678 0.133839820
v 0.187815629 -0.001331398 0.133305902
v 0.188671266 0.000354214 0.132780157
v 0.189293112 0.002144318 0.132271581
v 0.189670528 0.004008283 0.131788875
v 0.189797055 0.005914216 0.131340300
v 0.189670528 0.007829508 0.130933530
v 0.189293112 0.009721386 0.130575525
v 0.188671266 0.011557480 0.130272411
v 0.187815629 0.013306374 0.130029374
v 0.186740840 0.014938144 0.129850573
v 0.185465291 0.016424869 0.129739066
v 0.184010806 0.017741112 0.129696763
v 0.182402272 0.018864352 0.129724386
v 0.180667211 0.019775368 0.129821462
v 0.178835310 0.020458575 0.129986332
v 0.176937914 0.020902281 0.130216174
v 0.166661674 -0.004307415 0.085675517
v 0.175007489 0.006461122 0.133783583
f 1 2 50
f 1 50 49
f 97 2 1
f 98 49 50
f 2 3 51
f 2 51 50
f 97 3 2
f 98 50 51
f 3 4 52
f 3 52 51
f 97 4 3
f 98 51 52
f 4 5 53
f 4 53 52
f 97 5 4
f 98 52 53
f 5 6 54
f 5 54 53
f 97 6 5
f 98 53 54
f 6 7 55
f 6 55 54
f 97 7 6
f 98 54 55
f 7 8 56
f 7 56 55
f 97 8 7
f 98 55 56
f 8 9 57
f 8 57 56
f 97 9 8
f 98 56 57
f 9 10 58
f 9 58 57
f 97 10 9
f 98 57 58
f 10 11 59
f 10 59 58
f 97 11 10
f 98 58 59
f 11 12 60
f 11 60 59
f 97 12 11
f 98 59 60
f 12 13 61
f 12 61 60
f 97 13 12
f 98 60 61
f 13 14 62
f 13 62 61
f 97 14 13
f 98 61 62
f 14 15 63
f 14 63 62
f 97 15 14
f 98 62 63
f 15 16 64
f 15 64 63
f 97 16 15
f 98 63 64
f 16 17 65
f 16 65 64
f 97 17 16
f 98 64 65
f 17 18 66
f 17 66 65
f 97 18 17
f 98 65 66
f 18 19 67
f 18 67 66
f 97 19 18
f 98 66 67
f 19 20 68
f 19 68 67
f 97 20 19
f 98 67 68
f 20 21 69
f 20 69 68
f 97 21 20
f 98 68 69
f 21 22 70
f 21 70 69
f 97 22 21
f 98 69 70
f 22 23 71
f 22 71 70
f 97 23 22
f 98 70 71
f 23 24 72
f 23 72 71
f 97 24 23
f 98 71 72
f 24 25 73
f 24 73 72
f 97 25 24
f 98 72 73
f 25 26 74
f 25 74 73
f 97 26 25
f 98 73 74
f 26 27 75
f 26 75 74
f 97 27 26
f 98 74 75
f 27 28 76
f 27 76 75
f 97 28 27
f 98 75 76
f 28 29 77
f 28 77 76
f 97 29 28
f 98 76 77
f 29 30 78
f 29 78 77
f 97 30 29
f 98 77 78
f 30 31 79
f 30 79 78
f 97 31 30
f 98 78 79
f 31 32 80
f 31 80 79
f 97 32 31
f 98 79 80
f 32 33 81
f 32 81 80
f 97 33 32
f 98 80 81
f 33 34 82
f 33 82 81
f 97 34 33
f 98 81 82
f 34 35 83
f 34 83 82
f 97 35 34
f 98 82 83
f 35 36 84
f 35 84 83
f 97 36 35
f 98 83 84
f 36 37 85
f 36 85 84
f 97 37 36
f 98 84 85
f 37 38 86
f 37 86 85
f 97 38 37
f 98 85 86
f 38 39 87
f 38 87 86
f 97 39 38
f 98 86 87
f 39 40 88
f 39 88 87
f 97 40 39
f 98 87 88
f 40 41 89
f 40 89 88
f 97 41 40
f 98 88 89
f 41 42 90
f 41 90 89
f 97 42 41
f 98 89 90
f 42 43 91
f 42 91 90
f 97 43 42
f 98 90 91
f 43 44 92
f 43 92 91
f 97 44 43
f 98 91 92
f 44 45 93
f 44 93 92
f 97 45 44
f 98 92 93
f 45 46 94
f 45 94 93
f 97 46 45
f 98 93 94
f 46 47 95
f 46 95 94
f 97 47 46
f 98 94 95
f 47 48 96
f 47 96 95
f 97 48 47
f 98 95 96
f 48 1 49
f 48 49 96
f 97 1 48
f 98 96 49

View file

@ -1,20 +0,0 @@
v 0.194188036 -0.000897912 0.080137043
v 0.202533851 0.009870625 0.128245109
v 0.171134991 -0.031716677 0.091034788
v 0.179480806 -0.020948140 0.139142854
v 0.162188357 0.023101847 0.080316246
v 0.170534172 0.033870384 0.128424311
v 0.139135312 -0.007716918 0.091213991
v 0.147481127 0.003051619 0.139322057
f 1 2 4
f 5 7 8
f 1 5 6
f 3 4 8
f 1 3 7
f 2 6 8
f 1 4 3
f 5 8 6
f 1 6 2
f 3 8 7
f 1 7 5
f 2 8 4

View file

@ -1,20 +0,0 @@
v 0.085617132 -0.040025248 0.155025426
v 0.080829957 -0.034494274 0.204487444
v 0.013225494 -0.135659196 0.158713052
v 0.008438319 -0.130128222 0.208175070
v -0.009395754 0.031234417 0.137861171
v -0.014182929 0.036765391 0.187323190
v -0.081787392 -0.064399532 0.141548797
v -0.086574567 -0.058868558 0.191010816
f 1 2 4
f 5 7 8
f 1 5 6
f 3 4 8
f 1 3 7
f 2 6 8
f 1 4 3
f 5 8 6
f 1 6 2
f 3 8 7
f 1 7 5
f 2 8 4

View file

@ -1,20 +0,0 @@
v 0.330000000 -0.800000000 -0.600000000
v 0.330000000 -0.800000000 0.600000000
v 0.330000000 0.800000000 -0.600000000
v 0.330000000 0.800000000 0.600000000
v 0.900000000 -0.800000000 -0.600000000
v 0.900000000 -0.800000000 0.600000000
v 0.900000000 0.800000000 -0.600000000
v 0.900000000 0.800000000 0.600000000
f 1 2 4
f 5 7 8
f 1 5 6
f 3 4 8
f 1 3 7
f 2 6 8
f 1 4 3
f 5 8 6
f 1 6 2
f 3 8 7
f 1 7 5
f 2 8 4

File diff suppressed because it is too large Load diff

View file

@ -1,98 +0,0 @@
v 0.422516300 -0.006744500 -0.056400700
v 0.411035797 -0.009028114 -0.056400700
v 0.401303097 -0.015531297 -0.056400700
v 0.394799914 -0.025263997 -0.056400700
v 0.392516300 -0.036744500 -0.056400700
v 0.394799914 -0.048225003 -0.056400700
v 0.401303097 -0.057957703 -0.056400700
v 0.411035797 -0.064460886 -0.056400700
v 0.422516300 -0.066744500 -0.056400700
v 0.433996803 -0.064460886 -0.056400700
v 0.443729503 -0.057957703 -0.056400700
v 0.450232686 -0.048225003 -0.056400700
v 0.452516300 -0.036744500 -0.056400700
v 0.450232686 -0.025263997 -0.056400700
v 0.443729503 -0.015531297 -0.056400700
v 0.433996803 -0.009028114 -0.056400700
v 0.422516300 -0.006744500 0.243599300
v 0.411035797 -0.009028114 0.243599300
v 0.401303097 -0.015531297 0.243599300
v 0.394799914 -0.025263997 0.243599300
v 0.392516300 -0.036744500 0.243599300
v 0.394799914 -0.048225003 0.243599300
v 0.401303097 -0.057957703 0.243599300
v 0.411035797 -0.064460886 0.243599300
v 0.422516300 -0.066744500 0.243599300
v 0.433996803 -0.064460886 0.243599300
v 0.443729503 -0.057957703 0.243599300
v 0.450232686 -0.048225003 0.243599300
v 0.452516300 -0.036744500 0.243599300
v 0.450232686 -0.025263997 0.243599300
v 0.443729503 -0.015531297 0.243599300
v 0.433996803 -0.009028114 0.243599300
v 0.422516300 -0.036744500 -0.056400700
v 0.422516300 -0.036744500 0.243599300
f 1 2 18
f 1 18 17
f 33 2 1
f 34 17 18
f 2 3 19
f 2 19 18
f 33 3 2
f 34 18 19
f 3 4 20
f 3 20 19
f 33 4 3
f 34 19 20
f 4 5 21
f 4 21 20
f 33 5 4
f 34 20 21
f 5 6 22
f 5 22 21
f 33 6 5
f 34 21 22
f 6 7 23
f 6 23 22
f 33 7 6
f 34 22 23
f 7 8 24
f 7 24 23
f 33 8 7
f 34 23 24
f 8 9 25
f 8 25 24
f 33 9 8
f 34 24 25
f 9 10 26
f 9 26 25
f 33 10 9
f 34 25 26
f 10 11 27
f 10 27 26
f 33 11 10
f 34 26 27
f 11 12 28
f 11 28 27
f 33 12 11
f 34 27 28
f 12 13 29
f 12 29 28
f 33 13 12
f 34 28 29
f 13 14 30
f 13 30 29
f 33 14 13
f 34 29 30
f 14 15 31
f 14 31 30
f 33 15 14
f 34 30 31
f 15 16 32
f 15 32 31
f 33 16 15
f 34 31 32
f 16 1 17
f 16 17 32
f 33 1 16
f 34 32 17

View file

@ -1,290 +0,0 @@
v 0.422516300 -0.006744500 -0.056400700
v 0.418600514 -0.007001154 -0.056400700
v 0.414751729 -0.007766725 -0.056400700
v 0.411035797 -0.009028114 -0.056400700
v 0.407516300 -0.010763738 -0.056400700
v 0.404253457 -0.012943900 -0.056400700
v 0.401303097 -0.015531297 -0.056400700
v 0.398715700 -0.018481657 -0.056400700
v 0.396535538 -0.021744500 -0.056400700
v 0.394799914 -0.025263997 -0.056400700
v 0.393538525 -0.028979929 -0.056400700
v 0.392772954 -0.032828714 -0.056400700
v 0.392516300 -0.036744500 -0.056400700
v 0.392772954 -0.040660286 -0.056400700
v 0.393538525 -0.044509071 -0.056400700
v 0.394799914 -0.048225003 -0.056400700
v 0.396535538 -0.051744500 -0.056400700
v 0.398715700 -0.055007343 -0.056400700
v 0.401303097 -0.057957703 -0.056400700
v 0.404253457 -0.060545100 -0.056400700
v 0.407516300 -0.062725262 -0.056400700
v 0.411035797 -0.064460886 -0.056400700
v 0.414751729 -0.065722275 -0.056400700
v 0.418600514 -0.066487846 -0.056400700
v 0.422516300 -0.066744500 -0.056400700
v 0.426432086 -0.066487846 -0.056400700
v 0.430280871 -0.065722275 -0.056400700
v 0.433996803 -0.064460886 -0.056400700
v 0.437516300 -0.062725262 -0.056400700
v 0.440779143 -0.060545100 -0.056400700
v 0.443729503 -0.057957703 -0.056400700
v 0.446316900 -0.055007343 -0.056400700
v 0.448497062 -0.051744500 -0.056400700
v 0.450232686 -0.048225003 -0.056400700
v 0.451494075 -0.044509071 -0.056400700
v 0.452259646 -0.040660286 -0.056400700
v 0.452516300 -0.036744500 -0.056400700
v 0.452259646 -0.032828714 -0.056400700
v 0.451494075 -0.028979929 -0.056400700
v 0.450232686 -0.025263997 -0.056400700
v 0.448497062 -0.021744500 -0.056400700
v 0.446316900 -0.018481657 -0.056400700
v 0.443729503 -0.015531297 -0.056400700
v 0.440779143 -0.012943900 -0.056400700
v 0.437516300 -0.010763738 -0.056400700
v 0.433996803 -0.009028114 -0.056400700
v 0.430280871 -0.007766725 -0.056400700
v 0.426432086 -0.007001154 -0.056400700
v 0.422516300 -0.006744500 0.243599300
v 0.418600514 -0.007001154 0.243599300
v 0.414751729 -0.007766725 0.243599300
v 0.411035797 -0.009028114 0.243599300
v 0.407516300 -0.010763738 0.243599300
v 0.404253457 -0.012943900 0.243599300
v 0.401303097 -0.015531297 0.243599300
v 0.398715700 -0.018481657 0.243599300
v 0.396535538 -0.021744500 0.243599300
v 0.394799914 -0.025263997 0.243599300
v 0.393538525 -0.028979929 0.243599300
v 0.392772954 -0.032828714 0.243599300
v 0.392516300 -0.036744500 0.243599300
v 0.392772954 -0.040660286 0.243599300
v 0.393538525 -0.044509071 0.243599300
v 0.394799914 -0.048225003 0.243599300
v 0.396535538 -0.051744500 0.243599300
v 0.398715700 -0.055007343 0.243599300
v 0.401303097 -0.057957703 0.243599300
v 0.404253457 -0.060545100 0.243599300
v 0.407516300 -0.062725262 0.243599300
v 0.411035797 -0.064460886 0.243599300
v 0.414751729 -0.065722275 0.243599300
v 0.418600514 -0.066487846 0.243599300
v 0.422516300 -0.066744500 0.243599300
v 0.426432086 -0.066487846 0.243599300
v 0.430280871 -0.065722275 0.243599300
v 0.433996803 -0.064460886 0.243599300
v 0.437516300 -0.062725262 0.243599300
v 0.440779143 -0.060545100 0.243599300
v 0.443729503 -0.057957703 0.243599300
v 0.446316900 -0.055007343 0.243599300
v 0.448497062 -0.051744500 0.243599300
v 0.450232686 -0.048225003 0.243599300
v 0.451494075 -0.044509071 0.243599300
v 0.452259646 -0.040660286 0.243599300
v 0.452516300 -0.036744500 0.243599300
v 0.452259646 -0.032828714 0.243599300
v 0.451494075 -0.028979929 0.243599300
v 0.450232686 -0.025263997 0.243599300
v 0.448497062 -0.021744500 0.243599300
v 0.446316900 -0.018481657 0.243599300
v 0.443729503 -0.015531297 0.243599300
v 0.440779143 -0.012943900 0.243599300
v 0.437516300 -0.010763738 0.243599300
v 0.433996803 -0.009028114 0.243599300
v 0.430280871 -0.007766725 0.243599300
v 0.426432086 -0.007001154 0.243599300
v 0.422516300 -0.036744500 -0.056400700
v 0.422516300 -0.036744500 0.243599300
f 1 2 50
f 1 50 49
f 97 2 1
f 98 49 50
f 2 3 51
f 2 51 50
f 97 3 2
f 98 50 51
f 3 4 52
f 3 52 51
f 97 4 3
f 98 51 52
f 4 5 53
f 4 53 52
f 97 5 4
f 98 52 53
f 5 6 54
f 5 54 53
f 97 6 5
f 98 53 54
f 6 7 55
f 6 55 54
f 97 7 6
f 98 54 55
f 7 8 56
f 7 56 55
f 97 8 7
f 98 55 56
f 8 9 57
f 8 57 56
f 97 9 8
f 98 56 57
f 9 10 58
f 9 58 57
f 97 10 9
f 98 57 58
f 10 11 59
f 10 59 58
f 97 11 10
f 98 58 59
f 11 12 60
f 11 60 59
f 97 12 11
f 98 59 60
f 12 13 61
f 12 61 60
f 97 13 12
f 98 60 61
f 13 14 62
f 13 62 61
f 97 14 13
f 98 61 62
f 14 15 63
f 14 63 62
f 97 15 14
f 98 62 63
f 15 16 64
f 15 64 63
f 97 16 15
f 98 63 64
f 16 17 65
f 16 65 64
f 97 17 16
f 98 64 65
f 17 18 66
f 17 66 65
f 97 18 17
f 98 65 66
f 18 19 67
f 18 67 66
f 97 19 18
f 98 66 67
f 19 20 68
f 19 68 67
f 97 20 19
f 98 67 68
f 20 21 69
f 20 69 68
f 97 21 20
f 98 68 69
f 21 22 70
f 21 70 69
f 97 22 21
f 98 69 70
f 22 23 71
f 22 71 70
f 97 23 22
f 98 70 71
f 23 24 72
f 23 72 71
f 97 24 23
f 98 71 72
f 24 25 73
f 24 73 72
f 97 25 24
f 98 72 73
f 25 26 74
f 25 74 73
f 97 26 25
f 98 73 74
f 26 27 75
f 26 75 74
f 97 27 26
f 98 74 75
f 27 28 76
f 27 76 75
f 97 28 27
f 98 75 76
f 28 29 77
f 28 77 76
f 97 29 28
f 98 76 77
f 29 30 78
f 29 78 77
f 97 30 29
f 98 77 78
f 30 31 79
f 30 79 78
f 97 31 30
f 98 78 79
f 31 32 80
f 31 80 79
f 97 32 31
f 98 79 80
f 32 33 81
f 32 81 80
f 97 33 32
f 98 80 81
f 33 34 82
f 33 82 81
f 97 34 33
f 98 81 82
f 34 35 83
f 34 83 82
f 97 35 34
f 98 82 83
f 35 36 84
f 35 84 83
f 97 36 35
f 98 83 84
f 36 37 85
f 36 85 84
f 97 37 36
f 98 84 85
f 37 38 86
f 37 86 85
f 97 38 37
f 98 85 86
f 38 39 87
f 38 87 86
f 97 39 38
f 98 86 87
f 39 40 88
f 39 88 87
f 97 40 39
f 98 87 88
f 40 41 89
f 40 89 88
f 97 41 40
f 98 88 89
f 41 42 90
f 41 90 89
f 97 42 41
f 98 89 90
f 42 43 91
f 42 91 90
f 97 43 42
f 98 90 91
f 43 44 92
f 43 92 91
f 97 44 43
f 98 91 92
f 44 45 93
f 44 93 92
f 97 45 44
f 98 92 93
f 45 46 94
f 45 94 93
f 97 46 45
f 98 93 94
f 46 47 95
f 46 95 94
f 97 47 46
f 98 94 95
f 47 48 96
f 47 96 95
f 97 48 47
f 98 95 96
f 48 1 49
f 48 49 96
f 97 1 48
f 98 96 49

File diff suppressed because it is too large Load diff

View file

@ -1,98 +0,0 @@
v 0.002102258 0.487021467 -0.172957363
v -0.005005707 0.483903186 -0.173419737
v -0.011031550 0.479425392 -0.171395082
v -0.015057889 0.474269789 -0.167191631
v -0.016471751 0.469221270 -0.161449323
v -0.015057889 0.465048427 -0.155042372
v -0.011031550 0.462386538 -0.148946178
v -0.005005707 0.461640850 -0.144088831
v 0.002102258 0.462924889 -0.141209819
v 0.009210224 0.466043170 -0.140747444
v 0.015236067 0.470520964 -0.142772100
v 0.019262406 0.475676567 -0.146975550
v 0.020676268 0.480725085 -0.152717858
v 0.019262406 0.484897928 -0.159124809
v 0.015236067 0.487559817 -0.165221003
v 0.009210224 0.488305505 -0.170078350
v -0.016015388 0.524142023 -0.144782636
v -0.023123353 0.521023741 -0.145245011
v -0.029149196 0.516545948 -0.143220355
v -0.033175535 0.511390344 -0.139016905
v -0.034589398 0.506341826 -0.133274597
v -0.033175535 0.502168983 -0.126867645
v -0.029149196 0.499507094 -0.120771451
v -0.023123353 0.498761406 -0.115914104
v -0.016015388 0.500045444 -0.113035092
v -0.008907422 0.503163726 -0.112572717
v -0.002881579 0.507641519 -0.114597373
v 0.001144760 0.512797123 -0.118800823
v 0.002558622 0.517845641 -0.124543131
v 0.001144760 0.522018484 -0.130950082
v -0.002881579 0.524680373 -0.137046276
v -0.008907422 0.525426061 -0.141903623
v 0.002102258 0.474973178 -0.157083591
v -0.016015388 0.512093734 -0.128908864
f 1 2 18
f 1 18 17
f 33 2 1
f 34 17 18
f 2 3 19
f 2 19 18
f 33 3 2
f 34 18 19
f 3 4 20
f 3 20 19
f 33 4 3
f 34 19 20
f 4 5 21
f 4 21 20
f 33 5 4
f 34 20 21
f 5 6 22
f 5 22 21
f 33 6 5
f 34 21 22
f 6 7 23
f 6 23 22
f 33 7 6
f 34 22 23
f 7 8 24
f 7 24 23
f 33 8 7
f 34 23 24
f 8 9 25
f 8 25 24
f 33 9 8
f 34 24 25
f 9 10 26
f 9 26 25
f 33 10 9
f 34 25 26
f 10 11 27
f 10 27 26
f 33 11 10
f 34 26 27
f 11 12 28
f 11 28 27
f 33 12 11
f 34 27 28
f 12 13 29
f 12 29 28
f 33 13 12
f 34 28 29
f 13 14 30
f 13 30 29
f 33 14 13
f 34 29 30
f 14 15 31
f 14 31 30
f 33 15 14
f 34 30 31
f 15 16 32
f 15 32 31
f 33 16 15
f 34 31 32
f 16 1 17
f 16 17 32
f 33 1 16
f 34 32 17

Some files were not shown because too many files have changed in this diff Show more