Surface Sweep: sheets carried from open curves and profiles along a path (#843)
Surface Sweep carries open curves and closed profile boundaries along a sketch path into surface bodies, with smooth walls along curved point paths. Adds the SurfaceSweep feature (format 18 note), the surface.sweep command, a dialog with Edit Feature support, and kernel and engine tests. Part of #757.
This commit is contained in:
1 parent
57d738eac4
commit
aba57f1ca2
16 files changed
+480
-27
No files matched your search
@@ -324,6 +324,7 @@ impl FeatureKind {
|
|||||||
| FeatureKind::Extrude { .. }
|
| FeatureKind::Extrude { .. }
|
||||||
| FeatureKind::SurfaceExtrude { .. }
|
| FeatureKind::SurfaceExtrude { .. }
|
||||||
| FeatureKind::SurfaceRevolve { .. }
|
| FeatureKind::SurfaceRevolve { .. }
|
||||||
|
| FeatureKind::SurfaceSweep { .. }
|
||||||
| FeatureKind::Combine { .. }
|
| FeatureKind::Combine { .. }
|
||||||
| FeatureKind::Loft { .. }
|
| FeatureKind::Loft { .. }
|
||||||
| FeatureKind::Sweep { .. }
|
| FeatureKind::Sweep { .. }
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ impl FeatureKind {
|
|||||||
| FeatureKind::SheetContour { sketch, .. }
|
| FeatureKind::SheetContour { sketch, .. }
|
||||||
| FeatureKind::Rib { sketch, .. } => sk(sketch),
|
| FeatureKind::Rib { sketch, .. } => sk(sketch),
|
||||||
FeatureKind::Patch { sketch: Some(s), .. } => sk(s),
|
FeatureKind::Patch { sketch: Some(s), .. } => sk(s),
|
||||||
FeatureKind::Sweep { sketch, path_sketch, .. } => {
|
FeatureKind::Sweep { sketch, path_sketch, .. } | FeatureKind::SurfaceSweep { sketch, path_sketch, .. } => {
|
||||||
sk(sketch);
|
sk(sketch);
|
||||||
sk(path_sketch);
|
sk(path_sketch);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -971,6 +971,17 @@ pub enum FeatureKind {
|
|||||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
symmetric: bool,
|
symmetric: bool,
|
||||||
},
|
},
|
||||||
|
/// A surface body swept along a chain of sketch curves from open curve chains and the
|
||||||
|
/// boundaries of profiles: walls only, no caps.
|
||||||
|
SurfaceSweep {
|
||||||
|
sketch: u64,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
profiles: Option<ProfileSel>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
curves: Vec<String>,
|
||||||
|
path_sketch: u64,
|
||||||
|
path: Vec<String>,
|
||||||
|
},
|
||||||
/// A planar surface body extended outward by `distance` past the edges through `edges`.
|
/// A planar surface body extended outward by `distance` past the edges through `edges`.
|
||||||
SurfaceExtend {
|
SurfaceExtend {
|
||||||
body: String,
|
body: String,
|
||||||
@@ -1278,7 +1289,7 @@ impl FeatureKind {
|
|||||||
| K::Rib { sketch, .. }
|
| K::Rib { sketch, .. }
|
||||||
| K::SheetBase { sketch, .. }
|
| K::SheetBase { sketch, .. }
|
||||||
| K::SheetContour { sketch, .. } => vec![*sketch],
|
| K::SheetContour { sketch, .. } => vec![*sketch],
|
||||||
K::Sweep { sketch, path_sketch, .. } => vec![*sketch, *path_sketch],
|
K::Sweep { sketch, path_sketch, .. } | K::SurfaceSweep { sketch, path_sketch, .. } => vec![*sketch, *path_sketch],
|
||||||
K::Pipe { path_sketch, .. } | K::Pattern { pattern: PatternKind::Path { path_sketch, .. }, .. } => vec![*path_sketch],
|
K::Pipe { path_sketch, .. } | K::Pattern { pattern: PatternKind::Path { path_sketch, .. }, .. } => vec![*path_sketch],
|
||||||
K::Loft { sections, .. } => sections.iter().map(|s| s.sketch).collect(),
|
K::Loft { sections, .. } => sections.iter().map(|s| s.sketch).collect(),
|
||||||
K::SheetFold { sketch, .. } | K::SplitFace { sketch, .. } | K::Patch { sketch, .. } => sketch.iter().copied().collect(),
|
K::SheetFold { sketch, .. } | K::SplitFace { sketch, .. } | K::Patch { sketch, .. } => sketch.iter().copied().collect(),
|
||||||
@@ -1408,6 +1419,7 @@ impl FeatureKind {
|
|||||||
FeatureKind::Patch { .. } => "PatchFeature",
|
FeatureKind::Patch { .. } => "PatchFeature",
|
||||||
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrudeFeature",
|
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrudeFeature",
|
||||||
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolveFeature",
|
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolveFeature",
|
||||||
|
FeatureKind::SurfaceSweep { .. } => "SurfaceSweepFeature",
|
||||||
FeatureKind::Stitch { .. } => "StitchFeature",
|
FeatureKind::Stitch { .. } => "StitchFeature",
|
||||||
FeatureKind::Thicken { .. } => "ThickenFeature",
|
FeatureKind::Thicken { .. } => "ThickenFeature",
|
||||||
FeatureKind::SurfaceTrim { .. } => "TrimFeature",
|
FeatureKind::SurfaceTrim { .. } => "TrimFeature",
|
||||||
@@ -1478,6 +1490,7 @@ impl FeatureKind {
|
|||||||
FeatureKind::Patch { .. } => "Patch",
|
FeatureKind::Patch { .. } => "Patch",
|
||||||
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrude",
|
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrude",
|
||||||
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolve",
|
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolve",
|
||||||
|
FeatureKind::SurfaceSweep { .. } => "SurfaceSweep",
|
||||||
FeatureKind::Stitch { .. } => "Stitch",
|
FeatureKind::Stitch { .. } => "Stitch",
|
||||||
FeatureKind::Thicken { .. } => "Thicken",
|
FeatureKind::Thicken { .. } => "Thicken",
|
||||||
FeatureKind::SurfaceTrim { .. } => "Trim",
|
FeatureKind::SurfaceTrim { .. } => "Trim",
|
||||||
@@ -1502,7 +1515,8 @@ impl FeatureKind {
|
|||||||
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
|
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
|
||||||
FeatureKind::Patch { sketch: Some(sk), profiles, .. }
|
FeatureKind::Patch { sketch: Some(sk), profiles, .. }
|
||||||
| FeatureKind::SurfaceExtrude { sketch: sk, profiles: Some(profiles), .. }
|
| FeatureKind::SurfaceExtrude { sketch: sk, profiles: Some(profiles), .. }
|
||||||
| FeatureKind::SurfaceRevolve { sketch: sk, profiles: Some(profiles), .. } => {
|
| FeatureKind::SurfaceRevolve { sketch: sk, profiles: Some(profiles), .. }
|
||||||
|
| FeatureKind::SurfaceSweep { sketch: sk, profiles: Some(profiles), .. } => {
|
||||||
vec![(*sk, profiles)]
|
vec![(*sk, profiles)]
|
||||||
}
|
}
|
||||||
FeatureKind::Loft { sections, .. } => sections.iter_mut().filter(|s| s.point.is_none()).map(|s| (s.sketch, &mut s.profiles)).collect(),
|
FeatureKind::Loft { sections, .. } => sections.iter_mut().filter(|s| s.point.is_none()).map(|s| (s.sketch, &mut s.profiles)).collect(),
|
||||||
@@ -1520,7 +1534,8 @@ impl FeatureKind {
|
|||||||
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
|
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
|
||||||
FeatureKind::Patch { sketch: Some(sketch), profiles, .. }
|
FeatureKind::Patch { sketch: Some(sketch), profiles, .. }
|
||||||
| FeatureKind::SurfaceExtrude { sketch, profiles: Some(profiles), .. }
|
| FeatureKind::SurfaceExtrude { sketch, profiles: Some(profiles), .. }
|
||||||
| FeatureKind::SurfaceRevolve { sketch, profiles: Some(profiles), .. } => {
|
| FeatureKind::SurfaceRevolve { sketch, profiles: Some(profiles), .. }
|
||||||
|
| FeatureKind::SurfaceSweep { sketch, profiles: Some(profiles), .. } => {
|
||||||
vec![(*sketch, profiles)]
|
vec![(*sketch, profiles)]
|
||||||
}
|
}
|
||||||
FeatureKind::Loft { sections, .. } => {
|
FeatureKind::Loft { sections, .. } => {
|
||||||
@@ -1633,7 +1648,7 @@ impl FeatureKind {
|
|||||||
},
|
},
|
||||||
FeatureKind::Mirror { plane, .. } => plane_exprs(plane, &mut v),
|
FeatureKind::Mirror { plane, .. } => plane_exprs(plane, &mut v),
|
||||||
FeatureKind::Shell { thickness, .. } => v.push(thickness),
|
FeatureKind::Shell { thickness, .. } => v.push(thickness),
|
||||||
FeatureKind::Loft { .. } | FeatureKind::Sweep { .. } => {}
|
FeatureKind::Loft { .. } | FeatureKind::Sweep { .. } | FeatureKind::SurfaceSweep { .. } => {}
|
||||||
FeatureKind::Draft { angle, neutral, .. } => {
|
FeatureKind::Draft { angle, neutral, .. } => {
|
||||||
v.push(angle);
|
v.push(angle);
|
||||||
plane_exprs(neutral, &mut v);
|
plane_exprs(neutral, &mut v);
|
||||||
|
|||||||
@@ -880,6 +880,7 @@ fn cache_key(f: &Feature, chain_fp: u64, local: u64, ctx: u64, state: &ModelStat
|
|||||||
fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||||
let path = match &f.kind {
|
let path = match &f.kind {
|
||||||
FeatureKind::Sweep { path_sketch, .. }
|
FeatureKind::Sweep { path_sketch, .. }
|
||||||
|
| FeatureKind::SurfaceSweep { path_sketch, .. }
|
||||||
| FeatureKind::Pipe { path_sketch, .. }
|
| FeatureKind::Pipe { path_sketch, .. }
|
||||||
| FeatureKind::Pattern { pattern: crate::PatternKind::Path { path_sketch, .. }, .. } => Some(*path_sketch),
|
| FeatureKind::Pattern { pattern: crate::PatternKind::Path { path_sketch, .. }, .. } => Some(*path_sketch),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -892,7 +893,7 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
|||||||
for frame in [doc.component_transform(f.component), doc.component_transform(owner)] {
|
for frame in [doc.component_transform(f.component), doc.component_transform(owner)] {
|
||||||
frame.iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
frame.iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||||
}
|
}
|
||||||
if let FeatureKind::Sweep { sketch, .. } = &f.kind {
|
if let FeatureKind::Sweep { sketch, .. } | FeatureKind::SurfaceSweep { sketch, .. } = &f.kind {
|
||||||
let owner = doc.feature(*sketch).map(|feature| feature.component).unwrap_or(0);
|
let owner = doc.feature(*sketch).map(|feature| feature.component).unwrap_or(0);
|
||||||
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||||
}
|
}
|
||||||
@@ -3635,7 +3636,8 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
|||||||
| FeatureKind::SurfaceExtrude { .. }
|
| FeatureKind::SurfaceExtrude { .. }
|
||||||
| FeatureKind::Unstitch { .. }
|
| FeatureKind::Unstitch { .. }
|
||||||
| FeatureKind::ReverseNormal { .. }
|
| FeatureKind::ReverseNormal { .. }
|
||||||
| FeatureKind::SurfaceRevolve { .. } => surface::eval(doc, vals, f, st),
|
| FeatureKind::SurfaceRevolve { .. }
|
||||||
|
| FeatureKind::SurfaceSweep { .. } => surface::eval(doc, vals, f, st),
|
||||||
FeatureKind::Emboss { .. }
|
FeatureKind::Emboss { .. }
|
||||||
| FeatureKind::Rib { .. }
|
| FeatureKind::Rib { .. }
|
||||||
| FeatureKind::ReplaceFace { .. }
|
| FeatureKind::ReplaceFace { .. }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! Evaluation of the surface features: Patch, Stitch, Thicken, Trim, Extend, Surface Extrude,
|
//! Evaluation of the surface features: Patch, Stitch, Thicken, Trim, Extend, Surface Extrude,
|
||||||
//! Unstitch and Reverse Normal (kernel `surfaces`).
|
//! Revolve and Sweep, Unstitch and Reverse Normal (kernel `surfaces`).
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use solidcraft_geom::Seg2;
|
use solidcraft_geom::Seg2;
|
||||||
@@ -155,6 +155,32 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
FeatureKind::SurfaceSweep { sketch, profiles, curves, path_sketch, path } => {
|
||||||
|
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
|
||||||
|
let ps =
|
||||||
|
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
|
||||||
|
// As Sweep: a 3D path sketch is followed in the feature's frame, the curves with it.
|
||||||
|
let profile;
|
||||||
|
let ss = if !ps.sketch.wires.is_empty() {
|
||||||
|
profile = profile_in_frame(doc, ss, f.component)?;
|
||||||
|
&profile
|
||||||
|
} else {
|
||||||
|
ss
|
||||||
|
};
|
||||||
|
let mapped = path_in_frame(doc, ps, f.component)?;
|
||||||
|
let ps = mapped.as_ref();
|
||||||
|
let chains = sheet_chains(ss, profiles.as_ref(), curves)?;
|
||||||
|
// The path runs on from the end nearest the middle of the curves.
|
||||||
|
let pts: Vec<Vec2> = chains.iter().flatten().flat_map(|s| s.polyline(1e-2)).collect();
|
||||||
|
let start = ss.plane.to_world(pts.iter().fold(Vec2::default(), |a, p| a + *p) * (1.0 / pts.len().max(1) as f64));
|
||||||
|
let body = match path_points(ps, path, start)? {
|
||||||
|
Some(pts) => kernel::sweep_sheet_points(&ss.plane, &chains, &pts)?,
|
||||||
|
None => kernel::sweep_sheet(&ss.plane, &chains, &path_segments(ps, path, start)?)?,
|
||||||
|
};
|
||||||
|
let name = new_name(st, f, 0);
|
||||||
|
st.bodies.push(ModelBody::new(name, body, f.id));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
_ => Err(DocError::Invalid("not a surface feature".into())),
|
_ => Err(DocError::Invalid("not a surface feature".into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,8 @@
|
|||||||
//! saves as before. 0.9.0 would heal a gap the design leaves open.
|
//! saves as before. 0.9.0 would heal a gap the design leaves open.
|
||||||
//! A `SurfaceRevolve` feature (Surface Revolve: `sketch`, optional `profiles`, `curves`,
|
//! A `SurfaceRevolve` feature (Surface Revolve: `sketch`, optional `profiles`, `curves`,
|
||||||
//! `axis`, `angle`, optional `angle2` and `symmetric`) is also first saved in 18.
|
//! `axis`, `angle`, optional `angle2` and `symmetric`) is also first saved in 18.
|
||||||
|
//! A `SurfaceSweep` feature (Surface Sweep: `sketch`, optional `profiles` and `curves`,
|
||||||
|
//! `path_sketch`, `path`) is also first saved in 18.
|
||||||
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ impl FeatureKind {
|
|||||||
| FeatureKind::BoundaryFill { .. }
|
| FeatureKind::BoundaryFill { .. }
|
||||||
| FeatureKind::Loft { .. }
|
| FeatureKind::Loft { .. }
|
||||||
| FeatureKind::Sweep { .. }
|
| FeatureKind::Sweep { .. }
|
||||||
|
| FeatureKind::SurfaceSweep { .. }
|
||||||
| FeatureKind::Import { .. }
|
| FeatureKind::Import { .. }
|
||||||
| FeatureKind::MeshImport { .. }
|
| FeatureKind::MeshImport { .. }
|
||||||
| FeatureKind::BodyTransfer { .. } => {}
|
| FeatureKind::BodyTransfer { .. } => {}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use serde_json::Value;
|
|||||||
use solidcraft_doc::{FaceAt, FeatureKind, PlaneRef, ProfileSel, expr::Kind};
|
use solidcraft_doc::{FaceAt, FeatureKind, PlaneRef, ProfileSel, expr::Kind};
|
||||||
|
|
||||||
use super::CommandSpec;
|
use super::CommandSpec;
|
||||||
use super::features::{add_feature, axis_param, check_expr, extent_param, feature_sketch, operation, plane_param, profiles};
|
use super::features::{add_feature, axis_param, check_expr, extent_param, feature_sketch, operation, plane_param, profiles, sketch_id};
|
||||||
use crate::params::{bad, bool_, expr, str_, string_list, vec3};
|
use crate::params::{bad, bool_, expr, str_, string_list, vec3};
|
||||||
use crate::{Result, Session};
|
use crate::{Result, Session};
|
||||||
|
|
||||||
@@ -38,10 +38,10 @@ pub static COMMANDS: &[CommandSpec] = &[
|
|||||||
.at("SURFACE", "CREATE")
|
.at("SURFACE", "CREATE")
|
||||||
.icon("surface_revolve")
|
.icon("surface_revolve")
|
||||||
.params("sketch?: id|name; curves?: [curve ids] and/or profiles? (as surface.extrude); axis: sketch line id|x|y|X|Y|Z|{origin, dir}; angle?: expr (default 360 deg); angle2?: expr (the other side); symmetric?: bool; name?, body_name? (a surface body, no caps; a solid where a full turn closes it)"),
|
.params("sketch?: id|name; curves?: [curve ids] and/or profiles? (as surface.extrude); axis: sketch line id|x|y|X|Y|Z|{origin, dir}; angle?: expr (default 360 deg); angle2?: expr (the other side); symmetric?: bool; name?, body_name? (a surface body, no caps; a solid where a full turn closes it)"),
|
||||||
planned!("surface.sweep", "Surface Sweep", "757")
|
CommandSpec::new("surface.sweep", "Surface Sweep", surface_sweep)
|
||||||
.at("SURFACE", "CREATE")
|
.at("SURFACE", "CREATE")
|
||||||
.icon("surface_sweep")
|
.icon("surface_sweep")
|
||||||
.params("planned: the parameters of solid.sweep, with an open profile chain as well as a closed one (no caps); caps?: bool"),
|
.params("sketch?: id|name; curves?: [curve ids] and/or profiles? (as surface.extrude); path_sketch: sketch; path: [curve ids in order] (a path with corners, or 3D curves, gives ruled walls mitred at the corners); name?, body_name? (a surface body, no caps)"),
|
||||||
planned!("surface.loft", "Surface Loft", "757")
|
planned!("surface.loft", "Surface Loft", "757")
|
||||||
.at("SURFACE", "CREATE")
|
.at("SURFACE", "CREATE")
|
||||||
.icon("surface_loft")
|
.icon("surface_loft")
|
||||||
@@ -155,6 +155,18 @@ fn surface_revolve(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
add_feature(s, p, FeatureKind::SurfaceRevolve { sketch, profiles, curves, axis, angle, angle2, symmetric })
|
add_feature(s, p, FeatureKind::SurfaceRevolve { sketch, profiles, curves, axis, angle, angle2, symmetric })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn surface_sweep(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
|
let cmd = "surface.sweep";
|
||||||
|
let sketch = feature_sketch(s, p, cmd)?;
|
||||||
|
let (profiles, curves) = sheet_inputs(p, cmd)?;
|
||||||
|
let path_sketch = sketch_id(s, p.get("path_sketch"), cmd, "path_sketch")?;
|
||||||
|
let path = string_list(p, "path");
|
||||||
|
if path.is_empty() || path.len() > 1000 {
|
||||||
|
return Err(bad(cmd, "`path` must list 1…1000 curve ids"));
|
||||||
|
}
|
||||||
|
add_feature(s, p, FeatureKind::SurfaceSweep { sketch, profiles, curves, path_sketch, path })
|
||||||
|
}
|
||||||
|
|
||||||
fn bodies_param(s: &Session, p: &Value, cmd: &str) -> Result<Vec<String>> {
|
fn bodies_param(s: &Session, p: &Value, cmd: &str) -> Result<Vec<String>> {
|
||||||
let bodies = string_list(p, "bodies");
|
let bodies = string_list(p, "bodies");
|
||||||
if bodies.is_empty() {
|
if bodies.is_empty() {
|
||||||
@@ -432,13 +444,57 @@ mod tests {
|
|||||||
assert!(s.execute("surface.revolve", &json!({"sketch": "S", "curves": ["side"], "axis": "y", "angle": "400 deg"})).is_err());
|
assert!(s.execute("surface.revolve", &json!({"sketch": "S", "curves": ["side"], "axis": "y", "angle": "400 deg"})).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Surface Sweep: a line along y carried up a path in the XZ plane, straight then round a
|
||||||
|
/// quarter turn (every point of it 5 from the arc's axis: 10 × (5 + 5π/2)), or round a
|
||||||
|
/// corner (10 × both legs); a rectangle's walls up the straight leg; undo, redo, reopening.
|
||||||
|
#[test]
|
||||||
|
fn surface_sweep_curves_and_profiles() {
|
||||||
|
use std::f64::consts::FRAC_PI_2;
|
||||||
|
let mut s = Session::default();
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
|
||||||
|
run(&mut s, "sketch.line", json!({"points": [[0, 0], [0, 10]], "ids": ["l"]}));
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "XZ", "name": "P"}));
|
||||||
|
run(&mut s, "sketch.line", json!({"points": [[0, 0], [0, 5]], "ids": ["up"]}));
|
||||||
|
let arc = run(
|
||||||
|
&mut s,
|
||||||
|
"sketch.arc.three_point",
|
||||||
|
json!({"start": [0, 5], "end": [5, 10], "through": [5.0 - 5.0 / 2f64.sqrt(), 5.0 + 5.0 / 2f64.sqrt()]}),
|
||||||
|
)["created"]["curves"][0]
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
run(&mut s, "sketch.line", json!({"points": [[0, 5], [5, 5]], "ids": ["over"]}));
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
let close = |a: f64, want: f64| assert!((a - want).abs() < want * 2e-3, "{a} vs {want}"); // (measured on meshes)
|
||||||
|
run(&mut s, "surface.sweep", json!({"sketch": "S", "curves": ["l"], "path_sketch": "P", "path": ["up", arc], "body_name": "Bend"}));
|
||||||
|
assert!(s.world_state().body("Bend").unwrap().body.is_surface());
|
||||||
|
close(area(&s, "Bend"), 10.0 * (5.0 + 5.0 * FRAC_PI_2));
|
||||||
|
run(&mut s, "surface.sweep", json!({"sketch": "S", "curves": ["l"], "path_sketch": "P", "path": ["up", "over"], "body_name": "Corner"}));
|
||||||
|
close(area(&s, "Corner"), 100.0);
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "R"}));
|
||||||
|
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [-1, -1], "p1": [1, 2]}));
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
run(&mut s, "surface.sweep", json!({"sketch": "R", "path_sketch": "P", "path": ["up"], "body_name": "Walls"}));
|
||||||
|
let st = s.world_state();
|
||||||
|
assert!(st.body("Walls").unwrap().body.is_surface() && st.body("Walls").unwrap().body.face_count() == 4);
|
||||||
|
close(area(&s, "Walls"), 10.0 * 5.0);
|
||||||
|
run(&mut s, "edit.undo", json!({}));
|
||||||
|
assert!(s.world_state().body("Walls").is_none());
|
||||||
|
run(&mut s, "edit.redo", json!({}));
|
||||||
|
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||||
|
assert!((area(&cold, "Bend") - area(&s, "Bend")).abs() < 1e-9);
|
||||||
|
// Refused: no path, and a path curve that is not there.
|
||||||
|
assert!(s.execute("surface.sweep", &json!({"sketch": "S", "curves": ["l"], "path_sketch": "P"})).is_err());
|
||||||
|
assert!(s.execute("surface.sweep", &json!({"sketch": "S", "curves": ["l"], "path_sketch": "P", "path": ["nope"]})).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
/// The planned surface commands are registered but refuse to run: disabled with the
|
/// The planned surface commands are registered but refuse to run: disabled with the
|
||||||
/// issue that tracks them, an error (never a panic) from every caller, and the design left
|
/// issue that tracks them, an error (never a panic) from every caller, and the design left
|
||||||
/// as it was.
|
/// as it was.
|
||||||
#[test]
|
#[test]
|
||||||
fn planned_commands_refuse_and_leave_the_design_alone() {
|
fn planned_commands_refuse_and_leave_the_design_alone() {
|
||||||
let planned = [
|
let planned = [
|
||||||
"surface.sweep",
|
|
||||||
"surface.loft",
|
"surface.loft",
|
||||||
"surface.offset",
|
"surface.offset",
|
||||||
"surface.ruled",
|
"surface.ruled",
|
||||||
|
|||||||
@@ -1170,7 +1170,7 @@ pub enum PathSeg {
|
|||||||
|
|
||||||
impl PathSeg {
|
impl PathSeg {
|
||||||
/// Unit directions of the path where the segment starts and ends.
|
/// Unit directions of the path where the segment starts and ends.
|
||||||
fn tangents(&self) -> Option<(Vec3, Vec3)> {
|
pub(crate) fn tangents(&self) -> Option<(Vec3, Vec3)> {
|
||||||
match *self {
|
match *self {
|
||||||
PathSeg::Line { a, b } => {
|
PathSeg::Line { a, b } => {
|
||||||
let t = (b - a).normalized()?;
|
let t = (b - a).normalized()?;
|
||||||
@@ -1185,7 +1185,7 @@ impl PathSeg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Points along the segment (an arc within `tol` of its chords), without its start.
|
/// Points along the segment (an arc within `tol` of its chords), without its start.
|
||||||
fn points_after_start(&self, tol: f64) -> Vec<Vec3> {
|
pub(crate) fn points_after_start(&self, tol: f64) -> Vec<Vec3> {
|
||||||
match *self {
|
match *self {
|
||||||
PathSeg::Line { b, .. } => vec![b],
|
PathSeg::Line { b, .. } => vec![b],
|
||||||
PathSeg::Arc { a, center, axis, angle } => {
|
PathSeg::Arc { a, center, axis, angle } => {
|
||||||
@@ -1200,7 +1200,7 @@ impl PathSeg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `p` turned by `angle` about the unit axis `k` through `center` (right hand).
|
/// `p` turned by `angle` about the unit axis `k` through `center` (right hand).
|
||||||
fn rotate_about(p: Vec3, center: Vec3, k: Vec3, angle: f64) -> Vec3 {
|
pub(crate) fn rotate_about(p: Vec3, center: Vec3, k: Vec3, angle: f64) -> Vec3 {
|
||||||
let v = p - center;
|
let v = p - center;
|
||||||
let (s, c) = angle.sin_cos();
|
let (s, c) = angle.sin_cos();
|
||||||
center + v * c + k.cross(v) * s + k * (k.dot(v) * (1.0 - c))
|
center + v * c + k.cross(v) * s + k * (k.dot(v) * (1.0 - c))
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ pub use step_in::{ImportedBody, StepImport, StepNode, step_import, step_import_s
|
|||||||
pub use step_out::{ExportBody, ExportProduct, StepHeader, step_export_bodies, step_export_products};
|
pub use step_out::{ExportBody, ExportProduct, StepHeader, step_export_bodies, step_export_products};
|
||||||
pub use surface_kind::{FaceSurface, face_surface};
|
pub use surface_kind::{FaceSurface, face_surface};
|
||||||
pub use surfaces::{
|
pub use surfaces::{
|
||||||
copy_faces, extend, extrude_sheet, patch_edges, patch_region, remove_faces, reverse_faces, revolve_sheet, stitch, thicken, trim, unstitch,
|
copy_faces, extend, extrude_sheet, patch_edges, patch_region, remove_faces, reverse_faces, revolve_sheet, stitch, sweep_sheet,
|
||||||
|
sweep_sheet_points, thicken, trim, unstitch,
|
||||||
};
|
};
|
||||||
pub use sweep_path::sweep_path;
|
pub use sweep_path::sweep_path;
|
||||||
pub use topo::{CylinderFace, cylinder_face_at};
|
pub use topo::{CylinderFace, cylinder_face_at};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! planar sketch region), Stitch (surfaces sharing edges sewn
|
//! planar sketch region), Stitch (surfaces sharing edges sewn
|
||||||
//! into one body, a solid when they close), Trim (a surface cut by a tool, one side kept),
|
//! into one body, a solid when they close), Trim (a surface cut by a tool, one side kept),
|
||||||
//! Thicken (a surface offset along its normals into a solid), copying faces out of a body, and
|
//! Thicken (a surface offset along its normals into a solid), copying faces out of a body, and
|
||||||
//! sheets swept from sketch curves (surface extrude and revolve).
|
//! sheets swept from sketch curves (surface extrude, revolve and sweep).
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -162,6 +162,120 @@ pub fn revolve_sheet(plane: &Plane, chains: &[Vec<Seg2>], axis_origin: Vec2, axi
|
|||||||
body_of(faces, &[], &rec)
|
body_of(faces, &[], &rec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A surface swept from chains of sketch segments on `plane` along a path that starts near
|
||||||
|
/// them. Along a path whose direction runs on across every joint, lines slide the chains and
|
||||||
|
/// arcs turn them, exactly; any other path (curves followed as polylines) goes through
|
||||||
|
/// [`sweep_sheet_points`]. The walls form one surface body with no caps (a solid where they
|
||||||
|
/// close); its history has no operands: every face is new.
|
||||||
|
pub fn sweep_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[crate::PathSeg]) -> Result<Body> {
|
||||||
|
use crate::PathSeg;
|
||||||
|
if !crate::build::frame_ok(plane) {
|
||||||
|
return Err(KernelError::Invalid("sweep frame is not finite and orthonormal".into()));
|
||||||
|
}
|
||||||
|
if chains.is_empty() || chains.len() > MAX_CHAINS {
|
||||||
|
return Err(KernelError::Invalid("pick the curves or profiles to sweep".into()));
|
||||||
|
}
|
||||||
|
if path.is_empty() || path.len() > 1000 {
|
||||||
|
return Err(KernelError::Invalid("the path needs 1…1000 segments".into()));
|
||||||
|
}
|
||||||
|
let ends: Vec<(Vec3, Vec3)> =
|
||||||
|
path.iter().map(PathSeg::tangents).collect::<Option<_>>().ok_or_else(|| KernelError::Invalid("a path segment has no direction".into()))?;
|
||||||
|
let first = match path.first() {
|
||||||
|
Some(PathSeg::Line { a, .. } | PathSeg::Arc { a, .. }) => *a,
|
||||||
|
None => return Err(KernelError::Invalid("the path needs 1…1000 segments".into())),
|
||||||
|
};
|
||||||
|
if ends.windows(2).any(|w| w[0].1.dot(w[1].0) < (1e-3f64).cos()) {
|
||||||
|
let mut pts = vec![first];
|
||||||
|
for seg in path {
|
||||||
|
pts.extend(seg.points_after_start(1e-2));
|
||||||
|
}
|
||||||
|
return sweep_sheet_points(plane, chains, &pts);
|
||||||
|
}
|
||||||
|
let mut rec = Rebuild::default();
|
||||||
|
let mut faces = Vec::new();
|
||||||
|
for chain in chains {
|
||||||
|
let mut cur = crate::build::chain_wire(plane, chain)?;
|
||||||
|
for seg in path {
|
||||||
|
let (part, map): (mt::Shell, Box<dyn Fn(Vec3) -> Vec3>) = match *seg {
|
||||||
|
PathSeg::Line { a, b } => {
|
||||||
|
(guard("surface sweep", || Ok(mt::builder::tsweep(&cur, crate::body::v3(b - a))))?, Box::new(move |p| p + (b - a)))
|
||||||
|
}
|
||||||
|
PathSeg::Arc { center, axis, angle, .. } => {
|
||||||
|
let k = axis.normalized().ok_or_else(|| KernelError::Invalid("a path arc has no axis".into()))?;
|
||||||
|
let part = guard("surface sweep", || Ok(mt::builder::rsweep(&cur, p3(center), crate::body::v3(k), mt::Rad(angle))))?;
|
||||||
|
(part, Box::new(move |p| crate::build::rotate_about(p, center, k, angle)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let next = far_wire(&part, &cur, map.as_ref())?;
|
||||||
|
for f in part.face_iter() {
|
||||||
|
rec.made(f, Vec::new());
|
||||||
|
faces.push(f.clone());
|
||||||
|
}
|
||||||
|
cur = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body_of(faces, &[], &rec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A surface swept from chains of sketch segments on `plane` along a 3D polyline path: one smooth
|
||||||
|
/// B-spline wall per segment where the path has no corners, else ruled walls between the path's
|
||||||
|
/// points, mitred at the corners (see [`sweep_sheet`]).
|
||||||
|
pub fn sweep_sheet_points(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -> Result<Body> {
|
||||||
|
if !crate::build::frame_ok(plane) {
|
||||||
|
return Err(KernelError::Invalid("sweep frame is not finite and orthonormal".into()));
|
||||||
|
}
|
||||||
|
if chains.is_empty() || chains.len() > MAX_CHAINS {
|
||||||
|
return Err(KernelError::Invalid("pick the curves or profiles to sweep".into()));
|
||||||
|
}
|
||||||
|
let sheet = |faces: Vec<mt::Face>| {
|
||||||
|
let mut rec = Rebuild::default();
|
||||||
|
for f in &faces {
|
||||||
|
rec.made(f, Vec::new());
|
||||||
|
}
|
||||||
|
body_of(faces, &[], &rec)
|
||||||
|
};
|
||||||
|
let ruled = sheet(crate::sweep_path::ruled_sheet(plane, chains, path)?)?;
|
||||||
|
// Smooth walls where the path has no corners, kept when their area agrees with the ruled
|
||||||
|
// walls' (a fit that went wrong falls back to those).
|
||||||
|
if let Ok(Some(faces)) = crate::sweep_path::smooth_sheet(plane, chains, path)
|
||||||
|
&& let Ok(smooth) = sheet(faces)
|
||||||
|
&& let (Ok(a), Ok(b)) = (crate::measure(&smooth), crate::measure(&ruled))
|
||||||
|
&& (a.area - b.area).abs() <= 0.02 * b.area.abs()
|
||||||
|
{
|
||||||
|
return Ok(smooth);
|
||||||
|
}
|
||||||
|
Ok(ruled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The far side of a sweep of the wire `start`: for each of its edges, in order and the same
|
||||||
|
/// way round, the edge of `swept` that `map` carries it onto (matched by its ends and middle).
|
||||||
|
fn far_wire(swept: &mt::Shell, start: &mt::Wire, map: &dyn Fn(Vec3) -> Vec3) -> Result<mt::Wire> {
|
||||||
|
let near = |p: Vec3, q: Vec3| p.dist(q) <= 1e-6 * (1.0 + p.len().max(q.len()));
|
||||||
|
let mid = |e: &mt::Edge| {
|
||||||
|
let c = e.oriented_curve();
|
||||||
|
let (t0, t1) = c.range_tuple();
|
||||||
|
from_p3(c.subs((t0 + t1) / 2.0))
|
||||||
|
};
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for e in start.edge_iter() {
|
||||||
|
let (a, b, m) = (map(from_p3(e.front().point())), map(from_p3(e.back().point())), map(mid(e)));
|
||||||
|
let found = swept.edge_iter().find_map(|f| {
|
||||||
|
let (fa, fb) = (from_p3(f.front().point()), from_p3(f.back().point()));
|
||||||
|
if !near(mid(&f), m) {
|
||||||
|
None
|
||||||
|
} else if near(fa, a) && near(fb, b) {
|
||||||
|
Some(f)
|
||||||
|
} else if near(fa, b) && near(fb, a) {
|
||||||
|
Some(f.inverse())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
out.push(found.ok_or_else(|| KernelError::Failed("sweep: lost the curves along the path".into()))?);
|
||||||
|
}
|
||||||
|
Ok(out.into())
|
||||||
|
}
|
||||||
|
|
||||||
/// The edge of a body nearest a point (by samples along each edge).
|
/// The edge of a body nearest a point (by samples along each edge).
|
||||||
pub(crate) fn edge_near(body: &Body, p: Vec3) -> Option<mt::Edge> {
|
pub(crate) fn edge_near(body: &Body, p: Vec3) -> Option<mt::Edge> {
|
||||||
let mut best: Option<(f64, mt::Edge)> = None;
|
let mut best: Option<(f64, mt::Edge)> = None;
|
||||||
@@ -879,6 +993,66 @@ mod tests {
|
|||||||
assert!(revolve_sheet(&Plane::XY, &[vec![line((1.0, 0.0), (1.0, 1.0))]], Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0), 0.0, 1.0).is_err());
|
assert!(revolve_sheet(&Plane::XY, &[vec![line((1.0, 0.0), (1.0, 1.0))]], Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0), 0.0, 1.0).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A line along x swept up z: straight (length × height), then on round a quarter turn
|
||||||
|
/// (a quarter cylinder of radius 5 about an axis along x: 10 × 5π/2, the arc taking up the
|
||||||
|
/// line's far edge), and along a path with a corner (mitred ruled walls, 10 × both legs); a
|
||||||
|
/// closed square's four walls; a 3D point path.
|
||||||
|
#[test]
|
||||||
|
fn sweep_sheet_areas() {
|
||||||
|
use crate::PathSeg;
|
||||||
|
use std::f64::consts::FRAC_PI_2;
|
||||||
|
let line = vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 0.0) }];
|
||||||
|
let up = PathSeg::Line { a: Vec3::ZERO, b: Vec3::new(0.0, 0.0, 5.0) };
|
||||||
|
let area = |b: &Body| measure(b).unwrap().area;
|
||||||
|
let straight = sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[up]).unwrap();
|
||||||
|
assert!(straight.is_surface() && rel(area(&straight), 50.0) < 1e-9);
|
||||||
|
let bend = PathSeg::Arc { a: Vec3::new(0.0, 0.0, 5.0), center: Vec3::new(0.0, 5.0, 5.0), axis: -Vec3::X, angle: FRAC_PI_2 };
|
||||||
|
let smooth = sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[up, bend]).unwrap();
|
||||||
|
assert!(smooth.is_surface());
|
||||||
|
let want = 50.0 + 10.0 * 5.0 * FRAC_PI_2;
|
||||||
|
assert!(rel(area(&smooth), want) < 2e-3, "{} vs {want}", area(&smooth)); // (measured on meshes)
|
||||||
|
let over = PathSeg::Line { a: Vec3::new(0.0, 0.0, 5.0), b: Vec3::new(0.0, 5.0, 5.0) };
|
||||||
|
let cornered = sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[up, over]).unwrap();
|
||||||
|
assert!(cornered.is_surface() && rel(area(&cornered), 100.0) < 1e-9, "{}", area(&cornered));
|
||||||
|
let sq = Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(4.0, 0.0), Vec2::new(4.0, 4.0), Vec2::new(0.0, 4.0)]);
|
||||||
|
let walls = sweep_sheet(&Plane::XY, std::slice::from_ref(&sq.segs), &[up]).unwrap();
|
||||||
|
assert!(walls.is_surface() && walls.face_count() == 4 && rel(area(&walls), 80.0) < 1e-9);
|
||||||
|
let pts: Vec<Vec3> = (0..=10).map(|i| Vec3::new(0.0, 0.0, i as f64)).collect();
|
||||||
|
assert!(rel(area(&sweep_sheet_points(&Plane::XY, std::slice::from_ref(&line), &pts).unwrap()), 100.0) < 1e-6);
|
||||||
|
// A smooth point path: a quarter circle of radius 30 in the YZ plane. One smooth wall (not
|
||||||
|
// forty ruled strips), a piece of cylinder: 10 × 15π.
|
||||||
|
let arc: Vec<Vec3> = (0..=40)
|
||||||
|
.map(|i| {
|
||||||
|
let t = i as f64 / 40.0 * FRAC_PI_2;
|
||||||
|
Vec3::new(0.0, 30.0 * (1.0 - t.cos()), 30.0 * t.sin())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let curved = sweep_sheet_points(&Plane::XY, std::slice::from_ref(&line), &arc).unwrap();
|
||||||
|
assert_eq!(curved.face_count(), 1, "smooth wall");
|
||||||
|
let want = 10.0 * 30.0 * FRAC_PI_2;
|
||||||
|
assert!(rel(area(&curved), want) < 2e-3, "{} vs {want}", area(&curved)); // (measured on meshes)
|
||||||
|
// A bent chain along the same path: two smooth walls joined by one shared seam edge.
|
||||||
|
let bent = vec![line[0], Seg2::Line { a: Vec2::new(10.0, 0.0), b: Vec2::new(10.0, 5.0) }];
|
||||||
|
let two = sweep_sheet_points(&Plane::XY, &[bent], &arc).unwrap();
|
||||||
|
assert_eq!(two.face_count(), 2);
|
||||||
|
let edges: std::collections::HashSet<_> = two.solid.edge_iter().map(|e| e.id()).collect();
|
||||||
|
assert_eq!(edges.len(), 7, "the walls share their seam");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sweep_sheet_refuses_bad_input() {
|
||||||
|
use crate::PathSeg;
|
||||||
|
let line = vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 0.0) }];
|
||||||
|
let up = PathSeg::Line { a: Vec3::ZERO, b: Vec3::new(0.0, 0.0, 5.0) };
|
||||||
|
assert!(sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[]).is_err());
|
||||||
|
assert!(sweep_sheet(&Plane::XY, &[], &[up]).is_err());
|
||||||
|
assert!(sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[PathSeg::Line { a: Vec3::ZERO, b: Vec3::ZERO }]).is_err());
|
||||||
|
let no_axis = PathSeg::Arc { a: Vec3::ZERO, center: Vec3::new(0.0, 5.0, 0.0), axis: Vec3::ZERO, angle: 1.0 };
|
||||||
|
assert!(sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[no_axis]).is_err());
|
||||||
|
assert!(sweep_sheet_points(&Plane::XY, std::slice::from_ref(&line), &[Vec3::ZERO]).is_err());
|
||||||
|
assert!(sweep_sheet_points(&Plane::XY, std::slice::from_ref(&line), &[Vec3::ZERO, Vec3::new(f64::NAN, 0.0, 0.0)]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extrude_sheet_refuses_bad_input() {
|
fn extrude_sheet_refuses_bad_input() {
|
||||||
let line = vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 0.0) }];
|
let line = vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 0.0) }];
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//! between stations, mitred at the corners.
|
//! between stations, mitred at the corners.
|
||||||
|
|
||||||
use mt::builder;
|
use mt::builder;
|
||||||
use solidcraft_geom::{Plane, Region2, Vec2, Vec3};
|
use solidcraft_geom::{Plane, Region2, Seg2, Vec2, Vec3};
|
||||||
use truck_modeling as mt;
|
use truck_modeling as mt;
|
||||||
|
|
||||||
use crate::body::{Body, p3};
|
use crate::body::{Body, p3};
|
||||||
@@ -175,9 +175,9 @@ fn start_cos(plane: &Plane, tangents: &[Vec3]) -> f64 {
|
|||||||
tangents.first().map(|t| t.dot(plane.normal()).abs()).unwrap_or(1.0).max(1e-3)
|
tangents.first().map(|t| t.dot(plane.normal()).abs()).unwrap_or(1.0).max(1e-3)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ruled walls between stations at the path's points (mitred at corners), every loop of the
|
/// Stations at the path's points (at most [`MAX_RULED_STATIONS`]), mitred at corners, with the
|
||||||
/// (oriented) profile.
|
/// points they stand at.
|
||||||
fn ruled(plane: &Plane, profile: &NumberedRegion, pts: &[Vec3]) -> Result<Body> {
|
fn ruled_stations(pts: &[Vec3]) -> Result<(Vec<Vec3>, Vec<Station>)> {
|
||||||
let pts: Vec<Vec3> = if pts.len() > MAX_RULED_STATIONS { resample(pts, MAX_RULED_STATIONS) } else { pts.to_vec() };
|
let pts: Vec<Vec3> = if pts.len() > MAX_RULED_STATIONS { resample(pts, MAX_RULED_STATIONS) } else { pts.to_vec() };
|
||||||
let n = pts.len();
|
let n = pts.len();
|
||||||
let seg_dir = |i: usize| -> Option<Vec3> { (*pts.get(i + 1)? - *pts.get(i)?).normalized() };
|
let seg_dir = |i: usize| -> Option<Vec3> { (*pts.get(i + 1)? - *pts.get(i)?).normalized() };
|
||||||
@@ -202,6 +202,133 @@ fn ruled(plane: &Plane, profile: &NumberedRegion, pts: &[Vec3]) -> Result<Body>
|
|||||||
mitre.push(m);
|
mitre.push(m);
|
||||||
}
|
}
|
||||||
let st = stations(&pts, &tangents, &mitre)?;
|
let st = stations(&pts, &tangents, &mitre)?;
|
||||||
|
Ok((pts, st))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walls of chains of sketch segments on `plane` (open, or closed) carried along a 3D polyline
|
||||||
|
/// path that starts near them: ruled between stations at the path's points, mitred at corners,
|
||||||
|
/// with no caps. Neighbouring walls share their edges.
|
||||||
|
pub(crate) fn ruled_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -> Result<Vec<mt::Face>> {
|
||||||
|
let (pts, st) = ruled_stations(&clean(path)?)?;
|
||||||
|
let edges: usize = chains.iter().map(Vec::len).sum();
|
||||||
|
if st.len().saturating_mul(edges) > 400_000 {
|
||||||
|
return Err(KernelError::Invalid("the sweep is too detailed (curves × path points)".into()));
|
||||||
|
}
|
||||||
|
let s0 = pts.first().copied().unwrap_or_default();
|
||||||
|
let wires: Vec<Vec<mt::Wire>> = st
|
||||||
|
.iter()
|
||||||
|
.map(|s| {
|
||||||
|
let pl = station_plane(plane, s0, s);
|
||||||
|
chains.iter().map(|c| crate::build::chain_wire(&pl, c)).collect::<Result<Vec<_>>>()
|
||||||
|
})
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
guard("path sweep", || {
|
||||||
|
let mut faces = Vec::new();
|
||||||
|
for w in wires.windows(2) {
|
||||||
|
let (Some(w0), Some(w1)) = (w.first(), w.get(1)) else { continue };
|
||||||
|
for (a, b) in w0.iter().zip(w1) {
|
||||||
|
let sides: mt::Shell = builder::try_wire_homotopy(a, b).map_err(|e| KernelError::Failed(format!("path sweep: {e}")))?;
|
||||||
|
faces.extend(sides.face_iter().cloned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(faces)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walls of chains of sketch segments on `plane` carried along a smooth 3D polyline path (no
|
||||||
|
/// turn sharper than [`CORNER`]): one B-spline wall per segment through evenly spaced,
|
||||||
|
/// rotation-minimising stations, neighbouring walls sharing their seam edges; no caps. `None`
|
||||||
|
/// for a path with corners (see [`ruled_sheet`]).
|
||||||
|
pub(crate) fn smooth_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -> Result<Option<Vec<mt::Face>>> {
|
||||||
|
let pts = clean(path)?;
|
||||||
|
if pts.windows(3).any(|w| turn(w[1] - w[0], w[2] - w[1]) > CORNER) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let fail = |w: &str| KernelError::Failed(format!("path sweep: {w}"));
|
||||||
|
if chains.iter().map(Vec::len).sum::<usize>() > 4096 {
|
||||||
|
return Err(fail("too many curves for a smooth sweep"));
|
||||||
|
}
|
||||||
|
let n = (pts.len() - 1).clamp(4, MAX_SMOOTH_STATIONS);
|
||||||
|
let sp = resample(&pts, n);
|
||||||
|
let tangents = central_tangents(&sp)?;
|
||||||
|
let st = stations(&sp, &tangents, &[])?;
|
||||||
|
let s0 = st.first().map(|s| s.at).unwrap_or_default();
|
||||||
|
let at = |i: usize, q: Vec2| st.get(i).map(|s| s.at + s.apply(plane.to_world(q) - s0)).unwrap_or_default();
|
||||||
|
let n = st.len() - 1;
|
||||||
|
let mut faces = Vec::new();
|
||||||
|
for chain in chains {
|
||||||
|
let closed = chain.first().zip(chain.last()).is_some_and(|(f, l)| l.end().dist(f.start()) <= 1e-6);
|
||||||
|
// Each segment: its curve at the first and last station, the columns through its start
|
||||||
|
// and end, and the wall surface.
|
||||||
|
let mut data = Vec::with_capacity(chain.len());
|
||||||
|
for s in chain {
|
||||||
|
let mut station_curves = Vec::with_capacity(n + 1);
|
||||||
|
for i in 0..=n {
|
||||||
|
let q: Vec<Vec3> = (0..SEG_SAMPLES).map(|j| at(i, s.point_at(j as f64 / (SEG_SAMPLES - 1) as f64))).collect();
|
||||||
|
station_curves.push(interpolate_cubic(&q).ok_or_else(|| fail("profile curve"))?);
|
||||||
|
}
|
||||||
|
let kv = station_curves.first().map(|c| c.knot_vec().clone()).ok_or_else(|| fail("no stations"))?;
|
||||||
|
let mut columns = Vec::with_capacity(SEG_SAMPLES);
|
||||||
|
for j in 0..SEG_SAMPLES {
|
||||||
|
let q: Vec<Vec3> = station_curves
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
let p = c.control_point(j);
|
||||||
|
Vec3::new(p.x, p.y, p.z)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
columns.push(interpolate_cubic(&q).ok_or_else(|| fail("path curve"))?);
|
||||||
|
}
|
||||||
|
let ku = columns.first().map(|c| c.knot_vec().clone()).ok_or_else(|| fail("no columns"))?;
|
||||||
|
let net: Vec<Vec<mt::Point3>> = (0..=n).map(|m| columns.iter().map(|c| *c.control_point(m)).collect()).collect();
|
||||||
|
let surf = mt::BSplineSurface::try_new((ku, kv), net).map_err(|e| fail(&e.to_string()))?;
|
||||||
|
let (Some(c0), Some(c1), Some(first), Some(last)) =
|
||||||
|
(station_curves.first().cloned(), station_curves.last().cloned(), columns.first().cloned(), columns.last().cloned())
|
||||||
|
else {
|
||||||
|
return Err(fail("stations"));
|
||||||
|
};
|
||||||
|
data.push((c0, c1, first, last, surf));
|
||||||
|
}
|
||||||
|
let walls: Vec<mt::Face> = guard("path sweep", || {
|
||||||
|
let m = chain.len();
|
||||||
|
// Vertices along the chain (an open chain has one more than it has segments).
|
||||||
|
let corners: Vec<Vec2> = chain.iter().map(Seg2::start).chain(chain.last().map(Seg2::end).filter(|_| !closed)).collect();
|
||||||
|
let start: Vec<mt::Vertex> = corners.iter().map(|q| builder::vertex(p3(at(0, *q)))).collect();
|
||||||
|
let end: Vec<mt::Vertex> = corners.iter().map(|q| builder::vertex(p3(at(n, *q)))).collect();
|
||||||
|
let next = |j: usize| if closed { (j + 1) % m } else { j + 1 };
|
||||||
|
let mut seams = Vec::with_capacity(corners.len());
|
||||||
|
for (k, (a, b)) in start.iter().zip(&end).enumerate() {
|
||||||
|
// The column through each corner: the start column of the segment leaving it, or
|
||||||
|
// the end column of the last segment.
|
||||||
|
let col = match data.get(k) {
|
||||||
|
Some((_, _, first, _, _)) => first,
|
||||||
|
None => data.last().map(|d| &d.3).ok_or_else(|| fail("seam"))?,
|
||||||
|
};
|
||||||
|
seams.push(mt::Edge::try_new(a, b, mt::Curve::BSplineCurve(col.clone())).map_err(|e| fail(&e.to_string()))?);
|
||||||
|
}
|
||||||
|
let mut walls = Vec::with_capacity(m);
|
||||||
|
for (j, (c0, c1, _, _, surf)) in data.iter().enumerate() {
|
||||||
|
let (Some(a0), Some(b0), Some(a1), Some(b1), Some(sa), Some(sb)) =
|
||||||
|
(start.get(j), start.get(next(j)), end.get(j), end.get(next(j)), seams.get(j), seams.get(next(j)))
|
||||||
|
else {
|
||||||
|
return Err(fail("edges"));
|
||||||
|
};
|
||||||
|
let e0 = mt::Edge::try_new(a0, b0, mt::Curve::BSplineCurve(c0.clone())).map_err(|e| fail(&e.to_string()))?;
|
||||||
|
let e1 = mt::Edge::try_new(a1, b1, mt::Curve::BSplineCurve(c1.clone())).map_err(|e| fail(&e.to_string()))?;
|
||||||
|
let w: mt::Wire = vec![sa.clone(), e1, sb.inverse(), e0.inverse()].into();
|
||||||
|
walls.push(mt::Face::try_new(vec![w], mt::Surface::BSplineSurface(surf.clone())).map_err(|e| fail(&e.to_string()))?);
|
||||||
|
}
|
||||||
|
Ok(walls)
|
||||||
|
})?;
|
||||||
|
faces.extend(walls);
|
||||||
|
}
|
||||||
|
Ok(Some(faces))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ruled walls between stations at the path's points (mitred at corners), every loop of the
|
||||||
|
/// (oriented) profile.
|
||||||
|
fn ruled(plane: &Plane, profile: &NumberedRegion, pts: &[Vec3]) -> Result<Body> {
|
||||||
|
let (pts, st) = ruled_stations(pts)?;
|
||||||
let s0 = pts.first().copied().unwrap_or_default();
|
let s0 = pts.first().copied().unwrap_or_default();
|
||||||
let loops: Vec<&NumberedLoop> = profile.loops().collect();
|
let loops: Vec<&NumberedLoop> = profile.loops().collect();
|
||||||
let mut wires: Vec<Vec<mt::Wire>> = Vec::with_capacity(st.len());
|
let mut wires: Vec<Vec<mt::Wire>> = Vec::with_capacity(st.len());
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! Dialogs for the surface tools and face splitting (#19): Surface Extrude and Revolve, Patch,
|
//! Dialogs for the surface tools and face splitting (#19): Surface Extrude, Revolve and Sweep,
|
||||||
//! Stitch, Thicken, Extend, Trim and Split Face. Each has a live preview and Edit Feature.
|
//! Patch, Stitch, Thicken, Extend, Trim and Split Face. Each has a live preview and Edit Feature.
|
||||||
|
|
||||||
use serde_json::{Map, Value, json};
|
use serde_json::{Map, Value, json};
|
||||||
use solidcraft_engine::Sel;
|
use solidcraft_engine::Sel;
|
||||||
@@ -25,6 +25,7 @@ pub enum Surf {
|
|||||||
symmetric: bool,
|
symmetric: bool,
|
||||||
axis: Option<Value>,
|
axis: Option<Value>,
|
||||||
},
|
},
|
||||||
|
Sweep,
|
||||||
Patch,
|
Patch,
|
||||||
Unstitch,
|
Unstitch,
|
||||||
ReverseNormal,
|
ReverseNormal,
|
||||||
@@ -51,6 +52,7 @@ impl Surf {
|
|||||||
match self {
|
match self {
|
||||||
Surf::Extrude { .. } => "SURFACE EXTRUDE",
|
Surf::Extrude { .. } => "SURFACE EXTRUDE",
|
||||||
Surf::Revolve { .. } => "SURFACE REVOLVE",
|
Surf::Revolve { .. } => "SURFACE REVOLVE",
|
||||||
|
Surf::Sweep => "SURFACE SWEEP",
|
||||||
Surf::Patch => "PATCH",
|
Surf::Patch => "PATCH",
|
||||||
Surf::Unstitch => "UNSTITCH",
|
Surf::Unstitch => "UNSTITCH",
|
||||||
Surf::ReverseNormal => "REVERSE NORMAL",
|
Surf::ReverseNormal => "REVERSE NORMAL",
|
||||||
@@ -81,6 +83,7 @@ fn inputs(k: &Surf) -> Vec<SelInput> {
|
|||||||
match k {
|
match k {
|
||||||
Surf::Extrude { .. } => vec![SelInput::new("Profiles or curves", PROFILES | CURVES, true)],
|
Surf::Extrude { .. } => vec![SelInput::new("Profiles or curves", PROFILES | CURVES, true)],
|
||||||
Surf::Revolve { .. } => vec![SelInput::new("Profiles or curves", PROFILES | CURVES, true), SelInput::new("Axis", AXES, false)],
|
Surf::Revolve { .. } => vec![SelInput::new("Profiles or curves", PROFILES | CURVES, true), SelInput::new("Axis", AXES, false)],
|
||||||
|
Surf::Sweep => vec![SelInput::new("Profiles or curves", PROFILES | CURVES, true), SelInput::new("Path", CURVES, true)],
|
||||||
Surf::Patch => vec![SelInput::new("Boundary", PROFILES | EDGES, true)],
|
Surf::Patch => vec![SelInput::new("Boundary", PROFILES | EDGES, true)],
|
||||||
Surf::Unstitch => vec![SelInput::new("Body, or faces to take out", BODIES | FACES, true)],
|
Surf::Unstitch => vec![SelInput::new("Body, or faces to take out", BODIES | FACES, true)],
|
||||||
Surf::ReverseNormal => vec![SelInput::new("Surface, or faces to turn over", BODIES | FACES, true)],
|
Surf::ReverseNormal => vec![SelInput::new("Surface, or faces to turn over", BODIES | FACES, true)],
|
||||||
@@ -102,6 +105,7 @@ pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
|||||||
let k = match id {
|
let k = match id {
|
||||||
"surface.extrude" => Surf::Extrude { distance: "10 mm".into(), direction: 0 },
|
"surface.extrude" => Surf::Extrude { distance: "10 mm".into(), direction: 0 },
|
||||||
"surface.revolve" => Surf::Revolve { angle: "360 deg".into(), symmetric: false, axis: None },
|
"surface.revolve" => Surf::Revolve { angle: "360 deg".into(), symmetric: false, axis: None },
|
||||||
|
"surface.sweep" => Surf::Sweep,
|
||||||
"surface.patch" => Surf::Patch,
|
"surface.patch" => Surf::Patch,
|
||||||
"surface.unstitch" => Surf::Unstitch,
|
"surface.unstitch" => Surf::Unstitch,
|
||||||
"surface.reverse_normal" => Surf::ReverseNormal,
|
"surface.reverse_normal" => Surf::ReverseNormal,
|
||||||
@@ -164,7 +168,7 @@ pub fn rows(ui: &mut egui::Ui, k: &mut Surf) -> bool {
|
|||||||
combo(ui, "sx_dir", &EXTRUDE_SIDES.map(|(label, _)| label), direction);
|
combo(ui, "sx_dir", &EXTRUDE_SIDES.map(|(label, _)| label), direction);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Surf::Patch | Surf::Trim | Surf::SplitFace | Surf::Unstitch | Surf::ReverseNormal => {}
|
Surf::Patch | Surf::Trim | Surf::SplitFace | Surf::Unstitch | Surf::ReverseNormal | Surf::Sweep => {}
|
||||||
}
|
}
|
||||||
enter
|
enter
|
||||||
}
|
}
|
||||||
@@ -266,6 +270,17 @@ pub fn commands(app: &SolveApp, k: &Surf, inputs: &[SelInput], extra: &Map<Strin
|
|||||||
}
|
}
|
||||||
("surface.revolve", p)
|
("surface.revolve", p)
|
||||||
}
|
}
|
||||||
|
Surf::Sweep => {
|
||||||
|
need(0, "profiles or curves to sweep")?;
|
||||||
|
need(1, "the path")?;
|
||||||
|
let mut p = sheet_picks(app, items(inputs, 0))?;
|
||||||
|
let path: Vec<String> =
|
||||||
|
items(inputs, 1).iter().filter_map(|x| if let Sel::SketchCurve { id } = x { Some(id.clone()) } else { None }).collect();
|
||||||
|
p["path_sketch"] =
|
||||||
|
json!(crate::dialogs::curves_sketch(app, &path, p["sketch"].as_u64()).ok_or("the path curves must be in one other sketch")?);
|
||||||
|
p["path"] = json!(path);
|
||||||
|
("surface.sweep", p)
|
||||||
|
}
|
||||||
Surf::Patch => {
|
Surf::Patch => {
|
||||||
need(0, "a boundary (sketch profiles or a loop of edges)")?;
|
need(0, "a boundary (sketch profiles or a loop of edges)")?;
|
||||||
let profiles: Vec<(u64, usize)> =
|
let profiles: Vec<(u64, usize)> =
|
||||||
@@ -386,6 +401,10 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
|
|||||||
}
|
}
|
||||||
return Some((Kind::Surface(k), ins, extra));
|
return Some((Kind::Surface(k), ins, extra));
|
||||||
}
|
}
|
||||||
|
FeatureKind::SurfaceSweep { sketch, profiles, curves, path, .. } => (
|
||||||
|
Surf::Sweep,
|
||||||
|
vec![sheet_sels(app, *sketch, profiles.as_ref(), curves), path.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect()],
|
||||||
|
),
|
||||||
FeatureKind::SurfaceExtrude { sketch, profiles, curves, extent } => {
|
FeatureKind::SurfaceExtrude { sketch, profiles, curves, extent } => {
|
||||||
let sels = sheet_sels(app, *sketch, profiles.as_ref(), curves);
|
let sels = sheet_sels(app, *sketch, profiles.as_ref(), curves);
|
||||||
let direction = match extent.direction {
|
let direction = match extent.direction {
|
||||||
|
|||||||
@@ -882,6 +882,34 @@ fn surface_revolve_dialog_takes_curves_and_an_axis() {
|
|||||||
assert_eq!(back.inputs[1].items, vec![Sel::Axis { name: "y".into() }]);
|
assert_eq!(back.inputs[1].items, vec![Sel::Axis { name: "y".into() }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn surface_sweep_dialog_takes_curves_and_a_path() {
|
||||||
|
// A line along y carried 5 up a line in the XZ plane: a 10 × 5 sheet. Edit Feature brings
|
||||||
|
// back the curve and the path.
|
||||||
|
let mut app = app_with(json!([
|
||||||
|
{"command": "sketch.create", "params": {"plane": "XY"}},
|
||||||
|
{"command": "sketch.line", "params": {"points": [[0, 0], [0, 10]], "ids": ["l"]}},
|
||||||
|
{"command": "sketch.finish", "params": {}},
|
||||||
|
{"command": "sketch.create", "params": {"plane": "XZ"}},
|
||||||
|
{"command": "sketch.line", "params": {"points": [[0, 0], [0, 5]], "ids": ["up"]}},
|
||||||
|
{"command": "sketch.finish", "params": {}}
|
||||||
|
]));
|
||||||
|
let mut d = start(&mut app, vec![], "surface.sweep");
|
||||||
|
d.inputs[0].items = vec![Sel::SketchCurve { id: "l".into() }];
|
||||||
|
assert!(apply_commands(&app, &d).is_err(), "no path yet");
|
||||||
|
d.inputs[1].items = vec![Sel::SketchCurve { id: "up".into() }];
|
||||||
|
let (id, p) = apply_commands(&app, &d).unwrap().remove(0);
|
||||||
|
assert_eq!((id.as_str(), &p["path"], &p["curves"]), ("surface.sweep", &json!(["up"]), &json!(["l"])), "{p}");
|
||||||
|
assert_ne!(p["path_sketch"], p["sketch"], "{p}");
|
||||||
|
app.session.execute(&id, &p).unwrap();
|
||||||
|
let area = app.session.model.state().bodies.last().unwrap().mesh().measure().area;
|
||||||
|
assert!((area - 50.0).abs() < 1e-6, "{area}");
|
||||||
|
let fid = app.session.doc.features.last().unwrap().id;
|
||||||
|
let back = crate::dialogs::for_feature(&app, fid, None).unwrap();
|
||||||
|
assert_eq!(back.inputs[0].items, vec![Sel::SketchCurve { id: "l".into() }]);
|
||||||
|
assert_eq!(back.inputs[1].items, vec![Sel::SketchCurve { id: "up".into() }]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn patch_dialog_fills_a_loop_of_edges_that_is_not_planar() {
|
fn patch_dialog_fills_a_loop_of_edges_that_is_not_planar() {
|
||||||
// A box with its top cut away by a sideways cylinder: its rim is a closed loop of four
|
// A box with its top cut away by a sideways cylinder: its rim is a closed loop of four
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub(crate) fn icon_of(k: &FeatureKind) -> &'static str {
|
|||||||
FeatureKind::Unstitch { .. } => "unstitch",
|
FeatureKind::Unstitch { .. } => "unstitch",
|
||||||
FeatureKind::ReverseNormal { .. } => "reverse_normal",
|
FeatureKind::ReverseNormal { .. } => "reverse_normal",
|
||||||
FeatureKind::SurfaceRevolve { .. } => "surface_revolve",
|
FeatureKind::SurfaceRevolve { .. } => "surface_revolve",
|
||||||
|
FeatureKind::SurfaceSweep { .. } => "surface_sweep",
|
||||||
FeatureKind::Revolve { .. } => "revolve",
|
FeatureKind::Revolve { .. } => "revolve",
|
||||||
FeatureKind::Fillet { .. } => "fillet",
|
FeatureKind::Fillet { .. } => "fillet",
|
||||||
FeatureKind::Chamfer { .. } => "chamfer",
|
FeatureKind::Chamfer { .. } => "chamfer",
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
# Features
|
# Features
|
||||||
|
|
||||||
SolidCraft's commands by area, generated by `cargo xtask features` from the command registry (`solidcraft-cli commands`). **339 commands.** Planned (registered, not built yet): 9. Every command runs the same way from the toolbar, a script, `solidcraft-cli exec <id> '<json>'`, the control channel and MCP; parameters are in the [command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md).
|
SolidCraft's commands by area, generated by `cargo xtask features` from the command registry (`solidcraft-cli commands`). **340 commands.** Planned (registered, not built yet): 8. Every command runs the same way from the toolbar, a script, `solidcraft-cli exec <id> '<json>'`, the control channel and MCP; parameters are in the [command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md).
|
||||||
|
|
||||||
## Sketch (98)
|
## Sketch (98)
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm
|
|||||||
| Align | `solid.align` | SOLID › MODIFY |
|
| Align | `solid.align` | SOLID › MODIFY |
|
||||||
| Remove | `solid.remove` | SOLID › MODIFY |
|
| Remove | `solid.remove` | SOLID › MODIFY |
|
||||||
|
|
||||||
## Surfaces (9, 8 planned)
|
## Surfaces (10, 7 planned)
|
||||||
|
|
||||||
| Command | Id | Toolbar |
|
| Command | Id | Toolbar |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -156,7 +156,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm
|
|||||||
| Trim | `surface.trim` | SURFACE › MODIFY |
|
| Trim | `surface.trim` | SURFACE › MODIFY |
|
||||||
| Surface Extrude | `surface.extrude` | SURFACE › CREATE |
|
| Surface Extrude | `surface.extrude` | SURFACE › CREATE |
|
||||||
| Surface Revolve | `surface.revolve` | SURFACE › CREATE |
|
| Surface Revolve | `surface.revolve` | SURFACE › CREATE |
|
||||||
| Surface Sweep *(planned: [#757](https://github.com/bherbruck/solidcraft/issues/757))* | `surface.sweep` | SURFACE › CREATE |
|
| Surface Sweep | `surface.sweep` | SURFACE › CREATE |
|
||||||
| Surface Loft *(planned: [#757](https://github.com/bherbruck/solidcraft/issues/757))* | `surface.loft` | SURFACE › CREATE |
|
| Surface Loft *(planned: [#757](https://github.com/bherbruck/solidcraft/issues/757))* | `surface.loft` | SURFACE › CREATE |
|
||||||
| Offset Surface *(planned: [#758](https://github.com/bherbruck/solidcraft/issues/758))* | `surface.offset` | SURFACE › CREATE |
|
| Offset Surface *(planned: [#758](https://github.com/bherbruck/solidcraft/issues/758))* | `surface.offset` | SURFACE › CREATE |
|
||||||
| Ruled Surface *(planned: [#762](https://github.com/bherbruck/solidcraft/issues/762))* | `surface.ruled` | SURFACE › CREATE |
|
| Ruled Surface *(planned: [#762](https://github.com/bherbruck/solidcraft/issues/762))* | `surface.ruled` | SURFACE › CREATE |
|
||||||
|
|||||||
Reference in new issue
Block a user