Commit Graph
1450 Commits
Author SHA1 Message Date
Karim Jerbi 8ef65f59f8 fix(ribbon): highlight toggle buttons by their actual state
The UCS Icon, Properties, File Tabs, and Layout Tabs buttons were highlighted unconditionally because their ids fell through to an active_tool equality check in is_active_tool, which activate_tool sets on every click and never clears. Each id is now mapped to its corresponding visibility flag (show_ucs_icon, show_properties, show_file_tabs, show_layout_tabs), and those flags are kept in sync from their toggle handlers so the highlight reflects live state. The per-toggle booleans previously threaded through the render path are consolidated into a single ToggleState struct read from one place, so introducing a further toggle no longer requires changing every render-function signature and call site.
2026-07-12 19:53:48 +03:00
Hakan SevenandClaude Opus 4.8 15d1bdbdfe chore(deps): bump acadrust — read model-space CANNOSCALE from DWG
Picks up the DWG reader reflecting CANNOSCALE from the AcDbVariableDictionary
into the header, so the status-bar annotation-scale pill shows the drawing's
real model-space scale (e.g. 1:70) instead of the "1:1" default.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 19:36:46 +03:00
Hakan SevenandClaude Opus 4.8 d62057b869 feat(properties): show Annotative state + applied annotation scale
Resolve, per selected entity, whether it is annotative (from its text /
dimension / multileader style, or its own flag) and — when annotative — the
name(s) of the annotation scale(s) applied to it, shown as a read-only
"Annotative scale" row (Text / MText / Leader / MLeader).

The applied scale is read from the entity's per-scale object-context leaves
via the new acadrust CadDocument.context_scales map (context leaf ->
AcDbScale handle), walked from the entity's extension dictionary
(AcDbContextDataManager -> ACDB_ANNOTATIONSCALES). Bumps acadrust to pick up
that parsing.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 19:26:11 +03:00
Hakan SevenandClaude Opus 4.8 e3d5edf9b7 feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:

- Text: combined Justify (horizontal x vertical) dropdown; Position and
  Text-alignment rows become editable/grayed by justification; drop the
  Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
  (back-solves the factor) and Line space style; Defined width/height
  gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
  group (fold landing rows into Leaders), gate Text vs Block on content
  type, full 9-value attachment label set, resolve handle-backed rows
  (style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
  style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
  vertical text pos, overall scale); Current Vertex navigator; combined
  Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
  transparency by name. Add shared num_prop helper for conditionally
  editable numeric rows.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 18:01:44 +03:00
