Commit Graph
972 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.8 57136b9eda fix(textstyle): live-update style manager preview (#102)
The preview box rendered a hardcoded `text("AaBbCc 0123")` that ignored
every edited property. Replace it with a canvas that tessellates the
sample string through the same stroke path the text entities use
(`lff::tessellate_text_ex`), so font, width factor, oblique angle and the
backward/upside-down flags are reflected live as fields change. Effective
font follows the entity rule (TrueType name wins, else stroke font file);
the result is fit to the preview box with Y flipped.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-29 17:29:04 +03:00
Hakan SevenandClaude Opus 4.8 ea35479dd9 fix(web): restore wasm build broken by SECTION + file association
The v0.7.0 web (GitHub Pages) release failed to compile under
--no-default-features. Two symbols leaked into the wasm build:

* `solid_section` (SECTION command) used truck-meshalgo directly
  (`triangulation`/`extract_interference`), but that crate is gated
  behind the `solid3d` feature which is off on wasm. Gate the function
  with `#[cfg(feature = "solid3d")]` and add a stub for builds without
  it, mirroring `solid_model::boolean`.
* `register_as_handler` was `#[cfg(not(wasm32))]` while its caller
  `set_default_app_blocking` was not, so wasm couldn't resolve it. Drop
  the wasm gate; the existing non-win/lin/mac arm already returns `Ok`
  on wasm, matching `unregister_handler`.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-29 17:13:17 +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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
2026-06-29 11:20:01 +03:00
Hakan SevenandClaude Opus 4.8 c653a3f38e feat(commands): add FLATSHOT and CONVTOSURFACE
- FLATSHOT: project the selected solid's edges to 2D Line entities at Z=0
- CONVTOSURFACE: convert the selected solid(s) to Surface entities

Both reuse the cached B-rep's edge wires. COMMANDS.md: 219/262.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-29 09:16:44 +03:00
Hakan SevenandClaude Opus 4.8 6f9565e9f7 feat(commands): add ARCHIVE / ETRANSMIT (folder package)
Package the drawing plus its referenced files (xrefs + raster images)
into a sibling "<name>_archive" folder via std::fs, reusing the
existing block_records / entities iterators. COMMANDS.md: 217/262.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-29 09:10:57 +03:00
Hakan SevenandClaude Opus 4.8 f513899d49 feat(commands): add REGION (closed boundaries -> region entities)
Convert selected closed polylines and circles into Region entities,
one wire loop each (reuses acadrust Region/Wire). COMMANDS.md: 215/262.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-29 09:07:30 +03:00
Hakan SevenandClaude Opus 4.8 e2a83a6487 feat(commands): add CLOSE, HIDE, VISUALSTYLES, QUICKCALC, HYPERLINK, XOPEN
Six more standard commands by reusing existing infrastructure:
- CLOSE: close the active tab (Message::TabClose)
- HIDE / VISUALSTYLES: per-viewport render mode (Message::SetRenderMode)
- QUICKCALC / QC: alias the CAL arithmetic evaluator
- HYPERLINK <url>: attach a hyperlink via the PE_URL XData record
- XOPEN: open a selected xref's source file (Message::OpenRecent)

COMMANDS.md updated to 214 done / 15 partial / 33 missing of 262.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-29 09:04:18 +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 <[email protected]>
2026-06-29 08:23:58 +03:00
Hakan SevenandClaude Opus 4.8 f88c2f8327 style(ribbon): cargo fmt after PR213 merge
PR #213 introduced rustfmt drift in tip_style / swatch indentation and an
inline if-expression in two previously fmt-clean files. Format pass only;
no behaviour change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-29 01:19:05 +03:00
Karim Jerbi 63c13f0f63 refactor(ribbon): extract repeated inline color literals into named constants
Consolidates 21 occurrences of identical Color literals scattered across widgets.rs and mod.rs into 9 named constants (COMBO_BG, COMBO_HOVER_BG, COMBO_OPEN_BG, COMBO_BORDER, COMBO_ACTIVE_BORDER, COMBO_ARROW, SWATCH_BORDER, TIP_BG, HIST_INACTIVE_BG). -85 net lines, zero behaviour change.
2026-06-28 22:59:14 +01:00
Karim Jerbi 1e8a98d2cd fix(ribbon): center section titles within their content width
Section labels (Draw, Modify, Annotation, …) were left-aligned with a small padding. Set align_x(Center) on the parent column so the label container — still sized to its text width — is centred within the column, which itself remains sized by the tool buttons above. Sections keep their natural content-based width, no wasted space.
2026-06-28 22:23:01 +01:00
Hakan SevenandClaude Opus 4.8 87232bba2c fix(expr-eval): bound eval_to_string against super-linear DoS
The recursion-depth guard fixes the parser stack overflow, but user input
actually flows through eval_to_string (command line, viewport, dynamic
input — synchronous on the UI thread). Its embedded-expression fallback
re-tokenises every (start, end) window, so cost is ≈O(n³): a few-KB paste
of non-evaluable text or deeply nested parens froze the app for seconds to
minutes even though eval_number fast-returns None in microseconds
(measured: "x"×3200 → 16 s, 4 KB of parens → 51 s).

Cap the fallback scan at MAX_EVAL_SCAN_LEN (256) — whole-string numbers
and expressions of any length still evaluate via the fast paths; only the
mixed free-text scan is bounded, returning long input unchanged. Clarify
that MAX_RECURSION counts parser frames (256 ≈ 41 nested parens, not 256
levels). Add regression tests: the 41/42 nesting boundary, unary-chain and
function-arg-nesting rejection, and an eval_to_string over-cap promptness
test (50k junk + 100k nested parens return instantly).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-28 22:41:02 +03:00
Sebastian bfb81f6658 Adding expression evaluator against stack-overflow DoS 2026-06-28 22:38:09 +03:00
Hakan SevenandClaude Opus 4.8 a93271c1bd fix(block-cache): address PR211 review follow-ups
- expand_defn: dedup the <1px baseline LOD branch the same way the <5px
  greek branch does, so colour-split MTEXT no longer stacks N overlapping
  baselines (one per \C segment) over the same OBB.
- sysfont: memoise canonical_family_name process-wide. resolve_font calls
  it once per word on the MTEXT measure hot path and Face::resolve re-runs
  it right after; the fontdb query + linear family scans were uncached.
- dimension/multileader/table: reword the 6 fill_tris_low FIXMEs. They
  cited a stale line and the wrong consumer — these fills render on the
  top-level path (panic-safe .get().unwrap_or), not the block cache, so
  they cannot trip emit_wire's debug_assert. State the real status:
  latent f32-precision debt, not a crash. Also fix the mis-indented
  text-fill WireModel in dimension.rs.
- tests: add a host-independent colour-split test (two \C segments in a
  block keep ≥2 distinct wire colours — guards the per-wire colour fix)
  and a TTF-gated UTM fill test (paired, non-zero fill_tris_low). Note the
  arial-style test's LFF-fallback limitation.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-28 21:57:51 +03:00
Hakan SevenandClaude Opus 4.8 a84da122f4 fix(block-cache): harden fill_tris_low access against unpaired wires
emit_wire reconstructed fill verts via a raw `fill_tris_low[idx]` index,
guarded only by a debug_assert. Slice bounds checks are not gated by
debug-assertions, so a Leader with a filled arrowhead nested in a block
(populated fill_tris, empty fill_tris_low via the low-level tessellate
early-return) panicked in release as well as debug.

Use the panic-safe `.get(idx).copied().unwrap_or([0.0; 3])` — the same
"empty = all-zero low half" contract the other fill consumers
(face3d_gpu, xclip) already honour. The debug_assert stays as a tripwire.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-28 21:42:16 +03:00
Karim Jerbi d2cd728313 Merge branch 'main' of https://github.com/KarimJerbi/OpenCADStudio 2026-06-28 19:18:12 +01:00
Karim Jerbi ae2946a8a9 fix(block-cache): emit one greek substitute per source MTEXT entity
Colour-split MTEXT produces one outline wire per inline \C/\c override. At <5px zoom each outline independently emitted the full OBB greek rectangle — N overlapping boxes with last-colour-wins. Track last_lod_obb and skip duplicate substitutes.

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

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

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

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

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

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

Adds two unit tests covering the fallback and TTF-stem-to-family-name paths. Adds integration test verifying block-nested MTEXT produces geometry and correctly separates outline/fill wires.
2026-06-28 18:44:38 +01:00
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 <[email protected]>
2026-06-28 20:31:30 +03:00
Hakan SevenandClaude Opus 4.8 0ca3954b5d feat(commands): integrate standard CAD commands and fix behavior gaps
Broad pass over the command set: integrate missing commands, wire
previously-unreachable ribbon/menu buttons, and correct name/behaviour
mismatches. Every UI command token now resolves (real handler where the
infrastructure exists, an honest "not yet implemented" status otherwise).

New commands:
- Draw: 3DPOLY, 2D solid (SO/SOLID2D), HELIX, TRACE, SKETCH, DIMCENTER
  (DCE/CENTERMARK)
- Modify/inquiry: REVERSE, MEASUREGEOM (MEA), DBLIST
- Data/manage: BASE, OVERKILL (exact-duplicate removal), AUDIT (read-only
  integrity report), SETVAR (system-variable get/set), SCRIPT (.scr runner),
  FINDNONPURGEABLE

Behaviour fixes:
- LINE and SPLINE gain Close/Undo; AREA computes area/perimeter in f64
  (correct at large coordinates)
- ATTEDIT/ATE unified so the alias and the ribbon button open the
  interactive editor; -ATTEDIT is the command-line form
- Linear/aligned/radius/diameter dimensions gain a text override (Text) and
  text angle (Angle) option
- INSERT gains Scale/Rotate options; COPY gains an Array option
- LINETYPE SET sets the current entity linetype; SETBYLAYER resets overrides
- UNDO accepts a step count

Integrations: standard dimension/block aliases (DLI/DRA/DAN, BMAKE,
EXPORTPDF, DDIM), drafting-aid and screen toggles (GRID/SNAP/POLAR/OSNAP/
DSETTINGS/CLEANSCREEN/QUICKPROPERTIES), viewport tiling
(CASCADE/HORIZONTAL/VERTICAL/VPJOIN), and customization entry points
(CUI/ALIASEDIT).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-28 19:27:35 +03:00
Hakan SevenandClaude Opus 4.8 9928cbf0e5 feat(view): zoom-window ribbon button and bare ZOOM command
Bare `ZOOM` had no dispatch arm — only the sub-keyword forms (ZOOM
EXTENTS/IN/OUT/WINDOW/…) were handled, so typing `ZOOM` fell through to
an unknown-command error. It now starts the interactive window zoom.

Add the missing Zoom Window ribbon button: the command existed but had no
tool()/icon, so it was reachable only by typing. Wire it into View ▸
Navigate with a new dashed-rectangle magnifier icon.

Closes #210

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-28 16:57:00 +03:00
Hakan SevenandClaude Opus 4.8 d209e7bbb5 fix(selection): clear selection when an interactive command ends
Editing tools (MOVE, COPY, STRETCH, ROTATE, OFFSET, SCALE, MIRROR) and
other interactive commands left their working set selected after they
finished, so the next edit silently reused it — confusing especially for
copy-producing tools. Drop the selection centrally in apply_cmd_result
when a command ends (active_cmd Some->None), excepting Relaunch/Dispatch
which hand a deliberate selection to a chained command. Pure-selection
commands run without an active command, so their result is preserved.

Closes #207

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-28 10:28:23 +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 <[email protected]>
2026-06-28 02:46:13 +03:00
Hakan SevenandClaude Opus 4.8 12f4264183 feat(dimension): bake full arrow shapes, arrow-fit, fixed-length ext lines
Completes the bake↔live dimension parity from the audit:

- terminators now reproduce the style's actual arrow block: dim_metrics
  resolves arrow1/arrow2 via arrow_from_block (DIMTSZ tick, DIMBLK /
  DIMBLK1+DIMBLK2 per DIMSAH, else closed-filled), and dim_terminator bakes
  each kind (None, filled/blank triangle, tick, open, dot, origin, box,
  datum) as Line + SOLID — no more every arrow degrading to a filled triangle
- short dimensions flip their arrows outside with stubs (DIMSOXD honoured)
  when the arrowheads don't fit between the extension lines (DIM-ARROWS-OUTSIDE)
- DIMFXLON fixed-length extension lines via ext_endpoints (DIM-FXL)

Refs #181. Remaining (annotative viewport scale at non-1:1, the ~1deg angular
arrowhead-tangent cosmetic, and DIMTFILL text-break gap) are niche/cosmetic
and tracked separately.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 20:43:14 +03:00
Hakan SevenandClaude Opus 4.8 58da76ff70 fix(dimension): baked *D block matches live render (gaps, arrows, leaders, text)
Brings the save-time *D-block bake into parity with the live render so a
dimension no longer changes when the file is saved and reopened (the reload
draws from the block). Per a bake-vs-live audit:

