Files
Vernier/FUSION_LOG.md

65 KiB
Raw Permalink Blame History

FUSION_LOG

What Fusion 360 and Shapr3D express that VernierCAD cannot, within the brief's scope, ordered by what blocks a real part.

Reconstructed by analysis, not by use — entries 1–3 corrected 2026-08-28; entries 4–8, 9, 10 and 12 corrected 2026-08-29, also by analysis (entries 4 and 6 corrected twice that day, as whole-face fillet/chamfer and then sketch-fillet each landed; entry 4 corrected a third time as asymmetric chamfer and single-edge variable-radius fillet landed, and a fourth time as an empirical probe corrected its own "OCCT exposes no parameter for it" corner-setback claim; entry 5 corrected twice, as TangentCircles and then TangentLineCircle each reached the sketch and the UI, resolving it; entry 6 corrected a third time as sketch-fillet grew line-arc and arc-arc corners, a fourth time as trim/extend-to-a-point landed for both lines and arcs, and a fifth time as curve splitting landed too, resolving it; entry 9 corrected as sweep's and loft's own spline-refusal naming caught up to revolve's; entry 11 corrected as sweep's "keep orientation" mode landed at the kernel and document layers; entry 11 corrected a second time, 2026-08-30, as guide rails landed too, scoped down by a measured OCCT hang; entry 10 corrected the same day with the loft-smoothing workaround decision — a documentation-only change, not a code correction; entry 4 corrected a fifth time the same day as ProvenanceKey::VertexBlend landed, resolving it for constant-radius fillet). Re-derived again 2026-09-02 against 491c4a0 for card ed6218c4, which added the reachability section below and corrected four claims (and again the same day, after Cybersec gated it: the beta list was missing entry 4's three unreachable fillet/chamfer modes, the "no test crosses the gesture layer" line was too strong, and the note on the guard's own bug measured a set the bug cannot reach): entry 1's open caveat about principal, offset and angled datum planes, entry 3's silence about having no gesture at all, entry 10's "available today" workaround naming an unwired command, and this file's own closing sentence, which still said the first three entries stand between here and a bike light mount. PROJECT_BRIEF.md §2 says this file is written when Fusion is opened for a part Vernier cannot express, from L1 onward. Entries 1–3 were written before the work that closes them had landed (sketch planes, the Hole feature, EdgeVertex/EdgeCenter sketch references); entries 4–10 and 12 the same way, before later waves landed fillet/chamfer edge chains and whole-face fillet, four more sketch constraints, two-direction and arbitrary-span patterns, multi-source mirror, sketch offset and mirror-within-a-sketch, sketch-fillet, a fit-point spline, and whole-body/edge/point measurement. This correction is itself by code-reading, not by use — nobody has yet re-run L1's actual bar ("a sketch edit propagates through a fillet without exploding") on a real edit-after-the-fact part against this code. That is exactly what the dogfooding pass this file exists for should confirm next, and if it finds a hole, the log corrects again. When the dogfooding loop starts in earnest, entries move up or down by what actually stops a part, and that evidence beats everything here, corrections included.

Out of scope and deliberately absent from this list. CAM, simulation, generative design, cloud, rendering/animation, sheet metal, surfacing beyond loft and sweep are never in scope. Assemblies, joints, 2D drawings and multi-body are not-for-now. None of them is a gap; they are decisions. Nothing below asks for any of them.

What already exists, so the list reads honestly. The feature vocabulary (FeaturePayload, vernier-doc/src/document.rs) is Sketch, Extrude, Revolve, Fillet / Chamfer, Shell, Draft, PushPull, Hole, Pattern, Mirror, Loft, Sweep — and Revolve, Sweep and Loft are now creatable from the UI, not only editable. Seventeen sketch constraints solve, including Midpoint, Collinear, Concentric and Symmetric. Sketches carry a plane and can be drawn on any planar face, not just world XY. Sketch geometry can reference a face centroid or an edge's exact vertex/centre through the naming store. A fillet/chamfer can take a chain of edges, not just one. A pattern can step in two independent directions or over an arbitrary circular span, and a mirror can reflect an ordered list of source features, not just one. Measurement covers whole-body volume/area/centroid/bounding-box, edge length, arc radius and point-to-point distance, not only face distance and angle. A body can be sectioned by a plane for a cross-section length readout (Action::Section), the direct analogue of Fusion's Section Analysis. STEP imports and exports, STL exports, naming resolves through a real provenance machinery. That is a great deal more than the phase table implies, and it is why the gaps below are specific rather than sweeping.


Reachability: EXISTS, WIRED, COVERED — re-derived 2026-09-02, card ed6218c4

Card ed6218c4 asked for the three to be stated separately for every entry, because they came apart on entry 3 and this file never grew a column for it. A reader could finish an entry marked RESOLVED without learning that nothing in the running app performs it. This section is that column, derived from the code at 491c4a0 rather than remembered.

Definitions. EXISTS: the document or kernel layer can express it. WIRED: a gesture a person can make in the running app reaches it, meaning an arm of Action (vernier-ui/src/toolbar.rs, 60 arms) or of SketchTool (vernier-ui/src/shell/sketch_mode.rs, 8 arms), dispatched through an Edit in vernier-app. COVERED: a test drives it.

The instrument, which already existed. vernier-app's own every_document_command_is_reachable_or_declared_unreachable answers WIRED mechanically for every document command: 37 impl Command for blocks, each either dispatched from the app's production half or named in its NOT_YET table with a stated, machine-re-askable blocker. It passes at 491c4a0, and nine commands sit on that list. The table below reads WIRED from that guard, not from prose.

# Entry EXISTS WIRED COVERED
1 Sketch planes yes partial yes
2 Hole yes yes yes
3 Sketch references outward yes no yes
4 Fillet / chamfer, richer modes yes partial yes
5 Tangency constraints yes yes yes
6 Sketch curve editing yes partial yes
7 Pattern yes yes yes
8 Mirror yes yes yes
9 Fit-point spline yes yes yes
10 Loft continuity / guides no, decided n/a n/a
11 Sweep orientation / guides yes no yes
12 Measurement yes yes yes
13 STEP into the document no n/a n/a

Every one of the nine unwired commands belongs to an entry this file marks RESOLVED or NARROWED. That is the whole finding, and it is why the per-entry verdicts read better than the app behaves:

Command Entry Blocker as declared
ProjectFaceIntoSketch 3 Undecided — what the button says is a decision, not a wiring job
ProjectEdgeIntoSketch 3 no Action::ProjectEdge; the button's wording is the open question
ProjectEdgeCenterIntoSketch 3 no Action::ProjectEdgeCenter; ships with the edge one
RepointCurveEndpoint 6 Undecided — no picking gesture for "which point on which curve"
TrimCurveEndpoint 6 same gesture question
SplitSketchCurve 6 same gesture question, plus telling a split from a trim at one click
SetSweepOrientation 11 no gesture names a sweep's orientation mode
SetSweepGuide 11 no gesture names a guide rail
SetLoftSections 10 no FeatureInput slot names a LIST of chains

