// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
use super::super::*;

/// Dim an already-bg-adapted colour toward the background when the entity's
/// layer is locked, so locked objects read as non-editable (they stay visible
/// and snappable). No-op for unlocked layers.
fn fade_if_locked(
    document: &acadrust::CadDocument,
    e: &EntityType,
    color: [f32; 4],
    bg: [f32; 4],
) -> [f32; 4] {
    if view::render::layer_locked(document, e) {
        crate::scene::cache::block_cache::fade_toward_bg(color, bg)
    } else {
        color
    }
}

/// The section mark's viewing direction (paper-space, unit), derived entirely
/// from the decoded model-documentation view graph:
///
/// 1. `symbol.view_rep_handle` → the parent view's `AcDbViewRep`.
/// 2. The parent's refs name the section view's `AcDbViewRep` (any ref that
///    owns an `AcDbViewRepSectionDefinition`), falling back to the document's
///    section views.
/// 3. Each view's refs include its `AcDbViewBorder` entity, which carries the
///    view's *active* viewport — the entity holding the real camera.
/// 4. The section camera's sight (−view_direction) is projected onto the parent
///    camera's right/up basis (same convention as `camera_from_view`), giving
///    the on-paper arrow direction.
///
/// `None` when any link is missing (DXF files, older DWGs) — the caller then
/// falls back to a geometric heuristic.
fn section_arrow_dir_from_views(
    document: &acadrust::CadDocument,
    s: &acadrust::entities::SectionSymbol,
) -> Option<[f64; 2]> {
    use acadrust::types::Handle as AHandle;
    if s.view_rep_handle.is_null() {
        return None;
    }
    // A view's active viewport, via its border entity among the ViewRep refs.
    let border_vp = |h: &AHandle| -> Option<AHandle> {
        match document.get_entity(*h)? {
            EntityType::ViewBorder(b) if !b.active_viewport.is_null() => Some(b.active_viewport),
            _ => None,
        }
    };
    let parent_vr = s.view_rep_handle;
    let parent_refs = document.view_rep_refs.get(&parent_vr)?;
    let parent_vp_h = parent_refs.iter().find_map(border_vp)?;
    let sect_vr = parent_refs
        .iter()
        .find(|h| **h != parent_vr && document.section_view_reps.contains(h))
        .or_else(|| document.section_view_reps.iter().find(|&&h| h != parent_vr))
        .copied()?;
    let sect_refs = document.view_rep_refs.get(&sect_vr)?;
    let sect_vp_h = sect_refs.iter().find_map(border_vp)?;

    let viewport = |h: &AHandle| {
        document.entities().find_map(|e| match e {
            EntityType::Viewport(v) if v.common.handle == *h => Some(v),
            _ => None,
        })
    };
    let pvp = viewport(&parent_vp_h)?;
    let svp = viewport(&sect_vp_h)?;

    // Section sight: view_direction points target→camera, sight is its inverse.
    let sight = -glam::DVec3::new(
        svp.view_direction.x,
        svp.view_direction.y,
        svp.view_direction.z,
    )
    .try_normalize()?;

    // Parent camera basis — the same yaw/pitch/roll convention the viewport
    // renderer uses (`camera_from_view`), so the arrow lands exactly where the
    // view content does.
    let vd = glam::Vec3::new(
        pvp.view_direction.x as f32,
        pvp.view_direction.y as f32,
        pvp.view_direction.z as f32,
    )
    .normalize_or(glam::Vec3::Z);
    let pitch = vd.z.clamp(-1.0, 1.0).asin();
    let yaw = if vd.x.abs() < 1e-6 && vd.y.abs() < 1e-6 {
        0.0_f32
    } else {
        vd.x.atan2(-vd.y)
    };
    let rotation = view::camera::yaw_pitch_to_quat(yaw, pitch, -(pvp.twist_angle as f32));
    let right = (rotation * glam::Vec3::X).as_dvec3();
    let up = (rotation * glam::Vec3::Y).as_dvec3();

    let d = glam::DVec2::new(sight.dot(right), sight.dot(up));
    let len = d.length();
    (len > 1e-6).then(|| [d.x / len, d.y / len])
}

