solid.cylinder, box, sphere, torus and coil place by expressions: base, corner and center take numbers or length expressions per coordinate ("basin_width/2", "10 mm") and follow parameter changes, renames and component frames; before, a point that was not three plain numbers (or an unreadable axis) was silently replaced by the origin (or Z). The features now store the point as expressions (plain numbers still saved as numbers, so existing designs read and write the same) and evaluate it on every recompute; an unreadable or out-of-range point or axis is refused with an error. Tests: primitive points take expressions, follow parameters, renames and a moved component, and bad points are refused
This commit is contained in:
1 parent
31dadcee63
commit
c61b77613a
13 files changed
+412
-65
No files matched your search
@@ -164,6 +164,39 @@ fn map_geo_ref(m: &Mat, g: &mut crate::construct::GeoRef) {
|
||||
g.map_points(&mut |p: &mut Vec3| *p = apply_point(m, *p), &mut |pl: &mut PlaneRef| map_plane_ref(m, pl));
|
||||
}
|
||||
|
||||
/// Map a point given as expressions by `m`: numbers stay numbers; a coordinate that is an
|
||||
/// expression stays one (`m · (x, y, z) + t`, written out), so it still follows its parameters.
|
||||
pub fn map_point_expr(m: &Mat, p: &mut crate::PointExpr) {
|
||||
let num = |e: &String| e.trim().parse::<f64>().ok().filter(|v| v.is_finite());
|
||||
if let [Some(x), Some(y), Some(z)] = [num(&p[0]), num(&p[1]), num(&p[2])] {
|
||||
*p = crate::point_expr(apply_point(m, Vec3::new(x, y, z)));
|
||||
return;
|
||||
}
|
||||
let old = p.clone();
|
||||
for (row, out) in p.iter_mut().enumerate() {
|
||||
let mut constant = m[3][row];
|
||||
let mut terms: Vec<String> = Vec::new();
|
||||
for (col, e) in old.iter().enumerate() {
|
||||
let c = m[col][row];
|
||||
if c.abs() < 1e-12 {
|
||||
continue;
|
||||
}
|
||||
match num(e) {
|
||||
Some(v) => constant += c * v,
|
||||
None if (c - 1.0).abs() < 1e-12 => terms.push(format!("({e})")),
|
||||
None => terms.push(format!("{c} * ({e})")),
|
||||
}
|
||||
}
|
||||
let mut e = terms.join(" + ");
|
||||
if terms.is_empty() {
|
||||
e = format!("{constant} mm");
|
||||
} else if constant.abs() >= 1e-12 {
|
||||
e = format!("{e} {} {} mm", if constant < 0.0 { '-' } else { '+' }, constant.abs());
|
||||
}
|
||||
*out = e;
|
||||
}
|
||||
}
|
||||
|
||||
impl FeatureKind {
|
||||
/// Map the feature's world geometry (points, directions, explicit planes) by `m`.
|
||||
pub fn transform_geometry(&mut self, m: &Mat) {
|
||||
@@ -207,12 +240,12 @@ impl FeatureKind {
|
||||
map_plane_ref(m, neutral);
|
||||
dir(pull);
|
||||
}
|
||||
FeatureKind::Box { corner, .. } => pt(corner),
|
||||
FeatureKind::Box { corner, .. } => map_point_expr(m, corner),
|
||||
FeatureKind::Cylinder { base, axis, .. } => {
|
||||
pt(base);
|
||||
map_point_expr(m, base);
|
||||
dir(axis);
|
||||
}
|
||||
FeatureKind::Sphere { center, .. } | FeatureKind::Torus { center, .. } => pt(center),
|
||||
FeatureKind::Sphere { center, .. } | FeatureKind::Torus { center, .. } => map_point_expr(m, center),
|
||||
FeatureKind::Pattern { pattern: PatternKind::Rectangular { dir1, dir2, .. }, .. } => {
|
||||
dir(dir1);
|
||||
if let Some(d) = dir2 {
|
||||
@@ -259,7 +292,7 @@ impl FeatureKind {
|
||||
}
|
||||
FeatureKind::SheetBase { .. } | FeatureKind::SheetContour { .. } | FeatureKind::SheetUnfold { .. } => {}
|
||||
FeatureKind::Coil { base, axis, .. } => {
|
||||
pt(base);
|
||||
map_point_expr(m, base);
|
||||
dir(axis);
|
||||
}
|
||||
FeatureKind::ReplaceFace { faces, target, .. } => {
|
||||
|
||||
+67
-10
@@ -226,7 +226,8 @@ pub enum FeatureKind {
|
||||
body: Option<String>,
|
||||
},
|
||||
Box {
|
||||
corner: Vec3,
|
||||
#[serde(with = "point_expr")]
|
||||
corner: PointExpr,
|
||||
length: String,
|
||||
width: String,
|
||||
height: String,
|
||||
@@ -234,7 +235,8 @@ pub enum FeatureKind {
|
||||
operation: Operation,
|
||||
},
|
||||
Cylinder {
|
||||
base: Vec3,
|
||||
#[serde(with = "point_expr")]
|
||||
base: PointExpr,
|
||||
#[serde(default = "z_axis")]
|
||||
axis: Vec3,
|
||||
radius: String,
|
||||
@@ -243,13 +245,15 @@ pub enum FeatureKind {
|
||||
operation: Operation,
|
||||
},
|
||||
Sphere {
|
||||
center: Vec3,
|
||||
#[serde(with = "point_expr")]
|
||||
center: PointExpr,
|
||||
radius: String,
|
||||
#[serde(default)]
|
||||
operation: Operation,
|
||||
},
|
||||
Torus {
|
||||
center: Vec3,
|
||||
#[serde(with = "point_expr")]
|
||||
center: PointExpr,
|
||||
major: String,
|
||||
minor: String,
|
||||
#[serde(default)]
|
||||
@@ -482,7 +486,8 @@ pub enum FeatureKind {
|
||||
/// 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(with = "point_expr")]
|
||||
base: PointExpr,
|
||||
#[serde(default = "z_axis")]
|
||||
axis: Vec3,
|
||||
diameter: String,
|
||||
@@ -939,6 +944,45 @@ fn z_axis() -> Vec3 {
|
||||
Vec3::Z
|
||||
}
|
||||
|
||||
/// A point whose coordinates are length expressions (`["basin_width / 2", "10 mm", "0"]`), so
|
||||
/// it follows parameter changes. Primitives are placed by one.
|
||||
pub type PointExpr = [String; 3];
|
||||
|
||||
/// A point as expressions (plain numbers, mm).
|
||||
pub fn point_expr(p: Vec3) -> PointExpr {
|
||||
[format!("{}", p.x), format!("{}", p.y), format!("{}", p.z)]
|
||||
}
|
||||
|
||||
/// Stored as `[x, y, z]`: plain numbers as numbers (as points were before they took
|
||||
/// expressions, so such designs read and write the same), anything else as a string.
|
||||
mod point_expr {
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Coord {
|
||||
Num(f64),
|
||||
Expr(String),
|
||||
}
|
||||
|
||||
pub fn serialize<S: Serializer>(p: &super::PointExpr, s: S) -> Result<S::Ok, S::Error> {
|
||||
let c = |e: &String| match e.parse::<f64>() {
|
||||
Ok(v) if v.is_finite() && format!("{v}") == *e => Coord::Num(v),
|
||||
_ => Coord::Expr(e.clone()),
|
||||
};
|
||||
[c(&p[0]), c(&p[1]), c(&p[2])].serialize(s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<super::PointExpr, D::Error> {
|
||||
let c = |x: Coord| match x {
|
||||
Coord::Num(v) => format!("{v}"),
|
||||
Coord::Expr(e) => e,
|
||||
};
|
||||
let [x, y, z] = <[Coord; 3]>::deserialize(d)?;
|
||||
Ok([c(x), c(y), c(z)])
|
||||
}
|
||||
}
|
||||
|
||||
impl FeatureKind {
|
||||
/// Sketches the feature is built from (profiles, paths, points): deleting one of them
|
||||
/// deletes the feature. Every kind is listed, so a new one has to say what it uses.
|
||||
@@ -1228,10 +1272,22 @@ impl FeatureKind {
|
||||
v.extend(distance2.iter().map(String::as_str));
|
||||
v.extend(angle.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::Box { length, width, height, .. } => v.extend([length.as_str(), width, height]),
|
||||
FeatureKind::Cylinder { radius, height, .. } => v.extend([radius.as_str(), height]),
|
||||
FeatureKind::Sphere { radius, .. } => v.push(radius),
|
||||
FeatureKind::Torus { major, minor, .. } => v.extend([major.as_str(), minor]),
|
||||
FeatureKind::Box { length, width, height, corner, .. } => {
|
||||
v.extend([length.as_str(), width, height]);
|
||||
v.extend(corner.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::Cylinder { radius, height, base, .. } => {
|
||||
v.extend([radius.as_str(), height]);
|
||||
v.extend(base.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::Sphere { radius, center, .. } => {
|
||||
v.push(radius);
|
||||
v.extend(center.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::Torus { major, minor, center, .. } => {
|
||||
v.extend([major.as_str(), minor]);
|
||||
v.extend(center.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::Combine { .. } => {}
|
||||
FeatureKind::Pattern { pattern, .. } => match pattern {
|
||||
PatternKind::Rectangular { count1, spacing1, count2, spacing2, .. } => {
|
||||
@@ -1308,9 +1364,10 @@ impl FeatureKind {
|
||||
v.extend(gap.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::SheetBase { .. } | FeatureKind::SheetUnfold { .. } | FeatureKind::SheetConvert { .. } => {}
|
||||
FeatureKind::Coil { diameter, pitch, turns, section_size, start_angle, .. } => {
|
||||
FeatureKind::Coil { diameter, pitch, turns, section_size, start_angle, base, .. } => {
|
||||
v.extend([diameter.as_str(), pitch, turns, section_size]);
|
||||
v.extend(start_angle.iter().map(String::as_str));
|
||||
v.extend(base.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::Rib { thickness, depth, .. } => {
|
||||
v.push(thickness);
|
||||
|
||||
+25
-9
@@ -684,6 +684,13 @@ pub(crate) const DESIGN_LEN: &str = "\u{1}design_len";
|
||||
|
||||
/// An expression's value. A length that comes out unit-less without being a bare number (a
|
||||
/// unit-less parameter used as a length) is in the design's units.
|
||||
/// A point given as length expressions.
|
||||
fn point(vals: &BTreeMap<String, Value>, p: &crate::PointExpr) -> Result<Vec3> {
|
||||
let [x, y, z] = p;
|
||||
let v = Vec3::new(val(vals, x, Kind::Length)?, val(vals, y, Kind::Length)?, val(vals, z, Kind::Length)?);
|
||||
if [v.x, v.y, v.z].iter().all(|c| c.is_finite() && c.abs() < 1e9) { Ok(v) } else { Err(DocError::Invalid("the position is out of range".into())) }
|
||||
}
|
||||
|
||||
fn val(vals: &BTreeMap<String, Value>, e: &str, k: Kind) -> Result<f64> {
|
||||
let scale = vals.get(DESIGN_LEN).map(|x| x.v).unwrap_or(1.0);
|
||||
if k == Kind::Length && scale != 1.0 && !expr::is_literal(e) {
|
||||
@@ -1537,6 +1544,7 @@ fn feature_tools(vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -
|
||||
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 base = &point(vals, base)?;
|
||||
let k = axis.normalized().ok_or_else(|| DocError::Invalid("coil axis".into()))?;
|
||||
let (d, p, n, s) = (
|
||||
val(vals, diameter, Kind::Length)?,
|
||||
@@ -1624,14 +1632,15 @@ fn feature_tools(vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -
|
||||
}
|
||||
FeatureKind::Box { corner, length, width, height, .. } => {
|
||||
let s = Vec3::new(val(vals, length, Kind::Length)?, val(vals, width, Kind::Length)?, val(vals, height, Kind::Length)?);
|
||||
Ok(vec![kernel::box_solid(*corner, *corner + s)?])
|
||||
let corner = point(vals, corner)?;
|
||||
Ok(vec![kernel::box_solid(corner, corner + s)?])
|
||||
}
|
||||
FeatureKind::Cylinder { base, axis, radius, height, .. } => {
|
||||
Ok(vec![kernel::cylinder(*base, *axis, val(vals, radius, Kind::Length)?, val(vals, height, Kind::Length)?)?])
|
||||
Ok(vec![kernel::cylinder(point(vals, base)?, *axis, val(vals, radius, Kind::Length)?, val(vals, height, Kind::Length)?)?])
|
||||
}
|
||||
FeatureKind::Sphere { center, radius, .. } => Ok(vec![kernel::sphere(*center, val(vals, radius, Kind::Length)?)?]),
|
||||
FeatureKind::Sphere { center, radius, .. } => Ok(vec![kernel::sphere(point(vals, center)?, val(vals, radius, Kind::Length)?)?]),
|
||||
FeatureKind::Torus { center, major, minor, .. } => {
|
||||
Ok(vec![kernel::torus(*center, val(vals, major, Kind::Length)?, val(vals, minor, Kind::Length)?)?])
|
||||
Ok(vec![kernel::torus(point(vals, center)?, val(vals, major, Kind::Length)?, val(vals, minor, Kind::Length)?)?])
|
||||
}
|
||||
FeatureKind::Hole { diameter, depth, hole, to, .. } => {
|
||||
let (spots, dir) = hole_spots(st, f)?;
|
||||
@@ -2689,19 +2698,20 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
FeatureKind::Box { corner, length, width, height, operation } => {
|
||||
let s = Vec3::new(val(vals, length, Kind::Length)?, val(vals, width, Kind::Length)?, val(vals, height, Kind::Length)?);
|
||||
let b = kernel::box_solid(*corner, *corner + s)?;
|
||||
let corner = point(vals, corner)?;
|
||||
let b = kernel::box_solid(corner, corner + s)?;
|
||||
apply_op(st, f, vec![b], *operation, &[])
|
||||
}
|
||||
FeatureKind::Cylinder { base, axis, radius, height, operation } => {
|
||||
let b = kernel::cylinder(*base, *axis, val(vals, radius, Kind::Length)?, val(vals, height, Kind::Length)?)?;
|
||||
let b = kernel::cylinder(point(vals, base)?, *axis, val(vals, radius, Kind::Length)?, val(vals, height, Kind::Length)?)?;
|
||||
apply_op(st, f, vec![b], *operation, &[])
|
||||
}
|
||||
FeatureKind::Sphere { center, radius, operation } => {
|
||||
let b = kernel::sphere(*center, val(vals, radius, Kind::Length)?)?;
|
||||
let b = kernel::sphere(point(vals, center)?, val(vals, radius, Kind::Length)?)?;
|
||||
apply_op(st, f, vec![b], *operation, &[])
|
||||
}
|
||||
FeatureKind::Torus { center, major, minor, operation } => {
|
||||
let b = kernel::torus(*center, val(vals, major, Kind::Length)?, val(vals, minor, Kind::Length)?)?;
|
||||
let b = kernel::torus(point(vals, center)?, val(vals, major, Kind::Length)?, val(vals, minor, Kind::Length)?)?;
|
||||
apply_op(st, f, vec![b], *operation, &[])
|
||||
}
|
||||
FeatureKind::Combine { target, tools, operation, keep_tools } => {
|
||||
@@ -2970,7 +2980,13 @@ mod chain_tests {
|
||||
fn tangent_chain_follows_smooth_edges() {
|
||||
let mut doc = Document::new("T");
|
||||
doc.add_feature(
|
||||
FeatureKind::Box { corner: Vec3::ZERO, length: "40".into(), width: "30".into(), height: "20".into(), operation: Operation::NewBody },
|
||||
FeatureKind::Box {
|
||||
corner: crate::point_expr(Vec3::ZERO),
|
||||
length: "40".into(),
|
||||
width: "30".into(),
|
||||
height: "20".into(),
|
||||
operation: Operation::NewBody,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -25,7 +25,7 @@ mod project3d;
|
||||
pub use project3d::iso_grid;
|
||||
pub mod sheet;
|
||||
|
||||
pub use assembly::{IDENTITY, Mat, Occurrence, apply_point, apply_vector, is_identity, mat_inverse, mat_mul, rigid};
|
||||
pub use assembly::{IDENTITY, Mat, Occurrence, apply_point, apply_vector, is_identity, map_point_expr, mat_inverse, mat_mul, rigid};
|
||||
pub use document::*;
|
||||
pub use eval::{
|
||||
EdgeRef, FeatureResult, Model, ModelBody, ModelState, SolvedSketch, ThreadInfo, edge_names_for, edge_refs, parse_metric_thread, pattern_matrices,
|
||||
|
||||
@@ -276,6 +276,32 @@ impl FeatureKind {
|
||||
v
|
||||
}
|
||||
|
||||
/// A primitive's placement point (length expressions). Its coordinates are not inputs:
|
||||
/// they get no parameter names and keep bare numbers in mm whatever the design's units, as
|
||||
/// every other position does; renames and users still find them.
|
||||
pub fn point_expr_mut(&mut self) -> Option<&mut crate::PointExpr> {
|
||||
match self {
|
||||
FeatureKind::Box { corner: p, .. }
|
||||
| FeatureKind::Cylinder { base: p, .. }
|
||||
| FeatureKind::Sphere { center: p, .. }
|
||||
| FeatureKind::Torus { center: p, .. }
|
||||
| FeatureKind::Coil { base: p, .. } => Some(p),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only view of [`FeatureKind::point_expr_mut`].
|
||||
pub fn point_expr(&self) -> Option<&crate::PointExpr> {
|
||||
match self {
|
||||
FeatureKind::Box { corner: p, .. }
|
||||
| FeatureKind::Cylinder { base: p, .. }
|
||||
| FeatureKind::Sphere { center: p, .. }
|
||||
| FeatureKind::Torus { center: p, .. }
|
||||
| FeatureKind::Coil { base: p, .. } => Some(p),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only view of [`FeatureKind::inputs_mut`]: (label, expression, kind).
|
||||
pub fn inputs(&self) -> Vec<(&'static str, String, Kind)> {
|
||||
// Imports have no inputs (and their STEP text is large: don't clone it).
|
||||
@@ -655,6 +681,9 @@ impl Document {
|
||||
}
|
||||
}
|
||||
}
|
||||
if f.kind.point_expr().is_some_and(|p| p.iter().any(|e| expr::references(e).iter().any(|r| r == name))) {
|
||||
out.push(ParamUser::Feature { feature: f.id, name: f.name.clone(), input: "Position".into() });
|
||||
}
|
||||
if let FeatureKind::Sketch { sketch, .. } = &f.kind {
|
||||
for c in &sketch.constraints {
|
||||
if c.param.as_deref() == Some(name) {
|
||||
@@ -718,6 +747,13 @@ impl Document {
|
||||
*x = new.to_string();
|
||||
}
|
||||
}
|
||||
for e in f.kind.point_expr_mut().into_iter().flatten() {
|
||||
let r = expr::rename_reference(e, old, new);
|
||||
if r != *e {
|
||||
*e = r;
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
for (_, e, _) in f.kind.inputs_mut() {
|
||||
let r = expr::rename_reference(e, old, new);
|
||||
if r != *e {
|
||||
|
||||
+15
-3
@@ -165,7 +165,13 @@ fn errors_are_reported_not_fatal() {
|
||||
doc.add_feature(FeatureKind::Fillet { edges: vec![Vec3::new(500.0, 0.0, 0.0)], radius: "1".into(), body: None, style: Default::default() }, None)
|
||||
.unwrap();
|
||||
doc.add_feature(
|
||||
FeatureKind::Box { corner: Vec3::ZERO, length: "nope".into(), width: "1".into(), height: "1".into(), operation: Operation::NewBody },
|
||||
FeatureKind::Box {
|
||||
corner: point_expr(Vec3::ZERO),
|
||||
length: "nope".into(),
|
||||
width: "1".into(),
|
||||
height: "1".into(),
|
||||
operation: Operation::NewBody,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -202,13 +208,19 @@ fn params_and_serialization() {
|
||||
fn primitives_and_combine() {
|
||||
let mut doc = Document::new("p");
|
||||
doc.add_feature(
|
||||
FeatureKind::Box { corner: Vec3::ZERO, length: "10".into(), width: "10".into(), height: "10".into(), operation: Operation::NewBody },
|
||||
FeatureKind::Box {
|
||||
corner: point_expr(Vec3::ZERO),
|
||||
length: "10".into(),
|
||||
width: "10".into(),
|
||||
height: "10".into(),
|
||||
operation: Operation::NewBody,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
doc.add_feature(
|
||||
FeatureKind::Cylinder {
|
||||
base: Vec3::new(5.0, 5.0, 0.0),
|
||||
base: point_expr(Vec3::new(5.0, 5.0, 0.0)),
|
||||
axis: Vec3::Z,
|
||||
radius: "2".into(),
|
||||
height: "20".into(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Solid feature commands: extrude, revolve, fillet, chamfer, primitives, combine, move.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_doc::{AxisRef, Direction, Extent, FeatureKind, Operation, ProfileSel, expr::Kind};
|
||||
use solvecraft_doc::{AxisRef, Direction, Extent, FeatureKind, Operation, PointExpr, ProfileSel, expr::Kind, point_expr};
|
||||
use solvecraft_geom::Vec3;
|
||||
|
||||
use super::CommandSpec;
|
||||
@@ -23,10 +23,10 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("sweep")
|
||||
.params("sketch: profile sketch; profiles?; path_sketch: sketch; path: [curve ids in order]; operation?"),
|
||||
CommandSpec::new("solid.loft", "Loft", loft).at("SOLID", "CREATE").icon("loft").params("sections: [{sketch, profiles?} | {sketch, point: id} (an apex)] (2 or more, in order); operation?"),
|
||||
CommandSpec::new("solid.box", "Box", prim_box).at("SOLID", "CREATE").icon("box").params("length, width, height: expr; corner?: [x,y,z] | center?: [x,y,z]; operation?"),
|
||||
CommandSpec::new("solid.cylinder", "Cylinder", prim_cylinder).at("SOLID", "CREATE").icon("cylinder").params("radius | diameter, height: expr; base?: [x,y,z]; axis?: [x,y,z]; operation?"),
|
||||
CommandSpec::new("solid.sphere", "Sphere", prim_sphere).at("SOLID", "CREATE").icon("sphere").params("radius | diameter: expr; center?: [x,y,z]; operation?"),
|
||||
CommandSpec::new("solid.torus", "Torus", prim_torus).at("SOLID", "CREATE").icon("torus").params("major, minor: expr (radii); center?; operation?"),
|
||||
CommandSpec::new("solid.box", "Box", prim_box).at("SOLID", "CREATE").icon("box").params("length, width, height: expr; corner?: [x,y,z] | center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?"),
|
||||
CommandSpec::new("solid.cylinder", "Cylinder", prim_cylinder).at("SOLID", "CREATE").icon("cylinder").params("radius | diameter, height: expr; base?: [x,y,z] (centre of the bottom: numbers in mm or length expressions; default: the origin); axis?: X|Y|Z|[x,y,z] (default Z); operation?"),
|
||||
CommandSpec::new("solid.sphere", "Sphere", prim_sphere).at("SOLID", "CREATE").icon("sphere").params("radius | diameter: expr; center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?"),
|
||||
CommandSpec::new("solid.torus", "Torus", prim_torus).at("SOLID", "CREATE").icon("torus").params("major, minor: expr (radii); center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?"),
|
||||
CommandSpec::new("solid.fillet", "Fillet", fillet)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("fillet")
|
||||
@@ -484,19 +484,59 @@ fn radius_expr(p: &Value, cmd: &str) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A placement point: `[x, y, z]` (or `{x, y, z}`) of numbers (mm) or length expressions;
|
||||
/// the origin when it is not given. Anything else is refused (never read as the origin).
|
||||
pub(super) fn point_param(s: &Session, p: &Value, key: &str, cmd: &str) -> Result<Option<PointExpr>> {
|
||||
let Some(v) = p.get(key) else { return Ok(None) };
|
||||
let want = || bad(cmd, format!("`{key}` must be a point [x, y, z] of numbers or length expressions"));
|
||||
let coords: Vec<&Value> = match v {
|
||||
Value::Array(a) if a.len() == 3 => a.iter().collect(),
|
||||
Value::Object(o) => ["x", "y", "z"].iter().map(|k| o.get(*k)).collect::<Option<Vec<_>>>().ok_or_else(want)?,
|
||||
_ => return Err(want()),
|
||||
};
|
||||
let mut out = PointExpr::default();
|
||||
for (slot, (c, axis)) in out.iter_mut().zip(coords.into_iter().zip(["x", "y", "z"])) {
|
||||
let e = expr(&json!({ "v": c }), "v").ok_or_else(want)?;
|
||||
let x = s.doc.eval(&e, Kind::Length).map_err(|err| bad(cmd, format!("{key} {axis}: {err}")))?;
|
||||
if !(x.is_finite() && x.abs() < 1e9) {
|
||||
return Err(bad(cmd, format!("{key} {axis} is out of range")));
|
||||
}
|
||||
*slot = e;
|
||||
}
|
||||
Ok(Some(out))
|
||||
}
|
||||
|
||||
/// An axis direction: X|Y|Z or a non-zero `[x, y, z]`; `Z` when it is not given.
|
||||
pub(super) fn axis_dir_param(p: &Value, cmd: &str) -> Result<Vec3> {
|
||||
match p.get("axis") {
|
||||
None => Ok(Vec3::Z),
|
||||
Some(Value::String(a)) => match a.trim().to_ascii_uppercase().as_str() {
|
||||
"X" => Ok(Vec3::X),
|
||||
"Y" => Ok(Vec3::Y),
|
||||
"Z" => Ok(Vec3::Z),
|
||||
_ => Err(bad(cmd, "`axis` must be X, Y, Z or [x, y, z]")),
|
||||
},
|
||||
Some(v) => vec3(v).and_then(|a| a.normalized()).ok_or_else(|| bad(cmd, "`axis` must be X, Y, Z or a non-zero [x, y, z]")),
|
||||
}
|
||||
}
|
||||
|
||||
fn prim_box(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.box";
|
||||
let (l, w, h) = (req_expr(cmd, p, "length")?, req_expr(cmd, p, "width")?, req_expr(cmd, p, "height")?);
|
||||
for (e, n) in [(&l, "length"), (&w, "width"), (&h, "height")] {
|
||||
check_expr(s, e, Kind::Length, cmd, n)?;
|
||||
}
|
||||
let corner = match (p.get("corner").and_then(vec3), p.get("center").and_then(vec3)) {
|
||||
let corner = match (point_param(s, p, "corner", cmd)?, point_param(s, p, "center", cmd)?) {
|
||||
(Some(c), _) => c,
|
||||
(None, Some(c)) => {
|
||||
let v = |e: &str| s.doc.eval(e, Kind::Length).unwrap_or(0.0);
|
||||
c - Vec3::new(v(&l), v(&w), v(&h)) * 0.5
|
||||
// The corner half a size back from the centre (numbers stay numbers).
|
||||
(None, Some([cx, cy, cz])) => {
|
||||
let back = |c: String, size: &String| match (c.parse::<f64>(), size.parse::<f64>()) {
|
||||
(Ok(a), Ok(b)) => format!("{}", a - b / 2.0),
|
||||
_ => format!("({c}) - ({size}) / 2"),
|
||||
};
|
||||
[back(cx, &l), back(cy, &w), back(cz, &h)]
|
||||
}
|
||||
_ => Vec3::ZERO,
|
||||
_ => point_expr(Vec3::ZERO),
|
||||
};
|
||||
add_feature(s, p, FeatureKind::Box { corner, length: l, width: w, height: h, operation: operation(p, cmd)? })
|
||||
}
|
||||
@@ -507,8 +547,8 @@ fn prim_cylinder(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let height = req_expr(cmd, p, "height")?;
|
||||
check_expr(s, &radius, Kind::Length, cmd, "radius")?;
|
||||
check_expr(s, &height, Kind::Length, cmd, "height")?;
|
||||
let base = p.get("base").and_then(vec3).unwrap_or(Vec3::ZERO);
|
||||
let axis = p.get("axis").and_then(vec3).unwrap_or(Vec3::Z);
|
||||
let base = point_param(s, p, "base", cmd)?.unwrap_or_else(|| point_expr(Vec3::ZERO));
|
||||
let axis = axis_dir_param(p, cmd)?;
|
||||
add_feature(s, p, FeatureKind::Cylinder { base, axis, radius, height, operation: operation(p, cmd)? })
|
||||
}
|
||||
|
||||
@@ -516,7 +556,7 @@ fn prim_sphere(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.sphere";
|
||||
let radius = radius_expr(p, cmd)?;
|
||||
check_expr(s, &radius, Kind::Length, cmd, "radius")?;
|
||||
let center = p.get("center").and_then(vec3).unwrap_or(Vec3::ZERO);
|
||||
let center = point_param(s, p, "center", cmd)?.unwrap_or_else(|| point_expr(Vec3::ZERO));
|
||||
add_feature(s, p, FeatureKind::Sphere { center, radius, operation: operation(p, cmd)? })
|
||||
}
|
||||
|
||||
@@ -525,7 +565,7 @@ fn prim_torus(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (major, minor) = (req_expr(cmd, p, "major")?, req_expr(cmd, p, "minor")?);
|
||||
check_expr(s, &major, Kind::Length, cmd, "major")?;
|
||||
check_expr(s, &minor, Kind::Length, cmd, "minor")?;
|
||||
let center = p.get("center").and_then(vec3).unwrap_or(Vec3::ZERO);
|
||||
let center = point_param(s, p, "center", cmd)?.unwrap_or_else(|| point_expr(Vec3::ZERO));
|
||||
add_feature(s, p, FeatureKind::Torus { center, major, minor, operation: operation(p, cmd)? })
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use solvecraft_doc::expr::Kind;
|
||||
use solvecraft_geom::Vec3;
|
||||
|
||||
use super::CommandSpec;
|
||||
use super::features::{add_feature, check_expr, feature_sketch, plane_param, profiles};
|
||||
use super::features::{add_feature, axis_dir_param, check_expr, feature_sketch, plane_param, point_param, profiles};
|
||||
use crate::params::{bad, bool_, expr, req_expr, str_, string_list, vec3};
|
||||
use crate::{Result, Session};
|
||||
|
||||
@@ -16,7 +16,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
),
|
||||
CommandSpec::new("solid.coil", "Coil", coil).at("SOLID", "CREATE").icon("coil").params(
|
||||
"diameter; two of revolutions (or turns), height, pitch; section_size; section?: circular|square; section_position?: inside|center|outside; \
|
||||
base?: [x,y,z]; axis?: X|Y|Z|[x,y,z] (default Z); start_angle?; clockwise?: bool; operation?, targets?, name?, body_name?",
|
||||
base?: [x,y,z] (numbers in mm or length expressions; default: the origin); axis?: X|Y|Z|[x,y,z] (default Z); start_angle?; clockwise?: bool; operation?, targets?, name?, body_name?",
|
||||
),
|
||||
CommandSpec::new("solid.rib", "Rib", rib)
|
||||
.at("SOLID", "CREATE")
|
||||
@@ -43,17 +43,8 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
|
||||
fn coil(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.coil";
|
||||
let base = p.get("base").and_then(vec3).unwrap_or(Vec3::ZERO);
|
||||
let axis = match p.get("axis") {
|
||||
None => Vec3::Z,
|
||||
Some(Value::String(a)) => match a.to_ascii_uppercase().as_str() {
|
||||
"X" => Vec3::X,
|
||||
"Y" => Vec3::Y,
|
||||
"Z" => Vec3::Z,
|
||||
_ => return Err(bad(cmd, "`axis` must be X, Y, Z or [x, y, z]")),
|
||||
},
|
||||
Some(v) => vec3(v).and_then(|a| a.normalized()).ok_or_else(|| bad(cmd, "`axis` must be X, Y, Z or a non-zero [x, y, z]"))?,
|
||||
};
|
||||
let base = point_param(s, p, "base", cmd)?.unwrap_or_else(|| solvecraft_doc::point_expr(Vec3::ZERO));
|
||||
let axis = axis_dir_param(p, cmd)?;
|
||||
let diameter = req_expr(cmd, p, "diameter")?;
|
||||
check_expr(s, &diameter, Kind::Length, cmd, "diameter")?;
|
||||
let turns = expr(p, "revolutions").or_else(|| expr(p, "turns"));
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//! without moved components).
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
use solvecraft_doc::{Mat, apply_point, apply_vector, is_identity, mat_inverse};
|
||||
use solvecraft_doc::{Mat, apply_point, apply_vector, is_identity, map_point_expr, mat_inverse};
|
||||
use solvecraft_geom::Vec3;
|
||||
|
||||
use crate::Session;
|
||||
@@ -203,11 +203,29 @@ impl Mapper<'_> {
|
||||
}
|
||||
|
||||
fn make(&mut self, v: &Value) -> Option<Value> {
|
||||
let p = vec3(v)?;
|
||||
let m = self.f.active;
|
||||
let Some(p) = vec3(v) else { return self.make_expr(&m, v) };
|
||||
self.local_point(&m, p)
|
||||
}
|
||||
|
||||
/// A point with expression coordinates (`["w / 2", 0, 0]`): mapped as expressions.
|
||||
fn make_expr(&mut self, m: &Mat, v: &Value) -> Option<Value> {
|
||||
let a = v.as_array().filter(|a| a.len() == 3)?;
|
||||
let c = |i: usize| match a.get(i)? {
|
||||
Value::Number(n) => n.as_f64().filter(|x| x.is_finite()).map(|x| format!("{x}")),
|
||||
Value::String(e) => Some(e.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let mut p = [c(0)?, c(1)?, c(2)?];
|
||||
if is_identity(m) {
|
||||
return None;
|
||||
}
|
||||
let inv = mat_inverse(m)?;
|
||||
map_point_expr(&inv, &mut p);
|
||||
self.changed = true;
|
||||
Some(serde_json::json!(p))
|
||||
}
|
||||
|
||||
fn dir(&mut self, v: &Value) -> Option<Value> {
|
||||
let d = vec3(v)?;
|
||||
let m = self.first.unwrap_or(self.f.active);
|
||||
|
||||
@@ -36,6 +36,7 @@ fn join_extrude_from_a_chamfered_top_face() {
|
||||
}
|
||||
|
||||
mod component_frames;
|
||||
mod primitive_points;
|
||||
mod robustness;
|
||||
|
||||
/// Sketch a 40 x 30 rectangle with dimensions, extrude 20, fillet a vertical edge, cut a hole.
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Primitive placement points (`base`, `corner`, `center`) take expressions like their sizes do,
|
||||
//! follow parameter changes, and are refused when they cannot be read (user report: a cylinder
|
||||
//! with `base: ["basin_width/2", …]` silently landed at the origin).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::run;
|
||||
use crate::*;
|
||||
|
||||
fn bbox(s: &Session, body: &str) -> ([f64; 3], [f64; 3]) {
|
||||
let b = s.model.state().body(body).map(|b| b.body.clone()).unwrap();
|
||||
let m = solvecraft_kernel::measure(&b).unwrap();
|
||||
([m.bbox.min.x, m.bbox.min.y, m.bbox.min.z], [m.bbox.max.x, m.bbox.max.y, m.bbox.max.z])
|
||||
}
|
||||
|
||||
fn near(a: [f64; 3], b: [f64; 3]) -> bool {
|
||||
a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-3)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cylinder_base_expressions_follow_parameters() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.change", json!({"name": "basin_width", "expression": "100 mm"}));
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 5, "height": 10, "base": ["basin_width/2", "10 mm", "-2"], "body_name": "Pin"}));
|
||||
let (lo, hi) = bbox(&s, "Pin");
|
||||
assert!(near(lo, [45.0, 5.0, -2.0]) && near(hi, [55.0, 15.0, 8.0]), "{lo:?} {hi:?}");
|
||||
// A parameter change moves it on recompute.
|
||||
run(&mut s, "parameters.change", json!({"name": "basin_width", "expression": "60 mm"}));
|
||||
let (lo, _) = bbox(&s, "Pin");
|
||||
assert!(near(lo, [25.0, 5.0, -2.0]), "{lo:?}");
|
||||
// And it survives a save and reopen.
|
||||
let text = s.doc.to_json();
|
||||
assert!(text.contains("basin_width/2"), "{text}");
|
||||
let s2 = Session::new(solvecraft_doc::Document::from_json(&text).unwrap());
|
||||
let (lo, _) = bbox(&s2, "Pin");
|
||||
assert!(near(lo, [25.0, 5.0, -2.0]), "{lo:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn primitive_points_take_expressions() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.change", json!({"name": "w", "expression": "40 mm"}));
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": ["w", 0, 0], "body_name": "A"}));
|
||||
run(&mut s, "solid.box", json!({"length": "w", "width": 10, "height": 10, "center": [0, "w", 0], "body_name": "B"}));
|
||||
run(&mut s, "solid.sphere", json!({"radius": 2, "center": ["w * 2", 0, 0], "body_name": "C"}));
|
||||
run(&mut s, "solid.torus", json!({"major": 5, "minor": 1, "center": [0, 0, "w"], "body_name": "D"}));
|
||||
run(&mut s, "solid.coil", json!({"diameter": 10, "revolutions": 2, "pitch": 5, "section_size": 1, "base": ["-w", 0, 0], "body_name": "E"}));
|
||||
assert!(near(bbox(&s, "A").0, [40.0, 0.0, 0.0]));
|
||||
assert!(near(bbox(&s, "B").0, [-20.0, 35.0, -5.0]), "{:?}", bbox(&s, "B"));
|
||||
assert!(near(bbox(&s, "C").0, [78.0, -2.0, -2.0]));
|
||||
assert!(near(bbox(&s, "D").0, [-6.0, -6.0, 39.0]));
|
||||
assert!((bbox(&s, "E").0[0] + 45.5).abs() < 0.2, "{:?}", bbox(&s, "E"));
|
||||
run(&mut s, "parameters.change", json!({"name": "w", "expression": "20 mm"}));
|
||||
assert!(near(bbox(&s, "A").0, [20.0, 0.0, 0.0]));
|
||||
assert!(near(bbox(&s, "B").0, [-10.0, 15.0, -5.0]), "{:?}", bbox(&s, "B"));
|
||||
assert!(near(bbox(&s, "C").0, [38.0, -2.0, -2.0]));
|
||||
assert!(near(bbox(&s, "D").0, [-6.0, -6.0, 19.0]));
|
||||
assert!((bbox(&s, "E").0[0] + 25.5).abs() < 0.2, "{:?}", bbox(&s, "E"));
|
||||
}
|
||||
|
||||
/// Points and axes that cannot be read are refused, never silently put at the origin.
|
||||
#[test]
|
||||
fn unreadable_primitive_points_are_refused() {
|
||||
let mut s = Session::default();
|
||||
let cases: Vec<(&str, Value)> = vec![
|
||||
("solid.cylinder", json!({"radius": 5, "height": 10, "base": ["nope/2", 0, 0]})),
|
||||
("solid.cylinder", json!({"radius": 5, "height": 10, "base": [1, 2]})),
|
||||
("solid.cylinder", json!({"radius": 5, "height": 10, "base": "XY"})),
|
||||
("solid.cylinder", json!({"radius": 5, "height": 10, "base": [1, 2, "5 deg"]})),
|
||||
("solid.cylinder", json!({"radius": 5, "height": 10, "axis": ["x", 0, 1]})),
|
||||
("solid.cylinder", json!({"radius": 5, "height": 10, "axis": [0, 0, 0]})),
|
||||
("solid.box", json!({"length": 1, "width": 1, "height": 1, "corner": [0, null, 0]})),
|
||||
("solid.box", json!({"length": 1, "width": 1, "height": 1, "center": {"x": 1}})),
|
||||
("solid.sphere", json!({"radius": 1, "center": ["a +", 0, 0]})),
|
||||
("solid.torus", json!({"major": 5, "minor": 1, "center": [1e300, 0, 0]})),
|
||||
("solid.coil", json!({"diameter": 10, "revolutions": 2, "pitch": 5, "section_size": 1, "base": [true, 0, 0]})),
|
||||
];
|
||||
for (cmd, p) in cases {
|
||||
let before = s.doc.features.len();
|
||||
assert!(s.execute(cmd, &p).is_err(), "{cmd} {p} should be refused");
|
||||
assert_eq!(s.doc.features.len(), before, "{cmd} {p} added a feature");
|
||||
}
|
||||
}
|
||||
|
||||
/// Renaming a parameter follows into placement points; deleting it is refused while a point
|
||||
/// uses it. Plain-number points are stored as numbers, as before.
|
||||
#[test]
|
||||
fn point_parameters_rename_and_store() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.change", json!({"name": "off", "expression": "30 mm"}));
|
||||
run(&mut s, "solid.sphere", json!({"radius": 2, "center": ["off", 0, 0], "body_name": "S"}));
|
||||
run(&mut s, "solid.box", json!({"length": 1, "width": 1, "height": 1, "corner": [1.5, -2, 0], "body_name": "B"}));
|
||||
assert!(s.execute("parameters.delete", &json!({"name": "off"})).is_err());
|
||||
run(&mut s, "parameters.rename", json!({"name": "off", "new_name": "shift"}));
|
||||
run(&mut s, "parameters.change", json!({"name": "shift", "expression": "10 mm"}));
|
||||
assert!(near(bbox(&s, "S").0, [8.0, -2.0, -2.0]), "{:?}", bbox(&s, "S"));
|
||||
let v: Value = serde_json::from_str(&s.doc.to_json()).unwrap();
|
||||
let feats = v["features"].as_array().unwrap();
|
||||
assert_eq!(feats[0]["center"], json!(["shift", 0.0, 0.0]), "{v}");
|
||||
assert_eq!(feats[1]["corner"], json!([1.5, -2.0, 0.0]), "{v}");
|
||||
}
|
||||
|
||||
/// A primitive placed by an expression in a moved component lands in that component's frame
|
||||
/// and keeps following its parameter.
|
||||
#[test]
|
||||
fn point_expressions_in_a_moved_component() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.change", json!({"name": "x0", "expression": "10 mm"}));
|
||||
let c = run(&mut s, "component.create", json!({"name": "Arm"}));
|
||||
let (cid, occ) = (c["component"].as_u64().unwrap(), c["occurrence"].as_u64().unwrap());
|
||||
run(&mut s, "component.activate", json!({"component": 0}));
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": occ, "translate": [0, 50, 0]}));
|
||||
run(&mut s, "component.activate", json!({"component": cid}));
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 1, "height": 2, "base": ["x0", 60, 0], "body_name": "Pin"}));
|
||||
let wlo = |s: &Session| s.world_state().body("Pin").unwrap().mesh().bounds().min;
|
||||
let lo = wlo(&s);
|
||||
assert!((lo.x - 9.0).abs() < 1e-3 && (lo.y - 59.0).abs() < 1e-3, "{lo:?}");
|
||||
run(&mut s, "parameters.change", json!({"name": "x0", "expression": "20 mm"}));
|
||||
let lo = wlo(&s);
|
||||
assert!((lo.x - 19.0).abs() < 1e-3 && (lo.y - 59.0).abs() < 1e-3, "{lo:?}");
|
||||
}
|
||||
+22
-5
@@ -425,13 +425,19 @@ pub const MAX_STL_TRIANGLES: usize = 20_000_000;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solvecraft_doc::{FeatureKind, Model, Operation};
|
||||
use solvecraft_doc::{FeatureKind, Model, Operation, point_expr};
|
||||
use solvecraft_geom::Vec3;
|
||||
|
||||
fn model() -> (Document, Model) {
|
||||
let mut doc = Document::new("t");
|
||||
doc.add_feature(
|
||||
FeatureKind::Box { corner: Vec3::ZERO, length: "10".into(), width: "20".into(), height: "30".into(), operation: Operation::NewBody },
|
||||
FeatureKind::Box {
|
||||
corner: point_expr(Vec3::ZERO),
|
||||
length: "10".into(),
|
||||
width: "20".into(),
|
||||
height: "30".into(),
|
||||
operation: Operation::NewBody,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -558,7 +564,13 @@ mod tests {
|
||||
fn threemf_round_trip() {
|
||||
let mut doc = Document::new("t");
|
||||
doc.add_feature(
|
||||
FeatureKind::Box { corner: Vec3::ZERO, length: "40".into(), width: "30".into(), height: "10".into(), operation: Operation::NewBody },
|
||||
FeatureKind::Box {
|
||||
corner: point_expr(Vec3::ZERO),
|
||||
length: "40".into(),
|
||||
width: "30".into(),
|
||||
height: "10".into(),
|
||||
operation: Operation::NewBody,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -569,7 +581,7 @@ mod tests {
|
||||
.unwrap();
|
||||
doc.add_feature(
|
||||
FeatureKind::Cylinder {
|
||||
base: Vec3::new(80.0, 0.0, 0.0),
|
||||
base: point_expr(Vec3::new(80.0, 0.0, 0.0)),
|
||||
axis: Vec3::Z,
|
||||
radius: "6".into(),
|
||||
height: "15".into(),
|
||||
@@ -579,7 +591,12 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
doc.add_feature(
|
||||
FeatureKind::Torus { center: Vec3::new(0.0, 80.0, 0.0), major: "12".into(), minor: "3".into(), operation: Operation::NewBody },
|
||||
FeatureKind::Torus {
|
||||
center: point_expr(Vec3::new(0.0, 80.0, 0.0)),
|
||||
major: "12".into(),
|
||||
minor: "3".into(),
|
||||
operation: Operation::NewBody,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -2801,6 +2801,11 @@ fn pt3(v: Vec3) -> Value {
|
||||
json!([v.x, v.y, v.z])
|
||||
}
|
||||
|
||||
/// A point of expressions: numbers as numbers, expressions as strings.
|
||||
fn pt_expr(p: &solvecraft_engine::doc::PointExpr) -> Value {
|
||||
Value::Array(p.iter().map(|e| e.parse::<f64>().ok().filter(|x| x.is_finite()).map_or_else(|| json!(e), |x| json!(x))).collect())
|
||||
}
|
||||
|
||||
/// A dialog that edits an existing feature, filled from it. The timeline is rolled back to just
|
||||
/// before the feature, so its references show on the geometry they refer to.
|
||||
pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dialog> {
|
||||
@@ -3008,7 +3013,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
fields: vec![("length", length.clone()), ("width", width.clone()), ("height", height.clone())],
|
||||
operation: op_index(operation),
|
||||
};
|
||||
d.extra.insert("corner".into(), pt3(*corner));
|
||||
d.extra.insert("corner".into(), pt_expr(corner));
|
||||
d
|
||||
}
|
||||
FeatureKind::Cylinder { base, axis, radius, height, operation } => {
|
||||
@@ -3018,14 +3023,14 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
fields: vec![("radius", radius.clone()), ("height", height.clone())],
|
||||
operation: op_index(operation),
|
||||
};
|
||||
d.extra.insert("base".into(), pt3(*base));
|
||||
d.extra.insert("base".into(), pt_expr(base));
|
||||
d.extra.insert("axis".into(), pt3(*axis));
|
||||
d
|
||||
}
|
||||
FeatureKind::Sphere { center, radius, operation } => {
|
||||
let mut d = start("solid.sphere")?;
|
||||
d.kind = Kind::Primitive { cmd: "solid.sphere", fields: vec![("radius", radius.clone())], operation: op_index(operation) };
|
||||
d.extra.insert("center".into(), pt3(*center));
|
||||
d.extra.insert("center".into(), pt_expr(center));
|
||||
d
|
||||
}
|
||||
FeatureKind::Torus { center, major, minor, operation } => {
|
||||
@@ -3035,7 +3040,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
fields: vec![("major", major.clone()), ("minor", minor.clone())],
|
||||
operation: op_index(operation),
|
||||
};
|
||||
d.extra.insert("center".into(), pt3(*center));
|
||||
d.extra.insert("center".into(), pt_expr(center));
|
||||
d
|
||||
}
|
||||
FeatureKind::BoundaryFill { tools, cells, operation, remove_tools } => {
|
||||
|
||||
Reference in new issue
Block a user