feat: strengthen modeling, configuration, and assembly workflows
Preserve linked sketch identities through deterministic face projection and resizing. Fix origin-corner picking, persistent inline navigation, appearance editing, configuration styles, component properties, contact sweeps, sheet exports, analytic STEP offsets, and bounded rolling-ball fit accuracy. Validated with full cargo xtask ci, native workflow screenshots, repeated enclosure replays, and analytic geometry regressions.
This commit is contained in:
1 parent
22672585c9
commit
d71bdb78ec
54 files changed
+4179
-316
No files matched your search
@@ -1,7 +1,9 @@
|
||||
//! STEP geometry (ISO 10303-42 entities) to kernel curves and surfaces.
|
||||
//!
|
||||
//! Points, placements, lines, conics, B-splines (plain and rational), elementary surfaces and
|
||||
//! swept surfaces. Edge curves are trimmed to their vertices here, so every kernel edge runs
|
||||
//! swept surfaces, with exact offsets of planes, cylinders, cones, spheres and regular tori.
|
||||
//! Free-form offsets and offsets through collapsed radii remain explicit import warnings.
|
||||
//! Edge curves are trimmed to their vertices here, so every kernel edge runs
|
||||
//! exactly from its start vertex to its end vertex. Lengths are scaled to millimetres while
|
||||
//! reading.
|
||||
|
||||
@@ -761,6 +763,59 @@ pub(crate) fn bspline_surface(cx: &Ctx, e: &Entity) -> R<mt::Surface> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve exact analytic offsets. Each intermediate offset is checked: an invalid inner
|
||||
/// offset must not become valid merely because an outer offset cancels its distance.
|
||||
/// Entity attributes follow ISO 10303-42 (`basis_surface`, `distance`, `self_intersect`).
|
||||
fn offset_basis(cx: &Ctx, id: u64, depth: usize) -> R<(u64, f64)> {
|
||||
if depth > MAX_GEOM_DEPTH {
|
||||
return Err("surface definitions nested too deeply".into());
|
||||
}
|
||||
let e = entity(cx, id)?;
|
||||
if e.name() != "OFFSET_SURFACE" {
|
||||
return Ok((id, 0.0));
|
||||
}
|
||||
let p = e.params();
|
||||
let basis = p.get(1).and_then(Param::as_ref_id).ok_or("offset surface without basis")?;
|
||||
let distance = length(cx, p.get(2), "offset distance")?;
|
||||
if !matches!(p.get(3).and_then(Param::as_enum), Some("T" | "F" | "U")) {
|
||||
return Err("offset surface without a logical self_intersect flag".into());
|
||||
}
|
||||
let (basis, inner) = offset_basis(cx, basis, depth + 1)?;
|
||||
let offset = inner + distance;
|
||||
if !offset.is_finite() || offset.abs() >= MAX_COORD {
|
||||
return Err("offset distance out of range after unit conversion".into());
|
||||
}
|
||||
let e = entity(cx, basis)?;
|
||||
let p = e.params();
|
||||
match e.name() {
|
||||
"PLANE" => {}
|
||||
"CYLINDRICAL_SURFACE" | "SPHERICAL_SURFACE" => {
|
||||
let radius = length(cx, p.get(2), "radius")?;
|
||||
if radius <= 0.0 || !(1e-9..MAX_COORD).contains(&(radius + offset)) {
|
||||
return Err("offset surface collapses or reverses its radius".into());
|
||||
}
|
||||
}
|
||||
"CONICAL_SURFACE" => {
|
||||
let radius = length(cx, p.get(2), "radius")?;
|
||||
let angle = num(p.get(3), "semi angle")? * cx.ang;
|
||||
if radius < 0.0 || !(angle.abs() > 1e-9 && angle.abs() < FRAC_PI_2 - 1e-9) || !(0.0..MAX_COORD).contains(&(radius + offset / angle.cos()))
|
||||
{
|
||||
return Err("offset cone has an invalid reference radius or angle".into());
|
||||
}
|
||||
}
|
||||
"TOROIDAL_SURFACE" => {
|
||||
let major = length(cx, p.get(2), "major radius")?;
|
||||
let minor = length(cx, p.get(3), "minor radius")?;
|
||||
if minor <= 0.0 || major <= minor || major >= MAX_COORD || !(1e-9..major).contains(&(minor + offset)) {
|
||||
return Err("offset torus collapses or becomes self-intersecting".into());
|
||||
}
|
||||
}
|
||||
_ if offset == 0.0 => {}
|
||||
n => return Err(format!("unsupported OFFSET_SURFACE basis surface {n}")),
|
||||
}
|
||||
Ok((basis, offset))
|
||||
}
|
||||
|
||||
/// A STEP surface entity as a kernel surface whose normal is the STEP surface normal.
|
||||
/// `extent` holds points of the face boundary (bounds the analytic surfaces' parameter ranges).
|
||||
/// `sense` false gives the opposite normal (a face with `same_sense = .F.`).
|
||||
@@ -776,6 +831,7 @@ pub(crate) fn surface(cx: &Ctx, id: u64, extent: &[mt::Point3], seam: &[mt::Poin
|
||||
if depth > MAX_GEOM_DEPTH {
|
||||
return Err("surface definitions nested too deeply".into());
|
||||
}
|
||||
let (id, offset) = offset_basis(cx, id, depth)?;
|
||||
let e = entity(cx, id)?;
|
||||
if e.is_complex() {
|
||||
return bspline_surface(cx, e).map(flip);
|
||||
@@ -785,11 +841,12 @@ pub(crate) fn surface(cx: &Ctx, id: u64, extent: &[mt::Point3], seam: &[mt::Poin
|
||||
match e.name() {
|
||||
"PLANE" => {
|
||||
let f = placement()?;
|
||||
Ok(flip(mt::Surface::Plane(mt::Plane::new(f.o, f.o + f.x, f.o + f.y))))
|
||||
let o = f.o + f.z * offset;
|
||||
Ok(flip(mt::Surface::Plane(mt::Plane::new(o, o + f.x, o + f.y))))
|
||||
}
|
||||
"CYLINDRICAL_SURFACE" => {
|
||||
let f = seam_frame(&placement()?, extent, seam);
|
||||
let r = length(cx, p.get(2), "radius")?;
|
||||
let r = length(cx, p.get(2), "radius")? + offset;
|
||||
if r <= 0.0 {
|
||||
return Err("cylinder radius must be positive".into());
|
||||
}
|
||||
@@ -799,8 +856,9 @@ pub(crate) fn surface(cx: &Ctx, id: u64, extent: &[mt::Point3], seam: &[mt::Poin
|
||||
}
|
||||
"CONICAL_SURFACE" => {
|
||||
let f = seam_frame(&placement()?, extent, seam);
|
||||
let r = length(cx, p.get(2), "radius")?;
|
||||
let basis_r = length(cx, p.get(2), "radius")?;
|
||||
let a = num(p.get(3), "semi angle")? * cx.ang;
|
||||
let r = basis_r + offset / a.cos();
|
||||
if r < 0.0 || !(a.abs() > 1e-9 && a.abs() < FRAC_PI_2 - 1e-9) {
|
||||
return Err("bad cone".into());
|
||||
}
|
||||
@@ -821,7 +879,7 @@ pub(crate) fn surface(cx: &Ctx, id: u64, extent: &[mt::Point3], seam: &[mt::Poin
|
||||
}
|
||||
"SPHERICAL_SURFACE" => {
|
||||
let f = placement()?;
|
||||
let r = length(cx, p.get(2), "radius")?;
|
||||
let r = length(cx, p.get(2), "radius")? + offset;
|
||||
if r <= 0.0 {
|
||||
return Err("sphere radius must be positive".into());
|
||||
}
|
||||
@@ -844,7 +902,7 @@ pub(crate) fn surface(cx: &Ctx, id: u64, extent: &[mt::Point3], seam: &[mt::Poin
|
||||
}
|
||||
"TOROIDAL_SURFACE" => {
|
||||
let f = placement()?;
|
||||
let (big, small) = (length(cx, p.get(2), "major radius")?, length(cx, p.get(3), "minor radius")?);
|
||||
let (big, small) = (length(cx, p.get(2), "major radius")?, length(cx, p.get(3), "minor radius")? + offset);
|
||||
if small <= 0.0 || big < 0.0 {
|
||||
return Err("bad torus radii".into());
|
||||
}
|
||||
|
||||
@@ -76,6 +76,106 @@ fn imported_bodies_take_features() {
|
||||
const HEAD: &str = "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\nFILE_NAME('t','',(''),(''),'','hand','');\nFILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));\nENDSEC;\nDATA;\n";
|
||||
const TAIL: &str = "ENDSEC;\nEND-ISO-10303-21;\n";
|
||||
|
||||
/// Original fixtures: elementary basis surfaces plus offsets, with no external CAD files.
|
||||
fn offset_geometry(definition: &str, offset: &str, len: f64, sense: bool, points: &[mt::Point3]) -> crate::Result<mt::Surface> {
|
||||
let text = format!(
|
||||
"{HEAD}#1=CARTESIAN_POINT('',(0.,0.,0.));\n#2=AXIS2_PLACEMENT_3D('',#1,$,$);\n#3={definition};\n#4=OFFSET_SURFACE('',#3,{offset},.U.);\n{TAIL}"
|
||||
);
|
||||
let ex = p21::parse(&text).unwrap();
|
||||
let cx = Ctx { ex: &ex, len, ang: 1.0, tol: 1e-6 };
|
||||
crate::guard("test offset surface", || geom::surface(&cx, 4, points, &[], sense, 0).map_err(KernelError::Failed))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_surface_analytic_geometry_and_orientation() {
|
||||
use mt::{InnerSpace, MetricSpace, ParametricSurface, ParametricSurface3D, SearchNearestParameter};
|
||||
let angle: f64 = 0.4;
|
||||
let cases = [
|
||||
("PLANE('',#2)".to_string(), mt::Point3::new(2.0, 3.0, 1.0), mt::Vector3::unit_z()),
|
||||
("CYLINDRICAL_SURFACE('',#2,5.)".to_string(), mt::Point3::new(6.0, 0.0, 2.0), mt::Vector3::unit_x()),
|
||||
("SPHERICAL_SURFACE('',#2,5.)".to_string(), mt::Point3::new(6.0, 0.0, 0.0), mt::Vector3::unit_x()),
|
||||
("TOROIDAL_SURFACE('',#2,10.,2.)".to_string(), mt::Point3::new(13.0, 0.0, 0.0), mt::Vector3::unit_x()),
|
||||
(
|
||||
format!("CONICAL_SURFACE('',#2,5.,{angle})"),
|
||||
mt::Point3::new(5.0 + 1.0 / angle.cos() + 2.0 * angle.tan(), 0.0, 2.0),
|
||||
mt::Vector3::new(angle.cos(), 0.0, -angle.sin()),
|
||||
),
|
||||
];
|
||||
for (definition, point, normal) in cases {
|
||||
for sense in [true, false] {
|
||||
let extent = [point, point + mt::Vector3::unit_z()];
|
||||
let surface = offset_geometry(&definition, "1.", 1.0, sense, &extent).unwrap();
|
||||
let uv = surface.search_nearest_parameter(point, None, 100).unwrap();
|
||||
assert!(surface.subs(uv.0, uv.1).distance(point) < 1e-6, "{definition}");
|
||||
assert!(surface.normal(uv.0, uv.1).dot(normal) * if sense { 1.0 } else { -1.0 } > 0.99, "{definition}, sense {sense}");
|
||||
}
|
||||
}
|
||||
let p = mt::Point3::new(40.0, 0.0, 2.0);
|
||||
let surface = offset_geometry("CYLINDRICAL_SURFACE('',#2,5.)", "-1.", 10.0, true, &[p, p + mt::Vector3::unit_z()]).unwrap();
|
||||
let uv = surface.search_nearest_parameter(p, None, 100).unwrap();
|
||||
assert!(surface.subs(uv.0, uv.1).distance(p) < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_surfaces_nested_and_hostile() {
|
||||
let points = [mt::Point3::new(0.0, 0.0, 0.0), mt::Point3::new(0.0, 0.0, 1.0)];
|
||||
for (basis, offset) in [
|
||||
("CYLINDRICAL_SURFACE('',#2,5.)", "-5."),
|
||||
("SPHERICAL_SURFACE('',#2,5.)", "-6."),
|
||||
("TOROIDAL_SURFACE('',#2,10.,2.)", "8."),
|
||||
("TOROIDAL_SURFACE('',#2,10.,2.)", "-2."),
|
||||
("CONICAL_SURFACE('',#2,5.,0.4)", "-10."),
|
||||
("PLANE('',#2)", "1.E100"),
|
||||
("PLANE('',#2)", "$"),
|
||||
] {
|
||||
assert!(offset_geometry(basis, offset, 1.0, true, &points).is_err(), "{basis}, {offset}");
|
||||
}
|
||||
let text = format!("{HEAD}#1=OFFSET_SURFACE('',#1,1.,.F.);{TAIL}");
|
||||
let ex = p21::parse(&text).unwrap();
|
||||
let cx = Ctx { ex: &ex, len: 1.0, ang: 1.0, tol: 1e-6 };
|
||||
assert!(geom::surface(&cx, 1, &points, &[], true, 0).unwrap_err().contains("nested too deeply"));
|
||||
let text = format!(
|
||||
"{HEAD}#1=CARTESIAN_POINT('',(0.,0.,0.));#2=AXIS2_PLACEMENT_3D('',#1,$,$);#3=CYLINDRICAL_SURFACE('',#2,5.);#4=OFFSET_SURFACE('',#3,-6.,.F.);#5=OFFSET_SURFACE('',#4,6.,.F.);{TAIL}"
|
||||
);
|
||||
let ex = p21::parse(&text).unwrap();
|
||||
let cx = Ctx { ex: &ex, len: 1.0, ang: 1.0, tol: 1e-6 };
|
||||
assert!(geom::surface(&cx, 5, &points, &[], true, 0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_surface_cube_import_is_closed_and_editable() {
|
||||
// Move each basis plane half a file unit inward; nested quarter-unit offsets
|
||||
// bring its supporting surface back to the original unit cube boundary.
|
||||
let mut entities = cube_entities();
|
||||
let origins = [(0.0, 0.0, 0.5), (0.0, 0.0, 0.5), (0.0, 0.5, 0.0), (0.5, 0.0, 0.0), (1.0, 0.5, 0.0), (0.5, 1.0, 0.0)];
|
||||
for (f, (x, y, z)) in origins.into_iter().enumerate() {
|
||||
let base = 400 + f * 20;
|
||||
let extra = 1000 + f * 10;
|
||||
entities = entities
|
||||
.replace(&format!("#{}=PLANE('',#{});", base + 10, base + 9), &format!("#{}=OFFSET_SURFACE('',#{},0.25,.F.);", base + 10, extra + 3));
|
||||
entities += &format!(
|
||||
"#{extra}=CARTESIAN_POINT('',({x},{y},{z}));\n#{}=AXIS2_PLACEMENT_3D('',#{extra},#{},$);\n#{}=PLANE('',#{});\n#{}=OFFSET_SURFACE('',#{},0.25,.F.);\n",
|
||||
extra + 1,
|
||||
base + 8,
|
||||
extra + 2,
|
||||
extra + 1,
|
||||
extra + 3,
|
||||
extra + 2
|
||||
);
|
||||
}
|
||||
entities += &context(10, "(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.CENTI.,.METRE.))");
|
||||
entities += "#9=ADVANCED_BREP_SHAPE_REPRESENTATION('Offsets',(#101),#10);\n";
|
||||
let import = step_import(&format!("{HEAD}{entities}{TAIL}")).unwrap();
|
||||
assert!(import.warnings.is_empty(), "{:?}", import.warnings);
|
||||
assert!(import.bodies[0].closed);
|
||||
assert_eq!(import.bodies[0].file_faces, 6);
|
||||
let body = &import.bodies[0].body;
|
||||
assert!(rel(measure(body).unwrap().volume, 1000.0) < 1e-6);
|
||||
let cut = cylinder(Vec3::new(5.0, 5.0, -1.0), Vec3::Z, 2.0, 12.0).unwrap();
|
||||
let result = boolean(body, &cut, BoolOp::Cut).unwrap().unwrap();
|
||||
assert!(rel(measure(&result).unwrap().volume, 1000.0 - 40.0 * std::f64::consts::PI) < 1e-3);
|
||||
}
|
||||
|
||||
/// A hand-written unit cube brep (`#100`…) in a representation with the given unit and items.
|
||||
fn cube_entities() -> String {
|
||||
let mut s = String::new();
|
||||
|
||||
Reference in new issue
Block a user