Commit Graph
161 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.8 19a7343c80 feat(properties): collapse the panel to a vertical bar on a narrow 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>
2026-07-05 21:02:05 +03:00
Hakan SevenandClaude Opus 4.8 e2a56033d4 feat(start): three-part start page (Recent · Welcome · Supporters) that tabs when narrow
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>
2026-07-05 20:53:11 +03:00
Hakan SevenandClaude Opus 4.8 39043e6e56 feat(ui): anchor every ribbon dropdown and status-bar popup to its widget, flipping at window edges
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>
2026-07-05 18:51:54 +03:00
Hakan SevenandClaude Opus 4.8 56e48b8d0f feat(ribbon): collapse overflowing panels to flyout buttons, anchor dropdowns to their widget
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>
2026-07-05 18:04:55 +03:00
Hakan SevenandClaude Opus 4.8 3bcb5a04c5 feat(snap): mark the parallel reference line + toggle it off on re-hover
Two refinements to Parallel snap. The acquired reference now keeps a
point on the line and draws a ∥ marker there, so the user sees which line
the parallel is measured from. And dwelling on that same reference line
again removes it (toggle), matching how OTRACK tracking points clear;
dwelling on a different line switches the reference. Same-line detection
uses parallel direction plus perpendicular screen distance, so it holds
across zoom.