Hakan SevenandClaude Opus 4.8 b61bc0fdc0 fix(mleader): straight leaders, real landing + connection-anchored text (#347)
Render MultiLeader labels from the stored context fields instead of
re-deriving them:

- Draw each leader line with the line's own path type (code 170) rather
  than the MultiLeader's overall/style type. A spline-style leader can
  carry straight lines; splining a straight two-point run through
  Catmull-Rom bowed it, so the leader looked wavy with a dirty elbow.
- Drop the stray landing segment that ran from the dogleg end up to the
  text's top-left insertion, leaving just the horizontal dogleg.
- Anchor the horizontal-leader text so its bottom line sits at the
  connection point (the box then spans connection.y → text_location.y,
  matching the writer's content_base_point + landing_gap geometry).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 13:59:42 +03:00
Hakan SevenandClaude Opus 4.8 208bb099d5 chore(deps): bump acadrust — MLeader/WIPEOUT preview has_ds parse fix (#347)
Pulls acadrust 4ac3d68: entities with proxy/preview vector graphics
(MULTILEADER, WIPEOUT) omit the R2013+ has_ds_data bit, so reading it
desynced the record and produced garbage MultiLeader text_location
(~2e39 → NaN glyphs). The labels now parse with finite locations and
render through the existing MText SDF path.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 11:36:33 +03:00
Hakan SevenandClaude Opus 4.8 1e4b1f23a3 fix(text): grow the SDF glyph atlas instead of dropping glyphs (#347)
Text and dimensions vanished on a text-heavy, multi-font DWG. The global
SDF glyph atlas was a fixed 1024² page shared across drawings; once full,
pack() returned None and every further (font, char, bold) glyph was
silently skipped — so late-baked glyphs (dimension text especially) never
rendered.

Initialise the atlas at 2048² (covers real multi-font/CJK drawings with
no growth, so no stale UVs) and add a grow_height() fallback that doubles
height, appends zeroed rows, rescales cached entries' V/solid_uv and marks
the texture dirty (up to MAX_ATLAS_PX = 8192) rather than dropping glyphs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 11:36:33 +03:00
Hakan SevenandClaude Opus 4.8 8b133ceab2 fix(layout): create the CTAB variable so the exact paper tab round-trips
Saving from a non-first paper layout still reopened on the first paper
tab: set_saved_active_layout only updated an existing CTAB entry, but
documents authored here never carried one, so CTAB was never written and
the reader fell back to $TILEMODE (which only records model-vs-paper) →
the first paper layout.

set_saved_active_layout now creates the CTAB DICTIONARYVAR under the root
named-object dictionary when it is absent (updating in place otherwise).
The root dictionary is taken from the header handle, or found by scanning
for the dictionary that holds ACAD_LAYOUT when a from-scratch document
has not populated the header handle yet. The writers persist it (they
serialize the document's objects and root-dictionary entries; the
root-dict rebuild in CadDocument::build() is not run on save).

Adds tests/active_space_roundtrip.rs, including a full DXF save→reload
round-trip asserting both $TILEMODE and CTAB survive.

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 01:01:40 +03:00
Hakan SevenandClaude Opus 4.8 5215a9773b chore: remove orphaned manage/mview.rs dead code
manage/mod.rs never declared `mod mview`, so this file was never
compiled. It also carried a stale f32 `on_point` signature predating
the DVec3 command trait. The live MVIEW command is layout/mview.rs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 00:37:49 +03:00
Hakan SevenandClaude Opus 4.8 62bd360e46 refactor(precision): widen command transform scalars and defaults to f64 (#311)
Completes the f64 migration on the CPU data path: the only f32 left now
is the GPU/render/screen layer, which is architecturally required (wgpu
vertex buffers and iced are f32; f64 vertices would double memory).

- EntityTransform::Rotate.angle_rad and Scale.factor are now f64 (the
  transform was already applied in f64 around the DVec3 center, so this
  only preserves the typed angle/factor). apply_standard_transform and
  apply_standard_scale take f64.
- CmdResult::ExtrudeEntity.height and AlignSelected.{angle_rad,scale}
  are f64; the EXTRUDE height parses as f64.
- defaults.rs (last-used construction values: circle radius/diameter,
  rotate angle, scale factor, offset/fillet/chamfer distance, array
  spacing/counts/angle) moved from Cell<f32> to Cell<f64>. Array counts
  round-trip through u32 (exact below 2^53).
- Consumers in circle/array/fillet/offset/rotate/scale/align/solid3d and
  dimension.rs rotate_point/scale_point follow. `as f32` is kept only
  where a value feeds a WireModel rubber-band/ghost (the preview path is
  screen-space f32).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 00:34:38 +03:00
Hakan SevenandClaude Opus 4.8 553b91015f fix(precision): keep BLOCK base and TEXT/MTEXT insertion points in f64 (#311)
Three commit helpers narrowed a typed/picked point back to f32 before it
reached the persisted entity, so the coordinate was quantized to the f32
grid (worse at large/UTM extents):

- Scene::create_block_from_entities took the base point as glam::Vec3, so
  the block's local-space translation and the replacement INSERT's
  insert point were f32. Param is now DVec3 (matching the sibling
  define_block_from_owned_entities); the command_driver handler no longer
  downcasts with as_vec3().
- The MTEXT and TEXT in-place editors stored `pos` as Vec3 and built the
  committed insertion_point from it, quantizing a new label's position
  and re-quantizing an existing one on edit. Both `pos` fields are now
  DVec3.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 00:34:26 +03:00
Hakan SevenandClaude Opus 4.8 1f89b78a7e fix(precision): keep typed hatch boundaries exact in f64 (#311)
The HATCH/GRADIENT/BOUNDARY commands built the persisted hatch through
the render-side boundary (f32 offsets from world_origin), and with a
zero world_origin that meant absolute f32 - so a typed boundary vertex
of 2000.8 was stored as 2000.8020, and at UTM scale the fill landed far
from its boundary.

HatchModel.boundary stays f32 (the GPU render contract). Added
HatchModel.boundary_wcs: Option<Arc<Vec<[f64;2]>>>, an exact absolute-WCS
boundary set only by the draw commands. Scene::add_hatch persists from
boundary_wcs when present (exact f64 -> DxfHatch), else falls back to the
f32 + world_origin reconstruction used by DXF-rebuilt hatches. manual_pts
is now Vec<DVec3>; make_hatch takes the f64 boundary, stores the exact
copy and derives the RTE render rep. The committed render model is
rebuilt from the DXF entity, so nothing renders the command model's f32
boundary. This also fixes command hatches mis-placing at large
coordinates (world_origin was [0,0]).

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

Two-layer fix:

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 20:18:50 +03:00
Hakan SevenandClaude Opus 4.8 4bc196affd refactor(mtext): edit the structured document, not the raw string (phase 2)
Move the editor's body edit ops (typing, Enter, backspace, delete, paste,
selection replace/delete) off the raw-string + visible_spans splice and
onto acadrust's structured MTextDocument.

The document is flattened to a Vec<Cell> (one cell per visible slot: a
char, a paragraph break, or an atomic stacking-run glyph), spliced/drained
by flat visible index, and rebuilt into paragraphs+spans — so Enter splits
a paragraph, Backspace/Delete at a boundary merges, cross-paragraph
selection delete works, and stacking fractions stay atomic. rebuild is the
single doc-sync point (re-parse + restore the trailing empty paragraph that
parse_mtext drops, else Enter at the end loses the new line).

Also: the caret on a fresh trailing line — which the layout emits no glyph
box for — is made visible by injecting a space into the preview value only
(never committed) so the layout lays that line out and the caret gets a
correctly-sized box on it.

visible_spans now serves only the formatting (\L/\Q/…) splice path, which
moves to structured properties in the next phase. Adds 9 unit tests for the
cell model + round-trip/trailing-break handling.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 15:07:23 +03:00
Hakan SevenandClaude Opus 4.8 4b605d0ede refactor(mtext): add structured MTextDocument mirror to the editor (phase 1)
First phase of moving the in-app MTEXT editor onto acadrust's structured
MTEXT model. Adds a `doc: MTextDocument` field kept in sync with the raw
content via `parse_mtext` on every preview rebuild, plus `VisSlot` and
`doc_vis_map` — the unified visible-index space that will replace the
hand-rolled `visible_spans` walker (it matches the layout's vis counting:
one caret slot per paragraph break, one per visible char, none for a tab).

Purely additive scaffolding: nothing consumes the doc yet, so editor
behaviour is unchanged. Later phases make the doc authoritative for edits,
formatting, commit and load, then delete the raw-string path.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 12:36:26 +03:00
Hakan SevenandClaude Opus 4.8 d37dabb0fb fix(mtext): show preview glyphs and make Enter break the line
Two defects in the in-app MTEXT editor, both surfaced once the preview
text became visible:

1. Invisible preview text. After the SDF text migration, tessellate emits
   glyphs as SDF quads on `text_verts` and leaves the polyline `points`
   empty, but the editor preview draws on a 2D canvas that can't run the
   SDF shader — so only the selection box showed. Add a `force_text_strokes`
   parameter to tessellate: when set, TEXT/MTEXT run-groups also emit their
   glyph outline strokes as points. The preview passes true (real stroke
   font, aligned with glyph_boxes); every other caller passes false and
   keeps SDF-only text.

2. Enter did not start a new line. mtext_type inserted a literal newline,
   but the MTEXT layout only breaks on the `\P` code (a raw 0x0A is a break
   only to the caret logic). Normalise every literal newline — from Enter or
   a multi-line paste — to `\P`, so the line breaks and the saved value
   stays standard.

Closes #308

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

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

Refs #299

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 10:11:28 +03:00
Hakan SevenandClaude Opus 4.8 438cbd17b1 feat(help): open GitHub Discussions from the HELP command
HELP was registered for autocomplete but had no dispatch arm, so it
reported an unknown command. Route it through open_url to the project's
GitHub Discussions page, matching the DONATE/WEBVERSION external-link
commands.

Closes #307

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 10:02:08 +03:00
Hakan SevenandClaude Opus 4.8 2a72227d5d feat(attribute): route ATTMAN/BATTMAN to the attribute editor (ATTEDIT)
ATTMAN and BATTMAN previously produced a command-line listing of block
attributes. Route both to the attribute editor instead: extract the bare
ATTEDIT behavior into a shared open_attedit_dialog helper (edit the
selected block, or start the block-pick command) and call it from the
ATTEDIT, ATTMAN, and BATTMAN arms.

Kept as a match arm rather than an ocad.pgp alias so it works for
existing users, whose config pgp is only seeded on first run.

Replace the listing regression tests with one asserting ATTMAN and
BATTMAN behave byte-for-byte like ATTEDIT.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 09:37:54 +03:00
Hakan Seven bd6a8845b4 Merge PR #351: make BATTMAN work like ATTMAN (#295)
External PR by Kevin Griffin (@KevinGriffin-new).
The ATTMAN/BATTMAN arm stripped the argument with
trim_start_matches("ATTMAN"), which left "BATTMAN" intact as a bogus
block-name filter, so BATTMAN always reported 'no matching block'.
Strip whichever keyword was actually typed (BATTMAN first, since ATTMAN
is a suffix of it) and match the 'BATTMAN ' prefix form. Adds two
regression tests driving the real command dispatcher.
2026-07-11 09:28:48 +03:00
Hakan Seven 816cb059dd Merge PR #353: fix View Cube toggle (highlight follows state, cube clears on hide)
External PR by Karim Jerbi (@KarimJerbi).
- NAVVCUBE is a command toggle so it never set active_tool; add a
  show_viewcube flag to the ribbon and special-case is_active_tool so the
  button highlights only while the cube is on.
- show_viewcube was absent from render_signature, so toggling a cached
  view produced an identical signature and left stale cube pixels; hash
  it into the signature so the toggle invalidates the cache and clears.
2026-07-11 09:20:32 +03:00
Hakan SevenandClaude Opus 4.8 6032a024d2 feat(save): native OS dialog for Save As, keep in-app format picker
Replace the custom in-app file-browser save dialog with the native OS
save dialog for choosing the destination path — folder browsing and
overwrite confirmation now come from the OS. A compact in-app modal
still selects the DWG/DXF format and version (a native dialog cannot
carry the version year) and appears only for Save As. Plain Save on an
unsaved drawing defaults to DWG 2018 with no picker. The file-name field
is shown only on web, where there is no native dialog.

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 09:12:36 +03:00
Karim Jerbi 4e0bba9f6a fix(viewport): clear ViewCube when toggled off
show_viewcube was excluded from render_signature, so toggling a static (cached) view produced an identical signature — the frame was skipped and the last GPU cube pixels stayed as stale cache. Add vp.show_viewcube to render_signature so the toggle invalidates the cache and re-renders, clearing the cube. The 2D overlay is iced UI and re-laid-out correctly, which is why only it hid.
2026-07-11 00:40:28 +01:00
Karim Jerbi 585316945e fix(ribbon): highlight View Cube button by toggle state
NAVVCUBE is a command toggle, so it never set active_tool and is_active_tool never matched it. Add a show_viewcube flag to the ribbon (mirroring wireframe/ortho_mode) and special-case "NAVVCUBE" in is_active_tool to return the live show_viewcube state; ToggleViewCube now calls ribbon.set_viewcube.
2026-07-11 00:40:22 +01:00
Hakan SevenandClaude Opus 4.8 f3482e8b3e chore(release): bump to 0.8.0
Co-Authored-By: Claude Opus 4.8 <[email protected]>
v0.8.0
2026-07-11 02:32:06 +03:00
Hakan SevenandClaude Opus 4.8 3c57260ddb fix(thumbnailer-win): make the COM DLL compile on the MSVC target
First real Windows build surfaced errors that a Linux host couldn't catch:

* Enable the missing `windows` feature gates (Win32_System_LibraryLoader,
  Win32_System_SystemServices, Win32_Security — the last brings in the
  `Ex` registry APIs) so the imports resolve.
* Depend on `windows-core`: the `#[implement]` macro expands to
  `windows_core::` paths, which need the crate nameable in the root.
* `IClassFactory::LockServer` takes `Win32::Foundation::BOOL`, not a
  `core` BOOL.
* Name the returned image type via a new `dwg_thumbnailer::RgbaImage`
  re-export instead of a direct `image` dependency.

Verified with `cargo check --target x86_64-pc-windows-gnu -p
dwg-thumbnailer-win` (and the native check still passes).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 02:11:22 +03:00
Hakan SevenandClaude Opus 4.8 a5c6c3f116 ci(release): build the thumbnail helper crates as explicit targets
A plain `cargo build --release` at the workspace root builds only the app
package and its dependencies, so it never emitted the artifacts the
packaging steps consume:

* libdwg_thumbnailer.a — the staticlib crate-type is produced only when
  dwg-thumbnailer is built as a target (as a dependency cargo makes just
  the rlib); the macOS QuickLook .appex links this static lib.
* dwg_thumbnailer_win.dll — dwg-thumbnailer-win is a workspace member,
  not an app dependency, so the DLL the WiX MSI references was missing.

Build each helper crate explicitly on its platform, and link the extra
system libraries (Security/SystemConfiguration/iconv) the Rust staticlib
pulls in against the QuickLook extension.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 01:48:14 +03:00
Hakan SevenandClaude Opus 4.8 ba3b3202e8 feat(thumbnails): embed DWG previews and wire cross-platform file thumbnails
Round-trip a raster preview through the DWG's embedded preview slot and
surface it to the OS file managers, so drawings show their contents in
Explorer / Finder / Nautilus instead of a generic document icon.

OCS:
* io::thumbnail — rasterize the scene to a BMP DIB on save (embedded via
  acadrust's new Preview type) and read it back on open for the Start
  page recent list; `--dwg-thumbnail IN OUT SIZE` CLI extracts a badged
  PNG for external thumbnailers.
* io::file_association::install_thumbnailer — self-install the OS
  integration on startup: Linux writes a .thumbnailer + hicolor mimetype
  icons; Windows registers the IThumbnailProvider DLL under HKCU.

Shared core:
* crates/dwg-thumbnailer — lean (image-only) preview extractor + the
  `badge_dwg` full-width "DWG" band, used by every platform so the
  ribbon is single-sourced. Ships an rlib+staticlib.
* crates/dwg-thumbnailer-win — IThumbnailProvider COM in-proc server
  (cfg(windows), CI-built as a workspace member).
* macos/ — QuickLook thumbnail extension (Swift + C-ABI bridge to the
  core staticlib), assembled into a .appex.

Icons & packaging (single SVG source -> per-platform assets in CI):
* assets/mimetypes/image-vnd.{dwg,dxf}.svg — themed file icons.
* packaging: WiX ships dwg/dxf.ico + the thumbnail DLL and points the
  ProgIds at them; Info.plist gets CFBundleTypeIconFile + the QuickLook
  extension in Contents/PlugIns.
* release.yml — generate .ico (Windows) and .icns (macOS) from the SVGs,
  build the QuickLook .appex, and bundle everything.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 01:27:17 +03:00
Kevin GriffinandClaude Opus 4.8 0f6999a838 fix(blocks): make BATTMAN work instead of erroring like ATTMAN (#295)
BATTMAN matched the ATTMAN command arm but the argument was extracted
with `cmd.trim_start_matches("ATTMAN")`. "BATTMAN" has no leading
"ATTMAN" to strip, so the whole word survived as a bogus block-name
argument — BATTMAN always reported "ATTMAN: no matching block" instead
of listing attributes. `BATTMAN <name>` never matched at all.

Strip whichever keyword was actually typed (BATTMAN first, since ATTMAN
is a suffix of it) with `strip_prefix`, and match the `BATTMAN ` prefix
form too. BATTMAN now behaves identically to ATTMAN.

Adds headless regression tests driving the real dispatcher; they fail on
the previous code with the reported symptom and pass with the fix.

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

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

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 23:03:23 +03:00
Hakan SevenandClaude Opus 4.8 d38fcda07c feat(ribbon): fit tabs and collapse panels to dropdowns on narrow screens
Two narrow-mode (e.g. phone-width) ribbon fixes; both are no-ops when the
row has room, so desktop layouts are unchanged.

- Tab row: let the inter-tab gap squeeze from 6px down to -12px, tucking
  neighbouring tabs into each other's 14px side padding (labels never
  overlap) so all tabs stay on one row instead of wrapping "Manage" onto a
  second line. The gap only goes negative as far as needed to fit.
- Tool panels: at the tightest collapse step each panel is now a single
  non-running button — the panel's first tool icon + title + a down arrow
  that opens the tools flyout. No tool runs directly at this density; you
  pick one from the dropdown.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 21:24:24 +03:00
Hakan SevenandClaude Opus 4.8 0cd861cc76 fix(hatch): anchor prebaked pattern origin near the geometry
Prebaked hatch fills anchored each pattern family at its authored
base point minus world_origin. On UTM-scale drawings that offset is
~1e6, which broke the fill two ways:

  - the shader consumes x0/y0 as f32, so ~0.5 m quantization shredded
    the pattern phase; and
  - it evaluated the pattern that far from its origin, where the
    multi-family aggregate patterns (AR-CONC, AR-SAND, GRAVEL, ...) —
    effectively quasi-periodic — dissolve from their interlocking
    stones into scattered dashes.

Boundaries were unaffected throughout; they ride the double-single
relative-to-eye path.

Fold the origin's offset from world_origin down to a small,
coherence-safe remainder on a `spacing * 64` grid, applied as one
common fold so every family keeps the relative phase that forms the
stones. Because the origin grip / Origin X/Y edit shifts every base
point by the same delta, the sub-grid remainder tracks it 1:1, so the
grip still moves the fill; a grid crossing needs a full `spacing * 64`
drag and is never hit by realistic alignment drags.

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 15:54:57 +03:00
Hakan SevenandClaude Opus 4.8 6bbd88153b feat(statusbar): hide niche pills by default, user opts them in
The status bar showed all 17 pills out of the box. Hide the informational /
niche ones by default so the bar stays uncluttered — Coordinates, Show
Lineweight, Dynamic Input, Model/Paper Space, Drawing Units, Show
Transparency, Selection Cycling and Viewport Count. The user turns any of them
back on from the customization (gear) menu, which persists to statusbar.txt.
The main drafting toggles (Ortho, Polar, Otrack, Osnap, …) stay visible.

An existing statusbar.txt (even empty = user showed everything) is
authoritative; only a fresh install with no config gets the new defaults.

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

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 15:45:08 +03:00
Hakan SevenandClaude Opus 4.8 7cba5c8679 feat(viewport): hide ViewCube then render bar as the viewport narrows
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 <[email protected]>
2026-07-10 14:58:48 +03:00
Hakan SevenandClaude Opus 4.8 07eb9dcb9a fix(offset): allow OFFSET of XLINE (and Ellipse/Spline) objects (#296)
OFFSET rejected XLINE targets: on_entity_pick only accepted
Line/Circle/Arc/LwPolyline, compute_offset had no XLine arm, and
entity_wire_pts returned no points for it (so hover/preview/through-distance
failed too).

Add offset_xline (parallel infinite line: same direction, base point shifted
perpendicular toward the pick side), a compute_offset arm, and an
entity_wire_pts case (a long segment standing in for the infinite line during
hit-test and preview; the committed XLine renders true). Also widen the pick
filter to every type compute_offset supports — this additionally reaches
Ellipse and Spline, whose offset functions already existed but weren't
selectable.

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

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

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 13:32:32 +03:00
Hakan SevenandClaude Opus 4.8 824a508555 fix(snap): Intersection requires a real 3D meeting, not a plan crossing (#335)
seg_intersect_xy found where two segments' XY projections cross and returned
the point at z=0, ignoring height. Two lines at different Z whose plans cross
therefore fired a real Intersection snap when they only *appear* to cross in
top view — that case belongs to Apparent Intersection (view-space).

Rename to seg_intersect_3d and require the segments to be at the same height
at the crossing: compute Z on each at its crossing parameter and reject when
they differ (tolerance scaled to coordinate magnitude). A genuine crossing
now snaps at its true height instead of z=0; a plan-only crossing is left to
Apparent Intersection.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 13:05:48 +03:00
Hakan SevenandClaude Opus 4.8 a84c1c0bcb fix(snap): no spurious extension guide from arcs at line intersections
extension_bases_screen only skipped closed curves (circle/ellipse, keyed on
the Quadrant hint) when finding which endpoints' extension rays a snapped
point lies on. An arc is an open curve with no Quadrant hint, so its
tessellation chords were scanned; one straight chord-extension grazes a
nearby line-line intersection (beyond the arc endpoint, within 4px), drawing
a spurious dashed extension guide radiating from the arc.

Skip every round curve by the Center hint (which arcs, circles and ellipses
all carry and straight lines never do) — the same signal nearest_segment
uses. The intersection snap point itself was already correct; only the stray
guide line is removed.

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

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 12:39:22 +03:00
Hakan SevenandClaude Opus 4.8 f5b8a7d993 chore(deps): bump acadrust for *Model_Space block-record fix
Picks up acadrust 716d4b4: DWG reader synthesizes the mandatory
*Model_Space/*Paper_Space block records when a file's BLOCK_HEADER for
them never materialised, so model-space geometry is no longer dropped on
save (file shrank + reopened blank).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 11:33:40 +03:00
Hakan SevenandClaude Opus 4.8 c249a99d4c fix(clipboard): paste anchor at bounding-box lower-left, not center (#285)
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 <[email protected]>
2026-07-10 11:16:13 +03:00
Hakan SevenandClaude Opus 4.8 f5870b011d fix(text): show text in command previews (move/copy/etc) (#316)
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY,
PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/
scaled/stretched/mirrored, but those helpers moved only `points`, never
`text_verts`, and the ghosts' glyphs were never uploaded — so text
vanished (or froze at its source) during the preview, same root cause as
the grip-drag case.

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 10:39:44 +03:00
Hakan SevenandClaude Opus 4.8 88450f0cd1 fix(web): keep wasm target_features section so wasm-opt accepts bulk-memory
The release profile's strip = true (added in #324) strips the wasm
target_features custom section. Without it, wasm-opt can't tell that
bulk-memory is enabled and rejects the module's memory.fill/copy ops
("operations require bulk memory"), failing the release web build — the
newer Rust toolchain now emits those ops by default. Override strip only
for the wasm target with -Cstrip=none, so native release binaries stay
stripped while the wasm keeps its feature section.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 09:36:43 +03:00