Feature mirrors and patterns: preserve replay and add Flip/Center placement (#1619)
Thanks @Karl-VonRichter.
This commit is contained in:
1 parent
f57a62deb7
commit
9422853cf9
11 files changed
+456
-41
No files matched your search
@@ -41,11 +41,11 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("solid.pattern.rectangular", "Rectangular Pattern", pattern_rect)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("pattern_rect")
|
||||
.params("features: [names] | bodies: [names] | components: [names]; dir1: [x,y,z]; count1; spacing1; dir2?, count2?, spacing2?"),
|
||||
.params("features: [names] | bodies: [names] | components: [names]; dir1: [x,y,z]; count1; spacing1; dir2?, count2?, spacing2?; alignment?, alignment2?: left|center|right (default right; counts include the original); reversed?, reversed2?: bool (Flip)"),
|
||||
CommandSpec::new("solid.pattern.circular", "Circular Pattern", pattern_circ)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("pattern_circ")
|
||||
.params("features: [names] | bodies: [names] | components: [names]; axis: X|Y|Z | {origin, dir}; count; angle? (default 360 deg)"),
|
||||
.params("features: [names] | bodies: [names] | components: [names]; axis: X|Y|Z | {origin, dir}; count; angle? (default 360 deg; partial arcs include both ends); alignment?: left|center|right (default right; original stays fixed); reversed?: bool (Flip; selects the extra side for even centered counts)"),
|
||||
CommandSpec::new("solid.pattern.path", "Pattern on Path", pattern_path)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("pattern_rect")
|
||||
@@ -1263,7 +1263,18 @@ fn pattern_rect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(c) = &spacing2 {
|
||||
check_expr(s, c, Kind::Length, cmd, "spacing2")?;
|
||||
}
|
||||
let pattern = solidcraft_doc::PatternKind::Rectangular { dir1: Dir(dir1), count1, spacing1, dir2: dir2.map(Dir), count2, spacing2 };
|
||||
let pattern = solidcraft_doc::PatternKind::Rectangular {
|
||||
dir1: Dir(dir1),
|
||||
count1,
|
||||
spacing1,
|
||||
dir2: dir2.map(Dir),
|
||||
count2,
|
||||
spacing2,
|
||||
alignment: super::alignment_param(p, "alignment", cmd)?,
|
||||
alignment2: super::alignment_param(p, "alignment2", cmd)?,
|
||||
reversed: super::reversed_param(p, "reversed", cmd)?,
|
||||
reversed2: super::reversed_param(p, "reversed2", cmd)?,
|
||||
};
|
||||
finish_pattern(s, p, features, pattern, bodies)
|
||||
}
|
||||
|
||||
@@ -1275,7 +1286,14 @@ fn pattern_circ(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
check_expr(s, &count, Kind::Unitless, cmd, "count")?;
|
||||
let angle = expr(p, "angle").unwrap_or_else(|| "360 deg".into());
|
||||
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
|
||||
let pattern = solidcraft_doc::PatternKind::Circular { origin: Position(origin), axis: Dir(axis), count, angle };
|
||||
let pattern = solidcraft_doc::PatternKind::Circular {
|
||||
origin: Position(origin),
|
||||
axis: Dir(axis),
|
||||
count,
|
||||
angle,
|
||||
alignment: super::alignment_param(p, "alignment", cmd)?,
|
||||
reversed: super::reversed_param(p, "reversed", cmd)?,
|
||||
};
|
||||
finish_pattern(s, p, features, pattern, bodies)
|
||||
}
|
||||
|
||||
|
||||
@@ -318,3 +318,15 @@ fn edit_distance(a: &str, b: &str) -> usize {
|
||||
}
|
||||
row[b.len()]
|
||||
}
|
||||
|
||||
pub(super) fn alignment_param(p: &Value, key: &str, cmd: &str) -> Result<solidcraft_sketch::PatternAlignment> {
|
||||
p.get(key).map_or(Ok(solidcraft_sketch::PatternAlignment::default()), |value| {
|
||||
serde_json::from_value(value.clone()).map_err(|_| crate::params::bad(cmd, format!("`{key}` must be left, center or right")))
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn reversed_param(p: &Value, key: &str, cmd: &str) -> Result<bool> {
|
||||
p.get(key).map_or(Ok(false), |value| {
|
||||
serde_json::from_value(value.clone()).map_err(|_| crate::params::bad(cmd, format!("`{key}` must be true or false")))
|
||||
})
|
||||
}
|
||||
@@ -34,6 +34,97 @@ fn plate_with_hole() -> Session {
|
||||
const PLATE: f64 = 100.0 * 50.0 * 5.0;
|
||||
const HOLE: f64 = PI * 4.0 * 5.0;
|
||||
|
||||
fn feature_pattern_plate() -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length":120,"width":120,"height":5,"corner":[0,0,0],"name":"Plate"}));
|
||||
run(&mut s, "solid.hole", json!({"position":[60,60,5],"diameter":2,"name":"Hole"}));
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_rectangular_center_flip_and_oblique_direction_keep_source_and_recompute() {
|
||||
use solidcraft_geom::Vec3;
|
||||
for flipped in [false, true] {
|
||||
let mut s = feature_pattern_plate();
|
||||
let before = s.doc.to_json();
|
||||
run(
|
||||
&mut s,
|
||||
"solid.pattern.rectangular",
|
||||
json!({
|
||||
"features":["Hole"],"dir1":[1,0,0],"count1":4,"spacing1":15,
|
||||
"dir2":[0.6,0.8,0],"count2":2,"spacing2":20,
|
||||
"alignment":"center","alignment2":"center","reversed":flipped,"reversed2":flipped,"name":"Pattern"
|
||||
}),
|
||||
);
|
||||
let sign = if flipped { -1.0 } else { 1.0 };
|
||||
let st = s.world_state();
|
||||
let body = &st.bodies[0].body;
|
||||
// Analytic slots: the source, +1, -1, +2. The second direction is independent.
|
||||
for i in [0.0, 1.0, -1.0, 2.0] {
|
||||
for j in [0.0, 1.0] {
|
||||
let p = Vec3::new(60.0 + sign * (15.0 * i + 12.0 * j), 60.0 + sign * 16.0 * j, 2.5);
|
||||
assert!(!body.contains(p), "hole at {p:?}");
|
||||
assert!(body.contains(p + Vec3::new(0.0, 2.0, 0.0)));
|
||||
}
|
||||
}
|
||||
assert!(rel(volume(&mut s), 120.0 * 120.0 * 5.0 - 8.0 * PI * 5.0) < 1e-5);
|
||||
let saved = s.doc.to_json();
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(s.doc.to_json(), before);
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
assert_eq!(s.doc.to_json(), saved);
|
||||
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&saved).unwrap());
|
||||
assert!(rel(volume(&mut reopened), volume(&mut s)) < 1e-8);
|
||||
// The copies replay the edited feature rather than freezing its original tool.
|
||||
run(&mut reopened, "timeline.redefine", json!({"feature":"Hole","command":"solid.hole","params":{"position":[60,60,5],"diameter":3}}));
|
||||
assert!(rel(volume(&mut reopened), 120.0 * 120.0 * 5.0 - 8.0 * PI * 2.25 * 5.0) < 1e-5);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_circular_center_flip_uses_partial_arc_endpoints_and_full_circle_spacing() {
|
||||
use solidcraft_geom::Vec3;
|
||||
for total in [90.0_f64, 360.0] {
|
||||
for flipped in [false, true] {
|
||||
let mut s = feature_pattern_plate();
|
||||
run(
|
||||
&mut s,
|
||||
"solid.pattern.circular",
|
||||
json!({
|
||||
"features":["Hole"],"axis":{"origin":[40,60,0],"dir":[0,0,1]},
|
||||
"count":6,"angle":total,"alignment":"center","reversed":flipped
|
||||
}),
|
||||
);
|
||||
let step = total / if total == 360.0 { 6.0 } else { 5.0 };
|
||||
let st = s.world_state();
|
||||
for slot in [0.0, 1.0, -1.0, 2.0, -2.0, 3.0] {
|
||||
let a = (step * slot * if flipped { -1.0 } else { 1.0 }).to_radians();
|
||||
let p = Vec3::new(40.0 + 20.0 * a.cos(), 60.0 + 20.0 * a.sin(), 2.5);
|
||||
assert!(!st.bodies[0].body.contains(p), "hole at {p:?}");
|
||||
assert!(st.bodies[0].body.contains(p + Vec3::new(2.0 * a.cos(), 2.0 * a.sin(), 0.0)));
|
||||
}
|
||||
assert!(rel(volume(&mut s), 120.0 * 120.0 * 5.0 - 6.0 * PI * 5.0) < 1e-5);
|
||||
let saved = s.doc.to_json();
|
||||
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&saved).unwrap());
|
||||
assert!(rel(volume(&mut reopened), volume(&mut s)) < 1e-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_pattern_rejects_malformed_alignment_and_flip_atomically() {
|
||||
let mut s = feature_pattern_plate();
|
||||
let before = s.doc.to_json();
|
||||
let undo = s.undo.len();
|
||||
for (key, value) in [("alignment", json!("diagonal")), ("alignment2", json!(null)), ("reversed", json!(1)), ("reversed2", json!("true"))] {
|
||||
let mut p = json!({"features":["Hole"],"dir1":[1,0,0],"count1":4,"spacing1":10});
|
||||
p[key] = value;
|
||||
assert!(s.execute("solid.pattern.rectangular", &p).is_err());
|
||||
assert_eq!(s.doc.to_json(), before);
|
||||
assert_eq!(s.undo.len(), undo);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circular_axis_origin_rejects_invalid_values_atomically() {
|
||||
let mut s = plate_with_hole();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Associative sketch patterns. Editing retains ids at surviving instance indices.
|
||||
|
||||
use solidcraft_sketch::{PatternAlignment, PatternInstance, SketchPattern, SketchPatternKind};
|
||||
use solidcraft_sketch::{PatternInstance, SketchPattern, SketchPatternKind};
|
||||
|
||||
use super::*;
|
||||
use crate::cmd::{alignment_param, reversed_param};
|
||||
|
||||
pub(super) fn run(s: &mut Session, p: &Value, circular: bool) -> Result<Value> {
|
||||
let cmd = if circular { "sketch.pattern.circular" } else { "sketch.pattern.rectangular" };
|
||||
@@ -257,16 +258,6 @@ fn direction_ref(sk: &Sketch, p: &Value, key: &str, cmd: &str) -> Result<Option<
|
||||
}
|
||||
}
|
||||
|
||||
fn alignment_param(p: &Value, key: &str, cmd: &str) -> Result<PatternAlignment> {
|
||||
p.get(key).map_or(Ok(PatternAlignment::default()), |value| {
|
||||
serde_json::from_value(value.clone()).map_err(|_| bad(cmd, format!("`{key}` must be left, center or right")))
|
||||
})
|
||||
}
|
||||
|
||||
fn reversed_param(p: &Value, key: &str, cmd: &str) -> Result<bool> {
|
||||
p.get(key).map_or(Ok(false), |value| serde_json::from_value(value.clone()).map_err(|_| bad(cmd, format!("`{key}` must be true or false"))))
|
||||
}
|
||||
|
||||
fn line_direction(sk: &Sketch, id: Option<&str>) -> Option<Vec2> {
|
||||
let Shape::Line { a, b } = sk.shape(sk.curve_index(id?)?)? else { return None };
|
||||
(b - a).normalized()
|
||||
|
||||
Reference in new issue
Block a user