M0: workspace, geometry foundation, sketch constraint solver, kernel boundary

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-07 17:01:29 +00:00
commit dafce1f4e2
47 files changed
+5624

No files matched your search

+273
View File
@@ -0,0 +1,273 @@
//! Closed-profile detection.
//!
//! Non-construction lines and arcs form a planar graph (end points within a small tolerance are
//! merged). Dangling edges are pruned, then faces are traced with the "first clockwise turn"
//! rule, which yields every bounded region counter-clockwise. Circles are loops of their own.
//! A loop that lies inside another loop of a different connected component becomes a hole of
//! the innermost loop containing it, so a circle inside a rectangle gives two profiles: the
//! rectangle with a hole, and the disc.
use std::f64::consts::TAU;
use serde::{Deserialize, Serialize};
use solvecraft_geom::{Loop2, Region2, Seg2, Vec2};
use crate::model::{CurveKind, Sketch};
const MERGE_TOL: f64 = 1e-6;
const MAX_EDGES: usize = 50_000;
/// A closed region usable by features, with the ids of the curves bounding it.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Profile {
pub region: Region2,
/// Curve ids of the outer loop.
pub outer_curves: Vec<String>,
/// Curve ids of each hole loop.
pub hole_curves: Vec<Vec<String>>,
pub area: f64,
pub centroid: Vec2,
}
impl Profile {
/// All curve ids bounding the profile.
pub fn curve_ids(&self) -> Vec<String> {
let mut v = self.outer_curves.clone();
for h in &self.hole_curves {
v.extend(h.iter().cloned());
}
v
}
}
struct Edge {
curve: usize,
seg: Seg2,
u: usize,
v: usize,
}
fn find(p: &mut [usize], mut i: usize) -> usize {
while let Some(&q) = p.get(i) {
if q == i {
break;
}
i = q;
}
i
}
/// Find all closed profiles of a sketch.
pub fn find_profiles(sk: &Sketch) -> Vec<Profile> {
// Node ids: point indices merged by position.
let n = sk.points.len();
let mut parent: Vec<usize> = (0..n).collect();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|a, b| {
let (pa, pb) = (sk.points.get(*a).map(|p| p.pos.x).unwrap_or(0.0), sk.points.get(*b).map(|p| p.pos.x).unwrap_or(0.0));
pa.total_cmp(&pb)
});
for (k, &i) in order.iter().enumerate() {
let Some(pi) = sk.points.get(i).map(|p| p.pos) else { continue };
for &j in order.iter().skip(k + 1) {
let Some(pj) = sk.points.get(j).map(|p| p.pos) else { continue };
if pj.x - pi.x > MERGE_TOL {
break;
}
if pi.dist(pj) <= MERGE_TOL {
let (ri, rj) = (find(&mut parent, i), find(&mut parent, j));
if ri != rj
&& let Some(s) = parent.get_mut(rj)
{
*s = ri;
}
}
}
}
let mut loops: Vec<(Loop2, Vec<String>, usize)> = Vec::new(); // (ccw loop, curve ids, component)
let mut edges: Vec<Edge> = Vec::new();
for (ci, c) in sk.curves.iter().enumerate() {
if c.construction {
continue;
}
match c.kind {
CurveKind::Circle { .. } => {
let segs = sk.segs(ci);
if !segs.is_empty() {
loops.push((Loop2 { segs }, vec![c.id.clone()], usize::MAX - ci));
}
}
CurveKind::Line { a, b } | CurveKind::Arc { a, b, .. } => {
let (u, v) = (find(&mut parent, a), find(&mut parent, b));
if u == v {
continue;
}
if let Some(seg) = sk.segs(ci).into_iter().next()
&& seg.length() > MERGE_TOL
{
edges.push(Edge { curve: ci, seg, u, v });
}
}
}
if edges.len() > MAX_EDGES {
break;
}
}
// Prune dangling edges.
let mut alive = vec![true; edges.len()];
loop {
let mut deg = vec![0usize; n];
for (e, a) in edges.iter().zip(&alive) {
if *a {
if let Some(d) = deg.get_mut(e.u) {
*d += 1;
}
if let Some(d) = deg.get_mut(e.v) {
*d += 1;
}
}
}
let mut changed = false;
for (e, a) in edges.iter().zip(alive.iter_mut()) {
if *a && (deg.get(e.u).copied().unwrap_or(0) < 2 || deg.get(e.v).copied().unwrap_or(0) < 2) {
*a = false;
changed = true;
}
}
if !changed {
break;
}
}
// Components over nodes for nesting.
let mut comp: Vec<usize> = (0..n).collect();
for (e, a) in edges.iter().zip(&alive) {
if *a {
let (ru, rv) = (find(&mut comp, e.u), find(&mut comp, e.v));
if ru != rv
&& let Some(s) = comp.get_mut(rv)
{
*s = ru;
}
}
}
// Half-edges: 2e = forward (u→v), 2e+1 = backward.
let half = |h: usize| -> Option<(Seg2, usize, usize)> {
let e = edges.get(h / 2)?;
Some(if h.is_multiple_of(2) { (e.seg, e.u, e.v) } else { (e.seg.reversed(), e.v, e.u) })
};
let mut outgoing: Vec<Vec<usize>> = vec![Vec::new(); n];
for (ei, a) in alive.iter().enumerate() {
if !*a {
continue;
}
for h in [2 * ei, 2 * ei + 1] {
if let Some((_, from, _)) = half(h)
&& let Some(o) = outgoing.get_mut(from)
{
o.push(h);
}
}
}
let mut visited = vec![false; edges.len() * 2];
for start in 0..edges.len() * 2 {
if !alive.get(start / 2).copied().unwrap_or(false) || visited.get(start).copied().unwrap_or(true) {
continue;
}
let mut segs = Vec::new();
let mut ids = Vec::new();
let mut h = start;
let mut ok = true;
for _ in 0..=edges.len() * 2 {
if let Some(v) = visited.get_mut(h) {
*v = true;
}
let Some((seg, _, to)) = half(h) else {
ok = false;
break;
};
segs.push(seg);
if let Some(e) = edges.get(h / 2)
&& let Some(c) = sk.curves.get(e.curve)
{
ids.push(c.id.clone());
}
let back = (-seg.end_tangent()).angle();
let twin = h ^ 1;
let mut best: Option<(f64, usize)> = None;
for &o in outgoing.get(to).map(Vec::as_slice).unwrap_or(&[]) {
if o == twin {
continue;
}
let Some((os, _, _)) = half(o) else { continue };
let mut d = (back - os.start_tangent().angle()) % TAU;
if d <= 1e-12 {
d += TAU;
}
if best.is_none_or(|(bd, _)| d < bd) {
best = Some((d, o));
}
}
let Some((_, next)) = best else {
ok = false;
break;
};
h = next;
if h == start {
break;
}
if visited.get(h).copied().unwrap_or(true) {
ok = false;
break;
}
}
if !ok || h != start {
continue;
}
let lp = Loop2 { segs };
if lp.signed_area() > 1e-9 {
let c = edges.get(start / 2).map(|e| find(&mut comp, e.u)).unwrap_or(0);
loops.push((lp, ids, c));
}
}
// Nesting: parent = smallest containing loop from another component.
let areas: Vec<f64> = loops.iter().map(|(l, _, _)| l.signed_area().abs()).collect();
let mut parent_of: Vec<Option<usize>> = vec![None; loops.len()];
for (i, (li, _, ci)) in loops.iter().enumerate() {
let probe = li.segs.first().map(Seg2::mid).unwrap_or_default();
let ai = areas.get(i).copied().unwrap_or(0.0);
let mut best: Option<(f64, usize)> = None;
for (j, (lj, _, cj)) in loops.iter().enumerate() {
let aj = areas.get(j).copied().unwrap_or(0.0);
if i == j || ci == cj || aj <= ai {
continue;
}
if lj.contains(probe) && best.is_none_or(|(ba, _)| aj < ba) {
best = Some((aj, j));
}
}
if let Some(p) = parent_of.get_mut(i) {
*p = best.map(|(_, j)| j);
}
}
let mut out: Vec<Profile> = loops
.iter()
.enumerate()
.map(|(i, (l, ids, _))| {
let kids: Vec<usize> = (0..loops.len()).filter(|k| parent_of.get(*k).copied().flatten() == Some(i)).collect();
let holes: Vec<Loop2> = kids.iter().filter_map(|k| loops.get(*k).map(|(h, _, _)| h.ccw().reversed())).collect();
let hole_curves: Vec<Vec<String>> = kids.iter().filter_map(|k| loops.get(*k).map(|(_, ids, _)| ids.clone())).collect();
let region = Region2 { outer: l.ccw(), holes };
let area = region.area();
let centroid = region.centroid();
Profile { region, outer_curves: ids.clone(), hole_curves, area, centroid }
})
.collect();
let first_curve = |p: &Profile| p.outer_curves.iter().filter_map(|id| sk.curve_index(id)).min().unwrap_or(usize::MAX);
out.sort_by(|a, b| first_curve(a).cmp(&first_curve(b)).then(b.area.total_cmp(&a.area)));
out
}