Commit Graph
161 Commits
Author SHA1 Message Date
Kevin GriffinandClaude Fable 5 d2af4c0b34 fix(plot): never fail silently on PDF export; add EXPORTPDF <path> (#369)
Export to PDF could end in complete silence: the flow hands off to an
async rfd save dialog, and when that future resolves to None the app
mapped it straight to Task::none(). None does not only mean the user
cancelled — on Linux the XDG portal SaveFile call can fail outright
(rfd then tries a zenity fallback, which may not be installed) and the
future still just resolves to None. rfd reports those failures only
through the `log` crate, and logging is opt-in (--log / RUST_LOG), so
the reporter saw exactly what #369 describes: no dialog, no error, no
console output, no file.

- EXPORT / EXPORTPDF now accept an optional path argument that exports
  straight to the file with no dialog — both the workaround for systems
  where the native save dialog cannot open and a headless-testable
  entry point. A relative path lands next to the drawing; a missing
  extension gets .pdf.
- The dialog-None arms (PlotExportPath / PlotWindowExportPath) now push
  a command-line message pointing at EXPORTPDF <path> instead of doing
  nothing.
- The success message prints the full output path, not just the file
  name, so users can find the PDF.

Verified end to end on the real binary via --serve: LINE + CIRCLE then
EXPORTPDF <path> writes a valid PDF with the expected geometry. Both
new tests fail on the previous code.

Fixes #369

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-14 13:38:16 -07:00
Hakan SevenandClaude Opus 4.8 3548de332c feat(block): BEDIT block editor as a dedicated space tab (#261)
Selecting a block and running BEDIT (command, ribbon button, or a
double-click on the block) opens a block-editor "space" tab, named after
the block, alongside Model/Layout in the status bar. The tab draws ONLY
the block record's own entities in block-local coordinates; edits are
live on the definition, so every INSERT updates. Save Block / Discard
buttons (side toolbar) finish the session; Discard restores the on-entry
snapshot. Space switching is locked while a block is open, and BEDIT and
REFEDIT are mutually exclusive.

Implementation: a Scene.block_edit_block override at the top of
current_layout_block_handle scopes render/pick/new-entity ownership to the
edited block while current_layout stays "Model" (so the pervasive
Model-vs-paper code keeps model behaviour). The model render/pick paths
that hardcoded model_space_block_handle (wire tile set, hatch render +
pick, block-instance meshes) are made binding-aware so the block isolation
actually takes effect and model content does not leak in. Entering fits
the camera to the block (fit_all already scopes via the binding). Renaming
the block tab renames the block itself — new Scene::rename_block re-keys
the record, updates the Block marker and repoints every INSERT reference.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-14 13:03:04 +03:00
Hakan SevenandClaude Opus 4.8 c0844ad55c feat(annotative): synthesize per-object contexts + editable UI
Make objects genuinely annotative — carrying real per-scale
representations that interoperate with other CAD apps — rather than just
a native flag. `create_annotation_context` synthesizes the extension-dict
chain (AcDbContextDataManager -> ACDB_ANNOTATIONSCALES -> per-scale leaf)
from the entity's placement; the editable Annotative toggle now covers
Text and block references as well as MText/MLeader; and a new
"Annotation Object Scale" dialog (OBJECTSCALE) adds/removes an object's
per-scale memberships. Requires the acadrust ObjectContextData encoder
(Cargo.lock bump).

Also fixes a render bug where objects carrying an *empty*
ACDB_ANNOTATIONSCALES (a single-representation marker with no per-scale
reps) were treated as annotative and (mis)scaled by the annotation factor
in scaled paper viewports, ballooning the text: an object is now
annotative-by-context only when its scale collection is non-empty, so
such objects render at their base geometry.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 22:38:29 +03:00
Hakan SevenandClaude Opus 4.8 d005cc4ab1 feat(props): make dim-line colour editable for leaders and dimensions
The "Dim line color" row on Leader and Dimension entities was
read-only: an earlier attempt wired it to Leader.override_color, which
acadrust never serialises, so the pick was lost on save. Store it
instead as a standard ACAD_DSTYLE per-object dimension-style override
(DXF code 176, an ACI index) through the existing dim_override codec,
so it round-trips through both DWG and DXF like the other dim
overrides. RGB picks collapse to the nearest ACI, matching the rest of
the dim-colour stack (dimension styles are index-only through the file
layer). The renderer prefers the override over the style's DIMCLRD.

The write branch is guarded to leaders and dimensions so a mixed
selection cannot stamp the override onto other entity types.

Bumps acadrust to c9fe982, which carries parsed XDATA onto dimensions
on DXF read (it was dropping common.extended_data), so the dimension
override persists on DXF save as well as DWG.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 17:31:09 +03:00
Hakan SevenandClaude Opus 4.8 70225de11f fix(annotative): make scale-manager rename and edits actually persist
Double-click rename did nothing and ratio edits were lost when switching
scales — and on drawings saved by other programs nothing could be edited
at all.

- Fallback scales were virtual (no Scale objects), so edit/rename no-oped;
  materialize the standard set into real staged objects when the manager
  opens, reverted on close unless applied.
- On third-party DWGs the mutators walked the root named-objects dict ->
  ACAD_SCALELIST, which can be unreachable, and matched dictionary keys
  that are internal ids (*A1, A0) rather than scale names. Locate the
  scale-list dict robustly and match scales by object name + handle.
- Rename rows inline via double-click; repair a dangling current-scale
  header pointer on staged rollback.
- Stage text/mleader style-manager edits on switch so they persist like
  the scale manager.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 14:37:33 +03:00
Hakan SevenandClaude Opus 4.8 cd766bc1f5 fix(annotative): stage scale-manager edits like the style managers
The scale manager now runs as a transaction, matching the style managers'
staging: New starts a blank editor draft (nothing is added to the drawing
until Apply), Delete removes immediately but staged, and closing the window
without Apply restores the snapshot taken on open — so a scale created or
deleted mid-session is discarded on cancel. Apply commits the staged list
changes as a single undo entry. This replaces the earlier behaviour where
New copied the selected scale and edits landed immediately.

A `changed` flag guards the commit so a no-op / failed Apply (blank or
invalid fields, or an edit that didn't apply) no longer dirties the drawing
or pushes a phantom undo entry. Set Current is deliberately outside the
transaction — it takes effect immediately like the scale pill (and syncs
CANNOSCALEVALUE), so it is never rolled back on close. Renaming the current
scale follows the pointer; editing one of the built-in fallback scales (shown
when the drawing has no scale list) materialises it as a real scale; the
snapshot restore touches only the ACAD_SCALELIST root entry.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 12:14:40 +03:00
Hakan SevenandClaude Opus 4.8 1071570f40 feat(annotative): annotation-scale manager window
Replace the scale popup's inline delete/add affordances with a single
"Manage scales..." row that opens a proper manager, matching the style
managers: it reuses the shared style-manager palette and chrome (toolbar +
list + editor frame), so it looks and behaves like the Text / Dim / MLeader
managers.

The manager lists the drawing's annotation scales (name + ratio, the current
one checked) and edits them through a Name / Paper units / Drawing units
editor: New adds a scale from the fields, Apply renames / re-ratios the
selected one, Delete removes it (never the current scale), and Set Current
makes the selected scale the model-space annotation scale. Backed by
Scene::add_scale / remove_scale / edit_scale on ACAD_SCALELIST.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 10:39:14 +03:00
Hakan SevenandClaude Opus 4.8 b1eba180eb feat(annotative): add/remove drawing annotation scales
The drawing's annotation-scale list (ACAD_SCALELIST) is now editable, so the
set of scales offered by the status-bar scale pill can be grown or trimmed:

- Scene::add_scale / remove_scale create or drop a Scale object and its
  SCALELIST dictionary entry (creating the SCALELIST dictionary, and
  registering it in the root, for drawings that carry none).
- SCALELISTEDIT gains ADD <ratio> / DELETE <name> alongside the existing
  listing (e.g. `SCALELISTEDIT ADD 1:50`).
- The scale popup (model space) grows a delete affordance on each non-active
  scale and a "+ Add scale..." row that primes the command line. The current
  annotation scale is protected from deletion.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 10:18:05 +03:00
Hakan SevenandClaude Opus 4.8 fc5e93dcc1 feat(properties): editable Annotative toggle for MText and MLeader
The Annotative row becomes an editable toggle for the two entity types that
carry a native per-object annotative flag — MTEXT (is_annotative) and
MULTILEADER (enable_annotation_scale). Turning it on flags the object
annotative (it then scales with the current annotation scale in model space);
turning it off clears the flag AND removes the per-object annotation context
(the AcDbContextDataManager -> ACDB_ANNOTATIONSCALES leaf subtree) plus the
legacy annotative XDATA markers, via scene::annotative::set_entity_annotative /
clear_annotation_context, so the object stops resolving annotative instead of
staying on through a stale context. The shared SCALE objects are left intact.

Style-derived types (text/dimension/leader/table/insert) keep a read-only
Annotative row — their state comes from the assigned style, and making a single
object annotative there needs a per-object context object (a dedicated
acadrust ObjectContextData writer) which is out of scope here. Verified an
existing annotative DWG round-trips its 134 per-object contexts through a
save/reload unchanged, so the far more common open-and-save path is unaffected.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 09:32:22 +03:00
Hakan SevenandClaude Opus 4.8 7cf1bf014b feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.

XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.

Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.

Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 00:28:42 +03:00
Hakan SevenandClaude Opus 4.8 984fa79189 feat(properties): make the Hyperlink field editable
The General-group Hyperlink row becomes an editable text field; committing a
value writes the standard PE_URL XDATA record (an empty value clears it). Adds
scene::view::dispatch::set_common_xdata, which rebuilds an entity's XDATA to
replace or remove a single application record — ExtendedData only appends, so a
plain add_record would leave a stale duplicate the reader would pick first.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 23:24:34 +03:00
Hakan SevenandClaude Opus 4.8 1b037a68aa feat(annotative): scale text/dims/tables/blocks at the annotation scale + editable property pickers
Render: annotative-ness is resolved centrally in scene::annotative::is_annotative
(per-object context dictionary, legacy XDATA, or annotative style) so the bake and
the properties panel agree. Text, dimensions, tables and blocks now display at the
current annotation scale in model space. An annotative block scales as one uniform
unit about its insertion point — its internal geometry and attributes are carried by
that scale instead of being scaled individually, fixing the block-attribute size
regression. Tables scale their column/row/margin geometry.

Properties: the handle- and flag-backed rows are now editable pickers — MLEADER
multileader style / text style / arrowhead / leader linetype, General Material, and
Plot style (named plot-style mode only; the color-dependent mode stays read-only,
which is correct). A picked name is resolved back to its handle in the update loop,
where the document is in scope. Annotative Yes/No and the annotation-scale row are
shown per object.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 23:16:44 +03: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
Hakan SevenandClaude Opus 4.8 dd3ce6767c feat(layout): reopen a drawing in the space it was saved in
Files saved while a paper layout was active reopened in Model space
because the loader hardcoded the current layout to "Model" and never
consulted the space recorded in the file.

The active space is a standard part of the format and acadrust already
round-trips it: $TILEMODE (header.show_model_space) for model-vs-paper,
and the CTAB current-tab variable for the exact layout name.

- Read: on open, pick the current layout from CTAB (when it names a
  layout that exists), else $TILEMODE (Model, or the first paper layout).
- Write: Scene::sync_active_space_to_document() mirrors the active layout
  back into the document (show_model_space + CTAB). It runs from
  set_current_layout, so every active-layout change — the status-bar tab,
  the Layout Manager's Set Current, create / rename / delete, and
  undo/redo — keeps the document in sync for the next save.

$TILEMODE is the guaranteed mechanism (read and written for both DXF and
DWG); CTAB refines the exact tab when the variable is present.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 01:01:40 +03:00
Hakan SevenandClaude Opus 4.8 57710eec2b fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).

Two-layer fix:

1. Coordinate backbone -> f64. last_point, last_cursor_world,
   dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
   the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
   paths, command-line coordinate entry and the coordinate readout all
   compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
   geometry, projection, UCS-icon/viewcube drawing) with casts only at
   those boundaries.