/// Build the display wires for a decoded `AcDbSectionSymbol` (the "A-A" cut
/// mark on a Model-Documentation view), styled from its `AcDbSectionViewStyle`.
///
/// Everything drawn here is decoded from the file:
/// - Endpoints, end-tick lengths and the identifier come from the symbol.
/// - Whether the full cutting line is drawn (`show_plane_line` — off gives the
///   familiar broken line), the end segments (`show_end_lines`), the arrows
///   (`show_arrows`), and all sizes come from the section-view style's flags.
/// - The arrowhead shape resolves through the style's arrow block handle via
///   the shared dimension/leader arrow table (`arrow_from_block`); a null
///   handle is the standard ClosedFilled default.
/// - The arrow direction comes from the view graph
///   ([`section_arrow_dir_from_views`]); only when that chain is unavailable
///   does it fall back to the 90°-CCW rotation of END-A→END-B.
fn section_symbol_wires(
    document: &acadrust::CadDocument,
    s: &acadrust::entities::SectionSymbol,
    style: Option<&acadrust::entities::SectionViewStyle>,
    h: Handle,
    color: [f32; 4],
    sel: bool,
    lw_px: f32,
) -> Vec<WireModel> {
    use crate::scene::convert::tessellate::{append_arrow, arrow_from_block, ArrowKind, DimGeom};

    let nan = [f64::NAN; 3];
    let (ax, ay) = (s.end_a[0], s.end_a[1]);
    let (bx, by) = (s.end_b[0], s.end_b[1]);
    // Unit along the cut, pointing from END-B toward END-A (each end's tick
    // extends *outward*, away from the other end).
    let (dx, dy) = (ax - bx, ay - by);
    let clen = (dx * dx + dy * dy).sqrt().max(1e-9);
    let (ux, uy) = (dx / clen, dy / clen);
    // Viewing direction: from the decoded view graph when the chain resolves,
    // else the 90°-CCW rotation of (END-A → END-B).
    let [vx, vy] = section_arrow_dir_from_views(document, s).unwrap_or_else(|| {
        let (vx0, vy0) = (ay - by, bx - ax);
        let vlen = (vx0 * vx0 + vy0 * vy0).sqrt().max(1e-9);
        [vx0 / vlen, vy0 / vlen]
    });

    let (show_arrows, show_plane_line, show_end_lines, arrow_size, arrow_ext, label_h) = match style
    {
        Some(st) => (
            st.show_arrows,
            st.show_plane_line,
            st.show_end_lines,
            st.arrow_size,
            st.arrow_extension,
            st.label_height,
        ),
        None => {
            let t = s.tick_a.abs().max(s.tick_b.abs());
            (
                true,
                false,
                true,
                (t * 0.66).max(2.5),
                t.max(5.0),
                t.max(2.5),
            )
        }
    };
    // Arrowhead via the shared dimension/leader arrow table: the style's arrow
    // block handle (null → ClosedFilled), sized from the style.
    let arrow_kind = match style {
        Some(st) if st.arrow_start_handle != 0 => arrow_from_block(
            document,
            acadrust::types::Handle::from(st.arrow_start_handle),
            arrow_size as f32,
        ),
        _ => ArrowKind::Triangle {
            size: arrow_size as f32,
            filled: true,
            size_mul: 1.0,
        },
    };

    let mut lines: Vec<[f64; 3]> = Vec::new();
    let mut fill: Vec<[f64; 3]> = Vec::new();

    // Full cutting-plane line through the view, only when the style asks.
    if show_plane_line {
        lines.push([bx, by, 0.0]);
        lines.push([ax, ay, 0.0]);
    }

    // Each end: (endpoint, tick length, outward sign along the cut).
    for (ex, ey, tick, osign) in [
        (ax, ay, s.tick_a.abs(), 1.0),
        (bx, by, s.tick_b.abs(), -1.0),
    ] {
        let (ox, oy) = (ux * osign, uy * osign); // outward along the cut
        let tip = [ex + ox * tick, ey + oy * tick, 0.0]; // outer tick tip
                                                         // End segment: the short drawn extension past the end (the "broken"
                                                         // section line when show_plane_line is off).
        if show_end_lines && tick > 1e-9 {
            lines.push(nan);
            lines.push([ex, ey, 0.0]);
            lines.push(tip);
        }

        if show_arrows {
            // Arrowhead at the end of a short shaft along the viewing
            // direction, apex landing `arrow_ext` out from the tick tip. The
            // shared `append_arrow` takes the apex and the tip→base direction.
            let apex = [tip[0] + vx * arrow_ext, tip[1] + vy * arrow_ext, 0.0];
            let mut g = DimGeom::new();
            append_arrow(
                &mut g,
                glam::Vec3::new(apex[0] as f32, apex[1] as f32, 0.0),
                glam::Vec3::new(-vx as f32, -vy as f32, 0.0),
                &arrow_kind,
            );
            // Shaft from the tick tip to the arrowhead base.
            let a_len = match arrow_kind {
                ArrowKind::Triangle { size, size_mul, .. } => (size * size_mul) as f64,
                _ => arrow_size,
            };
            let base = [apex[0] - vx * a_len, apex[1] - vy * a_len, 0.0];
            lines.push(nan);
            lines.push(tip);
            lines.push(base);
            let mut it = g.dim_lines.chunks_exact(2);
            while let Some([p0, p1]) = it.next() {
                lines.push(nan);
                lines.push([p0[0] as f64, p0[1] as f64, 0.0]);
                lines.push([p1[0] as f64, p1[1] as f64, 0.0]);
            }
            for t in g.arrow_fill.chunks_exact(3) {
                for p in t {
                    fill.push([p[0] as f64, p[1] as f64, 0.0]);
                }
            }
        }

        // Identifier glyph outboard of the tick tip, centred on the cut line.
        if !s.label.trim().is_empty() && label_h > 1e-6 {
            let approx_w = label_h * 0.7 * s.label.chars().count().max(1) as f64;
            // Gap past the tip along the outward direction — the style's
            // `identifier_offset` when decoded; for the far (negative-outward)
            // end drop a text height so the glyph clears the tick. Centre the
            // run on the cut line's X.
            let gap = style
                .map(|st| st.label_offset)
                .filter(|v| *v > 1e-9)
                .unwrap_or(label_h * 0.35);
            let drop = if osign < 0.0 { label_h } else { 0.0 };
            let base = [ex - approx_w * 0.5, tip[1] + oy * gap - drop];
            let (strokes, _) = crate::scene::text::lff::tessellate_text_ex(
                [0.0, 0.0],
                label_h as f32,
                0.0,
                1.0,
                0.0,
                "standard",
                &s.label,
            );
            let mut tp: Vec<[f64; 3]> = Vec::new();
            for stroke in &strokes {
                if stroke.len() < 2 {
                    continue;
                }
                if !tp.is_empty() {
                    tp.push(nan);
                }
                for &[gx, gy] in stroke {
                    tp.push([base[0] + gx as f64, base[1] + gy as f64, 0.0]);
                }
            }
            lines.push(nan);
            lines.extend(tp);
        }
    }

    let mut wires = Vec::new();
    // Lines (ticks + shafts + glyphs): a fill-free wire so the strokes render.
    let (lp, lp_low) = convert::tessellate::points_to_ds(lines);
    let mut lw = WireModel::solid(h.value().to_string(), lp, color, sel);
    lw.points_low = lp_low;
    lw.line_weight_px = lw_px;
    wires.push(lw);
    // Filled arrowheads: a separate wire carrying only triangles.
    if !fill.is_empty() {
        let (fp, fp_low) = convert::tessellate::points_to_ds(fill);
        let mut fw = WireModel::solid(h.value().to_string(), Vec::new(), color, sel);
        fw.fill_tris = fp;
        fw.fill_tris_low = fp_low;
        fw.line_weight_px = lw_px;
        wires.push(fw);
    }
    wires
}

// ── Parallel tessellation free function ──────────────────────────────────────
//
// Takes only the `Send + Sync` data needed for tessellation so that
// `wires_for_block` can dispatch work across rayon's thread pool without
// requiring `Scene` (which contains `Rc<RefCell<...>>` and is `!Send`) to
// cross thread boundaries.

/// Tessellate a synthesised dimension-text entity through `tessellate_entity`
/// so it picks up the standard text LOD ladder (baseline / greek / full),
/// then re-color the returned wires with the dimension's resolved text colour
/// (so DIMCLRT / DIMSTYLE colours win over the synthetic Text's defaults).
pub(crate) fn tessellate_entity_dim_text(
    document: &acadrust::CadDocument,
    selected: &HashSet<Handle>,
    active_viewport: Option<Handle>,
    bg_color: [f32; 4],
    anno_scale: f32,
    e: &EntityType,
    view_aabb: Option<[f32; 4]>,
    world_per_pixel: Option<f32>,
    text_color: [f32; 4],
) -> Vec<WireModel> {
    let mut wires = tessellate_entity(
        document,
        selected,
        active_viewport,
        bg_color,
        anno_scale,
        e,
        None,
        view_aabb,
        world_per_pixel,
        false,
    );
    for w in &mut wires {
        // Synth dim text carries no real entity colour — paint everything
        // (including greek-LOD fill tris which read `wire.color`) with the
        // dim's text colour. Selection highlight already baked in by
        // tessellate_entity, so leave that alone.
        if !w.selected {
            w.color = text_color;
        }
    }
    wires
}

