M2: shell and draft (exact convex polyhedra from planes)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
431b87c2b9
commit
73a1ab40d0
9 files changed
+327
No files matched your search
@@ -224,6 +224,21 @@ pub fn to_script(recipe: &Value) -> Result<Value, String> {
|
||||
let axis = f.get("axis").map(|a| json!({"origin": a.get("origin").cloned().unwrap_or(json!([0, 0, 0])), "dir": a.get("dir")})).unwrap_or(Value::Null);
|
||||
out.push(json!({"command": "PatternCircular", "params": {"features": f.get("features"), "axis": axis, "count": f.get("count"), "angle": f.get("total_angle").cloned().unwrap_or(json!(360)), "name": name}}));
|
||||
}
|
||||
Some("shell") => {
|
||||
let faces: Vec<Value> = f.get("faces_removed").and_then(Value::as_array).into_iter().flatten().filter_map(|x| x.get("point").cloned()).collect();
|
||||
out.push(json!({"command": "FusionShellBodyCommand", "params": {"faces": faces, "thickness": f.get("inside_thickness"), "name": name}}));
|
||||
}
|
||||
Some("draft") => {
|
||||
let faces: Vec<Value> = f.get("faces").and_then(Value::as_array).into_iter().flatten().filter_map(|x| x.get("point").cloned()).collect();
|
||||
let np = f.get("neutral_plane").cloned().unwrap_or(Value::Null);
|
||||
let n = np.get("plane_normal_outward").or(np.get("normal_at_point_on_face")).cloned().unwrap_or(json!([0, 0, 1]));
|
||||
let neutral = json!({"origin": np.get("point").or(np.get("centroid")).cloned().unwrap_or(json!([0, 0, 0])), "normal": n.clone()});
|
||||
let mut angle = f.get("angle").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
if f.get("flipped").and_then(Value::as_bool).unwrap_or(false) {
|
||||
angle = -angle;
|
||||
}
|
||||
out.push(json!({"command": "FusionDraftCommand", "params": {"faces": faces, "angle": angle, "neutral": neutral, "pull": n, "name": name}}));
|
||||
}
|
||||
Some("hole") => {
|
||||
let n = f.get("face").and_then(|fc| fc.get("plane_normal_outward").or(fc.get("normal_at_point_on_face"))).cloned();
|
||||
let dir = n.and_then(|n| n.as_array().map(|a| a.iter().map(|x| -x.as_f64().unwrap_or(0.0)).collect::<Vec<f64>>()));
|
||||
|
||||
@@ -228,6 +228,22 @@ pub enum FeatureKind {
|
||||
#[serde(default)]
|
||||
hole: HoleKind,
|
||||
},
|
||||
/// Hollow a body leaving walls of `thickness`, removing the faces at the given points.
|
||||
Shell {
|
||||
faces: Vec<Vec3>,
|
||||
thickness: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
},
|
||||
/// Tilt faces (at the given points) about a neutral plane, leaning toward `pull`.
|
||||
Draft {
|
||||
faces: Vec<Vec3>,
|
||||
angle: String,
|
||||
neutral: PlaneRef,
|
||||
pull: Vec3,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
},
|
||||
/// A construction plane (sketch placement, split tool).
|
||||
ConstructionPlane {
|
||||
plane: PlaneRef,
|
||||
@@ -331,6 +347,8 @@ impl FeatureKind {
|
||||
FeatureKind::Mirror { .. } => "MirrorFeature",
|
||||
FeatureKind::ConstructionPlane { .. } => "ConstructionPlane",
|
||||
FeatureKind::Hole { .. } => "HoleFeature",
|
||||
FeatureKind::Shell { .. } => "ShellFeature",
|
||||
FeatureKind::Draft { .. } => "DraftFeature",
|
||||
FeatureKind::Split { .. } => "SplitBodyFeature",
|
||||
FeatureKind::Move { .. } => "MoveFeature",
|
||||
}
|
||||
@@ -353,6 +371,8 @@ impl FeatureKind {
|
||||
FeatureKind::Mirror { .. } => "Mirror",
|
||||
FeatureKind::ConstructionPlane { .. } => "Plane",
|
||||
FeatureKind::Hole { .. } => "Hole",
|
||||
FeatureKind::Shell { .. } => "Shell",
|
||||
FeatureKind::Draft { .. } => "Draft",
|
||||
FeatureKind::Split { .. } => "Split",
|
||||
FeatureKind::Move { .. } => "Move",
|
||||
}
|
||||
@@ -393,6 +413,11 @@ impl FeatureKind {
|
||||
PatternKind::Circular { count, angle, .. } => v.extend([count.as_str(), angle]),
|
||||
},
|
||||
FeatureKind::Mirror { plane, .. } => plane_exprs(plane, &mut v),
|
||||
FeatureKind::Shell { thickness, .. } => v.push(thickness),
|
||||
FeatureKind::Draft { angle, neutral, .. } => {
|
||||
v.push(angle);
|
||||
plane_exprs(neutral, &mut v);
|
||||
}
|
||||
FeatureKind::Hole { diameter, depth, hole, .. } => {
|
||||
v.push(diameter);
|
||||
v.extend(depth.iter().map(String::as_str));
|
||||
|
||||
@@ -472,6 +472,26 @@ fn extend_for_coplanar(st: &ModelState, plane: &Plane, r: &Region2, lo: f64, hi:
|
||||
(lo2, hi2)
|
||||
}
|
||||
|
||||
/// The body a face-based feature applies to: by name, or the one whose surface is nearest the
|
||||
/// first point.
|
||||
fn body_at(st: &ModelState, body: &Option<String>, points: &[Vec3]) -> Result<usize> {
|
||||
if let Some(n) = body {
|
||||
return st.bodies.iter().position(|b| &b.name == n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")));
|
||||
}
|
||||
let p = points.first().ok_or_else(|| DocError::Invalid("no faces selected".into()))?;
|
||||
st.bodies
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, b)| {
|
||||
let bb = b.mesh().bounds();
|
||||
let q = Vec3::new(p.x.clamp(bb.min.x, bb.max.x), p.y.clamp(bb.min.y, bb.max.y), p.z.clamp(bb.min.z, bb.max.z));
|
||||
(i, q.dist(*p))
|
||||
})
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map(|(i, _)| i)
|
||||
.ok_or_else(|| DocError::Invalid("there is no body".into()))
|
||||
}
|
||||
|
||||
/// The body a fillet/chamfer applies to: by name, or the one closest to the first edge point.
|
||||
fn blend_target(state: &ModelState, body: &Option<String>, edges: &[Vec3]) -> Result<usize> {
|
||||
if let Some(n) = body {
|
||||
@@ -830,6 +850,27 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
let tools = feature_tools(vals, f, st)?;
|
||||
apply_op(st, f, tools, Operation::Cut, &[])
|
||||
}
|
||||
FeatureKind::Shell { faces, thickness, body } => {
|
||||
let t = val(vals, thickness, Kind::Length)?;
|
||||
let i = body_at(st, body, faces)?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let nb = kernel::shell(&mb.body, faces, t)?;
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::new(mb.name, nb, mb.feature);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Draft { faces, angle, neutral, pull, body } => {
|
||||
let a = val(vals, angle, Kind::Angle)?;
|
||||
let pl = doc.resolve_plane(vals, neutral, 0)?;
|
||||
let i = body_at(st, body, faces)?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let nb = kernel::draft(&mb.body, faces, &pl, *pull, a)?;
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::new(mb.name, nb, mb.feature);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::ConstructionPlane { plane } => {
|
||||
doc.resolve_plane(vals, plane, 0)?;
|
||||
Ok(())
|
||||
|
||||
@@ -43,6 +43,14 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("hole")
|
||||
.key("H")
|
||||
.params("position: [x,y,z] on a face; direction?: [x,y,z] (default: into the face); diameter; depth? (default through all); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?"),
|
||||
CommandSpec::new("FusionShellBodyCommand", "Shell", shell)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("shell")
|
||||
.params("faces: [[x,y,z] points on the faces to remove]; thickness: expr (inside); body?"),
|
||||
CommandSpec::new("FusionDraftCommand", "Draft", draft)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("draft")
|
||||
.params("faces: [[x,y,z]]; angle: expr; neutral: XY|XZ|YZ|plane name|{origin, normal}; pull?: [x,y,z] (default: neutral plane normal); body?"),
|
||||
CommandSpec::new("ConstructionPlaneOffsetFromPlaneCommand", "Offset Plane", plane_offset)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("plane")
|
||||
@@ -535,3 +543,31 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
add_feature(s, p, FeatureKind::Hole { position, direction, diameter, depth, hole: kind })
|
||||
}
|
||||
|
||||
fn face_points(p: &Value, cmd: &str) -> Result<Vec<Vec3>> {
|
||||
let a = p.get("faces").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`faces` must be a list of points"))?;
|
||||
if a.is_empty() || a.len() > 1000 {
|
||||
return Err(bad(cmd, "select 1…1000 faces"));
|
||||
}
|
||||
a.iter().map(|v| vec3(v).or_else(|| v.get("point").and_then(vec3)).ok_or_else(|| bad(cmd, "a face is a point [x, y, z] on it"))).collect()
|
||||
}
|
||||
|
||||
fn shell(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "FusionShellBodyCommand";
|
||||
let faces = face_points(p, cmd)?;
|
||||
let thickness = req_expr(cmd, p, "thickness")?;
|
||||
check_expr(s, &thickness, Kind::Length, cmd, "thickness")?;
|
||||
add_feature(s, p, FeatureKind::Shell { faces, thickness, body: str_(p, "body").map(str::to_string) })
|
||||
}
|
||||
|
||||
fn draft(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "FusionDraftCommand";
|
||||
let faces = face_points(p, cmd)?;
|
||||
let angle = req_expr(cmd, p, "angle")?;
|
||||
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
|
||||
let neutral = plane_param(s, p.get("neutral"), cmd)?;
|
||||
let (vals, _) = s.doc.param_values();
|
||||
let pl = s.doc.resolve_plane(&vals, &neutral, 0)?;
|
||||
let pull = p.get("pull").and_then(vec3).unwrap_or_else(|| pl.normal());
|
||||
add_feature(s, p, FeatureKind::Draft { faces, angle, neutral, pull, body: str_(p, "body").map(str::to_string) })
|
||||
}
|
||||
@@ -17,6 +17,7 @@ mod body;
|
||||
mod build;
|
||||
mod measure;
|
||||
mod ops;
|
||||
mod polyhedron;
|
||||
mod step;
|
||||
mod topo;
|
||||
|
||||
@@ -25,6 +26,7 @@ pub use body::{Body, EdgeInfo, FaceInfo};
|
||||
pub use build::{box_solid, cylinder, extrude, extrude_tapered, revolve, sphere, torus};
|
||||
pub use measure::{BodyMeasure, measure};
|
||||
pub use ops::{BoolOp, boolean, split_by_plane, transform, transform_matrix};
|
||||
pub use polyhedron::{HalfSpace, convex_polyhedron, draft, shell};
|
||||
pub use step::step_export;
|
||||
pub use topo::{TopoCounts, merged_topology, seam_flags};
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
//! Convex polyhedra from planes, and the operations that use them on convex planar bodies:
|
||||
//! shell (hollow out with open faces) and draft (tilt faces about a neutral plane). The solid
|
||||
//! is built directly from its planes (vertices are triple-plane intersections), so it is exact
|
||||
//! and needs no booleans except the final shell subtraction.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use solvecraft_geom::{Plane, Vec3};
|
||||
use truck_modeling::{self as mt, builder};
|
||||
|
||||
use crate::body::{Body, Solid, from_p3, p3};
|
||||
use crate::{KernelError, Result, guard};
|
||||
|
||||
/// A half-space `n · x <= d` (n is the outward unit normal).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct HalfSpace {
|
||||
pub n: Vec3,
|
||||
pub d: f64,
|
||||
}
|
||||
|
||||
fn det3(a: Vec3, b: Vec3, c: Vec3) -> f64 {
|
||||
a.dot(b.cross(c))
|
||||
}
|
||||
|
||||
/// The convex polyhedron bounded by the half-spaces.
|
||||
pub fn convex_polyhedron(hs: &[HalfSpace]) -> Result<Body> {
|
||||
if hs.len() < 4 || hs.len() > 200 {
|
||||
return Err(KernelError::Invalid("a polyhedron needs 4…200 planes".into()));
|
||||
}
|
||||
let scale = hs.iter().map(|h| h.d.abs()).fold(1.0, f64::max);
|
||||
let eps = scale * 1e-9;
|
||||
// Vertices: intersections of three planes inside all others.
|
||||
let mut verts: Vec<Vec3> = Vec::new();
|
||||
for i in 0..hs.len() {
|
||||
for j in (i + 1)..hs.len() {
|
||||
for k in (j + 1)..hs.len() {
|
||||
let (a, b, c) = (hs[i], hs[j], hs[k]);
|
||||
let den = det3(a.n, b.n, c.n);
|
||||
if den.abs() < 1e-12 {
|
||||
continue;
|
||||
}
|
||||
let x = (b.n.cross(c.n) * a.d + c.n.cross(a.n) * b.d + a.n.cross(b.n) * c.d) / den;
|
||||
if !x.is_finite() || hs.iter().any(|h| h.n.dot(x) > h.d + eps * 10.0) {
|
||||
continue;
|
||||
}
|
||||
if !verts.iter().any(|v| v.dist(x) < scale * 1e-8) {
|
||||
verts.push(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if verts.len() < 4 {
|
||||
return Err(KernelError::Invalid("the planes don't enclose a solid".into()));
|
||||
}
|
||||
// Faces: the vertices on each plane, ordered counter-clockwise about its outward normal.
|
||||
let mut faces_idx: Vec<Vec<usize>> = Vec::new();
|
||||
for h in hs {
|
||||
let on: Vec<usize> = (0..verts.len()).filter(|i| verts.get(*i).is_some_and(|v| (h.n.dot(*v) - h.d).abs() < scale * 1e-7)).collect();
|
||||
if on.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let c = on.iter().filter_map(|i| verts.get(*i)).fold(Vec3::ZERO, |a, v| a + *v) / on.len() as f64;
|
||||
let u = h.n.any_perp();
|
||||
let w = h.n.cross(u);
|
||||
let mut ordered = on.clone();
|
||||
ordered.sort_by(|a, b| {
|
||||
let ang = |i: usize| verts.get(i).map(|v| (*v - c).dot(w).atan2((*v - c).dot(u))).unwrap_or(0.0);
|
||||
ang(*a).total_cmp(&ang(*b))
|
||||
});
|
||||
faces_idx.push(ordered);
|
||||
}
|
||||
guard("polyhedron", || {
|
||||
let tv: Vec<mt::Vertex> = verts.iter().map(|v| builder::vertex(p3(*v))).collect();
|
||||
let mut edges: HashMap<(usize, usize), mt::Edge> = HashMap::new();
|
||||
let mut faces: Vec<mt::Face> = Vec::new();
|
||||
for f in &faces_idx {
|
||||
let n = f.len();
|
||||
let mut wire: Vec<mt::Edge> = Vec::with_capacity(n);
|
||||
for k in 0..n {
|
||||
let (Some(&a), Some(&b)) = (f.get(k), f.get((k + 1) % n)) else { continue };
|
||||
let key = (a.min(b), a.max(b));
|
||||
let e = match edges.get(&key) {
|
||||
Some(e) => e.clone(),
|
||||
None => {
|
||||
let (Some(va), Some(vb)) = (tv.get(key.0), tv.get(key.1)) else { continue };
|
||||
let e = builder::line(va, vb);
|
||||
edges.insert(key, e.clone());
|
||||
e
|
||||
}
|
||||
};
|
||||
wire.push(if a < b { e } else { e.inverse() });
|
||||
}
|
||||
let face = builder::try_attach_plane(&[wire.into()]).map_err(|e| KernelError::Failed(format!("polyhedron face: {e}")))?;
|
||||
faces.push(face);
|
||||
}
|
||||
let shell: mt::Shell = faces.into();
|
||||
let solid = Solid::try_new(vec![shell]).map_err(|e| KernelError::Failed(format!("polyhedron: {e}")))?;
|
||||
Body::new(solid)
|
||||
})
|
||||
}
|
||||
|
||||
/// The half-spaces of a convex body with only planar faces, or an error.
|
||||
pub fn planar_convex(b: &Body) -> Result<Vec<(HalfSpace, Vec3)>> {
|
||||
let tol = (b.size() * 1e-3).max(1e-3);
|
||||
let faces = b.faces(tol)?;
|
||||
let mut out: Vec<(HalfSpace, Vec3)> = Vec::new();
|
||||
for f in &faces {
|
||||
let n = f.plane_normal.ok_or_else(|| KernelError::Failed("not supported yet: bodies with curved faces".into()))?;
|
||||
let d = n.dot(f.centroid);
|
||||
if !out.iter().any(|(h, _)| h.n.dot(n) > 1.0 - 1e-9 && (h.d - d).abs() < tol) {
|
||||
out.push((HalfSpace { n, d }, f.centroid));
|
||||
}
|
||||
}
|
||||
let verts: Vec<Vec3> = b.solid.vertex_iter().map(|v| from_p3(v.point())).collect();
|
||||
for (h, _) in &out {
|
||||
if verts.iter().any(|v| h.n.dot(*v) > h.d + tol) {
|
||||
return Err(KernelError::Failed("not supported yet: shell and draft of non-convex bodies".into()));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Hollow a convex planar body: walls of `thickness` inside, faces near `open` removed.
|
||||
pub fn shell(b: &Body, open: &[Vec3], thickness: f64) -> Result<Body> {
|
||||
if !(thickness.is_finite() && thickness > 1e-6) {
|
||||
return Err(KernelError::Invalid("shell thickness must be positive".into()));
|
||||
}
|
||||
let hs = planar_convex(b)?;
|
||||
let size = b.size();
|
||||
let inner: Vec<HalfSpace> = hs
|
||||
.iter()
|
||||
.map(|(h, c)| {
|
||||
let removed = open.iter().any(|p| (h.n.dot(*p) - h.d).abs() < size * 1e-4 + 1e-6 && p.dist(*c) < size * 2.0);
|
||||
if removed { HalfSpace { n: h.n, d: h.d + thickness + size * 0.05 } } else { HalfSpace { n: h.n, d: h.d - thickness } }
|
||||
})
|
||||
.collect();
|
||||
if !hs.iter().zip(&inner).any(|((h, _), i)| i.d > h.d) {
|
||||
return Err(KernelError::Invalid("select at least one face to remove".into()));
|
||||
}
|
||||
let cavity = convex_polyhedron(&inner)?;
|
||||
crate::ops::boolean(b, &cavity, crate::BoolOp::Cut)?.ok_or_else(|| KernelError::Failed("the shell removed everything".into()))
|
||||
}
|
||||
|
||||
/// Tilt the faces near `faces` by `angle` about their line on the neutral plane, so that they
|
||||
/// lean in (positive angle) going along `pull`.
|
||||
pub fn draft(b: &Body, faces: &[Vec3], neutral: &Plane, pull: Vec3, angle: f64) -> Result<Body> {
|
||||
if !angle.is_finite() || angle.abs() >= std::f64::consts::FRAC_PI_2 - 1e-3 {
|
||||
return Err(KernelError::Invalid("draft angle must be between −90° and 90°".into()));
|
||||
}
|
||||
let pull = pull.normalized().ok_or_else(|| KernelError::Invalid("pull direction".into()))?;
|
||||
let hs = planar_convex(b)?;
|
||||
let size = b.size();
|
||||
let mut out = Vec::new();
|
||||
let mut moved = 0;
|
||||
for (h, c) in &hs {
|
||||
let chosen = faces.iter().any(|p| (h.n.dot(*p) - h.d).abs() < size * 1e-4 + 1e-6 && p.dist(*c) < size * 2.0);
|
||||
if !chosen {
|
||||
out.push(*h);
|
||||
continue;
|
||||
}
|
||||
if h.n.dot(pull).abs() > 1e-6 {
|
||||
return Err(KernelError::Failed("not supported yet: drafting faces that aren't parallel to the pull direction".into()));
|
||||
}
|
||||
// Hinge: the face's line on the neutral plane; rotate the normal toward the pull.
|
||||
let n2 = (h.n * angle.cos() + pull * angle.sin()).normalized().ok_or_else(|| KernelError::Failed("draft".into()))?;
|
||||
let hinge_pt =
|
||||
neutral.intersect_ray(*c, pull).ok_or_else(|| KernelError::Failed("the neutral plane is parallel to the pull direction".into()))?;
|
||||
out.push(HalfSpace { n: n2, d: n2.dot(hinge_pt) });
|
||||
moved += 1;
|
||||
}
|
||||
if moved == 0 {
|
||||
return Err(KernelError::Invalid("no faces to draft".into()));
|
||||
}
|
||||
convex_polyhedron(&out)
|
||||
}
|
||||
@@ -169,6 +169,29 @@ fn tapered_extrudes() {
|
||||
assert!(extrude_tapered(&Plane::XY, &c, 10.0, 1.0, -60f64.to_radians()).is_err(), "closes the profile");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polyhedra_shell_and_draft() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(50.0, 40.0, 30.0)).unwrap();
|
||||
let s = shell(&b, &[Vec3::new(25.0, 20.0, 30.0)], 2.0).unwrap();
|
||||
let m = measure(&s).unwrap();
|
||||
assert!(rel(m.volume, 60000.0 - 46.0 * 36.0 * 28.0) < 1e-6, "{}", m.volume);
|
||||
assert_eq!(m.merged.faces, 11);
|
||||
// Draft two side faces 5° in, about the bottom, pulling up.
|
||||
let d = draft(&b, &[Vec3::new(0.0, 20.0, 15.0), Vec3::new(50.0, 20.0, 15.0)], &Plane::XY, Vec3::Z, 5f64.to_radians()).unwrap();
|
||||
let t = 30.0 * 5f64.to_radians().tan();
|
||||
assert!(rel(measure(&d).unwrap().volume, 40.0 * 30.0 * (50.0 + 50.0 - 2.0 * t) / 2.0) < 1e-6, "{}", measure(&d).unwrap().volume);
|
||||
let tet = convex_polyhedron(&[
|
||||
HalfSpace { n: Vec3::new(0.0, 0.0, -1.0), d: 0.0 },
|
||||
HalfSpace { n: Vec3::new(-1.0, 0.0, 0.0), d: 0.0 },
|
||||
HalfSpace { n: Vec3::new(0.0, -1.0, 0.0), d: 0.0 },
|
||||
HalfSpace { n: Vec3::new(1.0, 1.0, 1.0).normalized().unwrap(), d: 6.0 / 3f64.sqrt() },
|
||||
])
|
||||
.unwrap();
|
||||
assert!(rel(measure(&tet).unwrap().volume, 36.0) < 1e-9);
|
||||
let cyl = cylinder(Vec3::ZERO, Vec3::Z, 5.0, 5.0).unwrap();
|
||||
assert!(shell(&cyl, &[Vec3::new(0.0, 0.0, 5.0)], 1.0).is_err(), "curved faces");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn debug_convergence() {
|
||||
|
||||
@@ -155,6 +155,14 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
|
||||
pen.line(&[(7.0, 8.0), (7.0, 16.0)], a);
|
||||
pen.line(&[(17.0, 8.0), (17.0, 16.0)], a);
|
||||
}
|
||||
"shell" => {
|
||||
pen.iso_box(3.0, 9.0, 10.0, 8.0, 9.0);
|
||||
pen.poly(&[(6.0, 9.0), (13.0, 5.5), (18.0, 8.0), (11.0, 11.5)], Color32::WHITE, a);
|
||||
}
|
||||
"draft" => {
|
||||
pen.poly(&[(4.0, 20.0), (20.0, 20.0), (17.0, 5.0), (7.0, 5.0)], pen.fill, ink);
|
||||
pen.line(&[(20.0, 20.0), (20.0, 5.0)], a);
|
||||
}
|
||||
"split" => {
|
||||
pen.poly(&[(3.0, 6.0), (21.0, 6.0), (21.0, 18.0), (3.0, 18.0)], pen.fill, ink);
|
||||
pen.line(&[(8.0, 2.0), (16.0, 22.0)], a);
|
||||
|
||||
@@ -24,6 +24,8 @@ fn icon_of(k: &FeatureKind) -> &'static str {
|
||||
FeatureKind::Mirror { .. } => "mirror",
|
||||
FeatureKind::ConstructionPlane { .. } => "plane",
|
||||
FeatureKind::Hole { .. } => "hole",
|
||||
FeatureKind::Shell { .. } => "shell",
|
||||
FeatureKind::Draft { .. } => "draft",
|
||||
FeatureKind::Split { .. } => "split",
|
||||
FeatureKind::Move { .. } => "move",
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user