Commit Graph
183 Commits
Author SHA1 Message Date
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 <noreply@anthropic.com>
2026-05-31 03:31:20 +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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-05-30 21:29:01 +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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-05-29 20:30:56 +03:00
Hakan Seven 8a510c563c fix(render): adapt mesh + hatch load colours to background
`adapt_to_bg` (pure black → white on dark bg, pure white → black on
light bg) was only running through `render_style`, which the wire and
per-frame hatch paths already use. The mesh population paths and the
file-load hatch path went straight through `aci_to_rgba`, so:

- ACIS solids / regions / bodies appeared in their raw ACI colour
  even when that colour was pure white on a white paper bg.
- The initial hatch upload used the unadapted colour for the first
  frame after load (`synced_hatch_models` then re-ran adaptation per
  frame, so the issue cleared after one redraw — but visible).

Now every mesh / hatch creation site funnels through
`render_style_for` + `adapt_to_bg`:

- `build_derived_caches` (load): uses the default model bg as the
  adaptation target.
- `populate_meshes_from_document`: uses `self.render_style` so the
  per-frame paper / model bg selection applies.
- New `Scene::recolor_meshes` rewrites every cached mesh's colour
  field through `render_style`; called from `BACKGROUND` (after the
  user changes bg) and `set_current_layout` (Model ↔ paper bg
  switch) so ACIS mesh colour tracks the live bg without re-
  tessellating ACIS geometry.

