Coil (PrimitiveCoil: diameter, two of revolutions/height/pitch, circular or square section inside/on/outside, start angle, handedness) on a new kernel helical sweep; loft to a point (sketch point sections, pyramids); recipes: a sweep along a fitted 3D helix replays as a coil (oracle 49, compared within 0.5% since Fusion's path is a spline fit) and loft point sections (oracle 37)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
4455402eab
commit
9df954cd94
12 files changed
+490
-12
No files matched your search
@@ -22,7 +22,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("sweep")
|
||||
.params("sketch: profile sketch; profiles?; path_sketch: sketch; path: [curve ids in order]; operation?"),
|
||||
CommandSpec::new("SolidLoft", "Loft", loft).at("SOLID", "CREATE").icon("loft").params("sections: [{sketch, profiles?}] (2 or more, in order); operation?"),
|
||||
CommandSpec::new("SolidLoft", "Loft", loft).at("SOLID", "CREATE").icon("loft").params("sections: [{sketch, profiles?} | {sketch, point: id} (an apex)] (2 or more, in order); operation?"),
|
||||
CommandSpec::new("PrimitiveBox", "Box", prim_box).at("SOLID", "CREATE").icon("box").params("length, width, height: expr; corner?: [x,y,z] | center?: [x,y,z]; operation?"),
|
||||
CommandSpec::new("PrimitiveCylinder", "Cylinder", prim_cylinder).at("SOLID", "CREATE").icon("cylinder").params("radius | diameter, height: expr; base?: [x,y,z]; axis?: [x,y,z]; operation?"),
|
||||
CommandSpec::new("PrimitiveSphere", "Sphere", prim_sphere).at("SOLID", "CREATE").icon("sphere").params("radius | diameter: expr; center?: [x,y,z]; operation?"),
|
||||
@@ -845,7 +845,16 @@ fn loft(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut sections = Vec::new();
|
||||
for sec in list {
|
||||
let sketch = sketch_id(s, sec.get("sketch"), cmd, "sections[].sketch")?;
|
||||
sections.push(solvecraft_doc::LoftSection { sketch, profiles: profiles(sec, cmd)? });
|
||||
// A sketch point closes the loft to an apex.
|
||||
if let Some(pt) = str_(sec, "point") {
|
||||
let st = s.model.state();
|
||||
if !st.sketch(sketch).is_some_and(|ss| ss.sketch.points.iter().any(|q| q.id == pt)) {
|
||||
return Err(bad(cmd, format!("no point `{pt}` in the section's sketch")));
|
||||
}
|
||||
sections.push(solvecraft_doc::LoftSection { sketch, profiles: solvecraft_doc::ProfileSel::All, point: Some(pt.to_string()) });
|
||||
continue;
|
||||
}
|
||||
sections.push(solvecraft_doc::LoftSection { sketch, profiles: profiles(sec, cmd)?, point: None });
|
||||
}
|
||||
add_feature(s, p, FeatureKind::Loft { sections, operation: operation(p, cmd)?, targets: string_list(p, "targets") })
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ use crate::params::{bad, bool_, expr, req_expr, str_, string_list, vec3};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("PrimitiveCoil", "Coil", coil).at("SOLID", "CREATE").icon("coil").params(
|
||||
"diameter; two of revolutions (or turns), height, pitch; section_size; section?: circular|square; section_position?: inside|center|outside; \
|
||||
base?: [x,y,z]; axis?: X|Y|Z|[x,y,z] (default Z); start_angle?; clockwise?: bool; operation?, targets?, name?, body_name?",
|
||||
),
|
||||
CommandSpec::new("FusionRibCommand", "Rib", rib)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("rib")
|
||||
@@ -34,6 +38,70 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("SoftDeleteCommand", "Remove", remove).at("SOLID", "MODIFY").icon("delete").params("bodies: [names] (removed from here on in the timeline)"),
|
||||
];
|
||||
|
||||
fn coil(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "PrimitiveCoil";
|
||||
let base = p.get("base").and_then(vec3).unwrap_or(Vec3::ZERO);
|
||||
let axis = match p.get("axis") {
|
||||
None => Vec3::Z,
|
||||
Some(Value::String(a)) => match a.to_ascii_uppercase().as_str() {
|
||||
"X" => Vec3::X,
|
||||
"Y" => Vec3::Y,
|
||||
"Z" => Vec3::Z,
|
||||
_ => return Err(bad(cmd, "`axis` must be X, Y, Z or [x, y, z]")),
|
||||
},
|
||||
Some(v) => vec3(v).and_then(|a| a.normalized()).ok_or_else(|| bad(cmd, "`axis` must be X, Y, Z or a non-zero [x, y, z]"))?,
|
||||
};
|
||||
let diameter = req_expr(cmd, p, "diameter")?;
|
||||
check_expr(s, &diameter, Kind::Length, cmd, "diameter")?;
|
||||
let turns = expr(p, "revolutions").or_else(|| expr(p, "turns"));
|
||||
let (height, pitch) = (expr(p, "height"), expr(p, "pitch"));
|
||||
let (turns, pitch) = match (turns, height, pitch) {
|
||||
(Some(n), _, Some(pt)) => (n, pt),
|
||||
(Some(n), Some(h), None) => (n.clone(), format!("({h}) / ({n})")),
|
||||
(None, Some(h), Some(pt)) => (format!("({h}) / ({pt})"), pt),
|
||||
_ => return Err(bad(cmd, "give two of `revolutions`, `height` and `pitch`")),
|
||||
};
|
||||
check_expr(s, &turns, Kind::Unitless, cmd, "revolutions")?;
|
||||
check_expr(s, &pitch, Kind::Length, cmd, "pitch")?;
|
||||
let section_size = req_expr(cmd, p, "section_size")?;
|
||||
check_expr(s, §ion_size, Kind::Length, cmd, "section_size")?;
|
||||
let section = match str_(p, "section").map(str::to_ascii_lowercase).as_deref() {
|
||||
None | Some("circular" | "circle") => solvecraft_doc::CoilSection::Circular,
|
||||
Some("square") => solvecraft_doc::CoilSection::Square,
|
||||
Some(o) => return Err(bad(cmd, format!("unknown section `{o}` (circular or square)"))),
|
||||
};
|
||||
let position = match str_(p, "section_position").map(str::to_ascii_lowercase).as_deref() {
|
||||
None | Some("center" | "on" | "on_center") => 0,
|
||||
Some("inside") => -1,
|
||||
Some("outside") => 1,
|
||||
Some(o) => return Err(bad(cmd, format!("unknown section position `{o}` (inside, center or outside)"))),
|
||||
};
|
||||
let start_angle = expr(p, "start_angle");
|
||||
if let Some(a) = &start_angle {
|
||||
check_expr(s, a, Kind::Angle, cmd, "start_angle")?;
|
||||
}
|
||||
let clockwise = bool_(p, "clockwise").unwrap_or(false);
|
||||
let operation = super::features::operation(p, cmd)?;
|
||||
add_feature(
|
||||
s,
|
||||
p,
|
||||
FeatureKind::Coil {
|
||||
base,
|
||||
axis,
|
||||
diameter,
|
||||
pitch,
|
||||
turns,
|
||||
section_size,
|
||||
section,
|
||||
position,
|
||||
start_angle,
|
||||
clockwise,
|
||||
operation,
|
||||
targets: string_list(p, "targets"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn curves_param(p: &Value, cmd: &str) -> Result<Vec<String>> {
|
||||
let c = string_list(p, "curves");
|
||||
if c.is_empty() || c.len() > 1000 {
|
||||
|
||||
@@ -108,3 +108,32 @@ fn replace_face_align_remove() {
|
||||
run(&mut s, "SoftDeleteCommand", json!({"bodies": ["B"]}));
|
||||
assert_eq!(measure(&mut s)["body_count"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coil_and_loft_to_a_point() {
|
||||
use std::f64::consts::{PI, TAU};
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "PrimitiveCoil", json!({"diameter": 40, "revolutions": 2, "pitch": 8, "section_size": 4}));
|
||||
assert!(rel(volume(&mut s), PI * 4.0 * TAU * 20.0 * 2.0) < 1e-3, "{}", volume(&mut s));
|
||||
// Height and revolutions give the pitch; a square section outside the diameter.
|
||||
let mut s = Session::default();
|
||||
run(
|
||||
&mut s,
|
||||
"PrimitiveCoil",
|
||||
json!({"diameter": 40, "revolutions": 1.5, "height": 15, "section": "square", "section_size": 2, "section_position": "outside", "axis": "X"}),
|
||||
);
|
||||
assert!(rel(volume(&mut s), 4.0 * TAU * 21.0 * 1.5) < 1e-3, "{}", volume(&mut s));
|
||||
assert!(s.execute("PrimitiveCoil", &json!({"diameter": 40, "pitch": 1, "revolutions": 3, "section_size": 4})).is_err(), "runs into itself");
|
||||
assert!(s.execute("PrimitiveCoil", &json!({"diameter": 40, "section_size": 4, "pitch": 8})).is_err(), "two of three");
|
||||
// Pyramid: a square lofted to a point.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Base"}));
|
||||
run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [-10, -10], "p1": [10, 10]}));
|
||||
run(&mut s, "SketchStop", json!({}));
|
||||
run(&mut s, "SketchCreate", json!({"plane": {"origin": [0, 0, 30], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "name": "Top"}));
|
||||
run(&mut s, "DrawPoint", json!({"point": [0, 0], "id": "apex"}));
|
||||
run(&mut s, "SketchStop", json!({}));
|
||||
run(&mut s, "SolidLoft", json!({"sections": [{"sketch": "Base"}, {"sketch": "Top", "point": "apex"}]}));
|
||||
assert!(rel(volume(&mut s), 400.0 * 30.0 / 3.0) < 1e-6);
|
||||
assert!(s.execute("SolidLoft", &json!({"sections": [{"sketch": "Base"}, {"sketch": "Top", "point": "nope"}]})).is_err());
|
||||
}
|
||||
Reference in new issue
Block a user