pub(crate) fn tessellate_entity(
    document: &acadrust::CadDocument,
    selected: &HashSet<Handle>,
    active_viewport: Option<Handle>,
    bg_color: [f32; 4],
    anno_scale: f32,
    e: &EntityType,
    block_cache: Option<&cache::block_cache::BlockCache>,
    // World-space XY view AABB (post `world_offset` subtraction). When
    // `Some`, entities whose AABB doesn't intersect this rect are skipped.
    view_aabb: Option<[f32; 4]>,
    // World units per screen pixel for LOD culling. `None` = no LOD.
    world_per_pixel: Option<f32>,
    // True only when tessellating content shown inside a paper-space viewport.
    // Retained through recursive INSERT expansion; PSLTSCALE itself is applied
    // by the viewport's GPU uniform so it never changes resident wire content.
    paper_space: bool,
) -> Vec<WireModel> {
    let contextual = crate::scene::annotative::entity_for_active_context(document, e);
    let e = contextual.as_ref();
    let h = e.common().handle;
    let sel = selected.contains(&h);

    // Frustum + LOD cull for non-Insert, non-Viewport entities. Insert is
    // handled separately (its WCS bbox depends on the block defn AABB ×
    // Insert transform — done inside expand_insert). Viewports always emit
    // so the viewport frame stays visible regardless of zoom.
    let needs_cull = view_aabb.is_some() || world_per_pixel.is_some();
    if needs_cull {
        match e {
            EntityType::Viewport(_) | EntityType::Insert(_) => {}
            _ => {
                let ab = entity_aabb(e);
                if ab != WireModel::UNBOUNDED_AABB {
                    if let Some(view) = view_aabb {
                        if cache::block_cache::aabb_disjoint_xy(ab, view) {
                            return vec![];
                        }
                    }
                    if let Some(wpp) = world_per_pixel {
                        let w_px = (ab[2] - ab[0]).abs();
                        let h_px = (ab[3] - ab[1]).abs();
                        // Keep in sync with `cache::block_cache::MIN_PIXEL_SIZE`.
                        // Text entities render as SDF glyph quads (crisp at every
                        // zoom, no LOD), so they must reach the full path even at
                        // sub-5 px — never substitute the stub.
                        let is_text = matches!(
                            e,
                            EntityType::Text(_)
                                | EntityType::MText(_)
                                | EntityType::AttributeDefinition(_)
                                | EntityType::AttributeEntity(_)
                                | EntityType::Tolerance(_)
                        );
                        // Face3D is exempt from the sub-pixel stub: it is trivially
                        // cheap to tessellate (4 corners → 2 tris), so there is no
                        // cost to draw it full at any zoom, and the cube-stub
                        // otherwise pops/coarsens flat faces across the threshold.
                        let is_face3d = matches!(e, EntityType::Face3D(_));
                        let is_3d_entity = matches!(
                            e,
                            EntityType::Solid3D(_)
                                | EntityType::Mesh(_)
                                | EntityType::PolyfaceMesh(_)
                                | EntityType::PolygonMesh(_)
                                | EntityType::Body(_)
                                | EntityType::Region(_)
                                | EntityType::Surface(_)
                        );
                        if !is_text && !is_face3d && w_px.max(h_px) / wpp < 5.0 {
                            // Sub-pixel entity: emit a stub instead of
                            // nothing so it stays visible / selectable /
                            // hit-test'able at any zoom. 2-D entities
                            // get the cheap diagonal segment; 3-D
                            // entities get an AABB cube so their
                            // footprint doesn't drift when the camera
                            // crosses the LOD threshold. See #19.
                            let (entity_color, _, _, _, aci_idx) =
                                view::render::render_style_for(document, e);
                            let entity_color = view::render::adapt_to_bg(entity_color, bg_color);
                            let entity_color = fade_if_locked(document, e, entity_color, bg_color);
                            if is_3d_entity {
                                // `ab` is already in the local frame
                                // (entity_aabb subtracted world_offset
                                // XY). The bbox z fields are still in
                                // WCS, so subtract `world_offset[2]` to
                                // match — otherwise the stub sits at a
                                // different z than the full tessellation
                                // and the geometry visibly shifts when
                                // the camera crosses the LOD threshold.
                                let bbox = e.as_entity().bounding_box();
                                let oz = 0.0_f64;
                                let z_min = (bbox.min.z - oz) as f32;
                                let z_max = (bbox.max.z - oz) as f32;
                                return vec![lod_stub_wire_3d(
                                    h.value().to_string(),
                                    entity_color,
                                    sel,
                                    aci_idx,
                                    ab,
                                    z_min,
                                    z_max,
                                )];
                            }
                            return vec![lod_stub_wire(
                                h.value().to_string(),
                                entity_color,
                                sel,
                                aci_idx,
                                ab,
                                0.0,
                                0.0,
                            )];
                        }
                    }
                }
            }
        }
    }

    if let EntityType::Viewport(vp) = e {
        // The sheet viewport (overall/id=1) is never shown — it represents the
        // paper boundary, not a user-defined content window.
        if !Scene::is_content_viewport(vp) {
            return vec![];
        }
        let is_active = active_viewport == Some(h);
        let is_locked = vp.status.locked;
        let color = if sel {
            [1.0, 1.0, 1.0, 1.0]
        } else if is_active {
            [1.0, 0.90, 0.20, 1.0]
        } else if is_locked {
            [0.90, 0.55, 0.10, 1.0]
        } else {
            [0.0, 0.75, 0.75, 1.0]
        };
        let (pattern_length, pattern) = if is_active {
            (1.5_f32, [0.8, -0.4, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0_f32])
        } else {
            (0.0_f32, [0.0f32; 8])
        };
        let mut wires = convert::tessellate::tessellate(
            document,
            h,
            e,
            sel,
            color,
            pattern_length,
            pattern,
            1.5,
            1.0,
            world_per_pixel,
            bg_color,
            false,
        );
        let ab = entity_aabb(e);
        for w in &mut wires {
            set_wire_aabb(w, ab);
        }
        return wires;
    }

    let (entity_color, pattern_length, pattern, line_weight_px, aci) =
        view::render::render_style_for(document, e);
    let entity_color = view::render::adapt_to_bg(entity_color, bg_color);
    let entity_color = fade_if_locked(document, e, entity_color, bg_color);
    let lt_scale = document.header.linetype_scale as f32 * e.common().linetype_scale as f32;
    let lt_name = view::render::linetype_name_for(document, e);
    // Paper-space linetype scaling belongs to the viewport uniform. Keeping
    // resident geometry at its model-space scale prevents every MSPACE wheel
    // tick from rebuilding and uploading the viewport's complete wire set.
    let pslt_factor = 1.0_f32;
    // ── Proxy entity: draw its cached preview ───────────────────────────────
    //
    // An entity from an application we have no reader for (e.g. an Autodesk
    // Raster Design embedded raster image) arrives as `Unknown`. Its own data is
    // a private format we cannot decode — but it usually ships a proxy-graphics
    // blob, the vector preview its author cached for exactly this case. AutoCAD
    // draws that when the object enabler is missing; draw it too, so the entity
    // occupies its real place instead of silently disappearing.
    let proxy_blob: Option<std::borrow::Cow<'_, [u8]>> = match e {
        EntityType::Unknown(_) => e
            .common()
            .graphic_data
            .as_deref()
            .map(std::borrow::Cow::Borrowed),
        EntityType::Extended(entity) => match &entity.data {
            acadrust::entities::ExtendedEntityData::Proxy(proxy) => {
                Some(std::borrow::Cow::Owned(proxy.graphics.data()))
            }
            _ => None,
        },
        _ => None,
    };
    if let Some(blob) = proxy_blob {
            let dec = convert::proxy_graphics::decode(blob.as_ref());
            if !dec.polylines.is_empty() || !dec.texts.is_empty() {
                use crate::scene::convert::proxy_graphics::ProxyColor;
                use std::collections::BTreeMap;
                let nan = [f64::NAN; 3];
                // A specific ACI / RGB overrides the entity colour; ByLayer /
                // ByBlock inherit it.
                let resolve = |pc: ProxyColor| -> ([f32; 4], u8) {
                    match pc {
                        ProxyColor::Aci(a) => (
                            view::render::adapt_to_bg(
                                convert::tess_util::aci_to_rgba(&acadrust::types::Color::Index(a)),
                                bg_color,
                            ),
                            a,
                        ),
                        ProxyColor::Rgb(r, g, b) => (
                            view::render::adapt_to_bg(
                                [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0],
                                bg_color,
                            ),
                            0,
                        ),
                        ProxyColor::Inherit => (entity_color, aci),
                    }
                };
                let mut wires = Vec::new();
                // Lines / shells: group by (colour, lineweight), one wire each.
                let mut groups: BTreeMap<(ProxyColor, i16), Vec<[f64; 3]>> = BTreeMap::new();
                for poly in &dec.polylines {
                    let buf = groups.entry((poly.color, poly.lineweight)).or_default();
                    if !buf.is_empty() {
                        buf.push(nan);
                    }
                    buf.extend_from_slice(&poly.points);
                }
                for ((pcolor, plw), pts64) in groups {
                    let (col, w_aci) = resolve(pcolor);
                    let lw_px = if plw >= 0 {
                        view::render::lineweight_to_px(&acadrust::types::LineWeight::Value(plw))
                    } else {
                        line_weight_px
                    };
                    let (pts, pts_low) = convert::tessellate::points_to_ds(pts64);
                    let mut w = WireModel::solid(h.value().to_string(), pts, col, sel);
                    w.points_low = pts_low;
                    w.line_weight_px = lw_px;
                    w.aci = w_aci;
                    wires.push(w);
                }
                // Text labels: draw the glyph strokes (simplex.shx etc. are
                // single-stroke fonts, so the outline is the character).
                for t in &dec.texts {
                    let font = t
                        .font
                        .trim()
                        .trim_end_matches(".shx")
                        .trim_end_matches(".SHX");
                    let font = if font.is_empty() { "standard" } else { font };
                    let (strokes, _) = crate::scene::text::lff::tessellate_text_ex(
                        [0.0, 0.0],
                        t.height as f32,
                        t.rotation as f32,
                        1.0,
                        0.0,
                        font,
                        &t.text,
                    );
                    let mut pts64: Vec<[f64; 3]> = Vec::new();
                    for stroke in &strokes {
                        if stroke.len() < 2 {
                            continue;
                        }
                        if !pts64.is_empty() {
                            pts64.push(nan);
                        }
                        for &[x, y] in stroke {
                            pts64.push([
                                t.position[0] + x as f64,
                                t.position[1] + y as f64,
                                t.position[2],
                            ]);
                        }
                    }
                    if pts64.len() >= 2 {
                        let (col, w_aci) = resolve(t.color);
                        let (pts, pts_low) = convert::tessellate::points_to_ds(pts64);
                        let mut w = WireModel::solid(h.value().to_string(), pts, col, sel);
                        w.points_low = pts_low;
                        w.line_weight_px = line_weight_px;
                        w.aci = w_aci;
                        wires.push(w);
                    }
                }
                if !wires.is_empty() {
                    return wires;
                }
            }
    }

    // ── Section symbol (AcDbSectionSymbol): draw the "A-A" cut mark ──────────
    //
    // The DWG reader decodes the two cut-line endpoints, the signed end ticks
    // and the identifier. Synthesize the end ticks + arrowheads + label glyphs
    // so the mark is visible on the layout, the way AutoCAD draws it. The raw
    // record is still preserved for lossless write-back.
    if let EntityType::SectionSymbol(s) = e {
        return section_symbol_wires(
            document,
            s,
            document.section_view_style.as_ref(),
            h,
            entity_color,
            sel,
            line_weight_px,
        );
    }
    // Drawing-view borders are non-plotting aids: nothing is drawn normally,
    // but the view must still be click-selectable anywhere inside its
    // rectangle — emit an invisible pick-only wire carrying the border rect as
    // its interior pick surface. When selected, the rect outline is drawn so
    // the selection has visible feedback.
    if let EntityType::ViewBorder(b) = e {
        let (x0, y0) = (b.min[0], b.min[1]);
        let (x1, y1) = (b.max[0], b.max[1]);
        if !(x1 > x0 && y1 > y0) {
            return vec![];
        }
        let (pick_tris, pick_tris_low) =
            super::tessellate::points_to_ds(crate::entities::common::quad_pick_tris(&[
                [x0, y0, 0.0],
                [x1, y0, 0.0],
                [x1, y1, 0.0],
                [x0, y1, 0.0],
            ]));
        let points: Vec<[f32; 3]> = if sel {
            vec![
                [x0 as f32, y0 as f32, 0.0],
                [x1 as f32, y0 as f32, 0.0],
                [x1 as f32, y1 as f32, 0.0],
                [x0 as f32, y1 as f32, 0.0],
                [x0 as f32, y0 as f32, 0.0],
            ]
        } else {
            Vec::new()
        };
        return vec![WireModel {
            taper_widths: Vec::new(),
            world_width: 0.0,
            depth_override: None,
            fill_is_3d: false,
            pick_tris,
            pick_tris_low,
            dash_from_start: false,
            dash_align_end: None,
            text_verts: Vec::new(),
            name: h.value().to_string(),
            points,
            points_low: Vec::new(),
            color: WireModel::SELECTED,
            selected: sel,
            aci: 0,
            pattern_length: 0.0,
            pattern: [0.0; 8],
            line_weight_px: 1.0,
            snap_pts: vec![],
            tangent_geoms: vec![],
            key_vertices: vec![[x0, y0, 0.0], [x1, y1, 0.0]],
            aabb: [x0 as f32, y0 as f32, x1 as f32, y1 as f32],
            plinegen: true,
            fill_tris: vec![],
            fill_tris_low: Vec::new(),
        }];
    }

    // ── Dimension baked-block fast path ─────────────────────────────────────
    //
    // AutoCAD bakes each dimension's final geometry (extension lines, dim
    // line, arrows, text MText) into a per-instance block — usually
    // `*D<n>`, but custom names like `DIMBLOCK###-4NP` also occur. When the
    // block exists we render its contents through `tessellate_entity` so
    // sub-Text/MText get the standard baseline/greek/full LOD ladder, and
    // DIMTXT × DIMSCALE isn't re-applied on already-baked geometry.
    if let EntityType::Dimension(dim) = e {
        let block_name = &dim.base().block_name;
        if !block_name.trim().is_empty() {
            if let Some(br) = document
                .block_records
                .iter()
                .find(|br| br.name.eq_ignore_ascii_case(block_name))
            {
                if !br.entity_handles.is_empty() {
                    let mut wires: Vec<WireModel> = Vec::with_capacity(br.entity_handles.len());
                    // The Dimension's own layer style — layer-0 inheritance
                    // target for baked sub-entities on layer "0" (#221).
                    let dim_l0_color = view::render::adapt_to_bg(
                        view::render::layer_render_style(document, &e.common().layer).color,
                        bg_color,
                    );
                    let dim_l0_aci = document
                        .layers
                        .get(&e.common().layer)
                        .map(|l| match &l.color {
                            acadrust::types::Color::Index(i) => *i,
                            _ => 0,
                        })
                        .unwrap_or(0);
                    for &eh in &br.entity_handles {
                        let Some(sub) = document.get_entity(eh) else {
                            continue;
                        };
                        // A dimension's definition points are baked into the
                        // block as POINTs on the Defpoints layer. AutoCAD never
                        // draws them as PDMODE glyphs — they're grip markers, not
                        // geometry — so rendering them adds a stray tick at each
                        // measured point that makes the extension lines look like
                        // they run past the geometry. Skip them.
                        if matches!(sub, EntityType::Point(_)) {
                            continue;
                        }
                        // Sub-entities inside *D### / DIMBLOCK## blocks
                        // typically use ByBlock color/linetype/lineweight —
                        // they should inherit from the Dimension entity.
                        let has_book_color =
                            view::render::has_resolved_book_color(document, sub);
                        let sub_color_is_byblock = !has_book_color
                            && sub.common().color == acadrust::types::Color::ByBlock;
                        let sub_is_l0_bylayer =
                            !has_book_color
                            && view::render::is_effective_layer_zero(&sub.common().layer)
                            && sub.common().color == acadrust::types::Color::ByLayer;
                        let sub_wires = tessellate_entity(
                            document,
                            selected,
                            active_viewport,
                            bg_color,
                            // Block contents are baked at the final WCS size —
                            // don't let downstream paths re-apply anno_scale.
                            1.0,
                            sub,
                            block_cache,
                            view_aabb,
                            world_per_pixel,
                            paper_space,
                        );
                        for mut w in sub_wires {
                            w.name = h.value().to_string();
                            // Override ByBlock colour with the dim's resolved
                            // colour so text matches `DIMCLRT`-style behaviour
                            // (or layer colour) instead of the raw ByBlock
                            // fallback that render_style_for produces. A layer-0
                            // sub inherits the dim's layer colour instead.
                            if sub_color_is_byblock {
                                w.color = if sel {
                                    WireModel::SELECTED
                                } else {
                                    entity_color
                                };
                                w.aci = aci;
                            } else if sub_is_l0_bylayer && !sel {
                                w.color = dim_l0_color;
                                w.aci = dim_l0_aci;
                            }
                            wires.push(w);
                        }
                    }
                    if !wires.is_empty() {
                        let aabb = entity_aabb(e);
                        for w in &mut wires {
                            // Empty SDF-text cells keep their tight glyph-box
                            // AABB; only stroke/fill wires take the whole-block
                            // box as a broad-phase pick hint.
                            if !w.points.is_empty() || !w.fill_tris.is_empty() {
                                set_wire_aabb(w, aabb);
                            }
                        }
                        return wires;
                    }
                }
            }
        }
        // Fall through to the synthesis path below when no block is attached.
    }

    if let EntityType::Dimension(dim) = e {
        let aabb = entity_aabb(e);
        use crate::entities::dimension::DimensionTess;
        let mut wires = dim.tessellate(
            document,
            h,
            sel,
            entity_color,
            line_weight_px,
            anno_scale,
            selected,
            active_viewport,
            bg_color,
            view_aabb,
            world_per_pixel,
        );
        for w in &mut wires {
            w.aci = aci;
            // The whole-dimension box is a broad-phase hint for stroke/fill
            // wires (picked by proximity). An empty SDF-text wire instead
            // keeps its own tight glyph-box AABB so the text pick box hugs the
            // text — clicking empty space inside the dimension selects nothing,
            // only the lines or the text do.
            if !w.points.is_empty() || !w.fill_tris.is_empty() {
                set_wire_aabb(w, aabb);
            }
        }
        return wires;
    }

    if let EntityType::MultiLeader(ml) = e {
        let aabb = entity_aabb(e);
        use crate::entities::multileader::MultiLeaderTess;
        let mut wires = ml.tessellate(
            document,
            h,
            sel,
            entity_color,
            line_weight_px,
            anno_scale,
            world_per_pixel,
            bg_color,
        );
        for w in &mut wires {
            w.aci = aci;
            // As with dimensions: keep the whole-leader box only on stroke/fill
            // wires; empty SDF-text wires keep their tight glyph-box AABB.
            if !w.points.is_empty() || !w.fill_tris.is_empty() {
                set_wire_aabb(w, aabb);
            }
        }
        return wires;
    }

    // ── Table baked-block fast path ─────────────────────────────────────────
    //
    // AutoCAD bakes a Table's final rendered geometry (cell text, gridlines,
    // fill) into a per-instance block (usually `*T###`) referenced through
    // `table.block_record_handle`. The block's text uses the *displayed*
    // height; synthesising cells from `self.rows + TableStyle` instead would
    // re-apply the table's scale factor on top of already-baked geometry.
    // When the block exists we render it directly. Same pattern as
    // Dimension's `block_name`.
    if let EntityType::Table(tab) = e {
        if let Some(br_h) = tab.block_record_handle {
            if let Some(br) = document.block_records.iter().find(|br| br.handle == br_h) {
                if !br.entity_handles.is_empty() {
                    let mut wires: Vec<WireModel> = Vec::with_capacity(br.entity_handles.len());
                    // The Table's own layer style — layer-0 inheritance target
                    // for baked sub-entities on layer "0" (#221).
                    let tab_l0_color = view::render::adapt_to_bg(
                        view::render::layer_render_style(document, &e.common().layer).color,
                        bg_color,
                    );
                    let tab_l0_aci = document
                        .layers
                        .get(&e.common().layer)
                        .map(|l| match &l.color {
                            acadrust::types::Color::Index(i) => *i,
                            _ => 0,
                        })
                        .unwrap_or(0);
                    // The *T block is laid out in block-local space (origin at
                    // the table's top-left corner); the Table entity carries the
                    // world placement in `insertion_point` + `horizontal_direction`
                    // (like an INSERT). Place each baked sub-entity there before
                    // tessellating — without it the whole table renders at the
                    // origin. Rotating about the local origin then translating
                    // gives world = insertion + R·local; tessellate_entity then
                    // handles the UTM-scale relative-to-eye split as usual.
                    let ins = tab.insertion_point;
                    let angle = tab.horizontal_direction.y.atan2(tab.horizontal_direction.x);
                    for &eh in &br.entity_handles {
                        let Some(sub) = document.get_entity(eh) else {
                            continue;
                        };
                        let has_book_color =
                            view::render::has_resolved_book_color(document, sub);
                        let sub_color_is_byblock = !has_book_color
                            && sub.common().color == acadrust::types::Color::ByBlock;
                        let sub_is_l0_bylayer =
                            !has_book_color
                            && view::render::is_effective_layer_zero(&sub.common().layer)
                            && sub.common().color == acadrust::types::Color::ByLayer;
                        let mut placed = sub.clone();
                        {
                            let ent = placed.as_entity_mut();
                            if angle.abs() > 1e-9 {
                                ent.apply_rotation(
                                    acadrust::types::Vector3::new(0.0, 0.0, 1.0),
                                    angle,
                                );
                            }
                            ent.translate(ins);
                        }
                        let sub_wires = tessellate_entity(
                            document,
                            selected,
                            active_viewport,
                            bg_color,
                            anno_scale,
                            &placed,
                            block_cache,
                            view_aabb,
                            world_per_pixel,
                            paper_space,
                        );
                        for mut w in sub_wires {
                            w.name = h.value().to_string();
                            if sub_color_is_byblock {
                                w.color = if sel {
                                    WireModel::SELECTED
                                } else {
                                    entity_color
                                };
                                w.aci = aci;
                            } else if sub_is_l0_bylayer && !sel {
                                w.color = tab_l0_color;
                                w.aci = tab_l0_aci;
                            }
                            wires.push(w);
                        }
                    }
                    if !wires.is_empty() {
                        let aabb = entity_aabb(e);
                        for w in &mut wires {
                            // Empty SDF-text cells keep their tight glyph-box
                            // AABB; only stroke/fill wires take the whole-block
                            // box as a broad-phase pick hint.
                            if !w.points.is_empty() || !w.fill_tris.is_empty() {
                                set_wire_aabb(w, aabb);
                            }
                        }
                        return wires;
                    }
                }
            }
        }
        // No baked block (e.g. a table created in-app) — synthesise coloured
        // geometry from the rows + TableStyle so fills/colours/borders/margins
        // are honoured instead of the monochrome fallback.
        // Annotative tables scale with the current annotation scale (their
        // stored geometry is at paper size); non-annotative tables are already
        // model-size, so pass 1.0.
        let table_anno = if crate::scene::annotative::is_annotative(document, e) {
            anno_scale
        } else {
            1.0
        };
        let mut wires = crate::entities::table::tessellate_table(
            tab,
            document,
            sel,
            entity_color,
            line_weight_px,
            table_anno,
        );
        for insert in
            crate::entities::table::block_cell_inserts(tab, document, table_anno)
        {
            wires.extend(tessellate_entity(
                document,
                selected,
                active_viewport,
                bg_color,
                1.0,
                &EntityType::Insert(insert),
                block_cache,
                view_aabb,
                world_per_pixel,
                paper_space,
            ));
        }
        if !wires.is_empty() {
            let aabb = entity_aabb(e);
            for w in &mut wires {
                w.aci = aci;
                // Empty SDF-text cells keep their tight glyph-box AABB; only
                // stroke/fill wires take the whole-table box as a broad-phase
                // pick hint (matches the dim / mleader / baked-block paths).
                if !w.points.is_empty() || !w.fill_tris.is_empty() {
                    set_wire_aabb(w, aabb);
                }
            }
            return wires;
        }
    }

    if let EntityType::Insert(ins) = e {
        // Resolve the INSERT's own style so ByBlock sub-entities can inherit it.
        let (ins_color, ins_pat_len, ins_pat, ins_lw_px, _) =
            view::render::render_style_for(document, e);
        let ins_color = view::render::adapt_to_bg(ins_color, bg_color);
        // Resolve the INSERT's *layer* style — the layer-0 inheritance target
        // for sub-entities on layer "0" with ByLayer properties (#221).
        let ins_layer = {
            let mut s = view::render::layer_render_style(document, &ins.common.layer);
            s.color = view::render::adapt_to_bg(s.color, bg_color);
            s
        };
        let ip = glam::Vec3::new(
            (ins.insert_point.x) as f32,
            (ins.insert_point.y) as f32,
            (ins.insert_point.z) as f32,
        );
        let marker = WireModel {
            taper_widths: Vec::new(),
            world_width: 0.0,
            depth_override: None,
            fill_is_3d: false,
            pick_tris: Vec::new(),
            pick_tris_low: Vec::new(),
            dash_from_start: false,
            dash_align_end: None,
            text_verts: Vec::new(),
            name: h.value().to_string(),
            points: vec![],
            points_low: Vec::new(),
            color: entity_color,
            selected: sel,
            aci: 0,
            pattern_length: 0.0,
            pattern: [0.0; 8],
            line_weight_px: 1.0,
            snap_pts: vec![(ip.as_dvec3(), model::wire_model::SnapHint::Insertion)],
            tangent_geoms: vec![],
            key_vertices: vec![],
            aabb: WireModel::UNBOUNDED_AABB,
            plinegen: true,
            fill_tris: vec![],
            fill_tris_low: Vec::new(),
        };

        if let Some(cache) = block_cache {
            // Xrefs render with the same hue but faded toward `bg_color` so
            // the user can recognise external-reference geometry at a glance.
            let is_xref = document
                .block_records
                .get(&ins.block_name)
                .map(|br| br.flags.is_xref || br.flags.is_xref_overlay)
                .unwrap_or(false);
            if let Some(mut wires) = cache::block_cache::expand_insert(
                cache,
                ins,
                h,
                ins_color,
                ins_pat_len,
                ins_pat,
                ins_lw_px,
                ins_layer,
                sel,
                pslt_factor,
                view_aabb,
                world_per_pixel,
                is_xref,
                bg_color,
                anno_scale,
            ) {
                // XCLIP: if this INSERT carries an enabled spatial filter,
                // clip the expanded block geometry to the boundary polygon so
                // only the portion inside the clip is drawn.
                if let Some(sf) = pick::xclip::insert_spatial_filter(document, ins) {
                    let poly = pick::xclip::world_clip_polygon_f64(sf, ins);
                    pick::xclip::clip_wires(&mut wires, &poly);
                    // XCLIPFRAME 1/2 shows the boundary itself on screen
                    // (2 = shown but not plotted).
                    if document.header.xclip_frame != 0 && poly.len() >= 3 {
                        wires.push(pick::xclip::frame_wire(
                            &poly,
                            format!("{}_xclipframe", h.value()),
                            ins_color,
                            sel,
                            ins_lw_px,
                        ));
                    }
                }

                // Per-INSERT attribute values. The block defn carries the
                // AttributeDefinitions (templates) which expand_insert skips;
                // the AttributeEntity instances live on the Insert itself in
                // WCS and need their own tessellation so the user sees the
                // values they actually filled in. See #20.
                crate::entities::insert::append_insert_attribute_wires(
                    &mut wires,
                    document,
                    ins,
                    h,
                    sel,
                    ins_color,
                    ins_pat_len,
                    ins_pat,
                    ins_lw_px,
                    ins_layer,
                    bg_color,
                    is_xref,
                    pslt_factor,
                    anno_scale,
                );
                wires.push(marker);
                return wires;
            }
        }

        // Cache miss / unavailable: fall back to the original explode path.
        // The block_cache primary path covers all typical Inserts; this
        // branch only fires for pathological cache failures.
        let br = document.block_records.get(&ins.block_name);
        let is_xref = br
            .map(|br| br.flags.is_xref || br.flags.is_xref_overlay)
            .unwrap_or(false);
        let mut wires: Vec<WireModel> = ins
            .explode_from_document(document)
            .iter()
            .cloned()
            .map(crate::modules::draw::modify::explode::normalize_insert_entity)
            .flat_map(|sub| {
                let (sub_color, sub_pattern_length, sub_pattern, sub_line_weight_px, sub_aci) =
                    view::render::render_style_for_block_sub(
                        document,
                        &sub,
                        ins_color,
                        ins_pat_len,
                        ins_pat,
                        ins_lw_px,
                        ins_layer,
                    );
                let sub_color = view::render::adapt_to_bg(sub_color, bg_color);
                let sub_color = if is_xref && !sel {
                    cache::block_cache::fade_toward_bg(sub_color, bg_color)
                } else {
                    sub_color
                };
                let sub_aabb = entity_aabb(&sub);
                let sub_pattern_length = sub_pattern_length * pslt_factor;
                let sub_pattern = sub_pattern.map(|v| v * pslt_factor);
                let mut wires = convert::tessellate::tessellate(
                    document,
                    h,
                    &sub,
                    sel,
                    sub_color,
                    sub_pattern_length,
                    sub_pattern,
                    sub_line_weight_px,
                    anno_scale,
                    world_per_pixel,
                    bg_color,
                    false,
                );
                for w in &mut wires {
                    w.name = h.value().to_string();
                    w.aci = sub_aci;
                    // Keep the glyph-bounds AABB tessellate set on SDF text wires
                    // (their geometry is in `text_verts`, not `points`); clobbering
                    // it here left block text with an UNBOUNDED box. For every
                    // other wire use the sub-entity box, falling back to the wire's
                    // own world points when that box is degenerate/unimplemented
                    // (UNBOUNDED) — otherwise it never culls and stalls snapping on
                    // block-heavy drawings.
                    if w.text_verts.is_empty() {
                        let box_ = if sub_aabb == WireModel::UNBOUNDED_AABB {
                            wire_points_aabb(w)
                        } else {
                            sub_aabb
                        };
                        set_wire_aabb(w, box_);
                    }
                }
                wires
            })
            .collect();
        crate::entities::insert::append_insert_attribute_wires(
            &mut wires,
            document,
            ins,
            h,
            sel,
            ins_color,
            ins_pat_len,
            ins_pat,
            ins_lw_px,
            ins_layer,
            bg_color,
            is_xref,
            pslt_factor,
            anno_scale,
        );
        wires.push(marker);
        return wires;
    }

    let aabb = entity_aabb(e);

    // TEXT / MTEXT / ATTDEF / ATTRIB / Tolerance all render as SDF glyph quads
    // (crisp at every zoom), so there is no text LOD ladder — they fall through
    // to the full tessellation path below.

    let mut bases = convert::tessellate::tessellate(
        document,
        h,
        e,
        sel,
        entity_color,
        pattern_length,
        pattern,
        line_weight_px,
        anno_scale,
        world_per_pixel,
        bg_color,
        false,
    );
    for b in &mut bases {
        b.aci = aci;
        // SDF text wires carry a glyph-bounds AABB (the true text extent) set
        // in the Text arm; entity_aabb would mis-place the pick box for MTEXT,
        // so don't clobber it. Every other wire takes the entity AABB.
        if b.text_verts.is_empty() {
            set_wire_aabb(b, aabb);
        }
    }

    // Complex linetypes (with embedded shapes / text) expand the *base*
    // polyline along its tangent. Text-type entities never have a complex
    // linetype assigned, so we only consult the first wire here — multi-wire
    // returns come exclusively from MTEXT colour splits which can't trigger
    // this path.
    if let Some(clt) = crate::io::linetypes::resolve_complex_lt(document, lt_name) {
        if let Some(base) = bases.first() {
            // Walk the dash / glyph layout in a local frame so `apply_along`'s
            // f32 math stays precise, then lift each wire back to world DS.
            // `base.points` is the double-single HIGH half; at UTM coordinates
            // (−1.2M) that alone quantises to ~0.1, which jitters the complex
            // linetype's shapes and dashes. Reconstruct the absolute f64 path
            // (high + low) and re-origin it at the first vertex before walking.
            let abs = |i: usize| -> [f64; 3] {
                let p = base.points[i];
                let l = base.points_low.get(i).copied().unwrap_or([0.0; 3]);
                [
                    p[0] as f64 + l[0] as f64,
                    p[1] as f64 + l[1] as f64,
                    p[2] as f64 + l[2] as f64,
                ]
            };
            let origin = base
                .points
                .iter()
                .position(|p| !p[0].is_nan())
                .map(&abs)
                .unwrap_or([0.0; 3]);
            let local: Vec<[f32; 3]> = base
                .points
                .iter()
                .enumerate()
                .map(|(i, p)| {
                    if p[0].is_nan() {
                        [f32::NAN; 3]
                    } else {
                        let a = abs(i);
                        [
                            (a[0] - origin[0]) as f32,
                            (a[1] - origin[1]) as f32,
                            (a[2] - origin[2]) as f32,
                        ]
                    }
                })
                .collect();
            let mut wires = text::complex_lt::apply_along(
                &base.name,
                &local,
                &clt,
                (lt_scale * pslt_factor).max(1e-4),
                entity_color,
                sel,
                base.line_weight_px,
                // Single (non-MLINE) entity: keep the from-start tiling (no shared
                // A-type reference — there are no sibling elements to align with).
                None,
            );
            if !wires.is_empty() {
                for w in &mut wires {
                    convert::tessellate::shift_wire_to_world(w, origin);
                    set_wire_aabb(w, aabb);
                }
                return wires;
            }
        }
    }

    // DGN line-style: the linetype's real pattern lives in DGN line-style objects
    // (empty standard LTYPE), so `resolve_complex_lt` sees nothing. Render its
    // symbol blocks (e.g. a pipe's end circles) at the polyline endpoints. First
    // pass — exact dash pattern / placement need the undecoded leaf data.
    let dgn_syms = convert::dgn_linestyle::symbol_blocks(document, lt_name);
    if !dgn_syms.is_empty() {
        let verts = convert::dgn_linestyle::polyline_points(e);
        if verts.len() >= 2 {
            // The pipe body is drawn as two parallel walls, not a single centre
            // line: offset the host polyline by ±(symbol radius) so each wall
            // sits tangent to the end circles, and replace the centre line with
            // them. The radius is the rendered symbol extent (block / scale).
            let radius = dgn_syms
                .iter()
                .map(|s| convert::dgn_linestyle::symbol_radius(document, s.block, s.scale))
                .fold(0.0_f64, f64::max);
            if radius > 1e-6 {
                // The walls carry the line style's dash pattern. Its native
                // lengths scale to drawing units by f = radius / symbol-scale
                // (the same factor that turns the compound's native offset into
                // the measured wall offset). Sign-alternate: dash, gap, dash…
                let scale = dgn_syms
                    .iter()
                    .map(|s| s.scale)
                    .find(|s| *s > 1e-9)
                    .unwrap_or(1.0);
                let f = radius / scale;
                // The wall stroke's dash length (`wall_dashes[0]`) renders as an
                // equal dash/gap: dash-first `[+dash, -dash]`. Combined with the
                // `dash_from_start` flag set below, each wall tiles from its own
                // start vertex with a dash, no A-type end alignment.
                let native = convert::dgn_linestyle::wall_dashes(document, lt_name);
                let (wall_pat, wall_pat_len) = if f > 1e-9 && !native.is_empty() {
                    let dash = (native[0] * f) as f32;
                    if dash > 1e-6 {
                        let mut pat = [0.0_f32; 8];
                        pat[0] = dash;
                        pat[1] = -dash;
                        (pat, 2.0 * dash)
                    } else {
                        ([0.0_f32; 8], 0.0)
                    }
                } else {
                    ([0.0_f32; 8], 0.0)
                };
                let mut rails = Vec::new();
                for sgn in [1.0_f64, -1.0] {
                    if let Some(off) = convert::dgn_linestyle::offset_host_entity(e, sgn * radius) {
                        let mut w = convert::tessellate::tessellate(
                            document,
                            h,
                            &off,
                            sel,
                            entity_color,
                            pattern_length,
                            pattern,
                            line_weight_px,
                            anno_scale,
                            world_per_pixel,
                            bg_color,
                            false,
                        );
                        for x in &mut w {
                            x.aci = aci;
                            set_wire_aabb(x, aabb);
                            if wall_pat_len > 1e-6 {
                                x.pattern = wall_pat;
                                x.pattern_length = wall_pat_len;
                                // DGN: draw the dash from this wall's own start
                                // vertex, no A-type end forcing. Each wall is a
                                // separate wire, so the two tile independently.
                                x.dash_from_start = true;
                            }
                        }
                        rails.append(&mut w);
                    }
                }
                if !rails.is_empty() {
                    bases = rails;
                }
            }
            let last = *verts.last().unwrap();
            for (i, sym) in dgn_syms.iter().enumerate() {
                let at = if i == 0 { verts[0] } else { last };
                let mut wires = convert::dgn_linestyle::place_block_wires(
                    document,
                    sym.block,
                    sym.scale,
                    at,
                    entity_color,
                    line_weight_px,
                    anno_scale,
                    world_per_pixel,
                    bg_color,
                );
                // The symbol wires come back named after the anonymous block's
                // internal entity handle. Re-key them to the host entity (like the
                // walls above) so the whole DET pipe picks/selects as one entity
                // instead of the symbols acting as separate phantom entities.
                let host_name = h.value().to_string();
                for w in &mut wires {
                    w.name = host_name.clone();
                    w.aci = aci;
                    set_wire_aabb(w, aabb);
                    // A wipeout's frame sits ABOVE its own mask: the mask
                    // draws at the entity's rank, the frame half a rank
                    // closer, so the later mask pass fails the depth test at
                    // the frame's pixels instead of erasing it.
                    if matches!(e, EntityType::Wipeout(_)) {
                        w.depth_override = Some(0.5);
                    }
                }
                bases.extend(wires);
            }
        }
    }

    bases
}

