feat: edge flanges with their own angle and height per edge (#447)
One edge flange feature gave all its flanges one height and one angle. A
part whose flanges lean differently took one feature per angle, and
flanges of separate features don't close toward each other, so its
corners stayed open.
- sheet.flange (edge): an entry of `edges` is a point [x, y, z] as before,
or {point, angle?, height?} whose angle and height (expressions, checked
like the feature's) replace the feature's for that edge. Bad input is
refused with the edge named. Before, the object form was read for its
point and the rest was ignored without a word.
- The feature stores its edges as FlangeEdge. An edge without values of
its own is written as the plain point it always was, so designs without
them are read and written as before; an unknown key in an edge is
refused, not dropped.
- The edges' values are feature inputs of their own (Edge angle, Edge
height, after the feature's), so parameters, renames and
parameters.change reach them. timeline.edit names the edges again when
it changes them, so each value stays with its edge.
- SheetBody::add_flanges_each trims every edge by its own setback and
adds the flanges. The corners between them close as between equal
flanges: the closing already works from the two flanges it joins (each
one's fold and leg), so the flat pattern follows by hand from the
angles, the heights and the rule (the tests derive it).
- The Flange dialog shows a row per picked edge with an angle and a
height box (empty: the dialog's value) once more than one edge is
picked, and brings them back on Edit Feature.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0dee9deec1
commit
fb4c1d74b9
14 files changed
+1039
-38
No files matched your search
@@ -46,6 +46,9 @@ pub enum Sm {
|
||||
distance: String,
|
||||
reverse: bool,
|
||||
rule: usize,
|
||||
/// Edge flanges: the edges with an angle and/or a height of their own (the picked point,
|
||||
/// angle, height; an empty text means the dialog's), from the rows per edge.
|
||||
own: Vec<(Vec3, String, String)>,
|
||||
},
|
||||
Hem {
|
||||
length: String,
|
||||
@@ -125,6 +128,7 @@ fn new_flange(ty: usize) -> Sm {
|
||||
distance: "20 mm".into(),
|
||||
reverse: false,
|
||||
rule: 0,
|
||||
own: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +192,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
|
||||
let t = Tokens::get();
|
||||
let mut enter = false;
|
||||
match k {
|
||||
Sm::Flange { ty, height, angle, radius, position, flip, distance, reverse, rule } => {
|
||||
Sm::Flange { ty, height, angle, radius, position, flip, distance, reverse, rule, own } => {
|
||||
*ty = flange_type(inputs);
|
||||
row_label(ui, "Type");
|
||||
ui.label(FLANGE_TYPES.get(*ty).copied().unwrap_or(""));
|
||||
@@ -209,6 +213,34 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
|
||||
crate::params_dialog::complete(ui, &r, radius);
|
||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
ui.end_row();
|
||||
// With several edges, a row for each: an angle and a height of its own, left
|
||||
// empty for the values above (shown as the hints).
|
||||
let points = edge_point_list(inputs);
|
||||
if points.len() > 1 || own.iter().any(|o| !(o.1.trim().is_empty() && o.2.trim().is_empty())) {
|
||||
row_label(ui, "Per Edge");
|
||||
ui.label(RichText::new("angle, height").weak()).on_hover_text("An edge's own angle and height. Empty: the values above.");
|
||||
ui.end_row();
|
||||
let w = ((ui.spacing().text_edit_width - ui.spacing().item_spacing.x) / 2.0).max(40.0);
|
||||
for (n, p) in points.iter().enumerate() {
|
||||
let slot = own_slot(own, *p);
|
||||
let Some((_, a, h)) = own.get_mut(slot) else { continue };
|
||||
row_label(ui, &format!("Edge {}", n + 1));
|
||||
ui.horizontal(|ui| {
|
||||
for (text, hint, what, id) in
|
||||
[(a, angle.as_str(), "angle", "sm_edge_angle"), (h, height.as_str(), "height", "sm_edge_height")]
|
||||
{
|
||||
let r = ui
|
||||
.add(egui::TextEdit::singleline(text).hint_text(hint).desired_width(w).id_salt((id, n)))
|
||||
.on_hover_text(format!("The {what} of the edge picked at ({:.1}, {:.1}, {:.1})", p.x, p.y, p.z));
|
||||
// For scenarios and the control channel: `dialog:Edge 2 angle`.
|
||||
crate::scenario::publish_handle(&format!("dialog:Edge {} {what}", n + 1), r.rect.center());
|
||||
crate::params_dialog::complete(ui, &r, text);
|
||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
row_label(ui, "Distance");
|
||||
@@ -419,14 +451,48 @@ fn edge_points(inputs: &[SelInput]) -> Vec<Value> {
|
||||
items(inputs, 0).iter().filter_map(|x| if let Sel::Edge { point, .. } = x { Some(pt(*point)) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// The picked edges' points.
|
||||
fn edge_point_list(inputs: &[SelInput]) -> Vec<Vec3> {
|
||||
items(inputs, 0).iter().filter_map(|x| if let Sel::Edge { point, .. } = x { Some(*point) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// Where the own angle and height of the edge picked at `p` are kept (an empty entry on first use).
|
||||
fn own_slot(own: &mut Vec<(Vec3, String, String)>, p: Vec3) -> usize {
|
||||
own.iter().position(|o| o.0.dist(p) < 1e-9).unwrap_or_else(|| {
|
||||
own.push((p, String::new(), String::new()));
|
||||
own.len() - 1
|
||||
})
|
||||
}
|
||||
|
||||
/// The edges for `sheet.flange`: a plain point, or `{point, angle?, height?}` for an edge with
|
||||
/// an angle and/or a height of its own.
|
||||
fn flange_edges(inputs: &[SelInput], own: &[(Vec3, String, String)]) -> Vec<Value> {
|
||||
edge_point_list(inputs)
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let (a, h) = own.iter().find(|o| o.0.dist(p) < 1e-9).map(|o| (o.1.trim(), o.2.trim())).unwrap_or(("", ""));
|
||||
if a.is_empty() && h.is_empty() {
|
||||
return pt(p);
|
||||
}
|
||||
let mut e = json!({"point": pt(p)});
|
||||
for (k, v) in [("angle", a), ("height", h)] {
|
||||
if !v.is_empty() {
|
||||
e[k] = json!(v);
|
||||
}
|
||||
}
|
||||
e
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The commands the dialog's OK runs (`extra`: parameters kept from the feature being edited).
|
||||
pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map<String, Value>) -> Result<Vec<(String, Value)>, String> {
|
||||
let s = &app.session;
|
||||
let (cmd, mut p): (&str, Value) = match k {
|
||||
Sm::Flange { height, angle, radius, position, flip, distance, reverse, rule, .. } => {
|
||||
Sm::Flange { height, angle, radius, position, flip, distance, reverse, rule, own, .. } => {
|
||||
let mut p = match flange_type(inputs) {
|
||||
1 => {
|
||||
let mut p = json!({"type": "edge", "edges": edge_points(inputs), "height": height, "angle": angle,
|
||||
let mut p = json!({"type": "edge", "edges": flange_edges(inputs, own), "height": height, "angle": angle,
|
||||
"position": POSITIONS.get(*position).copied().unwrap_or("inside")});
|
||||
if !radius.trim().is_empty() {
|
||||
p["radius"] = json!(radius);
|
||||
@@ -572,6 +638,16 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
|
||||
if let Some(b) = body {
|
||||
extra.insert("body".into(), json!(b));
|
||||
}
|
||||
// The edges, and the angle and height some have of their own (kept by the point the
|
||||
// edge's selection carries).
|
||||
let (mut picked, mut own) = (Vec::new(), Vec::new());
|
||||
for e in edges {
|
||||
let Some(sel) = edge_sel(s, e.point) else { continue };
|
||||
if let (Sel::Edge { point, .. }, true) = (&sel, e.angle.is_some() || e.height.is_some()) {
|
||||
own.push((*point, e.angle.clone().unwrap_or_default(), e.height.clone().unwrap_or_default()));
|
||||
}
|
||||
picked.push(sel);
|
||||
}
|
||||
let f = Sm::Flange {
|
||||
ty: 1,
|
||||
height: height.clone(),
|
||||
@@ -582,8 +658,9 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
|
||||
distance: "20 mm".into(),
|
||||
reverse: false,
|
||||
rule: 0,
|
||||
own,
|
||||
};
|
||||
(f, edges.iter().filter_map(|p| edge_sel(s, *p)).collect())
|
||||
(f, picked)
|
||||
}
|
||||
FeatureKind::SheetHem { edges, length, gap, flip, body } => {
|
||||
if let Some(b) = body {
|
||||
|
||||
@@ -318,3 +318,65 @@ fn pocket_depth_labels_follow_inches_without_changing_flat_data() {
|
||||
assert_eq!(super::pocket_label(&app, &json!({"side":"top","depth_mm":25.4*0.1234})).as_deref(), Some("Top pocket: 0.1234 in deep"));
|
||||
assert!(super::pocket_label(&app, &json!({"side":"top","depth_mm":-1})).is_none());
|
||||
}
|
||||
|
||||
/// Edges with an angle and/or a height of their own: the texts of the dialog's rows per edge
|
||||
/// become `{point, angle?, height?}` edges while the others stay plain points; Edit Feature
|
||||
/// brings them back and rebuilds the same flanges.
|
||||
#[test]
|
||||
fn edge_flanges_can_have_an_angle_and_a_height_of_their_own() {
|
||||
let mut app = plate();
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
let edges: Vec<Sel> = [[50.0, 0.0, 2.5], [50.0, 60.0, 2.5], [0.0, 30.0, 2.5], [100.0, 30.0, 2.5]]
|
||||
.iter()
|
||||
.filter_map(|p| edge_sel(&app.session, Vec3::new(p[0], p[1], p[2])))
|
||||
.collect();
|
||||
assert_eq!(edges.len(), 4);
|
||||
let point = |i: usize| if let Sel::Edge { point, .. } = &edges[i] { *point } else { Vec3::ZERO };
|
||||
app.session.selection = edges.clone();
|
||||
app.start("sheet.flange");
|
||||
let mut d = app.dialog.clone().unwrap();
|
||||
if let Kind::Sheet(Sm::Flange { height, angle, own, .. }) = &mut d.kind {
|
||||
*height = "20 mm".into();
|
||||
*angle = "75 deg".into();
|
||||
// The second edge: its own angle; the fourth: both; the third: texts left blank.
|
||||
own.push((point(1), "100 deg".into(), String::new()));
|
||||
own.push((point(3), "90 deg".into(), "15 mm".into()));
|
||||
own.push((point(2), " ".into(), String::new()));
|
||||
}
|
||||
let c = apply_commands(&app, &d).unwrap();
|
||||
assert_eq!(
|
||||
c[0].1["edges"],
|
||||
json!([pt(point(0)), {"point": pt(point(1)), "angle": "100 deg"}, pt(point(2)), {"point": pt(point(3)), "angle": "90 deg", "height": "15 mm"}]),
|
||||
"no text or a blank one: a plain point"
|
||||
);
|
||||
run_all(&mut app, c);
|
||||
let bends = app.session.execute("sheet.flat_pattern", &json!({})).unwrap()["bends"].clone();
|
||||
let angles: Vec<f64> = bends.as_array().unwrap().iter().map(|b| b["angle_deg"].as_f64().unwrap().round()).collect();
|
||||
assert_eq!(angles, [75.0, 100.0, 75.0, 90.0], "{bends}");
|
||||
assert!(app.session.model.results.iter().all(|r| r.error.is_none()), "{:?}", app.session.model.results);
|
||||
// Edit Feature: the edges and their own values come back, and OK rebuilds the same part.
|
||||
let f = app.session.doc.features.last().unwrap().clone();
|
||||
let mut e = edit_dialog(&app, f.id, None).unwrap();
|
||||
assert_eq!(e.inputs[0].items.len(), 4);
|
||||
let Kind::Sheet(Sm::Flange { own, .. }) = &e.kind else { panic!("not a flange dialog") };
|
||||
assert_eq!(own.len(), 2, "{own:?}");
|
||||
assert!(own.iter().any(|o| o.0.dist(point(1)) < 1e-9 && o.1 == "100 deg" && o.2.is_empty()), "{own:?}");
|
||||
assert!(own.iter().any(|o| o.0.dist(point(3)) < 1e-9 && o.1 == "90 deg" && o.2 == "15 mm"), "{own:?}");
|
||||
let volume = |app: &mut SolveApp| app.session.execute("inspect.measure", &json!({})).unwrap()["total"]["volume_mm3"].as_f64().unwrap();
|
||||
let before = volume(&mut app);
|
||||
let c = apply_commands(&app, &e).unwrap();
|
||||
assert_eq!(c[0].0, "timeline.redefine");
|
||||
assert_eq!(c[0].1["params"]["edges"][1], json!({"point": pt(point(1)), "angle": "100 deg"}));
|
||||
assert_eq!(c[0].1["params"]["edges"][3]["height"], "15 mm");
|
||||
run_all(&mut app, c);
|
||||
let after = volume(&mut app);
|
||||
assert!((after - before).abs() < 1e-6 * before, "{after} vs {before}");
|
||||
// Taking edges out of the selection leaves each value with its edge.
|
||||
e.inputs[0].items.remove(2);
|
||||
e.inputs[0].items.remove(0);
|
||||
let c = apply_commands(&app, &e).unwrap();
|
||||
assert_eq!(
|
||||
c[0].1["params"]["edges"],
|
||||
json!([{"point": pt(point(1)), "angle": "100 deg"}, {"point": pt(point(3)), "angle": "90 deg", "height": "15 mm"}])
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user