* WIP deep rename (not for push) * WIP2 * WIP3 * WIP4 wxs fix * rustfmt * Rename the remaining identifiers in merged crash-report code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
344 lines
14 KiB
Rust
344 lines
14 KiB
Rust
//! Conservative analytic boundary roles for associative face identity.
|
|
//! Endpoint walks exclude support centres. Crossings/touches refuse before nesting.
|
|
use super::{LCurve, curve_points};
|
|
use crate::{Shape, intersections};
|
|
use solidcraft_geom::Vec2;
|
|
use std::f64::consts::TAU;
|
|
|
|
struct Boundary {
|
|
curves: Vec<usize>,
|
|
shapes: Vec<Shape>,
|
|
lo: Vec2,
|
|
hi: Vec2,
|
|
depth: usize,
|
|
signature: [usize; 3],
|
|
}
|
|
pub(super) struct Roles {
|
|
pub old: Vec<Vec<usize>>,
|
|
pub fresh: Vec<Vec<usize>>,
|
|
pub old_normal: Vec<Vec2>,
|
|
pub fresh_normal: Vec<Vec2>,
|
|
pub old_lines: Vec<Vec<Vec2>>,
|
|
pub fresh_lines: Vec<Vec<Vec2>>,
|
|
}
|
|
|
|
pub(super) fn same_lines(a: &[Vec2], b: &[Vec2]) -> bool {
|
|
a.len() == b.len() && a.iter().all(|x| b.iter().any(|y| x.dot(*y).abs() > 0.98))
|
|
}
|
|
fn on(s: &Shape, p: Vec2, tol: f64) -> bool {
|
|
match *s {
|
|
Shape::Line { a, b } => {
|
|
let d = b - a;
|
|
let t = (p - a).dot(d) / d.len2().max(1e-30);
|
|
t >= -tol / d.len().max(tol) && t <= 1.0 + tol / d.len().max(tol) && (a + d * t).dist(p) <= tol
|
|
}
|
|
Shape::Round { c, r, start, sweep } => {
|
|
(p.dist(c) - r).abs() <= tol && (sweep >= TAU - 1e-12 || ((p - c).angle() - start).rem_euclid(TAU) <= sweep + tol / r)
|
|
}
|
|
}
|
|
}
|
|
fn endpoints(s: &Shape) -> [Vec2; 2] {
|
|
let (_, end) = s.range();
|
|
[s.point(0.0), s.point(end)]
|
|
}
|
|
fn stable_intersections(a: &Shape, b: &Shape, tol: f64) -> Vec<Vec2> {
|
|
match (*a, *b) {
|
|
(Shape::Line { a, b }, Shape::Round { c, r, .. }) | (Shape::Round { c, r, .. }, Shape::Line { a, b }) => {
|
|
let Some(direction) = (b - a).normalized() else { return Vec::new() };
|
|
// Work in the circle's local orthonormal frame. The world-coordinate
|
|
// quadratic discriminant can lose every significant bit on long lines.
|
|
let foot = a + direction * (c - a).dot(direction);
|
|
let distance = foot.dist(c);
|
|
if distance > r + tol {
|
|
return Vec::new();
|
|
}
|
|
let half = ((r - distance).max(0.0) * (r + distance)).sqrt();
|
|
vec![foot - direction * half, foot + direction * half]
|
|
}
|
|
(Shape::Round { c: ac, r: ar, .. }, Shape::Round { c: bc, r: br, .. }) => {
|
|
let ((small, r), (large, radius)) = if ar <= br { ((ac, ar), (bc, br)) } else { ((bc, br), (ac, ar)) };
|
|
let delta = large - small;
|
|
let distance = delta.len();
|
|
if distance <= tol || distance > radius + r + tol || distance < radius - r - tol {
|
|
return Vec::new();
|
|
}
|
|
let direction = delta / distance;
|
|
// Compute from the smaller circle; factoring d^2-R^2 avoids the
|
|
// cancellation of R^2-a^2 for a small hole crossing a huge circle.
|
|
let along = ((distance - radius) * (distance + radius) + r * r) / (2.0 * distance);
|
|
let height = ((r - along).max(0.0) * (r + along).max(0.0)).sqrt();
|
|
let foot = small + direction * along;
|
|
vec![foot - direction.perp() * height, foot + direction.perp() * height]
|
|
}
|
|
_ => intersections(a, b),
|
|
}
|
|
}
|
|
fn clear(a: &Shape, b: &Shape, shared: &[Vec2], tol: f64) -> bool {
|
|
let allowed = |p: Vec2| {
|
|
shared.iter().any(|q| {
|
|
let joint_tol = 1e-7 + 128.0 * f64::EPSILON * (1.0 + p.len().max(q.len()));
|
|
q.dist(p) <= joint_tol
|
|
})
|
|
};
|
|
// Parallel coincident lines/circles have no isolated intersections.
|
|
// Interior overlap is nevertheless ambiguous, including duplicate boundaries.
|
|
for s in [a, b] {
|
|
let other = if std::ptr::eq(s, a) { b } else { a };
|
|
let (_, end) = s.range();
|
|
for t in [0.0, 0.5 * end, end] {
|
|
let p = s.point(t);
|
|
if on(other, p, tol) && !allowed(p) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
stable_intersections(a, b, tol).into_iter().all(|p| !on(a, p, tol) || !on(b, p, tol) || allowed(p))
|
|
}
|
|
fn contains(boundary: &Boundary, p: Vec2, tol: f64) -> Option<bool> {
|
|
let mut crossings = 0;
|
|
for shape in &boundary.shapes {
|
|
// Avoid ray/vertex coincidences rather than guessing a half-open arc rule.
|
|
if endpoints(shape).iter().any(|q| (q.y - p.y).abs() <= tol) {
|
|
return None;
|
|
}
|
|
match *shape {
|
|
Shape::Line { a, b } => {
|
|
if (a.y > p.y) != (b.y > p.y) {
|
|
let x = a.x + (p.y - a.y) * (b.x - a.x) / (b.y - a.y);
|
|
if (x - p.x).abs() <= tol {
|
|
return None;
|
|
}
|
|
crossings += usize::from(x > p.x);
|
|
}
|
|
}
|
|
Shape::Round { c, r, start, sweep } => {
|
|
let y = (p.y - c.y) / r;
|
|
if (y.abs() - 1.0).abs() <= tol / r {
|
|
return None;
|
|
}
|
|
if y.abs() < 1.0 {
|
|
let angle = y.asin();
|
|
for theta in [angle, std::f64::consts::PI - angle] {
|
|
let off = (theta - start).rem_euclid(TAU);
|
|
if off < sweep {
|
|
let x = c.x + r * theta.cos();
|
|
if (x - p.x).abs() <= tol {
|
|
return None;
|
|
}
|
|
crossings += usize::from(x > p.x);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Some(crossings % 2 == 1)
|
|
}
|
|
fn boundaries(curves: &[LCurve], pts: &[Vec2]) -> Option<Vec<Boundary>> {
|
|
let get = |i| pts.get(i).copied();
|
|
let mut shapes = Vec::new();
|
|
let mut ends = Vec::new();
|
|
let mut incidence = vec![Vec::new(); pts.len()];
|
|
for (i, curve) in curves.iter().enumerate() {
|
|
let (shape, ep) = match *curve {
|
|
LCurve::Line(a, b) => (Shape::Line { a: get(a)?, b: get(b)? }, Some((a, b))),
|
|
LCurve::Arc(c, a, b) => {
|
|
let (c, a_pos, b_pos) = (get(c)?, get(a)?, get(b)?);
|
|
let sweep = ((b_pos - c).angle() - (a_pos - c).angle()).rem_euclid(TAU);
|
|
(Shape::Round { c, r: c.dist(a_pos), start: (a_pos - c).angle(), sweep }, Some((a, b)))
|
|
}
|
|
LCurve::Circle(c, r) => (Shape::Round { c: get(c)?, r, start: 0.0, sweep: TAU }, None),
|
|
// Existing free-form identity matching remains conservative and unchanged.
|
|
_ => return None,
|
|
};
|
|
if let Some((a, b)) = ep {
|
|
incidence.get_mut(a)?.push(i);
|
|
incidence.get_mut(b)?.push(i);
|
|
}
|
|
shapes.push(shape);
|
|
ends.push(ep);
|
|
}
|
|
if incidence.iter().any(|v| !v.is_empty() && v.len() != 2) {
|
|
return None;
|
|
}
|
|
let mut visited = vec![false; curves.len()];
|
|
let mut out = Vec::new();
|
|
for first in 0..curves.len() {
|
|
if *visited.get(first)? {
|
|
continue;
|
|
}
|
|
let mut members = Vec::new();
|
|
let mut pending = vec![first];
|
|
while let Some(i) = pending.pop() {
|
|
if *visited.get(i)? {
|
|
continue;
|
|
}
|
|
*visited.get_mut(i)? = true;
|
|
members.push(i);
|
|
if let Some((a, b)) = *ends.get(i)? {
|
|
pending.extend(incidence.get(a)?);
|
|
pending.extend(incidence.get(b)?);
|
|
}
|
|
}
|
|
let mut lo = Vec2::new(f64::INFINITY, f64::INFINITY);
|
|
let mut hi = Vec2::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
|
|
let mut signature = [0; 3];
|
|
let mut bound_shapes = Vec::new();
|
|
for i in &members {
|
|
let shape = *shapes.get(*i)?;
|
|
let mut candidates = endpoints(&shape).to_vec();
|
|
if let Shape::Round { c, r, .. } = shape {
|
|
for angle in [0.0, std::f64::consts::FRAC_PI_2, std::f64::consts::PI, 3.0 * std::f64::consts::FRAC_PI_2] {
|
|
let p = c + Vec2::from_angle(angle) * r;
|
|
if on(&shape, p, 1e-7) {
|
|
candidates.push(p);
|
|
}
|
|
}
|
|
}
|
|
for p in candidates {
|
|
lo = Vec2::new(lo.x.min(p.x), lo.y.min(p.y));
|
|
hi = Vec2::new(hi.x.max(p.x), hi.y.max(p.y));
|
|
}
|
|
signature[match curves.get(*i)? {
|
|
LCurve::Line(..) => 0,
|
|
LCurve::Arc(..) => 1,
|
|
_ => 2,
|
|
}] += 1;
|
|
bound_shapes.push(shape);
|
|
}
|
|
out.push(Boundary { curves: members, shapes: bound_shapes, lo, hi, depth: 0, signature });
|
|
}
|
|
let scale = out.iter().map(|b| (b.hi - b.lo).len()).fold(1.0, f64::max);
|
|
let tol = (scale * 1e-10).max(1e-7);
|
|
let mut work = 0usize;
|
|
for i in 0..out.len() {
|
|
for j in i..out.len() {
|
|
for (a_i, a) in out.get(i)?.shapes.iter().enumerate() {
|
|
for (b_i, b) in out.get(j)?.shapes.iter().enumerate() {
|
|
if i == j && a_i >= b_i {
|
|
continue;
|
|
}
|
|
work += 1;
|
|
if work > 262_144 {
|
|
return None;
|
|
}
|
|
let mut shared = Vec::new();
|
|
if i == j {
|
|
let ai = *out.get(i)?.curves.get(a_i)?;
|
|
let bi = *out.get(j)?.curves.get(b_i)?;
|
|
if let (Some((a0, a1)), Some((b0, b1))) = (*ends.get(ai)?, *ends.get(bi)?) {
|
|
for point in [a0, a1] {
|
|
if point == b0 || point == b1 {
|
|
shared.push(get(point)?);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !clear(a, b, &shared, tol) {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for i in 0..out.len() {
|
|
let mut depth = 0;
|
|
for j in 0..out.len() {
|
|
if i == j {
|
|
continue;
|
|
}
|
|
let shape = out.get(i)?.shapes.first()?;
|
|
let (_, end) = shape.range();
|
|
let inside = [0.317, 0.613, 0.823].into_iter().find_map(|t| contains(out.get(j)?, shape.point(end * t), tol))?;
|
|
depth += usize::from(inside);
|
|
}
|
|
out.get_mut(i)?.depth = depth;
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
pub(super) fn valid(curves: &[LCurve], points: &[Vec2]) -> bool {
|
|
boundaries(curves, points).is_some()
|
|
}
|
|
fn normalize(p: Vec2, lo: Vec2, hi: Vec2) -> Vec2 {
|
|
Vec2::new((p.x - lo.x) / (hi.x - lo.x).max(1e-9), (p.y - lo.y) / (hi.y - lo.y).max(1e-9))
|
|
}
|
|
pub(super) fn roles(old_curves: &[LCurve], a: &[Vec2], fresh_curves: &[LCurve], b: &[Vec2]) -> Option<Roles> {
|
|
let (previous, incoming) = (boundaries(old_curves, a)?, boundaries(fresh_curves, b)?);
|
|
if previous.len() != incoming.len() {
|
|
return None;
|
|
}
|
|
let bounds = |loops: &[Boundary]| {
|
|
loops.iter().fold((Vec2::new(f64::INFINITY, f64::INFINITY), Vec2::new(f64::NEG_INFINITY, f64::NEG_INFINITY)), |(lo, hi), x| {
|
|
(Vec2::new(lo.x.min(x.lo.x), lo.y.min(x.lo.y)), Vec2::new(hi.x.max(x.hi.x), hi.y.max(x.hi.y)))
|
|
})
|
|
};
|
|
let ((alo, ahi), (blo, bhi)) = (bounds(&previous), bounds(&incoming));
|
|
let mut mapping = Vec::new();
|
|
for x in &previous {
|
|
let mut candidates: Vec<_> = incoming
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, y)| x.depth == y.depth && x.signature == y.signature)
|
|
.map(|(i, y)| (i, normalize((x.lo + x.hi) * 0.5, alo, ahi).dist(normalize((y.lo + y.hi) * 0.5, blo, bhi))))
|
|
.collect();
|
|
candidates.sort_by(|x, y| x.1.total_cmp(&y.1));
|
|
let (i, d) = *candidates.first()?;
|
|
if mapping.contains(&i) || (candidates.len() > 1 && (d >= 0.1 || candidates.get(1).is_some_and(|(_, second)| d >= second * 0.5))) {
|
|
return None;
|
|
}
|
|
let y = incoming.get(i)?;
|
|
// Preserve the conservative bound on relative boundary changes. This does
|
|
// not mistake a stationary hole's coordinates for the outer loop's scaling.
|
|
let old_size = normalize(x.hi, alo, ahi) - normalize(x.lo, alo, ahi);
|
|
let new_size = normalize(y.hi, blo, bhi) - normalize(y.lo, blo, bhi);
|
|
if old_size.dist(new_size) >= 0.1 {
|
|
return None;
|
|
}
|
|
mapping.push(i);
|
|
}
|
|
let membership = |loops: &[Boundary], curves: &[LCurve], pts: &[Vec2], remap: Option<&[usize]>| -> Option<(Vec<Vec<usize>>, Vec<Vec2>)> {
|
|
let mut roles = vec![Vec::new(); pts.len()];
|
|
let mut coordinates = vec![Vec2::ZERO; pts.len()];
|
|
for (i, loop_) in loops.iter().enumerate() {
|
|
let target = remap.and_then(|m| m.get(i)).copied().unwrap_or(i);
|
|
for curve in &loop_.curves {
|
|
for p in curve_points(curves.get(*curve)?) {
|
|
if !roles.get(p)?.contains(&target) {
|
|
roles.get_mut(p)?.push(target);
|
|
*coordinates.get_mut(p)? += normalize(*pts.get(p)?, loop_.lo, loop_.hi);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (role, coordinate) in roles.iter_mut().zip(&mut coordinates) {
|
|
if role.is_empty() {
|
|
return None;
|
|
}
|
|
role.sort_unstable();
|
|
*coordinate = *coordinate / role.len() as f64;
|
|
}
|
|
Some((roles, coordinates))
|
|
};
|
|
let ((old, old_normal), (fresh, fresh_normal)) =
|
|
(membership(&previous, old_curves, a, Some(&mapping))?, membership(&incoming, fresh_curves, b, None)?);
|
|
let line_roles = |loops: &[Boundary], curves: &[LCurve], pts: &[Vec2]| -> Option<Vec<Vec<Vec2>>> {
|
|
let mut roles = vec![Vec::new(); pts.len()];
|
|
for loop_ in loops {
|
|
let size = loop_.hi - loop_.lo;
|
|
for i in &loop_.curves {
|
|
if let LCurve::Line(a, b) = *curves.get(*i)? {
|
|
let d = *pts.get(b)? - *pts.get(a)?;
|
|
let direction = Vec2::new(d.x / size.x.max(1e-9), d.y / size.y.max(1e-9)).normalized()?;
|
|
roles.get_mut(a)?.push(direction);
|
|
roles.get_mut(b)?.push(direction);
|
|
}
|
|
}
|
|
}
|
|
Some(roles)
|
|
};
|
|
let old_lines = line_roles(&previous, old_curves, a)?;
|
|
let fresh_lines = line_roles(&incoming, fresh_curves, b)?;
|
|
Some(Roles { old, fresh, old_normal, fresh_normal, old_lines, fresh_lines })
|
|
}
|