mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #1489 from brett19/fix/swept-bar-shell-offsets
fix: shell a swept bar (SurfOfExtrusion offsets, input-scaled HANG-44 bound) (fixes #1476)
This commit is contained in:
@@ -53,7 +53,9 @@ use solidcore::nurbs::math::CPoint;
|
||||
use solidcore::nurbs::srf_basic::Surface as NurbSurface;
|
||||
use solidcore::offset::offset_executive::p1::{OffsetExecutive, OffsetOperation};
|
||||
use solidcore::offset::offset_geometry_creation::SsiData;
|
||||
use solidcore::surface::offset_surface::{OffsetSurface, SS_UNKNOWN};
|
||||
use solidcore::surface::plane::Plane;
|
||||
use solidcore::surface::surf_of_extrusion::SurfOfExtrusion;
|
||||
use solidcore::surface::surface::{Surface, SurfaceKind};
|
||||
use solidcore::topology::brep::p1 as brep_p1;
|
||||
use solidcore::topology::brep::p7::{self as brep_p7, FaceCurvesInput};
|
||||
@@ -89,6 +91,59 @@ enum Inputs {
|
||||
Probe { branch: Probe, approx_tol: f64 },
|
||||
FaceTail(FaceTailIn),
|
||||
MapTwin(MapTwinIn),
|
||||
Replace(ReplaceIn),
|
||||
}
|
||||
|
||||
/// `ReplaceImplicitOffsets` on one face whose surface is an owning
|
||||
/// `SmOffsetSurface(offset_distance, base)` (see [`ffi::Replace`]).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ReplaceIn {
|
||||
base: OffsetBaseIn,
|
||||
offset_distance: f64,
|
||||
approx_tol: f64,
|
||||
samples: Vec<[f64; 2]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
enum OffsetBaseIn {
|
||||
Surface {
|
||||
surface: SurfaceIn,
|
||||
},
|
||||
Extrusion {
|
||||
generator: CurveIn,
|
||||
origin: [f64; 3],
|
||||
direction: [f64; 3],
|
||||
domain: [f64; 4],
|
||||
swap_uv: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl ReplaceIn {
|
||||
fn ffi(&self) -> ffi::Replace {
|
||||
ffi::Replace {
|
||||
base: match &self.base {
|
||||
OffsetBaseIn::Surface { surface } => ffi::OffsetBase::Surface(surface.ffi()),
|
||||
OffsetBaseIn::Extrusion {
|
||||
generator,
|
||||
origin,
|
||||
direction,
|
||||
domain,
|
||||
swap_uv,
|
||||
} => ffi::OffsetBase::Extrusion {
|
||||
generator: generator.ffi(),
|
||||
origin: *origin,
|
||||
direction: *direction,
|
||||
domain: *domain,
|
||||
swap_uv: *swap_uv,
|
||||
},
|
||||
},
|
||||
offset_distance: self.offset_distance,
|
||||
approx_tol: self.approx_tol,
|
||||
samples: self.samples.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `BuildOffsetOfFaceuse` of a unit-square plane face onto the plane
|
||||
@@ -842,6 +897,30 @@ fn decode(input: &Inputs, raw: &[f64]) -> HResult<Output> {
|
||||
let uses = mapped_uses(&mut snapshot)?;
|
||||
output = map_twin_output(status, uses);
|
||||
}
|
||||
Inputs::Replace(_) => {
|
||||
output = output.int("replace.status", snapshot.integer()?);
|
||||
let faces = snapshot.count()?;
|
||||
output = output.int("replace.face_count", faces as i64);
|
||||
for face in 0..faces {
|
||||
let key = format!("replace.face.{face}");
|
||||
let kind = snapshot.integer()?;
|
||||
output = output.int(&format!("{key}.surface_type"), kind);
|
||||
output = output.values(&format!("{key}.uv_domain"), snapshot.take(4)?.to_vec());
|
||||
if kind < 0 {
|
||||
continue;
|
||||
}
|
||||
output = output.values(&format!("{key}.natural_domain"), snapshot.take(4)?.to_vec());
|
||||
let samples = snapshot.count()?;
|
||||
let mut statuses = Vec::with_capacity(samples);
|
||||
let mut points = Vec::with_capacity(samples * 3);
|
||||
for _ in 0..samples {
|
||||
statuses.push(snapshot.integer()?);
|
||||
points.extend_from_slice(snapshot.take(3)?);
|
||||
}
|
||||
output = output.ints(&format!("{key}.sample_status"), statuses);
|
||||
output = output.values(&format!("{key}.samples"), points);
|
||||
}
|
||||
}
|
||||
}
|
||||
snapshot.done()?;
|
||||
normalize_sync_ties(output)
|
||||
@@ -2309,6 +2388,79 @@ fn rust_map_twin(input: &MapTwinIn) -> RResult<Output> {
|
||||
Ok(map_twin_output(status, uses))
|
||||
}
|
||||
|
||||
/// The shim's `replace`: the same record, built from the port.
|
||||
fn rust_replace(input: &ReplaceIn) -> RResult<Output> {
|
||||
guard(ffi::validate_replace(&input.ffi()))?;
|
||||
let mut kernel = Kernel::default();
|
||||
let base = match &input.base {
|
||||
OffsetBaseIn::Surface { surface } => surface.rust()?,
|
||||
OffsetBaseIn::Extrusion {
|
||||
generator,
|
||||
origin,
|
||||
direction,
|
||||
domain,
|
||||
swap_uv,
|
||||
} => {
|
||||
let [u0, v0, u1, v1] = *domain;
|
||||
let extrusion = SurfOfExtrusion::new(
|
||||
&kernel,
|
||||
generator.rust()?,
|
||||
v3(*origin),
|
||||
v3(*direction),
|
||||
Extent2d::from_bounds(u0, v0, u1, v1),
|
||||
*swap_uv,
|
||||
)
|
||||
.map_err(setup)?;
|
||||
Surface::from(extrusion)
|
||||
}
|
||||
};
|
||||
let offset = OffsetSurface::new(&kernel, input.offset_distance, base, true, SS_UNKNOWN)
|
||||
.map_err(setup)?;
|
||||
let mut original = super::blend_fillet_geom_p1::new_brep(&mut kernel)?;
|
||||
super::blend_fillet_geom_p1::natural_face(&mut kernel, &mut original, Surface::from(offset))?;
|
||||
let result = probe_env(|env, _| {
|
||||
OffsetExecutive::replace_implicit_offsets(
|
||||
&mut kernel,
|
||||
&mut original,
|
||||
input.approx_tol,
|
||||
&mut solidcore::topology::edge::NotificationStores {
|
||||
records: &mut *env.records,
|
||||
attributes: &mut *env.attributes,
|
||||
key_of: env.key_of,
|
||||
},
|
||||
)
|
||||
});
|
||||
let (status, _) = operation(result)?;
|
||||
let faces = brep_p1::faces(&original, None).map_err(setup)?;
|
||||
let mut raw = vec![0.0, status as f64, faces.len() as f64];
|
||||
for f in faces {
|
||||
let face = original.face(f).map_err(setup)?;
|
||||
let d = face.uv_domain;
|
||||
let Some(surface) = face.surface.as_ref() else {
|
||||
raw.extend([-1.0, d.min().x, d.min().y, d.max().x, d.max().y]);
|
||||
continue;
|
||||
};
|
||||
raw.extend([surface.type_code() as f64, d.min().x, d.min().y, d.max().x, d.max().y]);
|
||||
let n = surface.natural_uv_domain_with_nurb(&kernel)?;
|
||||
raw.extend([n.min().x, n.min().y, n.max().x, n.max().y]);
|
||||
raw.push(input.samples.len() as f64);
|
||||
for &[fu, fv] in &input.samples {
|
||||
let uv = Vec2::new(
|
||||
(1.0 - fu) * n.min().x + fu * n.max().x,
|
||||
(1.0 - fv) * n.min().y + fv * n.max().y,
|
||||
);
|
||||
match surface.evaluate_point(&kernel, uv) {
|
||||
Ok(p) => raw.extend([f64::from(ffi::NATIVE_SUCCESS), p.x, p.y, p.z]),
|
||||
Err(SolidError::Upstream { code, .. }) => {
|
||||
raw.extend([f64::from(code), 0.0, 0.0, 0.0]);
|
||||
}
|
||||
Err(e) => return Err(RunError::Kernel(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(decode(&Inputs::Replace(input.clone()), &raw)?)
|
||||
}
|
||||
|
||||
fn vertex_point(brep: &Brep, edgeuse: EdgeuseId) -> HResult<Vec3> {
|
||||
let vertexuse = brep
|
||||
.edgeuse(edgeuse)
|
||||
@@ -2522,6 +2674,7 @@ impl UnitDiff for Runner {
|
||||
Inputs::Probe { branch, approx_tol } => ffi::probe(branch.ffi(), *approx_tol),
|
||||
Inputs::FaceTail(input) => ffi::face_tail(&input.ffi()),
|
||||
Inputs::MapTwin(input) => ffi::map_twin(&input.ffi()),
|
||||
Inputs::Replace(input) => ffi::replace(&input.ffi()),
|
||||
}
|
||||
.map_err(|error| bad(format!("native adapter: {error}")))?;
|
||||
decode(&input, &raw)
|
||||
@@ -2536,6 +2689,7 @@ impl UnitDiff for Runner {
|
||||
Inputs::Probe { branch, approx_tol } => rust_probe(k, branch, approx_tol),
|
||||
Inputs::FaceTail(input) => rust_face_tail(&input),
|
||||
Inputs::MapTwin(input) => rust_map_twin(&input),
|
||||
Inputs::Replace(input) => rust_replace(&input),
|
||||
};
|
||||
match result {
|
||||
Ok(output) => normalize_sync_ties(output),
|
||||
@@ -2802,6 +2956,37 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{compare, CaseSource};
|
||||
|
||||
/// Issue #1476: every `replace` fixture (extrusion, plane and B-spline bases)
|
||||
/// replaces its offset surface by a B-spline approximation on both sides, and the
|
||||
/// sides agree; an extrusion base was refused with `SM_ERR` before.
|
||||
#[test]
|
||||
fn replace_fixtures_build_bspline_approximations() {
|
||||
let context = CaseContext {
|
||||
root: crate::case::repo_root(),
|
||||
seeds: 0,
|
||||
};
|
||||
let mut extrusions = 0;
|
||||
for fixture in &Runner.cases(&context).unwrap() {
|
||||
let Inputs::Replace(input) = fixture.parse().unwrap() else {
|
||||
continue;
|
||||
};
|
||||
extrusions += usize::from(matches!(input.base, OffsetBaseIn::Extrusion { .. }));
|
||||
let native = Runner.run_cpp(fixture).unwrap();
|
||||
let rust = Runner.run_rust(fixture).unwrap();
|
||||
let diff = compare(&native, &rust, &[]);
|
||||
assert!(diff.pass(), "{}: {diff:?}", fixture.name);
|
||||
assert_eq!(rust.ints["replace.status"], [1000], "{}", fixture.name);
|
||||
assert_eq!(rust.ints["replace.face_count"], [1], "{}", fixture.name);
|
||||
assert_eq!(
|
||||
rust.ints["replace.face.0.surface_type"],
|
||||
[BSplineSurface::TYPE],
|
||||
"{}",
|
||||
fixture.name
|
||||
);
|
||||
}
|
||||
assert!(extrusions >= 10, "{extrusions}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertex_fixtures_compare_actual_caps_not_only_presence() {
|
||||
let context = CaseContext {
|
||||
@@ -3006,8 +3191,8 @@ mod tests {
|
||||
.iter()
|
||||
.filter(|case| matches!(case.source, CaseSource::Fixture { .. }))
|
||||
.collect();
|
||||
assert_eq!(fixtures.len(), 93 + 16 + 312 + 27);
|
||||
// 93 original, 16 vertex, 312 face_tail, 27 map_twin.
|
||||
assert_eq!(fixtures.len(), 93 + 16 + 312 + 27 + 12);
|
||||
// 93 original, 16 vertex, 312 face_tail, 27 map_twin, 12 replace (#1476).
|
||||
for case in fixtures {
|
||||
case.parse::<Inputs>().unwrap();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//! | [`vertex`] | `BuildOffsetOfVertex`, including both real `VertexOffset` callsites |
|
||||
//! | [`face_tail`] | `BuildOffsetOfFaceuse` (contained-domain branch), `MapEdgeuses`, `MakeFaceWithCurves` (fixture) |
|
||||
//! | [`map_twin`] | `MapEdgeuses` alone, between two `MakeFaceWithCurves` rectangle faces |
|
||||
//! | [`replace`] | `ReplaceImplicitOffsets` on one face whose surface is an owning `SmOffsetSurface` of a real `SmPlane`, `SmBSplineSurface` or `SmSurfOfExtrusion` |
|
||||
//! | [`probe`] | `ReplaceImplicitOffsets`, `RemoveExcessFaces`, `BuildSideWallOfEdgeuse`, `BuildOffsetOfEdgeuses`, `FillGapEdgeuses`, `BuildOffsetOfVertex`, `BuildOffsetOfFaceuse`, `PiecewiseMerge`, `MapEdgeuses`; `sm_NotifyEdgeAndVertices` indirectly via `MapEdgeuses` |
|
||||
//!
|
||||
//! Deviations: caller-owned flat snapshots instead of pointers/out-parameters;
|
||||
@@ -121,6 +122,9 @@
|
||||
//! `1, start_xyz[3], mate_start_xyz[3]` of the offset use.
|
||||
//! * `map_twin`: `0, status, number_original_edgeuses` and the same per-use
|
||||
//! records as `face_tail`.
|
||||
//! * `replace`: `0, status, face_count`, per face `surface_type, face_uv_domain[4]`
|
||||
//! and, with a surface, its `natural_uv_domain[4]` and counted samples (fractions
|
||||
//! of that domain) `point_status, xyz[3]`.
|
||||
//!
|
||||
//! **Relation ordering:** this checkout uses pointer-hashed `unordered_set`
|
||||
//! values in `SmRelation`. We preserve actual `GetSeconds` order; equal-minimum
|
||||
@@ -418,6 +422,42 @@ pub struct MapTwin {
|
||||
pub split_bottom: Option<f64>,
|
||||
}
|
||||
|
||||
/// The base of the offset surface of a [`replace`] fixture.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum OffsetBase {
|
||||
/// A real `SmPlane` or `SmBSplineSurface`.
|
||||
Surface(SurfaceFixture),
|
||||
/// `SmSurfOfExtrusion(generator, origin, direction, domain, swap_uv)`, without
|
||||
/// NURB (the executive's `GetOrCreateGwNurbPointer` makes it).
|
||||
Extrusion {
|
||||
/// The generator (an `SmBSplineCurve` kind).
|
||||
generator: CurveFixture,
|
||||
/// A point on the generator.
|
||||
origin: [f64; 3],
|
||||
/// The extrusion vector.
|
||||
direction: [f64; 3],
|
||||
/// The analytic domain `umin, vmin, umax, vmax` (U = the generator's domain).
|
||||
domain: [f64; 4],
|
||||
/// The generator is a constant-U iso-curve of the NURB.
|
||||
swap_uv: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// A [`replace`] fixture: one `CreateFaceFromSurface` face (natural domain) on an
|
||||
/// owning `SmOffsetSurface(offset_distance, base)`, then
|
||||
/// `ReplaceImplicitOffsets(brep, approx_tol)`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Replace {
|
||||
/// The offset surface's base.
|
||||
pub base: OffsetBase,
|
||||
/// The offset distance.
|
||||
pub offset_distance: f64,
|
||||
/// The approximation tolerance passed to `ReplaceImplicitOffsets`.
|
||||
pub approx_tol: f64,
|
||||
/// Sample fractions in `[0, 1]^2` of each result face surface's natural domain.
|
||||
pub samples: Vec<[f64; 2]>,
|
||||
}
|
||||
|
||||
mod sys {
|
||||
extern "C" {
|
||||
pub fn sc_offset_offset_executive_p1_eval(
|
||||
@@ -947,6 +987,55 @@ pub fn map_twin(input: &MapTwin) -> Result<Vec<f64>, FfiError> {
|
||||
run(5, &map_twin_input(input)?)
|
||||
}
|
||||
|
||||
fn replace_input(input: &Replace) -> Result<Pack, FfiError> {
|
||||
finite(&[input.offset_distance, input.approx_tol])?;
|
||||
if input.samples.len() > 64 {
|
||||
return Err(bad("at most 64 replace samples"));
|
||||
}
|
||||
let mut pack = Pack::default();
|
||||
match &input.base {
|
||||
OffsetBase::Surface(surface) => {
|
||||
pack.add(&[0.0])?;
|
||||
pack.surface(surface)?;
|
||||
}
|
||||
OffsetBase::Extrusion {
|
||||
generator,
|
||||
origin,
|
||||
direction,
|
||||
domain,
|
||||
swap_uv,
|
||||
} => {
|
||||
finite(origin)?;
|
||||
finite(direction)?;
|
||||
let [a, b, c, d] = *domain;
|
||||
interval([a, c], true)?;
|
||||
interval([b, d], true)?;
|
||||
pack.add(&[1.0])?;
|
||||
pack.curve(generator)?;
|
||||
pack.add(origin)?;
|
||||
pack.add(direction)?;
|
||||
pack.add(domain)?;
|
||||
pack.add(&[f64::from(u8::from(*swap_uv))])?;
|
||||
}
|
||||
}
|
||||
pack.add(&[input.offset_distance, input.approx_tol])?;
|
||||
pack.size(input.samples.len())?;
|
||||
for sample in &input.samples {
|
||||
finite(sample)?;
|
||||
if sample.iter().any(|v| !(0.0..=1.0).contains(v)) {
|
||||
return Err(bad("surface sample fractions must be in [0, 1]"));
|
||||
}
|
||||
pack.add(sample)?;
|
||||
}
|
||||
Ok(pack)
|
||||
}
|
||||
|
||||
/// Runs the native `ReplaceImplicitOffsets` on one real offset-surface face. See the
|
||||
/// module-level protocol for the returned record.
|
||||
pub fn replace(input: &Replace) -> Result<Vec<f64>, FfiError> {
|
||||
run(7, &replace_input(input)?)
|
||||
}
|
||||
|
||||
// The packers' input checks without the native call: the difftest runner's Rust
|
||||
// side refuses exactly what the FFI refuses by running the same code (counts,
|
||||
// finiteness, selector ranges, fixture geometry validation and the packed-size
|
||||
@@ -969,6 +1058,10 @@ pub fn validate_extend(input: &Extension) -> Result<(), FfiError> {
|
||||
pub fn validate_face_tail(input: &FaceTail) -> Result<(), FfiError> {
|
||||
face_tail_input(input).map(|_| ())
|
||||
}
|
||||
/// [`replace`]'s input checks alone.
|
||||
pub fn validate_replace(input: &Replace) -> Result<(), FfiError> {
|
||||
replace_input(input).map(|_| ())
|
||||
}
|
||||
/// [`map_twin`]'s input checks alone.
|
||||
pub fn validate_map_twin(input: &MapTwin) -> Result<(), FfiError> {
|
||||
map_twin_input(input).map(|_| ())
|
||||
|
||||
@@ -24,10 +24,15 @@
|
||||
//! [`MAX_SHORT_DEGREE`](crate::nurbs::bezier::MAX_SHORT_DEGREE) (impossible
|
||||
//! upstream, where `NL_DEGREE` is a `short`) is [`SolidError::InvalidInput`], so
|
||||
//! index arithmetic and allocations sized from a degree cannot overflow.
|
||||
//! - More than [`MAX_OFFSET_SAMPLES`] sample points in total is
|
||||
//! [`SolidError::InvalidInput`]. Upstream samples any count, but the knot removal
|
||||
//! that follows takes time quadratic in it (about 5 s for 23 000 samples, and
|
||||
//! practically forever for the counts a pole or a tiny tolerance produce).
|
||||
//! - HANG-44: more sample points in total than the input-scaled
|
||||
//! [`offset_sample_bound`] is [`SolidError::InvalidInput`]. Upstream samples any
|
||||
//! count, but the knot removal that follows takes time quadratic in it (practically
|
||||
//! forever for the counts a pole or a tiny tolerance produce). The bound is
|
||||
//! `min(2^17, max(16 384, 4 * segments * ceil((1/tol)^exp * 2 deg)))`: any count up
|
||||
//! to [`MAX_OFFSET_SAMPLES`] (the former fixed cap, so every input it accepted is
|
||||
//! unchanged), four times the curvature-free count a curve of that many Bezier
|
||||
//! segments needs at `tol` (a swept wall of 127 segments at `1e-5` needs 38 227),
|
||||
//! and never more than [`MAX_OFFSET_SAMPLES_ANY`] (about 30 s at opt-level 3).
|
||||
|
||||
use crate::error::{NlibCode, Result, SolidError};
|
||||
use crate::math::{max2, Vec3};
|
||||
@@ -47,10 +52,37 @@ use crate::nurbs::math::{blend_pt, float_op_is_bad, FloatOp, MODEL_TOL};
|
||||
|
||||
const OP: &str = "nurbs::crv_offset::Curve::offset_pt_sampling";
|
||||
|
||||
/// Most sample points [`Curve::offset_pt_sampling`] takes on a free-form curve (the
|
||||
/// sum of the per-segment counts; the difftest guard allows 4000).
|
||||
/// Sample points [`Curve::offset_pt_sampling`] always allows on a free-form curve (the
|
||||
/// sum of the per-segment counts; the difftest guard allows 4000). Above it the
|
||||
/// input-scaled [`offset_sample_bound`] applies.
|
||||
pub const MAX_OFFSET_SAMPLES: isize = 16_384;
|
||||
|
||||
/// How many times its curvature-free count (`ceil(f1 * 2 deg)` per Bezier segment) a
|
||||
/// curve may sample in [`offset_sample_bound`].
|
||||
pub const OFFSET_SAMPLE_SLACK: isize = 4;
|
||||
|
||||
/// The most sample points [`Curve::offset_pt_sampling`] takes on any curve: the knot
|
||||
/// removal that follows is quadratic in the count.
|
||||
pub const MAX_OFFSET_SAMPLES_ANY: isize = 1 << 17;
|
||||
|
||||
/// The most sample points [`Curve::offset_pt_sampling`] takes on a free-form curve of
|
||||
/// `segments` Bezier segments whose per-segment count without the curvature term
|
||||
/// (`ceil((1/tol)^exp * 2 deg)`, the least upstream samples a segment) is `floor`:
|
||||
/// `OFFSET_SAMPLE_SLACK * segments * floor`, clamped to
|
||||
/// `[MAX_OFFSET_SAMPLES, MAX_OFFSET_SAMPLES_ANY]`. A well-formed curve samples at
|
||||
/// most `OFFSET_SAMPLE_SLACK` times its floor (its offset's second derivative `Muu`
|
||||
/// stays below `8 (2 deg OFFSET_SAMPLE_SLACK)^2` on average); a pole or a cusp of the
|
||||
/// offset drives `Muu`, and so the count, far beyond it.
|
||||
#[must_use]
|
||||
pub fn offset_sample_bound(segments: usize, floor: isize) -> isize {
|
||||
let scaled = isize::try_from(segments)
|
||||
.ok()
|
||||
.and_then(|n| n.checked_mul(floor))
|
||||
.and_then(|n| n.checked_mul(OFFSET_SAMPLE_SLACK))
|
||||
.unwrap_or(isize::MAX);
|
||||
scaled.clamp(MAX_OFFSET_SAMPLES, MAX_OFFSET_SAMPLES_ANY)
|
||||
}
|
||||
|
||||
/// Upstream `NL_UNITSPAN`.
|
||||
const UNIT_SPAN: Interval = Interval::new(0.0, 1.0);
|
||||
|
||||
@@ -211,7 +243,9 @@ impl Curve {
|
||||
nu.push(c);
|
||||
mu = mu.checked_add(c).ok_or_else(unrepresentable)?;
|
||||
}
|
||||
if mu > MAX_OFFSET_SAMPLES {
|
||||
// HANG-44: the input-scaled bound, from `del` without the curvature term.
|
||||
if mu > MAX_OFFSET_SAMPLES && mu > offset_sample_bound(bez.len(), count((f1 * f2).ceil())?)
|
||||
{
|
||||
return Err(SolidError::InvalidInput {
|
||||
operation: OP,
|
||||
reason: "too many sample points",
|
||||
@@ -438,6 +472,9 @@ mod tests {
|
||||
1.0,
|
||||
];
|
||||
let c = Curve::new(pw, 3, u).unwrap();
|
||||
// HANG-44 addendum 2026-10-29: still refused under the input-scaled bound (5
|
||||
// segments, floor 13 each, so the bound is 16 384), before any sampling.
|
||||
let clock = std::time::Instant::now();
|
||||
let r = c.offset_pt_sampling(
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
0.0,
|
||||
@@ -446,7 +483,10 @@ mod tests {
|
||||
OffsetParam::Inherited,
|
||||
0.11799393559359235,
|
||||
);
|
||||
let took = clock.elapsed();
|
||||
eprintln!("fuzz00366 refused in {took:?}");
|
||||
assert!(matches!(r, Err(SolidError::InvalidInput { .. })), "{r:?}");
|
||||
assert!(took < std::time::Duration::from_secs(2), "{took:?}");
|
||||
let r = cubic().offset_pt_sampling(
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
0.1,
|
||||
@@ -457,4 +497,67 @@ mod tests {
|
||||
);
|
||||
assert!(matches!(r, Err(SolidError::InvalidInput { .. })), "{r:?}");
|
||||
}
|
||||
|
||||
/// HANG-44 addendum 2026-10-29: the bound scales with the segment count and the
|
||||
/// tolerance, never drops below the former cap and never exceeds the ceiling.
|
||||
#[test]
|
||||
fn sample_bound_is_input_scaled() {
|
||||
assert_eq!(offset_sample_bound(5, 13), MAX_OFFSET_SAMPLES);
|
||||
assert_eq!(offset_sample_bound(60, 301), 4 * 60 * 301);
|
||||
assert_eq!(offset_sample_bound(127, 301), MAX_OFFSET_SAMPLES_ANY);
|
||||
assert_eq!(
|
||||
offset_sample_bound(usize::MAX, isize::MAX),
|
||||
MAX_OFFSET_SAMPLES_ANY
|
||||
);
|
||||
assert_eq!(offset_sample_bound(0, 0), MAX_OFFSET_SAMPLES);
|
||||
}
|
||||
|
||||
/// A tiny tolerance asks for about 72 000 samples per segment even on a gentle
|
||||
/// curve: refused at once by the ceiling.
|
||||
#[test]
|
||||
fn tiny_tolerance_is_refused_quickly() {
|
||||
let clock = std::time::Instant::now();
|
||||
let r = cubic().offset_pt_sampling(
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
0.1,
|
||||
3,
|
||||
false,
|
||||
OffsetParam::Inherited,
|
||||
1e-12,
|
||||
);
|
||||
let took = clock.elapsed();
|
||||
assert!(matches!(r, Err(SolidError::InvalidInput { .. })), "{r:?}");
|
||||
assert!(took < std::time::Duration::from_secs(2), "{took:?}");
|
||||
}
|
||||
|
||||
/// A gentle cubic of `n` segments (control points on a sine).
|
||||
fn wavy(n: usize) -> Curve {
|
||||
let pw = (0..n + 3)
|
||||
.map(|i| {
|
||||
let x = i as f64 * 0.5;
|
||||
CPoint::new(x, (x * 0.7).sin() * 2.0, 0.0, crate::nurbs::math::NOW)
|
||||
})
|
||||
.collect();
|
||||
let mut u = vec![0.0; 4];
|
||||
u.extend((1..n).map(|i| i as f64 / n as f64));
|
||||
u.extend([1.0; 4]);
|
||||
Curve::new(pw, 3, u).unwrap()
|
||||
}
|
||||
|
||||
/// Issue #1476: 60 segments at `1e-5` sample 60 * 301 = 18 060 points, above the
|
||||
/// former fixed cap of 16 384, and are offset now. Slow in a debug build (the
|
||||
/// quadratic knot removal); `issue_1476_swept_bar_shell` keeps the 38 227-sample
|
||||
/// case live.
|
||||
#[test]
|
||||
#[ignore = "slow in debug builds (about 18 000 samples)"]
|
||||
fn many_segments_at_a_tight_tolerance_are_offset() {
|
||||
let c = wavy(60);
|
||||
let b = Vec3::new(0.0, 0.0, 1.0);
|
||||
let q = c
|
||||
.offset_pt_sampling(b, 0.5, 3, false, OffsetParam::Inherited, 1e-5)
|
||||
.unwrap();
|
||||
let (p0, t0) = c.eval_tangent(0.0, Side::Left).unwrap();
|
||||
let o = pt_along_vector(p0, 0.5, b.cross(t0));
|
||||
assert!((q.cpts()[0].to_euclid() - o).length() < 1e-5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,8 +669,10 @@ impl OffsetExecutive {
|
||||
let base = offset
|
||||
.extended_base_surface()
|
||||
.unwrap_or_else(|| offset.base_surface());
|
||||
// make sure base surface is an SmBSplineSurface
|
||||
let SurfaceKind::BSpline(nurb) = base.kind() else {
|
||||
// make sure base surface is an SmBSplineSurface (`SM_CAST_PTR`, which every
|
||||
// subclass passes: plane, extrusion, revolution, cone, sphere, torus); its
|
||||
// B-spline part with the NURB `GetOrCreateGwNurbPointer` makes when missing
|
||||
let Some(nurb) = crate::topology::brep::p3::with_nurb(kernel, base)? else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
// use NLIB function to build offset surface approximation
|
||||
@@ -3821,9 +3823,31 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!((p.z - 0.25).abs() < 1.0e-6, "{p:?}");
|
||||
|
||||
// a base that is not a B-spline surface is SM_ERR
|
||||
// a plane is an SmBSplineSurface too (issue #1476): its offset is approximated
|
||||
let offset = OffsetSurface::new(&kernel, 0.25, plane(0.0), true, 0).unwrap();
|
||||
let mut brep = Brep::new();
|
||||
let (offset_face, _) = add_face(&mut brep, Surface::from(offset));
|
||||
OffsetExecutive::replace_implicit_offsets(
|
||||
&mut kernel,
|
||||
&mut brep,
|
||||
1.0e-6,
|
||||
&mut NotificationStores {
|
||||
records: &mut records,
|
||||
attributes: &mut attributes,
|
||||
key_of: &loopuse::notify_ref,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let surface = brep.face(offset_face).unwrap().surface.clone().unwrap();
|
||||
assert!(matches!(surface.kind(), SurfaceKind::BSpline(_)));
|
||||
let mid = surface.natural_uv_domain_if_built().mid();
|
||||
let p = surface.evaluate_point(&kernel, mid).unwrap();
|
||||
assert!((p.z - 0.25).abs() < 1.0e-6, "{p:?}");
|
||||
|
||||
// a base that is not a B-spline surface (here an offset surface) is SM_ERR
|
||||
let inner = OffsetSurface::new(&kernel, 0.25, plane(0.0), true, 0).unwrap();
|
||||
let offset = OffsetSurface::new(&kernel, 0.25, Surface::from(inner), true, 0).unwrap();
|
||||
let mut brep = Brep::new();
|
||||
add_face(&mut brep, Surface::from(offset));
|
||||
let status = OffsetExecutive::replace_implicit_offsets(
|
||||
&mut kernel,
|
||||
|
||||
@@ -3547,7 +3547,7 @@ fn squeeze_loops(
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Unported`] from a surface of revolution's `MakeNurb`.
|
||||
fn with_nurb<'a>(k: &Kernel, s: &'a Surface) -> Result<Option<Cow<'a, BSplineSurface>>> {
|
||||
pub(crate) fn with_nurb<'a>(k: &Kernel, s: &'a Surface) -> Result<Option<Cow<'a, BSplineSurface>>> {
|
||||
// a cone, sphere, torus or surface of revolution makes its NURB on the surface and
|
||||
// keeps it (`Surface::kind_with_nurb`, status ignored)
|
||||
if let Some(base) = s.revolution_base_with_nurb(k)? {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,219 @@
|
||||
//! tangency-labs/solidcore#1476: shelling a swept bar failed with 1001. SolidCraft rig
|
||||
//! case shell/566: an 8 x 5 rectangle swept (by the C++ sweep) along a 30 mm line, a
|
||||
//! quarter arc of r 30 and a 30 mm line (14 faces: 12 B-spline or extrusion walls, two
|
||||
//! planar caps), shelled 0.5 inward open at its start cap (z = 0). The operand is the
|
||||
//! rig's working copy right before the shell (`data/issue_1476_swept_bar.smd`).
|
||||
//!
|
||||
//! Two causes, both fixed unconditionally:
|
||||
//! - `OffsetExecutive::replace_implicit_offsets` took only a plain B-spline base;
|
||||
//! upstream's `SM_CAST_PTR(SmBSplineSurface, pBase)` takes every subclass, here a
|
||||
//! straight wall's `SmSurfOfExtrusion`. Both tests fail with 1001 without it.
|
||||
//! - With SolidCraft's opt-in fixes on (the issue's configuration), the bend walls'
|
||||
//! offset curves (125 and 127 cubic segments) need 37 625 and 38 227 samples at
|
||||
//! 1e-5, above the former fixed HANG-44 cap of 16 384; the bound is now
|
||||
//! input-scaled (`nurbs::crv_offset::offset_sample_bound`). The second test fails
|
||||
//! with 1001 without it. With the defaults the shell takes another path and never
|
||||
//! samples that many.
|
||||
//!
|
||||
//! Expected: one solid, V = 40 L - 28 (L - 0.5) = 1299.48668 with L = 60 + 15 pi. The
|
||||
//! port gives 1299.48768 (defaults, 7.7e-7 relative above) and 1299.48231 (SolidCraft's
|
||||
//! fixes, 3.4e-6 below; SolidCraft measured 1299.48275 on an older main); the
|
||||
//! operand's own volume is 1.2e-2 below its closed form. Both are pinned to 1e-9.
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)] // test helpers
|
||||
|
||||
#[path = "support/offset_fixture.rs"]
|
||||
mod offset_fixture;
|
||||
|
||||
use offset_fixture::{planar_faces_at, volume, Notifications};
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::geom::object::ObjectRef;
|
||||
use solidcore::kernel::UpstreamFixes;
|
||||
use solidcore::offset::offset_executive::p1::OffsetExecutive;
|
||||
use solidcore::offset::offset_executive::p2::{OffsetOptions, ShellOptions};
|
||||
use solidcore::query::api_queries::brep_is_manifold_solid;
|
||||
use solidcore::topology::brep::p10::TopologyEnv;
|
||||
use solidcore::topology::brep::p7::read_from_file;
|
||||
use solidcore::topology::database_io::FileType;
|
||||
use solidcore::topology::topology::NotifyRecords;
|
||||
use solidcore::{Brep, EntityRef, Kernel};
|
||||
|
||||
/// `SmBrep::ReadFromFile` of the rig's operand.
|
||||
fn operand(k: &mut Kernel) -> Brep {
|
||||
let bytes = include_bytes!("data/issue_1476_swept_bar.smd").to_vec();
|
||||
let mut b = Brep::new_with_infinite_region().unwrap();
|
||||
let mut records = NotifyRecords::new();
|
||||
let mut attributes = AttributeStore::default();
|
||||
let key = |_: EntityRef| ObjectRef {
|
||||
key: ObjectKey(0),
|
||||
type_code: 0,
|
||||
};
|
||||
let mut env = TopologyEnv {
|
||||
all_attributes: &[],
|
||||
attributes: &mut attributes,
|
||||
key_of: &key,
|
||||
records: &mut records,
|
||||
next_key: 1 << 40,
|
||||
};
|
||||
read_from_file(k, &mut b, bytes, FileType::Ascii, false, &mut env).unwrap();
|
||||
b.refresh_cached_topology();
|
||||
b
|
||||
}
|
||||
|
||||
/// SolidCraft's `core::fixes()`: every opt-in `UpstreamFixes` flag it runs with (the
|
||||
/// issue's configuration).
|
||||
fn solidcraft_fixes() -> UpstreamFixes {
|
||||
let mut f = UpstreamFixes::default();
|
||||
f.fillet_retry_radius_as_first = true;
|
||||
f.fillet_rail_rim_on_face = true;
|
||||
f.fillet_refuse_rails_leaving_faces = true;
|
||||
f.fillet_refuse_face_consuming_blends = true;
|
||||
f.fillet_refuse_non_manifold_result = true;
|
||||
f.fillet_refuse_crossing_rails = true;
|
||||
f.fillet_corner_ball_patch = true;
|
||||
f.fillet_chamfer_cone_on_arc = true;
|
||||
f.fillet_torus_center_on_axis = true;
|
||||
f.offset_excess_faces_after_merge = true;
|
||||
f.offset_clip_right_side_out = true;
|
||||
f.revolution_offset_axis_both_sides = true;
|
||||
f.offset_extended_edges_mated = true;
|
||||
f.remove_slit_loops_where_solids_touch = true;
|
||||
f.keep_operand_uv_trims = true;
|
||||
f.geometric_g2_continuity = true;
|
||||
f.proto_vertex_miss_is_false = true;
|
||||
f.check_loop_vertex_tolerance = true;
|
||||
f.check_loop_projected_seam_ends = true;
|
||||
f.trim_boundary_seam_wrap = true;
|
||||
f.bspline_check_min_radius = true;
|
||||
f.oscillating_skips_failed_guesses = true;
|
||||
f.refine_cc_range_ends = true;
|
||||
f.flat_corners_test_bits = true;
|
||||
f.cylinder_rebuild_mean_radius = true;
|
||||
f.planar_path_uncapped_open_profiles = true;
|
||||
f.revolve_analytic_profile_faces = true;
|
||||
f.revolve_arc_centre_near_axis_is_sphere = true;
|
||||
f.taper_pivot_nearest_corner = true;
|
||||
f.revolve_nested_shells_as_cavities = true;
|
||||
f.curve_sweep_caps_at_sweep_ends = true;
|
||||
f.join_touching_loops = true;
|
||||
f.uv_trim_reversal_on_surface = true;
|
||||
f.offset_resolve_crossing_loops = true;
|
||||
f.offset_walls_touch_refused = true;
|
||||
f.offset_analytic_mate_edges = true;
|
||||
f.precise_properties_split_at_surface_knots = true;
|
||||
f.nurb_cylinder_inside_out_frame = true;
|
||||
f.shell_inner_shell_of_statuses = true;
|
||||
f.offset_collapsed_rounds_mated = true;
|
||||
f.offset_missing_surface_fails = true;
|
||||
f.fillet_nx1_end_rib_global_merge = true;
|
||||
f.uv_trim_corner_end_points_only = true;
|
||||
f.bspline_trim_preserves_uv_spans = true;
|
||||
f.cone_parallel_line_midpoint = true;
|
||||
f.cone_parallel_line_domains = true;
|
||||
f.proto_edge_self_loop_on_vertex_degenerate = true;
|
||||
f.slice_coincident_faces_sheet_side = true;
|
||||
f.boolean_outer_shell_first = true;
|
||||
f.edge_reversal_keeps_failed_edge = true;
|
||||
f.topology_failed_evaluations_propagate = true;
|
||||
f.heal_stage3_failures_propagate = true;
|
||||
f.sweep_frame_on_planar_path_normal = true;
|
||||
f.cover_refuses_folded_continuation = true;
|
||||
f.boolean_vertex_by_best_crossing = true;
|
||||
f.boolean_cleanup_removes_slits = true;
|
||||
f.classify_edge_interval = true;
|
||||
f.ellipse_trim_seam_lines_span_cone_height = true;
|
||||
f.degenerate_proto_edge_in_one_vertex = true;
|
||||
f.new_edge_vertex_tolerance_covers_gap = true;
|
||||
f.torus_signed_gen_curve_interval = true;
|
||||
f.sheet_sweep_joins_profile_wires = true;
|
||||
f.point_solve_keeps_triangle_guess = true;
|
||||
f.fillet_setback_from_start_vertex = true;
|
||||
f.imprint_trims_edge_to_proto_vertex = true;
|
||||
f.imprint_extends_edge_to_proto_vertex = true;
|
||||
f.fillet_point_step_per_point = true;
|
||||
f.analytic_copy_keeps_parameterization = true;
|
||||
f.fillet_point_bigger_step_retry = true;
|
||||
f.fillet_through_extended_rail_end = true;
|
||||
f.precise_properties_body_length_scale = true;
|
||||
f.offset_extension_from_base = true;
|
||||
f.corner_blend_stays_inside_neighbours = true;
|
||||
f.fillet_cleanup_keeps_target_faces = true;
|
||||
f.fillet_nx1_tangent_side_edge = true;
|
||||
f.offset_closed_curve_trim_arc = true;
|
||||
f.offset_stitch_at_body_tolerance = true;
|
||||
f.curve_contacts_at_curve_ends = true;
|
||||
f.shell_empty_cavity_fails = true;
|
||||
f.fillet_refuse_ball_not_fitting = true;
|
||||
f.boolean_lists_virtual_surface_copies = true;
|
||||
f.sphere_small_circle_not_touching = true;
|
||||
f.ssi_tangent_contact_points = true;
|
||||
f.ssi_trace_tangent_curves_once = true;
|
||||
f.found_torus_sphere_keep_tested_nurb = true;
|
||||
f
|
||||
}
|
||||
|
||||
/// The shell of the operand (`SmApiShellBrepFull(b, -0.5, TRUE, TRUE, TRUE, [start
|
||||
/// cap])`), with its run time.
|
||||
fn shell(fixes: UpstreamFixes) -> (Kernel, solidcore::Result<Brep>) {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.clone_from(&fixes);
|
||||
let mut part = operand(&mut k);
|
||||
assert_eq!(part.face_list().len(), 14);
|
||||
let start = planar_faces_at(&k, &part, 2, 0.0);
|
||||
assert_eq!(start.len(), 1);
|
||||
let mut notifications = Notifications::new(&mut part);
|
||||
let clock = std::time::Instant::now();
|
||||
let result = OffsetExecutive::shell_brep(
|
||||
&mut k,
|
||||
part,
|
||||
ShellOptions {
|
||||
offset: OffsetOptions {
|
||||
distance: -0.5,
|
||||
extend_convex_edges: true,
|
||||
merge_results: true,
|
||||
},
|
||||
create_offset_solid: true,
|
||||
merge_sheet_side_walls: true,
|
||||
},
|
||||
&start,
|
||||
&mut notifications.context(),
|
||||
)
|
||||
.result;
|
||||
eprintln!("issue_1476 shell: {:.2?}", clock.elapsed());
|
||||
(k, result)
|
||||
}
|
||||
|
||||
fn check(fixes: UpstreamFixes, want: f64) {
|
||||
let (k, result) = shell(fixes);
|
||||
let mut b = result.unwrap();
|
||||
b.refresh_cached_topology();
|
||||
assert!(brep_is_manifold_solid(&b).unwrap());
|
||||
let v = volume(&k, &mut b).unwrap();
|
||||
eprintln!(
|
||||
"issue_1476 shell: {} faces, volume {v}",
|
||||
b.face_list().len()
|
||||
);
|
||||
let analytic =
|
||||
40.0 * (60.0 + 15.0 * std::f64::consts::PI) - 28.0 * (59.5 + 15.0 * std::f64::consts::PI);
|
||||
assert!((v - analytic).abs() <= 1e-5 * analytic, "{v} vs {analytic}");
|
||||
assert!((v - want).abs() <= 1e-9 * want, "{v} vs {want}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1476_swept_bar_shells() {
|
||||
check(UpstreamFixes::default(), 1299.4876808844865);
|
||||
}
|
||||
|
||||
/// About 6 s at opt-level 3 but over 4 min in a debug build (the two 38 000-sample
|
||||
/// offsets' knot removal): run with `--ignored` (`--profile difftest`). The default
|
||||
/// test above keeps the extrusion fix live; `nurbs::crv_offset`'s unit tests keep the
|
||||
/// bound live.
|
||||
#[test]
|
||||
#[ignore = "slow in debug builds (two 38 000-sample curve offsets)"]
|
||||
fn issue_1476_swept_bar_shells_with_solidcraft_fixes() {
|
||||
check(solidcraft_fixes(), 1299.482314966473);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,9 @@
|
||||
// face tail -> BuildOffsetOfFaceuse's contained-domain branch on a MakeFaceWithCurves
|
||||
// plane face, including MapEdgeuses and self-intersection merging.
|
||||
// map twin -> MapEdgeuses alone, between two MakeFaceWithCurves rectangle faces.
|
||||
// replace -> ReplaceImplicitOffsets on one CreateFaceFromSurface face whose surface is
|
||||
// an owning SmOffsetSurface(d, base) of a real SmPlane, SmBSplineSurface or
|
||||
// SmSurfOfExtrusion (lazy: no NURB until GetOrCreateGwNurbPointer).
|
||||
// Deviations: bounded plain-C fixture protocol, traversal IDs instead of pointers;
|
||||
// extension-only nonnegative-distance validation. Native array ordering is unchanged.
|
||||
// No implementation file is included; no native numerical/helper code is copied.
|
||||
@@ -39,9 +42,11 @@
|
||||
#include <SmNurbsSrf.h>
|
||||
#include <SmOffsetExecutive.h>
|
||||
#include <SmOffsetGeometryCreation.h>
|
||||
#include <SmOffsetSurface.h>
|
||||
#include <SmPlane.h>
|
||||
#include <SmRegion.h>
|
||||
#include <SmShell.h>
|
||||
#include <SmSurfOfExtrusion.h>
|
||||
#include <SmSSIData.h>
|
||||
#include <SmVertex.h>
|
||||
#include <SmVertexuse.h>
|
||||
@@ -1112,10 +1117,71 @@ void map_twin(Reader& rd, std::vector<double>& out) {
|
||||
out.insert(out.end(), {0, (double)status});
|
||||
put_mapped_uses(out, executive, use);
|
||||
}
|
||||
void replace(Reader& rd, std::vector<double>& out) {
|
||||
SmContext ctx;
|
||||
int64_t base_kind = rd.index(1);
|
||||
std::unique_ptr<SmSurface> base;
|
||||
if (base_kind == 0) {
|
||||
base = read_surface(rd, ctx);
|
||||
} else {
|
||||
std::unique_ptr<SmCurve> curve = read_curve(rd, ctx);
|
||||
SmBSplineCurve* generator = SM_CAST_PTR(SmBSplineCurve, curve.get());
|
||||
if (!generator) throw Argument{};
|
||||
SmPoint3d origin = rd.point();
|
||||
SmVector3d direction = rd.point();
|
||||
SmExtent2d domain = rd.domain();
|
||||
bool swap = rd.flag();
|
||||
// The extrusion owns its generator curve.
|
||||
base.reset(new (ctx) SmSurfOfExtrusion(generator, origin, direction, domain,
|
||||
swap ? TRUE : FALSE));
|
||||
curve.release();
|
||||
}
|
||||
double distance = rd.number(), tol = rd.number();
|
||||
int64_t nsamples = rd.index(64);
|
||||
std::vector<SmPoint2d> samples;
|
||||
for (int64_t i=0; i<nsamples; ++i) {
|
||||
SmPoint2d sample = rd.uv();
|
||||
if (sample.x<0 || sample.x>1 || sample.y<0 || sample.y>1) throw Argument{};
|
||||
samples.push_back(sample);
|
||||
}
|
||||
rd.done();
|
||||
auto original = brep(ctx);
|
||||
struct Pending {
|
||||
SmSurface* p;
|
||||
~Pending() { if (p && !p->GetOwner()) delete p; }
|
||||
} pending{new (ctx) SmOffsetSurface(distance, *base, TRUE)};
|
||||
base.release();
|
||||
SmExtent2d natural = pending.p->GetNaturalUVDomain();
|
||||
SmFace* face = nullptr;
|
||||
constructed(original->CreateFaceFromSurface(pending.p, natural, face));
|
||||
if (!face) throw Argument{};
|
||||
SmStatus status = SmOffsetExecutive::ReplaceImplicitOffsets(original.get(), tol);
|
||||
SmTArray<SmFace*> faces;
|
||||
original->GetFaces(faces);
|
||||
out.insert(out.end(), {0, (double)status, (double)faces.GetSize()});
|
||||
for (ULONG i=0; i<faces.GetSize(); ++i) {
|
||||
SmSurface* surface = faces[i]->GetSurface();
|
||||
out.push_back(surface ? (double)surface->GetType() : -1.0);
|
||||
put_domain(out, faces[i]->GetUVDomain());
|
||||
if (!surface) continue;
|
||||
SmExtent2d domain = surface->GetNaturalUVDomain();
|
||||
put_domain(out, domain);
|
||||
out.push_back((double)samples.size());
|
||||
for (const SmPoint2d& f : samples) {
|
||||
SmPoint2d uv((1.0-f.x)*domain.GetMin().x + f.x*domain.GetMax().x,
|
||||
(1.0-f.y)*domain.GetMin().y + f.y*domain.GetMax().y);
|
||||
SmPoint3d point(0,0,0);
|
||||
SmStatus eval = surface->EvaluatePoint(uv, point);
|
||||
out.push_back(eval);
|
||||
if (eval == SM_SUCCESS) put_point(out, point);
|
||||
else out.insert(out.end(), 3, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Plain-C ABI, mode 0 config / 1 synchronize / 2 extend / 3 probe / 4 face tail /
|
||||
// 5 map twin / 6 vertex.
|
||||
// 5 map twin / 6 vertex / 7 replace.
|
||||
// Caller owns every buffer; no native allocation or object address crosses it.
|
||||
SC_EXPORT int32_t sc_offset_offset_executive_p1_eval(
|
||||
int32_t mode, const double* input, int64_t input_len,
|
||||
@@ -1135,6 +1201,7 @@ SC_EXPORT int32_t sc_offset_offset_executive_p1_eval(
|
||||
case 6: vertex(rd, result); break;
|
||||
case 4: face_tail(rd, result); break;
|
||||
case 5: map_twin(rd, result); break;
|
||||
case 7: replace(rd, result); break;
|
||||
default: throw Argument{};
|
||||
}
|
||||
} catch (const Rejected& rejection) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
Updates: HANG-44
|
||||
|
||||
#### HANG-44 addendum (2026-10-29). The port's sample cap is input-scaled (issue #1476)
|
||||
- **Severity:** Hang
|
||||
- **Area:**
|
||||
- **What changed:** the fixed `MAX_OFFSET_SAMPLES = 16 384` refused a legitimate
|
||||
offset: shelling a C++-swept bar (issue #1476, SolidCraft rig case shell/566) offsets
|
||||
bend walls whose curves have 125 and 127 cubic Bezier segments, and at
|
||||
`approximation_tol_3d = 1e-5` `N_CrvOffsetPtSampling` asks for 37 625 and 38 227
|
||||
samples. That is the least upstream samples on those segments: each takes
|
||||
`ceil((1/tol)^0.34 * max(2 deg, sqrt(Muu / 8)))` and here `2 deg` dominates
|
||||
(`ceil(50.12 * 6) = 301` per segment). The count grows with the segment count and
|
||||
the tolerance, not only with a pole.
|
||||
- **New bound** (`nurbs::crv_offset::offset_sample_bound`, unconditional, decision 78):
|
||||
the total count `mu` may be at most
|
||||
|
||||
`min(MAX_OFFSET_SAMPLES_ANY, max(MAX_OFFSET_SAMPLES, OFFSET_SAMPLE_SLACK * segments * ceil((1/tol)^exp * 2 deg)))`
|
||||
|
||||
with `MAX_OFFSET_SAMPLES = 16 384` (the former cap: every input it accepted is
|
||||
accepted with an identical result), `OFFSET_SAMPLE_SLACK = 4` and
|
||||
`MAX_OFFSET_SAMPLES_ANY = 2^17 = 131 072`. `ceil((1/tol)^exp * 2 deg)` is the
|
||||
per-segment count without the curvature term, the floor upstream samples any segment
|
||||
at; four times it on average admits an offset second derivative `Muu` up to
|
||||
`8 (8 deg)^2` per segment (cubic: 4 608), far above what smooth walls reach, while a
|
||||
pole or an offset cusp drives `Muu`, and the count, orders of magnitude past it. The
|
||||
ceiling bounds the quadratic knot removal for any input.
|
||||
- **Timings** (opt-level 3, `--profile difftest`, shared machine):
|
||||
- the swept bar's shell (`crates/solidcore/tests/issue_1476_swept_bar_shell.rs`)
|
||||
with SolidCraft's opt-in fixes, which offsets both 38 000-sample curves: 6.4 s for
|
||||
the whole shell (255 s in a debug build); with the default fixes it takes a path
|
||||
that never samples that many (1.2 s);
|
||||
- one curve of 127 cubic segments at `1e-5` (38 227 samples): 2.1 s to 3.6 s;
|
||||
- one curve of 435 segments (130 935 samples, just under the ceiling): 29.7 s; 436
|
||||
segments (131 236): refused in 16 ms;
|
||||
- fuzz00366 (727 541 samples, bound 16 384 there): refused before any sampling,
|
||||
in 7.7 ms in a debug build (unit test `sample_count_and_degree_are_bounded` asserts under 2 s in a debug
|
||||
build); a gentle cubic at `tol = 1e-12` (about 72 000 samples per segment) is
|
||||
refused by the ceiling the same way (`tiny_tolerance_is_refused_quickly`).
|
||||
- **Still open:** upstream has no bound at all; the C++ was not run on the refused
|
||||
inputs (the shim keeps its own 4 000-sample guard).
|
||||
Reference in New Issue
Block a user