/// Build the 4 OBB corners (CCW: bl, br, tr, tl) of a Text / MText entity
/// in its **native frame** — for top-level entities this is world coords,
/// for block-defn subs it's block-local. No offset/transform applied.
/// Width is approximated from glyph height × character count (TEXT) or
/// from `rectangle_width` (MTEXT). Returns `None` for non-text entities.
///
/// `mtext_lines_override` lets the caller plug in a wrap-aware line count
/// (from `text_support::mtext_line_count`). Without it, MText's OBB
/// height collapses to a single line when the file omits `rectangle_height`,
/// which makes downstream per-line LOD math degenerate.

/// Build a "low-LOD stub" wire for an entity that would otherwise be culled
/// to nothing — the entity's AABB diagonal as a 2-point segment, plus the
/// AABB itself so window / crossing selection picks the entity up. The
/// stored `selected` flag tracks across zoom levels so highlight visuals
/// don't disappear when the LOD level changes. See #19.
fn lod_stub_wire(
    name: String,
    color: [f32; 4],
    selected: bool,
    aci: u8,
    aabb: [f32; 4],
    z_min: f32,
    z_max: f32,
) -> WireModel {
    let [ax, ay, bx, by] = aabb;
    let cx = (ax + bx) * 0.5;
    let cy = (ay + by) * 0.5;
    let cz = (z_min + z_max) * 0.5;
    // Mirror what tessellate.rs does for the non-stub paths: bake the
    // selection-highlight colour into the wire so a re-tessellate triggered
    // by a zoom-induced LOD change keeps the entity highlighted. Without
    // this swap the wire's `selected` flag is true but its colour stays at
    // the entity's own hue, so the user sees the highlight vanish at the
    // LOD boundary. #19.
    let stored_color = if selected { WireModel::SELECTED } else { color };
    WireModel {
        taper_widths: Vec::new(),
        world_width: 0.0,
        depth_override: None,
        fill_is_3d: false,
        pick_tris: Vec::new(),
        pick_tris_low: Vec::new(),
        dash_from_start: false,
        dash_align_end: None,
        text_verts: Vec::new(),
        name,
        // Diagonal of the entity's 3D AABB so depth tests against
        // shaded / hidden-line geometry are correct — the stub doesn't
        // flatten to z=0 and pop in front of objects that sit at a
        // different elevation. 2D entities (text fallbacks) pass
        // z_min = z_max = 0 to keep the historical behaviour.
        points: vec![[ax, ay, z_min], [bx, by, z_max]],
        points_low: Vec::new(),
        color: stored_color,
        selected,
        aci,
        pattern_length: 0.0,
        pattern: [0.0; 8],
        line_weight_px: 1.0,
        snap_pts: vec![],
        tangent_geoms: vec![],
        key_vertices: vec![[cx as f64, cy as f64, cz as f64]],
        aabb,
        plinegen: true,
        fill_tris: vec![],
        fill_tris_low: Vec::new(),
    }
}

