doc: sweeps along linked and planar curves follow the exact curves

A sweep path through 3D sketch curves sampled every planar sketch curve in it
into a 0.01 mm polyline, and took every linked 3D curve as the points its link
stored, so SMLib got a many-cornered polyline (and refused a smooth one). Now:

- a planar sketch curve in such a path is its exact segments
  (PathPiece::Segs of PathSeg::of_sketch over exact_segs);
- a linked curve whose source has an exact curve keeps it: Wire::exact holds
  the record curves (WireCurve: schema 4 curve + interval) end to end along the
  wire, set with the wire when the link is resolved: an included body edge its
  record edge (kernel::brep_record), an included sketch curve leaving the plane
  its segments (kernel::sketch_record_curve). Wire::map carries them with a
  placement. The sweep follows them through kernel::curve_path (line, arc,
  ellipse as rational quadratic spans, polyline, B-spline as Bézier spans).
- curves known only as points (projected onto a face, intersection and iso
  curves: the link resolves them on the mesh) stay points.

Kernel, for the sheet and blend ports: planar_face_record (a profile face as a
schema 4 sheet record with exact pcurves), smlib_sweeps::chain_curves (a
segment chain as SMLib curves), body_from_record (any record into SMLib,
checked), sketch_record_curve; record curves from design files are checked in
curve_path. Format: pending/wire-exact-curves.rs.

