# 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 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** | | `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.