Thin Extrude builds walls of a set thickness along sketch curves, open chains and closed loops, with the wall centred, inside, outside or on both sides at two thicknesses. Saved under format 20 (thin on an Extrude); an extrude without thin saves as before. Closes #721.
This commit is contained in:
1 parent
86ea018d05
commit
6df69e9def
38 files changed
+1440
-34
No files matched your search
@@ -118,6 +118,7 @@ fn shell_planar_anchor_rebuild_cold_geometry_and_missing_source_errors() {
|
||||
let boss = doc
|
||||
.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: sk,
|
||||
profiles: crate::ProfileSel::All,
|
||||
extent: serde_json::from_value(json!({"distance":"2"})).unwrap(),
|
||||
@@ -280,6 +281,7 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
|
||||
let boss = doc
|
||||
.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: sk,
|
||||
profiles: crate::ProfileSel::All,
|
||||
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
|
||||
@@ -429,6 +431,7 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
|
||||
let boss = doc
|
||||
.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: sk,
|
||||
profiles: crate::ProfileSel::All,
|
||||
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
|
||||
@@ -553,6 +556,7 @@ fn saved_stepped_tab_cut_piece_projection_is_lost_without_expanding_material() {
|
||||
let material = doc
|
||||
.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: sk,
|
||||
profiles: crate::ProfileSel::All,
|
||||
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
|
||||
|
||||
@@ -179,6 +179,51 @@ pub struct Extent {
|
||||
pub from: Option<Vec3>,
|
||||
}
|
||||
|
||||
/// Thin Extrude: a wall of constant thickness along sketch curves instead of the profiles.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ThinWall {
|
||||
/// The sketch curves the wall follows: open chains and closed loops, joined where they
|
||||
/// meet end to end.
|
||||
pub curves: Vec<String>,
|
||||
/// The wall thickness (expression); with `side: two_sides`, the thickness inside.
|
||||
pub thickness: String,
|
||||
#[serde(default, skip_serializing_if = "ThinSide::is_center")]
|
||||
pub side: ThinSide,
|
||||
/// With `side: two_sides`, the thickness outside (expression).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub thickness2: Option<String>,
|
||||
}
|
||||
|
||||
/// Where a thin wall lies against its curves. A closed loop's inside is the area it encloses;
|
||||
/// an open chain's is the side it bends toward (a straight one's, the left of its first
|
||||
/// curve as drawn).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ThinSide {
|
||||
/// Half the thickness either side of the curves.
|
||||
#[default]
|
||||
Center,
|
||||
Inside,
|
||||
Outside,
|
||||
/// `thickness` inside and `thickness2` outside.
|
||||
TwoSides,
|
||||
}
|
||||
|
||||
impl ThinSide {
|
||||
pub fn is_center(&self) -> bool {
|
||||
*self == ThinSide::Center
|
||||
}
|
||||
pub fn parse(s: &str) -> Option<ThinSide> {
|
||||
match s.trim().to_ascii_lowercase().replace([' ', '-'], "_").as_str() {
|
||||
"center" | "centre" | "centered" | "centred" | "symmetric" => Some(ThinSide::Center),
|
||||
"inside" | "side1" | "side_1" => Some(ThinSide::Inside),
|
||||
"outside" | "side2" | "side_2" => Some(ThinSide::Outside),
|
||||
"two_sides" | "both" => Some(ThinSide::TwoSides),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Revolve axis.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
@@ -365,6 +410,9 @@ pub enum FeatureKind {
|
||||
/// Target body names for join/cut/intersect (empty = all bodies).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
targets: Vec<String>,
|
||||
/// Thin: a wall along sketch curves instead of the profiles (which are then unused).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
thin: Option<ThinWall>,
|
||||
},
|
||||
Revolve {
|
||||
sketch: u64,
|
||||
@@ -1464,6 +1512,8 @@ impl FeatureKind {
|
||||
/// The feature's profile selections, each with its sketch.
|
||||
pub fn profile_sels_mut(&mut self) -> Vec<(u64, &mut ProfileSel)> {
|
||||
match self {
|
||||
// A thin extrude follows curves; its profiles are not used.
|
||||
FeatureKind::Extrude { thin: Some(_), .. } => Vec::new(),
|
||||
FeatureKind::Extrude { sketch, profiles, .. }
|
||||
| FeatureKind::Revolve { sketch, profiles, .. }
|
||||
| FeatureKind::Sweep { sketch, profiles, .. }
|
||||
@@ -1484,6 +1534,8 @@ impl FeatureKind {
|
||||
/// Profile selections and their source sketches without cloning feature data.
|
||||
pub fn profile_sels(&self) -> Vec<(u64, &ProfileSel)> {
|
||||
match self {
|
||||
// A thin extrude follows curves; its profiles are not used.
|
||||
FeatureKind::Extrude { thin: Some(_), .. } => Vec::new(),
|
||||
FeatureKind::Extrude { sketch, profiles, .. }
|
||||
| FeatureKind::Revolve { sketch, profiles, .. }
|
||||
| FeatureKind::Sweep { sketch, profiles, .. }
|
||||
|
||||
+34
-2
@@ -2551,6 +2551,35 @@ fn centered_extrude(plane: &Plane, region: &Region2, start: f64, end: f64, sketc
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The regions a thin extrude's wall covers in its sketch.
|
||||
pub(crate) fn thin_regions(vals: &BTreeMap<String, Value>, ss: &SolvedSketch, thin: &crate::ThinWall) -> Result<Vec<Region2>> {
|
||||
if thin.curves.is_empty() || thin.curves.len() > 1000 {
|
||||
return Err(DocError::Invalid("a thin extrude needs 1…1000 sketch curves".into()));
|
||||
}
|
||||
let t = val(vals, &thin.thickness, Kind::Length)?;
|
||||
let (inside, outside) = match thin.side {
|
||||
crate::ThinSide::Center => (t / 2.0, t / 2.0),
|
||||
crate::ThinSide::Inside => (t, 0.0),
|
||||
crate::ThinSide::Outside => (0.0, t),
|
||||
crate::ThinSide::TwoSides => {
|
||||
let e = thin.thickness2.as_deref().ok_or_else(|| DocError::Invalid("a two-sided thin wall needs `thickness2`".into()))?;
|
||||
(t, val(vals, e, Kind::Length)?)
|
||||
}
|
||||
};
|
||||
if !(inside >= 0.0 && outside >= 0.0 && inside + outside > 0.0) {
|
||||
return Err(DocError::Invalid("the wall thickness must be positive".into()));
|
||||
}
|
||||
let curves = thin
|
||||
.curves
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let ci = ss.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("curve `{id}` of {}", ss.name)))?;
|
||||
Ok(ss.sketch.exact_segs(ci).unwrap_or_else(|| ss.sketch.segs(ci)))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
solidcraft_sketch::thin_wall_regions(&curves, inside, outside).map_err(|e| DocError::Invalid(format!("thin wall: {e}")))
|
||||
}
|
||||
|
||||
/// How far apart (mm) profiles must stay for their extruded tools to be cut as one.
|
||||
const APART_GAP: f64 = 1e-3;
|
||||
|
||||
@@ -2564,13 +2593,16 @@ fn feature_tools_with_constructor(
|
||||
) -> Result<Vec<Traced>> {
|
||||
let unnamed = |bodies: Vec<Body>| -> Vec<Traced> { bodies.into_iter().map(Traced::unnamed).collect() };
|
||||
match &f.kind {
|
||||
FeatureKind::Extrude { sketch, profiles, extent, operation, targets } => {
|
||||
FeatureKind::Extrude { sketch, profiles, extent, operation, targets, thin } => {
|
||||
let ss = sketch_in_feature_frame(
|
||||
doc,
|
||||
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
|
||||
f.component,
|
||||
)?;
|
||||
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
let regions = match thin {
|
||||
Some(thin) => thin_regions(vals, &ss, thin)?,
|
||||
None => solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?),
|
||||
};
|
||||
// From / To Object: where the face or point lies along the normal, measured over the
|
||||
// middle of the profiles. The points are in this feature's component frame, so the
|
||||
// model is seen from there: a target in another, moved component where it is shown.
|
||||
|
||||
@@ -446,6 +446,7 @@ mod tests {
|
||||
let producer = doc
|
||||
.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: source,
|
||||
profiles: crate::ProfileSel::Indices { indices: vec![0, 1] },
|
||||
extent,
|
||||
@@ -555,7 +556,14 @@ mod tests {
|
||||
let extent = serde_json::from_value(serde_json::json!({"distance":"2"})).unwrap();
|
||||
let boss = doc
|
||||
.add_feature(
|
||||
FeatureKind::Extrude { sketch: sk, profiles: crate::ProfileSel::All, extent, operation: crate::Operation::NewBody, targets: vec![] },
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: sk,
|
||||
profiles: crate::ProfileSel::All,
|
||||
extent,
|
||||
operation: crate::Operation::NewBody,
|
||||
targets: vec![],
|
||||
},
|
||||
Some("Boss"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -36,6 +36,11 @@ pub(super) fn fixtures() -> Vec<(&'static str, Value)> {
|
||||
("sketch", json!({"type": "sketch", "plane": plane("sketch"), "sketch": sk.clone()})),
|
||||
("sketch_along_path", json!({"type": "sketch", "plane": along("sketch"), "sketch": sk})),
|
||||
("extrude", json!({"type": "extrude", "sketch": 1, "profiles": {"type": "all"}, "extent": extent("extrude")})),
|
||||
(
|
||||
"extrude_thin",
|
||||
json!({"type": "extrude", "sketch": 1, "profiles": {"type": "all"}, "extent": extent("thin"),
|
||||
"thin": {"curves": ["c1"], "thickness": "thin.thickness", "side": "two_sides", "thickness2": "thin.thickness2"}}),
|
||||
),
|
||||
(
|
||||
"revolve",
|
||||
json!({"type": "revolve", "sketch": 1, "profiles": {"type": "all"}, "axis": {"type": "world", "axis": "X"}, "angle": "revolve.angle", "angle2": "revolve.angle2"}),
|
||||
@@ -158,7 +163,7 @@ pub(super) fn with_refs(name: &str, mut v: Value) -> Value {
|
||||
let o = [0, 0, 0];
|
||||
let tool = json!({"body": "B", "point": o});
|
||||
let extra = match name {
|
||||
"extrude" | "revolve" | "sweep" | "pipe" | "coil" | "thicken" => json!({"targets": ["B"]}),
|
||||
"extrude" | "extrude_thin" | "revolve" | "sweep" | "pipe" | "coil" | "thicken" => json!({"targets": ["B"]}),
|
||||
"fillet_constant" | "fillet_chord" | "fillet_curvature" | "fillet_variable" | "fillet_spline" | "chamfer" | "shell" | "draft"
|
||||
| "offset_face" | "delete_face" | "replace_face" | "sheet_flange" | "sheet_hem" | "sheet_unfold" | "boss" | "lip" | "snap_fit" | "rest"
|
||||
| "surface_offset" => {
|
||||
|
||||
@@ -16,6 +16,16 @@ extrude
|
||||
Taper angle = extrude.taper (Angle)
|
||||
To offset = extrude.to_offset (Length)
|
||||
expressions: extrude.distance, extrude.distance2, extrude.start_offset, extrude.taper, extrude.to_offset
|
||||
extrude_thin
|
||||
inputs:
|
||||
Distance = thin.distance (Length)
|
||||
Distance 2 = thin.distance2 (Length)
|
||||
Start offset = thin.start_offset (Length)
|
||||
Taper angle = thin.taper (Angle)
|
||||
To offset = thin.to_offset (Length)
|
||||
Thickness = thin.thickness (Length)
|
||||
Thickness 2 = thin.thickness2 (Length)
|
||||
expressions: thin.distance, thin.distance2, thin.start_offset, thin.taper, thin.to_offset, thin.thickness, thin.thickness2
|
||||
revolve
|
||||
inputs:
|
||||
Angle = revolve.angle (Angle)
|
||||
|
||||
@@ -7,6 +7,10 @@ extrude
|
||||
sketches: [1]
|
||||
remap_refs: sketch
|
||||
remap_bodies: targets[0]
|
||||
extrude_thin
|
||||
sketches: [1]
|
||||
remap_refs: sketch
|
||||
remap_bodies: targets[0]
|
||||
revolve
|
||||
sketches: [1]
|
||||
remap_refs: sketch
|
||||
|
||||
@@ -138,13 +138,18 @@
|
||||
//! Under the same version, a fillet's style can be `curvature` (a G2 quintic blend; `radius`
|
||||
//! is the equivalent circular setback). Fillets of the other styles save as before; 0.9.0
|
||||
//! would refuse the unknown style.
|
||||
//! - 20: an Extrude can be thin (`thin`: `curves`, `thickness`, optional `side` and
|
||||
//! `thickness2`): a wall along sketch curves instead of its profiles. Version 19 designs need
|
||||
//! no transformation, and an extrude without `thin` saves as before; the version stamp makes
|
||||
//! 0.10.0 (which reads up to 19) ask for an update instead of ignoring the wall and filling
|
||||
//! the sketch's profiles.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{DocError, Document, Result};
|
||||
|
||||
/// The format this build writes.
|
||||
pub const FORMAT: u32 = 19;
|
||||
pub const FORMAT: u32 = 20;
|
||||
|
||||
pub fn format_string(n: u32) -> String {
|
||||
format!("solidcraft/{n}")
|
||||
@@ -161,7 +166,7 @@ pub fn version_of(s: &str) -> Option<u32> {
|
||||
|
||||
/// JSON-level upgrades: entry `i` turns format `i + 1` into `i + 2`.
|
||||
type JsonStep = fn(&mut Value) -> Result<()>;
|
||||
const JSON_STEPS: [JsonStep; 18] = [
|
||||
const JSON_STEPS: [JsonStep; 19] = [
|
||||
|_| Ok(()),
|
||||
v2_to_v3,
|
||||
v3_to_v4,
|
||||
@@ -180,6 +185,7 @@ const JSON_STEPS: [JsonStep; 18] = [
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
|_| Ok(()),
|
||||
];
|
||||
|
||||
/// Stored face and edge names were worked out from geometry before format 14; they get the
|
||||
@@ -1048,6 +1054,7 @@ mod tests {
|
||||
document
|
||||
.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch,
|
||||
profiles: ProfileSel::All,
|
||||
extent: Extent {
|
||||
|
||||
@@ -423,6 +423,7 @@ mod tests {
|
||||
document
|
||||
.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: if missing_sketch { 999_999 } else { id },
|
||||
profiles: ProfileSel::OffsetBoundaries { associations: vec!["deleted-o1".into()] },
|
||||
extent: crate::Extent {
|
||||
|
||||
@@ -81,7 +81,28 @@ macro_rules! feature_walk {
|
||||
}
|
||||
}
|
||||
},
|
||||
FeatureKind::Extrude { extent, .. } | FeatureKind::SurfaceExtrude { extent, .. } => {
|
||||
FeatureKind::Extrude { extent, thin, .. } => {
|
||||
f("Distance", & $($m)? extent.distance, L, I);
|
||||
if let Some(d) = & $($m)? extent.distance2 {
|
||||
f("Distance 2", d, L, I);
|
||||
}
|
||||
if let Some(d) = & $($m)? extent.start_offset {
|
||||
f("Start offset", d, L, I);
|
||||
}
|
||||
if let Some(d) = & $($m)? extent.taper {
|
||||
f("Taper angle", d, A, I);
|
||||
}
|
||||
if let Some(d) = & $($m)? extent.to_offset {
|
||||
f("To offset", d, L, I);
|
||||
}
|
||||
if let Some(thin) = thin {
|
||||
f("Thickness", & $($m)? thin.thickness, L, I);
|
||||
if let Some(t) = & $($m)? thin.thickness2 {
|
||||
f("Thickness 2", t, L, I);
|
||||
}
|
||||
}
|
||||
}
|
||||
FeatureKind::SurfaceExtrude { extent, .. } => {
|
||||
f("Distance", & $($m)? extent.distance, L, I);
|
||||
if let Some(d) = & $($m)? extent.distance2 {
|
||||
f("Distance 2", d, L, I);
|
||||
|
||||
@@ -308,6 +308,7 @@ mod tests {
|
||||
document
|
||||
.add_feature(
|
||||
crate::FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: feature,
|
||||
profiles: selection.clone(),
|
||||
extent: crate::Extent {
|
||||
@@ -433,6 +434,7 @@ mod tests {
|
||||
document
|
||||
.add_feature(
|
||||
crate::FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: feature,
|
||||
profiles: selection.clone(),
|
||||
extent: crate::Extent {
|
||||
|
||||
@@ -29,6 +29,7 @@ fn plate_doc() -> (Document, u64) {
|
||||
let s = doc.add_feature(FeatureKind::Sketch { plane: PlaneRef::Origin { name: "XY".into() }, sketch: sk }, None).unwrap();
|
||||
doc.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: s,
|
||||
profiles: ProfileSel::All,
|
||||
extent: Extent {
|
||||
@@ -120,6 +121,7 @@ fn box_fillet_cut_timeline() {
|
||||
let s2 = doc.add_feature(FeatureKind::Sketch { plane: PlaneRef::Origin { name: "XY".into() }, sketch: sk }, None).unwrap();
|
||||
doc.add_feature(
|
||||
FeatureKind::Extrude {
|
||||
thin: None,
|
||||
sketch: s2,
|
||||
profiles: ProfileSel::Points { points: vec![Vec2::new(20.0, 15.0)] },
|
||||
extent: Extent {
|
||||
|
||||
Reference in new issue
Block a user