Commit Graph
942 Commits
Author SHA1 Message Date
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
Hakan SevenandClaude Opus 4.8 1cd7aaed4e refactor: move parallel tessellation fns into scene/convert/
scene/tess.rs held the free parallel-tessellation functions
(tessellate_entity, lod_stub_wire, entity_aabb, …) that the giant-file
split had left at the scene root. They belong with the rest of the
tessellation code, so move the file to scene/convert/tess.rs alongside
tessellate.rs / tess_util.rs and re-point the re-export.

Leaves the scene/ root as a consistent set of `impl Scene` method
groups; the free-function outlier is gone.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 23:39:43 +03:00
Hakan SevenandClaude Opus 4.8 9cd0479047 refactor: group ui/ style+popup dirs, rename cmd_result driver
- Group the flat src/ui/ dir: move the 9 style editors into ui/style/
  (dimstyle, mleaderstyle, mlstyle, plotstyle, textstyle, tablestyle,
  point_style, style_list, style_manager) and the 6 popups into
  ui/popup/ (cycle, isolate, scale, selection_filter, snap, units).
  Call sites repointed to crate::ui::style::/crate::ui::popup::; no
  compat re-exports. ui/ root drops from 36 to 21 files.
- Rename app/cmd_result.rs -> app/command_driver.rs: the file is the
  StepInput->feed_command driver, not the CmdResult type (which lives
  in command.rs). Name now matches contents.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 23:34:10 +03:00
Hakan SevenandClaude Opus 4.8 7cb666801f refactor: replace build.rs module codegen with a static registry
The core ribbon registry no longer needs build-time generation. Replace
the generated all_modules() (included from OUT_DIR) with a hand-written
static list in src/modules/registry.rs, listing the seven built-in tabs
in display order.

Trim build.rs down to its only remaining job: embedding the Windows
executable icon via winresource (issue #107). The external plugin system
is unaffected — plugin::registry still appends dynamically-loaded add-on
tabs on top of the core list.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 23:19:20 +03:00
Hakan SevenandClaude Opus 4.8 415dbe08d4 refactor: move resource/IO root modules under io/
Relocate three root-level modules into io/, where they belong by concern:

- update_check.rs -> io/update_check.rs (its own doc-comment notes it
  mirrors how crate::io wraps blocking calls in async).
- patterns.rs, linetypes.rs -> io/ (standard resource catalogs parsed
  from asset files and populated into the document — IO-shaped work).

Fix the moved files' include_str! paths (one level deeper), add the
mod declarations to io/mod.rs, drop them from lib.rs/main.rs, and
re-point all crate:: references to crate::io::. No behavior change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 23:06:40 +03:00
Hakan SevenandClaude Opus 4.8 bbdbd0850b chore: remove dead extend stub, flatten single-file module dirs
- Delete src/modules/draw/modify/extend.rs (a 1-line placeholder with no
  tool()/logic; `extend::` was referenced nowhere) and its `mod extend;`.
- Flatten single-file module dirs to the modern flat style:
  src/command/mod.rs -> src/command.rs, src/snap/mod.rs -> src/snap.rs.
  Parent `mod command;`/`mod snap;` are unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 23:00:26 +03:00
Hakan SevenandClaude Opus 4.8 bddc050b3e refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.

- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
  dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
  with thin delegating arms; the 66 fattest arm bodies move to topic
  methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
  mspace,project,selection,modify,group_layer,preview}. The impl Scene
  body is split across files via inherent-impl-per-file; struct, ctor,
  fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
  The 233-arm dispatch match becomes source-ordered per-family
  dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.

Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 22:51:29 +03:00
Hakan SevenandClaude Opus 4.8 2baabdf4a0 feat(viewport): reorder model controls, add close-vport button
Reorder the model-tile control chip to grid, grid snap, render mode,
vertical split, horizontal split. Add a close button (shown only with
more than one model tile) that merges the active pane into its
longest-contact neighbour.

