Commit Graph
506 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.8 0dd86eb658 feat(tablestyle): editable General section (margins, flow-suppress toggles)
The Table Style editor's General fields are now editable: H/V cell margins
(buffers + Apply) and title/header row-suppressed toggles, loaded from the
selected style on select. Per-cell (Data/Header/Title) text/fill/border
editing remains a follow-up.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-03 20:13:30 +03:00
Hakan SevenandClaude Opus 4.8 a21caf821b feat(dimstyle): expand Dimension Style editor to the full field set
Adds the remaining ~40 DIMSTYLE variables across the tabs (and a new
Alternate Units tab): dim/ext line colors+lineweights, fixed-length ext
lines, separate arrow blocks, arc symbol, jog angle, text color/justify/
vertical pos/fill/direction, fit options (DIMATFIT/DIMTIX/DIMSOXD/DIMTMOVE/
DIMUPT/DIMTOFL/DIMFIT), decimal separator, rounding, zero suppression,
fraction + angular units, alternate units, and tolerance vert-just/zero
suppression. Only the arrowhead-block / extension-line-linetype Handle
references are left (they need a block/linetype picker).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-03 20:09:08 +03:00
Hakan SevenandClaude Opus 4.8 4675a439da feat(tablestyle): add Annotative toggle to the Table Style editor
The table-style details panel gains an Annotative checkbox that flips the
new TableStyle.annotative flag, rounding out annotative across the Text,
Dimension and Table style editors.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-03 17:51:45 +03:00
Hakan SevenandClaude Opus 4.8 07a2a05975 feat(dimstyle): add Annotative toggle to the Dimension Style editor
Adds the annotative property (DsField::Annotative) to the dim-style edit
buffers, load/apply, and a checkbox on the Fit/Scale tab — consistent with
the Text Style editor's annotative toggle.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-03 17:49:38 +03:00
Hakan SevenandClaude Opus 4.8 52850947d2 feat(textstyle): full Text Style editor — big/TT font, fixed height, flags, annotative
Expands the Text Style manager beyond font/width/oblique: big-font and
TrueType font fields, fixed text height, Backward / Upside-down flags, and
an Annotative toggle (applied live). Bumps acadrust for the annotative
field.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-03 17:43:12 +03:00
Hakan SevenandClaude Opus 4.8 8b3d083789 feat(leader): dynamic MLEADER text placement, grips, and consistent arrows
Reworks MLEADER text/leader geometry and grip editing, and aligns LEADER
to MLEADER's conventions (kept as separate entities).

Text & landing (render path):
- Text is side-anchored on the leader-facing edge and reads outward; the
  side, alignment and the horizontal landing are recomputed every frame
  from the text grip's position relative to the leader, so moving the
  arrow or the text re-mirrors the whole layout and the landing stays
  attached to the text's near edge.
- Fixes #49: text was centred on the leader endpoint regardless of side.

Grips:
- Text-location grip plus an MTEXT-style triangle wrap-width grip at the
  box's far edge; dragging it sets the MText wrap width.
- Multifunction grip menu restored and made functional: leader-line
  vertices offer Add Leader / Remove Leader (Add grabs the new arrow so it
  follows the cursor — click places, Esc removes it); the text grip offers
  Move Independent (text only) and Move with Leader (translates the whole
  multileader via a move-all sentinel grip).

Arrows:
- LEADER and MLEADER arrowheads now render at the same size (text height),
  matching previews.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-03 16:17:29 +03:00
Hakan SevenandClaude Opus 4.8 6f64d8d0d9 fix(text): keep in-place MText/Text editor on-screen at window edges
The editor overlay's anchor was only clamped to the top/left edge, so an
MTEXT placed near the right/bottom of the viewport pushed the 640px panel
(and its toolbar buttons) off-screen where they were unclickable.

Both the MText editor and the single-line TEXT editor now receive the
viewport canvas size and clamp the anchor against the right/bottom edges
too, so the whole panel stays visible and usable wherever it is opened.