- text is now the live render's own Text/MText entity (alignment, MText
  codes, text style, alignment point) — fixes every label shifting ~half a
  glyph on reload
- extension lines carry the DIMEXO start gap + DIMEXE overshoot, and slant
  by ext_line_rotation (DIMEDIT Oblique)
- terminators follow the style: oblique tick when DIMTSZ>0, else a closed
  filled SOLID arrow (was a generic open stroke)
- angular dims bake the swept arc + tangent terminators, flush extension
  lines (matching live exactly)
- radius/diameter bake the leader leg; diameter runs edge-to-edge through
  the centre; centre mark honours negative DIMCEN (centre lines)
- per-element colours/lineweights (DIMCLRD/E/T, DIMLWD/E), DIMSE/DIMSD
  suppression, DIMDLE tick overshoot
- consistent arc sweep at exactly 180 degrees

Refs #181. Remaining (custom DIMBLK arrow shapes, DIMFXLON, arrow-outside
fit, annotative viewport scale) tracked separately.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 20:29:35 +03:00
Kevin GriffinandClaude Opus 4.8 73eabc3aaa fix(shortcuts): honor Cmd as the accelerator modifier on macOS (#202) (#206)
Keyboard accelerators (copy/paste/save/undo/new/open) were gated on
`Modifiers::control()`, which on macOS matches only the physical Control
key. Mac users pressing Cmd+C/V/S/Z got nothing, contrary to platform
convention and every other Mac app.

Switch the gate to `Modifiers::command()`, which iced resolves to the
Logo (Cmd) key on macOS and Ctrl on Windows/Linux. On non-mac targets
this expands to exactly `control()`, so behavior there is unchanged.

Also make the Keyboard Shortcuts reference window display the
platform-correct modifier name (Cmd vs Ctrl) so the help matches the
keys the user actually presses.

Closes #202

Co-authored-by: Claude Opus 4.8 <[email protected]>
2026-06-27 19:33:08 +03:00
Hakan SevenandClaude Opus 4.8 ddf576765b fix(view): drop non-standard camera View record; restore from VPORT
OCS wrote an app-specific "OpenCADStudio_Camera_Model" entry to the VIEW
table and preferred it when restoring the model view. That record polluted
the file for other CAD programs and its DXF round-trip was broken, so a
drawing saved in TOP reopened from DXF at an isometric-ish angle.

The model view now persists and restores entirely via the standard *Active
VPORT entry (view direction / target / twist), which round-trips natively in
both DWG and DXF. The custom View record is no longer written; existing ones
in older files are left untouched. Paper space already used the standard
sheet viewport and keeps a read-only fallback for very old files.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 19:27:23 +03:00
Hakan SevenandClaude Opus 4.8 959dd66e93 fix(dimension): land DXF round-trip fixes (re-pin acadrust)
Re-pins acadrust to the dimension DXF round-trip fixes (rotation/text/oblique
degree<->radian units, radial/angular/ordinate group codes, ordinate X/Y bit)
and drops the OCS-side fix_dxf_dimension_rotations dimension arm, which now
lives in the acadrust DXF reader (keeping it here would double-convert).

Completes the dimension audit (Batch 5). Refs #181.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 18:15:14 +03:00
Hakan SevenandClaude Opus 4.8 0d42d57927 feat(dimension): implement QDIM, fix DIMSPACE, honest DIMBREAK/DIMJOGLINE
Batch 4 of the dimension audit (command features).

- QDIM now works: the relaunched QDIM_PLACE stage decomposes the selected
  geometry into endpoints, picks the axis from their spread (not Z), and
  builds a continuous chain of linear dimensions at the picked line. Was a
  no-op that errored with 'Unknown command'. (DIM-007/008/032)
- DIMSPACE steps parallel dimensions along the in-plane perpendicular
  instead of Z (which never moved the dim line). (DIM-021)
- DIMBREAK / DIMJOGLINE no longer report success while doing nothing — they
  state honestly that they are not yet implemented (need a break-gap / jog
  field on the dimension model). (DIM-019/020)

Refs #181.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 17:56:01 +03:00
Hakan SevenandClaude Opus 4.8 3c5aa4453a fix(dimension): bake↔live consistency, edits, baseline/continue, grips
Batch 1-3 of the dimension audit (19 issues). The dominant class was the
baked *D block diverging from the live render: a reloaded dimension renders
from that block, so on save+reopen it silently changed.

Bake now matches the live render:
- text value/format via the shared style-resolved path (DIMDEC/DIMLFAC/
  DIMPOST/units/<>), correct height + rotation ladder, suppression
- angular dims bake their swept arc (no longer collapse to a V)
- diameter draws edge-to-edge through the centre (live + bake), centre mark
- radius/linear/aligned bake arrowheads
- group-10 (base.definition_point) synced from the edited geometry so an
  edited dim no longer shifts on reload

Editing / properties:
- aligned no longer corrupts ext_line_rotation when a coordinate is edited
- DIMTEDIT pins the moved text (text_user_positioned)
- angle properties shown/edited in degrees
- non-linear text respects the user-positioned gate

Commands / geometry:
- DIMBASELINE stacks outward (not toward the points) and its preview no
  longer tilts over non-level points
- DIMCONTINUE/DIMBASELINE inherit the base dim's text rotation (UCS chains)
- angular extension lines reach the arc (no gap)
- text grip sits on the text for auto-placed dims instead of the origin

Refs #181. (acadrust DXF round-trip + QDIM/DIMBREAK/DIMJOGLINE/DIMSPACE/
DIMEDIT to follow.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 17:47:50 +03:00
Hakan SevenandClaude Opus 4.8 b691854e7a feat(viewport): RMB 1st=Enter, 2nd=menu (no setting)
Replaces the opt-in `rmb_enter` setting (and `RMBENTER` command) with
always-on behavior. While a command is active, the first viewport
right-click acts as Enter (commit / close) and a second consecutive
right-click opens the context menu instead. Idle right-click still opens
the menu; right-drag still orbits.

The cycle is tracked per tab (`right_click_entered`) and restarts on any
other interaction — a left-click pick, a new command, an orbit, or
leaving the viewport — so the next right-click is Enter again.

Refines #185.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 16:32:40 +03:00
Hakan SevenandClaude Opus 4.8 098585bb2d feat(viewport): RMB=Enter option + timed right-click
Adds a configurable right-mouse-button behavior without disturbing orbit
or the context menu.

- New persisted setting `rmb_enter` (command `RMBENTER [ON|OFF]`, default
  off): when on, a right-click while a command is active acts as Enter
  (commit/close); when idle it still opens the context menu. Right-drag
  always orbits, so nothing is lost.
- Right-click vs orbit is now time-based: orbit engages only after the
  button is held >=150ms and the pointer moves, so a quick right-click is
  no longer swallowed by slight pointer jitter (was an 8px distance gate).
- The right-click Repeat menu now lists commands from every source
  (command line, ribbon, context menu, shortcuts) via a unified
  recents list recorded at the dispatch choke point, instead of only
  typed commands.

Closes #185

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 16:16:08 +03:00
Hakan SevenandClaude Opus 4.8 63de137106 feat(cmdline): Enter runs closest command match
Typing a partial command (e.g. `bac`) and pressing Enter with no active
command ran into an "Unknown command" error. Now the top autocomplete
match is highlighted and Enter runs it (`BACKGROUND`), matching standard
DWG command-line behavior.

The fallback only fires when no command family matches the typed verb, so
complete aliases that resolve through a dispatch arm but are absent from
the autocomplete registry (`LT`, `ZO`, …) still run as typed. Programmatic
dispatch (ribbon, plugins, headless automation) is unaffected.

Closes #198

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 14:45:51 +03:00
Hakan SevenandClaude Opus 4.8 3def2f2034 feat(statusbar): scrollable layout tabs + layout-list dropdown (#199)
Keep the right-side status-bar tools from being pushed off the bar when a
drawing has many layouts: the layout tabs now live in a horizontal scrollable
that only takes the space left between the menu and the tools, with ‹ › arrows
(and the mouse wheel) to scroll them.

Repurpose the leftmost hamburger — which previously fired a no-op MENU command
— to open a dropdown listing Model and every layout (scrollable, current
highlighted); picking one switches to it, an outside click dismisses it.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 14:24:47 +03:00
Hakan SevenandClaude Opus 4.8 ff23abee01 feat(start): replace info cards with a clickable intro video
Swap the two "Free and open source" / "Community driven" text cards on the
start page for the intro-video thumbnail with a play badge; clicking it opens
the video in the system browser (new Message::OpenUrl → sys::open_url). The
bundled thumbnail Handle is built once via OnceLock — Handle::from_bytes mints a
fresh unique id per call, so rebuilding it per view re-decoded and re-uploaded
the JPEG every frame and the image only appeared after a long delay.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 12:21:14 +03:00
Hakan SevenandClaude Opus 4.8 495b13ec4f refactor(viewport): migrate model-space tiling to iced pane_grid
Replace the custom model_tiles tiling with iced 0.14 pane_grid as the layout
source of truth, while paper-space floating viewports are left untouched (they
overlap/free-place, which pane_grid cannot model).

Each Model pane is its own shader widget rendering its camera into the pane
bounds; the renderer Primitive carries a base_slot so several shader widgets
share the type-keyed MultiPipeline storage on disjoint slots. A second pane_grid
of transparent mouse_areas (plus on_resize) layers above the crosshair overlay
to receive input — the overlay's Hidden cursor interaction otherwise levitates
the cursor and starves any layer beneath it. The active pane follows the cursor
(hover-activate), and grid / crosshair / UCS icon render per pane while the
ViewCube and render-mode picker stay on the active pane only.

Split / close / VPORTS presets and the VPort table round-trip now drive
pane_grid (guillotine reconstruction on load). Adds drag-to-swap via a controls
bar handle with a ghost-card preview, visible pane dividers from the pane_grid
split regions, a default model background of RGB(33,40,48), and removes all the
superseded custom-tile code.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 11:50:11 +03:00
Hakan SevenandClaude Opus 4.8 8bdf5228d2 refactor(viewcube): shrink cube 30%, compact controls, centre UCS picker
Reduce the ViewCube 30% (VIEWCUBE_PX 120→84) and tighten the nav region
(NAV_INSET_F 2.2→2.0) so the home / roll / nudge controls hug the cube in the
corner instead of floating in a large dead area, with sizes scaled to keep them
from overlapping. Bump the home and roll buttons back up slightly, shift the
UCS picker down below the region, and give it a fixed width so it centres
exactly under the cube centre.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 01:56:17 +03:00
Hakan SevenandClaude Opus 4.8 edcf24e324 feat(ucs): interactive UCS icon with origin-snap grips, world-fixed compass
The UCS icon now anchors to the projected UCS origin when it is on-screen
(falling back to the bottom-left corner), and becomes a selectable object:
hover highlights it, a click shows draggable grips, and dragging the origin
grip relocates the UCS while the X/Y tip grips rotate it. Drags snap to
geometry/grid through the existing snapper, and the new UCS is persisted on
release. The icon stays selectable while parked in the corner.

Also make the ViewCube compass (N/E/S/W) world-fixed: the cube body still
reorients with the active UCS, but the cardinals project through a camera-only
rotation and snap in world frame, so the directions no longer spin with the UCS.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 01:12:45 +03:00
Hakan SevenandClaude Opus 4.8 a502379048 refactor: group ui/ statusbar and window subdirs
Continue tidying the flat ui/ root:

- ui/statusbar/: statusbar.rs becomes statusbar/mod.rs (its
  crate::ui::statusbar::* paths are preserved) and absorbs
  statusbar_config.rs + statusbar_menu.rs.
- ui/window/: the standalone manager/dialog windows move here —
  about, layout_manager, page_setup, plugin_manager, shortcuts,
  layers, update_notice, open_progress. The crate::ui::LayerPanel
  re-export is repointed so callers are unchanged.

Call sites repointed to the new paths; no compat re-exports beyond the
existing LayerPanel one. ui/ root drops from 21 to 10 flat files with
five subdirs (ribbon, style, popup, statusbar, window).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 23:52:26 +03:00
Hakan SevenandClaude Opus 4.8 9b8ee94c0c refactor: rename mislabeled scene files to match contents
Two names collided with the pick subsystem and misdescribed their files:

- scene/hittest.rs -> scene/camera_ops.rs. Its contents are camera/view
  operations (zoom, fit-all, named-view restore, camera<->document sync,
  per-tile grid/snap), not hit-testing — the real screen-space hit-test
  lives in scene/pick/hit_test.rs.
- scene/pick/selection.rs -> scene/pick/selection_state.rs. It holds only
  the SelectionState struct; the name clashed with scene/selection.rs
  (the Scene selection operations).

Callers are unaffected (the crate::scene::SelectionState re-export path is
unchanged). No behavior change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 23:45:16 +03:00