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
The previous off-canvas fix sized each per-viewport MSAA / depth /
resolve texture to the full viewport rect, then UV-cropped the blit.
With deep paper-space zoom that rect can grow past wgpu's 8192-pixel
texture limit and the program panics in `Device::create_texture`
("Dimension Y value 9493 exceeds the limit of 8192").
`viewport_data_for` now clips the screen rect to the canvas before
returning the `ViewportData`, so the per-viewport textures are bounded
by the surface (≤ canvas size, always within the GPU limit). The
camera frustum is still built at the *full* vp aspect (so the world it
projects matches the viewport entity), and a new `crop_view_proj`
post-multiplies the standard view_proj by a clip-space transform that
remaps the visible sub-rect to NDC `[-1, 1]^2` — the off-center ortho
that lets the visible-sized MSAA carry the correct portion of the
view. `viewport_size` is set to the visible size for the wire shader's
screen-space line extrusion; `world_per_pixel` is crop-invariant (the
`vs` factor cancels) so the value `Uniforms::new` computes from the
full bounds remains correct.
Viewports whose screen rect doesn't intersect the canvas at all return
`None` and are filtered out before any inner pipeline is allocated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
Geometry passes draw with standard `SrcAlpha / 1-SrcAlpha` into a
transparent MSAA target, so AA-edge fragments sit as `(rgb * a, a)` in
the resolve texture. The previous straight-alpha blit then multiplied
those by `a` a second time, darkening thin lines and curves over the
transparent background. `PREMULTIPLIED_ALPHA_BLENDING` matches what is
actually in the resolve texture and renders edges at full intensity
while still leaving fully-transparent pixels untouched on the surface.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Collapse the paper-space rendering path into the same shader widget the
model layout uses. The PaperCanvas widget draws the paper sheet, paper
entities and viewport borders underneath; the unified shader paints all
content viewports on top, scissored to each viewport's rect.
- Drop PaperViewportPane / PaperViewportPipeline newtype /
PaperViewportPrimitive, ViewportPaneMode::Paper, ViewportPane::paper,
build_primitive and build_viewport_primitive. Scene exposes only
build_viewports.
- view.rs: the paper branch stacks PaperCanvas + the unified shader
instead of the old paper_canvas_view function. The active paper
viewport's blue border, render-mode picker and ViewCube hit-area are
layered in the same viewport_stack as the model layout.
- Transparent shader clear (alpha=0) + alpha-blended blit pipeline so
the underlying widget (container bg in model, PaperCanvas sheet in
paper) shows through everywhere the shader has not painted geometry —
including inside content viewport rectangles.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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>
build_viewports, active_viewports'un ViewportInstance listesinden
viewport başına ViewportData üretiyor (model: tüm model wire'ları;
paper viewport: layer-freeze'li alt küme). Model space artık
ViewportPane::model -> build_viewports üzerinden birleşik çoklu-viewport
yolundan render ediliyor — şimdilik tek tam-ekran viewport, davranış
birebir aynı.
Paper space hâlâ eski yolda (PaperCanvas + PaperViewportPane); sonraki
adımda birleşik yola taşınacak.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Birleşik viewport mimarisinin yapısal temeli. Primitive artık tek kamera/
geometri yerine viewport başına bir ViewportData listesi taşıyor; yeni
MultiPipeline her viewport için bir iç Pipeline instance tutuyor.
prepare/render viewport listesi üzerinde dönüp her iç-pipeline'ı kendi
(normalized) ekran rect'ine çiziyor.
Mevcut Pipeline kodu (upload/LOD/render/blit) HİÇ değişmedi — sadece
viewport başına bir kez çalışıyor. build_primitive ve
build_viewport_primitive tek-elemanlı liste (FULL_VIEWPORT_RECT)
ürettiği için render davranışı tek-viewport ile birebir aynı; çoklu
viewport sonraki adımda (view.rs tek widget + active_viewports) aktive
edilecek.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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>
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>
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>
PolyfaceMesh / PolygonMesh için outline edges WireModel.points üzerinden
gpu_wires'a giriyor; bu nedenle FlatShaded / GouraudShaded modlarında
shaded yüzeyin üstünde görünmeye devam ediyordu. WireGpu artık kaynak
WireModel'da fill_tris doluysa 'is_3d_mesh_edge' bayrağını set ediyor;
wire pass'i show_3d_edges=false iken bu instance'ları atlıyor.
*WithEdges, Wireframe ve HiddenLine modları show_3d_edges=true ile
geliyor — mesh kenarları orada görünür kalır.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3D yüzeyleri (Solid3D mesh, 3DFACE quad, PolyfaceMesh/PolygonMesh face
tris) HiddenLine modunda renk yazmadan z-buffer'a yazıyor; ardından
kenarlar wireframe pipeline ile çizilince LessEqual depth testi
arkadaki segmentleri eliyor. 2D fill'ler (text greek, MultiLeader
arka planı) ayrı bir vertex buffer'a ayrıldı ve her modda renkli
çizilmeye devam ediyor.
- Face3DGpu: vertex_buffer artık 3D / 2D olarak ikiye ayrıldı
- mesh_depth_pipeline + face3d_depth_pipeline (write_mask=empty)
- RenderModeFlags.hidden_line + Primitive.hidden_line plumbing
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wireframe 2D/3D modlarında Solid3D meshlerini edge-only çizmek için
LineList topolojili bir mesh wireframe pipeline ve her üçgenin
(a,b),(b,c),(c,a) kenarlarına genişletilmiş ayrı bir index buffer
eklendi. PolyfaceMesh / PolygonMesh face fill'leri (fill_tris + dolu
points) wireframe'de gizleniyor; text greek / MultiLeader gibi 2D
fill'ler (points boş) etkilenmiyor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the binary Wireframe/Solid toggle with a 7-mode dropdown
that maps onto acadrust's `ViewportRenderMode` enum: Wireframe 2D /
Wireframe 3D / Hidden Line / Flat Shaded / Gouraud Shaded / Flat
Shaded + Edges / Gouraud Shaded + Edges. Per-tab state lives on
`DocumentTab::render_mode`; the old `SetWireframe` message forwards
to `SetRenderMode` so the ribbon + WIREFRAME / SOLID command line
keep working.
`scene::render::render_mode_flags()` translates the mode into three
pipeline-facing booleans (`face3d_fill`, `mesh_fill`,
`show_3d_edges`); `mesh_fill` / `show_3d_edges` are stubs that the
upcoming phases will consume. `Primitive` carries the derived
`view_wireframe` plus the two new fields through to `prepare()`.
`Pipeline::upload_face3d` takes a `wireframe_only` flag that drops
3DFACE quad fills (built from `key_vertices`) while keeping the
`all_wires.fill_tris` channel — that channel carries text-LOD greek
rectangles and PolyfaceMesh face triangles which must stay visible
in wireframe mode. 3DFACE edges stay on regardless of mode.
Solid3D mesh wireframe rendering, Hidden Line z-prepass, and Flat /
Gouraud shader uniforms come in phases 1–4.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two behaviour changes that bring the command line in line with
established CAD workflow:
1. **Always-on keyboard capture.** The input was blurred whenever no
command was running, so typed letters went nowhere until the user
clicked into the field. Replace the blur with a re-focus on every
command lifecycle transition (cmd_result.rs) and on viewport / tab
/ ribbon clicks (update.rs), focus the input at startup, and add
a global key subscription that catches any unhandled character key
or backspace and routes it into the buffer via two new messages
(CommandAppendChar / CommandBackspace) — so the command line keeps
accumulating keystrokes even if focus parks somewhere not
focusable. Removes the unused blur_cmd_input helper.
2. **Space submits.** Typing a space at the end of the input now
submits the buffer the same way Enter does. Only exception: an
active command that's currently collecting free-form prose
(TEXT / MTEXT / DDEDIT body, ATTDEF prompt / default value,
INSERT attribute value, ATTEDIT cell). Those advertise the new
`CadCommand::wants_text_with_spaces()` flag so the Space character
reaches the buffer as a literal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`HatchModel.boundary` stores multi-region hatches as a single Vec
with NaN-NaN sentinels separating each disconnected sub-path
(documented on the field). The paper-canvas hatch fill ignored the
sentinels and forwarded every vertex straight to the iced canvas
path builder. When a layout contained a multi-region hatch, lyon
received a polygon containing NaN coordinates and panicked with
`Tessellate path.: Internal(IncorrectActiveEdgeOrder(4))` from
inside `iced_wgpu::geometry::Frame::fill`, taking the whole app down
the first time Layout 1 painted.
Walk the boundary instead, opening a fresh sub-path on each sentinel
and closing whichever was active. Each region is now filled as its
own closed polygon. Reproduces with #22.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Two near-identical match-on-EntityType tables lived far apart:
scene/properties.rs::entity_type_name returned mixed-case UI labels;
app/commands.rs::entity_type_name returned uppercase DXF record names.
Both enumerated ~30 variants; adding a new entity meant remembering to
touch two files in two crates' worth of source layout.
Move both into a single entities/names.rs as `ui_name()` and
`dxf_name()`. Adjacent so a new variant gets both labels added at once;
function names make the intended audience explicit at the callsite.
scene/properties.rs and app/commands.rs each lose their local match
plus one re-export, gaining a `crate::entities::names::*` call.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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>
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>
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>
Step 4 (final) of the wire pipeline cold-open optimization plan. Color
shrinks from `[f32; 4]` (16 B) to `[u8; 4]` (4 B) and the vertex attribute
format flips to `Unorm8x4`; the GPU still hands the shader a `vec4<f32>`
in [0, 1], so wire.wgsl needs no change. 8-bit per channel is below
display precision.
Net: 76 B per instance, ~14 % smaller than the Step-3 instance layout and
~7.6× smaller than the pre-Step-3 per-vertex format. Wire upload memory
and PCIe bandwidth on cold open scale down accordingly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 3 of the wire pipeline cold-open optimization plan — the biggest
single lever.
Each segment used to upload as 6 vertices × 96 B = 576 B with the segment
payload (pos_a/pos_b/color/half_width/distance/pattern_length/pat0/pat1)
duplicated across all six corners of the quad. Wire pipeline now uploads
one INSTANCE per segment (88 B) and the vertex shader derives `which_end`
and `side` from `@builtin(vertex_index)` against a 6-entry corner table.
Net effect on cold open: ~6.5× less wire vertex memory and PCIe upload,
plus a smaller Vec to allocate and copy on the CPU side.
Wire instance attributes also collapse distance_a / distance_b into a
single mix() inside the vertex shader, so per-segment distance still
interpolates correctly for the GPU-side linetype dash test.
Touches the wire, wire_xray, and face3d-edges draw passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 2 of the wire pipeline cold-open optimization plan. Extracts the
per-wire vertex expansion into a free function and drives it from
`from_batch` via `par_iter().map(...).reduce(...)`. Per-wire emission is
already independent (no shared state, no order coupling — `block_cache`
groups wires by style upstream), so the parallel-then-concatenate shape
is correctness-preserving.
Cold-open wire upload now scales with available cores; a drawing with
many short wires shows the biggest win. Also collapses two near-identical
emission loops (`from_batch`, `build`) into one shared helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 1 of the wire pipeline cold-open optimization plan. Replaces
`create_buffer_init` (intermediate staging-buffer memcpy) with a
`mapped_at_creation` buffer the build path writes into directly.
Cuts one alloc + one memcpy per wire buffer; on cold open with hundreds
of MB of wire data this shows up immediately as lower peak memory and
faster upload.
Guarded against size-0 buffers by allocating a single-vertex placeholder
when the wire is empty (the renderer already skips draws on vertex_count
below the per-segment minimum).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
LWDISPLAY (Show Lineweight) flipping the toggle no longer triggers a full
retessellate. `render_style_for` always bakes the entity's resolved (layer-
inherited) lineweight, and the wire shader reads `Uniforms.lwdisplay_enable`
to either honour the per-vertex half_width or collapse the quad to a 1 px line.
Side effect: PDF export now uses the real lineweight regardless of LWDISPLAY,
matching AutoCAD's plot semantics (LWDISPLAY only affects screen render).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lift the MText parser + renderer into a reusable `layout_mtext` helper in
`text_support` and re-point the remaining text-bearing entities at it, so
every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`,
`\p…`, decorations, stacked fractions, paragraph indents, tab stops, …)
reaches the stroke output uniformly.
Per-entity changes:
- TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds
`layout_mtext`; `\C7;Red`-style codes no longer render as literal
glyph noise.
- MLEADER MText content (both entity path in [entities/multileader.rs]
and the LOD-aware path in [scene/tessellate.rs]) — drops the
`strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0
baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame /
bg-fill rectangle now read line widths from the layout.
- ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine
the value goes through the MText pipeline (SingleLine stays on the
Text-style anchor maths because the DXF spec disallows codes there).
- DIMENSION text_override — `dimension_text_entity` emits a synthetic
MText (instead of Text) whenever the value contains MText escapes;
the LOD ladder via `tessellate_entity_dim_text` handles both kinds.
Infrastructure:
- `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`,
`BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly.
- `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so
callers that draw chrome (frames, fills, baselines, greek rects)
share the same numbers as the rendered strokes.
- `mtext_line_count` re-routed through the new parser so LOD row counts
and rendered row counts can never disagree.
- Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`,
`measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the
`v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`)
deleted — all callers migrated.
Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented
here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
- MultiLeader: stop double-scaling when ctx.text_height (WCS-baked) is set;
use it as-is and only multiply by scale_factor + anno on the fallback path.
ctx.text_width gets the same treatment.
- Text / Attribute: width_factor and oblique_angle are entity-authoritative.
The previous entity*style multiplication / addition mis-rendered any text
whose TXSTYLE had a non-default width_factor or oblique_angle (RR 0.6,
STYLE2 15°, Romans/ARIEL/A32 0.65-0.8 in the sample file).
- MText: honour TXSTYLE is_backward (negative width_factor mirror) and
is_upside_down (180° rotation). measure_mtext_chars uses positive
magnitude so anchor math stays correct under backward text.
- Tolerance: use tol.dimension_gap when set instead of the hard-coded
0.35 * height heuristic.
- Table: cell text height / font / alignment resolve through
CellContent -> CellStyle -> TableStyle.{title|header|data}_row_style
-> compiled-in defaults. Row classification respects the suppressed
flags. Cell + content rotation applied.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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>
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>