From ffb1e650df33d7dbbeaa6ed32c19fdaec347fffb Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:41:33 -0400 Subject: [PATCH] Isocurve Analysis: count curves each way, evenly spaced across the face MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/doc/src/eval.rs | 6 +- crates/kernel/src/iso.rs | 110 ++++-------------- .../expected.json | 102 ++++++++++++++++ .../recipe.json | 27 +++++ 4 files changed, 154 insertions(+), 91 deletions(-) create mode 100644 tests/kernel-cases/inspect/cov-isocurve-40-by-30-face-count-3/expected.json create mode 100644 tests/kernel-cases/inspect/cov-isocurve-40-by-30-face-count-3/recipe.json diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 289dab12ab..efcd233ab7 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -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. diff --git a/crates/kernel/src/iso.rs b/crates/kernel/src/iso.rs index 391ed144bf..1e6e7cbd08 100644 --- a/crates/kernel/src/iso.rs +++ b/crates/kernel/src/iso.rs @@ -191,14 +191,14 @@ pub fn iso_curves(body: &Body, face: usize, at: Vec3, along_u: bool) -> Result Result> { 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 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 = 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 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 { - 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 { - 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. diff --git a/tests/kernel-cases/inspect/cov-isocurve-40-by-30-face-count-3/expected.json b/tests/kernel-cases/inspect/cov-isocurve-40-by-30-face-count-3/expected.json new file mode 100644 index 0000000000..61b4efb794 --- /dev/null +++ b/tests/kernel-cases/inspect/cov-isocurve-40-by-30-face-count-3/expected.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/tests/kernel-cases/inspect/cov-isocurve-40-by-30-face-count-3/recipe.json b/tests/kernel-cases/inspect/cov-isocurve-40-by-30-face-count-3/recipe.json new file mode 100644 index 0000000000..4aa2e11332 --- /dev/null +++ b/tests/kernel-cases/inspect/cov-isocurve-40-by-30-face-count-3/recipe.json @@ -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 + } + } + ] +} \ No newline at end of file