65 KiB
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.
-
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.
-
The sketch plane picker reaches two of five frames.
Edit::NewSketchtakeson_face: Option<u64>and nothing else, so the app producesFaceorWorld(Xy).World(Yz),World(Zx)andOffsetexist, 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 invernier-app.Action::SetMirrorPlanedispatches onlyMirrorPlane::Principal, which is documented as "not parametric — a recorded deviation". Both the CLI selftest andvernier-uibuild SketchLine planes, andMirrorPlaneisDeserialize, 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. -
Entry 6's trim, extend and split have no gesture, blocked on a real design question rather than on work.
-
Entry 11's sweep orientation and guide rails have no gesture.
-
Entry 10's recommended workaround is not available in the app. It names "the existing
AddLoft/SetLoftSectionscommands", andSetLoftSectionsis onNOT_YET. -
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-apphas 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. -
Three of entry 4's fillet and chamfer modes have no gesture, and the UI DESTROYS them on edit.
FilletRadius::Variable,ChamferMethod::TwoDistancesandChamferMethod::DistanceAngleare built and tested (fillet-variable-radius-editandchamfer-two-distance-editare selftest checks of their own), and the app constructs none of them:Edit::SetFilletSpeconly ever writesFilletRadius::ConstantorChamferMethod::Equal(vernier-app/src/main.rs:3362-3364). Worse than unreachable —main.rs:2564reads 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:2564now reportsricheralongside the first number, andedit_forREFUSESAction::SetFilletSpecfor 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 featurer2.00+, because without the marker it reads as an ordinaryr2.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. -
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.