Commit Graph
81 Commits
Author SHA1 Message Date
Karim Jerbi 0dab3ec54a feat: add COMMANDLINEFADETIME sysvar for overlay history fade
Controls how long command-line history lines stay visible, in ms (0-60000, default 3000). Bare form stays active until a value or Esc.
2026-09-06 02:13:09 +01:00
Karim Jerbi a9fda66ac0 fix(ui): add non-color marker to CLI info entries and drop dead desk parameter
- 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
2026-09-04 16:26:13 +01:00
Karim Jerbi b53cd580f6 feat(ui): shared dropdown popup width calculation with wide-character support
- 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
2026-09-04 16:26:12 +01:00
Karim Jerbi a7e3cf3a6d fix(ui): enforce WCAG AA contrast thresholds across UI themes
- 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
2026-09-04 15:34:24 +01:00
Karim Jerbi 23a9884f90 fix(ui): canvas-aware selection colors and paper space detection
- 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
2026-09-04 15:28:25 +01:00
Karim Jerbi 886d5777d8 fix(ViewCube): WCS dropdown no longer appears to clip view cube
- enlarged render area for the viewcube container, and increased the gap between cube region and WCS dropdown
2026-09-04 10:21:33 +01:00
Karim Jerbi 688785a6cf feat(model-space): theme-aware model space canvas and CAD selection/grip sysvars
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)
2026-09-03 17:44:44 +01:00
Karim Jerbi 46190ccba1 refactor(selection): migrate Scene.selection to Arc<RefCell> to avoid per-frame deep clone
Scene.selection was Rc<RefCell<SelectionState>> (31 fields, ~1.6KB
with poly_points) cloned deep per view_main per pane via
(*sel_ref).clone() at view/mod.rs:760. Replace with
Arc<RefCell<SelectionState>> (Arc::new(RefCell::new(...)) at
scene/mod.rs:1436,1868), SelectionCanvas holds Arc<RefCell> and
selection_overlay takes Arc<RefCell> (overlay.rs:365,415),
view_main does Arc::clone(&tab.scene.selection) (atomic bump)
instead of deep clone. sel_ref stays borrowed for field reads
(vp_size etc.).

Bench (release, n=5000): SelectionState::clone 39-68ns (64pts) /
11-22ns (0pts) vs Arc::clone 7ns — 5-9x on the hot path.

Tests:
- selection_is_arc_not_refcell: asserts type_name contains Arc
- bench_selection_state_clone / bench_selection_arc_clone / bench_selection_state_clone_empty (ignored, release)
2026-09-02 22:14:32 +01:00
Karim Jerbi 50cb21e4d4 refactor(ribbon): make StyleContext borrowed to avoid 8 clones per view
Replace the 8 owned clones in StyleContext construction (4 Vec<String>
+ 4 String) at ribbon/mod.rs:568 and :1059 with borrowed refs.
StyleContext is now StyleContext<'a> with &'a [String]/&'a str
(widgets.rs:313), RenderCtx holds &'a StyleContext<'a> (:633),
and call sites pass &self.x instead of .clone(). The view-local
borrow is valid for the duration of Ribbon::view() and the style
dropdown overlay.