Wipeout fills stay bg-coloured by design (that's their job). Raster
images skip adaptation — pixel data is not entity colour.
2026-05-28 21:15:28 +03:00
Hakan Seven e9e3bbbca1 fix(paper): drop model-block hatches from 2-D canvas
The previous commit let model-block hatches into the shared
`hatch_models_arc` so paper-layout content viewports could pick them
up through their GPU pipeline. PaperCanvas was reading the same arc
and projecting every entry through the paper camera, so model hatches
got a second copy on the paper sheet — at huge / off-position
coordinates because their `world_origin` is in offset-subtracted
local space, not paper millimetres.

Split the canvas-side accessors:

- `paper_canvas_hatches` now iterates `self.hatches` directly (the
  source map is keyed by entity handle) and keeps only entries whose
  owner is the active paper-layout block. The flattened arc only
  carries pattern names, so handle-based filtering had to move here.
- `paper_canvas_wipeouts` iterates entities likewise; paper-block
  wipeouts get `[0;3]` `world_offset` so the boundary is in paper
  millimetres.

GPU paths still call `hatch_models_arc` / `wipeout_models_arc` which
return everything — the per-viewport scissor / camera projection
silently culls the wrong-block hatches there.
2026-05-28 21:05:55 +03:00
Hakan Seven 4927804b77 fix(viewport): include model-block hatches in paper layouts
Paper-space content viewports rendered the model's wires + meshes but
no hatches. `synced_hatch_models` only kept entries owned by the active
layout's block, so flipping to a paper layout dropped every hatch
created in model space from the arc that `viewport_data_for` uploads
to the per-vp GPU pipeline.

`populate_hatches_from_document` already bakes the right per-block
world_offset into each `world_origin` (model → `self.world_offset`,
paper → 0), so adding both buckets back is safe: projecting a paper-
block hatch through a model viewport's camera lands it outside the
frustum and the per-vp scissor / LOD culls it (and vice versa). No
double-rendering.

`wipeout_models` had the same shape of bug — its `world_offset` was
picked from the active layout instead of the wipeout's owner. Now it
chooses per-entity, so model wipeouts also show up in paper viewports.
2026-05-28 20:59:12 +03:00
Hakan Seven 74a685d0e8 fix(viewport): UTM auto-fit + mesh world_offset
Two world_offset bugs in viewport rendering.

(1) Solid3D / Region / Body meshes were tessellated straight out of
the ACIS SAT data in WCS, while the wire / hatch / face3d pipelines
all run in `(WCS - world_offset)` local space. At UTM scale the
result was triangles drifting millions of units off-camera every
frame. `offset_mesh_lod_set` post-processes the freshly built
`MeshLodSet`, subtracting `world_offset` from every vertex and
recomputing `world_aabb` so the LOD / cull math agrees with the
shared camera space. Applied at the three `tessellate_volume`
call sites — load, `add_entity`, and `populate_meshes_from_document`.

(2) Paper-space content viewports rendered blank on UTM drawings.
The DWG / DXF saved `view_target = (0, 0, 0)` while the model sat
around `world_offset`; the CPU projection in `viewport_content_wires`
already had a "saved view doesn't overlap content cluster →
auto-fit to (world_offset ± local_extent_max)" fallback, but the
GPU path read `vp.view_target` literally and culled every entity
against the empty WCS rect. `camera_for_viewport` now applies the
same overlap test + auto-fit, and `model_wires_for_viewport`
derives its frustum AABB / wpp from the resulting camera so both
the camera and the cull agree on which area to show.

Folding `view_center` directly into `effective_target_wcs` matches
the CPU path's `display_center_x = view_target + view_center`,
removing the prior `+ view_right * view_center.x` shift that would
double-count once auto-fit had overwritten the target.
2026-05-28 20:51:16 +03:00
Hakan Seven 1eb54ee8a3 perf(viewport): per-vp frustum + LOD cull in paper layout
`model_wires_for_viewport` previously passed `view_aabb: None` and
`wpp: None` to `tessellate_entity` — the assumption that paper-space
viewports were too small for culling to matter falls over the moment
the document carries 100k+ entities. Each frame re-tessellated the
full model through every floating viewport.

`wires_for_block_culled` grows two optional parameters
(`frozen_layers`, `anno_scale_override`) and now flips its
`world_offset` / `bg` / `anno` selection on `is_model_block` rather
than `current_layout`, so a paper-layout caller asking for the model
block still gets WCS-subtracted coordinates and the right annotation
scale.

`model_wires_for_viewport`:
- derives the viewport's frustum AABB from its (paper-zoom-invariant)
  camera + entity aspect — 25 % margin to match `view_world_aabb`,
- derives `wpp` from the on-paper pixel height the viewport currently
  occupies, so LOD stubs and zoom-adaptive curve sampling track the
  paper-zoom state,
- delegates everything else to `wires_for_block_culled` so block /
  layer / frozen-layer / curve-tolerance handling stays in one place.

`model_wires_for_viewport_arc` keys on `(geometry_epoch,
round(screen_height_px))` — sub-pixel jitter still hits the cache,
real paper-zoom steps invalidate. Hit-test / CPU-projection callers
pass `screen_height_px = 0.0` to get the no-LOD (full-fidelity) wire
list in their own cache slot.
2026-05-28 20:32:16 +03:00
Hakan Seven 0eaac008dd fix(viewport): per-tile wire cache for Model layout
Tiled Model panes were all reading from the shared `entity_wires_arc`,
whose tessellation runs `view_world_aabb` / `world_per_pixel` against
the live `Scene::camera`. Zooming the active tile re-built that arc
with the active camera's LOD / frustum cull, then every other tile
rendered from the same arc — content in the inactive panes flickered
between full and LOD-stub tessellations as you zoomed.

- `wires_for_block` now delegates to `wires_for_block_culled`, which
  takes `view_aabb` + `wpp` explicitly. The original caller passes
  the live-camera values; tile rendering passes its own.
- New `model_tile_wires_arc` builds a per-tile view AABB + wpp from
  the tile's camera + pixel rect and caches the result. Cache is
  keyed by tile index; the value holds `(geometry_epoch,
  camera_state_hash)`, so a stale hit (camera moved, doc changed)
  still misses and rebuilds.
- `ViewportInstance` gains `tile_idx` so `viewport_data_for` can route
  Model tiles into the per-tile cache without disturbing the paper-
  layout content-viewport path (it still uses `model_wires_for_viewport_arc`).
2026-05-28 20:13:23 +03:00
Hakan Seven 39eab9a4c4 feat(viewport): draggable Model-tile dividers
Tiled Model layouts now show the inner edges between panes as 2-px
dividers that act as resize grips: cursor on a divider → resize cue,
press + drag moves the edge (with every tile sharing that edge
updating together), release runs a collapse pass that absorbs any
tile that fell below the viewcube-comfort minimum into the neighbour
with the longest shared contact edge — drag a divider all the way
across to remove a pane.

- Scene grows `model_tile_edges`, `hit_model_tile_edge`,
  `move_model_tile_edge`, `collapse_small_model_tiles`. Edges are
  derived from the existing `model_tiles` rects (no extra storage).
- App carries a `tile_drag` Option; `ViewportLeftPress` starts it
  ahead of tile activation / picking, `ViewportMove` drives the
  edge, `ViewportLeftRelease` collapses + clears.
- `SelectionCanvas` draws the dividers and switches the cursor to
  `Resizing{Horizontally, Vertically}` over them.
2026-05-28 18:42:39 +03:00
Hakan Seven e3c962bdd1 fix(viewport): adapt entity colours to visible bg
`model_wires_for_viewport` was tessellating with `self.bg_color` (the
model bg), so model content rendered inside a paper-layout viewport
adapted to a dark bg even though the transparent shader now reveals
the PaperCanvas sheet behind it — white wires stayed white on a light
sheet. Pass the paper bg in a paper layout.

Wire cache also needed an explicit invalidation when the bg or the
active layout changes; otherwise the cached, already-adapted wires
were returned against the new bg unchanged.

- BACKGROUND command bumps `geometry_epoch` after both branches
- new `Scene::set_current_layout` setter bumps on actual change; layout
  switch + history restore use it
2026-05-28 18:28:18 +03:00
Hakan SevenandClaude Opus 4.7 3d24dd5168 fix(viewport): blit UV crop for off-canvas viewports
Refactor the per-viewport blit so a content viewport whose screen rect
extends off the canvas composites the correct slice of its MSAA target
to the visible portion of the surface, instead of being slammed against
the canvas corner by an `(f32 as u32)` saturation on the negative
offset.

- blit.wgsl + Pipeline gain a small uniform buffer (`uv_offset` /
  `uv_scale`). `Primitive::prepare` computes the per-viewport crop from
  `screen_rect`'s on-canvas fraction and writes it via the new
  `upload_blit_uv` helper while it still has the `Queue`.
- `Pipeline::render` now takes `vp_size` (full vp — MSAA / camera size)
  and `surface_dest` (visible intersection — blit destination) and
  reads the crop from the uniform; passes that don't have a non-empty
  surface intersection are skipped.
- `Primitive::render` clips the full vp rect against the surface clip
  in `i32` and skips empty intersections. The earlier "skip GPU for
  inactive off-canvas viewports" workaround in `active_viewports` is
  no longer needed.
- ViewCube draws only when its hosting vp is fully on-canvas (its own
  pipeline writes directly to the surface and would distort under a
  clamped `set_viewport`).

With the GPU path correct for every vp position, drop
`viewport_content_wires` from `paper_canvas_wires` — the inactive
viewport CPU projection was only there to mask the blit bug and was
duplicating the unified shader's output. The hit-test / snap path
(`entity_wires_arc`, `wires_for_block`) still calls
`viewport_content_wires` directly, so picking through inactive
viewports keeps working.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:15:00 +03:00
Hakan SevenandClaude Opus 4.7 600717c1ec fix(paper): skip GPU for off-canvas inactive viewports
Zooming the paper view in PSPACE grows each floating viewport's screen
rectangle until its edges run past the canvas. The per-viewport blit
computes its surface offset via `(screen_rect.x * cw) as u32`, which
saturates negative values to 0 — so a viewport hanging off the left
edge was being painted at the canvas top-left instead of its actual
position, producing a drifting ghost copy on top of the CPU projection
that PaperCanvas was already drawing correctly clipped.

`active_viewports` for paper now skips the GPU pass for any inactive
content viewport whose screen rect would extend beyond the canvas. The
CPU projection already handles those, so only one (correct) copy is
visible. The active viewport keeps the GPU path so its per-viewport
render mode (Hidden Line, Gouraud, …) still applies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 10:38:56 +03:00
Hakan SevenandClaude Opus 4.7 a058ded74f feat(viewport): persist model tiled layout in VPort table
A multi-pane Model view (split / VPORTS 2H|2V|4) used to collapse back
to a single tile after save→reopen — only the *Active VPort entry was
written, the rest of model_tiles was discarded.

Each ModelTile now round-trips through a dedicated `*OCS_Tile_<i>` entry
in the VPort table:
- save_model_tiles_to_vports stashes the live camera into the active
  tile, removes any previous tile entries, and writes one VPort per
  tile (rect via lower_left/upper_right, camera via
  view_target/direction/height); the active tile is mirrored to
  `*Active` so apps that only read that entry still get a sensible
  view.
- restore_model_tiles_from_vports rebuilds the tile list on file open
  and selects the active tile by matching the entry whose view_target
  equals `*Active`'s, falling back to index 0.

Single-tile configurations and files from other apps skip the tile
entries entirely — restore returns false and the existing *Active /
View-table path runs, so behaviour for non-tiled files is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 10:16:09 +03:00
Hakan SevenandClaude Opus 4.7 417a708275 feat(viewport): tile-relative input + overlays in unified model
Route every per-frame interaction through the active model tile's screen
rectangle so a tiled VPORTS layout behaves like independent panes while
still sharing one shader widget:

- pick / draw / snap / box+lasso: cursor mapped to tile-local space; the
  hover-activates-tile rule on ViewportMove keeps the active tile under
  the cursor, so the picked camera matches what the user sees.
- pan: switched to Camera::pan_screen(dx, dy, viewport_height) so the
  world-per-pixel rate matches the active tile's height instead of the
  full canvas.
- grid: clipped via Frame::with_clip to the active tile rect; world→screen
  adds the tile origin so the lines align with the panel.
- UCS icon: origin offset by the tile rect so the gizmo sits in the
  active tile's bottom-left corner.
- render-mode picker (+ split buttons) and ViewCube hit-area: positioned
  via active_model_tile_bounds with leading Spaces, mirroring the
  floating-viewport pattern.
- ViewCube hover: driven by the CursorMoved/ViewportMove handlers into a
  new Scene::viewcube_hover cell, so the highlight survives the cube
  hit-area overlay masking events from shader::Program::update.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:45:19 +03:00
Hakan SevenandClaude Opus 4.7 6fe25e4b5f feat(viewport): interactive VPORTS model tiled presets
Model space'de VPORTS artık iki aşamalı: bare VPORTS bir yapılandırma
istiyor (SIngle/2H/2V/4), sonraki komut-satırı girdisi düzeni kuruyor
(boş giriş = SINGLE). set_model_tile_layout normalized rect listesiyle
tiled düzeni değiştiriyor:
- SINGLE: tek tam-ekran
- 2H: üst/alt
- 2V: sol/sağ
- 4: 2x2

awaiting_vports app flag'i prompt sonrası girdiyi yakalıyor. Paper-space
VPORTS davranışı değişmedi.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:52:55 +03:00
Hakan SevenandClaude Opus 4.7 c45146bbda feat(viewport): model-space tiled split (▤ / ▥ buttons)
Model layout artık tiled viewport destekliyor. Scene.model_tiles
normalized ekran rect'i + kamera tutan tile listesi; active_viewports
model layout'ta her tile için bir ViewportInstance üretiyor (aktif tile
canlı scene.camera'yı, inaktif tile'lar saklı snapshot'ı kullanıyor).

Render-mode picker'ın yanına iki buton eklendi: ▤ aktif tile'ı yatay
(üst/alt), ▥ dikey (sol/sağ) ikiye böler. split_active_model_tile
bölmeyi yapıyor; her iki yarı aktif kameranın kopyasını alıyor.

set_active_model_tile_at (cursor → aktif tile, kamera swap) ve
reset_model_tiles eklendi ama henüz bağlanmadı — aktif tile cursor
seçimi + pick yönlendirme sonraki adımda.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:33:36 +03:00
Hakan SevenandClaude Opus 4.7 fbc127b8d9 feat(viewport): ViewportInstance + Scene::active_viewports (step 1/6)
Birleşik viewport mimarisinin ilk parçası: tek render geçişinde
çizilecek viewport'ları tanımlayan ViewportInstance (handle, ekran
rect, kamera, render mode, aktif) ve bunları üreten active_viewports.
Model layout tek tam-ekran instance, paper layout her content viewport
entity'si için bir instance döndürüyor. Henüz render'a bağlı değil
(additive); mevcut davranış değişmedi.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:09:13 +03:00
Hakan SevenandClaude Opus 4.7 a41a05baa1 feat(paper): render-mode picker + ViewCube on active viewport
Paper-space'de bir viewport'a çift tıklanıp aktif hale gelince, model
space'deki gibi sol üst köşede render-mode dropdown'ı ve sağ üst köşede
ViewCube gösteriliyor.

