Commit graph

1,085 commits

Author SHA1 Message Date
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
Hakan Seven
c599c2ba09 fix(pdf): export linetypes and true lineweights (#155)
The internal PDF export set each wire's colour and thickness but never a
dash pattern, so every dashed / centre / dash-dot line printed solid. It
also derived thickness from the on-screen pixel weight, which carries a 2×
screen-legibility boost — so print weights were inconsistent and barely
differentiated.

Emit a PDF dash pattern per wire from its linetype run lengths (mm→pt,
real length = index of the last non-zero element + 1, 1 pt floor so a dot
still marks), resetting to solid where there is no pattern. Convert
lineweight with a factor that undoes both the 96-dpi scaling and the
display boost, so print thickness is the true physical mm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:13:19 +03:00
Hakan Seven
918f6c540f fix(layout): centre the camera on the sheet when first opening a layout
A new drawing's layouts carry paper limits but no sheet viewport; it is
created lazily on the first switch into the layout. That viewport kept
Viewport::new's default view (target 0,0 / height 210), so camera_from_view
aimed at the paper's bottom-left corner, and with no geometry yet the
fit_all fallback was a no-op — the layout opened off-centre.

Give the freshly created sheet viewport a view that frames the page:
view_target at the paper centre and view_height a little taller than the
sheet. The first switch now opens centred on the page, and the view persists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:55:20 +03:00
Hakan Seven
244f951c00 fix(viewport): show new paper-space viewport border without a zoom
Creating a viewport in a layout commits through document.add_entity_to_layout
+ Scene::auto_fit_viewport, both of which skip the geometry-cache bump that
Scene::add_entity performs for every other entity. So the new viewport's
border wasn't tessellated until the next zoom/pan happened to invalidate the
cache. Bump the wire tessellation (no-blocks: a viewport touches no block
definitions) right after the viewport is added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:55:20 +03:00
Hakan Seven
8f8fa40279 fix(scale): restore default scale list when drawing has none (#154)
Driving the annotation / viewport scale picker purely from the drawing's
ACAD_SCALELIST regressed every drawing that carries no Scale objects
(most fresh drawings and minimal DXF/DWG exports): scale_list() returned
empty, so the status-bar scale popup opened with nothing to pick and the
button looked dead.

Fall back to the standard ratio set (1:500 … 1:1 … 10:1) when the drawing
defines no scales of its own; file-defined scales still take precedence
whenever present. Also re-enables the viewport-scale picker in the
properties panel for those drawings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:44:02 +03:00
Hakan Seven
73497850e2 fix(ribbon): float annotate style dropdowns so they aren't clipped (#153)
The text / dimension / multileader / table style combos rendered their
open list inline inside the ribbon row, so the popup was clipped to the
fixed ribbon-row height and got cut off even with only two entries. The
Draw-tab dropdowns and the layer combo avoid this by rendering as
floating overlays.

Render the style list as an overlay too: the combo now draws only its
button inline, and a new `style_combo_overlay` builds the list as a
floating panel that grows to fit its entries, positioned with the
existing `compute_dropdown_left` (which already accounts for
StyleComboGroup widths) and mirroring `layer_combo_overlay`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:35:49 +03:00
Hakan Seven
7086a79c79 fix(dimension): render diameter text and shrink point markers (#139)
Two issues in a file whose dimensions carry baked *D### blocks.

Missing diameter text: the baked MText values are "\U+220520" /
"\U+220510" — the \U+2205 (⌀) escape immediately followed by the literal
digits "20"/"10". The MText parser read up to six hex digits for a \U+
escape, so it greedily consumed "220520" = 0x220520, which is past the
maximum code point, char::from_u32 returned None, and the whole text
dropped. A \U+ escape is exactly four hex digits; read four so the symbol
decodes and the trailing digits stay literal. (Angular "90\U+00B0" only
worked because its escape sits at the end of the string.)

Node artifacts: PDMODE=0 points were drawn as a fixed 0.1-world-unit "+",
which in a small drawing turned the 31 dimension def-points on the
Defpoints layer into visible crosses. Size the dot from world-per-pixel
(~1 px, zoom-adaptive via the tessellation cache key) to match the near-
invisible dot other CAD apps draw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:22:35 +03:00
Hakan Seven
177ae20be4 fix(dimension): keep DIMCONTINUE chain collinear with the base (#151)
The continued dimension line was positioned with an offset measured from
the base dimension's *first* extension origin, then re-applied to each new
chain point (the previous dim's second origin). When the two extension
origins sat at different perpendicular positions — i.e. the extension
lines had different lengths — that offset placed the new dim line at
`D + (p2 - p1)·perp` instead of the base line's `D`, so the chain drifted
off the base line. Equal-length extension lines have `(p2 - p1)·perp = 0`,
which is why it only misbehaved sometimes.

Store the base dim line's absolute perpendicular coordinate and project
each extension origin onto it, so the whole chain stays collinear
regardless of extension-line lengths. The preview uses the same
projection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:04:20 +03:00
Hakan Seven
9d5bed2727 fix(dimension): correct aligned/ordinate placement and preview plane (#150)
DIMALIGNED's drag preview never matched where the dimension landed. The
preview offset the dimension line in the XZ plane while the committed
entity's renderer offsets in the XY plane, and the commit path passed the
straight-line p2→cursor *distance* to `set_offset`, which re-applies it
along the perpendicular — wrong distance, always on the +perp side. Both
now store the cursor as the definition point and project it onto the
XY-plane perpendicular, matching the renderer and DIMLINEAR.

The same XZ-vs-XY plane confusion affected other annotate commands:

- DIMORDINATE worked entirely in the XZ plane (dropping the Y coordinate)
  and left `definition_point` at the origin, so the leader kinked through
  (0,0,0). Reworked onto the XY plane with a proper orthogonal elbow and a
  live preview that matches the placed entity.
- DIMTEDIT / DIMJOGLINE cursor markers were built in the XZ plane, so in
  the top-down view they collapsed to a flat line instead of a box /
  zigzag. Rebuilt in the XY plane.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 17:55:39 +03:00
Hakan Seven
fcb97b12da fix(linetype): render dotted and dash-dot patterns (#149)
A dot in an LTYPE pattern is a zero-length element. The pattern resolver
encoded it as a tiny `0.01 * scale` world-length dash, which had two bad
effects: it was sub-pixel at normal zoom so the dash LOD saw the pattern's
smallest feature drop below one pixel and collapsed the whole line to
solid, and at larger LTSCALE it left only invisible sub-pixel dots between
big gaps. Either way dotted and dash-dot linetypes were never visible;
only plain dashed (no zero elements) worked.

Keep dots as literal 0.0 so they no longer drag `min_elem` down, and
render each as a fixed ~1.5 px mark in the wire shader (pixel-snapped via
world_per_pixel, mirroring the hatch shader's dot handling). The shader
tells a real dot from trailing array padding by element count — the index
of the last non-zero element plus one — so no producer needs a sentinel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 17:37:24 +03:00
Hakan Seven
ce2365e8f9 fix(osnap): snap to arc/circle centre when hovering the curve (#152)
The Center object snap was a pre-baked point gated purely on the
cursor's screen distance to the centre point. A circle's centre sits
inside it and an arc's centre is usually off in empty space well away
from the curve, so hovering the curve — the natural gesture — never
landed within the snap radius and no centre marker appeared.

Add a curve-proximity pass: when Center is enabled and the cursor is
near a circle/arc/ellipse curve, offer that curve's centre, ranked by
the cursor's distance to the curve. It runs after the `try_pt` closure's
borrow ends so it can update the candidate state directly, mirroring the
Tangent pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 17:01:23 +03:00
Hakan Seven
6fb5c89a7b fix(hatch): preserve all island loops in complex hatches (#148)
The hatch boundary was flat-truncated to 1024 vertices before upload.
The active renderer is the batched path, which stores boundaries in a
GPU storage buffer with no length limit, so the cap only ever served
the per-hatch uniform path (now wipeout-only, and self-clamping).

On a real drawing a 36-loop, depth-3 nested hatch (1482 verts) was cut
mid-boundary: trailing island loops were dropped and the final partial
loop was left open, flipping the even-odd parity so the fill bled across
the rest of the shape and islands rendered incorrectly.

Drop the flat truncate. Keep every loop; only guard pathological vertex
counts (16384), and when trimming, cut at a NaN sentinel (whole-loop
boundary) so no sub-loop is ever left open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:11:15 +03:00
Hakan Seven
e6e8167b36 feat(render): boost model-space lineweight display for legibility (#147)
Lineweights were rendered at their true physical size (mm × 96/25.4 px),
which is ~2 px for a 0.5 mm line — too timid to tell weights apart, and
every weight ≤ 0.25 mm floored to 1 px (no visible difference). Other DWG
editors display model-space lineweights larger than true size for screen
legibility; apply the same 2× boost so weights are pronounced and
distinguishable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:39:12 +03:00
Hakan Seven
57153254bc build(deps): bump acadrust for the SORTENTSTABLE draw-order fix (#146)
Pull in acadrust 16332e5, which reads/writes the DWG SORTENTSTABLE in the
correct stream layout (sort handles inline in the data section; owner block
+ entity handles in the handle stream). Draw order set in other CAD apps —
e.g. a line sent behind a hatch — is now honoured instead of falling back to
handle order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:04:53 +03:00
Hakan Seven
44fa9df278 build(web): restore the wasm build — gate native-only deps off wasm
The web build had bit-rotted: native-only features added since it last
compiled pulled crates that don't build for wasm32-unknown-unknown.

- printpdf (PDF export / printing) → native-only target; the web build gets
  stub export_pdf / print_wires. It pulls a wasm-incompatible memchr 1.0.2
  via lopdf → nom_locate, and the web has no filesystem.
- getrandom 0.3 (via ahash via acadrust) → enable its wasm_js feature and add
  the matching --cfg getrandom_backend rustflag in .cargo/config.toml.
- ocs_plugin_api "host" feature (out-of-process plugin runtime: interprocess,
  libloading, memmap2, rkyv) → native-only target; the web gets the
  dependency-free manifest/ribbon contract. The host code was already
  wasm-gated; only the Cargo wiring pulled it in unconditionally.

`trunk build --release` succeeds again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:11:18 +03:00
Hakan Seven
8b1e37e0da fix(web): render OSNAP tooltip caret as SVG, not a tofu glyph (#138)
The OSNAP settings tooltip read "Click ▾"; the ▾ (U+25BE) is absent from the
web font and showed as a tofu box. Tooltips are plain text and can't embed
SVG, so add a rich-body tooltip variant (tip_node) and use the existing SVG
caret (icons::arrow_down) — consistent with the SVG-icon approach the rest of
the chrome already uses for the web.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:11:06 +03:00
Hakan Seven
df0e283eba feat(commands): interactive value prompts for PDMODE/PDSIZE/LTSCALE/CELTSCALE (F4)
Bare PDMODE / PDSIZE / LTSCALE / CELTSCALE now enter a generic
ValuePromptCommand front-end that prompts for the value and delegates to the
existing inline `<name> <value>` handler via CmdResult::Dispatch, so they
prompt step-by-step like UCS. A bare Enter reports the current value;
`PDMODE 3` still works as one typed / headless line.

Only pure single-value setting commands are converted. Dialog-opening
commands (LAYER, PURGE, DIMSTYLE, STYLE …) intentionally keep their
bare-name behaviour (open the panel), and subcommand-tree commands keep
their inline handlers — all already typable with arguments since F1.

Adds headless test value_prompt_commands_inline_args. Fourth phase of the
unified step driver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:35:25 +03:00
Hakan Seven
52f6aed2f2 feat(cmdline): type whole command lines with spaces; unify with headless (F1, #169)
Space is now a literal character in the command line, so a whole command
line — `UCS Z 90`, `LINE 0,0 10,10`, `PDMODE 3` — is typed before Enter
instead of the first Space submitting the buffer. Enter tokenises the line
and runs it.

The command line and the headless automation feeder now share one runner,
`run_command_line` (moved to the non-wasm-gated cmd_result.rs): a single or
inline-argument command dispatches as-is; a multi-token line whose first
word starts an interactive tool feeds the rest as steps; plugin commands
get the whole line first (#162). The shared feeder is now UCS-aware —
typed coordinates are interpreted in the active UCS in both the GUI and
headless. `run_headless` is a thin wrapper over it.

A free-form text command (TEXT / MTEXT / a name with spaces) still receives
the whole line as one input, via the existing `wants_text_with_spaces`
guard. Unfocused Space still repeats the last command.

Closes the command-line half of #169 generically for every command, not
just UCS. Third phase of the unified step driver: every input source funnels
through feed_command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:16:28 +03:00
Hakan Seven
36d2f33301 feat(ucs): interactive UCS command — UCS Z 90 typable everywhere (F2, #169)
Bare `UCS` now enters an interactive front-end (UcsCommand) that gathers the
option then its value as separate steps, instead of the command line
submitting `UCS` alone on the first Space. The front-end delegates execution
back to the existing inline `UCS <option> …` handler via a new
`CmdResult::Dispatch(String)`, so the coordinate-system math and the
save/persist path are untouched.

Now `UCS Z 90` works the same from the command line (Space-separated),
headless automation, and any source that feeds the active command — closing
the input half of #169. Inline `UCS Z 90` (one-shot / paste) still hits the
inline handler directly.

Second phase of routing input sources through the unified step driver: the
interactive command is driven by feed_command like any tool. Adds headless
test ucs_interactive_inline_args.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:36:49 +03:00
Hakan Seven
5fdd212592 refactor(commands): unified StepInput → feed_command step router (F0)
Introduce the single place every input source funnels through to drive the
active command's step machine: a `StepInput` vocabulary (one variant per
`CadCommand::on_*` method) and `OpenCADStudio::feed_command`, which routes
the input to the matching `on_*` and applies the result. Sources keep their
own classification (raw input → which StepInput); only the routing is
centralized.

Migrate the headless automation feeder onto it: feed_active_cmd builds a
StepInput, the terminator sends StepInput::Enter. No behavior change —
automation_ops_round_trip still passes.

Also fixes headless inline-argument loss for plugin commands (#162):
run_headless now offers the whole line to plugin dispatch first (plugins
parse their own inline args, e.g. `HC_PIPE 2B 2C 1.25 0.013`), falling back
to the first-word + fed-tokens path for built-in interactive tools.

First phase of routing all input sources (command line, dynamic input,
plugin API, viewport clicks) through one driver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:24:17 +03:00
Hakan Seven
69ea050124 feat(pan): interactive PAN mode with left-drag (no middle button needed)
The PAN command/tool dispatched a command nobody handled, so it did
nothing. Combined with no middle mouse button on a trackpad/web client,
there was no way to pan at all (issue 164).

PAN (and the "P" alias) now enters a pan mode: a left-button drag pans the
view, reusing the existing middle-button pan path. Exit with Esc or by
starting another command. While active the crosshair is hidden and the
cursor becomes a hand (open when hovering, closed while dragging). PAN/P
are also added to command-line autocomplete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:03:54 +03:00
Hakan Seven
efade01896 fix(offset): pick offset side by containment for closed polylines
OFFSET derived the side from the first segment alone: which half-plane of
that edge's infinite line the pick fell on. For a closed loop that misreads
a pick placed beside the shape — outside the loop yet on the inner side of
the first edge's line — so a counter-clockwise rectangle (RECTANG's winding
when dragged bottom-left to top-right) offset outward by a side pick
collapsed inward instead.

Decide the side for a closed polyline with a point-in-polygon test mapped
through the loop winding (left normal points inward for a CCW loop): inside
→ inward, outside → outward, regardless of winding. Open paths keep the
first-segment heuristic. Adds a regression test covering both windings and
both directions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:49:28 +03:00
Hakan Seven
d641ecfea2 fix(viewport): frame paper viewports on the robust cluster centre
Floating viewports on a survey-coordinate drawing rendered blank: the
model camera was correct, but content viewports whose saved view was
rejected auto-fitted onto empty space.

Two causes, both in model-space extents handling:

- compute_model_space_extents tested vertex finiteness in f64 but stored
  f32. A long RAY/XLINE endpoint is a huge-but-finite f64 that overflows
  to inf in f32, poisoning the extents to (inf, inf). Also, the wire/mesh
  AABB paths checked only x while folding y in, so a vertical ray's
  infinite y leaked through. Now every component is validated after the
  f32 cast.

- camera_for_viewport framed its overlap test and auto-fit on the raw
  extents centre. When a drawing has a second, far cluster (a small-
  coordinate legend beside a UTM survey), that centre sits in the empty
  gap, so a valid saved view fails the overlap test and the auto-fit aims
  at blank space. cluster_extent_from_centers already computed a robust
  centre (median of entity centroids) but discarded it; surface it as
  Scene.local_center and frame on that instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:31:22 +03:00
Hakan Seven
dfdf8f6702 build(deps): bump acadrust to twist-aware VPort view fix
Pull in acadrust 88ea9f0, which fixes the DWG VPort zoom-extents safety
net to be twist-aware. A twisted saved view on a survey-coordinate
drawing no longer has its camera flung to empty space on save/reopen
(the view_center was double-rotated about the origin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:57:38 +03:00
Hakan Seven
b4093724f2 fix(plugin): f64 pick coords + harden out-of-process lifecycle
Follow-ups after merging the out-of-process plugin isolation work:

- Convert the out-of-process interactive adapter's `on_point` /
  `on_entity_pick` to `glam::DVec3`, matching the `CadCommand` trait,
  which moved to f64 pick coordinates. The branch predated that change
  and no longer compiled against the trait.
- Contain plugin constructor panics in `export_plugin!`: the constructor
  runs across the C ABI boundary in `ocs_plugin_register`, where an
  unwinding panic is undefined behavior. Catch it and return null, which
  the loader already treats as a failed registration.
- Reap killed runner processes. The spawn-timeout and disconnect paths
  called `child.kill()` without `wait()`, leaving a zombie on Unix until
  the host exited. Route both — and `shutdown()` — through a shared
  `reap()` that kills and waits on a detached thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 00:16:08 +03:00
Hakan Seven
deec2a8fd3 fix(xclip): keep clipped hatch loops in world_origin-relative space
`clip_hatch_boundary` lifted each loop into the clip polygon's world
space (`+world_origin`) to run the Sutherland–Hodgman clip, but returned
the result in absolute world coordinates. The renderer adds the per-hatch
`world_origin` anchor back in the vertex shader, so clipped hatches with a
large origin (e.g. UTM drawings) were drawn double-shifted and lost f32
precision. Subtract the origin again on the way out so the output stays in
the same precision-preserving anchor space as the input.

Also drop a stale third argument from two xclip tests left over from the
world-offset removal, which had broken `cargo test` compilation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 00:15:58 +03:00
Sebastian
d268170790 Fixing plugin_host.rs to double precision 2026-06-25 00:02:17 +03:00
Sebastian
d0003534a0 Fix 5: Allow backward-compatibility and add plugin-template-v2 2026-06-25 00:02:17 +03:00
Sebastian
2b898d6f22 Fix 4: shared-memory 2026-06-25 00:01:36 +03:00
Sebastian
a93d425002 Fix 3: optimize shutdown times 2026-06-25 00:01:36 +03:00
Sebastian
8eb199bfc7 Fix 2: optimize src 2026-06-25 00:01:36 +03:00
Sebastian
d430981da3 Fix 1: ribbon icon not deactivated 2026-06-25 00:01:36 +03:00
Sebastian
dcfaed0182 Initial commit 2026-06-25 00:01:36 +03:00
Hakan Seven
125b89d8da feat(save-dialog): extend drive navigation to macOS and Linux
Issue #170's fix added a "This PC" volumes root above the drive
letters on Windows. Generalize it so the Save As dialog can switch
volumes on every platform:

- parent_folder() now climbs the filesystem root into the volumes
  list on all platforms (`/` -> Computer), not just Windows.
- available_drives() is platform-specific: Windows probes A:-Z:,
  macOS lists `/` + /Volumes/*, Linux lists `/` + the removable
  mounts under /run/media/$USER, /media/$USER and /mnt. Other Unix
  and wasm fall back to `/`.
- drives_root_label() shows "This PC" on Windows, "Computer" elsewhere.
- read_dir_entries() lists the volumes at the drives root on every
  platform.

Also gitignore the dist/ web build output so the wasm binary is never
committed by accident.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:45:46 +03:00
Kevin Griffin
ef229f5d85 fix(save-dialog): allow navigating to other drives (#170)
The custom Save As dialog navigated up the tree solely via
`Path::parent()`, which returns `None` at a drive root (`C:\`).
Navigation therefore dead-ended on the starting volume, so other
drives and USB sticks were unreachable.

Add a synthetic Windows "This PC" drives root above the drive
letters. `parent_folder()` now climbs `…\sub → C:\ → This PC`, and
`read_dir_entries()` lists the available volumes (probed A:–Z: by
stat, no new dependency) when at that root. The dialog's up-button
and path bar route through the same helper and show "This PC".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:45:30 +03:00
Hakan Seven
8020286553 chore(release): v0.6.6
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:22:43 +03:00
Hakan Seven
bd1c51c682 fix(viewport): correct ViewCube flip + activation/border for off-screen viewports
ViewCube "already there → flip to opposite" tested the main (paper) camera,
which always looks straight down, so inside a floating viewport clicking
"top" landed on "bottom". Route the check through active_gaze_dir() — the
gaze of the camera the cube actually reflects.

Off-screen viewports:
- The active-viewport outline clamped only its origin while keeping full
  width/height, shifting the whole border inward. Clip it to the visible
  canvas (origin AND size) so it hugs the real viewport edge.
- Double-click activation tested each viewport's full paper rect, so a
  click on empty space beside a viewport that runs off-screen matched its
  partly-off-canvas rect and switched to it. Activation now keys on the
  visible on-screen rect (viewport_at_screen_point), picks the smallest
  (topmost) on overlap, and accepts the border-wire pick only when the
  click also lands inside that viewport's visible rect — so a far
  viewport's edge passing near the cursor can't steal the activation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:14:46 +03:00
Hakan Seven
7f36cf512f fix(viewcube): snap a paper viewport square to the UCS, not WCS
Clicking the ViewCube inside a floating viewport set only view_direction,
so the rebuilt camera fell back to WCS-up and the view opened tilted
relative to the UCS-oriented cube the user clicked. Build the full
UCS-aligned orientation (snap_to_direction with the ViewCube UCS) and
decode it to the viewport's stored (view_direction, twist_angle).

twist can't come from -Camera::roll(): camera_from_view rebuilds rotation
with its own yaw convention (atan2(x,-y)) and roll = -twist, which differs
from the camera's internal sync_yaw_pitch. Instead reproduce the decoder's
zero-twist basis and store twist = -signed_angle(up0 -> desired_up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 22:17:44 +03:00
Hakan Seven
3a4d842a7b fix(viewcube): hover/click hit-test must match the drawn cube under UCS
The hover hit-tests used the bare camera rotation (no UCS) and the main
camera even inside a floating viewport, so the highlighted face diverged
from the cursor whenever a UCS was active or in a paper viewport — while
the snap path was correct. Route hover, click-consume and snap through
active_view_rotation_mat(), and compose the ViewCube UCS in its viewport
branch so all three match ViewportData's drawn cam_rotation exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:38:51 +03:00
Hakan Seven
2507a8fe55 refactor: unify eye() to a single f64 accessor (drop eye_f64)
The whole pipeline is relative-to-eye now, so the f32/f64 split is
vestigial. Merge eye_f64 into eye() returning DVec3; direction-only
callers take .as_vec3().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:38:51 +03:00
Hakan Seven
bbf148f601 refactor: remove block_cache local_offset (fully absolute + double-single)
With the relative-to-eye double-single render path, the per-defn
`local_offset` precision origin is redundant: block geometry can be stored
absolute and stays precise via the high/low residual. Remove it
end-to-end:

- Drop the Pass-1 centroid computation, the BlockDefn.local_offset field,
  and the `defn_lo` parameter threaded through emit_wire /
  emit_greeked_text / emit_text_baseline / transform_tangent and the AABB
  helper (transform_offset_aabb_xy → transform_aabb_xy), plus the nested
  parent/child offset re-basing.
- Store text_obb_local as absolute f64 (was offset-relative f32) so the
  greek rectangles stay precise; emit splits to double-single. Give the
  greek-baseline points a low residual too.

Geometry is now absolute + double-single throughout the block cache,
matching the top-level path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:37:52 +03:00
Hakan Seven
62c7e20f42 refactor: drop the discarded offset from world_offset_from_centers
The function computed a median-centroid offset that is now always
discarded (geometry is absolute + double-single). Rename it to
cluster_extent_from_centers, return only the cluster span used for camera
fit / cull, and drop the dead centroid computation. Rename the companion
WORLD_OFFSET_SANE_EXTENT const to CLUSTER_SANE_EXTENT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:22:54 +03:00
Hakan Seven
04c93df2c4 refactor: remove the legacy full view_proj (CPU cull now relative-to-eye)
The GPU draws exclusively via the rotation-only `view_rot` + eye double-
single; the full view-projection survived only to project AABBs in the
CPU frustum-cull / scissor / LOD passes. Migrate those to the same
relative-to-eye math (project `point - eye` through `view_rot`) — pixel-
equivalent but precise at UTM scale — then drop the dead uniform:

- pipeline cull/scissor/LOD helpers take (view_rot, eye) instead of the
  full view_proj; render.rs reconstructs eye from the uniform's
  eye_high/eye_low and passes the cropped view_rot.
- Remove the `view_proj` field from the shared Uniforms struct and all six
  shader structs that only declared it for layout (none read it), and
  delete Camera::view_proj(). The struct shrinks by one mat4 on both
  sides, keeping the Rust / WGSL layouts in lock-step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:14:41 +03:00
Hakan Seven
5a8ec35f4f fix(blocks): restore per-defn precision after world_offset removal
The world_offset removal stripped two precision mechanisms in the block
cache that were masked while geometry was offset-relative (small coords)
but broke once coordinates became absolute UTM:

- build_defn no longer subtracted the defn's `local_offset` from the
  tessellated wire (it was passed as tessellate's world_offset). The defn
  was stored absolute while expand still re-added local_offset → block
  lines shifted. Restore the subtraction (with a double-single re-split)
  locally in tessellate_sub_local.
- Block fill triangles (greek-text rectangles, block solid fills) were
  batched with f32-only `fill_tris` and an empty `fill_tris_low`, so at
  UTM scale the absolute f32 quantized to ~0.5 m and the greek rects
  sheared. Plumb `fill_tris_low` through BatchEntry and emit the
  double-single residual from emit_wire and emit_greeked_text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:04:41 +03:00
Hakan Seven
cc43969ce0 refactor: remove remaining world_offset vestiges in app/
Clean up the last zero-valued world_offset locals outside the scene
tessellation core: the UCS wire-affine / grid-basis origin shift
(document.rs), the coordinate readout (view.rs), grip absolute-apply
(update.rs), the dead properties-edit re-offset guard, the MTEXT preview
glyph-box shift (mtext_editor.rs), and the cross-drawing paste correction
(commands.rs / cmd_result.rs) — including the now-dead
`clipboard_world_offset` field. Coordinates are absolute throughout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 19:01:25 +03:00
Hakan Seven
ef8d5b2610 refactor: remove the last world_offset zero-locals
Clean up the remaining `let [ox,oy,oz] = [0,0,0]` bindings and their
zero-valued add/subtract arithmetic across scene/mod.rs (mesh LOD resplit,
model AABB, wide-polyline fills, wipeout boundary, hatch/solid models,
insert/entity AABB), table, multileader and xclip. Coordinates now read as
the plain absolute values they are. No world_offset reference remains.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:55:00 +03:00
Hakan Seven
5b1a5a71a0 refactor: finish OST f64 integration; drop vestigial _f64 suffixes
- Object-snap-tracking points now project through the relative-to-eye path
  (view_proj_rte + eye) instead of the full view-projection, so they stay
  precise at UTM-scale coordinates — the last consumer of the legacy full
  view_proj outside the CPU frustum-cull / scissor path.
- Rename Camera::project_f64 → project and unproject_on_plane_f64 →
  unproject_on_plane: the f32 twins are gone, so the disambiguating suffix
  is vestigial. eye_f64 keeps its name — the f32 eye() still builds the
  view matrices.
- Remove stale #[allow(dead_code)] from eye_f64 / project / unproject_on_plane
  (all live now).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:41:25 +03:00
Hakan Seven
29415cac20 refactor: drop residual zero-subtraction from world_offset removal
In the files where ox/oy/oz came solely from the now-zero offset
destructure (tessellate, block_cache, dimension, leader, text_support),
remove the `let [ox,oy,oz] = [0,0,0]` binding and the `- ox` arithmetic so
the coordinate math reads as the plain absolute value it now is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:38:23 +03:00
Hakan Seven
56d507fd88 refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.

- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
  to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
  tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
  entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
  helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
  multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.

Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
Hakan Seven
02d40bd2ec fix(viewport): clip pick/snap to the viewport rect (no off-screen hits)
The GPU scissors floating-viewport content to the viewport rectangle, but
the CPU hit-test/snap wire set runs past it (the frustum cull keeps a
margin and lines extend beyond the rect), so box selection, lasso, click
and object snap could reach geometry clipped out of the viewport.

Reject candidates whose projected screen point falls outside `bounds` (the
pane rectangle), mirroring the scissor. `bounds` is the full canvas in
model space, so every change is a no-op there.

- snap: reject out-of-bounds points in try_pt, the grid block and tangent.
- click_hit / click_hits_all: no hit when the cursor is off-pane.
- box_hit: clamp the selection box to the pane rectangle.
- poly_hit: drop wire points projected off-pane.
- hatch click: no hit when the cursor is off-pane.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:45:33 +03:00