Compare commits

..
14 changed files with 142 additions and 328 deletions
+17 -3
View File
@@ -8,6 +8,9 @@ use cadcraft_geom::{Line, Vec2};
pub const MAX_OBJECTS: usize = 200;
/// Most candidates returned.
pub const MAX_CANDIDATES: usize = 2000;
/// Residual-evaluation work all the solves of one AUTOCONSTRAIN may make together (four
/// full-size solves; a few thousand small ones).
const MAX_AUTO_WORK: f64 = 4.0 * crate::MAX_EVAL_WORK;
/// Inference options.
#[derive(Clone, Debug)]
@@ -309,11 +312,22 @@ fn on_arc(d: &Drawing, h: Handle, c: Vec2, p: Vec2) -> bool {
/// Infer and apply constraints over `handles`; returns the ids added. Redundant or conflicting
/// candidates are skipped.
pub fn autoconstrain(d: &mut Drawing, handles: &[Handle], o: &InferOptions) -> Vec<u32> {
autoconstrain_report(d, handles, o).0
}
/// [`autoconstrain`], and whether it stopped early because the solver-work allowance of the command
/// was spent (the remaining candidates were not tried).
pub fn autoconstrain_report(d: &mut Drawing, handles: &[Handle], o: &InferOptions) -> (Vec<u32>, bool) {
let mut added = Vec::new();
// One allowance of solver work for all the candidates: each is a full solve of its component.
let mut work = MAX_AUTO_WORK;
for c in infer(d, handles, o) {
if let Ok(id) = crate::add(d, c, &crate::SolveOptions::default()) {
added.push(id);
match crate::add_within(d, c, &crate::SolveOptions::default(), &mut work) {
Ok(id) => added.push(id),
// The allowance is spent: what was added stays, the remaining candidates are not tried.
Err(crate::SolveError::TooLarge) if work < 0.0 => return (added, true),
Err(_) => {}
}
}
added
(added, false)
}
+43 -4
View File
@@ -36,7 +36,7 @@ use cadcraft_doc::{Constraint, ConstraintKind, Drawing, GeomRef, Handle};
use cadcraft_geom::Vec2;
use serde::Serialize;
pub use auto::{INFER_TYPES, InferOptions, autoconstrain, infer, infer_placed};
pub use auto::{INFER_TYPES, InferOptions, autoconstrain, autoconstrain_report, infer, infer_placed};
pub use dof::{DofReport, DofStatus, ObjectDof, dof};
pub use expr::{Env, ExprError};
pub use model::{ref_points, rigid_points, supported};
@@ -53,6 +53,14 @@ const MAX_ITER: usize = 200;
/// Largest residual-evaluation work (including spline rebuilds) one numerical Jacobian may take,
/// shared by solving, redundancy checks and DOF analysis.
pub(crate) const MAX_EVAL_WORK: f64 = 2e9;
/// Most multiply-adds (rows x columns squared) of the dense rank factorisations one Fix makes:
/// about a thousand parameters locked at once is 1e9; 2e10 is a few seconds.
const MAX_QR_WORK: f64 = 2e10;
/// Whether a dense rank factorisation of a `rows` x `cols` matrix passes [`MAX_QR_WORK`].
fn qr_too_large(rows: usize, cols: usize) -> bool {
(rows as f64) * (cols as f64) * (cols as f64) > MAX_QR_WORK
}
/// Residual evaluations a solver iteration makes besides its Jacobian (damped trial steps).
const TRIAL_EVALS: f64 = 14.0;
@@ -335,8 +343,14 @@ impl Comp {
}
/// Constraint ids in conflict (each one whose removal makes the rest solvable).
fn conflicts(&self, x_failed: &[f64]) -> Vec<u32> {
self.conflicts_probing(x_failed, true)
}
/// [`Comp::conflicts`]; `probe` false skips the re-solves that look for the constraints whose
/// removal lets the rest converge.
fn conflicts_probing(&self, x_failed: &[f64], probe: bool) -> Vec<u32> {
let mut ids = Vec::new();
if self.built.len() <= MAX_CONFLICT_PROBES {
if probe && self.built.len() <= MAX_CONFLICT_PROBES {
for i in 0..self.built.len() {
if let Ok(o) = self.run(Some(i))
&& o.converged
@@ -741,7 +755,14 @@ pub fn purge_invalid(d: &mut Drawing) -> Vec<u32> {
/// Add a constraint: assigns its id (and name/expression for dimensional constraints when
/// empty), refuses redundant or conflicting constraints, solves and writes the geometry back.
/// On error the drawing is unchanged.
pub fn add(d: &mut Drawing, mut c: Constraint, opts: &SolveOptions) -> Result<u32, SolveError> {
pub fn add(d: &mut Drawing, c: Constraint, opts: &SolveOptions) -> Result<u32, SolveError> {
let mut unlimited = f64::INFINITY;
add_within(d, c, opts, &mut unlimited)
}
/// [`add`] charging the solve's residual-evaluation work to `work`: `TooLarge` (nothing changed)
/// once it would take more than is left.
pub(crate) fn add_within(d: &mut Drawing, mut c: Constraint, opts: &SolveOptions, work: &mut f64) -> Result<u32, SolveError> {
if d.constraints.len() >= MAX_CONSTRAINTS {
return Err(SolveError::Other("too many constraints".into()));
}
@@ -777,10 +798,24 @@ pub fn add(d: &mut Drawing, mut c: Constraint, opts: &SolveOptions) -> Result<u3
let Some(comp) = comps.into_iter().find(|k| k.built.iter().any(|b| b.id == id)) else {
return Err(SolveError::Invalid { id, msg: "could not be interpreted".into() });
};
*work -= comp.eval_work(&comp.free(None), None);
if *work < 0.0 {
return Err(SolveError::TooLarge);
}
let o = comp.run(None)?;
if !o.converged {
// The new constraint first, then the existing ones it fights with.
let mut ids = comp.conflicts(&o.x);
// Each probe is another solve (up to one per constraint): they are charged too, and skipped
// when the allowance cannot pay for them.
let probes = if comp.built.len() <= MAX_CONFLICT_PROBES { comp.built.len() as f64 } else { 0.0 };
let cost = probes * comp.eval_work(&comp.free(None), None);
// Paid for before it is spent: when the allowance cannot cover the probes they are skipped and
// it is left as it was for the candidates still to try.
let affordable = cost <= *work;
if affordable {
*work -= cost;
}
let mut ids = comp.conflicts_probing(&o.x, affordable);
ids.retain(|i| *i != id);
ids.insert(0, id);
return Err(SolveError::Conflict { ids });
@@ -819,6 +854,10 @@ pub fn add(d: &mut Drawing, mut c: Constraint, opts: &SolveOptions) -> Result<u3
let locked: Vec<usize> = before.iter().enumerate().filter(|(_, j)| !after.contains(j)).map(|(k, _)| k).collect();
let rows_old = comp.rows(Some(pos));
let nf = before.len();
// The synthetic lock rows enlarge the matrix after the solver's own limits were checked.
if qr_too_large(rows_old + locked.len(), nf) {
return Err(SolveError::TooLarge);
}
let f = |x: &[f64], out: &mut Vec<f64>| comp.eval(x, out, Some(pos));
let mut jac = lm::jacobian(&f, &o.x, &before, rows_old);
let r_old = lm::rank_qr(&jac, rows_old, nf);
+38
View File
@@ -516,3 +516,41 @@ fn next_name_skips_names_in_use() {
});
assert_eq!(next_name(&d, ConstraintKind::Distance(DistAxis::Aligned)), "d2");
}
#[test]
fn a_fix_over_a_huge_component_is_refused_before_its_dense_factorisation() {
assert!(!super::qr_too_large(1_000, 1_000));
assert!(super::qr_too_large(3_000, 3_000));
assert!(super::qr_too_large(usize::MAX, 2));
}
#[test]
fn a_spent_work_allowance_refuses_a_constraint_and_leaves_the_drawing() {
let mut d = Drawing::default();
let l = line(&mut d, (0.0, 0.0), (10.0, 0.01));
let before = d.clone();
let c = con(ConstraintKind::Horizontal, &[w(l)]);
assert!(matches!(add_within(&mut d, c.clone(), &SolveOptions::default(), &mut 0.0), Err(SolveError::TooLarge)));
assert_eq!(d, before);
let mut plenty = 1e9;
assert!(add_within(&mut d, c, &SolveOptions::default(), &mut plenty).is_ok());
assert!(plenty < 1e9, "the solve was charged");
let (_, stopped) = autoconstrain_report(&mut d, &[l], &InferOptions::default());
assert!(!stopped, "a small command never spends its allowance");
}
#[test]
fn skipped_conflict_probes_do_not_spend_the_allowance() {
let mut d = Drawing::default();
let b = line(&mut d, (0.0, 0.0), (3.0, 4.0));
ok(&mut d, con(ConstraintKind::Fix, &[w(b)]));
let candidate = || con(ConstraintKind::Vertical, &[w(b)]);
// The whole cost of the conflicting candidate: its solve and its probes.
let mut rich = 1e12;
assert!(matches!(add_within(&mut d.clone(), candidate(), &SolveOptions::default(), &mut rich), Err(SolveError::Conflict { .. })));
let total = 1e12 - rich;
// Enough for the solve but not the probes: they are skipped, and what is left stays positive.
let mut work = total * 0.6;
assert!(matches!(add_within(&mut d, candidate(), &SolveOptions::default(), &mut work), Err(SolveError::Conflict { .. })));
assert!(work > 0.0 && work < total * 0.6, "{work} of {total}");
}
+6 -2
View File
@@ -821,11 +821,15 @@ fn run_autoconstrain(s: &mut Session, p: &Value) -> Result<Value> {
o.types = t.iter().filter_map(Value::as_str).map(|n| kind_from_name(n).map_or_else(|| n.to_string(), |k| k.name().to_string())).collect();
}
let mut d = s.doc()?.clone();
let ids = cs::autoconstrain(&mut d, &hs, &o);
let (ids, stopped) = cs::autoconstrain_report(&mut d, &hs, &o);
if !ids.is_empty() {
*s.doc_mut()? = d;
}
Ok(json!({ "added": ids.len(), "ids": ids, "message": format!("{} constraint(s) applied to {} object(s)", ids.len(), hs.len()) }))
let mut message = format!("{} constraint(s) applied to {} object(s)", ids.len(), hs.len());
if stopped {
message.push_str("; stopped early: the rest need more solver work than one command may spend, so run AUTOCONSTRAIN on fewer objects");
}
Ok(json!({ "added": ids.len(), "ids": ids, "stopped": stopped, "message": message }))
}
fn run_delconstraint(s: &mut Session, p: &Value) -> Result<Value> {
+2 -3
View File
@@ -12,17 +12,16 @@ pub(crate) fn sub_ctx<'a>(ctx: &Ctx<'a>, e: &Entity, m: Mat3) -> Ctx<'a> {
};
let lw = match e.common.lineweight {
Lineweight::ByBlock => ctx.block_lw,
Lineweight::ByLayer => ctx.layer(layer.as_deref().unwrap_or("0")).map(|l| l.lineweight).unwrap_or(Lineweight::Default),
Lineweight::ByLayer => ctx.d.layer(layer.as_deref().unwrap_or("0")).map(|l| l.lineweight).unwrap_or(Lineweight::Default),
x => x,
};
let lt = match e.common.linetype.to_ascii_lowercase().as_str() {
"byblock" => ctx.block_ltype.clone(),
"bylayer" => ctx.layer(layer.as_deref().unwrap_or("0")).map(|l| l.linetype.clone()).unwrap_or_else(|| "Continuous".into()),
"bylayer" => ctx.d.layer(layer.as_deref().unwrap_or("0")).map(|l| l.linetype.clone()).unwrap_or_else(|| "Continuous".into()),
_ => e.common.linetype.clone(),
};
Ctx {
d: ctx.d,
layers: ctx.layers,
xf: ctx.xf.then_before(m),
block_color: color,
block_layer: layer,
+2 -70
View File
@@ -256,16 +256,8 @@ pub struct PolyClip {
band_h: f64,
/// The region as triangles (for clipping fills), with each triangle's bounds.
tris: Vec<([Vec2; 3], Bounds2)>,
/// Indices into `tris` of the triangles whose y range meets each band (at most
/// [`TRI_BAND_SPAN`] bands of it), so a fill triangle meets only the clip triangles near it.
tri_bands: Vec<Vec<u32>>,
/// Indices of the triangles spanning more bands than that: tested against every fill triangle.
tall_tris: Vec<u32>,
}
/// Most bands one clip triangle is registered in; a taller one goes to [`PolyClip::tall_tris`].
const TRI_BAND_SPAN: usize = 8;
impl PolyClip {
/// A clip region for `poly` (see [`clean_polygon`]); `None` for a degenerate outline.
pub fn new(poly: &[Vec2]) -> Option<PolyClip> {
@@ -274,15 +266,7 @@ impl PolyClip {
let band_h = bounds.height() / BANDS as f64;
let mut bands: Vec<Vec<u32>> = vec![Vec::new(); BANDS];
let n = poly.len();
let mut clip = PolyClip {
poly: Vec::new(),
bounds,
bands: Vec::new(),
band_h,
tris: Vec::new(),
tri_bands: vec![Vec::new(); BANDS],
tall_tris: Vec::new(),
};
let mut clip = PolyClip { poly: Vec::new(), bounds, bands: Vec::new(), band_h, tris: Vec::new() };
for i in 0..n {
let (Some(a), Some(b)) = (poly.get(i), poly.get((i + 1) % n)) else { continue };
let (lo, hi) = (clip.band(a.y.min(b.y)), clip.band(a.y.max(b.y)));
@@ -298,17 +282,6 @@ impl PolyClip {
.map(|t| (*t, Bounds2::from_points(t.iter().copied())))
.filter(|(_, b)| b.width() > 0.0 && b.height() > 0.0)
.collect();
for k in 0..clip.tris.len() {
let Some((_, tb)) = clip.tris.get(k) else { continue };
let (lo, hi) = (clip.band(tb.min.y), clip.band(tb.max.y));
if hi - lo >= TRI_BAND_SPAN {
clip.tall_tris.push(k as u32);
} else {
for band in clip.tri_bands.iter_mut().take(hi + 1).skip(lo) {
band.push(k as u32);
}
}
}
clip.poly = poly;
clip.bands = bands;
Some(clip)
@@ -431,15 +404,7 @@ impl PolyClip {
out.extend_from_slice(t);
continue;
}
// The clip triangles near `t`, in clip order (the same output as a scan of all of them).
let (lo, hi) = (self.band(tb.min.y), self.band(tb.max.y));
let mut near: Vec<u32> = self.tall_tris.clone();
for band in self.tri_bands.iter().take(hi + 1).skip(lo) {
near.extend_from_slice(band);
}
near.sort_unstable();
near.dedup();
for (c, cb) in near.iter().filter_map(|&k| self.tris.get(k as usize)) {
for (c, cb) in &self.tris {
if !cb.intersects(&tb) {
continue;
}
@@ -625,39 +590,6 @@ mod tests {
assert!(t.iter().all(|p| !(p.x > 5.0 + 1e-9 && p.y > 5.0 + 1e-9)));
}
/// A fill clipped through the band index equals the same fill against a plain scan of every clip
/// triangle (the unindexed result), for a boundary with tall and short triangles.
#[test]
fn indexed_triangle_clipping_equals_a_full_scan() {
// A comb: tall teeth over a base, so its triangles span many bands.
let mut poly = vec![Vec2::new(0.0, 0.0), Vec2::new(40.0, 0.0)];
for i in (0..20).rev() {
let x = i as f64 * 2.0;
poly.extend([
Vec2::new(x + 2.0, 20.0),
Vec2::new(x + 1.0, 20.0),
Vec2::new(x + 1.0, 2.0),
Vec2::new(x + 0.5, 2.0),
Vec2::new(x + 0.5, 20.0),
Vec2::new(x, 20.0),
]);
}
let c = PolyClip::new(&poly).unwrap();
assert!(!c.tall_tris.is_empty());
let fill: Vec<Vec2> = (0..30)
.flat_map(|i| {
let (x, y) = (i as f64 * 1.3, (i % 7) as f64 * 2.5);
[Vec2::new(x, y), Vec2::new(x + 6.0, y), Vec2::new(x + 2.0, y + 9.0)]
})
.collect();
let got = c.clip_triangles(&fill);
let mut scan = c.clone();
scan.tri_bands = vec![(0..scan.tris.len() as u32).collect(); BANDS];
scan.tall_tris.clear();
assert_eq!(got, scan.clip_triangles(&fill));
assert!(!got.is_empty());
}
#[test]
fn degenerate_and_hostile_outlines() {
assert!(PolyClip::new(&[Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)]).is_none());
+1 -1
View File
@@ -42,7 +42,7 @@ pub(crate) fn dimension(
let dim_rgb = ink(style.dim_line_color, layer_color, own);
let ext_rgb = ink(style.ext_line_color, layer_color, own);
let txt_rgb = ink(style.text_color, layer_color, own);
let layer = ctx.layer(layer_name);
let layer = d.layer(layer_name);
let weight = |v: i16| -> f32 {
if !b.opts.lineweights {
return 0.0;
+15 -118
View File
@@ -1,109 +1,40 @@
//! Polygon filling: even-odd regions to triangles (slab decomposition), wide polylines.
use cadcraft_doc::LwPolyline;
use cadcraft_geom::{Bounds2, Polyline, Vec2};
use cadcraft_geom::{Polyline, Vec2};
/// Triangulate an even-odd region bounded by closed loops (holes allowed) by cutting it into
/// horizontal slabs at every vertex y and emitting trapezoids. Two edges that cross inside a slab
/// (a bowtie, two loops crossing each other) swap order there, so such a slab is cut again at the
/// crossing and each part is paired on its own. Robust for any input; output size is
/// O(slabs × crossings), capped for hostile inputs. Groups of loops whose bounding boxes are
/// separated (by a vertical or a horizontal gap, recursively) cannot affect each other's even-odd
/// parity, so each group is cut on its own: disjoint outlines do not multiply each other's slabs.
/// O(slabs × crossings), capped for hostile inputs.
pub fn triangulate_evenodd(loops: &[Vec<Vec2>]) -> Vec<Vec2> {
// (original index, bounds, edges) per loop.
let mut items: Vec<(usize, Bounds2, Vec<(Vec2, Vec2)>)> = Vec::new();
let mut total = 0usize;
for (k, l) in loops.iter().enumerate() {
let mut edges: Vec<(Vec2, Vec2)> = Vec::new();
for l in loops {
let n = l.len();
if n < 3 {
continue;
}
let mut edges = Vec::new();
for i in 0..n {
let (Some(&a), Some(&b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
if (a.y - b.y).abs() > 1e-15 && a.is_finite() && b.is_finite() {
edges.push(if a.y < b.y { (a, b) } else { (b, a) });
}
}
total += edges.len();
if total > 200_000 {
return Vec::new();
}
if !edges.is_empty() {
let bounds = Bounds2::from_points(edges.iter().flat_map(|(a, b)| [*a, *b]));
items.push((k, bounds, edges));
}
}
let mut out = Vec::new();
// Slabs created by cutting at crossings, over the whole region; past this the remaining
// crossings are left unsplit rather than letting a hostile input run away.
let mut cuts_left = MAX_CROSSING_CUTS;
// Loop visits all the partitioning may make: an input that peels one loop off a big group at a
// time would rescan the rest each time, so past this a group is cut as it stands (as before partitioning existed).
let mut split_work = 64 * items.len() + 4_000_000;
let mut stack = vec![items];
while let Some(mut group) = stack.pop() {
if group.is_empty() {
continue;
}
let parts = match split_work.checked_sub(2 * group.len()) {
Some(rest) => {
split_work = rest;
let parts = split_groups(&mut group, true);
if parts.len() > 1 { parts } else { split_groups(&mut group, false) }
}
None => Vec::new(),
};
if parts.len() > 1 {
// Lowest-first when popped.
stack.extend(parts.into_iter().rev());
continue;
}
group.sort_by_key(|g| g.0);
let edges: Vec<(Vec2, Vec2)> = group.into_iter().flat_map(|g| g.2).collect();
if slabs(edges, &mut out, &mut cuts_left) {
break;
}
if edges.is_empty() || edges.len() > 200_000 {
return Vec::new();
}
out
}
type Item = (usize, Bounds2, Vec<(Vec2, Vec2)>);
/// `group` cut into the chains of loops whose extents along x (or y) overlap, in order along it;
/// one chain when nothing separates them (then `group` is left as it was, but for its order).
fn split_groups(group: &mut Vec<Item>, along_x: bool) -> Vec<Vec<Item>> {
let span = |b: &Bounds2| if along_x { (b.min.x, b.max.x) } else { (b.min.y, b.max.y) };
group.sort_by(|a, b| span(&a.1).0.total_cmp(&span(&b.1).0));
let mut parts: Vec<Vec<Item>> = Vec::new();
let mut reach = f64::NEG_INFINITY;
for item in std::mem::take(group) {
match parts.last_mut() {
Some(part) if span(&item.1).0 <= reach => {
reach = reach.max(span(&item.1).1);
part.push(item);
}
_ => {
reach = span(&item.1).1;
parts.push(vec![item]);
}
}
}
if parts.len() == 1 {
*group = parts.pop().unwrap_or_default();
}
parts
}
/// The slab decomposition of `edges` appended to `out`; true once `out` passes its cap.
fn slabs(mut edges: Vec<(Vec2, Vec2)>, out: &mut Vec<Vec2>, cuts_left: &mut usize) -> bool {
let mut ys: Vec<f64> = edges.iter().flat_map(|(a, b)| [a.y, b.y]).collect();
ys.sort_by(f64::total_cmp);
ys.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
edges.sort_by(|a, b| a.0.y.total_cmp(&b.0.y));
let mut out = Vec::new();
let mut active: Vec<(Vec2, Vec2)> = Vec::new();
let mut next = 0usize;
// Slabs created by cutting at crossings, over the whole region; past this the remaining
// crossings are left unsplit rather than letting a hostile input run away.
let mut cuts_left = MAX_CROSSING_CUTS;
let mut pending: Vec<(f64, f64)> = Vec::new();
for w in ys.windows(2) {
let (Some(&y0), Some(&y1)) = (w.first(), w.get(1)) else { continue };
@@ -123,7 +54,7 @@ fn slabs(mut edges: Vec<(Vec2, Vec2)>, out: &mut Vec<Vec2>, cuts_left: &mut usiz
let ymid = (ya + yb) / 2.0;
let mut xs: Vec<(f64, f64, f64)> = active.iter().map(|e| (x_at(e, ymid), x_at(e, ya), x_at(e, yb))).collect();
xs.sort_by(|a, b| a.0.total_cmp(&b.0));
if *cuts_left > 0 {
if cuts_left > 0 {
let mut cuts: Vec<f64> = Vec::new();
for pair in xs.windows(2) {
let (Some(l), Some(r)) = (pair.first(), pair.get(1)) else { continue };
@@ -134,8 +65,8 @@ fn slabs(mut edges: Vec<(Vec2, Vec2)>, out: &mut Vec<Vec2>, cuts_left: &mut usiz
if !cuts.is_empty() {
cuts.sort_by(f64::total_cmp);
cuts.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
cuts.truncate(*cuts_left);
*cuts_left -= cuts.len();
cuts.truncate(cuts_left);
cuts_left -= cuts.len();
// Push the parts top-down so the lowest is processed first.
let mut top = yb;
for &c in cuts.iter().rev() {
@@ -153,11 +84,11 @@ fn slabs(mut edges: Vec<(Vec2, Vec2)>, out: &mut Vec<Vec2>, cuts_left: &mut usiz
}
}
if out.len() > 6_000_000 {
return true;
return out;
}
}
}
false
out
}
/// How many extra slab boundaries crossings may add over one region.
@@ -233,40 +164,6 @@ mod tests {
assert!((area(&t) - 96.0).abs() < 1e-9);
}
/// A thousand disjoint squares side by side (every slab of one would otherwise be cut at the
/// vertex heights of all the others) triangulate in linear output.
#[test]
fn disjoint_outlines_do_not_multiply_each_others_slabs() {
let n = 1_000;
let loops: Vec<Vec<Vec2>> = (0..n)
.map(|i| {
let (x, jitter) = (i as f64 * 3.0, i as f64 * 0.001);
vec![Vec2::new(x, jitter), Vec2::new(x + 2.0, jitter), Vec2::new(x + 2.0, 2.0 + jitter), Vec2::new(x, 2.0 + jitter)]
})
.collect();
let t = triangulate_evenodd(&loops);
assert!((area(&t) - 4.0 * n as f64).abs() < 1e-6);
// Two triangles per square: no slab was cut at another square's height.
assert_eq!(t.len(), 6 * n);
}
/// Rectangles that peel off one at a time along alternating axes (each split isolates a single loop
/// from a large group) still triangulate correctly, with the partition work bounded.
#[test]
fn alternating_peeling_is_bounded_and_correct() {
let m = 1_000;
let (mf, mut loops) = (m as f64, Vec::new());
let rect = |x0: f64, x1: f64, y0: f64, y1: f64| vec![Vec2::new(x0, y0), Vec2::new(x1, y0), Vec2::new(x1, y1), Vec2::new(x0, y1)];
for i in 0..m {
let i = i as f64;
loops.push(rect(2.0 * i, 2.0 * i + 1.0, 2.0 * i, 2.0 * mf));
loops.push(rect(2.0 * i + 2.0, 2.0 * mf + 1.0, 2.0 * i, 2.0 * i + 1.0));
}
let expected: f64 = (0..m).map(|i| (2.0 * mf - 2.0 * i as f64) + (2.0 * mf - 2.0 * i as f64 - 1.0)).sum();
let t = triangulate_evenodd(&loops);
assert!((area(&t) - expected).abs() < 1e-6 * expected, "{} vs {expected}", area(&t));
}
#[test]
fn triangle_area() {
let t = triangulate_evenodd(&[vec![Vec2::new(0.0, 0.0), Vec2::new(4.0, 0.0), Vec2::new(0.0, 3.0)]]);
+1 -33
View File
@@ -78,32 +78,10 @@ fn max_scale(m: &Mat3) -> f64 {
if s.is_finite() { s } else { 0.0 }
}
/// A drawing's layers by lower-cased name (the first of a duplicated name wins, as the table's own
/// search does), built once per display list: a lookup per object over the layer vector multiplies
/// the object count by the layer count.
struct LayerIndex(Option<std::collections::HashMap<String, usize>>);
impl LayerIndex {
fn new(d: &Drawing) -> Self {
let mut m = std::collections::HashMap::with_capacity(d.layers.len());
for (i, l) in d.layers.iter().enumerate() {
m.entry(l.name.to_ascii_lowercase()).or_insert(i);
}
LayerIndex(Some(m))
}
/// No index: lookups scan the layer vector, as `Drawing::layer` does. For a few objects (a
/// rubber band redrawn at every pointer move), where building an index would cost more.
fn linear() -> Self {
LayerIndex(None)
}
}
/// Graphics state inherited through block references.
#[derive(Clone)]
struct Ctx<'a> {
d: &'a Drawing,
layers: &'a LayerIndex,
xf: Mat3,
block_color: Color,
block_layer: Option<String>,
@@ -125,19 +103,12 @@ struct Ctx<'a> {
}
impl Ctx<'_> {
/// The layer called `name` (any case), as [`Drawing::layer`] finds it.
fn layer(&self, name: &str) -> Option<&cadcraft_doc::Layer> {
let Some(map) = &self.layers.0 else { return self.d.layer(name) };
let i = if name.bytes().any(|b| b.is_ascii_uppercase()) { map.get(&name.to_ascii_lowercase()) } else { map.get(name) };
i.and_then(|i| self.d.layers.get(*i))
}
/// A layer's colour, honouring the viewport's override.
fn layer_color(&self, name: &str) -> Option<Color> {
if let Some((_, c)) = self.vp_colors.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)) {
return Some(*c);
}
self.layer(name).map(|l| l.color)
self.d.layer(name).map(|l| l.color)
}
}
@@ -154,8 +125,6 @@ struct Builder<'a> {
shared_budget: Option<&'a std::sync::atomic::AtomicUsize>,
/// Transparency (percent) of the entity being drawn.
transparency: u8,
/// The drawing's layers by name.
layers: &'a LayerIndex,
}
/// Upper bound on the block contents drawn per display list (each block reference, MINSERT copy
@@ -371,7 +340,6 @@ impl<'a> Builder<'a> {
named: self.named,
shared_budget: self.shared_budget,
transparency: self.transparency,
layers: self.layers,
}
}
/// Take one unit of the block-expansion budget; false once it is spent.
+10 -56
View File
@@ -3,9 +3,7 @@ use super::display::append;
use super::display::{DisplayList, Kind, NO_LAYER, PlotText, VIEWPORT_CONTENT};
use super::draw::entity;
use super::viewports::{MAX_VIEWPORTS, viewport};
use super::{
Bounds2, Builder, Color, Ctx, Drawing, Entity, EntityKind, Handle, LayerIndex, Lineweight, Mat3, Space, Vec2, paper, pen, plotstyle, point,
};
use super::{Bounds2, Builder, Color, Ctx, Drawing, Entity, EntityKind, Handle, Lineweight, Mat3, Space, Vec2, paper, pen, plotstyle, point};
/// Build options.
#[derive(Clone, Debug)]
@@ -82,17 +80,9 @@ pub fn build_plot(d: &Drawing, space: &Space, opts: &Options) -> DisplayList {
build_space(d, space, opts, true)
}
pub(crate) fn top_ctx<'a>(
d: &'a Drawing,
layers: &'a LayerIndex,
xf: Mat3,
top: Handle,
frozen: &'a [String],
vp_colors: &'a [(String, Color)],
) -> Ctx<'a> {
pub(crate) fn top_ctx<'a>(d: &'a Drawing, xf: Mat3, top: Handle, frozen: &'a [String], vp_colors: &'a [(String, Color)]) -> Ctx<'a> {
Ctx {
d,
layers,
xf,
block_color: Color::Index(7),
block_layer: None,
@@ -111,17 +101,7 @@ pub(crate) fn top_ctx<'a>(
fn build_space(d: &Drawing, space: &Space, opts: &Options, plotting: bool) -> DisplayList {
let opts = &with_point_view(d, space, opts);
let styler = style_table(d, space, opts, plotting).map(|t| plotstyle::Styler::new(d, t));
let layers = LayerIndex::new(d);
let mut b = Builder {
list: DisplayList::default(),
opts,
plotting,
styles: styler.as_ref(),
named: None,
shared_budget: None,
transparency: 0,
layers: &layers,
};
let mut b = Builder { list: DisplayList::default(), opts, plotting, styles: styler.as_ref(), named: None, shared_budget: None, transparency: 0 };
if let Space::Paper(name) = space {
b.list.sheet = paper::sheet(d, name);
}
@@ -144,7 +124,7 @@ fn build_space(d: &Drawing, space: &Space, opts: &Options, plotting: bool) -> Di
}
continue;
}
entity(&mut b, &top_ctx(d, &layers, Mat3::IDENTITY, e.handle, &[], &[]), e);
entity(&mut b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
}
}
// A layout is drawn in paper units, so its pens become geometry now.
@@ -168,22 +148,14 @@ fn build_parallel(b: &mut Builder, d: &Drawing, store: &cadcraft_doc::EntityStor
use rayon::prelude::*;
let ents: Vec<&std::sync::Arc<Entity>> = store.iter().collect();
let budget = std::sync::atomic::AtomicUsize::new(0);
let (opts, plotting, layers) = (b.opts, b.plotting, b.layers);
let (opts, plotting) = (b.opts, b.plotting);
let parts: Vec<DisplayList> = ents
.par_chunks(PARALLEL_CHUNK)
.map(|chunk| {
let mut part = Builder {
list: DisplayList::default(),
opts,
plotting,
styles: None,
named: None,
shared_budget: Some(&budget),
transparency: 0,
layers,
};
let mut part =
Builder { list: DisplayList::default(), opts, plotting, styles: None, named: None, shared_budget: Some(&budget), transparency: 0 };
for e in chunk {
entity(&mut part, &top_ctx(d, layers, Mat3::IDENTITY, e.handle, &[], &[]), e);
entity(&mut part, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
}
part.list
})
@@ -222,10 +194,6 @@ pub(crate) fn with_point_view(d: &Drawing, space: &Space, opts: &Options) -> Opt
o
}
/// Loose entity lists shorter than this, with no block reference (whose contents expand to many
/// objects), look layers up by scanning rather than building an index.
const SMALL_LIST: usize = 64;
/// Build the display list for a set of loose entities (previews, rubber bands).
pub fn build_entities<'a, I: IntoIterator<Item = &'a Entity>>(d: &Drawing, ents: I, opts: &Options) -> DisplayList {
let ents: Vec<&Entity> = ents.into_iter().collect();
@@ -237,23 +205,9 @@ pub fn build_entities<'a, I: IntoIterator<Item = &'a Entity>>(d: &Drawing, ents:
} else {
opts
};
let layers = if ents.len() < SMALL_LIST && !ents.iter().any(|e| matches!(e.kind, EntityKind::Insert(_))) {
LayerIndex::linear()
} else {
LayerIndex::new(d)
};
let mut b = Builder {
list: DisplayList::default(),
opts,
plotting: false,
styles: None,
named: None,
shared_budget: None,
transparency: 0,
layers: &layers,
};
let mut b = Builder { list: DisplayList::default(), opts, plotting: false, styles: None, named: None, shared_budget: None, transparency: 0 };
for e in ents {
entity(&mut b, &top_ctx(d, &layers, Mat3::IDENTITY, e.handle, &[], &[]), e);
entity(&mut b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
}
b.list
}
+1 -1
View File
@@ -59,7 +59,7 @@ impl Styler {
let normal = || by_name(cadcraft_doc::NORMAL_STYLE);
let p = c.plot_style.trim();
if p.is_empty() || p.eq_ignore_ascii_case("bylayer") {
ctx.layer(layer_name).and_then(|l| by_name(&l.plot_style)).or_else(normal)
ctx.d.layer(layer_name).and_then(|l| by_name(&l.plot_style)).or_else(normal)
} else if p.eq_ignore_ascii_case("byblock") {
ctx.block_pstyle.or_else(normal)
} else {
+1 -1
View File
@@ -14,7 +14,7 @@ pub(crate) fn resolve_common(ctx: &Ctx, c: &cadcraft_doc::Common, plotting: bool
let d = ctx.d;
// Layer "0" inside a block takes the insert's layer.
let layer_name = if c.layer == "0" { ctx.block_layer.as_deref().unwrap_or("0") } else { c.layer.as_str() };
let layer = ctx.layer(layer_name);
let layer = d.layer(layer_name);
let visible =
c.visible && layer.is_none_or(|l| l.visible() && (!plotting || l.plot)) && !ctx.frozen.iter().any(|f| f.eq_ignore_ascii_case(layer_name));
let layer_color = ctx.layer_color(layer_name).unwrap_or(Color::Index(7));
-17
View File
@@ -1217,20 +1217,3 @@ fn rotated_and_reversed_copies_of_a_curved_loop_fill_once() {
});
assert_eq!(loops_of(&h).len(), 2);
}
/// The per-list layer index finds what `Drawing::layer` finds: any case, the first of a duplicate.
#[test]
fn the_layer_index_agrees_with_the_layer_table() {
let mut d = Drawing::new_imperial();
let mut first = cadcraft_doc::Layer::new("Walls");
first.color = Color::Index(1);
let mut dup = cadcraft_doc::Layer::new("WALLS");
dup.color = Color::Index(2);
d.layers.extend([first, dup, cadcraft_doc::Layer::new("Doors")]);
for index in [LayerIndex::new(&d), LayerIndex::linear()] {
let top = crate::pipeline::top_ctx(&d, &index, cadcraft_geom::Mat3::IDENTITY, cadcraft_doc::Handle(1), &[], &[]);
for name in ["Walls", "walls", "WALLS", "doors", "0", "Missing"] {
assert_eq!(top.layer(name).map(|l| (&l.name, l.color)), d.layer(name).map(|l| (&l.name, l.color)), "{name}");
}
}
}
+5 -19
View File
@@ -5,9 +5,7 @@ use super::draw::entity;
#[cfg(not(target_arch = "wasm32"))]
use super::pipeline::PARALLEL_CHUNK;
use super::pipeline::{Options, style_table, top_ctx, with_point_view};
use super::{
Bounds2, Builder, Ctx, Drawing, Entity, EntityKind, Handle, LayerIndex, MAX_BLOCK_EXPANSION, Mat3, Pen, Space, Vec2, clip, paper, pen, plotstyle,
};
use super::{Bounds2, Builder, Ctx, Drawing, Entity, EntityKind, Handle, MAX_BLOCK_EXPANSION, Mat3, Pen, Space, Vec2, clip, paper, pen, plotstyle};
/// Upper bound on viewports drawn per layout (hostile files).
pub(crate) const MAX_VIEWPORTS: usize = 256;
@@ -23,7 +21,7 @@ pub(crate) fn viewport(b: &mut Builder, d: &Drawing, e: &Entity, vp: &cadcraft_d
let poly = viewport_clip(d, e.handle, vp);
// Border (on the viewport's layer; layer off hides only the border).
if poly.is_none() {
entity(b, &top_ctx(d, b.layers, Mat3::IDENTITY, e.handle, &[], &[]), e);
entity(b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
}
if b.opts.skip_viewport == Some(e.handle) {
return;
@@ -69,17 +67,8 @@ pub fn viewport_content(d: &Drawing, space: &Space, handle: Handle, area: Bounds
}
let opts = &with_point_view(d, space, opts);
let styler = style_table(d, space, opts, false).map(|t| plotstyle::Styler::new(d, t));
let layers = LayerIndex::new(d);
let mut b = Builder {
list: DisplayList::default(),
opts,
plotting: false,
styles: styler.as_ref(),
named: None,
shared_budget: None,
transparency: 0,
layers: &layers,
};
let mut b =
Builder { list: DisplayList::default(), opts, plotting: false, styles: styler.as_ref(), named: None, shared_budget: None, transparency: 0 };
contents(&mut b, d, vp, area, &Clip::Rect(area), paper::unit_mm_of(d, handle));
b.list
}
@@ -108,8 +97,7 @@ fn contents(b: &mut Builder, d: &Drawing, vp: &cadcraft_doc::Viewport, area: Bou
// PSLTSCALE on: model-space dashes are scaled so they measure the same on paper as in
// paper space, whatever the viewport scale.
let lt_factor = if d.header.i64("PSLTSCALE", 1) != 0 { 1.0 / s } else { 1.0 };
let layers = b.layers;
let ctx = || Ctx { lt_factor, ..top_ctx(d, layers, xf, VIEWPORT_CONTENT, &vp.frozen_layers, &vp.layer_colors) };
let ctx = || Ctx { lt_factor, ..top_ctx(d, xf, VIEWPORT_CONTENT, &vp.frozen_layers, &vp.layer_colors) };
let seen = |me: &Entity| {
if matches!(me.kind, EntityKind::Viewport(_)) {
return false;
@@ -132,7 +120,6 @@ fn contents(b: &mut Builder, d: &Drawing, vp: &cadcraft_doc::Viewport, area: Bou
named: None,
shared_budget: Some(&budget),
transparency: 0,
layers,
};
#[cfg(not(target_arch = "wasm32"))]
let parallel = !b.plotting && b.styles.is_none() && d.model.len() >= 2 * PARALLEL_CHUNK;
@@ -156,7 +143,6 @@ fn contents(b: &mut Builder, d: &Drawing, vp: &cadcraft_doc::Viewport, area: Bou
named: None,
shared_budget: Some(&budget),
transparency: 0,
layers,
};
let c = ctx();
for me in chunk.iter().filter(|me| seen(me)) {