- render_mode_picker helper'a çıkarıldı; model space ve aktif paper
  viewport ortak kullanıyor (kod tekrarı yok)
- SetRenderMode artık viewport-aware: aktif viewport varsa o viewport
  entity'sinin render_mode'unu, yoksa model-layout tab stilini günceller
  (Scene::active_viewport_render_mode getter/setter)
- build_viewport_primitive'e show_viewcube parametresi; aktif viewport
  primitive'i ViewCube gizmo'sunu çiziyor
- PaperViewportPane shader::Program artık Message'a özelleşti ve sol
  tık ViewCube hit-test'ini bounds-relative yapıp ViewCubeSnap üretiyor

Bilinen sorun: picker/viewcube doğru konumda görünüyor ancak tıklamalar
üstteki viewport_mouse mouse_area katmanına gidiyor; katman sırası bir
sonraki adımda düzeltilecek.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 18:05:09 +03:00
Hakan SevenandClaude Opus 4.7 874226bd43 feat(input): relative coordinate entry (@/#) and DYN default — #26
Koordinat girişi artık göreli/mutlak ayrımını destekliyor:
- `@x,y[,z]` → son girilen noktaya göreli (UCS eksenlerinde döndürülür)
- `#x,y[,z]` → her zaman mutlak (DYN durumundan bağımsız)
- ön ek yok → DYN açıksa göreli, kapalıysa mutlak
İlk noktada referans olmadığından göreli giriş mutlağa düşer. Çözülen
WCS noktası last_point olarak saklanıyor, böylece ardışık göreli
girişler zincirleniyor.

parse_coord artık (Vec3, CoordKind) döndürüyor; relative offset için
ucs_rotate_vec eklendi (origin translation olmadan UCS rotasyonu).
UCS ORIGIN komutu prefix'i yok sayıp mutlak kalıyor.

Ayrıca ViewCube snap refactor'undan kalan kullanılmayan kod temizlendi:
angle_close, active_viewport_yaw_pitch, CubeRegion::snap_angles.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:40:28 +03:00
Hakan SevenandClaude Opus 4.7 451239c4f9 fix(viewcube): snap to direction with nearest-canonical up axis
ViewCube tıklaması artık sabit (yaw, pitch) tablosu yerine bir yön
vektörüne (eye direction) snap ediyor. Up vektörü şöyle seçiliyor:

  1. Mevcut up alın.
  2. ±X / ±Y / ±Z arasından (yeni gaze ile (anti-)paralel olmayanlar)
     mevcut up'a en yüksek dot-product veren cardinal axis'i seç.
  3. Bu axis'i yeni gaze yönüne dik plane'e projekte et ve up olarak
     kullan.

Sonuç:
- Küçük tilt'ler ilgili canonical axis'e snap olarak temizleniyor.
- TOP yukarıda iken TOP-LEFT edge tıklayınca TOP yukarıda kalıyor;
  cube edge'e dönüyor ama kullanıcının "yukarı" hissi korunuyor.
- Upside-down orbit edilmişse -Z up olarak seçilir, snap baş aşağı
  yapılandırmayı bozmuyor.

CubeRegion::snap_angles ve Camera::snap_to_angles yerini
snap_direction / snap_to_direction aldı.
snap_active_viewport_to_direction viewport'un view_direction'ını
günceller, twist_angle'a dokunmaz.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:09:38 +03:00
Hakan SevenandClaude Opus 4.7 26bcd89358 feat(lod): preserve 3D position for sub-pixel entity stubs
Yakındayken tessellate edilen 3D entity'ler (Face3D, Solid3D, Mesh,
PolyfaceMesh, PolygonMesh, Body, Region) <5 px eşiğine düştüğünde,
eski tek-çizgili stub'ın z=0 düzlemine indirilmesi yüzünden farklı
bir konumda görünüyordu. Yeni lod_stub_wire_3d entity'nin 3D AABB
kübünü (8 köşe, 12 kenar) çiziyor:
- XY'de entity_aabb local frame'i (world_offset shift edilmiş)
- Z aralığı bbox.min/max.z, world_offset[2] subtract edilmiş