2. Per-command committed storage -> DVec3. on_point already received a
   DVec3, but many command impls narrowed it back to f32 (let pt =
   pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
   the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
   INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
   wipeout, attdef, table, tolerance, array center, mview, plot_window,
   plus the QDIM_PLACE handler and qdim_collect_points. Preview,
   hit-test and rubber-band paths keep f32 at the boundary.

Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 21:26:40 +03:00
Hakan SevenandClaude Opus 4.8 8ed10ceb2a feat(mtext): structured editor with selection, copy, and visible bold
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.

Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard

Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
  through the whole text pipeline; the resolved style font name is stamped
  so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector

Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 20:18:50 +03:00
Hakan SevenandClaude Opus 4.8 cf4074b0f2 fix(ribbon): make command icons inert on the Start page
On the Start page there is no drawing to act on, but ribbon panel
command icons still dispatched into the empty welcome tab. Guard
on_ribbon_tool_click: when the active tab is the Start page, close any
open flyout, show a "use New or Open first" hint, and do nothing. The
quick-access New / Open / Save buttons use a separate path and stay
available, as do the welcome page's own actions.

The icons are not yet visually greyed out (that needs an enabled flag
threaded through the ribbon render) — this stops them from running.

Refs #299

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 10:11:28 +03:00
Hakan Seven 816cb059dd Merge PR #353: fix View Cube toggle (highlight follows state, cube clears on hide)
External PR by Karim Jerbi (@KarimJerbi).
- NAVVCUBE is a command toggle so it never set active_tool; add a
  show_viewcube flag to the ribbon and special-case is_active_tool so the
  button highlights only while the cube is on.
- show_viewcube was absent from render_signature, so toggling a cached
  view produced an identical signature and left stale cube pixels; hash
  it into the signature so the toggle invalidates the cache and clears.
2026-07-11 09:20:32 +03:00
Hakan SevenandClaude Opus 4.8 6032a024d2 feat(save): native OS dialog for Save As, keep in-app format picker
Replace the custom in-app file-browser save dialog with the native OS
save dialog for choosing the destination path — folder browsing and
overwrite confirmation now come from the OS. A compact in-app modal
still selects the DWG/DXF format and version (a native dialog cannot
carry the version year) and appears only for Save As. Plain Save on an
unsaved drawing defaults to DWG 2018 with no picker. The file-name field
is shown only on web, where there is no native dialog.

Remove the hand-rolled directory listing, folder navigation, and
overwrite modal.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 09:12:36 +03: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
Hakan SevenandClaude Opus 4.8 5c4ba97c5f feat(thumbnail): embed a DWG preview on save, show it on the Start page
Generate a small wire-raster thumbnail when saving a DWG and embed it via
acadrust's preview API, so OCS drawings show a thumbnail in file browsers
and other CAD applications. Read the embedded preview back cheaply (file
header + preview bytes only, no full parse) to show a thumbnail per file
in the Start page's Recent Documents panel.

- io/thumbnail.rs: CPU rasterizer (current-layout wires -> BMP DIB) plus
  a preview-only reader/decoder (BMP/PNG -> iced handle) for the Start
  page.
- Embedded on save via stamp_thumbnail on both save paths.
- recent_thumbs cache filled on config load / recents change (never from
  a view); a thumbnail is drawn beside each recent file.

Requires acadrust at 3111694 (preview read/write), already pinned in
Cargo.lock.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 23:03:23 +03:00
Hakan SevenandClaude Opus 4.8 dc7117f203 refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).

- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
  <config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
  PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
  fields so only print preferences persist. Their bespoke .txt load/save is
  removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
  startup distributes into live state; every settings-change site
  (persist_settings_if_changed, recent add/remove/limit, status-bar pill
  toggle, ribbon density, plot commit) routes through save_config, which diffs
  against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
  separate hand-editable file.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 16:41:34 +03:00
Hakan SevenandClaude Opus 4.8 256f9a282b refactor(settings): drop Ortho/OSNAP from settings.txt, now per-drawing only
Ortho and the running object-snap set are stored in the drawing header
($ORTHOMODE / $OSMODE) since they were bound to the file, so persisting them
app-globally in settings.txt too was redundant. Remove the ortho, osnap and
snap_modes keys (and the now-dead modes_from / snap_id / snap_from_id helpers)
from UserSettings, and stop reading/writing them in apply_settings /
current_settings.

No boot behavior change: Snapper::new already defaults to the same running-snap
set (Endpoint, Midpoint, Center, Node, Quadrant, Intersection, Nearest) with
the master off, and Ortho defaults to false — exactly what settings.txt used to
seed. Old settings.txt files keep working; the stale keys are simply ignored.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 16:05:12 +03:00
Hakan SevenandClaude Opus 4.8 71db52a0a4 feat(statusbar): honor $COORDS in the coordinate readout, click to cycle
The Coords pill always showed live absolute X,Y,Z, ignoring the drawing's
$COORDS (coords_mode) sysvar. Drive the readout from it:

- 0 static  — the last picked point; the readout freezes between picks.
- 1 live    — continuous absolute X,Y,Z (previous behavior, default).
- 2 polar   — distance<angle relative to the last point while a command is
              prompting for a point; absolute otherwise.

The cursor and last point are reported in the active UCS, matching the rest of
the readout. Clicking the pill cycles the mode 0→1→2→0 (CycleCoordsMode), marks
the tab dirty and echoes it; SETVAR COORDS still works. $COORDS round-trips per
drawing via acadrust's coords_mode.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 15:54:57 +03:00
Hakan SevenandClaude Opus 4.8 da2c600953 feat(sysvars): persist Ortho, running OSNAP and Isolate in the drawing
Ortho and the running object-snap set were app-global (settings.txt only) and
Isolate/Hide was a session-only view state, so none survived save/reopen and
none were per-drawing. Bind all three to the file like AutoCAD:

- Ortho -> header.ortho_mode ($ORTHOMODE); running OSNAP <-> header
  .object_snap_mode ($OSMODE bitmask) via a new SnapType<->bit map in settings
  (osmode_from_snaps / snaps_from_osmode, round-trip tested). The header is the
  per-drawing store, live state mirrors the active drawing: adopt header->live
  on open and tab-switch, stamp live->header on save and when leaving a tab, a
  fresh drawing inherits the current values, and SETVAR ORTHOMODE/OSMODE now
  also update the live state. settings.txt stays as the boot/new-drawing
  default; the per-file header overrides it on open.
- Isolate/Hide now set each entity's persisted invisible flag (DXF code 60,
  which acadrust round-trips in DXF and DWG) in addition to the session set;
  End Isolation clears it. The set is reseeded from invisible entities on load
  and after undo/redo, the commands are undoable and mark the tab dirty. The
  renderer already skipped invisible entities, so no render change was needed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 15:45:08 +03:00
