doc, engine, ui: placing the model returns its error; placements are checked where they are set

doc: world_state, world_state_reusing, state_in_frame_at, state_in_frame, face_point_state,
face_point_names and construction_state return Result. A body that could not be placed, a
frame that could not be inverted, a sketch plane or thread axis that did not survive the
placement, or a failed placing worker used to leave things where they were without a word;
now the call fails with what could not be placed. is_placement is the one test of an
occurrence's placement (finite, a rotation that does not mirror, a translation within
1e9 mm). Opening a design names every occurrence that fails it; add_occurrence refuses
one; a joint solution that would place an occurrence outside one is not applied and the
joints placing it report it.

engine: Session::world_state returns Result and keeps a failure in its cache as it keeps a
placed model. bounds, home_camera, open_sketch_plane, the scene builders, the world
previews, frames::to_local and auto_operation pass the error on; helpers answering "not
found" take the placed model from their caller. Commands setting a placement check it
(valid_position, exploded view, fastener at a point, STEP insert matrix).

ui: panels, picks and hovers read the session's placed model through
open_meshing::session_world; a model that cannot be placed shows no placed bodies, and the
viewport says why across its bottom. Pictures and the control channel report the error.

cli, mcp: snapshots, renders, benches and the oracle report the error; the kernel-case rig
records it as the case's failure. Tests read the new Results with unwrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 13:38:41 -04:00
parent 2752eee616
commit 9a79588575
189 changed files with 1273 additions and 1123 deletions
+3 -3
View File
@@ -184,7 +184,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
s.execute(id, &c["params"]).map_err(|e| format!("command {i} ({id}): {e}"))?;
run.push(t.elapsed().as_secs_f64() * 1000.0);
let t = std::time::Instant::now();
s.world_state().warm_meshes(|| true);
s.world_state().map_err(|e| e.to_string())?.warm_meshes(|| true);
shown.push(t.elapsed().as_secs_f64() * 1000.0);
if run.len() == 100 || i + 1 == commands.len() {
let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len().max(1) as f64;
@@ -198,7 +198,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
shown.clear();
}
}
let world = s.world_state();
let world = s.world_state().map_err(|e| e.to_string())?;
println!(
"{}",
json!({"step": "built", "ms": began.elapsed().as_secs_f64() * 1000.0, "commands": commands.len(), "features": s.doc.features.len(), "bodies": world.bodies.len()})
@@ -233,7 +233,7 @@ mod tests {
}
}
assert!(s.model.results.iter().all(|r| r.error.is_none()));
let world = s.world_state();
let world = s.world_state().unwrap();
assert!(world.bodies.len() > 40, "{} bodies", world.bodies.len());
assert!(world.bodies.iter().any(|b| b.name.contains("spoke")));
}
+8 -1
View File
@@ -211,7 +211,14 @@ fn check_case(dir: &str, dumps: Option<&std::path::Path>) -> Value {
}
// Every body in the world, imported components included.
let world = s.world_state();
let world = match s.world_state() {
Ok(w) => w,
Err(e) => {
out["status"] = json!("fail");
out["error"] = json!(e.to_string());
return out;
}
};
let mut bodies = Vec::new();
let mut c = Checks { list: Vec::new(), pass: true };
let want_valid = want["valid"].as_bool().unwrap_or(true);
+8 -5
View File
@@ -564,9 +564,12 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
}
(None, None) => {}
}
let cam = s.camera.unwrap_or_else(|| solidcraft_engine::view::home_camera(&s));
let cam = match s.camera {
Some(cam) => cam,
None => solidcraft_engine::view::home_camera(&s).map_err(|e| e.to_string())?,
};
let guides = Guides { grid: !args.iter().any(|a| a == "--no-grid"), axes: !args.iter().any(|a| a == "--no-axes") };
let scene = solidcraft_engine::view::scene_with_guides(&s, &cam, s.section, guides);
let scene = solidcraft_engine::view::scene_with_guides(&s, &cam, s.section, guides).map_err(|e| e.to_string())?;
let png = solidcraft_engine::capture::png(&scene, &cam, w, h)?;
std::fs::write(out, &png.bytes).map_err(|e| format!("{out}: {e}"))?;
let mut reply = json!({"path": out, "width": w, "height": h, "bytes": png.bytes.len(), "renderer": png.renderer});
@@ -803,7 +806,7 @@ fn bench_ops(mut s: Session, script: &str) -> Result<Session, String> {
let r = s.execute(id, p).map_err(|e| format!("{id}: {e}"))?;
let ms = t.elapsed().as_secs_f64() * 1000.0;
let t = std::time::Instant::now();
s.world_state().warm_meshes(|| true);
s.world_state().map_err(|e| e.to_string())?.warm_meshes(|| true);
let view_ms = t.elapsed().as_secs_f64() * 1000.0;
println!("{}", json!({"step": "op", "command": id, "ms": ms, "view_ms": view_ms}));
Ok(r)
@@ -859,7 +862,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
}
let t = std::time::Instant::now();
let st = s.world_state();
let st = s.world_state().map_err(|e| e.to_string())?;
let place_ms = t.elapsed().as_secs_f64() * 1000.0;
// As the desktop app prepares a design it opened.
st.warm_meshes(|| true);
@@ -929,7 +932,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
let t = std::time::Instant::now();
s.execute("parameters.change", &json!({"name": name, "expression": expr})).map_err(|e| e.to_string())?;
let eval_ms = t.elapsed().as_secs_f64() * 1000.0;
let st = s.world_state();
let st = s.world_state().map_err(|e| e.to_string())?;
let t = std::time::Instant::now();
st.warm_meshes(|| true);
let mesh_ms = t.elapsed().as_secs_f64() * 1000.0;
+1 -1
View File
@@ -186,7 +186,7 @@ fn step_export_check(s: &solidcraft_engine::Session, volume: f64) -> (bool, Stri
/// Our part as a triangle soup (curved faces refined onto the surface).
fn our_mesh(s: &solidcraft_engine::Session) -> Result<Vec<Tri>, String> {
let mut ours: Vec<Tri> = Vec::new();
for b in &s.world_state().bodies {
for b in &s.world_state().map_err(|e| e.to_string())?.bodies {
let m = b.body.mesh(&solidcraft_engine::kernel::MeshTolerance::chord(MESH_TOL)).map_err(|e| e.to_string())?;
let p = |i: u32| m.positions.get(i as usize).map(|v: &Vec3| [v.x, v.y, v.z]);
for t in &m.triangles {
+13 -5
View File
@@ -196,7 +196,7 @@ fn round3(x: f64) -> f64 {
/// Every action on every entity of the design's final bodies, in a fixed order.
fn actions(s: &Session) -> Vec<Action> {
let st = s.world_state();
let st = s.world_state().unwrap();
let placed = s.placed_document();
let paths: BTreeMap<String, Vec<u64>> =
placed.body_frames_with_source(&s.model.state()).into_iter().map(|(world, _, _, path, _)| (world, path)).collect();
@@ -1338,7 +1338,9 @@ fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
fn a_round_on_a_knuckle_cap_circle() {
for (cmd, size, removed) in [("solid.fillet", "radius", (2.4, 2.8)), ("solid.chamfer", "distance", (5.6, 6.3))] {
let mut s = load(&root().join("tests/oracle/63-hinge-knuckle/recipe.json")).unwrap();
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().measure().volume).sum::<f64>();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().measure().volume).sum::<f64>()
};
let before = volume(&s);
s.execute(cmd, &json!({"edges": [[40.0, 29.837_722, 6.449_490]], size: 0.728})).unwrap_or_else(|e| panic!("{cmd}: {e}"));
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{cmd}: {kind}: {e}"));
@@ -1358,7 +1360,9 @@ fn a_round_on_a_knuckle_cap_circle() {
#[test]
fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
let mut s = load(&root().join("examples/regressions/loft_cut_beside_round_hole.json")).unwrap();
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.005)).unwrap().measure().volume).sum::<f64>();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.005)).unwrap().measure().volume).sum::<f64>()
};
let before = volume(&s);
s.execute("solid.hole", &json!({"diameter": 0.133, "position": [357.22, -150.993_333, 730.0]})).unwrap();
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
@@ -1375,7 +1379,9 @@ fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
#[test]
fn a_small_hole_through_the_wall_of_a_plastic_rest() {
let mut s = load(&root().join("apps/solidcraft-cli/tests/corpus/modern-plastic.solidcraft")).unwrap();
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
};
let before = volume(&s);
s.execute("solid.hole", &json!({"diameter": 0.8, "position": [63.069_841_5, 28.333_333, 4.0]})).unwrap();
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
@@ -1390,7 +1396,9 @@ fn a_small_hole_through_the_wall_of_a_plastic_rest() {
#[test]
fn a_hole_square_to_a_45_degree_flange() {
let mut s = load(&root().join("tests/oracle/70-sm-flange-45/recipe.json")).unwrap();
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
};
let before = volume(&s);
s.execute("solid.hole", &json!({"diameter": 3.448, "position": [66.666_667, -4.511_845, 4.511_845]})).unwrap();
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
+1 -1
View File
@@ -63,7 +63,7 @@ fn model(name: &str, s: &Session) -> Value {
.iter()
.map(|r| json!({"id": r.id, "name": r.name, "error": r.error, "warning": r.warning, "skipped": r.skipped, "state": state(name, &r.state)}))
.collect();
json!({"results": results, "world": state(name, &s.world_state())})
json!({"results": results, "world": state(name, &s.world_state().unwrap())})
}
/// The same edit on both: a parameter's value when the design has one, else a new body.
+2 -2
View File
@@ -168,7 +168,7 @@ fn record_refs(out: &mut Vec<String>, s: &Session, f: &Feature) {
// Faces a feature picks by point and stores by name (shell, draft, hole, …).
let explicit_emboss = matches!(&f.kind, FeatureKind::Emboss { face: Some(_), .. });
if f.face_names.iter().any(|n| !n.is_empty()) {
let fst = solidcraft_engine::doc::face_point_state(doc, &before, &f.kind, f.component);
let fst = solidcraft_engine::doc::face_point_state(doc, &before, &f.kind, f.component).unwrap();
for (k, (p, n)) in f.kind.face_points().into_iter().zip(&f.face_names).enumerate() {
if n.is_empty() {
continue;
@@ -269,7 +269,7 @@ fn record_refs(out: &mut Vec<String>, s: &Session, f: &Feature) {
FaceAttachmentStatus::Lost { message, .. } => format!("lost: {message}"),
FaceAttachmentStatus::Legacy { .. } => {
// Found by its name among the faces facing the same way, nearest the pick.
let fst = solidcraft_engine::doc::state_in_frame(doc, &before, f.component);
let fst = solidcraft_engine::doc::state_in_frame(doc, &before, f.component).unwrap();
let mut list: Vec<String> = match name {
Some(n) => fst
.bodies
@@ -31,7 +31,7 @@ struct Topology {
/// Every solid body's topology, by name.
fn topology(s: &Session) -> BTreeMap<String, Topology> {
let st = s.world_state();
let st = s.world_state().unwrap();
st.bodies
.iter()
.filter(|b| !b.body.is_mesh())
@@ -174,7 +174,7 @@ fn pi_case_faces_per_step_at_both_heights() {
for (k, want) in FACES {
let mut t = s.scratch();
t.execute("timeline.rollback", &json!({"position": k})).unwrap();
let st = t.world_state();
let st = t.world_state().unwrap();
let body = st.bodies.iter().find(|b| b.name == "Body1").unwrap();
assert_eq!(body.body.face_count(), want, "{case}: faces after feature {k}");
// Each outer side: the faces in its plane.
+4 -4
View File
@@ -171,9 +171,9 @@ fn a_saved_view_is_its_camera_and_a_standard_view_is_unchanged() {
let top = dir.snapshot(&script, "top.png", &["--view", "top"]);
let mut s = Session::default();
s.run_script(&serde_json::from_slice(&std::fs::read(&script).unwrap()).unwrap()).unwrap();
let mut cam = solidcraft_engine::view::home_camera(&s);
let mut cam = solidcraft_engine::view::home_camera(&s).unwrap();
cam.set_view(solidcraft_engine::render::StandardView::Top);
let scene = solidcraft_engine::view::scene(&s, &cam);
let scene = solidcraft_engine::view::scene(&s, &cam).unwrap();
assert_same(&top, &solidcraft_engine::capture::png(&scene, &cam, SIZE.0, SIZE.1).unwrap().bytes, "the home camera turned to the top");
assert_same(&top, &dir.mcp(&script, "mcp-top.png", json!({"view": "top"})), "the MCP top view");
assert_differs(&top, &saved, "top and the saved view");
@@ -335,9 +335,9 @@ fn a_forced_cpu_snapshot_is_the_cpu_rasterizers_and_says_so() {
assert!(stderr.contains("CPU rasterizer"), "{stderr}");
let mut s = Session::default();
s.run_script(&serde_json::from_slice(&std::fs::read(&script).unwrap()).unwrap()).unwrap();
let mut cam = solidcraft_engine::view::home_camera(&s);
let mut cam = solidcraft_engine::view::home_camera(&s).unwrap();
cam.set_view(solidcraft_engine::render::StandardView::Top);
let scene = solidcraft_engine::view::scene(&s, &cam);
let scene = solidcraft_engine::view::scene(&s, &cam).unwrap();
let cpu = solidcraft_engine::render::render_png(&scene, &cam, SIZE.0 as usize, SIZE.1 as usize).unwrap();
assert_eq!(std::fs::read(dir.0.join("cpu.png")).unwrap(), cpu);
}
@@ -48,7 +48,7 @@ fn key(v: Vec3) -> [i64; 3] {
fn straight_edges_are_one_edge_after_many_cuts() {
for (what, s) in [("as saved", open(&design())), ("built from scratch", rebuilt())] {
assert_eq!(errors(&s), vec![], "{what}: every feature evaluates");
let st = s.world_state();
let st = s.world_state().unwrap();
let bodies: Vec<_> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
assert_eq!(bodies.len(), 1, "{what}: one body");
let body = &bodies[0].body;
+1 -1
View File
@@ -6,7 +6,7 @@ pub fn state_in_face_anchor_frame<'a>(doc: &Document, state: &'a ModelState, anc
let frame = anchor.author_frame(doc, author)?;
let raw = state.body(&anchor.target.body).ok_or_else(|| DocError::Unknown(format!("anchored body `{}`", anchor.target.body)))?;
let relative = anchor.source_to_author(doc, author, raw)?;
let mut output = crate::state_in_frame_at(doc, state, author, frame).into_owned();
let mut output = crate::state_in_frame_at(doc, state, author, frame)?.into_owned();
// In place, the body is the model's own, display mesh included: a sketch projecting the
// face's edges meshes it once for the features after it too, rather than a copy.
let body = state.body_in_frame(raw, &relative)?;
+22 -1
View File
@@ -84,6 +84,23 @@ pub fn mat_inverse(m: &Mat) -> Option<Mat> {
Some(out)
}
/// Whether `m` is a placement an occurrence may have: finite, a rotation (orthonormal axes, not
/// mirrored) and a translation within 1e9 mm, its last column `0, 0, 0, 1`. Every placement is
/// checked against this where it enters the document (a design file, a command, an import).
pub fn is_placement(m: &Mat) -> bool {
let axis = |i: usize| Vec3::new(m[i][0], m[i][1], m[i][2]);
let (x, y, z) = (axis(0), axis(1), axis(2));
m.iter().flatten().all(|v| v.is_finite())
&& m[3][..3].iter().all(|v| v.abs() <= 1e9)
&& [x, y, z].iter().all(|v| (v.dot(*v) - 1.0).abs() < 1e-6)
&& x.dot(y).abs() < 1e-6
&& x.dot(z).abs() < 1e-6
&& y.dot(z).abs() < 1e-6
&& x.cross(y).dot(z) > 0.0
&& [m[0][3], m[1][3], m[2][3]].iter().all(|v| v.abs() < 1e-9)
&& (m[3][3] - 1.0).abs() < 1e-9
}
/// A rigid transform: rotation by `angle` about `axis` through `origin`, then a translation.
pub fn rigid(translate: Vec3, origin: Vec3, axis: Vec3, angle: f64) -> Mat {
let k = axis.normalized().unwrap_or(Vec3::Z);
@@ -392,7 +409,8 @@ impl Document {
false
}
/// Add an occurrence of an existing component into `parent`.
/// Add an occurrence of an existing component into `parent`, placed by `transform` (which
/// must pass [`is_placement`]).
pub fn add_occurrence(&mut self, component: u64, parent: u64, transform: Mat) -> Result<u64> {
if component == 0 || !self.components.iter().any(|c| c.id == component) {
return Err(DocError::Unknown(format!("component {component}")));
@@ -403,6 +421,9 @@ impl Document {
if self.occurrences.len() >= 100_000 {
return Err(DocError::Invalid("too many occurrences".into()));
}
if !is_placement(&transform) {
return Err(DocError::Invalid("an occurrence's placement must be a rigid motion (no scaling or mirroring) within 1e9 mm".into()));
}
let id = self.next_id.max(1);
self.next_id = id.checked_add(1).ok_or_else(|| DocError::Invalid("too many items in the design".into()))?;
let cname = self.components.iter().find(|c| c.id == component).map(|c| c.name.clone()).unwrap_or_default();
+1 -12
View File
@@ -1,20 +1,9 @@
//! Recorded ownership changes are evaluated at their timeline position.
use crate::{DocError, Document, Mat, ModelBody, ModelState, Result};
use solidcraft_geom::Vec3;
use std::collections::BTreeSet;
pub fn valid_transfer_matrix(m: &Mat) -> bool {
let axis = |i: usize| Vec3::new(m[i][0], m[i][1], m[i][2]);
let (x, y, z) = (axis(0), axis(1), axis(2));
m.iter().flatten().all(|v| v.is_finite())
&& m[3][..3].iter().all(|v| v.abs() <= 1e9)
&& [x, y, z].iter().all(|v| (v.dot(*v) - 1.0).abs() < 1e-6)
&& x.dot(y).abs() < 1e-6
&& x.dot(z).abs() < 1e-6
&& y.dot(z).abs() < 1e-6
&& x.cross(y).dot(z) > 0.0
&& [m[0][3], m[1][3], m[2][3]].iter().all(|v| v.abs() < 1e-9)
&& (m[3][3] - 1.0).abs() < 1e-9
crate::is_placement(m)
}
/// Validate the stored boundary before allocation, loading, or evaluation.
+76 -68
View File
@@ -2192,19 +2192,24 @@ impl Model {
// Face references by name: move each face point onto its named face (on the body the
// feature names, when it names one and the face is there).
let renamed;
let mut frame_error: Option<String> = None;
let f = match with_named_face_points(doc, &state, f) {
Some((g, notes)) => {
Ok(Some((g, notes))) => {
if !notes.is_empty() {
warning = Some(notes.join("; "));
}
renamed = g;
&renamed
}
None => f,
Ok(None) => f,
Err(e) => {
frame_error = Some(e.to_string());
f
}
};
// A parameter with an error fails the features using it, with that error.
let bad_param = param_names(f).into_iter().find_map(|n| self.param_errors.get(&n).map(|e| format!("parameter `{n}`: {e}")));
let r = match bad_param {
let r = match frame_error.or(bad_param) {
Some(e) => Err(kernel::KernelError::Failed(e)),
None => kernel::guard(&f.name, || {
eval_feature(doc, &vals, f, &mut next, &mut warning).map_err(|e| kernel::KernelError::Failed(e.to_string()))
@@ -4516,7 +4521,7 @@ fn feature_tools_with_constructor(
// model is seen from there: a target in another, moved component where it is shown.
let n = ss.plane.normal();
let tst =
if extent.to.is_some() || extent.from.is_some() { state_in_frame(doc, st, f.component) } else { std::borrow::Cow::Borrowed(st) };
if extent.to.is_some() || extent.from.is_some() { state_in_frame(doc, st, f.component)? } else { std::borrow::Cow::Borrowed(st) };
// Through all, and where the tool meets its targets, is measured where they are shown.
let shown = targets_as_shown(st, f, &crate::plain(targets))?;
let st: &ModelState = &shown;
@@ -4724,7 +4729,7 @@ fn feature_tools_with_constructor(
let (o, d) =
revolve_axis(&ss, axis, &axis_state(doc, st, f.component, axis), &|name| construction_axis_in(Some(doc), st, name, f.component))?;
// To Object is in this feature's component frame: the model as seen from there.
let tst = if to.is_some() { state_in_frame(doc, st, f.component) } else { std::borrow::Cow::Borrowed(st) };
let tst = if to.is_some() { state_in_frame(doc, st, f.component)? } else { std::borrow::Cow::Borrowed(st) };
// Where the tool meets its targets is measured where they are shown (#3383).
let shown = targets_as_shown(st, f, &feature_op(f).1)?;
let st: &ModelState = &shown;
@@ -5692,13 +5697,14 @@ fn copied_features(doc: &Document, vals: &BTreeMap<String, Value>, st: &ModelSta
/// own, so no two copies of one feature share an instance. Suppressed sources copy nothing.
/// `f` with each face point moved onto its named face (on the body the feature names, when it
/// names one and the face is there) in `state`, and notes for names that no longer find their
/// face. `None` when `f` names no face.
fn with_named_face_points(doc: &Document, state: &ModelState, f: &Feature) -> Option<(Feature, Vec<String>)> {
/// face. `None` when `f` names no face; an error when the frame the faces are looked up in
/// cannot be made (the feature fails with it).
fn with_named_face_points(doc: &Document, state: &ModelState, f: &Feature) -> Result<Option<(Feature, Vec<String>)>> {
// Cylindrical emboss resolves its named face in the selected body's frame
// and certifies an analytic anchor; mesh projection here would undo that.
let explicit_emboss = matches!(&f.kind, FeatureKind::Emboss { face: Some(_), .. });
if explicit_emboss || !f.face_names.iter().any(|n| !n.is_empty()) {
return None;
return Ok(None);
}
let mut g = f.clone();
let mut notes = Vec::new();
@@ -5709,7 +5715,7 @@ fn with_named_face_points(doc: &Document, state: &ModelState, f: &Feature) -> Op
FeatureKind::Hole { direction, points: None | Some(crate::SketchPoints { single: true, .. }), .. } => direction.normalized(),
_ => None,
};
let fst = face_point_state(doc, state, &f.kind, f.component);
let fst = face_point_state(doc, state, &f.kind, f.component)?;
for (k, (p, n)) in g.kind.face_points_mut().into_iter().zip(&f.face_names).enumerate() {
if n.is_empty() {
continue;
@@ -5735,7 +5741,7 @@ fn with_named_face_points(doc: &Document, state: &ModelState, f: &Feature) -> Op
None => notes.push(format!("face `{n}` no longer exists; using the face at the picked point")),
}
}
Some((g, notes))
Ok(Some((g, notes)))
}
fn copy_plan<'a>(
@@ -5927,7 +5933,7 @@ fn replay(
// Each copy's faces are named as the source feature's, with the instance. Its face
// points follow their named faces, as the feature's own did (#2121): a To Object
// extrude's copies reach the face where it is now, not where it was picked.
let moved = with_named_face_points(doc, st, src).map(|(g, _)| g);
let moved = with_named_face_points(doc, st, src)?.map(|(g, _)| g);
let tools = feature_tools_with_constructor(doc, vals, moved.as_ref().unwrap_or(src), st, &mut None, &mut None, &mut None)?;
for (k, m) in mats.iter().enumerate() {
let start = copies.len();
@@ -6404,7 +6410,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
// component's bodies moved into its frame: placing every body of a large
// assembly again is what made each sketch edit as slow as the design is big.
None if !plane_reads_geometry(plane) && sketch.links.is_empty() => std::borrow::Cow::Borrowed(st),
None => state_in_frame(doc, st, f.component),
None => state_in_frame(doc, st, f.component)?,
};
let (plane, face_warning) = match (anchor, plane) {
(Some(_), plane) => doc.resolve_attached_plane_noted(vals, plane, f.component, st, &fst)?,
@@ -6644,14 +6650,14 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
// A plane of numbers alone reads no body: placing them all is the design's size.
None if plane_is_fixed(plane) => doc.resolve_plane_in(vals, plane, 0, Some(st))?,
// Picks on bodies of other (moved) components are where those bodies are shown.
None => doc.resolve_plane_in(vals, plane, 0, Some(&construction_state(doc, st, f.component)))?,
None => doc.resolve_plane_in(vals, plane, 0, Some(&*construction_state(doc, st, f.component)?))?,
};
st.construct.push(crate::construct::Construct { feature: f.id, name: f.name.clone(), geom: crate::construct::ConstructGeom::Plane(pl) });
Ok(())
}
FeatureKind::ConstructionAxis { def } => {
let (origin, dir) =
crate::construct::resolve_axis(&crate::construct::Ctx { doc, vals, st: &construction_state(doc, st, f.component), depth: 0 }, def)?;
crate::construct::resolve_axis(&crate::construct::Ctx { doc, vals, st: &*construction_state(doc, st, f.component)?, depth: 0 }, def)?;
st.construct.push(crate::construct::Construct {
feature: f.id,
name: f.name.clone(),
@@ -6660,8 +6666,10 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
Ok(())
}
FeatureKind::ConstructionPoint { def } => {
let p =
crate::construct::resolve_point(&crate::construct::Ctx { doc, vals, st: &construction_state(doc, st, f.component), depth: 0 }, def)?;
let p = crate::construct::resolve_point(
&crate::construct::Ctx { doc, vals, st: &*construction_state(doc, st, f.component)?, depth: 0 },
def,
)?;
st.construct.push(crate::construct::Construct { feature: f.id, name: f.name.clone(), geom: crate::construct::ConstructGeom::Point(p) });
Ok(())
}
@@ -7246,9 +7254,9 @@ fn unique_body_name(st: &ModelState, base: &str) -> String {
/// What construction geometry of component `comp` is built on: the model as that component sees
/// it, other components' bodies where they are placed. In the root, every placement of a
/// component (its pasted instances too) is there, as the viewport shows it.
fn construction_state<'a>(doc: &Document, st: &'a ModelState, comp: u64) -> std::borrow::Cow<'a, ModelState> {
fn construction_state<'a>(doc: &Document, st: &'a ModelState, comp: u64) -> Result<std::borrow::Cow<'a, ModelState>> {
if comp == 0 && !doc.occurrences.is_empty() {
return std::borrow::Cow::Owned(world_state(doc, st));
return Ok(std::borrow::Cow::Owned(world_state(doc, st)?));
}
state_in_frame(doc, st, comp)
}
@@ -7391,7 +7399,7 @@ fn tool_in(tool: &Traced, m: Option<Mat>) -> Result<Traced> {
}
}
pub fn state_in_frame<'a>(doc: &Document, st: &'a ModelState, comp: u64) -> std::borrow::Cow<'a, ModelState> {
pub fn state_in_frame<'a>(doc: &Document, st: &'a ModelState, comp: u64) -> Result<std::borrow::Cow<'a, ModelState>> {
state_in_frame_at(doc, st, comp, doc.component_transform(comp))
}
@@ -7400,10 +7408,10 @@ pub fn state_in_frame<'a>(doc: &Document, st: &'a ModelState, comp: u64) -> std:
/// (`comp`): the model as that component sees it, a target in another, moved component where it
/// is shown (#928). Other face points are picks, kept in the frame of the body they lie on: the
/// model itself.
pub fn face_point_state<'a>(doc: &Document, st: &'a ModelState, kind: &FeatureKind, comp: u64) -> std::borrow::Cow<'a, ModelState> {
pub fn face_point_state<'a>(doc: &Document, st: &'a ModelState, kind: &FeatureKind, comp: u64) -> Result<std::borrow::Cow<'a, ModelState>> {
match kind {
FeatureKind::Extrude { .. } | FeatureKind::Revolve { .. } => state_in_frame(doc, st, comp),
_ => std::borrow::Cow::Borrowed(st),
_ => Ok(std::borrow::Cow::Borrowed(st)),
}
}
@@ -7411,19 +7419,20 @@ pub fn face_point_state<'a>(doc: &Document, st: &'a ModelState, kind: &FeatureKi
/// [`face_point_state`]. A To / From point is named after the nearest face over every body, the
/// one `object_height` resolves it to, not the first body with a face within tolerance: a part
/// resting a hair from the picked face would otherwise take its name.
pub fn face_point_names(doc: &Document, st: &ModelState, kind: &FeatureKind, comp: u64) -> Vec<String> {
pub fn face_point_names(doc: &Document, st: &ModelState, kind: &FeatureKind, comp: u64) -> Result<Vec<String>> {
let pts = kind.face_points();
if !matches!(kind, FeatureKind::Extrude { .. } | FeatureKind::Revolve { .. }) {
return crate::naming::face_names_at(st, &pts);
return Ok(crate::naming::face_names_at(st, &pts));
}
let fst = face_point_state(doc, st, kind, comp);
let fst = face_point_state(doc, st, kind, comp)?;
let normal = match kind {
FeatureKind::Extrude { sketch, .. } => {
st.sketch(sketch.0).and_then(|ss| sketch_in_feature_frame(doc, ss, comp).ok()).map(|ss| ss.plane.normal())
}
_ => None,
};
pts.iter()
Ok(pts
.iter()
.map(|p| {
fst.bodies
.iter()
@@ -7436,16 +7445,16 @@ pub fn face_point_names(doc: &Document, st: &ModelState, kind: &FeatureKind, com
.and_then(|(_, b, fi)| crate::naming::face_names(b).get(fi).cloned())
.unwrap_or_default()
})
.collect()
.collect())
}
/// As `state_in_frame`, using the exact active placement for its shared source geometry.
pub fn state_in_frame_at<'a>(doc: &Document, st: &'a ModelState, comp: u64, frame: Mat) -> std::borrow::Cow<'a, ModelState> {
pub fn state_in_frame_at<'a>(doc: &Document, st: &'a ModelState, comp: u64, frame: Mat) -> Result<std::borrow::Cow<'a, ModelState>> {
use std::borrow::Cow;
if doc.occurrences.is_empty() && doc.body_offsets.is_empty() {
return Cow::Borrowed(st);
return Ok(Cow::Borrowed(st));
}
let Some(inv) = crate::mat_inverse(&frame) else { return Cow::Borrowed(st) };
let inv = crate::mat_inverse(&frame).ok_or_else(|| DocError::Invalid(format!("component {comp}'s frame cannot be inverted")))?;
let comp_of_feature = |id: u64| doc.feature(id).map(|f| f.component).unwrap_or(0);
let rel = |c: u64, body: Option<&str>| {
let m = crate::mat_mul(&inv, &if c == comp { frame } else { doc.component_transform(c) });
@@ -7466,13 +7475,12 @@ pub fn state_in_frame_at<'a>(doc: &Document, st: &'a ModelState, comp: u64, fram
let world_wires_need_mapping = !crate::is_identity(&inv)
&& st.sketches.iter().any(|sketch| sketch.sketch.wires.iter().any(|wire| wire.space == solidcraft_sketch::WireSpace::World));
if body_m.iter().chain(&sketch_m).all(crate::is_identity) && !world_wires_need_mapping {
return Cow::Borrowed(st);
return Ok(Cow::Borrowed(st));
}
let mut out = st.clone();
for (b, m) in out.bodies.iter_mut().zip(&body_m) {
if !crate::is_identity(m)
&& let Ok(moved) = st.body_in_frame(b, m)
{
if !crate::is_identity(m) {
let moved = st.body_in_frame(b, m).map_err(|e| DocError::Invalid(format!("body `{}` cannot be moved into the frame: {e}", b.name)))?;
*b = moved;
}
}
@@ -7483,24 +7491,22 @@ pub fn state_in_frame_at<'a>(doc: &Document, st: &'a ModelState, comp: u64, fram
continue;
}
let s = Arc::make_mut(s);
if !crate::is_identity(m)
&& let Some(np) = Plane::new(crate::apply_point(m, s.plane.origin), crate::apply_vector(m, s.plane.x), crate::apply_vector(m, s.plane.y))
{
s.plane = np;
if !crate::is_identity(m) {
s.plane = placed_plane(m, &s.plane)?;
}
for wire in &mut s.sketch.wires {
let matrix = if wire.space == solidcraft_sketch::WireSpace::Component { m } else { &inv };
map_wire(wire, matrix);
}
}
Cow::Owned(out)
Ok(Cow::Owned(out))
}
/// The model placed in the world: each component's bodies, sketches, threads and construction
/// geometry through every occurrence of it (bodies of the first placement keep their names;
/// further instances get the occurrence path in brackets). Without components this is the model
/// itself.
pub fn world_state(doc: &Document, st: &ModelState) -> ModelState {
pub fn world_state(doc: &Document, st: &ModelState) -> Result<ModelState> {
world_state_reusing(doc, st, &mut PlacedBodies::default())
}
@@ -7532,19 +7538,16 @@ impl PlacedBodies {
.map(|(_, placed)| placed)
}
/// The body `b` placed by `m`, from an earlier call when `b` is the same body as then.
fn placed(&self, b: &ModelBody, m: &crate::Mat, name: String) -> ModelBody {
/// The body `b` placed by `m`, from an earlier call when `b` is the same body as then; an
/// error when it cannot be placed.
fn placed(&self, b: &ModelBody, m: &crate::Mat, name: String) -> Result<ModelBody> {
if let Some(placed) = self.taken(b, m, &name) {
return placed;
}
match kernel::place_matrix(&b.body, *m) {
Ok(body) => {
let mut fresh = ModelBody::new(name, body, b.feature);
fresh.mesh_placement = placed_mesh::PlacedMesh::new(b, m).map(Arc::new);
fresh
}
Err(_) => ModelBody::new(name, b.body.clone(), b.feature),
return Ok(placed);
}
let body = kernel::place_matrix(&b.body, *m).map_err(|e| DocError::Invalid(format!("body `{}` cannot be placed: {e}", b.name)))?;
let mut fresh = ModelBody::new(name, body, b.feature);
fresh.mesh_placement = placed_mesh::PlacedMesh::new(b, m).map(Arc::new);
Ok(fresh)
}
/// The body `b` placed by `m` as an earlier call placed it, when `b` is the same body as then.
@@ -7631,9 +7634,9 @@ fn placed_key(b: &ModelBody, m: &crate::Mat) -> (usize, [u64; 16]) {
/// [`world_state`], reusing the bodies `placed` holds from an earlier call and leaving it holding
/// this call's.
pub fn world_state_reusing(doc: &Document, st: &ModelState, placed_before: &mut PlacedBodies) -> ModelState {
pub fn world_state_reusing(doc: &Document, st: &ModelState, placed_before: &mut PlacedBodies) -> Result<ModelState> {
if doc.occurrences.is_empty() {
return st.clone();
return Ok(st.clone());
}
let mut placed_now = PlacedBodies::default();
let comp_of_feature = |id: u64| doc.feature(id).map(|f| f.component).unwrap_or(0);
@@ -7673,7 +7676,10 @@ pub fn world_state_reusing(doc: &Document, st: &ModelState, placed_before: &mut
let reused: Vec<_> = jobs.iter().map(|(b, bm, name)| placed_before.taken(b, bm, name)).collect();
let fresh: Vec<_> = jobs.iter().zip(&reused).filter(|(_, r)| r.is_none()).map(|(job, _)| job).collect();
let mut placed_fresh = kernel::map_on_threads(&fresh, 16, |(b, bm, name)| placed_before.placed(b, bm, name.clone())).into_iter();
let mut moved = reused.into_iter().map(|r| r.or_else(|| placed_fresh.next().flatten())).zip(&jobs);
let mut moved = reused.into_iter().map(|r| match r {
Some(placed) => Some(Ok(placed)),
None => placed_fresh.next().flatten(),
});
for (comp, path, m) in placements {
let primary = path == first_path(comp);
let ident = crate::is_identity(&m);
@@ -7686,11 +7692,11 @@ pub fn world_state_reusing(doc: &Document, st: &ModelState, placed_before: &mut
placed.name = names.name(&b.name, comp, &path);
placed
} else {
// The same order as the jobs: this body's, made above (again here if it failed).
// The same order as the jobs: this body's, made above.
let placed = match moved.next() {
Some((Some(placed), _)) => placed,
Some((None, (_, _, name))) => placed_before.placed(b, &bm, name.clone()),
None => placed_before.placed(b, &bm, names.name(&b.name, comp, &path)),
Some(Some(placed)) => placed?,
Some(None) => return Err(DocError::Invalid(format!("body `{}` could not be placed (its worker failed)", b.name))),
None => return Err(DocError::Invalid(format!("body `{}` has no placement", b.name))),
};
placed_now.by_source.insert(placed_key(b, &bm), (b.clone(), placed.clone()));
placed
@@ -7717,12 +7723,7 @@ pub fn world_state_reusing(doc: &Document, st: &ModelState, placed_before: &mut
continue;
}
let mut s2 = SolvedSketch::clone(s);
if !ident {
let p = &s.plane;
if let Some(np) = Plane::new(crate::apply_point(&m, p.origin), crate::apply_vector(&m, p.x), crate::apply_vector(&m, p.y)) {
s2.plane = np;
}
}
s2.plane = placed_plane(&m, &s.plane)?;
for wire in &mut s2.sketch.wires {
if wire.space == solidcraft_sketch::WireSpace::Component {
map_wire(wire, &m);
@@ -7748,12 +7749,19 @@ pub fn world_state_reusing(doc: &Document, st: &ModelState, placed_before: &mut
t2.body = names.name(&t.body, comp, &path);
let frame = st.body(&t.body).and_then(|body| doc.model_body_offset(body)).map(|offset| crate::mat_mul(&m, offset)).unwrap_or(m);
t2.axis_point = crate::apply_point(&frame, t.axis_point);
t2.axis = crate::apply_vector(&frame, *t.axis).unit().unwrap_or(t.axis);
t2.axis = crate::apply_vector(&frame, *t.axis).unit().ok_or_else(|| DocError::Invalid("a thread's axis cannot be placed".into()))?;
out.threads.push(t2);
}
}
*placed_before = placed_now;
out
Ok(out)
}
/// `plane` moved by the placement `m`; an error when `m` does not keep it a plane (a placement
/// that is not a rigid motion, which [`Document`] does not take).
fn placed_plane(m: &crate::Mat, plane: &Plane) -> Result<Plane> {
Plane::new(crate::apply_point(m, plane.origin), crate::apply_vector(m, plane.x), crate::apply_vector(m, plane.y))
.ok_or_else(|| DocError::Invalid("a sketch plane cannot be placed: the placement is not a rigid motion".into()))
}
#[cfg(test)]
@@ -7830,7 +7838,7 @@ mod world_placement_cache_tests {
let source = &state.bodies[0];
assert!(source.names.origin.is_some());
assert!(source.mesh.get().is_none());
let world = world_state(&doc, &state);
let world = world_state(&doc, &state).unwrap();
let names = crate::naming::face_names(&world.bodies[0]);
assert!(Arc::ptr_eq(&names, &crate::naming::face_names(source)));
assert!(names.iter().all(|name| name.starts_with(&format!("F{feature}:"))));
@@ -7853,7 +7861,7 @@ mod world_placement_cache_tests {
state.bodies.push(ModelBody::new("Block".into(), kernel::box_solid(Vec3::ZERO, Vec3::new(2., 3., 4.)).unwrap(), 1));
state.bodies[0].key = 1234;
let source = &state.bodies[0];
let world = world_state(&doc, &state);
let world = world_state(&doc, &state).unwrap();
assert_eq!(world.bodies.len(), 2);
let first = world.body("Block").unwrap();
let second_name = doc.world_body_name(&state, "Block", component, &[second]);
@@ -7876,7 +7884,7 @@ mod world_placement_cache_tests {
assert!(Arc::ptr_eq(&names, &crate::naming::face_names(placed)));
let original_mesh = source.mesh();
assert!(Arc::ptr_eq(&original_mesh, &first.mesh()));
let again = world_state(&doc, &state);
let again = world_state(&doc, &state).unwrap();
assert!(Arc::ptr_eq(&original_mesh, &again.body("Block").unwrap().mesh()));
assert!(Arc::ptr_eq(&names, &crate::naming::face_names(again.body(&second_name).unwrap())));
}
+8 -24
View File
@@ -654,10 +654,14 @@ pub fn read(s: &str) -> Result<Document> {
if let Some(id) = crate::non_contiguous_timeline_group(&d.features, &d.timeline_groups) {
return Err(DocError::Invalid(format!("damaged design: timeline folder {id} is not contiguous")));
}
for o in &d.occurrences {
if !rigid(&o.transform) {
return Err(DocError::Invalid(format!("damaged design: occurrence `{}` has an invalid placement", o.name)));
}
// Every occurrence whose placement is not a rigid motion within range, by name.
let unplaced: Vec<String> = d.occurrences.iter().filter(|o| !crate::is_placement(&o.transform)).map(|o| format!("`{}`", o.name)).collect();
if !unplaced.is_empty() {
return Err(DocError::Invalid(format!(
"damaged design: {} not placed by a rigid motion within 1e9 mm: {}",
if unplaced.len() == 1 { "an occurrence is" } else { "occurrences are" },
unplaced.join(", ")
)));
}
if !all_unique(d.features.iter().map(|f| f.id))
|| !all_unique(d.components.iter().map(|c| c.id))
@@ -1122,26 +1126,6 @@ fn check_numbers(v: &Value, depth: usize) -> Result<()> {
Ok(())
}
/// A placement: rotation (orthonormal, not mirrored) and a translation in range.
fn rigid(m: &crate::Mat) -> bool {
let row = |i: usize| m.get(i).copied().unwrap_or_default();
let (x, y, z, t) = (row(0), row(1), row(2), row(3));
let dot = |a: [f64; 4], b: [f64; 4]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let unit = |a: [f64; 4]| (dot(a, a) - 1.0).abs() < 1e-6;
let det = x[0] * (y[1] * z[2] - y[2] * z[1]) - x[1] * (y[0] * z[2] - y[2] * z[0]) + x[2] * (y[0] * z[1] - y[1] * z[0]);
m.iter().flatten().all(|v| v.is_finite())
&& unit(x)
&& unit(y)
&& unit(z)
&& dot(x, y).abs() < 1e-6
&& dot(y, z).abs() < 1e-6
&& dot(x, z).abs() < 1e-6
&& det > 0.0
&& [x[3], y[3], z[3]].iter().all(|v| v.abs() < 1e-9)
&& (t[3] - 1.0).abs() < 1e-9
&& t[..3].iter().all(|v| v.abs() <= MAX_REAL)
}
/// Feature input names and component occurrences, which format 1 files may predate.
fn normalise(d: &mut Document) -> Result<()> {
d.normalise_component_color_indices()?;
+9
View File
@@ -1904,6 +1904,15 @@ pub fn solve_holding_with_analysis(doc: &Document, st: &ModelState, held: &Held,
dof.clear();
}
placed.remove(&0);
// A placement that is not a rigid motion within range (values that ran off to infinity, say)
// is not applied: the joints placing that occurrence report it.
let bad: Vec<u64> = placed.iter().filter(|(_, m)| !crate::is_placement(m)).map(|(occ, _)| *occ).collect();
for occ in bad {
placed.remove(&occ);
for j in asm.joints.iter().filter(|j| !j.suppressed && (j.a.occurrence == occ || j.b.occurrence == occ)) {
sol.errors.push((j.id, j.name.clone(), format!("it would place occurrence {occ} outside a rigid placement within 1e9 mm")));
}
}
sol.world = placed;
sol.dof = dof;
sol
+2 -2
View File
@@ -69,8 +69,8 @@ pub mod sheet;
pub mod text_params;
pub use assembly::{
IDENTITY, MAX_PLACEMENT_DEPTH, MAX_PLACEMENTS, Mat, Occurrence, WorldNames, apply_point, apply_vector, is_identity, map_point_expr, mat_inverse,
mat_mul, rigid,
IDENTITY, MAX_PLACEMENT_DEPTH, MAX_PLACEMENTS, Mat, Occurrence, WorldNames, apply_point, apply_vector, is_identity, is_placement,
map_point_expr, mat_inverse, mat_mul, rigid,
};
pub use document::*;
pub use eval::KeptResults;
+3 -3
View File
@@ -363,18 +363,18 @@ mod tests {
let mut state = crate::ModelState::default();
state.bodies.push(ModelBody::new("Pin".into(), kernel::cylinder(Vec3::ZERO, Vec3::Z, 2., 8.).unwrap(), 1));
let mut cache = super::super::PlacedBodies::default();
let before = super::super::world_state_reusing(&doc, &state, &mut cache);
let before = super::super::world_state_reusing(&doc, &state, &mut cache).unwrap();
assert!(state.bodies[0].cached_mesh().is_none(), "placing bodies must remain lazy");
let old = before.bodies[0].mesh();
assert!(state.bodies[0].cached_mesh().is_some());
let (_, work) = kernel::profile::capture(|| before.bodies[1].mesh());
assert_eq!(work.tessellate_ms, 0., "the second instance shares the first one's triangulation");
doc.occurrences.iter_mut().find(|o| o.id == first).unwrap().transform = crate::rigid(Vec3::new(30., 0., 0.), Vec3::ZERO, Vec3::Y, 0.9);
let moved = super::super::world_state_reusing(&doc, &state, &mut cache);
let moved = super::super::world_state_reusing(&doc, &state, &mut cache).unwrap();
let (_, work) = kernel::profile::capture(|| moved.bodies[0].mesh());
assert_eq!(work.tessellate_ms, 0., "a new pose shares the same definition");
state.bodies[0] = ModelBody::new("Pin".into(), kernel::cylinder(Vec3::ZERO, Vec3::Z, 4., 8.).unwrap(), 1);
let edited = super::super::world_state_reusing(&doc, &state, &mut cache);
let edited = super::super::world_state_reusing(&doc, &state, &mut cache).unwrap();
assert!(state.bodies[0].cached_mesh().is_none());
let new = edited.bodies[0].mesh();
assert!(state.bodies[0].cached_mesh().is_some(), "changed geometry warms its new definition cache");
+2 -1
View File
@@ -216,7 +216,8 @@ impl Evaluation {
if !due {
return;
}
let world = Arc::new(crate::world_state(doc, state));
// A world that cannot be placed is shown with the evaluation's own error, at its end.
let Ok(world) = crate::world_state(doc, state).map(Arc::new) else { return };
if let Ok(mut p) = progress.partial.lock() {
*p = (Some(Partial { features, world }), Some(web_time::Instant::now()));
}
+10 -8
View File
@@ -196,7 +196,8 @@ fn is_construction(k: &FeatureKind) -> bool {
matches!(k, FeatureKind::ConstructionPlane { .. } | FeatureKind::ConstructionAxis { .. } | FeatureKind::ConstructionPoint { .. })
}
fn shown_item(s: &Session, key: &str) -> Option<Shown> {
/// The item `key` names; bodies are looked for in the placed model `world`.
fn shown_item(s: &Session, world: &solidcraft_doc::ModelState, key: &str) -> Option<Shown> {
if let Some(f) = s.doc.find_feature(key) {
if matches!(f.kind, FeatureKind::Sketch { .. }) {
return Some(Shown::Sketch(f.id));
@@ -205,7 +206,7 @@ fn shown_item(s: &Session, key: &str) -> Option<Shown> {
return Some(Shown::Origin(f.name.clone()));
}
}
if s.world_state().bodies.iter().any(|b| b.name == key) {
if world.bodies.iter().any(|b| b.name == key) {
return Some(Shown::Body(key.to_string()));
}
if let Some(j) = s.doc.assembly.joints.iter().find(|j| j.name == key) {
@@ -333,7 +334,7 @@ fn visibility(s: &mut Session, p: &Value) -> Result<Value> {
list.iter().map(|j| super::joints::joint_id(s, Some(j), cmd).map(Shown::Joint)).collect::<Result<_>>()?
}
(Some(_), Some(_)) => return Err(bad(cmd, "give `items` or `folder`, not both")),
(Some("bodies"), None) => s.world_state().bodies.iter().map(|b| Shown::Body(b.name.clone())).collect(),
(Some("bodies"), None) => s.world_state()?.bodies.iter().map(|b| Shown::Body(b.name.clone())).collect(),
(Some("sketches"), None) => s.model.state().sketches.iter().map(|x| Shown::Sketch(x.feature)).collect(),
(Some("construction"), None) => s.doc.features.iter().filter(|f| is_construction(&f.kind)).map(|f| Shown::Origin(f.name.clone())).collect(),
(Some("origin"), None) => ORIGIN.iter().map(|o| Shown::Origin(o.to_string())).collect(),
@@ -355,8 +356,9 @@ fn visibility(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, "`items` must list sketches, bodies or construction geometry"));
}
// Every item is found before anything changes.
let world = s.world_state()?;
keys.iter()
.map(|k| shown_item(s, k).ok_or_else(|| bad(cmd, format!("no sketch, body, construction item or joint `{k}`"))))
.map(|k| shown_item(s, &world, k).ok_or_else(|| bad(cmd, format!("no sketch, body, construction item or joint `{k}`"))))
.collect::<Result<_>>()?
}
(None, None) => return Err(bad(cmd, "`items` or `folder` is required")),
@@ -712,9 +714,9 @@ fn view_apply(s: &mut Session, p: &Value) -> Result<Value> {
// Standard view names come first, as in the browser's Named Views folder.
(Some(name), None) => match crate::render::StandardView::parse(name) {
Some(sv) => {
let mut c = crate::view::home_camera(s);
let mut c = crate::view::home_camera(s)?;
c.set_view(sv);
c.fit(&crate::view::bounds(s));
c.fit(&crate::view::bounds(s)?);
c
}
None => s.doc.named_views.iter().find(|v| v.name == name).map(camera_of).ok_or_else(|| bad(cmd, format!("no named view `{name}`")))?,
@@ -1603,7 +1605,7 @@ mod tests {
let body = s.model.state().bodies[0].name.clone();
let drain_name = s.doc.feature(drain).unwrap().name.clone();
let cam = solidcraft_render::Camera::default();
let lines = |s: &Session| crate::view::scene(s, &cam).lines.len();
let lines = |s: &Session| crate::view::scene(s, &cam).unwrap().lines.len();
let shown = |s: &mut Session| {
let d = run(s, "document.inspect", json!({}));
d["sketches"].as_array().unwrap().iter().filter(|x| x["visible"] == true).count()
@@ -1631,7 +1633,7 @@ mod tests {
run(&mut s, "browser.visibility", json!({"tree_folders": ["c0/sketches"], "visible": true}));
// Bodies (following a rename) and construction planes.
run(&mut s, "browser.visibility", json!({"items": [body.clone(), "Floor", "xy"], "visible": false}));
assert!(crate::view::scene(&s, &cam).meshes.is_empty());
assert!(crate::view::scene(&s, &cam).unwrap().meshes.is_empty());
assert_eq!(s.visibility.hidden_origin, vec!["Floor".to_string(), "XY".to_string()]);
run(&mut s, "body.rename", json!({"body": body, "name": "Tray"}));
assert_eq!(s.visibility.hidden_bodies, vec!["Tray".to_string()]);
+1 -1
View File
@@ -88,7 +88,7 @@ fn clearance(s: &mut Session, p: &Value) -> Result<Value> {
},
};
let st = s.world_state();
let st = s.world_state()?;
let (mesh_bodies, solid): (Vec<&solidcraft_doc::ModelBody>, Vec<&solidcraft_doc::ModelBody>) = st.bodies.iter().partition(|b| b.body.is_mesh());
// Mesh bodies are not measured. A mesh body named in `bodies` or `against` is left out of the
// pairs and listed as unchecked, rather than refused as "no solid body".
+1 -1
View File
@@ -441,7 +441,7 @@ fn insert_two_mesh_cubes(s: &mut Session, tag: &str) -> Vec<String> {
run(s, "file.insert_mesh", json!({"path": stl.to_string_lossy()}));
}
let _ = std::fs::remove_dir_all(&dir);
s.world_state().bodies.iter().filter(|b| b.body.is_mesh()).map(|b| b.name.clone()).collect()
s.world_state().unwrap().bodies.iter().filter(|b| b.body.is_mesh()).map(|b| b.name.clone()).collect()
}
/// #3249: mesh bodies are not measured, and used to vanish from the answer (`bodies: 0`, nothing
+1 -1
View File
@@ -620,7 +620,7 @@ mod tests {
let spring = r["pasted"][1]["name"].as_str().unwrap_or_default().to_string();
let def = run(&mut s, "timeline.inspect", json!({"feature": spring}));
assert_eq!(def["definition"]["targets"], json!(["Plate (2)"]), "{def}");
let st = s.world_state();
let st = s.world_state().unwrap();
let v = |name: &str| solidcraft_kernel::measure(&st.body(name).unwrap().body).unwrap().volume;
// A spring on the wrong plate is hundreds of mm³ off. The two plates are measured apart
// (one moved 50 mm), which the boolean and the measure each round in their own way:
+1 -1
View File
@@ -155,7 +155,7 @@ fn compare_bodies(s: &mut Session, p: &Value) -> Result<Value> {
let max_points = number(cmd, p, "max_points", 8e6, 1e4..=5e7)? as usize;
let max_regions = number(cmd, p, "max_regions", 50.0, 1.0..=1000.0)? as usize;
let colors = colours(cmd, p)?;
let st = s.world_state();
let st = s.world_state()?;
let (na, nb) = pair(s, &st, p, cmd)?;
let (Some(ba), Some(bb)) = (st.body(&na), st.body(&nb)) else { return Err(bad(cmd, "no such body")) };
// Meshed within a quarter of the tolerance (the kernel goes no finer than a millionth of a
+1 -11
View File
@@ -125,17 +125,7 @@ fn move_vector(p: &Value, key: &str, default: Vec3, coordinate: bool, cmd: &str)
}
pub(super) fn valid_position(m: &Mat) -> bool {
let column = |i: usize| Vec3::new(m[i][0], m[i][1], m[i][2]);
let (x, y, z) = (column(0), column(1), column(2));
m.iter().flatten().all(|x| x.is_finite())
&& m[3][..3].iter().all(|x| x.abs() <= 1e9)
&& [x, y, z].iter().all(|axis| (axis.dot(*axis) - 1.0).abs() < 1e-6)
&& x.dot(y).abs() < 1e-6
&& x.dot(z).abs() < 1e-6
&& y.dot(z).abs() < 1e-6
&& x.cross(y).dot(z) > 0.0
&& [m[0][3], m[1][3], m[2][3]].iter().all(|x| x.abs() < 1e-9)
&& (m[3][3] - 1.0).abs() < 1e-9
solidcraft_doc::is_placement(m)
}
pub(super) fn new_component(s: &mut Session, p: &Value) -> Result<Value> {
+1 -1
View File
@@ -110,7 +110,7 @@ fn session_bodies(part: &Session, shown: &str) -> Result<Linked> {
if let Some(e) = part.model.results.iter().find_map(|r| r.error.clone()) {
return Err(EngineError::Other(format!("{shown}: the design has a feature that fails ({e}); fix it before linking it")));
}
let world = part.world_state();
let world = part.world_state()?;
let solids: Vec<String> = world.bodies.iter().filter(|b| !b.body.is_mesh()).map(|b| b.name.clone()).collect();
if solids.is_empty() {
return Err(EngineError::Other(format!("{shown}: the design has no solid bodies to link")));
@@ -24,10 +24,10 @@ fn recorded_assignment_preserves_sample_joined_material_and_timeline() {
let index = s.doc.feature_index(feature).unwrap();
s.doc_mut().marker = Some(index);
s.refresh();
assert_eq!(s.world_state().body("Plate").unwrap().component, 0);
assert_eq!(s.world_state().unwrap().body("Plate").unwrap().component, 0);
s.doc_mut().marker = None;
s.refresh();
assert_eq!(s.world_state().body("Plate").unwrap().component, to);
assert_eq!(s.world_state().unwrap().body("Plate").unwrap().component, to);
}
#[test]
@@ -39,19 +39,19 @@ fn transformed_pending_destination_repeated_transfer_preserves_world_pose() {
let occurrence = created["occurrence"].as_u64().unwrap();
run(&mut s, "occurrence.move", json!({"occurrence":occurrence,"translate":[30,20,0],"angle":90,"axis":[0,0,1],"capture":false}));
run(&mut s, "component.move_bodies", json!({"component":to,"bodies":["Block"]}));
let state = s.world_state();
let state = s.world_state().unwrap();
let bounds = state.body("Block").unwrap().mesh().bounds();
assert!(bounds.min.dist(Vec3::ZERO) < 1e-6);
assert!(bounds.max.dist(Vec3::new(10.0, 10.0, 10.0)) < 1e-6);
assert!(!s.pending_moves.contains_key(&occurrence));
let mut destination_reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
destination_reopened.refresh();
assert!(destination_reopened.world_state().body("Block").unwrap().mesh().bounds().min.dist(Vec3::ZERO) < 1e-6);
assert!(destination_reopened.world_state().unwrap().body("Block").unwrap().mesh().bounds().min.dist(Vec3::ZERO) < 1e-6);
run(&mut s, "component.move_bodies", json!({"component":0,"bodies":["Block"]}));
assert!(s.world_state().body("Block").unwrap().mesh().bounds().min.dist(Vec3::ZERO) < 1e-6);
assert!(s.world_state().unwrap().body("Block").unwrap().mesh().bounds().min.dist(Vec3::ZERO) < 1e-6);
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!(reopened.world_state().body("Block").unwrap().mesh().bounds().min.dist(Vec3::ZERO) < 1e-6);
assert!(reopened.world_state().unwrap().body("Block").unwrap().mesh().bounds().min.dist(Vec3::ZERO) < 1e-6);
}
#[test]
@@ -134,7 +134,7 @@ fn failed_transfer_cannot_change_evaluated_world_owner() {
.unwrap();
s.refresh();
assert!(s.model.result(id).unwrap().error.is_some());
assert_eq!(s.world_state().body("Block").unwrap().component, 0);
assert_eq!(s.world_state().unwrap().body("Block").unwrap().component, 0);
}
#[test]
@@ -142,18 +142,18 @@ fn transferred_thread_follows_body_frame_without_changing_world_axis() {
let mut s = Session::default();
run(&mut s, "solid.cylinder", json!({"radius":4,"height":20,"body_name":"Shaft"}));
run(&mut s, "solid.thread", json!({"face":[4,0,10],"designation":"M8"}));
let before = s.world_state().threads[0].clone();
let before = s.world_state().unwrap().threads[0].clone();
let c = run(&mut s, "component.create", json!({"name":"Threaded","grounded":false}));
let to = c["component"].as_u64().unwrap();
let occurrence = c["occurrence"].as_u64().unwrap();
run(&mut s, "occurrence.move", json!({"occurrence":occurrence,"translate":[30,20,10],"axis":[1,0,0],"angle":90,"capture":true}));
run(&mut s, "component.move_bodies", json!({"component":to,"bodies":["Shaft"]}));
let after = s.world_state().threads[0].clone();
let after = s.world_state().unwrap().threads[0].clone();
assert!(after.axis_point.dist(before.axis_point) < 1e-6);
assert!(after.axis.dist(*before.axis) < 1e-6);
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!(reopened.world_state().threads[0].axis.dist(*before.axis) < 1e-6);
assert!(reopened.world_state().unwrap().threads[0].axis.dist(*before.axis) < 1e-6);
}
#[test]
@@ -178,10 +178,10 @@ fn inherited_component_appearance_and_legacy_assignment_survive_transfer() {
legacy.refresh();
assert!(!legacy.model.state().body("Old").unwrap().transferred);
run(&mut legacy, "component.move_bodies", json!({"component":0,"bodies":["Old"]}));
assert!((legacy.world_state().body("Old").unwrap().mesh().bounds().min.x - 7.0).abs() < 1e-6);
assert!((legacy.world_state().unwrap().body("Old").unwrap().mesh().bounds().min.x - 7.0).abs() < 1e-6);
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&legacy.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!((reopened.world_state().body("Old").unwrap().mesh().bounds().min.x - 7.0).abs() < 1e-6);
assert!((reopened.world_state().unwrap().body("Old").unwrap().mesh().bounds().min.x - 7.0).abs() < 1e-6);
}
#[test]
@@ -209,7 +209,7 @@ fn earlier_selected_cylinder_emboss_keeps_source_frame_after_transfer_and_edit()
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[-2,5],"p1":[2,10]}));
run(&mut s, "sketch.finish", json!({}));
let emboss = run(&mut s, "solid.emboss", json!({"sketch":"Badge","face":[30,10,7.5],"depth":1}))["feature"].as_u64().unwrap();
let before = solidcraft_kernel::measure(&s.world_state().body("Sleeve").unwrap().body).unwrap().volume;
let before = solidcraft_kernel::measure(&s.world_state().unwrap().body("Sleeve").unwrap().body).unwrap().volume;
run(&mut s, "component.activate", json!({"component":0}));
let destination = run(&mut s, "component.create", json!({"name":"Destination"}));
let to = destination["component"].as_u64().unwrap();
@@ -220,10 +220,10 @@ fn earlier_selected_cylinder_emboss_keeps_source_frame_after_transfer_and_edit()
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!(reopened.doc.features.iter().all(|feature| reopened.model.result(feature.id).is_some_and(|result| result.error.is_none())));
assert!((solidcraft_kernel::measure(&reopened.world_state().body("Sleeve").unwrap().body).unwrap().volume - before).abs() < 1e-5);
assert!((solidcraft_kernel::measure(&reopened.world_state().unwrap().body("Sleeve").unwrap().body).unwrap().volume - before).abs() < 1e-5);
let depth = reopened.doc.feature(emboss).unwrap().param_names[0].clone();
run(&mut reopened, "parameters.change", json!({"name":depth,"expression":"2 mm"}));
assert!(solidcraft_kernel::measure(&reopened.world_state().body("Sleeve").unwrap().body).unwrap().volume > before);
assert!(solidcraft_kernel::measure(&reopened.world_state().unwrap().body("Sleeve").unwrap().body).unwrap().volume > before);
}
#[test]
@@ -235,16 +235,16 @@ fn transfer_undo_redo_restores_original_pending_source_world_pose() {
run(&mut s, "solid.box", json!({"length":2,"width":2,"height":2,"body_name":"Block"}));
run(&mut s, "occurrence.move", json!({"occurrence":occurrence,"translate":[10,0,0],"capture":false}));
let pending = s.pending_moves.clone();
let before = s.world_state().body("Block").unwrap().mesh().bounds();
let before = s.world_state().unwrap().body("Block").unwrap().mesh().bounds();
run(&mut s, "component.move_bodies", json!({"component":0,"bodies":["Block"]}));
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.pending_moves, pending);
assert_eq!(s.world_state().body("Block").unwrap().component, source);
assert!(s.world_state().body("Block").unwrap().mesh().bounds().min.dist(before.min) < 1e-6);
assert_eq!(s.world_state().unwrap().body("Block").unwrap().component, source);
assert!(s.world_state().unwrap().body("Block").unwrap().mesh().bounds().min.dist(before.min) < 1e-6);
run(&mut s, "edit.redo", json!({}));
assert!(s.pending_moves.is_empty());
assert_eq!(s.world_state().body("Block").unwrap().component, 0);
assert!(s.world_state().body("Block").unwrap().mesh().bounds().min.dist(before.min) < 1e-6);
assert_eq!(s.world_state().unwrap().body("Block").unwrap().component, 0);
assert!(s.world_state().unwrap().body("Block").unwrap().mesh().bounds().min.dist(before.min) < 1e-6);
}
#[test]
@@ -256,15 +256,15 @@ fn transfer_checkpoint_restores_pending_world_pose_and_history() {
run(&mut s, "solid.box", json!({"length":2,"width":2,"height":2,"body_name":"Block"}));
run(&mut s, "occurrence.move", json!({"occurrence":occurrence,"translate":[10,0,0],"capture":false}));
let pending = s.pending_moves.clone();
let before = s.world_state().body("Block").unwrap().mesh().bounds();
let before = s.world_state().unwrap().body("Block").unwrap().mesh().bounds();
let undo_count = s.undo.len();
let checkpoint = s.checkpoint();
run(&mut s, "component.move_bodies", json!({"component":0,"bodies":["Block"]}));
s.restore_checkpoint(checkpoint).unwrap();
assert_eq!(s.pending_moves, pending);
assert_eq!(s.undo.len(), undo_count);
assert_eq!(s.world_state().body("Block").unwrap().component, source);
assert!(s.world_state().body("Block").unwrap().mesh().bounds().min.dist(before.min) < 1e-6);
assert_eq!(s.world_state().unwrap().body("Block").unwrap().component, source);
assert!(s.world_state().unwrap().body("Block").unwrap().mesh().bounds().min.dist(before.min) < 1e-6);
}
#[test]
+1 -1
View File
@@ -77,7 +77,7 @@ fn column_of(s: &Session, p: &Value, cmd: &str) -> Result<Column> {
}
let body_name = value.get("body").and_then(Value::as_str).ok_or_else(|| bad(cmd, "face body required"))?;
let point = value.get("point").and_then(crate::params::vec3).ok_or_else(|| bad(cmd, "finite face point required"))?;
let world = s.world_state();
let world = s.world_state()?;
let body = world.body(body_name).ok_or_else(|| bad(cmd, "no face body"))?;
if body.body.is_mesh() {
return Err(bad(cmd, "mesh faces have no supported semantic lineage"));
@@ -225,14 +225,14 @@ fn certified_side_transfer_preserves_geometry_and_unsupported_taper_refuses_atom
run(&mut s, "component.move_bodies", json!({"component":destination,"bodies":["Round"]}));
run(&mut s, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&s, "Round", 10.), 2);
let before = s.world_state().body("Round").unwrap().mesh().bounds();
let before = s.world_state().unwrap().body("Round").unwrap().mesh().bounds();
let mut s = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
run(&mut s, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&s, "Round", 10.), 2);
assert!(
s.world_state().body("Round").unwrap().mesh().bounds().min.dist(before.min) < 1e-8,
s.world_state().unwrap().body("Round").unwrap().mesh().bounds().min.dist(before.min) < 1e-8,
"before {before:?}, reopened {:?}",
s.world_state().body("Round").unwrap().mesh().bounds()
s.world_state().unwrap().body("Round").unwrap().mesh().bounds()
);
// A successful ordinary modifier has no exact untapered source certificate.
let FeatureKind::Extrude { extent, .. } = &mut s.doc_mut().feature_mut(source).unwrap().kind else { panic!() };
+4 -4
View File
@@ -23,11 +23,11 @@ fn component_appearance_shared_definition_inheritance_and_delete() {
let key = table["columns"][0].as_str().unwrap().to_string();
run(&mut s, "config.row", json!({"name":"Red","values":{key.clone():"ABS - Red"}}));
run(&mut s, "config.activate", json!({"row":"Red"}));
let world = s.world_state();
let world = s.world_state().unwrap();
assert_eq!(world.bodies.len(), 2);
assert!(world.bodies.iter().all(|body| s.doc.model_body_look(body).unwrap().name == "ABS - Red"));
run(&mut s, "appearance.assign", json!({"bodies":[body],"appearance":"ABS - Yellow"}));
assert!(s.world_state().bodies.iter().all(|body| s.doc.model_body_look(body).unwrap().name == "ABS - Yellow"));
assert!(s.world_state().unwrap().bodies.iter().all(|body| s.doc.model_body_look(body).unwrap().name == "ABS - Yellow"));
run(&mut s, "config.activate", json!({"row":"Default"}));
assert!(!s.doc.appearances.components.contains_key(&component));
assert_eq!(s.doc.appearances.bodies[&body].name, "ABS - Yellow");
@@ -147,7 +147,7 @@ fn face_occurrence_pick_uses_definition_identity_and_coordinates() {
let component = run(&mut s, "component.create", json!({"name":"Part"}))["component"].as_u64().unwrap();
let body = block(&mut s);
run(&mut s, "occurrence.copy", json!({"component":component,"translate":[30,0,0]}));
let world = s.world_state();
let world = s.world_state().unwrap();
let placed = world.bodies.iter().find(|body| body.name != body.definition_name()).unwrap();
let placed_name = placed.name.clone();
let point = solidcraft_doc::apply_point(&placed.source_transform, solidcraft_geom::Vec3::new(5., 5., 10.));
@@ -157,7 +157,7 @@ fn face_occurrence_pick_uses_definition_identity_and_coordinates() {
assert!(target.point.dist(solidcraft_geom::Vec3::new(5., 5., 10.)) < 1e-8);
run(&mut s, "config.row", json!({"name":"Paint","values":{key:"ABS - Blue"}}));
run(&mut s, "config.activate", json!({"row":"Paint"}));
assert!(s.world_state().bodies.iter().all(|body| s.doc.face_colors(body).len() == 1));
assert!(s.world_state().unwrap().bodies.iter().all(|body| s.doc.face_colors(body).len() == 1));
}
#[test]
+1 -1
View File
@@ -1110,7 +1110,7 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result<Value> {
}
// New face points: their faces' names now.
if nf.kind.face_points() != f.kind.face_points() {
nf.face_names = solidcraft_doc::face_point_names(&s.doc, &s.model.state_before(id), &nf.kind, nf.component);
nf.face_names = solidcraft_doc::face_point_names(&s.doc, &s.model.state_before(id), &nf.kind, nf.component)?;
}
if let Some(slot) = s.doc_mut().feature_mut(id) {
*slot = nf;
+6 -6
View File
@@ -290,17 +290,17 @@ fn redefine_keeps_allocator_above_component_and_occurrence_ids() {
assert!(
matches!(&s.doc.features[combine_index + 1].kind, solidcraft_doc::FeatureKind::BodyTransfer { bodies, to, .. } if bodies == &["Base"] && *to == destination)
);
assert!((solidcraft_kernel::measure(&s.world_state().body("Base").unwrap().body).unwrap().volume - 6000.0).abs() < 1e-6);
assert_eq!(s.world_state().bodies.len(), 1);
assert!((solidcraft_kernel::measure(&s.world_state().unwrap().body("Base").unwrap().body).unwrap().volume - 6000.0).abs() < 1e-6);
assert_eq!(s.world_state().unwrap().bodies.len(), 1);
run(&mut s, "timeline.rollback", json!({"position":combine_index + 1}));
assert_eq!(s.world_state().body("Base").unwrap().component, 0, "the result transfers only AFTER Combine");
assert_eq!(s.world_state().unwrap().body("Base").unwrap().component, 0, "the result transfers only AFTER Combine");
run(&mut s, "edit.undo", json!({}));
run(&mut s, "edit.undo", json!({}));
assert_eq!(*s.doc, *original);
run(&mut s, "edit.redo", json!({}));
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert_eq!(reopened.world_state().body("Base").unwrap().component, destination);
assert!((solidcraft_kernel::measure(&reopened.world_state().body("Base").unwrap().body).unwrap().volume - 6000.0).abs() < 1e-6);
assert_eq!(reopened.world_state().unwrap().body("Base").unwrap().component, destination);
assert!((solidcraft_kernel::measure(&reopened.world_state().unwrap().body("Base").unwrap().body).unwrap().volume - 6000.0).abs() < 1e-6);
let max_id =
s.doc.components.iter().map(|c| c.id).chain(s.doc.occurrences.iter().map(|o| o.id)).chain(s.doc.features.iter().map(|f| f.id)).max().unwrap();
assert!(s.doc.next_id > max_id);
@@ -332,7 +332,7 @@ fn redefine_cannot_publish_a_second_transfer_that_breaks_existing_ownership() {
assert_eq!(s.pending_moves, pending);
}
let reopened = Session::new(solidcraft_doc::Document::from_json(&full).unwrap());
assert_eq!(reopened.world_state().body("Base").unwrap().component, reopened.doc.find_component("Original").unwrap());
assert_eq!(reopened.world_state().unwrap().body("Base").unwrap().component, reopened.doc.find_component("Original").unwrap());
assert!(reopened.doc.features.iter().all(|feature| reopened.model.result(feature.id).is_some_and(|result| result.error.is_none())));
}
+2 -2
View File
@@ -81,13 +81,13 @@ fn face_join_maps_the_picked_instance_body_and_point_together() {
run(&mut s, "component.activate", json!({"component":0}));
let occurrence = run(&mut s, "occurrence.copy", json!({"component":component,"translate":[100,0,0]}))["occurrence"].as_u64().unwrap();
run(&mut s, "component.activate", json!({"component":component,"occurrence":occurrence}));
let world = s.world_state();
let world = s.world_state().unwrap();
let body = world.bodies.iter().find(|body| body.body.contains(Vec3::new(139.0, 15.0, 10.0))).unwrap().name.clone();
assert_ne!(body, "Wall", "exercise an instance body name");
let result = run(&mut s, "solid.extrude", json!({"face":[140,15,10],"face_body":body,"distance":10,"operation":"join"}));
let FeatureKind::Extrude { targets, .. } = &s.doc.feature(result["feature"].as_u64().unwrap()).unwrap().kind else { panic!("extrude") };
assert_eq!(targets, &["Wall"]);
let world = s.world_state();
let world = s.world_state().unwrap();
assert_eq!(world.bodies.len(), 2);
for point in [Vec3::new(49.0, 15.0, 10.0), Vec3::new(149.0, 15.0, 10.0)] {
assert!(world.bodies.iter().any(|body| body.body.contains(point)), "{point:?}");
+3 -3
View File
@@ -113,7 +113,7 @@ fn generated_profiles_and_press_pull_convert_rotated_occurrence_picks_once() {
run(&mut s, "edit.redo", json!({}));
assert_eq!(s.doc.to_json(), after_cut);
check(&mut s, area * height, hole, height);
let world = s.world_state();
let world = s.world_state().unwrap();
let occupied = Vec3::new(35.0, 21.0 - height, 15.0);
let absent = Vec3::new(35.0, 19.0 - height, 15.0);
assert!(world.bodies.iter().any(|body| body.body.contains(occupied)));
@@ -124,7 +124,7 @@ fn generated_profiles_and_press_pull_convert_rotated_occurrence_picks_once() {
let before_volume: f64 = world.bodies.iter().map(|body| solidcraft_kernel::measure(&body.body).unwrap().volume).sum();
let before_bounds: Vec<_> = world.bodies.iter().map(|body| (body.name.clone(), body.mesh().bounds())).collect();
let reopened = Session::new(Document::from_json(&s.doc.to_json()).unwrap());
let after = reopened.world_state();
let after = reopened.world_state().unwrap();
assert!(after.bodies.iter().any(|body| body.body.contains(occupied)));
let v: f64 = after.bodies.iter().map(|body| solidcraft_kernel::measure(&body.body).unwrap().volume).sum();
assert!((v - area * height).abs() < area * height * 1e-4);
@@ -191,7 +191,7 @@ fn repeated_source_face_profile_requires_and_uses_the_exact_active_occurrence()
assert_eq!(s.doc.to_json(), before);
run(&mut s, "component.activate", json!({"component":cid,"occurrence":occurrence}));
run(&mut s, "solid.extrude", json!({"face":[55,5,10],"distance":5,"operation":"new"}));
let state = s.world_state();
let state = s.world_state().unwrap();
for x in [5.0, 55.0] {
assert!(state.bodies.iter().any(|body| body.body.contains(Vec3::new(x, 5.0, 14.0))));
assert!(!state.bodies.iter().any(|body| body.body.contains(Vec3::new(x, 5.0, 16.0))));
+21 -10
View File
@@ -179,8 +179,12 @@ const START_PROBE_MM: f64 = 0.1;
/// "Goes into a body" is decided where the extrude starts: just past the profile in the
/// direction it grows, or halfway along it. With the halfway point alone, a cut dragged far
/// past a body's far side read as a Join (#1257).
pub fn auto_operation(s: &Session, p: &Value) -> Option<&'static str> {
auto_operation_in(s, p, true)
///
/// Faces picked in the world are probed in the placed model; one that cannot be placed is the
/// error.
pub fn auto_operation(s: &Session, p: &Value) -> Result<Option<&'static str>> {
let st = if grouped_face(p).is_some() { s.world_state()? } else { s.model.state() };
Ok(auto_operation_in(s, p, &st))
}
impl Session {
@@ -193,17 +197,22 @@ impl Session {
}
}
fn auto_operation_in(s: &Session, p: &Value, world_faces: bool) -> Option<&'static str> {
/// The first of an extrude's grouped face picks.
fn grouped_face(p: &Value) -> Option<Vec3> {
p.get("faces").and_then(Value::as_array).and_then(|faces| faces.first()).and_then(vec3)
}
/// [`auto_operation`] probing the model `st`.
fn auto_operation_in(s: &Session, p: &Value, st: &solidcraft_doc::ModelState) -> Option<&'static str> {
let symmetric = p.get("to").is_none() && str_(p, "direction").is_some_and(|d| d.eq_ignore_ascii_case("symmetric"));
// Imported face groups can belong to a child of an empty active component.
// Probe in the same coordinate frame as the face picks.
let grouped = p.get("faces").and_then(Value::as_array).and_then(|faces| faces.first()).and_then(vec3);
let st = if grouped.is_some() && world_faces { s.world_state() } else { s.model.state() };
let grouped = grouped_face(p);
// The body the extrude starts on, when it is in a component inside the active one: it takes
// part as the active component's own bodies do (#5679).
let mut started_on = None;
let (base, n) = if let Some(point) = grouped {
let (mesh, index, distance, name) = super::face::face_at_state(&st, point, str_(p, "face_body")).ok().flatten()?;
let (mesh, index, distance, name) = super::face::face_at_state(st, point, str_(p, "face_body")).ok().flatten()?;
if distance > (mesh.bounds().diagonal() * 1e-3).max(1e-3) * 10.0 {
return None;
}
@@ -663,7 +672,7 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
} else if pts.is_empty() {
Vec::new()
} else {
solidcraft_doc::face_point_names(&s.doc, &s.model.state(), &kind, s.active_component)
solidcraft_doc::face_point_names(&s.doc, &s.model.state(), &kind, s.active_component)?
}
};
// Primitives have no `targets` of their own: their `targets` are the bodies taking part (#259).
@@ -820,7 +829,9 @@ fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
_ => None,
};
let operation = match str_(p, "operation") {
Some(o) if o.eq_ignore_ascii_case("auto") => Operation::parse(auto_operation_in(s, p, false).unwrap_or("new")).unwrap_or(Operation::NewBody),
Some(o) if o.eq_ignore_ascii_case("auto") => {
Operation::parse(auto_operation_in(s, p, &s.model.state()).unwrap_or("new")).unwrap_or(Operation::NewBody)
}
_ => operation(p, cmd)?,
};
let mut targets = string_list(p, "targets");
@@ -3079,8 +3090,8 @@ mod corner_tests {
let (default, reply) = cornered(None);
assert_eq!(reply["warning"], json!(null), "{reply}");
let (rolling, _) = cornered(Some("rolling"));
let ball = default.world_state().body("B").unwrap().body.clone();
let same = rolling.world_state().body("B").unwrap().body.clone();
let ball = default.world_state().unwrap().body("B").unwrap().body.clone();
let same = rolling.world_state().unwrap().body("B").unwrap().body.clone();
// `corner: rolling` is the default: the same body, to the last bit of its area.
assert_eq!(solidcraft_kernel::measure(&ball).unwrap().area, solidcraft_kernel::measure(&same).unwrap().area);
assert!(solidcraft_kernel::measure(&ball).unwrap().volume > 7000.0);
+5 -6
View File
@@ -189,7 +189,7 @@ fn emboss(s: &mut Session, p: &Value) -> Result<Value> {
let face = if let Some(value) = p.get("face") {
let local = vec3(value).ok_or_else(|| bad(cmd, "`face` must be a finite point [x,y,z]"))?;
let at = solidcraft_doc::apply_point(&s.active_component_frame(), local);
let state = s.world_state();
let state = s.world_state()?;
let mut candidates = Vec::new();
// The nearest face off the point: (distance, body), for the error when none holds it.
let mut nearest: Option<(f64, &str)> = None;
@@ -328,7 +328,7 @@ fn infer_emboss_selection(
let regions = solidcraft_doc::select_profiles(ss, profiles).map_err(|e| bad(cmd, e.to_string()))?;
let sketch_owner = s.doc.feature(sketch).map(|f| f.component).ok_or_else(|| bad(cmd, "profile sketch no longer exists"))?;
let d = s.doc.eval(&s.doc.with_design_unit(depth, Kind::Length), Kind::Length).map_err(|e| bad(cmd, e.to_string()))?;
let world = s.world_state();
let world = s.world_state()?;
let placed = s.placed_document();
let mut chosen = None;
let mut faces = 0usize;
@@ -448,7 +448,7 @@ fn align(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(o) = p.get(pk).filter(|v| v.is_object()) {
let at = o.get("at").and_then(vec3).ok_or_else(|| bad(cmd, format!("`{pk}.at` must be [x, y, z]")))?;
let kind = o.get("snap").and_then(Value::as_str).unwrap_or("vertex");
return snap_point(s, kind, at).map(|q| (q, None)).ok_or_else(|| bad(cmd, format!("no {kind} to snap to near `{pk}`")));
return snap_point(&*s.world_state()?, kind, at).map(|q| (q, None)).ok_or_else(|| bad(cmd, format!("no {kind} to snap to near `{pk}`")));
}
p.get(pk)
.and_then(vec3)
@@ -505,9 +505,8 @@ fn remove(s: &mut Session, p: &Value) -> Result<Value> {
mod tests;
/// A snap point near `at` on the model: the nearest vertex, edge midpoint, planar face's centre
/// or circular edge's centre (the end of a cylindrical face).
pub(super) fn snap_point(s: &Session, kind: &str, at: Vec3) -> Option<Vec3> {
let st = s.world_state();
/// or circular edge's centre (the end of a cylindrical face) of the placed model `st`.
pub(super) fn snap_point(st: &solidcraft_doc::ModelState, kind: &str, at: Vec3) -> Option<Vec3> {
let near = |pts: Vec<Vec3>| pts.into_iter().min_by(|a, b| a.dist(at).total_cmp(&b.dist(at)));
match kind {
"vertex" => near(
+17 -15
View File
@@ -17,14 +17,14 @@ fn grouped_face_auto_operation_keeps_implicit_and_explicit_participants() {
for (height, expected) in [(8., "cut"), (12., "join")] {
let params = json!({"faces":[[offset+2.,2,10],[offset+8.,8,10]],"face_body":"Stock",
"to":[offset+5.,5,height],"direction":"negative"});
assert_eq!(crate::auto_operation(&s, &params), Some(expected));
assert_eq!(crate::auto_operation(&s, &params).unwrap(), Some(expected));
}
for distance in [-2., 2.] {
let mut params = json!({"faces":[[offset+2.,2,10],[offset+8.,8,10]],"distance":distance});
let expected = if distance < 0. { "cut" } else { "join" };
assert_eq!(crate::auto_operation(&s, &params), Some(expected));
assert_eq!(crate::auto_operation(&s, &params).unwrap(), Some(expected));
params["face_body"] = json!("Stock");
assert_eq!(crate::auto_operation(&s, &params), Some(expected));
assert_eq!(crate::auto_operation(&s, &params).unwrap(), Some(expected));
let mut actual = s.scratch();
if offset != 0. {
run(&mut actual, "component.activate", json!({"component":part["component"]}));
@@ -54,7 +54,7 @@ fn grouped_face_auto_operation_keeps_implicit_and_explicit_participants() {
let mut params = json!({"faces":[[32,2,10],[38,8,10]],"face_body":"Stock","distance":-2});
// The picked part takes part beside the active component's own body (#5679): going in cuts
// it, and the top level's own body is left alone.
assert_eq!(crate::auto_operation(&s, &params), Some("cut"), "the picked body takes part beside the active component's own");
assert_eq!(crate::auto_operation(&s, &params).unwrap(), Some("cut"), "the picked body takes part beside the active component's own");
for operation in ["auto", "cut"] {
let mut t = s.scratch();
let mut p = params.clone();
@@ -69,9 +69,9 @@ fn grouped_face_auto_operation_keeps_implicit_and_explicit_participants() {
);
}
params["participants"] = json!(["Stock"]);
assert_eq!(crate::auto_operation(&s, &params), Some("cut"));
assert_eq!(crate::auto_operation(&s, &params).unwrap(), Some("cut"));
params["participants"] = json!(["Other"]);
assert_eq!(crate::auto_operation(&s, &params), Some("join"));
assert_eq!(crate::auto_operation(&s, &params).unwrap(), Some("join"));
}
/// A part inserted into a new design (a STEP file) sits in its own component under the empty
@@ -628,6 +628,7 @@ fn cylinder_cross_component_emboss_keeps_source_owner_and_placements() {
let placements = serde_json::to_value(&s.doc.occurrences).unwrap();
let target = s
.world_state()
.unwrap()
.bodies
.iter()
.find(|b| {
@@ -658,7 +659,7 @@ fn cylinder_cross_component_emboss_keeps_source_owner_and_placements() {
let selected = s.doc.features.last().unwrap().face_names.first().unwrap();
assert!(!selected.is_empty() && source_names.contains(selected), "{selected:?} not in {source_names:?}");
assert!((exact_cylinder_volume(&s) - before - 21.).abs() < 0.02);
let world = s.world_state();
let world = s.world_state().unwrap();
for x in [39.5, 119.5] {
assert!(
world.bodies.iter().any(|b| b
@@ -816,6 +817,7 @@ fn check_body_offset(auto: bool) {
assert_eq!(s.doc.body_component("Sleeve", s.model.state().body("Sleeve").unwrap().feature), holder);
assert!(
s.world_state()
.unwrap()
.body("Sleeve")
.unwrap()
.body
@@ -1320,7 +1322,7 @@ fn cutting_a_text_removes_each_letter_whether_its_letters_stand_apart_or_touch()
} else {
run(&mut s, "sketch.text", json!({"text":text,"at":[5,12],"height":14}));
}
let state = s.world_state();
let state = s.world_state().unwrap();
let ss = state.sketch(s.active_sketch.unwrap()).unwrap();
// The profiles cut: the letters, not their counters (the inside of an O, whose outline
// is a hole of its letter).
@@ -1335,7 +1337,7 @@ fn cutting_a_text_removes_each_letter_whether_its_letters_stand_apart_or_touch()
run(&mut s, "solid.extrude", json!({"distance":2,"operation":"cut"}));
let volume = run(&mut s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
assert!((volume - (200.0 * 40.0 * 10.0 - 2.0 * area)).abs() < 1e-3 * area, "{text:?}: {volume} for {area} mm² cut 2 mm deep");
assert_eq!(s.world_state().bodies.len(), 1);
assert_eq!(s.world_state().unwrap().bodies.len(), 1);
}
}
@@ -1353,7 +1355,7 @@ fn a_tapered_text_joins_a_block_on_its_face_or_hanging_over_its_edge() {
run(&mut s, "solid.extrude", json!({"distance":-10}));
run(&mut s, "sketch.create", json!({"plane":"XY"}));
run(&mut s, "sketch.text", json!({"text":"tres long","at":at,"height":8}));
let state = s.world_state();
let state = s.world_state().unwrap();
let ss = state.sketch(s.active_sketch.unwrap()).unwrap();
// The letters' area, counters left out (a letter's hole is its own profile too).
let sorted = |ids: &[String]| {
@@ -1370,7 +1372,7 @@ fn a_tapered_text_joins_a_block_on_its_face_or_hanging_over_its_edge() {
let volume = run(&mut s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
let (block, text) = (200.0 * 40.0 * 10.0, 2.0 * area);
assert!(volume > block + text && volume < block + 1.3 * text, "hanging {hanging}: {volume} for {text} mm³ of straight text");
assert_eq!(s.world_state().bodies.len(), 1);
assert_eq!(s.world_state().unwrap().bodies.len(), 1);
}
}
@@ -1445,7 +1447,7 @@ fn a_tapered_foot_under_a_part_joins_as_drawn() {
// for a hair first.
let tol = if round || half < 15.0 { 2e-4 } else { 1e-9 } * (part + frustum);
assert!((v - (part + frustum)).abs() < tol, "round {round}, half {half}, taper {taper}: {v} for {}", part + frustum);
assert_eq!(s.world_state().bodies.len(), 1);
assert_eq!(s.world_state().unwrap().bodies.len(), 1);
let state = s.model.state();
let mesh = state.body("B").unwrap().mesh();
// Beside the part, above the foot's start: nothing.
@@ -1826,7 +1828,7 @@ fn a_line_of_text_cuts_into_a_block_at_any_depth() {
run(&mut s, "solid.extrude", json!({"distance":-40}));
run(&mut s, "sketch.create", json!({"plane":"XY"}));
run(&mut s, "sketch.text", json!({"text":"textvraiment tres long","at":[3,9],"height":7.81}));
let state = s.world_state();
let state = s.world_state().unwrap();
let ss = state.sketch(s.active_sketch.unwrap()).unwrap();
// The letters' area, counters left out (a letter's hole is its own profile too).
let sorted = |ids: &[String]| {
@@ -1840,7 +1842,7 @@ fn a_line_of_text_cuts_into_a_block_at_any_depth() {
run(&mut s, "solid.extrude", json!({"distance":-depth,"operation":"cut"}));
let volume = run(&mut s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
removed.push((105.0 * 26.0 * 40.0 - volume) / depth);
assert_eq!(s.world_state().bodies.len(), 1);
assert_eq!(s.world_state().unwrap().bodies.len(), 1);
}
let (shallow, deep) = (removed[0], removed[1]);
assert!((deep - shallow).abs() < 1e-3 * shallow, "{shallow} mm² taken out at 1 mm, {deep} mm² at 3.5 mm");
@@ -1863,7 +1865,7 @@ fn fillets_on_tapered_bosses_saved_before_still_find_their_edges() {
}
// The build that saved it measured 69454.885209 mm³.
assert!((volume - 69454.8852092132).abs() < 1e-3, "{volume}");
assert_eq!(s.world_state().bodies.len(), 1);
assert_eq!(s.world_state().unwrap().bodies.len(), 1);
}
/// #1360: a ring drawn round a hole, its inner circle on the hole's wall, cut into a chamfered
+18 -14
View File
@@ -262,7 +262,11 @@ fn insert_step(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(m) = placement {
let occ = out["occurrence"].as_u64().ok_or_else(|| EngineError::Other("the import has no placement to move".into()))?;
if let Some(o) = s.doc_mut().occurrences.iter_mut().find(|o| o.id == occ) {
o.transform = solidcraft_doc::mat_mul(&m, &o.transform);
let placed = solidcraft_doc::mat_mul(&m, &o.transform);
if !solidcraft_doc::is_placement(&placed) {
return Err(bad(cmd, "`matrix` moves the import out of a rigid placement within 1e9 mm"));
}
o.transform = placed;
out["transform"] = json!(o.transform);
}
s.refresh();
@@ -610,7 +614,7 @@ fn export_to(s: &Session, path: &str, format: Format, bodies: &[String], quality
} else {
// Bodies carry the colour they are shown in (appearance, material or imported), and the
// file the design's properties where the format has a place for them.
solidcraft_io::export_with_quality(&s.doc.painted(&s.world_state()), bodies, format, &name, &s.doc.metadata, quality)?
solidcraft_io::export_with_quality(&s.doc.painted(&*s.world_state()?), bodies, format, &name, &s.doc.metadata, quality)?
};
solidcraft_io::vfs::write(path, &bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
Ok(json!({"path": path, "bytes": bytes.len(), "format": format!("{format:?}")}))
@@ -972,7 +976,7 @@ mod tests {
let input = target.join("issue303-step.json");
let doc = solidcraft_io::read_design(&std::fs::read(input).unwrap()).unwrap();
let mut session = Session::new(doc);
for body in &session.world_state().bodies {
for body in &session.world_state().unwrap().bodies {
body.mesh();
}
let mut samples = [Vec::new(), Vec::new()];
@@ -1011,7 +1015,7 @@ mod tests {
let mut s = Session::default();
s.execute("solid.box", &json!({"length":10,"width":20,"height":30})).unwrap();
assert!(saved_preview(&s).is_none(), "save must not triangulate a cold body");
let state = s.world_state();
let state = s.world_state().unwrap();
for body in &state.bodies {
body.mesh();
}
@@ -1062,19 +1066,19 @@ mod tests {
assert!(saved_preview(&s).is_none());
assert!(saved_geometry(&s, &s.doc).is_some(), "the timeline's bodies need no placement");
assert!(s.world_cache.lock().unwrap().is_none(), "saving must not prepare cold placements");
for body in &s.world_state().bodies {
for body in &s.world_state().unwrap().bodies {
body.mesh();
}
assert!(saved_preview(&s).is_some());
s.execute("occurrence.move", &json!({"occurrence":occurrence,"translate":[10,0,0],"capture":false})).unwrap();
for body in &s.world_state().bodies {
for body in &s.world_state().unwrap().bodies {
body.mesh();
}
assert!(!s.pending_moves.is_empty());
assert!(saved_preview(&s).is_none(), "uncaptured positions are not in the saved document");
assert!(saved_geometry(&s, &s.doc).is_none());
s.execute("component.capture_position", &json!({})).unwrap();
for body in &s.world_state().bodies {
for body in &s.world_state().unwrap().bodies {
body.mesh();
}
let preview = saved_preview(&s).unwrap();
@@ -1082,7 +1086,7 @@ mod tests {
assert!((min_x - 10.0).abs() < 1e-9);
let mut editing = s.scratch();
editing.execute("sketch.create", &json!({"plane":"XY"})).unwrap();
for body in &editing.world_state().bodies {
for body in &editing.world_state().unwrap().bodies {
body.mesh();
}
assert!(editing.active_sketch.is_some());
@@ -1096,7 +1100,7 @@ mod tests {
crate::capture::install_test_renderer();
let mut session = Session::new(Document::new("Cache opt-in"));
session.execute("solid.box", &json!({"length":10,"width":20,"height":30})).unwrap();
let world = session.world_state();
let world = session.world_state().unwrap();
for body in &world.bodies {
body.mesh();
}
@@ -1150,7 +1154,7 @@ mod tests {
fn saved_model_needs_no_meshes_and_is_adopted_only_for_its_document() {
let mut session = Session::new(Document::new("Saved model"));
session.execute("solid.box", &json!({"length":10,"width":20,"height":30})).unwrap();
let world = session.world_state();
let world = session.world_state().unwrap();
assert!(world.bodies.iter().all(|b| b.cached_mesh().is_none()));
let bytes = session.design_container(solidcraft_io::DesignCompression::Fast, false, true).unwrap();
assert!(world.bodies.iter().all(|b| b.cached_mesh().is_none()), "saving the model must not triangulate");
@@ -1230,7 +1234,7 @@ mod tests {
let target = std::env::var_os("CARGO_TARGET_DIR").map(std::path::PathBuf::from).unwrap_or_else(|| std::path::PathBuf::from("target"));
let input = std::env::var_os("SOLIDCRAFT_BENCH_FILE").map(std::path::PathBuf::from).unwrap_or_else(|| target.join("issue303-step.json"));
let mut session = Session::new(solidcraft_io::read_design(&std::fs::read(input).unwrap()).unwrap());
for body in &session.world_state().bodies {
for body in &session.world_state().unwrap().bodies {
body.mesh();
}
let modes = [("json", false, "fast"), ("container", false, "fast"), ("container", true, "fast"), ("container", true, "compact")];
@@ -1272,7 +1276,7 @@ mod tests {
assert_eq!(result["errors"], 0);
let reconstructed = start.elapsed().as_secs_f64() * 1000.;
let mut triangles = 0;
let world = session.world_state();
let world = session.world_state().unwrap();
for body in &world.bodies {
triangles += body.mesh().triangles.len();
}
@@ -1410,14 +1414,14 @@ mod tests {
let mut opened = Session::default();
watch(progress.clone(), || opened.execute("doc.open", &json!({"path":path}))).unwrap();
assert_eq!(progress.features(), (3, 3));
assert_eq!(opened.world_state().bodies.len(), 3);
assert_eq!(opened.world_state().unwrap().bodies.len(), 3);
let progress = Arc::new(Progress::default());
progress.cancel();
let mut stopped = Session::default();
watch(progress, || stopped.execute("doc.open", &json!({"path":path}))).unwrap();
assert!(stopped.model.results.iter().all(|r| r.skipped), "no feature was rebuilt after Cancel");
assert!(stopped.world_state().bodies.is_empty());
assert!(stopped.world_state().unwrap().bodies.is_empty());
std::fs::remove_dir_all(dir).unwrap();
}
+3 -3
View File
@@ -62,7 +62,7 @@ mod tests {
session.execute("solid.box", &serde_json::json!({"length":10,"width":20,"height":30,"body_name":"Visible"})).unwrap();
session.execute("solid.box", &serde_json::json!({"length":100,"width":100,"height":100,"body_name":"Hidden"})).unwrap();
session.execute("browser.visibility", &serde_json::json!({"items":["Hidden"],"visible":false})).unwrap();
let world = session.world_state();
let world = session.world_state().unwrap();
for body in &world.bodies {
body.mesh();
}
@@ -85,7 +85,7 @@ mod tests {
crate::capture::install_test_renderer();
let mut session = Session::default();
session.execute("solid.box", &serde_json::json!({"length":10,"width":20,"height":30})).unwrap();
let world = session.world_state();
let world = session.world_state().unwrap();
assert!(world.bodies.iter().all(|b| b.cached_mesh().is_none()));
assert!(saved(&session).is_none());
assert!(world.bodies.iter().all(|b| b.cached_mesh().is_none()));
@@ -106,7 +106,7 @@ mod tests {
fn without_a_renderer_a_save_has_no_thumbnail() {
let mut session = Session::default();
session.execute("solid.box", &serde_json::json!({"length":10,"width":20,"height":30})).unwrap();
for body in &session.world_state().bodies {
for body in &session.world_state().unwrap().bodies {
body.mesh();
}
let (scene, camera) = scene(&session).unwrap();
+7 -8
View File
@@ -104,7 +104,7 @@ fn measure(s: &mut Session, p: &Value) -> Result<Value> {
return super::measure_sel::measure_items(s, items);
}
let want = string_list(p, "bodies");
let st = s.world_state();
let st = s.world_state()?;
let selected: Vec<_> = st.bodies.iter().filter(|b| want.is_empty() || want.contains(&b.name)).collect();
// Each body is measured on its own: on all cores, in body order.
let mut bodies: Vec<Value> = solidcraft_kernel::map_on_threads(&selected, 16, |b| measure_json(b))
@@ -168,11 +168,10 @@ fn measure(s: &mut Session, p: &Value) -> Result<Value> {
)
}
fn sketch_json(s: &Session, id: u64) -> Option<Value> {
/// Sketch `id`, placed where the placed model `world` has it (its component's placement).
fn sketch_json(s: &Session, world: &solidcraft_doc::ModelState, id: u64) -> Option<Value> {
let state = s.model.state();
let ss = state.sketch(id)?;
// Where the sketch is in the world (its component's placement).
let world = s.world_state();
let plane = world.sketch(id).map(|w| w.plane).unwrap_or(ss.plane);
let sk = &ss.sketch;
let curves: Vec<Value> = sk
@@ -250,7 +249,7 @@ fn sketch_inspect(s: &mut Session, p: &Value) -> Result<Value> {
_ => s.active_sketch,
}
.ok_or_else(|| bad("sketch.inspect", "no such sketch (and none is active)"))?;
sketch_json(s, id).ok_or_else(|| EngineError::Other("that sketch is not evaluated (rolled back or not a sketch)".into()))
sketch_json(s, &*s.world_state()?, id).ok_or_else(|| EngineError::Other("that sketch is not evaluated (rolled back or not a sketch)".into()))
}
/// Revisions of every design of this run (#6646).
@@ -369,7 +368,7 @@ fn body_of<'a>(st: &'a solidcraft_doc::ModelState, p: &Value, cmd: &str) -> Resu
}
fn model_edges(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.world_state();
let st = s.world_state()?;
let b = body_of(&st, p, "model.edges")?;
let tol = (b.body.size() * 2e-3).max(1e-3);
let names = solidcraft_doc::naming::edge_names(b);
@@ -383,7 +382,7 @@ fn model_edges(s: &mut Session, p: &Value) -> Result<Value> {
}
fn model_faces(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.world_state();
let st = s.world_state()?;
let b = body_of(&st, p, "model.faces")?;
let names = solidcraft_doc::naming::face_names(b);
let tol = (b.body.size() * 2e-3).max(1e-3);
@@ -436,7 +435,7 @@ fn commands(s: &mut Session, _p: &Value) -> Result<Value> {
}
fn model_threads(s: &mut Session, _p: &Value) -> Result<Value> {
let st = s.world_state();
let st = s.world_state()?;
Ok(json!({ "threads": st.threads }))
}
+1 -1
View File
@@ -72,7 +72,7 @@ pub(super) fn interference(s: &mut Session, p: &Value) -> Result<Value> {
};
let budget = std::time::Duration::from_secs_f64(budget_s);
let want = string_list(p, "bodies");
let st = s.world_state();
let st = s.world_state()?;
let (meshes_left, bodies): (Vec<&solidcraft_doc::ModelBody>, Vec<&solidcraft_doc::ModelBody>) =
st.bodies.iter().filter(|b| want.is_empty() || want.contains(&b.name)).partition(|b| b.body.is_mesh());
+2 -2
View File
@@ -70,7 +70,7 @@ fn a_piece_of_a_body_inside_another_is_still_found() {
run(&mut s, "solid.box", json!({"length": 20, "width": 20, "height": 20, "corner": [0, 0, 0], "body_name": "Block"}));
run(&mut s, "solid.box", json!({"length": 2, "width": 2, "height": 2, "corner": [5, 5, 5], "operation": "new", "body_name": "Pieces"}));
run(&mut s, "solid.box", json!({"length": 2, "width": 2, "height": 2, "corner": [30, 5, 5], "operation": "join", "targets": ["Pieces"]}));
let st = s.world_state();
let st = s.world_state().unwrap();
let pieces = st.bodies.iter().find(|b| b.name == "Pieces").map(|b| b.body.shell_count());
assert_eq!(pieces, Some(2), "the two cubes are one body in two pieces");
let i = run(&mut s, "inspect.interference", json!({}));
@@ -122,7 +122,7 @@ pub(super) fn insert_two_mesh_cubes(s: &mut Session, tag: &str) -> Vec<String> {
run(s, "file.insert_mesh", json!({"path": stl.to_string_lossy()}));
}
let _ = std::fs::remove_dir_all(&dir);
s.world_state().bodies.iter().filter(|b| b.body.is_mesh()).map(|b| b.name.clone()).collect()
s.world_state().unwrap().bodies.iter().filter(|b| b.body.is_mesh()).map(|b| b.name.clone()).collect()
}
/// #3249: mesh bodies (STL, 3MF, OBJ imports) are not intersected, but they used to vanish from
+1 -1
View File
@@ -56,7 +56,7 @@ fn setup() -> Session {
}
fn measure(s: &Session, body: &str) -> solidcraft_kernel::BodyMeasure {
solidcraft_kernel::measure(&s.world_state().body(body).unwrap().body).unwrap()
solidcraft_kernel::measure(&s.world_state().unwrap().body(body).unwrap().body).unwrap()
}
fn loft(s: &mut Session, body: &str, extra: Value) -> Result<Value, String> {
+9 -7
View File
@@ -269,8 +269,8 @@ fn face_slot(b: &solidcraft_doc::ModelBody, m: &Mesh, shape: &Shape, pts: &[Vec3
super::measure_shape::slot_from_faces(shape, pts, &surfaces)
}
fn describe(s: &Session, sel: &Sel) -> Option<Item> {
let st = s.world_state();
/// `sel` in the placed model `st`.
fn describe(s: &Session, st: &solidcraft_doc::ModelState, sel: &Sel) -> Option<Item> {
let plain = |geo, dir, json| Some(Item { geo, dir, json, shape: Shape::Other, slot: None });
match sel {
Sel::Vertex { point, .. } => Some(Item {
@@ -342,8 +342,8 @@ fn describe(s: &Session, sel: &Sel) -> Option<Item> {
let v = m.measure().volume;
plain(Geo::Tris(tris), None, json!({"type": "body", "name": name, "closed": closed, "volume_mm3": closed.then_some(v)}))
}
Sel::SketchCurve { id, sketch } => sketch_curve(sketch_of(s, &st, *sketch, |sk| sk.curve_index(id).is_some())?, id),
Sel::ExternalSketchCurve { sketch, id } => sketch_curve(sketch_of(s, &st, Some(*sketch), |_| true)?, id),
Sel::SketchCurve { id, sketch } => sketch_curve(sketch_of(s, st, *sketch, |sk| sk.curve_index(id).is_some())?, id),
Sel::ExternalSketchCurve { sketch, id } => sketch_curve(sketch_of(s, st, Some(*sketch), |_| true)?, id),
Sel::SketchPoint { id } | Sel::ExternalSketchPoint { id, .. } => {
let given = match sel {
Sel::ExternalSketchPoint { sketch, .. } => Some(*sketch),
@@ -354,7 +354,7 @@ fn describe(s: &Session, sel: &Sel) -> Option<Item> {
Some((k, p)) if given.is_none() => (Some(k), p),
_ => (given, id.as_str()),
};
let ss = sketch_of(s, &st, given, |sk| sk.point_index(id).is_some())?;
let ss = sketch_of(s, st, given, |sk| sk.point_index(id).is_some())?;
let p = ss.plane.to_world(ss.sketch.points.get(ss.sketch.point_index(id)?)?.pos);
Some(Item {
geo: Geo::Points(vec![p]),
@@ -479,7 +479,7 @@ pub(crate) fn measure_items(s: &Session, items: &Value) -> Result<Value> {
if list.is_empty() || list.len() > 1000 {
return Err(bad(cmd, "measure one or two items"));
}
let st = s.world_state();
let st = s.world_state()?;
let mut sels = Vec::new();
for value in list {
let sel = match value.get("type").and_then(Value::as_str) {
@@ -498,10 +498,12 @@ pub(crate) fn measure_items(s: &Session, items: &Value) -> Result<Value> {
return Err(bad(cmd, "measure one or two items"));
}
}
let world = s.world_state()?;
let mut described = Vec::new();
for x in &sels {
described.push(
describe(s, x).ok_or_else(|| bad(cmd, "an item can't be measured (pick vertices, edges, faces, bodies, sketch curves or profiles)"))?,
describe(s, &world, x)
.ok_or_else(|| bad(cmd, "an item can't be measured (pick vertices, edges, faces, bodies, sketch curves or profiles)"))?,
);
}
let mut out = json!({"items": described.iter().map(|d| d.json.clone()).collect::<Vec<_>>()});
+5 -1
View File
@@ -1066,7 +1066,11 @@ fn explode_show(s: &mut Session, p: &Value) -> Result<Value> {
}
let i = view_index(s, p, cmd)?;
let v = s.doc.assembly.exploded.get(i).cloned().ok_or_else(|| bad(cmd, "view"))?;
s.pending_moves = exploded_at(&explode_moves(s, &v), 1.0);
let moves = exploded_at(&explode_moves(s, &v), 1.0);
if !moves.values().all(solidcraft_doc::is_placement) {
return Err(bad(cmd, format!("exploded view `{}` moves an occurrence out of range", v.name)));
}
s.pending_moves = moves;
s.revision += 1;
Ok(json!({"view": v.id, "name": v.name}))
}
+10 -7
View File
@@ -98,11 +98,11 @@ pub(super) fn export_with(s: &Session, p: &Value, renderer: Option<&Renderer>) -
Motion::Explode(moves, _) => {
let mut t = s.scratch();
t.pending_moves.clear();
let assembled = crate::view::bounds(&t);
let assembled = crate::view::bounds(&t)?;
t.pending_moves = exploded_at(moves, 1.0);
let mut c = crate::view::home_camera(&t);
c.fit(&assembled.union(&crate::view::bounds(&t)));
c
let mut c = crate::view::home_camera(&t)?;
c.fit(&assembled.union(&crate::view::bounds(&t)?));
Ok(c)
}
Motion::Drive(..) => crate::view::home_camera(s),
};
@@ -144,7 +144,7 @@ pub(super) fn export_with(s: &Session, p: &Value, renderer: Option<&Renderer>) -
t.pending_moves = exploded_at(moves, if *back { 1.0 - (2.0 * u - 1.0).abs() } else { u });
}
}
let scene = crate::view::scene_with_guides(&t, &cam, s.section, guides);
let scene = crate::view::scene_with_guides(&t, &cam, s.section, guides)?;
let picture = capture::rgba_with(Some(renderer), &scene, &cam, width, height).map_err(|e| bad(cmd, e))?;
drawn_by = picture.renderer;
if gif {
@@ -178,8 +178,11 @@ pub(super) fn export_with(s: &Session, p: &Value, renderer: Option<&Renderer>) -
/// The camera the frames are drawn from: the session's (`view.apply`), or `home` framing the
/// model, with the fields `given` sets (`ui.camera`'s reply sets them all).
fn camera(s: &Session, home: impl FnOnce() -> Camera, given: Option<&Value>, cmd: &str) -> Result<Camera> {
let base = s.camera.unwrap_or_else(home);
fn camera(s: &Session, home: impl FnOnce() -> Result<Camera>, given: Option<&Value>, cmd: &str) -> Result<Camera> {
let base = match s.camera {
Some(c) => c,
None => home()?,
};
let Some(given) = given else { return Ok(base) };
let fields = given.as_object().ok_or_else(|| bad(cmd, "`camera` must be an object, such as ui.camera's reply"))?;
let mut v = serde_json::to_value(base).map_err(|e| bad(cmd, e.to_string()))?;
+1 -1
View File
@@ -648,7 +648,7 @@ fn exploded_views_show_without_changing_the_design() {
run(&mut s, "explode.show", json!({"view": "Apart"}));
// Centres at x 5 and 25 → middle 15: each moves 10 mm further out.
let bb = |s: &mut Session, b: &str| run(s, "inspect.measure", json!({"bodies": [b]}))["bodies"][0]["bbox"]["min"][0].as_f64().unwrap_or(f64::NAN);
let names: Vec<String> = s.world_state().bodies.iter().map(|b| b.name.clone()).collect();
let names: Vec<String> = s.world_state().unwrap().bodies.iter().map(|b| b.name.clone()).collect();
assert!((bb(&mut s, &names[0]) + 10.0).abs() < 1e-6, "{names:?}");
assert!((bb(&mut s, &names[1]) - 30.0).abs() < 1e-6);
assert_eq!(s.doc.occurrences, before, "shown only");
+5 -5
View File
@@ -248,7 +248,7 @@ fn instance_selection_delete_removes_only_selected_instance() {
run(&mut s, "selection.delete", json!({"items": items}));
assert!(s.doc.occurrences.iter().any(|o| o.id == first));
assert!(!s.doc.occurrences.iter().any(|o| o.id == second));
assert_eq!(s.world_state().bodies.len(), 1);
assert_eq!(s.world_state().unwrap().bodies.len(), 1);
assert!(s.doc.components.iter().any(|c| c.id == component));
}
@@ -343,7 +343,7 @@ fn synthesized_name_collision_keeps_source_and_instance_separate() {
let instance = s.occurrence_bodies(&[second], false).unwrap()[0].clone();
assert_ne!(instance, candidate);
let frames = s.doc.body_frames(&s.model.state());
let world = s.world_state();
let world = s.world_state().unwrap();
assert!(frames.iter().all(|(name, _, _, _)| world.body(name).is_some()));
assert_eq!(world.bodies.len(), world.bodies.iter().map(|b| &b.name).collect::<std::collections::BTreeSet<_>>().len());
run(&mut s, "browser.visibility", json!({"visible":false,"paths":[[second]]}));
@@ -556,10 +556,10 @@ fn instance_styles_follow_source_identity_after_collision_rename_and_pending_mov
let instance = s.occurrence_bodies(&[second], false).unwrap()[0].clone();
let source = s.occurrence_placement(&[second]).unwrap().bodies[0].source.clone();
run(&mut s, "material.assign", json!({"bodies":[instance],"material":"Brass"}));
let world = s.world_state();
let world = s.world_state().unwrap();
assert_eq!(s.doc.body_color(world.body(&instance).unwrap()), s.doc.body_color(world.body(&source).unwrap()));
run(&mut s, "appearance.assign", json!({"components":[component.to_string()],"color":"#0000ff","opacity":0.7}));
assert_eq!(s.doc.body_color(s.world_state().body(&instance).unwrap()), Some([0.0, 0.0, 1.0]));
assert_eq!(s.doc.body_color(s.world_state().unwrap().body(&instance).unwrap()), Some([0.0, 0.0, 1.0]));
run(&mut s, "appearance.assign", json!({"bodies":[instance],"color":"#ff0000","opacity":0.5}));
run(&mut s, "appearance.assign", json!({"faces":[{"body":instance,"point":[21,1,2]}],"color":"#00ff00","opacity":0.4}));
// Exercise the point-based face fallback as well as persistent face-name matching.
@@ -568,7 +568,7 @@ fn instance_styles_follow_source_identity_after_collision_rename_and_pending_mov
}
run(&mut s, "body.rename", json!({"body":source,"name":"Shared Renamed"}));
run(&mut s, "occurrence.move", json!({"occurrence":second,"translate":[5,0,0],"capture":false}));
let world = s.world_state();
let world = s.world_state().unwrap();
for id in [first, second] {
let bodyname = s.occurrence_bodies(&[id], false).unwrap()[0].clone();
let body = world.body(&bodyname).unwrap();
@@ -100,10 +100,10 @@ fn offset_guard_live_child_follows_radius_then_retirement_is_honest() {
}
Ok(_) => {
assert!(s.model.results.iter().any(|r| r.error.is_some()), "retired parent must not keep a stale successful child");
assert!(s.world_state().body("Child").is_none());
assert!(s.world_state().unwrap().body("Child").is_none());
let reopened = Session::new(Document::from_json(&s.doc.to_json()).unwrap());
assert!(reopened.model.results.iter().any(|r| r.error.is_some()));
assert!(reopened.world_state().body("Child").is_none());
assert!(reopened.world_state().unwrap().body("Child").is_none());
}
}
}
@@ -174,7 +174,7 @@ fn offset_guard_complete_delete_keeps_dangling_profile_loadable_without_circle_r
let check = |s: &Session| {
assert!(solved(s, sketch).curve_index("Unrelated").is_some());
assert!(s.model.results.iter().find(|r| r.id == s.doc.find_feature("OffsetBody").unwrap().id).unwrap().error.is_some());
assert!(s.world_state().body("Selected").is_none(), "dangling selection must never adopt the unrelated circle");
assert!(s.world_state().unwrap().body("Selected").is_none(), "dangling selection must never adopt the unrelated circle");
};
check(&s);
let reopened = Session::new(Document::from_json(&saved).unwrap());
@@ -129,7 +129,7 @@ fn pasted_offset_boundary_has_scoped_identity_and_independent_dimensions_and_mat
healthy(&reopened);
assert!((volume(&reopened, extrusion) - sharp).abs() < 1e-6);
assert!((volume(&reopened, copied_extrusion) - rounded).abs() < 0.002);
let state = reopened.world_state();
let state = reopened.world_state().unwrap();
let bounds = state.body(&copied_body).unwrap().mesh().bounds();
assert!((bounds.min.x - (100. - 37.6)).abs() < 1e-6);
assert!((bounds.max.x - (100. + 37.6)).abs() < 1e-6);
@@ -18,7 +18,7 @@ fn breaking_offset_preserves_downstream_material_history_and_source_independence
let curves = session.model.state().sketch(sketch).unwrap().sketch.offset_boundary(&association).unwrap();
run(&mut session, "sketch.finish", json!({}));
run(&mut session, "solid.extrude", json!({"sketch":sketch,"profiles":{"offset_boundaries":[association]},"distance":2,"operation":"new"}));
let before_volume = session.world_state().bodies.last().unwrap().mesh().measure().volume.abs();
let before_volume = session.world_state().unwrap().bodies.last().unwrap().mesh().measure().volume.abs();
run(&mut session, "sketch.edit", json!({"sketch":sketch}));
let before = session.doc.to_json();
let history = session.undo.len();
@@ -29,17 +29,17 @@ fn breaking_offset_preserves_downstream_material_history_and_source_independence
matches!(&session.doc.features.last().unwrap().kind, FeatureKind::Extrude { profiles: ProfileSel::Curves { loops }, .. } if loops == &vec![curves.clone()])
);
let frozen = session.doc.to_json();
assert!((session.world_state().bodies.last().unwrap().mesh().measure().volume.abs() - before_volume).abs() < 1e-6);
assert!((session.world_state().unwrap().bodies.last().unwrap().mesh().measure().volume.abs() - before_volume).abs() < 1e-6);
run(&mut session, "edit.undo", json!({}));
assert_eq!(session.doc.to_json(), before);
run(&mut session, "edit.redo", json!({}));
assert_eq!(session.doc.to_json(), frozen);
run(&mut session, "parameters.change", json!({"name":radius,"expression":"2 mm"}));
assert!(session.model.results.iter().all(|result| result.error.is_none()));
assert!((session.world_state().bodies.last().unwrap().mesh().measure().volume.abs() - before_volume).abs() < 1e-6);
assert!((session.world_state().unwrap().bodies.last().unwrap().mesh().measure().volume.abs() - before_volume).abs() < 1e-6);
let reopened = Session::new(solidcraft_doc::Document::from_json(&session.doc.to_json()).unwrap());
assert!(reopened.model.results.iter().all(|result| result.error.is_none()));
assert!((reopened.world_state().bodies.last().unwrap().mesh().measure().volume.abs() - before_volume).abs() < 1e-6);
assert!((reopened.world_state().unwrap().bodies.last().unwrap().mesh().measure().volume.abs() - before_volume).abs() < 1e-6);
}
}
@@ -76,7 +76,7 @@ fn inset_past_corner_radius_creates_a_real_boundary_and_saved_profile_identity()
assert!(matches!(&feature.kind, FeatureKind::Extrude { profiles: ProfileSel::OffsetBoundaries { associations }, .. } if associations.len() == 1));
let check = |session: &Session| {
assert!(session.model.results.iter().all(|result| result.error.is_none()));
let state = session.world_state();
let state = session.world_state().unwrap();
let body = state.bodies.last().unwrap();
// The exact inward parallel set is a sharp 73.2 by 63.2 rectangle.
let mesh = body.mesh();
+4 -1
View File
@@ -466,6 +466,9 @@ fn insert(s: &mut Session, p: &Value) -> Result<Value> {
);
let mut m = m;
m[3] = [pt.x, pt.y, pt.z, 1.0];
if !solidcraft_doc::is_placement(&m) {
return Err(bad(cmd, "`point` must be within 1e9 mm and `direction` finite"));
}
if let Some(o) = s.doc_mut().occurrences.iter_mut().find(|o| o.id == occ) {
o.transform = m;
}
@@ -564,7 +567,7 @@ mod tests {
}
fn bbox(s: &Session, prefix: &str) -> (solidcraft_geom::Vec3, solidcraft_geom::Vec3) {
let st = s.world_state();
let st = s.world_state().unwrap();
let comp = s.doc.components.iter().find(|c| c.name.starts_with(prefix)).map(|c| c.id).unwrap_or(0);
let b = st
.bodies
+2 -2
View File
@@ -116,7 +116,7 @@ fn feature_rectangular_center_flip_and_oblique_direction_keep_source_and_recompu
}),
);
let sign = if flipped { -1.0 } else { 1.0 };
let st = s.world_state();
let st = s.world_state().unwrap();
let body = &st.bodies[0].body;
// Analytic slots: the source, +1, -1, +2. The second direction is independent.
for i in [0.0, 1.0, -1.0, 2.0] {
@@ -155,7 +155,7 @@ fn feature_circular_center_flip_uses_partial_arc_endpoints_and_full_circle_spaci
}),
);
let step = total / if total == 360.0 { 6.0 } else { 5.0 };
let st = s.world_state();
let st = s.world_state().unwrap();
for slot in [0.0, 1.0, -1.0, 2.0, -2.0, 3.0] {
let a = (step * slot * if flipped { -1.0 } else { 1.0 }).to_radians();
let p = Vec3::new(40.0 + 20.0 * a.cos(), 60.0 + 20.0 * a.sin(), 2.5);
@@ -221,7 +221,7 @@ fn settings_history_preserves_geometry_cache_and_dirty_state() {
s.execute("occurrence.move", &json!({"occurrence":component["occurrence"],"translate":[30,0,0]})).unwrap();
s.mark_saved();
let model = s.model.state();
let world = s.world_state();
let world = s.world_state().unwrap();
assert!(!Arc::ptr_eq(&model, &world), "exercise the prepared occurrence cache");
let mesh = model.bodies[0].mesh();
let undo = s.undo.len();
@@ -254,7 +254,7 @@ fn settings_history_preserves_geometry_cache_and_dirty_state() {
assert_eq!(s.doc.render.setups["Studio"].width, 1024);
assert!(Arc::ptr_eq(&model, &s.model.state()));
assert!(Arc::ptr_eq(&mesh, &s.model.state().bodies[0].mesh()));
assert!(Arc::ptr_eq(&world, &s.world_state()));
assert!(Arc::ptr_eq(&world, &s.world_state().unwrap()));
assert_eq!(s.evaluations, 0);
// A geometry change and its history still require normal evaluation.
s.execute("solid.box", &json!({"length":2,"width":3,"height":4})).unwrap();
+10 -10
View File
@@ -396,7 +396,7 @@ fn deleting_sheet_material_prunes_cosmetic_threads_and_undo_restores_them() {
assert!(s.model.state().bodies.is_empty());
assert!(s.model.state().sheets.is_empty());
assert!(s.model.state().threads.is_empty());
assert!(solidcraft_doc::world_state(&s.doc, &s.model.state()).threads.is_empty());
assert!(solidcraft_doc::world_state(&s.doc, &s.model.state()).unwrap().threads.is_empty());
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.model.state().threads.len(), 1);
}
@@ -419,7 +419,7 @@ fn sheet_pocket_transfer_keeps_local_depth_world_pose_and_history() {
let floor = sh.frame.to_world(solidcraft_geom::Vec2::new(25.0, 25.0)) + sh.frame.normal() * 1.5;
assert!(!mb.body.contains(cut));
assert!(mb.body.contains(floor));
let world = s.world_state();
let world = s.world_state().unwrap();
let body = &world.body(name).unwrap().body;
let bounds = body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap().bounds();
assert!(bounds.min.dist(Vec3::ZERO) < 1e-6, "{bounds:?}");
@@ -2629,7 +2629,7 @@ fn partial_depth_intersect_flipped_tool_and_transferred_sheet_keep_depth_side_an
base(&mut s, 100.0, 60.0);
rectangle_tool(&mut s, "RetainDepth", [20.0, 20.0], [30.0, 30.0]);
run(&mut s, "solid.extrude", json!({"sketch":"RetainDepth","distance":1,"start_offset":0.5,"operation":"intersect","name":"RetainSlab"}));
let before = s.world_state().bodies[0].mesh().bounds();
let before = s.world_state().unwrap().bodies[0].mesh().bounds();
let name = s.model.state().sheets[0].body.clone();
let destination = run(&mut s, "component.create", json!({"name":"Destination","parent":0,"grounded":false}));
let owner = destination["component"].as_u64().unwrap();
@@ -2648,7 +2648,7 @@ fn partial_depth_intersect_flipped_tool_and_transferred_sheet_keep_depth_side_an
assert_eq!(sh.pockets.len(), 2);
assert_eq!(state.body(&name).unwrap().component, owner);
assert!(state.body(&name).unwrap().transferred);
let world = s.world_state();
let world = s.world_state().unwrap();
let body = &world.body(&name).unwrap().body;
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.25)));
@@ -2658,12 +2658,12 @@ fn partial_depth_intersect_flipped_tool_and_transferred_sheet_keep_depth_side_an
assert!((solidcraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-6);
}
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened_world = reopened.world_state();
let reopened_world = reopened.world_state().unwrap();
let body = &reopened_world.body(&name).unwrap().body;
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
assert!((solidcraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-6);
run(&mut reopened, "timeline.edit", json!({"feature":"RetainSlab","set":{"extent":{"distance":"1.5"}}}));
let world = reopened.world_state();
let world = reopened.world_state().unwrap();
let body = &world.body(&name).unwrap().body;
assert!((solidcraft_kernel::measure(body).unwrap().volume - 150.0).abs() < 1e-6);
assert!(body.contains(Vec3::new(25.0, 25.0, 1.75)));
@@ -2715,7 +2715,7 @@ fn partial_depth_intersect_full_gauge_in_rotated_frames_does_not_invent_pockets(
run(&mut s, command, json!({}));
}
assert!(s.model.state().sheets[0].pockets.is_empty(), "full gauge must not acquire roundoff-depth pockets");
let world = s.world_state();
let world = s.world_state().unwrap();
let body = &world.body(&name).unwrap().body;
assert!((solidcraft_kernel::measure(body).unwrap().volume - 250.0).abs() < 1e-5);
assert!(body.contains(Vec3::new(25.0, 25.0, 0.1)));
@@ -2731,7 +2731,7 @@ fn partial_depth_intersect_full_gauge_in_rotated_frames_does_not_invent_pockets(
run(&mut s, command, json!({}));
}
assert_eq!(s.model.state().sheets[0].pockets.len(), 2);
let world = s.world_state();
let world = s.world_state().unwrap();
let body = &world.body(&name).unwrap().body;
let measured = solidcraft_kernel::measure(body).unwrap().volume;
assert!((measured - 100.0).abs() < 1e-5, "retained volume {measured}");
@@ -2740,7 +2740,7 @@ fn partial_depth_intersect_full_gauge_in_rotated_frames_does_not_invent_pockets(
assert!(!body.contains(Vec3::new(25.0, 25.0, 2.4)));
}
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let world = cold.world_state();
let world = cold.world_state().unwrap();
let body = &world.body(&name).unwrap().body;
assert!((solidcraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-5);
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
@@ -2875,7 +2875,7 @@ fn partial_depth_intersect_sub_extrusion_resolution_layers_refuse_atomically() {
assert!(error.contains("resolution") || error.contains("distance is zero") || error.contains("sheet machining"), "{error}");
assert_eq!(s.doc.to_json(), before);
assert!(s.model.state().sheets[0].pockets.is_empty());
assert!((solidcraft_kernel::measure(&s.world_state().body(&name).unwrap().body).unwrap().volume - 15000.0).abs() < 1e-5);
assert!((solidcraft_kernel::measure(&s.world_state().unwrap().body(&name).unwrap().body).unwrap().volume - 15000.0).abs() < 1e-5);
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ fn run(s: &mut Session, p: &Value) -> Result<Value> {
let camera = camera(p)?;
let rows = rows(p)?;
let wanted = body_names(p)?;
let state = s.world_state();
let state = s.world_state()?;
if let Some(names) = &wanted {
for name in names {
if !state.bodies.iter().any(|b| b.name == *name) {
+11 -11
View File
@@ -40,7 +40,7 @@ fn wire_frame_second_occurrence_world_inputs_move_and_reopen() {
}
near(sk.wires[sk.wire_index(&a).unwrap()].fit[0], Vec3::new(2.0, 3.0, 5.0));
near(sk.wires[sk.wire_index(&point).unwrap()].fit[0], Vec3::new(2.0, 3.0, 5.0));
let world = s.world_state();
let world = s.world_state().unwrap();
let ws = &world.sketch(sketch).unwrap().sketch;
let sw = &ws.wires[ws.wire_index(&spline).unwrap()];
near(sw.fit[0], Vec3::new(14.0, 32.0, 55.0));
@@ -61,9 +61,9 @@ fn wire_frame_second_occurrence_world_inputs_move_and_reopen() {
assert_eq!(restored.sketch(sketch).unwrap().wires, s.doc.sketch(sketch).unwrap().wires);
// Same definition at the first placement has the local geometry; selected second is exact.
run(&mut s, "component.activate", json!({"component":component,"occurrence":first,"path":[first]}));
near(s.world_state().sketch(sketch).unwrap().sketch.wires.iter().find(|w| w.id == point).unwrap().fit[0], Vec3::new(3.0, 2.0, 6.0));
near(s.world_state().unwrap().sketch(sketch).unwrap().sketch.wires.iter().find(|w| w.id == point).unwrap().fit[0], Vec3::new(3.0, 2.0, 6.0));
run(&mut s, "component.activate", json!({"component":component,"occurrence":second,"path":[second]}));
near(s.world_state().sketch(sketch).unwrap().sketch.wires.iter().find(|w| w.id == point).unwrap().fit[0], Vec3::new(18.0, 33.0, 46.0));
near(s.world_state().unwrap().sketch(sketch).unwrap().sketch.wires.iter().find(|w| w.id == point).unwrap().fit[0], Vec3::new(18.0, 33.0, 46.0));
}
#[test]
@@ -77,10 +77,10 @@ fn wire_frame_pending_moves_other_owner_and_legacy_world_do_not_join() {
run(&mut s, "occurrence.move", json!({"occurrence":second,"translate":[5,0,0],"capture":false}));
let point = run(&mut s, "sketch.point3d", json!({"point":[22,32,45]}))["point"].as_str().unwrap().to_string();
near(s.doc.sketch(sketch).unwrap().wires.iter().find(|w| w.id == point).unwrap().fit[0], Vec3::new(2.0, 3.0, 5.0));
near(s.world_state().sketch(sketch).unwrap().sketch.wires.iter().find(|w| w.id == legacy).unwrap().fit[0], Vec3::new(2.0, 3.0, 5.0));
near(s.world_state().sketch(sketch).unwrap().sketch.wires.iter().find(|w| w.id == point).unwrap().fit[0], Vec3::new(22.0, 32.0, 45.0));
near(s.world_state().unwrap().sketch(sketch).unwrap().sketch.wires.iter().find(|w| w.id == legacy).unwrap().fit[0], Vec3::new(2.0, 3.0, 5.0));
near(s.world_state().unwrap().sketch(sketch).unwrap().sketch.wires.iter().find(|w| w.id == point).unwrap().fit[0], Vec3::new(22.0, 32.0, 45.0));
run(&mut s, "component.capture_position", json!({}));
near(s.world_state().sketch(sketch).unwrap().sketch.wires.iter().find(|w| w.id == point).unwrap().fit[0], Vec3::new(22.0, 32.0, 45.0));
near(s.world_state().unwrap().sketch(sketch).unwrap().sketch.wires.iter().find(|w| w.id == point).unwrap().fit[0], Vec3::new(22.0, 32.0, 45.0));
// Explicit target in root must not inherit the unrelated active component frame.
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "component.activate", json!({"component":0}));
@@ -138,7 +138,7 @@ fn wire_frame_include_other_component_refresh_break_lost_and_reopen() {
let sk = s.doc.sketch(sketch).unwrap();
assert_eq!(sk.wires[0], initial);
assert!(!sk.link(&link).unwrap().lost);
let world = s.world_state();
let world = s.world_state().unwrap();
assert!(world.sketch(sketch).unwrap().sketch.wires[0].pts.iter().all(|p| (p.x - 10.0).abs() < 1e-7));
let before = s.doc.to_json();
run(&mut s, "sketch.break_link", json!({"link":link}));
@@ -179,7 +179,7 @@ fn wire_frame_pipe_component_and_world_paths_have_analytic_volume_and_position()
};
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.pipe", json!({"path_sketch":sketch,"path":[wire],"diameter":4}));
let world = s.world_state();
let world = s.world_state().unwrap();
let body = world.bodies.last().unwrap();
// Property measurement uses fine adaptive tessellation; the viewport mesh alone
// undershoots a small circular profile by about 0.64% at its display chord tolerance.
@@ -188,7 +188,7 @@ fn wire_frame_pipe_component_and_world_paths_have_analytic_volume_and_position()
let bounds = body.mesh().bounds();
near((bounds.min + bounds.max) / 2.0, Vec3::new(17.0, 32.0, 50.0));
run(&mut s, "occurrence.move", json!({"occurrence":second,"translate":[5,0,0]}));
let world = s.world_state();
let world = s.world_state().unwrap();
let body = world.bodies.last().unwrap();
let bounds = body.mesh().bounds();
near((bounds.min + bounds.max) / 2.0, Vec3::new(if space == WireSpace::Component { 22.0 } else { 17.0 }, 32.0, 50.0));
@@ -242,7 +242,7 @@ fn wire_frame_cross_owner_sweep_follows_world_path_edit_undo_and_reopen() {
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.sweep", json!({"sketch":profile,"path_sketch":path_sketch,"path":[wire],"operation":"new"}));
let check = |s: &Session, length: f64| {
let world = s.world_state();
let world = s.world_state().unwrap();
let body = world.bodies.last().unwrap();
let measure = solidcraft_kernel::measure(&body.body).unwrap();
assert!((measure.volume - 4.0 * std::f64::consts::PI * length).abs() < 0.05, "{}", measure.volume);
@@ -284,7 +284,7 @@ fn wire_frame_three_owner_sweep_profile_move_invalidates_warm_cache() {
run(&mut s, "occurrence.move", json!({"occurrence":consumer_occurrence,"axis":[0,0,1],"angle":"90 deg","translate":[100,-30,20]}));
run(&mut s, "solid.sweep", json!({"sketch":profile,"path_sketch":path_sketch,"path":[wire],"operation":"new"}));
let measure = |session: &Session, x: f64| {
let state = session.world_state();
let state = session.world_state().unwrap();
let body = state.bodies.last().unwrap();
assert_eq!(body.component, consumer["component"].as_u64().unwrap());
let result = solidcraft_kernel::measure(&body.body).unwrap();
@@ -188,7 +188,7 @@ fn paste_into_another_sketch_lands_on_its_plane() {
assert!(p[0].dist(Vec2::new(0.0, 0.0)) < 1e-6 && p[1].dist(Vec2::new(20.0, 0.0)) < 1e-6, "{p:?}");
assert_eq!(sk.constraints.iter().filter(|c| c.kind.is_dimension()).count(), 3);
assert!(!sk.constraints.iter().any(|c| c.kind.refers_to_point(0)));
let st = s.world_state();
let st = s.world_state().unwrap();
let ss = st.sketch(b).unwrap();
let w = ss.plane.to_world(Vec2::new(20.0, 10.0));
assert!(w.y.abs() < 1e-9 && (w.x - 20.0).abs() < 1e-9, "{w:?}");
@@ -216,7 +216,7 @@ fn dimensioned_authoring_uses_selected_occurrence_and_keeps_local_geometry() {
let local = model.sketch(s.active_sketch.unwrap()).unwrap();
close(local.plane.origin.x, 0.0);
close(local.plane.origin.z, 2.0);
let world = s.world_state();
let world = s.world_state().unwrap();
let displayed = world.sketch(s.active_sketch.unwrap()).unwrap();
close(displayed.plane.origin.x, 20.0);
close(displayed.plane.origin.z, 2.0);
@@ -103,7 +103,7 @@ fn pt(s: &Session, id: &str) -> solidcraft_geom::Vec2 {
fn corner(s: &Session) -> solidcraft_geom::Vec2 {
let id = s.active_sketch.expect("sketch mode");
let st = s.world_state();
let st = s.world_state().unwrap();
let sk = &st.sketch(id).expect("the shown sketch").sketch;
sk.resolve_point("l1.start").and_then(|i| sk.point(i)).expect("the corner")
}
+6 -6
View File
@@ -6,7 +6,7 @@ fn run(s: &mut Session, command: &str, params: Value) -> Value {
s.execute(command, &params).unwrap_or_else(|error| panic!("{command} {params}: {error}"))
}
fn top_pick(s: &Session, body: &str) -> Value {
let world = s.world_state();
let world = s.world_state().unwrap();
let body = world.body(body).unwrap();
let face = body
.body
@@ -35,11 +35,11 @@ fn exact_selected_base_follows_height_and_never_retargets_control() {
let height = s.doc.find_feature("BaseFeature").unwrap().param_names[2].clone();
run(&mut s, "parameters.change", json!({"name":height,"expression":"20 mm"}));
assert!((s.model.state().sketch(sketch).unwrap().plane.origin.z - 20.).abs() < 1e-6);
assert!((s.world_state().body("Badge").unwrap().mesh().bounds().max.z - 22.).abs() < 1e-6);
assert!((s.world_state().unwrap().body("Badge").unwrap().mesh().bounds().max.z - 22.).abs() < 1e-6);
run(&mut s, "timeline.delete", json!({"features":["BaseFeature"]}));
assert!(s.model.result(sketch).unwrap().error.is_some());
assert!(s.model.state().body("Badge").is_none());
assert!((s.world_state().body("Control").unwrap().mesh().bounds().max.z - 30.).abs() < 1e-6);
assert!((s.world_state().unwrap().body("Control").unwrap().mesh().bounds().max.z - 30.).abs() < 1e-6);
let before = s.doc.to_json();
let reopened = Session::new(solidcraft_doc::Document::from_json(&before).unwrap());
assert!(reopened.model.result(sketch).unwrap().error.is_some());
@@ -163,7 +163,7 @@ fn exact_nested_source_and_cross_owner_pending_paths_capture_only_involved_posit
run(&mut s, "occurrence.move", json!({"occurrence":unrelated["occurrence"],"translate":[300,0,0],"capture":false}));
let author = run(&mut s, "component.create", json!({"name":"Author","parent":0}));
run(&mut s, "occurrence.move", json!({"occurrence":author["occurrence"],"translate":[100,0,0],"angle":"90 deg","capture":false}));
let world = s.world_state();
let world = s.world_state().unwrap();
let key = s
.placed_document()
.body_frames_with_source(&s.model.state())
@@ -592,7 +592,7 @@ fn offset_rectangle_pocket_has_one_logical_outer_wall_and_closed_face_projection
/// Every flat face of every body: (body, face index, a point well inside it, its normal). The
/// middle of the face's largest triangle, so an L-shaped face is picked on itself.
fn flat_faces(s: &Session) -> Vec<(String, usize, Vec3, Vec3)> {
let world = s.world_state();
let world = s.world_state().unwrap();
let mut out = Vec::new();
for body in &world.bodies {
let mesh = body.mesh();
@@ -673,7 +673,7 @@ fn every_flat_face_of_one_sketch_extruded_up_and_down_takes_an_attached_sketch()
let script: Value = serde_json::from_str(&std::fs::read_to_string(root.join("up-down-join.json")).unwrap()).unwrap();
let mut s = Session::default();
s.run_script(&script).unwrap();
assert_eq!(s.world_state().bodies.len(), 1, "the two extrudes join into one body");
assert_eq!(s.world_state().unwrap().bodies.len(), 1, "the two extrudes join into one body");
assert_eq!(sketch_and_extrude_on_every_flat_face(&s, &dir, "synthetic"), 12, "8 side walls, top, bottom, 2 at the sketch plane");
let reopened = save_and_reopen(&mut s, &dir, "synthetic.solidcraft");
assert_eq!(sketch_and_extrude_on_every_flat_face(&reopened, &dir, "synthetic-reopened"), 12);
+7 -7
View File
@@ -1496,7 +1496,7 @@ fn irregular_closed_spline_offset_certificate_edits_undoes_and_reopens() {
let parameter = result["param"].as_str().unwrap().to_string();
let point_ids = sketch(&s).curves[1].kind.point_ids();
let verify = |s: &Session, distance: f64| {
let state = s.world_state();
let state = s.world_state().unwrap();
let sk = &state.sketch(1).unwrap().sketch;
assert_eq!(sk.curves[0], original.curves[0]);
assert_eq!(&sk.points[..original.points.len()], &original.points);
@@ -1686,7 +1686,7 @@ fn periodic_closed_offset_chain_break_drag_reopen_and_material_volume() {
let sketch_id = s.active_sketch.unwrap();
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch":sketch_id,"profiles":[selected.0],"distance":3,"operation":"new"}));
let state = s.world_state();
let state = s.world_state().unwrap();
let volume = solidcraft_kernel::measure(&state.bodies.last().unwrap().body).unwrap().volume;
assert!((volume - expected).abs() < 0.05, "{volume} vs {expected}");
}
@@ -1764,7 +1764,7 @@ fn periodic_offset_extrusion_survives_post_extrude_span_changes_and_cold_reopen(
let initial_doc = s.doc.to_json();
for major in [12.0, 16.0, 20.0] {
run(&mut s, "parameters.change", json!({"name":"d1","expression":major}));
let state = s.world_state();
let state = s.world_state().unwrap();
let sk = state.sketch(sk_id).unwrap();
let ring = sk.profiles.iter().find(|p| p.whole_curve_boundaries().as_ref() == Some(&boundary)).unwrap();
assert_ne!(ring.outer_curves.len(), initial_count, "the repro must actually change graph span multiplicity");
@@ -1819,7 +1819,7 @@ fn periodic_offset_extrusion_survives_post_extrude_span_changes_and_cold_reopen(
assert!((volume - expected).abs() < 0.05, "{major}: {volume} vs independent {expected}");
let mut cold = Session { doc: solidcraft_doc::format::read(&s.doc.to_json()).unwrap().into(), ..Default::default() };
cold.refresh();
let cold_state = cold.world_state();
let cold_state = cold.world_state().unwrap();
assert_eq!(cold_state.bodies.len(), 1);
let cold_volume = solidcraft_kernel::measure(&cold_state.bodies[0].body).unwrap().volume;
assert!((cold_volume - volume).abs() < 1e-8);
@@ -2024,7 +2024,7 @@ fn offset_driver_edits_reopens_and_undoes_without_moving_sources() {
let param = result["param"].as_str().unwrap().to_string();
let outputs = ids(&result["curves"]);
let check = |s: &Session, distance: f64| {
let state = s.world_state();
let state = s.world_state().unwrap();
let sk = &state.sketch(1).unwrap().sketch;
assert!((sk.offset_distance(&outputs[0]).unwrap().abs() - distance).abs() < 1e-8);
let constraint = sk.constraints.iter().find(|c| c.param.as_deref() == Some(&param)).unwrap();
@@ -2200,7 +2200,7 @@ fn offset_driver_uses_design_units_and_dimension_delete_freezes_current_distance
assert!((sketch(&s).offsets[0].distance - 5.08).abs() < 1e-8);
assert!(s.doc.param(param).is_none());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!((reopened.world_state().sketch(1).unwrap().sketch.offsets[0].distance - 5.08).abs() < 1e-8);
assert!((reopened.world_state().unwrap().sketch(1).unwrap().sketch.offsets[0].distance - 5.08).abs() < 1e-8);
}
#[test]
@@ -2227,7 +2227,7 @@ fn offset_driver_arc_first_retires_breaks_undoes_and_reopens() {
assert!(sketch(&s).offsets.is_empty());
assert_eq!(sketch(&s).segs(sketch(&s).curve_index(&survivor).unwrap()), before);
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(reopened.world_state().sketch(1).unwrap().sketch.offsets.is_empty());
assert!(reopened.world_state().unwrap().sketch(1).unwrap().sketch.offsets.is_empty());
run(&mut s, "edit.undo", json!({}));
assert!(s.doc.param(param).is_some());
run(&mut s, "edit.redo", json!({}));
+2 -2
View File
@@ -144,7 +144,7 @@ fn state_for_sketch<'a>(
});
match attachment {
Some(anchor) => solidcraft_doc::state_in_face_anchor_frame(doc, st, anchor, comp),
None => Ok(solidcraft_doc::state_in_frame_at(doc, st, comp, owner_frame(s, doc, comp))),
None => solidcraft_doc::state_in_frame_at(doc, st, comp, owner_frame(s, doc, comp)),
}
}
@@ -655,7 +655,7 @@ pub(super) fn project_internal_faces(s: &mut Session, id: u64, body: &str, indic
let mut doc = (*s.doc).clone();
let raw = s.model.state_before(id);
let component = doc.feature(id).map(|feature| feature.component).unwrap_or(0);
let state = solidcraft_doc::state_in_frame_at(&doc, &raw, component, owner_frame(s, &doc, component));
let state = solidcraft_doc::state_in_frame_at(&doc, &raw, component, owner_frame(s, &doc, component))?;
let plane = sketch_plane(&doc, id, &state)?;
let names = state.body(body).map(solidcraft_doc::naming::face_names).unwrap_or_default();
@@ -36,7 +36,7 @@ fn manual_projection_captures_the_requested_coincident_body_producer() {
assert!(matches!(&sk.links[0].source,solidcraft_sketch::LinkSource::Face{body,at} if body=="Target" && (at.z-20.).abs()<1e-6));
assert!(sk.points.iter().any(|point| point.pos.x > 19.));
assert!(sk.points.iter().any(|point| point.pos.y > 19.));
assert!((session.world_state().body("Control").unwrap().mesh().bounds().max.z - 10.).abs() < 1e-6);
assert!((session.world_state().unwrap().body("Control").unwrap().mesh().bounds().max.z - 10.).abs() < 1e-6);
};
verify(&s);
verify(&Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
@@ -87,7 +87,7 @@ fn live_text_resize_follows_each_frame_and_rebuilds_downstream_on_release() {
assert!((*angle + 10_f64.to_radians()).abs() < 1e-12);
// Same letters, same outlines: what references them keeps working.
assert_eq!(sk.link_curves(&link).len(), curves);
let shown = s.world_state();
let shown = s.world_state().unwrap();
assert!(shown.sketch(1).unwrap().profiles.len() >= 2, "the sketch shown follows the frame");
}
run(&mut s, "sketch.drag_end", json!({}));
@@ -122,13 +122,13 @@ fn text_move_in_a_rotated_component_preserves_font_and_local_coordinates() {
let sid = created["sketch"].as_u64().unwrap();
let result = run(&mut s, "sketch.text", json!({"text":"I","at":[0,0],"height":10,"font":solidcraft_sketch::DEFAULT_FONT_FAMILY}));
let link = result["link"].as_str().unwrap();
let before = s.world_state().sketch(sid).unwrap().plane;
let before = s.world_state().unwrap().sketch(sid).unwrap().plane;
run(&mut s, "sketch.move", json!({"entities":[result["curves"][0]],"translate":[5,2]}));
let sk = s.doc.sketch(sid).unwrap();
let LinkSource::Text { at, font, .. } = &sk.link(link).unwrap().source else { panic!() };
assert!(at.dist(Vec2::new(5.0, 2.0)) < 1e-9);
assert_eq!(font.as_deref(), Some(solidcraft_sketch::DEFAULT_FONT_FAMILY));
let state = s.world_state();
let state = s.world_state().unwrap();
assert!(state.sketch(sid).unwrap().plane.to_world(*at).dist(before.to_world(Vec2::new(5.0, 2.0))) < 1e-9);
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert_eq!(reopened.doc.sketch(sid).unwrap().link(link).unwrap().source, sk.link(link).unwrap().source);
@@ -153,7 +153,7 @@ fn text_move_rejects_malformed_values_without_changing_history() {
/// The box around the solved outlines of text `link` in sketch 1: (min, max).
fn text_box(s: &Session, link: &str) -> (Vec2, Vec2) {
let state = s.world_state();
let state = s.world_state().unwrap();
let sk = &state.sketch(1).unwrap().sketch;
let pts: Vec<Vec2> = sk.link_curves(link).into_iter().flat_map(|ci| sk.polyline(ci)).collect();
let lo = pts.iter().fold(Vec2::new(f64::MAX, f64::MAX), |m, p| Vec2::new(m.x.min(p.x), m.y.min(p.y)));
@@ -384,7 +384,7 @@ fn a_text_on_a_spline_turns_its_path_into_a_guide() {
let t = run(&mut s, "sketch.text", json!({"text":"Phago","at":[0,0],"height":10,"path":spline}));
assert_eq!(t["guide"], json!(spline));
assert!(construction(&s));
let state = s.world_state();
let state = s.world_state().unwrap();
let ss = state.sketch(s.active_sketch.unwrap()).unwrap();
for (i, p) in ss.profiles.iter().enumerate() {
let alone = p.curve_ids().iter().all(|id| ss.sketch.curves.iter().any(|c| &c.id == id && c.link.is_some()));
+67 -66
View File
@@ -773,12 +773,12 @@ mod tests {
s.execute(c, &p).unwrap_or_else(|e| panic!("{c}: {e}"))
}
fn volume(s: &Session, name: &str) -> f64 {
let st = s.world_state();
let st = s.world_state().unwrap();
solidcraft_kernel::measure(&st.body(name).unwrap().body).unwrap().volume
}
fn area(s: &Session, name: &str) -> f64 {
solidcraft_kernel::measure(&s.world_state().body(name).unwrap().body).unwrap().area
solidcraft_kernel::measure(&s.world_state().unwrap().body(name).unwrap().body).unwrap().area
}
/// Surface Extrude of open curves: a line and an arc joined end to end (listed in either
@@ -795,19 +795,19 @@ mod tests {
.to_string();
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "surface.extrude", json!({"sketch": "S", "curves": [arc, "l1"], "distance": 4, "body_name": "Wall"}));
let st = s.world_state();
let st = s.world_state().unwrap();
let wall = &st.body("Wall").unwrap().body;
assert!(wall.is_surface() && wall.face_count() == 2);
// The line's wall and the half circle's (radius 5): length × height.
let want = (10.0 + std::f64::consts::PI * 5.0) * 4.0;
assert!((area(&s, "Wall") - want).abs() < want * 1e-3, "{} vs {want}", area(&s, "Wall"));
run(&mut s, "surface.extrude", json!({"sketch": "S", "curves": ["l1"], "distance": 2, "direction": "symmetric", "body_name": "Both"}));
let bb = solidcraft_kernel::measure(&s.world_state().body("Both").unwrap().body).unwrap().bbox;
let bb = solidcraft_kernel::measure(&s.world_state().unwrap().body("Both").unwrap().body).unwrap().bbox;
assert!((bb.min.z + 2.0).abs() < 1e-9 && (bb.max.z - 2.0).abs() < 1e-9, "{bb:?}");
run(&mut s, "edit.undo", json!({}));
assert!(s.world_state().body("Both").is_none());
assert!(s.world_state().unwrap().body("Both").is_none());
run(&mut s, "edit.redo", json!({}));
assert!(s.world_state().body("Both").is_some());
assert!(s.world_state().unwrap().body("Both").is_some());
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!((area(&cold, "Wall") - area(&s, "Wall")).abs() < 1e-9);
// Refused: no such curve, a zero distance, and an extent a sheet does not take yet.
@@ -825,7 +825,7 @@ mod tests {
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 6]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "surface.extrude", json!({"sketch": "R", "distance": 3, "body_name": "Walls"}));
let st = s.world_state();
let st = s.world_state().unwrap();
let walls = &st.body("Walls").unwrap().body;
assert!(walls.is_surface() && walls.face_count() == 4);
assert!((area(&s, "Walls") - 96.0).abs() < 1e-9);
@@ -833,7 +833,7 @@ mod tests {
run(&mut s, "surface.patch", json!({"edges": edges(0.0), "body": "Walls", "body_name": "Bottom"}));
run(&mut s, "surface.patch", json!({"edges": edges(3.0), "body": "Walls", "body_name": "Top"}));
run(&mut s, "surface.stitch", json!({"bodies": ["Walls", "Bottom", "Top"]}));
let st = s.world_state();
let st = s.world_state().unwrap();
assert!(!st.body("Walls").unwrap().body.is_surface());
run(&mut s, "solid.extrude", json!({"sketch": "R", "distance": 3, "body_name": "Solid"}));
assert!((volume(&s, "Walls") - volume(&s, "Solid")).abs() < 1e-9);
@@ -849,7 +849,7 @@ mod tests {
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Box"}));
let names_before: Vec<String> = solidcraft_doc::naming::face_names(s.model.state().body("Box").unwrap()).to_vec();
run(&mut s, "surface.unstitch", json!({"body": "Box"}));
let st = s.world_state();
let st = s.world_state().unwrap();
assert!(st.body("Box").is_none());
let pieces: Vec<String> = st.bodies.iter().map(|b| b.name.clone()).collect();
assert_eq!(pieces.len(), 6);
@@ -864,19 +864,19 @@ mod tests {
b.sort();
assert_eq!(a, b, "the faces keep their names");
run(&mut s, "surface.stitch", json!({"bodies": pieces}));
let st = s.world_state();
let st = s.world_state().unwrap();
let back = st.bodies.first().unwrap();
assert!(!back.body.is_surface());
assert!((solidcraft_kernel::measure(&back.body).unwrap().volume - 1000.0).abs() < 1e-9);
run(&mut s, "edit.undo", json!({}));
run(&mut s, "edit.undo", json!({}));
assert!(s.world_state().body("Box").is_some());
assert!(s.world_state().unwrap().body("Box").is_some());
run(&mut s, "surface.unstitch", json!({"body": "Box", "faces": [[5, 5, 10], [5, 5, 0]]}));
let st = s.world_state();
let st = s.world_state().unwrap();
assert_eq!(st.bodies.len(), 3);
assert_eq!(st.body("Box").unwrap().body.face_count(), 4);
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert_eq!(cold.world_state().bodies.len(), 3);
assert_eq!(cold.world_state().unwrap().bodies.len(), 3);
assert!(s.execute("surface.unstitch", &json!({"body": "Nope"})).is_err());
assert!(s.execute("surface.unstitch", &json!({})).is_err());
}
@@ -890,11 +890,11 @@ mod tests {
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "surface.patch", json!({"sketch": "S", "body_name": "Sheet"}));
run(&mut s, "surface.thicken", json!({"bodies": ["Sheet"], "thickness": 2, "body_name": "Up"}));
let up = solidcraft_kernel::measure(&s.world_state().body("Up").unwrap().body).unwrap().bbox;
let up = solidcraft_kernel::measure(&s.world_state().unwrap().body("Up").unwrap().body).unwrap().bbox;
run(&mut s, "edit.undo", json!({}));
run(&mut s, "surface.reverse_normal", json!({"body": "Sheet"}));
run(&mut s, "surface.thicken", json!({"bodies": ["Sheet"], "thickness": 2, "body_name": "Down"}));
let down = solidcraft_kernel::measure(&s.world_state().body("Down").unwrap().body).unwrap().bbox;
let down = solidcraft_kernel::measure(&s.world_state().unwrap().body("Down").unwrap().body).unwrap().bbox;
assert!((up.max.z - down.max.z - 2.0).abs() < 1e-9 && (up.min.z - down.min.z - 2.0).abs() < 1e-9, "{up:?} vs {down:?}");
run(&mut s, "solid.box", json!({"length": 1, "width": 1, "height": 1, "corner": [20, 0, 0], "body_name": "Cube"}));
assert!(s.execute("surface.reverse_normal", &json!({"body": "Cube"})).is_err());
@@ -908,20 +908,20 @@ mod tests {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Box"}));
run(&mut s, "face.delete", json!({"faces": [[5, 5, 10]], "heal": false}));
let st = s.world_state();
let st = s.world_state().unwrap();
let open = &st.body("Box").unwrap().body;
assert!(open.is_surface() && open.face_count() == 5);
let rim = json!([[5, 0, 10], [10, 5, 10], [5, 10, 10], [0, 5, 10]]);
run(&mut s, "surface.patch", json!({"edges": rim, "body": "Box", "body_name": "Lid"}));
run(&mut s, "surface.stitch", json!({"bodies": ["Box", "Lid"]}));
assert!(!s.world_state().body("Box").unwrap().body.is_surface());
assert!(!s.world_state().unwrap().body("Box").unwrap().body.is_surface());
assert!((volume(&s, "Box") - 1000.0).abs() < 1e-9);
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!((volume(&cold, "Box") - 1000.0).abs() < 1e-9);
// By default the gap heals, and a box's sides can't close its top: refused, the box kept.
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0], "body_name": "Healed"}));
assert!(s.execute("face.delete", &json!({"faces": [[25, 5, 10]], "body": "Healed"})).is_err());
assert!(!s.world_state().body("Healed").unwrap().body.is_surface());
assert!(!s.world_state().unwrap().body("Healed").unwrap().body.is_surface());
let all = json!([[25, 5, 10], [25, 5, 0], [25, 0, 5], [25, 10, 5], [20, 5, 5], [30, 5, 5]]);
assert!(s.execute("face.delete", &json!({"faces": all, "body": "Healed", "heal": false})).is_err());
}
@@ -947,14 +947,14 @@ mod tests {
"surface.revolve",
json!({"sketch": "S", "curves": ["side"], "axis": "y", "angle": "90 deg", "symmetric": true, "body_name": "Both"}),
);
let bb = solidcraft_kernel::measure(&s.world_state().body("Both").unwrap().body).unwrap().bbox;
let bb = solidcraft_kernel::measure(&s.world_state().unwrap().body("Both").unwrap().body).unwrap().bbox;
let z = 3.0 * (PI / 4.0).sin();
assert!((bb.min.z + z).abs() < 1e-3 && (bb.max.z - z).abs() < 1e-3, "{bb:?}");
run(&mut s, "surface.revolve", json!({"sketch": "S", "curves": ["slant"], "axis": "y", "body_name": "Cone"}));
close(area(&s, "Cone"), PI * 3.0 * 5.0);
assert!(s.world_state().body("Cone").unwrap().body.is_surface());
assert!(s.world_state().unwrap().body("Cone").unwrap().body.is_surface());
run(&mut s, "edit.undo", json!({}));
assert!(s.world_state().body("Cone").is_none());
assert!(s.world_state().unwrap().body("Cone").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, "Cone") - area(&s, "Cone")).abs() < 1e-9);
@@ -964,7 +964,7 @@ mod tests {
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "surface.revolve", json!({"sketch": "R", "axis": "y", "body_name": "Ring"}));
run(&mut s, "solid.revolve", json!({"sketch": "R", "axis": "y", "body_name": "Solid"}));
assert!(!s.world_state().body("Ring").unwrap().body.is_surface());
assert!(!s.world_state().unwrap().body("Ring").unwrap().body.is_surface());
close(volume(&s, "Ring"), volume(&s, "Solid"));
// Refused: a curve across the axis, no axis, and a turn of more than 360°.
assert!(s.execute("surface.revolve", &json!({"sketch": "S", "curves": ["side"], "axis": {"origin": [3, 2, 0], "dir": [1, 0, 0]}})).is_err());
@@ -996,7 +996,7 @@ mod tests {
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());
assert!(s.world_state().unwrap().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);
@@ -1004,11 +1004,11 @@ mod tests {
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();
let st = s.world_state().unwrap();
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());
assert!(s.world_state().unwrap().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);
@@ -1034,15 +1034,15 @@ mod tests {
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [20, 0], "p1": [30, 6]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "surface.loft", json!({"sections": [{"sketch": "A", "curves": ["a"]}, {"sketch": "B", "curves": ["b"]}], "body_name": "Strip"}));
assert!(s.world_state().body("Strip").unwrap().body.is_surface());
assert!(s.world_state().unwrap().body("Strip").unwrap().body.is_surface());
assert!((area(&s, "Strip") - 50.0).abs() < 1e-6, "{}", area(&s, "Strip"));
run(&mut s, "surface.loft", json!({"sections": [{"sketch": "A", "profiles": [0]}, {"sketch": "B", "profiles": [0]}], "body_name": "Walls"}));
let st = s.world_state();
let st = s.world_state().unwrap();
let walls = &st.body("Walls").unwrap().body;
assert!(walls.is_surface() && walls.face_count() == 4);
assert!((area(&s, "Walls") - 160.0).abs() < 1e-6, "{}", area(&s, "Walls"));
run(&mut s, "edit.undo", json!({}));
assert!(s.world_state().body("Walls").is_none());
assert!(s.world_state().unwrap().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, "Walls") - area(&s, "Walls")).abs() < 1e-9);
@@ -1102,7 +1102,7 @@ mod tests {
let mut s = Session::default();
run(&mut s, "solid.cylinder", json!({"radius": 10, "height": 20, "body_name": "Can"}));
run(&mut s, "surface.offset", json!({"faces": [[10, 0, 10]], "chain": true, "distance": 2, "body": "Can", "body_name": "Sleeve"}));
let st = s.world_state();
let st = s.world_state().unwrap();
let sleeve = &st.body("Sleeve").unwrap().body;
assert!(sleeve.is_surface() && sleeve.face_count() == 2);
let want = 2.0 * std::f64::consts::PI * 12.0 * 20.0;
@@ -1110,7 +1110,7 @@ mod tests {
assert!(st.body("Can").is_some(), "the source body stays");
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [30, 0, 0], "body_name": "Box"}));
run(&mut s, "surface.offset", json!({"faces": [[35, 5, 10]], "distance": "5 mm", "body_name": "Lid"}));
let bb = solidcraft_kernel::measure(&s.world_state().body("Lid").unwrap().body).unwrap().bbox;
let bb = solidcraft_kernel::measure(&s.world_state().unwrap().body("Lid").unwrap().body).unwrap().bbox;
assert!((bb.min.z - 15.0).abs() < 1e-9 && (area(&s, "Lid") - 100.0).abs() < 1e-9);
run(&mut s, "surface.offset", json!({"faces": [[35, 5, 10]], "distance": 0, "body_name": "Copy"}));
assert!((area(&s, "Copy") - 100.0).abs() < 1e-9);
@@ -1118,7 +1118,7 @@ mod tests {
assert!(s.execute("surface.offset", &json!({"faces": [[10, 0, 10]], "chain": true, "distance": -11, "body": "Can"})).is_err());
assert_eq!(s.doc.features.len(), before);
run(&mut s, "edit.undo", json!({}));
assert!(s.world_state().body("Copy").is_none());
assert!(s.world_state().unwrap().body("Copy").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, "Sleeve") - area(&s, "Sleeve")).abs() < 1e-9);
@@ -1138,7 +1138,7 @@ mod tests {
let top = json!([5, 5, 10]);
run(&mut s, "surface.ruled", json!({"body": "Box", "edges": rim, "distance": "2 mm", "mode": "tangent", "face": top, "body_name": "Flange"}));
assert!((area(&s, "Flange") - 96.0).abs() < 1e-9, "{}", area(&s, "Flange"));
assert!(s.world_state().body("Flange").unwrap().body.is_surface() && s.world_state().body("Box").is_some());
assert!(s.world_state().unwrap().body("Flange").unwrap().body.is_surface() && s.world_state().unwrap().body("Box").is_some());
run(&mut s, "surface.ruled", json!({"body": "Box", "edges": rim, "distance": 2, "face": top, "body_name": "Wall"}));
assert!((area(&s, "Wall") - 80.0).abs() < 1e-9);
run(
@@ -1153,9 +1153,9 @@ mod tests {
"surface.ruled",
json!({"body": "Box", "edges": [[5, 0, 10]], "distance": 2, "mode": "direction", "direction": [0, -1, 0], "body_name": "Lip"}),
);
let bb = solidcraft_kernel::measure(&s.world_state().body("Lip").unwrap().body).unwrap().bbox;
let bb = solidcraft_kernel::measure(&s.world_state().unwrap().body("Lip").unwrap().body).unwrap().bbox;
assert!((area(&s, "Lip") - 20.0).abs() < 1e-9 && (bb.min.y + 2.0).abs() < 1e-9);
let st = s.world_state();
let st = s.world_state().unwrap();
let names = solidcraft_doc::naming::face_names(st.body("Flange").unwrap());
assert!(names.len() == 4 && names.iter().all(|n| !n.is_empty()), "{names:?}");
let before = s.doc.features.len();
@@ -1172,7 +1172,7 @@ mod tests {
}
assert_eq!(s.doc.features.len(), before);
run(&mut s, "edit.undo", json!({}));
assert!(s.world_state().body("Lip").is_none());
assert!(s.world_state().unwrap().body("Lip").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, "Draft") - area(&s, "Draft")).abs() < 1e-9);
@@ -1187,7 +1187,7 @@ mod tests {
}
fn edge_count(s: &Session, name: &str) -> usize {
s.world_state().body(name).unwrap().body.edge_count()
s.world_state().unwrap().body(name).unwrap().body.edge_count()
}
/// Untrim the plate's top: every hole taken away gives one face of the whole 40 × 20
@@ -1201,7 +1201,7 @@ mod tests {
let name =
solidcraft_doc::naming::face_name_at_body(s.model.state().body("Plate").unwrap(), solidcraft_geom::Vec3::new(5.0, 5.0, 2.0)).unwrap();
run(&mut s, "surface.untrim", json!({"face": {"body": "Plate", "point": top}, "body_name": "Top"}));
let st = s.world_state();
let st = s.world_state().unwrap();
let made = &st.body("Top").unwrap().body;
assert!(made.is_surface() && made.face_count() == 1);
assert_eq!(edge_count(&s, "Top"), 4, "the rectangle alone");
@@ -1209,7 +1209,7 @@ mod tests {
assert_eq!(solidcraft_doc::naming::face_names(s.model.state().body("Top").unwrap()).first(), Some(&name), "the face keeps its name");
assert!(st.body("Plate").is_some(), "the solid stays");
run(&mut s, "edit.undo", json!({}));
assert!(s.world_state().body("Top").is_none());
assert!(s.world_state().unwrap().body("Top").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, "Top") - 800.0).abs() < 1e-9);
@@ -1256,7 +1256,7 @@ mod tests {
let on = [10.0 * 2.5f64.cos(), 10.0 * 2.5f64.sin(), 5.0];
assert!(s.execute("surface.untrim", &json!({"face": {"body": "Half", "point": on}})).is_err(), "no holes, outer kept");
run(&mut s, "surface.untrim", json!({"face": {"body": "Half", "point": on}, "outer": true, "in_place": true}));
let st = s.world_state();
let st = s.world_state().unwrap();
assert_eq!(st.bodies.len(), 1, "untrimmed where it is");
assert!((area(&s, "Half") - 150.0 * pi).abs() < 150.0 * pi * 1e-3, "{}", area(&s, "Half")); // (measured on meshes)
let bb = solidcraft_kernel::measure(&st.body("Half").unwrap().body).unwrap().bbox;
@@ -1270,13 +1270,13 @@ mod tests {
fn untrim_an_imported_step_face() {
let mut s = Session::default();
plate_with_three_holes(&mut s);
let step = solidcraft_kernel::step_export(&[&s.world_state().body("Plate").unwrap().body], "untrim test").unwrap();
let step = solidcraft_kernel::step_export(&[&s.world_state().unwrap().body("Plate").unwrap().body], "untrim test").unwrap();
let path = std::env::temp_dir().join(format!("solidcraft-untrim-{}.step", std::process::id()));
std::fs::write(&path, step).unwrap();
let mut s = Session::default();
run(&mut s, "file.insert_step", json!({"path": path.to_string_lossy()}));
let _ = std::fs::remove_file(&path);
let body = s.world_state().bodies.first().unwrap().name.clone();
let body = s.world_state().unwrap().bodies.first().unwrap().name.clone();
run(&mut s, "surface.untrim", json!({"face": {"body": body, "point": [5, 5, 2]}, "holes": "all", "outer": true, "body_name": "Top"}));
assert_eq!(edge_count(&s, "Top"), 4, "the rectangle alone");
assert!((area(&s, "Top") - 800.0).abs() < 1e-9, "{}", area(&s, "Top"));
@@ -1308,7 +1308,7 @@ mod tests {
run(&mut s, "surface.extend", json!({"body": "Strip", "edges": [top], "distance": "5 mm", "name": "Up"}));
let want = 10.0 * pi / 2.0 * 25.0;
assert!((area(&s, "Strip") - want).abs() < want * 1e-3, "{} vs {want}", area(&s, "Strip")); // (measured on meshes)
assert_eq!(s.world_state().body("Strip").unwrap().body.face_count(), 1);
assert_eq!(s.world_state().unwrap().body("Strip").unwrap().body.face_count(), 1);
let saved = s.doc.to_json();
assert!(!saved.contains("\"mode\"") && !saved.contains("\"up_to\"") && !saved.contains("\"neighbours\""), "the default saves as before");
run(&mut s, "edit.undo", json!({}));
@@ -1328,7 +1328,7 @@ mod tests {
let parabola = 2.5 * (1.0 + 25.0 * k * k).sqrt() + (5.0 * k).asinh() / (2.0 * k);
for (mode, run_off) in [("tangent", 5.0), ("curvature", parabola)] {
run(&mut s, "surface.extend", json!({"body": "Strip", "edges": [side], "distance": 5, "mode": mode}));
let st = s.world_state();
let st = s.world_state().unwrap();
assert_eq!(st.body("Strip").unwrap().body.face_count(), 2, "{mode}");
let want = 10.0 * pi / 2.0 * 20.0 + run_off * 20.0;
assert!((area(&s, "Strip") - want).abs() < want * 1e-3, "{mode}: {} vs {want}", area(&s, "Strip")); // (measured on meshes)
@@ -1432,7 +1432,7 @@ mod tests {
// (A patch's face is named from geometry; its pieces inherit that, no surer.)
let sure = exact(&s, "Floor", 0);
run(&mut s, "surface.trim", json!({"body": "Floor", "keep": [5, 5, 0], "with": "Wall", "keep_with": [0, 5, 5], "stitch": true}));
let st = s.world_state();
let st = s.world_state().unwrap();
assert!(st.body("Wall").is_none(), "sewn into the floor");
let l = &st.body("Floor").unwrap().body;
assert!(l.is_surface());
@@ -1448,15 +1448,15 @@ mod tests {
assert!(exact(&s, "Floor", 0) == sure && exact(&s, "Floor", 1) == sure);
let saved = s.doc.to_json();
run(&mut s, "edit.undo", json!({}));
assert!(s.world_state().body("Wall").is_some() && (area(&s, "Floor") - 200.0).abs() < 1e-9);
assert!(s.world_state().unwrap().body("Wall").is_some() && (area(&s, "Floor") - 200.0).abs() < 1e-9);
run(&mut s, "edit.redo", json!({}));
let cold = Session::new(solidcraft_doc::Document::from_json(&saved).unwrap());
assert!((area(&cold, "Floor") - 200.0).abs() < 1e-9 && cold.world_state().body("Wall").is_none());
assert!((area(&cold, "Floor") - 200.0).abs() < 1e-9 && cold.world_state().unwrap().body("Wall").is_none());
// Without stitching, each keeps its half under its own name.
run(&mut s, "edit.undo", json!({}));
run(&mut s, "surface.trim", json!({"body": "Floor", "keep": [-5, 5, 0], "with": "Wall", "keep_with": [0, 5, -5]}));
assert!((area(&s, "Floor") - 100.0).abs() < 1e-9 && (area(&s, "Wall") - 100.0).abs() < 1e-9);
let bb = solidcraft_kernel::measure(&s.world_state().body("Wall").unwrap().body).unwrap().bbox;
let bb = solidcraft_kernel::measure(&s.world_state().unwrap().body("Wall").unwrap().body).unwrap().bbox;
assert!((bb.max.z).abs() < 1e-9, "the wall's lower half: {bb:?}");
}
@@ -1473,7 +1473,7 @@ mod tests {
let mut asked = mutual.clone();
asked["extend"] = json!(true);
run(&mut s, "surface.trim", asked);
let st = s.world_state();
let st = s.world_state().unwrap();
let l = &st.body("Floor").unwrap().body;
assert_eq!((l.face_count(), l.edge_count()), (2, 7));
assert!((area(&s, "Floor") - 200.0).abs() < 1e-6, "{}", area(&s, "Floor"));
@@ -1618,7 +1618,7 @@ mod tests {
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "surface.patch", json!({"sketch": "S", "body_name": "Sheet"}));
let st = s.world_state();
let st = s.world_state().unwrap();
assert!(st.body("Sheet").unwrap().body.is_surface());
run(&mut s, "surface.trim", json!({"body": "Sheet", "plane": {"origin": [3, 0, 0], "normal": [1, 0, 0]}, "keep": [8, 5, 0]}));
run(&mut s, "surface.extend", json!({"body": "Sheet", "edges": [[10, 5, 0]], "distance": 5}));
@@ -1628,7 +1628,7 @@ mod tests {
// Stitch the patch of a box's open top back on.
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0], "body_name": "Cup"}));
run(&mut s, "surface.patch", json!({"edges": [[25, 0, 10], [30, 5, 10], [25, 10, 10], [20, 5, 10]], "body": "Cup", "body_name": "Lid"}));
assert!(s.world_state().body("Lid").unwrap().body.is_surface());
assert!(s.world_state().unwrap().body("Lid").unwrap().body.is_surface());
// A rim that is not planar: a box with its top cut away by a sideways cylinder.
run(&mut s, "solid.box", json!({"length": 28, "width": 28, "height": 20, "corner": [-14, -14, 0], "body_name": "Saddle"}));
run(
@@ -1639,7 +1639,7 @@ mod tests {
let z = |x: f64| 50.0 - (1600.0 - x * x).sqrt();
let rim = json!([[14, 0, z(14.0)], [0, 14, z(0.0)], [-14, 0, z(-14.0)], [0, -14, z(0.0)]]);
run(&mut s, "surface.patch", json!({"edges": rim, "body": "Saddle", "body_name": "Cover"}));
let st = s.world_state();
let st = s.world_state().unwrap();
let cover = &st.body("Cover").unwrap().body;
assert!(cover.is_surface() && cover.face_count() == 1);
// The cover is a smooth surface on the rim, close to the cylinder that cut it (its area
@@ -1676,10 +1676,10 @@ mod tests {
/// always was. Hostile crowns are refused.
#[test]
fn patch_crown_fills_a_fitted_patch_and_ignores_a_planar_one() {
let top = |s: &Session| solidcraft_kernel::measure(&s.world_state().body("Cover").unwrap().body).unwrap().bbox.max.z;
let top = |s: &Session| solidcraft_kernel::measure(&s.world_state().unwrap().body("Cover").unwrap().body).unwrap().bbox.max.z;
let (plain, reply) = saddle_cover(None);
assert_eq!(reply["crown"], json!(0.0), "the crown is reported, 0 by default: {reply}");
let plain_cover = plain.world_state().body("Cover").unwrap().body.clone();
let plain_cover = plain.world_state().unwrap().body("Cover").unwrap().body.clone();
// The middle of the loop's box (28 × 28 + 1%, so a 39.9 mm diagonal) rises by crown × 39.9.
let (small, reply) = saddle_cover(Some(0.05));
assert_eq!(reply["crown"], json!(0.05), "{reply}");
@@ -1690,7 +1690,7 @@ mod tests {
// Crown 0 is the body the default makes, and stitching still closes on the rim (the
// crowned patch adds material, so the sealed cup holds more).
let (zero, _) = saddle_cover(Some(0.0));
let zero_area = solidcraft_kernel::measure(&zero.world_state().body("Cover").unwrap().body).unwrap().area;
let zero_area = solidcraft_kernel::measure(&zero.world_state().unwrap().body("Cover").unwrap().body).unwrap().area;
assert_eq!(zero_area, solidcraft_kernel::measure(&plain_cover).unwrap().area, "crown 0 is the plain patch");
let sealed = |mut s: Session| {
run(&mut s, "surface.stitch", json!({"bodies": ["Saddle", "Cover"], "body_name": "Sealed"}));
@@ -1699,7 +1699,7 @@ mod tests {
let (sealed_plain, sealed_small, sealed_big) = (sealed(plain), sealed(small), sealed(big));
// The stitch sews the cover onto the cup: one body is left, under the cup's name.
let only = |s: &Session| {
let st = s.world_state();
let st = s.world_state().unwrap();
assert_eq!(st.bodies.len(), 1, "the cover still meets the rim: {:?}", st.bodies.iter().map(|b| b.name.clone()).collect::<Vec<_>>());
st.bodies.first().map(|b| b.name.clone()).unwrap_or_default()
};
@@ -1717,7 +1717,7 @@ mod tests {
"surface.patch",
json!({"edges": [[25, 0, 10], [30, 5, 10], [25, 10, 10], [20, 5, 10]], "body": "Cup", "body_name": "Lid", "crown": 1.0}),
);
let lid = s.world_state().body("Lid").unwrap().body.clone();
let lid = s.world_state().unwrap().body("Lid").unwrap().body.clone();
let bb = solidcraft_kernel::measure(&lid).unwrap().bbox;
assert!((bb.max.z - 10.0).abs() < 1e-9, "a planar rim stays the plane: {bb:?}");
// Hostile crowns.
@@ -1758,7 +1758,7 @@ mod tests {
s
};
let only = |s: &Session| {
let st = s.world_state();
let st = s.world_state().unwrap();
assert_eq!(st.bodies.len(), 1, "the blend sews onto both boxes: {:?}", st.bodies.iter().map(|b| b.name.clone()).collect::<Vec<_>>());
st.bodies.first().map(|b| b.name.clone()).unwrap_or_default()
};
@@ -1774,7 +1774,7 @@ mod tests {
// measurable effect in this fixture (the volumes agree to 2e-5 mm³) — the blend's crown
// wiring is item 2/4's to check; this item only owns the patch's crown.
for (bulge, s, name) in [(0.0, &plain, &plain_name), (0.6, &full, &full_name)] {
let bb = solidcraft_kernel::measure(&s.world_state().body(name).unwrap().body).unwrap().bbox;
let bb = solidcraft_kernel::measure(&s.world_state().unwrap().body(name).unwrap().body).unwrap().bbox;
assert!(bb.min.z.abs() < 1e-9 && (bb.max.z - 15.0).abs() < 1e-9, "the rims stay the boxes' tops at bulge {bulge}: {bb:?}");
}
}
@@ -1799,12 +1799,12 @@ mod tests {
assert_eq!(r["warning"], json!(null), "{r}");
// The flat sheet between the two top faces: 40 x 10, at z = 10.
assert!((area(&s, "Bridge") - 400.0).abs() < 1e-6, "{}", area(&s, "Bridge"));
let bb = solidcraft_kernel::measure(&s.world_state().body("Bridge").unwrap().body).unwrap().bbox;
let bb = solidcraft_kernel::measure(&s.world_state().unwrap().body("Bridge").unwrap().body).unwrap().bbox;
assert!((bb.min.z - 10.0).abs() < 1e-9 && (bb.max.z - 10.0).abs() < 1e-9, "{bb:?}");
assert!(s.world_state().body("Bridge").unwrap().body.is_surface());
assert!(s.world_state().unwrap().body("Bridge").unwrap().body.is_surface());
// Undo, redo and a cold reopen keep it and its bodies.
run(&mut s, "edit.undo", json!({}));
assert!(s.world_state().body("Bridge").is_none());
assert!(s.world_state().unwrap().body("Bridge").is_none());
run(&mut s, "edit.redo", json!({}));
assert!((area(&s, "Bridge") - 400.0).abs() < 1e-6);
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
@@ -1815,7 +1815,7 @@ mod tests {
sewn["body_name"] = json!("Seam");
let stitched = run(&mut s, "surface.blend", sewn);
assert_eq!(stitched["removed_bodies"], json!(["A", "B"]), "{stitched}");
let st = s.world_state();
let st = s.world_state().unwrap();
assert!(st.body("A").is_none() && st.body("B").is_none() && st.body("Seam").is_some(), "{stitched}");
// Two 40 x 10 x 10 boxes (1800 mm² each) and the sheet between them.
let want = 2.0 * 1800.0 + 400.0;
@@ -1851,7 +1851,7 @@ mod tests {
let want = 2.0 * std::f64::consts::PI * 100.0;
assert!((area(&s, "Neck") - want).abs() < want * 1e-3, "{} vs {want}", area(&s, "Neck"));
let turn = |s: &Session, name: &str| {
let grid = solidcraft_kernel::sample_grid(&s.world_state().body(name).unwrap().body, 17, 5).unwrap();
let grid = solidcraft_kernel::sample_grid(&s.world_state().unwrap().body(name).unwrap().body, 17, 5).unwrap();
grid.iter()
.map(|row| {
let (a, b) = (row[0], row[row.len() - 1]);
@@ -1897,7 +1897,8 @@ mod tests {
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.mesh(&solidcraft_kernel::MeshTolerance::chord(0.05)).unwrap();
let mesh =
|s: &Session, name: &str| s.world_state().unwrap().body(name).unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(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}");
@@ -1966,7 +1967,7 @@ mod tests {
run(&mut s, "solid.sphere", json!({"radius": 20, "body_name": "Ball"}));
// A sphere is neither a plane nor a cylinder, so its offset is a fitted B-spline face.
run(&mut s, "surface.offset", json!({"faces": [[0, 0, 20]], "body": "Ball", "distance": 2, "body_name": "Shell"}));
let st = s.world_state();
let st = s.world_state().unwrap();
assert!(st.body("Shell").unwrap().body.is_surface(), "an offset sphere face is a sheet");
let before: Vec<solidcraft_geom::Vec3> = st.body("Shell").unwrap().body.edges(0.1).unwrap().iter().flat_map(|e| e.points.clone()).collect();
assert!(!before.is_empty());
@@ -2026,7 +2027,7 @@ mod tests {
"body_name": "Blend"
}),
);
let body = s.world_state().body("Blend").unwrap().body.clone();
let body = s.world_state().unwrap().body("Blend").unwrap().body.clone();
// A fine grid in the run direction: the seams sit at u = 0, 1/2 and 1, and the first step
// off each one is the tangent there (a thousandth of the run, so it is the tangent).
let grid = solidcraft_kernel::sample_grid(&body, 1001, 9).unwrap();
+1 -1
View File
@@ -133,7 +133,7 @@ fn continuity(s: &mut Session, p: &Value) -> Result<Value> {
curvature_per_mm: tolerance(p, "curvature_tolerance_per_mm", DEFAULT_CURVATURE_PER_MM)?,
};
let picks = edge_picks(p)?;
let st = s.world_state();
let st = s.world_state()?;
let named = str_(p, "body");
let candidates: Vec<&solidcraft_doc::ModelBody> = match named {
Some(name) => vec![st.body(name).ok_or_else(|| bad(cmd, format!("no body `{name}`")))?],
@@ -114,7 +114,7 @@ fn a_network_of_3d_lines_is_a_hyperbolic_paraboloid() {
// The face's own area comes from the coarse reference mesh (a chord of a fiftieth of a per
// cent of the body); the measuring mesh is refined onto the surface.
assert!((face - want).abs() < 1e-2 * want && (total - want).abs() < 1e-3 * want, "{face} / {total} vs {want}");
let st = s.world_state();
let st = s.world_state().unwrap();
let mesh = st.body(&body).map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01))).and_then(Result::ok).unwrap_or_default();
assert!(!mesh.positions.is_empty());
for p in &mesh.positions {
@@ -194,13 +194,13 @@ fn hostile_networks_are_refused() {
let mut s = Session::default();
let (sheet, top) = cylinder_and_line(&mut s);
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [40, 40, 0]}));
let boxed = s.world_state().bodies.last().map(|b| b.name.clone()).unwrap_or_default();
let boxed = s.world_state().unwrap().bodies.last().map(|b| b.name.clone()).unwrap_or_default();
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Flat"}));
let a = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
let b = line(&mut s, [0.0, 5.0], [10.0, 5.0]);
let far = line(&mut s, [0.0, 20.0], [10.0, 20.0]);
let ring = first(&run(&mut s, "sketch.circle.center", json!({"center": [50, 50], "radius": 3}))["created"]["curves"]);
let bodies = s.world_state().bodies.len();
let bodies = s.world_state().unwrap().bodies.len();
let edge = |g: &str| json!({"body": sheet, "edges": [[5.0, 10.0, 0.0]], "continuity": g});
let many: Vec<Value> = (0..33).map(|_| json!(a)).collect();
let cases: Vec<(Value, &str)> = vec![
@@ -240,7 +240,7 @@ fn hostile_networks_are_refused() {
assert!(!matches!(e, crate::EngineError::Internal(..)), "{params}: {e}");
assert!(e.to_string().contains(want), "{params}: {e}");
}
assert_eq!(s.world_state().bodies.len(), bodies, "a refused network left a body behind");
assert_eq!(s.world_state().unwrap().bodies.len(), bodies, "a refused network left a body behind");
}
/// An edit (`timeline.redefine`) may turn a loop of sketch curves into the network of the same
@@ -259,7 +259,7 @@ fn an_edit_turns_a_loop_into_a_network_and_back() {
run(&mut s, "timeline.redefine", json!({"feature": "Patch", "command": "surface.boundary", "params": network}));
let kind = |s: &Session| s.doc.find_feature("Patch").map(|f| f.kind.clone());
assert!(matches!(kind(&s), Some(solidcraft_doc::FeatureKind::SurfaceNetwork { .. })), "{:?}", kind(&s));
let body = s.world_state().bodies.first().map(|b| b.name.clone()).unwrap_or_default();
let body = s.world_state().unwrap().bodies.first().map(|b| b.name.clone()).unwrap_or_default();
assert!((areas(&mut s, &body).0 - 100.0).abs() < 1e-6);
run(&mut s, "timeline.redefine", json!({"feature": "Patch", "command": "surface.boundary", "params": {"sketch": "Square", "curves": sides}}));
assert!(matches!(kind(&s), Some(solidcraft_doc::FeatureKind::SurfaceBoundary { .. })), "{:?}", kind(&s));
+3 -3
View File
@@ -73,7 +73,7 @@ mod tests {
/// at either end. The seam is where `solid.extrude` starts the circle, so it is read off the
/// body rather than assumed.
fn picks(s: &Session) -> Vec<Vec3> {
let boss = s.world_state().body("Boss").unwrap().body.clone();
let boss = s.world_state().unwrap().body("Boss").unwrap().body.clone();
let e = boss
.edges(boss.size() * 1e-2)
.unwrap()
@@ -109,7 +109,7 @@ mod tests {
/// is from the picked seam line, the angle between the two surfaces' normals across it, and
/// the continuity it holds.
fn seam(s: &Session) -> (f64, f64, Continuity) {
let st = s.world_state();
let st = s.world_state().unwrap();
let made = st.body("Skirt").unwrap().body.clone();
let target = st.body("Boss").unwrap().body.clone();
solidcraft_kernel::seam_continuity(&made, &target, &picks(s)).unwrap()
@@ -127,7 +127,7 @@ mod tests {
assert!((gap - 1.0).abs() < 0.05, "the skirt starts a millimetre off the seam: {gap}");
let chain = picks(&s);
run(&mut s, "surface.match", json!({"body": "Skirt", "target": "Boss", "edges": chain, "order": "g2"}));
let st = s.world_state();
let st = s.world_state().unwrap();
assert!(st.body("Skirt").is_some(), "the matched body keeps its name");
assert!(st.body("Boss").is_some(), "the body it was matched to stays too");
assert!(st.body("Skirt").unwrap().body.is_surface(), "a match stays a surface");
+2 -2
View File
@@ -31,7 +31,7 @@ fn setup() -> Session {
}
fn measure(s: &Session, body: &str) -> solidcraft_kernel::BodyMeasure {
solidcraft_kernel::measure(&s.world_state().body(body).unwrap().body).unwrap()
solidcraft_kernel::measure(&s.world_state().unwrap().body(body).unwrap().body).unwrap()
}
fn sweep(s: &mut Session, sketch: &str, body: &str, extra: Value) -> Result<Value, String> {
@@ -92,7 +92,7 @@ fn twisted_sweep_faces_follow_the_profile_curves() {
// The end cap turns with the twist: at 90 deg a square's corners are back where they were,
// at 45 deg its top reaches out to the corner radius along the axes.
let reach = |s: &Session| {
let mesh = s.world_state().body("Twisted").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
let mesh = s.world_state().unwrap().body("Twisted").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
mesh.positions.iter().filter(|p| (p.z - 40.0).abs() < 1e-6).map(|p| p.x).fold(f64::MIN, f64::max)
};
assert!((reach(&s) - 5.0).abs() < 1e-3, "{}", reach(&s));
+1 -1
View File
@@ -39,7 +39,7 @@ fn setup() -> Session {
}
fn measure(s: &Session, body: &str) -> solidcraft_kernel::BodyMeasure {
solidcraft_kernel::measure(&s.world_state().body(body).unwrap().body).unwrap()
solidcraft_kernel::measure(&s.world_state().unwrap().body(body).unwrap().body).unwrap()
}
fn sweep(s: &mut Session, body: &str, extra: Value) -> Result<Value, String> {
+1 -1
View File
@@ -90,7 +90,7 @@ fn rigid(m: &Mat) -> bool {
}
pub(super) fn prepare(s: &Session, tolerance: f64) -> Result<Scene> {
let world = s.world_state();
let world = s.world_state()?;
let state = s.model.state();
let mut definitions = BTreeMap::new();
for body in &state.bodies {
+3 -3
View File
@@ -64,7 +64,7 @@ fn steps_around(m: &Mesh, face: usize, origin: Vec3, axis: Vec3) -> (usize, f64)
#[test]
fn a_large_sheet_shows_its_small_bends_and_holes_round() {
let s = large_sheet();
let st = s.world_state();
let st = s.world_state().unwrap();
let b = &st.bodies[0];
let m = b.mesh();
assert!(b.body.size() * 1e-3 > 3.0, "the chord tolerance is more than any radius here");
@@ -97,7 +97,7 @@ fn a_large_sheet_shows_its_small_bends_and_holes_round() {
}
}
// And the scene the renderers take draws those same polylines.
let scene = crate::view::scene(&s, &crate::view::home_camera(&s));
let scene = crate::view::scene(&s, &crate::view::home_camera(&s).unwrap()).unwrap();
let drawn: usize =
scene.lines.iter().filter(|l| l.points.iter().all(|p| ((p.x - 30.0).hypot(p.y - 20.0) - 2.0).abs() < 1e-6)).map(|l| l.points.len() - 1).sum();
assert!(drawn >= 2 * (TAU / DISPLAY_ANGLE).ceil() as usize, "{drawn} rim segments in the scene");
@@ -106,7 +106,7 @@ fn a_large_sheet_shows_its_small_bends_and_holes_round() {
#[test]
fn what_reads_the_display_mesh_of_a_large_sheet_answers_as_before() {
let s = large_sheet();
let st = s.world_state();
let st = s.world_state().unwrap();
let b = &st.bodies[0];
let m = b.mesh();
let coarse = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord((b.body.size() * 1e-3).max(1e-3))).unwrap();
+19 -9
View File
@@ -143,13 +143,12 @@ fn pt(p: Vec3) -> Value {
}
/// The frame of the body a world point lies on (the nearest), or of the named body.
fn frame_at(s: &Session, f: &Frames, p: Vec3, body: Option<&str>) -> Option<Mat> {
fn frame_at(world: &solidcraft_doc::ModelState, f: &Frames, p: Vec3, body: Option<&str>) -> Option<Mat> {
if let Some(b) = body
&& let Some((_, _, m)) = f.bodies(s).iter().find(|(w, _, _)| w == b)
{
return Some(*m);
}
let world = s.world_state();
let mut best: Option<(f64, Mat)> = None;
for wb in &world.bodies {
let Some((_, _, m)) = f.bodies(s).iter().find(|(w, _, _)| *w == wb.name) else { continue };
@@ -183,7 +182,8 @@ fn tri_dist(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> f64 {
}
struct Mapper<'a> {
s: &'a Session,
/// The placed model the picks are on.
world: &'a solidcraft_doc::ModelState,
f: Frames,
/// The frame of the command's first pick (its directions turn with it).
first: Option<Mat>,
@@ -195,7 +195,7 @@ struct Mapper<'a> {
impl Mapper<'_> {
fn pick(&mut self, v: &Value, body: Option<&str>) -> Option<Value> {
let p = vec3(v)?;
let m = if self.here { self.f.sketch } else { 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.world, &self.f, p, body).unwrap_or(self.f.active) };
self.first.get_or_insert(m);
self.local_point(&m, p)
}
@@ -362,8 +362,18 @@ impl Drop for Nested {
}
}
/// `params` with world picks mapped into component frames (`None`: nothing to change).
pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
/// `params` with world picks mapped into component frames (`None`: nothing to change). A model
/// that cannot be placed is the error.
pub fn to_local(s: &Session, id: &str, params: &Value) -> crate::Result<Option<Value>> {
if !placed(s) {
return Ok(None);
}
let world = s.world_state()?;
Ok(map_local(s, &world, id, params))
}
/// [`to_local`] on the placed model `world`.
fn map_local(s: &Session, world: &solidcraft_doc::ModelState, id: &str, params: &Value) -> Option<Value> {
if id == "library.apply" {
// Library targets are the same world selections as appearance.assign,
// nested one level down. Convert at the outer boundary exactly once;
@@ -389,7 +399,7 @@ pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
if let solidcraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &feature.kind {
context.active_occurrence_path = (!face.author_path.is_empty()).then(|| face.author_path.clone());
}
let mut inner = to_local(&context, cmd, params.get("params")?)?;
let mut inner = map_local(&context, world, cmd, params.get("params")?)?;
// An untouched explicit axis is displayed in world coordinates by Edit
// Feature. Keep its exact saved values instead of an affine round trip.
if cmd == "solid.revolve"
@@ -434,7 +444,7 @@ pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
return None;
}
let keys = table(id);
if keys.is_empty() || !placed(s) {
if keys.is_empty() {
return None;
}
let obj = params.as_object()?;
@@ -459,7 +469,7 @@ pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
obj.insert("origin".into(), serde_json::json!([0.0, 0.0, 0.0]));
}
let obj = &obj;
let mut m = Mapper { s, f: command_frames, first, changed: false, here: false };
let mut m = Mapper { world, f: command_frames, first, changed: false, here: false };
let body = obj.get("body").or_else(|| obj.get("face_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.
+1 -1
View File
@@ -18,7 +18,7 @@ fn near(a: Vec3, b: Vec3) -> bool {
}
fn world_bbox(s: &Session, body: &str) -> (Vec3, Vec3) {
let w = s.world_state();
let w = s.world_state().unwrap();
let b = w.body(body).unwrap_or_else(|| panic!("no body {body}")).mesh().bounds();
(b.min, b.max)
}
+2 -2
View File
@@ -69,7 +69,7 @@ fn generated_assembly_opens_with_every_body_in_place() {
let doc = generated_assembly(parts);
let mut s = Session::new(doc.clone());
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{:?}", s.model.results.iter().find(|r| r.error.is_some()));
let world = s.world_state();
let world = s.world_state().unwrap();
assert_eq!(world.bodies.len(), parts * 4);
assert!(world.bodies.iter().all(|b| b.component != 0 && b.transferred));
world.warm_meshes(|| true);
@@ -109,7 +109,7 @@ fn bench_generated_assembly() {
let evaluate = ms(t);
let features: f64 = s.model.results.iter().map(|r| r.ms).sum();
let t = std::time::Instant::now();
let world = s.world_state();
let world = s.world_state().unwrap();
world.warm_meshes(|| true);
let mesh = ms(t);
let t = std::time::Instant::now();
+42 -35
View File
@@ -261,7 +261,9 @@ struct WorldCache {
pending_moves: std::collections::BTreeMap<u64, solidcraft_doc::Mat>,
active_component: u64,
active_occurrence_path: Option<Vec<u64>>,
world: Arc<solidcraft_doc::ModelState>,
/// The placed model, or why it could not be placed (kept too, so a model that cannot be
/// placed is not placed again on every read).
world: std::result::Result<Arc<solidcraft_doc::ModelState>, String>,
/// The moved components' bodies as placed for `world`, reused by the next world state.
placed: Arc<std::sync::Mutex<solidcraft_doc::PlacedBodies>>,
/// Every placed body's world name, own name and frame for `world` ([`Session::body_frames`]),
@@ -730,29 +732,30 @@ impl Session {
if self.doc.occurrences.is_empty() {
return Some(state);
}
self.world_cache.lock().ok()?.as_ref().filter(|cache| cache.matches(self, &state)).map(|cache| cache.world.clone())
self.world_cache.lock().ok()?.as_ref().filter(|cache| cache.matches(self, &state)).and_then(|cache| cache.world.clone().ok())
}
/// Re-evaluate the model after a document change.
/// The model placed in the world through component occurrences (and uncaptured moves): what
/// to display, measure, pick and export. The same as `model.state()` without components.
pub fn world_state(&self) -> Arc<solidcraft_doc::ModelState> {
/// An error when the model cannot be placed (see [`solidcraft_doc::world_state`]).
pub fn world_state(&self) -> Result<Arc<solidcraft_doc::ModelState>> {
let st = self.model.state();
if self.doc.occurrences.is_empty() {
return st;
return Ok(st);
}
if let Ok(c) = self.world_cache.lock()
&& let Some(cached) = c.as_ref()
&& cached.matches(self, &st)
{
return cached.world.clone();
return cached.world.clone().map_err(EngineError::Other);
}
// The bodies the last world state placed: unchanged ones are not transformed and meshed
// again (a design built command by command changed one body at a time).
let placed = self.world_cache.lock().ok().and_then(|c| c.as_ref().map(|c| c.placed.clone())).unwrap_or_default();
// Taken out while placing, so a scratch copy of the session sharing it never waits.
let mut reuse = placed.lock().map(|mut p| std::mem::take(&mut *p)).unwrap_or_default();
let mut w = {
let w = {
if self.pending_moves.is_empty() {
solidcraft_doc::world_state_reusing(&self.doc, &st, &mut reuse)
} else {
@@ -768,21 +771,40 @@ impl Session {
if let Ok(mut p) = placed.lock() {
*p = reuse;
}
let w = w.map_err(EngineError::from).and_then(|w| self.authored_sketches(w)).map(Arc::new);
if let Ok(mut c) = self.world_cache.lock() {
*c = Some(WorldCache {
doc: self.doc.clone(),
model: st,
pending_moves: self.pending_moves.clone(),
active_component: self.active_component,
active_occurrence_path: self.active_occurrence_path.clone(),
world: w.as_ref().map(Arc::clone).map_err(ToString::to_string),
placed,
frames: Arc::default(),
});
}
w
}
/// `w` with the active component's sketches shown at the placement being authored.
fn authored_sketches(&self, mut w: solidcraft_doc::ModelState) -> Result<solidcraft_doc::ModelState> {
// Sketch definitions are displayed once, at the placement currently being authored.
if self.active_occurrence_path.is_some() && self.active_component != 0 {
let first = self.occurrence_placement(&self.doc.first_path(self.active_component)).map(|p| p.world).unwrap_or(solidcraft_doc::IDENTITY);
if let Some(inverse) = solidcraft_doc::mat_inverse(&first) {
let inverse = solidcraft_doc::mat_inverse(&first)
.ok_or_else(|| EngineError::Other(format!("the placement of component {} cannot be inverted", self.active_component)))?;
{
let delta = solidcraft_doc::mat_mul(&self.active_component_frame(), &inverse);
for sketch in w.sketches.iter_mut().filter(|sk| self.doc.feature(sk.feature).is_some_and(|f| f.component == self.active_component)) {
let sketch = Arc::make_mut(sketch);
let plane = sketch.plane;
if let Some(mapped) = solidcraft_geom::Plane::new(
sketch.plane = solidcraft_geom::Plane::new(
solidcraft_doc::apply_point(&delta, plane.origin),
solidcraft_doc::apply_vector(&delta, plane.x),
solidcraft_doc::apply_vector(&delta, plane.y),
) {
sketch.plane = mapped;
}
)
.ok_or_else(|| EngineError::Other("a sketch plane cannot be placed at the authored placement".into()))?;
for wire in &mut sketch.sketch.wires {
if wire.space == solidcraft_doc::sketch::WireSpace::Component {
solidcraft_doc::map_wire(wire, &delta);
@@ -791,20 +813,7 @@ impl Session {
}
}
}
let w = Arc::new(w);
if let Ok(mut c) = self.world_cache.lock() {
*c = Some(WorldCache {
doc: self.doc.clone(),
model: st,
pending_moves: self.pending_moves.clone(),
active_component: self.active_component,
active_occurrence_path: self.active_occurrence_path.clone(),
world: w.clone(),
placed,
frames: Arc::default(),
});
}
w
Ok(w)
}
/// Every placed body's world name, own name and frame. Kept with the world state while it is
@@ -1134,7 +1143,7 @@ impl Session {
// Picks in the world go into their component's frame (once: commands run by commands
// already have them there).
let (_nested, outermost) = frames::Nested::enter(spec.id);
let mapped = if outermost { frames::to_local(self, spec.id, params) } else { None };
let mapped = if outermost { frames::to_local(self, spec.id, params)? } else { None };
let params = mapped.as_ref().unwrap_or(params);
if outermost {
crash::command_begin(self, spec.id, given);
@@ -1542,22 +1551,20 @@ impl Session {
/// The same evaluated preview in occurrence coordinates, including uncaptured moves.
pub fn preview_world(&self, commands: &[(String, Value)]) -> Result<Preview> {
self.scratch().preview_in_place(commands, None).map(|(after, replies)| Preview {
before: self.world_state(),
after: after.world_state(),
replies,
})
let (after, replies) = self.scratch().preview_in_place(commands, None)?;
Ok(Preview { before: self.world_state()?, after: after.world_state()?, replies })
}
/// World preview with each session's visibility applied, including new body placements.
pub fn preview_visible_world(&self, commands: &[(String, Value)]) -> Result<Preview> {
let visible = |session: &Session| {
let world = session.world_state();
let visible = |session: &Session| -> Result<Arc<solidcraft_doc::ModelState>> {
let world = session.world_state()?;
let mut shown = (*world).clone();
shown.bodies.retain(|body| session.body_visible(&body.name));
Arc::new(shown)
Ok(Arc::new(shown))
};
self.scratch().preview_in_place(commands, None).map(|(after, replies)| Preview { before: visible(self), after: visible(&after), replies })
let (after, replies) = self.scratch().preview_in_place(commands, None)?;
Ok(Preview { before: visible(self)?, after: visible(&after)?, replies })
}
/// Run `commands` on a [`Session::scratch`] copy that keeps the undo and redo stacks: what
+1 -1
View File
@@ -52,7 +52,7 @@ fn enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs() {
s.doc.features.iter().find(|feature| feature.name == "USB" && matches!(feature.kind, solidcraft_doc::FeatureKind::Sketch { .. })).unwrap();
assert!((s.model.state().sketch(usb.id).unwrap().plane.origin.y + 35.5).abs() < 1e-6);
let assert_material = |session: &Session| {
let state = session.world_state();
let state = session.world_state().unwrap();
let case = state.body("Case").unwrap().mesh();
let lid = state.body("Lid").unwrap().mesh();
assert!(case.contains(solidcraft_geom::Vec3::new(-24.5, -21.5, 7.)));
+2 -2
View File
@@ -12,7 +12,7 @@ pub fn extract(s: &Session) -> Result<ProductScene> {
if s.model.is_stale() {
return Err(EngineError::Other("render scene is stale; finish the modelling gesture before previewing".into()));
}
let world = s.world_state();
let world = s.world_state()?;
let mut scene = ProductScene { meshes: Vec::new(), bounds: Aabb3::EMPTY };
for body in world.bodies.iter().filter(|b| s.body_visible(&b.name)) {
if scene.meshes.len() >= MAX_PRODUCT_BODIES {
@@ -80,7 +80,7 @@ mod tests {
assert_eq!(m.color, [32, 160, 128]);
assert_eq!(m.opacity, 0.4);
assert!(m.faces.iter().any(|f| f.color == [255, 0, 0] && f.opacity == 0.7));
assert!(Arc::ptr_eq(&m.mesh, &s.world_state().body(&m.name).unwrap().mesh()));
assert!(Arc::ptr_eq(&m.mesh, &s.world_state().unwrap().body(&m.name).unwrap().mesh()));
}
s.selection = vec![Sel::Body { name: first.meshes[0].name.clone() }];
s.section = Some((Vec3::ZERO, Vec3::X));
+2 -2
View File
@@ -66,13 +66,13 @@ fn a_port_picked_by_index_stays_a_cut_after_a_width_change() {
assert!(s.model.result(id).is_some_and(|r| r.error.is_none()), "{:?}", s.model.result(id));
let port = s.doc.find_feature("USB port").unwrap().id;
assert!((s.model.state().sketch(port).unwrap().plane.origin.x - 50.).abs() < 1e-6);
let world = s.world_state();
let world = s.world_state().unwrap();
let mesh = world.body("Enclosure").unwrap().mesh();
assert!(!mesh.contains(solidcraft_geom::Vec3::new(49., 0., 12.)));
assert!(mesh.contains(solidcraft_geom::Vec3::new(49., 0., 20.)));
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(reopened.model.result(id).unwrap().error.is_none());
let world = reopened.world_state();
let world = reopened.world_state().unwrap();
let mesh = world.body("Enclosure").unwrap().mesh();
assert!(!mesh.contains(solidcraft_geom::Vec3::new(49., 0., 12.)));
assert!(mesh.contains(solidcraft_geom::Vec3::new(49., 0., 20.)));
+2 -2
View File
@@ -161,7 +161,7 @@ mod tests {
assert!(session.model.results.iter().all(|result| result.error.is_none()));
assert!((session.model.state().sketch(boss).unwrap().plane.origin.z - 12.).abs() < 1e-6);
assert!((session.model.state().sketch(bore).unwrap().plane.origin.z - 26.).abs() < 1e-6);
let world = session.world_state();
let world = session.world_state().unwrap();
let plate = world.body("Plate").unwrap().mesh();
assert!((plate.bounds().max.z - 26.).abs() < 1e-6);
assert!(plate.contains(solidcraft_geom::Vec3::new(5., 25., 10.)));
@@ -171,7 +171,7 @@ mod tests {
assert!(plate.contains(solidcraft_geom::Vec3::new(40., 35., 2.)));
let reopened = crate::Session::new(solidcraft_doc::Document::from_json(&session.doc.to_json()).unwrap());
assert!((reopened.model.state().sketch(bore).unwrap().plane.origin.z - 26.).abs() < 1e-6);
let reopened_world = reopened.world_state();
let reopened_world = reopened.world_state().unwrap();
let reopened_plate = reopened_world.body("Plate").unwrap().mesh();
assert!(!reopened_plate.contains(solidcraft_geom::Vec3::new(40., 25., 2.)));
assert!(reopened_plate.contains(solidcraft_geom::Vec3::new(40., 35., 2.)));
@@ -110,7 +110,7 @@ fn a_body_the_sketch_sits_on_stays_shown() {
let mut s = Session::default();
block(&mut s, "Gone", 0.0);
let top = {
let world = s.world_state();
let world = s.world_state().unwrap();
let face = world
.body("Gone")
.and_then(|body| body.body.faces(0.01).ok())
@@ -143,7 +143,7 @@ fn every_placement_of_a_component_body_is_left_out() {
run(&mut s, "occurrence.copy", json!({"component": component, "translate": [50, 0, 0]}));
run(&mut s, "timeline.roll_to", json!({"feature": "Edited"}));
run(&mut s, "sketch.edit", json!({"sketch": "Edited"}));
let world = s.world_state();
let world = s.world_state().unwrap();
assert_eq!(world.bodies.len(), 2, "one body, placed twice");
for body in &world.bodies {
assert!(!s.body_visible(&body.name), "{} is deleted later", body.name);
+33 -33
View File
@@ -1386,14 +1386,14 @@ fn extrude_auto_operation() {
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20}));
// A face pushed in (negative distance) cuts.
let p = json!({"face": [20, 15, 20], "distance": -5, "operation": "auto"});
assert_eq!(auto_operation(&s, &p), Some("cut"));
assert_eq!(auto_operation(&s, &p).unwrap(), Some("cut"));
run(&mut s, "solid.extrude", p);
assert!(rel(volume(&mut s), 40.0 * 30.0 * 15.0) < 1e-6);
// Pulled out, it joins.
assert_eq!(auto_operation(&s, &json!({"face": [20, 15, 15], "distance": 5})), Some("join"));
assert_eq!(auto_operation(&s, &json!({"face": [20, 15, 15], "distance": 5})).unwrap(), Some("join"));
// Pushed in far past the bottom (halfway is below the box), it still cuts (#1257).
assert_eq!(auto_operation(&s, &json!({"face": [20, 15, 15], "distance": -50})), Some("cut"));
assert_eq!(auto_operation(&s, &json!({"face": [20, 15, 15], "distance": 50, "direction": "negative"})), Some("cut"));
assert_eq!(auto_operation(&s, &json!({"face": [20, 15, 15], "distance": -50})).unwrap(), Some("cut"));
assert_eq!(auto_operation(&s, &json!({"face": [20, 15, 15], "distance": 50, "direction": "negative"})).unwrap(), Some("cut"));
// A sketch on XY under the box: up goes into it (cut), down grows out of it (join), and a
// profile beside the box still defaults to Join (New Body remains explicit).
run(&mut s, "sketch.create", json!({"plane": "XY"}));
@@ -1401,9 +1401,9 @@ fn extrude_auto_operation() {
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [60, 5], "p1": [70, 15]}));
run(&mut s, "sketch.finish", json!({}));
let at = |x: f64, d: f64| json!({"profiles": [{"point": [x, 10]}], "distance": d});
assert_eq!(auto_operation(&s, &at(10.0, 8.0)), Some("cut"));
assert_eq!(auto_operation(&s, &at(10.0, -8.0)), Some("join"));
assert_eq!(auto_operation(&s, &at(65.0, 8.0)), Some("join"));
assert_eq!(auto_operation(&s, &at(10.0, 8.0)).unwrap(), Some("cut"));
assert_eq!(auto_operation(&s, &at(10.0, -8.0)).unwrap(), Some("join"));
assert_eq!(auto_operation(&s, &at(65.0, 8.0)).unwrap(), Some("join"));
run(&mut s, "solid.extrude", json!({"profiles": [{"point": [10, 10]}], "distance": 8, "operation": "auto"}));
assert!(rel(volume(&mut s), 40.0 * 30.0 * 15.0 - 100.0 * 8.0) < 1e-6);
}
@@ -1480,11 +1480,11 @@ fn extrude_auto_operation_follows_the_to_object() {
// From the top face to the bottom face: through the body.
let down = json!({"face": [20, 15, 20], "to": [10, 10, 0], "distance": 19, "direction": direction});
assert!(extrude_reach(&s, &down).is_some_and(|h| (h + 20.0).abs() < 1e-6), "{down}");
assert_eq!(auto_operation(&s, &down), Some("cut"), "{down}");
assert_eq!(auto_operation(&s, &down).unwrap(), Some("cut"), "{down}");
// From the top face up to the box above: out of the body.
let up = json!({"face": [20, 15, 20], "to": [10, 10, 50], "distance": 19, "direction": direction});
assert!(extrude_reach(&s, &up).is_some_and(|h| (h - 30.0).abs() < 1e-6), "{up}");
assert_eq!(auto_operation(&s, &up), Some("join"), "{up}");
assert_eq!(auto_operation(&s, &up).unwrap(), Some("join"), "{up}");
}
// Without a To object the direction still counts.
assert_eq!(extrude_reach(&s, &json!({"face": [20, 15, 20], "distance": 5, "direction": "negative"})), Some(-5.0));
@@ -1537,7 +1537,7 @@ fn auto_extrude_joins_side_contact_elbow_and_keeps_new_body_explicit() {
// The profile centre is outside the vertical arm. Contact is along its
// y=10 side, not behind the centre along the extrusion's X direction.
let p = json!({"distance": 10, "to": [0, 15, 25], "operation": operation});
assert_eq!(auto_operation(&s, &p), Some("join"));
assert_eq!(auto_operation(&s, &p).unwrap(), Some("join"));
run(&mut s, "solid.extrude", p);
assert!((volume(&mut s) - 5000.0).abs() < 1e-6);
let st = s.model.state();
@@ -1561,7 +1561,7 @@ fn auto_extrude_joins_corner_screw_boss() {
run(&mut s, "sketch.circle.center", json!({"center": [10, 10], "radius": 1}));
run(&mut s, "sketch.finish", json!({}));
let p = json!({"profiles": [{"point": [12, 10]}], "distance": 5, "operation": "auto"});
assert_eq!(auto_operation(&s, &p), Some("join"));
assert_eq!(auto_operation(&s, &p).unwrap(), Some("join"));
run(&mut s, "solid.extrude", p);
assert!(rel(volume(&mut s), 3000.0 + PI * 15.0 * 5.0) < 1e-3);
let st = s.model.state();
@@ -1580,8 +1580,8 @@ fn auto_extrude_starts_new_body_in_empty_component() {
run(&mut s, "sketch.create", json!({"plane": "XY"}));
run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 2}));
run(&mut s, "sketch.finish", json!({}));
assert_eq!(auto_operation(&s, &json!({"distance": 5})), Some("new"));
assert_eq!(auto_operation(&s, &json!({"distance": 5, "participants": ["missing"]})), Some("new"));
assert_eq!(auto_operation(&s, &json!({"distance": 5})).unwrap(), Some("new"));
assert_eq!(auto_operation(&s, &json!({"distance": 5, "participants": ["missing"]})).unwrap(), Some("new"));
}
/// Measuring picked geometry: distance between faces, angle, edge length.
@@ -1817,7 +1817,7 @@ fn holes_at_sketch_points_threads_and_components() {
);
let reopened = Session::new(back);
assert_eq!(reopened.model.state().body("Shaft").unwrap().component, assigned);
assert_eq!(reopened.world_state().threads.len(), s.world_state().threads.len());
assert_eq!(reopened.world_state().unwrap().threads.len(), s.world_state().unwrap().threads.len());
}
#[test]
@@ -1938,11 +1938,11 @@ fn components_occurrences_and_world_placement() {
run(&mut s, "component.activate", json!({"component": 0}));
// Instances share the contents; a copy is independent.
run(&mut s, "occurrence.copy", json!({"component": cid, "translate": [0, 20, 0]}));
let w = s.world_state();
let w = s.world_state().unwrap();
assert_eq!(w.bodies.len(), 3, "{:?}", w.bodies.iter().map(|b| &b.name).collect::<Vec<_>>());
let pn = run(&mut s, "component.paste_new", json!({"component": cid, "translate": [0, -30, 0]}));
assert_eq!(pn["features"].as_array().map(|a| a.len()), Some(2));
assert_eq!(s.world_state().bodies.len(), 4);
assert_eq!(s.world_state().unwrap().bodies.len(), 4);
// Ground stops moves; pending moves show until captured or reverted.
run(&mut s, "occurrence.ground", json!({"occurrence": occ}));
assert!(s.execute("occurrence.move", &json!({"occurrence": occ, "translate": [1, 0, 0]})).is_err());
@@ -1988,8 +1988,8 @@ fn components_occurrences_and_world_placement() {
run(&mut s, "edit.redo", json!({}));
run(&mut s, "timeline.roll_to", json!({"position": 1}));
// The ownership boundary is later: before it Base remains the sole root body.
assert_eq!(s.world_state().bodies.len(), 1);
assert_eq!(s.world_state().body("Base").unwrap().component, 0);
assert_eq!(s.world_state().unwrap().bodies.len(), 1);
assert_eq!(s.world_state().unwrap().body("Base").unwrap().component, 0);
run(&mut s, "timeline.roll_to", json!({}));
// Saved and loaded; older designs get occurrences.
let back = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
@@ -2071,7 +2071,7 @@ fn sample_plate_step_round_trip() {
let mut s = Session::default();
s.run_script(&crate::sample::script()).unwrap();
let imp = step_round_trip(&mut s, "sample");
let st = s.world_state();
let st = s.world_state().unwrap();
assert_eq!(imp.bodies.len(), st.bodies.len());
for (a, b) in st.bodies.iter().zip(&imp.bodies) {
assert_eq!(a.name, b.name);
@@ -2142,7 +2142,7 @@ fn face_colours_and_opacity_export() {
let mut s2 = Session::default();
run(&mut s2, "doc.open", json!({"path": p.to_string_lossy()}));
let _ = std::fs::remove_file(&p);
let st = s2.world_state();
let st = s2.world_state().unwrap();
let looks = s2.doc.face_colors(&st.bodies[0]);
assert_eq!(looks.len(), 1);
assert_eq!(looks[0].1.color, [255, 0, 0]);
@@ -2156,13 +2156,13 @@ fn iges_export_and_open() {
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 5, "body_name": "Plate"}));
run(&mut s, "solid.cylinder", json!({"radius": 4, "height": 20, "base": [10, 10, 5], "body_name": "Pin"}));
run(&mut s, "appearance.assign", json!({"bodies": ["Pin"], "color": "#ff0000"}));
let want: Vec<f64> = s.world_state().bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).collect();
let want: Vec<f64> = s.world_state().unwrap().bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).collect();
let p = std::env::temp_dir().join(format!("solidcraft-iges-{}.igs", std::process::id()));
run(&mut s, "file.export", json!({"path": p.to_string_lossy()}));
let mut s2 = Session::default();
run(&mut s2, "doc.open", json!({"path": p.to_string_lossy()}));
let _ = std::fs::remove_file(&p);
let st = s2.world_state();
let st = s2.world_state().unwrap();
let names: Vec<&str> = st.bodies.iter().map(|b| b.name.as_str()).collect();
assert_eq!(names, ["Plate", "Pin"]);
for (b, v) in st.bodies.iter().zip(&want) {
@@ -2179,7 +2179,7 @@ fn split_body_by_a_face() {
run(&mut s, "solid.box", json!({"length": 10, "width": 20, "height": 30, "body_name": "Block"}));
run(&mut s, "solid.cylinder", json!({"radius": 4, "height": 40, "base": [5, 10, -5], "body_name": "Tool"}));
run(&mut s, "solid.split_body", json!({"body": "Block", "tool": {"body": "Tool", "point": [9, 10, 10]}}));
let st = s.world_state();
let st = s.world_state().unwrap();
let mut v: Vec<f64> =
st.bodies.iter().filter(|b| b.name.starts_with("Block")).map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).collect();
v.sort_by(f64::total_cmp);
@@ -2200,7 +2200,7 @@ fn split_body_by_a_sketch_line() {
run(&mut s, "sketch.circle.center", json!({"center": [50, 50], "radius": 2, "id": "c"}));
run(&mut s, "sketch.finish", json!({}));
let volumes = |s: &Session| {
let st = s.world_state();
let st = s.world_state().unwrap();
let mut v: Vec<f64> =
st.bodies.iter().filter(|b| b.name.starts_with("Block")).map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).collect();
v.sort_by(f64::total_cmp);
@@ -2237,12 +2237,12 @@ fn chord_and_variable_fillets() {
run(&mut s, "solid.box", json!({"length": 10, "width": 20, "height": 30, "body_name": "B"}));
run(&mut s, "solid.fillet", json!({"edges": [[10, 10, 30]], "radius": 4, "type": "chord"}));
let r = 4.0 / 2f64.sqrt();
let v = solidcraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume;
let v = solidcraft_kernel::measure(&s.world_state().unwrap().body("B").unwrap().body).unwrap().volume;
assert!(rel(v, 6000.0 - (1.0 - PI / 4.0) * r * r * 20.0) < 1e-4, "{v}");
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 10, "width": 20, "height": 30, "body_name": "B"}));
run(&mut s, "solid.fillet", json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable", "radius2": 5, "start": [10, 0, 30]}));
let v = solidcraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume;
let v = solidcraft_kernel::measure(&s.world_state().unwrap().body("B").unwrap().body).unwrap().volume;
let removed = (1.0 - PI / 4.0) * 20.0 * (4.0 + 10.0 + 25.0) / 3.0;
assert!(rel(v, 6000.0 - removed) < 1e-4, "{v}");
assert!(s.execute("solid.fillet", &json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable"})).is_err());
@@ -2250,7 +2250,7 @@ fn chord_and_variable_fillets() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 10, "width": 20, "height": 30, "body_name": "B"}));
run(&mut s, "solid.fillet", json!({"edges": [[10, 10, 30]], "radius": 4, "type": "variable", "radius2": 0, "start": [10, 0, 30]}));
let v = solidcraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume;
let v = solidcraft_kernel::measure(&s.world_state().unwrap().body("B").unwrap().body).unwrap().volume;
assert!(rel(v, 6000.0 - (1.0 - PI / 4.0) * 20.0 * 16.0 / 3.0) < 1e-4, "{v}");
// A radius through a middle point (a user parameter), straight between: 2 → 4 → 2.
let mut s = Session::default();
@@ -2261,7 +2261,7 @@ fn chord_and_variable_fillets() {
"solid.fillet",
json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable", "radius2": 2, "start": [10, 0, 30], "mid": [[0.5, "waist"]]}),
);
let vol = |s: &Session| solidcraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume;
let vol = |s: &Session| solidcraft_kernel::measure(&s.world_state().unwrap().body("B").unwrap().body).unwrap().volume;
let removed = 2.0 * (1.0 - PI / 4.0) * 10.0 * (4.0 + 8.0 + 16.0) / 3.0;
assert!(rel(vol(&s), 6000.0 - removed) < 1e-4, "{}", vol(&s));
// The middle radius follows its parameter.
@@ -2285,7 +2285,7 @@ fn components_export_as_step_assembly() {
run(&mut s, "component.create", json!({"name": "Pin"}));
run(&mut s, "solid.cylinder", json!({"radius": 2, "height": 10, "base": [5, 5, 5]}));
run(&mut s, "occurrence.copy", json!({"component": "Pin", "translate": [20, 0, 0]}));
let want: f64 = s.world_state().bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).sum();
let want: f64 = s.world_state().unwrap().bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).sum();
let imp = step_round_trip(&mut s, "asm");
assert_eq!(imp.bodies.len(), 3, "{:?}", imp.bodies.iter().map(|b| &b.name).collect::<Vec<_>>());
let got: f64 = imp.bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).sum();
@@ -2783,7 +2783,7 @@ fn booleans_stay_in_their_component() {
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [8, 2], "p1": [28, 8]}));
run(&mut s, "sketch.finish", json!({}));
let vol = |s: &mut Session, b: &str| run(s, "inspect.measure", json!({"bodies": [b]}))["total"]["volume_mm3"].as_f64().unwrap_or(0.0);
assert_eq!(auto_operation(&s, &json!({"sketch": "Slot", "distance": 10})), Some("cut"));
assert_eq!(auto_operation(&s, &json!({"sketch": "Slot", "distance": 10})).unwrap(), Some("cut"));
run(&mut s, "solid.extrude", json!({"sketch": "Slot", "distance": 10, "operation": "cut", "name": "Cut"}));
assert!((vol(&mut s, "ABox") - (2000.0 - 12.0 * 6.0 * 10.0)).abs() < 1e-6, "{}", vol(&mut s, "ABox"));
assert!((vol(&mut s, "BBox") - 2000.0).abs() < 1e-6, "B untouched: {}", vol(&mut s, "BBox"));
@@ -3408,14 +3408,14 @@ fn adopting_a_computed_component_keeps_its_warm_world_geometry() {
run(&mut s, "solid.box", json!({"length":10,"width":10,"height":10}));
let mut computed = s.scratch();
run(&mut computed, "solid.box", json!({"length":5,"width":5,"height":5,"corner":[30,0,0]}));
let prepared = computed.world_state();
let prepared = computed.world_state().unwrap();
for b in &prepared.bodies {
let _ = b.mesh();
}
s.accept_computed(computed);
assert!(std::sync::Arc::ptr_eq(&prepared, &s.world_state()));
assert!(std::sync::Arc::ptr_eq(&prepared, &s.world_state().unwrap()));
s.invalidate_view(100);
assert!(std::sync::Arc::ptr_eq(&prepared, &s.world_state()));
assert!(std::sync::Arc::ptr_eq(&prepared, &s.world_state().unwrap()));
}
/// Every file command checked the size with the metadata and then read to the end. A character
+7 -7
View File
@@ -19,7 +19,7 @@ fn moved_component_picks_and_frames() {
run(&mut s, "solid.box", json!({"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"}));
run(&mut s, "component.activate", json!({"component": 0}));
run(&mut s, "occurrence.move", json!({"occurrence": occ, "translate": [0, 50, 0]}));
let wbox = |s: &Session, b: &str| s.world_state().body(b).unwrap().mesh().bounds();
let wbox = |s: &Session, b: &str| s.world_state().unwrap().body(b).unwrap().mesh().bounds();
let vol = |s: &mut Session, b: &str| run(s, "inspect.measure", json!({"bodies": [b]}))["total"]["volume_mm3"].as_f64().unwrap();
// Root active: a fillet picked at the moved edge in the world.
run(&mut s, "solid.fillet", json!({"edges": [[25, 54, 4]], "radius": 1}));
@@ -40,7 +40,7 @@ fn moved_component_picks_and_frames() {
// A pasted instance: a pick on it lands on the shared definition.
run(&mut s, "occurrence.copy", json!({"component": cid, "translate": [0, 20, 0]}));
let v0 = vol(&mut s, "ArmBody");
let names: Vec<String> = s.world_state().bodies.iter().map(|b| b.name.clone()).collect();
let names: Vec<String> = s.world_state().unwrap().bodies.iter().map(|b| b.name.clone()).collect();
let inst = names.iter().find(|n| n.starts_with("ArmBody (")).cloned().unwrap();
let ib = wbox(&s, &inst);
// Its bottom edge along x at the far side (y max, z 0).
@@ -71,7 +71,7 @@ fn extrude_in_a_moved_component_from_a_root_sketch() {
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "component.activate", json!({"component": jaw}));
run(&mut s, "solid.extrude", json!({"sketch": "Tab", "distance": 10, "operation": "join", "targets": ["Jaw"]}));
let b = s.world_state().body("Jaw").unwrap().mesh().bounds();
let b = s.world_state().unwrap().body("Jaw").unwrap().mesh().bounds();
let near = |a: f64, e: f64| (a - e).abs() < 1e-6;
assert!(near(b.min.x, 0.0) && near(b.max.x, 40.0) && near(b.min.y, -5.0) && near(b.max.y, 7.0), "{b:?}");
let v = run(&mut s, "inspect.measure", json!({"bodies": ["Jaw"]}))["total"]["volume_mm3"].as_f64().unwrap();
@@ -101,7 +101,7 @@ fn extrude_to_a_face_in_a_moved_component() {
run(&mut s, "component.activate", json!({"component": jaw}));
// Up to the jaw's top face, picked in the world at its middle: the tab rises from z 0 to 15.
run(&mut s, "solid.extrude", json!({"sketch": "Tab", "to": [20, 0, 15], "operation": "join", "targets": ["Jaw"]}));
let b = s.world_state().body("Jaw").unwrap().mesh().bounds();
let b = s.world_state().unwrap().body("Jaw").unwrap().mesh().bounds();
let near = |a: f64, e: f64| (a - e).abs() < 1e-6;
assert!(
near(b.min.x, 0.0) && near(b.max.x, 40.0) && near(b.min.y, -5.0) && near(b.max.y, 7.0) && near(b.min.z, 0.0) && near(b.max.z, 15.0),
@@ -175,7 +175,7 @@ fn extrude_from_a_slanted_face_in_another_moved_component() {
"solid.extrude",
json!({"sketch": "Pad", "from": [0, 5, -20], "distance": 4, "direction": "negative", "operation": "new", "body_name": "Leg"}),
);
let b = s.world_state().body("Leg").unwrap().mesh().bounds();
let b = s.world_state().unwrap().body("Leg").unwrap().mesh().bounds();
assert!((b.max.z + 22.5).abs() < 1e-6 && (b.min.z + 26.5).abs() < 1e-6, "{b:?}");
}
@@ -220,7 +220,7 @@ fn revolve_to_a_cylinder_in_another_moved_component() {
"solid.revolve",
json!({"sketch": "Profile", "axis": {"origin": [0, 0, 0], "dir": [0, 0, 1]}, "to": to, "operation": "new", "body_name": "Arc"}),
);
let world = s.world_state();
let world = s.world_state().unwrap();
let arc = &world.body("Arc").unwrap().body;
let post = &world.body("PostBody").unwrap().body;
let overlap = solidcraft_kernel::boolean(arc, post, solidcraft_kernel::BoolOp::Intersect).unwrap();
@@ -255,7 +255,7 @@ fn extrude_to_names_the_face_it_is_on() {
"solid.extrude",
json!({"sketch": "Pad", "distance": "1 mm", "direction": "negative", "to": [5, 5, -20], "operation": "new", "body_name": "Leg"}),
);
let b = s.world_state().body("Leg").unwrap().mesh().bounds();
let b = s.world_state().unwrap().body("Leg").unwrap().mesh().bounds();
assert!((b.min.z + 20.0).abs() < 1e-9 && b.max.z.abs() < 1e-9, "moved {moved}: {b:?}");
}
}
+1
View File
@@ -62,6 +62,7 @@ impl Cube {
fn shown_boxes(s: &Session) -> Vec<[i64; 6]> {
let mut out: Vec<[i64; 6]> = s
.world_state()
.unwrap()
.bodies
.iter()
.map(|b| {
+3 -3
View File
@@ -80,7 +80,7 @@ fn key(b: solidcraft_geom::Aabb3) -> [i64; 6] {
/// Where the design shows its bodies, sorted.
fn shown_boxes(s: &Session) -> Vec<[i64; 6]> {
let mut out: Vec<[i64; 6]> = s.world_state().bodies.iter().map(|b| key(solidcraft_kernel::measure(&b.body).unwrap().bbox)).collect();
let mut out: Vec<[i64; 6]> = s.world_state().unwrap().bodies.iter().map(|b| key(solidcraft_kernel::measure(&b.body).unwrap().bbox)).collect();
out.sort();
out
}
@@ -112,7 +112,7 @@ fn corner_of(s: &Session, occurrence: &str) -> Vec3 {
};
let names = s.occurrence_bodies(&path, true).unwrap();
assert!(!names.is_empty());
let world = s.world_state();
let world = s.world_state().unwrap();
world.bodies.iter().filter(|b| names.contains(&b.name)).fold(Aabb3::EMPTY, |a, b| a.union(&solidcraft_kernel::measure(&b.body).unwrap().bbox)).min
}
@@ -288,7 +288,7 @@ fn undo_takes_the_whole_import_away() {
run(&mut s, "edit.undo", json!({}));
assert!(s.doc.features.is_empty() && s.doc.components.is_empty() && s.doc.occurrences.is_empty(), "nothing left");
assert!(s.doc.assembly.joints.is_empty() && s.doc.body_components.is_empty() && s.doc.body_offsets.is_empty());
assert!(s.world_state().bodies.is_empty());
assert!(s.world_state().unwrap().bodies.is_empty());
run(&mut s, "edit.redo", json!({}));
assert_eq!(shown_boxes(&s), file_boxes(&path));
let _ = std::fs::remove_dir_all(&dir);
+2 -2
View File
@@ -9,7 +9,7 @@ use super::run;
use crate::*;
fn world_box(s: &Session, body: &str) -> (Vec3, Vec3) {
let b = s.world_state().body(body).unwrap_or_else(|| panic!("no body `{body}`")).mesh().bounds();
let b = s.world_state().unwrap().body(body).unwrap_or_else(|| panic!("no body `{body}`")).mesh().bounds();
(b.min, b.max)
}
@@ -56,7 +56,7 @@ fn a_further_instance_is_mirrored_at_its_own_placement() {
let mut s = Session::default();
let (component, _) = moved_arm(&mut s);
run(&mut s, "occurrence.copy", json!({"component": component, "translate": [0, 0, 40]}));
let names: Vec<String> = s.world_state().bodies.iter().map(|b| b.name.clone()).collect();
let names: Vec<String> = s.world_state().unwrap().bodies.iter().map(|b| b.name.clone()).collect();
let instance = names.iter().find(|n| n.starts_with("ArmBody (")).cloned().unwrap();
let r = run(&mut s, "solid.mirror", json!({"bodies": ["ArmBody", instance], "plane": "YZ"}));
let copies: Vec<String> = r["created_bodies"].as_array().unwrap().iter().map(|n| n.as_str().unwrap().to_string()).collect();
+1 -1
View File
@@ -19,7 +19,7 @@ fn file(tag: &str, name: &str, bytes: &[u8]) -> (std::path::PathBuf, String) {
}
fn extent(s: &Session) -> ([f64; 3], [f64; 3]) {
let b = crate::view::bounds(s);
let b = crate::view::bounds(s).unwrap();
([b.min.x, b.min.y, b.min.z], [b.max.x, b.max.y, b.max.z])
}
+1 -1
View File
@@ -112,7 +112,7 @@ fn point_expressions_in_a_moved_component() {
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 wlo = |s: &Session| s.world_state().unwrap().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"}));
@@ -44,7 +44,7 @@ fn frame_file(tag: &str) -> String {
/// The lowest and highest corner of everything the design shows.
fn extent(s: &Session) -> ([f64; 3], [f64; 3]) {
let b = crate::view::bounds(s);
let b = crate::view::bounds(s).unwrap();
([b.min.x, b.min.y, b.min.z], [b.max.x, b.max.y, b.max.z])
}
+52 -50
View File
@@ -8,7 +8,7 @@ use solidcraft_geom::{Aabb3, Plane, Vec3};
use solidcraft_render::{AnalysisColors, Camera, Rgb, Scene, SceneAnalysis, SceneLine, SceneMesh};
use solidcraft_sketch::Sketch;
use crate::{Sel, Session};
use crate::{Result, Sel, Session};
/// Colours (shared with the GPU viewport).
pub mod colors {
@@ -105,20 +105,21 @@ impl Default for Palette {
/// Bounds of everything visible now, in world space: shown bodies where their occurrences,
/// joints and moves place them, and the sketches the view draws (not ones used by a feature,
/// hidden ones, or ones in hidden components). What Home, Fit and the orbit pivot frame.
pub fn bounds(s: &Session) -> Aabb3 {
/// hidden ones, or ones in hidden components). What Home, Fit and the orbit pivot frame. A model
/// that cannot be placed is the error.
pub fn bounds(s: &Session) -> Result<Aabb3> {
bounds_by(s, |body| body.mesh_bounds())
}
/// [`bounds`] without meshing anything: a body not meshed yet counts with its outline
/// ([`solidcraft_kernel::Body::outline_bounds`]), which a face curving out past all of its edges
/// can exceed. Frames a design whose bodies are still being meshed.
pub fn unmeshed_bounds(s: &Session) -> Aabb3 {
pub fn unmeshed_bounds(s: &Session) -> Result<Aabb3> {
bounds_by(s, |body| body.cached_mesh().map(|m| m.bounds()).unwrap_or_else(|| body.outline_bounds()))
}
fn bounds_by(s: &Session, of: impl Fn(&solidcraft_doc::ModelBody) -> Aabb3) -> Aabb3 {
let st = s.world_state();
fn bounds_by(s: &Session, of: impl Fn(&solidcraft_doc::ModelBody) -> Aabb3) -> Result<Aabb3> {
let st = s.world_state()?;
let mut b = Aabb3::EMPTY;
for body in st.bodies.iter().filter(|x| s.body_visible(&x.name)) {
b = b.union(&of(body));
@@ -136,7 +137,7 @@ fn bounds_by(s: &Session, of: impl Fn(&solidcraft_doc::ModelBody) -> Aabb3) -> A
}
}
}
b
Ok(b)
}
/// The origin plane a grid lies on.
@@ -464,23 +465,24 @@ pub fn component_color(s: &Session, component: u64) -> Option<[u8; 3]> {
Some(s.doc.component_identification_color(component))
}
/// The scene for a headless render, cut by the session's section.
pub fn scene(s: &Session, cam: &Camera) -> Scene {
/// The scene for a headless render, cut by the session's section. A model that cannot be placed
/// is the error.
pub fn scene(s: &Session, cam: &Camera) -> Result<Scene> {
scene_with_section(s, cam, s.section)
}
/// The scene for a headless render, cut by `section` (origin, normal) instead of the session's:
/// the app passes the cut its viewport shows (an open Section Analysis dialog's preview, a
/// sketch's Slice), so model renders match the window.
pub fn scene_with_section(s: &Session, cam: &Camera, section: Option<(Vec3, Vec3)>) -> Scene {
pub fn scene_with_section(s: &Session, cam: &Camera, section: Option<(Vec3, Vec3)>) -> Result<Scene> {
scene_with_guides(s, cam, section, Guides::default())
}
/// The plane of the sketch that is open, if any: it always shows a grid, whatever the Grid switch
/// says (outside a sketch the grid is the user's choice).
pub fn open_sketch_plane(s: &Session) -> Option<Plane> {
let id = s.active_sketch?;
s.world_state().sketch(id).map(|ss| ss.plane)
pub fn open_sketch_plane(s: &Session) -> Result<Option<Plane>> {
let Some(id) = s.active_sketch else { return Ok(None) };
Ok(s.world_state()?.sketch(id).map(|ss| ss.plane))
}
/// The grid on a sketch plane: its centre (the view target's foot on the plane, snapped to the
@@ -508,14 +510,14 @@ impl Default for Guides {
}
/// [`scene_with_section`] with the grid and the axes shown or left out.
pub fn scene_with_guides(s: &Session, cam: &Camera, section: Option<(Vec3, Vec3)>, guides: Guides) -> Scene {
pub fn scene_with_guides(s: &Session, cam: &Camera, section: Option<(Vec3, Vec3)>, guides: Guides) -> Result<Scene> {
scene_with_palette_and_guides(s, cam, section, world_curve_parts, &Palette::DEFAULT, guides)
}
/// A headless scene using supplied display paths for its visible sketch curves.
/// Callers can use the same pixel sampler as a GPU viewport without changing the
/// body, grid, appearance, section or sketch styling policies of [`scene`].
pub fn scene_with_sketch_sampler(s: &Session, cam: &Camera, sampler: impl FnMut(&Sketch, usize, &Plane) -> Vec<Vec<Vec3>>) -> Scene {
pub fn scene_with_sketch_sampler(s: &Session, cam: &Camera, sampler: impl FnMut(&Sketch, usize, &Plane) -> Vec<Vec<Vec3>>) -> Result<Scene> {
scene_with_section_and_sketch_sampler(s, cam, s.section, sampler)
}
@@ -525,7 +527,7 @@ pub fn scene_with_section_and_sketch_sampler(
cam: &Camera,
section: Option<(Vec3, Vec3)>,
sampler: impl FnMut(&Sketch, usize, &Plane) -> Vec<Vec<Vec3>>,
) -> Scene {
) -> Result<Scene> {
scene_with_palette(s, cam, section, sampler, &Palette::DEFAULT)
}
@@ -536,7 +538,7 @@ pub fn scene_with_palette(
section: Option<(Vec3, Vec3)>,
sampler: impl FnMut(&Sketch, usize, &Plane) -> Vec<Vec<Vec3>>,
p: &Palette,
) -> Scene {
) -> Result<Scene> {
build_scene(s, cam, section, Guides::default(), sampler, p, false)
}
@@ -548,7 +550,7 @@ pub fn scene_with_palette_and_guides(
sampler: impl FnMut(&Sketch, usize, &Plane) -> Vec<Vec<Vec3>>,
p: &Palette,
guides: Guides,
) -> Scene {
) -> Result<Scene> {
build_scene(s, cam, section, guides, sampler, p, false)
}
@@ -574,8 +576,8 @@ fn build_scene(
mut sampler: impl FnMut(&Sketch, usize, &Plane) -> Vec<Vec<Vec3>>,
p: &Palette,
cosmetic_threads: bool,
) -> Scene {
let st = s.world_state();
) -> Result<Scene> {
let st = s.world_state()?;
let section = section.filter(|(o, n)| o.is_finite() && n.is_finite() && n.len() > 1e-9);
let mut sc = Scene {
background: Some(p.background),
@@ -585,14 +587,14 @@ fn build_scene(
analysis: s.analysis.map(|a| SceneAnalysis { mode: a.mode(), colors: p.analysis }),
..Default::default()
};
let b = bounds(s);
let b = bounds(s)?;
sc.radius = (b.diagonal() * 0.5).max(cam.half_height()).max(10.0) + b.center().dist(cam.target);
// Grid on the ground plane (XY with Z up, XZ with Y up), drawn under the model as in the viewport.
if guides.grid {
let (minor, major) = grid_step(cam.half_height());
// Headless pictures keep the grid on the ground, the viewport's default; an open sketch
// has its own plane's grid (world coordinates, no camera frame).
let (c, u, v, slant, w): (Vec3, Vec3, Vec3, f64, Box<dyn Fn(Vec3) -> Vec3>) = if let Some(pl) = open_sketch_plane(s) {
let (c, u, v, slant, w): (Vec3, Vec3, Vec3, f64, Box<dyn Fn(Vec3) -> Vec3>) = if let Some(pl) = open_sketch_plane(s)? {
let (c, u, v) = sketch_grid_frame(&pl, cam.target, major);
let n = u.cross(v);
(c, u, v, cam.back().dot(n), Box::new(|p| p))
@@ -701,7 +703,7 @@ fn build_scene(
sc.lines.push(SceneLine { points: pts, color: col, alpha: 1.0, width: if cons { 1.0 } else { 1.6 }, on_top: active });
}
}
sc
Ok(sc)
}
/// Where the plane through `o` with normal `n` cuts a mesh: one segment per crossed triangle,
@@ -855,15 +857,15 @@ pub fn sketch_consumed(s: &Session, id: u64) -> bool {
}
/// Camera framing the model (iso view).
pub fn home_camera(s: &Session) -> Camera {
pub fn home_camera(s: &Session) -> Result<Camera> {
let mut c = Camera::default();
let b = bounds(s);
let b = bounds(s)?;
if !b.is_empty() {
c.fit(&b);
} else {
c.distance = 150.0;
}
c
Ok(c)
}
/// Construction planes of the timeline, as evaluated: (feature id, name, plane).
@@ -937,8 +939,8 @@ mod tests {
s.execute("solid.cylinder", &json!({"diameter": 14, "height": 30, "base": [50, 15, 0], "operation": "new"})).unwrap();
s.execute("appearance.assign", &json!({"bodies": ["Body2"], "color": [220, 60, 40]})).unwrap();
s.execute("appearance.assign", &json!({"faces": [[20, 15, 20]], "body": "Body1", "color": [40, 170, 80]})).unwrap();
let cam = home_camera(&s);
let sc = scene(&s, &cam);
let cam = home_camera(&s).unwrap();
let sc = scene(&s, &cam).unwrap();
assert!(sc.meshes.iter().any(|m| m.color == Rgb(220, 60, 40)));
assert!(sc.meshes.iter().any(|m| m.face_colors.iter().any(|(_, c)| *c == Rgb(40, 170, 80))));
}
@@ -949,16 +951,16 @@ mod tests {
fn guides_leave_the_grid_and_the_axes_out() {
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20, "corner": [10, -15, -10]})).unwrap();
let cam = home_camera(&s);
let cam = home_camera(&s).unwrap();
let grid = [colors::GRID, colors::GRID_MAJOR];
let axes = [colors::AXIS_X, colors::AXIS_Y, colors::AXIS_Z];
let count = |sc: &Scene, colours: &[Rgb]| sc.grid.iter().chain(&sc.lines).filter(|l| colours.contains(&l.color)).count();
let all = scene(&s, &cam);
let all = scene(&s, &cam).unwrap();
let (grid_lines, edges) = (count(&all, &grid), count(&all, &[colors::EDGE]));
assert!(grid_lines > 20 && edges >= 12, "{grid_lines} grid lines, {edges} edges");
assert_eq!(count(&all, &axes), 3);
assert_eq!(all.lines.len(), scene_with_guides(&s, &cam, s.section, Guides::default()).lines.len());
let render = |g: Guides| scene_with_guides(&s, &cam, s.section, g);
assert_eq!(all.lines.len(), scene_with_guides(&s, &cam, s.section, Guides::default()).unwrap().lines.len());
let render = |g: Guides| scene_with_guides(&s, &cam, s.section, g).unwrap();
let no_grid = render(Guides { grid: false, axes: true });
assert_eq!((count(&no_grid, &grid), count(&no_grid, &axes), count(&no_grid, &[colors::EDGE])), (0, 3, edges));
let no_axes = render(Guides { grid: true, axes: false });
@@ -973,10 +975,10 @@ mod tests {
fn hidden_origin_axes_stay_out_of_renders() {
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
let cam = home_camera(&s);
let cam = home_camera(&s).unwrap();
let axes = [colors::AXIS_X, colors::AXIS_Y, colors::AXIS_Z];
let shown = |s: &Session| -> Vec<Rgb> {
let sc = scene(s, &cam);
let sc = scene(s, &cam).unwrap();
axes.into_iter().filter(|c| sc.lines.iter().any(|l| l.color == *c)).collect()
};
assert_eq!(shown(&s), axes);
@@ -984,7 +986,7 @@ mod tests {
assert_eq!(shown(&s), [colors::AXIS_X, colors::AXIS_Z]);
s.execute("browser.visibility", &json!({"folder": "origin", "visible": false})).unwrap();
assert!(shown(&s).is_empty());
let sc = scene_with_guides(&s, &cam, None, Guides { grid: true, axes: true });
let sc = scene_with_guides(&s, &cam, None, Guides { grid: true, axes: true }).unwrap();
assert!(sc.grid.iter().any(|l| l.color == colors::GRID), "the grid is not part of the origin");
s.execute("browser.visibility", &json!({"folder": "origin", "visible": true})).unwrap();
assert_eq!(shown(&s), axes);
@@ -996,14 +998,14 @@ mod tests {
fn the_section_cuts_the_scene() {
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
let cam = home_camera(&s);
assert!(scene(&s, &cam).clip.is_none());
let cam = home_camera(&s).unwrap();
assert!(scene(&s, &cam).unwrap().clip.is_none());
s.execute("inspect.section", &json!({"plane": {"origin": [0, 15, 0], "normal": [0, -1, 0]}})).unwrap();
let sc = scene(&s, &cam);
let sc = scene(&s, &cam).unwrap();
let (n, d) = sc.clip.unwrap();
assert!((n - Vec3::new(0.0, -1.0, 0.0)).len() < 1e-12 && (d + 15.0).abs() < 1e-12, "{n:?} {d}");
assert_eq!(sc.cap, Some(colors::SECTION_CAP));
assert!(!section_segments(&s.world_state().bodies[0].mesh(), Vec3::new(0.0, 15.0, 0.0), -Vec3::Y).is_empty());
assert!(!section_segments(&s.world_state().unwrap().bodies[0].mesh(), Vec3::new(0.0, 15.0, 0.0), -Vec3::Y).is_empty());
}
/// The surface analysis shades the headless scene too, in the palette's colours.
@@ -1011,10 +1013,10 @@ mod tests {
fn the_surface_analysis_shades_the_scene() {
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
let cam = home_camera(&s);
assert!(scene(&s, &cam).analysis.is_none());
let cam = home_camera(&s).unwrap();
assert!(scene(&s, &cam).unwrap().analysis.is_none());
s.analysis = Some(crate::SurfaceAnalysis::Curvature { radius: 5.0 });
let a = scene(&s, &cam).analysis.unwrap();
let a = scene(&s, &cam).unwrap().analysis.unwrap();
assert_eq!((a.mode, a.colors), (solidcraft_render::AnalysisMode::Curvature { radius: 5.0 }, Palette::DEFAULT.analysis));
}
@@ -1024,8 +1026,8 @@ mod tests {
fn cpu_pictures_draw_silhouettes_on_curved_surfaces() {
let mut s = Session::default();
s.execute("solid.sphere", &json!({"diameter": 20})).unwrap();
let cam = home_camera(&s);
let img = solidcraft_render::render(&scene(&s, &cam), &cam, 400, 400);
let cam = home_camera(&s).unwrap();
let img = solidcraft_render::render(&scene(&s, &cam).unwrap(), &cam, 400, 400);
let dark = img.rgba.chunks(4).filter(|p| p[0] < 70 && p[1] < 70 && p[2] < 75).count();
assert!(dark > 40, "silhouette pixels: {dark}");
}
@@ -1035,10 +1037,10 @@ mod tests {
fn cpu_pictures_show_the_section() {
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
let cam = home_camera(&s);
let whole = solidcraft_render::render(&scene(&s, &cam), &cam, 200, 140).rgba;
let cam = home_camera(&s).unwrap();
let whole = solidcraft_render::render(&scene(&s, &cam).unwrap(), &cam, 200, 140).rgba;
s.execute("inspect.section", &json!({"plane": {"origin": [0, 15, 0], "normal": [0, -1, 0]}})).unwrap();
let sc = scene(&s, &cam);
let sc = scene(&s, &cam).unwrap();
assert!(sc.clip.is_some());
let cut = solidcraft_render::render(&sc, &cam, 200, 140).rgba;
let changed = whole.chunks(4).zip(cut.chunks(4)).filter(|(a, b)| a != b).count();
@@ -1055,10 +1057,10 @@ mod tests {
// Cut away the front half (y < 15), so the iso camera looks into the cut.
s.execute("inspect.section", &json!({"plane": {"origin": [0, 15, 0], "normal": [0, -1, 0]}})).unwrap();
for fov in [0.0, 0.6] {
let mut cam = home_camera(&s);
let mut cam = home_camera(&s).unwrap();
cam.fov = fov;
let mut sc = scene(&s, &cam);
assert!(!section_segments(&s.world_state().bodies[0].mesh(), Vec3::new(0.0, 15.0, 0.0), -Vec3::Y).is_empty());
let mut sc = scene(&s, &cam).unwrap();
assert!(!section_segments(&s.world_state().unwrap().bodies[0].mesh(), Vec3::new(0.0, 15.0, 0.0), -Vec3::Y).is_empty());
// A marker inside the body, behind the cut face.
let red = Rgb(255, 0, 0);
sc.lines.push(SceneLine {
+20 -20
View File
@@ -15,7 +15,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value {
/// What the viewport draws: visible bodies' meshes and the sketches it shows, from the scene.
fn drawn(s: &Session) -> Aabb3 {
let cam = solidcraft_render::Camera { distance: 1e5, ..Default::default() };
let scene = crate::view::scene(s, &cam);
let scene = crate::view::scene(s, &cam).unwrap();
let mut b = Aabb3::EMPTY;
for m in &scene.meshes {
b = b.union(&m.mesh.bounds());
@@ -51,11 +51,11 @@ fn block(s: &mut Session) {
fn moved_body_leaves_its_used_sketch_out() {
let mut s = Session::default();
block(&mut s);
near(crate::view::bounds(&s), [0., 0., 0.], [10., 10., 5.]);
let body = s.world_state().bodies[0].name.clone();
near(crate::view::bounds(&s).unwrap(), [0., 0., 0.], [10., 10., 5.]);
let body = s.world_state().unwrap().bodies[0].name.clone();
run(&mut s, "solid.move", json!({"bodies": [body], "translate": [500, 0, 0]}));
near(crate::view::bounds(&s), [500., 0., 0.], [510., 10., 5.]);
same(crate::view::bounds(&s), drawn(&s));
near(crate::view::bounds(&s).unwrap(), [500., 0., 0.], [510., 10., 5.]);
same(crate::view::bounds(&s).unwrap(), drawn(&s));
}
#[test]
@@ -68,14 +68,14 @@ fn moved_component_frames_where_it_is_now() {
block(&mut s);
run(&mut s, "component.activate", json!({"component": "root"}));
run(&mut s, "occurrence.move", json!({"occurrence": b, "translate": [800, 0, 0]}));
near(crate::view::bounds(&s), [0., 0., 0.], [810., 10., 5.]);
near(crate::view::bounds(&s).unwrap(), [0., 0., 0.], [810., 10., 5.]);
run(&mut s, "occurrence.move", json!({"occurrence": b, "translate": [-780, 0, 0]}));
near(crate::view::bounds(&s), [0., 0., 0.], [30., 10., 5.]);
same(crate::view::bounds(&s), drawn(&s));
near(crate::view::bounds(&s).unwrap(), [0., 0., 0.], [30., 10., 5.]);
same(crate::view::bounds(&s).unwrap(), drawn(&s));
// Pending (not captured) moves too.
run(&mut s, "occurrence.move", json!({"occurrence": b, "translate": [0, 300, 0], "capture": false}));
near(crate::view::bounds(&s), [0., 0., 0.], [30., 310., 5.]);
same(crate::view::bounds(&s), drawn(&s));
near(crate::view::bounds(&s).unwrap(), [0., 0., 0.], [30., 310., 5.]);
same(crate::view::bounds(&s).unwrap(), drawn(&s));
}
#[test]
@@ -83,11 +83,11 @@ fn body_consumed_by_a_join_or_cut_is_not_framed() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [200, 0, 0], "operation": "new"}));
let names: Vec<String> = s.world_state().bodies.iter().map(|b| b.name.clone()).collect();
let names: Vec<String> = s.world_state().unwrap().bodies.iter().map(|b| b.name.clone()).collect();
run(&mut s, "solid.move", json!({"bodies": [names[1].clone()], "translate": [-195, 0, 0]}));
run(&mut s, "solid.combine", json!({"target": names[0].clone(), "tools": [names[1].clone()], "operation": "cut"}));
near(crate::view::bounds(&s), [0., 0., 0.], [5., 10., 10.]);
same(crate::view::bounds(&s), drawn(&s));
near(crate::view::bounds(&s).unwrap(), [0., 0., 0.], [5., 10., 10.]);
same(crate::view::bounds(&s).unwrap(), drawn(&s));
}
#[test]
@@ -107,11 +107,11 @@ fn hidden_component_is_not_framed() {
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "component.activate", json!({"component": "root"}));
run(&mut s, "occurrence.move", json!({"occurrence": b, "translate": [400, 0, 0]}));
near(crate::view::bounds(&s), [0., 0., 0.], [440., 40., 5.]);
same(crate::view::bounds(&s), drawn(&s));
near(crate::view::bounds(&s).unwrap(), [0., 0., 0.], [440., 40., 5.]);
same(crate::view::bounds(&s).unwrap(), drawn(&s));
run(&mut s, "browser.visibility", json!({"visible": false, "paths": [[b]]}));
near(crate::view::bounds(&s), [0., 0., 0.], [10., 10., 5.]);
same(crate::view::bounds(&s), drawn(&s));
near(crate::view::bounds(&s).unwrap(), [0., 0., 0.], [10., 10., 5.]);
same(crate::view::bounds(&s).unwrap(), drawn(&s));
}
/// Two leaves joined by a hinge (as in the joint tests).
@@ -147,16 +147,16 @@ fn block_leaf(s: &mut Session) {
fn joint_and_drive_frame_the_joined_place() {
let mut s = Session::default();
hinge(&mut s);
let b = crate::view::bounds(&s);
let b = crate::view::bounds(&s).unwrap();
assert!(b.max.x < 50.0, "the joint brought B onto A: {b:?}");
same(b, drawn(&s));
run(&mut s, "joint.drive", json!({"joint": "Hinge", "value": 90}));
let b = crate::view::bounds(&s);
let b = crate::view::bounds(&s).unwrap();
assert!(b.max.x < 50.0 && b.max.z > 30.0, "{b:?}");
same(b, drawn(&s));
run(&mut s, "motion.study", json!({"name": "Swing", "steps": 10, "keys": [{"joint": "Hinge", "points": [[0, 0], [10, 180]]}]}));
run(&mut s, "motion.play", json!({"study": "Swing", "step": 10}));
let b = crate::view::bounds(&s);
let b = crate::view::bounds(&s).unwrap();
assert!(b.max.x < 50.0 && b.max.y > 60.0, "{b:?}");
same(b, drawn(&s));
}
+5 -4
View File
@@ -154,8 +154,8 @@ fn scene_sampler_uses_exact_active_occurrence_plane_and_preserves_other_scene_co
s.execute("sketch.circle.center", &json!({"center":[2,3],"radius":1})).unwrap();
s.section = Some((Vec3::new(0., 0., 3.), Vec3::Z));
let cam = Camera::default();
let old = scene(&s, &cam);
let default = scene_with_sketch_sampler(&s, &cam, world_curve_parts);
let old = scene(&s, &cam).unwrap();
let default = scene_with_sketch_sampler(&s, &cam, world_curve_parts).unwrap();
assert_eq!(old.lines.len(), default.lines.len());
for (a, b) in old.lines.iter().zip(&default.lines) {
assert_eq!((&a.points, a.color, a.width, a.on_top), (&b.points, b.color, b.width, b.on_top));
@@ -165,7 +165,8 @@ fn scene_sampler_uses_exact_active_occurrence_plane_and_preserves_other_scene_co
let sampled = scene_with_sketch_sampler(&s, &cam, |_, _, plane| {
planes.push(*plane);
parts.clone()
});
})
.unwrap();
assert_eq!(planes.len(), 1);
assert!(planes[0].origin.dist(Vec3::new(20., 30., 40.)) < 1e-9);
assert!(planes[0].x.dist(Vec3::X) < 1e-9);
@@ -186,7 +187,7 @@ fn scene_sampler_uses_exact_active_occurrence_plane_and_preserves_other_scene_co
let active = s.active_sketch.unwrap();
s.execute("sketch.finish", &json!({})).unwrap();
s.visibility.hidden_sketches.push(active);
let hidden = scene_with_sketch_sampler(&s, &cam, |_, _, _| panic!("hidden sketch sampled"));
let hidden = scene_with_sketch_sampler(&s, &cam, |_, _, _| panic!("hidden sketch sampled")).unwrap();
assert!(hidden.lines.iter().all(|line| !line.on_top));
}
+9 -9
View File
@@ -206,13 +206,13 @@ fn every_scene_colour_comes_from_its_palette_and_appearances_stay() {
s.execute("sketch.finish", &json!({})).unwrap();
s.selection = vec![Sel::Body { name: "Picked".into() }];
s.section = Some((Vec3::new(0., 0., 5.), Vec3::Z));
let cam = home_camera(&s);
let names: Vec<String> = s.world_state().bodies.iter().filter(|b| s.body_visible(&b.name)).map(|b| b.name.clone()).collect();
let cam = home_camera(&s).unwrap();
let names: Vec<String> = s.world_state().unwrap().bodies.iter().filter(|b| s.body_visible(&b.name)).map(|b| b.name.clone()).collect();
let mesh = |sc: &Scene, name: &str| {
names.iter().position(|n| n == name).and_then(|i| sc.meshes.get(i)).map(|m| (m.color, m.face_colors.clone())).unwrap()
};
let marked = scene_with_palette(&s, &cam, s.section, world_curve_parts, &MARK);
let marked = scene_with_palette(&s, &cam, s.section, world_curve_parts, &MARK).unwrap();
assert_eq!((marked.background, marked.cap), (Some(MARK.background), Some(MARK.cap)));
let allowed = [
MARK.grid,
@@ -244,8 +244,8 @@ fn every_scene_colour_comes_from_its_palette_and_appearances_stay() {
assert!(!faces.is_empty() && faces.iter().all(|(_, c)| *c == Rgb(0, 255, 0)), "face colours are the design's: {faces:?}");
// The default palette is the scene everyone else gets, line for line.
let default = scene_with_palette(&s, &cam, s.section, world_curve_parts, &Palette::DEFAULT);
let plain_scene = scene(&s, &cam);
let default = scene_with_palette(&s, &cam, s.section, world_curve_parts, &Palette::DEFAULT).unwrap();
let plain_scene = scene(&s, &cam).unwrap();
assert_eq!(
(default.background, default.cap, default.clip, default.radius),
(plain_scene.background, plain_scene.cap, plain_scene.clip, plain_scene.radius)
@@ -274,9 +274,9 @@ fn a_see_through_appearance_reaches_the_scene() {
s.execute("appearance.assign", &json!({"bodies": ["Target"], "color": "#b0bac6", "opacity": 0.3})).unwrap();
s.execute("appearance.assign", &json!({"bodies": ["Picked"], "color": "#b0bac6", "opacity": 0.3})).unwrap();
s.selection = vec![Sel::Body { name: "Picked".into() }];
let cam = home_camera(&s);
let sc = scene(&s, &cam);
let names: Vec<String> = s.world_state().bodies.iter().filter(|b| s.body_visible(&b.name)).map(|b| b.name.clone()).collect();
let cam = home_camera(&s).unwrap();
let sc = scene(&s, &cam).unwrap();
let names: Vec<String> = s.world_state().unwrap().bodies.iter().filter(|b| s.body_visible(&b.name)).map(|b| b.name.clone()).collect();
let opacity = |name: &str| names.iter().position(|n| n == name).and_then(|i| sc.meshes.get(i)).map(|m| (m.opacity, m.glass())).unwrap();
assert_eq!(opacity("Target"), (0.3, true), "the design's opacity is the one the renderers draw");
@@ -285,6 +285,6 @@ fn a_see_through_appearance_reaches_the_scene() {
// Clearing the appearance makes it solid again.
s.execute("appearance.assign", &json!({"bodies": ["Target"], "color": null})).unwrap();
let sc = scene(&s, &cam);
let sc = scene(&s, &cam).unwrap();
assert_eq!(sc.meshes.first().map(|m| m.opacity), Some(1.0));
}
@@ -21,8 +21,8 @@ fn world_placement_cache_preserves_pending_picks_materials_scratch_and_named_joi
let path = vec![second];
let placement = session.occurrence_placement(&path).unwrap();
let name = placement.bodies[0].world.clone();
let world = session.world_state();
assert!(Arc::ptr_eq(&world, &session.world_state()));
let world = session.world_state().unwrap();
assert!(Arc::ptr_eq(&world, &session.world_state().unwrap()));
let source_state = session.model.state();
let source = source_state.body("Block").unwrap();
let placed = world.body(&name).unwrap();
@@ -38,10 +38,10 @@ fn world_placement_cache_preserves_pending_picks_materials_scratch_and_named_joi
let top = placed.body.faces(0.001).unwrap().into_iter().find(|face| face.plane_normal.is_some_and(|n| n.z > 0.99)).unwrap().index;
run(&mut session, "select.set", json!({"items":[{"type":"face","body":name,"index":top,"point":[pick.x,pick.y,pick.z]}]}));
assert!(matches!(&session.selection[0], crate::Sel::Face { body, index, .. } if body == &name && *index == top));
assert!(Arc::ptr_eq(&world, &session.world_state()), "selection changes must not discard prepared placements");
assert!(Arc::ptr_eq(&world, &session.world_state().unwrap()), "selection changes must not discard prepared placements");
let scratch = session.scratch();
assert_eq!(scratch.pending_moves, pending);
let scratch_world = scratch.world_state();
let scratch_world = scratch.world_state().unwrap();
assert!(Arc::ptr_eq(&world, &scratch_world));
let scratch_body = scratch_world.body(&name).unwrap();
assert!(scratch_body.mesh().bounds().min.dist(bounds.min) < 1e-9);
@@ -53,7 +53,7 @@ fn world_placement_cache_preserves_pending_picks_materials_scratch_and_named_joi
let before_joint = crate::doc::mat_mul(&crate::doc::joints::occurrence_world(&session.doc, second), &local_frame);
assert!(crate::doc::apply_point(&before_joint, Vec3::ZERO).dist(pick) < 1e-9);
run(&mut session, "occurrence.move", json!({"occurrence":second,"translate":[5,0,0],"capture":true}));
let moved = session.world_state();
let moved = session.world_state().unwrap();
assert!(!Arc::ptr_eq(&world, &moved), "a changed transform must invalidate placed meshes");
assert!(moved.body(&name).unwrap().mesh().bounds().min.dist(bounds.min + Vec3::new(5., 0., 0.)) < 1e-9);
assert!(world.body(&name).unwrap().mesh().bounds().min.dist(bounds.min) < 1e-9, "prior snapshots must remain immutable");
@@ -62,7 +62,7 @@ fn world_placement_cache_preserves_pending_picks_materials_scratch_and_named_joi
let after_joint = crate::doc::mat_mul(&crate::doc::joints::occurrence_world(&session.doc, second), &local_frame);
assert!(crate::doc::apply_point(&after_joint, Vec3::ZERO).dist(pick + Vec3::new(5., 0., 0.)) < 1e-9);
let reopened = Session::new(crate::doc::Document::from_json(&session.doc.to_json()).unwrap());
assert!(reopened.world_state().body(&name).unwrap().mesh().bounds().min.dist(bounds.min + Vec3::new(5., 0., 0.)) < 1e-9);
assert!(reopened.world_state().unwrap().body(&name).unwrap().mesh().bounds().min.dist(bounds.min + Vec3::new(5., 0., 0.)) < 1e-9);
assert_eq!(reopened.model.results.iter().filter(|r| r.error.is_some()).count(), 0);
}
@@ -71,22 +71,22 @@ fn selection_visibility_section_and_view_revisions_reuse_world_geometry() {
let mut session = Session::default();
run(&mut session, "component.create", json!({"name":"Part"}));
run(&mut session, "solid.box", json!({"length":2,"width":3,"height":4,"body_name":"Block","operation":"new"}));
let world = session.world_state();
let world = session.world_state().unwrap();
let revision = session.revision;
run(&mut session, "select.set", json!({"items":[{"type":"body","name":"Block"}]}));
assert!(session.revision > revision);
assert!(Arc::ptr_eq(&world, &session.world_state()));
assert!(Arc::ptr_eq(&world, &session.world_state().unwrap()));
run(&mut session, "select.clear", json!({}));
assert!(Arc::ptr_eq(&world, &session.world_state()));
assert!(Arc::ptr_eq(&world, &session.world_state().unwrap()));
run(&mut session, "browser.visibility", json!({"items":["Block"],"visible":false}));
assert!(!session.body_visible("Block"));
assert!(Arc::ptr_eq(&world, &session.world_state()));
assert!(Arc::ptr_eq(&world, &session.world_state().unwrap()));
run(&mut session, "inspect.section", json!({"plane":{"origin":[0,0,2],"normal":[0,0,1]}}));
assert!(session.section.is_some());
assert!(Arc::ptr_eq(&world, &session.world_state()));
assert!(Arc::ptr_eq(&world, &session.world_state().unwrap()));
session.invalidate_view(session.revision + 10);
assert!(Arc::ptr_eq(&world, &session.world_state()));
assert!(Arc::ptr_eq(&world, &session.scratch().world_state()));
assert!(Arc::ptr_eq(&world, &session.world_state().unwrap()));
assert!(Arc::ptr_eq(&world, &session.scratch().world_state().unwrap()));
}
#[test]
@@ -99,50 +99,53 @@ fn world_cache_requires_exact_design_model_pending_and_author_context() {
let sketch = run(&mut session, "sketch.create", json!({"plane":"XY"}))["sketch"].as_u64().unwrap();
run(&mut session, "sketch.finish", json!({}));
let second = run(&mut session, "occurrence.copy", json!({"occurrence":first}))["occurrence"].as_u64().unwrap();
let initial = session.world_state();
let initial = session.world_state().unwrap();
run(&mut session, "occurrence.move", json!({"occurrence":second,"translate":[10,20,0],"capture":false}));
let pending = session.world_state();
let pending = session.world_state().unwrap();
assert!(!Arc::ptr_eq(&initial, &pending));
let name = session.occurrence_placement(&[second]).unwrap().bodies[0].world.clone();
assert!(pending.body(&name).unwrap().mesh().bounds().min.dist(Vec3::new(10., 20., 0.)) < 1e-9);
run(&mut session, "component.activate", json!({"component":component,"occurrence":second,"path":[second]}));
let authored = session.world_state();
let authored = session.world_state().unwrap();
assert!(!Arc::ptr_eq(&pending, &authored));
assert!(authored.sketch(sketch).unwrap().plane.origin.dist(Vec3::new(10., 20., 0.)) < 1e-9);
let mut scratch = session.scratch();
assert!(Arc::ptr_eq(&authored, &scratch.world_state()));
assert!(Arc::ptr_eq(&authored, &scratch.world_state().unwrap()));
scratch.active_occurrence_path = Some(vec![first]);
let scratch_world = scratch.world_state();
let scratch_world = scratch.world_state().unwrap();
assert!(!Arc::ptr_eq(&authored, &scratch_world));
assert!(scratch_world.sketch(sketch).unwrap().plane.origin.dist(Vec3::ZERO) < 1e-9);
assert!(Arc::ptr_eq(&authored, &session.world_state()), "scratch context must not replace the source cache");
assert!(Arc::ptr_eq(&authored, &session.world_state().unwrap()), "scratch context must not replace the source cache");
// Evaluation and placement never read appearances: an appearance edit, its undo and its redo
// keep the evaluated model and the placed world (the cache follows the new document).
let model = session.model.state();
run(&mut session, "appearance.assign", json!({"bodies":["Block"],"color":"#ff0000"}));
let painted = session.world_state();
let painted = session.world_state().unwrap();
assert!(Arc::ptr_eq(&authored, &painted), "an appearance edit keeps the placed world");
assert!(Arc::ptr_eq(&model, &session.model.state()), "and the evaluated model");
assert_ne!(session.doc.model_body_look(painted.body("Block").unwrap()), None);
run(&mut session, "edit.undo", json!({}));
let undone = session.world_state();
let undone = session.world_state().unwrap();
assert!(Arc::ptr_eq(&painted, &undone), "undoing it too");
assert_eq!(session.doc.model_body_look(undone.body("Block").unwrap()), None);
run(&mut session, "edit.redo", json!({}));
let redone = session.world_state();
let redone = session.world_state().unwrap();
assert!(Arc::ptr_eq(&undone, &redone), "and redoing it");
assert!(session.doc.model_body_look(redone.body("Block").unwrap()).is_some());
let before_edit = session.world_state();
let before_edit = session.world_state().unwrap();
run(&mut session, "solid.box", json!({"length":5,"width":6,"height":7,"body_name":"Other","operation":"new"}));
let edited = session.world_state();
let edited = session.world_state().unwrap();
assert!(!Arc::ptr_eq(&before_edit, &edited));
assert!(edited.body("Other").is_some());
session.doc = Arc::new(crate::doc::Document::from_json(&session.doc.to_json()).unwrap());
let replaced = session.world_state();
let replaced = session.world_state().unwrap();
assert!(!Arc::ptr_eq(&edited, &replaced), "equal contents in a different document cannot share the cache key");
session.model = crate::doc::Model::default();
session.model.evaluate(&session.doc);
assert!(!Arc::ptr_eq(&replaced, &session.world_state()), "a new evaluated model must invalidate even when the document Arc is unchanged");
assert!(
!Arc::ptr_eq(&replaced, &session.world_state().unwrap()),
"a new evaluated model must invalidate even when the document Arc is unchanged"
);
}
#[test]
@@ -150,35 +153,35 @@ fn adopted_scratch_keeps_its_warm_geometry_and_history_stages_invalidate_it() {
let mut session = Session::default();
run(&mut session, "component.create", json!({"name":"Part"}));
run(&mut session, "solid.box", json!({"length":2,"width":3,"height":4,"body_name":"Block","operation":"new"}));
let source = session.world_state();
let source = session.world_state().unwrap();
let source_json = session.doc.to_json();
let mut scratch = session.scratch();
run(&mut scratch, "solid.box", json!({"length":5,"width":6,"height":7,"body_name":"Draft","operation":"new"}));
let warmed = scratch.world_state();
let warmed = scratch.world_state().unwrap();
assert!(!Arc::ptr_eq(&source, &warmed));
assert_eq!(session.doc.to_json(), source_json);
assert!(Arc::ptr_eq(&source, &session.world_state()));
assert!(Arc::ptr_eq(&source, &session.world_state().unwrap()));
assert!(source.body("Draft").is_none());
let feature = warmed.body("Draft").unwrap().feature;
session.accept_computed(scratch);
assert!(Arc::ptr_eq(&warmed, &session.world_state()), "adoption/view revision must retain the worker's prepared geometry");
assert!(Arc::ptr_eq(&warmed, &session.world_state().unwrap()), "adoption/view revision must retain the worker's prepared geometry");
session.invalidate_view(session.revision + 20);
assert!(Arc::ptr_eq(&warmed, &session.world_state()));
assert!(Arc::ptr_eq(&warmed, &session.world_state().unwrap()));
run(&mut session, "timeline.rollback", json!({"position":1}));
let rolled = session.world_state();
let rolled = session.world_state().unwrap();
assert!(!Arc::ptr_eq(&warmed, &rolled));
assert!(rolled.body("Block").is_some());
assert!(rolled.body("Draft").is_none());
run(&mut session, "timeline.rollback", json!({}));
let restored = session.world_state();
let restored = session.world_state().unwrap();
assert!(!Arc::ptr_eq(&rolled, &restored));
assert!(restored.body("Draft").is_some());
run(&mut session, "timeline.suppress", json!({"feature":feature,"suppressed":true}));
let suppressed = session.world_state();
let suppressed = session.world_state().unwrap();
assert!(!Arc::ptr_eq(&restored, &suppressed));
assert!(suppressed.body("Draft").is_none());
run(&mut session, "timeline.suppress", json!({"feature":feature,"suppressed":false}));
assert!(session.world_state().body("Draft").is_some());
assert!(session.world_state().unwrap().body("Draft").is_some());
}
/// A moved component's bodies keep their placed geometry and display mesh while the design
@@ -192,26 +195,26 @@ fn moved_component_bodies_are_not_placed_again_when_something_else_changes() {
run(&mut session, "solid.box", json!({"length":60,"width":60,"height":5,"body_name":"Rim"}));
run(&mut session, "component.activate", json!({"component":"root"}));
run(&mut session, "occurrence.move", json!({"occurrence":occurrence,"translate":[100,0,0]}));
let placed = session.world_state();
let placed = session.world_state().unwrap();
let rim = placed.bodies.iter().find(|b| b.source_name.as_deref() == Some("Rim")).unwrap().clone();
let mesh = rim.mesh();
assert!(mesh.bounds().min.dist(Vec3::new(100., 0., 0.)) < 1e-6);
// A body added elsewhere: the rim is the same placed body, already meshed.
run(&mut session, "solid.box", json!({"length":5,"width":6,"height":7,"body_name":"Frame"}));
let after = session.world_state();
let after = session.world_state().unwrap();
let again = after.body(&rim.name).unwrap();
assert!(Arc::ptr_eq(&mesh, &again.mesh()), "an unchanged placed body keeps its mesh");
assert!(again.body.same(&rim.body));
// The placement moves: new world coordinates, reusing the definition's triangulation.
run(&mut session, "occurrence.move", json!({"occurrence":occurrence,"translate":[0,50,0]}));
let moved = session.world_state();
let moved = session.world_state().unwrap();
let moved_rim = moved.bodies.iter().find(|b| b.source_name.as_deref() == Some("Rim")).unwrap();
assert!(!Arc::ptr_eq(&mesh, &moved_rim.mesh()));
assert!(moved_rim.mesh().bounds().min.dist(Vec3::new(100., 50., 0.)) < 1e-6);
// The body itself changes: placed again too, and the old snapshot is untouched.
run(&mut session, "body.rename", json!({"body":"Frame","name":"Frame tube"}));
let source = session.model.state().body("Rim").unwrap().clone();
let world = session.world_state();
let world = session.world_state().unwrap();
let kept = world.bodies.iter().find(|b| b.source_name.as_deref() == Some("Rim")).unwrap();
assert!(Arc::ptr_eq(&moved_rim.mesh(), &kept.mesh()));
assert_eq!(kept.source_transform, moved_rim.source_transform);
@@ -233,14 +236,14 @@ fn joint_drive_reuses_reference_tessellation_across_rigid_poses() {
json!({"type":"revolute","name":"Crank","a":0,"b":"Crank","at":{"occurrence":0,"point":[0,0,0],"z":[0,0,1]}}),
);
let definitions = session.model.state();
let original = session.world_state();
let original = session.world_state().unwrap();
let local = original.body("Crank").unwrap().mesh();
let bounds = local.bounds();
for angle in [37., 91., 37., 0.] {
let driven = run(&mut session, "joint.drive", json!({"joint":"Crank","value":angle}));
assert_eq!(driven["moved"], true);
assert!(Arc::ptr_eq(&definitions, &session.model.state()));
let world = session.world_state();
let world = session.world_state().unwrap();
let body = world.body("Crank").unwrap();
let (mesh, work) = crate::kernel::profile::capture(|| body.mesh());
assert_eq!(work.tessellate_ms, 0., "a warm definition must not enter the kernel mesher at a new pose: {work:?}");
+1 -1
View File
@@ -13,7 +13,7 @@ fn assembly() -> Session {
}
fn warm(session: &Session) {
for body in &session.world_state().bodies {
for body in &session.world_state().unwrap().bodies {
body.mesh();
}
}
+4 -1
View File
@@ -196,7 +196,10 @@ fn a_resident_mesh_matches_the_cpu() {
#[test]
fn a_scene_in_one_dispatch_matches_each_body_on_the_cpu() {
let Some(gpu) = gpu() else { return };
let plans: Vec<TessPlan> = bodies().iter().map(|(_, b)| ok(b.mesh_plan(&solidcraft_kernel::MeshTolerance { chord: 0.01, angle: Some(std::f64::consts::PI / 9.0) }))).collect();
let plans: Vec<TessPlan> = bodies()
.iter()
.map(|(_, b)| ok(b.mesh_plan(&solidcraft_kernel::MeshTolerance { chord: 0.01, angle: Some(std::f64::consts::PI / 9.0) })))
.collect();
let refs: Vec<&TessPlan> = plans.iter().collect();
let mesh = ok(gpu.prepare_many(&refs));
assert_eq!(mesh.parts.len(), plans.len());
+16 -15
View File
@@ -70,8 +70,8 @@ fn opengl_viewport_pipelines_render_a_model() {
.unwrap();
let c = Capturer::new(&adapter, device, queue).unwrap();
let s = session(&[("solid.box", json!({"length": 40, "width": 30, "height": 20}))]);
let cam = view::home_camera(&s);
let img = c.rgba(&view::scene_with_guides(&s, &cam, None, Guides { grid: false, axes: false }), &cam, 160, 100).unwrap();
let cam = view::home_camera(&s).unwrap();
let img = c.rgba(&view::scene_with_guides(&s, &cam, None, Guides { grid: false, axes: false }).unwrap(), &cam, 160, 100).unwrap();
let empty = c.rgba(&Default::default(), &cam, 160, 100).unwrap();
let changed = pixels(&img).zip(pixels(&empty)).filter(|(a, b)| a != b).count();
assert!(changed > 500, "the model covers only {changed} pixels");
@@ -87,8 +87,8 @@ fn pictures_show_appearances() {
("appearance.assign", json!({"bodies": ["Body2"], "color": [220, 60, 40]})),
("appearance.assign", json!({"faces": [[20, 15, 20]], "body": "Body1", "color": [40, 170, 80]})),
]);
let cam = view::home_camera(&s);
let img = c.rgba(&view::scene(&s, &cam), &cam, 200, 140).unwrap();
let cam = view::home_camera(&s).unwrap();
let img = c.rgba(&view::scene(&s, &cam).unwrap(), &cam, 200, 140).unwrap();
assert_eq!(img.len(), 200 * 140 * 4);
let green = pixels(&img).filter(|p| p[1] > 120 && p[0] < 90 && p[2] < 110).count();
let red = pixels(&img).filter(|p| p[0] > 150 && p[1] < 90 && p[2] < 80).count();
@@ -126,8 +126,8 @@ fn the_renderer_counts_what_it_draws() {
fn guides_change_the_picture() {
let Some(c) = capturer() else { return };
let s = session(&[("solid.box", json!({"length": 40, "width": 30, "height": 20, "corner": [10, -15, -10]}))]);
let cam = view::home_camera(&s);
let picture = |grid, axes| c.rgba(&view::scene_with_guides(&s, &cam, None, Guides { grid, axes }), &cam, 160, 100).unwrap();
let cam = view::home_camera(&s).unwrap();
let picture = |grid, axes| c.rgba(&view::scene_with_guides(&s, &cam, None, Guides { grid, axes }).unwrap(), &cam, 160, 100).unwrap();
let (all, no_grid, no_axes, bare) = (picture(true, true), picture(false, true), picture(true, false), picture(false, false));
assert!(all != no_grid && all != no_axes && no_grid != bare && no_axes != bare);
}
@@ -137,8 +137,8 @@ fn guides_change_the_picture() {
fn pictures_draw_silhouettes_on_curved_surfaces() {
let Some(c) = capturer() else { return };
let s = session(&[("solid.sphere", json!({"diameter": 20}))]);
let cam = view::home_camera(&s);
let img = c.rgba(&view::scene_with_guides(&s, &cam, None, Guides { grid: false, axes: false }), &cam, 400, 400).unwrap();
let cam = view::home_camera(&s).unwrap();
let img = c.rgba(&view::scene_with_guides(&s, &cam, None, Guides { grid: false, axes: false }).unwrap(), &cam, 400, 400).unwrap();
let dark = pixels(&img).filter(|p| p[0] < 90 && p[1] < 90 && p[2] < 95).count();
assert!(dark > 40, "silhouette pixels: {dark}");
}
@@ -149,17 +149,17 @@ fn pictures_draw_silhouettes_on_curved_surfaces() {
fn pictures_show_the_section_and_its_cap() {
let Some(c) = capturer() else { return };
let mut s = session(&[("solid.box", json!({"length": 40, "width": 30, "height": 20}))]);
let cam = view::home_camera(&s);
let whole = c.rgba(&view::scene(&s, &cam), &cam, 200, 140).unwrap();
let cam = view::home_camera(&s).unwrap();
let whole = c.rgba(&view::scene(&s, &cam).unwrap(), &cam, 200, 140).unwrap();
// Cut away the front half (y < 15), so the iso camera looks into the cut.
s.execute("inspect.section", &json!({"plane": {"origin": [0, 15, 0], "normal": [0, -1, 0]}})).unwrap();
let cut = c.rgba(&view::scene(&s, &cam), &cam, 200, 140).unwrap();
let cut = c.rgba(&view::scene(&s, &cam).unwrap(), &cam, 200, 140).unwrap();
let changed = pixels(&whole).zip(pixels(&cut)).filter(|(a, b)| a != b).count();
assert!(changed > 500, "{changed} pixels changed");
for fov in [0.0, 0.6] {
let mut cam = view::home_camera(&s);
let mut cam = view::home_camera(&s).unwrap();
cam.fov = fov;
let mut sc = view::scene(&s, &cam);
let mut sc = view::scene(&s, &cam).unwrap();
// A marker inside the body, behind the cut face.
let red = Rgb(255, 0, 0);
sc.lines.push(SceneLine {
@@ -266,8 +266,9 @@ fn pictures_show_the_surface_analysis() {
("solid.box", json!({"length": 60, "width": 40, "height": 20})),
("solid.hole", json!({"position": [30, 20, 20], "diameter": 10, "depth": 20})),
]);
let cam = view::home_camera(&s);
let picture = |s: &Session| c.rgba(&view::scene_with_guides(s, &cam, None, Guides { grid: false, axes: false }), &cam, 320, 200).unwrap();
let cam = view::home_camera(&s).unwrap();
let picture =
|s: &Session| c.rgba(&view::scene_with_guides(s, &cam, None, Guides { grid: false, axes: false }).unwrap(), &cam, 320, 200).unwrap();
let plain = picture(&s);
s.execute("inspect.curvature_map", &json!({})).unwrap();
let curvature = picture(&s);
+4 -4
View File
@@ -59,7 +59,7 @@ fn session(script: &[(&str, serde_json::Value)]) -> Session {
}
fn picture(c: &Capturer, s: &Session, cam: &Camera, w: u32, h: u32) -> Vec<u8> {
c.rgba(&view::scene_with_guides(s, cam, None, Guides { grid: false, axes: false }), cam, w, h).unwrap()
c.rgba(&view::scene_with_guides(s, cam, None, Guides { grid: false, axes: false }).unwrap(), cam, w, h).unwrap()
}
fn differing(a: &[u8], b: &[u8]) -> usize {
@@ -114,7 +114,7 @@ fn at_ten_metres_a_thin_label_is_whole_and_a_washer_behind_stays_hidden() {
continue;
}
// The label's front face (y = -0.3), sampled inside its outline.
let radius = view::scene(&with, &cam).radius;
let radius = view::scene(&with, &cam).unwrap().radius;
let mut grey = Vec::new();
for i in 1..20 {
for j in 1..20 {
@@ -271,12 +271,12 @@ fn an_edge_on_the_cut_face_draws_over_the_cap() {
s.execute("inspect.section", &json!({"plane": {"origin": [0, 15, 0], "normal": [0, -1, 0]}})).unwrap();
for (mode, c) in capturers() {
for fov in [0.0, 0.6] {
let mut cam = view::home_camera(&s);
let mut cam = view::home_camera(&s).unwrap();
cam.fov = fov;
let count = |on_top: bool| {
// The session's section (`scene_with_guides` takes its own). The line is 2 µm on
// the kept side: exactly on the plane, rounding cuts parts of it away with the rest.
let mut sc = view::scene(&s, &cam);
let mut sc = view::scene(&s, &cam).unwrap();
sc.lines.push(red_line(Vec3::new(5.0, 15.002, 10.0), Vec3::new(35.0, 15.002, 10.0), on_top));
pixels(&c.rgba(&sc, &cam, 300, 220).unwrap()).filter(|p| red(*p)).count()
};
+1 -1
View File
@@ -295,7 +295,7 @@ fn matrix_of(numbers: &[f64]) -> Result<[[f64; 4]; 4]> {
pub(super) fn geometry_preview(document: &[u8], model: &SavedModel) -> Result<DesignPreview> {
let doc = Document::from_json(std::str::from_utf8(document).map_err(invalid)?)?;
let world = solidcraft_doc::world_state(&doc, &*model.final_state()?);
let world = solidcraft_doc::world_state(&doc, &*model.final_state()?)?;
let mut preview = DesignPreview::default();
let mut triangles = 0usize;
let mut display_bytes = 0usize;

Some files were not shown because too many files have changed in this diff Show More