2D entity stub'ı (lod_stub_wire) text fallback'leri ve diğer 2D
geometriler için z=0'da kalıyor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:17:42 +03:00
Hakan SevenandClaude Opus 4.7 e66905e9b1 refactor(entities): Solid3D/Region/Body accessor trio in entities/solid3d.rs
The same three-arm `match entity { Solid3D | Region | Body }` block
appeared at five callsites — twice in tessellate.rs (point_of_reference,
fallback wires) and three times in scene/mod.rs (mesh tess dispatch,
apply_grip pre/post position).

Add three accessors in entities/solid3d.rs:

  point_of_reference(e) → Option<&Vector3>
  fallback_wires(e)     → Option<&[Wire]>
  tessellate_volume(e, color, facet_res) → Option<MeshLodSet>

Callsites shrink from 5–8 lines to one call. The apply_grip path
collapses two parallel pre/post matches plus the entity-list `matches!`
guard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 17:22:02 +03:00
Hakan SevenandClaude Opus 4.7 5e36e85e07 refactor(common): unify bulge → arc math into BulgeArc
The same DXF/DWG bulge → (center, radius, sweep) formula was inlined
in six places (hatch outline, polyline tess + truck path, lwpolyline
×4, scene hatch fill, explode bulge_to_arc) — each with subtly
different sweep-direction handling for the bulge sign.

