106 Commits
Author SHA1 Message Date
Hakan Seven 9eb47c43a9 fix(linux): focus browser on external links 2026-07-30 21:07:31 +03:00
Hakan Seven 2c64888048 fix(blocks): preserve UCS transformations
Bake BEDIT frames into block-local geometry while keeping model and viewport UCS isolated. Normalize legacy nonzero block origins during load.
2026-07-30 19:50:33 +03:00
Hakan Seven c9aab122f0 feat(find): add find and replace
Closes #583
2026-07-30 17:56:54 +03:00
Hakan Seven 3ab8dc3ca5 fix(ui): make modal layouts intrinsic
Pin Iced 0.15 so dialogs can measure content before distributing the resolved frame across child panels. Keep the web plugin notice compact and non-resizable.\n\nCloses #582
2026-07-30 14:47:28 +03:00
Hakan Seven 3897fb2661 fix(viewports): fit extents per pane
Closes #554
2026-07-29 23:03:06 +03:00
Hakan Seven ff19353a21 feat(draw): add BLEND command
Closes #560
2026-07-29 21:21:26 +03:00
Hakan Seven 1e624cc6b7 feat(layers): add native state manager
Replace session-only layer snapshots with drawing-backed DWG and DXF states, exposed through the ribbon and LAYERSTATE commands.\n\nCloses #562
2026-07-29 18:00:41 +03:00
Hakan Seven fcc6e1f8e1 fix(web): link plugins to desktop app
Keep browser entry points visible while explaining that native
plugins require a desktop install.

Refs #567
2026-07-29 16:28:20 +03:00
Hakan Seven 2424eeb74d fix(web): hide unsupported plugin manager
Fixes #567
2026-07-29 16:17:17 +03:00
Hakan Seven 99dbc8254d fix(plot): parent PDF save dialog
Remove the temporary EXPORTPDF <path> fallback now that native save requests are tied to the main window.\n\nCloses #537
2026-07-28 19:23:29 +03:00
Hakan Seven d77eb8f419 fix(block): open nested editor tabs
Closes #538
2026-07-28 19:13:27 +03:00
Hakan Seven 8050fc79fd fix(scene): clear selection after erase 2026-07-28 17:35:21 +03:00
Hakan Seven 05b044f367 perf(scene): make geometry edits incremental
Avoid full document, cache, and GPU rebuilds for entity deltas so add, delete, history, selection, and live polyline edits stay responsive on large drawings.
2026-07-28 17:16:27 +03:00
Hakan Seven d5be78a187 fix(selection): separate transient visibility
Keep object isolation out of persisted entity visibility and clear stale grips across block-editor transitions.

Refs #512

Refs #517
2026-07-27 23:36:02 +03:00
Hakan Seven b629dc32de feat(wipeout): add polygon creation modes
Closes #528
2026-07-27 21:50:50 +03:00
Hakan Seven b8889ccd20 feat(mview): add viewport creation options
Support polygonal and object viewports through native clip-boundary handles, including clipped rendering and selection.

Closes #525
2026-07-27 19:28:51 +03:00
Hakan Seven 8018ab1523 feat(view): implement LIMITS command
Closes #478
2026-07-27 16:15:28 +03:00
Hakan Seven 2acc4f7978 fix(io): guard concurrent drawing saves
Hold edit leases and compare disk fingerprints before replacement so another editor cannot be silently overwritten.

Offer retry, reload, Save As, or explicit overwrite recovery.

Closes #498
2026-07-27 13:18:31 +03:00
Hakan Seven 731df4a725 feat(cad): integrate standard codec coverage
Render, edit, query, select, and explode newly decoded standard entity variants. Pin acadifc de8d6b5 and preserve None color semantics.
2026-07-26 19:30:43 +03:00
Hakan Seven deec48ad99 perf: keep heavy work off UI thread 2026-07-25 18:49:18 +03:00
Hakan Seven 12bf126646 perf: remove large drawing stalls
Keep resident geometry and GPU resources stable across edits, MSPACE zoom, and Model/Paper switches. Add unified PERF output for tracing remaining costs.
2026-07-25 13:34:48 +03:00
Hakan Seven ea93e169ea perf: make edit history incremental
Replace full-document snapshots with Arc first-touch deltas and batch cache invalidation. Reuse interaction and GPU category caches across unrelated edits.
2026-07-24 23:25:33 +03:00
Hakan SevenandClaude Fable 5 3326c5f0c8 fix(purge): drop orphaned blocks, anonymous blocks, and stale draw-order
PURGE left behind three classes of dead data that AutoCAD removes,
so an OCS-purged drawing stayed far larger than AutoCAD's (a 23 MB test
file purged to 3.5 MB in OCS vs 0.58 MB in AutoCAD):

