mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Point and Plane Along Path: exact on the path's curves by arc length, and a t outside 0…1 is refused
Both walked the picked curve's display polyline: a point a third along an R10 sketch arc was
0.87 µm off (8.6603, 5). They now evaluate the path's exact curves by arc length: lines and arcs
in closed form, a free-form span at the parameter its arc-length integral reaches (body edges by
their record curves, 3D sketch curves by their arc, spline, line or linked exact curves; only a
curve that exists as points alone, an intersection edge or a projected curve, runs along them).
The arc's point is now within 1e-12 mm of (10 cos 30°, 5, 0).
A t outside 0…1 was clamped silently, which the contract (`t: expr, 0…1 along it`) does not
say: it is refused ("t is 1.5: a point along a path is 0 (its start) to 1 (its end) of the way
along it"). The construction cases grade the arc and line to 1e-9 mm, past-the-end is refused,
and a new case takes the middle of a symmetric cubic spline, (20, 7.5).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+59
-24
@@ -428,31 +428,66 @@ fn centre(ctx: &Ctx, g: &GeoRef) -> Result<Vec3> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Point and unit tangent at fraction `t` along a polyline.
|
||||
fn along(pts: &[Vec3], t: f64) -> Option<(Vec3, Vec3)> {
|
||||
let total: f64 = pts.windows(2).map(|w| (w[1] - w[0]).len()).sum();
|
||||
if !(total > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let mut left = t.clamp(0.0, 1.0) * total;
|
||||
for w in pts.windows(2) {
|
||||
let l = (w[1] - w[0]).len();
|
||||
if l <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
if left <= l {
|
||||
return Some((w[0] + (w[1] - w[0]) * (left / l), (w[1] - w[0]) * (1.0 / l)));
|
||||
}
|
||||
left -= l;
|
||||
}
|
||||
let (a, b) = (pts.get(pts.len() - 2)?, pts.last()?);
|
||||
Some((*b, (*b - *a).normalized()?))
|
||||
/// The point a fraction `t` (0…1) of the way along a picked edge or sketch curve by arc length,
|
||||
/// and the unit direction of travel there ([`kernel::along_path`]): exact on the curve itself.
|
||||
fn along(ctx: &Ctx, g: &GeoRef, t: f64) -> Result<(Vec3, Vec3)> {
|
||||
Ok(kernel::along_path(&path_segs(ctx, g)?, t)?)
|
||||
}
|
||||
|
||||
fn path_points(ctx: &Ctx, g: &GeoRef) -> Result<Vec<Vec3>> {
|
||||
/// A picked edge or sketch curve as the path segments that are its curve exactly, from the end
|
||||
/// the drawn curve starts at: a body edge's record curve ([`kernel::edge_path`]), a planar sketch
|
||||
/// curve's exact pieces, a 3D sketch curve's arc, spline, line or linked exact curves. Only a
|
||||
/// curve that exists as points alone runs along them: an intersection edge (its record holds no
|
||||
/// exact curve, only the surfaces and a leader near it), a 3D curve known only as its points
|
||||
/// (projected onto a face, iso curves), a spline above the exact pieces' degree.
|
||||
fn path_segs(ctx: &Ctx, g: &GeoRef) -> Result<Vec<kernel::PathSeg>> {
|
||||
let lines = |pts: &[Vec3]| -> Vec<kernel::PathSeg> {
|
||||
pts.windows(2).filter(|w| w[0].dist(w[1]) > 0.0).map(|w| kernel::PathSeg::Line { a: w[0], b: w[1] }).collect()
|
||||
};
|
||||
match g {
|
||||
GeoRef::Edge { at, name } => edge_points(ctx.st, *at, name),
|
||||
GeoRef::SketchCurve { sketch, curve } => sketch_curve_points(ctx, *sketch, curve),
|
||||
GeoRef::Edge { at, name } => {
|
||||
let (b, i, pts) = edge_pick(ctx.st, *at, name)?;
|
||||
let procedural =
|
||||
b.body.record().edges.get(i).is_some_and(|e| matches!(e.parameters(), solidcraft_geom::brep::Curve::SurfaceIntersection { .. }));
|
||||
if procedural {
|
||||
return Ok(lines(&pts));
|
||||
}
|
||||
let segs = kernel::edge_path(&b.body, i)?;
|
||||
// The drawn edge sets which way t runs, as the picked edge's polyline has it.
|
||||
let (Some(first), Some(start)) = (pts.first(), segs.first().map(kernel::PathSeg::start)) else { return Ok(segs) };
|
||||
let end = segs.last().map(kernel::PathSeg::end).unwrap_or(start);
|
||||
Ok(if end.dist(*first) < start.dist(*first) { segs.iter().rev().map(kernel::PathSeg::reversed).collect() } else { segs })
|
||||
}
|
||||
GeoRef::SketchCurve { sketch, curve } => {
|
||||
let ss = ctx.st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?;
|
||||
// Evaluation has already mapped wires into this component's frame: their coordinates
|
||||
// are independent of the sketch plane.
|
||||
if let Some(wire) = ss.sketch.wires.iter().find(|wire| wire.id == *curve) {
|
||||
if let Some(arc) = &wire.arc {
|
||||
let angle = solidcraft_sketch::arc_sweep(arc);
|
||||
return Ok(vec![kernel::PathSeg::Arc { a: arc.start, center: arc.center, axis: arc.normal, angle }]);
|
||||
}
|
||||
if wire.fit.len() >= 3 {
|
||||
let spline = kernel::PathPiece::Spline { fit: wire.fit.clone(), reversed: false };
|
||||
if let Some(spans) = spline.spans()? {
|
||||
return Ok(spans);
|
||||
}
|
||||
}
|
||||
if let [a, b] = wire.fit.as_slice() {
|
||||
return Ok(vec![kernel::PathSeg::Line { a: *a, b: *b }]);
|
||||
}
|
||||
if !wire.exact.is_empty() {
|
||||
let mut segs = Vec::new();
|
||||
for c in &wire.exact {
|
||||
segs.extend(kernel::curve_path(&c.curve, c.interval)?);
|
||||
}
|
||||
return Ok(segs);
|
||||
}
|
||||
return Ok(lines(&wire.pts));
|
||||
}
|
||||
let ci = ss.sketch.curve_index(curve).ok_or_else(|| DocError::Unknown(format!("curve `{curve}`")))?;
|
||||
Ok(ss.sketch.exact_pieces(ci).iter().map(|s| kernel::PathSeg::of_sketch(&ss.plane, s)).collect())
|
||||
}
|
||||
_ => Err(bad("pick an edge or sketch curve as the path")),
|
||||
}
|
||||
}
|
||||
@@ -571,7 +606,7 @@ pub(crate) fn resolve_point(ctx: &Ctx, d: &PointDef) -> Result<Vec3> {
|
||||
}
|
||||
PointDef::AlongPath { path, t } => {
|
||||
let t = Document::eval_in(ctx.vals, t, Kind::Unitless)?;
|
||||
along(&path_points(ctx, path)?, t).map(|(p, _)| p).ok_or_else(|| bad("the path has no length"))
|
||||
along(ctx, path, t).map(|(p, _)| p)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -634,7 +669,7 @@ pub(crate) fn resolve_geo_plane(ctx: &Ctx, p: &PlaneRef) -> Result<Plane> {
|
||||
}
|
||||
PlaneRef::AlongPath { path, t } => {
|
||||
let t = Document::eval_in(ctx.vals, t, Kind::Unitless)?;
|
||||
let (p, tan) = along(&path_points(ctx, path)?, t).ok_or_else(|| bad("the path has no length"))?;
|
||||
let (p, tan) = along(ctx, path, t)?;
|
||||
Plane::from_normal(p, tan).ok_or_else(degenerate)
|
||||
}
|
||||
PlaneRef::ThroughPoint { base, point: at } => {
|
||||
|
||||
@@ -213,6 +213,24 @@ fn point_along_a_path() {
|
||||
run(&mut s, "construct.point.along_path", json!({"path": {"edge": [20, 0, 0]}, "t": 0.25, "name": "P"}));
|
||||
let p = point(&s, "P");
|
||||
assert!(near(p, Vec3::new(10.0, 0.0, 0.0)) || near(p, Vec3::new(30.0, 0.0, 0.0)), "{p:?}");
|
||||
// On an arc it is the arc's own point by arc length, to rounding: a third of a quarter turn
|
||||
// of radius 10 is 30° round. A t past either end is refused, not moved onto it.
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Round"}));
|
||||
let arc = run(&mut s, "sketch.arc.center_point", json!({"center": [0, 0], "start": [10, 0], "end": [0, 10]}))["created"]["curves"][0]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let path = json!({"sketch": "Round", "curve": arc});
|
||||
run(&mut s, "construct.point.along_path", json!({"path": path, "t": "1/3", "name": "Third"}));
|
||||
let third = point(&s, "Third");
|
||||
let want = Vec3::new(10.0 * (std::f64::consts::PI / 6.0).cos(), 5.0, 0.0);
|
||||
assert!(third.dist(want) < 1e-12, "{third:?} is {:e} off", third.dist(want));
|
||||
for t in [json!(1.5), json!(-0.25), json!("2")] {
|
||||
let e = s.execute("construct.point.along_path", &json!({"path": path, "t": t})).unwrap_err();
|
||||
assert!(e.to_string().contains("0 (its start) to 1 (its end)"), "t {t}: {e}");
|
||||
assert!(s.execute("construct.plane.along_path", &json!({"path": path, "t": t})).is_err(), "plane at t {t}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1234,6 +1234,62 @@ impl PathSeg {
|
||||
}
|
||||
}
|
||||
|
||||
/// The segment as the rational Bézier span in space that is the same curve on the same
|
||||
/// parameter: a sketch cubic, Bézier or conic carried onto its plane (an affine image keeps
|
||||
/// the weights). `None` for a line or an arc, which have closed forms of their own, and for a
|
||||
/// span that does not evaluate.
|
||||
fn space_bezier(&self) -> Option<crate::point_path::SpaceBezier> {
|
||||
use solidcraft_geom::bspline::HPoint;
|
||||
let PathSeg::Curve { plane, seg } = *self else {
|
||||
return match *self {
|
||||
PathSeg::Bezier(b) => Some(b),
|
||||
_ => None,
|
||||
};
|
||||
};
|
||||
let w = |p: Vec2, weight: f64| HPoint::weighted(plane.to_world(p), weight);
|
||||
match seg {
|
||||
Seg2::Cubic { p0, p1, p2, p3 } => crate::point_path::SpaceBezier::new(&[w(p0, 1.0), w(p1, 1.0), w(p2, 1.0), w(p3, 1.0)]),
|
||||
Seg2::Bezier { n, p } => {
|
||||
let ctrl: Vec<HPoint> = p.get(..=usize::from(n))?.iter().map(|q| w(*q, 1.0)).collect();
|
||||
crate::point_path::SpaceBezier::new(&ctrl)
|
||||
}
|
||||
Seg2::Conic { a, apex, b, w: weight } => crate::point_path::SpaceBezier::new(&[w(a, 1.0), w(apex, weight), w(b, 1.0)]),
|
||||
Seg2::Line { .. } | Seg2::Arc { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The segment's length: a line's and an arc's in closed form, a free-form span's by its
|
||||
/// arc-length integral ([`crate::SpaceBezier::length`]). `None` for a segment that does not
|
||||
/// evaluate.
|
||||
pub fn length(&self) -> Option<f64> {
|
||||
let l = match *self {
|
||||
PathSeg::Line { a, b } => a.dist(b),
|
||||
PathSeg::Arc { a, center, angle, .. } => a.dist(center) * angle.abs(),
|
||||
PathSeg::Curve { plane, seg: seg @ (Seg2::Line { .. } | Seg2::Arc { .. }) } => return PathSeg::of_sketch(&plane, &seg).length(),
|
||||
PathSeg::Curve { .. } | PathSeg::Bezier(_) => self.space_bezier()?.length()?,
|
||||
};
|
||||
l.is_finite().then_some(l)
|
||||
}
|
||||
|
||||
/// The point a fraction `f` (0…1) of the way along the segment by arc length, and the unit
|
||||
/// direction of travel there: a line's and an arc's in closed form (an arc's length grows
|
||||
/// with its angle), a free-form span's at the parameter where its arc-length integral reaches
|
||||
/// that fraction ([`crate::SpaceBezier::along`]). `None` for a segment that does not
|
||||
/// evaluate or has no direction.
|
||||
pub fn along(&self, f: f64) -> Option<(Vec3, Vec3)> {
|
||||
let f = f.clamp(0.0, 1.0);
|
||||
match *self {
|
||||
PathSeg::Line { a, b } => Some((a.lerp(b, f), (b - a).normalized()?)),
|
||||
PathSeg::Arc { a, center, axis, angle } => {
|
||||
let k = axis.normalized()?;
|
||||
let p = rotate_about(a, center, k, angle * f);
|
||||
Some((p, (k.cross(p - center) * angle.signum()).normalized()?))
|
||||
}
|
||||
PathSeg::Curve { plane, seg: seg @ (Seg2::Line { .. } | Seg2::Arc { .. }) } => PathSeg::of_sketch(&plane, &seg).along(f),
|
||||
PathSeg::Curve { .. } | PathSeg::Bezier(_) => self.space_bezier()?.along(f),
|
||||
}
|
||||
}
|
||||
|
||||
/// The segment the other way round.
|
||||
pub fn reversed(&self) -> PathSeg {
|
||||
match *self {
|
||||
|
||||
@@ -201,7 +201,8 @@ pub use native::{brep_record, decode_native_body, encode_native_body, native_pre
|
||||
pub use offset::with_same_surface;
|
||||
pub use ops::{BoolOp, boolean, half_space, join_apart, place_matrix, split_by_plane, transform, transform_matrix, union_all, union_apart};
|
||||
pub use point_path::{
|
||||
PathPiece, PointPath, SpaceBezier, curve_path, edge_path, face_section_along, project_onto_face, sketch_record_curve, sweep_point_path_with,
|
||||
PathPiece, PointPath, SpaceBezier, along_path, curve_path, edge_path, face_section_along, project_onto_face, sketch_record_curve,
|
||||
sweep_point_path_with,
|
||||
};
|
||||
pub use polybool::faceted_boolean;
|
||||
pub use polyhedron::{HalfSpace, convex_polyhedron, draft, offset_faces, shell};
|
||||
|
||||
@@ -65,7 +65,7 @@ impl PathPiece {
|
||||
/// The piece's exact segments in the direction of travel: a spline of three or more fit
|
||||
/// points as its spans ([`PathSeg::Bezier`]), a curve held exactly as its segments; `None`
|
||||
/// for a polyline (and a spline of one or two points, a point or a line).
|
||||
pub(crate) fn spans(&self) -> Result<Option<Vec<PathSeg>>> {
|
||||
pub fn spans(&self) -> Result<Option<Vec<PathSeg>>> {
|
||||
let (fit, reversed) = match self {
|
||||
PathPiece::Spline { fit, reversed } => (fit, reversed),
|
||||
PathPiece::Segs(segs) => return Ok(Some(segs.clone())),
|
||||
@@ -141,6 +141,33 @@ pub fn sweep_point_path_with(plane: &Plane, region: &Region2, path: &PointPath,
|
||||
crate::sweep_path::smlib_sweep_points(plane, region, path, ctl)
|
||||
}
|
||||
|
||||
/// The point a fraction `t` of the way along `path` (its segments end to end) by arc length, and
|
||||
/// the unit direction of travel there: 0 is where the path starts, 1 where it ends. Each
|
||||
/// segment's length and its point at a length are exact ([`PathSeg::length`],
|
||||
/// [`PathSeg::along`]): closed forms for lines and arcs, the arc-length parameter of a free-form
|
||||
/// span. A `t` outside 0…1 is refused: the path does not go on past its ends.
|
||||
pub fn along_path(path: &[PathSeg], t: f64) -> Result<(Vec3, Vec3)> {
|
||||
if !(0.0..=1.0).contains(&t) {
|
||||
return Err(KernelError::Invalid(format!("t is {t}: a point along a path is 0 (its start) to 1 (its end) of the way along it")));
|
||||
}
|
||||
let bad = || KernelError::Invalid("a segment of the path does not evaluate".into());
|
||||
let lengths: Vec<f64> = path.iter().map(|s| s.length().ok_or_else(bad)).collect::<Result<_>>()?;
|
||||
let total: f64 = lengths.iter().sum();
|
||||
if !(total > 0.0 && total.is_finite()) {
|
||||
return Err(KernelError::Invalid("the path has no length".into()));
|
||||
}
|
||||
let mut left = t * total;
|
||||
let live = || path.iter().zip(&lengths).filter(|(_, l)| **l > 0.0);
|
||||
let last = live().next_back().ok_or_else(bad)?;
|
||||
for (seg, &l) in live() {
|
||||
if left <= l || std::ptr::eq(seg, last.0) {
|
||||
return seg.along((left / l).min(1.0)).ok_or_else(bad);
|
||||
}
|
||||
left -= l;
|
||||
}
|
||||
Err(bad())
|
||||
}
|
||||
|
||||
/// Most segments one edge's curve may give a path.
|
||||
const MAX_EDGE_SEGS: usize = 1000;
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ crates/engine/src/cmd/construct.rs
|
||||
# How a construction reference resolves, and the planes the document resolves.
|
||||
crates/doc/src/construct.rs
|
||||
crates/doc/src/document.rs
|
||||
# A point along a path: the path's exact curves by arc length.
|
||||
crates/kernel/src/point_path.rs
|
||||
# The constructions as evaluated, which the grader reads.
|
||||
crates/engine/src/view.rs
|
||||
tests/kernel-cases/construction/**
|
||||
|
||||
+5
-2
@@ -2,7 +2,7 @@
|
||||
"sources": [],
|
||||
"what": "a point a third along a quarter-circle arc",
|
||||
"source": "analytic",
|
||||
"method": "the arc r 10 about the origin from (10,0) to (0,10) counter-clockwise: a third of its length is 30 deg round: (10 cos 30, 10 sin 30, 0) = (8.6603, 5, 0)",
|
||||
"method": "the arc r 10 about the origin from (10,0) to (0,10) counter-clockwise: a third of its length is 30 deg round: (10 cos 30, 10 sin 30, 0) = (8.6603, 5, 0); the point is on the arc itself, so it is graded to 1e-9 mm (a chord polyline is 0.87 um off)",
|
||||
"construction": {
|
||||
"P": {
|
||||
"point": [
|
||||
@@ -11,5 +11,8 @@
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"tolerance": {
|
||||
"abs_mm": 1e-09
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"sources": [],
|
||||
"what": "a point half way along a cubic spline symmetric about x = 20",
|
||||
"source": "analytic",
|
||||
"method": "the Bezier (0,0), (10,10), (30,10), (40,0) is its own mirror image in x = 20 (the parameter running backwards), so half its length is reached at its middle, u = 1/2: B(1/2) = (P0 + 3 P1 + 3 P2 + P3) / 8 = (20, 7.5, 0). Graded to 1e-9 mm: the point by the spline's own arc length, not a polyline's",
|
||||
"tolerance": {
|
||||
"abs_mm": 1e-09
|
||||
},
|
||||
"construction": {
|
||||
"P": {
|
||||
"point": [
|
||||
20.0,
|
||||
7.5,
|
||||
0.0
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "sketch.create",
|
||||
"params": {
|
||||
"plane": "XY",
|
||||
"name": "Path"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.spline.control_point",
|
||||
"params": {
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
10,
|
||||
10
|
||||
],
|
||||
[
|
||||
30,
|
||||
10
|
||||
],
|
||||
[
|
||||
40,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "sketch.finish",
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"command": "construct.point.along_path",
|
||||
"params": {
|
||||
"path": {
|
||||
"sketch": "Path",
|
||||
"curve": "s1"
|
||||
},
|
||||
"t": 0.5,
|
||||
"name": "P"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,15 +1,8 @@
|
||||
{
|
||||
"sources": [],
|
||||
"what": "a point at t 1.5, past the end of a sketch line",
|
||||
"what": "a point at t 1.5, past the end of a sketch line, is refused",
|
||||
"source": "analytic",
|
||||
"method": "t runs 0..1 along the path; a point along it can go no further than its end: (40,0,0)",
|
||||
"construction": {
|
||||
"P": {
|
||||
"point": [
|
||||
40,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"method": "t runs 0..1 along the path (the command's contract: `t: expr, 0\u20261 along it`); 1.5 is past its end, and a point along the path can go no further, so it is refused rather than moved back to the end",
|
||||
"outcome": "refused",
|
||||
"refusal": "0 (its start) to 1 (its end)"
|
||||
}
|
||||
+5
-2
@@ -2,7 +2,7 @@
|
||||
"sources": [],
|
||||
"what": "a point a quarter along a sketch line",
|
||||
"source": "analytic",
|
||||
"method": "the line l1 from (0,0,0) to (40,0,0): t 0.25 is (10,0,0)",
|
||||
"method": "the line l1 from (0,0,0) to (40,0,0): t 0.25 is (10,0,0); exact, graded to 1e-9 mm",
|
||||
"construction": {
|
||||
"P": {
|
||||
"point": [
|
||||
@@ -11,5 +11,8 @@
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"tolerance": {
|
||||
"abs_mm": 1e-09
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user