Add `entities::common::BulgeArc { center, radius, start_angle,
end_angle, sweep }` with one `from_bulge(p0, p1, bulge)` constructor
and a `sample(t)` helper. Callsites collapse to a few lines.

Net: -121 lines, one canonical sweep-direction convention shared by
all consumers.

array.rs (the path-along-curve f32 helper) keeps its inline math —
its sign convention is inverted relative to the others and changing
it without exercising the array-path command risks regressing the
output.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:42:26 +03:00
Hakan SevenandClaude Opus 4.7 40180e9089 refactor(scene/mod.rs): pull entity-specific helpers into entities/
Five chunks moved out of the 5965-line scene/mod.rs to live with the
entities they belong to:

- wide_lwpolyline_fills → entities/lwpolyline.rs::wide_fills
- wide_polyline2d_fills → entities/polyline.rs::wide_fills
- polyline_segment_fill → entities/common.rs (shared by both above)
- text_obb_corners_native + text_baseline_points + text_greek_obb_tris
  → entities/text_support.rs (handles Text/MText/Attribute alike)
- append_insert_attribute_wires → entities/insert.rs

Net: scene/mod.rs 5965 → 5404 lines (~560 lines extracted). Three
helpers in scene/render.rs / scene/block_cache.rs widened from
pub(super) to pub(crate) so the relocated functions can still reach
them. scene::entity_aabb also widened for the same reason.

scene::tessellate_entity_dim_text stays put — it's a thin wrapper
around the private tessellate_entity dispatcher and moving it would
require exposing tessellate_entity's full signature across the crate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:26:14 +03:00
Hakan SevenandClaude Opus 4.7 b413fe0efe refactor(tess): rename LegacyTess → FallbackTess
The trait isn't deprecating anything — it carries the path entities
take when they're not covered by the truck topology pipeline
(Viewport, Insert, Hatch outline, Ole2Frame). "Fallback" names the
purpose; "Legacy" implied retirement that isn't actually on the
roadmap.

Same rename for LegacyGeometry → FallbackGeometry,
legacy_geometry() → fallback_geometry().

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:16:57 +03:00
Hakan SevenandClaude Opus 4.7 aadb64860d refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:

  LegacyTess     — Viewport / Insert / Hatch / Ole2Frame
                   (fallback path for entities not on the truck pipeline)
  LeaderTess     — Leader
  MultiLeaderTess — MultiLeader
  DimensionTess  — Dimension (carries its dim-only helpers too:
                   ~1700 lines of dim_geometry, dim_text_*, format_*,
                   tolerance, suppression, etc.)

scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.

scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
Hakan SevenandClaude Opus 4.7 ef1f9a08d6 chore: rename H7CAD to Open CAD Studio
Joining the OpenAEC Foundation (open-aec.com). Crate/struct become
OpenCADStudio, user-facing strings use the spaced "Open CAD Studio"
form, repo URLs point to HakanSeven12/open-cad-studio. Logo (window
icon + assets/logo.svg) redrawn with OCS initials. Drop examples/
binaries — unused.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 17:16:15 +03:00
Hakan SevenandClaude Opus 4.7 fb3dd58d09 fix(polyline): correct wide-polyline fill band arc center + add LWDISPLAY toggle
Wide LWPolyline/Polyline2D fill bands now sit on the centerline:
- polyline_segment_fill arc center matched explode.rs / acadrust convention
  (signed d_perp = r·(1-b²)/(1+b²), handles major arcs correctly)
- per-vertex hw0/hw1 taper applied along arc; tess density 24/circle
- synced_hatch_models subtracts hatch_offset from band boundary so it lives
  in the same offset-rel space as the wire tessellation

