mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 19:22:01 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 785ab93f16 | |||
| b992941d5d | |||
| 23c8f45798 |
+267
-6
@@ -42,7 +42,10 @@
|
||||
//! - **Surface swap.** `DeleteEdge` swaps the two faces' surfaces through
|
||||
//! `SmFace::SetSurface` (ported in `topology.face_p1`, which moves an owned
|
||||
//! `Surface`): the swap takes the surface out of the deleted face and hands the
|
||||
//! replaced one back.
|
||||
//! replaced one back. When `flip_face` then flips the surviving face's orientation
|
||||
//! (`SmBrep.cpp:6196`), `same_orientation_faces` is toggled too (`WR-759`) so loops
|
||||
//! moved from `del_f` onto `surv_f` (`SmBrep.cpp:6526`) land on the matching faceuse
|
||||
//! instead of the inverted one.
|
||||
//! - **`SmLockBrepBox`** is RAII upstream; `StitchFaces` holds `topology.brep_p1`'s
|
||||
//! [`LockBrepBox`] guard (unlocked on drop, on every path). Its lock
|
||||
//! (`SmBrep::LockBoundingBox`, unit `query.brep`) is not ported yet.
|
||||
@@ -885,7 +888,7 @@ fn delete_manifold_edge(
|
||||
let split_loop = lu == lu_rad;
|
||||
|
||||
// mark when face and radial face have same orientation
|
||||
let same_orientation_faces =
|
||||
let mut same_orientation_faces =
|
||||
eu1::faceuse_orientation(b, faceuse)? == eu1::faceuse_orientation(b, fu_rad)?;
|
||||
|
||||
// Determine if either end of edge is a strut end
|
||||
@@ -985,10 +988,19 @@ fn delete_manifold_edge(
|
||||
// swap the surfaces between the faces
|
||||
swap_face_surfaces(k, b, env.key_of, sf, df)?;
|
||||
if flip_face {
|
||||
ignore_status(crate::error::passed_on(
|
||||
face1::flip_face_orientation(b, sf),
|
||||
k.fixes.brep_edit_statuses_pass_on,
|
||||
)?)?;
|
||||
// WR-759: `SmFace::FlipFaceOrientation` (`SmFace.cpp:10171-10198`)
|
||||
// returns `Err` (`NER`) only before flipping `m_pFU` and the
|
||||
// faceuses' orientations (`:10191-10193`), and `Ok(())`
|
||||
// (`SM_SUCCESS`) whenever the flip happened; an ignored pass-on
|
||||
// `Err` therefore did not flip `sf` and must leave
|
||||
// `same_orientation_faces` unchanged.
|
||||
match face1::flip_face_orientation(b, sf) {
|
||||
Ok(()) => same_orientation_faces = !same_orientation_faces,
|
||||
Err(e) => ignore_status(crate::error::passed_on::<()>(
|
||||
Err(e),
|
||||
k.fixes.brep_edit_statuses_pass_on,
|
||||
)?)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
// else: WARN (asked to keep a surface not used by the faces)
|
||||
@@ -2364,4 +2376,253 @@ mod tests {
|
||||
assert!(matches!(r, Err(SolidError::InvalidInput { .. })));
|
||||
assert_eq!(b.face(f).unwrap().rectangular_trim, face2::UNSURE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wr759_delete_edge_flipping_surviving_face_preserves_moved_inner_loop_orientation() {
|
||||
use crate::curve::curve::Curve;
|
||||
use crate::curve::ellipse::Ellipse;
|
||||
use crate::curve::line::Line;
|
||||
use crate::geom::axis2_placement::Axis2Placement;
|
||||
use crate::geom::tol::USE_DEFAULT;
|
||||
use crate::mass::integrator::properties::{brep_volume, PropertyAttributes};
|
||||
use crate::math::{Point3, Vec3};
|
||||
use crate::surface::cone::Cone;
|
||||
use crate::surface::plane::Plane;
|
||||
use crate::surface::surface::Surface;
|
||||
use crate::topology::brep::validation::{check_brep, CheckLevel};
|
||||
use crate::topology::brep_constructor::{BrepConstructor, VertexNode};
|
||||
use crate::topology::trimming_tools::create_uv_trim_curves;
|
||||
|
||||
let mut k = Kernel::default();
|
||||
let frame_top_up = Axis2Placement::new(Point3::ZERO, Vec3::X, Vec3::Y); // z = 0, +Z
|
||||
let frame_top_dn = Axis2Placement::new(Point3::ZERO, Vec3::X, -Vec3::Y); // z = 0, -Z
|
||||
let frame_bot_up = Axis2Placement::new(Point3::new(0.0, 0.0, -5.0), Vec3::X, Vec3::Y); // z = -5, +Z
|
||||
let frame_bot_dn = Axis2Placement::new(Point3::new(0.0, 0.0, -5.0), Vec3::X, -Vec3::Y); // z = -5, -Z
|
||||
let plane_dom = Extent2d::from_bounds(-15.0, -15.0, 15.0, 15.0);
|
||||
|
||||
let c_top_10 = Curve::from(
|
||||
Ellipse::create_canonical_circle(&k, &frame_top_up, 10.0, None, None).unwrap(),
|
||||
);
|
||||
let c_top_5 = Curve::from(
|
||||
Ellipse::create_canonical_circle(&k, &frame_top_up, 5.0, None, None).unwrap(),
|
||||
);
|
||||
let c_top_2 = Curve::from(
|
||||
Ellipse::create_canonical_circle(&k, &frame_top_up, 2.0, None, None).unwrap(),
|
||||
);
|
||||
let c_bot_10 = Curve::from(
|
||||
Ellipse::create_canonical_circle(&k, &frame_bot_up, 10.0, None, None).unwrap(),
|
||||
);
|
||||
let c_bot_2 = Curve::from(
|
||||
Ellipse::create_canonical_circle(&k, &frame_bot_up, 2.0, None, None).unwrap(),
|
||||
);
|
||||
let seam_10 = Curve::from(Line::from_points(
|
||||
Point3::new(10.0, 0.0, -5.0),
|
||||
Point3::new(10.0, 0.0, 0.0),
|
||||
3,
|
||||
));
|
||||
let seam_2 = Curve::from(Line::from_points(
|
||||
Point3::new(2.0, 0.0, -5.0),
|
||||
Point3::new(2.0, 0.0, 0.0),
|
||||
3,
|
||||
));
|
||||
let mut cyl_10 =
|
||||
Surface::from(Cone::create_canonical_cylinder(&k, &frame_bot_up, 10.0).unwrap());
|
||||
cyl_10
|
||||
.adjust_step_uv_domain(&k, &Extent2d::from_bounds(0.0, -1.0, 360.0, 6.0))
|
||||
.unwrap();
|
||||
let cyl_10_dom = cyl_10.natural_uv_domain_with_nurb(&k).unwrap();
|
||||
let mut cyl_2 =
|
||||
Surface::from(Cone::create_canonical_cylinder(&k, &frame_bot_up, 2.0).unwrap());
|
||||
cyl_2
|
||||
.adjust_step_uv_domain(&k, &Extent2d::from_bounds(0.0, -1.0, 360.0, 6.0))
|
||||
.unwrap();
|
||||
let cyl_2_dom = cyl_2.natural_uv_domain_with_nurb(&k).unwrap();
|
||||
|
||||
let mut c = BrepConstructor::new();
|
||||
c.start_brep(&mut k, USE_DEFAULT).unwrap();
|
||||
c.start_region(true, None).unwrap();
|
||||
c.start_shell().unwrap();
|
||||
|
||||
let v_t10 = c.start_vertex_of_loop(Point3::new(10.0, 0.0, 0.0)).unwrap();
|
||||
let v_t5 = c.start_vertex_of_loop(Point3::new(5.0, 0.0, 0.0)).unwrap();
|
||||
let v_t2 = c.start_vertex_of_loop(Point3::new(2.0, 0.0, 0.0)).unwrap();
|
||||
let v_b10 = c
|
||||
.start_vertex_of_loop(Point3::new(10.0, 0.0, -5.0))
|
||||
.unwrap();
|
||||
let v_b2 = c.start_vertex_of_loop(Point3::new(2.0, 0.0, -5.0)).unwrap();
|
||||
|
||||
// Face 1 (`surv_f`): z = 0 annulus r in [5, 10] on +Z plane, outward normal +Z (Same).
|
||||
let surv_f = c.start_face(OrientType::Same).unwrap();
|
||||
c.set_face_surface(
|
||||
Surface::from(Plane::create_canonical(&frame_top_up).unwrap()),
|
||||
&plane_dom,
|
||||
)
|
||||
.unwrap();
|
||||
c.start_loop(OrientType::Same).unwrap();
|
||||
let e_t10 = c
|
||||
.start_edge(&mut k, OrientType::Same, None, Some(v_t10), Some(v_t10))
|
||||
.unwrap();
|
||||
c.set_edge_curve(&k, Some(c_top_10), false).unwrap();
|
||||
let n_t10 = VertexNode::from_point(Point3::new(10.0, 0.0, 0.0));
|
||||
c.set_limits(&k, &n_t10, &n_t10).unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.end_loop(&k).unwrap();
|
||||
|
||||
c.start_loop(OrientType::Same).unwrap();
|
||||
let e_mid = c
|
||||
.start_edge(&mut k, OrientType::Opposite, None, Some(v_t5), Some(v_t5))
|
||||
.unwrap();
|
||||
c.set_edge_curve(&k, Some(c_top_5), false).unwrap();
|
||||
let n_t5 = VertexNode::from_point(Point3::new(5.0, 0.0, 0.0));
|
||||
c.set_limits(&k, &n_t5, &n_t5).unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.end_loop(&k).unwrap();
|
||||
c.end_face().unwrap();
|
||||
|
||||
// Face 2 (`del_f`): z = 0 annulus r in [2, 5] on -Z plane, outward normal +Z (Opposite).
|
||||
let del_f = c.start_face(OrientType::Opposite).unwrap();
|
||||
c.set_face_surface(
|
||||
Surface::from(Plane::create_canonical(&frame_top_dn).unwrap()),
|
||||
&plane_dom,
|
||||
)
|
||||
.unwrap();
|
||||
c.start_loop(OrientType::Opposite).unwrap();
|
||||
c.start_edge(&mut k, OrientType::Same, Some(e_mid), None, None)
|
||||
.unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.end_loop(&k).unwrap();
|
||||
|
||||
c.start_loop(OrientType::Opposite).unwrap();
|
||||
let e_t2 = c
|
||||
.start_edge(&mut k, OrientType::Opposite, None, Some(v_t2), Some(v_t2))
|
||||
.unwrap();
|
||||
c.set_edge_curve(&k, Some(c_top_2), false).unwrap();
|
||||
let n_t2 = VertexNode::from_point(Point3::new(2.0, 0.0, 0.0));
|
||||
c.set_limits(&k, &n_t2, &n_t2).unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.end_loop(&k).unwrap();
|
||||
c.end_face().unwrap();
|
||||
|
||||
// Face 3: z = -5 bottom annulus r in [2, 10] on -Z plane, outward normal -Z (Same).
|
||||
c.start_face(OrientType::Same).unwrap();
|
||||
c.set_face_surface(
|
||||
Surface::from(Plane::create_canonical(&frame_bot_dn).unwrap()),
|
||||
&plane_dom,
|
||||
)
|
||||
.unwrap();
|
||||
c.start_loop(OrientType::Same).unwrap();
|
||||
let e_b10 = c
|
||||
.start_edge(&mut k, OrientType::Opposite, None, Some(v_b10), Some(v_b10))
|
||||
.unwrap();
|
||||
c.set_edge_curve(&k, Some(c_bot_10), false).unwrap();
|
||||
let n_b10 = VertexNode::from_point(Point3::new(10.0, 0.0, -5.0));
|
||||
c.set_limits(&k, &n_b10, &n_b10).unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.end_loop(&k).unwrap();
|
||||
|
||||
c.start_loop(OrientType::Same).unwrap();
|
||||
let e_b2 = c
|
||||
.start_edge(&mut k, OrientType::Same, None, Some(v_b2), Some(v_b2))
|
||||
.unwrap();
|
||||
c.set_edge_curve(&k, Some(c_bot_2), false).unwrap();
|
||||
let n_b2 = VertexNode::from_point(Point3::new(2.0, 0.0, -5.0));
|
||||
c.set_limits(&k, &n_b2, &n_b2).unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.end_loop(&k).unwrap();
|
||||
c.end_face().unwrap();
|
||||
|
||||
// Face 4: outer cylinder r = 10, z in [-5, 0], outward normal +R (Same).
|
||||
c.start_face(OrientType::Same).unwrap();
|
||||
c.set_face_surface(cyl_10, &cyl_10_dom).unwrap();
|
||||
c.start_loop(OrientType::Same).unwrap();
|
||||
c.start_edge(&mut k, OrientType::Same, Some(e_b10), None, None)
|
||||
.unwrap();
|
||||
c.end_edge().unwrap();
|
||||
let e_s10 = c
|
||||
.start_edge(&mut k, OrientType::Same, None, Some(v_b10), Some(v_t10))
|
||||
.unwrap();
|
||||
c.set_edge_curve(&k, Some(seam_10), false).unwrap();
|
||||
c.set_limits(&k, &n_b10, &n_t10).unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.start_edge(&mut k, OrientType::Opposite, Some(e_t10), None, None)
|
||||
.unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.start_edge(&mut k, OrientType::Opposite, Some(e_s10), None, None)
|
||||
.unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.end_loop(&k).unwrap();
|
||||
c.end_face().unwrap();
|
||||
|
||||
// Face 5: inner bore cylinder r = 2, z in [-5, 0], outward normal -R (Opposite).
|
||||
c.start_face(OrientType::Opposite).unwrap();
|
||||
c.set_face_surface(cyl_2, &cyl_2_dom).unwrap();
|
||||
c.start_loop(OrientType::Opposite).unwrap();
|
||||
c.start_edge(&mut k, OrientType::Opposite, Some(e_b2), None, None)
|
||||
.unwrap();
|
||||
c.end_edge().unwrap();
|
||||
let e_s2 = c
|
||||
.start_edge(&mut k, OrientType::Same, None, Some(v_b2), Some(v_t2))
|
||||
.unwrap();
|
||||
c.set_edge_curve(&k, Some(seam_2), false).unwrap();
|
||||
c.set_limits(&k, &n_b2, &n_t2).unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.start_edge(&mut k, OrientType::Same, Some(e_t2), None, None)
|
||||
.unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.start_edge(&mut k, OrientType::Opposite, Some(e_s2), None, None)
|
||||
.unwrap();
|
||||
c.end_edge().unwrap();
|
||||
c.end_loop(&k).unwrap();
|
||||
c.end_face().unwrap();
|
||||
|
||||
c.end_shell(&mut k, false).unwrap();
|
||||
c.end_region().unwrap();
|
||||
let mut b = c.end_brep().unwrap();
|
||||
for f in brep1::faces(&b, None).unwrap() {
|
||||
create_uv_trim_curves(&k, &mut b, f).unwrap();
|
||||
}
|
||||
let pre = check_brep(&mut k, &mut b, CheckLevel::Level0).unwrap();
|
||||
assert!(
|
||||
pre.pointer_reports.is_empty(),
|
||||
"pre-merge pointer reports: {:?}",
|
||||
pre.pointer_reports
|
||||
);
|
||||
|
||||
b.set_editing_enabled(true);
|
||||
let serial = b.notify_serial().unwrap_or(1);
|
||||
let key_of = |entity| super::super::p3::topology_ref(serial, entity);
|
||||
let mut records = crate::topology::topology::NotifyRecords::new();
|
||||
super::super::p3::seed_records(&mut records, &b, &key_of);
|
||||
let mut attributes = AttributeStore::default();
|
||||
let mut env = DeleteEnv {
|
||||
records: &mut records,
|
||||
attributes: &mut attributes,
|
||||
key_of: &key_of,
|
||||
context: None,
|
||||
};
|
||||
|
||||
delete_edge(&mut k, &mut b, &mut env, e_mid, Some(del_f)).unwrap();
|
||||
b.refresh_cached_topology();
|
||||
assert_eq!(brep1::faces(&b, None).unwrap().len(), 4);
|
||||
create_uv_trim_curves(&k, &mut b, surv_f).unwrap();
|
||||
|
||||
let chk = check_brep(&mut k, &mut b, CheckLevel::Level0).unwrap();
|
||||
assert!(
|
||||
chk.pointer_reports.is_empty(),
|
||||
"expected clean topology after merging opposite-normal faces, got: {:?}",
|
||||
chk.pointer_reports
|
||||
);
|
||||
|
||||
let store = AttributeStore::default();
|
||||
let attrs = PropertyAttributes {
|
||||
store: &store,
|
||||
key_of: &|_| crate::geom::attribute::ObjectKey(0),
|
||||
};
|
||||
let vol = brep_volume(&k, &mut b, 1e-7, &attrs).unwrap();
|
||||
let expected_vol = core::f64::consts::PI * (100.0 - 4.0) * 5.0; // 480 * pi
|
||||
assert!(
|
||||
(vol - expected_vol).abs() < 5e-2,
|
||||
"merged hollow cylinder volume {vol} != {expected_vol}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"area": "booleans",
|
||||
"bodies": {
|
||||
"b1": {
|
||||
"keeping_splines": false,
|
||||
"trusted": true
|
||||
},
|
||||
"b2": {
|
||||
"keeping_splines": false,
|
||||
"trusted": true
|
||||
}
|
||||
},
|
||||
"calls": [
|
||||
{
|
||||
"inputs": [
|
||||
"b1"
|
||||
],
|
||||
"op": "tagged",
|
||||
"outputs": [
|
||||
"b3"
|
||||
],
|
||||
"params": {
|
||||
"operand": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b2"
|
||||
],
|
||||
"op": "tagged",
|
||||
"outputs": [
|
||||
"b4"
|
||||
],
|
||||
"params": {
|
||||
"operand": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b3",
|
||||
"b4"
|
||||
],
|
||||
"op": "boolean",
|
||||
"outputs": [
|
||||
"b5"
|
||||
],
|
||||
"params": {
|
||||
"op": "subtract"
|
||||
}
|
||||
}
|
||||
],
|
||||
"case": "2927-flanged-spool-cut-at-bore-step",
|
||||
"expected": {
|
||||
"body_count": 1,
|
||||
"method": "Subtracting z >= 130 removes the upper 10 mm [130, 140] of the scalloped top flange right at the horizontal bore step plane z = 130 (r = 31 -> 35). From V_full = 248052.0320278483 mm^3, the removed [130, 140] slab has 9 mm of constant scalloped section A(0) = 5509.766677996032 mm^2 on [130, 139], 1 mm of upper chamfer [139, 140] of exact Simpson integral (A(0) + 4 A(0.5) + A(1)) / 6 = 5444.496350618749 mm^2, less the r = 35 bore of height 10 (pi * 1225 * 10 = 38484.510006474966 mm^3) and 6 countersunk holes (each pi * 25 * 9 + pi/3 * (25 + 30 + 36) = 802.1533245966821 mm^3), removing 30555.35494957667 mm^3 and leaving 217496.67707827163 mm^3.",
|
||||
"outcome": "solid",
|
||||
"source": "analytic",
|
||||
"sources": [
|
||||
"tangency-labs/solidcore#2927"
|
||||
],
|
||||
"volume_mm3": 217496.67707827163,
|
||||
"what": "half-space box z >= 130 subtracted from the 92-face flanged spool right on the inner bore step plane z = 130 (r = 31 to r = 35), where merging the tool's +Z cap face with the spool's -Z step face across r = 35 previously inverted the r = 31 bore loop (WR-759) and returned volume 609976 mm^3"
|
||||
},
|
||||
"expressible": true,
|
||||
"gallery_note": "Horizontal cut on the z = 130 bore step plane: merging the opposite-normal annular step face with the tool cap preserves the r = 31 inner bore loop.",
|
||||
"operation_expressible": true,
|
||||
"records": "2927-flanged-spool-cut-at-bore-step.bodies.json.gz",
|
||||
"result": [
|
||||
{
|
||||
"body": "spool",
|
||||
"made": "b5"
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
"tangency-labs/solidcore#2927"
|
||||
],
|
||||
"ungraded": [],
|
||||
"what": "half-space box z >= 130 subtracted from the 92-face flanged spool right on the inner bore step plane z = 130 (r = 31 to r = 35), where merging the tool's +Z cap face with the spool's -Z step face across r = 35 previously inverted the r = 31 bore loop (WR-759) and returned volume 609976 mm^3",
|
||||
"why_not": []
|
||||
}
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"area": "booleans",
|
||||
"bodies": {
|
||||
"b1": {
|
||||
"keeping_splines": false,
|
||||
"trusted": true
|
||||
},
|
||||
"b2": {
|
||||
"keeping_splines": false,
|
||||
"trusted": true
|
||||
}
|
||||
},
|
||||
"calls": [
|
||||
{
|
||||
"inputs": [
|
||||
"b1"
|
||||
],
|
||||
"op": "tagged",
|
||||
"outputs": [
|
||||
"b3"
|
||||
],
|
||||
"params": {
|
||||
"operand": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b2"
|
||||
],
|
||||
"op": "tagged",
|
||||
"outputs": [
|
||||
"b4"
|
||||
],
|
||||
"params": {
|
||||
"operand": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b3",
|
||||
"b4"
|
||||
],
|
||||
"op": "boolean",
|
||||
"outputs": [
|
||||
"b5"
|
||||
],
|
||||
"params": {
|
||||
"op": "subtract"
|
||||
}
|
||||
}
|
||||
],
|
||||
"case": "2927-flanged-spool-cut-through-axis",
|
||||
"expected": {
|
||||
"body_count": 1,
|
||||
"method": "The 92-face flanged spool is symmetric across y = 0, so subtracting the half-space box y >= 0 leaves half of the exact spool volume V_full / 2. On z in [0, 128] the outer revolved profile consists of four 45-deg frusta r = 64 <-> 65 of height 1 (4 * pi/3 * (64^2 + 64*65 + 65^2)), two flange cylinders r = 65 of height 3 (2 * pi * 65^2 * 3), one barrel cylinder r = 35 of height 83 (pi * 35^2 * 83), three concave quarter-tori (R0 = 45, rho = 10, each pi * (45^2 * 10 - pi/2 * 45 * 10^2 + 2000/3)) and one neck concave quarter-torus (R0 = 40, rho = 5, pi * (40^2 * 5 - pi/2 * 40 * 25 + 250/3)), less the bore r = 31 of height 128 and 24 bolt holes r = 4 of height 5. On z in [128, 140] the 6-lobe scalloped flange (6 convex arcs R1 = 10 - delta at R = 45 and 6 tangent concave arcs R2 = 40 + delta, exact quadratic in delta integrated over [128, 129], [129, 139], [139, 140]) less the stepped bore (r = 31 on [128, 130], r = 35 on [130, 140]) and 6 countersunk holes (r = 5 on [128, 139], frustum 5 -> 6 on [139, 140]) gives V_full = 248052.0320278483 mm^3, hence V_full / 2 = 124026.01601392415 mm^3.",
|
||||
"outcome": "solid",
|
||||
"source": "analytic",
|
||||
"sources": [
|
||||
"tangency-labs/solidcore#2927"
|
||||
],
|
||||
"volume_mm3": 124026.01601392415,
|
||||
"what": "half-space box y >= 0 subtracted from the 92-face flanged spool (planes, cylinders, cones, four concave fillet tori and 30 flange holes) along its axis: previously returned an empty 8-face shell of volume 0"
|
||||
},
|
||||
"expressible": true,
|
||||
"gallery_note": "Axial y = 0 cut across all four concave fillet tori, chamfered bolt-circle flanges, stepped inner bore and scalloped top flange.",
|
||||
"operation_expressible": true,
|
||||
"records": "2927-flanged-spool-cut-through-axis.bodies.json.gz",
|
||||
"result": [
|
||||
{
|
||||
"body": "spool",
|
||||
"made": "b5"
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
"tangency-labs/solidcore#2927"
|
||||
],
|
||||
"ungraded": [],
|
||||
"what": "half-space box y >= 0 subtracted from the 92-face flanged spool (planes, cylinders, cones, four concave fillet tori and 30 flange holes) along its axis: previously returned an empty 8-face shell of volume 0",
|
||||
"why_not": []
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
},
|
||||
"booleans": {
|
||||
"cases": 125,
|
||||
"cases": 127,
|
||||
"chain": {
|
||||
"1": 9,
|
||||
"121": 1,
|
||||
@@ -29,7 +29,7 @@
|
||||
"17": 1,
|
||||
"193": 1,
|
||||
"28": 1,
|
||||
"3": 2,
|
||||
"3": 4,
|
||||
"31": 1,
|
||||
"4": 3,
|
||||
"5": 1,
|
||||
@@ -40,9 +40,9 @@
|
||||
"8": 6,
|
||||
"9": 2
|
||||
},
|
||||
"expressible": 110,
|
||||
"expressible": 112,
|
||||
"not_expressible": 12,
|
||||
"operation_expressible": 113,
|
||||
"operation_expressible": 115,
|
||||
"reasons": {
|
||||
"a final body built or changed by SolidCraft (its own geometry or a change to the kernel's record)": 10,
|
||||
"refused or failed in SolidCraft, not in a kernel operation": 2
|
||||
|
||||
+2576
-16
File diff suppressed because it is too large
Load Diff
+4
-11
@@ -586,19 +586,12 @@ fn face_input(kernel: &Kernel, body: &Body, face: &sm::Face) -> Result<FaceInput
|
||||
curves: Vec::new(),
|
||||
orients: Vec::new(),
|
||||
};
|
||||
for bound in &face.loops {
|
||||
let Bound::Edges(edges) = &bound.bound else {
|
||||
for bound in constructor::normalized_face_loops(kernel, body, face)
|
||||
.map_err(|message| BuildError { message })?
|
||||
{
|
||||
let constructor::NormalizedLoop::Edges(oriented) = bound else {
|
||||
continue;
|
||||
};
|
||||
let oriented: Vec<(usize, bool)> = if bound.orientation {
|
||||
edges.iter().map(|edge| (edge.edge, edge.forward)).collect()
|
||||
} else {
|
||||
edges
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|edge| (edge.edge, !edge.forward))
|
||||
.collect()
|
||||
};
|
||||
out.loops.push(oriented.len());
|
||||
for (index, forward) in oriented {
|
||||
let edge = body.edges.get(index).ok_or_else(|| BuildError {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+32
@@ -0,0 +1,32 @@
|
||||
#### WR-759. `SmBrep::DeleteEdge` leaves `bSameOrientationFaces` stale after flipping the surviving face
|
||||
|
||||
- **Severity:** Wrong result
|
||||
- **Area:** Topology, attributes, solvers
|
||||
- **Location:** `source/SMLib/src/SmBrep.cpp:5872-5873, 6194-6197, 6524-6530`, `SmBrep::DeleteEdge` at v1.0.1 (`3fd5b9548d19aa8c12c29bc9637a96d42a06f8a5`).
|
||||
- **What happens:** `bSameOrientationFaces` is computed before face-surface swapping (`:5872-5873`). When merging an outer loop with an inner loop (`:5901-5906`), the face owning the inner loop is forced to be `pSurvF` regardless of `pOptSurfaceToKeep`. If `pDelF` owns `pOptSurfaceToKeep` and the two faces have opposite surface normals (`bFlipFace == TRUE`), `SmBrep::DeleteEdge` swaps their surfaces and calls `pSurvF->FlipFaceOrientation()` (`:6194-6197`), inverting `pSurvF`'s primary faceuse orientation so it now matches `pDelF`'s. `bSameOrientationFaces` is never updated after that flip, so when `pDelF`'s remaining loops are moved onto `pSurvF` (`:6524-6530`), `if (!bSameOrientationFaces) pMoveLoop->m_pLU = pMoveLoop->m_pLU->GetOtherLoopuse();` flips each moved loop onto the wrong side of `pSurvF`.
|
||||
- **Trigger:** Merging two coplanar faces with opposite surface normals across a shared closed loop that is inner on one face and outer on the other, passing `pDelF`'s surface as `pOptSurfaceToKeep`, when `pDelF` also carries an inner hole loop. Reached in Issue [#2927](https://github.com/tangency-labs/solidcore/issues/2927) (`flanged_spool.step` cut at `z = 130`, where the tool box's `+Z` cap face merges across `r = 35` with the spool's `-Z` step face `r in [31, 35]`, inverting the `r = 31` bore loop on the merged cap face, failing `SmBrep` Pointer Test 98 `Edgeuses share same region`, and inflating the integrated volume from `217496.967` to `609976.137`).
|
||||
- **How found:** Issue [#2927](https://github.com/tangency-labs/solidcore/issues/2927), traced in `topology::brep::p4::delete_edge` during [PR #2969](https://github.com/tangency-labs/solidcore/pull/2969) and verified directly in `source/SMLib/src/SmBrep.cpp:5872-6531`.
|
||||
- **Confirmed:** yes, by reading the pinned C++ source (`source/SMLib/src/SmBrep.cpp` at `3fd5b9548d19`):
|
||||
|
||||
```cpp
|
||||
// source/SMLib/src/SmBrep.cpp:5872-5873
|
||||
SmBoolean bSameOrientationFaces = ( pLU-> GetFaceuse()->GetOrientation()
|
||||
== pLURad->GetFaceuse()->GetOrientation() ) ;
|
||||
// ...
|
||||
// source/SMLib/src/SmBrep.cpp:6194-6197
|
||||
if(bFlipFace )
|
||||
{
|
||||
pSurvF->FlipFaceOrientation();
|
||||
}
|
||||
// ...
|
||||
// source/SMLib/src/SmBrep.cpp:6524-6530
|
||||
SmLoop *pMoveLoop = sLoops[ii];
|
||||
SER(pDelF->RemoveLoop(pMoveLoop));
|
||||
if (!bSameOrientationFaces)
|
||||
{
|
||||
pMoveLoop->m_pLU = pMoveLoop->m_pLU->GetOtherLoopuse();
|
||||
}
|
||||
SER(pSurvF->InsertLoop(pMoveLoop));
|
||||
```
|
||||
|
||||
Reproduced on the direct port in `wr759_delete_edge_flipping_surviving_face_preserves_moved_inner_loop_orientation` (`crates/solidcore/src/topology/brep/p4.rs`, where a concentric hollow cylinder `r in [2, 10], h = 5` returns `520 * pi = 1633.628` and Pointer Test 98 without the toggle vs exact `480 * pi = 1507.964` with it) and on `cut z > 130` (`crates/step-brep/tests/issue_2927_spool_boolean.rs`). With the toggle applied when `flip_face_orientation` succeeds (`SmFace.cpp:10171-10198` returns `NER` only prior to flipping `m_pFU`), the merged `z = 130` cap face has exact area `3912.008850 mm^2` (`3.6e-8 mm^2` from closed form) and common-origin volume additivity within `1.3e-5 mm^3`; the `+0.290 mm^3` difference in `brep_volume` (`217496.967` vs `217496.677`) is not a kernel defect — `brep_volume` integrates `iint (z - z_0) n_z dA` (`OdeIsoCurveLength`), so shifting `z_0` to `bbox.mid().z = 65 mm` shifts the `+0.588 mm^3` origin integral by `-65 mm * 0.004596 mm^2 = -0.299 mm^3` due to the input STEP file's pre-existing `6.663e-3 mm` B-spline chamfer-edge tolerance on `z in [128, 129]`, shared identically by non-coincident cuts `z = 129.5, 130.5, 135.0`.
|
||||
Reference in New Issue
Block a user