/// Sub-pixel LOD stub for 3D entities. Emits the entity's 3D AABB as a
/// 12-edge cube so the geometry occupies the same screen footprint and
/// depth range as the full tessellation, just with a tiny constant cost
/// (12 line segments). Without this, the diagonal stub used by
/// `lod_stub_wire` cuts off at two opposite bbox corners and drifts
/// visibly when the camera crosses the LOD threshold.
fn lod_stub_wire_3d(
    name: String,
    color: [f32; 4],
    selected: bool,
    aci: u8,
    aabb: [f32; 4],
    z_min: f32,
    z_max: f32,
) -> WireModel {
    let [x0, y0, x1, y1] = aabb;
    let (z0, z1) = if z_min <= z_max {
        (z_min, z_max)
    } else {
        (z_max, z_min)
    };
    let p = [
        [x0, y0, z0],
        [x1, y0, z0],
        [x1, y1, z0],
        [x0, y1, z0],
        [x0, y0, z1],
        [x1, y0, z1],
        [x1, y1, z1],
        [x0, y1, z1],
    ];
    // 12 edges = 4 bottom-face + 4 top-face + 4 vertical connectors.
    const EDGES: [(usize, usize); 12] = [
        (0, 1),
        (1, 2),
        (2, 3),
        (3, 0),
        (4, 5),
        (5, 6),
        (6, 7),
        (7, 4),
        (0, 4),
        (1, 5),
        (2, 6),
        (3, 7),
    ];
    let mut points: Vec<[f32; 3]> = Vec::with_capacity(EDGES.len() * 3);
    for (a, b) in EDGES {
        if !points.is_empty() {
            points.push([f32::NAN; 3]);
        }
        points.push(p[a]);
        points.push(p[b]);
    }
    let stored_color = if selected { WireModel::SELECTED } else { color };
    WireModel {
        taper_widths: Vec::new(),
        world_width: 0.0,
        depth_override: None,
        fill_is_3d: false,
        pick_tris: Vec::new(),
        pick_tris_low: Vec::new(),
        dash_from_start: false,
        dash_align_end: None,
        text_verts: Vec::new(),
        name,
        points,
        points_low: Vec::new(),
        color: stored_color,
        selected,
        aci,
        pattern_length: 0.0,
        pattern: [0.0; 8],
        line_weight_px: 1.0,
        snap_pts: vec![],
        tangent_geoms: vec![],
        // No `key_vertices` — Face3DGpu requires 4 corners to emit a
        // fill quad, and we don't want this stub painted as a solid
        // face. The wire pass still draws its 12 edges.
        key_vertices: vec![],
        aabb,
        plinegen: true,
        fill_tris: vec![],
        fill_tris_low: Vec::new(),
    }
}