LWDISPLAY:
- new command `LWDISPLAY [ON|OFF]` toggles header.lineweight_display
- LWT status-bar pill (between ORTHO and POLAR) drives the same flag
- bump_geometry() on change so the viewport re-tess immediately

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 07:15:26 +03:00
HakanandClaude Opus 4.7 8e6491d4a8 fix(wipeout): use real clip vertices, correct image-pixel→WCS mapping
acadrust's DWG reader read the wipeout/raster-image clip boundary
points only to advance the bit cursor and then discarded them, so every
wipeout fell back to `Wipeout::new()`'s default placeholder rectangle
(`-0.5..0.5`). For files that store the real polygon mask in clip
vertices (anteen.dwg's `DIMBLOCK` style: u_vector ≈ 643k, size = (1, 1),
clip polygon ~0.0016×0.0033 in pixel space), the wipeout rendered at the
full image bbox — hundreds of thousands of units wide — instead of the
intended ~1000×2000 mask.

Pinned to the new `fix/wipeout-clip-vertices` acadrust branch (carries
the upstream parser fix). On the H7CAD side, the polygon mapping now
follows the DXF convention:

  x_off = (clip.x + size.x/2) × u_vec
  y_off = (size.y/2 − clip.y) × v_vec      (image-Y points down)

…in `wipeout_boundary_2d` (fill), `Wipeout::to_truck` (border line),
and `Wipeout::grips` (vertex handles) so all three stay in sync. The
fill path also explicitly closes the polygon loop — without it the GPU
`in_polygon` ray-cast skipped the v(n-1)→v(0) edge and the solid mask
bled far past the boundary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 14:14:22 +03:00
Hakan SevenandClaude Opus 4.7 f12e864aa6 feat(mtext): integrate every inline format code in MTEXT renderer
Replace the line-based MTEXT pipeline with a run-aware parser + renderer.
Every DXF MTEXT inline code now reaches the stroke output: \H \W \Q \T \A
\C \c \f \F \M \X, the \p block (q/i/l/r/t/s), brace-scoped state, and
the \L\l \O\o \K\k decoration toggles.

Parser ([text_support]): emits Vec<MTextLine{ runs, align, indent_*,
tab_stops }> with per-run RunState; braces scope character state, paragraph
props persist across them.

Renderer ([mtext]): atomise → wrap → render. Per-run height/width/oblique/
tracking/font/colour/decorations/valign; paragraph indents and tab stops
honoured; default 4-em tab fallback.

Plumbing: tessellate() now returns Vec<WireModel>; TextStroke gains an
optional inline-colour override so per-run \C/\c emits one wire per colour.
cxf::tessellate_text_run adds a tracking multiplier. block_cache folds
multi-wire results into a single LocalWire (colour split preserved at
top-level, flattened inside cached defns).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 23:42:22 +03:00
HakanandClaude Opus 4.7 3fb23ae31b feat(scene): integrate text LOD across every text-bearing entity
Moves the Dimension and Table baked-block paths up into
`tessellate_entity` so sub-Text/MText recurse through the same LOD
ladder (baseline / greek / full) the top-level entities already use,
and routes the synth-dim text path through `tessellate_entity` as well
so files that omit `block_name` (e.g. typical DXFs) also get LOD.

Extends the text-height LOD branch from Text/MText to
AttributeDefinition, AttributeEntity, and Tolerance — the OBB helpers
gained the same coverage so the baseline/greek rect lines up.

