kernel: extend on SMLib: the surface's own extension, up to a target by trim, to bounds by untrim

extend_surface keeps its signature, options and error messages. One model:
SMLib extends the surface past a side of the face's parameter box
(smlib_extend_sheet, CreateExtendedSurface: natural continues the end spans,
tangent G1, curvature G2) and the face stays one face on it, tagged so the
face is Modified and its sides keep their edges. Several picked sides grow
the box on each. Up to a target, the face is extended 50 body sizes past
its edge and trimmed back with trim_keep (SMLib's exact intersection),
keeping the piece the face lies in. Back to the bounds is untrim of the
picked edge's face, in place, with its holes; one already there is
refused. A plane's straight edges with neighbours lengthened still go
through surfaces::extend. Edges are picked on the schema 4 record (samples
of each edge's curve), an edge two faces share or of a hole refused as before.

Not ported (every far edge there was a cubic fitted through marched
samples, which the exact-B-splines rule forbids): an edge that is not a
whole side of its surface's box (trimmed edges, curved planar edges such as
a disc's, a notch), mitred corners on such faces, sides run square to the
edge on a non-box face, and bodies of more than one face. These now answer
"not supported yet: ...". Tangent and curvature no longer add a separate
run-off face: SMLib's G1/G2 extension is one face (the engine's
surface.extend description says so).

Tests ported keeping their assertions: the planar extension, the cylinder
strip along its axis, around and both (now an extruded arc, not a truck
revolution), the extruded strip, the parabola extended naturally (now a
conic extruded), back to bounds (the saddle from loft_sheet and trim),
hostile requests. Deleted, as what they test no longer exists:
neighbours_lengthened_or_square, self_intersecting_extensions_are_refused,
a_disc_grows_by_its_curved_edges, neighbouring_faces_extend_together, the
tangent/curvature run-off half of the B-spline test, and
a_continued_bspline_is_unchanged_on_its_range (the deleted continue_span).

SMLIB_REV: fork 9531966 (tags carried through untrim and extend).

(not built: base in progress)

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:34 -04:00
parent 7bbdd37130
commit 0a216d6925
5 changed files with 224 additions and 1431 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("surface.extend", "Extend", extend)
.at("SURFACE", "MODIFY")
.icon("surface_extend")
.params("body: surface name; edges: [[x,y,z]] (points on boundary edges to push out); distance: expr (unless up_to); mode?: natural (the surface itself continued: exact for planes and surfaces of revolution, a B-spline's end spans continued; the default) | tangent (a ruled run-off face along each edge, G1) | curvature (a run-off face keeping the surface's curvature across the edge, G2); up_to?: plane (XY|XZ|YZ|plane name|{origin, normal}) | {body, point} (a face, its surface extended) | \"bounds\" (natural: back to the surface's own bounds, undoing a trim); neighbours?: bool (default true: the edges next to an extended edge are lengthened to meet it; false: the extension's sides run square to the edge)"),
.params("body: surface name; edges: [[x,y,z]] (points on boundary edges to push out); distance: expr (unless up_to); mode?: natural (the surface itself continued: a B-spline's end spans continued, which keeps planes and surfaces of revolution on themselves; the default) | tangent (the face continued tangent to its surface across the edge, G1) | curvature (continued keeping the surface's curvature across the edge too, G2); up_to?: plane (XY|XZ|YZ|plane name|{origin, normal}) | {body, point} (a face, its surface extended) | \"bounds\" (natural: back to the surface's own bounds, undoing a trim); neighbours?: bool (default true: the edges next to an extended edge are lengthened to meet it; false: the extension's sides run square to the edge)"),
CommandSpec::new("surface.trim", "Trim", trim)
.at("SURFACE", "MODIFY")
.icon("surface_trim")
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -107,7 +107,7 @@ fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
}
/// A point inside face `face` of `body`: the centre of its largest mesh triangle.
fn face_point(body: &Body, face: usize) -> Result<Vec3> {
pub(crate) fn face_point(body: &Body, face: usize) -> Result<Vec3> {
let m = body.mesh(&crate::MeshTolerance::chord((body.size() * 1e-3).max(1e-3)))?;
let area = |[a, b, c]: &[Vec3; 3]| (*b - *a).cross(*c - *a).len();
m.triangles
@@ -244,12 +244,13 @@ mod tests {
}
/// A free-form face goes back to its surface's own parameter box: the saddle z = x y / 10
/// over 0…10 × 0…10 (ruled between two straight lines: a bilinear patch), cut at x = 5 and
/// over 0…10 × 0…10 (ruled between two straight lines, sketched on planes y = 0 and 10 whose sketch y is −z: a
/// bilinear patch), cut at x = 5 and
/// restored to the whole patch: x and y 0…10, z 0…10.
#[test]
fn a_free_form_face_is_restored_to_its_surfaces_bounds() {
let front = (Plane::XZ, vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 0.0) }]);
let back = (Plane { origin: Vec3::new(0.0, 10.0, 0.0), ..Plane::XZ }, vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 10.0) }]);
let back = (Plane { origin: Vec3::new(0.0, 10.0, 0.0), ..Plane::XZ }, vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, -10.0) }]);
let saddle = crate::loft_sheet(&[front, back]).unwrap();
let tool = crate::SplitTool::Plane(Plane::from_normal(Vec3::new(5.0, 0.0, 0.0), Vec3::X).unwrap());
let half = crate::trim(&saddle, &tool, Vec3::new(1.0, 5.0, 0.5)).unwrap();
+18 -2
View File
@@ -1,5 +1,6 @@
//! The untrim block of `capi/include/smlib.h` (`smlib_untrim_face`; the fork's
//! `capi/src/smlib_untrim.cpp`), declared by hand. Part of the FFI boundary, like its parent: called only through [`crate::trim`].
//! The untrim and extend blocks of `capi/include/smlib.h` (`smlib_untrim_face`,
//! `smlib_extend_sheet`; the fork's `capi/src/smlib_untrim.cpp` and `smlib_extend.cpp`), declared
//! by hand. Part of the FFI boundary, like its parent: called only through [`crate::trim`].
#![allow(unsafe_code)]
@@ -12,6 +13,10 @@ use super::{RawBrep, new_brep};
/// The answer is not exact in the input's geometry (`SMLIB_ERR_NOT_EXACT`).
pub const ERR_NOT_EXACT: i32 = -1106;
pub const EXTEND_NATURAL: i32 = 0;
pub const EXTEND_TANGENT: i32 = 1;
pub const EXTEND_CURVATURE: i32 = 2;
#[cfg(feature = "native")]
unsafe extern "C" {
fn smlib_untrim_face(
@@ -24,6 +29,17 @@ unsafe extern "C" {
in_place: i32,
out: *mut *mut RawBrep,
) -> i32;
fn smlib_extend_sheet(sheet: *const RawBrep, point: *const f64, tol: f64, distance: f64, mode: i32, out: *mut *mut RawBrep) -> i32;
}
#[cfg(feature = "native")]
pub fn extend_sheet(sheet: &Handle, point: &[f64; 3], tol: f64, distance: f64, mode: i32) -> Result<Handle, i32> {
new_brep(|out| unsafe { smlib_extend_sheet(sheet.raw(), point.as_ptr(), tol, distance, mode, out) })
}
#[cfg(not(feature = "native"))]
pub fn extend_sheet(_: &Handle, _: &[f64; 3], _: f64, _: f64, _: i32) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
/// `holes`: `None` for every hole.
+29 -1
View File
@@ -1,8 +1,19 @@
//! Untrim of a face. Each returns a new body and leaves its input unchanged;
//! Untrim and extend of a sheet's faces. Each returns a new body and leaves its input unchanged;
//! faces and holes are chosen by points, as [`crate::ops`] chooses them.
use crate::{Brep, Result, error, ffi, init};
/// How [`Brep::extend_sheet`] continues a surface past its side.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Continuation {
/// The surface's own geometry (a B-spline's end spans continued).
Natural,
/// A run-off tangent to the surface across the side (G1).
Tangent,
/// A run-off that also keeps the surface's curvature across the side (G2).
Curvature,
}
/// The inner loops [`Brep::untrim_face`] takes away.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Holes<'a> {
@@ -28,4 +39,21 @@ impl Brep {
s => error(s),
})
}
/// The one-face sheet `self`, whose face is its surface's whole parameter box, continued
/// `distance` past the side its edge nearest `point` (within `tolerance`) runs along, as
/// `how` says: SMLib's extended surface over the grown box, the face and its sides carrying
/// the tags of those they continue. A sheet of another shape is [`crate::Error::Unsupported`].
pub fn extend_sheet(&self, point: [f64; 3], tolerance: f64, distance: f64, how: Continuation) -> Result<Brep> {
init()?;
let mode = match how {
Continuation::Natural => ffi::trim::EXTEND_NATURAL,
Continuation::Tangent => ffi::trim::EXTEND_TANGENT,
Continuation::Curvature => ffi::trim::EXTEND_CURVATURE,
};
ffi::trim::extend_sheet(&self.0, &point, tolerance, distance, mode).map(Brep).map_err(|s| match s {
ffi::ops::ERR_PICK_NONE => crate::Error::NoMatch { point: 0 },
s => error(s),
})
}
}