Patterns of features: Object Type (Bodies / Features) in the rectangular, circular and path pattern dialogs; features are picked in the timeline (or pre-selected there), so a hole can be patterned with a live preview; Edit Feature shows a pattern of holes as features
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
f4c39320ec
commit
bee763959c
4 files changed
+126
-9
No files matched your search
@@ -257,7 +257,8 @@ fn fits(a: Accept, s: &Sel) -> bool {
|
|||||||
Sel::Axis { .. } => a & AXES != 0,
|
Sel::Axis { .. } => a & AXES != 0,
|
||||||
Sel::SketchCurve { .. } => a & (AXES | CURVES) != 0,
|
Sel::SketchCurve { .. } => a & (AXES | CURVES) != 0,
|
||||||
Sel::Vertex { .. } => a & selection::VERTICES != 0,
|
Sel::Vertex { .. } => a & selection::VERTICES != 0,
|
||||||
Sel::Feature { .. } | Sel::SketchPoint { .. } | Sel::SketchConstraint { .. } => false,
|
Sel::Feature { .. } => a & selection::FEATURES != 0,
|
||||||
|
Sel::SketchPoint { .. } | Sel::SketchConstraint { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,6 +409,14 @@ impl Dialog {
|
|||||||
Dialog::new(kind, inputs)
|
Dialog::new(kind, inputs)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Patterns of features picked in the timeline: the objects are features.
|
||||||
|
if is_pattern(&d.kind)
|
||||||
|
&& s.selection.iter().any(|x| matches!(x, Sel::Feature { .. }))
|
||||||
|
&& let Some(inp) = d.inputs.first_mut()
|
||||||
|
{
|
||||||
|
inp.accept = selection::FEATURES;
|
||||||
|
inp.label = objects_label(inp.accept);
|
||||||
|
}
|
||||||
// Pre-selection: what is selected now becomes the input (first input that takes it); a
|
// Pre-selection: what is selected now becomes the input (first input that takes it); a
|
||||||
// face or edge stands for its body where bodies are wanted.
|
// face or edge stands for its body where bodies are wanted.
|
||||||
for sel in &s.selection {
|
for sel in &s.selection {
|
||||||
@@ -780,8 +789,34 @@ pub(crate) fn row_label(ui: &mut egui::Ui, text: &str) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rows above the selection inputs: the pattern type, the Hole placement (one face point, or
|
/// The pattern objects' label: features are picked in the timeline.
|
||||||
/// sketch points).
|
fn objects_label(accept: Accept) -> &'static str {
|
||||||
|
if accept == selection::FEATURES { "Features" } else { "Objects" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_pattern(k: &Kind) -> bool {
|
||||||
|
matches!(k, Kind::PatternRect { .. } | Kind::PatternCirc { .. } | Kind::PathPattern { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Object Type row of a pattern: bodies (picked in the view) or features (picked in the
|
||||||
|
/// timeline, e.g. a hole).
|
||||||
|
fn object_type_row(d: &mut Dialog, ui: &mut egui::Ui) {
|
||||||
|
let Some(inp) = d.inputs.first_mut() else { return };
|
||||||
|
row_label(ui, "Object Type");
|
||||||
|
let cur = usize::from(inp.accept == selection::FEATURES);
|
||||||
|
let mut ty = cur;
|
||||||
|
combo(ui, "pat_obj", &["Bodies", "Features"], &mut ty);
|
||||||
|
ui.end_row();
|
||||||
|
if ty != cur {
|
||||||
|
inp.accept = if ty == 1 { selection::FEATURES } else { BODIES };
|
||||||
|
inp.label = objects_label(inp.accept);
|
||||||
|
inp.items.clear();
|
||||||
|
d.active = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rows above the selection inputs: the pattern type and object type, the Hole placement (one
|
||||||
|
/// face point, or sketch points).
|
||||||
fn placement_row(d: &mut Dialog, ui: &mut egui::Ui) {
|
fn placement_row(d: &mut Dialog, ui: &mut egui::Ui) {
|
||||||
let current = match d.kind {
|
let current = match d.kind {
|
||||||
Kind::PatternRect { .. } => Some(0),
|
Kind::PatternRect { .. } => Some(0),
|
||||||
@@ -811,14 +846,18 @@ fn placement_row(d: &mut Dialog, ui: &mut egui::Ui) {
|
|||||||
vec![SelInput::new("Objects", BODIES, true), SelInput::new("Path", CURVES, true)],
|
vec![SelInput::new("Objects", BODIES, true), SelInput::new("Path", CURVES, true)],
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
let accept = d.inputs.first().map(|i| i.accept).unwrap_or(BODIES);
|
||||||
if let Some(i) = inputs.first_mut() {
|
if let Some(i) = inputs.first_mut() {
|
||||||
i.items = objects;
|
i.items = objects;
|
||||||
|
i.accept = accept;
|
||||||
|
i.label = objects_label(accept);
|
||||||
}
|
}
|
||||||
d.kind = kind;
|
d.kind = kind;
|
||||||
d.inputs = inputs;
|
d.inputs = inputs;
|
||||||
d.extra.clear();
|
d.extra.clear();
|
||||||
d.advance();
|
d.advance();
|
||||||
}
|
}
|
||||||
|
object_type_row(d, ui);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Kind::Hole { opts, .. } = &mut d.kind else { return };
|
let Kind::Hole { opts, .. } = &mut d.kind else { return };
|
||||||
@@ -1699,7 +1738,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
|||||||
(None, Some(k)) => k,
|
(None, Some(k)) => k,
|
||||||
(None, None) => return Err("select a direction (an axis or a sketch line) first".into()),
|
(None, None) => return Err("select a direction (an axis or a sketch line) first".into()),
|
||||||
};
|
};
|
||||||
let mut p = json!({"features": source_features(s, &body_names(0)), "dir1": pt(d1), "count1": count, "spacing1": spacing});
|
let mut p = json!({"features": pattern_features(s, sels(d, 0)), "dir1": pt(d1), "count1": count, "spacing1": spacing});
|
||||||
if let Some((_, d2)) = sels(d, 2).first().and_then(|x| axis_of(app, x)) {
|
if let Some((_, d2)) = sels(d, 2).first().and_then(|x| axis_of(app, x)) {
|
||||||
p["dir2"] = pt(d2);
|
p["dir2"] = pt(d2);
|
||||||
p["count2"] = json!(count2);
|
p["count2"] = json!(count2);
|
||||||
@@ -1718,7 +1757,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
|||||||
}
|
}
|
||||||
None => return Err("select an axis".into()),
|
None => return Err("select an axis".into()),
|
||||||
};
|
};
|
||||||
("PatternCircular", json!({"features": source_features(s, &body_names(0)), "axis": axis, "count": count, "angle": angle}))
|
("PatternCircular", json!({"features": pattern_features(s, sels(d, 0)), "axis": axis, "count": count, "angle": angle}))
|
||||||
}
|
}
|
||||||
Kind::Loft { operation } => {
|
Kind::Loft { operation } => {
|
||||||
need(0, "profiles of two or more sketches")?;
|
need(0, "profiles of two or more sketches")?;
|
||||||
@@ -1822,7 +1861,13 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
|||||||
need(1, "the path")?;
|
need(1, "the path")?;
|
||||||
let path = curve_ids(d, 1);
|
let path = curve_ids(d, 1);
|
||||||
let sketch = curves_sketch(app, &path, None).ok_or("the path curves must be in one sketch")?;
|
let sketch = curves_sketch(app, &path, None).ok_or("the path curves must be in one sketch")?;
|
||||||
("PatternOnPath", json!({"bodies": body_names(0), "path_sketch": sketch, "path": path, "count": count, "spacing": spacing}))
|
let mut p = json!({"path_sketch": sketch, "path": path, "count": count, "spacing": spacing});
|
||||||
|
if sels(d, 0).iter().any(|x| matches!(x, Sel::Feature { .. })) {
|
||||||
|
p["features"] = json!(pattern_features(s, sels(d, 0)));
|
||||||
|
} else {
|
||||||
|
p["bodies"] = json!(body_names(0));
|
||||||
|
}
|
||||||
|
("PatternOnPath", p)
|
||||||
}
|
}
|
||||||
Kind::Pipe { diameter, wall } => {
|
Kind::Pipe { diameter, wall } => {
|
||||||
need(0, "the path")?;
|
need(0, "the path")?;
|
||||||
@@ -1999,6 +2044,27 @@ fn curve_ids(d: &Dialog, i: usize) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The features that made these bodies (what patterns and mirrors copy).
|
/// The features that made these bodies (what patterns and mirrors copy).
|
||||||
|
/// The features a pattern repeats: picked features as they are, picked bodies by the features
|
||||||
|
/// that made them.
|
||||||
|
fn pattern_features(s: &Session, objects: &[Sel]) -> Vec<String> {
|
||||||
|
let mut out: Vec<String> = Vec::new();
|
||||||
|
let bodies: Vec<String> = objects.iter().filter_map(|x| if let Sel::Body { name } = x { Some(name.clone()) } else { None }).collect();
|
||||||
|
for x in objects {
|
||||||
|
if let Sel::Feature { id } = x
|
||||||
|
&& let Some(f) = s.doc.feature(*id)
|
||||||
|
&& !out.contains(&f.name)
|
||||||
|
{
|
||||||
|
out.push(f.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for f in source_features(s, &bodies) {
|
||||||
|
if !out.contains(&f) {
|
||||||
|
out.push(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
fn source_features(s: &Session, bodies: &[String]) -> Vec<String> {
|
fn source_features(s: &Session, bodies: &[String]) -> Vec<String> {
|
||||||
let st = s.model.state();
|
let st = s.model.state();
|
||||||
let mut features: Vec<String> = Vec::new();
|
let mut features: Vec<String> = Vec::new();
|
||||||
@@ -2381,8 +2447,16 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
|||||||
// No dialog for a path pattern yet: edit it with timeline.redefine.
|
// No dialog for a path pattern yet: edit it with timeline.redefine.
|
||||||
PatternKind::Path { .. } => return None,
|
PatternKind::Path { .. } => return None,
|
||||||
};
|
};
|
||||||
|
// Features that made no body of their own (a hole, a fillet) were picked as features.
|
||||||
|
let as_features = ids.iter().any(|id| !st.bodies.iter().any(|b| b.feature == *id));
|
||||||
if let Some(inp) = d.inputs.first_mut() {
|
if let Some(inp) = d.inputs.first_mut() {
|
||||||
inp.items = bodies;
|
if as_features {
|
||||||
|
inp.accept = selection::FEATURES;
|
||||||
|
inp.label = objects_label(inp.accept);
|
||||||
|
inp.items = ids.iter().map(|id| Sel::Feature { id: *id }).collect();
|
||||||
|
} else {
|
||||||
|
inp.items = bodies;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
d
|
d
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,3 +131,34 @@ fn hole_at_sketch_points() {
|
|||||||
let r = app.session.execute(&cmds[0].0, &cmds[0].1);
|
let r = app.session.execute(&cmds[0].0, &cmds[0].1);
|
||||||
assert!(r.is_ok(), "{r:?}");
|
assert!(r.is_ok(), "{r:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_hole_is_patterned_as_a_feature() {
|
||||||
|
let mut app = app_with(json!([
|
||||||
|
{"command": "PrimitiveBox", "params": {"length": 60, "width": 40, "height": 10}},
|
||||||
|
{"command": "FusionHoleCommand", "params": {"position": [10, 10, 10], "diameter": 5}}
|
||||||
|
]));
|
||||||
|
let hole = app.session.doc.features.last().unwrap().id;
|
||||||
|
// Picked in the timeline before the command: the objects become features.
|
||||||
|
let mut d = start(&mut app, vec![Sel::Feature { id: hole }], "PatternRectangular");
|
||||||
|
assert_eq!(d.inputs[0].accept, crate::selection::FEATURES);
|
||||||
|
assert_eq!(d.inputs[0].items, vec![Sel::Feature { id: hole }]);
|
||||||
|
d.inputs[1].items = vec![Sel::Axis { name: "X".into() }];
|
||||||
|
if let Kind::PatternRect { count, spacing, .. } = &mut d.kind {
|
||||||
|
*count = "4".into();
|
||||||
|
*spacing = "12 mm".into();
|
||||||
|
}
|
||||||
|
let cmds = apply_commands(&app, &d).unwrap();
|
||||||
|
assert_eq!(cmds[0].1["features"], json!(["Hole1"]));
|
||||||
|
let before = app.session.model.state().bodies[0].mesh().measure().volume;
|
||||||
|
for (id, p) in cmds {
|
||||||
|
app.session.execute(&id, &p).unwrap();
|
||||||
|
}
|
||||||
|
let after = app.session.model.state().bodies[0].mesh().measure().volume;
|
||||||
|
assert!(after < before - 500.0, "three more holes: {before} -> {after}");
|
||||||
|
// Edit Feature shows the hole as a feature again.
|
||||||
|
let pat = app.session.doc.features.last().unwrap().id;
|
||||||
|
let e = crate::dialogs::for_feature(&app, pat, None).unwrap();
|
||||||
|
assert_eq!(e.inputs[0].accept, crate::selection::FEATURES);
|
||||||
|
assert_eq!(e.inputs[0].items, vec![Sel::Feature { id: hole }]);
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@ pub const VERTICES: Accept = 128;
|
|||||||
pub const CURVES: Accept = 256;
|
pub const CURVES: Accept = 256;
|
||||||
/// Sketch points of any visible sketch (hole centres); picked as "<sketch id>:<point id>".
|
/// Sketch points of any visible sketch (hole centres); picked as "<sketch id>:<point id>".
|
||||||
pub const POINTS: Accept = 512;
|
pub const POINTS: Accept = 512;
|
||||||
|
/// Timeline features (pattern objects), picked in the timeline.
|
||||||
|
pub const FEATURES: Accept = 1024;
|
||||||
|
|
||||||
/// One selection input of a command dialog ("Profiles", "Edges", "Plane", ...).
|
/// One selection input of a command dialog ("Profiles", "Edges", "Plane", ...).
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
let err = res.and_then(|r| r.error.clone());
|
let err = res.and_then(|r| r.error.clone());
|
||||||
let warn = res.and_then(|r| r.warning.clone());
|
let warn = res.and_then(|r| r.warning.clone());
|
||||||
let rolled = i >= marker;
|
let rolled = i >= marker;
|
||||||
let sel = app.session.selection.iter().any(|s| matches!(s, solvecraft_engine::Sel::Feature { id: f } if f == id));
|
let is_it = |s: &solvecraft_engine::Sel| matches!(s, solvecraft_engine::Sel::Feature { id: f } if f == id);
|
||||||
|
let sel = app.session.selection.iter().any(is_it) || app.dialog.as_ref().is_some_and(|d| d.items().iter().any(is_it));
|
||||||
if resp.hovered() {
|
if resp.hovered() {
|
||||||
hovered = Some(*id);
|
hovered = Some(*id);
|
||||||
}
|
}
|
||||||
@@ -183,7 +184,16 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
}
|
}
|
||||||
let resp = resp.on_hover_text(tip);
|
let resp = resp.on_hover_text(tip);
|
||||||
if resp.clicked() {
|
if resp.clicked() {
|
||||||
let _ = app.run("select.set", json!({"items": [{"type": "feature", "id": id}]}));
|
// A dialog input that takes features (pattern objects) gets the click.
|
||||||
|
let takes = app.dialog.as_ref().and_then(|d| d.active_input()).is_some_and(|i| i.accept & crate::selection::FEATURES != 0);
|
||||||
|
if takes {
|
||||||
|
if let Some(mut d) = app.dialog.take() {
|
||||||
|
d.pick(&app.session, solvecraft_engine::Sel::Feature { id: *id });
|
||||||
|
app.dialog = Some(d);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = app.run("select.set", json!({"items": [{"type": "feature", "id": id}]}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if resp.double_clicked() {
|
if resp.double_clicked() {
|
||||||
edit = Some(*id);
|
edit = Some(*id);
|
||||||
|
|||||||
Reference in new issue
Block a user