AND THE POPULATION THIS TABLE IS DRAWN FROM HAS A BLIND SPOT, which is how item 7 of the beta list below went missing from the first version of it. The guard enumerates impl Command for blocks, so its unit is the COMMAND. A command that is dispatched but whose PARAMETER is an enum with modes the UI cannot construct scores as fully wired: SetFilletSpec is reachable, and three of its five FilletSpec modes are not. Nothing mechanical here sees that, and nothing will until the derivation takes its population from the constructible variants rather than from the commands. Until then, an entry marked partial in the table above may hide a gap of exactly the shape this section exists to surface, and the beta list has to be checked by hand against each partial entry's own body.

A limit of the instrument, found while using it. The guard's two halves disagree about what counts as the app. The staleness half correctly reads only the production code (APP.split("#[cfg(test)]").next()), and says why. The unreachability half searches the whole of main.rs, test module included, so a command mentioned only in a test would be scored reachable. Corrected 2026-09-02, and the correction is about the POPULATION: this paragraph first measured that eight of the nine unreachable commands appear in main.rs only after #[cfg(test)] and are all on NOT_YET. That is a true sentence about a set the bug cannot reach, because the loop continues on every NOT_YET name before the buggy line runs. The set the bug is actually exposed to is the 28 commands not on the list, and of those, 0 are hidden by it. Same conclusion, right reason. Fixed in 8b4fc12, whose two-sided arm did use a command outside the list and so tested the exposed population correctly.

What the CLI selftest does not cross, which is narrower than "no test". vernier-cli does not depend on vernier-app (crates/vernier-cli/Cargo.toml), so not one of the selftest's 24 green checks passes through a gesture. l1-bike-light-mount and l1-fillet-edit both pass, and both prove the document and kernel layers only. The app has its own coverage, 183 server.handle(Edit::…) calls, but a command with no Edit variant has nothing there to cover. So for the nine, EXISTS and COVERED are both true and say nothing about WIRED, which is exactly the confusion this section exists to end.

What stands between here and beta

Ranked, and this is the list to sequence from rather than the rung tables above, which now describe the document layer more accurately than they describe the product.

  1. Entry 3's three projection commands have no gesture. L1's bar is "a sketch edit propagates through a fillet without exploding". Propagation needs a reference, and no user can create one. This is the single item that makes a green L1 misleading, and its blocker is a wording decision, not engineering.

  2. The sketch plane picker reaches two of five frames. Edit::NewSketch takes on_face: Option<u64> and nothing else, so the app produces Face or World(Xy). World(Yz), World(Zx) and Offset exist, are exercised by the CLI selftest, and no gesture selects them. There is also no command anywhere that re-planes an existing sketch: the choice is made once, at creation, from two options.

    The mirror plane has the same gap, and it was unwritten until card ab7ac5e7. MirrorPlane::SketchLine — a plane through two sketch points, and therefore parametric: it follows the sketch under a dimension edit — has ZERO constructions in vernier-app. Action::SetMirrorPlane dispatches only MirrorPlane::Principal, which is documented as "not parametric — a recorded deviation". Both the CLI selftest and vernier-ui build SketchLine planes, and MirrorPlane is Deserialize, so a document carrying one is reachable rather than hypothetical. Re-planing such a mirror used to REPLACE the parametric plane with a recorded one, silently — the destruction half of entry 7's shape, one enum over. That half is fixed: the edit now refuses. Whether the gesture should exist, or the variant be removed or documented as CLI-only, is still open.

  3. Entry 6's trim, extend and split have no gesture, blocked on a real design question rather than on work.

  4. Entry 11's sweep orientation and guide rails have no gesture.

  5. Entry 10's recommended workaround is not available in the app. It names "the existing AddLoft/SetLoftSections commands", and SetLoftSections is on NOT_YET.

  6. L1 has never been driven by hand. Narrower than an earlier draft of this line, which said no green check crosses the gesture layer: vernier-app has substantial coverage of its own gesture layer. What no check crosses is the CLI selftest's 24, which are where the L1 and L2 rung proofs live. So the rung bars are proven at the document and kernel layers only. This is the dogfooding pass the file's own front matter asks for, and the cheapest way to find the gaps above in the order a user meets them.

  7. Three of entry 4's fillet and chamfer modes have no gesture, and the UI DESTROYS them on edit. FilletRadius::Variable, ChamferMethod::TwoDistances and ChamferMethod::DistanceAngle are built and tested (fillet-variable-radius-edit and chamfer-two-distance-edit are selftest checks of their own), and the app constructs none of them: Edit::SetFilletSpec only ever writes FilletRadius::Constant or ChamferMethod::Equal (vernier-app/src/main.rs:3362-3364). Worse than unreachable — main.rs:2564 reads an existing richer spec only to seed the simple value card with its FIRST number, and its own comment says re-editing "flattens it to symmetric". So a variable fillet that reaches a document by any route is silently destroyed by an ordinary edit. Entry 4's heading advertises an asymmetric chamfer nobody can perform.

    THE DESTRUCTION HALF IS FIXED (card 147c6810); the gesture half is not. The read side at main.rs:2564 now reports richer alongside the first number, and edit_for REFUSES Action::SetFilletSpec for a spec the value card cannot express — so an ordinary edit no longer rewrites a variable radius or an asymmetric chamfer as a symmetric one. The timeline's parameter column marks such a feature r2.00+, because without the marker it reads as an ordinary r2.00, which is the same untruth the flattening used to tell by doing it. What is still true above: the app constructs none of the three, so they remain reachable only by a route outside the gesture layer, and entry 4's heading still advertises a chamfer nobody can perform. Refusing is the beta answer; the gesture is the larger piece and is not this fix.

  8. Entry 13, STEP geometry into the document, still absent and still lowest.

Angled datum planes do not exist at all. SketchPlane is World(PrincipalPlane), Offset { from, distance_mm } and Face { face }. Offset is parallel-only, by construction. A plane at an angle to a base plane that is not a picked face cannot be named. This settles the open caveat entry 1 recorded and could not confirm.


Rung L1 — blocks the milestone the project exists for

L1 is the bike light mount: constraints, holes, fillets, edit-after-the-fact. These three stop it outright. Nothing further down matters until they are closed.

1. Sketches have no plane. Every sketch is XY at Z = 0. — RESOLVED

Blocked: L1, and everything above it.

Evidence this closed. SketchData now carries a plane (vernier-doc/src/sketch.rs, landed across commits 29e3186→09d979d, "a sketch plane is a placement the caller chooses" through "L1 builds — the bike light mount, tab on an angled face"). A sketch can be drawn on a face and follows it (424cac3). The bike light mount itself — a clamp with a tab at an angle to it, the exact case this entry named — has been built on this line of work.

What Fusion and Shapr3D do. Sketch on any of the three principal planes, on any planar face of the model, or on an offset/angled datum plane created from one. VernierCAD now does the face case. Settled 2026-09-02, correcting this entry's own open caveat: SketchPlane has exactly three variants, World(PrincipalPlane), Offset { from, distance_mm } and Face { face }, so the principal planes and parallel offset datum planes DO exist, and the CLI selftest drives all three. An angled datum plane does not exist at all, since Offset displaces along its own normal and nothing else tilts. Of the frames that do exist, Edit::NewSketch reaches two: it takes on_face: Option<u64> and nothing else, so the app makes a Face sketch or a World(Xy) one. Nothing anywhere re-planes an existing sketch. See the reachability section.

