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 <noreply@anthropic.com>
Holding Shift while a command asks for a point captures the nearest
polar increment (or ortho axis, UCS-aware) from the rubber-band base
toward the cursor and pins the pick to that ray. Every candidate —
osnap hits included — projects onto the locked direction, so a snap far
off-axis contributes only its along-axis component; the click commits
exactly what the preview shows. The direction stays fixed while Shift
is held and drops the moment it releases. Grip drags get the same lock
from the grip origin.
Guards: engages only when the active command wants a point — never
during entity picks (Shift there is the TRIM/EXTEND swap), tangent
picks, selection gathering or window-corner picks.
Closes#312
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
iced's clipboard rides window_clipboard, which has no wasm backend — a
focused text_input captured Ctrl+C/V on the web and did nothing.
Ctrl+V (captured, wasm only): read the async browser clipboard (the
keypress is a user gesture, so it's permitted) and replay the text as
synthetic KeyboardEvents on the winit canvas — the only route into a
focused text_input, since iced exposes no insert-text operation.
Control characters are dropped and the length is capped. The MText and
inline-TEXT editors keep their dedicated paste paths (no double feed),
and an unfocused Ctrl+V still runs PASTECLIP.
Ctrl+C (captured, wasm only): a widget operation pairs each text_input
with its focus state (iced fires both callbacks back-to-back on the
same widget) to collect the focused field's visible text, then writes
it with navigator.clipboard.writeText. Whole-field text, not the
selection — iced doesn't expose the selected range.
Native behaviour is unchanged.
Closes#346
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shift+right-click in the viewport opens a cursor-anchored grid of snap
icons (names as hover tooltips). Picking one engages a one-shot
override: only that snap applies — even with running osnap off — until
the next point pick commits, which restores the saved configuration.
Esc drops an unconsumed override; an outside click closes the menu.
The status-bar snap toggles are untouched — this is a separate,
temporary mechanism.
Covers items 1 and 2 of #337 (mid-between-two-points still pending).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dropping a supported file (dwg / dxf / bak / sv$, case-insensitive)
opens it through the existing open flow: an already-open drawing
switches to its tab, a load in progress queues the drop behind it
(multi-file drops arrive one event each), anything unsupported reports
on the command line instead of failing silently in the parser.
Platform support follows winit 0.30: Windows, macOS and Linux/X11
(incl. XWayland) deliver FileDropped; the Wayland backend does not
implement file drag & drop yet, so drops are inert there until winit
gains it.
Closes#344
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Block reference Name (Misc) is editable: the dropdown re-points the
insert to another definition; typing a new name renames the definition
everywhere (record re-key, Block marker, every INSERT reference).
Anonymous (*) and xref blocks stay read-only, with the same guards
added to Scene::rename_block.
- Custom single-control dropdown (text field + caret) built on the
shared floating_below overlay so the list always opens downward,
unlike iced's space-based menu placement.
- Consecutive "<Base> X/Y/Z" rows (Position, Scale, Start, Center, ...)
collapse into one expandable summary row across all entities; the
expand state persists across rebuilds and selection changes.
- The BEDIT block tab no longer offers the layout Rename/Delete
context menu.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
The ribbon layer dropdown gets a live search field above the list
(cleared when the dropdown closes, panel height tracks the filtered
count), and the Layer Manager toolbar gets one too. Both filter by
case-insensitive substring while toggle/rename messages keep the
original row indices, so acting on a filtered row hits the right layer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A thin divider after the Name header resizes the column by drag
(60-640 px, session-persistent), riding the shared modal drag flow.
The name elide budget follows the column width, so widening the
column reveals long layer names; the hover tooltip stays for overflow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'>' button after the Command: label turns the leading-'>' literal-space
behavior into a mode: while on, Space stays in the line instead of
submitting, until toggled off. State persists in settings.json; a
hand-typed leading '>' still works for a single line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Property edits on a hatch were invisible: fills render from the
prebuilt model cache, which move/copy refreshed but the properties
path never did — so a pattern-scale change (which must also rescale
the stored final pattern lines by the ratio) changed nothing on
screen (#415). invalidate_property_targets now rebuilds the edited
fill's cached model via Scene::refresh_fill_model.
The background row gains an on/off box: off removes the
HATCHBACKGROUNDCOLOR record entirely (the colour could never be
reverted before), on seeds ByLayer. The codec covers the full
colour-method range (ByLayer / ByBlock / RGB / ACI) and the backdrop
resolves ByLayer / ByBlock through the style chain. "More Colors…" on
the background picker now opens the palette window targeting the
background colour — it used to just close the picker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A selected INSERT's Geometry section gains a "Uniform scale" box
(#427): while the three factors are equal it is checked and the panel
shows a single Scale row that writes all three axes; unchecking
expands the familiar Scale X/Y/Z rows without touching the values
(remembered per entity), and re-checking collapses Y/Z onto X as an
undoable CHPROP step.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Source half of b672023 (which captured only the removed bundled
thumbnails — the add aborted on the already-staged deletions): the
videos module (playlist scan, oEmbed titles, config-dir thumbnail
cache), the boot fetch on its own thread with the loading state, the
overlay rail + Videos tab on the Start page, and the earlier tabbed-
layout handover.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Route the MTEXT editor through the shared in-canvas modal frame with an
Apply-at-top toolbar (style-manager style): Apply commits and keeps the
editor open (creates on first apply, updates in place after), the ✕
cancels. Give every modal a bottom-right corner resize grip driven by a
shared grow-only `modal_resize` delta that resets with `modal_offset` on
open/close, so each dialog opens at its natural size and can be dragged
larger — style managers included.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The AppImage ships both the .desktop file and the hicolor icon, yet the dock
button came up blank on Wayland. A Wayland compositor has no StartupWMClass to
key on — it matches a window's app_id against the basename of an installed
.desktop file to find its icon — and iced defaults window app_id to an empty
string, which OCS never overrode, so nothing matched.
Set application_id to the reverse-DNS id the .desktop is installed under,
reusing the existing file_association::APP_ID const rather than a second copy.
iced enables both the x11 and wayland winit backends by default and feeds the
same application_id to each: on Wayland it is the app_id, on X11 it becomes
WM_CLASS. So this also changes the X11 WM_CLASS, and the old
StartupWMClass=OpenCADStudio would no longer match — realign it to the same id
so X11 keeps working too.
Confirmed on COSMIC/Wayland: the dock icon now appears. This is the Linux half
of the same class of bug as the Windows taskbar report (#392, still open): the
window not carrying the identity the OS needs to find its packaged icon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Double-clicking a drawing started a whole second copy of the app. Now it
lands as a tab in the editor already open, and that window comes forward.
The election is the bind itself: every GUI launch tries a deterministic
per-user loopback port, and the OS grants it to exactly one process and
releases it on any death, SIGKILL included. Nothing to reap, no PID to
probe, no stale lock after a crash. The winner serves the port from an
iced subscription; the loser checks it is talking to a matching editor,
hands the paths over and exits. Every surprise — no answer, a stranger on
the port, a timeout — boots a normal window, i.e. the old behaviour: the
feature can degrade, but it cannot lose the file.
The gate in main is positional, after every headless mode has returned.
The plugin runner is this same binary re-spawning itself, so a flag list
would rot the first time a mode is added; a position cannot.
Handing a drawing over that is already open switches to its tab instead
of loading a second copy, comparing resolved paths so a symlink or a
`..` hop is still recognised as the same file.
`opening` is a single slot that a second open would overwrite, and
on_file_opened drops any result arriving once it is clear — so drawings
handed over at once would silently vanish. They queue in pending_opens,
drained from every path that clears the slot, error and cancel included.
Exec becomes %F so one launch takes the whole selection; the CLI takes a
list and boot fans it out through the same door. --new-instance forces a
separate process; --read-only / --script / --new always get their own.
Not reused: the automation server. It dispatches arbitrary commands and
writes arbitrary paths, and its open replaces the active tab's document
in place — publishing that on a port every local process can reach would
be unforced privilege escalation. Only its JSON-lines framing is shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WebGL2 has no vertex/fragment storage buffers, so the batched storage-buffer
hatch renderer (hatch_gpu) is disabled on wasm and real hatch fills never
rendered on the web build — the pick highlighted the boundary and the command
then appeared to cancel (issue #204).
Add a wasm-only per-hatch renderer (hatch_web_gpu + hatch_web.wgsl) that packs
the variable-length boundary / family / dash arrays into one RGBA32F data
texture read with textureLoad, instead of the fixed-size uniforms of the
wipeout path. This removes the MAX_FAMILIES (16) / MAX_HATCH_BOUNDARY_VERTS
(1024) / MAX_DASHES (128) caps, so every hatch — solid, gradient, and
arbitrarily complex line patterns — renders on the web. The fill algorithm
(in_polygon, per-family line/dash/dot, gradient, LOD, double-single vertex
transform) is identical to the desktop path; only the array ACCESS changes.
Pattern phase is anchored at world_origin with the boundary stored raw, and
scale / gradient-range clamped, to match the desktop batched renderer exactly.
Native (hatch_gpu + hatch.wgsl) and the wipeout mask renderer are untouched;
the new module and pipeline are gated on target_arch = "wasm32".
Also silence the resulting (and pre-existing) wasm-only dead-code warnings for
native-only paths so both the native and web builds compile clean.
Closes#204.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
When the viewport got too narrow the top-left render-mode bar and the
top-right ViewCube overlapped. Hide them adaptively, measuring the render
bar's real width rather than guessing:
- Wrap the render bar in DensitySwap, which measures its natural width every
frame and swaps it for an empty spacer once it no longer fits the tile. Add
report_width0 so it publishes that measured width to render_bar_w.
- Compute viewcube_visible from that measured width: the cube shows only while
bar_w + gap + cube region fits. This one flag drives the ViewCube nav/UCS
widgets, the hover hit-test, and — threaded through build_viewports /
build_viewport_for_pane / viewport_data_for — the GPU cube itself, which
previously ignored the pane flag (show_viewcube was inst.active only), so
the cube stayed while its surrounding widgets vanished.
Result: the ViewCube gives way first, then the render bar once it too has no
room — no overlap, sized to the bar's real width (which varies with the
render-mode label, split buttons and tile count).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
COPYCLIP/CUTCLIP used the mean of each entity's bounding-box centre as the
paste anchor. Standard clipboard behaviour puts the lower-left corner of the
box enclosing all copied items under the cursor. Union each entity's bbox min
corner instead, and rename clipboard_centroid -> clipboard_base (COPYBASE also
stores a picked point there, so "centroid" was inaccurate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
External contribution by sLuCHa. A dimension draws from its baked *D block
(baked in absolute WCS); the in-drawing COPY re-points block_name to a
transformed copy (#161) but clipboard PASTE did not, so a pasted dimension
rendered at the source location (same drawing) or was missing (cross drawing).
Copy now snapshots each copied dimension's *D block into the clipboard
(dim_blocks); paste builds a fresh copy transformed by the paste offset and
re-points block_name — same- and cross-drawing alike, no orphan blocks.
Refactors out shared define_transformed_block / snapshot_block helpers
(behaviour-preserving).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Copy-paste (CTRL+C / CTRL+V) dropped dimensions. A dimension draws from
its baked *D block, whose geometry is in absolute WCS coordinates, and
the clipboard paste path added the dimension entity but never gave it its
own transformed block — unlike the in-drawing COPY, which does (#161). So
the pasted dimension kept pointing at the source block and rendered at
the original location instead of the paste point (same drawing), or had
no block at all (cross-drawing).
Capture each copied dimension's *D block into the clipboard, and on paste
build a fresh transformed copy of it and re-point the dimension's
block_name — for same- and cross-drawing pastes alike. Factored the
block-building out of clone_transformed_block into a shared
define_transformed_block, and the block snapshotting out of
capture_blocks into snapshot_block.
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 <noreply@anthropic.com>
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).
When the window is too narrow for the full properties panel it now
collapses to a slim vertical "Properties" bar (its label drawn rotated 90°
on a canvas), freeing width for the viewport. Clicking the bar expands the
panel back out alongside the bar; clicking it again re-collapses
(props_expanded state + TogglePropertiesBar). Reuses the rotated-label bar
helper originally built for the Start page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuild the Start page as three parts — Recent Files (left), Welcome
(center) and Supporters (right) — that sit side by side while they fit.
When the page is too narrow for all three it falls back to a tab bar
(Recent Files / Welcome / Supporters) showing one section at a time, with
Welcome open by default (new StartSection state + StartSectionSelect).
The Recent Documents list moves off the left properties slot and into the
page itself. The Welcome column's button rows now flex-wrap, and the two
intro-video cards sit side by side, wrap to a stacked column when the
width gets tight, then shrink to fit the remaining height so they never
spill over the status bar. Trimmed the headline to just "Open CAD Studio"
(dropped "Welcome to" + the subtitle) and the "Release Notes" button, and
tightened the page's side padding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All ribbon dropdowns (tool dropdowns, undo/redo history, and the layer /
colour / linetype / lineweight / style combos) and all status-bar popups
(scale, units, OSNAP, isolate, filter, customization menu, layout list)
now open directly at their widget instead of at a layout-estimated
position, and flip to the other side when the panel would cross a window
edge.
Each dropdown/pill widget is wrapped in a transparent PosReport that
records its screen bounds into a thread-local map on draw. The overlays
read those bounds:
- Ribbon: Ribbon::dd_anchor(id, panel_w, win_w) returns
(align_right, h_pad, top); position_ribbon_dropdown places the panel
below the widget, flipping right-aligned near the right edge.
- Status bar: popup builders take (pill_bounds, win_size) and call
popup::position_statusbar_popup / popup_anchor, opening just above the
pill and flipping left/right at the window edge.
The window size is now tracked (win_size, set on WindowResized). The old
layout estimators compute_dropdown_left / compute_layer_combo_left /
compute_prop_combo_left / compute_history_dropdown_left and
osnap_popup_right_offset are deleted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Third stage of the adaptive tool area (shrink -> compact -> collapse).
When even the compact panels don't fit, DensitySwap picks a new
CollapsePanels widget that keeps a left prefix of panels inline and
collapses the rest to buttons; clicking a button opens the full panel as
an overlay flyout anchored below it (closes on outside press, mirroring
color_select's Floating).
A collapsed panel's button shows a live representative tool — the panel's
last-used tool (tracked per panel in Ribbon::note_panel_tool on every
tool click), defaulting to its first — rendered as a large icon. To
support this, render_small now also renders Large* items small and
render_large now renders plain Tool/Dropdown items large. Large dropdowns
render as icon + label + a bottom arrow strip via a shared
render_large_dropdown helper, used in both the full ribbon and collapsed
panels.
Ribbon dropdowns (tool dropdowns + undo/redo history) now anchor their
overlay directly below their widget at any size: a transparent PosReport
wrapper records each dropdown button's screen bounds into a thread-local
map on draw, and the overlays read it via Ribbon::dd_anchor instead of the
layout-estimating compute_*_left helpers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Layer Manager already applied colour/linetype/lineweight in bulk;
extend that to the rest:
- Visibility, lock and freeze toggles now act on every selected layer, not
just the clicked row. The clicked row's new state becomes the target for
all. Transparency edits apply to the whole selection too.
- Clicking a linetype/lineweight combo on a row that is already part of a
multi-selection no longer collapses the selection (the whole row is one
mouse_area whose plain-click select would otherwise reset it), so those
edits stay bulk.
- Ctrl/Cmd+A selects every layer row while the Layer Manager is open, and
all drawing objects otherwise.
Two now-unused Scene layer-toggle helpers are removed (the handlers set the
flags directly and bump once).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>