//! A DWF sheet or DGN model drawn as vector wires (lines and flat fills) //! in world space, so it stays sharp at every zoom as the reference draws //! it, instead of as a raster rescaled to the view. use std::collections::HashMap; use codec::entities::{Underlay, UnderlayDefinition, UnderlayDisplayFlags, UnderlayType}; use super::{model, sheet_without, Backdrop, Segment, Sheet}; use crate::scene::model::wire_model::WireModel; /// Wires for the underlay's content, one per colour and kind (lines, fills), /// named after the underlay so they draw in its place. They are drawn and /// plotted but never picked or snapped (the frame selects the underlay; the /// underlay's own snap geometry serves object snaps). pub(crate) fn display_wires( u: &Underlay, def: &UnderlayDefinition, name: &str, background: [f32; 4], ) -> Vec { if def.underlay_type == UnderlayType::Pdf || def.unloaded || !u.flags.contains(UnderlayDisplayFlags::ON) { return Vec::new(); } let page = crate::entities::underlay::page_of(def); let hidden = crate::scene::model::pdf_layers::hidden_layers(u); let Some(sheet) = sheet_without(def.underlay_type, &def.file_path, page, &hidden) else { return Vec::new(); }; let adjust = crate::scene::model::image_model::underlay_adjust(u, def, background); let backdrop = Backdrop::of([background[0], background[1], background[2]]); // A DGN model's fade is in its colours; a DWF sheet's blends it with // what is under it. let alpha = if def.underlay_type == UnderlayType::Dgn { 1.0 } else { 1.0 - u.fade.min(100) as f32 / 100.0 }; let shown = |c: [u8; 3]| -> [f32; 4] { let c = if sheet.table_colors { model::dgn_table_color(c, backdrop.max_channel, backdrop.light) } else { c }; let rgb = crate::scene::model::pdf_raster::adjust_rgb( [ c[0] as f32 / 255.0, c[1] as f32 / 255.0, c[2] as f32 / 255.0, ], &adjust, ); [rgb[0], rgb[1], rgb[2], alpha] }; let world = |p: [f64; 2]| crate::entities::underlay::local_to_world(u, p); let tolerance = curve_tolerance(&sheet); let scale = u.x_scale.abs().max(u.y_scale.abs()); // Lines per (colour, width), fills per colour, each in first-drawn order. let key = |c: [f32; 4]| c.map(f32::to_bits); let mut lines: Vec<([f32; 4], f64, Vec<[f64; 3]>)> = Vec::new(); let mut line_at: HashMap<([u32; 4], u64), usize> = HashMap::new(); let mut fills: Vec<([f32; 4], Vec<[f64; 3]>)> = Vec::new(); let mut fill_at: HashMap<[u32; 4], usize> = HashMap::new(); for path in &sheet.paths { if let Some(c) = path.fill { let color = shown(c); let rings: Vec> = path .subpaths .iter() .map(|sp| polyline(&sp.segments, tolerance)) .filter(|r| r.len() >= 3) .collect(); if !rings.is_empty() { let (points, triangles) = kernel::geom2d::triangulate_rings(&rings); let at = *fill_at.entry(key(color)).or_insert_with(|| { fills.push((color, Vec::new())); fills.len() - 1 }); for triangle in triangles { for index in triangle { if let Some(&p) = points.get(index) { fills[at].1.push(world(p)); } } } } } if let Some((c, width)) = path.stroke { let color = shown(c); let at = *line_at .entry((key(color), width.to_bits())) .or_insert_with(|| { lines.push((color, width, Vec::new())); lines.len() - 1 }); for sp in &path.subpaths { let mut pts = polyline(&sp.segments, tolerance); if sp.closed && pts.len() >= 2 && pts.first() != pts.last() { pts.push(pts[0]); } if pts.len() < 2 { continue; } let run = &mut lines[at].2; if !run.is_empty() { run.push([f64::NAN; 3]); } run.extend(pts.into_iter().map(world)); } } } let mut wires = Vec::new(); for (color, tris) in fills { let (fp, fp_low) = crate::scene::convert::tessellate::points_to_ds(tris); let mut wire = content_wire(name, Vec::new(), color); wire.fill_tris = fp; wire.fill_tris_low = fp_low; wires.push(wire); } for (color, width, points) in lines { let (lp, lp_low) = crate::scene::convert::tessellate::points_to_ds(points); let mut wire = content_wire(name, lp, color); wire.points_low = lp_low; if width < 0.0 { // A width in pixels (DGN line weights are not shown). wire.set_fixed_screen_width((-width) as f32); } else if width > 0.0 { wire.world_width = (width * scale) as f32; } else { wire.set_fixed_screen_width(1.0); } wires.push(wire); } if crate::entities::underlay::is_clipped(u) { let clip: Vec<[f64; 2]> = crate::entities::underlay::clip_polygon_local(u) .into_iter() .map(|p| { let w = world(p); [w[0], w[1]] }) .collect(); let ring = if u.clip_inverted { let [x0, y0, x1, y1] = sheet.rect; let corners = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]].map(|p| { let w = world(p); [w[0], w[1]] }); let lo = [ corners.iter().map(|p| p[0]).fold(f64::MAX, f64::min), corners.iter().map(|p| p[1]).fold(f64::MAX, f64::min), ]; let hi = [ corners.iter().map(|p| p[0]).fold(f64::MIN, f64::max), corners.iter().map(|p| p[1]).fold(f64::MIN, f64::max), ]; crate::scene::pick::xclip::inverted_ring(&clip, (lo, hi)) } else { clip }; crate::scene::pick::xclip::clip_wires(&mut wires, &ring); } for wire in &mut wires { crate::scene::convert::tess::set_wire_aabb(wire, WireModel::UNBOUNDED_AABB); } wires } fn content_wire(name: &str, points: Vec<[f32; 3]>, color: [f32; 4]) -> WireModel { let mut wire = WireModel::solid(name.to_string(), points, color, false); wire.aci = 0; wire.bg_adapt = None; wire.display_visible = true; wire.plot_visible = true; // Drawn, but neither picked nor snapped (see `WireModel::snap_only`). wire.snap_only = true; wire } /// How far a curve's polyline may stray from it: a small share of the /// sheet, so curves stay smooth when zoomed far in. fn curve_tolerance(sheet: &Sheet) -> f64 { let [x0, y0, x1, y1] = sheet.rect; ((x1 - x0).hypot(y1 - y0) * 2e-5).max(1e-9) } /// A subpath's points, curves divided within `tolerance`. fn polyline(segments: &[Segment], tolerance: f64) -> Vec<[f64; 2]> { let mut out: Vec<[f64; 2]> = Vec::new(); for seg in segments { let start = seg.start(); if out.last() != Some(&start) { out.push(start); } match seg { Segment::Line(_, b) => out.push(*b), Segment::Cubic(a, c1, c2, b) => { // The control polygon's length bounds the curve's; the // flattening error of n pieces falls as 1/n². let hull = dist(*a, *c1) + dist(*c1, *c2) + dist(*c2, *b); let n = ((hull / tolerance).sqrt() * 0.5).ceil().clamp(2.0, 256.0) as usize; for i in 1..=n { let t = i as f64 / n as f64; let u = 1.0 - t; let (k0, k1, k2, k3) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t); out.push([ k0 * a[0] + k1 * c1[0] + k2 * c2[0] + k3 * b[0], k0 * a[1] + k1 * c1[1] + k2 * c2[1] + k3 * b[1], ]); } } } } out } fn dist(a: [f64; 2], b: [f64; 2]) -> f64 { (b[0] - a[0]).hypot(b[1] - a[1]) }