/// Tessellate each visible AttributeEntity attached to an Insert and append
/// the resulting wires. AttributeEntity positions are already in WCS — the
/// INSERT only stamps the geometry once, attribute text sits at the world
/// position recorded on each ATTRIB. See #20.
#[allow(clippy::too_many_arguments)]
/// World-space XY AABB of a wire computed from its own points (double-single
/// `points + points_low` sum = absolute world, matching `wire_in_range`'s
/// `cursor_world`). Used as a fallback when an entity's `bounding_box()` is
/// degenerate/unimplemented (`entity_aabb` → `UNBOUNDED`), so the wire still
/// gets a real, cullable box instead of never being pre-rejected during snap.
pub(crate) fn wire_points_aabb(w: &WireModel) -> [f32; 4] {
    let mut min = [f32::INFINITY; 2];
    let mut max = [f32::NEG_INFINITY; 2];
    let mut any = false;
    for (i, p) in w.points.iter().enumerate() {
        let lo = w.points_low.get(i).copied().unwrap_or([0.0; 3]);
        let (x, y) = (p[0] + lo[0], p[1] + lo[1]);
        if x.is_finite() && y.is_finite() {
            min[0] = min[0].min(x);
            min[1] = min[1].min(y);
            max[0] = max[0].max(x);
            max[1] = max[1].max(y);
            any = true;
        }
    }
    if any {
        [min[0], min[1], max[0], max[1]]
    } else {
        WireModel::UNBOUNDED_AABB
    }
}

/// Assign `entity_box` as `w`'s cullable box, widened to cover the wire's
/// actual tessellated geometry and pick-only geometry.
///
/// `entity_aabb`'s box comes from acadrust's `bounding_box()`, which for a
/// polyline is often the box of its stored vertices — it may know nothing about
/// bulge arcs between them, the band a width paints around them, or the wall a
/// thickness extrudes. Hit-testing rejects on this box before it looks at the
/// wire segments or `pick_tris`, so a box that stops short of the drawn geometry
/// makes that geometry silently unpickable.
pub(crate) fn set_wire_aabb(w: &mut WireModel, entity_box: [f32; 4]) {
    let mut out = if entity_box == WireModel::UNBOUNDED_AABB {
        wire_points_aabb(w)
    } else {
        entity_box
    };

    let mut extend = |p: [f32; 3], lo: [f32; 3]| {
        let (x, y) = (p[0] + lo[0], p[1] + lo[1]);
        if x.is_finite() && y.is_finite() {
            if out == WireModel::UNBOUNDED_AABB {
                out = [x, y, x, y];
            } else {
                out[0] = out[0].min(x);
                out[1] = out[1].min(y);
                out[2] = out[2].max(x);
                out[3] = out[3].max(y);
            }
        }
    };

    for (i, &p) in w.points.iter().enumerate() {
        extend(p, w.points_low.get(i).copied().unwrap_or([0.0; 3]));
    }
    for (i, &p) in w.pick_tris.iter().enumerate() {
        extend(p, w.pick_tris_low.get(i).copied().unwrap_or([0.0; 3]));
    }

    w.aabb = out;
}