Hakan SevenandClaude Opus 4.8 e015fbe328 fix(stretch): free crossing-window corners from Ortho, draw filled marquee (#291)
STRETCH picks its crossing window as two corner points. After the first
corner set last_point, Ortho/Polar constrained the opposite corner to an axis
through it, collapsing the window to a line and making selection impossible.

Add CadCommand::window_corner_pick(); STRETCH returns true for its two window
steps, and the viewport skips the ortho/polar (and OTRACK) lock for that point
in both the preview and commit paths — the corners are now free.

Also render the window like a normal box selection instead of a bare green
outline: add SelectionState::preview_box (a hit-test-free marquee), draw it
with the shared draw_marquee helper (green crossing fill + stroke), and drive
it from the command's new window_first_corner(). Cleared on command end.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 13:32:32 +03:00
Hakan SevenandClaude Opus 4.8 91ba9f7699 feat(alias): centralize command aliases in user-editable ocad.pgp (#286/#288)
Command aliases (L→LINE, CC→COPYCLIP) were scattered across dispatch match
arms and CommandRegistration name arrays. Move them all into a single
user-editable file:

- assets/ocad.pgp: shipped default alias table (ALIAS,*COMMAND format),
  embedded and copied to config/ocad.pgp on first launch; only that user
  copy is read after. AA→AREA default closes #286.
- resolve_alias() rewrites the leading command token at the top of
  dispatch_command_inner, so aliases resolve for every path (families,
  plugins, Repeat menu). Args after the first space are untouched.
- Autocomplete hides alias keys and, when the whole input is an alias,
  surfaces its target command as the top (highlighted) suggestion so the
  highlight matches what Enter runs (AA→AREA, L→LINE).
- ALIASEDIT opens a new editor modal (add/edit/delete rows) with an Apply
  button; Apply persists to ocad.pgp, closing discards unapplied edits.
- Strip alias tokens from all 8 dispatch match-arm files and 71 module
  CommandRegistration arrays, keeping canonical/synonym/distinct-command
  names. ocad.pgp is now the sole source: deleting or remapping an alias
  there takes effect. CUI stays on the keyboard-shortcut panel.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 12:39:22 +03:00
Hakan SevenandClaude Opus 4.8 f5870b011d fix(text): show text in command previews (move/copy/etc) (#316)
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY,
PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/
scaled/stretched/mirrored, but those helpers moved only `points`, never
`text_verts`, and the ghosts' glyphs were never uploaded — so text
vanished (or froze at its source) during the preview, same root cause as
the grip-drag case.

Transform text_verts alongside points in all five helpers (shared
map_text_verts, reconstructing + re-splitting the double-single world
position), and gather preview_wires' glyphs into the per-frame
preview-text buffer in the render path. This covers every command
preview automatically. The grip-drag else branch no longer sets
preview_text explicitly (the render gather handles it); the grip-slide
fast path keeps its own scene.preview_text channel (empty preview_wires,
so no double-draw).

MIRROR honours MIRRTEXT: on -> true glyph mirror via mirrored(); off ->
readable relocation via translated().

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 10:49:13 +03:00
Hakan SevenandClaude Opus 4.8 67fe363775 fix(text): grip-drag no longer hides text (#316)
Dragging text by a grip hid it until release. The dragged entity is
hidden from the base wire set and shown as a preview overlay, but its
SDF glyph quads (text_verts) were never uploaded for the preview, so
the text vanished mid-drag.

Add a per-frame preview_text GPU buffer (sibling to preview_wires). A
rigid whole-entity move of pure text (TEXT/MTEXT, Square insertion grip)
slides the drag-start glyphs by the drag delta instead of re-shaping the
run every move; reshape grips (MTEXT width), dimensions and wire
entities re-tessellate and feed the same buffer.

Also fold preview_text into render_signature: a pure-text slide leaves
preview_wires empty, so without it the scene-render cache froze the
dragged text at its first frame (grip moved but text stayed, then
jumped to place on release).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 10:39:44 +03:00
Hakan SevenandClaude Opus 4.8 363ff614ec feat(polar): angle picker dropdown on the POLAR pill (#264)
The polar-tracking pill only right-click-cycled 15→30→45→90. Add an
AutoCAD-style angle picker opened from a caret on the pill: presets
90/45/30/22.5/18/15/10/5/1 plus a free-text field for any custom angle,
mirroring the existing units / scale status-bar popups. The chosen angle
persists like the other user settings.

Factor the shared chrome into one `split_pill(main, caret_msg, active,
open)` helper (+ `pill_text_color`) so the OSNAP and POLAR pills stop
hand-rolling the same outer border, background, caret and sizing — each
pill now only supplies its own main region. The caret is sized and
aligned to sit inside the single pill border on both.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 21:35:47 +03:00
Hakan SevenandClaude Opus 4.8 3c3ca89d97 feat(text): TEXTFILL — fill or hollow TrueType glyphs (#260)
TEXTFILL 1 (default) fills TrueType text; TEXTFILL 0 draws it hollow
(outline only). Settable directly or via SETVAR, 0/1 or ON/OFF. SHX/LFF
stroke fonts are unaffected.

Reuses the existing stroke-SDF path: bake_glyph treats a TTF glyph as a
stroke (outline) field instead of a signed fill field when TEXTFILL is
off. Toggling resets the glyph atlas and re-tessellates text so quads
pick up the re-baked tiles. Persisted in user settings.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 16:41:53 +03:00
Hakan SevenandClaude Opus 4.8 2367bdf47f feat(cmdline): Space submits, uppercase entry, > for literal spaces
Space now acts like Enter in the command line so commands advance
token-by-token (CAD convention). The whole value is handed to the submit
path, which tokenises multi-token lines, so a pasted or API-fed
`LINE 0,0 10,10` runs each space as a step separator instead of stopping
at the first token. A leading `>` switches to literal-space mode (for a
text string, a path, etc.) and is stripped on submit. All command-line
entry is shown uppercase.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 13:18:49 +03:00
Hakan SevenandClaude Opus 4.8 c45fe0267c feat(plot): QUICKPRINT/QP — plot a selection's bounding box to PDF (#325)
Run QUICKPRINT (or QP), select objects, then Enter: the selection's
bounding box becomes the plot window and a PDF is written next to the
drawing (<name>_<timestamp>.pdf) using the active page setup, with no
dialog. Model space.

Implemented as a selection-gathering CadCommand (src/modules/view/
quick_print.rs) returning CmdResult::QuickPrint(handles); the host
(on_quick_print_handles) unions the picked entities' AABBs, sets the
plot window, and reuses the tested clipped window-export path.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 12:57:35 +03:00
Hakan SevenandClaude Opus 4.8 a1ee4d9269 feat(plot): page setup manager — named setups, layouts, <none>/<previous>
Restructure the plot dialog into a style-manager-style master/detail: a
left list of page setups plus a right settings editor. The list shows
<none> (defaults + PDF) and <previous> (the settings the dialog opened
with), then the drawing's layouts (*Model*, *Layout1*, …) and its
standalone named page setups.

- Named page setups live in the ACAD_PLOTSETTINGS dictionary
  (src/scene/page_setup.rs: names/get/save/delete/rename, creating the
  dictionary if absent); DWG round-trip verified.
- Toolbar New / Copy / Delete | Set current / Preview / Print. Selecting
  a row loads it into the editor; Set current writes to the active
  layout. Copy duplicates a layout or a setup into a new named setup;
  Delete/Rename apply only to named setups.
- Scene::plot_settings_for(name) generalizes effective_plot_settings so
  any layout's settings can be loaded; on open the settings are
  snapshotted for <previous>.
- Default selection: model space → <none>; a layout → its own setup.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 12:25:46 +03:00
Hakan SevenandClaude Opus 4.8 8820b43127 feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.

- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
  print-quality, Resolution; Windows printto verb; list_printers via
  lpstat. Preview opens a temp PDF in the OS viewer (never a save
  dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
  clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
  config/plot.txt (printer, copies, quality, output options, area,
  scale); drawing-specific fields (offset/center/rotation) seed from the
  layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
  buffer state; fold on_page_setup_commit into apply_plot_page_settings.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 10:27:54 +03:00
Hakan SevenandClaude Opus 4.8 e1c19d8af3 perf(snap): keep snap/hover responsive on block-heavy drawings
Snap and hover hit-testing scanned the whole model wire set every cursor move; a heavily block-instanced drawing explodes to millions of wires, so this stalled for over a second per move. Add a cached spatial grid (wire_grid) keyed on the geometry epoch and query only the cursor neighbourhood via Scene::hit_test_wires_near. Gate the O(in-range segments squared) intersection passes on the in-range POINT count, not the wire count — a handful of curved wires, each hundreds of tessellated points, blew the quadratic up while the wire count looked modest. Give zero-size entities away from the origin (POINTs) a real cullable AABB, and fall back to a block sub-wire's own world points when its entity bounding box is degenerate.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 01:00:51 +03:00
Hakan SevenandClaude Opus 4.8 b5106c82b5 feat(hatch): editable gradient colors + background color
Gradient Type (One/Two color), Color 1 and Color 2 editable via a generic field-keyed colour picker path. Background colour editable and stored as HATCHBACKGROUNDCOLOR extended data (group 1071 true colour), rendered in OCS as a solid backdrop drawn behind the pattern. Hide Origin X/Y for gradients (pattern-only concept) and drop the redundant gradient rows from the pattern branch.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 23:35:27 +03:00
Hakan SevenandClaude Opus 4.8 c4fcfacb18 feat(draw): Arc Continue command; 3-Point default
Arc ribbon default + icon + dropdown order set to 3-Point. New ARC_CONT: tangent-continuing arc from the last line/arc endpoint, pick end only; Ctrl flips direction. Exit anchor recorded on commit (disambiguated by last pick) so chaining continues from the correct end.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 20:39:40 +03:00
Hakan SevenandClaude Opus 4.8 b3c8c0a2f1 feat(properties): navigate polyline vertices from the panel
The Current Vertex row is now a ◀ / ▶ stepper (new PropValue::Stepper)
showing "i / N". Stepping it moves the panel's focus through the
polyline's vertices — Vertex X/Y and the per-vertex start/end widths show
and edit the focused vertex instead of always the first — and the focused
vertex's grip is drawn hot (filled) in the drawing so it's visible while
navigating. Focus wraps around and resets to the first vertex when the
selection changes. Adds Area and Length to the LwPolyline Geometry group
(Polyline2D already had them). Applies to LwPolyline and Polyline2D.

The focused vertex is threaded to the per-entity property builder/editor
through a dispatch thread-local (like the curve-tolerance override), so
the PropertyEditable trait signatures are unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 17:53:05 +03:00
Hakan SevenandClaude Opus 4.8 b446786d7d feat(supporters): merge hand-maintained supporters, rank by amount
Add a MANUAL_SUPPORTERS list in patreon.rs for donations that arrive
outside Patreon; both the native and web boot paths merge it with the
fetched patrons and sort the combined list by amount (highest first,
then alphabetically). This also sorts the web supporters list, which
previously arrived unordered, and keeps hand-maintained supporters
visible when the Patreon fetch returns nothing (offline / no token).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 00:32:12 +03:00
Hakan SevenandClaude Opus 4.8 31ac5372d6 feat(command): clickable option buttons in the command line (#304)
Commands now expose their step options as clickable buttons rendered
inline beside the command-line prompt, so options need not be typed.

Adds a CmdOption/options() API to CadCommand: each command returns the
options for its current step; the command line renders them as
uppercase buttons and a click feeds the keyword to the command exactly
as if typed (an empty keyword submits like Enter).

Construction commands become "central" — CIRCLE offers [3P/2P/TTR/TTT/
DIAMETER], ELLIPSE [ARC/AXIS], ARC [SCE/SCA/SEA/SER/CSA/3P], RECTANGLE
[ROTATION/CENTER], POLYGON [CIRCUMSCRIBED/EDGE] — each keyword hands off
to the existing dedicated variant command via CmdResult::Dispatch,
instead of the variants only being reachable as isolated commands.

Migrates the existing bracketed-option prompts (PLINE, PEDIT, SPLINE,
SCALE, FILLET, CHAMFER, HATCH, GRADIENT, SOLID, POINT, RAY, XLINE, TRIM,
EXTEND) to the same button API and strips the option text from the
prompt strings, keeping value/coordinate hints. PEDIT's Width option
now enters a value sub-step so the button acts on click.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 00:16:59 +03:00
Hakan SevenandClaude Opus 4.8 cd7306f530 feat(text): MTEXT background fill / mask + text frame, editable in Properties
Render the MTEXT background (never drawn before — the flags were only shown
read-only in the panel) and make it editable.

- Text arm emits an opaque fill_tris rect behind the glyphs: flag 0x01 uses
  the entity background colour, 0x02 uses the canvas colour (a wipeout-style
  mask). Box = glyph bounds padded by (background_scale - 1) × text height.
- Text frame (flag 0x10) draws a closed rectangle in the text colour around
  the same box.
- Threaded `bg_color` into `tessellate()` (+ all callers and the
  MultiLeaderTess trait) so the mask can use the drawing-window colour.
- Properties: "Background mask" is a Off / Fill / Mask dropdown, "Background
  color" is its own colour picker (independent of the entity colour — new
  PropBgColor* messages; picking a colour switches to Fill mode), and "Text
  frame" is a checkbox toggle. Edits re-tessellate so the change shows live.

Renders via the face3d fill path, so the fill shows with FILLMODE on (same as
MLEADER background fill); the frame is a stroke and always shows.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 17:41:16 +03:00
Hakan SevenandClaude Opus 4.8 110683f419 refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).

- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
  cached by the tess memo, cloned on hit, and transformed by the block
  expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
  the strokes; render gathers text_verts from the viewport wire set
  (`gather_text_verts`, cached on wire content id) — the old collector is
  removed. Covers model, dimension, table, block-internal and paper-sheet
  text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
  SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
  entity_aabb which mis-places the box for MTEXT; composite paths no longer
  clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
  highlight, keyed on selection_generation) mirroring the selected-wire
  xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 17:01:36 +03:00
Hakan SevenandClaude Opus 4.8 9490c85b32 revert: drop PLOTWINDOW forced endpoint snap (#293)
Remove the forces_endpoint_snap mechanism merged in #293:
CadCommand::forces_endpoint_snap, Snapper::snap_forced_corners /
forced_corner_modes, the PlotWindowCommand override, the on_tick +
click-commit wiring, and the associated tests. PLOTWINDOW now uses the
normal snapper like every other command.

Rationale: forcing Endpoint snap on regardless of the global OSNAP master
overrides an explicit user setting and is surprising — a plot window is a
coarse print region that usually wants a margin, not a corner lock, and
the forced set only caught Endpoint (not center/mid/intersection) so it
was a partial, opinionated helper. Parking it pending a design discussion
with the PR author; can be reinstated behind a clearer UX (e.g. respecting
OSNAP, or an opt-in modifier) if justified.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 20:35:50 +03:00
Hakan SevenandClaude Opus 4.8 cb41910f12 fix: address review findings on merged #293/#292 hatch+snap
Three defects found reviewing the just-merged PRs:

- PLOTWINDOW forced corner snap (#293) only ran in the on_tick preview
  path, so the marker highlighted an endpoint but the click committed a
  plain snap — with the global OSNAP master (or Endpoint mode) off the
  corner landed at the raw cursor, defeating the feature. Apply the same
  snap_forced_corners branch at the click-commit recompute.

- build_dxf_pattern (#292) wrote the pattern line's LOCAL step into the
  world-frame HatchPatternLine.offset. The new prebaked reader
  inverse-rotates offset assuming world frame, so app-created hatches
  (HATCH command) collapsed their spacing by cos(angle) — ANSI31 at 45deg
  rendered 2.245 instead of 3.175 on both viewport and PDF/plot export.
  Rotate the local step into world here so it round-trips and the stored
  offset is format-correct for other CAD apps.

- far_from_origin_pattern_hatch_still_fills placed its offset ALONG the
  45deg lines (projects to k~0), so it never exercised the span-cap fix and
  passed even with the old absolute-index clamp restored. Move the offset
  perpendicular to the lines so |k| >> the cap. Add
  app_created_hatch_roundtrips_catalog_spacing to guard the offset-frame
  fix (verified: fails at 2.245 without it, passes at 3.175 with it).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 20:23:59 +03:00
sLuCHa 6cd422e5b9 feat(plot): snap plot-window corners to geometry endpoints
PLOTWINDOW / PW now forces Endpoint object-snap while picking its two
corners, independent of the global OSNAP toggle, so the plot area lands
exactly on frame corners. The snapper's master toggle and configured
modes are saved and restored around the pick, so the user's OSNAP
settings are unchanged afterward.

- CadCommand::forces_endpoint_snap() opt-in (default false; PlotWindowCommand true).
- Snapper::snap_forced_corners() forces Endpoint only, then restores state.
  Intersection is deliberately excluded: it is O(n^2) over the hit-test wire
  set and froze the UI on large drawings when a plot window is picked
  zoomed-out. Frame corners are endpoints, so Endpoint alone suffices.
- the cursor-move handler routes the plot-window pick through the forced path.

Works in model and paper space (shared pick path). Tests: opt-in flag;
forced mode set is endpoint-only; snapper state is restored after the call.
2026-07-06 13:38:56 +02:00
Hakan SevenandClaude Opus 4.8 b50921923f chore: post-merge cleanup for #287/#289
- Reword the two new explanatory comments to describe the behaviour on its
  own instead of citing another product.
- window_to_sheet: drop the unused window_min parameter (the caller already
  derives its coordinate offset from the returned min-corner position) and fix
  the doc comment to match what the function returns.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 01:22:10 +03:00
Hakan Seven dd4f678c41 Merge PR #287: plot a model-space window to PDF 2026-07-06 01:17:03 +03:00
Hakan SevenandClaude Opus 4.8 f4ed263425 feat(ui): ribbon collapse-mode selector + start-page recent controls
Ribbon:
- A ▾ selector at the right of the tab row picks the tool-panel density —
  Auto (size to window), Full, Compact, or Collapsed. The choice is persisted.
  It hides itself when the tool row is already tight, giving the cramped tab
  row its space back.

Start page:
- The Recent Documents card is restyled to match the Supporters card
  (rounded corners, 20px padding, matching title + colours).
- Its scrollbar no longer sits on top of the row ✕ remove buttons (a gutter
  is reserved).
- A footer control sets how many recent files to keep: [-] an editable count
  [+] with the max shown. Typing applies on Enter; an over-max entry snaps to
  the max. The limit is persisted and enforced on load and on every open.
- The "Support on Patreon" heart is now the SVG icon instead of a font glyph.

Config:
- Add a shared `config::config_dir()` helper; the recent-files and status-bar
  stores now go through it instead of each re-deriving the path. New minus.svg
  icon for the stepper.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 01:06:01 +03:00
sLuCHa 363593b09f feat: plot a model-space window to PDF
Pick a rectangular window in model space with the existing PLOTWINDOW command (now enabled in model space, previously paper-space only) and export it to a PDF at a chosen ISO paper size (A4-A0), orientation and scale (fit or ratio). The window is a hard clip boundary. Adds paper-size presets, a window-to-sheet transform, scale + clip support in the PDF exporter, and Page Setup controls (format/orientation/pick window).
2026-07-05 23:13:43 +02:00