When a sub-entity inside a baked dim/table block has ByBlock colour,
the recursive call's resolved colour is replaced with the parent's so
dimension text no longer renders in the raw ByBlock fallback hue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:29:57 +03:00
HakanandClaude Opus 4.7 eab72062f9 fix(scene): render baked block geometry for Dimension and Table
AutoCAD bakes a per-instance block with the final, displayed geometry
into Dimension and Table entities (typically *D### / *T###, but custom
names also occur — sample-file-H uses DIMBLOCK*-4NP). The block's text
already encodes DIMTXT * DIMSCALE (or the table's scale factor), so
synthesising the entity from the dim/table style on top of that
double-applies the scale.

When the referenced block exists with content, render it directly:
- Dimension: dim.base.block_name -> block_record
- Table: block_record_handle -> block_record

The synthesis fallback stays in place for files where the block is
missing or empty.

Fixes sample-file-H.dwg dimensions rendering at 60000 instead of 200
(DIMTXT=199.196 * DIMSCALE=304.8).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 12:54:49 +03:00
Hakan SevenandClaude Opus 4.7 ff2485eba9 fix(scene): bake selection colour into greek-LOD text fill
The greek-LOD text path emits no `points` and only `fill_tris`; the
face3d fill pipeline colours each triangle with `wire.color`. We were
storing the entity's own colour there, so selecting a text and then
zooming past the LOD boundary kept the wire.selected flag but the
on-screen colour dropped back to the entity hue.

Swap `wire.color` to `WireModel::SELECTED` when `selected` is set, the
same way `lod_stub_wire` already does it.

Follow-up to #19.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 06:15:44 +03:00
Hakan SevenandClaude Opus 4.7 f93d7cf9d2 fix(scene): bake selection colour into lod_stub_wire
The stub wires emitted at low LOD carried wire.selected = true but
stored the entity's own colour, so the actual on-screen pixel kept
the entity hue. Selecting an object and then zooming past the LOD
boundary made the highlight vanish even though the entity was still
in the selection set.

Swap to WireModel::SELECTED at construction time, matching what
tessellate.rs does for full-LOD wires.

Follow-up to #19.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 06:09:56 +03:00
Hakan SevenandClaude Opus 4.7 8355e5a278 fix(scene): keep low-LOD objects visible and selectable
At deep zoom-out, entities that projected to under ~5 px were dropped
from the scene entirely (and text below 2 px-baseline followed the
same path). The objects disappeared visually AND fell out of
window / crossing selection, and any prior selection highlight stopped
rendering once the zoom transition crossed the LOD threshold. #19.

Two changes:

1. `tessellate_entity`: replace the sub-5-px `return vec![]` cull, the
   sub-2-px text-baseline `return vec![]`, and the empty-greek fall-out
   with a new `lod_stub_wire` helper. The stub is a 2-point AABB
   diagonal carrying the same `selected` flag, ACI, and AABB as the
   entity — so the entity stays visible as a 1-pixel speck, tracks its
   highlight colour across LOD changes, and remains hit-test'able.

2. `box_hit` / `poly_hit`: when a wire has no `points` but a finite
   `aabb` (greek text emits only fill_tris), fall back to the AABB
   rectangle as the hit-test shape. Defense in depth so future fill-
   only wires don't silently drop out of selection.

Closes #19

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 06:07:04 +03:00
Hakan SevenandClaude Opus 4.7 9c4043ef45 feat(scene): scale Solid3D / Region / Body tessellation by header FACETRES
ACIS-bearing entities tessellated with a hard-coded LOD ladder
(circ_segs / grid_u / grid_v) so a high-detail drawing produced the
same coarse mesh as a low-detail one. Multiply the per-LOD counts by
`header.facet_resolution` (FACETRES), clamped to AutoCAD's
documented [0.01, 10.0] range with a floor of 4 segments to keep
degenerate values harmless.

All four call sites (build_caches, commit_entity mesh seed, undo/redo
populate_meshes_from_document, and the helper signatures themselves)
take a FACETRES argument now.

ISOLINES / surface_u_density / surface_v_density and the
camera / sun / geo header fields stay metadata until matching render
work lands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 04:51:26 +03:00
Hakan SevenandClaude Opus 4.7 65920959d4 feat(scene): fall back to header EXTMIN/EXTMAX for fit-all
When a fresh document hasn't tessellated yet (no wire AABB cache,
no per-entity key vertices found), compute_model_space_extents()
returned None, so ZOOM EXTENTS / auto_fit_viewport had to wait for
the next pass before they could fit. Use the header's saved
model_space_extents_min/max as a last-resort answer so the very first
ZOOM EXTENTS / fit-all after open lands on the drawing immediately.

LIMMIN/LIMMAX and INSBASE remain read-only metadata for now — they
need a bounded-grid mode (LIMMIN/LIMMAX) and a "save as block" anchor
(INSBASE) before they map onto a visible behaviour.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 04:45:37 +03:00
Hakan SevenandClaude Opus 4.7 2d795e64ed feat(scene): honour render-affecting HeaderVariables (PDMODE/ATTMODE/FILLMODE/LWDISPLAY/MIRRTEXT)
Header system variables that change how the file renders were being
ignored. Wire them through the render path:

- PDMODE + PDSIZE: Point entities draw the requested glyph shape
  (dot / + / × / | with optional enclosing circle and square) sized by
  PDSIZE. Default PDMODE 0 keeps the single-vertex fast path.
- ATTMODE: INSERT attribute rendering now respects 0=Off (no attribs),
  1=Normal (per-attrib invisible flag), 2=On (force all visible).
- FILLMODE: when false, hatch / wipeout / face3d-fill uploads are
  short-circuited so the renderer draws wireframe only.
- LWDISPLAY: when false every entity falls back to the 1-pixel base
  width, matching AutoCAD's "Show Lineweight" toggle.
- MIRRTEXT: when false, MIRROR keeps text / mtext / shape rotation +
  oblique angle (so text stays right-reading) while still mirroring
  position.

DISPSILH / PLINEGEN were also reviewed: render path already honours
the per-polyline plinegen bit and we don't yet render Solid3D
silhouettes, so the header values are read for round-trip but produce
no extra effect here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 04:44:07 +03:00
Hakan SevenandClaude Opus 4.7 e0ec7f1999 fix(scene): render INSERT attribute values
Inserting a block with attributes (ATTRIB entities attached to the
INSERT) drew only the block's primary geometry — the attribute values
the user filled in were silently dropped. block_cache::expand_insert
walks the block's BlockRecord which lists the ATTDEF templates, not the
per-INSERT ATTRIB instances, so they were never tessellated.

Add append_insert_attribute_wires: after expanding the block sub-wires,
iterate ins.attributes, skip the entries flagged invisible, and emit a
text wire for each at its stored WCS position. The wire's name reuses
the parent INSERT handle so selection / picking still treats the block
and its attribute text as one entity. Runs on both the block-cache
fast path and the legacy explode fallback.

