`view-detail` on an edit frame:
total=105.6ms chrome=105.4 [every other section 0.0]
and the edit latency measurement puts ~102 ms of a 192 ms drawn point in
the gap between the handler returning and the scene primitive being built
— which is that same `chrome`.
The status bar asks for `entity_type_names_in_layout` on every frame to
populate the selection-filter menu, and the cache behind it was keyed on
`geometry_epoch`. Every drawn point bumps that epoch, so every drawn point
walked all 444 937 handles a model space owns to answer a question the
line did not change: adding a line to a drawing that already has lines
leaves the set of type names exactly as it was.
The cache now carries the set alongside the list, and folds edits in from
the delta journal — the same incremental shape `entity_index` already
uses. Additions can only grow the set, so they are applied directly and
the existing `Arc` is handed back untouched when nothing new appeared. A
removal can retire a type and cannot be resolved from a handle that is
already gone, so those still fall back to the full walk.
`entity_type_name` returns a literal for every arm but one — `Extended`
borrows its class name from the entity — so the set holds owned strings,
and the hot path tests `contains` before allocating.
Tested: a second line of an existing type must hand back the same `Arc`,
a new type must still appear, and the folded answer must equal what a
rebuild from scratch produces.
Co-Authored-By: Claude Opus 5 <[email protected]>
The first measurement showed four consecutive edits at ~192 ms each with
a handler cost of 0.55 ms. That is below the `update` line's 5 ms
threshold, so the log reports the latency but not what asked for it —
and whether a live command preview is bumping the geometry epoch on every
mouse move is exactly the question the numbers raise.
`nav-prepare` now carries the message label alongside the op.
Co-Authored-By: Claude Opus 5 <[email protected]>
`Table::get` normalises the name with `to_uppercase`, so every layer
lookup allocates a String and runs a Unicode pass.
`resident_entity_visible` did one per entity, and the selection calls it
once per candidate to look up a few hundred distinct layers.
The predicate splits so the layer can be passed in, and the selection
resolves each distinct name once into a small map keyed on the entity's
own layer string. Keys borrow from the document, so naming the lifetime on
`wires_for_block_culled` is what lets them outlive the closure call.
**Measured, and smaller than it looks.** On a 585 786-entity drawing with
37 826 candidates, A/B through `OCS_NO_LAYER_MEMO`:
with memo visible=55.1ms
without visible=57.1ms
3.5 %, not the order of magnitude the allocation count suggested. The
per-entity cost of that selection is dominated by traversing entities
scattered across the document arena, not by the work done at each one — a
resolve-only pass over the same handles measures 1.4 ms, and only because
it runs second, on a warm cache.
Kept because it is measured, cheap and correct, not because it is the
answer to the selection's cost. `OCS_NO_LAYER_MEMO` stays so the claim can
be rechecked on other drawings.
The keying is on the **raw** name while the table is case-insensitive, so
two spellings of one layer take two slots and both resolve to it — one
extra normalisation, never a different answer. Tested, along with the
predicate that reads it.
The early cheap checks moved into the layer-taking variant rather than
staying in the wrapper, so calling it directly cannot skip them.
`vis_index=` reports what `block_members` costs inside `visible`: 19.2 of
the 55 ms here, rebuilt by walking the document once per `geometry_epoch`
— which moves on every edit.
Co-Authored-By: Claude Opus 5 <[email protected]>
The first drawn point costs 1068 ms and `wires-build` owns 710 of it:
visible=402.5 build=148.4 [classify=18.4 hits=127.5] sort=41.9
memo_hit=186455 memo_miss=1 candidates=444938
The memo is doing its job. `visible` is not, and two things in that loop
could own the 402 ms: resolving each handle through the document's hash
index, or the predicate — which reaches `document.layers.get(&c.layer)`,
a **string-keyed** lookup, so a string hash per entity, 444 938 times.
They call for different fixes, so `visible_probe=` times a resolve-only
pass and the remainder is the predicate. The pass repeats the resolve
rather than reading the clock inside the loop, where the measurement would
cost more than the thing measured, and it runs under PERF only.
Co-Authored-By: Claude Opus 5 <[email protected]>
The open sequence computes it twice:
[perf] draw-depth 293.5ms entries=1166328 ← loader thread
…
[perf] draw-depth 302.0ms entries=1166328 ← main thread
[perf] update 1427.3ms message=other
`DrawDepthCache` is keyed on `geometry_epoch`, the loader builds it on its
temporary scene, and `PreparedOpenGeometry` carried the wire set, the
tessellation memo and the block definitions across — but not this. So the
receiving scene walked all 1 166 328 entities again to rebuild a map that
already existed, inside the message that finishes opening the file.
It travels on exactly the terms the block definitions already do: the
document is the loader's own and has not been touched between the loader
finishing and the install, so its order labels describe the receiving
scene exactly. Only the epoch is restamped.
Tested by handing a computed map back into a scene through
`install_prepared_open_geometry` and asserting both that it is served
as-is and that it equals what that scene recomputes from scratch — the
second assertion is the one that matters, since serving a map that does
not describe the document would put every entity at the wrong draw order.
Co-Authored-By: Claude Opus 5 <[email protected]>
Drawing a point in a large drawing feels slow, and nothing currently
measures the thing the user actually waits for. `update` times the message
handler, `wires-build` and friends time the rebuild, but the span between
them — press to the frame that shows the new geometry — has no number.
Navigation already has exactly that plumbing: `record_nav_perf` stamps an
instant when the input arrives, `prepare` and `encode` pick it up, and
`nav-prepare` / `nav-render` report `input`, `build`, `prepare` and
`elapsed` from the original event.
`NavPerfOp::Edit` reuses it. `update` snapshots `geometry_epoch` before
dispatch and stamps the sample when it moved, so any message that changed
geometry — a drawn point, a grip commit, an undo — reports its full
latency rather than only the handler's share.
Diagnostic only: `record_nav_perf` sets a pending sample and nothing else,
so no LOD or settle behaviour changes.
Nothing is optimised here on purpose. Four call sites feed the snapper
along different branches, and picking one to instrument before knowing
which dominates is how the earlier rounds of this work were wasted.
Co-Authored-By: Claude Opus 5 <[email protected]>
Suppress leading 0 feet notation when feet = 0 in architectural (LUNITS 4)
and engineering (LUNITS 3) formats. Values less than one foot should display
as inches only (e.g. 9 1/4 inches instead of 0 feet 9 1/4 inches).
- For Architectural: 9.25 inches now displays as 9 1/4 inch mark instead of 0 feet 9 1/4 inch mark
- For Engineering: 11.94 now displays as 11.9 inch mark instead of 0 feet 11.9 inch mark
- Proper feet notation is preserved for values >= 1 foot
- Updated tests to reflect correct expected values
Fixes issue 1066
Co-authored-by: Copilot <[email protected]>
Use SELECTOR_PAD_V (calculated from ROW_H and FONT_SZ) instead of
hardcoded [3, 6] padding for color selector buttons. This matches the
padding used by combo_box widgets for Lineweight and Linetype,
ensuring all three ByLayer dropdowns align vertically and have
consistent height.
Fixes#1047
Co-authored-by: Copilot <[email protected]>
Use atomic file operations (write to temp file, then rename) instead of
direct File::create() to avoid crashes when the target PDF is locked by
another process (e.g., open in PDF reader). The new write_pdf_atomically()
function ensures the file is either fully written or left unchanged, with
a clear error message if the file is in use.
Fixes#1062
Co-authored-by: Copilot <[email protected]>
- Multiverwijslijn → Multiverwijs Lijn (proper spacing)
- Multiline → Multilijn Tekst (better clarity)
- Unie → Samenvoegen (more accurate for boolean union)
- Aftrekken → Verschil (better term for boolean subtract)
These terms align better with Dutch CAD industry standards.
Co-authored-by: Copilot <[email protected]>
Replace Wrapping::WordOrGlyph with Wrapping::Word to prevent labels
like 'Dimensions' from breaking as 'Dimensio' + 'ns'. Word-only wrapping
ensures labels only wrap at word boundaries.
Fixes#1046
Co-authored-by: Copilot <[email protected]>
Handling a document-open Apple Event (a warm "Open With" while the app
is already running) silently promotes this process from
NSApplicationActivationPolicyAccessory to a normal foreground app —
confirmed with lsappinfo, not assumed: its own entry flips from
type="UIElement" to type="Foreground" the instant it handles a second
Apple Event (a cold launch's own first event doesn't trigger this).
That gave the launcher its own, contentless Dock icon sitting next to
the real editor's, both apparently "the same app" to the user but
actually two separate Dock entries.
Re-set Accessory explicitly right after relaying, in the one place
every open (cold fallback, document open, and reopen-while-running)
already funnels through.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Quitting the real GUI binary (Cmd+Q) leaves the launcher itself still
running — that's by design, so it can keep relaying opens for the
session. But reopening the app afterward (Dock icon, double-clicking
it in Finder again) reactivates that same long-lived launcher process
rather than triggering a fresh cold launch, since Launch Services
still considers the bundle running. Neither of the delegate methods
already handled covered that: applicationDidFinishLaunching: only
fires once, at the launcher's own original cold start, and
application:openURLs: only fires when a document was named. The net
effect: the Dock icon would appear, but no window ever showed.
applicationShouldHandleReopen:hasVisibleWindows: is exactly the
callback AppKit fires for this reactivation case. Reproduced and
verified the fix against the real sequence (cold launch, kill only the
GUI process to simulate Cmd+Q, reopen) rather than assuming it from
the delegate method's docs alone.
Co-Authored-By: Claude Sonnet 5 <[email protected]>