#3188 follow-up: the caller's twist and scale limits in Laws; rail helpers crate-visible

- Laws::max_twist and Laws::end_scale_within: the caller's limits (SolidCraft's four
  turns and 0.01..100), refused past by name; defaults take any finite twist and
  positive scale. Ops passes its constants, no float checks there.
- rail_crossing (now returning the rail's parameter too), rail_frame, Map and apply
  are pub(crate) for the rail-guided loft (#3140).


Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruck
2026-11-09 10:50:46 -05:00
parent 62026d2103
commit bdcebb4998
2 changed files with 60 additions and 10 deletions
+31 -10
View File
@@ -52,8 +52,9 @@
//! limits are refused here (a scale not positive, a path not crossing a parallel profile's
//! plane or running along the orientation direction, a corner doubling back); SolidCraft's
//! limits on them (four turns of twist, scales 0.01 to 100, 5 degrees and `1e-3`) are the
//! ops entry's, labelled there, and the last two reach this module as
//! [`Laws::min_crossing`] and [`Laws::min_across`].
//! ops entry's, labelled there, and reach this module as the caller's limits
//! [`Laws::max_twist`], [`Laws::end_scale_within`], [`Laws::min_crossing`] and
//! [`Laws::min_across`].
//! - `twist` turns the profile about its normal (turned to point the way the path leaves it)
//! by `twist u` radians, right-handed, and `end_scale` scales it about the path by
//! `1 + (end_scale - 1) u`, `u` the fraction of the path's arc length run.
@@ -130,11 +131,12 @@ fn rotate(axis: Vec3, angle: f64, v: Vec3) -> Vec3 {
}
/// A linear map by its columns.
type Map = [Vec3; 3];
pub(crate) type Map = [Vec3; 3];
const IDENTITY: Map = [Vec3::X, Vec3::Y, Vec3::Z];
fn apply(m: &Map, v: Vec3) -> Vec3 {
/// `m v`.
pub(crate) fn apply(m: &Map, v: Vec3) -> Vec3 {
let [a, b, c] = *m;
a * v.x + b * v.y + c * v.z
}
@@ -498,6 +500,12 @@ pub struct Laws<'a> {
/// path's tangent and the direction at a station: the caller's limit (SolidCraft's is
/// `1e-3`). At 0, only a path along the direction is refused.
pub min_across: f64,
/// The most twist (radians, either way) the caller takes: its limit (SolidCraft's is four
/// turns). At infinity, any finite twist.
pub max_twist: f64,
/// The end scales the caller takes, `[least, most]`: its limits (SolidCraft's are 0.01 and
/// 100). At `[0, infinity]`, any finite positive scale.
pub end_scale_within: [f64; 2],
}
impl Default for Laws<'_> {
@@ -510,6 +518,8 @@ impl Default for Laws<'_> {
rail_fit: RailFit::Scale,
min_crossing: 0.0,
min_across: 0.0,
max_twist: f64::INFINITY,
end_scale_within: [0.0, f64::INFINITY],
}
}
}
@@ -524,6 +534,16 @@ impl Laws<'_> {
if !(self.end_scale.is_finite() && self.end_scale > 0.0) {
return Err(invalid("an end scale not finite and positive"));
}
// the caller's limits on them (`max_twist`, `end_scale_within`)
let twist_taken = self.twist.abs() <= self.max_twist;
if !twist_taken {
return Err(invalid("a twist past the most the caller takes"));
}
let [least, most] = self.end_scale_within;
let scale_taken = least <= self.end_scale && self.end_scale <= most;
if !scale_taken {
return Err(invalid("an end scale outside the caller's range"));
}
if let Orientation::Direction(d) = self.orientation {
if !(finite(d) && d.length_squared().is_finite()) {
return Err(invalid("an orientation direction not finite"));
@@ -711,11 +731,12 @@ fn transpose_rotation(r: &Map) -> Map {
]
}
/// Where `rail` crosses the plane through `p` square to the unit `t`: the root in its
/// parameter of its signed distance from the plane, taken span by span in Bernstein form.
/// Where `rail` crosses the plane through `p` square to the unit `t`, and its parameter there:
/// the root in its parameter of its signed distance from the plane, taken span by span in
/// Bernstein form.
/// Refused unless exactly one root, at which the distance changes sign over the rail (a touch
/// is not a crossing), and refused where the roots could not be isolated.
fn rail_crossing(k: &Kernel, rail: &Curve, p: Point3, t: Vec3) -> Result<Point3> {
pub(crate) fn rail_crossing(k: &Kernel, rail: &Curve, p: Point3, t: Vec3) -> Result<(f64, Point3)> {
let base = rail
.b_spline_base()
.ok_or_else(|| unsupported("a guide rail with no NURBS form"))?;
@@ -826,7 +847,7 @@ fn rail_crossing(k: &Kernel, rail: &Curve, p: Point3, t: Vec3) -> Result<Point3>
"a guide rail touching a station's plane without crossing it",
));
}
rail.evaluate_point(k, *x)
Ok((*x, rail.evaluate_point(k, *x)?))
}
[] => Err(invalid(
"a guide rail that does not reach a station's plane: it must run the path's length",
@@ -840,7 +861,7 @@ fn rail_crossing(k: &Kernel, rail: &Curve, p: Point3, t: Vec3) -> Result<Point3>
/// How guide rails turn and size a section (SolidCraft's `rail_frame`): `along` (unit) turns
/// onto `along_to`, `across` (unit, square to `along`) onto `across_to` sized by `k`, and the
/// third way follows, sized by `k` ([`RailFit::Scale`]) or kept ([`RailFit::Stretch`]).
fn rail_frame(
pub(crate) fn rail_frame(
along: Vec3,
across: Vec3,
along_to: Vec3,
@@ -967,7 +988,7 @@ fn railed(
let (p, t, _, _) = locate(measured, stop)?;
let mut hits = Vec::new();
for rail in &laws.rails {
hits.push(rail_crossing(k, rail, p, t)?);
hits.push(rail_crossing(k, rail, p, t)?.1);
}
let (anchor, v) = match hits.as_slice() {
[r] => (p, *r - p),
+29
View File
@@ -781,3 +781,32 @@ fn a_joint_is_a_corner_past_the_procedural_sweep_s_g1_rule() {
assert!(kinked(1e-10).corner_fractions().unwrap().is_empty());
assert_eq!(kinked(1e-7).corner_fractions().unwrap().len(), 1);
}
/// The caller's limits on twist and end scale are refused past, by name; the defaults take
/// any finite twist and positive scale.
#[test]
fn the_caller_s_twist_and_scale_limits_refuse_past_them() {
let k = Kernel::default();
let path = along_x(5.0);
let at = stops(&[0.0, 1.0]);
let free = Laws {
twist: 5.0 * TAU,
end_scale: 200.0,
..Laws::default()
};
stations(&k, &path, Vec3::X, &free, &at).unwrap();
let limited = |twist: f64, end_scale: f64| Laws {
twist,
end_scale,
max_twist: 4.0 * TAU,
end_scale_within: [0.01, 100.0],
..Laws::default()
};
let err = stations(&k, &path, Vec3::X, &limited(5.0 * TAU, 1.0), &at).unwrap_err();
assert!(format!("{err:?}").contains("twist past"), "{err:?}");
for s in [0.005, 200.0] {
let err = stations(&k, &path, Vec3::X, &limited(0.0, s), &at).unwrap_err();
assert!(format!("{err:?}").contains("end scale outside"), "{err:?}");
}
stations(&k, &path, Vec3::X, &limited(-4.0 * TAU, 100.0), &at).unwrap();
}