Fix modelling regressions and reconcile WIP UI work
This commit is contained in:
1 parent
49bd89de13
commit
d6cdec40da
67 files changed
+1793
-260
No files matched your search
@@ -495,7 +495,9 @@ mod tests {
|
||||
assert!((zmax(&mut s) - 5.0).abs() < 1e-6);
|
||||
run(&mut s, "sketch.redefine", json!({"sketch": sk, "plane": "XZ"}));
|
||||
let z = zmax(&mut s);
|
||||
assert!(z > 9.0, "the profile stands up on XZ: {z}");
|
||||
assert!(z.abs() < 1e-6, "positive sketch Y on XZ extends toward -Z: {z}");
|
||||
let ymin = run(&mut s, "inspect.measure", json!({}))["bodies"][0]["bbox"]["min"][2].as_f64().unwrap();
|
||||
assert!((ymin + 20.0).abs() < 1e-6);
|
||||
assert!(s.execute("sketch.redefine", &json!({"sketch": sk, "plane": "nope"})).is_err());
|
||||
assert!(s.execute("sketch.redefine", &json!({"sketch": 999, "plane": "XY"})).is_err());
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ fn cyl(s: &mut Session, d: f64, z0: f64, z1: f64, op: &str) -> Result<()> {
|
||||
/// A solid of revolution about z from a closed (r, z) outline, as one body or joined.
|
||||
fn revolve(s: &mut Session, pts: &[(f64, f64)], op: &str) -> Result<()> {
|
||||
run(s, "sketch.create", json!({"plane": "XZ"}))?;
|
||||
let points: Vec<[f64; 2]> = pts.iter().map(|(r, z)| [*r, *z]).collect();
|
||||
let points: Vec<[f64; 2]> = pts.iter().map(|(r, z)| [*r, -*z]).collect();
|
||||
run(s, "sketch.line", json!({"points": points, "closed": true}))?;
|
||||
run(s, "sketch.finish", json!({}))?;
|
||||
run(s, "solid.revolve", json!({"axis": "y", "operation": op}))?;
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{Result, Session};
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] =
|
||||
&[CommandSpec::new("inspect.section", "Section Analysis", section).at("SOLID", "INSPECT").icon("section").noundo().params(
|
||||
"plane: XY|XZ|YZ | {origin, normal}; offset?: expr along the plane's normal (XY: +Z, XZ: -Y, YZ: +X); \
|
||||
"plane: XY|XZ|YZ | {origin, normal}; offset?: expr along the plane's normal (XY: +Z, XZ: +Y, YZ: +X); \
|
||||
at?: expr, the cut's world coordinate on the named plane's axis (XY: Z, XZ: Y, YZ: X), instead of offset; \
|
||||
flip?: bool; or clear: true. Returns the cut's origin and normal, and for a named plane its axis and at",
|
||||
)];
|
||||
|
||||
@@ -359,8 +359,10 @@ pub(super) fn plane_ref(s: &Session, p: &Value, cmd: &str) -> Result<PlaneRef> {
|
||||
}
|
||||
Some(v @ Value::Object(o)) => {
|
||||
if let Some(fp) = o.get("face").and_then(vec3) {
|
||||
// A planar face of a body at this point.
|
||||
let st = s.model.state();
|
||||
// A planar face of a body at this point, as the active component sees the model
|
||||
// (another component's face where it is placed).
|
||||
let model = s.model.state();
|
||||
let st = solvecraft_doc::state_in_frame(&s.doc, &model, s.active_component);
|
||||
let mut found = None;
|
||||
let mut face_name = None;
|
||||
for b in &st.bodies {
|
||||
@@ -406,7 +408,10 @@ fn face_contains(m: &solvecraft_geom::Mesh, fi: usize, p: Vec3, tol: f64) -> boo
|
||||
let s1 = (b - a).cross(p - a).dot(n);
|
||||
let s2 = (c - b).cross(p - b).dot(n);
|
||||
let s3 = (a - c).cross(p - c).dot(n);
|
||||
(s1 >= 0.0 && s2 >= 0.0 && s3 >= 0.0) || (s1 <= 0.0 && s2 <= 0.0 && s3 <= 0.0)
|
||||
(s1 >= 0.0 && s2 >= 0.0 && s3 >= 0.0)
|
||||
|| (s1 <= 0.0 && s2 <= 0.0 && s3 <= 0.0)
|
||||
// Exact curved boundaries can sit just outside their tessellated chords.
|
||||
|| p.dist_to_segment(a, b).min(p.dist_to_segment(b, c)).min(p.dist_to_segment(c, a)) <= tol * 0.1
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ fn make(s: &mut Session, p: &Value, cmd: &str, decal: bool) -> Result<Value> {
|
||||
return Err(bad(cmd, "`width` must be positive"));
|
||||
}
|
||||
let opacity = num(p, "opacity").unwrap_or(if decal { 1.0 } else { 0.5 }).clamp(0.0, 1.0);
|
||||
// It belongs to the active component (its plane is in that frame) and moves with it.
|
||||
let component = s.active_component;
|
||||
let doc = s.doc_mut();
|
||||
let id = doc.canvases.iter().map(|c| c.id).max().unwrap_or(0) + 1;
|
||||
let base = if decal { "Decal" } else { "Canvas" };
|
||||
@@ -74,6 +76,7 @@ fn make(s: &mut Session, p: &Value, cmd: &str, decal: bool) -> Result<Value> {
|
||||
opacity,
|
||||
flip: bool_(p, "flip").unwrap_or(false),
|
||||
visible: true,
|
||||
component,
|
||||
});
|
||||
Ok(json!({"canvas": id, "name": name, "pixels": pixels}))
|
||||
}
|
||||
|
||||
@@ -813,6 +813,12 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let ci = sk.add_circle(cc, nr, Some(c), None)?;
|
||||
made.push(ci);
|
||||
sk.offsets.push(solvecraft_sketch::OffsetChain {
|
||||
sources: vec![(ids[0].clone(), true)],
|
||||
outputs: ids_of(sk, &made),
|
||||
distance: r - nr,
|
||||
closed: true,
|
||||
});
|
||||
return Ok((ids_of(sk, &made), cons));
|
||||
}
|
||||
let pool: Vec<usize> = if use_chain {
|
||||
@@ -916,7 +922,13 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((ids_of(sk, &made), cons))
|
||||
let outputs = ids_of(sk, &made);
|
||||
if pieces.iter().all(|p| !matches!(p, Piece::Free(_))) {
|
||||
let sources = order.iter().filter_map(|(i, forward)| sk.curves.get(*i).map(|c| (c.id.clone(), *forward))).collect();
|
||||
sk.offsets.push(solvecraft_sketch::OffsetChain { sources, outputs: outputs.clone(), distance: d, closed });
|
||||
sk.refresh_offsets()?;
|
||||
}
|
||||
Ok((outputs, cons))
|
||||
})?;
|
||||
Ok(json!({"curves": out.0, "constraints": out.1, "sketch": info}))
|
||||
}
|
||||
|
||||
@@ -140,8 +140,20 @@ pub(super) fn add_link(s: &Session, doc: &Document, id: u64, sk: &mut Sketch, ki
|
||||
let plane = sketch_plane(doc, id)?;
|
||||
let (vals, _) = doc.param_values();
|
||||
let st = s.model.state_before(id);
|
||||
// The model as the sketch's component sees it.
|
||||
let comp = doc.feature(id).map(|f| f.component).unwrap_or(0);
|
||||
let st = solvecraft_doc::state_in_frame(doc, &st, comp);
|
||||
let r = resolve(doc, &vals, &st, &plane, kind, &src)?;
|
||||
Ok(sk.add_link_with_wires(kind, r.source, &r.geom, &r.wires)?)
|
||||
let name = if let LinkSource::Face { at, .. } = &r.source {
|
||||
solvecraft_doc::naming::face_names_at(&st, &[*at]).into_iter().next().filter(|n| !n.is_empty())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let link = sk.add_link_with_wires(kind, r.source, &r.geom, &r.wires)?;
|
||||
if let Some(l) = sk.links.iter_mut().find(|l| l.id == link) {
|
||||
l.face_name = name;
|
||||
}
|
||||
Ok(link)
|
||||
}
|
||||
|
||||
fn link_json(sk: &Sketch, link: &str) -> Value {
|
||||
|
||||
@@ -30,6 +30,12 @@ enum K {
|
||||
Plane,
|
||||
/// A body name, or a list of them.
|
||||
Body,
|
||||
/// A point (or a list of picks) for the sketch being made or edited: into that sketch's
|
||||
/// component frame, whichever component the geometry under it belongs to (a sketch in the
|
||||
/// root on a moved component's face finds the face where it is shown).
|
||||
Here,
|
||||
/// A plane object for a new sketch or canvas: into the active component's frame.
|
||||
HerePlane,
|
||||
}
|
||||
|
||||
use K::*;
|
||||
@@ -56,11 +62,12 @@ fn table(id: &str) -> &'static [(&'static str, K)] {
|
||||
"plastic.lip" | "sheet.convert" => &[("face", Pick), ("body", Body)],
|
||||
"sheet.flange" | "sheet.hem" => &[("edges", Picks), ("body", Body)],
|
||||
"sheet.fold" => &[("points", Picks), ("fixed", Pick), ("body", Body)],
|
||||
"sketch.create" | "sketch.redefine" => &[("plane", Plane)],
|
||||
"canvas.decal" => &[("face", Pick)],
|
||||
"sketch.intersect" => &[("refs", Picks)],
|
||||
"sketch.project_to_surface" | "sketch.isoparametric_curve" => &[("face", Pick), ("body", Body)],
|
||||
"sketch.intersection_curve" => &[("a", Picks), ("b", Picks)],
|
||||
"sketch.create" | "sketch.redefine" => &[("plane", HerePlane)],
|
||||
"canvas.decal" => &[("face", Here)],
|
||||
"canvas.insert" => &[("plane", HerePlane), ("at", Here)],
|
||||
"sketch.project" | "sketch.intersect" | "sketch.include_3d" => &[("refs", Here), ("ref", Here)],
|
||||
"sketch.project_to_surface" | "sketch.isoparametric_curve" => &[("face", Here)],
|
||||
"sketch.intersection_curve" => &[("a", Here), ("b", Here)],
|
||||
"inspect.curvature_comb" | "inspect.minimum_radius" | "inspect.isocurve" => &[("edges", Picks), ("faces", Picks)],
|
||||
"parts.insert" | "parts.fastener" => &[("at", Pick), ("point", Make), ("direction", Dir)],
|
||||
"appearance.assign" => &[("faces", Picks), ("bodies", Body), ("body", Body)],
|
||||
@@ -75,6 +82,8 @@ fn table(id: &str) -> &'static [(&'static str, K)] {
|
||||
struct Frames {
|
||||
bodies: Vec<(String, String, Mat)>,
|
||||
active: Mat,
|
||||
/// The frame of the sketch being edited (its component), else the active component's.
|
||||
sketch: Mat,
|
||||
}
|
||||
|
||||
fn frames(s: &Session) -> Frames {
|
||||
@@ -104,7 +113,8 @@ fn frames(s: &Session) -> Frames {
|
||||
(world, own, m)
|
||||
})
|
||||
.collect();
|
||||
Frames { bodies, active: doc.component_transform(s.active_component) }
|
||||
let sketch_comp = s.active_sketch.and_then(|id| doc.feature(id)).map(|f| f.component).unwrap_or(s.active_component);
|
||||
Frames { bodies, active: doc.component_transform(s.active_component), sketch: doc.component_transform(sketch_comp) }
|
||||
}
|
||||
|
||||
/// Does any placement move anything (so the world differs from the components' frames)?
|
||||
@@ -171,12 +181,14 @@ struct Mapper<'a> {
|
||||
/// The frame of the command's first pick (its directions turn with it).
|
||||
first: Option<Mat>,
|
||||
changed: bool,
|
||||
/// Picks go into the sketch's frame (`Here`), not the frame of the body under them.
|
||||
here: bool,
|
||||
}
|
||||
|
||||
impl Mapper<'_> {
|
||||
fn pick(&mut self, v: &Value, body: Option<&str>) -> Option<Value> {
|
||||
let p = vec3(v)?;
|
||||
let m = frame_at(self.s, &self.f, p, body).unwrap_or(self.f.active);
|
||||
let m = if self.here { self.f.sketch } else { frame_at(self.s, &self.f, p, body).unwrap_or(self.f.active) };
|
||||
self.first.get_or_insert(m);
|
||||
self.local_point(&m, p)
|
||||
}
|
||||
@@ -325,14 +337,14 @@ pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
|
||||
return None;
|
||||
}
|
||||
let obj = params.as_object()?;
|
||||
let mut m = Mapper { s, f: frames(s), first: None, changed: false };
|
||||
let mut m = Mapper { s, f: frames(s), first: None, changed: false, here: false };
|
||||
let body = obj.get("body").and_then(Value::as_str).map(str::to_string);
|
||||
let mut out: Map<String, Value> = obj.clone();
|
||||
// Picks first: their frame turns the directions.
|
||||
for pass in [true, false] {
|
||||
for (k, kind) in keys {
|
||||
let Some(v) = obj.get(*k) else { continue };
|
||||
let is_pick = matches!(kind, Pick | Picks | Plane);
|
||||
let is_pick = matches!(kind, Pick | Picks | Plane | Here | HerePlane);
|
||||
if is_pick != pass {
|
||||
continue;
|
||||
}
|
||||
@@ -343,6 +355,19 @@ pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
|
||||
Dir => m.dir(v),
|
||||
Plane => m.plane(v),
|
||||
Body => m.body(v),
|
||||
Here | HerePlane => {
|
||||
// A new sketch or canvas goes into the active component.
|
||||
let keep = m.f.sketch;
|
||||
if *kind == HerePlane {
|
||||
m.f.sketch = m.f.active;
|
||||
}
|
||||
m.here = true;
|
||||
// Body names stay: the sketch's view of the model keeps them.
|
||||
let r = if *kind == HerePlane { m.plane(v) } else { m.picks(v, None) };
|
||||
m.here = false;
|
||||
m.f.sketch = keep;
|
||||
r
|
||||
}
|
||||
};
|
||||
if let Some(n) = new {
|
||||
out.insert((*k).to_string(), n);
|
||||
|
||||
@@ -144,3 +144,106 @@ fn sketch_on_a_face_follows_a_moved_body() {
|
||||
let c = sketch_point_world(&mut s, "OnTop", "p1");
|
||||
assert!((c.z - 25.0).abs() < 1e-6, "{c:?}");
|
||||
}
|
||||
|
||||
/// A 1 x 1 PNG for canvases.
|
||||
const PNG: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
|
||||
|
||||
/// Canvases belong to the active component and move with its occurrence; one put on a moved
|
||||
/// component's face from the root sits on the face (and stays the root's).
|
||||
#[test]
|
||||
fn canvases_move_with_their_component() {
|
||||
let mut s = moved_part();
|
||||
let part = s.active_component;
|
||||
assert_ne!(part, 0);
|
||||
run(&mut s, "canvas.insert", json!({"data": PNG, "plane": "XY", "name": "Plan"}));
|
||||
// From the root, on the moved block's top face (world).
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
run(&mut s, "canvas.insert", json!({"data": PNG, "plane": {"face": [65, 10, 10]}, "name": "Lid", "width": 10}));
|
||||
run(&mut s, "canvas.insert", json!({"data": PNG, "plane": "XZ", "name": "Root"}));
|
||||
let (vals, _) = s.doc.param_values();
|
||||
let world = |s: &Session, name: &str| {
|
||||
let c = s.doc.canvases.iter().find(|c| c.name == name).unwrap();
|
||||
(c.component, s.doc.canvas_plane(&vals, c).unwrap())
|
||||
};
|
||||
let (c, p) = world(&s, "Plan");
|
||||
assert_eq!(c, part);
|
||||
assert!(near(p.origin, Vec3::new(50.0, 0.0, 0.0)), "{:?}", p.origin);
|
||||
let (c, p) = world(&s, "Lid");
|
||||
assert_eq!(c, 0, "the active component's");
|
||||
let lid = s.doc.canvases.iter().find(|c| c.name == "Lid").unwrap();
|
||||
let centre = p.to_world(lid.center);
|
||||
assert!(near(centre, Vec3::new(65.0, 10.0, 10.0)), "{centre:?}");
|
||||
assert_eq!(world(&s, "Root").0, 0);
|
||||
// Moving the occurrence carries its canvases, not the root's.
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": "Part:1", "translate": [0, 40, 0]}));
|
||||
let (vals, _) = s.doc.param_values();
|
||||
let plan = s.doc.canvases.iter().find(|c| c.name == "Plan").unwrap();
|
||||
assert!(near(s.doc.canvas_plane(&vals, plan).unwrap().origin, Vec3::new(50.0, 40.0, 0.0)));
|
||||
let lid = s.doc.canvases.iter().find(|c| c.name == "Lid").unwrap();
|
||||
let centre = s.doc.canvas_plane(&vals, lid).unwrap().to_world(lid.center);
|
||||
assert!(near(centre, Vec3::new(65.0, 10.0, 10.0)), "{centre:?}");
|
||||
let root = s.doc.canvases.iter().find(|c| c.name == "Root").unwrap();
|
||||
assert!(near(s.doc.canvas_plane(&vals, root).unwrap().origin, Vec3::ZERO));
|
||||
// The component survives a save and reopen.
|
||||
let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
|
||||
assert_eq!(back.canvases, s.doc.canvases);
|
||||
}
|
||||
|
||||
/// World points of a sketch's curves' points (sorted).
|
||||
fn sketch_world_points(s: &mut Session, sketch: &str) -> Vec<Vec3> {
|
||||
let si = run(s, "sketch.inspect", json!({ "sketch": sketch }));
|
||||
si["points"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let w = &p["world"];
|
||||
Vec3::new(w[0].as_f64().unwrap(), w[1].as_f64().unwrap(), w[2].as_f64().unwrap())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A block "C" (40 x 30 x 20) in its own component, the root active again.
|
||||
fn block_in_component() -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "component.create", json!({"name": "C"}));
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20}));
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
s
|
||||
}
|
||||
|
||||
/// The user's "big square": a sketch made in the root on a moved component's face lies on the
|
||||
/// face where it is shown, its projected loop on the face's edges (moved and turned), and it
|
||||
/// follows when the occurrence moves on.
|
||||
#[test]
|
||||
fn root_sketch_on_a_moved_components_face() {
|
||||
let mut s = block_in_component();
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": "C:1", "translate": [100, 0, 0]}));
|
||||
let r = run(&mut s, "sketch.create", json!({"plane": {"face": [120, 15, 20]}, "name": "Top"}));
|
||||
assert!(!r["projected"].is_null(), "the face's edges are projected: {r}");
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let pts = sketch_world_points(&mut s, "Top");
|
||||
assert!(pts.len() >= 4, "{pts:?}");
|
||||
// Every point but the sketch's origin (the model origin on the face plane).
|
||||
for p in pts.iter().filter(|p| !near(**p, Vec3::new(0.0, 0.0, 20.0))) {
|
||||
assert!((p.z - 20.0).abs() < 1e-6 && p.x > 100.0 - 1e-6 && p.x < 140.0 + 1e-6 && p.y > -1e-6 && p.y < 30.0 + 1e-6, "{p:?}");
|
||||
}
|
||||
for corner in [Vec3::new(100.0, 0.0, 20.0), Vec3::new(140.0, 30.0, 20.0)] {
|
||||
assert!(pts.iter().any(|p| near(*p, corner)), "{corner:?} in {pts:?}");
|
||||
}
|
||||
assert_eq!(s.doc.find_feature("Top").unwrap().component, 0, "the sketch is the root's");
|
||||
|
||||
// Turned 90 degrees about Z (around the world origin): the top face is now x -30..0, y 0..40.
|
||||
let mut s = block_in_component();
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": "C:1", "axis": [0, 0, 1], "angle": "90 deg", "origin": [0, 0, 0]}));
|
||||
run(&mut s, "sketch.create", json!({"plane": {"face": [-15, 20, 20]}, "name": "Top"}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let pts = sketch_world_points(&mut s, "Top");
|
||||
for corner in [Vec3::new(0.0, 0.0, 20.0), Vec3::new(-30.0, 40.0, 20.0)] {
|
||||
assert!(pts.iter().any(|p| near(*p, corner)), "{corner:?} in {pts:?}");
|
||||
}
|
||||
// Projected geometry stays on the face when the occurrence moves on (the link re-resolves).
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": "C:1", "translate": [0, 0, 5]}));
|
||||
let pts = sketch_world_points(&mut s, "Top");
|
||||
assert!(pts.iter().any(|p| near(*p, Vec3::new(-30.0, 40.0, 25.0))), "{pts:?}");
|
||||
}
|
||||
@@ -20,6 +20,21 @@ fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs().max(1e-12)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_extrude_from_a_chamfered_top_face() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 10}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 20}));
|
||||
run(&mut s, "solid.chamfer", json!({"edges": [[10, 0, 20]], "distance": 2}));
|
||||
let base = volume(&mut s);
|
||||
run(&mut s, "sketch.create", json!({"plane": {"face": [0, 0, 20]}}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 5, "operation": "join"}));
|
||||
assert_eq!(s.model.state().bodies.len(), 1);
|
||||
assert!(rel(volume(&mut s), base + PI * 64.0 * 5.0) < 5e-4);
|
||||
}
|
||||
|
||||
mod component_frames;
|
||||
mod robustness;
|
||||
|
||||
@@ -650,10 +665,10 @@ fn section_analysis() {
|
||||
assert!(s.execute("inspect.section", &json!({"plane": {"normal": [0, 0, 0]}})).is_err());
|
||||
run(&mut s, "inspect.section", json!({"clear": true}));
|
||||
assert!(s.section.is_none());
|
||||
// XZ's normal is -Y: an offset moves the cut to -Y, `at` places it at a world Y, and the
|
||||
// XZ's normal is +Y: an offset moves the cut to +Y, `at` places it at a world Y, and the
|
||||
// result says where the cut landed either way.
|
||||
let r = run(&mut s, "inspect.section", json!({"plane": "XZ", "offset": 10}));
|
||||
assert_eq!((r["section"]["axis"].as_str(), r["section"]["at"].as_f64()), (Some("Y"), Some(-10.0)), "{r}");
|
||||
assert_eq!((r["section"]["axis"].as_str(), r["section"]["at"].as_f64()), (Some("Y"), Some(10.0)), "{r}");
|
||||
for (plane, at, flip) in [("XZ", -282.5, false), ("XZ", 7.0, true), ("XY", 3.0, false), ("YZ", -4.0, true)] {
|
||||
let r = run(&mut s, "inspect.section", json!({"plane": plane, "at": at, "flip": flip}));
|
||||
assert_eq!(r["section"]["at"].as_f64(), Some(at), "{plane} {r}");
|
||||
|
||||
Reference in new issue
Block a user