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 <[email protected]> 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
@@ -187,6 +187,10 @@ impl FeatureKind {
|
||||
dir(direction);
|
||||
}
|
||||
FeatureKind::Thread { face, .. } => pt(face),
|
||||
FeatureKind::Coil { base, axis, .. } => {
|
||||
pt(base);
|
||||
dir(axis);
|
||||
}
|
||||
FeatureKind::ReplaceFace { faces, target, .. } => {
|
||||
faces.iter_mut().for_each(pt);
|
||||
map_plane_ref(m, target);
|
||||
|
||||
@@ -379,6 +379,33 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
flip: bool,
|
||||
},
|
||||
/// A helical coil: a circular or square section swept `turns` times about `axis`, rising
|
||||
/// `pitch` per turn, on a helix of `diameter` (the section's centre, or its inside or outside).
|
||||
Coil {
|
||||
base: Vec3,
|
||||
#[serde(default = "z_axis")]
|
||||
axis: Vec3,
|
||||
diameter: String,
|
||||
pitch: String,
|
||||
turns: String,
|
||||
section_size: String,
|
||||
#[serde(default)]
|
||||
section: CoilSection,
|
||||
/// -1 inside, 0 on the centre, 1 outside of the diameter.
|
||||
#[serde(default, skip_serializing_if = "is_zero_i8")]
|
||||
position: i8,
|
||||
/// Start angle about the axis (expression; 0 = from the axis toward +x, or a
|
||||
/// perpendicular direction when the axis is x).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
start_angle: Option<String>,
|
||||
/// Turn the other way (left-handed).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
clockwise: bool,
|
||||
#[serde(default)]
|
||||
operation: Operation,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
targets: Vec<String>,
|
||||
},
|
||||
/// Remove bodies from the model (from here on in the timeline).
|
||||
Remove {
|
||||
bodies: Vec<String>,
|
||||
@@ -446,6 +473,22 @@ pub struct Component {
|
||||
pub struct LoftSection {
|
||||
pub sketch: u64,
|
||||
pub profiles: ProfileSel,
|
||||
/// A sketch point instead of a profile (the loft closes to it, as a pyramid's apex).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub point: Option<String>,
|
||||
}
|
||||
|
||||
/// Coil cross-sections.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CoilSection {
|
||||
#[default]
|
||||
Circular,
|
||||
Square,
|
||||
}
|
||||
|
||||
fn is_zero_i8(v: &i8) -> bool {
|
||||
*v == 0
|
||||
}
|
||||
|
||||
/// Hole shapes.
|
||||
@@ -563,6 +606,7 @@ impl FeatureKind {
|
||||
FeatureKind::BoundingSolid { .. } => "BoundingSolidFeature",
|
||||
FeatureKind::Pipe { .. } => "PipeFeature",
|
||||
FeatureKind::Emboss { .. } => "EmbossFeature",
|
||||
FeatureKind::Coil { .. } => "CoilFeature",
|
||||
FeatureKind::Rib { web: false, .. } => "RibFeature",
|
||||
FeatureKind::Rib { .. } => "WebFeature",
|
||||
FeatureKind::ReplaceFace { .. } => "ReplaceFaceFeature",
|
||||
@@ -603,6 +647,7 @@ impl FeatureKind {
|
||||
FeatureKind::BoundingSolid { .. } => "BoundingSolid",
|
||||
FeatureKind::Pipe { .. } => "Pipe",
|
||||
FeatureKind::Emboss { .. } => "Emboss",
|
||||
FeatureKind::Coil { .. } => "Coil",
|
||||
FeatureKind::Rib { web: false, .. } => "Rib",
|
||||
FeatureKind::Rib { .. } => "Web",
|
||||
FeatureKind::ReplaceFace { .. } => "ReplaceFace",
|
||||
@@ -683,6 +728,10 @@ impl FeatureKind {
|
||||
}
|
||||
}
|
||||
FeatureKind::Emboss { depth, .. } => v.push(depth),
|
||||
FeatureKind::Coil { diameter, pitch, turns, section_size, start_angle, .. } => {
|
||||
v.extend([diameter.as_str(), pitch, turns, section_size]);
|
||||
v.extend(start_angle.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::Rib { thickness, depth, .. } => {
|
||||
v.push(thickness);
|
||||
v.extend(depth.iter().map(String::as_str));
|
||||
|
||||
@@ -943,6 +943,56 @@ fn feature_tools(vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -
|
||||
let regions: Vec<Region2> = regions.iter().map(|r| grow_profile_for_coplanar(st, r, operation, &targets, &probe)).collect();
|
||||
Ok(kernel::revolve(&ss.plane, ®ions, o, d, ang)?)
|
||||
}
|
||||
FeatureKind::Loft { sections, .. } if sections.iter().any(|s| s.point.is_some()) => {
|
||||
// A profile lofted to a sketch point (an apex).
|
||||
let [a, b] = §ions[..] else { return Err(DocError::Invalid("not supported yet: a point section in a loft of more than two".into())) };
|
||||
let (prof, pt) = if a.point.is_some() { (b, a) } else { (a, b) };
|
||||
let ss = st.sketch(prof.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", prof.sketch)))?;
|
||||
let regions = solvecraft_sketch::merge_regions(&select_profiles(ss, &prof.profiles)?);
|
||||
let [r] = ®ions[..] else { return Err(DocError::Invalid("each loft section must be one profile".into())) };
|
||||
let ps = st.sketch(pt.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", pt.sketch)))?;
|
||||
let id = pt.point.as_deref().unwrap_or_default();
|
||||
let q = ps.sketch.points.iter().find(|p| p.id == id).ok_or_else(|| DocError::Unknown(format!("point `{id}` in {}", ps.name)))?;
|
||||
Ok(vec![kernel::loft_to_point(&ss.plane, &r.outer, ps.plane.to_world(q.pos))?])
|
||||
}
|
||||
FeatureKind::Coil { base, axis, diameter, pitch, turns, section_size, section, position, start_angle, clockwise, .. } => {
|
||||
let k = axis.normalized().ok_or_else(|| DocError::Invalid("coil axis".into()))?;
|
||||
let (d, p, n, s) = (
|
||||
val(vals, diameter, Kind::Length)?,
|
||||
val(vals, pitch, Kind::Length)?,
|
||||
val(vals, turns, Kind::Unitless)?,
|
||||
val(vals, section_size, Kind::Length)?,
|
||||
);
|
||||
if !(d > 0.0 && s > 0.0 && n > 0.0) {
|
||||
return Err(DocError::Invalid("the coil diameter, section size and revolutions must be positive".into()));
|
||||
}
|
||||
let r = d / 2.0 + f64::from(*position) * s / 2.0;
|
||||
if r <= s / 2.0 {
|
||||
return Err(DocError::Invalid("the section reaches the coil's axis".into()));
|
||||
}
|
||||
let a0 = match start_angle {
|
||||
Some(e) => val(vals, e, Kind::Angle)?,
|
||||
None => 0.0,
|
||||
};
|
||||
// Radial direction at the start; the section lies in the plane of the axis.
|
||||
let x0 = if k.cross(Vec3::X).len() > 1e-6 { (Vec3::X - k * k.dot(Vec3::X)).normalized() } else { Some(k.any_perp()) };
|
||||
let x0 = x0.ok_or_else(|| DocError::Invalid("coil axis".into()))?;
|
||||
let x = x0 * a0.cos() + k.cross(x0) * a0.sin();
|
||||
let plane = Plane { origin: *base, x, y: k };
|
||||
let c = Vec2::new(r, 0.0);
|
||||
let outer = match section {
|
||||
crate::CoilSection::Circular => solvecraft_geom::Loop2::circle(c, s / 2.0),
|
||||
crate::CoilSection::Square => {
|
||||
let h = s / 2.0;
|
||||
solvecraft_geom::Loop2::polygon(&[c + Vec2::new(-h, -h), c + Vec2::new(h, -h), c + Vec2::new(h, h), c + Vec2::new(-h, h)])
|
||||
}
|
||||
};
|
||||
let region = Region2 { outer, holes: Vec::new() };
|
||||
let axis = if *clockwise { k * -1.0 } else { k };
|
||||
// A left-handed coil turns about −axis and climbs along +axis: mirror the pitch.
|
||||
let pitch = if *clockwise { -p } else { p };
|
||||
Ok(vec![kernel::sweep_helix(&plane, ®ion, *base, axis, pitch, n)?])
|
||||
}
|
||||
FeatureKind::Loft { sections, .. } => {
|
||||
let mut secs = Vec::new();
|
||||
for sec in sections {
|
||||
@@ -1076,6 +1126,7 @@ fn feature_op(f: &Feature) -> (Operation, Vec<String>) {
|
||||
FeatureKind::Hole { .. } => (Operation::Cut, Vec::new()),
|
||||
FeatureKind::Pipe { operation, targets, .. } => (*operation, targets.clone()),
|
||||
FeatureKind::Loft { operation, targets, .. } | FeatureKind::Sweep { operation, targets, .. } => (*operation, targets.clone()),
|
||||
FeatureKind::Coil { operation, targets, .. } => (*operation, targets.clone()),
|
||||
_ => (Operation::NewBody, Vec::new()),
|
||||
}
|
||||
}
|
||||
@@ -1342,6 +1393,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
| FeatureKind::Pipe { operation, targets, .. }
|
||||
| FeatureKind::Revolve { operation, targets, .. }
|
||||
| FeatureKind::Loft { operation, targets, .. }
|
||||
| FeatureKind::Coil { operation, targets, .. }
|
||||
| FeatureKind::Sweep { operation, targets, .. } => {
|
||||
let tools = feature_tools(vals, f, st)?;
|
||||
apply_op(st, f, tools, *operation, targets)
|
||||
|
||||
@@ -138,6 +138,15 @@ impl FeatureKind {
|
||||
}
|
||||
}
|
||||
FeatureKind::Emboss { depth, .. } => v.push(("Depth", depth, L)),
|
||||
FeatureKind::Coil { diameter, pitch, turns, section_size, start_angle, .. } => {
|
||||
v.push(("Diameter", diameter, L));
|
||||
v.push(("Pitch", pitch, L));
|
||||
v.push(("Revolutions", turns, U));
|
||||
v.push(("Section size", section_size, L));
|
||||
if let Some(a) = start_angle {
|
||||
v.push(("Start angle", a, A));
|
||||
}
|
||||
}
|
||||
FeatureKind::Rib { thickness, depth, .. } => {
|
||||
v.push(("Thickness", thickness, L));
|
||||
if let Some(d) = depth {
|
||||
|
||||
Reference in new issue
Block a user