- Anonymous blocks (`*U`/`*D`/`*T`/dynamic-block variants) were skipped
  wholesale. Leftover unreferenced ones now purge like AutoCAD's, held
  live only when actually referenced.

- Block liveness now comes from REACHABILITY: a block is kept only if it
  is reached from a real layout container by following block references
  (INSERT names, dimension *D, table *T / cell blocks, MultiLeader
  content). The old flat "named by some INSERT" scan could not free a
  mutually-referencing dead subgraph — e.g. the dependent blocks a
  detached xref leaves behind, whose head carried a DANGLING layout
  handle that `is_layout()` mistook for a real layout and shielded. That
  subgraph (here 467k lines) is now purged; live xrefs, reached from the
  layout that inserts them, stay. The used-set also gains the by-handle
  block references (table/MultiLeader/dimstyle-arrow) so purging
  anonymous blocks can't delete an in-use block no INSERT names.

- SortEntitiesTable objects (per-block draw order) were never removed, so
  each purged block's table lingered still listing the handle of every
  deleted entity — 341k stale handles / ~2.7 MB of dead weight the save
  re-emitted. Drop every table whose owning block is gone.

Net on the test file: 3.5 MB -> 0.81 MB (AutoCAD 0.58 MB).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-23 13:32:26 +03:00
Hakan SevenandClaude Fable 5 0c578b03f3 feat(select): PICKADD/PICKDRAG command-line toggles (#226)
PICKADD 0 = click/box/lasso replaces the selection (Shift toggles),
empty click clears; PICKADD 1 (default) keeps today's accumulate.
PICKDRAG 1 = press-drag spans a rectangle marquee through the box
machinery; PICKDRAG 0 (default) keeps the freeform lasso. Both persist
in settings.json. Also re-points the start-page gate test at the
start_allowed fn it moved into (#388/389 refactor left it broken).

Closes #226

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-22 21:35:37 +03:00
Hakan SevenandClaude Fable 5 e7eb84b514 feat(pedit): convert prompt, join, fit/spline/decurve; join fixes (#263)
PEDIT reworked around a mode machine:

- Picking a line/arc asks "Object is not a polyline. Turn it into one?
  [Yes/No] <Y>" — Yes converts in place (arc keeps its bulge) and
  editing continues on the result. Pickfirst: an already-selected
  polyline skips the select step, a selected line/arc goes straight to
  the convert prompt.
- Join gathers with the normal selection system (single picks AND
  window/crossing boxes) and merges through the JOIN machinery.
- Fit replaces every segment with a classic equal-parameter BIARC: two
  arcs per segment meeting tangentially at an inserted knee, both
  segments at a vertex sharing that vertex's tangent — the whole run is
  tangent-continuous (verified: zero tangent jump on L/closed/zigzag/S
  shapes). Spline resamples the vertex frame as a uniform cubic
  B-spline (8 samples per span); Decurve straightens every bulge.
- Earlier hardening kept: undo snapshot before the mutation, immediate
  repaint, polyline-only pick validation.
- Vertex editing lives on grips, so no Edit-vertex submode; instead the
  vertex grip hover menu gains Break — a closed polyline opens at the
  vertex, an open one splits into two (interior vertices only),
  handled driver-side as an entity replacement.

JOIN itself: segs_of now emits one segment per span of an OPEN
polyline (bulges kept), so polylines merge with their neighbours —
PEDIT Join's set always contains the target polyline and used to abort
every time; plain JOIN had the same gap. JOIN also honours pickfirst:
launched with two or more objects selected it joins them immediately.

Closes #263

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-22 20:06:04 +03:00
Hakan SevenandClaude Fable 5 b32a9b4083 fix(plan): show options on activation instead of flipping the view (#326)
PLAN acted like a ViewCube TOP click the moment it ran — already on Top
meant an immediate flip to Bottom, and the options never appeared. It
now prompts "[Current ucs / Ucs / World] <Current>:" right on
activation and only changes the view once an option runs: Current =
plan of the active UCS, World = world XY, Ucs = prompts for a named UCS
(case-insensitive lookup). The new plan_snap goes straight to the plan
view with no flip-to-opposite cube behaviour.

KeywordCommand gains with_default(): a bare Enter at the verb step
dispatches the default verb (PLAN's <Current>) instead of cancelling.

Closes #326

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-22 16:53:32 +03:00
Hakan SevenandClaude Fable 5 e009d2d7a3 fix(rename): BLOCK type, stepped prompts, table re-keying
- RENAME gains the BLOCK type (stepped button + inline arm) via the
  shared Scene::rename_block.
- Stepped command front-ends (Rename, UserReg, Keyword, TwoValue,
  SelectThenTwoValue) returned None after consuming a typed token, and
  the driver treats None as "not consumed" — the same text was offered
  again and advanced two steps at once (RENAME dispatched old==new,
  SETVAR "X" became "SETVAR X X"). Consumed-but-prompting inputs now
  return NeedPoint.
- Symbol renames re-key the name-keyed Table (remove -> rename -> add)
  instead of mutating .name in place, which left the map keyed by the
  old name so later lookups silently failed. References (entity
  layer/linetype/style fields, layer linetypes, header current-* names,
  dim style refs on Dimension/Leader/Tolerance) are chased
  case-insensitively; layer 0/Defpoints and the built-in linetypes are
  protected; the undo snapshot is taken before the mutation so undo
  actually restores the old name.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-22 11:11:20 +03:00
Hakan SevenandClaude Fable 5 b7457ba09d fix(ribbon): highlight follows each tool's mechanism; start-tab dimming (#355)
Tool highlights used to stick: every click set active_tool, but only
interactive commands and a hand-maintained per-modal list ever cleared
it, so one-shots (Cleanup/AUDIT, view actions) and unlisted dialogs
(Customization/ALIASEDIT, CUI, Plugin Manager…) stayed blue forever.

Now the rule is mechanical: state toggles light from state only (the
click highlight drops immediately), interactive commands stay lit
while running, dialog openers stay lit until the modal closes (generic
clear replaces the per-modal list), and one-shots never keep it — the
dispatch site turns the highlight off when nothing stayed running.

On the Start tab, tools whose command the start gate refuses now render
dimmed (grey icon + label) across all button styles, dropdowns and the
quick-access strip; start_allowed() is the single shared authority.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-22 00:13:48 +03:00
Hakan SevenandClaude Fable 5 3207cb391b feat(view): bare ZOOM prompts with mode options
ZOOM alone used to jump straight into window zoom. It now offers
Window / Extents / All / In / Out / Scale as clickable keywords
(Scale collects the factor in a second step); ZW/ZE/… shortcuts and
the spelled sub-forms still run directly.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 23:18:18 +03:00
Hakan SevenandClaude Fable 5 dc10c5dfba feat(edit): STRETCH crossing window selects objects implicitly (#338)
Bare STRETCH used to need a separate object-selection round before the
crossing window. Now the window itself picks the objects: with no prior
selection the command emits StretchWindow, the host gathers entities
whose world AABB touches the window (locked layers excluded) and
restarts at the base-point step. Over-selection is harmless since only
points inside the window move. The noun-verb flow (select first, then
STRETCH) is unchanged.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 22:35:35 +03:00
Hakan SevenandClaude Fable 5 3205f7a2b9 feat(purge): nested fixpoint purge + option prompt (#375)
PURGE now repeats until a pass removes nothing, so definitions freed by
a purged block (nested blocks, layers, linetypes, styles) go in one run.
Reference scan keeps layer linetypes, ATTDEF/ATTRIB text styles, and
dimstyle DIMTXSTY + arrow blocks; the undo snapshot moves before the
first removal so one UNDO restores everything. Bare PURGE prompts with
clickable category options (aliases B/LA/LT/ST run directly), and
pinned prompts drop their "[A / B / …]" listing when the same options
render as buttons.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 22:15:33 +03:00
Hakan SevenandClaude Fable 5 ccd1086f9f fix(app): running OSNAP set is app-level, not per-drawing
Modern DWG (R2000+) has no file slot for OSMODE — reading a file left
the header at 0, and adopt_header_sysvars wiped the user's running snap
set on every open. Persist the set (as an $OSMODE-style bitmask) in
settings.json, save it on every snap toggle, and only let a file
override it when it actually carries a nonzero mask (R13/R14 DWG, DXF).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 22:00:55 +03:00
Hakan SevenandClaude Fable 5 0cd991e42f fix(xref): show merged xref layers in Layer Manager on attach (#407)
XATTACH / XRELOAD merge the xref's layer and linetype tables into the
document, but the Layers panel cache only rebuilds via
sync_with_viewports - which nothing triggered until the file was saved
and reopened. Refresh the layer panel (and ribbon mirrors) right after
the merge in both paths.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 15:59:26 +03:00
Hakan SevenandClaude Fable 5 33864bace6 feat(offset): start from the pre-selected objects
OFFSET now follows the pick-first flow of MOVE / COPY (#422): with
offsettable objects already selected it skips the pick step and goes
straight to the distance / side step, offsetting every selected object
toward the clicked side — each through the cursor point in through
mode, all by the typed magnitude once one is locked. Preview covers
every target and a multi-object result commits as one undo step; with
nothing selected the command gathers a pick exactly as before.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 11:20:04 +03:00
Hakan SevenandClaude Fable 5 554921c66f fix(refedit): objects created during the session join the block
REFCLOSE only wrote the temp copies made at REFEDIT entry back into
the block, so anything drawn (or pasted / offset) during the session
stayed behind in model space (#423). The session now captures a
handle watermark at entry; on close, every current-space entity
allocated at or above it joins the working set — Save folds it into
the block through the same inverse transform, Discard removes it.
Covers every creation path with no per-command hooks.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 11:13:47 +03:00
Hakan SevenandClaude Fable 5 f03288f120 fix(bedit): restore the view when leaving the block editor
Entering the block editor reframes the camera on the block's own
geometry, but nothing remembered where the model view was — Save /
Discard returned to the layout with the camera still parked at the
editor's framing (#425). The session now captures the camera on entry
and both exits restore it exactly.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 11:06:42 +03:00
Hakan Seven e85e2aeb78 feat(commands): step POLYSOLID (select polyline, then width + height)
Add SelectThenTwoValueCommand (gather a selection, then two values) and use it
for POLYSOLID: bare POLYSOLID gathers the polyline if none is selected, prompts
for wall width then height, and dispatches POLYSOLID <width> <height>.
2026-07-16 14:47:45 +03:00
Hakan Seven 49c10bc806 feat(commands): step the remaining inquiry / solid commands (bucket E)
- ValuePromptCommand: CAL/QUICKCALC/QC (expression), FIND, FINDALL (search).
- SelectThenKeywordCommand (select the solid, then axis + value): SECTION,
  SLICE, 3DMIRROR, 3DROTATE. SLICE's optional TOP/BOTTOM stays typeable.
- TwoValuePromptCommand: PYRAMID (radius + height; sides typeable).

Left as-is by design: UNDO (bare already undoes one step — a correct default,
not a usage line); POLYSOLID (select a polyline then width + height — needs a
two-value-after-selection form) and 3DALIGN (six 3-D points) stay typeable.
2026-07-16 14:43:34 +03:00
Hakan Seven f0f9a57bf6 feat(commands): prompt the path/file commands for their path (bucket D)
SCRIPT/SCR, DATALINK, LANDXMLIMPORT, CUIEXPORT, CUIIMPORT/CUILOAD now prompt
for the path via ValuePromptCommand instead of printing usage when run bare;
each dispatches `NAME <path>` to the existing inline handler. The path keeps
its case (dispatch does not upper-case arguments), so case-sensitive paths on
Linux/macOS still resolve. A native file dialog would be a nicer follow-up.
2026-07-16 14:40:11 +03:00
Hakan Seven 208e617868 feat(commands): step the selection-first text commands (bucket C)
Add SelectThenValueCommand (gather selection, then one value) and convert:
- SelectThenValueCommand: HYPERLINK (url), ARCTEXT (text), TEXTFIT (width),
  SCALETEXT (factor), TCOUNT (start), TEXTMASK (Enter to apply).
- SelectThenKeywordCommand: JUSTIFYTEXT (Left/Center/Right/Middle/Aligned/Fit),
  TCASE (Upper/Lower/Sentence/Title).

Each bare form gathers a selection when none is set, prompts, and delegates to
the inline handler. OBJECTSCALE already opens its dialog; TABLE CELL takes three
values (row/col/text) and stays typeable — TABLE bare already builds a table
interactively.
2026-07-16 14:37:10 +03:00
Hakan Seven cf5362aeab feat(count): prompt COUNT for the tally mode
Bare COUNT now offers [All (by type) / By layer] as buttons instead of
immediately dumping the by-type tally. "All" dispatches COUNT TYPE, whose
output is identical to the old bare COUNT; "By layer" dispatches COUNT LAYER.
Counting a specific layer (COUNT <layer>) stays typeable. Finishes bucket B.
2026-07-16 13:57:44 +03:00
Hakan Seven 7195e1e8fb feat(commands): step the selection-first keyword commands
Add SelectThenKeywordCommand: if nothing is selected it first gathers a
selection (Enter confirms, following the ALIGN contract), then shows the
sub-verbs as buttons like KeywordCommand and dispatches `<name> <verb>
[value]` to the inline handler, which reads the still-selected set.

Convert CHPROP, ADJUST, XDATA, UNDERLAY and DRAWORDER. Verbs the single-value
generic can't express keep working by typing the full line: XDATA SET
<app> <value> and DRAWORDER Above/Under <handle> (a reference pick) are left
off the buttons; CHPROP/ADJUST/UNDERLAY are fully covered.

Verified end to end: SELECTALL then `CHPROP LAYER <name>` moves the selected
entity to that layer through the stepped front-end.
2026-07-16 13:55:49 +03:00
Hakan Seven 4c3509fa51 feat(commands): step the rest of the keyword/value commands
Convert the remaining bucket-B commands to prompt step by step instead of
requiring arguments or printing usage:

- KeywordCommand (buttons): VIEW, LINETYPE, SCALELISTEDIT, UCSICON, VSCURRENT.
- ValuePromptCommand (one value): LAYDEL (layer), ATTSYNC (block).
- TwoValuePromptCommand (new; two values): LAYMRG (source + target),
  SETVAR (variable + value, blank second reads).

Each bare form now starts the stepped front-end and delegates to the existing
inline handler; the argument form is kept and is the dispatch target. The
direct visual-style shortcuts (HIDDENLINE / XRAY / …) and UCSICON's argument
form are untouched.

COUNT is intentionally left alone: bare COUNT already reports the tally (a
useful inquiry action, not a usage line), so prompting would only add friction.
2026-07-16 13:29:04 +03:00
Hakan Seven 4fa6c49445 feat(commands): prompt ATTDISP and LAYERSTATE by keyword
Add KeywordCommand, a generic stepped front-end for keyword sub-verb
commands: bare NAME shows the sub-verbs as clickable buttons, and picking
one either dispatches `NAME <verb>` (action verbs like ATTDISP On/Off/Normal)
or prompts for an argument and dispatches `NAME <verb> <value>` (LAYERSTATE
Save/Restore/Delete <name>). It delegates to the existing inline handler, so
the logic stays in one place. First two of bucket B converted; the rest
(VIEW, LINETYPE, SCALELISTEDIT, SETVAR, LAYDEL/LAYMRG, and the selection-first
XDATA/UNDERLAY/ADJUST/DRAWORDER/CHPROP) can follow the same pattern.
2026-07-16 13:19:04 +03:00
Hakan Seven 97cc647055 fix(annoscale): make the OBJECTSCALE dialog reachable
The Annotation Object Scale dialog (advertised in v0.8.2) was dead code:
bare OBJECTSCALE was caught by the immediate "mark selection annotative"
arm, so the `"OBJECTSCALE" =>` dialog arm below it never ran and nothing
else opened the dialog. Move the quick mark to an explicit `OBJECTSCALE ADD`
keyword (what the ribbon "Add Scale" button now sends, dispatched whole so
it doesn't collide), leaving bare OBJECTSCALE to open the dialog.
2026-07-16 13:14:37 +03:00
Hakan Seven 1533c66a3e feat(userreg): prompt USERI/USERR for register then value
USERI/USERR set one of five integer/real header registers and needed both
inputs (register + value) typed on one line; bare printed usage. Add a
two-step front-end (UserRegCommand): it prompts for the register as 1–5
buttons, then the value, and dispatches USERI/USERR <n> <value> to the
existing inline handler. The argument arm now requires a space so the bare
name reaches the stepped command.
2026-07-16 13:14:37 +03:00
Hakan Seven 5f1019edec feat(commands): prompt the single-value settings step by step
Bare CLAYER / CDIMSTY / COLOR / LWDISPLAY / ISAVEBAK / FILEASSOC /
SAVETIME / ANNOSCALE printed the current value or a usage line and
stopped; only the fully-typed `NAME <value>` form set anything. Route the
bare form through ValuePromptCommand (the pattern LTSCALE / PDMODE / PDSIZE
already use): it prompts for the value and re-dispatches `NAME <value>` to
the existing inline handler, so the set/get logic stays in one place. The
bare-Enter getter is preserved — ValuePromptCommand dispatches `NAME `,
which the argument arm reports the current value for (added that empty
case to ISAVEBAK / FILEASSOC / SAVETIME, which previously errored on it).
COLOR keeps BYLAYER as an immediate shortcut rather than a prompt.

USERR / USERI are left as-is: they take two inputs (register 1-5 + value),
so they need a two-step command rather than the single-value prompt.
2026-07-16 13:00:10 +03:00
Hakan Seven 7cd5010729 feat(rename): prompt RENAME step by step instead of requiring all args
Bare RENAME printed a usage line and did nothing; only the fully-typed
`RENAME <type> <old> <new>` form worked. Add a stepped front-end
(RenameCommand) that prompts for the object type — as clickable buttons
(Layer, Text style, Dim style, Linetype, UCS, View) — then the current
name, then the new name, and dispatches to the existing inline handler.
The rename logic stays in one place; the argument form is kept and is also
the front-end's dispatch target. BLOCK is left off the buttons because the
inline handler does not implement it.
2026-07-16 12:51:34 +03:00
Hakan Seven 5b4a6719aa Merge pull request #383 from KevinGriffin-new/fix/pdf-export
Never fail silently on PDF export; add EXPORTPDF <path> (#369).

The export flow handed off to an async rfd save dialog and mapped a None
result straight to Task::none(). On Linux None also means the dialog never
opened (broken XDG portal / missing zenity), and rfd reports that only via
log, so PDF export could end in total silence. The dialog-None arms now
print a message pointing at EXPORTPDF <path>, and EXPORT/EXPORTPDF take an
optional path that exports straight to a file with no dialog — the
workaround where the native dialog can't open, and a headless entry point.

Conflict: the fix (display.rs, file.rs, mod.rs, COMMANDS.md) auto-merged;
only the additive test block in automation.rs conflicted. Resolved by
keeping main's tests and adding the two #369 tests.

# Conflicts:
#	src/app/automation.rs
2026-07-16 11:52:46 +03:00
Hakan SevenandClaude Opus 4.8 f028e93a33 fix(start): let the welcome page's own buttons work (#388, #389)
Donate, Send Feedback, OCS Web and Manage > About all answered "No
drawing open — use New or Open first." and did nothing.

Two doors guarded the welcome tab and the outer one shadowed the inner.
`dispatch_command` already knew which commands stand alone — About,
Donate, Report and the links were all on its list — but
`on_ribbon_tool_click` refused every ribbon event before dispatch was
ever reached.

The welcome page's own buttons emit RibbonToolClick, so they are only
ever clickable while `is_start` holds: the refusal covered exactly the
case they exist in, and they had never worked. New Drawing / Open File /
Plugins send their own messages, which is why the page still looked
alive.

So the ribbon door now defers commands to dispatch and keeps refusing
only what touches the scene. The policy stays in one place instead of
being half-remembered in two. A drawing tool is still turned away on the
welcome tab (#299) — the wording just comes from dispatch now.

The same gap was swallowing tools the issues never mentioned: CUI and
ALIASEDIT configure the application, not a drawing, and were missing
from the list, so fixing only the ribbon door would have moved their
refusal one door down rather than removing it. HELP and CUILOAD /
CUIIMPORT join them.

The new test drives the real message path and fails on the old code with
the reported wording. It reads the allowlist from source rather than
dispatching DONATE / REPORT / WEBVERSION, which shell out to a browser —
that also fails the day someone adds a welcome-page button and forgets
the list.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-15 16:29:42 +03:00