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]>
`text_obb_corners_native` derived MText line count from explicit `\n`
matches in `m.value`, so any MText that relied on `\P`/`\N` breaks or
word-wrap (rectangle_width-driven) reported a single line. The greek
emitter then split the OBB into one row and the user saw a single box
on top of a 5-line wrapped paragraph.
- text_support: new `mtext_line_count` — strip codes, split on
\n/\P/\N, word-wrap each paragraph against `rectangle_width`,
sum. Same line splitting the MText/MultiLeader renderers already
perform; centralized for reuse.
- text_obb_corners_native: takes `mtext_lines_override: Option<usize>`.
When supplied, OBB height = h_world × n × line_spacing_factor; this
also overrides a possibly stale `rectangle_height` on resaved DWGs.
- Top-level greek paths (scene/mod.rs) compute the line count via
`mtext_line_count` and pass it through to baseline / rect emission.
- block_cache: tessellate_sub_local computes the count at defn build
so the stored `text_obb_local` is wrap-aware; emit_text_baseline
and emit_greeked_text recover n_lines from `ulen / h_local` as
before and now produce the correct per-row split inside Inserts.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Greek rects rendered through fill_tris went through the face3d 0.45
dim path; the pre-boost (color × 1/0.45) compensated only until clamp
hit at 1.0, leaving bright user colors washed out. The earlier outline
workaround dodged the dim but lost the filled box visual that AutoCAD
uses for "text lives here".
- face3d pipeline now skips the 0.45 dim when `wire.points` is
empty. PolyfaceMesh / PolygonMesh keep their dim (both points and
fill_tris populated); greek and MultiLeader background-fill emit
`points: vec![]` so they render at the literal color.
- All three greek paths (top-level, block-defn, MultiLeader) emit a
2-triangle filled rect instead of an outline / boosted fill.
- Greek rects are clamped to single-line height so 5-line MText
doesn't blow the box up to the full text-block height. MultiLeader
emits one rect per line — keeps the per-row visual hint.
- BatchEntry/StyleKey gain an `is_fill_only` discriminator so greek
batches never collide with regular wire batches; otherwise the
finalized WireModel could carry both points and fill_tris and
defeat the pipeline-side empty-points check.
Xref fade still applies to the resolved color via the existing
`fade_toward_bg` path, so xref greek stays faded as expected.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Replaces the 2/4 drop/greek/full ladder with 1/5 baseline/greek/full so
zoomed-out text still leaves a visual hint instead of disappearing:
h_px < 1 → OBB baseline line in the text color
1 ≤ h < 5 → greeked OBB rect
h_px ≥ 5 → full per-glyph stroke
Extra guard: in the baseline tier, the line itself is dropped when its
projected length is under 2 px (single-char text seen edge-on would
otherwise emit a sub-pixel segment).
Text/MText is exempted from the generic 5 px AABB cull (top-level and
block-defn paths) so it can reach this ladder even when its bounding
box projects below the threshold.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
`Entity::bounding_box()` for empty / vertex-less entities (Polyline,
LwPolyline, Hatch, etc.) returns `BoundingBox3D::from_point(Vector3::ZERO)` —
the (0, 0, 0)→(0, 0, 0) placeholder. Including those in the LO centroid
scan dragged it toward the origin even for UTM-authored blocks, defeating
the precision-preservation: stored f32 then sat at UTM-minus-near-zero,
losing precision before tessellate even returned. Reject the zero
placeholder so only real geometry contributes.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Two latent traps in the first-pass centroid scan:
- A bbox where x/y were finite but z was NaN would skip the z update;
if NO entity contributed a finite z, `min[2]` stayed at `INFINITY` and
`max[2]` at `NEG_INFINITY`. `(INF + NEG_INF) * 0.5 = NaN`, so the defn's
`local_offset.z` was NaN, propagating to every cast point in the defn.
Track `have` per axis and default missing axes to 0.
- A single corrupt entity at parser-garbage coords (1e15+) silently
dragged the centroid out of the real geometry's neighbourhood. Reject
per-axis values above `SANE_EXTENT` so one bad bbox doesn't ruin the
whole defn.
Also include `Insert::insert_point` as a contributing sample so a
composite block (only nested Inserts, no direct wires) still gets a
non-zero offset — useful for the nested-defn AABB transforms even
though no direct LocalWire stores against it.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Xref work:
- merge_xref_into_block now copies the xref's layer / linetype / nested
block_record tables into the host under `{xref}|{name}` prefixes and
remaps entity references, so ByLayer/ByBlock resolution lands on the
right entry instead of falling back to WHITE + 1 px.
- expand_insert (and the explode fallback) detect xref BlockRecords by
flag and apply `fade_toward_bg(color, bg, 0.5)` to every emitted final
color (selection bypasses the fade). ExpandCtx propagates `is_xref` +
`bg_color` through nested expansions.
Block precision groundwork:
- BlockDefn gains `local_offset: [f64; 3]` set in build_defn from the
AABB centre of the defn's direct entities. tessellate_sub_local now
passes that offset to tessellate(), so LocalWire.points / fill_tris /
aabb_local / text_obb_local are stored in offset-frame f32 and don't
lose precision for distant content.
- emit_wire / emit_greeked_text / transform_tangent take the defn offset
and re-add it in f64 before composing with accum_xform. defn_aabb_
recursive transforms nested children from their offset frame into the
parent's. transform_offset_aabb_xy replaces transform_aabb_xy.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Top-level OBB used insertion_point as anchor and hardcoded MText anchor to
bottom-left, so greeked rects landed on the opposite side of the actual
text whenever MText used a non-default attachment_point or TEXT used a
non-Left/Baseline alignment. Block-defn subs never set or read
`text_obb_local`, so greeked text inside Inserts fell back to the
axis-aligned AABB and ignored entity rotation entirely.
- Share OBB corner computation via `text_obb_corners_native`.
- Map all 9 MText AttachmentPoint variants to (h_anchor, v_anchor).
- For TEXT, anchor on `alignment_point` (DXF 11) when alignment != Left+Baseline.
- Populate `LocalWire.text_obb_local` in `tessellate_sub_local`.
- `emit_greeked_text` transforms OBB corners by `accum_xform`, falls back
to AABB only when no OBB was precomputed.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Text greeking previously only fired for top-level Text/MText — text
that was baked into a block defn at cache build time still ran the
full glyph stroke path on every expand, so cadastral parcel labels
(typically inside xrefed blocks) saw none of the LOD benefit.
- `LocalWire` now records `text_height_local` for Text / MText subs.
- `expand_defn`'s wire arm applies the same 5 / 10 px ladder: cull
below 5 px (after scaling the local glyph height by the accumulated
transform's Y-scale), greek between 5 and 10, full glyphs above.
- `emit_greeked_text` reuses the batch infrastructure so multiple
greeked rects in the same style still collapse into one buffer.
Frustum culling only helps at zoom-in (offscreen geometry); zoom-out
still tessellated and uploaded every wire even when an entire entity
projected to a single pixel. Now we also drop wires whose world AABB
projects smaller than 0.5px in the active view — a fit-all view of a
city plan skips sub-pixel symbols, lines, and curves before they ever
hit the GPU.
- `Scene` captures `world_per_pixel` each render from camera ortho_size
/ widget height.
- `expand_insert` / `tessellate_entity` take an optional
`world_per_pixel`; per-LocalWire / per-nested / whole-Insert /
top-level entity AABB → pixel-size test → skip if < MIN_PIXEL_SIZE.
- One-off paths (hit-test, viewport pane) pass `None` so they still
see full geometry regardless of zoom.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Block defns and top-level entities now know their world-space XY
AABB, and `tessellate_entity` / `expand_insert` early-out for anything
that doesn't intersect the camera's view rect. Tested commit-only on
time_open (correct subset emitted with default camera); GUI run
pending user verification.
Cache plumbing:
- `LocalWire` / `BlockDefn` carry `aabb_local` (post-build recursive
union over nested defns).
- `Scene::view_world_aabb()` derives a 25%-margined world rect from
the active camera + last-seen render aspect.
- `wire_cache` / `paper_sheet_cache` keys widen to
`(geometry_epoch, camera_generation)` so pan/zoom invalidates the
cached wire list — and pipeline `cached_epoch` widens to match
so the GPU re-uploads on view change.
- `XrefStatus::Unloaded` ack already shipped — orthogonal to this.
Also adds `examples/xref_states.rs` to dump per-block_record xref
metadata for diagnosing the is_loaded / unloaded distinction.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Tessellates each block_record once into local-coord LocalWires; Insert
references transform-copy + ByBlock-resolve the cache rather than
explode+re-tessellate per reference. Same-style wires inside one Insert
are NaN-batched into a single WireModel to keep wgpu buffer counts
sane — large blocks that previously bailed to a marker now render in
a handful of buffers each.
Co-Authored-By: Claude Opus 4.7 <[email protected]>