Fixes #52

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-03 10:59:59 +03:00
Hakan SevenandClaude Opus 4.8 f0b11acc88 feat(viewport): per-tile model render mode, persisted via VPORT entries
Each model-space tile now carries its own visual style instead of one
global mode shared by every pane:

- ModelTile gains a `render_mode`; the active tile renders with the live
  picker mode, inactive tiles keep their stored style, so changing one
  tile no longer changes the others. Split inherits the source style.
- SetRenderMode writes only the active tile; switching the active tile
  mirrors its style back into the tab (picker + live render).
- Persisted through the *Active VPORT entries (acadrust now round-trips
  the render mode via DXF code 281 / DWG RC 281), restored on open for
  both tiled and single-tile model layouts.
- Bump acadrust to pick up the VPORT render-mode field.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-03 10:32:48 +03:00
Hakan SevenandClaude Opus 4.8 aec9a9575e fix(paper): keep floating-viewport fills clipped + show lines while editing in MSPACE
Two paper-space rendering fixes:

- The full-canvas sheet instance no longer draws model-block hatches/
  wipeouts/images. Those belong inside the floating content viewports;
  rendering them on the sheet let them bleed past the viewport borders
  when model coords overlapped the paper area. The sheet now draws only
  the paper block's own fills plus the synthetic white printable area.

- Pan/zoom/orbit inside a double-clicked floating viewport (MSPACE) now
  re-culls and re-uploads the viewport's wire set. The per-viewport wire
  cache key now hashes the viewport's own view (pan + zoom + orbit), and
  the active-viewport pan/zoom/orbit handlers bump camera_generation so
  the GPU re-uploads. Previously the stale frustum-culled subset stayed
  on the GPU, so newly-revealed lines were invisible (hatches, uploaded
  as the full set, still showed) until MSPACE was exited.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-03 00:36:28 +03:00
Hakan SevenandClaude Opus 4.8 ef855b56c5 fix(layout): persist camera before switching layouts
Switching between Model and a paper layout reset the camera to the last
saved state. The current camera was only written to the document on a
periodic tick, which may not have run since the last pan/zoom — so the
camera of the layout being left was lost. Sync it explicitly before
set_current_layout so returning restores where the user left off.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-02 23:51:17 +03:00
Hakan SevenandClaude Opus 4.8 ac4126072c refactor(paper): drop sheet viewport border + clean stale PaperCanvas docs
The overall "sheet" viewport now IS the paper view, so its own border
rectangle must not be drawn as an entity — filter it out of
paper_sheet_wires_arc (content viewport borders are kept).

Also update the doc comments that still referenced the removed
PaperCanvas widget / paper_canvas_wires to describe the GPU sheet path.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-02 20:33:51 +03:00
Hakan SevenandClaude Opus 4.8 206ad1a4aa feat(paper): render the whole sheet via the shader, drop PaperCanvas
Paper space now renders entirely through the unified GPU shader, exactly
like model space — there is no separate 2-D canvas. The full-canvas
top-locked "sheet" viewport draws the layout's entities, hatches,
wipeouts and images; the desk is the container background and the white
printable area is a synthetic solid hatch.

- The white sheet fill uses draw_depth -2.0 (entity fills/wires are in
  the signed (-1, 1) range) so it is always strictly behind every object,
  with a negligible z offset (BIAS 0.001) — no far-plane clipping.
- Delete src/scene/paper_canvas.rs (the PaperCanvas widget) and its dead
  helpers (paper_canvas_wires / paper_sheet_wires / paper_canvas_cache).
  paper_canvas_hatches / _wipeouts remain — still used by paper hit-test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-02 20:25:50 +03:00
Hakan SevenandClaude Opus 4.8 0ef99ff2fc feat(paper): render the sheet as a top-locked GPU viewport
Paper space now renders its sheet through the unified shader, the same
path as model space: a full-canvas "sheet" ViewportInstance draws the
layout's own entities + viewport borders + interim/preview wires, with a
camera locked to the top/plan orientation (paper is 2-D and never
orbits). Floating content viewports overlay it.

