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 <noreply@anthropic.com>
`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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>