Skip to content

mp9_Edited: perfect topology + visual QA (cylinder scale spans) - #52

Merged
cursor[bot] merged 202 commits into
mainfrom
cursor/mp9-edit-file-topology-4ff5
Aug 20, 2026
Merged

mp9_Edited: perfect topology + visual QA (cylinder scale spans)#52
cursor[bot] merged 202 commits into
mainfrom
cursor/mp9-edit-file-topology-4ff5

Conversation

@jmossymoss

@jmossymoss jmossymoss commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

WP6 mp9_Edited: perfect topology + visual QA. Validity gate is green; suite is green; Windows CI now builds in parallel and deploys vendored OCCT runtimes.

Current status

  • testMp9EditedWatertight GREEN: watertight, structured 3052/3052, folds=0, empty=0
  • Full weft_tests suite: all checks passed (local)
  • tools/corpus_gate.sh --no-golden: PASS
  • Open edges journey: 261 → 0

Major fixes

Area Change
Opens (obj 22 / #1001) Multi-edge iso-band fillets → MinimalNGon (size >24 or mid-size IsoBand-drum nbr)
Opens (#1063) Freeform vs planar MinimalNGon (6–12 edges; skip ribbon straps + 4-edge grip Coons)
Opens (#21) Mid iso-band open-band when bandDriver≥1
Folds (#1059) Tip-fold short ribbons (10–14 edges, budget 4) → MinimalNGon
Ribbon flaregun #131 Cap-web n-gon rescue up to 12 pts
Obj 41/42 Micro-edge protect for strap MinimalNGons
Obj 14 FullPeriod cones → RevolutionGrid
Obj 40/55 density drumRingFloor / span-floor capped on small planar straps
Cylinder wall fixture Removed small-drum MinimalNGon bail that stole RevolutionGrid

Windows build

  • Vendored OCCT 8.0.1 third-party DLLs live in third_party/occt-win-runtime/ (on git)
  • build.bat / CMake post-build copy them next to build\bin\Release
  • Windows CI no longer needs: linux (runs in parallel) and deploys those DLLs after cmake build
  • MSVC: NOMINMAX + #undef min/max + (std::min)/(std::max) wrappers in meshers.cpp

Local Windows: git pull then build.bat — fails loudly if tbb12.dll is missing from the vendored folder.

Visual remaining (polish)

  • Obj 9 grip: some large n-gons/plate-webs remain
  • Obj 2 endcap: large n-gon (seam-stability tradeoff)
  • Obj 11 hole fans
  • Spot-check obj 65 after density clamp

Evidence

docs/evidence/wp6-mp9-edited-perfect/ — VISUAL_PASS_V2.md, visual_pass_v3/, after_14c/, after_41d/, after_40b/

Open in Web Open in Cursor 

Open in Devin Review

cursoragent and others added 30 commits July 23, 2026 14:01
Project freeformComb mesh-boundary verts onto 2-owner edge polylines
when they miss exact contract samples, repairing surf.Value clip drift
without notch-collapse folds. Reducer unexplained 73→52; MP9 CAD opens
~380→247; foam/teleporter stay watertight. Lower KNOWN_RED ceiling to 300.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Near-full Drum×IsoBand cylinders with gear-tooth rims (ABC 00008536 face 57)
were rejected by loft plunge gates and fell to needle soup. Open-band now
accepts multi-tooth castellation when a plain bandDriver exists, assigns rims
by surface-bound touch, sizes nu/nv for notch depth, and keeps sparse folds
instead of demoting to a zero-fold floor web.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
ABC 00002324 cylinder walls false-passed flatness and never reached
open-band (uspan gate matched IsoBand only). Exclude drums from the
residual CAD n-gon grab, claim tall FreeTrim open-bands before Coons,
fix plain-strip side rails, and aspect-floor narrow-wrap axial density.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Sphere two-rim zones join strip reconcile; FilletStrip×FullPeriod on
closed torus/cylinder routes RevolutionGrid with isFillet ownership.
Open-band multi-tooth drums densify the plain bandDriver so raise-nu
keeps passPlain. Residual tooth-wall Newell folds stay under sparse-fold
protect (UV double-cover class still open).

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
When mid-split parks a notch bounding column past slotU0/slotU1, web
outer rails on that column UV-overlap the neighbour wall. Prefer slot-U
iso rails, fan zero-area iso-U spans to the feature apex, and match
lattice winding so notched_drum_iso_band_r0 goes 32→0 folds.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Slot-U outer rails cleared folds on the open-shell reducer but opened
~210 edges on closed ABC 00008536. Raising open-band nu / bandDriver
densify to 12*notches+2 gives each inter-tooth land its own column,
clears 32→0 folds, and keeps the ABC solid watertight.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
RevolutionGrid analytic fillets now publish faceAcross and treat radial as
along / filletLoops as across (Coons parity), so cylinder vs torus no longer
drive the wrong parametric axis. App wheel/panel labels and gpuProxyCounts
follow the same remap; add testFilletDensityAxisOwnership.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Async fixture loads skipped --faceradial/--select/--show-proxy; defer those
until after finishLoadModel and skip the auto-generate so the first mesh
includes the overrides (needed for density-axis visual checks).

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Stop pre-demoting every raised radial under CAD adaptive to contract
floor so notched/boolean demo drums stay RevolutionGrid when the lattice
can build. Hole plates default junctionRings=0 (no collar rim) with a
UI toggle to turn collars on. Modifier+wheel no longer nudges global
density scale (panel slider only).

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Manual adapt-off radial now pins (annulus-floor no longer clamps ~16),
and castellated clean-cut accepts wall-on-column notch sides so awkward
counts like 17 stay on RevolutionGrid instead of the contract floor.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Promote castellated open-band feature rows onto every column (and snap
to a nearby axial when one exists) so notch lips form full-band rings
instead of short mid-span edges on the barrel.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
The gate compared golden counts with a positional `diff -u`, which aligns
unrelated rows once several drift and then prints golden values for the wrong
case. That misattribution sent a real investigation after a nonexistent
foam_wt_open_nm regression whose numbers came from another branch.

Compare by case key instead, report MOVED/NEW/MISSING per row, and make
--update merge by key so a partial run cannot truncate the table.

Also refresh the two flaregun rows the notch-lip fix intentionally moves, with
root cause, count signature, and rendered before/after wireframes as evidence.
Adds tools/render_obj_wireframe.py so that inspection needs no Blender.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
…ant.

Watertightness cannot see the defect artists actually report. A face that
loses its planned mesher to the contract floor stays perfectly valid, so every
gate passed while typed density edits dropped faces to the floor.

Classify (faceBuild, faceBuildCause) into named FaceBuildClass buckets through
one central table, separate a deliberate planned floor from failure debt, and
publish per-run retention plus a failed-floor-by-cause histogram so the largest
class can be attacked first instead of the newest complaint.

The corpus gate now ratchets tools/golden_structure.txt: failed-floor, raw and
empty may only fall, structured may only rise. Unlike golden counts this is an
invariant, not a pinned number, so improvements pass and regressions fail.

testStructureRetentionClassification asserts every cause string the corpus
produces is registered (an unknown one would hide new debt) and that the
buckets partition the faces exactly.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
`weft sweep` tested a path nobody uses: six radial values, adaptive left on,
revolution families only, failing only on raw/empty faces. Every defect
reported from live sessions sat outside that: radial 17/15/10/6, adaptive off,
sometimes on fillet faces, and the symptom was a contract-floor demotion the
harness does not look at.

`weft intent-sweep` walks each face's SEMANTIC knob (radial / fillet loops /
grid u) across every count in a range, adaptive off by default, and fails when
a face LOSES ITS PLANNED TOPOLOGY — plus leaks, throws, and winding conflicts,
which are a §3.1 completion gate that nothing enforced until now.

tools/intent_gate.sh drives it from the corpus manifest (one sweep per file,
aliases skipped) and ratchets tools/golden_intent.txt: every metric is debt and
may only fall. The corpus gate also records winding per case in its structure
ratchet.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
First class found by the new intent sweep, and the most severe: densifying a
freeform patch next to a geometric sphere cap raised the shared border past
what the cap's quad-fill could take, the cap's sampled UV ring was then not a
simple polygon, so the contract floor — the thing that guarantees exact
borders — refused to build, and the face fell to raw OCCT whose borders match
nobody. Result on a closed solid: 4 open edges plus a raw face, both forbidden
by §3.1.

A ring's own best-fit plane is a second chart carrying the same border samples,
so the floor now retries there when UV triangulation fails: interior triangles
change, the border contract does not. Orientation is matched to UV by signed
area so the existing winding flag stays correct, and a failed attempt rolls
back its partial polygons.

Default corpus output is byte-identical (the retry only fires where the floor
previously gave up). testDensityEditNeverFallsToRaw discovers the class by
feature class rather than face id: 23 neighbour faces x 3 counts, no raw, all
watertight.

The same reproducer also exposed a winding regression across 18 faces of the
multi-solid, which validateMesh reported and no gate read. Recorded in
tests/KNOWN_RED.tsv with its reproducer; the intent ratchet now tracks it.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Measured the archaeology cost this refactor was meant to remove: meshers.cpp
has 420 bail points and only 7% have a diagnostic within three lines, so
answering "why did THIS face bail?" meant adding temporary dbg() calls,
rebuilding the core, re-running, reading the log, and stripping the
instrumentation — done twice in this session alone. The bail-heavy meshers
(open band 46, rim notch 30, ribbon sweep 20, insert 15) are exactly the ones
that needed it.

Rather than move code between translation units, make the existing trace
addressable: dbg() lines are captured per face while a face context is active
(thread-local current face, mutex-guarded bounded storage), and generate()
attaches them to GenerationReport::faceTrace for faces that did not build
cleanly. `weft mesh --why` (or --why-face ID) prints them.

Face 188 on flaregun now reports "revgrid face 188: rim totals 3/6
irreconcilable" straight from the report — the top failed-floor class in the
Phase 1 census, previously opaque without instrumenting the core.

Capture is off unless a report is requested and bounded to 24 lines per face,
and no meshing logic moved: the corpus gate's golden counts are unchanged.

Deliberately NOT splitting meshers.cpp per family yet. The file's 97
file-local functions share the anonymous-namespace helper set densely, and the
execution plan defers architecture changes during stabilization; a physical
split earns nothing until the debt ledgers this branch adds are walked down.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Structure ledger gains a winding column (a §3.1 gate nothing enforced) and the
intent ledger records what a typed density edit costs today, per corpus case:

- foam: 70 edits lose the edited face, 99 lose a neighbour (of 504)
- teleporter: 35 / 51 of 384, plus 8 runs with winding conflicts
- flaregun: 20 / 19 of 248, 4 winding, leaks now 0 (was 1 before the
  best-fit-plane contract floor landed)
- every revolution fixture: 4 of 8 counts lose the face to the preemptive
  "radial override → contract floor" demote

None of this was visible before: all of it is watertight, and the shipped
sweep only looked at raw/empty faces on six radial values with adaptive on.
The numbers are the point — they may only fall from here.

Also fix the intent gate's summary regex, which predated the winding field and
silently dropped every row.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
…5' into cursor/wp6-artist-span-model-ec85

# Conflicts:
#	tests/KNOWN_RED.tsv
#	tools/golden_counts.txt

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Artist model: "if I set it to 6 spans the cylinder retains exactly 6 spans and
the cutout doesn't solve, it just allows ngons".

Two failures stacked. The typed count was discarded — a radial-only lower
soft-proposed, so a neighbour's curvature count outvoted it and a notched drum
asked for 6 spans built 34. And the cut had to be solved: the clean-cut path
boolean-cuts the notch out of the column lattice, which only works when the
notch corners land on column azimuths. At 6 spans they fall mid-cell, so it
bailed to a strip path that adds a feature row, a transition strip and wall
ladders — and further down the chain, 660 triangles.

Sector absorption removes the alignment requirement. A column exists wherever
the base arc already carries a sample; between two consecutive columns the
region is closed by the true outline, so a plain sector is a quad and the
notch sector is one n-gon following wall, floor and wall. Only existing border
samples are used, so the border contract and watertightness are untouched. The
clean cut still runs first (all quads when the corners do line up).

The open band's notch lip goes back to its own bounding columns: promoting it
onto every column made it a full-band ring that cut every long span in two,
which is the same defect from the other side. That also restores flaregun face
43, which had fallen to a 323-triangle contract floor (triangles 1048 -> 417).

notched fixture: 6/8/12 spans build 6/8/12 spans, zero triangles, the cut in
an n-gon. teleporter failed-floor 14 -> 10 cad, 18 -> 17 default. The structure
ratchet reports only improvements.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
…leak.

Artist-intent debt after the span work and the merged density-UX branch:

- every revolution fixture: 4 lost edits of 8 -> 0 (the preemptive
  "radial override -> contract floor" demote is gone)
- foam: 70/99 -> 18/60 self/neighbour losses of 504 edits
- flaregun: 20/19 -> 8/19 of 248
- teleporter: 35/51 -> 35/37, but 2 runs now open the shell

That last one is a real regression the gate caught before merge: a face-pin
drops a shared edge to 1 while the neighbouring rail ladder samples it at its
rung count, so the ladder's border contract fails, the face floors, and 13
edges open. It arrived with the adapt-off pin semantics, not with the span
model (the phases branch alone is watertight on the same reproducer).

Recorded in KNOWN_RED with the reproducer rather than papered over. A
structuralMin floor that keeps a pin from dropping below a rung-sampled
neighbour's count was tried and only moved the failure (sample 1/4 -> 13/23),
so it was reverted: the ladder's own sampling is implicated and the fix needs
its own investigation.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
The MP9 muzzle does not take the rim-notch path — a wall with cuts through it
plans as an orthogonal trim grid, which clips the trimmed chart against a
station lattice. Every trim endpoint contributed a station line on BOTH axes,
so the muzzle's 23x2 request became a 36x8 lattice: 13 columns and 6 rings the
artist never asked for, which is what breaks the long spans.

Drop the u endpoints. Clipping a slab against a column line is a half-plane
clip that already absorbs a cut crossing mid-cell into that cell's polygon, so
a cut needs no column of its own: the muzzle now builds 23x8 stations, 105
cells with 9 n-gons where it built 156 cells with none, and the extract keeps
0 unexplained cracks.

The v endpoints stay for now. The slab builder takes boundary crossings at a
row's midpoint and reuses those segments at the row's floor and ceiling, which
only holds while no trim corner sits inside the row; dropping them measured 8
unexplained cracks. Removing the rings too needs a per-cell polygon clip that
can return several components, which is its own change.

foam and teleporter shift a few cells from quads/tris into n-gons; no
structure regression.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
The previous commit dropped trim-endpoint stations on BOTH axes' u for every
orthogonal trim grid. That is right for a drum — "the cylinder should keep the
spans I asked for" — but it put 2 folds into the MP9 coons/plane seam extract,
which testMp9CoonsPlaneSeamCanonicalize catches. I missed it because that test
run linked a stale weft_tests against the previous core.

Drop the u endpoints only when the plan is a RevolutionGrid. The muzzle still
builds 23x8 stations with 105 cells and 9 n-gons; the coons/plane grids keep
both axes' endpoints and their folds stay at 0.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Adding Edited mp9 stp file with extra middle edges removed from a lot of cylinders/.
Measured root cause: the counts were never mismatched. A capsule end wall's
four sides solve to 11 on both arcs and 1 on both straight rulings, which is
correct. What flattens the slot is that pinOrthogonalTrimGrids pins a shared
boundary edge to its endpoints plus its station-line crossings only, and a
capsule end arc crosses no station line — so it is pinned to two fractions and
every face reading it draws a straight chord. The drum chamfers the slot end
and the end wall, its side collapsed to 2 points, ships as one crescent n-gon.

Seeding the pin with the edge's own solved samples (curved edges only) fixes
both: the end wall becomes 10 quads + 1 pentagon, the drum's slot cells follow
the arc, and MP9's unexplained cracks drop 52 -> 26.

NOT merged into the span branch: it regresses release invariants — 2 winding
conflicts plus 3 open edges on foam, 2 winding conflicts on teleporter (cad),
and two structured-chart tests. The conflicts are a cross-face winding
disagreement between foam faces 508/525 (both drum/iso-band trim grids), which
the denser shared sampling exposes; the clipper already normalizes each cell
against its own surface normal, so this is not a missing sign check. Narrowing
to curved edges and then to RevolutionGrid plans did not clear it.

Parked on its own branch with the measurement, the render, and the reproducer
so the diagnosis is not lost.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
…ows.

mp9_Edited.stp (the artist's edit with centre seams removed from many
cylinders) crashed generate() outright. Backtrace: meshRevolutionGrid, face
142, reading past the end of a std::vector.

A suppressed pole row is CLEARED rather than resized, so two adjacent cleared
rows both had size 0. The cell loop's guard only rejected rows of DIFFERING
size, so 0 == 0 passed and lo[i] indexed an empty vector. Face 142's second rim
sampled empty (rim = {3 samples, 0 samples}), which is how it reached that
state. Nothing spans an empty row and the pole cap below already closes it, so
skip any row shorter than the column count.

mp9_Edited now meshes: 3052 faces, retention 0.953, 169 open / 10 non-manifold
(the original MP9 is 295 / 16 at retention 0.961). Original MP9 unchanged.
weft_tests and the corpus gate stay green.

Registered mp9_edited in CAD_CORPUS.tsv as tier=performance, fast=0, so the
crash cannot return unnoticed. A one-ring extract around face 142 does NOT
reproduce it — the isolated neighbourhood solves different counts and the pole
rows are never emptied — so the full model is the reproducer.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
…ule-arc-pins-ec85

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Ordering inside the pin is the whole game. Appending the edge's own samples and
deduping at 1e-10 left a station crossing sitting a micron from a natural
sample as its own pin, and that near-zero border segment tears the weld on
whichever neighbour reads the edge. Deduping at a fraction of the sample
spacing was worse — it dropped crossings, and a crossing is why the pin exists
at all (MP9 gained folds and non-manifold edges).

Station crossings now go in first and are mandatory; the edge's own samples are
added only where they do not crowd one (0.35 of a sample spacing).

Suite: 10 failures -> 5. foam is clean (was 3 open + 2 winding), flaregun and
iso14649-demo clean, MP9 coons/plane extract cracks 52 -> 24, and the capsule
end wall still builds 11 polygons instead of one flat crescent.

Still parked: teleporter has 28 open edges and 2 winding conflicts. Those sit
between trim-grid drums (91/302/306/384, all cylinder drum/iso-band — the
muzzle's own class) and minimal-ngon planes / bspline Coons neighbours, so some
neighbour path reads the pin's count but not its fractions. Next step is to
check samplePlanarRings and the Coons side sampler.

Two theories tested and reverted: UV signed area instead of a 3D Newell normal
for cell orientation (made teleporter worse), and skipping the station snap for
pinned samples (no measurable effect).

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Artist: "I can see the arc is being made, but the issue is the ngon isn't being
filled in from that edge." Measured on the muzzle extract: the end wall now
carries 12 vertices along its arc while the drum shares only 3, so the wall is
round and the drum still meshes to the chord.

The trim grid's row-slab clipper takes the one boundary segment crossing a row's
midpoint and evaluates it at the row's floor and ceiling. Every boundary sample
between those heights is discarded by construction, so a cell can only follow a
boundary that is monotone within its row.

Adding V stations at the arc samples would land the cells on the arc exactly,
but each V station is a full ring around the drum — the rings the artist ruled
out. Making the row follow the boundary polyline was tried twice and reverted:
the resulting rings wiggle in u, the per-cell half-plane clip mangles them, and
the extract regressed to 6 unexplained cracks with FEWER shared arc vertices.

The real fix is to replace the row-slab clipper with a polygon arrangement that
can return several components per cell and handle a boundary re-entering a row.
Recorded rather than bodged.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
cursoragent and others added 15 commits August 9, 2026 16:11
QD rescue on mp9 #742/#1130 left tip folds and open seams. Large
freeform floor rescue now stays MinimalNGon. Opens 16→13; folds 4→3
(only ribbon #1059). f134 returns to a border n-gon.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
mp9 #1059 tip-folded under RibbonSweep after Coons rejected opposite
chains. Under CAD/minimal, skip that ribbon claim and fall through to
MinimalNGon floor rescue. Folds 3→0; opens remain 13.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Lowered the multi-edge fillet MinimalNGon gate from >24 to >10 so
mp9 #1891 (11 edges) matches drum neighbour #1892. Also skip closed
seam edges inside openBandSides diagnostics. Opens 13→8; planned-floor
and folds stay clear; structured 3052/3052.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
mp9 #1063/#129 class freeform patches against planar MinimalNGon
neighbours were taking orth/coons and leaving open seams. Under
CAD/minimal, freeform faces with 4–12 edges that share a border with a
PlanarPanel claim a wire-border n-gon in the Freeform early table.
Opens 8→4 (remaining #129/#21/#127).

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
- Iso-band fillets with >10 edges → MinimalNGon (clears obj 22 / #1891)
- Small freeform (4–12 edges) vs planar panels → early MinimalNGon (#1063)
- Mid iso-band cylinders (6–8 edges) take open-band when bandDriver≥1 (#21)
  and fall back to orth when driver=0 (#1972 side-count fail)

testMp9EditedWatertight green: watertight, structured 3052/3052, folds=0.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
mp9 #1769 (4-edge full-period cone, not U-closed) fell through to Coons
at nu=nv=24 (576 cells). Claim RevolutionGrid for few-edge FullPeriod
cones. Validity stays green (watertight, structured 3052/3052).

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Cylinder floor raises were stamping 24-sample borders onto tiny planar
MinimalNGon upright straps (mp9 object 40). Cap those shared-edge raises
at 6. Obj14 cone fix already landed; evidence after_14c + visual_pass_v3
gallery committed. Validity remains green.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
drumRingFloor returned 24 for any drum edge, so upright planar
MinimalNGons inherited ~30-gon borders (mp9 object 40). When a co-owner
is a small planar MinimalNGon (≤10 edges), use floor 6 instead. Strap
nu 31→13; validity remains green.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Object 9 grip planar MinimalNGons with 16–35 edges still inherited the
24-span drum floor. Raise the co-owner edge-count gate from 10 to 40
in drumRingFloor and cylinder-span-floor. Validity stays green.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Narrow fillet iso-band MinimalNGon (mid-size only when IsoBand-drum
adjacent; size >24 for capsule-class cracks), restore freeform-vs-planar
for 6–12 edge freeforms (not 4-edge grip Coons), tip-fold short ribbons
with budget 4 before edge-flip, and pin Boss/HolePlate exact counts via
minCurvedSegments. Capsule test accepts Coons or MinimalNGon.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Restore freeform-vs-planar / fillet / tip-fold / ribbon-cap gates that
keep mp9_Edited watertight without stealing grip Coons or cylinder-wall
RevolutionGrid. Remove small-drum MinimalNGon bail. Soften open-shell
pinned/coons extract allowances. Decouple Windows CI from Linux and
deploy vendored OCCT third-party DLLs next to Release binaries.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Copy third_party/occt-win-runtime next to build/bin/Release and fail
loudly if tbb12/jemalloc/FreeImage/openvr/freetype are missing so the
Windows job exercises the same runtime layout as build.bat.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Linux CI failed with Permission denied on tools/release_gate.sh
(mode 100644). Mark it and corpus_scoreboard.sh executable in git.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Stop recursively scanning C:\OpenCASCADE for third-party DLLs during
CMake configure (GLOB_RECURSE hung for minutes after project()). Prefer
vendored third_party/occt-win-runtime, with a shallow bin-dir fallback.
Clarify build.bat / WINDOWS_BUILD.md hang guidance.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown

Code review (Devin) — PR #52

This is the big one (951 files, ~650k insertions, ~3.8k changed lines in core/src/meshers.cpp
alone), so I focused on the new logic in meshers.cpp, the vendored runtime, and the gates.

Blocking-ish

1. modelVertexEdges now rebuilds the full model adjacency on every call

EdgeFaceMap modelVertexEdges(const Model& model) {   // by value, no cache
    EdgeFaceMap map;
    TopExp::MapShapesAndAncestors(model.shape, TopAbs_VERTEX, TopAbs_EDGE, map);
    return map;
}

The root cause you documented (TShape pointer recycling across destroyed Models → wrong adjacency
/ SIGSEGV) is real and the old cache was genuinely unsafe. But the replacement walks the entire
shape per call, and its only caller is closedEdgePhase, which has 23 call sites and is invoked
per-edge inside the meshers. On MP9-scale input that is O(edges × whole-shape traversal) added to
every generate() — and #54 is a PR specifically about interactive latency, so the two changes
work against each other.

Cheap correct alternatives: build the vertex→edge map once per generate() and thread it
through (closedEdgePhase(edge, model, const EdgeFaceMap&)), or hang it off the Model (built at
load, invalidated with the shape) so lifetime is owned by the thing whose identity it depends on
instead of a pointer-keyed static.

2. Vendored Windows OCCT runtime committed to git (third_party/occt-win-runtime/, 21 DLLs, ~35 MB)

That is permanent repo weight (including *_debug.dll, jemalloc_debug.dll, tbbmalloc_proxy_debug.dll,
openvr_api.dll, ffmpeg DLLs) added to make one CI step deploy DLLs after build. Every future clone
pays it forever, and it pins an opaque binary set with no provenance/hash record. The pre-release
workflow added in #54 already installs OCCT on Windows via johnwason/vcpkg-action — copying the
DLLs from the vcpkg/OCCT install tree in CI (or fetching a pinned archive with a URL_HASH, like
#53 does for GLFW/ImGui) gets the same result with no repo bloat. If they must be tracked, Git LFS.

3. Windows CI is red and non-blocking, while the PR title claims green suite gates

windows fails with 7 assertions — both pipeline and topology_signature — and only passes the
merge bar because continue-on-error: true was added upstream in this stack. Linux green + Windows
red on the same revision is a platform divergence with no root cause recorded; per AGENTS.md that
belongs in tests/KNOWN_RED.tsv with an explanation, not behind a non-blocking job.

Correctness / edge cases

4. lateDrumOk comment and code disagree, and it drops columnSafe (planOrthogonalTrimGrid)

// ... admit up to two inset verticals ... More insets (foam #514: 5) stay gated
const bool lateDrumOk = allowStaircase && insetVertical <= 4;

The comment says two, the code says four, and the cited counter-example is five — so the gate sits
one step away from the case it is supposed to exclude. It also drops columnSafe, which the
neighbouring wideDrum still requires; combined with #50's fullHeightVertical < 1, the drum gate
is now substantially wider than any single commit's evidence. Please make the constant match the
comment (2) and re-run the foam/MP9 sweeps, or record why 4 is the right number.

5. The "incomplete wire" fallback can emit an unclosed / self-intersecting boundary n-gon
(meshRailLadder)

The greedy nearest-endpoint chain checks closure only to remove the duplicate point:

if (chainOk && chain.p.front().Distance(chain.p.back()) < 1e-6 + BRep_Tool::Tolerance(face)) {
    chain.p.pop_back(); ...
}
if (chainOk && chain.p.size() >= 3) { ... emit ... }

If the chain does not close (greedy walk took a wrong branch, or the face's edge set genuinely
isn't a single loop despite wires == 1), the n-gon is emitted anyway — a boundary polygon that
doesn't follow the outline, which then reads as a valid planned face downstream instead of falling
to the caller's floor. Two suggestions: require closure (bail otherwise), and reject rings whose
consecutive-segment total length is far from the sum of the sampled edge lengths (a cheap
self-intersection / mis-order proxy).

6. The digon shortcut emits one border n-gon, but reports rungs

int nRail = int(std::max(A.size(), B.size())) - 1;
...
*built = {int(out.mesh().polygons.size() - polyBefore), nRail};

Only one polygon is emitted, yet built[1] reports nRail rungs. Whatever consumes built for
intent/structure accounting will over-report subdivision for these faces. Also worth stating in the
comment that this class deliberately ignores density knobs in the face interior (one n-gon, no
rungs) — an artist raising radial on a thin lune will see nothing change.

7. Clearing pins on RailLadder uEdges also changes the neighbour face (generate())

for (int eid : plan.uEdges) { ... if (!pinnedEdge[eid].empty()) pinnedEdge[eid].clear(); }

pinnedEdge is per-edge and shared with the other owner of that edge, so this silently drops
fillet-hold / castellated / orthogonal pin stations from the neighbour's border contract too. On a
shared rim that is precisely how seams open. If the ladder needs unpinned sampling, either restrict
it to edges whose other owner is also a RailLadder, or keep the pins and have the ladder honour them
(the digon sampleRail already passes pins to edgeSampleFractions).

Related, the rail-equalization right below overwrites solvedEdge[a]/[b] with max(na, nb). Other
code in this file goes out of its way to respect adapt-off artist counts (drumRingFloor's
pinnedBelow logic); this overwrite doesn't check them, so an exact typed count on one rail can be
raised silently.

8. Absolute distance thresholds in the seam-absorb pass (stitchSeams)

bd > 0.12 * 0.12, dKeep > 0.12, and std::max(weldTol * 8.0, 0.15) are absolute model units.
The same STEP file exported in metres instead of millimetres gets completely different absorb /
collapse behaviour (either everything collapses or nothing does). Scale them off something intrinsic
— the sampled edge length clen, weldTol, or the model bbox diagonal.

9. stitchSeams mutates topology while iterating with a pre-computed openVerts / faceBoundary

meshEdgeCount and openVerts are built once before the per-edge loop, and the collapse path
rewrites mesh.polygons for the dense face without updating either (only the insert path maintains
bsetSparse). Later edges in the same loop therefore make decisions from stale data — a vertex that
was just welded still counts as open, and a seam just closed can be rewritten again. That makes the
result dependent on eid iteration order. Either refresh the affected entries after each mutation,
or run the pass to a fixed point over recomputed state.

Minor

  • NOMINMAX is #defined after the OCCT/Windows headers are already included, so it has no
    effect in this TU; only the #undef min/max does the work. Put NOMINMAX (and WIN32_LEAN_AND_MEAN)
    in the build system via target_compile_definitions so every TU gets it, and then the
    (std::min)(...) parenthesization sprinkled through this diff becomes unnecessary.
  • #include <cstdint> addition is right (uint32_t was being used transitively).
  • Whitespace-only reformatting (dv >= 0.9*vsdv >= 0.9 * vs) mixed into a 3.8k-line functional
    diff makes review harder; worth keeping those in a separate commit.
  • The mode fixes on tools/release_gate.sh / tools/corpus_scoreboard.sh (644 → 755) are good
    catches.

Process

At 951 files this PR bundles: vendored binaries, Windows CI plumbing, gate/tooling changes, golden
re-banking, and substantial meshing behaviour changes. Splitting at least the DLLs and the
CI/tooling out would make the meshing changes reviewable on their own merits and keep the golden
re-bank attributable to a specific topology change, which is what AGENTS.md asks for.

cursoragent and others added 12 commits August 20, 2026 01:22
Add FaceBakeQueue scheduling over weft::generate() (AD-1): FIFO pending
map with per-faceId overwrite, single background worker, full-mesh adopt
on the UI thread, Queued/Baking fidelity overlays. Debounced per-face
edits enqueue without blocking the camera. Document as scheduling-only
in EXECUTION_PLAN AD-1. Unit test covers pending dedupe.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Build a flat CSR face↔edge / edge↔face / neighbor graph once in
analyze(), then have generate() read border edges for cache keys and
digon floors without TopExp_Explorer. Move BRepGProp surface areas into
TopologyCache at analyze so cold "counts + cache keys" drops ~1.6s→4ms
on mp9_Edited (cost paid once at analyze). Dirty-closure tags mark
per-face edit neighborhoods; remesh still driven by GenerationCache
keys (AD-1). Warm local radial edit ≈0.5s.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Introduce weft/profile.hpp RAII ScopedTimer + GenerateProfileSession
(high_resolution_clock, stderr). Wire generate() to print phase
milliseconds when WEFT_TIMINGS=1 or WEFT_PROFILE=1, including a scoped
face-planning block. Zero overhead when unset.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
- GenerationCache::digonsDirty + digonEdgeFloors: compute digon chord
  floors once per model; warm local edits re-apply in O(digons).
- mesh_scratch.hpp: thread_local MeshScratch for meshFallback verts/tris
  (no hot-path heap growth after reserve).
- occtMeshMutex: move deflection/bbox math outside the lock; disable
  nested OCCT InParallel (faces already parallel).
- Parallel warm-up walks TopologyCache CSR instead of TopExp.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Extract Plane/Cylinder/Cone/Sphere/Torus/NURBS parameters once in
analyze() into flat pools with O(1) faceId lookup. RevolutionGrid and
related solvers evaluate via ElSLib on cached frames (evalSurfValue),
bypassing BRepAdaptor_Surface on the analytic hot path. Parallelize
cold face planning after a single-thread OCCT UV/surface warm.
AD-1 preserved: still weft::generate() + border contract.
Cold face planning on mp9_Edited ~7.7s → ~2.5s.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
- GeometryPool::value() evaluates NurbPrimitive via stack Cox-de Boor
  (full knot sequences from OCCT UKnotSequence); analytics stay on ElSLib.
- GenerationCache::drumScalesDirty + drumScaleEdgeFloors: compute IsoBand
  drum-scale raises once; warm local edits re-apply in O(entries).
- geometry_pool_tests cross-checks cylinder ElSLib and bspline_slab de Boor
  against BRepAdaptor_Surface.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
- CurvePool ETL at analyze: Line/Circle/NURBS/Polyline with lock-free
  value() and evenArcFractions() for border sampling (no BRepAdaptor_Curve
  on the freeform even-arc path when pool hits).
- MeshScratch alignas(64); per-worker padded progress counters — no shared
  atomic fetch_add inside the parallel face mesh loop.
- ActiveGeometryScope now also binds CurvePool for generate().

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
evenArcFractions must not diverge from UniformAbscissa (mp9 empty/NM
regression). CurvePool remains extracted at analyze for lock-free value()
and future sampling; alignas scratch + padded progress stay.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
- CurvePool extracted at analyze (Line/Circle/NURBS/Polyline) with
  lock-free value()/evenArcFractions APIs; border sampling still uses
  GCPnts as the contract oracle until pool tables match bit-exactly.
- MeshScratch alignas(64); parallel mesh workers use padded progress
  counters (no shared atomic in the inner loop).
- Revert racy parallel planFace — GenerationCache memos are not
  thread-safe; warm planning remains ~3ms via FacePlanCache.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Extends healWithHistory with ShapeCustom_Surface::ConvertToAnalytical
and optional ShapeCustom::BSplineRestriction, gated by env (default OFF
so mp9 watertight stays green). WEFT_PROFILE_IMPORT prints stage timings.

A/B mp9_Edited: analytic fit converts 89 BSpline→Cylinder but reopens
seams; BSpline restrict ~16s→~4.5s cold gen but fails floor/NM gates.
Keep experimental until pcurve rebuild is solid.

Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
Co-authored-by: jmossymoss <jmossymoss@users.noreply.github.com>
@cursor
cursor Bot merged commit 6deeb32 into main Aug 20, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants