// 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 { 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(§_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(§_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 { 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>` 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, active_viewport: Option, bg_color: [f32; 4], anno_scale: f32, e: &EntityType, view_aabb: Option<[f32; 4]>, world_per_pixel: Option, text_color: [f32; 4], ) -> Vec { let mut wires = tessellate_entity( document, selected, active_viewport, bg_color, anno_scale, None, 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, active_viewport: Option, bg_color: [f32; 4], anno_scale: f32, annotation_scale_handle: Option, 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, // 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 { let contextual = crate::scene::annotative::entity_for_annotation_context( document, e, annotation_scale_handle, ); let e = contextual.as_ref(); let h = e.common().handle; let sel = selected.contains(&h); // Per-object annotation contexts store each representation relative to the // native/default scale. Resolve that ratio once before TEXT/MTEXT, // DIMENSION, and MULTILEADER reach their independent tessellation paths. let anno_scale = if matches!( e, EntityType::Text(_) | EntityType::MText(_) | EntityType::Dimension(_) | EntityType::MultiLeader(_) ) { crate::scene::annotative::effective_annotation_scale_for( document, e, anno_scale, annotation_scale_handle, ) } else { anno_scale }; // 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_viewport(document, e, active_viewport); 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, annotation_scale_handle, 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_viewport(document, e, active_viewport); 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_viewport(document, e, active_viewport); // 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. Draw it // when the object enabler is missing, so the entity // occupies its real place instead of silently disappearing. let proxy_blob: Option> = 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 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, fill_is_2d_solid: false, render_instance: None, 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 ───────────────────────────────────── // // A DIMENSION carries the block "that contains the entities that make up // the dimension picture" (DXF group 2), and that block IS the picture: // AutoCAD requires it and draws it, BricsCAD draws it when present and only // falls back to rendering from the dimension variables when it is missing. // OCS re-derived the picture from DIMVARS every time instead, which means a // drawing whose style disagrees with what it actually drew comes out wrong // — a DIMTXT stored in different units from the DIMSCALE applied to it, or // a per-object override that is already in drawing units, and the text and // extension lines land hundreds of times too large. // // Drawing the block puts OCS on the same footing as the CAD that wrote the // file: it shows what the file says it looks like. Re-deriving stays as the // fallback, for a dimension with no block (one OCS just created, or one // whose block was dropped because it was edited). // // Annotative dimensions keep the old path: their several representations // are separate blocks, and choosing between them is what the annotation // machinery already does. The doctrine above assumes one picture. if let EntityType::Dimension(dim) = e { let baked = Some(dim.base().block_name.trim()) .filter(|name| !name.is_empty()) .filter(|_| !crate::scene::annotative::is_annotative(document, e)) .and_then(|name| { document .block_records .iter() .find(|record| record.name.eq_ignore_ascii_case(name)) }) .filter(|record| !record.entity_handles.is_empty()); if let Some(record) = baked { // The block's contents are already in world space and its base // point is the origin, so the instance that carries them is the // identity — it exists only to reuse the graph's ByBlock / layer-0 // colour resolution and its handling of anything nested inside. let mut insert = acadrust::entities::Insert::new( record.name.clone(), acadrust::types::Vector3::ZERO, ); insert.common = dim.base().common.clone(); let depths = rustc_hash::FxHashMap::default(); let graph = crate::scene::render_graph::RenderSceneGraph::new( document, None, annotation_scale_handle, true, &depths, ); let mut wires = Vec::new(); graph.walk_insert( &insert, h, |_, _| true, |sub, context| { let has_book_color = view::render::has_resolved_book_color(document, sub); let color_byblock = !has_book_color && sub.common().color == acadrust::types::Color::ByBlock; let color_layer0 = !has_book_color && view::render::is_effective_layer_zero(&sub.common().layer) && sub.common().color == acadrust::types::Color::ByLayer; let style = context.style_for(document, sub); let mut placed = sub.clone(); placed.apply_transform(&context.transform); let sub_wires = tessellate_entity( document, selected, active_viewport, bg_color, anno_scale, annotation_scale_handle, &placed, block_cache, view_aabb, world_per_pixel, paper_space, ); for mut wire in sub_wires { // Every wire answers to the dimension, so a pick selects // the dimension rather than one of its pieces. wire.name = h.value().to_string(); if sel { wire.selected = true; wire.color = WireModel::SELECTED; } else if color_byblock || color_layer0 { wire.color = view::render::adapt_to_bg(style.0, bg_color); wire.aci = style.4; } wires.push(wire); } }, ); if !wires.is_empty() { let aabb = entity_aabb(e); for wire in &mut wires { // As below: only stroke / fill wires take the whole-dimension // box. An SDF-text wire keeps its own tight glyph box so the // text pick area hugs the text. if !wire.points.is_empty() || !wire.fill_tris.is_empty() { set_wire_aabb(wire, aabb); } } return wires; } } } 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 ───────────────────────────────────────── // // A table may store 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(table) = e { if let Some(record) = table.block_record_handle.and_then(|handle| { document .block_records .iter() .find(|record| record.handle == handle) }) { if !record.entity_handles.is_empty() { let mut insert = acadrust::entities::Insert::new( record.name.clone(), table.insertion_point, ); insert.rotation = table .horizontal_direction .y .atan2(table.horizontal_direction.x); insert.common = table.common.clone(); let depths = rustc_hash::FxHashMap::default(); let graph = crate::scene::render_graph::RenderSceneGraph::new( document, None, annotation_scale_handle, true, &depths, ); let mut wires = Vec::new(); graph.walk_insert( &insert, h, |_, _| true, |sub, context| { let has_book_color = view::render::has_resolved_book_color(document, sub); let color_byblock = !has_book_color && sub.common().color == acadrust::types::Color::ByBlock; let color_layer0 = !has_book_color && view::render::is_effective_layer_zero( &sub.common().layer, ) && sub.common().color == acadrust::types::Color::ByLayer; let style = context.style_for(document, sub); let mut placed = sub.clone(); placed.apply_transform(&context.transform); let sub_wires = tessellate_entity( document, selected, active_viewport, bg_color, anno_scale, annotation_scale_handle, &placed, block_cache, view_aabb, world_per_pixel, paper_space, ); for mut wire in sub_wires { wire.name = h.value().to_string(); if color_byblock { wire.color = if sel { WireModel::SELECTED } else { view::render::adapt_to_bg(style.0, bg_color) }; wire.aci = style.4; } else if color_layer0 && !sel { wire.color = view::render::adapt_to_bg(style.0, bg_color); wire.aci = style.4; } wires.push(wire); } }, ); if !wires.is_empty() { let aabb = entity_aabb(e); for wire in &mut wires { if !wire.points.is_empty() || !wire.fill_tris.is_empty() { set_wire_aabb(wire, aabb); } } return wires; } } } let table_anno = 1.0; let mut wires = crate::entities::table::tessellate_table( table, document, sel, entity_color, line_weight_px, table_anno, ); for insert in crate::entities::table::block_cell_inserts(table, document, table_anno) { wires.extend(tessellate_entity( document, selected, active_viewport, bg_color, 1.0, None, &EntityType::Insert(insert), block_cache, view_aabb, world_per_pixel, paper_space, )); } if !wires.is_empty() { let aabb = entity_aabb(e); for wire in &mut wires { wire.aci = aci; if !wire.points.is_empty() || !wire.fill_tris.is_empty() { set_wire_aabb(wire, 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, ins_aci) = view::render::render_style_for_viewport(document, e, active_viewport); 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_viewport( document, &ins.common.layer, active_viewport, ); s.color = view::render::adapt_to_bg(s.color, bg_color); s }; let ins_layer_aci = document .layers .get(&ins.common.layer) .and_then(|layer| match &layer.color { acadrust::types::Color::Index(index) => Some(*index), _ => None, }) .unwrap_or(0); 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, fill_is_2d_solid: false, render_instance: None, 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(), }; let fallback_depths = rustc_hash::FxHashMap::default(); let fallback_cache; let cache = match block_cache { Some(cache) if cache.defn(&ins.block_name).is_some() => cache, _ => { fallback_cache = cache::block_cache::BlockCache::build_for_block( document, &ins.block_name, anno_scale, annotation_scale_handle, true, bg_color, active_viewport, &fallback_depths, ); &fallback_cache } }; let is_xref = document .block_records .get(&ins.block_name) .map(|record| record.flags.is_xref || record.flags.is_xref_overlay) .unwrap_or(false); let mut wires = cache::block_cache::expand_insert( cache, ins, h, ins_color, ins_aci, ins_pat_len, ins_pat, ins_lw_px, ins_layer, ins_layer_aci, sel, pslt_factor, view_aabb, world_per_pixel, is_xref, bg_color, anno_scale, ) .unwrap_or_default(); if let Some(filter) = pick::xclip::insert_spatial_filter(document, ins) { let transform = crate::scene::render_graph::insert_transform(document, ins); let polygon = pick::xclip::world_clip_polygon_for_transform(filter, &transform); pick::xclip::clip_wires(&mut wires, &polygon); for wire in &mut wires { if let Some(mut instance) = wire.render_instance { instance.source_id = cache.clip_source_id( instance.source_id, &polygon, instance.translation, ); wire.render_instance = Some(instance); } } if document.header.xclip_frame != 0 && polygon.len() >= 3 { wires.push(pick::xclip::frame_wire( &polygon, format!("{}_xclipframe", h.value()), ins_color, sel, ins_lw_px, )); } } 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, annotation_scale_handle, 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 and // apply the typed DGN stroke pattern to its parallel walls. 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 { let display_scale = lt_scale.max(1.0e-4) as f64; // 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 after applying the // entity/global linetype display scale. let radius = dgn_syms .iter() .map(|s| { convert::dgn_linestyle::symbol_radius( document, s.block, s.scale / display_scale, ) }) .fold(0.0_f64, f64::max); if radius > 1e-6 { // Preserve the typed stroke's signed dash/gap sequence and scale // every element by the linetype display scale. Combined with the // `dash_from_start` flag below, each wall tiles from its own start // vertex with no A-type end alignment. let native = convert::dgn_linestyle::wall_dashes(document, lt_name); let (wall_pat, wall_pat_len) = if !native.is_empty() { let mut pat = [0.0_f32; 8]; let mut length = 0.0_f32; for (slot, value) in pat.iter_mut().zip(native.iter().take(8)) { let scaled = (*value * display_scale) as f32; if scaled.is_finite() && scaled.abs() > 1.0e-6 { *slot = scaled; length += scaled.abs(); } } (pat, length) } 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, annotation_scale_handle, 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 / display_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, fill_is_2d_solid: false, render_instance: None, 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, fill_is_2d_solid: false, render_instance: None, 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(_) ) }