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 TemporaryBRepManager; the user’s data was never touched

Probes

p13_ui_smoke, p14a_dialog_build + p14b_dialog_verify, p15_preview_visibility

1. Add-in lifecycle (FR-9.1)

Claim

Verdict

start() creates the panel, the command definition and both toolbar controls

CONFIRMED

Calling start() twice — what a reload does — leaves exactly one of each

CONFIRMED

stop() removes the panel, the command and both controls

CONFIRMED

addButtonDefinition(id, name, tooltip, "") accepts an empty resource folder

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 panelSolidCreatePanel, 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

require_single_host_face refuses rather than choosing

CONFIRMED (AmbiguousHostFaceError)

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 +Z

100 mm

100.000000000 mm

100.000000000 mm

untrimmed, tilted (1,1,1)

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) * radius agreed with the evaluated points to 0.000000000 mm on all five faces, including the one with isParamReversed = 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 that isParamReversed varies between faces of one design. This is the corpus’s first measurement of a face with isParamReversed = True; the cone residual in PRD 6.0 remains open, as that is a different surface type.

  • getPointAtParameter returned ok = True at a parametric extreme where isParameterOnFace was False (the trimmed coped face). That is the documented behavior — ok is 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 one v.

  • 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

(0, 0, +1) — away from the cut at z = 0, as required

unrolled x span

89.849549893 mm

2πr

89.849549893 mm

Tier A closure error

0.000000000 mm

FR-3.3: template x of the deepest point of the cut

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:

  1. 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.

  2. 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.

  3. 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

CommandDefinition.execute() from the MCP script endpoint

commandCreated never dispatched (dispatched = False)

the same call deferred through a registered CustomEvent

the custom event handler never fired (event_fired = False)

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.SelectionFilters is 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 an AttributeError at import rather than a silent dead button. Control: SelectionFilters.CylindricalFace (singular) and SelectionFilters.SolidEdges are both absent, so the lookup discriminates.

  • Every other API name the handler touches was checked against the runtime — seven CommandInputs factories, five Command events, thirteen further members (addSelectionFilter, setSelectionLimits, clearSelection, addSelection, formattedText, isExpanded, ValueInput.createByReal, createFileDialog, TextListDropDownStyle, DialogOK, DialogYes, YesNoButtonType, QuestionIconType) and seven CustomGraphics members. All present. These are now tests/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

CustomGraphicsCoordinates.create(flat_list)

CONFIRMED

group.addLines(coords, [], True, [n]) returns CustomGraphicsLines

CONFIRMED

addLines with several strip lengths in one call, e.g. [2, 2]

CONFIRMED

lines.weight settable

CONFIRMED

lines.lineStylePattern = centerLineStylePattern

CONFIRMED (reads back as 1)

lines.color = CustomGraphicsSolidColorEffect.create(Color.create(...))

CONFIRMED

group.deleteMe() returns the component’s group count to its previous value

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

CustomGraphicsEntity.depthPriority is readable and settable

CONFIRMED

reads 0 on all four entities as drawn; accepts 1

Raising depthPriority improves an on-surface preview

REFUTED

image 2 is indistinguishable from image 1

The group leaves nothing behind

CONFIRMED

image 3 (control) shows the bare body; customGraphicsGroups.count == 0

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.