Rib and Web (to the next face or a depth, flip), Emboss/deboss on planar faces (EmbossCmd), Replace Face onto a parallel plane or face, Align (point to point, face to face), Remove (SoftDeleteCommand), timeline Copy/Paste of features (independent copies: own dimension parameters and input names, references remapped, optional translation)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
075808e7ee
commit
b5051b2744
13 files changed
+864
-6
No files matched your search
@@ -187,6 +187,20 @@ impl FeatureKind {
|
||||
dir(direction);
|
||||
}
|
||||
FeatureKind::Thread { face, .. } => pt(face),
|
||||
FeatureKind::ReplaceFace { faces, target, .. } => {
|
||||
faces.iter_mut().for_each(pt);
|
||||
map_plane_ref(m, target);
|
||||
}
|
||||
FeatureKind::Align { from, to, from_normal, to_normal, .. } => {
|
||||
pt(from);
|
||||
pt(to);
|
||||
if let Some(n) = from_normal {
|
||||
dir(n);
|
||||
}
|
||||
if let Some(n) = to_normal {
|
||||
dir(n);
|
||||
}
|
||||
}
|
||||
FeatureKind::Scale { origin, .. } => pt(origin),
|
||||
FeatureKind::Move { rotate_axis, .. } => {
|
||||
if let Some(a) = rotate_axis {
|
||||
@@ -200,6 +214,9 @@ impl FeatureKind {
|
||||
| FeatureKind::Sweep { .. }
|
||||
| FeatureKind::BoundingSolid { .. }
|
||||
| FeatureKind::Pipe { .. }
|
||||
| FeatureKind::Emboss { .. }
|
||||
| FeatureKind::Rib { .. }
|
||||
| FeatureKind::Remove { .. }
|
||||
| FeatureKind::Import { .. }
|
||||
| FeatureKind::MeshImport { .. } => {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//! Copies of features (timeline Copy / Paste): references to sketches and features inside the
|
||||
//! copied set are moved to the copies.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{FeatureKind, PatternKind, PlaneRef};
|
||||
|
||||
fn plane_names(p: &mut PlaneRef, names: &BTreeMap<String, String>) {
|
||||
let mut p = p;
|
||||
for _ in 0..64 {
|
||||
match p {
|
||||
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => p = base,
|
||||
PlaneRef::Construction { name } => {
|
||||
if let Some(n) = names.get(name) {
|
||||
*name = n.clone();
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FeatureKind {
|
||||
/// Point references to sketches (by id) and features (by name) at their copies.
|
||||
pub fn remap_refs(&mut self, sketches: &BTreeMap<u64, u64>, names: &BTreeMap<String, String>) {
|
||||
let sk = |id: &mut u64| {
|
||||
if let Some(n) = sketches.get(id) {
|
||||
*id = *n;
|
||||
}
|
||||
};
|
||||
let feats = |list: &mut Vec<String>| {
|
||||
for x in list.iter_mut() {
|
||||
if let Some(n) = names.get(x) {
|
||||
*x = n.clone();
|
||||
}
|
||||
}
|
||||
};
|
||||
match self {
|
||||
FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } | FeatureKind::Split { plane, .. } => {
|
||||
plane_names(plane, names)
|
||||
}
|
||||
FeatureKind::Extrude { sketch, .. }
|
||||
| FeatureKind::Revolve { sketch, .. }
|
||||
| FeatureKind::Emboss { sketch, .. }
|
||||
| FeatureKind::Rib { sketch, .. } => sk(sketch),
|
||||
FeatureKind::Sweep { sketch, path_sketch, .. } => {
|
||||
sk(sketch);
|
||||
sk(path_sketch);
|
||||
}
|
||||
FeatureKind::Pipe { path_sketch, .. } => sk(path_sketch),
|
||||
FeatureKind::Loft { sections, .. } => sections.iter_mut().for_each(|s| sk(&mut s.sketch)),
|
||||
FeatureKind::Hole { points, .. } => {
|
||||
if let Some(p) = points {
|
||||
sk(&mut p.sketch);
|
||||
}
|
||||
}
|
||||
FeatureKind::Pattern { features, pattern, .. } => {
|
||||
feats(features);
|
||||
if let PatternKind::Path { path_sketch, .. } = pattern {
|
||||
sk(path_sketch);
|
||||
}
|
||||
}
|
||||
FeatureKind::Mirror { features, plane, .. } => {
|
||||
feats(features);
|
||||
plane_names(plane, names);
|
||||
}
|
||||
FeatureKind::Draft { neutral, .. } => plane_names(neutral, names),
|
||||
FeatureKind::ReplaceFace { target, .. } => plane_names(target, names),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,6 +333,56 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
targets: Vec<String>,
|
||||
},
|
||||
/// Raise (emboss) or sink (deboss) sketch profiles on the planar face they lie on.
|
||||
Emboss {
|
||||
sketch: u64,
|
||||
profiles: ProfileSel,
|
||||
depth: String,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
deboss: bool,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
targets: Vec<String>,
|
||||
},
|
||||
/// A thin wall from open sketch curves: `thickness` across the sketch plane, filling from
|
||||
/// the curves toward the body (to the next face, or `depth`). A web makes one per curve.
|
||||
Rib {
|
||||
sketch: u64,
|
||||
curves: Vec<String>,
|
||||
thickness: String,
|
||||
/// Depth from the curves; `None` = to the next face of the body.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
depth: Option<String>,
|
||||
/// Fill toward the other side of the curves.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
flip: bool,
|
||||
/// Web: each curve is its own wall.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
web: bool,
|
||||
},
|
||||
/// Move planar faces (at the given points) onto a target plane parallel to them.
|
||||
ReplaceFace {
|
||||
faces: Vec<Vec3>,
|
||||
target: PlaneRef,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
},
|
||||
/// Move bodies so that `from` lands on `to` (and, with normals, the faces meet face to face).
|
||||
Align {
|
||||
bodies: Vec<String>,
|
||||
from: Vec3,
|
||||
to: Vec3,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
from_normal: Option<Vec3>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
to_normal: Option<Vec3>,
|
||||
/// Normals the same way instead of facing each other.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
flip: bool,
|
||||
},
|
||||
/// Remove bodies from the model (from here on in the timeline).
|
||||
Remove {
|
||||
bodies: Vec<String>,
|
||||
},
|
||||
Move {
|
||||
bodies: Vec<String>,
|
||||
translate: [String; 3],
|
||||
@@ -512,6 +562,12 @@ impl FeatureKind {
|
||||
FeatureKind::OffsetFace { .. } => "OffsetFacesFeature",
|
||||
FeatureKind::BoundingSolid { .. } => "BoundingSolidFeature",
|
||||
FeatureKind::Pipe { .. } => "PipeFeature",
|
||||
FeatureKind::Emboss { .. } => "EmbossFeature",
|
||||
FeatureKind::Rib { web: false, .. } => "RibFeature",
|
||||
FeatureKind::Rib { .. } => "WebFeature",
|
||||
FeatureKind::ReplaceFace { .. } => "ReplaceFaceFeature",
|
||||
FeatureKind::Align { .. } => "AlignFeature",
|
||||
FeatureKind::Remove { .. } => "RemoveFeature",
|
||||
FeatureKind::Import { .. } => "BaseFeature",
|
||||
FeatureKind::MeshImport { .. } => "MeshFeature",
|
||||
}
|
||||
@@ -546,6 +602,12 @@ impl FeatureKind {
|
||||
FeatureKind::OffsetFace { .. } => "OffsetFace",
|
||||
FeatureKind::BoundingSolid { .. } => "BoundingSolid",
|
||||
FeatureKind::Pipe { .. } => "Pipe",
|
||||
FeatureKind::Emboss { .. } => "Emboss",
|
||||
FeatureKind::Rib { web: false, .. } => "Rib",
|
||||
FeatureKind::Rib { .. } => "Web",
|
||||
FeatureKind::ReplaceFace { .. } => "ReplaceFace",
|
||||
FeatureKind::Align { .. } => "Align",
|
||||
FeatureKind::Remove { .. } => "Remove",
|
||||
FeatureKind::Import { .. } => "Import",
|
||||
FeatureKind::MeshImport { .. } => "Mesh",
|
||||
}
|
||||
@@ -620,6 +682,13 @@ impl FeatureKind {
|
||||
v.push(a);
|
||||
}
|
||||
}
|
||||
FeatureKind::Emboss { depth, .. } => v.push(depth),
|
||||
FeatureKind::Rib { thickness, depth, .. } => {
|
||||
v.push(thickness);
|
||||
v.extend(depth.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::ReplaceFace { target, .. } => plane_exprs(target, &mut v),
|
||||
FeatureKind::Align { .. } | FeatureKind::Remove { .. } => {}
|
||||
FeatureKind::Import { .. } | FeatureKind::MeshImport { .. } => {}
|
||||
}
|
||||
v
|
||||
@@ -851,6 +920,7 @@ impl Document {
|
||||
FeatureKind::Sweep { sketch, path_sketch, .. } if *sketch == id || *path_sketch == id => gone.push(f.id),
|
||||
FeatureKind::Loft { sections, .. } if sections.iter().any(|s| s.sketch == id) => gone.push(f.id),
|
||||
FeatureKind::Pattern { pattern: PatternKind::Path { path_sketch, .. }, .. } if *path_sketch == id => gone.push(f.id),
|
||||
FeatureKind::Emboss { sketch, .. } | FeatureKind::Rib { sketch, .. } if *sketch == id => gone.push(f.id),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1305,8 +1305,16 @@ fn replay(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[path = "eval_more.rs"]
|
||||
mod more;
|
||||
|
||||
fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState, warning: &mut Option<String>) -> Result<()> {
|
||||
match &f.kind {
|
||||
FeatureKind::Emboss { .. }
|
||||
| FeatureKind::Rib { .. }
|
||||
| FeatureKind::ReplaceFace { .. }
|
||||
| FeatureKind::Align { .. }
|
||||
| FeatureKind::Remove { .. } => more::eval(doc, vals, f, st),
|
||||
FeatureKind::Sketch { plane, sketch } => {
|
||||
let plane = doc.resolve_plane(vals, plane, 0)?;
|
||||
let mut sk = sketch.clone();
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
//! Evaluation of Emboss, Rib/Web, Replace Face, Align and Remove (features built from the
|
||||
//! kernel's extrude, boolean, offset and transform operations).
|
||||
|
||||
use super::*;
|
||||
use solvecraft_geom::{Loop2, Seg2};
|
||||
|
||||
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
|
||||
match &f.kind {
|
||||
FeatureKind::Emboss { sketch, profiles, depth, deboss, targets } => {
|
||||
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?;
|
||||
let regions = select_profiles(ss, profiles)?;
|
||||
let d = val(vals, depth, Kind::Length)?;
|
||||
if !(d > 0.0) {
|
||||
return Err(DocError::Invalid("the emboss depth must be positive".into()));
|
||||
}
|
||||
let (a, b) = if *deboss { (-d, 0.0) } else { (0.0, d) };
|
||||
let tools = kernel::extrude(&ss.plane, ®ions, a, b)?;
|
||||
if st.bodies.is_empty() {
|
||||
return Err(DocError::Invalid("there is no body to emboss".into()));
|
||||
}
|
||||
apply_op(st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, targets)
|
||||
}
|
||||
FeatureKind::Rib { sketch, curves, thickness, depth, flip, web } => {
|
||||
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
|
||||
let t = val(vals, thickness, Kind::Length)?;
|
||||
if !(t > 0.0) {
|
||||
return Err(DocError::Invalid("the rib thickness must be positive".into()));
|
||||
}
|
||||
let depth = match depth {
|
||||
Some(e) => Some(val(vals, e, Kind::Length)?)
|
||||
.filter(|d| *d > 0.0)
|
||||
.ok_or_else(|| DocError::Invalid("the rib depth must be positive".into()))
|
||||
.map(Some)?,
|
||||
None => None,
|
||||
};
|
||||
let groups: Vec<Vec<String>> = if *web { curves.iter().map(|c| vec![c.clone()]).collect() } else { vec![curves.clone()] };
|
||||
if groups.is_empty() || curves.is_empty() || curves.len() > 1000 {
|
||||
return Err(DocError::Invalid("a rib needs 1…1000 sketch curves".into()));
|
||||
}
|
||||
let mut tools = Vec::new();
|
||||
for g in &groups {
|
||||
tools.push(rib_tool(st, &ss, g, t, depth, *flip)?);
|
||||
}
|
||||
apply_op(st, f, tools, Operation::Join, &[])
|
||||
}
|
||||
FeatureKind::ReplaceFace { faces, target, body } => {
|
||||
let tp = doc.resolve_plane(vals, target, 0)?;
|
||||
let tn = tp.normal();
|
||||
let i = body_at(st, body, faces)?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let mut b = mb.body.clone();
|
||||
for p in faces {
|
||||
let n = planar_normal_at(&b, *p).ok_or_else(|| DocError::Invalid("not supported yet: replacing a face that is not planar".into()))?;
|
||||
if n.dot(tn).abs() < 1.0 - 1e-9 {
|
||||
return Err(DocError::Invalid("not supported yet: a target that is not parallel to the face".into()));
|
||||
}
|
||||
let d = (tp.origin - *p).dot(n);
|
||||
if d.abs() > 1e-9 {
|
||||
b = kernel::offset_faces(&b, &[*p], d)?;
|
||||
}
|
||||
}
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::new(mb.name, b, mb.feature);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Align { bodies, from, to, from_normal, to_normal, flip } => {
|
||||
let mut m = translation(Vec3::ZERO);
|
||||
if let (Some(a), Some(b)) = (from_normal.and_then(Vec3::normalized), to_normal.and_then(Vec3::normalized)) {
|
||||
// Turn `a` onto −b (faces meet) or b (flipped), about the `from` point.
|
||||
let want = if *flip { b } else { b * -1.0 };
|
||||
let axis = a.cross(want);
|
||||
let ang = axis.len().atan2(a.dot(want));
|
||||
let axis = axis.normalized().unwrap_or_else(|| a.any_perp());
|
||||
if ang.abs() > 1e-12 {
|
||||
m = rotation(*from, axis, ang);
|
||||
}
|
||||
}
|
||||
let d = *to - *from;
|
||||
m[3][0] += d.x;
|
||||
m[3][1] += d.y;
|
||||
m[3][2] += d.z;
|
||||
if bodies.is_empty() {
|
||||
return Err(DocError::Invalid("no bodies to align".into()));
|
||||
}
|
||||
for n in bodies {
|
||||
let i = st.bodies.iter().position(|b| &b.name == n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")))?;
|
||||
if let Some(mb) = st.bodies.get_mut(i) {
|
||||
let nb = kernel::transform_matrix(&mb.body, m)?;
|
||||
*mb = ModelBody::new(mb.name.clone(), nb, mb.feature);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Remove { bodies } => {
|
||||
if bodies.is_empty() {
|
||||
return Err(DocError::Invalid("no bodies to remove".into()));
|
||||
}
|
||||
for n in bodies {
|
||||
if st.body(n).is_none() {
|
||||
return Err(DocError::Unknown(format!("body `{n}`")));
|
||||
}
|
||||
}
|
||||
st.bodies.retain(|b| !bodies.contains(&b.name));
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(DocError::Invalid(format!("{} is not evaluated here", f.name))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unit outward normal of the planar face through `p`.
|
||||
fn planar_normal_at(b: &Body, p: Vec3) -> Option<Vec3> {
|
||||
let tol = (b.size() * 2e-3).max(1e-3);
|
||||
let faces = b.faces(tol).ok()?;
|
||||
faces
|
||||
.iter()
|
||||
.filter_map(|f| f.plane_normal.map(|n| (n, ((p - f.centroid).dot(n)).abs())))
|
||||
.filter(|(_, d)| *d < tol * 5.0)
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map(|(n, _)| n)
|
||||
}
|
||||
|
||||
/// The curves of a chain as one polyline in sketch coordinates, in order.
|
||||
fn chain_points(ss: &SolvedSketch, ids: &[String]) -> Result<Vec<Vec2>> {
|
||||
let mut segs: Vec<Seg2> = Vec::new();
|
||||
for id in ids {
|
||||
let ci = ss.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("curve `{id}`")))?;
|
||||
segs.extend(ss.sketch.segs(ci));
|
||||
}
|
||||
let Some(first) = segs.first().copied() else { return Err(DocError::Invalid("no curves".into())) };
|
||||
// Orient the first segment toward the second, then chain.
|
||||
let mut chain = vec![first];
|
||||
if let Some(n) = segs.get(1)
|
||||
&& (first.start().dist(n.start()) < 1e-6 || first.start().dist(n.end()) < 1e-6)
|
||||
{
|
||||
chain[0] = first.reversed();
|
||||
}
|
||||
for s in segs.iter().skip(1) {
|
||||
let prev = chain.last().map(Seg2::end).unwrap_or_default();
|
||||
let s = if s.end().dist(prev) < s.start().dist(prev) { s.reversed() } else { *s };
|
||||
if s.start().dist(prev) > 1e-5 {
|
||||
return Err(DocError::Invalid("the rib curves are not connected end to end".into()));
|
||||
}
|
||||
chain.push(s);
|
||||
}
|
||||
let mut pts = vec![chain[0].start()];
|
||||
for s in &chain {
|
||||
let n = if matches!(s, Seg2::Line { .. }) { 1 } else { 24 };
|
||||
for k in 1..=n {
|
||||
pts.push(s.point_at(k as f64 / n as f64));
|
||||
}
|
||||
}
|
||||
Ok(pts)
|
||||
}
|
||||
|
||||
/// A rib wall: the band swept from the curves across the plane by `depth` (or to the body),
|
||||
/// `t` thick about the sketch plane.
|
||||
fn rib_tool(st: &ModelState, ss: &SolvedSketch, ids: &[String], t: f64, depth: Option<f64>, flip: bool) -> Result<Body> {
|
||||
let pts = chain_points(ss, ids)?;
|
||||
let (Some(a), Some(b)) = (pts.first().copied(), pts.last().copied()) else { return Err(DocError::Invalid("empty rib curve".into())) };
|
||||
let chord = (b - a).normalized().ok_or_else(|| DocError::Invalid("a closed or zero-length rib curve".into()))?;
|
||||
let mid = pts.get(pts.len() / 2).copied().unwrap_or(a);
|
||||
// Toward the material: the side of the curve where the bodies are (unless flipped).
|
||||
let mut n = chord.perp();
|
||||
let reach = st.bodies.iter().map(|b| b.mesh().bounds()).fold(0.0f64, |acc, bb| acc.max((bb.max - bb.min).len()));
|
||||
let probe = |side: Vec2| {
|
||||
let mut hits = 0;
|
||||
for k in 1..=20 {
|
||||
let q = ss.plane.to_world(mid + side * (reach * k as f64 / 20.0));
|
||||
if st.bodies.iter().any(|b| b.mesh().contains(q)) {
|
||||
hits += 1;
|
||||
}
|
||||
}
|
||||
hits
|
||||
};
|
||||
if probe(n * -1.0) > probe(n) {
|
||||
n = n * -1.0;
|
||||
}
|
||||
if flip {
|
||||
n = n * -1.0;
|
||||
}
|
||||
// From each point of the curves, walk across the plane until the body begins (or `depth`);
|
||||
// walls that reach the body go a little into it so they merge with it.
|
||||
let inside = |q: Vec2| {
|
||||
let w = ss.plane.to_world(q);
|
||||
st.bodies.iter().any(|b| b.mesh().contains(w))
|
||||
};
|
||||
let len: f64 = pts.windows(2).map(|w| w[0].dist(w[1])).sum();
|
||||
let samples = ((len / (t.min(len) * 0.25).max(len / 400.0)).ceil() as usize).clamp(2, 400);
|
||||
let along: Vec<Vec2> = (0..=samples).map(|k| point_on_polyline(&pts, len * k as f64 / samples as f64)).collect();
|
||||
let limit = depth.unwrap_or(reach * 2.0 + 1.0);
|
||||
let step = (limit.min(reach * 2.0 + 1.0) / 400.0).max(1e-3);
|
||||
let overlap = (len / samples as f64 * 2.0).max(step * 2.0).min(t);
|
||||
let mut far = Vec::with_capacity(along.len());
|
||||
for q in &along {
|
||||
let mut hit = None;
|
||||
let mut k = 1;
|
||||
while (k as f64) * step <= limit + step && k < 100_000 {
|
||||
if inside(*q + n * (k as f64 * step)) {
|
||||
// Refine between the last outside and the first inside sample.
|
||||
let (mut lo, mut hi) = ((k - 1) as f64 * step, k as f64 * step);
|
||||
for _ in 0..30 {
|
||||
let m = 0.5 * (lo + hi);
|
||||
if inside(*q + n * m) { hi = m } else { lo = m }
|
||||
}
|
||||
hit = Some(hi);
|
||||
break;
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
let h = match (hit, depth) {
|
||||
(Some(h), Some(d)) if h >= d => d,
|
||||
(Some(h), _) => h + overlap,
|
||||
(None, Some(d)) => d,
|
||||
(None, None) => return Err(DocError::Invalid("the rib curves do not face a body (give a depth)".into())),
|
||||
};
|
||||
far.push(*q + n * h);
|
||||
}
|
||||
let mut poly = along.clone();
|
||||
poly.extend(far.into_iter().rev());
|
||||
let mut lp = Loop2::polygon(&poly);
|
||||
if lp.signed_area() < 0.0 {
|
||||
lp = lp.reversed();
|
||||
}
|
||||
let region = Region2 { outer: lp, holes: Vec::new() };
|
||||
let tools = kernel::extrude(&ss.plane, &[region], -t / 2.0, t / 2.0)?;
|
||||
tools.into_iter().next().ok_or_else(|| DocError::Invalid("the rib made no solid".into()))
|
||||
}
|
||||
|
||||
/// The point `d` along a polyline.
|
||||
fn point_on_polyline(pts: &[Vec2], d: f64) -> Vec2 {
|
||||
let mut left = d.max(0.0);
|
||||
for w in pts.windows(2) {
|
||||
let l = w[0].dist(w[1]);
|
||||
if left <= l && l > 0.0 {
|
||||
return w[0].lerp(w[1], left / l);
|
||||
}
|
||||
left -= l;
|
||||
}
|
||||
pts.last().copied().unwrap_or_default()
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod assembly;
|
||||
mod clipboard;
|
||||
mod document;
|
||||
mod eval;
|
||||
pub mod expr;
|
||||
|
||||
@@ -137,7 +137,17 @@ impl FeatureKind {
|
||||
v.push(("Angle", a, A));
|
||||
}
|
||||
}
|
||||
FeatureKind::Emboss { depth, .. } => v.push(("Depth", depth, L)),
|
||||
FeatureKind::Rib { thickness, depth, .. } => {
|
||||
v.push(("Thickness", thickness, L));
|
||||
if let Some(d) = depth {
|
||||
v.push(("Depth", d, L));
|
||||
}
|
||||
}
|
||||
FeatureKind::ReplaceFace { target, .. } => plane_inputs_mut(target, &mut v),
|
||||
FeatureKind::Combine { .. }
|
||||
| FeatureKind::Align { .. }
|
||||
| FeatureKind::Remove { .. }
|
||||
| FeatureKind::Loft { .. }
|
||||
| FeatureKind::Sweep { .. }
|
||||
| FeatureKind::Import { .. }
|
||||
|
||||
Reference in new issue
Block a user