Thanks @pablopeza.
This commit is contained in:
1 parent
e347f820e6
commit
cb857f1fa7
19 files changed
+173
-10
No files matched your search
@@ -901,7 +901,8 @@ pub enum FeatureKind {
|
||||
targets: Vec<BodyName>,
|
||||
},
|
||||
/// 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.
|
||||
/// the curves toward the body (to the next face, or `depth`), or with `normal` along the
|
||||
/// sketch normal. A web makes one per curve.
|
||||
Rib {
|
||||
sketch: SketchId,
|
||||
curves: Vec<String>,
|
||||
@@ -917,6 +918,10 @@ pub enum FeatureKind {
|
||||
/// Web: each curve is its own wall.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
web: bool,
|
||||
/// Grow the wall along the sketch normal (thickness across the curves, in the sketch
|
||||
/// plane) instead of across the plane from the curves.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
normal: bool,
|
||||
},
|
||||
/// Move planar faces (at the given points) onto a target plane parallel to them.
|
||||
ReplaceFace {
|
||||
|
||||
@@ -69,7 +69,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
}
|
||||
apply_op(st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))
|
||||
}
|
||||
FeatureKind::Rib { sketch, curves, thickness, depth, flip, web } => {
|
||||
FeatureKind::Rib { sketch, curves, thickness, depth, flip, web, normal } => {
|
||||
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
|
||||
let t = val(vals, thickness, Kind::Length)?;
|
||||
if !(t > 0.0) {
|
||||
@@ -88,7 +88,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
}
|
||||
let mut tools = Vec::new();
|
||||
for g in &groups {
|
||||
tools.push(rib_tool(st, &ss, g, t, depth, *flip)?);
|
||||
tools.push(if *normal { rib_tool_normal(st, &ss, g, t, depth, *flip)? } else { rib_tool(st, &ss, g, t, depth, *flip)? });
|
||||
}
|
||||
apply_op(st, f, tools, Operation::Join, &[])
|
||||
}
|
||||
@@ -286,6 +286,83 @@ fn rib_tool(st: &ModelState, ss: &SolvedSketch, ids: &[String], t: f64, depth: O
|
||||
tools.into_iter().next().ok_or_else(|| DocError::Invalid("the rib made no solid".into()))
|
||||
}
|
||||
|
||||
/// A rib wall along the sketch normal: the curves widened to `t` in the sketch plane, run from
|
||||
/// the sketch toward the body by `depth` (or until the whole wall reaches the body).
|
||||
fn rib_tool_normal(st: &ModelState, ss: &SolvedSketch, ids: &[String], t: f64, depth: Option<f64>, flip: bool) -> Result<Body> {
|
||||
let pts = chain_points(ss, ids)?;
|
||||
let len: f64 = pts.windows(2).map(|w| w[0].dist(w[1])).sum();
|
||||
if !(len > 1e-9) {
|
||||
return Err(DocError::Invalid("a zero-length rib curve".into()));
|
||||
}
|
||||
let samples = ((len / (t * 0.5).max(len / 400.0)).ceil() as usize).clamp(2, 400);
|
||||
let along: Vec<Vec3> = (0..=samples).map(|k| ss.plane.to_world(point_on_polyline(&pts, len * k as f64 / samples as f64))).collect();
|
||||
let normal = ss.plane.normal();
|
||||
let reach = st.bodies.iter().map(|b| b.mesh().bounds()).fold(0.0f64, |acc, bb| acc.max((bb.max - bb.min).len()));
|
||||
let inside = |q: Vec3| st.bodies.iter().any(|b| b.mesh().contains(q));
|
||||
// Where the body begins from each point of the curves, along `sign` × the sketch normal.
|
||||
let limit = reach * 2.0 + 1.0;
|
||||
let step = (limit / 400.0).max(1e-3);
|
||||
let hits = |sign: f64| -> Vec<f64> {
|
||||
let mut out = Vec::new();
|
||||
for q in &along {
|
||||
let Some(k) = (1..=400).find(|k| inside(*q + normal * (sign * *k as f64 * step))) else { continue };
|
||||
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 + normal * (sign * m)) { hi = m } else { lo = m }
|
||||
}
|
||||
out.push(hi);
|
||||
}
|
||||
out
|
||||
};
|
||||
// Toward the material: the side of the sketch where the curves meet the bodies (unless flipped).
|
||||
let (front, back) = (hits(1.0), hits(-1.0));
|
||||
let mut sign = if back.len() > front.len() { -1.0 } else { 1.0 };
|
||||
let mut reached = if sign > 0.0 { front } else { back };
|
||||
if flip {
|
||||
sign = -sign;
|
||||
reached = hits(sign);
|
||||
}
|
||||
let h = match depth {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
let far = reached.iter().copied().fold(f64::NAN, f64::max);
|
||||
if !far.is_finite() {
|
||||
return Err(DocError::Invalid("the rib curves do not face a body along the sketch normal (give a depth)".into()));
|
||||
}
|
||||
far + (t * 0.5).max(step)
|
||||
}
|
||||
};
|
||||
// The curves widened to `t`: each point offset by half the thickness along the miter of its
|
||||
// neighbours, both sides joined into one band.
|
||||
let half = t / 2.0;
|
||||
let mut left = Vec::with_capacity(pts.len());
|
||||
let mut right = Vec::with_capacity(pts.len());
|
||||
for (i, p) in pts.iter().enumerate() {
|
||||
let prev = i.checked_sub(1).and_then(|j| pts.get(j)).and_then(|q| (*p - *q).normalized());
|
||||
let next = pts.get(i + 1).and_then(|q| (*q - *p).normalized());
|
||||
let (n, scale) = match (prev, next) {
|
||||
(Some(a), Some(b)) => {
|
||||
let m = (a.perp() + b.perp()).normalized().unwrap_or(a.perp());
|
||||
(m, 1.0 / m.dot(a.perp()).max(0.25))
|
||||
}
|
||||
(Some(a), None) | (None, Some(a)) => (a.perp(), 1.0),
|
||||
(None, None) => continue,
|
||||
};
|
||||
left.push(*p + n * (half * scale));
|
||||
right.push(*p - n * (half * scale));
|
||||
}
|
||||
left.extend(right.into_iter().rev());
|
||||
let mut lp = Loop2::polygon(&left);
|
||||
if lp.signed_area() < 0.0 {
|
||||
lp = lp.reversed();
|
||||
}
|
||||
let region = Region2 { outer: lp, holes: Vec::new() };
|
||||
let (a, b) = if sign > 0.0 { (0.0, h) } else { (-h, 0.0) };
|
||||
let tools = kernel::extrude(&ss.plane, &[region], a, b)?;
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
//! A Rib or Web can grow along the sketch normal (`normal: true`): the wall is `thickness` wide
|
||||
//! across its curves in the sketch plane and runs from the sketch toward the body. Older designs
|
||||
//! need no transformation: a rib without `normal` grows across the sketch plane as before; 0.14.x
|
||||
//! asks for an update instead of building a normal rib in the plane.
|
||||
@@ -25,11 +25,11 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("solid.rib", "Rib", rib)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("rib")
|
||||
.params("sketch, curves: [open curve ids, in order]; thickness (across the sketch plane); depth? (default: to the next face); flip?: bool (fill to the other side)"),
|
||||
.params("sketch, curves: [open curve ids, in order]; thickness (across the sketch plane, or across the curves when normal); depth? (default: to the next face); flip?: bool (fill to the other side); direction?: in_plane|normal (grow across the sketch plane, the default, or along its normal)"),
|
||||
CommandSpec::new("solid.web", "Web", web)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("web")
|
||||
.params("sketch, curves: [open curve ids] (one wall each); thickness; depth? (default: to the next face); flip?: bool"),
|
||||
.params("sketch, curves: [open curve ids] (one wall each); thickness; depth? (default: to the next face); flip?: bool; direction?: in_plane|normal"),
|
||||
CommandSpec::new("solid.emboss", "Emboss", emboss)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("emboss")
|
||||
@@ -133,7 +133,12 @@ fn rib_like(s: &mut Session, p: &Value, cmd: &str, web: bool) -> Result<Value> {
|
||||
check_expr(s, d, Kind::Length, cmd, "depth")?;
|
||||
}
|
||||
let flip = bool_(p, "flip").unwrap_or(false);
|
||||
add_feature(s, p, FeatureKind::Rib { sketch: SketchId(sketch), curves, thickness, depth, flip, web })
|
||||
let normal = match str_(p, "direction").map(str::to_ascii_lowercase).as_deref() {
|
||||
None | Some("in_plane" | "plane") => false,
|
||||
Some("normal") => true,
|
||||
Some(o) => return Err(bad(cmd, format!("unknown direction `{o}` (in_plane or normal)"))),
|
||||
};
|
||||
add_feature(s, p, FeatureKind::Rib { sketch: SketchId(sketch), curves, thickness, depth, flip, web, normal })
|
||||
}
|
||||
|
||||
fn rib(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -77,6 +77,28 @@ fn rib_with_a_depth_and_web() {
|
||||
assert!(e.is_err_and(|e| e.to_string().contains("do not face a body")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rib_along_the_sketch_normal_reaches_the_body() {
|
||||
// A 20 x 20 x 10 box; a line along X on a sketch 10 in front of it (y = -10), whose plane
|
||||
// the box does not cross, so only a rib along the sketch normal can reach it.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 20, "width": 20, "height": 10, "corner": [0, 0, 0]}));
|
||||
run(&mut s, "sketch.create", json!({"plane": {"origin": [0, -10, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, 1]}, "name": "Front"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[2, 5], [18, 5]], "ids": ["l1"]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let e = s.execute("solid.rib", &json!({"sketch": "Front", "curves": ["l1"], "thickness": 2}));
|
||||
assert!(e.is_err_and(|e| e.to_string().contains("do not face a body")));
|
||||
run(&mut s, "solid.rib", json!({"sketch": "Front", "curves": ["l1"], "thickness": 2, "direction": "normal"}));
|
||||
// 16 long, 2 thick, 10 from the sketch to the box.
|
||||
assert!(rel(volume(&mut s), 4000.0 + 320.0) < 1e-6, "{}", volume(&mut s));
|
||||
assert_eq!(measure(&mut s)["body_count"], 1);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "solid.rib", json!({"sketch": "Front", "curves": ["l1"], "thickness": 2, "direction": "normal", "depth": 4}));
|
||||
assert!(rel(volume(&mut s), 4000.0 + 128.0) < 1e-6, "{}", volume(&mut s));
|
||||
let e = s.execute("solid.rib", &json!({"sketch": "Front", "curves": ["l1"], "thickness": 2, "direction": "sideways"}));
|
||||
assert!(e.is_err_and(|e| e.to_string().contains("unknown direction")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emboss_and_deboss_on_a_face() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -151,6 +151,8 @@ pub enum Kind {
|
||||
thickness: String,
|
||||
depth: String,
|
||||
flip: bool,
|
||||
/// Grow along the sketch normal rather than across the plane.
|
||||
normal: bool,
|
||||
},
|
||||
/// Raise or sink profiles of a sketch on a face.
|
||||
Emboss {
|
||||
@@ -491,7 +493,7 @@ impl Dialog {
|
||||
Dialog::new(Kind::Mirror, vec![SelInput::new("Objects", BODIES, true), SelInput::new("Mirror Plane", PLANES | PLANAR_FACES, false)])
|
||||
}
|
||||
"solid.rib" | "solid.web" => Dialog::new(
|
||||
Kind::Rib { web: id == "solid.web", thickness: "2 mm".into(), depth: String::new(), flip: false },
|
||||
Kind::Rib { web: id == "solid.web", thickness: "2 mm".into(), depth: String::new(), flip: false, normal: false },
|
||||
vec![SelInput::new("Curves", CURVES, true)],
|
||||
),
|
||||
"solid.emboss" => Dialog::new(
|
||||
@@ -2701,7 +2703,12 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
ui.end_row();
|
||||
}
|
||||
Kind::Split | Kind::ReplaceFace | Kind::Remove | Kind::Midplane => {}
|
||||
Kind::Rib { thickness, depth, flip, .. } => {
|
||||
Kind::Rib { thickness, depth, flip, normal, .. } => {
|
||||
row_label(ui, "Direction");
|
||||
let mut i = usize::from(*normal);
|
||||
combo(ui, "rib_direction", &["In plane", "Normal to sketch"], &mut i);
|
||||
*normal = i == 1;
|
||||
ui.end_row();
|
||||
row_label(ui, "Thickness");
|
||||
enter |= field(ui, thickness);
|
||||
ui.end_row();
|
||||
@@ -3977,11 +3984,14 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
};
|
||||
("construct.plane.at_angle", json!({"base": base, "axis": axis, "angle": angle}))
|
||||
}
|
||||
Kind::Rib { web, thickness, depth, flip } => {
|
||||
Kind::Rib { web, thickness, depth, flip, normal } => {
|
||||
need(0, "open sketch curves")?;
|
||||
let curves = curve_ids(d, 0);
|
||||
let sketch = curves_sketch(app, &curves, None).ok_or("the curves must be in one sketch")?;
|
||||
let mut p = json!({"sketch": sketch, "curves": curves, "thickness": thickness, "flip": flip});
|
||||
if *normal {
|
||||
p["direction"] = json!("normal");
|
||||
}
|
||||
if !depth.trim().is_empty() {
|
||||
p["depth"] = json!(depth);
|
||||
}
|
||||
@@ -5204,13 +5214,14 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
}
|
||||
d
|
||||
}
|
||||
FeatureKind::Rib { curves, thickness, depth, flip, web, .. } => {
|
||||
FeatureKind::Rib { curves, thickness, depth, flip, web, normal, .. } => {
|
||||
let mut d = start(if *web { "solid.web" } else { "solid.rib" })?;
|
||||
d.kind = Kind::Rib {
|
||||
web: *web,
|
||||
thickness: thickness.to_string(),
|
||||
depth: depth.as_deref().map(str::to_string).unwrap_or_default(),
|
||||
flip: *flip,
|
||||
normal: *normal,
|
||||
};
|
||||
if let Some(inp) = d.inputs.first_mut() {
|
||||
inp.items = curves.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect();
|
||||
|
||||
Reference in new issue
Block a user