Phase 5.3 (CPU tessellation slice). Add a `PERF` command that toggles an
overlay on the active viewport showing the cost of the most recent wire
re-tessellation: ms, wire count, geometry epoch. Anchored top-left via
position_canvas_overlay.
Timing is captured at the wire-cache miss paths (model_tile_wires_arc,
paper_sheet_wires_arc) and stored in Cells on Scene, read directly by the
UI view (Scene is single-threaded / Rc-based, so build_primitive and view
share a thread). Reads ~0 ms on a warm cache, so it isolates the exact
work a miss costs — the slice 2.2 / 3.1 / 3.3 each move, making them
PR-to-PR comparable.
GPU upload/draw/GPU-wait spans (need wgpu timestamp queries) remain open.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 3.1 (partial). set_hover_highlight bumped the geometry epoch on
every hover-target change, forcing a full model re-tessellation. The
hover handle is folded into the tessellation highlight set as
selected ∪ {hover}, so hovering an already-selected entity contributes
nothing — the effective set and every resulting WireModel are identical.
Guard the bump on the effective highlight contribution actually changing;
hovering over or between selected entities is now free. The field is
still updated so hit-test / UI state stays current.
The full camera/selection-from-tessellation cache split (3.1 / 2.2) is
left open: highlight colour is baked into WireModel.color across several
tessellation sites, so decoupling it needs running-app verification.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 5.2. Time each open phase — parse, purge, derived-cache build on
the loader thread, plus xref resolve on the UI thread — and emit a
breakdown line on open completion:
parse 1240ms · purge 80ms · caches 340ms · xref 60ms · total 1820ms
Makes open-time regressions (and the wins from 1.1 / 2.4) visible at a
glance. Timings ride back on DerivedCaches; total is wall time from the
Open click to post-xref.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 4.1. Handle / u64 keys dominate the hot maps (block_defn,
hatch / image / mesh caches, draw-depth map, viewport wire caches);
the default SipHash is wasted work on integer keys. Alias the std
types to rustc_hash::FxHashMap / FxHashSet crate-wide so every map and
set — including the cross-module draw-depth map shared between scene
and the GPU pipelines — uses the faster hasher consistently.
Mechanical: no behavioural change. Constructors moved from ::new() to
::default() (Fx variants carry no RandomState). Build + 13 tests green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 1.1: resolve_xrefs now purges corrupt entities inline as it
merges each xref and returns the dropped count, removing the second
full-document purge_corrupt_entities walk in the FileOpened handler.
Every xref-bearing open saves one O(N) entities() pass.
Phase 2.4: split compute_world_offset into offset_prep / offset_centroid
/ world_offset_from_centers; centroid accumulation now rides the existing
cache-handle walk in build_derived_caches. Two O(N) entity walks collapse
into one, with identical MSPACE-membership filtering preserved.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a scene hover_highlight handle that renders an entity with the
selection colour without joining the real selection. The cycling list
box sets it on row enter / clears on exit, so hovering a row glows the
matching object in the drawing.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a per-type selection filter to the scene: the FILTER pill opens a
checklist of the entity types in the layout, and unchecking a type
excludes it from interactive picking (click, box and lasso selection all
honour it). The pill lights up while any type is filtered out.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a transparency_enable uniform read by the wire shader: when the new
TPY pill is off, every line is forced opaque; when on, baked entity
transparency shows through. Toggling is a uniform write, no retessellate
(same mechanism as the lineweight-display toggle). Pill is toggleable
from the status-bar customization menu.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a hidden-entity set to the scene that the tessellation visibility
test skips, so hidden objects neither render nor hit-test. New commands
ISOLATEOBJECTS (show only the selection), HIDEOBJECTS (hide the
selection) and UNISOLATEOBJECTS (restore). An ISO status pill lights up
while anything is hidden and restores everything on click.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Read the model-space annotation scale from the standard CANNOSCALE /
CANNOSCALEVALUE header variables instead of a user-variable hack, and
populate both the status-bar scale picker and the viewport Scale
property purely from the drawing's ACAD_SCALELIST — no built-in
fallback set. Xref-derived scales (`_XREF` suffix, `name|` prefix)
are filtered out so only the host drawing's own scales appear.
Bumps the patched acadrust to the commit that adds CANNOSCALE/
CANNOSCALEVALUE read/write.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Tables without a baked block (e.g. created in-app) were drawn as a single
monochrome wire, ignoring cell colours/fills/border styling. Add a
coloured synthesis path that emits per-colour WireModels for:
- cell background fill (fill_color + fill_enabled),
- per-cell text in its content/text colour,
- grid borders with per-edge type/weight/colour/visibility (cell-style
override → row-style → default),
- horizontal/vertical margins and flow direction (down/up).
Imported tables keep using AutoCAD's baked block. Resolution layers the
per-cell CellStyle over the TableStyle row style over defaults.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Each model-space tile now carries its own visual style instead of one
global mode shared by every pane:
- ModelTile gains a `render_mode`; the active tile renders with the live
picker mode, inactive tiles keep their stored style, so changing one
tile no longer changes the others. Split inherits the source style.
- SetRenderMode writes only the active tile; switching the active tile
mirrors its style back into the tab (picker + live render).
- Persisted through the *Active VPORT entries (acadrust now round-trips
the render mode via DXF code 281 / DWG RC 281), restored on open for
both tiled and single-tile model layouts.
- Bump acadrust to pick up the VPORT render-mode field.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two paper-space rendering fixes:
- The full-canvas sheet instance no longer draws model-block hatches/
wipeouts/images. Those belong inside the floating content viewports;
rendering them on the sheet let them bleed past the viewport borders
when model coords overlapped the paper area. The sheet now draws only
the paper block's own fills plus the synthetic white printable area.
- Pan/zoom/orbit inside a double-clicked floating viewport (MSPACE) now
re-culls and re-uploads the viewport's wire set. The per-viewport wire
cache key now hashes the viewport's own view (pan + zoom + orbit), and
the active-viewport pan/zoom/orbit handlers bump camera_generation so
the GPU re-uploads. Previously the stale frustum-culled subset stayed
on the GPU, so newly-revealed lines were invisible (hatches, uploaded
as the full set, still showed) until MSPACE was exited.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The overall "sheet" viewport now IS the paper view, so its own border
rectangle must not be drawn as an entity — filter it out of
paper_sheet_wires_arc (content viewport borders are kept).
Also update the doc comments that still referenced the removed
PaperCanvas widget / paper_canvas_wires to describe the GPU sheet path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Paper space now renders entirely through the unified GPU shader, exactly
like model space — there is no separate 2-D canvas. The full-canvas
top-locked "sheet" viewport draws the layout's entities, hatches,
wipeouts and images; the desk is the container background and the white
printable area is a synthetic solid hatch.
- The white sheet fill uses draw_depth -2.0 (entity fills/wires are in
the signed (-1, 1) range) so it is always strictly behind every object,
with a negligible z offset (BIAS 0.001) — no far-plane clipping.
- Delete src/scene/paper_canvas.rs (the PaperCanvas widget) and its dead
helpers (paper_canvas_wires / paper_sheet_wires / paper_canvas_cache).
paper_canvas_hatches / _wipeouts remain — still used by paper hit-test.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Paper space now renders its sheet through the unified shader, the same
path as model space: a full-canvas "sheet" ViewportInstance draws the
layout's own entities + viewport borders + interim/preview wires, with a
camera locked to the top/plan orientation (paper is 2-D and never
orbits). Floating content viewports overlay it.
- PaperCanvas no longer draws entity wires (still paints the desk, sheet
and hatch/wipeout/image fills; full removal is the next step).
- Right-drag on the paper sheet no longer orbits the camera (it would
corrupt the frame and skew subsequent pans).
- Fix floating-viewport orbit oscillation: orbit_active_viewport wrote
view_direction.y negated while camera_for_viewport reads it un-negated,
so each drag step read back a Y-mirrored camera and flipped the model
between a rotation and its opposite every frame. Store eye directly,
matching snap_active_viewport_to_direction and the reader.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Persist the paper-space camera through the sheet viewport entity (which
round-trips natively in both DXF and DWG) instead of the
OpenCADStudio_Camera_* named-View side-channel:
- apply_sheet_viewport_camera reads the sheet viewport entity first; the
named View is only a backward-compat fallback for older files.
- sync_camera_to_document (paper) writes the camera only to the sheet
viewport entity. Model branch is unchanged.
- Add Scene::ensure_sheet_viewport: guarantee every paper layout has its
full-screen overall (id==1) viewport, creating one if a loaded file
lacks it; wired into layout enter and add_layout.
- Restore the owner-handle guard on the sheet-viewport finder so a
foreign layout's viewport can't be picked as the sheet viewport.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Single-click selection only tested wire-edge proximity, so clicking a
solid's face missed — only its thin projected edges were selectable.
Add mesh_click_hit: project each mesh triangle to screen and test the
cursor against it (front-most wins), wired as a fallback after the wire
and hatch hit-tests. Works for any meshed solid.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Rename the Home tab to "Draw" and add a Model tab beside it with two
groups:
- Model: BOX / CYLINDER / CONE / SPHERE / WEDGE / TORUS, placed with a
footprint + height. Each is built as a real ACIS Solid3D (acadrust
primitive builders) plus a truck B-rep, tessellated into the shaded
mesh pipeline, with edge wires for picking + wireframe.
- Design: UNION / SUBTRACT / INTERSECT via truck-shapeops.
A session-only Scene.model_solids cache holds each solid's truck B-rep
so the boolean tools can combine and chain them. Supersedes the old
command-line BOX/SPHERE/CYLINDER (insert::solid3d_cmds keeps EXTRUDE/
REVOLVE/SWEEP/LOFT).
Known limitations: booleans need solids created this session; curved
ACIS primitives render in-session but not after reload (the SAT
tessellator handles only planar caps); geometry assumes world_offset 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Single-click picking cleared the selection first (exclusive), so clicking a
second object dropped the first (issue #47). Plain clicks now add to the
selection and Shift+click removes the picked entity; empty-space click and
Esc still clear. Shift state is tracked from keyboard ModifiersChanged
events since mouse click messages carry no modifiers.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The viewport context-menu item dispatched a bare "ZOOM" string, which no
command branch handled, so it did nothing. Send "ZOOM EXTENTS" instead.
fit_all() also fed on the frustum-culled wire set, so it only framed the
entities already on screen — each invocation revealed a little more and the
view converged on the true extent only after several uses (issue #51). It
now builds an un-culled wire set at a fixed tessellation tolerance, so the
bounds are zoom-independent and the first call fits the whole drawing.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Two ways to extend the current selection by entity type + layer:
* `Select Similar` (right-click → Select Similar, or `SELECTSIMILAR` /
`SELSIM`): for every (type, layer) pair in the current selection, add
every entity in the active layout that matches the pair. The seed
selection stays selected; matches already selected are not
double-counted.
* `Quick Select` (right-click → Quick Select…, or `QSELECT` / `QS`):
opens a centred floating panel with Object-type and Layer pick_lists
(populated only with values that actually exist in the active layout)
and an "Append to current selection" checkbox. `(Any)` in either slot
skips that filter. Apply replaces the selection (or extends it with
Append on) and reports the match count on the command line. Cancel,
Esc, and outside-click all dismiss without applying. When opened with
a selection present, the panel pre-fills both filters from the first
selected entity so QSelect doubles as an editable Select Similar.
Scoped to entities owned by the active layout's block-record so a paper
layout's QSELECT never reaches into model-space entities (and vice
versa). Entity-type names live in a single `entity_type_name` helper in
`entities::traits` to keep the UI strings and the filter keys aligned.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
`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.
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.
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.
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.
`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.
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`).
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.
`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
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>