Closes #20

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:04:40 +03:00
Hakan Seven a73dffdd76 fix(scene): rescale dash pattern when projecting wires to paper space
`viewport_content_wires` cloned model-space tessellations and overwrote
`points` with paper-space projections (× scale) but left `pattern_length`
and `pattern` at their model-coord values (pre-multiplied by PSLTSCALE's
1/vp_scale). The GPU then compared a paper-coord distance against a
model-coord pattern, so dashed linetypes in viewport content collapsed to
solid (a typical 9 mm paper line fell inside the first 180 mm dash).

Multiply the projected wire's pattern by the same vp scale used for the
points. Works for both PSLTSCALE on (paper-uniform dashes) and off
(dashes shrink with viewport scale).

Adds two diagnostic examples used to track this down:
  - inspect_lt: which layers/entities carry dashed linetypes
  - inspect_block: enumerate entities + linetypes inside a named block
2026-05-19 08:27:05 +03:00
Hakan Seven 66243e53ca feat: correct viewport scale calculation with correct dimension and text scaling 2026-05-19 00:26:08 +03:00
Hakan SevenandClaude Opus 4.7 b4745efb24 fix(scene): emit entities with degenerate bbox from the quadtree path
Phase 2.1's wire path was silently dropping any entity whose
`bounding_box()` returns the degenerate default (or non-finite
coords). The legacy `entity_aabb` mapped those to the
`UNBOUNDED_AABB` sentinel, which `tessellate_entity` treated as
"render unconditionally". The new path filtered them at index time
(no bbox → no quadtree entry) AND `is_unindexable_entity` only
matched type variants (Insert/Viewport/Block/BlockEnd), so those
entities fell through neither path.

Symptom: opening a non-UTM file showed the floor plan with most
walls / dimensions / hatching dropped while isolated entities (text,
circles) still rendered.

Fix: `Scene::entity_index()` now returns an `EntityIndex` carrying
both the quadtree and a parallel `unbounded_handles: Vec<Handle>` of
entities whose bbox came back degenerate. The wire path always emits
that list in addition to the view-rect candidates, mirroring the
legacy "no bbox → never cull" behaviour.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 16:02:09 +03:00
Hakan SevenandClaude Opus 4.7 b70c9f9262 perf(pipeline): per-frame hatch+wipeout frustum cull at draw time (Phase 2.3)
The Phase 2.1 step-4 attempt at culling hatches inside
`synced_hatch_models` was wrong: GPU hatch/wipeout buffers are
uploaded on geometry_epoch only (see render.rs — they're "static
buffers"), so any CPU-side cull at build time would freeze the
visible set at the geometry epoch boundary and never re-evaluate on
pan. Reverted that approach; the build-time function now returns the
full visible set again and the cache key is back to geometry_epoch.

The actual per-frame frustum cull moves into the existing draw-time
skip-flag machinery:

- `compute_hatch_lod` now also folds in `aabb_offscreen` (ORed with
  the existing Phase 3.3 sub-pixel skip), so out-of-view hatches are
  skipped at the draw call.
- New `compute_wipeout_lod` + `wipeout_skip_flags` mirror the hatch
  path. The wipeout draw loop honors the flag.
- `Primitive::prepare` calls the new compute step alongside the rest.

Per-frame projection + skip is a few hundred microseconds on dense
docs; far cheaper than re-uploading GPU buffers per pan tick.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 15:02:07 +03:00
Hakan SevenandClaude Opus 4.7 49fddea1ef fix(scene): include camera_generation in hatch/wipeout cache keys
The Phase 2.1 step-4 change made `synced_hatch_models` and
`wipeout_models` view-cull through `entity_index().query_rect`, but
left both caches keyed on `geometry_epoch` alone. Cache hit would
return a list culled to a previous camera's view, dropping hatches as
the user panned. Now keyed by `(geometry_epoch, camera_generation)`
— same pattern as `wire_cache`.

`image_cache` stays geometry-only: images self-cull per frame via
`vp_scissor` in the GPU pipeline; the cached list is camera-agnostic.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:56:20 +03:00
Hakan SevenandClaude Opus 4.7 5b8cdd940e perf(scene): quadtree culling in hatch + wipeout passes (Phase 2.1, step 4)
`synced_hatch_models` and `wipeout_models` now pre-filter their
per-entity scans through `entity_index().query_rect(view)` when a
Model-layout view AABB is available. Affects:

- Top-level hatch HashMap iteration (the dominant cost — clone +
  render_style + selection tint per hatch).
- Wide LWPolyline / Polyline2D solid-fill loop.
- Wipeout entity scan.

The Insert→exploded-hatch loop is left alone; Insert is not
quadtree-indexed (its WCS bbox depends on block defn × insert
transform, handled inside block_cache).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:50:00 +03:00
Hakan SevenandClaude Opus 4.7 e43498d6c4 perf(scene): quadtree culling in wire tessellation (Phase 2.1, step 3)
`wires_for_block` now drives candidate selection from the quadtree
when a Model-layout view AABB is available — `query_rect` replaces
the O(N) `doc.entities().filter(...)` scan. Visibility predicate is
hoisted into a closure and reused for the quadtree-candidate path
and the unindexable-entity append (Insert/Viewport).

Paper space and the pre-fit first-frame settle path keep the
original full-scan behaviour (no view AABB → no cull). Per-entity
LOD + frustum checks inside `tessellate_entity` stay in place; the
quadtree is purely a candidate reducer so culling correctness still
funnels through one place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:46:51 +03:00