Files
OneCAD-Draft/src/scene/model/underlay_vector/wires.rs
T

224 lines
8.3 KiB
Rust

//! 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<WireModel> {
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<Vec<[f64; 2]>> = 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])
}