Divider drags now clamp each side to the minimum tile size, so dragging
a boundary to the screen edge no longer collapses a pane — closing is
done with the button instead.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 21:15:00 +03:00
Hakan Seven 9c03ae2b4d Merge pull request #196 from KevinGriffin-new/feat/background-presets
feat(background): named presets for BACKGROUND (WHITE/BLACK/GRAY/…) (#188)
2026-06-26 20:44:20 +03:00
Hakan Seven f45d85c7ec Merge pull request #195 from KevinGriffin-new/feat/persist-background
feat(background): persist viewport background across sessions (#188)
2026-06-26 20:42:38 +03:00
Kevin GriffinandClaude Opus 4.8 e22afea0a0 feat(background): named presets for BACKGROUND (WHITE/BLACK/GRAY/…) (#188)
Builds on the background-persistence change: let BACKGROUND take a single
named-colour token in addition to `<r> <g> <b>`, so a common dark/light
background is one word instead of a remembered triplet:

    BACKGROUND WHITE | BLACK | GRAY|GREY | DARKGRAY|DARKGREY | LTGRAY

Colour parsing moves into `parse_background_color`; the persisted-default
and recolor paths are unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 10:27:54 -07:00
Kevin GriffinandClaude Opus 4.8 66fc30429c feat(background): persist viewport background across sessions (#188)
The BACKGROUND command set a per-tab colour that was lost on restart and
didn't apply to new drawings, so there was no way to make a non-black
background stick. Persist the chosen model/paper background in UserSettings
(settings.txt) and re-apply it on launch and to every drawing tab created
afterward.

- settings.rs: persist `bg_color` / `paper_bg_color` as 0–255 RGB triplets.
- App gains `default_bg_color` / `default_paper_bg_color`; current_settings /
  apply_settings round-trip them, and apply_settings pushes the restored
  colour onto existing tabs. New/opened/replaced tabs call `apply_bg_default`.
- BACKGROUND updates the persisted default, saved centrally via
  persist_settings_if_changed; RESET clears it.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 10:26:53 -07:00
Kevin GriffinandClaude Opus 4.8 21192011be fix(dist): measure in f64 so snapped endpoints stay precise (#193)
DistCommand stored the first point as Vec3 and ran on_point through
`let pt = pt.as_vec3();`, downcasting the f64 picked point to f32 before
computing distance/deltas. At survey-scale coordinates (eastings ~5e5)
f32's 24-bit mantissa gives ~0.03–0.06 unit resolution, so a snapped
endpoint measured slightly off — matching the report in #193.

Store `first` as DVec3 and keep the math in f64; cast to f32 only for
the visual preview wire vertices.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 09:52:36 -07:00
Hakan SevenandClaude Opus 4.8 169a8cde3f chore: clear dead-code and unused-import warnings
Remove the unused `crate::par::prelude` glob import in wire_gpu.rs and
the never-called `viewport_at_paper_point` method; fold its doc into the
screen-space twin that is actually used so no intra-doc link dangles.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 18:04:52 +03:00
Hakan SevenandClaude Opus 4.8 14ebdd36d1 perf(render): hold model wire set static instead of re-tessellating on camera moves
The model-space wire geometry is camera-independent, yet every pan/zoom
re-tessellated the whole model and re-uploaded it to the GPU. On a
block/text-heavy drawing that is millions of segments, costing a ~100ms
re-tessellation plus a ~150ms GPU re-upload per camera change (~258ms
freeze) while the GPU itself draws the full set comfortably in real time.

Tessellate the full, un-culled model once per geometry epoch, hold it
resident, and return it for every camera and tile. A pan/zoom now only
changes the view uniform; no frustum cull, no zoom LOD, no re-tess or
re-upload on camera moves. Pan/zoom frame time drops to ~7ms (~140fps)
with no stutter.

Removes the now-dead per-tile cull cache, pan-invariant hash, AABB
containment helper, and the reuse-during-motion / settle-regen machinery
that an earlier iteration introduced. Also exempts Face3D from the
sub-pixel stub LOD: a flat face is trivially cheap to tessellate, so it
is drawn full at any zoom and no longer pops a cube stub at the threshold.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 16:06:11 +03:00
Hakan SevenandClaude Opus 4.8 4e8521010b fix(wipeout): keep fill boundary precise at UTM coordinates
`wipeout_boundary_2d` built the fill boundary from the wipeout's
insertion point cast straight to f32, stored in a HatchModel with
world_origin = [0, 0]. At UTM-scale coordinates (~1e6) f32 resolves to
~0.06, so the boundary collapsed and the relative-to-eye fill shader
cancelled `boundary - eye` to noise — the mask rendered as squares.

Return the boundary as small f32 offsets relative to a world_origin
(the insertion point, kept in f64) and set HatchModel.world_origin to
it at both the model-space and paper/viewport fill sites. Same fix as
the wide-polyline and on-create hatch fills.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 11:20:50 +03:00
Hakan SevenandClaude Opus 4.8 2672807c13 fix(polyline): keep wide-polyline fills precise at UTM coordinates
The solid fill band of a wide LwPolyline / Polyline2D was built from
absolute WCS vertex coordinates cast straight to f32, then handed to a
HatchModel with world_origin = [0, 0]. At UTM-scale coordinates (~1e6)
f32 resolves to ~0.06, so the half-width offset collapsed and the
relative-to-eye fill shader cancelled `boundary - eye` to noise — the
band rendered as a string of squares. (Exploding the polyline drops the
width, hence no band and a clean line, which is why that "fixed" it.)

Build the band relative to a world_origin (the first vertex) in f64 and
store the small f32 offsets, mirroring how baked hatch fills keep
precision. Same class of bug as the on-create hatch-fill fix.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 10:55:28 +03:00
Hakan SevenandClaude Opus 4.8 0317dad14e perf(render): hoist per-wire constants to a storage buffer (native)
Every wire segment is one instance, and each carried the wire's color,
line-weight, dash pattern and draw-depth (~44 B) inline — re-fetched per
segment even though they're constant along the wire. On native, move
those into a per-wire `WireConst` storage buffer (group 1) indexed by a
`wire_id` on the instance. The instance drops from 104 B to 60 B (~42 %)
across every entity's wires, and the shared constants are fetched once
per wire (cached) instead of once per segment — less vertex-attribute
bandwidth and less GPU upload on rebuilds.

WebGL2 has no vertex-stage storage buffers, so the wasm build keeps the
original self-contained fat instance and wire.wgsl; native uses the new
slim instance, wire_indexed.wgsl, and a group-1 bind group built per
batch in WireGpu::from_run / from_batch. Visual output is unchanged
(colors are now full f32 rather than 8-bit, if anything more precise).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 10:39:23 +03:00
Hakan SevenandClaude Opus 4.8 0db513e07a fix(hatch): one grip per corner, and live preview while editing
Two follow-ups to dropping the hatch boundary outline from the wire set:

- `closed_outlines` copied segment-list wire points (LwPolyline emits
  each segment as a NaN-separated pair, so every shared corner repeats)
  straight into the hatch boundary, giving two grips at each corner.
  Collapse the points back into a clean ring — skip NaN separators and
  any vertex coincident with the previous one, and drop a trailing
  vertex equal to the first. One grip per corner; point-in-polygon and
  the BOUNDARY / GRADIENT commands get the clean ring too.

- Edit previews (move / copy / array / grip-drag) tessellate the edited
  entity into preview wires, but a hatch now produces none, so a dragged
  hatch didn't update until release. Build a live outline from the
  current HatchModel for the preview only (`apply_grip` keeps it in
  step), so the shape tracks the cursor again. The normal render still
  shows just the fill.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 09:47:29 +03:00
Hakan SevenandClaude Opus 4.8 1831a63236 perf(hatch): drop the boundary outline wire from the render set (#131)
A HATCH was emitting its boundary as a dense outline polyline in the
wire set on top of its fill. On hatch-heavy drawings that outline was
the dominant wire-instance cost — 14.9M of 19.1M segments on the #131
sample — and on integrated GPUs (shared system RAM) the per-segment
instances pushed memory past the OOM threshold on open.

Skip the outline in `tessellate`: hatches render as a fill, are
highlighted by their fill tint when selected (#71), and are still
picked by their fill area via the existing `click_hit_hatch` path, so
nothing else depends on it. Wire segments drop ~78% on the sample.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 09:31:59 +03:00
Hakan SevenandClaude Opus 4.8 21d0db0c25 fix(hatch): show fill on creation, not only after a boundary edit
`add_hatch` let `add_entity` build the render model from the new DXF
entity (via `hatch_model_from_dxf`, AABB-centred `world_origin`) and
insert it — then overwrote it with the command-built model, whose
`world_origin` is [0, 0]. After the world_offset removal the fill is
drawn relative-to-eye from `world_origin`, so a [0, 0] origin with a
full-WCS boundary cancels to garbage in f32 and the fill renders
off-screen — invisible until a later edit rebuilt it from the DXF.

Drop the overwrite so creation keeps the correct seed; it now matches
what an edit or reload produces. Fixes the regression for hatches drawn
away from the origin.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 09:31:59 +03:00
Hakan SevenandClaude Opus 4.8 34e750407b fix(render): draw command-preview wires always on top
Preview / interim / grip-drag overlay wires were drawn in the main wire
pass with the depth-tested wire_pipeline (depth_compare=LessEqual), so a
live command preview could be hidden behind occluding geometry — a 3D
solid, or 2D geometry drawn in front of it.

Draw them with the existing wire_xray_pipeline instead (depth_compare=
Always, no depth write), the same always-on-top path selection highlights
already use. Previews are now never occluded, for every command.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 07:29:28 +03:00
Hakan SevenandClaude Opus 4.8 c7ecabffb7 fix(trim): trim circles into arcs, fix arc/circle preview plane (#184)
TRIM ignored circles: the target dispatch handled Line/Arc/Ellipse/
Spline/Ray/XLine/LwPolyline but had no Circle arm, so clicking a circle
fell through to a no-op. Add `trim_circle()` — a full circle needs ≥2
boundary crossings; the arc segment under the click is removed and the
circle becomes a single Arc (center/radius/thickness/normal preserved).
Wire it into both the pick (apply) and hover-preview paths.

Also fix `arc_pts`, which emitted points as [x, y, cy+r·sin] — the XZ
plane — so arc and circle previews drew in the wrong plane (obvious on a
full circle). Match the XY convention used by line_pts and fillet's
arc_pts: [cx+r·cos, cy+r·sin, z]; callers now pass center.z.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 07:25:02 +03:00
Kevin GriffinandClaude Opus 4.8 d477c39cbe fix(dimension): bake a *D block on save so dimensions survive in other CAD
A DWG DIMENSION references an anonymous *D<n> block that holds its drawn
geometry; AutoCAD-family readers render that block rather than re-deriving it
from the definition points. OCS re-tessellates dimensions for display and never
materialises the block, so on save a created dimension goes out referencing a
block that doesn't exist and the writer emits a null block handle. Strict
readers (DWG TrueView, QCAD) then drop the dimension or demand a recovery, and
lenient ones (BricsCAD) regenerate it at a different position.

Add bake_dimension_blocks(), called on the cloned document in
io::save_as_version / save_to_bytes, which builds a *D<n> anonymous block from
each block-less dimension's exploded geometry (extension lines + dimension line
+ measurement text) and points its block_name at it. Dimensions that already
own a block (imported, or copied via the *D-cloning copy path) are left
untouched.

Verified end-to-end: before, AutoCAD/Civil 3D 2026 reads 0 dimensions from an
OCS-saved file; after baking it reads all of them.

Fixes #178, #181, #182.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 17:01:10 -07:00
Hakan SevenandClaude Opus 4.8 dee5a05652 fix(text): address PR #180 review
Follow-up cleanups on the merged TTF-fill work:

- Drop the unconditional resolve_text_style log line — it fired for every
  text style and spammed ~7k stderr lines opening a single real drawing.
- Drop the per-glyph tessellation-error eprintln; a malformed font would
  otherwise log once per character. The glyph still falls back silently.
- Tessellate glyph fills with the nonzero winding rule (lyon defaults to
  even-odd, which mis-fills TrueType outlines whose contours overlap).
- Make the sysfont prefix fallback deterministic (sorted) and require ≥3
  chars so a 1–2 letter request can't grab an arbitrary family.
- Remove the unrelated windows-gnu --exclude-all-symbols linker flag that
  slipped into .cargo/config.toml.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 00:27:40 +03:00
Hakan Seven e9cb9af924 Merge PR #180: TTF solid glyph fills, font fallback, Z-order fix 2026-06-26 00:20:59 +03:00
Hakan SevenandClaude Opus 4.8 012ec3febc fix(viewcube): land face clicks upright, never upside-down
A drawing opened with a twisted view made a TOP-face click keep the
twist, so north ended up at the bottom. Add Camera::snap_to_face, which
snaps to a canonical orientation — north (UCS +Y) up for top/bottom,
world (UCS +Z) up for the side elevations — ignoring the current
up-sense. Face clicks use it; edges/corners still preserve their up so
they spin smoothly around the clicked feature.

The Home button routes through the same path, giving a reliable reset
for a twisted view.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 00:11:40 +03:00
Hakan SevenandClaude Opus 4.8 f00c48bebb feat(viewcube): add compass ring, cardinals, and nav controls
Build the ViewCube out into a full navigation widget:

- Compass ring painted in the cube's ground plane (z = base) so it
  reads as a ground compass and foreshortens with the view.
- N/E/S/W cardinals painted flat onto the ring band; clicking one snaps
  to that side elevation. Added S/W glyphs to the bitmap atlas.
- Home button (top-down view), roll arrows (90° roll about the view
  axis), and four nudge triangles (90° tip/spin) as SVG iced controls
  laid out around the cube. New home.svg.
- WCS / named-UCS selector under the cube, switching the active UCS.

The cube hit area now shares one stack with the control buttons, so a
click on a button is caught by the button while a click on the cube
falls through. Face clicks snap to the highlighted region rather than a
cursor position the viewport's move handler can overwrite.

Camera gains home_view / roll_by / nudge_90; floating viewports drive
the same ops through mutate_active_viewport_camera, which re-encodes the
result to the stored view direction + twist.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 23:46:43 +03:00
Hakan SevenandClaude Opus 4.8 6c4b981424 feat(viewcube): paint face names onto the cube surface
Lay each label's glyph quads out in the face's own local plane and
project the corners individually, instead of drawing screen-aligned
billboard text at the projected face centre. The letters now skew with
perspective and sit on the surface as the cube rotates.

Per-face (u, v) basis vectors are chosen so u x v equals the outward
normal, which keeps the text from ever reading mirrored.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 22:40:17 +03:00
Hakan SevenandClaude Opus 4.8 a72b143628 fix(copy): duplicate a dimension's baked block so COPY copies it (#161)
A dimension's drawn geometry lives in a baked anonymous *D block. COPY
cloned the Dimension entity (translating its definition points) but left
its block_name pointing at the source block, whose sub-entities stay at
the original location — so the copy rendered on top of the original and
appeared not to copy at all.

Add `clone_transformed_block`: when copying a dimension that has a baked
block, duplicate that block under a fresh *D name with every sub-entity
transformed by the same offset, and repoint the copy at it. The copy now
lands at the drop point with its baked geometry and text preserved (no
synthesis, so diameter/radius values stay correct). Covered by a portable
regression test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 22:30:45 +03:00
Karim Jerbi 627761b629 fix(text): render TTF glyphs as solid fills, fix font fallback, correct Z-order
Text elements previously rendered as hollow wireframe outlines and sometimes
fell back to the wrong system font due to case-sensitive exact matching. Also,
the viewport background grid drew on top of solid entities.

Case-insensitive and partial-name fallback matching (e.g., mapping "arialn"
to "Arial Narrow") is now added to `sysfont::canonical_family_name`, ensuring
text resolves to the correct installed system font.

TrueType font contours are now triangulated into solid fills (`fill_tris`)
using `lyon_tessellation` in `ttf_glyph.rs`. To prevent the GPU from misclassifying
this 2D text as a 3D mesh (which broke flat shading), text tessellation splits
the output into separate outline and fill `WireModel`s.

The background grid is extracted from `SelectionCanvas` into `GridCanvas` and
moved beneath the 3D viewport to ensure proper layering. Finally, `DepthBiasState`
is enabled on all face/fill pipelines to apply a polygon offset, perfectly
resolving Z-fighting between coplanar wireframe lines and solid faces.
2026-06-25 18:17:01 +01:00