Rectangular and circular pattern, loft and sweep dialogs with live previews (pattern spacing arrow, loft sections by sketch in pick order, sweep path from sketch curves)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
6e20038906
commit
61ed21847a
3 files changed
+185
-31
No files matched your search
@@ -37,6 +37,7 @@ pub fn anchor(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Option<Vec3>)> {
|
||||
// Shell thickness grows into the body.
|
||||
Kind::Shell { .. } => (at, normal.map(|n| -n)),
|
||||
Kind::Move { .. } => (at, Some(Vec3::Z)),
|
||||
Kind::PatternRect { .. } => (at, d.inputs.get(1).and_then(|i| i.items.first()).and_then(|x| crate::dialogs::axis_of(app, x)).map(|a| a.1)),
|
||||
_ => (at, None),
|
||||
})
|
||||
}
|
||||
@@ -182,26 +183,7 @@ fn revolve_axis(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Vec3)> {
|
||||
if !matches!(d.kind, Kind::Revolve { .. }) {
|
||||
return None;
|
||||
}
|
||||
match d.inputs.get(1)?.items.first()? {
|
||||
Sel::Axis { name } => Some((
|
||||
Vec3::ZERO,
|
||||
match name.as_str() {
|
||||
"X" => Vec3::X,
|
||||
"Y" => Vec3::Y,
|
||||
_ => Vec3::Z,
|
||||
},
|
||||
)),
|
||||
Sel::SketchCurve { id } => {
|
||||
let st = app.session.model.state();
|
||||
let sketch = d.inputs.first()?.items.iter().find_map(|x| if let Sel::Profile { sketch, .. } = x { Some(*sketch) } else { None })?;
|
||||
let ss = st.sketch(sketch)?;
|
||||
let c = ss.sketch.curves.get(ss.sketch.curve_index(id)?)?;
|
||||
let solvecraft_engine::sketch::CurveKind::Line { a, b } = c.kind else { return None };
|
||||
let (pa, pb) = (ss.plane.to_world(ss.sketch.point(a)?), ss.plane.to_world(ss.sketch.point(b)?));
|
||||
Some((pa, (pb - pa).normalized()?))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
crate::dialogs::axis_of(app, d.inputs.get(1)?.items.first()?)
|
||||
}
|
||||
|
||||
/// The revolve angle rotator: an arc from the profile around the axis with a handle at its end
|
||||
|
||||
+180
-11
@@ -13,7 +13,7 @@ use solvecraft_engine::doc::{AxisRef, Direction, FeatureKind, HoleKind, Operatio
|
||||
use solvecraft_engine::geom::Vec3;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::selection::{self, AXES, Accept, BODIES, EDGES, FACES, PLANAR_FACES, PLANES, PROFILES, SelInput};
|
||||
use crate::selection::{self, AXES, Accept, BODIES, CURVES, EDGES, FACES, PLANAR_FACES, PLANES, PROFILES, SelInput};
|
||||
use crate::theme::Tokens;
|
||||
use crate::viewport::Hit;
|
||||
|
||||
@@ -52,6 +52,26 @@ pub enum Kind {
|
||||
angle: String,
|
||||
},
|
||||
Mirror,
|
||||
/// Rectangular pattern of bodies: along one direction, optionally a second.
|
||||
PatternRect {
|
||||
count: String,
|
||||
spacing: String,
|
||||
count2: String,
|
||||
spacing2: String,
|
||||
},
|
||||
/// Circular pattern of bodies around an axis.
|
||||
PatternCirc {
|
||||
count: String,
|
||||
angle: String,
|
||||
},
|
||||
/// Loft through profiles of different sketches, in pick order.
|
||||
Loft {
|
||||
operation: usize,
|
||||
},
|
||||
/// Sweep a profile along a path of sketch curves.
|
||||
Sweep {
|
||||
operation: usize,
|
||||
},
|
||||
/// Move bodies by a distance along X, Y and Z.
|
||||
Move {
|
||||
x: String,
|
||||
@@ -119,7 +139,8 @@ fn fits(a: Accept, s: &Sel) -> bool {
|
||||
Sel::Face { .. } => a & (FACES | PLANAR_FACES) != 0,
|
||||
Sel::Body { .. } => a & BODIES != 0,
|
||||
Sel::Plane { .. } => a & PLANES != 0,
|
||||
Sel::Axis { .. } | Sel::SketchCurve { .. } => a & AXES != 0,
|
||||
Sel::Axis { .. } => a & AXES != 0,
|
||||
Sel::SketchCurve { .. } => a & (AXES | CURVES) != 0,
|
||||
Sel::Vertex { .. } => a & selection::VERTICES != 0,
|
||||
Sel::Feature { .. } | Sel::SketchPoint { .. } => false,
|
||||
}
|
||||
@@ -157,6 +178,19 @@ impl Dialog {
|
||||
"MirrorCommand" => {
|
||||
Dialog::new(Kind::Mirror, vec![SelInput::new("Bodies", BODIES, true), SelInput::new("Mirror plane", PLANES | PLANAR_FACES, false)])
|
||||
}
|
||||
"PatternRectangular" => Dialog::new(
|
||||
Kind::PatternRect { count: "3".into(), spacing: "20 mm".into(), count2: "1".into(), spacing2: "20 mm".into() },
|
||||
vec![SelInput::new("Objects", BODIES, true), SelInput::new("Direction", AXES, false), SelInput::new("Direction 2", AXES, false)],
|
||||
),
|
||||
"PatternCircular" => Dialog::new(
|
||||
Kind::PatternCirc { count: "6".into(), angle: "360 deg".into() },
|
||||
vec![SelInput::new("Objects", BODIES, true), SelInput::new("Axis", AXES, false)],
|
||||
),
|
||||
"SolidLoft" => Dialog::new(Kind::Loft { operation: usize::from(has_bodies) }, vec![SelInput::new("Profiles", PROFILES, true)]),
|
||||
"Sweep" => Dialog::new(
|
||||
Kind::Sweep { operation: usize::from(has_bodies) },
|
||||
vec![SelInput::new("Profile", PROFILES, true), SelInput::new("Path", CURVES, true)],
|
||||
),
|
||||
"FusionMoveCommand" => {
|
||||
Dialog::new(Kind::Move { x: "0 mm".into(), y: "0 mm".into(), z: "10 mm".into() }, vec![SelInput::new("Bodies", BODIES, true)])
|
||||
}
|
||||
@@ -248,6 +282,10 @@ impl Dialog {
|
||||
| Kind::Shell { .. }
|
||||
| Kind::Draft { .. }
|
||||
| Kind::Mirror
|
||||
| Kind::PatternRect { .. }
|
||||
| Kind::PatternCirc { .. }
|
||||
| Kind::Loft { .. }
|
||||
| Kind::Sweep { .. }
|
||||
| Kind::Move { .. }
|
||||
| Kind::Hole { .. }
|
||||
| Kind::Primitive { .. }
|
||||
@@ -265,6 +303,8 @@ impl Dialog {
|
||||
Kind::Draft { angle } => ("Angle", ValueKind::Angle, angle),
|
||||
Kind::Hole { diameter, .. } => ("Diameter", ValueKind::Length, diameter),
|
||||
Kind::Move { z, .. } => ("Z", ValueKind::Length, z),
|
||||
Kind::PatternRect { spacing, .. } => ("Spacing", ValueKind::Length, spacing),
|
||||
Kind::PatternCirc { angle, .. } => ("Angle", ValueKind::Angle, angle),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -312,6 +352,7 @@ impl Dialog {
|
||||
}
|
||||
// Extrude and revolve take profiles of one sketch.
|
||||
if let Sel::Profile { sketch, .. } = &sel
|
||||
&& !matches!(self.kind, Kind::Loft { .. })
|
||||
&& let Some(inp) = self.inputs.get_mut(self.active)
|
||||
{
|
||||
inp.items.retain(|x| !matches!(x, Sel::Profile { sketch: s2, .. } if s2 != sketch));
|
||||
@@ -411,6 +452,10 @@ fn title(k: &Kind) -> &'static str {
|
||||
Kind::Shell { .. } => "SHELL",
|
||||
Kind::Draft { .. } => "DRAFT",
|
||||
Kind::Mirror => "MIRROR",
|
||||
Kind::PatternRect { .. } => "RECTANGULAR PATTERN",
|
||||
Kind::PatternCirc { .. } => "CIRCULAR PATTERN",
|
||||
Kind::Loft { .. } => "LOFT",
|
||||
Kind::Sweep { .. } => "SWEEP",
|
||||
Kind::Move { .. } => "MOVE",
|
||||
Kind::Hole { .. } => "HOLE",
|
||||
Kind::Primitive { cmd, .. } => match *cmd {
|
||||
@@ -437,6 +482,8 @@ fn hint(inp: &SelInput) -> &'static str {
|
||||
"click a plane or planar face"
|
||||
} else if a & AXES != 0 {
|
||||
"click an axis or sketch line"
|
||||
} else if a & CURVES != 0 {
|
||||
"click sketch curves"
|
||||
} else if a & BODIES != 0 {
|
||||
"click bodies"
|
||||
} else {
|
||||
@@ -590,6 +637,30 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
ui.end_row();
|
||||
}
|
||||
Kind::Mirror => {}
|
||||
Kind::PatternRect { count, spacing, count2, spacing2 } => {
|
||||
for (l, v) in [("Quantity", count), ("Spacing", spacing), ("Quantity 2", count2), ("Spacing 2", spacing2)] {
|
||||
ui.label(l);
|
||||
enter |= field(ui, v);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
Kind::PatternCirc { count, angle } => {
|
||||
for (l, v) in [("Quantity", count), ("Total angle", angle)] {
|
||||
ui.label(l);
|
||||
enter |= field(ui, v);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
Kind::Loft { operation } => {
|
||||
ui.label("Operation");
|
||||
combo(ui, "lf_op", &OP_LABELS, operation);
|
||||
ui.end_row();
|
||||
}
|
||||
Kind::Sweep { operation } => {
|
||||
ui.label("Operation");
|
||||
combo(ui, "sw_op", &OP_LABELS, operation);
|
||||
ui.end_row();
|
||||
}
|
||||
Kind::Move { x, y, z } => {
|
||||
for (l, v) in [("X distance", x), ("Y distance", y), ("Z distance", z)] {
|
||||
ui.label(l);
|
||||
@@ -954,18 +1025,64 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
Kind::Mirror => {
|
||||
need(0, "bodies")?;
|
||||
need(1, "the mirror plane")?;
|
||||
let st = s.model.state();
|
||||
let mut features: Vec<String> = Vec::new();
|
||||
for n in body_names(0) {
|
||||
if let Some(f) = st.body(&n).and_then(|b| s.doc.feature(b.feature)).map(|f| f.name.clone())
|
||||
&& !features.contains(&f)
|
||||
{
|
||||
features.push(f);
|
||||
}
|
||||
}
|
||||
let features = source_features(s, &body_names(0));
|
||||
let plane = plane_value(s, sels(d, 1).first()).ok_or("the mirror plane must be a plane or a planar face")?;
|
||||
("MirrorCommand", json!({"features": features, "plane": plane}))
|
||||
}
|
||||
Kind::PatternRect { count, spacing, count2, spacing2 } => {
|
||||
need(0, "objects")?;
|
||||
need(1, "a direction (an axis or a sketch line)")?;
|
||||
let (_, d1) = sels(d, 1).first().and_then(|x| axis_of(app, x)).ok_or("the direction must be an axis or a sketch line")?;
|
||||
let mut p = json!({"features": source_features(s, &body_names(0)), "dir1": pt(d1), "count1": count, "spacing1": spacing});
|
||||
if let Some((_, d2)) = sels(d, 2).first().and_then(|x| axis_of(app, x)) {
|
||||
p["dir2"] = pt(d2);
|
||||
p["count2"] = json!(count2);
|
||||
p["spacing2"] = json!(spacing2);
|
||||
}
|
||||
("PatternRectangular", p)
|
||||
}
|
||||
Kind::PatternCirc { count, angle } => {
|
||||
need(0, "objects")?;
|
||||
need(1, "an axis")?;
|
||||
let axis = match sels(d, 1).first() {
|
||||
Some(Sel::Axis { name }) => json!(name),
|
||||
Some(x) => {
|
||||
let (o, dir) = axis_of(app, x).ok_or("the axis must be an origin axis or a sketch line")?;
|
||||
json!({"origin": pt(o), "dir": pt(dir)})
|
||||
}
|
||||
None => return Err("select an axis".into()),
|
||||
};
|
||||
("PatternCircular", json!({"features": source_features(s, &body_names(0)), "axis": axis, "count": count, "angle": angle}))
|
||||
}
|
||||
Kind::Loft { operation } => {
|
||||
need(0, "profiles of two or more sketches")?;
|
||||
// One section per sketch, in the order the sketches were first picked.
|
||||
let mut sections: Vec<(u64, Vec<usize>)> = Vec::new();
|
||||
for x in sels(d, 0) {
|
||||
if let Sel::Profile { sketch, index } = x {
|
||||
match sections.iter_mut().find(|(sk, _)| sk == sketch) {
|
||||
Some((_, v)) => v.push(*index),
|
||||
None => sections.push((*sketch, vec![*index])),
|
||||
}
|
||||
}
|
||||
}
|
||||
if sections.len() < 2 {
|
||||
return Err("pick profiles in two or more sketches".into());
|
||||
}
|
||||
let sections: Vec<Value> = sections.into_iter().map(|(sk, v)| json!({"sketch": sk, "profiles": v})).collect();
|
||||
("SolidLoft", json!({"sections": sections, "operation": OPS.get(*operation).copied().unwrap_or("new")}))
|
||||
}
|
||||
Kind::Sweep { operation } => {
|
||||
need(0, "a profile")?;
|
||||
need(1, "the path")?;
|
||||
let (sketch, idx) = profiles();
|
||||
let path: Vec<String> = sels(d, 1).iter().filter_map(|x| if let Sel::SketchCurve { id } = x { Some(id.clone()) } else { None }).collect();
|
||||
let path_sketch = curves_sketch(app, &path, sketch.as_u64()).ok_or("the path curves must be in one sketch")?;
|
||||
(
|
||||
"Sweep",
|
||||
json!({"sketch": sketch, "profiles": idx, "path_sketch": path_sketch, "path": path, "operation": OPS.get(*operation).copied().unwrap_or("new")}),
|
||||
)
|
||||
}
|
||||
Kind::Move { x, y, z } => {
|
||||
need(0, "bodies")?;
|
||||
("FusionMoveCommand", json!({"bodies": body_names(0), "translate": [x, y, z]}))
|
||||
@@ -1027,6 +1144,58 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
Ok(vec![(cmd.to_string(), params)])
|
||||
}
|
||||
|
||||
/// The features that made these bodies (what patterns and mirrors copy).
|
||||
fn source_features(s: &Session, bodies: &[String]) -> Vec<String> {
|
||||
let st = s.model.state();
|
||||
let mut features: Vec<String> = Vec::new();
|
||||
for n in bodies {
|
||||
if let Some(f) = st.body(n).and_then(|b| s.doc.feature(b.feature)).map(|f| f.name.clone())
|
||||
&& !features.contains(&f)
|
||||
{
|
||||
features.push(f);
|
||||
}
|
||||
}
|
||||
features
|
||||
}
|
||||
|
||||
/// The sketch holding all these curve ids (the active sketch first, never `not`).
|
||||
fn curves_sketch(app: &SolveApp, ids: &[String], not: Option<u64>) -> Option<u64> {
|
||||
let st = app.session.model.state();
|
||||
let has = |sid: u64| st.sketch(sid).is_some_and(|ss| ids.iter().all(|id| ss.sketch.curve_index(id).is_some()));
|
||||
if let Some(a) = app.session.active_sketch
|
||||
&& Some(a) != not
|
||||
&& has(a)
|
||||
{
|
||||
return Some(a);
|
||||
}
|
||||
st.sketches.iter().rev().map(|ss| ss.feature).find(|sid| Some(*sid) != not && has(*sid))
|
||||
}
|
||||
|
||||
/// An axis selection as a line: a point on it and its unit direction (origin axes, or a straight
|
||||
/// sketch line).
|
||||
pub fn axis_of(app: &SolveApp, sel: &Sel) -> Option<(Vec3, Vec3)> {
|
||||
match sel {
|
||||
Sel::Axis { name } => Some((
|
||||
Vec3::ZERO,
|
||||
match name.as_str() {
|
||||
"X" => Vec3::X,
|
||||
"Y" => Vec3::Y,
|
||||
_ => Vec3::Z,
|
||||
},
|
||||
)),
|
||||
Sel::SketchCurve { id } => {
|
||||
let st = app.session.model.state();
|
||||
let sid = curves_sketch(app, std::slice::from_ref(id), None)?;
|
||||
let ss = st.sketch(sid)?;
|
||||
let c = ss.sketch.curves.get(ss.sketch.curve_index(id)?)?;
|
||||
let solvecraft_engine::sketch::CurveKind::Line { a, b } = c.kind else { return None };
|
||||
let (pa, pb) = (ss.plane.to_world(ss.sketch.point(a)?), ss.plane.to_world(ss.sketch.point(b)?));
|
||||
Some((pa, (pb - pa).normalized()?))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Which profiles of a sketch a profile selection means.
|
||||
fn profile_indices(ss: &solvecraft_engine::doc::SolvedSketch, sel: &ProfileSel) -> Vec<usize> {
|
||||
let ps = &ss.profiles;
|
||||
|
||||
@@ -21,6 +21,8 @@ pub const PLANES: Accept = 32;
|
||||
/// Origin axes and straight sketch curves.
|
||||
pub const AXES: Accept = 64;
|
||||
pub const VERTICES: Accept = 128;
|
||||
/// Any sketch curve (a sweep path).
|
||||
pub const CURVES: Accept = 256;
|
||||
|
||||
/// One selection input of a command dialog ("Profiles", "Edges", "Plane", ...).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -50,6 +52,7 @@ impl SelInput {
|
||||
Hit::Plane { name, .. } if a & PLANES != 0 => Some(Sel::Plane { name: name.clone() }),
|
||||
Hit::Axis { name } if a & AXES != 0 => Some(Sel::Axis { name: name.clone() }),
|
||||
Hit::SketchCurve { id, straight: true, .. } if a & AXES != 0 => Some(Sel::SketchCurve { id: id.clone() }),
|
||||
Hit::SketchCurve { id, .. } if a & CURVES != 0 => Some(Sel::SketchCurve { id: id.clone() }),
|
||||
Hit::Vertex { body, point } if a & VERTICES != 0 => Some(Sel::Vertex { body: body.clone(), point: *point }),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user