- PaperCanvas no longer draws entity wires (still paints the desk, sheet
  and hatch/wipeout/image fills; full removal is the next step).
- Right-drag on the paper sheet no longer orbits the camera (it would
  corrupt the frame and skew subsequent pans).
- Fix floating-viewport orbit oscillation: orbit_active_viewport wrote
  view_direction.y negated while camera_for_viewport reads it un-negated,
  so each drag step read back a Y-mirrored camera and flipped the model
  between a rotation and its opposite every frame. Store eye directly,
  matching snap_active_viewport_to_direction and the reader.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-02 20:05:51 +03:00
Hakan SevenandClaude Opus 4.8 568422679d feat(paper): native sheet viewport for paper-space camera
Persist the paper-space camera through the sheet viewport entity (which
round-trips natively in both DXF and DWG) instead of the
OpenCADStudio_Camera_* named-View side-channel:

- apply_sheet_viewport_camera reads the sheet viewport entity first; the
  named View is only a backward-compat fallback for older files.
- sync_camera_to_document (paper) writes the camera only to the sheet
  viewport entity. Model branch is unchanged.
- Add Scene::ensure_sheet_viewport: guarantee every paper layout has its
  full-screen overall (id==1) viewport, creating one if a loaded file
  lacks it; wired into layout enter and add_layout.
- Restore the owner-handle guard on the sheet-viewport finder so a
  foreign layout's viewport can't be picked as the sheet viewport.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-02 19:38:42 +03:00
Hakan Seven 4a55cb1b0f fix(scene): use layout-specific viewport classification and preserve duplicate AutoCAD *Active viewports 2026-06-01 19:57:36 +03:00
Hakan SevenandClaude Opus 4.8 47a4bf19b3 fix(select): pick 3D solids by clicking their body, not just edges
Single-click selection only tested wire-edge proximity, so clicking a
solid's face missed — only its thin projected edges were selectable.
Add mesh_click_hit: project each mesh triangle to screen and test the
cursor against it (front-most wins), wired as a fallback after the wire
and hatch hit-tests. Works for any meshed solid.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 19:47:07 +03:00
Hakan SevenandClaude Opus 4.8 7bf9ce31ae feat(model): 3D solid modelling tab — primitives + boolean ops
Rename the Home tab to "Draw" and add a Model tab beside it with two
groups:

- Model: BOX / CYLINDER / CONE / SPHERE / WEDGE / TORUS, placed with a
  footprint + height. Each is built as a real ACIS Solid3D (acadrust
  primitive builders) plus a truck B-rep, tessellated into the shaded
  mesh pipeline, with edge wires for picking + wireframe.
- Design: UNION / SUBTRACT / INTERSECT via truck-shapeops.

A session-only Scene.model_solids cache holds each solid's truck B-rep
so the boolean tools can combine and chain them. Supersedes the old
command-line BOX/SPHERE/CYLINDER (insert::solid3d_cmds keeps EXTRUDE/
REVOLVE/SWEEP/LOFT).

Known limitations: booleans need solids created this session; curved
ACIS primitives render in-session but not after reload (the SAT
tessellator handles only planar caps); geometry assumes world_offset 0.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 19:27:06 +03:00
Hakan SevenandClaude Opus 4.8 4acb820e01 fix(ribbon): dismiss stuck style dropdown on viewport click
The annotation style combo renders its panel inline with no backdrop to
catch outside clicks, so it stayed open after clicking into the viewport
or starting another tool. Close any open ribbon dropdown on viewport
press (left/right/middle) and on command dispatch.

Fixes #50

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 14:57:50 +03:00
Hakan SevenandClaude Opus 4.8 2627d06224 feat(text): in-place editors for all text entities
Route every text-bearing entity to an in-place editor instead of the
command line: a plain text box for single-line text (Text, attributes,
dimension override, tolerance) and the rich MText editor for MText /
MultiLeader. A Leader resolves to the entity it annotates.

