mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Isocurve Analysis: count curves each way, evenly spaced across the face
The curves were held on a 1-2-5 ladder from the world origin, so count 3 on a 40 x 30 face gave 3 one way and 2 the other. They now follow the command's contract: the held parameter at lo + k (hi - lo) / (count + 1), k = 1…count, over the face's extent. Faces of the body on the same surface record are spaced together over their combined extent, so a face a boss splits still shows the same lines straight across the split. New case cov-isocurve-40-by-30-face-count-3: x = 10, 20, 30 and y = 7.5, 15, 22.5, to 1e-9 mm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -347,9 +347,9 @@ impl ModelBody {
|
||||
self.mesh.get().cloned()
|
||||
}
|
||||
|
||||
/// The isoparametric curves of face `face` for Isocurve Analysis: spaced by `count` on the
|
||||
/// face's surface (so faces on one surface share them), exact and cut to its trims
|
||||
/// ([`kernel::iso_grid`]), their points at the chord
|
||||
/// The isoparametric curves of face `face` for Isocurve Analysis: `count` each way, evenly
|
||||
/// spaced across the face (faces on one surface spaced together, so they share them), exact
|
||||
/// and cut to its trims ([`kernel::iso_grid`]), their points at the chord
|
||||
/// tolerance of the viewport's display quality. Worked out once per face, count and display
|
||||
/// quality, and shared by every clone of this body (timeline snapshots, world states). A mesh
|
||||
/// body has no surfaces, so none.
|
||||
|
||||
+22
-88
@@ -191,14 +191,14 @@ pub fn iso_curves(body: &Body, face: usize, at: Vec3, along_u: bool) -> Result<V
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The isoparametric curves of face `face` of `body` for Isocurve Analysis, the held parameter
|
||||
/// on a ladder of the face's surface ([`iso_ladder`]) rather than on the face's own extent, so
|
||||
/// faces on one surface (a face split by a boss, the pieces of a split cylinder) show the same
|
||||
/// lines, straight across where they meet. `count` (1…[`MAX_ISO_COUNT`]) sets the density: a
|
||||
/// bounded parameter's range in `count + 1` steps, a linear one in steps of the body's size over
|
||||
/// `count + 1`, rounded to 1, 2 or 5 times a power of ten. The rungs strictly inside the face's
|
||||
/// extent are cut to the face ([`FaceTrims::clip`]), their points at chord tolerance `chord`.
|
||||
/// Along u first, then v.
|
||||
/// The isoparametric curves of face `face` of `body` for Isocurve Analysis: `count`
|
||||
/// (1…[`MAX_ISO_COUNT`]) curves each way, evenly spaced across the face, the held parameter at
|
||||
/// `lo + k · (hi − lo) / (count + 1)` for `k = 1…count` over the face's extent in (u, v), so
|
||||
/// neither side of the extent (the face's boundary) is drawn as a curve. Faces of the body on the
|
||||
/// very same surface (a face a boss splits in two) are spaced together, across the extent of all
|
||||
/// of them, so their curves are the same lines, straight across where they meet. Each curve is
|
||||
/// cut to the face ([`FaceTrims::clip`]), its points at chord tolerance `chord`. Along u first,
|
||||
/// then v.
|
||||
pub fn iso_grid(body: &Body, face: usize, count: usize, chord: f64) -> Result<Vec<IsoLine>> {
|
||||
if !(1..=MAX_ISO_COUNT).contains(&count) {
|
||||
return Err(KernelError::Invalid(format!("iso curves: 1…{MAX_ISO_COUNT} each way")));
|
||||
@@ -207,28 +207,15 @@ pub fn iso_grid(body: &Body, face: usize, count: usize, chord: f64) -> Result<Ve
|
||||
return Err(KernelError::Invalid("iso curves: the chord tolerance must be positive".into()));
|
||||
}
|
||||
let trims = FaceTrims::new(body, face)?;
|
||||
let spacing = round_spacing(body.size() / (count + 1) as f64)
|
||||
.ok_or_else(|| KernelError::Invalid("iso curves: the body has no size to space them by".into()))?;
|
||||
let [[u0, u1], [v0, v1]] = trims.extent();
|
||||
let [[u0, u1], [v0, v1]] = shared_extent(body, face, &trims)?;
|
||||
let mut out = Vec::new();
|
||||
for along_u in [true, false] {
|
||||
let (lo, hi) = if along_u { (v0, v1) } else { (u0, u1) };
|
||||
if !(hi > lo) {
|
||||
continue;
|
||||
}
|
||||
let Some((start, step)) = iso_ladder(trims.surface, &trims.eval, !along_u, count, spacing) else { continue };
|
||||
// Rungs on the extent's sides are its boundary, not curves across it.
|
||||
let eps = step * 1e-6;
|
||||
let first = ((lo + eps - start) / step).ceil();
|
||||
let last = ((hi - eps - start) / step).floor();
|
||||
if !(first.is_finite() && last.is_finite()) {
|
||||
continue;
|
||||
}
|
||||
// The ladder is spaced so that a face holds at most about `count + 1` rungs; the cap
|
||||
// only guards a surface whose parameters run far faster than the body's size.
|
||||
let rungs = (last - first + 1.0).clamp(0.0, (4 * MAX_ISO_COUNT) as f64) as usize;
|
||||
for k in 0..rungs {
|
||||
let fixed = start + (first + k as f64) * step;
|
||||
for k in 1..=count {
|
||||
let fixed = lo + (hi - lo) * k as f64 / (count + 1) as f64;
|
||||
for (curve, interval) in trims.clip(along_u, fixed)? {
|
||||
let segs = curve_path(&curve, interval)?;
|
||||
let mut points: Vec<Vec3> = segs.first().map(crate::PathSeg::start).into_iter().collect();
|
||||
@@ -242,72 +229,19 @@ pub fn iso_grid(body: &Body, face: usize, count: usize, chord: f64) -> Result<Ve
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Where the iso lines of a surface fall in one of its parameters (`held_u`: u, else v):
|
||||
/// `start + k * step` for whole k, fixed by the surface alone. A bounded parameter (an angle, a
|
||||
/// B-spline's knot range) is its range in `count + 1` steps from its start. A linear one (a
|
||||
/// plane's, a cylinder's or cone's along its axis) has rungs `spacing` apart in the world,
|
||||
/// counted from the world origin along the parameter's gradient, so another record of the same
|
||||
/// plane or axis (its own origin, its own face) gets the same lines. None for a parameter that is
|
||||
/// neither.
|
||||
fn iso_ladder(surface: &brep::Surface, eval: &SurfaceEval, held_u: bool, count: usize, spacing: f64) -> Option<(f64, f64)> {
|
||||
use solidcraft_geom::brep::Surface;
|
||||
let d = if held_u { 0 } else { 1 };
|
||||
if let Some((a, b)) = eval.domain().range.get(d).copied().flatten().filter(|(a, b)| b > a) {
|
||||
return Some((a, (b - a) / (count + 1) as f64));
|
||||
}
|
||||
let v = |q: [f64; 3]| Vec3::new(q[0], q[1], q[2]);
|
||||
// The parameter as (P - origin) · g, P a point of the surface.
|
||||
let (origin, g) = match surface {
|
||||
Surface::Plane { origin, u_axis, v_axis } => (v(*origin), plane_gradient(v(*u_axis), v(*v_axis), held_u)?),
|
||||
Surface::PlaneThrough { origin, u_point, v_point } => {
|
||||
let o = v(*origin);
|
||||
(o, plane_gradient(v(*u_point) - o, v(*v_point) - o, held_u)?)
|
||||
/// The extent in (u, v) of face `face` together with every other face of `body` on the same
|
||||
/// surface record (the same surface and parameters, so one (u, v) for all of them): the box round
|
||||
/// all their extents ([`FaceTrims::extent`]).
|
||||
fn shared_extent(body: &Body, face: usize, trims: &FaceTrims) -> Result<[[f64; 2]; 2]> {
|
||||
let mut b = trims.extent();
|
||||
for (i, other) in body.record().faces.iter().enumerate() {
|
||||
if i == face || other.parameters() != trims.surface {
|
||||
continue;
|
||||
}
|
||||
// Along the axis: P = origin + z * v + (radial part, at right angles to z).
|
||||
Surface::Cylinder { frame, .. } | Surface::Cone { frame, .. } if !held_u => {
|
||||
let z = v(frame.x_axis).cross(v(frame.y_axis));
|
||||
let zz = z.dot(z);
|
||||
if !(zz > 1e-300) {
|
||||
return None;
|
||||
}
|
||||
(v(frame.origin), z * (1.0 / zz))
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let len = g.len();
|
||||
if !(len.is_finite() && len > 0.0) {
|
||||
return None;
|
||||
let [[a0, a1], [c0, c1]] = FaceTrims::new(body, i)?.extent();
|
||||
b = [[b[0][0].min(a0), b[0][1].max(a1)], [b[1][0].min(c0), b[1][1].max(c1)]];
|
||||
}
|
||||
// The parameter of the world plane P · g/|g| = k * spacing is k * spacing * |g| - origin · g.
|
||||
Some((-origin.dot(g), spacing * len))
|
||||
}
|
||||
|
||||
/// The gradient of a plane's u (`held_u`) or v in the world: the dual of its axes in their plane.
|
||||
fn plane_gradient(du: Vec3, dv: Vec3, held_u: bool) -> Option<Vec3> {
|
||||
let n = du.cross(dv);
|
||||
let nn = n.dot(n);
|
||||
if !(nn > 1e-300) {
|
||||
return None;
|
||||
}
|
||||
Some(if held_u { dv.cross(n) } else { n.cross(du) } * (1.0 / nn))
|
||||
}
|
||||
|
||||
/// `x` rounded down to 1, 2 or 5 times a power of ten (None unless it is positive and finite).
|
||||
fn round_spacing(x: f64) -> Option<f64> {
|
||||
if !(x.is_finite() && x > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let p = 10f64.powf(x.log10().floor());
|
||||
let m = x / p;
|
||||
Some(
|
||||
p * if m >= 5.0 {
|
||||
5.0
|
||||
} else if m >= 2.0 {
|
||||
2.0
|
||||
} else {
|
||||
1.0
|
||||
},
|
||||
)
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
/// A pcurve's parameters and points at [`ISO_SAMPLES`] per knot span, in its direction.
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"sources": [],
|
||||
"what": "three isocurves each way across a 40 x 30 face",
|
||||
"source": "analytic",
|
||||
"method": "box 40 x 30 x 10 with a corner at the origin: its top is the plane z = 10, x 0..40 and y 0..30. count 3 curves each way, evenly spaced across it (the command's contract), divide each side into 4: x = 10, 20, 30 (each from y 0 to 30) and y = 7.5, 15, 22.5 (each from x 0 to 40), every curve the straight line across the face. In the order the reply lists them (along the surface's first parameter first, as its parameters run), to 1e-9 mm",
|
||||
"results": [
|
||||
{
|
||||
"step": 2,
|
||||
"pointer": "/faces/0/count",
|
||||
"json": 3
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"pointer": "/faces/0/u_curves",
|
||||
"json": 3
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"pointer": "/faces/0/v_curves",
|
||||
"json": 3
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"pointer": "/curves",
|
||||
"json": [
|
||||
[
|
||||
[
|
||||
30.0,
|
||||
0.0,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
30.0,
|
||||
30.0,
|
||||
10.0
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
20.0,
|
||||
0.0,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
20.0,
|
||||
30.0,
|
||||
10.0
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
10.0,
|
||||
0.0,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
10.0,
|
||||
30.0,
|
||||
10.0
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
40.0,
|
||||
7.5,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
7.5,
|
||||
10.0
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
40.0,
|
||||
15.0,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
15.0,
|
||||
10.0
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
40.0,
|
||||
22.5,
|
||||
10.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
22.5,
|
||||
10.0
|
||||
]
|
||||
]
|
||||
],
|
||||
"abs_mm": 1e-09
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "solid.box",
|
||||
"params": {
|
||||
"length": 40,
|
||||
"width": 30,
|
||||
"height": 10,
|
||||
"body_name": "Slab",
|
||||
"operation": "new"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "inspect.isocurve",
|
||||
"params": {
|
||||
"faces": [
|
||||
[
|
||||
20,
|
||||
15,
|
||||
10
|
||||
]
|
||||
],
|
||||
"count": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user