Switching to a Layout re-tessellated the whole model — `memo_miss=186455`,
2034 ms — to arrive at geometry identical to what was already in the memo.
The only thing that differed was colour.
`local_wires_for` already knows this: it stores the raw colour and drops
its background argument with `let _ = bg_color;`, noting that
`Batches::finalize` applies `adapt_to_bg` last, "so the cache no longer
has to rebuild on BACKGROUND / layout-switch". The tessellation memo's
guard had not been told, and still mixed the background in, so every
entry was discarded on a switch.
`finalize` now records what its colour resolution consumed — the raw
colour, the raw glyph colours, and the three flags it branches on — as
`WireModel::bg_adapt`, and `resolve_colors_for_bg` replays it for another
background. The guard drops the background, so a memo entry serves any
layout.
The resolution recomputes from the recorded raw colour rather than from
the resolved one, which makes it idempotent and therefore safe over a set
that mixes memo hits with freshly tessellated misses. Re-adapting the
resolved colour would not be safe: `adapt_to_bg` snaps a near-white colour
to pure black on a light background, and pure black comes back pure
*white* on a dark one, losing the tint. A test pins that asymmetry so the
raw colour is not optimised away later.
The contract is tested against the real `finalize` rather than a copy of
its logic: for every background pair, building under one and resolving for
the other lands exactly where building under the other would have.
`bg_adapt` is `None` for wires built anywhere else, and the resolution
skips them, so nothing outside the block cache changes colour. It is also
`None` when the resolution would be a no-op — a preserved colour with no
canvas fill looks the same under every background — which keeps the common
case at no cost.
Ordering is unchanged: the pass runs inside the builder, before
`append_scene_markers` and `apply_refedit_fade`, both of which have their
own separate background dependency.
`wires-build` gains `colors=` so the pass has a number of its own.
Co-Authored-By: Claude Opus 5 <[email protected]>
Switching to a Layout stops the main thread long enough that the
compositor decides the application has stopped responding and offers to
kill it. The switch itself is not the problem — `layout-switch` reports
about 10 ms — it is the frame after it, which rebuilds the whole wire set.
That frame now has a cheap one in front of it. `layout_settling` is set
when the switch takes, and while it holds, the viewport is replaced by a
centered "Preparing layout…" on the canvas background. No shader widget
means no scene primitive, so `prepare` has nothing to do and the frame
costs nothing. A `window::frames` subscription clears the flag, which is
the guarantee this needs: that callback runs once a frame has actually
been presented, so the notice is on screen and the compositor has been
answered before the expensive frame starts.
The deferral is of the *frame*, not the switch. Two callers depend on the
switch having completed when it returns — one calls `fit_all` immediately
after, another compares `current_layout` — so deferring the switch itself
would have broken both.
This restarts the compositor's timer immediately before the slow frame and
replaces a stale window with something that says what is happening. It
does not make the slow frame fast: if the rebuild still exceeds the
compositor's timeout the dialog can return, and the durable fix is the
2 s `memo_miss` behind it, which is a separate change.
Only `en-US` is validated against at build time; Greek is filled in too
since it is in use here, and the rest fall back until translated.
Co-Authored-By: Claude Opus 5 <[email protected]>
`view-detail` located the first paper frame's cost precisely: 7826.9 of
7832.8 ms in `grid`. Once, on the first frame after a layout switch —
every other frame in a 2769-frame session reports it at 0.0.
That is the largest single cost in a layout switch, larger than the model
re-tessellation and the arena rebuild combined, and it is not geometry
work at all. `grid_overlay` itself only wraps a canvas; `grid_segments`
runs later, in `draw`. So the time is in `grid_views` or in the per-view
parameter build, and reading the two does not settle which: both reach
helpers that can fall back to a full document walk, and neither obviously
fires for this drawing.
`grid-detail views= params= bg= n=` splits it, and reports only when the
block costs at least a millisecond so the quiet frames stay quiet.
Co-Authored-By: Claude Opus 5 <[email protected]>
The first Model→Layout switch costs ~9.2 s, and the largest single piece
of it is `view` at 5810 ms — iced widget-tree construction, not the scene.
The scene work that follows it in `prepare` (two `wires-build` calls plus
`frame-prepare`) accounts for 3377 ms, and the log's ordering shows the
two are sequential, not nested: `view` prints before any of them.
`view` being one number could not say which part of a 1950-line
`view_main` that is. It now reports eight sections — viewport_3d,
model_input, grid, selection_overlay, viewport_mouse, viewport_stack,
active_vp_rect, chrome — plus the preamble as `head`.
The marks go through a closure over a `RefCell` rather than one binding
per section: two of the sections sit inside a nested block, where
per-section bindings are out of scope by the time the report runs.
Co-Authored-By: Claude Opus 5 <[email protected]>
Reuse the application library from the binary entry point instead of compiling the module tree twice. Keep desktop-only transports and caches out of web builds, remove unused test code, and check executable targets in CI.
The first hover over the canvas after opening a large drawing froze the
UI for a second:
hover-detail query=0.2 handles=986.1 ... candidates=253
hover-dwell 986.6ms
`interaction_handle_index` and the insert-hatch map it reads are built
lazily, and first use is that hover. Neither can move to the loader
thread: they read `hatches`, `meshes` and `block_meshes`, which are
installed on the real `Scene` only after the loader has finished.
Build them at the end of the open instead, where the user is already
waiting. This does not make the work cheaper -- open grows by roughly the
second the hover used to cost -- but a second inside an operation that is
already seconds long reads very differently from a second-long freeze on
a mouse move.
Co-Authored-By: Claude Opus 5 <[email protected]>
- Add 'i ' prefix to EntryKind::Info and coordinate set_step_prompt deduplication to satisfy WCAG 1.4.1 across all 22 themes
- Drop unused _desk parameter from crosshair_background and clean up its call sites
- Document AutoCAD-standard context-based crosshair drafting surface architecture in crosshair_background
- Add doc comments documenting deliberate alpha omission in canvas_is_light and backlog scope in is_wide
- Add dropdown_popup_width helper in common.rs replacing magic formula heuristics
- Implement is_wide range table to count East Asian Wide/Fullwidth (CJK) glyphs double for localized strings
- Calibrate unit scale factor to 0.68 * font_size preserving existing width behavior at font size 11
- Apply shared calculation across viewport controls and ribbon dropdowns
- Update unit tests to verify width calculation on standard and CJK labels
- Raise accessible_accent default threshold from 2.0 to 3.0 (WCAG 1.4.11 UI state indicators)
- Raise EntryKind::Info command line history threshold from 3.0 to 4.5 (WCAG 1.4.3 normal text)
- Update themed_check_cell to verify >= 3.0:1 contrast across both weak popup panel and strong highlighted rows
- Replace fully-qualified accessible_accent_threshold calls inside closures with use imports
- Add comprehensive 9-suite theme accessibility contrast test suite covering all 22 themes across core surfaces
- Add canvas_is_light helper based on WCAG luminance (> 0.5)
- Ensure crosshair_background always returns sheet white in paper space
- Update resolve_selection_base_color to resolve user ACI -> light-canvas palette -> theme palette
- Add light_canvas_color with curated deep-green/deep-blue pairs for light surfaces
- Extract selection_fill_alpha with 1.35x contrast compensation on light surfaces
- Thread canvas_bg through draw_marquee and poly selection
- Note deliberate limitation for desk-spanning marquee edge case
- Add test coverage for paper space, ClassicDark, custom light bg, and fill alpha clamping
Everything measured so far accounts for about 1 ms of a ~120 ms pan
frame: update is 5-28 ms at worst and usually far less, prepare 0.45 ms,
encode 0.5 ms. Meanwhile the GPU idles at 0-8%, encoding no scene at all
leaves latency unchanged, major page faults stay flat at 17 across forty
seconds of panning, and freeing 6 GB of swap changes nothing either.
So the time is in code that has never been timed. `view()` runs between
the input handler returning and `prepare` being called, rebuilds the
whole widget tree every frame, and is the largest unmeasured span in
that gap.
Reports only when it exceeds 1 ms, so a healthy frame stays quiet.
Co-Authored-By: Claude Opus 5 <[email protected]>
Pan and even plain cursor movement over the canvas ran at ~8 fps on a
large drawing. Nothing rendered was responsible: the GPU idled at 0-8%,
encoding no scene at all changed nothing, and vertices, pixels and draw
calls could each be cut without moving the number. `view` was the frame
-- 110 ms of it against 3-6 ms on a small drawing -- and inside `view`
it was the status bar.
The status bar builds the selection-filter menu, which asks for the
distinct entity type names in the layout. That call cloned the layout's
entire handle list (1 166 357 handles, ~9 MB), then did a map lookup, a
String allocation and a BTreeSet insert for each one. About 107 ms of a
110 ms frame, to produce roughly twenty names, for a menu that is only
visible when opened.
Cache the result against geometry_epoch and the layout block, since it
only changes when entities are added or removed, and make the scan
itself borrow the handle list rather than clone it and collect &str,
allocating only for the names it returns.
The immediate-mode UI rebuilds every widget on every mouse move, which is
fine as long as nothing in that path is O(document). This was.
Co-Authored-By: Claude Opus 5 <[email protected]>
Refresh stale depth bounds after geometry edits, allow orbiting through camera poles, and preserve hidden-line occlusion for selected solids.\n\nFixes #957
Model space appearance now follows the active UI theme instead of a
hardcoded dark background. Canvas mode is user-selectable (Match Theme /
Classic CAD Dark / Custom RGB), with matching controls for the paper
sheet and desk surround backgrounds and grid opacity.
Implements industry-standard CAD system variables, all settable via
command name, SETVAR, and the Options dialog:
- COLORTHEME (0/1 dark/light UI scheme, preserves custom palettes)
- COLORSCHEME (interactive named-theme selection)
- BACKGROUND (THEME/CLASSIC/DEFAULT/DESK presets, RGB triplets)
- SELECTIONAREA, SELECTIONAREAOPACITY (translucent marquee fill)
- WINDOWSAREACOLOR/WINDOWAREACOLOR, CROSSINGAREACOLOR (ACI or theme)
- SELECTIONEFFECT, SELECTIONEFFECTCOLOR (GPU highlight pass, driven by
selection_generation invalidation — no retessellation)
- SELECTIONPREVIEW (bit 0 = idle rollover, bit 1 = in-command hover)
- GRIPSIZE, GRIPCOLOR, GRIPHOT, GRIPHOVER (grip overlay styling)
parse_power accepted a single exponent (`unary ('^' unary)?`), so a
chained power stopped after the first `^`. The remaining `^2` was left
unconsumed, and eval_to_string's embedded-expression scan then handed it
back as literal text: typing `2^3^2` into a coordinate or distance field
produced the string "8^2" rather than 512, and `2^3^2+1" produced "8^3".
Recurse into parse_power for the exponent so `2^3^2` reads as
`2^(3^2)", the standard right-associative meaning, and the whole
expression is consumed.