Thanks @jyviko.
This commit is contained in:
1 parent
77bdcc47e9
commit
fac3f96bdd
24 files changed
+2688
-77
No files matched your search
@@ -2,7 +2,9 @@
|
||||
//! solids), and the planned surface toolset (`planned!`: registered, not built yet).
|
||||
|
||||
use serde_json::Value;
|
||||
use solidcraft_doc::{BlendSide, ExtendMode, ExtendTo, FaceAt, FeatureKind, PlaneRef, ProfileSel, RuledCorners, RuledMode, expr::Kind};
|
||||
use solidcraft_doc::{
|
||||
BlendSide, EdgeContinuity, ExtendMode, ExtendTo, FaceAt, FeatureKind, PatchContinuity, PlaneRef, ProfileSel, RuledCorners, RuledMode, expr::Kind,
|
||||
};
|
||||
use solidcraft_kernel::Continuity;
|
||||
|
||||
use super::CommandSpec;
|
||||
@@ -14,7 +16,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("surface.patch", "Patch", patch)
|
||||
.at("SURFACE", "CREATE")
|
||||
.icon("patch")
|
||||
.params("sketch?: id|name and profiles? (planar patches of sketch regions) | edges: [[x,y,z]] (points on a closed loop of a body's edges: one face on them, planar when the loop is, else a smooth B-spline surface) and body?; crown?: number −0.9…3 (default 0: only a fitted patch, whose middle becomes fuller by this many times the loop's size — the bump vanishes on the loop, so the face still meets the picked edges; a loop that is planar is the exact plane whatever the crown); name?, body_name?"),
|
||||
.params("sketch?: id|name and profiles? (planar patches of sketch regions) | edges: [[x,y,z]] (points on a closed loop of a body's edges: one face on them, planar when the loop is, else a smooth B-spline surface) and body?; continuity?: g0|g1|g2 (edges only, default g0: how the patch meets the faces beside the loop — g1 their tangent planes, g2 also their curvature across the edges) or [{edge: [x,y,z], g: g0|g1|g2}] (those edges so, the rest g0) or {all, edges}; points?: [[x,y,z]] (edges only, up to 64: points the patch passes through); crown?: number −0.9…3 (default 0: only a fitted patch, whose middle becomes fuller by this many times the loop's size — the bump vanishes on the loop, so the face still meets the picked edges and the continuity across them; a loop that is planar and g0 is the exact plane whatever the crown; with points, crown needs a g1 or g2 edge); name?, body_name?"),
|
||||
CommandSpec::new("surface.stitch", "Stitch", stitch)
|
||||
.at("SURFACE", "MODIFY")
|
||||
.icon("stitch")
|
||||
@@ -84,24 +86,91 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
fn patch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "surface.patch";
|
||||
let crown = crown_param(p, cmd)?;
|
||||
let continuity = patch_continuity(p, cmd)?;
|
||||
let points = patch_points(p, cmd)?;
|
||||
let edges: Vec<solidcraft_geom::Vec3> = p.get("edges").and_then(Value::as_array).map(|a| a.iter().filter_map(vec3).collect()).unwrap_or_default();
|
||||
let mut v = if !edges.is_empty() {
|
||||
if edges.len() > 10_000 {
|
||||
return Err(bad(cmd, "too many edges"));
|
||||
}
|
||||
let body = str_(p, "body").map(str::to_string);
|
||||
add_feature(s, p, FeatureKind::Patch { sketch: None, profiles: ProfileSel::All, body, edges, crown })?
|
||||
add_feature(s, p, FeatureKind::Patch { sketch: None, profiles: ProfileSel::All, body, edges, crown, continuity: continuity.clone(), points })?
|
||||
} else {
|
||||
if !continuity.is_g0() || !points.is_empty() {
|
||||
return Err(bad(cmd, "`continuity` and `points` are for a patch on a loop of edges"));
|
||||
}
|
||||
let sketch = feature_sketch(s, p, cmd)?;
|
||||
let profiles = profiles(p, cmd)?;
|
||||
add_feature(s, p, FeatureKind::Patch { sketch: Some(sketch), profiles, body: None, edges: Vec::new(), crown })?
|
||||
let kind = FeatureKind::Patch {
|
||||
sketch: Some(sketch),
|
||||
profiles,
|
||||
body: None,
|
||||
edges: Vec::new(),
|
||||
crown,
|
||||
continuity: PatchContinuity::default(),
|
||||
points,
|
||||
};
|
||||
add_feature(s, p, kind)?
|
||||
};
|
||||
if let Some(o) = v.as_object_mut() {
|
||||
o.insert("crown".into(), serde_json::json!(crown));
|
||||
o.insert("continuity".into(), serde_json::to_value(&continuity).unwrap_or(Value::Null));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// A continuity word for a patch: g0, g1 or g2 (a patch does not hold g3).
|
||||
fn patch_order(v: Option<&Value>, cmd: &str) -> Result<Continuity> {
|
||||
match v.and_then(Value::as_str).and_then(Continuity::parse) {
|
||||
Some(g) if g != Continuity::G3 => Ok(g),
|
||||
_ => Err(bad(cmd, "a patch's continuity is g0, g1 or g2")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The patch's `continuity`: one word for every edge, a list of `{edge, g}` for some edges (the
|
||||
/// rest g0), or `{all, edges}`.
|
||||
fn patch_continuity(p: &Value, cmd: &str) -> Result<PatchContinuity> {
|
||||
let edges = |list: &Value| -> Result<Vec<EdgeContinuity>> {
|
||||
let list = list.as_array().ok_or_else(|| bad(cmd, "`continuity` edges must be a list of {edge: [x,y,z], g}"))?;
|
||||
if list.len() > 10_000 {
|
||||
return Err(bad(cmd, "`continuity` lists at most 10000 edges"));
|
||||
}
|
||||
list.iter()
|
||||
.map(|e| {
|
||||
let edge = e.get("edge").and_then(vec3).ok_or_else(|| bad(cmd, "a `continuity` entry's `edge` must be a point [x,y,z]"))?;
|
||||
Ok(EdgeContinuity { edge, g: patch_order(e.get("g"), cmd)? })
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
match p.get("continuity") {
|
||||
None => Ok(PatchContinuity::default()),
|
||||
Some(v) if v.is_string() => Ok(PatchContinuity { all: patch_order(Some(v), cmd)?, edges: Vec::new() }),
|
||||
Some(v) if v.is_array() => Ok(PatchContinuity { all: Continuity::G0, edges: edges(v)? }),
|
||||
Some(v) if v.is_object() => {
|
||||
let all = match v.get("all") {
|
||||
None => Continuity::G0,
|
||||
some => patch_order(some, cmd)?,
|
||||
};
|
||||
let edges = match v.get("edges") {
|
||||
None => Vec::new(),
|
||||
Some(list) => edges(list)?,
|
||||
};
|
||||
Ok(PatchContinuity { all, edges })
|
||||
}
|
||||
Some(_) => Err(bad(cmd, "`continuity` must be g0, g1 or g2, or a list of {edge, g}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The points a patch passes through (at most 64, each a finite point).
|
||||
fn patch_points(p: &Value, cmd: &str) -> Result<Vec<solidcraft_geom::Vec3>> {
|
||||
let Some(v) = p.get("points") else { return Ok(Vec::new()) };
|
||||
let list = v
|
||||
.as_array()
|
||||
.filter(|a| a.len() <= solidcraft_geom::fill_general::MAX_FILL_POINTS)
|
||||
.ok_or_else(|| bad(cmd, "`points` must list at most 64 points [x,y,z]"))?;
|
||||
list.iter().map(|q| vec3(q).filter(|q| q.is_finite()).ok_or_else(|| bad(cmd, "a patch point must be a finite [x,y,z]"))).collect()
|
||||
}
|
||||
|
||||
/// The patch's `crown`: how full a fitted patch's middle is made, -0.9…3 (default 0). A hostile
|
||||
/// value (NaN, infinity, out of range, a string) is refused.
|
||||
fn crown_param(p: &Value, cmd: &str) -> Result<f64> {
|
||||
@@ -1329,4 +1398,69 @@ mod tests {
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
/// A tube whose top face is taken off (`surface.unstitch`), and points round its rim.
|
||||
fn open_tube() -> (Session, Value) {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 10, "height": 30, "body_name": "Tube"}));
|
||||
run(&mut s, "surface.unstitch", json!({"body": "Tube", "faces": [[0, 0, 30]]}));
|
||||
let rim: Vec<Value> = (0..8).map(|k| f64::from(k) * 0.785).map(|a| json!([10.0 * a.cos(), 10.0 * a.sin(), 30.0])).collect();
|
||||
(s, json!(rim))
|
||||
}
|
||||
|
||||
/// `surface.patch continuity` and `points`: the tube's open end capped G1, per-edge
|
||||
/// continuity, a point the cap passes through, all saved with the design and read back;
|
||||
/// hostile values refused.
|
||||
#[test]
|
||||
fn patch_continuity_and_points_cap_a_tube() {
|
||||
// A coarse mesh is enough to tell the patches apart (measuring a fitted face's area
|
||||
// exactly tessellates it finely).
|
||||
let mesh = |s: &Session, name: &str| s.world_state().body(name).unwrap().body.tessellate(0.05).unwrap();
|
||||
let (mut s, rim) = open_tube();
|
||||
let reply = run(&mut s, "surface.patch", json!({"edges": rim, "body": "Tube", "continuity": "g1", "body_name": "Cap"}));
|
||||
assert_eq!(reply["continuity"], json!({"all": "g1"}), "{reply}");
|
||||
// A dome over the rim, not the flat disc a G0 patch of a planar rim is.
|
||||
let disc = std::f64::consts::PI * 100.0;
|
||||
let dome = mesh(&s, "Cap").measure().area;
|
||||
assert!(dome > disc * 1.2, "a dome, not the disc: {dome}");
|
||||
// Through a point 8 above the rim, G1 to the wall: the cap reaches it.
|
||||
run(&mut s, "surface.patch", json!({"edges": rim, "body": "Tube", "continuity": {"all": "g1"}, "points": [[0, 0, 38]], "body_name": "Tall"}));
|
||||
let top = mesh(&s, "Tall").bounds().max.z;
|
||||
assert!(top >= 38.0 - 1e-2, "the cap passes through the point: top {top}");
|
||||
// One edge G2, the rest G0 (the list form).
|
||||
let first = rim[0].clone();
|
||||
let reply =
|
||||
run(&mut s, "surface.patch", json!({"edges": rim, "body": "Tube", "continuity": [{"edge": first, "g": "g2"}], "body_name": "Mixed"}));
|
||||
assert_eq!(reply["continuity"]["edges"][0]["g"], json!("g2"), "{reply}");
|
||||
// Saved and read back: the same patches.
|
||||
let back = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
|
||||
let reopened = Session::new(back);
|
||||
for name in ["Cap", "Tall", "Mixed"] {
|
||||
let (a, b) = (mesh(&s, name).positions, mesh(&reopened, name).positions);
|
||||
assert_eq!(a, b, "{name}: the same surface read back");
|
||||
}
|
||||
assert!(s.doc.to_json().contains("\"continuity\""), "the continuity is saved");
|
||||
// Hostile and misplaced values.
|
||||
for (key, v) in [
|
||||
("continuity", json!("g3")),
|
||||
("continuity", json!("smooth")),
|
||||
("continuity", json!(5)),
|
||||
("continuity", json!([{"edge": "here", "g": "g1"}])),
|
||||
("continuity", json!([{"edge": [f64::INFINITY, 0, 0], "g": "g1"}])),
|
||||
("continuity", json!({"all": "g9"})),
|
||||
("continuity", json!([{"edge": [0, 0, 0], "g": "g1"}])),
|
||||
("points", json!([[0, 0, f64::NAN]])),
|
||||
("points", json!("up")),
|
||||
("points", json!(vec![[0, 0, 35]; 65])),
|
||||
("points", json!([[100, 0, 30]])),
|
||||
] {
|
||||
let mut p = json!({"edges": rim, "body": "Tube"});
|
||||
p[key] = v.clone();
|
||||
assert!(s.execute("surface.patch", &p).is_err(), "accepted {key}: {v}");
|
||||
}
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [30, 0], "p1": [40, 10]}));
|
||||
let sketch_patch = json!({"sketch": 1, "continuity": "g1"});
|
||||
assert!(s.execute("surface.patch", &sketch_patch).is_err(), "continuity on a sketch patch");
|
||||
}
|
||||
}
|
||||
@@ -779,6 +779,8 @@ fn hostile_params_never_panic() {
|
||||
"orientation",
|
||||
"twist",
|
||||
"scale",
|
||||
"continuity",
|
||||
"crown",
|
||||
];
|
||||
// Commands are independent; worker threads take them one at a time (their costs vary a lot).
|
||||
let ids: Vec<&str> = command_specs()
|
||||
|
||||
Reference in new issue
Block a user