mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 11:05:56 +00:00
kernel: an intersection curve is where SMLib's imprint cuts, exactly
sketch.intersection_curve (LinkSource::Intersection, crates/doc/src/
project.rs) intersected the two bodies' display meshes triangle by triangle
and chained the segments into polylines: a curve known only as points, off
the true intersection by the meshes' chord.
kernel::meeting_edges(a, a_face, b, b_face, chord) takes each side whole or
as one face (that face alone as a surface body, surfaces::taken), tags both
and imprints the first by the second in SMLib (smlib_imprint: "each of its
faces that `tool` meets cut along the intersection", capi/include/smlib.h).
SMLib intersects the faces' surfaces and trims each curve to both faces, so
the edges the imprint makes are exactly where they meet; its own history
says which they are (an edge no edge of either operand became). Each comes
back as its record curve on its interval, from SMLib's export of the
imprinted body, with its points from that body's mesh at `chord`. Sides that
do not meet give none.
The intersection link now stores each edge as a wire with its exact curve
(Wire::exact, the existing WireCurve; no new type or format change), its
points at 1/1000 of the larger body. project3d::mesh_intersection, its
triangle/triangle test and segment chaining go; project3d::mesh_of becomes
part_of (the body and, for a face reference, the face index).
Needs in crates/kernel/src/lib.rs (the coordinator's): `mod intersection;`
and `pub use intersection::{MeetingEdge, meeting_edges};`.
Not built: base in progress (kernel-smlib/no-truck 513ebe47 does not build).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
@@ -213,10 +213,15 @@ fn resolve_uncached(
|
||||
Vec::new()
|
||||
}
|
||||
LinkSource::Intersection { a, b } => {
|
||||
let (ma, fa) = crate::project3d::mesh_of(st, a).ok_or_else(|| lost(&a.describe()))?;
|
||||
let (mb, fb) = crate::project3d::mesh_of(st, b).ok_or_else(|| lost(&b.describe()))?;
|
||||
wires = crate::project3d::mesh_intersection(&ma, fa, &mb, fb);
|
||||
exact = vec![Vec::new(); wires.len()];
|
||||
// Where the bodies (or faces) meet, as the edges SMLib's imprint of one by the other
|
||||
// makes: each its exact record curve, its points at the display tolerance.
|
||||
let (ba, fa) = crate::project3d::part_of(st, a).ok_or_else(|| lost(&a.describe()))?;
|
||||
let (bb, fb) = crate::project3d::part_of(st, b).ok_or_else(|| lost(&b.describe()))?;
|
||||
let chord = (ba.body.size().max(bb.body.size()) * 1e-3).max(1e-3);
|
||||
for e in crate::kernel::meeting_edges(&ba.body, fa, &bb.body, fb, chord)? {
|
||||
wires.push(e.points);
|
||||
exact.push(vec![WireCurve { curve: e.curve, interval: e.interval }]);
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
LinkSource::Iso { body, at, dir } => {
|
||||
|
||||
+8
-168
@@ -1,26 +1,20 @@
|
||||
//! 3D sketch curves from the model: curves projected onto faces, intersection curves of bodies
|
||||
//! and faces (on their display meshes), and spun profiles.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
//! 3D sketch curves from the model: curves projected onto faces (on their display meshes), the
|
||||
//! bodies and faces an intersection curve is taken on, and spun profiles.
|
||||
|
||||
use solidcraft_geom::{Mesh, Plane, Vec2, Vec3};
|
||||
use solidcraft_sketch::LinkSource;
|
||||
|
||||
use crate::eval::ModelState;
|
||||
|
||||
/// Most triangle pairs examined by one intersection.
|
||||
const MAX_PAIRS: usize = 20_000_000;
|
||||
|
||||
/// The mesh (and face, for a face source) of a body or face reference.
|
||||
pub(crate) fn mesh_of(st: &ModelState, src: &LinkSource) -> Option<(Arc<Mesh>, Option<u32>)> {
|
||||
/// The body of a body or face reference, and the face (its index in the body's face order) for a
|
||||
/// face reference: the face nearest its point, on the named body or else the nearest body.
|
||||
pub(crate) fn part_of<'a>(st: &'a ModelState, src: &LinkSource) -> Option<(&'a crate::ModelBody, Option<usize>)> {
|
||||
match src {
|
||||
LinkSource::Body { body } => Some((st.body(body)?.mesh(), None)),
|
||||
LinkSource::Body { body } => Some((st.body(body)?, None)),
|
||||
LinkSource::Face { body, at } => {
|
||||
let b = st.body(body).or_else(|| st.bodies.iter().min_by(|x, y| dist_to(&x.mesh(), *at).total_cmp(&dist_to(&y.mesh(), *at))))?;
|
||||
let m = b.mesh();
|
||||
let (f, _, _) = crate::project::nearest_face(&m, *at)?;
|
||||
Some((m, Some(f)))
|
||||
let (f, _, _) = crate::project::nearest_face(&b.mesh(), *at)?;
|
||||
Some((b, usize::try_from(f).ok()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -99,160 +93,6 @@ pub(crate) fn onto_face(m: &Mesh, f: u32, pts: &[Vec3], dir: Vec3) -> Vec<Vec<Ve
|
||||
out
|
||||
}
|
||||
|
||||
/// Segment where two triangles cross (None when they do not, or are coplanar).
|
||||
fn tri_tri(a: &[Vec3; 3], b: &[Vec3; 3]) -> Option<(Vec3, Vec3)> {
|
||||
let nb = (b[1] - b[0]).cross(b[2] - b[0]).normalized()?;
|
||||
let na = (a[1] - a[0]).cross(a[2] - a[0]).normalized()?;
|
||||
// Points where one triangle's edges pass through the other's plane.
|
||||
let cut = |t: &[Vec3; 3], n: Vec3, o: Vec3| -> Option<(Vec3, Vec3)> {
|
||||
let h: Vec<f64> = t.iter().map(|p| (*p - o).dot(n)).collect();
|
||||
if h.iter().all(|x| *x > 1e-12) || h.iter().all(|x| *x < -1e-12) {
|
||||
return None;
|
||||
}
|
||||
let mut pts = Vec::new();
|
||||
for (i, j) in [(0usize, 1usize), (1, 2), (2, 0)] {
|
||||
let (hi, hj) = (h[i], h[j]);
|
||||
if (hi > 0.0) != (hj > 0.0) {
|
||||
pts.push(t[i].lerp(t[j], hi / (hi - hj)));
|
||||
} else if hi == 0.0 {
|
||||
pts.push(t[i]);
|
||||
}
|
||||
}
|
||||
match pts[..] {
|
||||
[p, q, ..] => Some((p, q)),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
if na.cross(nb).len() < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let (a0, a1) = cut(a, nb, b[0])?;
|
||||
let (b0, b1) = cut(b, na, a[0])?;
|
||||
// Overlap of the two segments on the common line.
|
||||
let dir = na.cross(nb);
|
||||
let pr = |p: Vec3| p.dot(dir);
|
||||
let (sa0, sa1) = if pr(a0) <= pr(a1) { (a0, a1) } else { (a1, a0) };
|
||||
let (sb0, sb1) = if pr(b0) <= pr(b1) { (b0, b1) } else { (b1, b0) };
|
||||
let lo = if pr(sa0) >= pr(sb0) { sa0 } else { sb0 };
|
||||
let hi = if pr(sa1) <= pr(sb1) { sa1 } else { sb1 };
|
||||
(pr(hi) - pr(lo) > 1e-12).then_some((lo, hi))
|
||||
}
|
||||
|
||||
/// Where two meshes (or faces of them) meet, as polylines.
|
||||
pub(crate) fn mesh_intersection(ma: &Mesh, fa: Option<u32>, mb: &Mesh, fb: Option<u32>) -> Vec<Vec<Vec3>> {
|
||||
let (ta, tb) = (tris(ma, fa), tris(mb, fb));
|
||||
let bbox = |t: &[Vec3; 3]| (t[0].min(t[1]).min(t[2]), t[0].max(t[1]).max(t[2]));
|
||||
// Uniform grid over B.
|
||||
let lo = tb.iter().fold(Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY), |m, t| m.min(bbox(t).0));
|
||||
let hi = tb.iter().fold(Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY), |m, t| m.max(bbox(t).1));
|
||||
if !(lo.is_finite() && hi.is_finite()) {
|
||||
return Vec::new();
|
||||
}
|
||||
let n = ((tb.len() as f64).cbrt().ceil() as usize).clamp(1, 48);
|
||||
let ext = hi - lo;
|
||||
let cell = |p: Vec3| -> [usize; 3] {
|
||||
let f = |v: f64, l: f64, e: f64| if e > 1e-12 { (((v - l) / e) * n as f64).floor().clamp(0.0, (n - 1) as f64) as usize } else { 0 };
|
||||
[f(p.x, lo.x, ext.x), f(p.y, lo.y, ext.y), f(p.z, lo.z, ext.z)]
|
||||
};
|
||||
let mut grid: BTreeMap<[usize; 3], Vec<usize>> = BTreeMap::new();
|
||||
for (i, t) in tb.iter().enumerate() {
|
||||
let (b0, b1) = bbox(t);
|
||||
let (c0, c1) = (cell(b0), cell(b1));
|
||||
for x in c0[0]..=c1[0] {
|
||||
for y in c0[1]..=c1[1] {
|
||||
for z in c0[2]..=c1[2] {
|
||||
grid.entry([x, y, z]).or_default().push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut segs: Vec<(Vec3, Vec3)> = Vec::new();
|
||||
let mut pairs = 0usize;
|
||||
for t in &ta {
|
||||
let (b0, b1) = bbox(t);
|
||||
if b1.x < lo.x || b1.y < lo.y || b1.z < lo.z || b0.x > hi.x || b0.y > hi.y || b0.z > hi.z {
|
||||
continue;
|
||||
}
|
||||
let (c0, c1) = (cell(b0), cell(b1));
|
||||
let mut cand: Vec<usize> = Vec::new();
|
||||
for x in c0[0]..=c1[0] {
|
||||
for y in c0[1]..=c1[1] {
|
||||
for z in c0[2]..=c1[2] {
|
||||
if let Some(v) = grid.get(&[x, y, z]) {
|
||||
cand.extend(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cand.sort_unstable();
|
||||
cand.dedup();
|
||||
for j in cand {
|
||||
pairs += 1;
|
||||
if pairs > MAX_PAIRS {
|
||||
break;
|
||||
}
|
||||
if let Some(u) = tb.get(j)
|
||||
&& let Some(sg) = tri_tri(t, u)
|
||||
{
|
||||
segs.push(sg);
|
||||
}
|
||||
}
|
||||
}
|
||||
chain3(&segs, ma.bounds().diagonal().max(mb.bounds().diagonal()).max(1e-9))
|
||||
}
|
||||
|
||||
/// Chain 3D segments into polylines by shared end points.
|
||||
fn chain3(segs: &[(Vec3, Vec3)], size: f64) -> Vec<Vec<Vec3>> {
|
||||
let q = size * 1e-7;
|
||||
let key = |p: Vec3| ((p.x / q).round() as i64, (p.y / q).round() as i64, (p.z / q).round() as i64);
|
||||
let mut adj: BTreeMap<(i64, i64, i64), (Vec3, Vec<usize>)> = BTreeMap::new();
|
||||
let mut uniq: Vec<((i64, i64, i64), (i64, i64, i64))> = Vec::new();
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for (a, b) in segs {
|
||||
let (ka, kb) = (key(*a), key(*b));
|
||||
if ka == kb {
|
||||
continue;
|
||||
}
|
||||
let k = if ka <= kb { (ka, kb) } else { (kb, ka) };
|
||||
if !seen.insert(k) {
|
||||
continue;
|
||||
}
|
||||
let i = uniq.len();
|
||||
uniq.push(k);
|
||||
adj.entry(ka).or_insert_with(|| (*a, Vec::new())).1.push(i);
|
||||
adj.entry(kb).or_insert_with(|| (*b, Vec::new())).1.push(i);
|
||||
}
|
||||
let mut used = vec![false; uniq.len()];
|
||||
let mut out = Vec::new();
|
||||
let starts: Vec<(i64, i64, i64)> = adj.iter().filter(|(_, v)| v.1.len() != 2).map(|(k, _)| *k).chain(adj.keys().copied()).collect();
|
||||
for s in starts {
|
||||
while let Some(first) = adj.get(&s).and_then(|v| v.1.iter().copied().find(|e| !used.get(*e).copied().unwrap_or(true))) {
|
||||
let mut chain = vec![adj.get(&s).map(|v| v.0).unwrap_or_default()];
|
||||
let mut cur = s;
|
||||
let mut e = first;
|
||||
loop {
|
||||
if let Some(u) = used.get_mut(e) {
|
||||
*u = true;
|
||||
}
|
||||
let Some(&(ka, kb)) = uniq.get(e) else { break };
|
||||
cur = if ka == cur { kb } else { ka };
|
||||
chain.push(adj.get(&cur).map(|v| v.0).unwrap_or_default());
|
||||
let Some(next) = adj
|
||||
.get(&cur)
|
||||
.and_then(|v| if v.1.len() == 2 { v.1.iter().copied().find(|x| !used.get(*x).copied().unwrap_or(true)) } else { None })
|
||||
else {
|
||||
break;
|
||||
};
|
||||
e = next;
|
||||
}
|
||||
if chain.len() >= 2 {
|
||||
out.push(chain);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Outline of a body spun about the axis `origin + t·dir`: the largest distance from the axis
|
||||
/// at each station along it, laid into the sketch plane on the side of the axis the plane's
|
||||
/// in-plane perpendicular points to.
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Where two bodies meet, each whole or one face of it: the edges SMLib's imprint of the first by
|
||||
//! the second makes (`capi/include/smlib.h`, "Imprint": each face the tool meets is cut along the
|
||||
//! intersection). SMLib intersects the faces' surfaces and trims each curve to both faces, so an
|
||||
//! edge it makes is exactly where they meet: its record curve (a B-spline SMLib fitted within its
|
||||
//! tolerance, or the line, circle or ellipse it is) on its interval. Which edges are new comes from
|
||||
//! the imprint's own history: an edge no edge of either operand became.
|
||||
|
||||
use solidcraft_geom::Vec3;
|
||||
use solidcraft_geom::brep::Curve;
|
||||
|
||||
use crate::body::{Body, MeshTolerance};
|
||||
use crate::{KernelError, Result};
|
||||
|
||||
/// One edge where two bodies meet.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MeetingEdge {
|
||||
/// The edge's record curve, exactly as SMLib made it.
|
||||
pub curve: Curve,
|
||||
/// Its interval on `curve`.
|
||||
pub interval: [f64; 2],
|
||||
/// Its points at the chord tolerance asked for, from its start to its end.
|
||||
pub points: Vec<Vec3>,
|
||||
}
|
||||
|
||||
fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib intersection curve: {what}: {e}"))
|
||||
}
|
||||
|
||||
/// `body` itself, or its face `face` (in its face order) alone as a surface body.
|
||||
fn part(body: &Body, face: Option<usize>) -> Result<Body> {
|
||||
match face {
|
||||
None => Ok(body.clone()),
|
||||
Some(f) if f < body.record().faces.len() => crate::surfaces::taken(body, body.record(), &[(f, false)]),
|
||||
Some(f) => Err(KernelError::Invalid(format!("the body has no face {f}"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where `a` (or its face `a_face`) meets `b` (or its face `b_face`): every edge SMLib's imprint
|
||||
/// of the one by the other makes, its points at `chord` mm. Empty where they do not meet.
|
||||
pub fn meeting_edges(a: &Body, a_face: Option<usize>, b: &Body, b_face: Option<usize>, chord: f64) -> Result<Vec<MeetingEdge>> {
|
||||
let (a, b) = (part(a, a_face)?, part(b, b_face)?);
|
||||
let ta = a.smlib()?.tagged(0).map_err(|e| failed("tag", e))?;
|
||||
let tb = b.smlib()?.tagged(1).map_err(|e| failed("tag", e))?;
|
||||
let Some(cut) = ta.imprint(&tb).map_err(|e| failed("imprint", e))? else { return Ok(Vec::new()) };
|
||||
let history = cut.history().map_err(|e| failed("history", e))?;
|
||||
let (body, order) = crate::native::body_from_smlib_ordered(&cut)?;
|
||||
let order = order.ok_or_else(|| failed("history", "the imprint's edges cannot be numbered"))?;
|
||||
let mesh = body.mesh(&MeshTolerance::chord(chord))?;
|
||||
let record = body.record();
|
||||
let mut out = Vec::new();
|
||||
for (i, edge) in history.edges.iter().enumerate() {
|
||||
if !edge.origins.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let at = if order.edges.is_empty() { Some(i) } else { order.edges.get(i).copied() };
|
||||
let (Some(e), Some(points)) = (at.and_then(|k| record.edges.get(k)), at.and_then(|k| mesh.edges.get(k))) else {
|
||||
return Err(failed("history", format!("edge {i} is not in the imprinted body")));
|
||||
};
|
||||
out.push(MeetingEdge { curve: e.curve.clone(), interval: e.interval, points: points.clone() });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -366,7 +366,7 @@ pub(crate) fn all_new(body: Body, operands: &[&Body]) -> Result<Body> {
|
||||
|
||||
/// The faces `picked` of `body` (its record `r`; each with whether to turn it over) as their own
|
||||
/// body, each the body's face (Kept), every edge the body's.
|
||||
fn taken(body: &Body, r: &Brep, picked: &[(usize, bool)]) -> Result<Body> {
|
||||
pub(crate) fn taken(body: &Body, r: &Brep, picked: &[(usize, bool)]) -> Result<Body> {
|
||||
let c = Parts::of(r, picked)?.canonical()?;
|
||||
let faces = c.faces.iter().map(|p| picked.get(*p).map_or(0, |(f, _)| *f)).map(|f| record::from_face(0, f, Change::Kept)).collect();
|
||||
let h = record_history(&c.brep, 1, faces, |e, _, _| c.edges.get(e).map(|&edge| EdgeOrigin::Edge { operand: 0, edge }).into_iter().collect());
|
||||
|
||||
@@ -63,9 +63,9 @@ pub struct Wire {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub arc: Option<WireArc>,
|
||||
/// A linked curve's exact geometry where its source has one (an included edge or sketch
|
||||
/// curve): record curves end to end along `pts`, in `space` coordinates. Empty for a curve
|
||||
/// known only as its points (projected onto a face, intersection and iso curves). Set when
|
||||
/// the link is resolved, with `pts`.
|
||||
/// curve, an intersection curve): record curves end to end along `pts`, in `space`
|
||||
/// coordinates. Empty for a curve known only as its points (projected onto a face, iso
|
||||
/// curves). Set when the link is resolved, with `pts`.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub exact: Vec<WireCurve>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user