Refs #277

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:50:33 +03:00
Hakan SevenandClaude Opus 4.8 e85cec8274 feat(snap): parallel object snap (#277)
Parallel snap did nothing — the type was in the osnap set, priority and
marker, but there was no logic behind it. Implement it: hovering a line
(or polyline segment) for a short dwell acquires its direction as a
reference; then while drawing from the last point, when the cursor runs
parallel to that reference the point locks onto the line through the last
point parallel to it, showing the ∥ marker and the alignment guide.
Curves (circle/arc/ellipse) are ignored — parallel to a curve is
undefined. Works with only the Parallel object snap on, independent of
OTRACK; the reference clears with the command's tracking state.

Closes #277

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 13:24:43 +03:00
Hakan SevenandClaude Opus 4.8 e366e47b05 feat(draw): LINE deferred tangent — a line tangent to two circles
A line whose FIRST point is a Tangent snap on a circle can't be fixed
until its direction is known, and a line tangent to TWO circles is their
common tangent — neither is resolvable at a single pick. A plain LINE
dropped the tangent object, so it drew a wrong line (TAN-TAN between two
circles).

Add an opt-in on_point_with_tangent hook (default ignores the tangent, so
every other command is byte-for-byte unchanged) and route the running
Tangent object through the point commit. LINE defers a first-point circle
tangent and resolves it on the next pick: against a second circle tangent
it picks the common tangent of the two circles (up to four — chosen by
the two cursor hits, so hovering selects inner/outer + side); against a
plain point it draws the tangent from that point to the circle.
last_point syncs to the resolved anchor. Common-tangent and point-tangent
math is f64 with perpendicularity unit tests.

Refs #274

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 13:06:36 +03:00
Hakan SevenandClaude Opus 4.8 13de968ee9 perf(xref): fix O(n²) AcDs parse and skip host re-tessellation
Opening a drawing that xrefs several 3D-heavy DWGs (e.g. Clinic.dwg with
5 discipline files) took ~137s, effectively all of it inside the DWG
parser's AcDs SAB blob extraction.

Bump the acadrust pin to ceef045, which bounds the AcDs end-marker search
so a missing marker family no longer scans the whole buffer per record.
Total xref resolve for Clinic.dwg drops from ~97s to ~4s.

Also make the post-xref mesh pass incremental: the host solids were
already tessellated by the background loader and the merge assigns fresh
handles to every imported entity, so `populate_missing_meshes_from_document`
keeps the cached host meshes and tessellates only the merged xref solids
instead of clearing and rebuilding the whole document.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 00:38:38 +03:00
Hakan SevenandClaude Opus 4.8 77c0c5a9d0 feat(plugin): surface loaded plugin commands in command-line autocomplete
Autocomplete suggestions came only from all_registered_command_names() —
the compile-time inventory registry returning &'static str — so
runtime-loaded plugin commands could never appear. Typing a plugin's
command prefix (e.g. `LS_`) showed nothing even with the plugin loaded
and its ribbon tab active; dispatch worked, only discovery was blind.

Collect each enabled plugin's ribbon ToolDef command ids plus its
manifest command_prefixes into a dynamic candidate pool
(OwnedRibbonGroup::command_ids → PluginManager::command_names →
plugin_command_names), refreshed in rebuild_ribbon_modules on startup
load, settings reload, and every enable/disable toggle. ranked_matches
now merges this pool with the static registry and returns owned strings.

A plugin.toml `commands = [...]` list for sub-verbs with no ribbon button
(e.g. LS_AUTOLABEL) stays a follow-up — it's a plugin-API surface change.

Closes #272

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:01:53 +03:00
Hakan SevenandClaude Opus 4.8 d644a85c72 feat(layers): alphabetical default order + scroll to a new layer
The Layer Manager table and the ribbon's quick layer dropdown both
showed layers in document (insertion) order. Default the panel sort to
Name so both list layers alphabetically; a header click still re-sorts
by any other column. `apply_sort` reorders the panel's layer vector in
place, which the ribbon dropdown reads, so one default covers both.

With alphabetical ordering a freshly created layer can land anywhere in
a long list, so give the table scrollable an id and scroll its row into
view after creation, keeping the rename prompt visible.

Closes #270, closes #271

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 22:28:07 +03:00
Hakan SevenandClaude Opus 4.8 450dbf79ac fix(dynin): read a comma-separated second point as cartesian, not W/H
A rectangle's opposite corner is entered through Width/Height dynamic
fields, whose sign is taken from the cursor quadrant on commit. Typing
"50,50" is a cartesian coordinate pair, not two cursor-signed
dimensions, but the comma advance kept the Width/Height roles so the
committed corner mirrored into whatever quadrant the cursor sat in.

Key off `dyn_user_reshaped` (set only by `,`, never by Tab) to treat a
comma-separated entry as a literal cartesian coordinate for every field,
in both the point resolver and the live dyn-box display. Tab-separated
entry still reads as signed Width/Height dimensions.

Closes #269

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 22:27:59 +03:00
Hakan SevenandClaude Opus 4.8 c3e86d08cc feat(snap): OTRACK ortho lock + edge-extension tracking (#218, #219)
#218: with Ortho on and a base point the axis is now a hard lock — a lone
tracking ray can't pull the cursor off it. Only a tracking ray crossing
the ortho axis locks (the corner-finding case); otherwise the ortho
constraint applies as the fallback, so Ortho reliably locks when OTRACK
is also on.

#219: an acquired corner now contributes an alignment ray along its own
edge direction(s) (captured once at acquisition), so OTRACK follows a
segment's extension, not just the ortho/polar axes. Acquisition is also
captured on leave — a perfectly still cursor emits no move events, so
"pause on a corner, then drag" now reliably acquires it. The acquired
marker projects through the active pane's camera + rect (canvas offset
included) instead of vanishing, and the active alignment path is drawn
as a full-length dashed guide through the corner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:34:20 +03:00
Hakan SevenandClaude Opus 4.8 0322128f48 fix(snap): extension guides stay visible at extended intersections (#247, #259)
Extension osnap now offers a ray off every polyline vertex, not just the
first and last, and snaps to where two segments' extension lines would
cross. That crossing is emitted as an Intersection so it outranks the
per-segment extension feet on their own lines. When it wins, both
contributing extensions are re-detected — off-ray distance measured in
screen space so the tolerance holds at UTM coordinates — and the overlay
draws a dashed guide from each endpoint through the crossing, so both
extension paths remain visible instead of vanishing when caught.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:46:08 +03:00
Hakan SevenandClaude Opus 4.8 818638d3aa feat(ui): bulk layer on/off, lock, freeze, transparency + Ctrl+A (#236)
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>
2026-07-03 16:06:10 +03:00
Hakan SevenandClaude Opus 4.8 f9b6834ad9 feat(export): exact ACIS geometry for planar solids on save
OCS models 3-D solids with truck, but extrude/boolean results carried no ACIS
body, so other CAD apps dropped them on open. Add an exact planar exporter and
run it at save time.

- scene::convert::acis_export::planar_solid_to_sat walks a truck Solid whose
  faces are all planes bounded by straight edges into a vertex/face-ring mesh
  and builds an exact ACIS body via acadrust's build_planar_body. Curved
  faces/edges return None (left for the NURBS path). A signed-volume check
  flips the winding if the shell comes out inside-out.
- OpenCADStudio::sync_truck_solids_to_acis fills in ACIS geometry for every
  cached truck solid that still has none, just before a Save / Save-As, so the
  written DWG/DXF carries real modeler geometry.
- Bump acadrust to 9142a50 (build_planar_body + classic-ACIS AcDs roundtrip fix).

Booleans (already cached as truck solids) now export exact geometry. Extruded
polygons will too once the EXTRUDE command caches its truck solid (a separate
change kept out of this commit because command_driver.rs holds unrelated
in-progress edits).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:43:37 +03:00
Hakan SevenandClaude Opus 4.8 a24232842c feat(commands): prompt for a system-variable value on the next line
Typing a bare system-variable name (MIRRTEXT, FILLMODE, LTSCALE, …) now
echoes the current value and prompts "Enter new value for <NAME> <cur>:",
capturing the next command-line entry as the new value — Enter keeps the
current one, Escape cancels. The single-line "MIRRTEXT 1" form still works.
Mirrors the awaiting_vports pattern: a pending_setvar field on the app,
consumed at the top of on_command_submit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:42:55 +03:00
Hakan SevenandClaude Opus 4.8 7bab7c7b8d feat(ui): replace app-menu logo with a quick-access file toolbar
Remove the OCS-logo button and the dropdown menu it opened; the top strip
now shows direct New / Open / Save / Save As / Print buttons ahead of the
undo/redo controls. Drop the HELP and OPTIONS commands (HELP dumped text to
the command line, OPTIONS aliased the drafting-settings popup).

The app-menu overlay is gone, so its rendering code, MENU_ITEMS, four dead
messages, and the LOGO_RED / GEAR / HELP assets are removed. The recents
list it held is the only survivor — moved into OpenCADStudio.recent_files
via a new app/recent.rs, backing the Start page's Recent Documents panel.

Quick-access icons are redrawn in a cleaner style (file-plus, folder-open,
device-floppy, file-export, printer) and tinted light so they read on the
dark strip; FOLDER / DOC keep their old glyphs for the file browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:51:49 +03:00
Hakan SevenandClaude Opus 4.8 123d4e9083 feat(properties): make read-only values selectable text instead of click-copy
Replace the click-to-instantly-copy read-only cell with a non-editable but
selectable text field: the user selects the value (drag / double-click /
Ctrl+A) and copies with Ctrl+C, the normal expected gesture. The field
carries the full un-truncated value, styled flat (no input box or focus
ring) so it still reads as read-only but with a visible selection colour.
Keystrokes route to Noop, so it can be selected/copied but never edited.

Removes the PropCopyValue message + handler and the mouse_area copy hook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:14:37 +03:00
Hakan SevenandClaude Opus 4.8 c59a584d14 feat(properties): copy read-only property values to the clipboard
A read-only value cell is now click-to-copy: clicking writes the full,
un-elided value to the system clipboard (copy cursor on hover). One
change in render_ro_row, so it covers every read-only row for every
entity. Adds Message::PropCopyValue routed through iced::clipboard::write.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:06:00 +03:00
Hakan SevenandClaude Opus 4.8 a763144622 fix(xref): tessellate xref'd solids so they render (#203)
XREFs are merged into the document on the UI thread AFTER the background
worker already built the mesh caches, so those caches contained none of
the xref'd geometry. The wire pass rebuilds from the document each frame
(bump_geometry covers it), but 3D-solid meshes are only produced by
populate_meshes_from_document, which never re-ran after the merge — so
xref'd solids (walls, floors, roofs) were loaded but never tessellated
and never drawn. Re-run it once a xref actually resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:20:24 +03:00
Hakan SevenandClaude Opus 4.8 608fe5ec3e feat(start): show paying Patreon supporters on the Start page
Add a right-hand rail on the Start page listing active paying patrons
(name + pledge amount, highest first) with a "Support on Patreon" button.
The list is fetched once at boot from the Patreon API in the background;
free followers, $0 tiers, declined and former patrons are excluded.

The creator access token is read at build time from OCS_PATREON_TOKEN
(option_env!), so it never lives in source or git — the release workflow
passes it from a repo secret, and build.rs re-bakes when it changes.
Without a token the rail just shows the support button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 01:00:22 +03:00
Hakan SevenandClaude Opus 4.8 98aa051a74 feat(nav): turntable orbit, selection/cursor pivot, Shift+MMB rotate (#229)
Rework 3D view navigation:

- Turntable / lock-horizon orbit: horizontal drag yaws about world +Z,
  vertical drag pitches about the horizontal camera-right axis, so the
  view never banks. Pitch is pole-clamped but the straight-down default
  view can still be tilted out of.
- Orbit centre: the selection's centre when something is selected,
  otherwise the point under the cursor — no longer only the file centre.
  The camera revolves rigidly about the pivot, so it stays put on screen
  and the view doesn't jump.
- Mouse scheme: Zoom = wheel, Pan = MMB, Rotate = Shift+MMB. Right-drag
  no longer orbits (right-click stays context menu / Enter). Floating
  viewports orbit with Shift+MMB too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:58:04 +03:00
Hakan SevenandClaude Opus 4.8 c303900bd2 fix(select): pin box-selection anchor to the world during zoom/pan (#234)
The window/crossing selection anchor was stored only in screen pixels, so
zooming or panning mid-drag left it frozen while the drawing moved under
it — the rectangle then covered the wrong world area and selected too much
or too little.

Store the anchor's world point and re-project it to the screen anchor
whenever the model-space view zooms (on_viewport_scroll) or pans
(on_viewport_move), so the rectangle tracks the drawing. The moving corner
already follows the cursor. (MSPACE floating-viewport box-select would
need the viewport camera and is left as-is.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:10:04 +03:00
Hakan SevenandClaude Opus 4.8 d6e5b38847 fix(properties): close color picker when a panel dropdown opens (#235)
The Properties panel color picker is a custom bool-driven dropdown, while
Layer/Linetype/Lineweight are combo_boxes. Combo_boxes close each other on
blur, but nothing closed the color picker — so opening the Layer dropdown
while the color picker was open left both showing and overlapping.

Give every panel combo_box an on_open handler that collapses the color
picker, so at most one panel dropdown is open at a time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 19:47:32 +03:00
Hakan SevenandClaude Opus 4.8 1614fd9ab2 feat(snap): dashed extension guide line for the Extension osnap (#238)
The Extension object snap worked but drew no tracking path, so it was
easy to miss — users had to keep the cursor exactly on the invisible
extension line without any visual cue.

When an Extension snap is active, draw a dashed guide line from the
endpoint it extends from, through the snap point and a little beyond,
and orient the three-dot marker along that direction. SnapResult carries
the endpoint screen position (filled once the winning snap is known);
the overlay renders the guide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 19:39:21 +03:00
Hakan SevenandClaude Opus 4.8 7f3ebde663 fix(properties): repaint on linetype/lineweight/layer edits (#231 class)
Color/linetype/lineweight/layer are baked into the cached wire geometry
at tessellation time, so a property edit only repaints if the handler
bumps the geometry epoch. The color path did; several siblings only set
tabs.dirty (a save flag) and never re-tessellated, leaving the viewport
showing the stale look.

Route every handle-based edit through invalidate_property_targets
(mark dirty + recolor meshes + bump geometry), and full-bump the
name-based LAYER COLOR change:

- ribbon layer / linetype / lineweight dropdowns applied to a selection
- CHPROP (color/linetype/ltscale/transparency/layer)
- LAYMATCH (MatchEntityLayer)
- LAYER COLOR <name> <aci>

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 19:14:35 +03:00
Hakan SevenandClaude Opus 4.8 1e9de924be feat(command-line): copy/clear history + selectable log (#232)
The command-line history dropdown was display-only, so users had to
screenshot and OCR the log to report output. Make it copyable:

- Render the whole backlog in one read-only text_editor so a single
  mouse drag selects across lines and Ctrl+C copies the span.
- Add Copy (whole log to clipboard) and Clear buttons to the dropdown.
- Gate the Ctrl+C/X/V accelerators on event Status::Ignored so a focused
  text widget's copy/cut/paste wins instead of firing COPYCLIP/CUTCLIP;
  the drawing's clipboard shortcuts still work when it has focus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:17:46 +03:00
Hakan SevenandClaude Opus 4.8 ada9952f06 feat(layers): multi-select layers in the Layer Manager (#236)
The Layer Manager only allowed one layer at a time. Now:

- Ctrl/Cmd-click toggles a layer in the selection; Shift-click selects
  the range from the anchor; a plain click selects just one.
- Colour, linetype and lineweight changes apply to every selected layer.
- Delete removes all selected layers at once (reusing the #237 non-empty
  warning, now pluralised); layer "0" and the current layer are skipped.

Selection is stored as row indices but re-resolved by name across sorts
and layer-table rebuilds, so it never points at the wrong rows. Modifier
state is tracked via a new SetModifiers message (shift + ctrl/cmd).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:44:19 +03:00
Hakan SevenandClaude Opus 4.8 2712bb80e4 fix(layers): deleting a non-empty layer warns and removes its objects (#237)
Deleting a layer only dropped the layer record, leaving its objects in the
file still tagged with the (now-gone) layer name. Now:

- Empty layer → deleted straight away.
- Non-empty layer → a warning modal ("Layer X is not empty — N objects;
  deleting also removes them. Continue?") with Delete Objects / Cancel.
  Confirming erases every object on the layer, then removes the layer, as
  one undo step. The layer record is dropped before the erase so the
  locked-layer guard can't keep its objects.
- Layer "0" and the current layer can't be deleted (reported on the
  command line) instead of silently leaving a broken state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:23:32 +03:00
Hakan SevenandClaude Opus 4.8 2723e4d47f fix(layers): close locked-layer gaps found in review
- Double-clicking a locked object no longer opens its text / attribute /
  in-place block editor.
- Clicking a locked object during a command's object-gather no longer
  counts as completing the selection (don't set selection_just_completed
  on the locked path).
- copy_entities skips locked entities.
- The attribute editor's Apply refuses to write when the block's layer was
  locked while the dialog was open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:14:33 +03:00
Hakan SevenandClaude Opus 4.8 56e1c03ac2 feat(layers): enforce layer lock (select/modify/fade + cursor badge)
Locked-layer objects were fully editable. Now they stay visible and
snappable but are otherwise protected:

- Not selectable: the five interactive pick paths (box, lasso, single
  click, cycling, box-release) plus SELECTALL and QSELECT skip locked
  entities.
- Not modifiable (defense-in-depth even if a handle slips through):
  transform_entities, erase_entities, apply_grip and the Properties
  commit all skip locked entities.
- Faded: entities on a locked layer render dimmed toward the background
  (render_style + wire tessellation); toggling lock re-tessellates.
- Snap still works: locked entities are intentionally kept in the wire
  set (visibility_ok untouched), so object snap keeps finding them.
- Feedback: a small padlock is drawn by the crosshair when hovering a
  locked object, and clicking one prints "Object is on locked layer …".

New Scene::is_layer_locked / locked_layer_name helpers centralise the
check (mirroring layer_hidden).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:10:39 +03:00
Hakan SevenandClaude Opus 4.8 7764b6cbf9 feat(statusbar): Select All/Clear All in filter popup, drop osnap settings row
The selection-filter popup now has the same Select All / Clear All header
as the OSNAP popup: Select All clears every exclusion (all types
selectable), Clear All excludes every present type. Each button disables
when it would be a no-op.

Also remove the "Object Snap Settings…" footer from the OSNAP popup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:47:13 +03:00
Hakan SevenandClaude Opus 4.8 0b60bcc075 feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
  its value below.
- Text Options: the selected attribute's Text Style, Justification,
  Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
  Lineweight (the colour list also surfaces a non-standard current
  colour so it displays and round-trips).

Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
Hakan SevenandClaude Opus 4.8 0eb347bf47 feat(attedit): show attribute prompts in the editor
Each row in the attribute editor now shows the attribute's prompt — the
text defined on the block's ATTDEF — falling back to the tag when the
block defines no prompt. The prompt is read from the block definition,
since attribute instances carry only tag + value.

Bumps the acadrust pin to pull the matching reader fix: its DWG object
reader was discarding the ATTDEF prompt, so every prompt came back empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 08:23:54 +03:00
Hakan SevenandClaude Opus 4.8 86a59d6c7e feat(attedit): attribute editor dialog for blocks
Double-clicking a block reference that has attributes, or running ATTEDIT
on one, now opens an editor dialog listing every attribute (tag + editable
value) with OK / Cancel. OK writes the values back to the block, with undo
and a repaint; Cancel discards. A block with no attributes still enters
in-place block edit (REFEDIT) on double-click as before. (#192)

ATTEDIT opens the dialog directly when a suitable block is already
selected; otherwise it runs the pick command and the dialog opens once a
block is chosen. This replaces the earlier command-line, per-attribute
prompt flow, which is removed along with its __ATTEDIT__ sentinel path and
the now-unused attedit_set_attrs trait hook; the ATTEDIT command is reduced
to a plain block picker.

The dialog is a tab-scoped in-canvas modal (Plan B): its working copy holds
a document-local handle, so closing that tab or switching away dismisses the
editor rather than risk applying edits to another tab's document.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 08:05:25 +03:00
Hakan SevenandClaude Opus 4.8 5f2e920b75 feat(properties): show & edit block attributes in panel
Selecting an INSERT with attributes now adds an editable "Attributes"
section to the Properties panel — each tag is a row whose value can be
changed inline (Enter commits, undo works, the attribute text repaints).
Previously attribute values could only be reached through ATTEDIT, which
didn't let them be changed (#192).

Attribute tags are runtime strings, so they can't ride the geometry
edit path whose field key is `&'static str`; a dedicated PropValue::
AttrText variant plus PropAttrInput/PropAttrCommit messages carry the
tag. Values are stored verbatim (no expression evaluation, since
attribute text is free-form).

Also guard the panel's in-progress edit buffer: it now only carries
across a rebuild when the selection is unchanged (a commit-triggered
refresh), so a typed-but-uncommitted value can't display or commit onto
a different entity — e.g. two title blocks sharing a REV1 tag. This
closes a latent stale-buffer leak that affected geometry fields too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 07:42:50 +03:00
Hakan SevenandClaude Opus 4.8 3da81571a1 fix(ribbon): scrollable layer dropdown, stop mouse leak to viewport (#227)
The ribbon layer dropdown grew unbounded so a long layer list ran off the
bottom of the screen with no way to scroll. Cap the panel height and make
the list scrollable; short lists still shrink to fit.

While a dropdown was open, the 3D viewport beneath it kept tracking the
cursor over the panel's empty areas, and the OS cursor vanished there. In
iced 0.14 mouse_area/opaque only capture button presses, never CursorMoved,
so a higher stack layer can't swallow motion — gate it state-side instead:

- ignore viewport motion in on_viewport_move while ribbon.open_dropdown is set
- suppress the crosshair overlay's Hidden cursor (and crosshair draw) so the
  normal OS cursor shows over an open dropdown
- route all ribbon dropdown backdrops through a shared dropdown_backdrop()

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:45:23 +03:00
Hakan SevenandClaude Opus 4.8 f6a7b0885f fix(render): repaint on layer/property edits (#231)
Layer color/linetype/lineweight and MATCHPROP edits set the
document dirty flag but never bumped geometry_epoch. The wire
caches key on that epoch and bake a ByLayer entity's resolved
color at tessellation time, so they returned stale wires until
a new entity forced a rebuild. Bump geometry on those four
handlers so the edit repaints immediately.

Also remove REGEN/REGENALL/REDRAW/REDRWALL: the GPU raster
pipeline keeps the display continuously in sync, so on-demand
regen/redraw is a no-op concept in OCS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:41:13 +03:00
Hakan SevenandClaude Opus 4.8 a994a148fe perf(render): batch solid meshes, fix per-pick freezes on heavy 3D models
Opening / navigating a heavy 3D DWG (e.g. a BIM model with ~10k ACIS solids)
was effectively frozen. Several independent costs, all addressed here:

- Face3D edge/fill buffers were re-uploaded on every camera move (keyed on
  camera_generation). They are world-space and selection-independent, so gate
  them on (geometry_epoch, fill_mode) instead — a pan no longer re-walks every
  wire.
- Solid meshes were drawn one buffer + one draw call per solid (~10k draws/
  frame), which strangled the GPU front end. Concatenate every solid's LOD0
  into a few large buffers (split only to stay under the 256 MB buffer cap) and
  draw the whole set in a handful of calls. Built once per geometry epoch, so
  selection / hover never re-pack it.
- Selection / hover highlight is now a small tinted overlay of just the picked
  solids, drawn last with an Always-depth pipeline so it shows on top even when
  the solid is occluded — instead of re-tinting and re-uploading all meshes.
- Hover / pick hit-testing scanned every triangle of every solid each move
  (seconds on a heavy model). Add a 3D-AABB screen-rect broad phase
  (MeshLodSet now carries z_aabb) and reuse the renderer's cached expanded mesh
  set, so picking is O(solids) cheap projections plus the few solids actually
  under the cursor.
- Block-internal hatch hit-testing exploded every INSERT on every hover.
  Cache the result per geometry epoch and skip blocks that contain no hatch.
- Curved-surface (cone/sphere/torus) tessellation tolerance is now radius-
  relative (CURVE_REL_TOL) so a cylinder's facet count is size-independent and
  matches the circle/arc wire tessellation, instead of exploding on large radii.

Also adds docs/tessellation.md mapping every EntityType to its tessellation
path (truck B-rep vs truck curve topology vs direct).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:46:29 +03:00
Hakan SevenandClaude Opus 4.8 a9214db344 fix(autosave): autosave unsaved drawings + clean up .sv$ on file-close (#205)
Two gaps in the autosave recovery files:

1. A brand-new, never-saved drawing was never autosaved — on_autosave only
   wrote tabs that already had a path. A shared autosave_target() now writes
   saved drawings to <file>.sv$ and unsaved ones to the system temp dir
   (OpenCADStudio_<name>.sv$).

2. Closing a file (tab) left its .sv$ behind, which also defeated the
   program-close cleanup (it only scans still-open tabs). on_tab_close and the
   unsaved-discard path now delete the closing tab's .sv$, and Save/Save-As
   drop the temp copy a drawing used while it was still unsaved.

Program-close cleanup (exit_app -> cleanup_autosaves) already worked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 16:39:42 +03:00
Hakan SevenandClaude Opus 4.8 286caa9c61 feat(io): delete .sv$ autosave files on clean save/exit (#205)
The .sv$ recovery copies only need to survive a crash, so they are removed on
a clean save (Save and Save-As both delete the file's .sv$ after writing) and
on a clean exit — a new exit_app() runs cleanup_autosaves over every open tab
before quitting, and all iced::exit() paths now funnel through it. A leftover
.sv$ therefore signals an unclean shutdown, i.e. something to recover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:34:16 +03:00
Hakan SevenandClaude Opus 4.8 02d2764e45 feat(io): autosave to .sv$ recovery files (SAVETIME command) (#205)
A periodic subscription (every SAVETIME minutes — default 10, 0 disables)
writes a <name>.sv$ recovery copy of each dirty, already-saved drawing at its
own DWG version. Non-destructive: it never touches the original file or the
dirty flag, and skips unsaved (path-less) tabs. The SAVETIME command
sets/queries the interval and persists it. Recovery files (.sv$ and .bak) open
through the normal Open dialog — the loader detects DWG vs DXF from the leading
magic bytes, and a "Backup / Autosave" filter lists them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:17:40 +03:00
Hakan SevenandClaude Opus 4.8 54186309b2 refactor(io): set up file association once, not on every launch (#205)
Drop the every-launch (re)registration thread. File association is now set up
once via the first-launch prompt (Yes → register + set default; No → decline,
persisted as file_assoc_enabled) and managed afterwards by the FILEASSOC
command. set_default_app now registers the handler entries (.desktop / mime /
ProgIDs) itself before asking the OS to default to us, since boot no longer
does — so the default never points at a missing handler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:06:33 +03:00
Hakan SevenandClaude Opus 4.8 9ea800f049 feat(io): FILEASSOC command to toggle file association on/off (#205)
`FILEASSOC 1` (re)registers the app as a .dwg/.dxf/.bak handler; `FILEASSOC 0`
unregisters it (removes the .desktop + CAD mime package on Linux, the per-user
registry keys on Windows); bare `FILEASSOC` reports the current state. The
choice persists (file_assoc_enabled) and gates the every-launch
re-registration, so turning it off stays off across restarts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:58:23 +03:00
Hakan SevenandClaude Opus 4.8 962268e16e feat(io): .bak backup-on-save + open .bak as a supported type (#205)
- Save: overwriting an existing file first copies it to a sibling <name>.bak
  (default on; toggle with the ISAVEBAK command, persisted in settings).
- Open: .bak is a first-class type — the Open dialog lists it and the loader
  detects DWG vs DXF from the file's leading magic bytes (the extension hides
  the real format).
- File association registers .bak too, every launch (so users who associated
  earlier get it automatically): Windows via the registry; Linux via a
  high-priority shared-mime-info magic that resolves DWG/DXF content under the
  .bak extension, with the generic *.bak glob claimed only once the app is the
  default DWG handler — so it never hijacks other apps' backups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:49:02 +03:00
Hakan SevenandClaude Opus 4.8 b77187e672 feat(save): warn before dropping AEC/Civil objects or overwriting on Save-As
Two in-canvas confirmations on the Save-As path:

- AEC/Civil drop warning: when the drawing carries unsupported AEC/Civil
  (application) objects that survive only as verbatim source-version bytes,
  saving to a different version or to DXF would silently drop them. The
  dialog reports the count and offers "Save in source version" (keep) vs
  "Save anyway" (drop). Same-version DWG saves are lossless and skip it.
- Overwrite confirmation: when the chosen filename already exists in the
  target folder, "Replace" / "Cancel" before clobbering it.

The two chain (AEC first, then overwrite) via per-flow acknowledgement flags
reset whenever the Save dialog opens or closes. Detection is a new
io::dropped_on_save_count over the document's Unknown (raw-passthrough)
objects/entities.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:47:37 +03:00
Hakan SevenandClaude Opus 4.8 ab9285f114 feat(viewport): run typed command-line text on right-click
When the command line holds a typed-but-unentered command (or option
keyword / value), a viewport right-click now runs it, mirroring Enter.
Routed through CommandFinalize so the MText preview and grip-popup
guards apply and a non-empty line forwards to the submit path. All
other right-click behaviour is unchanged and applies only when the
command line is empty: right-drag orbits, the first right-click during
an active command acts as Enter, a second opens the context menu, and
an idle right-click opens the menu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:20:01 +03:00
Hakan SevenandClaude Opus 4.8 c19c18c2f2 feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.

3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)

Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
  (XData markers honoured by the existing geometry-gen / tessellator)

Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
  system variables, and layer/style/view commands

POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
Hakan SevenandClaude Opus 4.8 c23b956861 feat(io): default DWG save to 2018; bump acadrust handle-counter fix
- Save As now defaults the version dropdown to DWG/DXF 2018 (latest)
  instead of the loaded file's version; the selected version is written
  as-is (removed the source-version fallback for version-locked data).
- Pin acadrust 55a2caa: allocate_handle respects header.handle_seed as a
  floor, fixing the corruption where opening a file and saving it
  silently overwrote existing XRECORD/DICTIONARY objects (handle reuse)
  and AutoCAD refused the result.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 20:31:30 +03:00
Hakan SevenandClaude Opus 4.8 d1efa4ea7c feat(io): preserve source DWG version for files with version-locked data
Civil3D/AEC objects (and raw MLEADER/Surface records, EED) can only be
written losslessly in their source DWG version. On save, fall back to the
source version when the chosen version is an incompatible encoding family
(acadrust CadDocument::has_version_locked_data), with a command-line
notice, across every save path (Save, Save As, save-before-close/quit)
via a shared dwg_save_version helper. Default the Save-As format dropdown
to the loaded file's version so a round-trip preserves it.

Picks up acadrust DWG-writer conformance fixes (3dbae05) so a Civil3D
R2004 drawing round-trips through OCS and opens in AutoCAD with no errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 02:46:13 +03:00