mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 19:22:01 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2eb31816c |
+29
@@ -140,6 +140,35 @@ pub fn recorded(
|
||||
Ok(history)
|
||||
}
|
||||
|
||||
/// `op`, a call that makes a new body from `body` (and leaves `body` as it was, such as
|
||||
/// [`crate::offset_surface`]), run with `body`'s faces and edges tagged by their record indices
|
||||
/// (`order`), and the new body with its [`History`] relative to `body`, in the new body's record
|
||||
/// order. The tags and the offset roles are removed from both bodies before this returns,
|
||||
/// whether `op` succeeds or not.
|
||||
///
|
||||
/// # Errors
|
||||
/// `op`'s error; the kernel's, from the tags or the new body's record.
|
||||
pub fn recorded_into(
|
||||
body: &Body,
|
||||
order: &Order,
|
||||
made: Made<'_>,
|
||||
op: impl FnOnce() -> Result<Body>,
|
||||
) -> Result<(Body, History)> {
|
||||
tag(body, order)?;
|
||||
let done = op().and_then(|new| {
|
||||
let (_, out) = new.to_record()?;
|
||||
let history = read(&new.lock(), &out, made);
|
||||
let cleared = clear(&mut new.lock());
|
||||
let history = history?;
|
||||
cleared?;
|
||||
Ok((new, history))
|
||||
});
|
||||
let cleared = clear(&mut body.lock());
|
||||
let result = done?;
|
||||
cleared?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Every face and edge of `order` tagged with its index, after every ops tag the body carried
|
||||
/// is gone.
|
||||
fn tag(body: &Body, order: &Order) -> Result<()> {
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ pub use measure::{
|
||||
point_distance, Distance, EdgePoint, FaceCentroid, GapSample, MassPropertiesEstimate, Normal,
|
||||
PointClass,
|
||||
};
|
||||
pub use offset::{draft, offset_faces, offset_sheet, shell, thicken};
|
||||
pub use offset::{draft, offset_faces, offset_sheet, offset_surface, shell, thicken};
|
||||
pub use order::Order;
|
||||
pub use step_assembly::{placed_bodies, PlacedBody};
|
||||
|
||||
|
||||
@@ -419,5 +419,8 @@ pub fn draft(
|
||||
})?
|
||||
}
|
||||
|
||||
mod surface;
|
||||
pub use surface::offset_surface;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
//! Offset surfaces: faces of a body copied out at a signed distance along their normals, as a
|
||||
//! new body. Each face goes onto its surface's offset on its own, the kernel's offset of the face
|
||||
//! as a sheet ([`super::offset_sheet`]); the offsets are then joined and stitched where they meet
|
||||
//! within the caller's tolerance, so neighbours moved to the same place along their shared edge
|
||||
//! (meeting smoothly) keep sharing it, and across a sharp edge they come apart. Faces that close
|
||||
//! up make a solid. An offset the kernel cannot make (one that would fold a face over) is its
|
||||
//! refusal, never a fitted surface. Ops sequences: every face, offset and stitch is the
|
||||
//! kernel's.
|
||||
|
||||
use solidcore::geom::tol::USE_DEFAULT;
|
||||
use solidcore::topology::brep::p2::{copy_source_ref, CopyNotifyState};
|
||||
use solidcore::topology::brep::p3::{brep_copy_mapped, new_brep};
|
||||
use solidcore::topology::brep::p7::copy_faces_from;
|
||||
use solidcore::topology::loopuse::notify_ref;
|
||||
use solidcore::topology::{Brep, FaceId};
|
||||
use solidcore::EntityRef;
|
||||
|
||||
use super::{offset_sheet, MAX_DISTANCE};
|
||||
use crate::body::Body;
|
||||
use crate::error::status;
|
||||
use crate::order::{self, Order};
|
||||
use crate::session::run_on;
|
||||
use crate::split::copies_of;
|
||||
use crate::stitch::stitch;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Record faces `faces` of `body` (through `order`, each once) offset by `distance` along their
|
||||
/// outward normals (negative: against them) into one new body, joined where their offsets'
|
||||
/// edges lie within `tolerance` of each other; with whether the result closes into a solid
|
||||
/// ([`stitch`]'s). A distance of zero copies the faces. `body` is unchanged; the faces and edges
|
||||
/// of the result carry what their sources carry (an application's provenance,
|
||||
/// [`crate::history::recorded_into`]).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for no face, an index past the faces or repeated, a distance not finite
|
||||
/// or past [`MAX_DISTANCE`], a tolerance not finite and positive; the kernel's refusals (an
|
||||
/// offset that would fold a face, [`Error::Empty`] for one that leaves nothing); those of
|
||||
/// [`stitch`].
|
||||
pub fn offset_surface(
|
||||
body: &Body,
|
||||
order: &Order,
|
||||
faces: &[usize],
|
||||
distance: f64,
|
||||
tolerance: f64,
|
||||
) -> Result<(Body, bool)> {
|
||||
if faces.is_empty()
|
||||
|| !(distance.is_finite() && distance.abs() <= MAX_DISTANCE)
|
||||
|| !(tolerance.is_finite() && tolerance > 0.0)
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut picked: Vec<FaceId> = Vec::new();
|
||||
for &i in faces {
|
||||
let f = order::face(order, i)?;
|
||||
if picked.contains(&f) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
picked.push(f);
|
||||
}
|
||||
let mut sheets = Vec::new();
|
||||
for f in picked {
|
||||
let sheet = face_sheet(body, f)?;
|
||||
if distance != 0.0 {
|
||||
offset_sheet(&sheet, distance)?;
|
||||
}
|
||||
sheets.push(sheet);
|
||||
}
|
||||
stitch(joined(&sheets)?, tolerance)
|
||||
}
|
||||
|
||||
/// Face `f` of `body` copied out as a sheet of its own, with what it and its edges carry.
|
||||
fn face_sheet(body: &Body, f: FaceId) -> Result<Body> {
|
||||
run_on("offset_surface", &[("brep", body)], |k| {
|
||||
let (mut whole, src) = {
|
||||
let g = body.lock();
|
||||
if !g.face_list().contains(&f) {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
let (whole, map) = brep_copy_mapped(k, &g)?;
|
||||
let face = |e| match e {
|
||||
EntityRef::Face(x) => Some(x),
|
||||
_ => None,
|
||||
};
|
||||
let Some([src]) = copies_of(&map, &[f], EntityRef::Face, face)
|
||||
.and_then(|v| <[FaceId; 1]>::try_from(v).ok())
|
||||
else {
|
||||
return Ok(Err(Error::Argument));
|
||||
};
|
||||
(whole, src)
|
||||
};
|
||||
let mut w = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
w.set_tolerance(whole.tolerance());
|
||||
match copied_into(k, &mut whole, &[src], &mut w) {
|
||||
Ok(1) => {}
|
||||
Ok(_) => return Ok(Err(Error::Empty)),
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
}
|
||||
Ok(Ok(Body::new(w)))
|
||||
})?
|
||||
}
|
||||
|
||||
/// The faces of `sheets` copied into one body, each sheet's topology kept.
|
||||
fn joined(sheets: &[Body]) -> Result<Body> {
|
||||
let inputs: Vec<(&'static str, &Body)> = sheets.iter().map(|s| ("sheet", s)).collect();
|
||||
run_on("offset_surface", &inputs, |k| {
|
||||
let mut dest = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
for s in sheets {
|
||||
let mut g = s.lock();
|
||||
let faces = g.face_list().to_vec();
|
||||
if let Err(e) = copied_into(k, &mut g, &faces, &mut dest) {
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
}
|
||||
Ok(Ok(Body::new(dest)))
|
||||
})?
|
||||
}
|
||||
|
||||
/// `faces` of `src` copied into `dest` through the merge, with both bodies' attributes in the
|
||||
/// copy's notifications so each copy carries what its source carries; how many faces it made.
|
||||
fn copied_into(
|
||||
k: &mut solidcore::Kernel,
|
||||
src: &mut Brep,
|
||||
faces: &[FaceId],
|
||||
dest: &mut Brep,
|
||||
) -> solidcore::Result<usize> {
|
||||
let mut state = CopyNotifyState::with_attributes_of(src, dest)?;
|
||||
let this_face = |x: FaceId| notify_ref(EntityRef::Face(x));
|
||||
let copied = copy_faces_from(
|
||||
k,
|
||||
src,
|
||||
faces,
|
||||
dest,
|
||||
true,
|
||||
©_source_ref,
|
||||
&this_face,
|
||||
&mut state.env(None),
|
||||
);
|
||||
state.restore_attributes(src, dest)?;
|
||||
let (made, _) = copied?;
|
||||
dest.refresh_cached_topology();
|
||||
Ok(made.len())
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//! Offset surfaces (tangency-labs/solidcore#3138): faces copied out at a distance, each on its
|
||||
//! surface's exact offset, joined where they meet; the cases of SolidCraft's
|
||||
//! `surface_offset.rs`, on closed forms measured on the exact B-rep.
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::disallowed_methods // the closed forms' oracle, not ops geometry
|
||||
)]
|
||||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use solidcore_ops::history::{recorded_into, Change, FaceOrigin, Made};
|
||||
use solidcore_ops::{make_box, make_cylinder, make_sphere, offset_surface, session, Body, Order};
|
||||
|
||||
fn all_fixes() {
|
||||
session::set_thread_fixes(solidcore::kernel::UpstreamFixes::NAMES.iter().copied()).unwrap();
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs().max(1e-9)
|
||||
}
|
||||
|
||||
fn face_at(b: &Body, o: &Order, p: [f64; 3]) -> usize {
|
||||
b.face_at(o, p, 1e-6).unwrap().unwrap()
|
||||
}
|
||||
|
||||
fn edges(b: &Body) -> usize {
|
||||
b.to_record().unwrap().1.edges.len()
|
||||
}
|
||||
|
||||
/// Planes move exactly: a 10 cube's top 5 up is a 10 × 10 square at z = 15, the offset of the
|
||||
/// top. Two faces across a sharp edge come apart (8 edges, none shared); a zero distance copies.
|
||||
#[test]
|
||||
fn planes_move_and_sharp_edges_come_apart() {
|
||||
all_fixes();
|
||||
let b = make_box([0.0; 3], [10.0; 3]).unwrap();
|
||||
let (_, o) = b.to_record().unwrap();
|
||||
let top = face_at(&b, &o, [5.0, 5.0, 10.0]);
|
||||
let (up, h) = recorded_into(&b, &o, Made::Offset, || {
|
||||
offset_surface(&b, &o, &[top], 5.0, 0.01).map(|(r, _)| r)
|
||||
})
|
||||
.unwrap();
|
||||
assert!(rel(up.area(1e-8).unwrap(), 100.0) < 1e-9);
|
||||
let (lo, hi) = up.bounds().unwrap();
|
||||
assert!(
|
||||
(lo[2] - 15.0).abs() < 1e-9 && (hi[2] - 15.0).abs() < 1e-9,
|
||||
"{lo:?} {hi:?}"
|
||||
);
|
||||
assert_eq!(h.faces.len(), 1);
|
||||
assert_eq!(h.faces[0].change, Change::Generated);
|
||||
assert_eq!(h.faces[0].origins, vec![FaceOrigin::OffsetOf(top)]);
|
||||
let side = face_at(&b, &o, [5.0, 0.0, 5.0]);
|
||||
let (two, closed) = offset_surface(&b, &o, &[top, side], 1.0, 0.01).unwrap();
|
||||
assert!(!closed);
|
||||
assert_eq!(
|
||||
(two.to_record().unwrap().1.faces.len(), edges(&two)),
|
||||
(2, 8)
|
||||
);
|
||||
let (copy, _) = offset_surface(&b, &o, &[top, side], 0.0, 0.01).unwrap();
|
||||
assert!(rel(copy.area(1e-8).unwrap(), 200.0) < 1e-9);
|
||||
// the body itself is unchanged
|
||||
assert!(rel(b.volume(1e-8).unwrap(), 1000.0) < 1e-9);
|
||||
}
|
||||
|
||||
/// A cylinder's side (r 10, h 20, one face closed round its seam) moved out by 2 keeps its seam
|
||||
/// joined, with radius 12: area 2π·12·20. Moved in past its axis: refused.
|
||||
#[test]
|
||||
fn a_cylinder_side_changes_radius() {
|
||||
all_fixes();
|
||||
let c = make_cylinder([0.0; 3], 10.0, 20.0).unwrap();
|
||||
let (_, o) = c.to_record().unwrap();
|
||||
let d = 10.0 / 2f64.sqrt();
|
||||
let side = face_at(&c, &o, [d, d, 10.0]);
|
||||
assert_eq!(
|
||||
face_at(&c, &o, [-d, -d, 10.0]),
|
||||
side,
|
||||
"the side is one face"
|
||||
);
|
||||
let (out, _) = offset_surface(&c, &o, &[side], 2.0, 0.01).unwrap();
|
||||
assert!(rel(out.area(1e-8).unwrap(), 2.0 * PI * 12.0 * 20.0) < 1e-7);
|
||||
let (joined, _) = offset_surface(&c, &o, &[side], 0.0, 0.01).unwrap();
|
||||
assert_eq!(
|
||||
edges(&out),
|
||||
edges(&joined),
|
||||
"the side still closes on its seam"
|
||||
);
|
||||
assert!(offset_surface(&c, &o, &[side], -10.5, 0.01).is_err());
|
||||
}
|
||||
|
||||
/// A ball (r 10) moved out by 2: its faces meet smoothly, so their offsets share every edge and
|
||||
/// close into the ball of radius 12 (area 4π·144, volume 4π·1728/3); moved in to its centre:
|
||||
/// refused.
|
||||
#[test]
|
||||
fn a_ball_closes_and_is_refused_at_its_centre() {
|
||||
all_fixes();
|
||||
let s = make_sphere([0.0; 3], 10.0).unwrap();
|
||||
let (_, o) = s.to_record().unwrap();
|
||||
let all: Vec<usize> = (0..o.faces.len()).collect();
|
||||
let (out, closed) = offset_surface(&s, &o, &all, 2.0, 0.01).unwrap();
|
||||
assert!(closed, "the offset ball closes into a solid");
|
||||
assert_eq!(edges(&out), edges(&s));
|
||||
assert!(rel(out.area(1e-8).unwrap(), 4.0 * PI * 144.0) < 1e-7);
|
||||
assert!(rel(out.volume(1e-8).unwrap(), 4.0 / 3.0 * PI * 1728.0) < 1e-7);
|
||||
assert!(offset_surface(&s, &o, &all, -10.0, 0.01).is_err());
|
||||
}
|
||||
|
||||
/// Arguments: no face, a face twice, an index past the faces, a tolerance of 0.
|
||||
#[test]
|
||||
fn bad_arguments_are_refused() {
|
||||
all_fixes();
|
||||
let b = make_box([0.0; 3], [10.0; 3]).unwrap();
|
||||
let (_, o) = b.to_record().unwrap();
|
||||
assert!(offset_surface(&b, &o, &[], 1.0, 0.01).is_err());
|
||||
assert!(offset_surface(&b, &o, &[0, 0], 1.0, 0.01).is_err());
|
||||
assert!(offset_surface(&b, &o, &[6], 1.0, 0.01).is_err());
|
||||
assert!(offset_surface(&b, &o, &[0], 1.0, 0.0).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user