- TEXT places a plain text box at the click; double-click edits in place.
- DDEDIT and double-click on any text entity open the matching editor.
- MLEADER places the leader then opens the MText editor on its content.
- LEADER follows the classic flow: points, Enter, then a linked empty
  MText annotation opens in the editor; the leader's annotation_handle
  links the pair so they edit/erase as a unit.

Adds shared field read/write helpers and a begin_text_edit router, plus
CommitAndEditText / CommitManyAndEditText results that commit an entity
and open its editor (commit_entity now returns the new handle).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 14:44:18 +03:00
Hakan SevenandClaude Opus 4.8 ac9022676f feat(mtext): editable preview with caret, blink, and new-line support
Type directly into the rendered MText preview: click to position the
caret, insert/delete, Enter for a new line, Space for a literal space,
and arrow keys to move. The caret blinks when idle.

Keep the trailing empty paragraph in the editor so the caret appears on a
fresh line immediately after Enter, before any typing. parse_mtext_paragraphs
split into a _ex variant; layout_mtext skips the blank-edge trim when
emitting glyph boxes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 13:14:35 +03:00
Hakan Seven 9fa33ec7a0 fix(wire_gpu): correct offset calculation for wire midpoint positioning 2026-05-31 10:24:27 +03:00
Hakan SevenandClaude Opus 4.8 3cf21c1cc2 chore(mtext): drop unused preview-edit message variants
MTextType/MTextBackspace/MTextDelete were never emitted — preview typing,
backspace and delete route through the existing CommandAppendChar /
CommandBackspace / DeleteSelected handlers, which call the methods directly.
Removes the dead variants and their arms (fixes a never-constructed warning).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 10:20:39 +03:00
Hakan SevenandClaude Opus 4.8 7190df8274 feat(mtext): edit text directly in the preview with a caret
The rendered MText preview is now an editable surface: it shows a text caret,
click to position it between glyphs, and type/Backspace/Delete/Enter to edit.
Left/Right arrows move the caret; drag-selection + toolbar formatting still
work and typing replaces a selection.

Keystrokes are routed by mapping visible-character offsets to byte spans in
the value (raw newlines count as paragraph breaks, matching the layout). While
the preview is open the command line releases its on_input so typed glyphs
reach the preview instead, and the caret starts at the end of the text so
typing works without a click first.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 10:16:30 +03:00
Hakan SevenandClaude Opus 4.8 50115c55d8 feat(fonts): complex-linetype shapes via LFF + Turkish glyph fallback
Restore complex-linetype embedded shapes: ltypeshp is converted from the old
CXF shape file to LFF (assets/fonts/ltypeshp.lff), with the bulge moved to the
LibreCAD preceding-segment convention. The LFF parser now keeps named shapes
(blocks whose label is a word, e.g. BOX/CIRC1/ZIG) in a separate map since
their codepoints collide, exposed via lff::shape(); complex_lt::emit_shape
draws them again at the pen position.

Missing glyphs now fall back through the chosen family -> unicode -> iso3098,
so Turkish letters (Ğ ğ Ş ş İ) render in any font (dotless ı is the lone gap
iso3098 lacks).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 09:53:16 +03:00
Hakan SevenandClaude Opus 4.8 c1b33677e2 feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.

Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.

All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 03:31:20 +03:00
Hakan SevenandClaude Opus 4.8 32136dd9dd feat(mtext): character-level text selection on the preview
The rendered MText preview is now interactive: drag across glyphs to select
a character range and apply toolbar formatting (bold, italic, underline,
overline, strike, case, alignment) to exactly those characters.

The text layout gains an opt-in pass that emits one world-space box per
visible character carrying its reading-order index; the editor maps those
boxes into the preview frame, hit-tests pointer drags against them, and
highlights the selection. Formatting splices the matching inline codes into
the raw value at the selected byte range (mapped via a code-aware visible-
character scanner). With no preview selection the Edit-box path is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 02:33:47 +03:00
Hakan SevenandClaude Opus 4.8 a07af32714 feat(mtext): segmented Edit/Preview toggle, default to preview
The editor opens in preview by default and switches body views with a
segmented Edit | Preview control placed between the toolbar and the body
(active tab highlighted). All toolbar properties feed the rendered preview.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 01:59:39 +03:00
Hakan SevenandClaude Opus 4.8 a5ad0666ce feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.