TDD: RED style_context_tests failed E0308 (owned vs borrowed),
GREEN added lifetime and updated 4 type sites + 2 construction sites.
2026-09-02 18:20:29 +01:00
Karim Jerbi bdedcf63fc refactor(style): dedup muted_style into ui::style::common
Centralize the 7 byte-identical `muted_style` helpers (alpha 0.68)
previously duplicated across src/ui/style/* into a single
`src/ui/style/common.rs::muted_style` (+ alias `muted_text_style`).
All 7 style windows and the style_manager wrapper now call the
common helper, ensuring a theme palette change updates one place.

`btn_s` is intentionally NOT deduped: plotstyle.rs:41 and
style_manager.rs:299 have divergent bodies (Disabled handling +
text alpha 0.45). Unifying would silently change plotstyle
disabled appearance :  documented in common.rs and covered by test.

TDD: RED `common_tests` failed E0433 (missing common), GREEN
added common.rs and wired `pub mod common`, REFACTOR replaced
locals with `use crate::ui::style::common::muted_style`.

Tests:
- common_muted_style_matches_local_copy: asserts common output equals 0.68 alpha reference
- common_muted_text_style_alias_matches: alias identity
- btn_s_bodies_documented_as_divergent: documents intentional non-dedup
2026-09-02 16:43:57 +01:00
Karim Jerbi 199a484fcb refactor(view): scope SelectionState borrow in selection_overlay block
Hold a single Ref<SelectionState> across the block; replace six redundant borrow().vp_size/last_move_pos calls with field reads on the Ref. Move the deep clone to the widget call site ((*sel_ref).clone()), preserving one clone per frame but ensuring the widget receives the same snapshot as the geometry reads.
2026-09-02 16:43:57 +01:00
Karim Jerbi d6a6dd6a7d feat(table): add TABLEDIT point picking, and consolidate InputKind
- Implement `TABLEDIT` command supporting viewport point-picking on cell interiors
- Support overlapping table hit testing with selection cycling and top-table resolution
- Support `/n` and `\n` multiline paragraph breaks (`\P`) in table cell text editing
- Unify text input modes via `InputKind` (`Point`, `SingleToken`, `FreeText`) on `CadCommand`
- Migrate 15+ commands across annotate, draw, insert, layout, and view modules
- Preserve text casing, literal spaces, and bypass arithmetic eval during FreeText input
- Add dedicated `tabledit_tests` and `free_text_entry_tests` test suites
2026-09-02 14:55:49 +01:00
Karim Jerbi afff1a40dd feat(ui): add press-to-capture shortcut editing to Keyboard Shortcuts dialog
Replace manual key entry with per-row capture cells: clicking + Add
inserts a draft row at the top with its key cell armed, and the next
key combination is recorded and normalized (Ctrl+Shift+S →
CTRL+SHIFT+S). While armed, a dedicated keyboard listener swallows all
input so recorded shortcuts do not execute in the drawing.

Draft rows are transient and confirmed only through an explicit accept
button, so the table never retains empty rows. Duplicate keys and
unrunnable commands are flagged in danger styling with a persistent
warning naming the conflicting command. Added reset-to-defaults with
confirmation, apply-and-exit, a shortcut count, and an unsaved-changes
guard on close.
2026-09-01 12:17:47 +01:00
Karim Jerbi 166d758820 fix(ui): stabilize themed cache keys with static byte slices and harden tests
The themed handle cache (introduced in cc430cdb) keys entries by (address, length) of &'static [u8] art. However, the art was declared as const; Rust may inline distinct copies of a const at each use site, giving the same logical icon different addresses. This broke cache lookups for wrapper functions (e.g., themed_arrow_down) that refer to the same icon from a different module than the cache test. Convert the ~30 themed-relevant byte slices (TRI_*, HOME, UNDO, REDO, CHECK, etc.) to static so each artwork has one stable address process-wide.

Also refactor themed_handle to drop the immutable borrow before building a miss handle, preventing a reentrant call from panicking on a second borrow_mut.

Adjust themed_cache_tests to use delta assertions and per-test cache removal for isolation, and add coverage for second-call cache hits, wrapper delegation, and the length component of the key.

Test: ui::icons::themed_cache_tests::themed_cache_returns_same_handle_for_same_bytes
Test: ui::icons::themed_cache_tests::themed_cache_caches_distinct_bytes_separately
Test: ui::icons::themed_cache_tests::themed_cache_second_call_is_a_cache_hit
Test: ui::icons::themed_cache_tests::themed_wrapper_populates_cache_for_underlying_bytes
Test: ui::icons::themed_cache_tests::themed_cache_key_includes_length
Test: ui::icons::themed_cache_tests::themed_cache_speedup_over_uncached_handle_creation
2026-08-30 00:36:04 +01:00
Karim Jerbi 6bf84d8f5e feat(ui): show color swatch and layer in selection cycling popup
Improve the selection-cycling list box so overlapping objects can be
identified at a glance:

- Add a color swatch per row showing the object's effective drawn color
- Show the layer name right-aligned next to the element type
- Size the popup to fit its content with consistent padding

Widen render_style_for_viewport visibility from scene-internal to
crate-wide so the view layer resolves the same color the renderer uses.
2026-08-29 12:31:03 +01:00
Karim Jerbi 452749f58c fix(ribbon): align combo panel dropdowns to a shared baseline
Style selectors (Standard, layer, Color/Linetype/Lineweight) sat at
different heights across ribbon panels because each combo panel
vertically centered its content: panels with fewer icon rows
(Multiline Text, Table) centered their dropdown differently than
panels with two rows (Dimensions, Multileader).

- Pin all combo panels (Layers, Properties, style combos) to the top
  of the fixed 3-row panel with a shared COMBO_PANEL_PAD, so every
  primary dropdown shares one baseline regardless of icon-row count
- Extract the duplicated tool-row and panel-container code into
  tool_row() / combo_panel_col() helpers + named spacing constants
- Bundle the per-view render state into RenderCtx, shrinking
  render_large/render_large_dropdown from 12-14 positional args to
  (item, &ctx)
- print_to_printer: silence the Windows-only dead-code warning on
  PrintOptions::quality with the platform split documented (the field
  is read by the CUPS `lp -o print-quality` path)
2026-08-29 11:35:06 +01:00
Karim Jerbi cc430cdbe9 perf(ui): cache svg::Handle in themed* icon functions
The 9 themed* functions that take &'static [u8] artwork each built a
fresh svg::Handle::from_memory(bytes) on every call, with 113
icons::themed call sites per frame across ribbon/statusbar/popups.
Add a thread_local THEMED_CACHE keyed by the bytes' (address, length)
and a single themed_handle(bytes) helper; the 9 themed* functions all
go through it. The 13 wrapper functions (themed_arrow_*, themed_home,
themed_undo, themed_redo, themed_check_cell, ...) delegate to those 9
and pick up the cache transparently, so all 22 themed* functions (and
the 113 call sites) are deduped.

The cache key is (address, length) -- not pointer alone -- to match the
existing SEMANTIC_CACHE belt-and-braces pattern and guard against any
future call site that passes a sub-slice of a static (e.g. &BYTES[1..]),
which would otherwise alias the wrong handle. Every include_bytes!
produces a unique static slice today, so the length is defensive.

Construction is amortised to once per artwork per process. No semantic
change; visual output is byte-identical.

Measured performance gains (integrated
themed_cache_speedup_over_uncached_handle_creation bench, 20_000
iterations each, same machine):

  |                | dev profile   | release profile |
  |----------------|---------------|-----------------|
  | Before         | 235 ns/iter   | 26 ns/iter      |
  | After (cached) |  78 ns/iter   |  9 ns/iter      |
  | Cold miss      | 900 ns        | 100 ns          |
  | Speedup        | 3.0x          | 2.9x            |

Release-mode warm path: 9 ns/iter. At 60 fps with up to 113 themed*
calls per frame, that is ~1 us/frame of handle work -- a rounding
error against a 16.6 ms frame budget. The cold-miss cost (one parse
+ one insert, 100 ns release) is the one-time price per new artwork
and is amortised to negligible per-frame overhead (it happens at
most once per artwork per process). The speedup is stable at ~3x
across dev and release because the ratio of work is the same:
hashmap-get + Handle clone vs. a full SVG parse + allocation.

test ui::icons::themed_cache_tests::themed_cache_returns_same_handle_for_same_bytes
test ui::icons::themed_cache_tests::themed_cache_caches_distinct_bytes_separately
test ui::icons::themed_cache_tests::themed_cache_speedup_over_uncached_handle_creation
2026-08-28 06:05:31 +01:00
Karim Jerbi 4f2cf5f516 test(app): add chprop integration tests for handler dispatch and undo
Add a new #[cfg(test)] mod chprop_integration_tests to src/app/properties.rs
with four end-to-end tests that exercise the converted CHPROP / MATCHPROP
handlers through the real Message dispatcher and the CLI command driver:

- prop_color_change_multiple_entities_undo_redo: a colour change on two
  selected lines flows through Message::PropColorChanged -> the property-op
  handler -> apply_property_op, and is reversible via undo / redo. Seeds
  the panel's source_handles (selected_handles() is empty in this test
  setup) so the converted non-empty branch runs.

- prop_change_on_locked_layer_is_ignored: when a selected entity's layer
  is locked, property_target_handles filters it out and the handler's
  empty-handles branch leaves the entity itself untouched. Asserts the
  entity-level invariant only; the empty branch updates the document
  header (CECOLOR) and sets dirty as a separate, pre-existing behaviour.

- ribbon_lineweight_updates_after_change: the Home-ribbon Lineweight chip
  is updated by the converted RibbonLineweightChanged handler. Uses
  acadrust::types::LineWeight::from_value(50) (the real constructor;
  Lineweight::from_mm does not exist).

- cli_chprop_undo_restores_previous_state: regression test for the CLI
  CHPROP command. Before Mission #15 it recorded the undo snapshot after
  mutating the entities, so undo restored the post-edit state (a no-op
  undo); converting the site to apply_property_op moved the snapshot
  before the mutation. #[ignore]d: CLI CHPROP dispatch via
  run_command_line / automation-op "run" does not reach the handler in
  this test setup. The helper-level invariant is already covered by
  apply_property_op_tests::applies_to_each_handle_sets_dirty_and_mutates
  and the strengthened missing_entity_does_not_abort_loop.

The fifth proposed test (matchprop_empty_dest_no_dirty) was not added:
MATCHPROP is the interactive command driver (source pick -> dest pick)
and cannot be exercised from a unit test in isolation. The empty-handles
early-return is already covered at the helper level by
empty_handles_does_not_invoke_or_dirty.

Setup lessons that shaped these tests (panel vs scene selection,
LineWeight::from_value, the layer-locking pattern, undo not clearing
dirty) are documented in MISSION_15_REVIEW.md section 6.2.

Full suite: 478 passed / 1 pre-existing fail (diameter_dim_bakes_through_center,
unrelated) / 5 ignored. No regression. No production code changed -- the
new module is #[cfg(test)]-only.

test app::properties::chprop_integration_tests::prop_color_change_multiple_entities_undo_redo
test app::properties::chprop_integration_tests::cli_chprop_undo_restores_previous_state
test app::properties::chprop_integration_tests::prop_change_on_locked_layer_is_ignored
test app::properties::chprop_integration_tests::ribbon_lineweight_updates_after_change
2026-08-27 23:24:32 +01:00
Karim Jerbi 7beb729d85 refactor(app): single-source CHPROP/MATCHPROP 4-step recipe via apply_property_op
Extract the push-undo → per-handle → invalidate → dirty → refresh
contract into one helper on OpenCADStudio. The helper is the single
source of truth for property-op side effects, so future property
handlers can't forget the cache invalidation or the panel refresh.

Note: MATCHPROP now no-ops when the destination selection is empty
(previously it marked the tab dirty and refreshed the panel). This is
a minor correctness improvement; restore the old behaviour with a
2-line guard at the MATCHPROP call site if desired.

test app::properties::apply_property_op_tests::empty_handles_does_not_invoke_or_dirty
test app::properties::apply_property_op_tests::applies_to_each_handle_sets_dirty_and_mutates
test app::properties::apply_property_op_tests::missing_entity_does_not_abort_loop
2026-08-27 23:24:32 +01:00
Karim Jerbi 513ab958a8 perf(grid): cache overlay geometry behind a params-keyed key
Introduce a cache for the grid overlay to prevent redundant geometry
reconstruction on every UI redraw. Extract the pure projection logic
from draw_grid into a renderer-free grid_segments function, and add
GridKey and GridCanvasState to memoize the resulting Geometry.

The cache key includes all per-pane GridParams and the overlay bounds,
preventing stale geometry when camera changes within the same canvas
size. On a params-key miss, the iced cache is cleared before redrawing
to bypass its bounds-only reuse.

Tests: 8 new unit tests cover key invalidation and state handling.
Benchmarks: uncached geometry ~32 us/frame vs cached decision ~90 ns,
a >99% reduction. Verified: 472 passed / 1 pre-existing failure / 4
ignored.

Tests:
  ui::overlay::grid_key_tests::grid_key_matches_identical_params
  ui::overlay::grid_key_tests::grid_key_invalidates_on_changed_fields
  ui::overlay::grid_key_tests::grid_key_invalidates_when_any_pane_changes
  ui::overlay::grid_key_tests::should_reuse_empty
  ui::overlay::grid_key_tests::should_reuse_equal
  ui::overlay::grid_key_tests::should_reuse_changed
  ui::overlay::grid_canvas_state_tests::default_state_has_no_cached_key
  ui::overlay::grid_canvas_state_tests::stored_key_is_recognized_by_should_reuse
2026-08-26 23:53:31 +01:00
Karim Jerbi 9c8f841f71 fix(insert): capped interactive block search and CLIPROMPTLINES
INSERT enumerated all block names in a single prompt string, causing unbounded overlay growth and full-screen occlusion in drawings with many blocks. Replace prompt enumeration with a ranked, incrementally filtered suggestion model: BlockPicker maintains precomputed lower/upper caches and usage ranks (MRU/frequency), filters via O(n) substring scan and partial sorting (select_nth_unstable) to O(k), bounded to CLIPROMPTLINES.

Introduce CLIPROMPTLINES (Integer 0-50, Registry, default 3) for the number of temporary prompt lines above the command window; persist in UserSettings, expose via SETVAR and direct entry, enforce in CommandLine::view. Suggestion count is in direct relation to CLIPROMPTLINES (limit = clip, capped to 50).

Make search live without requiring Enter: add CadCommand::on_live_input, implement for INSERT/MINSERT via picker.set_needle, and wire to command-line input paths (Message::CommandInput, on_command_append_char, on_command_backspace). Updates use upper/lower caches and partial sort for <0.2ms per keystroke on 10k blocks, with debounced block-usage persistence (MRU cap 20, freq cap 200) to avoid I/O thrash. Correct tri_count test helper to use display_from_solid.
2026-08-25 23:18:05 +01:00
Karim Jerbi 7215faae44 feat(properties): make inherited properties editable, align dropdowns, and switch transparency to EditChoice
Enable in-place editing of inherited properties in the Properties panel:
- Transparency: Switch to PropValue::EditChoice with ByLayer and ByBlock presets to allow direct typing of any numeric percentage value (0..90%) without combo_box autocomplete interference.
- Select-all-on-click: Integrate EditChoice text inputs into build_field_key_map with dedicated widget IDs, preserving click-to-select-all across editable fields.
- Material: Provide bounded preset choices (ByLayer, ByBlock, Global, and active custom material) to avoid scanning large drawing dictionaries.
- Plot style: Implement mode-aware behavior with interactive Choice in STB mode and ReadOnlyWithTooltip in CTB mode.
- Read-only tooltips: Introduce PropValue::ReadOnlyWithTooltip to provide contextual explanations for locked fields while preserving mouse text selection and clipboard copying.
- Dropdown alignment & unified borders: Fix EditChoice internal input/caret background clipping so the container's 1px neutral/active border surrounds the entire control identically to Lineweight and text inputs.
- Document defaults & aggregation: Synchronize document header defaults when editing with No selection, and aggregate heterogeneous multi-selections (*VARIES*).
2026-08-23 23:20:34 +01:00
Karim Jerbi fb2ea1e831 test(draworder): large-drawing latency benchmark 2026-08-23 18:56:05 +01:00
Karim Jerbi 7c14c31f25 fix(draworder): distinct ordered sort keys for multi-select and send-to-back groups 2026-08-23 18:56:05 +01:00
Karim Jerbi 236b7756f8 feat(draworder): HATCHTOBACK command and interactive DRAWORDER workflows
Implement the AutoCAD compatible HATCHTOBACK (HB) command and enhance the interactive DRAWORDER command pipeline with comprehensive verb selection and viewport entity picking.

Key implementations and architectural details:

1. HATCHTOBACK command execution
   * Isolates entities belonging to the active layout block record or global space.
   * Filters out entities residing on locked layers using an efficient hash set lookup.
   * Records targeted delta undo operations on SortEntitiesTable objects (~100 bytes) rather than snapshotting full document structures.
   * Performs granular draw depth cache invalidation via Scene::invalidate_draw_depth, preserving existing geometry tessellations and spatial indices.
   * Emits localized user notifications when no qualifying hatch entities are found.

2. Interactive DrawOrderCommand state machine
   * Supports preselected entities as well as interactive on demand selection gathering.
   * Implements subverb selection supporting single character shortcuts (A for Above, U for Under, F for Front, B for Back) and case insensitive keyword parsing.
   * Enables interactive reference object picking directly in the viewport via needs_entity_pick and on_entity_pick callbacks.
   * Supports hexadecimal handle specification directly from the command line interface.

3. Ecosystem and system integration
   * Bumps DEFAULT_ALIASES_VERSION from 2 to 3 to register the HB alias while preserving existing user overrides.
   * Integrates HATCHTOBACK and HB into dynamic autocomplete registries.
   * Adds localized message strings across all 20 Fluent translation catalogs.
   * Corrects hatch boundary serialization flags to ensure outermost boundary rings consistently carry the external attribute.
2026-08-23 18:56:01 +01:00
Karim Jerbi 32c3844526 perf(ribbon): stop building and laying out 5 panel renderings per frame
Each ribbon panel was rendered five ways every frame so CollapsePanels could
roll back to any density at layout time: full, compact, button, tight and a
full-size flyout that duplicated the full rendering.
The layout pass ran four full measure-layouts per panel even in forced modes,
and the degradation decision was an untested u8 inline in Ribbon::view.

Now the choice is a pure, unit-tested decide_levels() whose levels are a typed
Level enum (Full/Compact/Collapsed/Tight). Panel carries its four renderings
as elements: [Element; 4] indexed by Level, with a slot(i, level) helper for
the tree stride. Forced modes (Full/Compact/Collapsed) measure nothing at all
and derive the row height from the placed panel. The open flyout draws the
panel's own `full` rendering inside a self-drawn box (identical weakest-bg +
1px neutral-border to the container it replaced), so no duplicate
render_group is built per panel per frame. Level, Panel, Widths, CollapsePanels
and decide_levels are pub(crate); CollapseMode stays pub (re-exported).

Tests (collapse.rs tests module, added RED first):
- forced_modes_pin_every_panel
- auto_huge_width_keeps_everything_full
- auto_degrades_from_the_right_one_panel_at_a_time
- auto_stops_degrading_once_the_row_fits
- auto_escalates_a_single_panel_past_collapsed_to_tight
- auto_cascade_drops_every_collapsed_panel_to_tight
- auto_runs_the_compact_phase_across_the_row_first
- no_panels_stays_empty
- single_panel_walks_the_whole_ladder
- zero_widths_never_overflow
- endless_row_degrades_everything_to_tight
- unbounded_row_keeps_everything_full
- nan_budget_degrades_both_phases_but_no_tight_cascade
- exact_fit_at_full_stays_full
- matches_the_pre_refactor_algorithm: byte-equivalence sweep against the
  verbatim pre-refactor u8 algorithm (280 width/budget combos incl. the
  degenerate 0/0/inf/NaN inputs), proving the decision logic is unchanged.

Measured with the committed #[ignore]d benchmark
(`cargo test --lib --release -- --ignored --nocapture bench_ribbon_view_construction`)
run as one instrument across pre-change / this change: Ribbon::view()
construction ~676 -> ~425 us/frame (~37% of view construction saved); forced
modes run 1 layout call per panel per frame instead of 5.

Verification: cargo build clean; cargo test --lib = 382 passed (367 baseline
+ 15 new ribbon tests) / 2 known pre-existing failures unchanged
(save_then_open_round_trips, tilted_ucs_places_planar_entities_with_the_plane_normal).
2026-08-16 10:23:44 +01:00
Karim Jerbi b4da342f0e refactor(ribbon): derive the Block Palette button highlight from app state
The ribbon kept its own \show_block_palette\ copy that the app had to
manually sync at every mutation point (the BLOCKPALETTE command and the
dock Close handler). Drop the copy and thread the app's authoritative
\show_block_palette\ into \Ribbon::view\ at render time, so the button
highlight can't drift from the panel's true visibility — those two
handlers are now mutate-points only and the ribbon view is the single
place that reads the state back.
2026-08-15 14:42:08 +01:00
Karim Jerbi c3e3132b39 refactor(props): key active fields by a typed enum, not a sentinel string
The \\\x01attr\\x01…\ sentinel-prefixed string was used to tell geometry
fields (\&'static str\ names) apart from block attribute rows (runtime
tags) in \ctive_field\ and \dit_buf\. Replace it with a \FieldKey\
enum (\Geom(&'static str)\ / \Attr(String)\), so the two namespaces can't
collide by construction: \dit_buf\ is now \HashMap<FieldKey, String>\,
\ctive_field\ is \Option<FieldKey>\, and the sentinel decoding in
\ctive_key_id\ is replaced by \FieldKey::widget_id\.

While touching the key plumbing, replace \prop_field_key_for_id\'s
per-event scan of every section x prop (with a \String\ format per
candidate) with an O(1) precomputed \HashMap<iced::widget::Id, FieldKey>\
(\ield_key_by_id\), built once in \
efresh_properties\ alongside the
sections — and rebuilt after \make_sections_read_only\ so rows demoted
to read-only stop mapping. \PropSyncActive\ now does a single map lookup
per event.
2026-08-15 14:29:20 +01:00
Karim Jerbi eead4eb5f0 perf(props): skip focus sweep when the properties panel is hidden
The global left-click listener fires PropPointerPressed on every mouse
press anywhere in the app, and the handler used to launch a full
widget-tree focus sweep on each one - even when the Properties panel
was closed. Gate the sweep on dock_panel_visible(Properties): when the
panel is hidden the handler now returns Task::none, turning an O(whole
UI) traversal per click into an O(1) boolean check in the common case.

The other sweep call sites already gate via sync_active_field_if_any;
this was the only ungated one.

Tests:
- hidden_properties_panel_skips_the_focus_sweep - a click with the
  panel closed returns a Task::none (units == 0), i.e. no sweep.
- visible_properties_panel_runs_the_focus_sweep - a click with the
  panel open still fires the sweep (units > 0), preserving the
  select-whole-value behavior.
2026-08-15 13:49:26 +01:00
Karim Jerbi 9afb634557 chore(git): enforce LF line endings via .gitattributes 2026-08-15 13:12:34 +01:00
Karim Jerbi 41223f09d7 feat(plot): read-only rows use the shared selectable read-only field 2026-08-15 11:59:45 +01:00
Karim Jerbi 8e6b60910c feat(style): align mleader read-only field width with editable fields 2026-08-15 11:59:45 +01:00
Karim Jerbi d5b5f0781f feat(style): dim style read-only fields use the shared read-only field 2026-08-15 11:59:45 +01:00
Karim Jerbi 71d4eb96e5 feat(style): mleader style read-only rows use the shared read-only field 2026-08-15 11:59:45 +01:00
Karim Jerbi 61c3497d7f feat(text-style): read-only rows use the shared selectable read-only field 2026-08-15 11:59:45 +01:00
Karim Jerbi f2c4d389db feat(props): read-only rows use the shared selectable read-only field 2026-08-15 11:59:44 +01:00
Karim Jerbi f2b375ab24 feat(read-only): add shared selectable read-only field helper 2026-08-15 11:59:44 +01:00
Karim Jerbi fbab4c4934 feat(ribbon): Block Palette button reflects panel open state
The Insert tab's Block Palette button now behaves like the View tab toggles
(UCS Icon, View Cube, Ortho, Properties, File Tabs, Layout Tabs): it stays
highlighted while the block palette panel is open and clears when closed.

The Ribbon gains a show_block_palette flag with a set_block_palette setter,
fed into ToggleState and mapped in is_active_tool for the BLOCKPALETTE tool
id. show_block_palette on the app remains the single source of truth; both
mutation paths now mirror it onto the ribbon:

- BLOCKPALETTE / BLOCKSPALETTE command toggles ribbon state alongside the
  panel (src/app/commands/blocks.rs).
- Closing the panel via the dock rail close button clears the ribbon state,
  matching how Close(Properties) already syncs (src/app/update/dialog.rs).
2026-08-14 23:33:45 +01:00
Karim Jerbi 28e5deb222 fix(dock): don't panic clamping panel width when window is minimized 2026-08-14 23:06:53 +01:00
Karim Jerbi df940ad58e fix(dock): size drag preview by visible panels, not stack size
The drag preview divided the edge height by the raw panel count on the target
side. Closed panels keep their stack slot, so a hidden panel (e.g. the block
palette tucked away while Properties stays open) split the ghost and the drop
index as if it were rendering — leaving a half-height preview on that edge.
Size slots from the panels actually rendered on the edge instead, via a shared
visibility helper used by both the drop-index math and the ghost/edge-tint
geometry.
2026-08-14 22:24:57 +01:00
Karim Jerbi 2a0b7805c0 fix(dock): size drag preview slots by the edge viewport height
The dock drag preview computed slot heights and drop indices from the full
window height, but docked panel columns only span the model viewport, so the
preview overlay and insertion point drifted below the actual dock edge. Use the
current viewport height so the ghost and drop index line up with the panels.
2026-08-14 21:21:40 +01:00
Karim Jerbi 9f68034a60 feat(props): select whole value when a property field is clicked
Clicking an editable property value field now selects its entire value instead
of only placing the caret, so typing immediately replaces it. The click is
observed through a global left-press subscription (PropPointerPressed); a focus
sweep reports which widget took focus and PropSyncActive maps it back to the
active property row, then issues a select-all operation only when focus landed
on a different field.

The active row stays highlighted while its text input holds keyboard focus and
clears when focus moves elsewhere.
2026-08-14 21:21:40 +01:00
Karim Jerbi dceb9626ae fix(dock): block palette reflects new blocks without reopening
Creating a block from selected entities now bumps the geometry epoch, so the
block palette's per-update stale check notices the new definition and refreshes
its list in place instead of requiring the user to close and reopen the panel.
2026-08-14 21:21:40 +01:00
Karim Jerbi bd33600ee7 fix(dock): single-panel drag preview is one full-height slot
drop_index previously mapped a pointer position inside a lone full-height panel
to a top/bottom insertion split, offering an impossible drop. Clamp to the
single slot when the edge holds one panel, and cover the multi-slot mapping with
tests.
2026-08-14 21:21:40 +01:00
Karim Jerbi 94667f9512 feat(dock): active and dragging rail tab styling
Style the dock rail tab by its panel state: the expanded (active) panel's tab
gets a primary-colored highlight and border, and a tab whose panel is being
dragged gets a lighter primary tint, so the user can see which panel owns the
edge at a glance.
2026-08-14 21:21:40 +01:00
Karim Jerbi 7bfb617745 feat(dock): stacked edge rendering and drag-to-reorder preview 2026-08-11 23:13:05 +01:00
Karim Jerbi 10a1d89f19 feat(dock): migrate block palette onto general edge-stack dock
Pin/close/resize/drag-between-edges/auto-collapse hover, themed and
persisted, mirroring the properties panel chrome.
2026-08-11 23:13:05 +01:00
Karim Jerbi dce555f5bc feat(config): persist general edge-stack dock layout 2026-08-11 23:13:05 +01:00
Karim Jerbi 20be91ccaa feat(dock): add edge-stack dock data model and slice geometry 2026-08-11 23:13:05 +01:00
Karim Jerbi d9016e7196 Merge pull request #734 from KarimJerbi/feature/redraw-regen
Add REDRAW / REDRAWALL / REGEN / REGENALL commands- #721
2026-08-10 23:59:14 +01:00
Karim Jerbi f3d8904fc7 fix(alias): only advance the migration marker after the merge persists
Advancing ocad.pgp.version (or the web counterpart) could succeed while the
merged alias table write failed, so a later launch skipped migration against a
stale file and permanently lost the new aliases. Gate the version write on the
alias-table write (native fs::write / web set_item) succeeding.
2026-08-10 23:49:43 +01:00
Karim Jerbi 7013aa8cbb fix(alias): migrate new default aliases into existing profiles
Existing installs only got newly-added default aliases when storage was
absent; load_aliases parsed an already-present ocad.pgp or web alias table
verbatim, so R/RA/RE/REA remained unknown to profiles seeded before version 2.

Introduce a versioned default merge: track the default-table version each
profile has seen (wordless ocad.pgp.version marker / localStorage key) and
fold in only the aliases introduced after that version, never overwriting
user-defined mappings or resurrecting deliberately removed aliases. On first
migration the merged table and the advanced marker are persisted so later
launches skip the already-delivered aliases.
2026-08-10 23:39:46 +01:00
Karim Jerbi de1c1798a0 fix(scene): cfg-gate test-only refresh_pending_any to silence dead-code warning 2026-08-10 23:26:27 +01:00
Karim Jerbi 319710f36b test(commands): alias R routes like REDRAW 2026-08-10 23:26:27 +01:00
Karim Jerbi d2e18ead38 feat(aliases): R/RA/RE/REA for REDRAW/REGEN; register verbs 2026-08-10 23:26:26 +01:00
Karim Jerbi 692bb3babf feat(commands): REDRAW/REDRAWALL/REGEN/REGENALL 2026-08-10 23:26:26 +01:00
Karim Jerbi 7ace7fef9e feat(render): per-viewport REDRAW force_rasterize through builders to prepare gate 2026-08-10 23:26:26 +01:00
Karim Jerbi df103575b9 feat(scene): REDRAW refresh request resolved to per-viewport instance ids 2026-08-10 23:26:26 +01:00
Karim Jerbi 8f009d0058 feat(insert): add docked block palette
Add a searchable block palette with previews, file import, collision-safe nested definitions, and dependency/table-record import. Keep palette thumbnails current across drawing switches and block-definition edits.
2026-08-06 11:02:52 +03:00
Karim Jerbi 5299a483da fix(hatch): flag only the outer ring as external when serializing holes
When writing a hatched region with holes to DXF/DWG, every NaN-separated ring was created with BoundaryPath::external(), so consumers treated inner loops as outer islands instead of holes. Only the first (outer) ring should carry the external / outermost flags; hole rings are left unflagged. A picked big-minus-small hatch now persists with the small rectangle as a real hole.

Adds a regression test asserting only the first boundary path is flagged external.
2026-07-13 05:55:54 +01:00
Karim Jerbi 934570862c fix(hatch): pick innermost boundary and detect nested holes
The HATCH/GRADIENT/BOUNDARY "pick inside" mode returned the first outline containing the click point, so the result depended on draw order and any nested boundary was ignored. For a small rectangle inside a big one, clicking inside the small shape would hatch the whole big rectangle (or the small one, depending on which was drawn first), and clicking in the gap between them always hatched the full big rectangle with no hole.

Resolve the click against every enclosing outline and choose the smallest-area one as the fill. Any other outline fully contained by that ring but not containing the click point becomes a hole. The boundary is emitted as NaN-separated rings, which the GPU and CPU rasteriser already fill via even-odd, so pattern, solid, and gradient hatches all render the correct region (the small shape, or the big-minus-small ring) regardless of draw order. DXF export now writes one boundary path per ring so nested hatches persist with real holes.
2026-07-13 01:32:05 +01:00
Karim Jerbi 8ef65f59f8 fix(ribbon): highlight toggle buttons by their actual state
The UCS Icon, Properties, File Tabs, and Layout Tabs buttons were highlighted unconditionally because their ids fell through to an active_tool equality check in is_active_tool, which activate_tool sets on every click and never clears. Each id is now mapped to its corresponding visibility flag (show_ucs_icon, show_properties, show_file_tabs, show_layout_tabs), and those flags are kept in sync from their toggle handlers so the highlight reflects live state. The per-toggle booleans previously threaded through the render path are consolidated into a single ToggleState struct read from one place, so introducing a further toggle no longer requires changing every render-function signature and call site.
2026-07-12 19:53:48 +03:00
Karim Jerbi 4e0bba9f6a fix(viewport): clear ViewCube when toggled off
show_viewcube was excluded from render_signature, so toggling a static (cached) view produced an identical signature — the frame was skipped and the last GPU cube pixels stayed as stale cache. Add vp.show_viewcube to render_signature so the toggle invalidates the cache and re-renders, clearing the cube. The 2D overlay is iced UI and re-laid-out correctly, which is why only it hid.
2026-07-11 00:40:28 +01:00
Karim Jerbi 585316945e fix(ribbon): highlight View Cube button by toggle state
NAVVCUBE is a command toggle, so it never set active_tool and is_active_tool never matched it. Add a show_viewcube flag to the ribbon (mirroring wireframe/ortho_mode) and special-case "NAVVCUBE" in is_active_tool to return the live show_viewcube state; ToggleViewCube now calls ribbon.set_viewcube.
2026-07-11 00:40:22 +01:00
Karim Jerbi 73b05e1dec perf(core): cache ribbon_groups() with OnceLock, avoid per-frame tree allocations
Change CadModule::ribbon_groups() from -> Vec<RibbonGroup> to -> &[RibbonGroup],
backed by a per-module OnceLock cache. The ribbon group tree is static data
(&'static str, Copy enums) that was being fully reconstructed — Vecs, String
clones, enum discriminants — on every call, including all three per-frame
call sites (view(), dropdown_overlay(), style_combo_overlay()).

- 7 built-in modules + 2 plugin templates cache via function-local OnceLock.
  Safe because each is a unit struct with exactly one instance per process
  (plugins run in isolated child processes; see PluginProcess::spawn()).
- SharedCadModule avoids the static pattern entirely, storing groups in an
  instance field (owned.rs).
- IPC runner converts &RibbonGroup -> OwnedRibbonGroup directly via new
  From<&T> impls, avoiding an intermediate clone.
- render_small/render_large and the two make_tool_row closures now borrow
  (&RibbonItem, &[ToolDef]) instead of taking ownership, so the view loop
  doesn't need to clone items to satisfy the old by-value signatures.
- Various match-ergonomics deref fixes (*id, *default, *icon, etc.) from
  the &RibbonItem pattern change.
2026-07-02 21:00:57 +01:00
Karim Jerbi 3d23c45016 chore: disable printpdf default features (unused html deps)
printpdf's default = ["html"] pulled in azul-layout, rust-fontconfig,
kuchiki, xmlparser and their transitive trees (~40+ crates including
two with cdylib targets) — none of which are used by the project's
PDF export (only basic primitives like Mm, Line, Polygon, Rgb).

Also fixes a MinGW linker error (export ordinal too large: 100035)
triggered when ld.bfd tries to build a DLL for printpdf with >65535
exported symbols.
2026-06-29 01:06:28 +03:00
Karim Jerbi 63c13f0f63 refactor(ribbon): extract repeated inline color literals into named constants
Consolidates 21 occurrences of identical Color literals scattered across widgets.rs and mod.rs into 9 named constants (COMBO_BG, COMBO_HOVER_BG, COMBO_OPEN_BG, COMBO_BORDER, COMBO_ACTIVE_BORDER, COMBO_ARROW, SWATCH_BORDER, TIP_BG, HIST_INACTIVE_BG). -85 net lines, zero behaviour change.
2026-06-28 22:59:14 +01:00
Karim Jerbi 1e8a98d2cd fix(ribbon): center section titles within their content width
Section labels (Draw, Modify, Annotation, …) were left-aligned with a small padding. Set align_x(Center) on the parent column so the label container — still sized to its text width — is centred within the column, which itself remains sized by the tool buttons above. Sections keep their natural content-based width, no wasted space.
2026-06-28 22:23:01 +01:00
Karim Jerbi d2cd728313 Merge branch 'main' of https://github.com/KarimJerbi/OpenCADStudio 2026-06-28 19:18:12 +01:00
Karim Jerbi ae2946a8a9 fix(block-cache): emit one greek substitute per source MTEXT entity
Colour-split MTEXT produces one outline wire per inline \C/\c override. At <5px zoom each outline independently emitted the full OBB greek rectangle — N overlapping boxes with last-colour-wins. Track last_lod_obb and skip duplicate substitutes.

Also corrects the fill-only LOD bypass: suppress fill-only wires at <5px instead of sending them unconditionally through emit_wire. The outline sibling's baseline or greek box suffices at sub-pixel zoom.
2026-06-28 19:17:24 +01:00
Karim Jerbi d99b480eae Merge branch 'HakanSeven12:main' into main 2026-06-28 18:52:03 +01:00
Karim Jerbi ca6239a32d chore: mark fill_tris_low unpaired sites with FIXME
dimension.rs (x2), multileader.rs (x3), table.rs (x1) emit fill_tris
without a corresponding fill_tris_low. These will trip the debug_assert_eq!
added in the preceding commit when the entity appears inside a block
definition. Each site is marked with a FIXME referencing block_cache.rs:1397
and the correct fix pattern (split_ds on each vertex).
2026-06-28 18:46:02 +01:00
Karim Jerbi 7da8fbe316 fix(block-cache): preserve per-wire colour and fill precision in cached blocks
Three related bugs in tessellate_sub_local and expand_defn:

1. Wire folding lost per-segment colour. Multi-wire tessellator output
   was concatenated (NaN-separated) into a single LocalWire using the
   first wire's colour. Inline \C/\c MTEXT colour overrides inside block
   definitions rendered in the entity colour instead of the override colour.
   Fix: tessellate_sub_local returns Vec<LocalWire>; each wire from the
   tessellator becomes its own LocalWire with its own colour.

2. fill_tris_low was never forwarded. The double-single precision path
   was silently degraded on cached wires. Fix: fill_tris_low forwarded
   from WireModel through to LocalWire and BatchEntry. debug_assert_eq!
   added at emit site to catch future invariant violations.

3. Fill-only wires hit the text LOD ladder. MTEXT outline/fill pairs
   produce separate LocalWires; the fill-only wire was routed through
   emit_text_baseline/emit_greeked_text which ignore fill_tris (invisible
   double-draw). Fix: is_fill_only field on LocalWire bypasses the LOD
   ladder. Also fixes StyleKey::fill_only being hardcoded false at both
   emit_wire call sites (latent batch misclassification).
2026-06-28 18:45:57 +01:00
Karim Jerbi 06bb6b54ff fix(text): canonicalise TTF filename font refs in MTEXT inline overrides
resolve_font previously returned &str, passing raw stems like "arial" verbatim to Face::resolve. System font lookup requires the canonical family name ("Arial"), so inline \f/\F overrides from block-nested MTEXT were silently unresolvable and fell through to no geometry.

Change resolve_font to return Cow<'a, str>:
- no override -> Borrowed(base), zero allocation
- LFF built-in -> Borrowed(font), zero allocation
- system font resolved -> Owned(canonical), one allocation
- unresolvable -> Borrowed(base), falls back to style font

All 4 call sites updated to &font_name (&Cow<str> derefs to &str).

Adds two unit tests covering the fallback and TTF-stem-to-family-name paths. Adds integration test verifying block-nested MTEXT produces geometry and correctly separates outline/fill wires.
2026-06-28 18:44:38 +01:00
Karim Jerbi 627761b629 fix(text): render TTF glyphs as solid fills, fix font fallback, correct Z-order
Text elements previously rendered as hollow wireframe outlines and sometimes
fell back to the wrong system font due to case-sensitive exact matching. Also,
the viewport background grid drew on top of solid entities.

Case-insensitive and partial-name fallback matching (e.g., mapping "arialn"
to "Arial Narrow") is now added to `sysfont::canonical_family_name`, ensuring
text resolves to the correct installed system font.

TrueType font contours are now triangulated into solid fills (`fill_tris`)
using `lyon_tessellation` in `ttf_glyph.rs`. To prevent the GPU from misclassifying
this 2D text as a 3D mesh (which broke flat shading), text tessellation splits
the output into separate outline and fill `WireModel`s.

The background grid is extracted from `SelectionCanvas` into `GridCanvas` and
moved beneath the 3D viewport to ensure proper layering. Finally, `DepthBiasState`
is enabled on all face/fill pipelines to apply a polygon offset, perfectly
resolving Z-fighting between coplanar wireframe lines and solid faces.
2026-06-25 18:17:01 +01:00
Karim Jerbi 02f70ab0b3 fix(editor): prevent empty text commit from advancing command edit_count
- Propagates commit status out of inline editors so commands know whether a real change occurred.

- Fixes TEXTEDIT's Undo step rolling back unrelated document actions when the user presses OK on empty/cleared existing MText.
2026-06-18 17:55:30 +01:00
Karim Jerbi 8e48bf02a0 fix(annotate): correctly resolve Leader annotations in TEXTEDIT
- Custom recursive resolver ensures Leader entities trace back to their editable Text/MText annotations.

- Bypasses overly strict baseline field checks so TEXTEDIT can natively target and launch the inline editor on Leaders.
2026-06-18 17:49:29 +01:00
Karim Jerbi 9a258f29ce feat: implement TEXTEDIT command and TEXTEDITMODE system variable
- Adds TEXTEDIT command to seamlessly edit annotation objects in-place.
- Matches industry-standard CAD tools' prompt behavior and "Current settings" banner logic.
- Implements the TEXTEDITMODE system variable (0/1) for Multiple vs Single editing modes.
- Enhances command lifecycle with CmdResult::SuspendForTextEdit and post_editor_closed hooks, allowing commands to cleanly pause and resume after external UI editors close.
2026-06-18 15:56:27 +01:00
Karim Jerbi 7fc8c5985b Make TORIENT view-aware and fix DYN angle commit 2026-06-17 17:05:20 +01:00
Karim Jerbi 1e7bd1e9b2 Add TORIENT command 2026-06-17 16:36:45 +01:00
Karim Jerbi a431e02378 Temporarily suppress Ortho mode during RECTANG commands 2026-06-16 09:53:32 +01:00