One deviation did NOT retire. This entry predicted that closing it would also retire the recorded deviation in FeaturePayload::Sweep (path read in world XZ regardless of the picked path sketch's own plane). It did not: Sweep's own doc now calls this a structural limitation, not a stopgap. See entry 11 — it is still open, just no longer downstream of this one.

2. No Hole feature. — RESOLVED

Blocked: L1 (the ladder names holes for L1 by name).

Evidence this closed. FeaturePayload::Hole exists (vernier-doc/src/document.rs:289), points-based, with HoleDepth (ThroughAll/ToDepth) and an optional Counterbore for a fastener head — reachable from the toolbar by picking points, not by picking a face (its group caption says "from points" for exactly that reason).

Still short of Fusion/Shapr3D. No countersink, no tip angle, no tapped-thread spec, no sizing from a fastener standard. Simple and counterbore, through-all or blind, is what exists. Worth a fresh, smaller entry of its own if the dogfooding pass hits one of these — this entry is closed for what it originally asked (a hole that is one feature, not a circle-and-cut a user keeps consistent by hand), not for the fuller Fusion feature set.

3. A sketch cannot reference anything outside itself. — SUBSTANTIALLY RESOLVED

Blocked: L1, and it is the half of "edit-after-the-fact" that is not about the solver.

Amended 2026-08-14 (card 0482acc1), then again since. A sketch can carry references: points DRIVEN by geometry outside it, resolved through the NamingStore on every rebuild, so a downstream sketch genuinely depends on an upstream feature instead of holding a number that agrees today. Proven by the coincidence test this gap asks for — raise a plate from 4 mm to 12 mm, touch nothing downstream, and the projected point moves 8 mm — plus loud failure when the source is gone, a conflicting constraint that cannot quietly win, and survival across a save/reload of document and naming store.

The specific remaining gap this entry named is now closed too. As of the 2026-08-14 amendment the only thing projectable was a planar face's CENTROID — not an edge, and "keep this hole 5 mm from that wall" wants the wall's edge, not its middle. SketchReference now also has EdgeVertex (an exact edge endpoint, named the way the kernel names an edge — the adjacent face pair plus a sibling rank — and it projects exactly, unlike a curve) and EdgeCenter (the centre of a circular edge — a bolt circle's axis, a bore's middle — sound for both a full circle and an arc). Both resolve through the naming store exactly as FaceCentroid does (vernier-doc/src/sketch.rs:84-160, vernier-ui/src/compile/resolve.rs). "5 mm from that wall's edge" is expressible now, not just "5 mm from that wall's middle."

Why "substantially" and not fully closed. Projecting an edge as a CURVE (not just its endpoints/centre) is still absent and is not a small gap — SketchCurve has no ellipse, so a circle projected off-axis has nowhere to land; this is flagged in the code as a real, considered limit, not an oversight. Two straight-line endpoints name a straight edge completely, which covers a lot of real geometry, but a curved reference edge is still out of reach.

AND NOTHING IN THE RUNNING APP PERFORMS IT — added 2026-09-02, and this is the correction card ed6218c4 was opened for. All three commands that write a SketchReference (ProjectFaceIntoSketch, ProjectEdgeIntoSketch, ProjectEdgeCenterIntoSketch) are on vernier-app's NOT_YET list: no Action arm offers any of them, and each declared blocker is a decision about what the button should SAY rather than work outstanding. Every proof this entry cites constructs the command directly in a test. So the document layer genuinely propagates and a user cannot yet ask it to. This entry was already the file's example of EXISTS and WIRED coming apart, and it still did not record its own wiring half.

Why this entry mattered more than the others. The brief defines L1 as "a sketch edit propagates through a fillet without exploding". Propagation needs something to propagate to. This was the gap most likely to be mistaken for "L1 passed" while every downstream dimension was still an independent number that happened to agree today. It no longer is, for the straight-edge case — confirm this holds on a real part in the dogfooding pass before trusting it further.


Rung L1 — friction, not a wall

Each of these is expressible today but costs enough hand-work that a real part gets slower rather than impossible.

4. Fillet and chamfer take a chain or a whole face now, chamfer can be asymmetric, and one edge can taper. — RESOLVED FOR CONSTANT-RADIUS FILLET

Evidence this narrowed. fillet_edge (vernier-occt-sys/src/lib.rs) takes edges: &[EdgePick], folding every edge into one BRepFilletAPI_MakeFillet/MakeChamfer call; FeaturePayload::Fillet.edges: Vec<(u64, u64)> replaces the old single face_a/ face_b pair. "A box with rounded verticals is four separate features carrying four radii that must be kept equal by hand" is closed: one feature, one radius, picked as a sequence of face pairs. Picking a WHOLE FACE now works too: every edge bounding the picked face becomes the chain automatically (Edit::FilletFace, resolved server-side against the compiled body's face-adjacency data — the manually-picked chain resolves client-side with no body knowledge, but "which edges bound this face" genuinely needs it). Cross-checked, not just plausible: the starter block's top face, filleted whole-face vs. the equivalent manually- picked 4-edge chain, produce identical volume to 1e-9 relative tolerance. Naming-under-stress is not theoretical either way — a dedicated probe fillets a box's four vertical edges in one operation and asserts each resolves to its own distinct face, then survives a width edit with every identity intact (crates/vernier-kernel/tests/fillet_chain.rs).

Asymmetric chamfer landed. FeaturePayload::Fillet.radius/.chamfer reshaped into one FilletSpec (Fillet(FilletRadius) / Chamfer(ChamferMethod)) so an illegal combination cannot exist. ChamferMethod::TwoDistances/DistanceAngle call OCCT's own BRepFilletAPI_MakeChamfer::Add(Dis1, Dis2, edge, F) / AddDA(Dis, Angle, edge, F) — F (the reference face the first distance/angle is measured from) is the specific EdgePick's own face_a, so these two modes loop picks directly rather than the flattened, order-blind edge list Equal/fillet still use. That surfaced a real bug fixed in the same pass: Edit::FilletFace::apply resolved a whole face's bounding edges via EdgeKey::faces()'s ascending-EntityId order, not "picked face first" — harmless while every mode was symmetric, wrong the moment from_a_mm means something specific. Verified by the closed form itself: the frustum volume π·d1·d2·(R − d1/3) generalizes check_chamfer_edit's equal-leg π·c²·(R − c/3) exactly (d1 = d2 = c), and a dedicated regression test picks whichever face sorts LARGER by id on purpose — the case where the old order and the correct one disagree — and confirms from_a_mm still lands on the picked side. FromEdge's identity is unaffected either way — a dedicated naming test switches a live chamfer from Equal to TwoDistances against the same table and confirms the id does not churn (crates/vernier-kernel/tests/fillet_chain.rs).

Variable radius along a single edge landed too. FilletRadius::Variable { start_mm, end_mm } calls BRepFilletAPI_MakeFillet::Add(R1, R2, edge) — OCCT's linear radius-evolution law. Scoped to exactly one physical edge, refused otherwise (EvaluateError:: VariableRadiusNeedsSingleEdge): Add's per-edge contour is built by PROPAGATION from the edge it's given, so a ramp applied across an independently-picked multi-edge chain risks OCCT silently merging tangent edges into one contour, a correctness question invisible for a constant radius (every edge shares one value) but real for a ramp. start_mm/end_mm are ordered GEOMETRICALLY (the edge's own geometric_endpoints, shared with shape_edges's card-03377d20 fix), never by OCCT's topological orientation. This is where a real, measured-not-assumed OCCT finding turned up: Add(R1, R2, edge) keys R1/R2 off the edge's underlying CURVE parametrization, not the TopoDS_Edge's oriented first/last vertex — the first cut used the latter and a naming-under-stress-style fixture caught it cleanly (both end caps' areas matched the OTHER value's closed form exactly, not a near-miss). Total volume is swap-blind by construction (reversing a linear ramp doesn't change the values it takes over a fixed interval), so the regression test checks each end cap's area independently — the one observable a swap actually changes. The blend surface itself is a real (non-quadric) surface, unlike the exact cylinder/cone/plane every other mode here builds, so its volume integral carries a small measured approximation (~3e-5 relative) the closed-form check budgets for; the two cap areas, plain circular arcs at the ramp's own boundary values, stay exact.

No UI gesture yet for any of the three richer modes. The value card still edits one plain radius/distance field (Action::FilletEdge/ChamferEdge/SetFilletSpec all build FilletSpec::Fillet(FilletRadius::Constant(_))/FilletSpec::Chamfer(ChamferMethod::Equal(_)) from it, unchanged in effect from before this pass). TwoDistances/DistanceAngle/Variable are real, tested capability reachable only by constructing the command directly — the same "built, not yet a button" shape as ProjectFaceIntoSketch: what the mode-picker widget should look like is a UI design decision, not a wiring job.

What Fusion and Shapr3D still do that this doesn't, and a correction to the "OCCT exposes no parameter for it" line above. OCCT's own header tells a different story than assumed: measured by a probe (crates/vernier-kernel/examples/corner_fillet_probe.rs) against three converging edges at one box vertex in a single fillet chain, the result comes back Err("result face (token 8) has no attributed source") — not a façade failure (kOperationFailed/BRepCheck_Analyzer), a naming one. That message only fires after OCCT has already built a valid shape with an extra face this façade cannot yet name: ChFi3d_Builder (the engine behind BRepFilletAPI_MakeFillet) has dedicated internal methods for exactly this case (PerformThreeCorner, distinct from the ordinary per-edge path), and Generated() is documented to accept a vertex, not just an edge or face — this façade's record_generated simply never asks it the vertex question, for any fillet or chamfer call. So: fillet chains around a real corner (reachable today with the shipped multi-edge feature — picking every edge of a part) hit this failure now, cleanly (invariant #5 held — no misattribution, just a refusal), but a real gap all the same. Fixing it is a ProvenanceKey::VertexBlend { faces: BTreeSet<EntityId>, sibling } (named by the input faces meeting at the vertex, FromEdge's own idiom generalized past two faces) plus a vertex-walking pass in record_generated — a contained naming-substrate addition (measured: exactly two exhaustive ProvenanceKey matches in the whole workspace need a new arm), not the open-ended hand-built-topology project this entry originally feared.

Built, 2026-08-30 — ProvenanceKey::VertexBlend landed, exactly the shape predicted above. record_generated (vernier-occt-sys/src/facade.cpp) gained a vertex-walking loop, run BEFORE the edge loop so a vertex-owned face is claimed there first rather than risking misattribution to whichever adjacent edge's own Generated() list a caller happened to walk. Scoped to constant-radius fillet only, via a plain include_vertices parameter on record_generated that every other caller (shell, draft, chamfer, variable-radius fillet) leaves at its default false — chamfer's asymmetric per-edge modes have no settled answer for which distance applies in which direction at a shared corner, a materially harder question named rather than attempted. The closed form for the test was derived, not guessed, then confirmed exact (0.0 difference at OCCT's own double precision) against a standalone probe before being pinned: three mutually perpendicular edges of equal fillet radius R meeting at a corner are tangent-continuous only if the corner cap is a sphere of radius R — the unique surface tangent to all three cylindrical fillet surfaces at once — so the exact volume of a box with one corner rounded this way is DX·DY·DZ − (1 − π/4)·R²·(DX+DY+DZ) + (2 − 7π/12)·R³, the naive independent-edge formula re-anchored against the true, once-counted octant-of-a-sphere corner (fillet_corner.rs). Naming-under-stress holds the same way fillet_chain.rs already proved for a plain edge: two different corners filleted in one chain mint two distinct VertexBlend identities, and a width edit that leaves a corner's own local geometry untouched keeps that corner's identity exactly — assemble_from_history's existing centroid-rank grouping (already load-bearing for FromEdge collisions) covers VertexBlend collisions too, for free, once it is added to that one match arm; no new disambiguation machinery was needed. NamingStore's own save-format version bumped to 2 for the new persisted key vocabulary, the same discipline Bridged set at M4.

Cost today. A filleted rim on a face is one pick now, not N manual face-pair picks, and filleting every edge of a box-like part in one operation — the realistic "round the whole thing" action — now builds and names its corners correctly rather than refusing. Nothing this entry named remains open for constant-radius fillet; chamfer's own corner behavior is untouched and unexamined, named above as the harder remainder.

5. Tangency beyond line-to-circle exists in the solver but reaches no sketch. — RESOLVED

Evidence this narrowed. Seventeen constraints now exist (Constraint, vernier-solver/src/constraint.rs), the thirteen already here plus Midpoint, Collinear, Concentric and Symmetric — Symmetric proven after a real solve: lock one point, constrain the other symmetric about an axis, and it lands at the exact mirror image by coordinates, not just "the solve converged." "A symmetric bracket is fully constrained only by writing the same dimension twice" is closed.

TangentCircles (arc-to-arc, arc-to-circle) reaches the sketch and the UI. SketchConstraint::TangentCircles { a, b, external } (vernier-doc/src/sketch.rs) is wired into entities() and solve_sketch exactly like Concentric; plan_tangent_circles (vernier-doc/src/command/plan.rs) is the "decide once from live geometry, then persist" function its external/internal sense needed — the same shape plan_offset/plan_mirror_curves already established. It refuses up front, by name, the two cases that are not decidable from geometry alone: coincident centres, and equal radii under internal tangency (which side is "inside" is undecidable from the radii). Action::ConstrainTangent reaches Selection::TwoCircles alongside ConstrainConcentric. Proven through the real picking path: two circles drawn 9 mm apart (radii 4 and 2, no radius dimension on either) land with their centres exactly r1 + r2 apart after the solve.

TangentLineCircle (a line tangent to a circle or an arc) reaches the sketch and the UI too, closing this entry's last item. Its solver payload is vernier_solver::Side, which has no serde derives (vernier-solver has no serde dependency at all) — resolved with a doc-side Side twin in vernier-doc/src/sketch.rs, translated to the solver type at the one call site that needs the mapping (solve_sketch), the same typed-enum-then-translate idiom vernier_kernel::evaluate::BooleanKind already uses one layer down. plan_tangent_line_circle decides side once from the sign of the same cross-product test vernier_solver::Side::of_current uses, evaluated against this document's own points rather than a solver sketch. Action::ConstrainTangentLineCircle reaches the new Selection::LineAndCircle — previously a line and a circle/arc picked together selected to nothing at all (TwoLines's own doc comment used to say so explicitly). Proven through the real picking path: a locked line and a circle drawn 20 mm off it land with the circle's centre at exactly the circle's own radius of perpendicular distance from the line, on the geometrically correct side. DOCUMENT_FORMAT_VERSION bumped to 22 for the new SketchConstraint variant — which also retroactively covers TangentCircles, landed earlier without the bump its own variant-addition should have carried.

Cost today. Every tangency the solver can express now has a sketch constraint and a picking gesture: line-to-circle, circle-to-circle, arc-to-arc, and every mix. Nothing named by this entry remains open.

Closed by M3 (2026-09-07): tangency is now PROPOSED as well as expressible, and there is a tool that draws it. infer_arc_constraints / infer_circle_constraints (SOLVER.md §5b) offer tangent-to-line and tangent-to-circle on every drawing gesture, deciding the side and the external/internal sense once and never re-deriving them, and SketchTool::TangentArc continues a chain with an arc that leaves the previous curve tangentially — so the relation this entry was about can be drawn rather than only constrained after the fact. Two caveats, both measured rather than assumed: a proposal the sketch already IMPLIES is dropped by trial solve (a slot states Parallel between its own lines, so a second Horizontal would make it Redundant), and a tangency between two curves of the SAME primitive is never proposed at all.

6. Sketch geometry's curve-editing tools don't reach every curve kind — offset, mirror-within-a-sketch, sketch-fillet, trim/extend and curve splitting are now all built. — RESOLVED FOR WHAT THIS ENTRY ASKED

Evidence this narrowed. SketchCurve is four variants now (Line, Arc, Circle, Spline — vernier-doc/src/sketch.rs), and Rectangle/Slot were already confirmed mitigated as compound UI gestures over Line/Arc. plan_offset and plan_mirror_curves (vernier-doc/src/command/plan.rs) — a closed line-chain's offset (exact for a circle too — a growth/shrink of its radius) and a mirrored copy of picked curves — are reachable from the running app now: a picked chain plus a distance dispatches offset; picked curves plus a picked axis line dispatches mirror. Both proven through the real picking path, not just by constructing the command directly — offsetting a picked square by 3mm grows it to the exact 13mm half-square plan_offset's own closed form predicts; mirroring a picked line about a picked axis lands its reflection exactly where reflect_point's closed form says.

Sketch-fillet landed, narrower than a general "modify a curve" primitive. RepointCurveEndpoint (vernier-doc/src/command/sketch_geometry.rs) repoints one endpoint FIELD of an existing Line/Arc to a different, already-existing point entity — the one primitive this codebase's shared-corner convention (vernier-ui/src/primitives.rs's "no Coincident on the corners") was missing to let two curves that share a corner stop sharing it. AddSketchFillet composes it: plan_fillet_corner's closed-form line-line fillet math (tangent length radius / tan(phi/2), centre distance radius / sin(phi/2)) finds the two tangent points and the arc centre, then one apply() call allocates the arc via an internal AddSketchGeometry::apply() and repoints both lines via two internal RepointCurveEndpoint::apply() calls — a new pattern (one command's apply() calling other commands' apply() directly) adopted because the new arc's endpoints don't exist until AddSketchGeometry::apply() has already run, so nothing can pre-build a standalone repoint naming them ahead of time; Document::execute_all cannot express that ordering and was not used. Line-arc and arc-arc corners landed too — the classical Apollonius specializations (a circle of given radius tangent to a line and a circle; tangent to two circles), each a signed- radius intersection (Rc = R ± radius against the arc's own circle) with up to 4 (line-arc) or 8 (arc-arc) raw candidates, filtered to whichever stays within the corner's own leg (a line's finite length; an arc's remaining sweep toward its other endpoint) and, among what survives, whichever centre sits closest to the corner — plan_tangent_circles's own "decide once from current geometry" rule, generalized from one binary choice to a small candidate set. Verified against four hand-derived closed forms, including two disambiguation pairs proving the sense (external/internal on a line-arc corner; which arc curves which way on an arc-arc corner) is read from geometry alone, not guessed: the same two arcs, mirrored, land at two completely different exact centres. The one forced refusal is FilletCornerError::ConcentricArcs — two arcs sharing a centre also share the corner, which forces equal radii too, so there is no direction "tangent" could mean (the arc-arc sibling of TangentCirclesError::CoincidentCentres). Still refused: Circle and Spline legs — a closed circle has no leg to fillet along, and a spline is general trim/extend's still-open problem. The old shared corner point is left orphaned rather than removed, matching this document model's existing tolerance for dead entities. The naming-layer risk the investigation flagged — a new arc face changes which face-pairs are adjacent, which could silently break a downstream sketch's face-pair-keyed edge reference — is pinned by a_sketch_fillet_that_separates_two_faces_breaks_a_downstream_edge_reference_loudly (vernier-ui/src/compile/tests/external_references.rs): both faces individually still resolve (their curve identity never changed), but the compile now fails loudly with CompileError::ProjectionEdgeMissing rather than silently rebinding, invariant #5 held.

Trim/extend-to-a-point landed, general enough that the "real design task" framing above was wrong. RepointCurveEndpoint doesn't care whether its target point pre-exists or was just minted, and AddSketchFillet already composes "mint a point, then repoint an endpoint to it" — TrimCurveEndpoint (vernier-doc/src/command/sketch_geometry.rs) reuses exactly that shape rather than needing a new mutating primitive. plan_trim_curve_endpoint covers both Line and Arc: a line projects the click onto its own infinite carrier with one unclamped signed projection, Rc = start + t·dir, and whichever endpoint is nearer in that parametrization moves there — the same formula, unbranched, produces a trim when t falls inside the line's own span and an extend when it falls outside. An arc projects the click onto its own circle (center + radius·normalize(near−center)) and moves whichever endpoint is angularly nearer. Refused by name: WouldCollapse (result too short to survive POINT_COINCIDENCE_MM), UnsupportedCurve (Circle/Spline — no endpoint field to move), DegenerateProjection (an arc click at its own centre). One wraparound bug caught before it shipped, worth recording given this codebase's own measured-not-assumed habit: the first arc implementation measured "how far CCW from start" directionally, which reads a click just behind start as almost a full lap forward and moves the wrong endpoint; fixed by comparing shortest-path angular distance to each endpoint instead, direction- agnostic by construction, pinned by extend_an_arc_backward_past_its_start_moves_start_not_end. Naming-under-stress: trimming a shared corner endpoint necessarily opens whichever profile owned it, and the owning extrude's own recompile refuses it loudly (ProfileError::NotClosed, not a silent smaller shape) — trimming_a_shared_corner_endpoint_opens_the_profile_and_the_owning_extrude_refuses_to_compile_loudly (vernier-ui/src/compile/tests/external_references.rs); a companion test proves identity survives the opposite case, a corner slid by two coordinated trims that keep the profile closed throughout. UI wiring is not shipped yet — the picking gesture for "which point on which curve" is a genuine open design question, tracked as a Blocker::Undecided in vernier-app's reachability test — but plan_trim_curve_endpoint is pure, so a UI can call it directly once that gesture is designed.

Curve splitting landed too, scoped exactly as trim/extend's own precedent anticipated. SplitSketchCurve (vernier-doc/src/command/sketch_geometry.rs) composes one AddSketchGeometry call — mint the split point AND the far half's curve together, since the far curve's own start point does not exist until the split point does — with one RepointCurveEndpoint call that shrinks the original curve's end down to the split point: the same AddSketchFillet/ TrimCurveEndpoint "allocate then reference" shape a third time. plan_split_sketch_curve covers Line (unclamped signed projection, refusing a point outside (0, len) rather than reinterpreting it as an extend the way trim does) and Arc (projected onto its own circle, refusing a point outside the CCW sweep from start to end — directional on purpose, unlike trim's shortest-path measure, because a split must respect the arc's own explicit direction, not just find the nearer endpoint). The id rule is fixed by convention, never by geometry: the half touching the curve's own stored start always keeps the original EntityId — only its end field moves — and the far half is always the one minted fresh, so splitting needs no "which half is nearer" judgment call the way trim does. Per this project's explicit decision, no auto-rewrite of any chain that already names the split curve — a stale profile/path chain refuses to compile loudly through the existing NotConnected/NotClosed machinery until the user re-points it by hand to include both halves, matching invariant #5's discipline against guessing which of possibly several referencing chains "wants" the far half. Naming-under-stress, in both directions on the same split: a downstream reference keyed by the ORIGINAL curve's far-end adjacency (now met by the freshly-minted far curve instead) breaks loudly (splitting_a_curve_breaks_a_downstream_reference_to_its_far_side_adjacency_loudly, vernier-ui/src/compile/tests/external_references.rs), while a reference keyed by its near-end adjacency (untouched, since only end ever moves) still resolves exactly as before (splitting_a_curve_preserves_a_downstream_reference_to_its_near_side_adjacency). UI wiring shares trim/extend's own open question — an arbitrary click near a curve, not a selection of an existing entity — and is deliberately not bundled into the same design decision, since whatever gesture is chosen must also let a user tell a split apart from a trim at the same click.

Cost today. A rectangle is still four lines plus four constraints entered by hand under the covers, but the Rectangle/Slot tools already hide that keystroke cost. Offsetting a profile, mirroring a sketch half, rounding a corner — line-line, line-arc or arc-arc — trimming or extending a line or arc to a point, and splitting one into two are now all ordinary sketch-canvas capabilities headlessly (trim/extend and splitting both pending their picking gesture). Nothing this entry named remains unbuilt.

Closed by M3-4 (2026-09-07): trim and split have their picking gesture, and it is a TOOL rather than a selection. SketchTool::Trim and SketchTool::Split interpret an arbitrary click near a curve — which is what this entry left open, and the reason it was left open was that a click near a curve is not a selection of an entity. The armed tool answers the question the click could not: under Trim the near half is kept and the far end moves to the click, under Split the curve becomes two sharing one point. The branch table's refusals are reachable and named: a click on a curve's OWN end refuses instead of minting a duplicate point at the shared corner, and a split outside the span refuses. Both are driven through the real shell in scripts/drive/m3-b-trim-split.json.

One residue this entry did not name, found by the L2 gate: a trim leaves its old endpoint behind. The far point is not deleted — it becomes a free point of the sketch, with two degrees of freedom and nothing drawn at it — and NO GESTURE DELETES A POINT. So a trimmed sketch cannot reach zero free degrees of freedom until someone locks geometry that draws nothing, which is exactly what scripts/drive/l2-enclosure.json does before it can assert an empty warning list. The missing primitive is "remove a sketch entity", and it belongs with the curve-mutation work this entry already describes.


Rung L2 — blocks the jig and the enclosure

7. Pattern can grid and span, reachable from the running app now. — RESOLVED FOR WHAT THIS ENTRY ASKED

Evidence this closed. PatternPlacement::Linear.second: Option<LinearAxis> (a grid of count * second.count instances) and Circular.span (an arbitrary rotation span, not always a full turn) both reach the UI now: Action::Pattern's value card carries all six grid fields (count, dx, dy, count2, dx2, dy2); circular placement is a sibling action, Action::PatternCircular, with its own count, cx, cy, span. "A vent grille is a rectangular pattern. A four-bolt flange on a 90° arc is a circular pattern over a span" is buildable from the running app now, not just by constructing the feature directly. Wiring this up found pattern CREATION itself was more broken than the log credited: Edit::Pattern hardcoded dy: 0.0, second: None and always Linear — circular was uncreatable from the UI at all before this pass, not merely missing its span field. A step_mm parameter was also found wired end-to-end but never exposed via a ParamSpec — typing into it silently did nothing.

Still missing, and it's the one item left. Per-instance suppression (named in Fusion's feature list but not in what actually justified this entry's priority, so left out on purpose).

8. Mirror can take a list of sources, reachable from the running app now. — RESOLVED

Evidence this closed. FeaturePayload::Mirror.source: EntityId is sources: Vec<EntityId> (vernier-doc/src/document.rs) — confirmed safe to widen (not assumed) by reading how a single source was evaluated: each source resolves from solved sketch geometry and merges independently, with no source's reflection depending on any other's presence or position in the list. The UI gap named here is closed too: ctrl-clicking timeline rows accumulates them (the same "extend selection" convention already used for curves/faces), and Action::Mirror dispatches with the whole accumulated set as sources. "A symmetric enclosure half is a sequence of features, mirrored each, in order, by hand" is expressible from the running app now, not just by constructing the feature directly.


Rung L3 — blocks the organic bracket

9. A fit-point spline now exists, as one extrude-profile segment. — PARTIAL, MUCH NARROWER

Evidence this narrowed. SketchCurve::Spline { points: Vec<EntityId> } (vernier-doc/src/sketch.rs) draws through N ≥ 2 sketch points (GeomAPI_Interpolate in the façade) and can be one segment of a closed profile that extrudes. Two things were verified empirically before this was built, not assumed: vernier-solver needs zero changes — a spline mints no solver entity at all, its points are ordinary points the solver already handles — and the existing Generated{sketch curve} naming mechanism (kind-agnostic since M1) resolves a spline's side face correctly with no new naming code, proven by a test that edits an upstream point and confirms the spline face's identity survives, distinct from every other side.

Still absent, on purpose. Control-point/Bezier authoring with curvature handles (fit-point only, for this pass). Revolve, Sweep(-path) and Loft support — refused by name rather than attempted, since the axis-extent/tangent math those need has no closed form for a spline yet. Revolve had this naming from the start (RevolveError::SplineNotSupported); sweep and loft did not — a real, since-fixed hygiene gap, not a design choice: a spline in a sweep's PATH surfaced as the misleading SweepError::NonFinitePathCurve (its endpoints are Some, so the closed- curve check didn't catch it, and the caller couldn't tell "unsupported kind" from "genuinely broken geometry"), and a spline in a sweep's or a loft's PROFILE wasn't caught by Rust validation at all — only by the façade's blanket allow_spline=false, surfacing as a generic, curve-id-less argument error. SweepError::SplineNotSupported/LoftError::SplineNotSupported now name both, before the façade is ever called, mirroring revolve's own precedent exactly. Tangency or curvature-continuity constraints — nothing references a spline as a curve yet, only its ordinary points, so none was needed for this pass. Editing an existing spline's point sequence (trim/extend's equivalent) — the same missing curve-mutation primitive entry 6 already names.

What Fusion and Shapr3D still do that this doesn't. Splines in every profile role (revolve, sweep, loft), control-point authoring, tangency and curvature-continuity constraints, and editing a spline's shape after the fact.

Narrowed again by M3-11 (2026-09-07): a spline's fit points can be DRAGGED. "Editing an existing spline's point sequence" was this entry's last piece of the curve-mutation gap, and half of it is now ordinary: a fit point drags against the solver like any other point, alone, without dragging its neighbours (sketch-drag, and scripts/drive/m3-a-drag.json through the real shell). What is still absent is changing the SEQUENCE — adding a fit point to an existing spline, or removing one — which needs the same "remove a sketch entity" primitive entry 6's own residue now names.

10. Smooth section lofts ship; tangency and guides remain absent. — PARTIALLY RESOLVED 2026-09-15

FeaturePayload::Loft now persists LoftMode::{Ruled, Smooth}. Smooth mode uses OCCT's exact global B-spline interpolation through every section; it is not a per-span smoothing flag and does not polygonize spline sections. The real shell exposes the mode for creation and editing, including ordered section navigation, preview/cancel/apply, explicit body targets, persistence, and STEP/STL export. The five-section ergonomic-handle workflow is the driven acceptance path.

The earlier naming objection was valid and is now paid explicitly rather than ignored. Ruled spans retain local Bridged keys. Smooth lateral faces use SmoothColumn with the complete OCCT-reported section-edge set, naming-store v6, and fail-closed completeness/ambiguity checks. Any section-list change may re-mint every smooth column; that is the documented non-local identity contract.

Still absent are G1/G2 attachment to neighboring faces, guide rails, centerline lofts, periodic lofts, and per-section takeoff weights. A smooth section loft therefore promises interpolation through its sections only. Those remaining features require separate geometry, naming, and interaction designs and are not implied by the smooth control.

11. Sweep pins its path to the world XZ plane and offers no orientation control. — NARROWED TO ONE ITEM

Evidence. FeaturePayload::Sweep's own doc: "the profile is read in the sketch plane (XY at Z = 0) and the path in the world XZ plane — a path point's (u, v) becomes (u, 0, v) … A recorded deviation, retired when sketch planes land."

Scale along the path landed. FeaturePayload::Sweep::scale_end (version 18) plus a Law_Linear/SetLaw call in vernier-occt-sys/src/facade.cpp gives a continuous, exact homothetic scale from 1.0 at the path start to scale_end at its end — confirmed against the actual OCCT call, not just the doc comment.

"Keep orientation" versus "follow path" landed, at the kernel and document layers, with a measured limit that is now part of the feature's own contract rather than a surprise. FeaturePayload::Sweep::keep_orientation (version 21) sets BRepOffsetAPI_MakePipeShell::SetMode(gp_Ax2) instead of the pre-existing moving trihedron — confirmed correct on a straight run (both modes build the byte-identical solid, since there is nothing to follow) and on the M4 gate's own tangent-continuous bend (both build valid, legitimately different solids). Measured, not assumed, and the reason this narrows to two items rather than one: a fixed trihedron at a REAL path corner — a genuine tangent discontinuity, the same shape a_corner_sweep_mitres already probes — builds an invalid solid under every BRepBuilderAPI_TransitionMode this façade could choose (RightCorner, RoundCorner, Transformed all failed identically). This is not a bug in which mode was picked; a non-rotating trihedron simply has nothing for a corner-handling mode to act on. No corner-detection was added to route around it — the pre-existing BRepCheck_Analyzer gate already refuses it, kOperationFailed, the same way it already refuses a bend tighter than the profile. A second, load-bearing measurement made the façade implementation simpler than planned: the fixed trihedron's exact orientation is irrelevant to the output (probed against three unrelated axis choices on the same bend — all agreed), because a trihedron that never rotates only ever contributes a constant offset that cancels out algebraically, so sketch_plane's own frame is reused with no new geometry query. No UI gesture reaches it yet, and for a real, structural reason rather than an oversight: the value card only renders for a command carrying a numeric parameter (card_for filters on it), so a bare bool command has nowhere to appear — Chamfer's and Hole's own toggle chips both ride on an existing numeric command's card rather than needing one of their own. Whether orientation becomes a rider chip on the sweep's scale card, and if so whether that dispatches one bundled edit or two, is a real design question, named as SetSweepOrientation's own Undecided entry in main.rs's reachability check rather than answered unilaterally — the same treatment ProjectFaceIntoSketch and sketch-fillet's RepointCurveEndpoint already have.

Guide rails landed too, at the kernel and document layers, scoped down hard by a hang a spike found before any façade code was written. FeaturePayload::Sweep::guide: Option<SweepGuide> (version 23) sets BRepOffsetAPI_MakePipeShell::SetMode(AuxiliarySpine, true, BRepFill_ContactOnBorder) — the one KeepContact mode this codebase ships, since NoContact overlaps with keep_orientation and Contact's fixed-width precondition has no closed-form pre-check, the same declined-scope reasoning the sweep's own arc-radius self-intersection check already carries. The spike that scoped it: a throwaway C++ program (not part of the crate build) found SetMode(AuxiliarySpine, true, ...) hangs indefinitely — not merely slow, over two minutes at 100% CPU with no observed exit — when both the path and the guide have a sharp corner at a matching point; CurvilinearEquivalence = false avoids the hang but throws a C++ exception instead, a different failure mode, not a fix. A guide against a path with no corner at all hits neither, so path is restricted to exactly one segment whenever a guide is present — SweepError::GuidePathMustBeStraight, refused before the façade is ever called, never left for OCCT to hang on. The guide chain itself may still bend. A second and third measured exclusion, matching the first spike's own caution: a guide combined with scale_end != 1.0 or with keep_orientation was measured to build something — no crash, no hang — but a shape unlike either input alone, with no OCCT documentation describing what the combination is even supposed to mean; both are refused (GuideConflictsWithScale, GuideConflictsWithOrientation) rather than shipped unverified, at both the document layer (checked eagerly, by SetSweepGuide itself, since — unlike the path-corner interaction SetSweepScale/ SetSweepOrientation defer to compile — none of these three checks need solved geometry) and the façade (defense-in-depth against a forged save). A fourth exclusion, found while writing this step's own tests rather than anticipated: a guide held EXACTLY PARALLEL to a straight path — any constant offset, not only one at the profile's own boundary — reliably fails too (SelfIntersecting or kOperationFailed depending on the offset), while a guide with any divergence or convergence, however slight, builds cleanly; the existing BRepCheck_Analyzer/ BRepAlgoAPI_Check gates already catch it, so no new façade code was needed, only a test recording the finding. Translation invariance was probed and holds exactly (a sweep and its guide both shifted by the same vector produce a solid shifted identically, volume unchanged to 9 significant figures). No UI gesture reaches it, and unlike orientation this is not close to one: a guide is a THIRD picked chain, and every sweep slot today — the create session, the value card — holds exactly two (profile, path). Named as SetSweepGuide's own Undecided entry in main.rs's reachability check, the same treatment SetSweepOrientation already has.

Still absent: twist. Not merely unwired — it is not buildable the way scale, orientation and guide rails were. This OCCT version's Law_Linear/SetLaw is homothetic (scale) only; there is no law-based twist mechanism. The only path is manually constructing N pre-rotated intermediate sections via repeated Add() calls, which needs its own accuracy spike (no obviously-correct section count) and has an unverified effect on the sweep's Bridged{profile curve, path segment} face keying, which assumes exactly one profile curve × one path segment — multiple intermediate sections within one segment may not still produce that. Needs its own spike and possibly its own NAMING.md amendment before a real plan exists — scoped as its own dedicated future investigation, deliberately not attempted opportunistically alongside guide rails.

Note the dependency. The XZ-plane pinning half of this entry is entry 1 wearing a different hat. Sketch planes retire it.


Cross-cutting

12. Measurement now covers volume, area, centroid, bounding box, edge length and point distance. — SUBSTANTIALLY RESOLVED

Evidence this closed. Most of what this entry asked for already existed internally — FeatureEvaluation.summary.geometry had volume/area/centroid since Phase 1, used for golden-file tests, just never exposed as a user-facing measurement. Nothing selected now reports the whole body's volume, area, centroid and bounding box in one readout; two picked sketch points report their plane-native distance; two faces sharing exactly one edge report that edge's length and, if it's circular, its radius, alongside the existing distance/angle. A real bug was caught before shipping: BRepBndLib::Add bakes each face's own geometric tolerance into a bounding box on purpose (a 30 mm box measured [-1e-7, 30.0000001]) — fixed with AddOptimal(..., useShapeTolerance=false).

Deliberately still absent. Mass — there is no density model, and OCCT's own "mass" is unit-density volume, so exposing it would be a duplicate number, not a new one. Body vertex-to-vertex distance — no vertex-picking UI gesture exists, only face-level ID-buffer picking, and sketch points already cover the realistic case. Measuring to a sketch CURVE (rather than a point) — not attempted.

Rank. No longer "low" in the sense of unaddressed — this is the entry closest to fully done of any that were partial. What's left is genuinely low-priority: mass needs a whole material/density concept nothing else in the app has, and curve measurement is a small addition once someone hits it in real use.

13. Imported STEP geometry cannot enter the document. — ABSENT

Evidence. import_step exists in the FFI and in occt.rs, and there is no FeaturePayload variant that carries an imported shape. So a STEP file can be read but not referenced by a feature.

Rank. Lowest here, and flagged rather than pressed: modelling around a bought part is single-body reference work, not assembly, so it is in scope — but it is also the entry most likely to drag scope, and it should not be built before L1.


Noted for the boolean layer, not a Fusion gap (M2-4, 2026-09-06)

Fuzzy booleans inflate OCCT result tolerances, and bbox_diagonal (NAMING.md §5's scale D) is tolerance-inclusive (BRepBndLib::Add): it grew 1.6× the fuzzy value, which is what set FUZZY_FRACTION's ceiling. Worth a façade switch to a tolerance-free box (BRepBndLib::AddOptimal with useShapeTolerance = false) before any larger fuzzy is ever wanted. And the m3 bracket's solved mirror plane — tilted ~1.5e-10 rad, enough to miss Precision::Angular() by 150× — shows a fixture "authored elsewhere, locked onto the plane" is a near-coincidence generator in its own right: the caps unify, the walls do not, and no linear tolerance changes that.

14. There are no variables and no expressions. Every dimension is a literal. — DEFERRED TO M6, MEASURED

What is absent. A dimension is an f64 in the document and nothing else: there is no named parameter, no expression, no = wall * 2, no table of driving values. A part whose wall thickness appears in nine dimensions is nine numbers to retype, and a design intent that says "these two are always equal" can only be written as a constraint between geometry, never between NUMBERS.

Why it is not in M3, and the measurement rather than the impression. It was sized during M3's planning and came out XL on its own: a Value that may be a literal or an expression touches every dimension-carrying payload (extrude height, revolve angle, every SketchConstraint that holds a number, fillet radius, shell thickness, draft angle, pattern count and spacing, hole diameter and depth), needs an expression parser and an evaluation order with a cycle guard, needs the solver to be handed the EVALUATED numbers while the document keeps the unevaluated ones, and needs the UI to show both. It is also a second break of the save format inside one milestone — M3 already spends version 24 on construction geometry and SketchPlane::Angled — and the migration ladder M5-1 owes has not landed, so a second break would be a second unmigrated file generation.

The decision (2026-09-06): M6, after the migration ladder. Recorded here rather than left as a hole in the list, because "no variables" is the single most visible difference between this and any parametric CAD a person has used, and its absence should be a dated decision rather than an oversight.

The click snap subsumes two inference rules (M3-1, 2026-09-07)

DRAW_SNAP_MM is 2 mm and the inference distance tolerance is 0.5 mm, so a click near enough to infer Coincident or Concentric has ALREADY reused the existing point: the two curves share one point entity and the relation holds by identity, with nothing left to constrain. Both rules remain reachable through a drag or a solve, and the hover glyph is unaffected. Two of M3 lane A's own tests were asserting those rules through a gesture that cannot exercise them, and were green on a constraint that made the sketch Redundant.

Not a Fusion gap and not a defect — a tolerance relationship nobody chose. Whether the two numbers should be RELATED (one derived from the other, or the snap stated in terms of the inference tolerance) is an open question for whoever tunes snapping at dogfooding time; it is recorded here so the answer is not re-derived from scratch the first time an inference looks like it did nothing.

What this list is not

It contains no assemblies, joints, drawings or multi-body work, and no CAM, simulation, generative, cloud, rendering, sheet metal or surfacing beyond loft and sweep. Those are decided, and a gap list that re-raised them would be arguing with the brief rather than serving it.

It is also not a list of everything Fusion has. It is what Fusion and Shapr3D express, within this project's scope, that this code cannot.

The ordering claim that used to close this file is retired. It said the first three entries were the ones standing between here and a bike light mount. All three are resolved at the document layer, so that sentence had been false since 2026-08-29 and survived the same day's correction pass. What stands between here and a real part is the reachability list near the top of this file, not the first three rung entries.