The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.

Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 01:51:20 +03:00
Hakan Seven de08bd598c feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
Hakan SevenandClaude Opus 4.8 e2507d5c54 fix(grip): Stretch grip-menu action engages the grip
Picking Stretch from the grip popup just closed the menu and returned, so
the grip never became hot and the follow-up click did nothing (issue #48).
It now engages the picked grip — tracking the cursor until the placement
click — the same as picking the grip directly in the viewport.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-30 22:09:05 +03:00
Hakan SevenandClaude Opus 4.8 dcdcb0f2b7 feat(grip): click-move-click grip editing
Grip editing now engages on a click and places on the next click, with the
entity tracking the cursor in between — no need to hold the button down.
Legacy press-drag still commits on release; the two are told apart by
whether the grip actually moved. Escape ends a hot grip at its last preview
position. Lingering press state from the engaging click is cleared so later
moves don't read as an in-progress drag.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-30 22:05:43 +03:00
Hakan SevenandClaude Opus 4.8 1b3760913e fix(selection): individual picks accumulate; Shift+click removes
Single-click picking cleared the selection first (exclusive), so clicking a
second object dropped the first (issue #47). Plain clicks now add to the
selection and Shift+click removes the picked entity; empty-space click and
Esc still clear. Shift state is tracked from keyboard ModifiersChanged
events since mouse click messages carry no modifiers.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-30 21:54:35 +03:00
Hakan SevenandClaude Opus 4.8 e4668c66e7 fix(zoom): Zoom Extents fits full drawing on first use
The viewport context-menu item dispatched a bare "ZOOM" string, which no
command branch handled, so it did nothing. Send "ZOOM EXTENTS" instead.

fit_all() also fed on the frustum-culled wire set, so it only framed the
entities already on screen — each invocation revealed a little more and the
view converged on the true extent only after several uses (issue #51). It
now builds an un-culled wire set at a fixed tessellation tolerance, so the
bounds are zoom-independent and the first call fits the whole drawing.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-30 21:40:48 +03:00
Hakan SevenandClaude Opus 4.8 09e7e90aae feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.

Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.

Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-30 21:29:01 +03:00
Hakan SevenandClaude Opus 4.7 14a981dc87 perf(wire): collapse sub-pixel dash patterns to solid line
When the smallest dash / gap element of a linetype pattern would project
below one pixel on screen, the gaps either shimmer as aliasing or vanish
entirely — the user reads the line as solid anyway. Compute the min
non-zero element of `pat0 / pat1` once in the vertex shader, flat-
interpolate it across the instance, and let the fragment stage skip the
arc-length dash test (and the `discard` for gap fragments) when
`min_elem >= u.world_per_pixel` fails. Mirrors the existing hatch-LOD
branch in `hatch.wgsl`; primary win is visual quality at extreme
zoom-out, with a small fragment-shader op saving as a side effect.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-30 02:02:43 +03:00
Hakan SevenandClaude Opus 4.7 a0800d95c9 feat(packaging): open files from argv, ship Windows MSI installer (#36)
Cross-platform prerequisite for OS-level file associations: the boot
task now consumes `std::env::args_os().nth(1)` as a path and dispatches
`Message::OpenRecent` for it. Flag-style args (starting with `-`) are
ignored, and `OpenRecent` already does the file-existence check, so a
bogus path lands as a clean command-line error instead of a panic.

Linux: the AppImage's desktop entry adds `image/vnd.dwg` alongside the
existing `image/vnd.dxf` MIME type and grows a `%f` placeholder in
`Exec=` so the launcher forwards the picked file path. Once the
AppImage is integrated with the system (xdg-mime / AppImageLauncher),
double-clicking a .dwg / .dxf opens it in Open CAD Studio.

Windows: new `packaging/windows/main.wxs` defines an MSI installer —
per-machine install to `Program Files\Open CAD Studio`, Start Menu
shortcut, `MajorUpgrade` so newer MSIs replace older ones, and a
single ProgID `OpenCADStudio.Drawing` that owns both `.dwg` and
`.dxf` and launches the exe with the file as `argv[1]`. The icon is
pulled from the SVG logo, converted to multi-resolution ICO at build
time with the runner's pre-installed ImageMagick. CI now also runs
`candle` / `light` (WiX Toolset 3, pre-installed on `windows-latest`)
and uploads both the bare `.exe` (portable) and `.msi` (installer)
to the release.

macOS Info.plist already declares the DWG / DXF UTIs; combined with
the argv-handling change above, double-clicking a drawing in Finder
will open it in Open CAD Studio without further packaging changes.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-29 23:34:14 +03:00
Hakan SevenandClaude Opus 4.7 92f368f275 fix(layer): refresh viewport after LAYOFF/LAYFRZ/LAYON/LAYTHW/LAYLCK… (#42)
The eight layer-state commands (LAYOFF, LAYFRZ, LAYLCK, LAYULK, LAYON,
LAYTHW, LAYISO, LAYUNISO) mutated `document.layers` directly through
`dl.turn_off()` / `dl.freeze()` / `dl.lock()` / … and then called
`refresh_layer_panel()` — but never bumped the scene's geometry epoch.
The 3D viewport caches its uploaded geometry keyed by that epoch, so
visibility / freeze changes only took effect once something else
triggered a re-render. Pressing Esc was the workaround because the Esc
handler calls `deselect_all()`, which internally bumps geometry.

The Layer Properties panel toggles already did this (their handlers in
`update.rs` call `scene.bump_geometry()` after each freeze / visibility
toggle), which is why the panel worked and only the command path
exhibited the lag.

Add `scene.bump_geometry()` after the layer mutation loop in each of
the eight command branches so the viewport repaints immediately. Lock /
unlock don't strictly affect rendering today but are bumped for
consistency — selection / grip code may grow lock-aware later.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-29 23:17:58 +03:00
Hakan SevenandClaude Opus 4.7 6994837d0e fix(shader): satisfy DX12/FXC X3507 in hatch fs_main (#31)
Both hatch fragment shaders ended their `fs_main` with a bare
`discard;` after the pattern-match for-loop falls through without
matching. WGSL accepts this — discard kills the fragment, so there is
nothing left to return — and naga (the Linux/Vulkan path wgpu defaults
to) lowers it without complaint. DX12's FXC (the default Windows
compiler) is stricter and rejects it with `X3507: 'fs_main': Not all
control paths return a value`, taking the entire pipeline create-down
on every drawing open.

Append an unreachable `return vec4<f32>(0.0);` after each terminal
`discard;` so every control path syntactically ends in a return. The
return never executes at runtime; it only exists to make FXC's
all-paths-return analysis succeed. Naga still accepts the shader
unchanged, so the Vulkan / Metal / WebGPU paths are unaffected.

Reported by @Ward-Vandepitte (Windows 11, DX12, wgpu 27.0.1).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-29 21:16:21 +03:00
Hakan SevenandClaude Opus 4.7 7d9ac435cb feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.

Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.

Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.

The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-29 21:09:56 +03:00
Hakan SevenandClaude Opus 4.7 9db206f310 feat(select): Select Similar + Quick Select panel (#28)
Two ways to extend the current selection by entity type + layer:

* `Select Similar` (right-click → Select Similar, or `SELECTSIMILAR` /
  `SELSIM`): for every (type, layer) pair in the current selection, add
  every entity in the active layout that matches the pair. The seed
  selection stays selected; matches already selected are not
  double-counted.
* `Quick Select` (right-click → Quick Select…, or `QSELECT` / `QS`):
  opens a centred floating panel with Object-type and Layer pick_lists
  (populated only with values that actually exist in the active layout)
  and an "Append to current selection" checkbox. `(Any)` in either slot
  skips that filter. Apply replaces the selection (or extends it with
  Append on) and reports the match count on the command line. Cancel,
  Esc, and outside-click all dismiss without applying. When opened with
  a selection present, the panel pre-fills both filters from the first
  selected entity so QSelect doubles as an editable Select Similar.

Scoped to entities owned by the active layout's block-record so a paper
layout's QSELECT never reaches into model-space entities (and vice
versa). Entity-type names live in a single `entity_type_name` helper in
`entities::traits` to keep the UI strings and the filter keys aligned.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-29 20:30:56 +03:00
Hakan SevenandClaude Opus 4.7 cf925be861 fix(ctx-menu): anchor right-click menu under cursor, share overlay infra
The right-click context menu was being placed via window-relative
coordinates while the cursor position it anchored to was canvas-relative,
so the menu drifted away from the cursor by the size of the surrounding
ribbon / sidebar / status bar.

Move the menu inside `viewport_stack` so its anchor resolves in the same
coordinate space the cursor was captured in. Introduce
`position_canvas_overlay(anchor, panel)` — wraps `panel` in a column+row
of fixed-width spacers and `iced::widget::opaque(panel)` so events on the
menu itself don't fall through to the viewport mouse area underneath.
The multi-functional grip popup is rewritten to use the same helper.

Outside-click dismiss now mirrors the grip popup: `ViewportLeftPress`
checks `selection.context_menu.take()` at the top and returns early
when it was set, so a press that reaches the viewport mouse area
(i.e. outside the opaque panel) closes the menu without running the
rest of the left-click handler. The redundant `context_menu = None`
later in the handler and the now-unused `Message::ViewportContextMenuClose`
are removed.

Two related issues fixed in the same pass:

* The right-drag threshold for orbit was 3 px squared, small enough that
  normal hand jitter between a right-button press and release was
  promoting a click to a drag and suppressing the menu on release.
  Bumped to 8 px squared.
* `Message::ViewportExit` was clearing `context_menu`. With the menu now
  living above the viewport mouse area, the opaque panel capturing the
  cursor fires the underlying area's exit handler — which closed the
  menu the instant it opened. Stop clearing the menu there; outside-click
  dismiss is enough.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-29 15:58:23 +03:00
Hakan SevenandClaude Opus 4.7 0954d6d123 feat(update-check): suppress popup for releases under 30 min old
GitHub Actions takes ~15 min to build and attach the platform binaries
after a tag is pushed; the latest-release API returns the tag the moment
it's published, so users were getting a popup pointing at a release page
whose asset list was still empty. Parse `published_at` (minimal
YYYY-MM-DDTHH:MM:SSZ → UNIX seconds via days-from-civil, no new deps)
and skip the notification while the release is younger than 30 minutes.
A missing or unparseable timestamp falls through to the old behaviour so
a malformed payload can't permanently silence notifications.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-29 15:29:00 +03:00
Hakan SevenandClaude Opus 4.7 4b4fe5ca6d feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.

Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.

Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-29 15:14:43 +03:00
Hakan Seven 559cd76369 feat(grip): value-prompt actions Lengthen/Radius/Arc Length
Lengthen / Radius / Arc Length need a number after the pick. Stash
`grip_pending` on the picked action, push prompt to command line,
route next `CommandSubmit` parse into `apply_grip_menu_value`.
Escape cancels.

- `Grippable::grip_menu_value_prompt` returns label when action
  wants value; `apply_grip_menu_value` runs the edit. Dispatched
  via `EntityTypeOps`.
- Line: Lengthen extends either endpoint along line direction.
- Arc: Radius sets radius; Arc Length redrives end angle from
  `value / radius`; Lengthen extends start / end angle the same
  way.
2026-05-29 14:58:35 +03:00
Hakan Seven 86ca1b7033 fix(grip): popup sizes to widest label + full-row highlight
The previous single-widget refactor left the column at
`Length::Shrink` and the buttons at `Length::Fill`, which iced
resolves to a zero-width column — the popup degenerated into a
1-pixel vertical line. Set the column to a fixed pixel width
derived from the longest item's label (and restore `width(Fill)`
on the buttons) so the selection highlight covers the full row
instead of just the text glyphs.

Also silences a `dead_code` warning on `GripMenuAction` variants
that the Phase 2 / 3 wiring will consume next.
2026-05-29 10:00:30 +03:00
Hakan Seven 391aa3a76f feat(grip): keyboard nav + click-outside dismiss + single-widget popup
- Popup items now render inside one bordered container with
  borderless buttons so the menu reads as a single widget instead
  of stacked tiles.
- Arrow Up / Down (and Tab) walk `grip_popup.selected` while the
  popup is open; Enter commits the highlighted item by re-emitting
  `GripMenuPick`; Escape dismisses without acting; the underlying
  handlers (`CommandHistoryPrev/Next`, `CommandFinalize`,
  `CommandEscape`, `DynTabNext`) keep their normal behaviour when
  no popup is open.
- A press inside the viewport that reaches the mouse_area (i.e. not
  on a popup item) closes the popup and consumes the click — the
  user's next press then starts a normal selection / drag.
2026-05-29 09:51:12 +03:00
Hakan Seven a4c65649e1 feat(grip): entity-specific popup menus for Line/Arc/Dimension
- Line: endpoint grips expose Stretch + Lengthen; midpoint exposes
  Stretch only.
- Arc: endpoint exposes Stretch + Lengthen; midpoint exposes
  Stretch + Radius + Arc Length; centre exposes Stretch.
- Dimension text grip exposes the full text-position vocabulary
  (Move with Dim Line / with Leader / Independent, Reset Text,
  Rotate Text, Above Dim Line, Center). Dim-line-position grip
  exposes Stretch + Reverse Arrows. Extension origins keep the
  default Stretch.
- Each affected entity drops `impl_entity_basics!` and writes the
  three trait impls (Grippable, PropertyEditable, Transformable)
  manually so `grip_menu` / `apply_grip_menu` can override.
- `apply_grip_menu` handles Reset Text (clear text override) and
  Center (snap text to midpoint of extension origins) inline;
  remaining actions stub to no-op pending the follow-up
  prompt / drag plumbing.
2026-05-29 09:46:05 +03:00
Hakan Seven 595ee3e245 fix(grip): open hover popup without requiring mouse motion
Dwell timing only advanced when `ViewportMove` fired; a perfectly
still cursor never triggered the popup. Subscribe to animation
frames while `grip_hover` is set and dispatch `GripDwellTick` from
them so `update_grip_hover` re-checks the elapsed dwell each frame.
Subscription auto-stops as soon as the hover clears or the popup
opens.
2026-05-29 09:38:37 +03:00
Hakan Seven 6c1462932d feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.

- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
  stretch, add/remove vertex, convert to arc/line, reverse arrows,
  text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
  trait-default `Stretch`-only impls so every existing entity gets
  the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
  list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
  `grip_popup` (open menu). `update_grip_hover` runs from
  `ViewportMove`: hit-tests grips, refreshes the dwell timer, and
  opens the popup after 600 ms. Cursor drift past 80 px or a grip
  click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
  buttons at the grip's screen anchor; each button publishes
  `Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
  macro) so its vertex grip exposes Add/Remove Vertex and its
  segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
  `apply_grip_menu` performs the edits: vertex insertion at the
  chord midpoint inheriting the previous bulge, vertex removal,
  and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
Hakan Seven 5755c2e130 refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.

- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
  Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
  vector consumed by `Rectangle` to orient the box along its
  segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
  square marker, flagged as translate), `rectangle_grip(id, world,
  dir)` for oriented mid-segment handles, and a kept-for-Phase-2
  `triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
  through to the canvas painter; the Rectangle path rotates the
  box around the grip centre using the world-XY direction (flipping
  the sin for screen-Y).

Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.

`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00