pub(crate) fn entity_aabb(e: &acadrust::EntityType) -> [f32; 4] {
    let bbox = e.as_entity().bounding_box();
    let min_x = (bbox.min.x) as f32;
    let min_y = (bbox.min.y) as f32;
    let max_x = (bbox.max.x) as f32;
    let max_y = (bbox.max.y) as f32;
    // The all-zero box is bounding_box()'s Default — returned by entities with
    // no usable box (unimplemented) — so treat it as UNBOUNDED (never
    // pre-rejected). A genuinely zero-size box *away* from the origin (e.g. a
    // POINT) is a valid, cullable position: keep it, otherwise point-heavy
    // drawings fill the always-checked set and stall hit-testing.
    if min_x == 0.0 && min_y == 0.0 && max_x == 0.0 && max_y == 0.0 {
        return WireModel::UNBOUNDED_AABB;
    }
    [min_x, min_y, max_x, max_y]
}

/// AABB of `e` in WCS f64 (no world_offset subtraction). `None` for
/// entities whose `bounding_box()` returned the degenerate default
/// (which `entity_aabb` collapses to `UNBOUNDED_AABB`). Quadtree
/// indexing uses this so changing `world_offset` doesn't invalidate
/// the index.
pub(crate) fn entity_world_aabb_f64(e: &acadrust::EntityType) -> Option<[f64; 4]> {
    let bbox = e.as_entity().bounding_box();
    let (xmin, ymin, xmax, ymax) = (bbox.min.x, bbox.min.y, bbox.max.x, bbox.max.y);
    if xmin == xmax && ymin == ymax {
        return None;
    }
    if !xmin.is_finite() || !ymin.is_finite() || !xmax.is_finite() || !ymax.is_finite() {
        return None;
    }
    Some([xmin, ymin, xmax, ymax])
}

/// True if `e` is a type the quadtree should skip. `Insert` and
/// `Viewport` are sized only after extra transformation; tessellation
/// already handles them via dedicated code paths. `Block`/`BlockEnd`
/// are block-defn sentinels with no geometry.
pub(crate) fn is_unindexable_entity(e: &acadrust::EntityType) -> bool {
    use acadrust::EntityType as E;
    matches!(
        e,
        E::Insert(_) | E::Viewport(_) | E::Block(_) | E::BlockEnd(_)
    )
}
