L4 live-UI record — Fusion 2704.1.53 — 2026-09-06¶
Milestone M2, step 5 (the command dialog and the CustomGraphics preview). Companion to
L4-2704.1.53-2026-09-06.md, which covers the adapter. Every figure below came from a probe
run through tools/fusion_exec.py; none is inferred, and the negative results are recorded
in as much detail as the positive ones.
Fusion version |
2704.1.53 |
Python |
3.14.0 (Fusion’s bundled interpreter) |
Platform |
macOS x86_64 |
Documents opened |
one scratch design per probe, closed without saving; 0 open at exit |
Bodies |
all from |
Probes |
|
1. Add-in lifecycle (FR-9.1)¶
Claim |
Verdict |
|---|---|
|
CONFIRMED |
Calling |
CONFIRMED |
|
CONFIRMED |
|
CONFIRMED |
FR-9.1’s stated location does not exist on this build. FR-9.1 asks for a button in “the
Solid tab (Utilities panel or a dedicated panel)”. Measured: the SolidTab (display name
SOLID) has ten panels and none of them is a Utilities panel — SolidCreatePanel,
SolidModifyPanel, AssemblePanel, ConfigurePanel, ConstructionPanel, InspectPanel,
InsertPanel, PartAssemblePanel, SelectPanel, SnapshotPanel. What Fusion calls
UTILITIES is a tab, ToolsTab, and it holds SolidScriptsAddinsPanel (display name
“Add-ins”).
The add-in therefore takes FR-9.1’s other option — a dedicated panel — creating
MightyMiterPanel in SolidTab, and adds its second entry to SolidScriptsAddinsPanel.
Both anchors are asserted in tests/live/test_ui_api_shapes.py so a renamed tab id fails
loudly rather than leaving the add-in with no button.
2. Host-face candidates on the reference joint (FR-1.5, PRD 7.13)¶
The cope edge of a 28.6 mm tube cut by a 32 mm tube at 60°:
1. - D 28.60 mm, 7739 mm2, 92.9 mm long, centered at 3.6 mm
2. - D 32.00 mm, 858 mm2, 33.0 mm long, centered at 9.2 mm
Claim |
Verdict |
|---|---|
The cope edge bounds two cylindrical faces |
CONFIRMED |
|
CONFIRMED ( |
Note the empty component names in that listing: that is the finding below, caught by this probe and fixed.
TemporaryBRepManager bodies report name == "", not None. The candidate label read
face.body.name if face.body else "unnamed body", which passes the truthiness test on
body and hands the empty string through, so the listing lost the component FR-1.5
requires. Every fixture in the corpus is a temporary body, so this is the normal case under
test and the rare one in production — the shape of bug that survives a test suite. Fixed by
ui.command._body_label.
3. Axial extent of a face — and why the obvious method is unusable¶
axial_extent_mm needs the axial reach of a cylindrical face for FR-2.4’s straight-run
readout and FR-1.5’s candidate labels. Two candidate methods, measured on five faces:
Face |
true length |
evaluated parametric extremes |
world bounding box |
|---|---|---|---|
untrimmed, axis-aligned |
100 mm |
100.000000000 mm |
100.000000000 mm |
untrimmed, tilted |
100 mm |
100.000000000 mm |
140.446507884 mm |
untrimmed, 7 mm long |
7 mm |
7.000000000 mm |
7.000000000 mm |
trimmed coped host, r = 14.3 mm |
— |
92.850291271 mm |
— |
trimmed cutting tool, r = 16 mm |
— |
33.024435398 mm |
— |
The world-axis-aligned bounding box over-reports a tilted tube by 40 %, 4045× the FR-4.1 budget, because a world-aligned box around a tilted cylinder is not a box around the cylinder. The implementation evaluates the two parametric extremes and projects the resulting points onto the axis.
Two further facts from the same run:
(umax - umin) * radiusagreed with the evaluated points to 0.000000000 mm on all five faces, including the one withisParamReversed = True(the r = 16 mm cutting-tool face). It is still not used: evaluating points is immune to the parameterization’s direction by construction, and CLAUDE.md section 6 records thatisParamReversedvaries between faces of one design. This is the corpus’s first measurement of a face withisParamReversed = True; the cone residual in PRD 6.0 remains open, as that is a different surface type.getPointAtParameterreturnedok = Trueat a parametric extreme whereisParameterOnFacewasFalse(the trimmed coped face). That is the documented behavior —okis not a domain check — and it is what makes the method correct here: the extent wanted is that of the parametric box, not of the trimmed region at onev.The trimmed host face reported
v ∈ [−π, +π]where its untrimmed twin reported[0, 2π], independently reproducing PRD 7.14 for the third time.
4. The pipeline, run through the product’s own _solve¶
Quantity |
Measured |
|---|---|
host radius |
14.300000000 mm |
outward axis |
|
unrolled |
89.849549893 mm |
|
89.849549893 mm |
Tier A closure error |
0.000000000 mm |
FR-3.3: template |
0.000000000 mm |
FR-3.4: axial offset of the tip of the cut |
0.000000000 mm |
PRD 11.9(c) reproduced a second time, now through the UI layer’s own code path rather than the adapter’s.
A limitation of the default datum, measured and not fixed. The datum reference came out
as (+0.999933, −0.011554, 0) rather than exactly +X, because FR-3.3’s “deepest point” is
taken from the stroked samples, and the nearest sample sits up to half a spacing from the
true extremum. Here that is 0.662°, 0.165 mm of arc at r = 14.3 mm. The consequence is
that the default datum moves slightly when the tessellation tolerance changes; the template
is internally consistent regardless, since everything on it is measured from that datum.
It matters only for clocking two templates to each other, which FR-3.3 explicitly says the
default is not a substitute for. Fitting a parabola through the three samples around the
extremum would make it tolerance-independent to second order; that is a candidate for M3
when “clock to” arrives, not a defect in M2.
5. Preview performance against FR-9.6’s 300 ms budget¶
Measured inside Fusion’s own interpreter, which is the only place the figure means
anything (PRD 11.12: ~5× slower than stock CPython). The clock covers the whole
executePreview path — stroke, solve, unroll, build, draw.
15 previews across five probe runs, every one drawing the identical 151 curve points, 36 major + 36 minor ticks, 223 primitives:
Run |
Attempt 1 |
Attempt 2 |
Attempt 3 |
|---|---|---|---|
1 |
55.04 ms |
51.19 ms |
54.05 ms |
2 |
— |
— |
91.22 ms (worst) |
3 |
88.94 ms |
50.88 ms |
52.82 ms |
4 |
8.57 ms |
7.41 ms |
6.97 ms |
5 |
56.28 ms |
52.12 ms |
48.28 ms |
Best 6.97 ms, typical ~52 ms, worst 91.22 ms — 30.4 % of the 300 ms budget.
The 13× spread deserves a sentence, because the first run of this measurement was reported as “55.04 ms, 18.3 % of budget” from a single set of three attempts, and that was the wrong number to quote. The work is bit-for-bit identical on every one of the 15 previews — same edge, same tolerance, same 223 primitives — so the variance is Fusion-side, not ours. A budget conclusion therefore has to rest on the worst observed case, which still leaves 69 % of FR-9.6’s budget unspent. A run that happens to land at 6.97 ms proves nothing.
Everything drawn lay on the host cylinder to 0.000000000 mm, and the graphics group count
returned to its starting value after deleteMe().
6. getStrokes is not monotonic in its tolerance — PRD 11.7’s reading is corrected¶
The preview was first written to stroke coarsely, on PRD 11.7’s evidence that a looser tolerance returns fewer points. Measured on the reference joint’s cope edge:
tolerance (mm) |
tolerance (cm) |
points |
max chordal sag (mm) |
sag / tolerance |
|---|---|---|---|---|
1.0 |
0.1 |
44 |
0.088881487 |
0.089 |
0.5 |
0.05 |
65 |
0.047007535 |
0.094 |
0.1 |
0.01 |
151 |
0.009655191 |
0.097 |
0.05 |
0.005 |
65 |
0.047007535 |
0.940 |
0.01 |
0.001 |
151 |
0.009655191 |
0.966 |
0.005 |
0.0005 |
192 |
0.004781183 |
0.956 |
0.001 |
0.0001 |
456 |
0.000815806 |
0.816 |
Three conclusions, in order of how much they matter:
The tolerance is always honored. The achieved chordal sag never exceeded the requested tolerance at any value tried. FR-4.1’s accuracy claim rests on this, and it holds. The sag is measured as
r − |midpoint|, the amount a chord between two points on a cylinder falls inside it — computable without knowing the intersection curve’s closed form.The point count is not monotonic in the tolerance. Tightening from 0.1 mm to 0.05 mm reduces the count from 151 to 65. Fusion offers a discrete ladder of refinement levels and picks one by a rule that is not “the coarsest that fits”; a coarser request can land on a finer rung. PRD 11.7’s “floor on refinement” reading is therefore wrong — a floor is monotonic, and this is not. What PRD 11.7 actually observed was two requests landing on the same rung.
The count says nothing about accuracy, and neither does it about work. Any reasoning from point counts — including the preview’s original “coarse stroking is cheaper” — is unsound. The preview now strokes at the same tolerance as the export, which is what the 18.3 % budget figure above makes affordable, and it no longer claims to be approximate when it is not.
Consumed budget at the default tolerance. FR-4.4’s default of 0.01 mm achieves a 0.009655 mm sag, which is 96.6 % of FR-4.1’s 0.01 mm template budget. The unrolling contributes 0.000000000 mm, so essentially the whole budget goes to tessellation. Tightening the default to 0.005 mm would halve it for 27 % more points. The PRD names 0.01 mm explicitly, so this is recorded for a decision rather than changed.
7. What could NOT be verified, and why¶
The command dialog cannot be executed from the development loop.
ui.command._CommandCreatedHandler runs only inside Fusion’s own command dispatch, and two
ways of reaching that dispatch were tried and measured:
Mechanism |
Result |
|---|---|
|
|
the same call deferred through a registered |
the custom event handler never fired ( |
Fusion additionally swallows any exception escaping an event handler, so a broken dialog
leaves no traceback anywhere: the symptom is a toolbar button that does nothing. A first
attempt at this probe also produced a verdict it could not interpret, because the observing
run purged mighty_miter from sys.modules and thereby dropped the last strong reference
to the handler — the very garbage-collection hazard ui/command.py documents.
Three things were done rather than accepting the gap:
The selection filter strings are no longer literals.
adsk.core.SelectionFiltersis backed by the C++ runtime, and the values were read from it:Edges == "Edges",CylindricalFaces == "CylindricalFaces". The dialog now uses those constants, so a typo is anAttributeErrorat import rather than a silent dead button. Control:SelectionFilters.CylindricalFace(singular) andSelectionFilters.SolidEdgesare both absent, so the lookup discriminates.Every other API name the handler touches was checked against the runtime — seven
CommandInputsfactories, fiveCommandevents, thirteen further members (addSelectionFilter,setSelectionLimits,clearSelection,addSelection,formattedText,isExpanded,ValueInput.createByReal,createFileDialog,TextListDropDownStyle,DialogOK,DialogYes,YesNoButtonType,QuestionIconType) and sevenCustomGraphicsmembers. All present. These are nowtests/live/test_ui_api_shapes.py.What remains unverified is stated rather than papered over: the argument shapes of those seven factory calls, and the behavior of the five event handlers. The only way to close it is to load the add-in in Fusion and click the button — a manual step, of the same kind as the L5 gates.
pytest is absent from Fusion’s bundled interpreter (ImportError: No module named 'pytest'), so nothing in tests/live/ is executed by pytest on this machine. Those modules
are the tripwire’s specification; the probes are what execute the same facts today.
8. CustomGraphics shapes (PRD 11.5 V8, re-confirmed by execution)¶
Claim |
Verdict |
|---|---|
|
CONFIRMED |
|
CONFIRMED |
|
CONFIRMED |
|
CONFIRMED |
|
CONFIRMED (reads back as |
|
CONFIRMED |
|
CONFIRMED |
9. New stub/runtime divergence¶
Workspace.productType raises RuntimeError: 3 — “No product type is associated to
this workspace object yet. Please get workspace object by
UserInterface::workspacesByProductType()” — for at least one workspace reached by
iterating ui.workspaces. The property is declared readable. This joins CLAUDE.md
section 5’s list; nothing in the add-in reads it, and the add-in resolves its workspace by
id (FusionSolidEnvironment) instead.
10. Session hygiene¶
documents open at exit: 0 on every probe, verified by p04b_state.py before and after.
All bodies temporary; the sandbox project holds 0 data files; the toolbar was left clean
(allToolbarPanels.itemById("MightyMiterPanel") is None).
11. The first run by hand — and a coverage gap in section 5¶
Section 5 measured the preview path and declared it good. It was measured in an empty
scratch document, where nothing can occlude a line, and what it actually established was
that a CustomGraphicsGroup is created, styled and deleted. Whether the user can see
anything was never in evidence. When the add-in was loaded and driven by hand for the first
time on 2026-09-06, on a head-tube / top-tube joint, no preview appeared.
p15_preview_visibility closes the gap the only way a visual question can be closed: it
adds a real BRepBody to the document, draws the preview on it, and saves the viewport with
Viewport.saveAsImageFile(path, 1400, 900) — three images, one per condition.
Claim |
Verdict |
Evidence |
|---|---|---|
The preview is visible when drawn coincident with a solid face |
CONFIRMED |
image 1: orange miter curve, blue datum ruling and 73 grey ticks all legible against the tube |
|
CONFIRMED |
reads |
Raising |
REFUTED |
image 2 is indistinguishable from image 1 |
The group leaves nothing behind |
CONFIRMED |
image 3 (control) shows the bare body; |
depthPriority was not adopted. It was the leading hypothesis for the invisible
preview — lines drawn exactly on a face tie in the depth buffer, and Fusion exposes the
property for that case — and the measurement refuted it. A change that buys nothing is
still a change someone has to defend later.
What the failure actually was is still open. The status box in the user’s screenshot
still held the message written by the previous input change, which means executePreview
raised before reaching its own status update and Fusion discarded the exception (CLAUDE.md
section 5; FR-10.2 forbids exactly this). Since the same code path was then run end-to-end
against a fixture with no error, the cause is specific to that design and cannot be
reproduced from the development loop.
The remedy is not a guess at the cause. inputChanged, validateInputs and
executePreview now each catch their own exceptions, log through FR-10.3’s rotating file
with the Fusion version, the add-in version and the OS, and write the failing line into the
status box. The next occurrence names itself.