diff --git a/src/entities/common.rs b/src/entities/common.rs index 6e01a7267..49a8f91b6 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -167,3 +167,84 @@ pub fn ro_prop(label: &'static str, field: &'static str, value: impl Into Option { value.trim().parse::().ok() } +/// Compute the filled boundary polygon for one polyline segment. +/// For straight segments: a rectangle/trapezoid. +/// For arc segments: an arc band (outer arc + reversed inner arc). +/// Returns `None` if the segment is degenerate. +pub(crate) fn polyline_segment_fill( + p0: [f32; 2], + p1: [f32; 2], + hw0: f32, + hw1: f32, + bulge: f32, +) -> Option> { + if bulge.abs() < 1e-9 { + // Straight segment — rectangle or trapezoid + let dx = p1[0] - p0[0]; + let dy = p1[1] - p0[1]; + let len = (dx * dx + dy * dy).sqrt(); + if len < 1e-9 { + return None; + } + let nx = -dy / len; + let ny = dx / len; + Some(vec![ + [p0[0] + hw0 * nx, p0[1] + hw0 * ny], + [p1[0] + hw1 * nx, p1[1] + hw1 * ny], + [p1[0] - hw1 * nx, p1[1] - hw1 * ny], + [p0[0] - hw0 * nx, p0[1] - hw0 * ny], + ]) + } else { + // Arc segment — arc band polygon. + // Center math matches `bulge_to_arc` in modules/home/modify/explode.rs: + // r = chord * (1 + b²) / (4·|b|) + // d = r * (1 - b²) / (1 + b²) (signed: negative ⇒ major arc, center + // flips to the opposite side of chord) + // center = midpoint + sign(b) · d · left_perp(chord) + let b = bulge as f64; + let b2 = b * b; + let dx = (p1[0] - p0[0]) as f64; + let dy = (p1[1] - p0[1]) as f64; + let chord_len = (dx * dx + dy * dy).sqrt(); + if chord_len < 1e-9 || b.abs() < 1e-12 { + return None; + } + let r = chord_len * (1.0 + b2) / (4.0 * b.abs()); + let d_perp = r * (1.0 - b2) / (1.0 + b2); + let mx = ((p0[0] + p1[0]) * 0.5) as f64; + let my = ((p0[1] + p1[1]) * 0.5) as f64; + let perp_x = -dy / chord_len; + let perp_y = dx / chord_len; + let sign = b.signum(); + let cx = (mx + sign * d_perp * perp_x) as f32; + let cy = (my + sign * d_perp * perp_y) as f32; + let a0 = ((p0[1] - cy) as f32).atan2((p0[0] - cx) as f32); + let a1 = ((p1[1] - cy) as f32).atan2((p1[0] - cx) as f32); + let (sa, mut ea) = if bulge > 0.0 { (a0, a1) } else { (a1, a0) }; + if ea < sa { + ea += std::f32::consts::TAU; + } + let span = ea - sa; + let segs = ((span.abs() / std::f32::consts::TAU) * 24.0) + .ceil() + .max(4.0) as u32; + let r = r as f32; + let r_outer = |t: f32| r + (hw0 + (hw1 - hw0) * t); + let r_inner = |t: f32| (r - (hw0 + (hw1 - hw0) * t)).max(0.0); + let mut boundary = Vec::with_capacity((segs as usize + 1) * 2); + let inv = 1.0 / segs as f32; + for j in 0..=segs { + let t = j as f32 * inv; + let ang = sa + span * t; + let ro = r_outer(t); + boundary.push([cx + ro * ang.cos(), cy + ro * ang.sin()]); + } + for j in (0..=segs).rev() { + let t = j as f32 * inv; + let ang = sa + span * t; + let ri = r_inner(t); + boundary.push([cx + ri * ang.cos(), cy + ri * ang.sin()]); + } + Some(boundary) + } +} diff --git a/src/entities/insert.rs b/src/entities/insert.rs index 5ca7ac6db..96ea1a720 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -1,11 +1,14 @@ use acadrust::entities::Insert; use acadrust::types::{Transform, Vector3}; +use acadrust::{EntityType, Handle}; use glam::Vec3; use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable}; use crate::scene::object::{GripApply, GripDef, PropSection}; +use crate::scene::{block_cache, render, tessellate}; +use crate::scene::wire_model::WireModel; fn grips(ins: &Insert) -> Vec { let p = Vec3::new( @@ -142,3 +145,75 @@ impl crate::entities::traits::FallbackTess for Insert { ) } } +pub(crate) fn append_insert_attribute_wires( + wires: &mut Vec, + document: &acadrust::CadDocument, + ins: &acadrust::entities::Insert, + insert_handle: Handle, + sel: bool, + ins_color: [f32; 4], + ins_pat_len: f32, + ins_pat: [f32; 8], + ins_lw_px: f32, + bg_color: [f32; 4], + is_xref: bool, + pslt_factor: f32, + anno_scale: f32, + world_offset: [f64; 3], +) { + if ins.attributes.is_empty() { + return; + } + // ATTMODE (header.attribute_visibility): + // 0 = Off — every attribute hidden + // 1 = Normal — honour per-attribute `invisible` flag (default) + // 2 = On — every attribute forced visible, ignoring its flag + let attmode = document.header.attribute_visibility; + if attmode == 0 { + return; + } + for attr in &ins.attributes { + let per_attr_hidden = attr.common.invisible || attr.flags.invisible; + if attmode == 1 && per_attr_hidden { + continue; + } + let attr_entity = EntityType::AttributeEntity(attr.clone()); + let (sub_color, sub_plen, sub_pat, sub_lw_px, sub_aci) = + render::render_style_for_block_sub( + document, + &attr_entity, + ins_color, + ins_pat_len, + ins_pat, + ins_lw_px, + ); + let sub_color = render::adapt_to_bg(sub_color, bg_color); + let sub_color = if is_xref && !sel { + block_cache::fade_toward_bg(sub_color, bg_color) + } else { + sub_color + }; + let sub_aabb = crate::scene::entity_aabb(&attr_entity, world_offset); + let mut attr_wires = tessellate::tessellate( + document, + insert_handle, + &attr_entity, + sel, + sub_color, + sub_plen * pslt_factor, + sub_pat.map(|v| v * pslt_factor), + sub_lw_px, + world_offset, + anno_scale, + ); + // Use the INSERT's handle so selection / picking groups attribute + // text with the parent insert instead of treating it as a stray text. + for w in &mut attr_wires { + w.name = insert_handle.value().to_string(); + w.aci = sub_aci; + w.aabb = sub_aabb; + } + wires.extend(attr_wires); + } +} + diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index 3a9fbf476..af1bbc5bf 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -516,3 +516,36 @@ impl Transformable for LwPolyline { apply_transform(self, t); } } +pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> Vec> { + let hw_const = (pl.constant_width / 2.0) as f32; + let verts = &pl.vertices; + let n = verts.len(); + if n < 2 { + return vec![]; + } + let seg_count = if pl.is_closed { n } else { n - 1 }; + let mut out = Vec::new(); + for i in 0..seg_count { + let v0 = &verts[i]; + let v1 = &verts[(i + 1) % n]; + let hw0 = if v0.start_width > 1e-9 { + v0.start_width as f32 / 2.0 + } else { + hw_const + }; + let hw1 = if v0.end_width > 1e-9 { + v0.end_width as f32 / 2.0 + } else { + hw_const + }; + if hw0 < 1e-6 && hw1 < 1e-6 { + continue; + } + let p0 = [v0.location.x as f32, v0.location.y as f32]; + let p1 = [v1.location.x as f32, v1.location.y as f32]; + if let Some(poly) = crate::entities::common::polyline_segment_fill(p0, p1, hw0, hw1, v0.bulge as f32) { + out.push(poly); + } + } + out +} diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index 4b6f67df2..b50cb3e0e 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -596,3 +596,37 @@ impl Transformable for Polyline3D { }); } } +/// Generate solid-fill boundary polygons for each wide segment of a Polyline2D. +pub(crate) fn wide_fills(pl: &acadrust::entities::Polyline2D) -> Vec> { + let hw_default = (pl.start_width.max(pl.end_width) / 2.0) as f32; + let verts = &pl.vertices; + let n = verts.len(); + if n < 2 { + return vec![]; + } + let seg_count = if pl.is_closed() { n } else { n - 1 }; + let mut out = Vec::new(); + for i in 0..seg_count { + let v0 = &verts[i]; + let v1 = &verts[(i + 1) % n]; + let hw0 = if v0.start_width > 1e-9 { + v0.start_width as f32 / 2.0 + } else { + hw_default + }; + let hw1 = if v0.end_width > 1e-9 { + v0.end_width as f32 / 2.0 + } else { + hw_default + }; + if hw0 < 1e-6 && hw1 < 1e-6 { + continue; + } + let p0 = [v0.location.x as f32, v0.location.y as f32]; + let p1 = [v1.location.x as f32, v1.location.y as f32]; + if let Some(poly) = crate::entities::common::polyline_segment_fill(p0, p1, hw0, hw1, v0.bulge as f32) { + out.push(poly); + } + } + out +} diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index f77aa3c12..c5742adf6 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -1,5 +1,5 @@ use acadrust::types::aci_table::aci_to_rgb; -use acadrust::CadDocument; +use acadrust::{CadDocument, EntityType}; use crate::scene::acad_to_truck::TextStroke; use crate::scene::cxf; @@ -1364,3 +1364,334 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout { v_offset, } } +pub(crate) fn text_obb_corners_native( + e: &EntityType, + anno_scale: f32, + mtext_lines_override: Option, +) -> Option<[[f64; 3]; 4]> { + use acadrust::entities::attribute_definition::{ + HorizontalAlignment as AttrHA, VerticalAlignment as AttrVA, + }; + use acadrust::entities::{AttachmentPoint, TextHorizontalAlignment, TextVerticalAlignment}; + + let anno = anno_scale as f64; + + // Map Attribute alignment enums to the same h/v anchor fractions used by + // Text. Kept local because the Attribute enum is distinct from Text's. + let attr_h_anchor = |ha: AttrHA| -> f64 { + match ha { + AttrHA::Left => 0.0, + AttrHA::Center | AttrHA::Middle | AttrHA::Aligned | AttrHA::Fit => 0.5, + AttrHA::Right => 1.0, + } + }; + let attr_v_anchor = |va: AttrVA| -> f64 { + match va { + AttrVA::Baseline | AttrVA::Bottom => 0.0, + AttrVA::Middle => 0.5, + AttrVA::Top => 1.0, + } + }; + let attr_use_align_pt = |ha: AttrHA, va: AttrVA| -> bool { + !matches!((ha, va), (AttrHA::Left, AttrVA::Baseline)) + }; + + let (ix, iy, iz, w, h, rot, h_anchor, v_anchor) = match e { + EntityType::Text(t) => { + let h_world = t.height * anno; + let w_factor = if t.width_factor > 0.0 { t.width_factor } else { 1.0 }; + let n = t.value.chars().count().max(1) as f64; + // Approximate glyph width: AutoCAD's stroke fonts average ~0.6 em. + let w_world = n * h_world * w_factor * 0.6; + let h_anchor = match t.horizontal_alignment { + TextHorizontalAlignment::Left => 0.0, + TextHorizontalAlignment::Center + | TextHorizontalAlignment::Middle + | TextHorizontalAlignment::Aligned + | TextHorizontalAlignment::Fit => 0.5, + TextHorizontalAlignment::Right => 1.0, + }; + let v_anchor = match t.vertical_alignment { + TextVerticalAlignment::Baseline | TextVerticalAlignment::Bottom => 0.0, + TextVerticalAlignment::Middle => 0.5, + TextVerticalAlignment::Top => 1.0, + }; + // AutoCAD writes `alignment_point` (DXF 11) whenever the text + // isn't simple Left+Baseline; in that case it — not the + // insertion_point — is the anchor the alignment fractions map to. + let use_alignment_pt = !matches!( + (t.horizontal_alignment, t.vertical_alignment), + ( + TextHorizontalAlignment::Left, + TextVerticalAlignment::Baseline, + ) + ); + let anchor = match (use_alignment_pt, t.alignment_point) { + (true, Some(p)) => p, + _ => t.insertion_point, + }; + ( + anchor.x, + anchor.y, + anchor.z, + w_world, + h_world, + t.rotation, + h_anchor, + v_anchor, + ) + } + EntityType::AttributeDefinition(a) => { + let h_world = a.height * anno; + let w_factor = if a.width_factor > 0.0 { a.width_factor } else { 1.0 }; + // Render the tag in preview when no default; matches `attribute.rs`. + let display = if a.default_value.is_empty() { &a.tag } else { &a.default_value }; + let n = display.chars().count().max(1) as f64; + let w_world = n * h_world * w_factor * 0.6; + let h_anchor = attr_h_anchor(a.horizontal_alignment); + let v_anchor = attr_v_anchor(a.vertical_alignment); + let anchor = if attr_use_align_pt(a.horizontal_alignment, a.vertical_alignment) { + a.alignment_point + } else { + a.insertion_point + }; + ( + anchor.x, anchor.y, anchor.z, w_world, h_world, a.rotation, h_anchor, v_anchor, + ) + } + EntityType::AttributeEntity(a) => { + let h_world = a.height * anno; + let w_factor = if a.width_factor > 0.0 { a.width_factor } else { 1.0 }; + let n = a.value.chars().count().max(1) as f64; + let w_world = n * h_world * w_factor * 0.6; + let h_anchor = attr_h_anchor(a.horizontal_alignment); + let v_anchor = attr_v_anchor(a.vertical_alignment); + let anchor = if attr_use_align_pt(a.horizontal_alignment, a.vertical_alignment) { + a.alignment_point + } else { + a.insertion_point + }; + ( + anchor.x, anchor.y, anchor.z, w_world, h_world, a.rotation, h_anchor, v_anchor, + ) + } + EntityType::Tolerance(t) => { + // Approximate: a feature control frame is roughly 1 line tall; + // width comes from char-count of the (already-stripped) text. + // GD&T symbols all advance one cell, so plain char count is close. + let raw_h = if t.text_height > 0.0 { t.text_height } else { 2.5 }; + let h_world = raw_h * anno; + // Each cell ≈ 1.4 × height (matches `tolerance.rs` min_cell_w). + let n = t.text.chars().filter(|c| *c != '\n').count().max(1) as f64; + let w_world = h_world * 1.4 * n * 0.5; // rough — fine for LOD greek + // direction encodes rotation as a vector. + let rot = (t.direction.y).atan2(t.direction.x); + ( + t.insertion_point.x, + t.insertion_point.y, + t.insertion_point.z, + w_world, + h_world * 1.5, // frame is taller than glyph cap by ~0.5 h + rot, + 0.0, // anchored at insertion point (bottom-left) + 0.0, + ) + } + EntityType::MText(m) => { + let h_world = m.height * anno; + let raw_lines = (m.value.matches('\n').count() + 1) as f64; + let effective_lines = match mtext_lines_override { + Some(n) => n.max(1) as f64, + None => raw_lines, + }; + let w_world = if m.rectangle_width > 0.0 { + m.rectangle_width + } else { + h_world * 8.0 * effective_lines.max(1.0) + }; + // Wrap-aware override beats `rectangle_height` — the stored + // height can be stale on DWGs that were re-saved without + // updating the bounds. + let total_h = if mtext_lines_override.is_some() { + h_world * effective_lines.max(1.0) * m.line_spacing_factor.max(0.5) + } else { + m.rectangle_height.unwrap_or( + h_world * raw_lines.max(1.0) * m.line_spacing_factor.max(0.5), + ) + }; + // MText `attachment_point` puts `insertion_point` at one of the + // 9 corners/midpoints of the text bbox. h_anchor / v_anchor are + // fractions from (left, bottom) of the bbox. + let (h_anchor, v_anchor) = match m.attachment_point { + AttachmentPoint::TopLeft => (0.0, 1.0), + AttachmentPoint::TopCenter => (0.5, 1.0), + AttachmentPoint::TopRight => (1.0, 1.0), + AttachmentPoint::MiddleLeft => (0.0, 0.5), + AttachmentPoint::MiddleCenter => (0.5, 0.5), + AttachmentPoint::MiddleRight => (1.0, 0.5), + AttachmentPoint::BottomLeft => (0.0, 0.0), + AttachmentPoint::BottomCenter => (0.5, 0.0), + AttachmentPoint::BottomRight => (1.0, 0.0), + }; + ( + m.insertion_point.x, + m.insertion_point.y, + m.insertion_point.z, + w_world, + total_h, + m.rotation, + h_anchor, + v_anchor, + ) + } + _ => return None, + }; + + let x0 = -h_anchor * w; + let x1 = (1.0 - h_anchor) * w; + let y0 = -v_anchor * h; + let y1 = (1.0 - v_anchor) * h; + + let (s, c) = (rot.sin(), rot.cos()); + let rot_pt = |lx: f64, ly: f64| -> [f64; 3] { + let rx = lx * c - ly * s; + let ry = lx * s + ly * c; + [ix + rx, iy + ry, iz] + }; + + Some([ + rot_pt(x0, y0), + rot_pt(x1, y0), + rot_pt(x1, y1), + rot_pt(x0, y1), + ]) +} +pub(crate) fn text_baseline_points( + e: &EntityType, + anno_scale: f32, + world_offset: [f64; 3], + n_lines: usize, +) -> Vec<[f32; 3]> { + let Some(corners) = text_obb_corners_native(e, anno_scale, Some(n_lines)) else { + return vec![]; + }; + let line_h = match e { + EntityType::Text(t) => (t.height * anno_scale as f64) as f32, + EntityType::MText(m) => (m.height * anno_scale as f64) as f32, + EntityType::AttributeDefinition(a) => (a.height * anno_scale as f64) as f32, + EntityType::AttributeEntity(a) => (a.height * anno_scale as f64) as f32, + EntityType::Tolerance(t) => { + let raw = if t.text_height > 0.0 { t.text_height } else { 2.5 }; + (raw * anno_scale as f64) as f32 + } + _ => return vec![], + }; + if line_h <= 0.0 { + return vec![]; + } + let n_lines = n_lines.max(1); + let [ox, oy, oz] = world_offset; + let cast = |p: [f64; 3]| -> [f32; 3] { + [(p[0] - ox) as f32, (p[1] - oy) as f32, (p[2] - oz) as f32] + }; + let bl = cast(corners[0]); + let br = cast(corners[1]); + let full_tl = cast(corners[3]); + + let (ux, uy, uz) = (full_tl[0] - bl[0], full_tl[1] - bl[1], full_tl[2] - bl[2]); + let ulen = (ux * ux + uy * uy + uz * uz).sqrt(); + if ulen < 1e-9 { + return vec![bl, br]; + } + let (nx, ny, nz) = (ux / ulen, uy / ulen, uz / ulen); + + let mut pts = Vec::with_capacity(n_lines * 3); + for i in 0..n_lines { + // i = 0 is the topmost line — its bottom sits one line_h below + // the OBB top (≈ `ulen`). For n_lines > ulen/line_h the deepest + // baselines clamp to the OBB bottom. + let bot_off = ((i + 1) as f32) * line_h; + let along = (ulen - bot_off).max(0.0); + let p0 = [bl[0] + nx * along, bl[1] + ny * along, bl[2] + nz * along]; + let p1 = [br[0] + nx * along, br[1] + ny * along, br[2] + nz * along]; + if !pts.is_empty() { + pts.push([f32::NAN; 3]); + } + pts.extend_from_slice(&[p0, p1]); + } + pts +} + +/// Filled tris for a greeked top-level Text / MText. One 2-triangle rect +/// per visible line — `n_lines` is the actual rendered line count from +/// `mtext_line_count` (1 for Text). Stacked top → bottom along the OBB's +/// up direction. The face3d pipeline skips its 0.45 dim for wires with +/// empty `points`, so these tris render at the literal text color. +pub(crate) fn text_greek_obb_tris( + e: &EntityType, + anno_scale: f32, + world_offset: [f64; 3], + n_lines: usize, +) -> Vec<[f32; 3]> { + let Some(corners) = text_obb_corners_native(e, anno_scale, Some(n_lines)) else { + return vec![]; + }; + let line_h = match e { + EntityType::Text(t) => (t.height * anno_scale as f64) as f32, + EntityType::MText(m) => (m.height * anno_scale as f64) as f32, + EntityType::AttributeDefinition(a) => (a.height * anno_scale as f64) as f32, + EntityType::AttributeEntity(a) => (a.height * anno_scale as f64) as f32, + EntityType::Tolerance(t) => { + let raw = if t.text_height > 0.0 { t.text_height } else { 2.5 }; + (raw * anno_scale as f64) as f32 + } + _ => return vec![], + }; + if line_h <= 0.0 { + return vec![]; + } + let n_lines = n_lines.max(1); + let [ox, oy, oz] = world_offset; + let cast = |p: [f64; 3]| -> [f32; 3] { + [(p[0] - ox) as f32, (p[1] - oy) as f32, (p[2] - oz) as f32] + }; + let bl = cast(corners[0]); + let br = cast(corners[1]); + let full_tl = cast(corners[3]); + + let (ux, uy, uz) = (full_tl[0] - bl[0], full_tl[1] - bl[1], full_tl[2] - bl[2]); + let ulen = (ux * ux + uy * uy + uz * uz).sqrt(); + if ulen < 1e-9 { + return vec![]; + } + let (nx, ny, nz) = (ux / ulen, uy / ulen, uz / ulen); + + let mut tris = Vec::with_capacity(n_lines * 6); + for i in 0..n_lines { + let top_along = (ulen - (i as f32) * line_h).max(0.0); + let bot_along = (ulen - ((i + 1) as f32) * line_h).max(0.0); + let tl = [ + bl[0] + nx * top_along, + bl[1] + ny * top_along, + bl[2] + nz * top_along, + ]; + let tr = [ + br[0] + nx * top_along, + br[1] + ny * top_along, + br[2] + nz * top_along, + ]; + let lbl = [ + bl[0] + nx * bot_along, + bl[1] + ny * bot_along, + bl[2] + nz * bot_along, + ]; + let lbr = [ + br[0] + nx * bot_along, + br[1] + ny * bot_along, + br[2] + nz * bot_along, + ]; + tris.extend_from_slice(&[lbl, lbr, tr, lbl, tr, tl]); + } + tris +} + diff --git a/src/scene/block_cache.rs b/src/scene/block_cache.rs index f0fd5ebd0..bb1b0dc32 100644 --- a/src/scene/block_cache.rs +++ b/src/scene/block_cache.rs @@ -456,7 +456,7 @@ fn tessellate_sub_local( _ => None, }; let text_obb_local: Option<[[f32; 3]; 4]> = - crate::scene::text_obb_corners_native(sub, anno_scale, mtext_lines).map(|c| { + crate::entities::text_support::text_obb_corners_native(sub, anno_scale, mtext_lines).map(|c| { [ [(c[0][0] - lo_x) as f32, (c[0][1] - lo_y) as f32, (c[0][2] - lo_z) as f32], [(c[1][0] - lo_x) as f32, (c[1][1] - lo_y) as f32, (c[1][2] - lo_z) as f32], @@ -941,7 +941,7 @@ struct ExpandCtx<'a> { /// Fade `color` toward `bg` by 50%, preserving alpha. Used to mark xref /// geometry — the hue stays recognizable but the contrast against the /// background drops, reading as "washed out". -pub(super) fn fade_toward_bg(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] { +pub(crate) fn fade_toward_bg(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] { const T: f32 = 0.5; [ color[0] * (1.0 - T) + bg[0] * T, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 9a85f6498..880e91d4b 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2637,8 +2637,8 @@ impl Scene { let oy = oy as f32; for entity in self.document.entities() { let (common, fills) = match entity { - EntityType::LwPolyline(pl) => (&pl.common, wide_lwpolyline_fills(pl)), - EntityType::Polyline2D(pl) => (&pl.common, wide_polyline2d_fills(pl)), + EntityType::LwPolyline(pl) => (&pl.common, crate::entities::lwpolyline::wide_fills(pl)), + EntityType::Polyline2D(pl) => (&pl.common, crate::entities::polyline::wide_fills(pl)), _ => continue, }; if fills.is_empty() { @@ -5047,7 +5047,7 @@ fn tessellate_entity( // 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. - append_insert_attribute_wires( + crate::entities::insert::append_insert_attribute_wires( &mut wires, document, ins, @@ -5119,7 +5119,7 @@ fn tessellate_entity( wires }) .collect(); - append_insert_attribute_wires( + crate::entities::insert::append_insert_attribute_wires( &mut wires, document, ins, @@ -5176,7 +5176,7 @@ fn tessellate_entity( _ => 1, }; if h_px < 1.0 { - let pts = text_baseline_points(e, anno_scale, world_offset, n_lines); + let pts = crate::entities::text_support::text_baseline_points(e, anno_scale, world_offset, n_lines); if pts.len() < 2 { return vec![]; } @@ -5218,7 +5218,7 @@ fn tessellate_entity( }]; } if h_px < 5.0 && aabb != WireModel::UNBOUNDED_AABB { - let fill_tris = text_greek_obb_tris(e, anno_scale, world_offset, n_lines); + let fill_tris = crate::entities::text_support::text_greek_obb_tris(e, anno_scale, world_offset, n_lines); if fill_tris.is_empty() { return vec![lod_stub_wire( h.value().to_string(), @@ -5309,208 +5309,6 @@ fn tessellate_entity( /// (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. -pub(crate) fn text_obb_corners_native( - e: &EntityType, - anno_scale: f32, - mtext_lines_override: Option, -) -> Option<[[f64; 3]; 4]> { - use acadrust::entities::attribute_definition::{ - HorizontalAlignment as AttrHA, VerticalAlignment as AttrVA, - }; - use acadrust::entities::{AttachmentPoint, TextHorizontalAlignment, TextVerticalAlignment}; - - let anno = anno_scale as f64; - - // Map Attribute alignment enums to the same h/v anchor fractions used by - // Text. Kept local because the Attribute enum is distinct from Text's. - let attr_h_anchor = |ha: AttrHA| -> f64 { - match ha { - AttrHA::Left => 0.0, - AttrHA::Center | AttrHA::Middle | AttrHA::Aligned | AttrHA::Fit => 0.5, - AttrHA::Right => 1.0, - } - }; - let attr_v_anchor = |va: AttrVA| -> f64 { - match va { - AttrVA::Baseline | AttrVA::Bottom => 0.0, - AttrVA::Middle => 0.5, - AttrVA::Top => 1.0, - } - }; - let attr_use_align_pt = |ha: AttrHA, va: AttrVA| -> bool { - !matches!((ha, va), (AttrHA::Left, AttrVA::Baseline)) - }; - - let (ix, iy, iz, w, h, rot, h_anchor, v_anchor) = match e { - EntityType::Text(t) => { - let h_world = t.height * anno; - let w_factor = if t.width_factor > 0.0 { t.width_factor } else { 1.0 }; - let n = t.value.chars().count().max(1) as f64; - // Approximate glyph width: AutoCAD's stroke fonts average ~0.6 em. - let w_world = n * h_world * w_factor * 0.6; - let h_anchor = match t.horizontal_alignment { - TextHorizontalAlignment::Left => 0.0, - TextHorizontalAlignment::Center - | TextHorizontalAlignment::Middle - | TextHorizontalAlignment::Aligned - | TextHorizontalAlignment::Fit => 0.5, - TextHorizontalAlignment::Right => 1.0, - }; - let v_anchor = match t.vertical_alignment { - TextVerticalAlignment::Baseline | TextVerticalAlignment::Bottom => 0.0, - TextVerticalAlignment::Middle => 0.5, - TextVerticalAlignment::Top => 1.0, - }; - // AutoCAD writes `alignment_point` (DXF 11) whenever the text - // isn't simple Left+Baseline; in that case it — not the - // insertion_point — is the anchor the alignment fractions map to. - let use_alignment_pt = !matches!( - (t.horizontal_alignment, t.vertical_alignment), - ( - TextHorizontalAlignment::Left, - TextVerticalAlignment::Baseline, - ) - ); - let anchor = match (use_alignment_pt, t.alignment_point) { - (true, Some(p)) => p, - _ => t.insertion_point, - }; - ( - anchor.x, - anchor.y, - anchor.z, - w_world, - h_world, - t.rotation, - h_anchor, - v_anchor, - ) - } - EntityType::AttributeDefinition(a) => { - let h_world = a.height * anno; - let w_factor = if a.width_factor > 0.0 { a.width_factor } else { 1.0 }; - // Render the tag in preview when no default; matches `attribute.rs`. - let display = if a.default_value.is_empty() { &a.tag } else { &a.default_value }; - let n = display.chars().count().max(1) as f64; - let w_world = n * h_world * w_factor * 0.6; - let h_anchor = attr_h_anchor(a.horizontal_alignment); - let v_anchor = attr_v_anchor(a.vertical_alignment); - let anchor = if attr_use_align_pt(a.horizontal_alignment, a.vertical_alignment) { - a.alignment_point - } else { - a.insertion_point - }; - ( - anchor.x, anchor.y, anchor.z, w_world, h_world, a.rotation, h_anchor, v_anchor, - ) - } - EntityType::AttributeEntity(a) => { - let h_world = a.height * anno; - let w_factor = if a.width_factor > 0.0 { a.width_factor } else { 1.0 }; - let n = a.value.chars().count().max(1) as f64; - let w_world = n * h_world * w_factor * 0.6; - let h_anchor = attr_h_anchor(a.horizontal_alignment); - let v_anchor = attr_v_anchor(a.vertical_alignment); - let anchor = if attr_use_align_pt(a.horizontal_alignment, a.vertical_alignment) { - a.alignment_point - } else { - a.insertion_point - }; - ( - anchor.x, anchor.y, anchor.z, w_world, h_world, a.rotation, h_anchor, v_anchor, - ) - } - EntityType::Tolerance(t) => { - // Approximate: a feature control frame is roughly 1 line tall; - // width comes from char-count of the (already-stripped) text. - // GD&T symbols all advance one cell, so plain char count is close. - let raw_h = if t.text_height > 0.0 { t.text_height } else { 2.5 }; - let h_world = raw_h * anno; - // Each cell ≈ 1.4 × height (matches `tolerance.rs` min_cell_w). - let n = t.text.chars().filter(|c| *c != '\n').count().max(1) as f64; - let w_world = h_world * 1.4 * n * 0.5; // rough — fine for LOD greek - // direction encodes rotation as a vector. - let rot = (t.direction.y).atan2(t.direction.x); - ( - t.insertion_point.x, - t.insertion_point.y, - t.insertion_point.z, - w_world, - h_world * 1.5, // frame is taller than glyph cap by ~0.5 h - rot, - 0.0, // anchored at insertion point (bottom-left) - 0.0, - ) - } - EntityType::MText(m) => { - let h_world = m.height * anno; - let raw_lines = (m.value.matches('\n').count() + 1) as f64; - let effective_lines = match mtext_lines_override { - Some(n) => n.max(1) as f64, - None => raw_lines, - }; - let w_world = if m.rectangle_width > 0.0 { - m.rectangle_width - } else { - h_world * 8.0 * effective_lines.max(1.0) - }; - // Wrap-aware override beats `rectangle_height` — the stored - // height can be stale on DWGs that were re-saved without - // updating the bounds. - let total_h = if mtext_lines_override.is_some() { - h_world * effective_lines.max(1.0) * m.line_spacing_factor.max(0.5) - } else { - m.rectangle_height.unwrap_or( - h_world * raw_lines.max(1.0) * m.line_spacing_factor.max(0.5), - ) - }; - // MText `attachment_point` puts `insertion_point` at one of the - // 9 corners/midpoints of the text bbox. h_anchor / v_anchor are - // fractions from (left, bottom) of the bbox. - let (h_anchor, v_anchor) = match m.attachment_point { - AttachmentPoint::TopLeft => (0.0, 1.0), - AttachmentPoint::TopCenter => (0.5, 1.0), - AttachmentPoint::TopRight => (1.0, 1.0), - AttachmentPoint::MiddleLeft => (0.0, 0.5), - AttachmentPoint::MiddleCenter => (0.5, 0.5), - AttachmentPoint::MiddleRight => (1.0, 0.5), - AttachmentPoint::BottomLeft => (0.0, 0.0), - AttachmentPoint::BottomCenter => (0.5, 0.0), - AttachmentPoint::BottomRight => (1.0, 0.0), - }; - ( - m.insertion_point.x, - m.insertion_point.y, - m.insertion_point.z, - w_world, - total_h, - m.rotation, - h_anchor, - v_anchor, - ) - } - _ => return None, - }; - - let x0 = -h_anchor * w; - let x1 = (1.0 - h_anchor) * w; - let y0 = -v_anchor * h; - let y1 = (1.0 - v_anchor) * h; - - let (s, c) = (rot.sin(), rot.cos()); - let rot_pt = |lx: f64, ly: f64| -> [f64; 3] { - let rx = lx * c - ly * s; - let ry = lx * s + ly * c; - [ix + rx, iy + ry, iz] - }; - - Some([ - rot_pt(x0, y0), - rot_pt(x1, y0), - rot_pt(x1, y1), - rot_pt(x0, y1), - ]) -} /// 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 @@ -5560,213 +5358,7 @@ fn lod_stub_wire( /// INSERT only stamps the geometry once, attribute text sits at the world /// position recorded on each ATTRIB. See #20. #[allow(clippy::too_many_arguments)] -fn append_insert_attribute_wires( - wires: &mut Vec, - document: &acadrust::CadDocument, - ins: &acadrust::entities::Insert, - insert_handle: Handle, - sel: bool, - ins_color: [f32; 4], - ins_pat_len: f32, - ins_pat: [f32; 8], - ins_lw_px: f32, - bg_color: [f32; 4], - is_xref: bool, - pslt_factor: f32, - anno_scale: f32, - world_offset: [f64; 3], -) { - if ins.attributes.is_empty() { - return; - } - // ATTMODE (header.attribute_visibility): - // 0 = Off — every attribute hidden - // 1 = Normal — honour per-attribute `invisible` flag (default) - // 2 = On — every attribute forced visible, ignoring its flag - let attmode = document.header.attribute_visibility; - if attmode == 0 { - return; - } - for attr in &ins.attributes { - let per_attr_hidden = attr.common.invisible || attr.flags.invisible; - if attmode == 1 && per_attr_hidden { - continue; - } - let attr_entity = EntityType::AttributeEntity(attr.clone()); - let (sub_color, sub_plen, sub_pat, sub_lw_px, sub_aci) = - render::render_style_for_block_sub( - document, - &attr_entity, - ins_color, - ins_pat_len, - ins_pat, - ins_lw_px, - ); - let sub_color = render::adapt_to_bg(sub_color, bg_color); - let sub_color = if is_xref && !sel { - block_cache::fade_toward_bg(sub_color, bg_color) - } else { - sub_color - }; - let sub_aabb = entity_aabb(&attr_entity, world_offset); - let mut attr_wires = tessellate::tessellate( - document, - insert_handle, - &attr_entity, - sel, - sub_color, - sub_plen * pslt_factor, - sub_pat.map(|v| v * pslt_factor), - sub_lw_px, - world_offset, - anno_scale, - ); - // Use the INSERT's handle so selection / picking groups attribute - // text with the parent insert instead of treating it as a stray text. - for w in &mut attr_wires { - w.name = insert_handle.value().to_string(); - w.aci = sub_aci; - w.aabb = sub_aabb; - } - wires.extend(attr_wires); - } -} - -/// NaN-separated baseline segments — one per visible wrap line — for a -/// sub-pixel Text / MText, in world-offset-subtracted f32 space. `n_lines` -/// must be the actual rendered line count (1 for Text, the wrap-expanded -/// count for MText). Lines are emitted top → bottom along the OBB's up -/// direction, each at the bottom edge of its slab. -fn text_baseline_points( - e: &EntityType, - anno_scale: f32, - world_offset: [f64; 3], - n_lines: usize, -) -> Vec<[f32; 3]> { - let Some(corners) = text_obb_corners_native(e, anno_scale, Some(n_lines)) else { - return vec![]; - }; - let line_h = match e { - EntityType::Text(t) => (t.height * anno_scale as f64) as f32, - EntityType::MText(m) => (m.height * anno_scale as f64) as f32, - EntityType::AttributeDefinition(a) => (a.height * anno_scale as f64) as f32, - EntityType::AttributeEntity(a) => (a.height * anno_scale as f64) as f32, - EntityType::Tolerance(t) => { - let raw = if t.text_height > 0.0 { t.text_height } else { 2.5 }; - (raw * anno_scale as f64) as f32 - } - _ => return vec![], - }; - if line_h <= 0.0 { - return vec![]; - } - let n_lines = n_lines.max(1); - let [ox, oy, oz] = world_offset; - let cast = |p: [f64; 3]| -> [f32; 3] { - [(p[0] - ox) as f32, (p[1] - oy) as f32, (p[2] - oz) as f32] - }; - let bl = cast(corners[0]); - let br = cast(corners[1]); - let full_tl = cast(corners[3]); - - let (ux, uy, uz) = (full_tl[0] - bl[0], full_tl[1] - bl[1], full_tl[2] - bl[2]); - let ulen = (ux * ux + uy * uy + uz * uz).sqrt(); - if ulen < 1e-9 { - return vec![bl, br]; - } - let (nx, ny, nz) = (ux / ulen, uy / ulen, uz / ulen); - - let mut pts = Vec::with_capacity(n_lines * 3); - for i in 0..n_lines { - // i = 0 is the topmost line — its bottom sits one line_h below - // the OBB top (≈ `ulen`). For n_lines > ulen/line_h the deepest - // baselines clamp to the OBB bottom. - let bot_off = ((i + 1) as f32) * line_h; - let along = (ulen - bot_off).max(0.0); - let p0 = [bl[0] + nx * along, bl[1] + ny * along, bl[2] + nz * along]; - let p1 = [br[0] + nx * along, br[1] + ny * along, br[2] + nz * along]; - if !pts.is_empty() { - pts.push([f32::NAN; 3]); - } - pts.extend_from_slice(&[p0, p1]); - } - pts -} - -/// Filled tris for a greeked top-level Text / MText. One 2-triangle rect -/// per visible line — `n_lines` is the actual rendered line count from -/// `mtext_line_count` (1 for Text). Stacked top → bottom along the OBB's -/// up direction. The face3d pipeline skips its 0.45 dim for wires with -/// empty `points`, so these tris render at the literal text color. -fn text_greek_obb_tris( - e: &EntityType, - anno_scale: f32, - world_offset: [f64; 3], - n_lines: usize, -) -> Vec<[f32; 3]> { - let Some(corners) = text_obb_corners_native(e, anno_scale, Some(n_lines)) else { - return vec![]; - }; - let line_h = match e { - EntityType::Text(t) => (t.height * anno_scale as f64) as f32, - EntityType::MText(m) => (m.height * anno_scale as f64) as f32, - EntityType::AttributeDefinition(a) => (a.height * anno_scale as f64) as f32, - EntityType::AttributeEntity(a) => (a.height * anno_scale as f64) as f32, - EntityType::Tolerance(t) => { - let raw = if t.text_height > 0.0 { t.text_height } else { 2.5 }; - (raw * anno_scale as f64) as f32 - } - _ => return vec![], - }; - if line_h <= 0.0 { - return vec![]; - } - let n_lines = n_lines.max(1); - let [ox, oy, oz] = world_offset; - let cast = |p: [f64; 3]| -> [f32; 3] { - [(p[0] - ox) as f32, (p[1] - oy) as f32, (p[2] - oz) as f32] - }; - let bl = cast(corners[0]); - let br = cast(corners[1]); - let full_tl = cast(corners[3]); - - let (ux, uy, uz) = (full_tl[0] - bl[0], full_tl[1] - bl[1], full_tl[2] - bl[2]); - let ulen = (ux * ux + uy * uy + uz * uz).sqrt(); - if ulen < 1e-9 { - return vec![]; - } - let (nx, ny, nz) = (ux / ulen, uy / ulen, uz / ulen); - - let mut tris = Vec::with_capacity(n_lines * 6); - for i in 0..n_lines { - let top_along = (ulen - (i as f32) * line_h).max(0.0); - let bot_along = (ulen - ((i + 1) as f32) * line_h).max(0.0); - let tl = [ - bl[0] + nx * top_along, - bl[1] + ny * top_along, - bl[2] + nz * top_along, - ]; - let tr = [ - br[0] + nx * top_along, - br[1] + ny * top_along, - br[2] + nz * top_along, - ]; - let lbl = [ - bl[0] + nx * bot_along, - bl[1] + ny * bot_along, - bl[2] + nz * bot_along, - ]; - let lbr = [ - br[0] + nx * bot_along, - br[1] + ny * bot_along, - br[2] + nz * bot_along, - ]; - tris.extend_from_slice(&[lbl, lbr, tr, lbl, tr, tl]); - } - tris -} - -fn entity_aabb(e: &acadrust::EntityType, world_offset: [f64; 3]) -> [f32; 4] { +pub(crate) fn entity_aabb(e: &acadrust::EntityType, world_offset: [f64; 3]) -> [f32; 4] { let bbox = e.as_entity().bounding_box(); let [ox, oy, _] = world_offset; let min_x = (bbox.min.x - ox) as f32; @@ -5810,156 +5402,3 @@ fn is_unindexable_entity(e: &acadrust::EntityType) -> bool { ) } -/// Generate solid-fill boundary polygons for each wide segment of an LWPolyline. -/// Returns one polygon per segment that has non-zero width; empty if the polyline -/// has zero `constant_width` and all vertex widths are zero. -fn wide_lwpolyline_fills(pl: &acadrust::entities::LwPolyline) -> Vec> { - let hw_const = (pl.constant_width / 2.0) as f32; - let verts = &pl.vertices; - let n = verts.len(); - if n < 2 { - return vec![]; - } - let seg_count = if pl.is_closed { n } else { n - 1 }; - let mut out = Vec::new(); - for i in 0..seg_count { - let v0 = &verts[i]; - let v1 = &verts[(i + 1) % n]; - let hw0 = if v0.start_width > 1e-9 { - v0.start_width as f32 / 2.0 - } else { - hw_const - }; - let hw1 = if v0.end_width > 1e-9 { - v0.end_width as f32 / 2.0 - } else { - hw_const - }; - if hw0 < 1e-6 && hw1 < 1e-6 { - continue; - } - let p0 = [v0.location.x as f32, v0.location.y as f32]; - let p1 = [v1.location.x as f32, v1.location.y as f32]; - if let Some(poly) = polyline_segment_fill(p0, p1, hw0, hw1, v0.bulge as f32) { - out.push(poly); - } - } - out -} - -/// Generate solid-fill boundary polygons for each wide segment of a Polyline2D. -fn wide_polyline2d_fills(pl: &acadrust::entities::Polyline2D) -> Vec> { - let hw_default = (pl.start_width.max(pl.end_width) / 2.0) as f32; - let verts = &pl.vertices; - let n = verts.len(); - if n < 2 { - return vec![]; - } - let seg_count = if pl.is_closed() { n } else { n - 1 }; - let mut out = Vec::new(); - for i in 0..seg_count { - let v0 = &verts[i]; - let v1 = &verts[(i + 1) % n]; - let hw0 = if v0.start_width > 1e-9 { - v0.start_width as f32 / 2.0 - } else { - hw_default - }; - let hw1 = if v0.end_width > 1e-9 { - v0.end_width as f32 / 2.0 - } else { - hw_default - }; - if hw0 < 1e-6 && hw1 < 1e-6 { - continue; - } - let p0 = [v0.location.x as f32, v0.location.y as f32]; - let p1 = [v1.location.x as f32, v1.location.y as f32]; - if let Some(poly) = polyline_segment_fill(p0, p1, hw0, hw1, v0.bulge as f32) { - out.push(poly); - } - } - out -} - -/// Compute the filled boundary polygon for one polyline segment. -/// For straight segments: a rectangle/trapezoid. -/// For arc segments: an arc band (outer arc + reversed inner arc). -/// Returns `None` if the segment is degenerate. -fn polyline_segment_fill( - p0: [f32; 2], - p1: [f32; 2], - hw0: f32, - hw1: f32, - bulge: f32, -) -> Option> { - if bulge.abs() < 1e-9 { - // Straight segment — rectangle or trapezoid - let dx = p1[0] - p0[0]; - let dy = p1[1] - p0[1]; - let len = (dx * dx + dy * dy).sqrt(); - if len < 1e-9 { - return None; - } - let nx = -dy / len; - let ny = dx / len; - Some(vec![ - [p0[0] + hw0 * nx, p0[1] + hw0 * ny], - [p1[0] + hw1 * nx, p1[1] + hw1 * ny], - [p1[0] - hw1 * nx, p1[1] - hw1 * ny], - [p0[0] - hw0 * nx, p0[1] - hw0 * ny], - ]) - } else { - // Arc segment — arc band polygon. - // Center math matches `bulge_to_arc` in modules/home/modify/explode.rs: - // r = chord * (1 + b²) / (4·|b|) - // d = r * (1 - b²) / (1 + b²) (signed: negative ⇒ major arc, center - // flips to the opposite side of chord) - // center = midpoint + sign(b) · d · left_perp(chord) - let b = bulge as f64; - let b2 = b * b; - let dx = (p1[0] - p0[0]) as f64; - let dy = (p1[1] - p0[1]) as f64; - let chord_len = (dx * dx + dy * dy).sqrt(); - if chord_len < 1e-9 || b.abs() < 1e-12 { - return None; - } - let r = chord_len * (1.0 + b2) / (4.0 * b.abs()); - let d_perp = r * (1.0 - b2) / (1.0 + b2); - let mx = ((p0[0] + p1[0]) * 0.5) as f64; - let my = ((p0[1] + p1[1]) * 0.5) as f64; - let perp_x = -dy / chord_len; - let perp_y = dx / chord_len; - let sign = b.signum(); - let cx = (mx + sign * d_perp * perp_x) as f32; - let cy = (my + sign * d_perp * perp_y) as f32; - let a0 = ((p0[1] - cy) as f32).atan2((p0[0] - cx) as f32); - let a1 = ((p1[1] - cy) as f32).atan2((p1[0] - cx) as f32); - let (sa, mut ea) = if bulge > 0.0 { (a0, a1) } else { (a1, a0) }; - if ea < sa { - ea += std::f32::consts::TAU; - } - let span = ea - sa; - let segs = ((span.abs() / std::f32::consts::TAU) * 24.0) - .ceil() - .max(4.0) as u32; - let r = r as f32; - let r_outer = |t: f32| r + (hw0 + (hw1 - hw0) * t); - let r_inner = |t: f32| (r - (hw0 + (hw1 - hw0) * t)).max(0.0); - let mut boundary = Vec::with_capacity((segs as usize + 1) * 2); - let inv = 1.0 / segs as f32; - for j in 0..=segs { - let t = j as f32 * inv; - let ang = sa + span * t; - let ro = r_outer(t); - boundary.push([cx + ro * ang.cos(), cy + ro * ang.sin()]); - } - for j in (0..=segs).rev() { - let t = j as f32 * inv; - let ang = sa + span * t; - let ri = r_inner(t); - boundary.push([cx + ri * ang.cos(), cy + ri * ang.sin()]); - } - Some(boundary) - } -} diff --git a/src/scene/render.rs b/src/scene/render.rs index 5f17bac4e..706de0df8 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -279,7 +279,7 @@ pub(super) fn render_style_for( /// Like `render_style_for` but resolves ByBlock properties by inheriting from /// the INSERT entity's already-resolved style. Call this for exploded block /// sub-entities so that ByBlock color/linetype/lineweight propagate correctly. -pub(super) fn render_style_for_block_sub( +pub(crate) fn render_style_for_block_sub( document: &CadDocument, e: &EntityType, insert_color: [f32; 4], @@ -314,7 +314,7 @@ pub(super) fn render_style_for_block_sub( /// Adapt white→black or black→white based on background luminance. /// White entities on light backgrounds become black, black entities on dark /// backgrounds become white. All other colors pass through unchanged. -pub(super) fn adapt_to_bg(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] { +pub(crate) fn adapt_to_bg(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] { let lum = 0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2]; let is_white = color[0] > 0.95 && color[1] > 0.95 && color[2] > 0.95; let is_black = color[0] < 0.05 && color[1] < 0.05 && color[2] < 0.05;