lib.rs (coordinator): export SpaceBezier, curve_path, edge_path,
sketch_record_curve and planar_face_record from the kernel.
(not built: base in progress; kernel and sketch check clean, doc not reachable)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 01:01:03 -04:00
parent 33cfa34d41
commit 7952516eb4
9 changed files with 347 additions and 81 deletions
+17 -48
View File
@@ -5844,17 +5844,7 @@ fn pipe_region(vals: &BTreeMap<String, Value>, r: f64, wall: &Option<crate::Expr
}
pub fn map_wire(wire: &mut solidcraft_sketch::Wire, matrix: &Mat) {
for point in wire.pts.iter_mut().chain(&mut wire.fit) {
*point = crate::apply_point(matrix, *point);
}
if let Some(arc) = &mut wire.arc {
let normal = crate::apply_vector(matrix, arc.normal);
arc.center = crate::apply_point(matrix, arc.center);
arc.start = crate::apply_point(matrix, arc.start);
arc.end = crate::apply_point(matrix, arc.end);
arc.normal = normal.normalized().unwrap_or(arc.normal);
arc.radius = arc.center.dist(arc.start);
}
wire.map(|p| crate::apply_point(matrix, p), |v| crate::apply_vector(matrix, v));
}
/// A feature's sketch as its component sees it: a sketch of another component that is placed
@@ -5909,8 +5899,9 @@ fn path_in_frame<'a>(doc: &Document, sketch: &'a SolvedSketch, consumer: u64) ->
/// A path that uses 3D sketch curves (wires), as one chain of pieces from the end nearest
/// `start` (no start: the first curve runs toward the second): each drawn 3D spline as its fit
/// points (the kernel has its exact curve), every other curve as world points. `None` when the
/// path is all planar curves.
/// points (the kernel has its exact curve), each curve held exactly (a planar sketch curve, a
/// linked curve with [`solidcraft_sketch::Wire::exact`]) as its path segments, and a linked
/// curve known only as points as those points. `None` when the path is all planar curves.
fn path_points(ps: &SolvedSketch, ids: &[String], start: Vec3) -> Result<Option<kernel::PointPath>> {
if !ids.iter().any(|id| ps.sketch.wire_index(id).is_some()) {
return Ok(None);
@@ -5923,20 +5914,19 @@ fn path_points(ps: &SolvedSketch, ids: &[String], start: Vec3) -> Result<Option<
if let Some(wi) = ps.sketch.wire_index(id) {
pieces.push(match ps.sketch.wires.get(wi) {
Some(w) if w.fit.len() >= 3 => kernel::PathPiece::Spline { fit: w.fit.clone(), reversed: false },
Some(w) if !w.exact.is_empty() => {
let mut segs = Vec::new();
for c in &w.exact {
segs.extend(kernel::curve_path(&c.curve, c.interval)?);
}
kernel::PathPiece::Segs(segs)
}
w => kernel::PathPiece::Polyline(w.map(|w| w.pts.clone()).unwrap_or_default()),
});
} else {
let ci = ps.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("path curve `{id}`")))?;
let mut pts: Vec<Vec3> = Vec::new();
for s in ps.sketch.segs(ci) {
for q in s.polyline(1e-2) {
let w = ps.plane.to_world(q);
if pts.last().is_none_or(|l| l.dist(w) > 1e-9) {
pts.push(w);
}
}
}
pieces.push(kernel::PathPiece::Polyline(pts));
let segs = ps.sketch.exact_segs(ci).unwrap_or_else(|| ps.sketch.segs(ci));
pieces.push(kernel::PathPiece::Segs(segs.iter().map(|s| kernel::PathSeg::of_sketch(&ps.plane, s)).collect()));
}
}
// Pieces meet within the sketch's tolerance (projected curves are only that close).
@@ -6022,19 +6012,7 @@ pub(crate) fn path_segments_at(ps: &SolvedSketch, ids: &[String], start: Vec3, p
if let Some(profile) = profile {
segs = from_profile_on_closed_path(segs, &ps.plane, profile, start);
}
let n = ps.plane.normal();
Ok(segs
.iter()
.flat_map(|s| match *s {
solidcraft_geom::Seg2::Line { a, b } => vec![kernel::PathSeg::Line { a: w(a), b: w(b) }],
solidcraft_geom::Seg2::Arc { center, sweep, .. } => {
vec![kernel::PathSeg::Arc { a: w(s.start()), center: w(center), axis: n * sweep.signum(), angle: sweep.abs() }]
}
solidcraft_geom::Seg2::Cubic { .. } | solidcraft_geom::Seg2::Conic { .. } | solidcraft_geom::Seg2::Bezier { .. } => {
vec![kernel::PathSeg::Curve { plane: ps.plane, seg: *s }]
}
})
.collect())
Ok(segs.iter().map(|s| kernel::PathSeg::of_sketch(&ps.plane, s)).collect())
}
/// A closed chain `segs` (in the plane `path`) begun where `profile`'s plane crosses it nearest
@@ -6342,7 +6320,7 @@ fn seg_len(s: &kernel::PathSeg) -> f64 {
kernel::PathSeg::Line { a, b } => (b - a).len(),
kernel::PathSeg::Arc { a, center, angle, .. } => (a - center).len() * angle.abs(),
// Patterns flatten their paths; a curve left is measured along its chord.
kernel::PathSeg::Curve { .. } | kernel::PathSeg::Cubic { .. } => s.start().dist(s.end()),
kernel::PathSeg::Curve { .. } | kernel::PathSeg::Bezier(_) => s.start().dist(s.end()),
}
}
@@ -6366,7 +6344,7 @@ fn seg_at(s: &kernel::PathSeg, t: f64) -> (Vec3, Vec3) {
let v = rotate_vec(a - center, k, s * t / r);
(center + v, k.cross(v).normalized().unwrap_or(Vec3::X) * s)
}
kernel::PathSeg::Curve { .. } | kernel::PathSeg::Cubic { .. } => {
kernel::PathSeg::Curve { .. } | kernel::PathSeg::Bezier(_) => {
let (a, b) = (s.start(), s.end());
let d = (b - a).normalized().unwrap_or(Vec3::X);
(a + d * t, d)
@@ -6375,16 +6353,7 @@ fn seg_at(s: &kernel::PathSeg, t: f64) -> (Vec3, Vec3) {
}
fn reverse_seg(s: &kernel::PathSeg) -> kernel::PathSeg {
match *s {
kernel::PathSeg::Line { a, b } => kernel::PathSeg::Line { a: b, b: a },
kernel::PathSeg::Arc { a, center, axis, angle } => {
let k = axis.normalized().unwrap_or(Vec3::Z);
let end = center + rotate_vec(a - center, k, angle);
kernel::PathSeg::Arc { a: end, center, axis: k * -1.0, angle }
}
kernel::PathSeg::Curve { plane, seg } => kernel::PathSeg::Curve { plane, seg: seg.reversed() },
kernel::PathSeg::Cubic { p: [a, b, c, d] } => kernel::PathSeg::Cubic { p: [d, c, b, a] },
}
s.reversed()
}
/// Point and tangent at arc length `d` along a chain (clamped to its ends).
@@ -0,0 +1,6 @@
//! A linked 3D sketch curve (an included edge or an included sketch curve that leaves the sketch
//! plane) saves its exact geometry with its points: `wires[].exact`, the record curves (schema 4
//! curve records with their intervals) end to end along the curve, so a sweep along it follows
//! the curve itself rather than its points. An older design has none and gets them when its
//! links are next resolved; until then such a curve is followed as its points. The last release
//! refuses a design with this change (the newer format number).
+67 -15
View File
@@ -13,7 +13,7 @@
use std::collections::BTreeMap;
use solidcraft_geom::{Mesh, Plane, Seg2, Vec2, Vec3};
use solidcraft_sketch::{CurveKind, LinkGeom, LinkKind, LinkSource, Sketch};
use solidcraft_sketch::{CurveKind, LinkGeom, LinkKind, LinkSource, Sketch, WireCurve};
use crate::eval::ModelState;
use crate::expr::Value;
@@ -33,6 +33,9 @@ pub struct Resolved {
pub geom: Vec<LinkGeom>,
/// 3D curves (world coordinates).
pub wires: Vec<Vec<Vec3>>,
/// Each wire's exact curve where its source has one (an included edge or sketch curve),
/// one list per wire, empty for a curve known only as points ([`solidcraft_sketch::Wire::exact`]).
pub exact: Vec<Vec<WireCurve>>,
pub source: LinkSource,
/// The reference was not exactly where it was and was re-found on the nearest geometry.
pub moved: bool,
@@ -108,6 +111,7 @@ fn resolve_uncached(
let mut edge_index = None;
let mut source = src.clone();
let mut wires: Vec<Vec<Vec3>> = Vec::new();
let mut exact: Vec<Vec<WireCurve>> = Vec::new();
let geom = match src {
LinkSource::Edge { body, at } => {
let (bi, mesh) = body_mesh(st, body, *at)?;
@@ -120,8 +124,9 @@ fn resolve_uncached(
let name = st.bodies.get(bi).map(|b| b.name.clone()).unwrap_or_else(|| body.clone());
source = LinkSource::Edge { body: name, at: q };
edge_index = Some(e);
let exact = st.bodies.get(bi).and_then(|b| exact_edges(b, plane, &[e]).pop().flatten());
edge_geom(plane, kind, exact, mesh.edges.get(e).cloned().unwrap_or_default(), &mut wires)
let planar = st.bodies.get(bi).and_then(|b| exact_edges(b, plane, &[e]).pop().flatten());
let pts = mesh.edges.get(e).cloned().unwrap_or_default();
edge_geom(plane, kind, planar, (pts, || edge_curves(st.bodies.get(bi), e)), &mut wires, &mut exact)
}
LinkSource::Face { body, at } => {
let (bi, mesh) = body_mesh(st, body, *at)?;
@@ -182,6 +187,7 @@ fn resolve_uncached(
let pts: Vec<Vec3> = ss.sketch.polyline(ci).iter().map(|p| ss.plane.to_world(*p)).collect();
if off_plane(plane, &pts) {
wires.push(pts);
exact.push(sketch_curves(ss, ci));
Vec::new()
} else {
project_sketch_curve(&ss.sketch, &ss.plane, ci, plane)
@@ -203,12 +209,14 @@ fn resolve_uncached(
let name = st.bodies.get(bi).map(|b| b.name.clone()).unwrap_or_else(|| body.clone());
source = LinkSource::OnSurface { sketch: *sketch, curve: curve.clone(), body: name, at: q };
wires = crate::project3d::onto_face(&mesh, f, &pts, plane.normal());
exact = vec![Vec::new(); wires.len()];
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()];
Vec::new()
}
LinkSource::Iso { body, at, dir } => {
@@ -221,6 +229,7 @@ fn resolve_uncached(
let name = st.bodies.get(bi).map(|b| b.name.clone()).unwrap_or_else(|| body.clone());
source = LinkSource::Iso { body: name, at: q, dir: dir.clone() };
wires = crate::project3d::iso_curve(&mesh, f, q, dir);
exact = vec![Vec::new(); wires.len()];
Vec::new()
}
LinkSource::Spun { body, origin, dir } => {
@@ -275,7 +284,7 @@ fn resolve_uncached(
if geom.is_empty() && wires.is_empty() {
return Err(DocError::Invalid(format!("the {} projects to nothing on this sketch plane", src.describe())));
}
Ok(Resolved { geom: merge_split_circles(geom), wires, source, moved, face_index, edge_index })
Ok(Resolved { geom: merge_split_circles(geom), wires, exact, source, moved, face_index, edge_index })
}
/// Whether a curve (world points) leaves the sketch plane: included, it then stays 3D.
@@ -285,19 +294,51 @@ fn off_plane(plane: &Plane, pts: &[Vec3]) -> bool {
}
/// What a body edge (its `exact` curve when read, and its polyline `pts`) gives a sketch: its
/// section with the sketch plane, itself as a 3D curve (into `wires`) when it is included and
/// leaves the plane, else its projection (the exact curve, or the polyline fitted).
fn edge_geom(plane: &Plane, kind: LinkKind, exact: Option<Vec<LinkGeom>>, pts: Vec<Vec3>, wires: &mut Vec<Vec<Vec3>>) -> Vec<LinkGeom> {
/// section with the sketch plane, itself as a 3D curve (its points into `wires`, its exact
/// curve into `exact`) when it is included and leaves the plane, else its projection (the exact
/// planar curve `planar`, or the polyline fitted).
fn edge_geom(
plane: &Plane,
kind: LinkKind,
planar: Option<Vec<LinkGeom>>,
(pts, curves): (Vec<Vec3>, impl FnOnce() -> Vec<WireCurve>),
wires: &mut Vec<Vec<Vec3>>,
exact: &mut Vec<Vec<WireCurve>>,
) -> Vec<LinkGeom> {
match kind {
LinkKind::Intersect => section_of_polyline(plane, &pts),
LinkKind::Include if off_plane(plane, &pts) => {
wires.push(pts);
exact.push(curves());
Vec::new()
}
_ => exact.unwrap_or_else(|| fit_world(plane, &pts)),
_ => planar.unwrap_or_else(|| fit_world(plane, &pts)),
}
}
/// Edge `e` of `body` (its index in the body's edge order) as its exact curve from the body's
/// record; none for a mesh body or an intersection curve the record holds no curve for.
fn edge_curves(body: Option<&crate::ModelBody>, e: usize) -> Vec<WireCurve> {
let Some(body) = body.filter(|b| !b.body.is_mesh()) else { return Vec::new() };
let Ok(record) = crate::kernel::brep_record(&body.body) else { return Vec::new() };
record
.edges
.get(e)
.filter(|edge| !matches!(edge.curve, solidcraft_geom::brep::Curve::SurfaceIntersection { .. }))
.map(|edge| vec![WireCurve { curve: edge.curve.clone(), interval: edge.interval }])
.unwrap_or_default()
}
/// Curve `ci` of the solved sketch `ss` as its exact pieces in space, in the order its polyline
/// runs; none where a piece has no record curve.
fn sketch_curves(ss: &crate::eval::SolvedSketch, ci: usize) -> Vec<WireCurve> {
let segs = ss.sketch.exact_segs(ci).unwrap_or_else(|| ss.sketch.segs(ci));
segs.iter()
.map(|s| crate::kernel::sketch_record_curve(&ss.plane, s).map(|(curve, interval)| WireCurve { curve, interval }))
.collect::<Option<_>>()
.unwrap_or_default()
}
/// Edges `edges` of `body` (indices into its mesh's `edges`) on `plane` as their exact curves:
/// lines, circles and arcs, and the Bézier pieces of a free-form curve. `None` for an edge that
/// doesn't lie in a plane parallel to `plane` or isn't read exactly (a mesh body's): it is
@@ -375,14 +416,14 @@ fn resolve_named_edge(st: &ModelState, plane: &Plane, kind: LinkKind, body: &str
.filter_map(|w| Some(closest_on_segment(near, *w.first()?, *w.get(1)?)))
.min_by(|a, b| a.dist(near).total_cmp(&b.dist(near)))
.ok_or_else(|| lost("named projection edge"))?;
let mut wires = Vec::new();
let exact = exact_edges(target, plane, &[index]).pop().flatten();
let geom = edge_geom(plane, kind, exact, pts, &mut wires);
let (mut wires, mut exact) = (Vec::new(), Vec::new());
let planar = exact_edges(target, plane, &[index]).pop().flatten();
let geom = edge_geom(plane, kind, planar, (pts, || edge_curves(Some(target), index)), &mut wires, &mut exact);
if geom.is_empty() && wires.is_empty() {
return Err(DocError::Invalid("named edge projects to nothing on this sketch plane".into()));
}
let source = LinkSource::Edge { body: body.into(), at };
Ok(Resolved { geom: merge_split_circles(geom), wires, source, moved: false, face_index: None, edge_index: Some(index) })
Ok(Resolved { geom: merge_split_circles(geom), wires, exact, source, moved: false, face_index: None, edge_index: Some(index) })
}
fn resolve_named_face(st: &ModelState, plane: &Plane, kind: LinkKind, body: &str, index: usize, at: Vec3) -> Result<Resolved> {
@@ -403,7 +444,15 @@ fn resolve_named_face(st: &ModelState, plane: &Plane, kind: LinkKind, body: &str
if geom.is_empty() {
return Err(DocError::Invalid("named face projects to nothing on this sketch plane".into()));
}
Ok(Resolved { geom, wires: Vec::new(), source: LinkSource::Face { body: body.into(), at }, moved: false, face_index, edge_index: None })
Ok(Resolved {
geom,
wires: Vec::new(),
exact: Vec::new(),
source: LinkSource::Face { body: body.into(), at },
moved: false,
face_index,
edge_index: None,
})
}
fn projection_face_edges(body: &crate::ModelBody, mesh: &Mesh, index: u32, plane: &Plane) -> Result<Vec<usize>> {
@@ -484,6 +533,7 @@ pub fn resolve_face_indices(st: &ModelState, plane: &Plane, kind: LinkKind, body
Ok(Resolved {
geom: merge_split_circles(geometry),
wires: Vec::new(),
exact: Vec::new(),
source: LinkSource::Face { body: body.name.clone(), at: point },
moved: false,
face_index: if indices.len() == 1 { indices.first().copied() } else { None },
@@ -663,7 +713,7 @@ fn refresh_links_in(
if geom.is_empty() {
return Err(DocError::Invalid(format!("the {} makes no outline", source.describe())));
}
Ok(Resolved { geom, wires: Vec::new(), source: source.clone(), moved: false, face_index: None, edge_index: None })
Ok(Resolved { geom, wires: Vec::new(), exact: Vec::new(), source: source.clone(), moved: false, face_index: None, edge_index: None })
})
} else if l.auto_face {
anchor
@@ -690,7 +740,9 @@ fn refresh_links_in(
link.edge_name = Some(name);
}
sk.set_link_source(&l.id, r.source);
let _ = sk.set_link_wires(&l.id, &r.wires);
if let Err(e) = sk.set_link_wires(&l.id, &r.wires).and_then(|()| sk.set_link_wire_curves(&l.id, &r.exact)) {
warn.push(format!("projected {}: {e}", l.source.describe()));
}
// Words built from text parameters change shape when the parameters do: that
// rebuild is the text following them, not a reference that changed under it.
let words_follow = matches!(&l.source, LinkSource::Text { exprs, .. } if exprs.text.is_some());
+87 -1
View File
@@ -920,6 +920,90 @@ fn smlib_tapered(plane: &Plane, region: &Region2, from: f64, to: f64, foot: f64,
crate::smlib_sweeps::extrude(plane, &crate::smlib_sweeps::pieces([&bottom]), Some(region), from * sign, to - from, sign, draft)
}
/// A planar region on `plane` as the schema 4 record of one face (an open sheet): the plane's
/// own axes as its parameters (its natural normal the sketch's), the outer loop
/// counter-clockwise and the holes clockwise, each sketch segment one edge of its exact record
/// curve ([`crate::point_path::sketch_record_curve`]) with its exact pcurve on the plane. The record
/// equivalent of a profile face: SMLib takes it with `Brep::from_record`.
pub fn planar_face_record(plane: &Plane, r: &Region2) -> Result<solidcraft_geom::brep::Brep> {
use solidcraft_geom::brep::{self as rec, pcurve};
if !frame_ok(plane) {
return Err(KernelError::Invalid("the face's plane is not finite and orthonormal".into()));
}
region_ok(r)?;
let region = oriented(r)?;
let tolerance = 1e-7;
let d = |v: Vec3| [v.x, v.y, v.z];
let surface = rec::Surface::Plane { origin: d(plane.origin), u_axis: d(plane.x), v_axis: d(plane.y) };
let bad = |what: &str| KernelError::Invalid(format!("profile {what}"));
// Edges and vertices numbered as the face's loops first meet them.
let mut edges: Vec<rec::Edge> = Vec::new();
let mut vertices: Vec<rec::Vertex> = Vec::new();
let mut loops: Vec<Vec<u32>> = Vec::new();
let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
for (li, l) in region.loops().enumerate() {
let segs = &l.lp.segs;
let first = u32::try_from(vertices.len()).map_err(|_| bad("size"))?;
let mut ids = Vec::with_capacity(segs.len());
for (k, s) in segs.iter().enumerate() {
let (curve, interval) = crate::point_path::sketch_record_curve(plane, s).ok_or_else(|| bad("curve"))?;
let start = u32::try_from(vertices.len()).map_err(|_| bad("size"))?;
vertices.push(rec::Vertex { point: d(plane.to_world(s.start())), tolerance });
let end = if k + 1 == segs.len() { first } else { start + 1 };
let e = u32::try_from(edges.len()).map_err(|_| bad("size"))?;
let (li, k) = (u32::try_from(li).map_err(|_| bad("size"))?, u32::try_from(k).map_err(|_| bad("size"))?);
edges.push(rec::Edge {
curve,
interval,
vertices: [start, end],
tolerance,
uses: vec![rec::EdgeUseRef { face: 0, r#loop: li, position: k, upper: false }],
});
ids.push(e);
for q in s.polyline(tolerance) {
for (i, x) in [q.x, q.y].into_iter().enumerate() {
if let (Some(a), Some(b)) = (lo.get_mut(i), hi.get_mut(i)) {
*a = a.min(x);
*b = b.max(x);
}
}
}
}
loops.push(ids);
}
let uses: Vec<Vec<pcurve::EdgeUse<'_>>> = loops
.iter()
.map(|ids| {
ids.iter()
.filter_map(|&e| {
let edge = edges.get(e as usize)?;
Some(pcurve::EdgeUse { edge: e, curve: &edge.curve, interval: edge.interval, forward: true, tolerance, given: None })
})
.collect()
})
.collect();
let pcurves = pcurve::pcurves(&surface, &uses, tolerance).map_err(|e| KernelError::Failed(format!("profile pcurves: {e}")))?;
let face_loops = loops
.iter()
.zip(pcurves)
.map(|(ids, pcs)| rec::Loop::Edges {
edges: ids.iter().zip(pcs).map(|(&edge, pcurve)| rec::OrientedEdge { edge, forward: true, pcurve }).collect(),
})
.collect();
let pad = tolerance * 10.0;
let brep = rec::Brep {
version: rec::VERSION,
tolerance,
regions: vec![rec::Region { shells: vec![0], void: false }],
shells: vec![rec::Shell::Faces { faces: vec![rec::OrientedFace { face: 0, forward: true }], wires: vec![] }],
faces: vec![rec::Face { surface, domain: [[lo[0] - pad, hi[0] + pad], [lo[1] - pad, hi[1] + pad]], loops: face_loops }],
edges,
vertices,
};
brep.validate().map_err(|e| KernelError::Failed(format!("profile face: {e}")))?;
Ok(brep)
}
/// Ruled loft through two or more planar sections (outer loops only). The sections and the
/// same sections in reverse order give the same body.
pub fn loft(sections: &[(Plane, Loop2)]) -> Result<Body> {
@@ -1377,7 +1461,9 @@ impl PathSeg {
let w = |p: Vec2| plane.to_world(p);
match *s {
Seg2::Line { a, b } => PathSeg::Line { a: w(a), b: w(b) },
Seg2::Arc { center, sweep, .. } => PathSeg::Arc { a: w(s.start()), center: w(center), axis: plane.normal() * sweep.signum(), angle: sweep.abs() },
Seg2::Arc { center, sweep, .. } => {
PathSeg::Arc { a: w(s.start()), center: w(center), axis: plane.normal() * sweep.signum(), angle: sweep.abs() }
}
Seg2::Cubic { .. } | Seg2::Conic { .. } | Seg2::Bezier { .. } => PathSeg::Curve { plane: *plane, seg: *s },
}
}
+73 -6
View File
@@ -75,7 +75,9 @@ impl PathPiece {
if spans.is_empty() {
return Ok(None);
}
let seg = |p: [Vec3; 4]| SpaceBezier::cubic(p).map(PathSeg::Bezier).ok_or_else(|| KernelError::Invalid("the 3D sketch spline is not finite".into()));
let seg = |p: [Vec3; 4]| {
SpaceBezier::cubic(p).map(PathSeg::Bezier).ok_or_else(|| KernelError::Invalid("the 3D sketch spline is not finite".into()))
};
Ok(Some(if *reversed {
spans.into_iter().rev().map(|[a, b, c, d]| seg([d, c, b, a])).collect::<Result<_>>()?
} else {
@@ -153,13 +155,30 @@ pub fn edge_path(body: &Body, edge: usize) -> Result<Vec<PathSeg>> {
curve_path(&e.curve, e.interval)
}
/// The record curve `c` on `[t0, t1]` as path segments ([`edge_path`]).
pub(crate) fn curve_path(c: &brep::Curve, [t0, t1]: [f64; 2]) -> Result<Vec<PathSeg>> {
let bad = |what: &str| KernelError::Invalid(format!("the edge's {what} does not evaluate"));
if !(t0.is_finite() && t1.is_finite() && t0 < t1) {
/// Most control points a record curve given to [`curve_path`] may have.
const MAX_CURVE_POINTS: usize = 100_000;
/// The record curve `c` on `[t0, t1]` as path segments ([`edge_path`]); the numbers are checked
/// first (they may come from a design file).
pub fn curve_path(c: &brep::Curve, [t0, t1]: [f64; 2]) -> Result<Vec<PathSeg>> {
let bad = |what: &str| KernelError::Invalid(format!("the curve's {what} does not evaluate"));
if !(t0.is_finite() && t1.is_finite() && t0 < t1 && t0.abs() < 1e9 && t1.abs() < 1e9) {
return Err(bad("interval"));
}
let v = |q: [f64; 3]| Vec3::new(q[0], q[1], q[2]);
let frame_ok = |f: &brep::Frame| {
let (x, y) = (v(f.x_axis), v(f.y_axis));
v(f.origin).is_finite() && (x.len() - 1.0).abs() < 1e-6 && (y.len() - 1.0).abs() < 1e-6 && x.dot(y).abs() < 1e-6
};
let finite = |pts: &[[f64; 3]]| pts.len() <= MAX_CURVE_POINTS && pts.iter().all(|q| v(*q).is_finite());
match c {
brep::Curve::Line { start, end } if !finite(&[*start, *end]) => return Err(bad("line")),
brep::Curve::Circle { frame, radius } if !(frame_ok(frame) && radius.is_finite() && *radius > 0.0) => return Err(bad("circle")),
brep::Curve::Ellipse { frame, radii } if !(frame_ok(frame) && radii.iter().all(|r| r.is_finite() && *r > 0.0)) => return Err(bad("ellipse")),
brep::Curve::Polyline { points } | brep::Curve::Spline { points, .. } if !finite(points) => return Err(bad("points")),
brep::Curve::RationalSpline { homogeneous, .. } if homogeneous.len() > MAX_CURVE_POINTS => return Err(bad("points")),
_ => {}
}
let out = match c {
brep::Curve::Line { start, end } => {
let (s, e) = (v(*start), v(*end));
@@ -184,7 +203,8 @@ pub(crate) fn curve_path(c: &brep::Curve, [t0, t1]: [f64; 2]) -> Result<Vec<Path
.map(|i| {
let a = t0 + d * i as f64;
let w = (d / 2.0).cos();
let ctrl = [HPoint::weighted(at(a, 1.0), 1.0), HPoint::weighted(at(a + d / 2.0, 1.0 / w), w), HPoint::weighted(at(a + d, 1.0), 1.0)];
let ctrl =
[HPoint::weighted(at(a, 1.0), 1.0), HPoint::weighted(at(a + d / 2.0, 1.0 / w), w), HPoint::weighted(at(a + d, 1.0), 1.0)];
SpaceBezier::new(&ctrl).map(PathSeg::Bezier).ok_or_else(|| bad("ellipse"))
})
.collect::<Result<_>>()?
@@ -218,6 +238,53 @@ pub(crate) fn curve_path(c: &brep::Curve, [t0, t1]: [f64; 2]) -> Result<Vec<Path
Ok(out)
}
/// A sketch segment on `plane` as the record curve that is it, on its interval, running the
/// segment's way: a line, a circle (an arc's; clockwise ones on the frame turned over), a cubic
/// or Bézier as a spline of its degree, a conic as a rational quadratic. `None` for a segment
/// with non-finite numbers.
pub fn sketch_record_curve(plane: &Plane, s: &solidcraft_geom::Seg2) -> Option<(brep::Curve, [f64; 2])> {
use solidcraft_geom::Seg2;
let w = |p: solidcraft_geom::Vec2| {
let q = plane.to_world(p);
[q.x, q.y, q.z]
};
let d = |v: Vec3| [v.x, v.y, v.z];
let bezier = |pts: &[solidcraft_geom::Vec2]| {
let n = pts.len().checked_sub(1)?;
let knots = std::iter::repeat_n(0.0, n + 1).chain(std::iter::repeat_n(1.0, n + 1)).collect();
Some((brep::Curve::Spline { knots, points: pts.iter().map(|p| w(*p)).collect() }, [0.0, 1.0]))
};
let out = match *s {
Seg2::Line { a, b } => (brep::Curve::Line { start: w(a), end: w(b) }, [0.0, 1.0]),
Seg2::Arc { center, radius, start, sweep } => {
let (y, t0) = if sweep >= 0.0 { (plane.y, start) } else { (plane.y * -1.0, -start) };
let frame = brep::Frame { origin: w(center), x_axis: d(plane.x), y_axis: d(y) };
(brep::Curve::Circle { frame, radius }, [t0, t0 + sweep.abs()])
}
Seg2::Cubic { p0, p1, p2, p3 } => bezier(&[p0, p1, p2, p3])?,
Seg2::Bezier { n, p } => bezier(p.get(..=usize::from(n))?)?,
Seg2::Conic { a, apex, b, w: wt } => {
let h = |p: solidcraft_geom::Vec2, k: f64| {
let q = w(p);
[q[0] * k, q[1] * k, q[2] * k, k]
};
(
brep::Curve::RationalSpline { knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], homogeneous: vec![h(a, 1.0), h(apex, wt), h(b, 1.0)] },
[0.0, 1.0],
)
}
};
let finite = |q: &[f64]| q.iter().all(|x| x.is_finite());
let ok = match &out.0 {
brep::Curve::Line { start, end } => finite(start) && finite(end),
brep::Curve::Circle { frame, radius } => finite(&frame.origin) && radius.is_finite() && *radius > 0.0,
brep::Curve::Spline { points, .. } => points.iter().all(|q| finite(q)),
brep::Curve::RationalSpline { homogeneous, .. } => homogeneous.iter().all(|q| finite(q) && q[3] > 0.0),
_ => false,
};
(ok && out.1.iter().all(|t| t.is_finite())).then_some(out)
}
/// The B-spline (homogeneous `ctrl` on `knots`) on `[t0, t1]` as its Bézier spans, one per knot
/// span: each the piece of the spline between two neighbouring knots ([`segment`]), exactly.
fn spline_spans(knots: &[f64], ctrl: &[HPoint], t0: f64, t1: f64) -> Result<Vec<PathSeg>> {
+1 -1
View File
@@ -77,7 +77,7 @@ fn combinatorics(hs: &[HalfSpace]) -> Result<(Vec<Vec3>, Vec<(usize, Vec<usize>)
/// The convex polyhedron bounded by the half-spaces.
pub fn convex_polyhedron(hs: &[HalfSpace]) -> Result<Body> {
let (verts, faces) = combinatorics(hs)?;
crate::smlib_sweeps::solid_from_record(&polyhedron_record(hs, &verts, &faces)?, "polyhedron")
crate::smlib_sweeps::body_from_record(&polyhedron_record(hs, &verts, &faces)?, "polyhedron")
}
/// The record of the closed polyhedron with corners `verts` whose faces are `faces` (the
+13 -6
View File
@@ -71,7 +71,7 @@ pub(crate) struct Draft {
}
#[cfg(feature = "smlib")]
pub(crate) use imp::{extrude, loft, loft_rails, loft_to_point, revolve, screw, solid_from_record, sphere, sweep, sweep_controlled};
pub(crate) use imp::{body_from_record, extrude, loft, loft_rails, loft_to_point, revolve, screw, sphere, sweep, sweep_controlled};
/// The error of an SMLib operation in a build without SMLib.
#[cfg(not(feature = "smlib"))]
@@ -83,7 +83,7 @@ pub(crate) fn not_built() -> KernelError {
mod stubs {
use super::*;
pub(crate) fn solid_from_record(_: &solidcraft_geom::brep::Brep, _: &str) -> Result<Body> {
pub(crate) fn body_from_record(_: &solidcraft_geom::brep::Brep, _: &str) -> Result<Body> {
Err(not_built())
}
pub(crate) fn sphere(_: Vec3, _: f64) -> Result<Body> {
@@ -115,7 +115,7 @@ mod stubs {
}
}
#[cfg(not(feature = "smlib"))]
pub(crate) use stubs::{extrude, loft, loft_rails, loft_to_point, revolve, screw, solid_from_record, sphere, sweep, sweep_controlled};
pub(crate) use stubs::{body_from_record, extrude, loft, loft_rails, loft_to_point, revolve, screw, sphere, sweep, sweep_controlled};
/// Refuses what no SMLib sweep does, before any work.
pub(crate) fn refuse(what: &str) -> KernelError {
@@ -183,6 +183,13 @@ mod imp {
pieces.iter().map(|x| curve(plane, &x.seg)).collect()
}
/// A chain of sketch segments on `plane` as SMLib's curves, one each, exactly (lines, arcs,
/// Béziers and conics as NURBS): what SMLib's sweeps take as a profile or a path.
#[allow(dead_code)] // For the sheet builders, which still build their own wires.
pub(crate) fn chain_curves(plane: &Plane, segs: &[Seg2]) -> Result<Vec<Curve>> {
segs.iter().map(|s| curve(plane, s)).collect()
}
/// The kernel body of an SMLib result SMLib's own check finds valid.
fn body_of(brep: &Brep, what: &str) -> Result<Body> {
let check = brep.check(CheckLevel::All).map_err(|e| failed(what, e))?;
@@ -192,9 +199,9 @@ mod imp {
crate::native::body_from_smlib(brep)
}
/// The solid a schema 4 record describes, taken by SMLib as a modeller's own record (no
/// healing) and checked there.
pub(crate) fn solid_from_record(record: &solidcraft_geom::brep::Brep, what: &str) -> Result<Body> {
/// The body (a solid or a sheet) a schema 4 record describes, taken by SMLib as a
/// modeller's own record (no healing) and checked there.
pub(crate) fn body_from_record(record: &solidcraft_geom::brep::Brep, what: &str) -> Result<Body> {
let brep = Brep::from_record(record, true).map_err(|e| failed(what, e))?;
body_of(&brep, what)
}
+1 -1
View File
@@ -72,7 +72,7 @@ pub use solver::{
};
pub use text::{MAX_TEXT_CHARS, TextAlign, TextMetrics, TextStyle, text_geometry, text_geometry_in, text_geometry_styled};
pub use thin::{MAX_THIN_SEGMENTS, thin_wall_regions};
pub use wire::{MAX_FIT_POINTS, MAX_WIRE_POINTS, Wire, WireArc, WireSpace, arc_from_3_points, arc_polyline, arc_sweep, fit_polyline};
pub use wire::{MAX_FIT_POINTS, MAX_WIRE_POINTS, Wire, WireArc, WireCurve, WireSpace, arc_from_3_points, arc_polyline, arc_sweep, fit_polyline};
#[cfg(test)]
mod circle_edge_tests;
+82 -3
View File
@@ -62,6 +62,70 @@ pub struct Wire {
/// are its three defining points (start, through, end) and whose `pts` are its samples.
#[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`.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exact: Vec<WireCurve>,
}
/// One exact piece of a 3D sketch curve ([`Wire::exact`]): a record curve on its interval.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WireCurve {
pub curve: solidcraft_geom::brep::Curve,
pub interval: [f64; 2],
}
impl WireCurve {
/// The piece carried by the rigid motion `point` of points and `vector` of directions (a
/// placement). `None` for an intersection curve, whose surfaces are not carried here.
pub fn mapped(&self, point: &impl Fn(Vec3) -> Vec3, vector: &impl Fn(Vec3) -> Vec3) -> Option<WireCurve> {
use solidcraft_geom::brep::{Curve, Frame};
let a = |q: [f64; 3]| Vec3::new(q[0], q[1], q[2]);
let b = |v: Vec3| [v.x, v.y, v.z];
let p = |q: [f64; 3]| b(point(a(q)));
let frame = |f: &Frame| Frame { origin: p(f.origin), x_axis: b(vector(a(f.x_axis))), y_axis: b(vector(a(f.y_axis))) };
let curve = match &self.curve {
Curve::Line { start, end } => Curve::Line { start: p(*start), end: p(*end) },
Curve::Circle { frame: f, radius } => Curve::Circle { frame: frame(f), radius: *radius },
Curve::Ellipse { frame: f, radii } => Curve::Ellipse { frame: frame(f), radii: *radii },
Curve::Polyline { points } => Curve::Polyline { points: points.iter().map(|q| p(*q)).collect() },
Curve::Spline { knots, points } => Curve::Spline { knots: knots.clone(), points: points.iter().map(|q| p(*q)).collect() },
Curve::RationalSpline { knots, homogeneous } => Curve::RationalSpline {
knots: knots.clone(),
homogeneous: homogeneous
.iter()
.map(|h| {
let q = point(a([h[0], h[1], h[2]]) * (1.0 / h[3]));
[q.x * h[3], q.y * h[3], q.z * h[3], h[3]]
})
.collect(),
},
Curve::SurfaceIntersection { .. } => return None,
};
Some(WireCurve { curve, interval: self.interval })
}
}
impl Wire {
/// The wire carried by the rigid motion `point` of points and `vector` of directions: its
/// points, fit points and exact curves (dropped where one cannot be carried, leaving the
/// points).
pub fn map(&mut self, point: impl Fn(Vec3) -> Vec3, vector: impl Fn(Vec3) -> Vec3) {
for q in self.pts.iter_mut().chain(&mut self.fit) {
*q = point(*q);
}
if let Some(arc) = &mut self.arc {
let normal = vector(arc.normal);
arc.center = point(arc.center);
arc.start = point(arc.start);
arc.end = point(arc.end);
arc.normal = normal.normalized().unwrap_or(arc.normal);
arc.radius = arc.center.dist(arc.start);
}
self.exact = self.exact.iter().map(|c| c.mapped(&point, &vector)).collect::<Option<_>>().unwrap_or_default();
}
}
/// Most fit points of a drawn 3D curve.
@@ -271,7 +335,22 @@ impl Sketch {
Some(o) => o.clone(),
None => self.fresh("w")?,
};
self.wires.push(Wire { id: wid, pts, space: WireSpace::Component, link: Some(id.to_string()), fit: Vec::new(), arc: None });
self.wires.push(Wire { id: wid, pts, space: WireSpace::Component, link: Some(id.to_string()), fit: Vec::new(), arc: None, exact: Vec::new() });
}
Ok(())
}
/// Give a link's wires their exact curves ([`Wire::exact`]), one list per wire in the order
/// [`Self::set_link_wires`] took them (an empty list: known only as points).
pub fn set_link_wire_curves(&mut self, id: &str, exact: &[Vec<WireCurve>]) -> Result<()> {
let wires = self.link_wires(id);
if wires.len() != exact.len() {
return Err(SketchError::Invalid(format!("link {id} has {} 3D curves, not {}", wires.len(), exact.len())));
}
for (i, curves) in wires.into_iter().zip(exact) {
if let Some(w) = self.wires.get_mut(i) {
w.exact = curves.clone();
}
}
Ok(())
}
@@ -286,7 +365,7 @@ impl Sketch {
let pts = drawn_polyline(fit)?;
self.wire_budget(None, 1, pts.len() + fit.len())?;
let id = self.fresh(if fit.len() == 1 { "q" } else { "w" })?;
self.wires.push(Wire { id: id.clone(), pts, space, link: None, fit: fit.to_vec(), arc: None });
self.wires.push(Wire { id: id.clone(), pts, space, link: None, fit: fit.to_vec(), arc: None, exact: Vec::new() });
Ok(id)
}
@@ -306,7 +385,7 @@ impl Sketch {
let pts = arc_polyline(&arc, ARC_TOL);
self.wire_budget(None, 1, pts.len() + fit.len())?;
let id = self.fresh("w")?;
self.wires.push(Wire { id: id.clone(), pts, space, link: None, fit: fit.to_vec(), arc: Some(arc) });
self.wires.push(Wire { id: id.clone(), pts, space, link: None, fit: fit.to_vec(), arc: Some(arc), exact: Vec::new() });
Ok(id)
}