Merge fixes: every crate and test target compiles on the merged tree, but for crates/geom's hlr

world_state, view::bounds, home_camera and scene return Result on develop; new
main code takes that with ? or session_world. New fixtures carry the spline's
nurbs. ForeignCopy::of returns Result. occurrence_placement_with walks the path
with its moves, since the placement index holds pending_moves only.

The hlr port is separate and still to come: this commit does not compile as it
stands in crates/geom/src/hlr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNXLXkZLQ6T7TSi4wswcmk
This commit is contained in:
brett19
2026-10-31 17:01:30 -07:00
parent 5a161e542b
commit 1b17dd7f54
94 changed files with 310 additions and 260 deletions
Generated
+3 -3
View File
@@ -4237,7 +4237,7 @@ dependencies = [
[[package]]
name = "solidcraft-profile"
version = "0.24.0"
version = "0.29.0"
dependencies = [
"serde_json",
"web-time",
@@ -4272,7 +4272,7 @@ dependencies = [
[[package]]
name = "solidcraft-smlib"
version = "0.24.0"
version = "0.29.0"
dependencies = [
"cc",
"flate2",
@@ -4292,7 +4292,7 @@ dependencies = [
[[package]]
name = "solidcraft-step"
version = "0.24.0"
version = "0.29.0"
dependencies = [
"solidcraft-geom",
"solidcraft-profile",
+3 -3
View File
@@ -514,12 +514,12 @@ fn open_validating(
let load = |p: &str| load_with(p, check, validation);
match (base, path) {
(Some(b), Some(p)) => {
let mut s = load_checked(b, check)?;
let mut s = load(b)?;
run_script_warning(&mut s, p, &read_json(p)?)?;
Ok(s)
}
(Some(b), None) => load_checked(b, check),
(None, Some(p)) => load_checked(p, check),
(Some(b), None) => load(b),
(None, Some(p)) => load(p),
(None, None) => Err(usage()),
}
}
+4 -2
View File
@@ -2077,7 +2077,8 @@ impl Model {
name: f.name.clone(),
error: Some(e),
warning: None,
boolean_diagnostics: Default::default(),
import_report: None,
repair: None,
ms: 0.0,
skipped: false,
stale: false,
@@ -3354,7 +3355,8 @@ fn named_pieces_of(
} else {
let kept = stored.get(*extra).filter(|n| !n.trim().is_empty() && !taken.contains(*n)).cloned();
*extra += 1;
let name = kept.unwrap_or_else(|| (k..=k + taken.len()).map(|j| format!("{} ({j})", mb.name)).find(|n| !taken.contains(n)).unwrap_or_default());
let name =
kept.unwrap_or_else(|| (k..=k + taken.len()).map(|j| format!("{} ({j})", mb.name)).find(|n| !taken.contains(n)).unwrap_or_default());
(name, feature)
};
taken.insert(name.clone());
+1
View File
@@ -471,6 +471,7 @@ impl SavedModel {
// Not kept either: a saved model's states carry no lineage, so the check of a
// legacy split's picks (`lineage::legacy_picks_digest`) finds no pieces in them
// and takes them as the design's evaluation they were saved as.
legacy_picks: BTreeMap::new(),
})
});
let states = built.into_iter().map(|r| r.unwrap_or_else(|| Err(bad("saved model: state"))).map(Arc::new)).collect::<Result<Vec<_>>>()?;
+3
View File
@@ -95,6 +95,7 @@ pub(super) fn local_geometry(f: &Feature, components: &std::collections::HashMap
to,
to_offset: _,
from,
corner: _,
} = extent;
to.is_none() && from.is_none() && !through_all && components.get(&sketch.0) == Some(&f.component)
}
@@ -194,6 +195,7 @@ fn same_feature(a: &Feature, b: &Feature) -> bool {
edge_names,
face_names,
participants,
touching,
shell_piece_order,
kernel_piece_order,
inserted,
@@ -210,6 +212,7 @@ fn same_feature(a: &Feature, b: &Feature) -> bool {
&& *edge_names == b.edge_names
&& *face_names == b.face_names
&& *participants == b.participants
&& *touching == b.touching
&& *shell_piece_order == b.shell_piece_order
&& *kernel_piece_order == b.kernel_piece_order
&& *inserted == b.inserted
+8 -1
View File
@@ -471,13 +471,20 @@ mod tests {
control: true,
degree: 3,
periodic: false,
nurbs: None,
},
LinkGeom::Line(Vec2::new(12., 5. - 1e-5), Vec2::new(0., 0.1)),
];
super::super::meet_at_shared_ends(&mut geom, 1e-4);
assert_eq!(
geom[1],
LinkGeom::Spline { pts: vec![Vec2::new(10., 0.), Vec2::new(11., 1.), Vec2::new(12., 5.)], control: true, degree: 3, periodic: false }
LinkGeom::Spline {
pts: vec![Vec2::new(10., 0.), Vec2::new(11., 1.), Vec2::new(12., 5.)],
control: true,
degree: 3,
periodic: false,
nurbs: None
}
);
assert_eq!(geom[2], LinkGeom::Line(Vec2::new(12., 5.), Vec2::new(0., 0.1)));
}
+1 -1
View File
@@ -1372,7 +1372,7 @@ mod tests {
run(&mut s, "browser.visibility", json!({"items": ["KB (K:2)"], "visible": false}));
let r = run(&mut s, "body.rename", json!({"body": "KB (K:2)", "name": "Lid"}));
assert_eq!((r["body"].as_str(), r["name"].as_str()), (Some("KB"), Some("Lid")), "{r}");
let world = |s: &Session| s.world_state().bodies.iter().map(|b| b.name.clone()).collect::<Vec<_>>();
let world = |s: &Session| s.world_state().unwrap().bodies.iter().map(|b| b.name.clone()).collect::<Vec<_>>();
let renamed = world(&s);
assert!(renamed.iter().any(|n| n == "Lid") && renamed.iter().any(|n| n == "Lid (K:2)"), "{renamed:?}");
assert!(!renamed.iter().any(|n| n.starts_with("KB")), "{renamed:?}");
+12 -5
View File
@@ -325,15 +325,15 @@ mod tests {
[0]["id"]
.as_u64()
.unwrap();
let original = source.world_state().sketch(1).unwrap().plane;
let pasted = target.world_state().sketch(id).unwrap().plane;
let original = source.world_state().unwrap().sketch(1).unwrap().plane;
let pasted = target.world_state().unwrap().sketch(id).unwrap().plane;
assert!(pasted.origin.dist(original.origin) < 1e-6, "{original:?} vs {pasted:?}");
assert!(pasted.x.dist(original.x) < 1e-6);
assert!(pasted.y.dist(original.y) < 1e-6);
assert!((sketch_area(&target, id) - 200.).abs() < 1e-6);
assert_eq!(target.doc.feature(id).unwrap().component, component);
for (original, pasted) in
source.world_state().sketch(1).unwrap().sketch.wires.iter().zip(&target.world_state().sketch(id).unwrap().sketch.wires)
source.world_state().unwrap().sketch(1).unwrap().sketch.wires.iter().zip(&target.world_state().unwrap().sketch(id).unwrap().sketch.wires)
{
for (a, b) in original.pts.iter().chain(&original.fit).zip(pasted.pts.iter().chain(&pasted.fit)) {
assert!(a.dist(*b) < 1e-6, "wire {:?}: {a:?} vs {b:?}", original.space);
@@ -352,8 +352,15 @@ mod tests {
.unwrap();
let frame = target.placed_document().path_transform(component, &[occurrence]).unwrap();
let delta = solidcraft_doc::apply_vector(&frame, Vec3::new(2., 3., 4.));
for (original, pasted) in
source.world_state().sketch(1).unwrap().sketch.wires.iter().zip(&target.world_state().sketch(moved).unwrap().sketch.wires)
for (original, pasted) in source
.world_state()
.unwrap()
.sketch(1)
.unwrap()
.sketch
.wires
.iter()
.zip(&target.world_state().unwrap().sketch(moved).unwrap().sketch.wires)
{
for (a, b) in original.pts.iter().chain(&original.fit).zip(pasted.pts.iter().chain(&pasted.fit)) {
assert!((*a + delta).dist(*b) < 1e-6, "translated wire {:?}: {a:?} vs {b:?}", original.space);
+2 -2
View File
@@ -105,7 +105,7 @@ fn world_pick(
if p.get(pk).is_some() || p.get(fk).is_some() {
return Err(bad(cmd, format!("give `{pk}_plane` or `{pk}`/`{fk}`, not both")));
}
let world = s.world_state();
let world = s.world_state()?;
let pl = s.placed_document().resolve_plane_in(vals, pl_ref, 0, Some(&*world)).map_err(|e| bad(cmd, e.to_string()))?;
return Ok((pl.origin, Some(pl.normal())));
}
@@ -129,7 +129,7 @@ fn world_pick(
Some(Value::String(k)) if matches!(k.as_str(), "vertex" | "edge_mid" | "face_center" | "circle_center") => k.as_str(),
_ => return Err(bad(cmd, format!("`{pk}.snap` must be vertex, edge_mid, face_center or circle_center"))),
};
return super::super::features_more::snap_point(s, kind, at)
return super::super::features_more::snap_point(&*s.world_state()?, kind, at)
.map(|q| (q, None))
.ok_or_else(|| bad(cmd, format!("no {kind} to snap to near `{pk}`")));
}
@@ -20,7 +20,7 @@ fn design(name: &str) -> Session {
}
/// `s` copies its placement `path` for another design.
fn copied(s: &Session, path: Vec<u64>) -> Option<Arc<ForeignCopy>> {
Some(Arc::new(ForeignCopy::of(s, Vec::new(), vec![path])))
ForeignCopy::of(s, Vec::new(), vec![path]).ok().map(Arc::new)
}
/// A component copied in another design pastes as an independent copy of it and what it holds:
@@ -37,7 +37,7 @@ fn a_component_from_another_design_pastes_with_its_history() {
run(&mut source, "component.activate", json!({"component": 0}));
run(&mut source, "occurrence.ground", json!({"occurrence": at, "grounded": false}));
run(&mut source, "occurrence.move", json!({"occurrence": at, "translate": [0, 30, 0]}));
let was = source.world_state().body("Cube").unwrap().mesh().bounds();
let was = source.world_state().unwrap().body("Cube").unwrap().mesh().bounds();
let box_features = source.doc.features.len();
let mut s = design("Frame");
@@ -55,7 +55,7 @@ fn a_component_from_another_design_pastes_with_its_history() {
assert_eq!(s.doc.features.len(), box_features, "the box's history came along");
assert!((volume(&mut s, new) - 16.0).abs() < 1e-6);
assert_eq!(s.doc.appearances.bodies["Cube"].color, [0xab, 0xcd, 0xef]);
let is = s.world_state().body("Cube").unwrap().mesh().bounds();
let is = s.world_state().unwrap().body("Cube").unwrap().mesh().bounds();
assert!(was.min.dist(is.min) < 1e-6 && was.max.dist(is.max) < 1e-6, "{was:?} vs {is:?}");
// Its history is its own: changing it here leaves the original as it was.
let copy = s.doc.features.iter().find(|f| matches!(f.kind, solidcraft_doc::FeatureKind::Box { .. })).unwrap();
+3 -2
View File
@@ -221,7 +221,8 @@ fn design_bodies(file: &Path, bytes: &[u8], validation: solidcraft_kernel::Valid
} else {
(solidcraft_io::read_design(bytes).map_err(fail)?, None)
};
let (name, feature, faces, metadata, properties) = session_bodies(&Session::opened_at_validating(doc, saved.as_ref(), Some(&shown), validation), &shown)?;
let (name, feature, faces, metadata, properties) =
session_bodies(&Session::opened_at_validating(doc, saved.as_ref(), Some(&shown), validation), &shown)?;
remember_read(file, digest(bytes), &feature, &faces, &properties);
Ok((name, feature, faces, metadata, properties))
}
@@ -619,7 +620,7 @@ fn update_links(s: &mut Session, p: &Value) -> Result<Value> {
}
continue;
}
let (_, sf, faces, _, properties) = match design_bodies(&file, &bytes) {
let (_, sf, faces, _, properties) = match design_bodies(&file, &bytes, s.validation()) {
Ok(read) => read,
Err(e) if go_on => {
// It keeps its bodies, so it has not taken this file in yet.
@@ -312,6 +312,7 @@ fn block_and_part() -> (Session, u64) {
/// The lowest corner of every body in the world, rounded.
fn world_corners(s: &Session) -> Vec<Vec<f64>> {
s.world_state()
.unwrap()
.bodies
.iter()
.map(|b| {
+3 -3
View File
@@ -1247,7 +1247,7 @@ fn selection_item(s: &Session, value: Value) -> Result<Sel> {
point: Option<Vec3>,
}
let Pick { body, index, point } = serde_json::from_value(value).map_err(|e| bad("select.set", e.to_string()))?;
let state = s.world_state();
let state = s.world_state()?;
let b = state.body(&body).ok_or_else(|| bad("select.set", format!("no body `{body}`")))?;
let missing = || bad("select.set", format!("no {} of `{body}` at the given index or point", if face { "face" } else { "edge" }));
let (index, point) = match (index, point) {
@@ -1304,7 +1304,7 @@ fn select_set(s: &mut Session, p: &Value) -> Result<Value> {
// nothing, or the next body to take that name (#9737).
other => {
if let Some(name) = other.body_name()
&& s.world_state().body(name).is_none()
&& s.world_state()?.body(name).is_none()
{
return Err(bad("select.set", format!("no body `{name}`")));
}
@@ -1355,7 +1355,7 @@ fn select_same_look(s: &mut Session, p: &Value) -> Result<Value> {
_ => None,
})
.ok_or_else(|| bad("select.same_look", "select an object or face"))?;
let state = s.world_state();
let state = s.world_state()?;
let body = state.body(name).ok_or_else(|| bad("select.same_look", "unknown object"))?;
let face = request.face.or(match selected {
Some(Sel::Face { body, index, .. }) if request.body.is_none() && body == name => Some(*index),
+1 -1
View File
@@ -1491,7 +1491,7 @@ fn select_same_look_uses_resolved_looks_without_document_history_edits_and_refus
run(&mut s, "select.same_look", json!({"body":"A"}));
assert_eq!(s.selection.len(), 3);
let point = solidcraft_geom::Vec3::new(40.0, 5.0, 5.0);
let face = solidcraft_doc::appearance::face_index_at(s.world_state().body("C").unwrap(), point).unwrap();
let face = solidcraft_doc::appearance::face_index_at(s.world_state().unwrap().body("C").unwrap(), point).unwrap();
run(&mut s, "select.set", json!({"items":[Sel::Face {body:"C".into(),index:face,point}]}));
run(&mut s, "select.same_look", json!({}));
assert_eq!(s.selection.len(), 3);
+2 -2
View File
@@ -12,8 +12,8 @@ use solidcraft_geom::Vec3;
use super::{CommandSpec, not_in_sketch};
use crate::params::{UNKNOWN_COMMAND, bad, bool_, expr, expr_problem, req_expr, str_, string_list, string_list_or_default, vec2, vec3};
use crate::schema::features::{
BoundingSolidParams, BoxParams, ConeParams, CylinderParams, ExtrudeParams, FeatureNotice, FeatureResult, NoticeAction, OffsetPlaneParams, PipeParams,
PlaneAtAngleParams, ScaleParams, SphereParams, TorusParams,
BoundingSolidParams, BoxParams, ConeParams, CylinderParams, ExtrudeParams, FeatureNotice, FeatureResult, NoticeAction, OffsetPlaneParams,
PipeParams, PlaneAtAngleParams, ScaleParams, SphereParams, TorusParams,
};
use crate::schema::{TypedContract, output_schema_for};
use crate::{EngineError, Result, Session};
+2 -2
View File
@@ -738,7 +738,7 @@ fn export_laid_to(
quality: solidcraft_io::MeshQuality,
) -> Result<Value> {
let name = std::path::Path::new(path).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_else(|| s.doc.name.clone());
let mut state = s.doc.painted(&s.world_state());
let mut state = s.doc.painted(&*s.world_state()?);
let laid = print_face::lay_on_print_faces(s, &mut state, bodies, parts);
let bytes = solidcraft_io::export_with_parts(&state, bodies, parts, format, &name, &s.doc.metadata, quality)?;
solidcraft_io::vfs::write(path, &bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
@@ -765,7 +765,7 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
fn export_obj(s: &Session, path: &str, bodies: &[String], quality: solidcraft_io::MeshQuality) -> Result<Value> {
let mtl_path = std::path::Path::new(path).with_extension("mtl");
let mtl_name = mtl_path.file_name().map(|x| x.to_string_lossy().to_string()).unwrap_or_else(|| String::from("model.mtl"));
let files = solidcraft_io::export_obj(&s.doc.painted(&s.world_state()), bodies, &mtl_name, quality)?;
let files = solidcraft_io::export_obj(&s.doc.painted(&*s.world_state()?), bodies, &mtl_name, quality)?;
let mut targets: Vec<(String, &[u8])> = Vec::new();
if !files.mtl.is_empty() {
targets.push((mtl_path.to_string_lossy().to_string(), files.mtl.as_bytes()));
+3 -1
View File
@@ -71,7 +71,9 @@ impl crate::Ahead {
if inside && adds_nothing(&before, &state, &tools) {
return (Some(Addition::Inside), None);
}
after.world_state().warm_display();
if let Ok(world) = after.world_state() {
world.warm_display();
}
(Some(Addition::Joins), Some(crate::Evaluated(after.model)))
}
}
-1
View File
@@ -382,7 +382,6 @@ pub(super) fn checked_pairs(s: &Session, p: &Value, cmd: &str) -> Result<Contact
}
pub(super) fn interference(s: &mut Session, p: &Value) -> Result<Value> {
let s: &Session = s;
let cmd = "motion.interference";
let seconds = match p.get("budget_s") {
None | Some(Value::Null) => None,
+11 -11
View File
@@ -1566,7 +1566,7 @@ fn occurrence_align_moves_only_selected_instance_including_90_degree_face_mate()
);
assert_eq!(s.doc.features.len(), features_before, "aligning an occurrence must not add a body feature to the shared component");
let st = s.world_state();
let st = s.world_state().unwrap();
let b1 = st.body("Beam").unwrap().mesh().bounds();
assert!((b1.min.x - 0.0).abs() < 1e-5 && (b1.max.x - 10.0).abs() < 1e-5, "{b1:?}");
assert!((b1.min.y - 0.0).abs() < 1e-5 && (b1.max.y - 10.0).abs() < 1e-5, "{b1:?}");
@@ -1594,7 +1594,7 @@ fn occurrence_align_supports_snaps_planes_flip_nested_paths_and_pending_capture(
run(&mut s, "component.activate", json!({"component": "root"}));
// Align the child under a 90-deg rotated parent onto XY plane along plane normal only (capture: false).
let before = s.world_state().body("Plate").unwrap().mesh().bounds();
let before = s.world_state().unwrap().body("Plate").unwrap().mesh().bounds();
run(
&mut s,
"occurrence.align",
@@ -1605,13 +1605,13 @@ fn occurrence_align_supports_snaps_planes_flip_nested_paths_and_pending_capture(
"capture": false
}),
);
let moved = s.world_state().body("Plate").unwrap().mesh().bounds();
let moved = s.world_state().unwrap().body("Plate").unwrap().mesh().bounds();
assert!((moved.min.z - 0.0).abs() < 1e-5 && (moved.max.z - 4.0).abs() < 1e-5, "{moved:?}");
assert!((moved.center().x - before.center().x).abs() < 1e-5 && (moved.center().y - before.center().y).abs() < 1e-5, "{moved:?}");
// Revert drops the pending alignment; re-align with capture: true and vertex snap to origin.
run(&mut s, "component.revert_position", json!({}));
assert!((s.world_state().body("Plate").unwrap().mesh().bounds().min.z - 10.0).abs() < 1e-5);
assert!((s.world_state().unwrap().body("Plate").unwrap().mesh().bounds().min.z - 10.0).abs() < 1e-5);
run(
&mut s,
@@ -1623,7 +1623,7 @@ fn occurrence_align_supports_snaps_planes_flip_nested_paths_and_pending_capture(
"to": "origin"
}),
);
let at_origin = s.world_state().body("Plate").unwrap().mesh().bounds();
let at_origin = s.world_state().unwrap().body("Plate").unwrap().mesh().bounds();
assert!(at_origin.min.dist(solidcraft_geom::Vec3::ZERO) < 1e-5, "{at_origin:?}");
// Hostile / invalid parameters never panic.
@@ -1681,8 +1681,8 @@ fn a_body_copied_in_another_design_pastes_as_an_imported_body() {
let (_, holder) = created(&mut source, "Holder");
run(&mut source, "occurrence.move", json!({"occurrence": holder, "translate": [0,50,0]}));
run(&mut source, "solid.box", json!({"length": 2, "width": 2, "height": 2, "body_name": "Cube"}));
let was = source.world_state().body("Cube").unwrap().mesh().bounds();
let copy = crate::ForeignCopy::of(&source, vec!["Cube".into()], Vec::new());
let was = source.world_state().unwrap().body("Cube").unwrap().mesh().bounds();
let copy = crate::ForeignCopy::of(&source, vec!["Cube".into()], Vec::new()).unwrap();
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 1, "width": 1, "height": 1, "body_name": "Cube"}));
@@ -1694,7 +1694,7 @@ fn a_body_copied_in_another_design_pastes_as_an_imported_body() {
assert_eq!(out["bodies"], json!(["Cube (2)"]));
assert_eq!(out["feature"], "Import1");
assert_eq!(s.undo.len(), undo + 1);
let is = s.world_state().body("Cube (2)").unwrap().mesh().bounds();
let is = s.world_state().unwrap().body("Cube (2)").unwrap().mesh().bounds();
assert!(was.min.dist(is.min) < 1e-6 && was.max.dist(is.max) < 1e-6, "{was:?} vs {is:?}");
let state = s.model.state();
assert_eq!(s.doc.model_body_component(state.body("Cube (2)").unwrap()), a);
@@ -1716,13 +1716,13 @@ fn a_body_seen_in_another_placement_pastes_into_another_design_under_its_own_nam
run(&mut source, "occurrence.move", json!({"occurrence": second, "translate": [0,40,0]}));
let seen = source.placed_document().world_body_name(&source.model.state(), "Cube", holder, &[second]);
assert_ne!(seen, "Cube");
let was = source.world_state().body(&seen).unwrap().mesh().bounds();
let copy = crate::ForeignCopy::of(&source, vec![seen], Vec::new());
let was = source.world_state().unwrap().body(&seen).unwrap().mesh().bounds();
let copy = crate::ForeignCopy::of(&source, vec![seen], Vec::new()).unwrap();
assert_eq!(copy.names, vec!["Cube".to_string()]);
let mut s = Session { foreign_copy: Some(std::sync::Arc::new(copy)), ..Default::default() };
let out = run(&mut s, "component.paste_from_design", json!({}));
assert_eq!(out["bodies"], json!(["Cube"]));
let is = s.world_state().body("Cube").unwrap().mesh().bounds();
let is = s.world_state().unwrap().body("Cube").unwrap().mesh().bounds();
assert!(was.min.dist(is.min) < 1e-6 && was.max.dist(is.max) < 1e-6, "{was:?} vs {is:?}");
}
+2 -2
View File
@@ -799,7 +799,7 @@ mod tests {
// The profile: full width at the rim and up the flank (0.15 k = 0.50 high), drawing in
// over the crown (r = 4.62 halfway up the head), and a flat top of 0.7 s = 2.8, which
// leaves a ring of material around the socket's corners at r = 2.31.
let st = s.world_state();
let st = s.world_state().unwrap();
let widest = |above: f64| {
st.bodies.iter().flat_map(|b| b.mesh().positions.clone()).filter(|p| p.z > above).map(|p| p.x.hypot(p.y)).fold(0.0, f64::max)
};
@@ -893,7 +893,7 @@ mod tests {
assert_eq!(r["joint"], r["joints"][0]);
// Each placement sits in its own hole: the head flush on the top face, the shank down
// the hole's axis.
let st = s.world_state();
let st = s.world_state().unwrap();
let mut seated: Vec<(f64, f64)> = Vec::new();
for b in st.bodies.iter().filter(|b| s.doc.model_body_component(b) == comp) {
let (lo, hi) = (b.mesh().bounds().min, b.mesh().bounds().max);
@@ -955,7 +955,7 @@ fn appearance_reads_and_scopes_take_a_placed_copys_name() {
s.execute("component.create", &json!({"name":"K"})).unwrap();
s.execute("solid.box", &json!({"body_name":"KB","length":10,"width":10,"height":10})).unwrap();
s.execute("occurrence.copy", &json!({"component":"K","translate":[30,0,0]})).unwrap();
let world: Vec<String> = s.world_state().bodies.iter().map(|b| b.name.clone()).collect();
let world: Vec<String> = s.world_state().unwrap().bodies.iter().map(|b| b.name.clone()).collect();
assert!(world.iter().any(|n| n == "KB (K:2)"), "{world:?}");
s.execute("appearance.assign", &json!({"bodies":["KB"],"appearance":"ABS - Red"})).unwrap();
s.execute("appearance.assign", &json!({"components":["K"],"appearance":"ABS - Blue"})).unwrap();
+5 -5
View File
@@ -31,7 +31,7 @@ fn print_face(s: &mut Session, p: &Value) -> Result<Value> {
None | Some(Value::Null) => None,
Some(v) => Some(crate::params::vec3(v).ok_or_else(|| bad(cmd, "`point` must be [x, y, z] of numbers"))?),
};
let world = s.world_state();
let world = s.world_state()?;
let body = world.body(name).ok_or_else(|| bad(cmd, format!("no body `{name}`")))?;
let key = body.definition_name().to_string();
let face = match point {
@@ -189,7 +189,7 @@ mod tests {
/// Lay every body of `s` on its print face, as a slicer export does: Block's box and centre
/// of mass afterwards, and what was reported.
fn laid(s: &Session) -> (solidcraft_geom::Aabb3, Vec3, Laid) {
let mut state = s.doc.painted(&s.world_state());
let mut state = s.doc.painted(&s.world_state().unwrap());
let laid = lay_on_print_faces(s, &mut state, &[], &[]);
let block = state.bodies.iter().find(|b| b.name == "Block").expect("Block");
let m = solidcraft_kernel::measure_mass(&block.body).expect("measure");
@@ -206,7 +206,7 @@ mod tests {
// turning one of them on its own would pull it out of the others, so it stays as it lies.
let mut s = block();
run(&mut s, "body.print_face", json!({"body": "Block", "point": [20, 5, 2.5]}));
let mut state = s.doc.painted(&s.world_state());
let mut state = s.doc.painted(&s.world_state().unwrap());
let parts = [("Bracket".to_string(), vec!["Block".to_string(), "Other".to_string()])];
let laid = lay_on_print_faces(&s, &mut state, &[], &parts);
assert!(laid.bodies.is_empty(), "{:?}", laid.bodies);
@@ -306,7 +306,7 @@ mod tests {
/// Every body laid on its print face: (name, box, centre of mass) of those laid.
fn all_laid(s: &Session) -> Vec<(String, solidcraft_geom::Aabb3, Vec3)> {
let mut state = s.doc.painted(&s.world_state());
let mut state = s.doc.painted(&s.world_state().unwrap());
let laid = lay_on_print_faces(s, &mut state, &[], &[]);
assert!(laid.warnings.is_empty(), "{:?}", laid.warnings);
laid.bodies
@@ -341,7 +341,7 @@ mod tests {
run(&mut s, "solid.mirror", json!({"bodies": ["Block"], "plane": "YZ"}));
assert_eq!(s.doc.print_faces.len(), 2, "{:?}", s.doc.print_faces.keys().collect::<Vec<_>>());
let copy = s.doc.print_faces.keys().find(|k| *k != "Block").expect("the copy's print face").clone();
let world = s.world_state();
let world = s.world_state().unwrap();
let body = world.body(&copy).expect("copy");
let face = s.doc.print_face(&world, body).expect("marked").expect("found");
assert!(near(face.point.x, -20.) && near(face.normal.x, -1.), "{face:?}");
@@ -458,13 +458,13 @@ fn wire_frame_copies_keep_component_and_legacy_coordinates() {
assert_eq!(sk.wires[3].space, WireSpace::World);
near(sk.wires[2].arc.unwrap().center, Vec3::new(1.6, 0.0, 5.0));
near(sk.wires[3].arc.unwrap().center, Vec3::new(20.0, 31.6, 45.0));
for wire in s.world_state().sketch(sketch).unwrap().sketch.wires.iter().filter(|w| copied.contains(&w.id)) {
for wire in s.world_state().unwrap().sketch(sketch).unwrap().sketch.wires.iter().filter(|w| copied.contains(&w.id)) {
near(wire.arc.unwrap().center, Vec3::new(20.0, 31.6, 45.0));
near(wire.fit[0], Vec3::new(10.0, 31.6, 45.0));
}
let component = s.active_component;
run(&mut s, "component.activate", json!({"component":component,"occurrence":first,"path":[first]}));
let world = s.world_state();
let world = s.world_state().unwrap();
let wires = &world.sketch(sketch).unwrap().sketch.wires;
near(wires.iter().find(|w| w.id == copied[0]).unwrap().arc.unwrap().center, Vec3::new(1.6, 0.0, 5.0));
near(wires.iter().find(|w| w.id == copied[1]).unwrap().arc.unwrap().center, Vec3::new(20.0, 31.6, 45.0));
+3 -3
View File
@@ -799,7 +799,7 @@ fn a_sketch_on_the_mirrored_half_of_a_combined_body_sits_on_that_half() {
run(&mut s, "solid.mirror", json!({"bodies":["Part"],"plane":"XY","combine":true,"name":"Mirror1"}));
assert_eq!(s.model.state().bodies.len(), 1, "the mirror combined");
let pick = |s: &Session, normal: Vec3, point: Vec3| {
let world = s.world_state();
let world = s.world_state().unwrap();
let body = world.body("Part").unwrap();
let face = body
.body
@@ -833,7 +833,7 @@ fn a_sketch_on_the_ring_of_a_lid_underside_parted_by_its_lip_keeps_the_cap() {
let mut s = Session::default();
let script: Value = serde_json::from_str(include_str!("../../../../examples/acceptance/sensor_enclosure.json")).unwrap();
s.run_script(&script).unwrap();
let world = s.world_state();
let world = s.world_state().unwrap();
let lid = world.body("Lid").unwrap();
let names = solidcraft_doc::naming::face_names(lid);
let down: Vec<usize> = (0..names.len())
@@ -881,7 +881,7 @@ fn activate_part_makes_the_sketch_in_the_faces_component() {
assert_eq!(s.active_component, 0);
assert!(out.get("activated").is_none());
}
let world = s.world_state();
let world = s.world_state().unwrap();
let plane = world.sketch(id).unwrap().plane;
assert!((plane.origin.z - 10.).abs() < 1e-6 && plane.normal().z > 1. - 1e-9, "{activate}: {plane:?}");
draw(&mut s);
+1 -1
View File
@@ -445,7 +445,7 @@ fn center_of_mass(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "inspect.center_of_mass";
let names = string_list(cmd, p, "bodies")?;
// The world state, as Measure reads: placed occurrences where they are, each placement once.
let st = s.world_state();
let st = s.world_state()?;
let mut out = Vec::new();
let (mut vol, mut moment) = (0.0, Vec3::ZERO);
let mut skipped = Vec::new();
+1 -1
View File
@@ -3098,7 +3098,7 @@ fn drawn_copy_selection(s: &Session, p: &Value, ids: Vec<String>) -> Result<(Vec
}
let plane = match wire.space {
WireSpace::Component => s.model.state().sketch(id).map(|sk| sk.plane),
WireSpace::World => s.world_state().sketch(id).map(|sk| sk.plane),
WireSpace::World => s.world_state()?.sketch(id).map(|sk| sk.plane),
}
.ok_or_else(|| bad(cmd, "sketch plane is unavailable"))?;
drawn.push((wire.clone(), plane));
+2 -2
View File
@@ -2717,7 +2717,7 @@ fn a_tangent_cap_follows_its_offset_when_the_source_length_changes() {
// endpoint first makes every length change look contradictory, before the offset can move.
let mut s = capped_line_offset(false);
let check = |s: &Session, height: f64| {
let world = s.world_state();
let world = s.world_state().unwrap();
let solved = world.sketch(1).unwrap();
assert!(solved.report.keeps_orientation(), "{:?}", solved.report);
assert!(solved.report.fully_constrained());
@@ -2822,7 +2822,7 @@ fn a_cap_on_an_offset_arc_keeps_its_radius_through_large_length_edits() {
let params =
if batch { json!({"changes":{"height":height.to_string()}}) } else { json!({"name":"height","expression":height.to_string()}) };
run(&mut s, "parameters.change", params);
let world = s.world_state();
let world = s.world_state().unwrap();
let solved = world.sketch(1).unwrap();
assert!(solved.report.keeps_orientation() && solved.report.fully_constrained(), "{:?}", solved.report);
for (p, before) in solved.sketch.points.iter().zip(&initial.points) {
+3 -3
View File
@@ -3,7 +3,7 @@
//! continuity seams, isocurves and curvature combs) are one group: one at a time is on, and
//! `inspect.analysis` says which or turns it off.
use serde_json::Value;
use serde_json::{Value, json};
use solidcraft_geom::Vec3;
use super::CommandSpec;
@@ -1169,12 +1169,12 @@ mod tests {
let revision = s.revision;
assert!(!s.continuity_details()[0].current);
assert_eq!(s.revision, revision, "descriptor does not prepare a stale world or mutate it");
let _placed = s.world_state();
let _placed = s.world_state().unwrap();
assert!(!s.continuity_details()[0].current, "a warmed moved occurrence is still different geometry placement");
assert!(s.continuity_target_points(&record.body, record.edge).is_none());
s.execute("component.revert_position", &json!({"occurrences":[occurrence]})).unwrap();
assert!(!s.pending_moves.contains_key(&occurrence));
let _restored = s.world_state();
let _restored = s.world_state().unwrap();
assert!(s.continuity_details()[0].current, "matching restored placement is validated by real geometry identity");
}
+4 -4
View File
@@ -247,14 +247,14 @@ pub struct ForeignCopy {
impl ForeignCopy {
/// What `session` copies when `bodies` and `placements` are copied in it now.
pub fn of(session: &Session, bodies: Vec<String>, placements: Vec<Vec<u64>>) -> ForeignCopy {
pub fn of(session: &Session, bodies: Vec<String>, placements: Vec<Vec<u64>>) -> Result<ForeignCopy> {
let frames = session.placed_document().body_frames_with_source(&session.model.state());
let names = bodies.iter().map(|b| frames.iter().find(|f| f.0 == *b).map_or_else(|| b.clone(), |f| f.1.clone())).collect();
let placed = placements.iter().filter_map(|p| session.occurrence_placement(p).ok()).map(|p| (p.component, p.occurrence, p.world)).collect();
ForeignCopy {
Ok(ForeignCopy {
design: session.doc.name.clone(),
features: Vec::new(),
world: session.world_state(),
world: session.world_state()?,
bodies,
names,
placements,
@@ -262,7 +262,7 @@ impl ForeignCopy {
doc: session.doc.clone(),
model: session.model.clone(),
pending_moves: session.pending_moves.clone(),
}
})
}
/// Timeline Copy's snapshot. No placed body geometry is needed to paste sketches.
+17 -16
View File
@@ -399,17 +399,10 @@ impl Session {
/// it is shown (uncaptured moves and the exploded view included).
/// The placement index answers from lookups; the plain walk stays as the fallback and the error reporter.
pub fn occurrence_placement(&self, path: &[u64]) -> Result<OccurrencePlacement> {
self.occurrence_placement_with(path, &self.pending_moves)
}
/// [`Session::occurrence_placement`] with the occurrences moved by `moves` instead of
/// `pending_moves` (the design's own, [`Session::uncaptured_moves`], for what reports the
/// design's placements).
pub fn occurrence_placement_with(&self, path: &[u64], moves: &std::collections::BTreeMap<u64, Mat>) -> Result<OccurrencePlacement> {
if path.is_empty() || path.len() > 64 {
return Err(invalid("occurrence path must contain 1..64 ids"));
}
if let Some(placement) = self.occurrence_placement_indexed(path) {
if !path.is_empty()
&& path.len() <= 64
&& let Some(placement) = self.occurrence_placement_indexed(path)
{
return Ok(placement);
}
self.occurrence_placement_walk(path)
@@ -469,6 +462,13 @@ impl Session {
/// The plain walk over `doc.occurrences`, kept as the fallback for paths the index can't
/// answer and as the reference the indexed answers are checked against (#7502).
pub(crate) fn occurrence_placement_walk(&self, path: &[u64]) -> Result<OccurrencePlacement> {
self.occurrence_placement_with(path, &self.pending_moves)
}
/// [`Session::occurrence_placement`] with the occurrences moved by `moves` instead of
/// `pending_moves` (the design's own, [`Session::uncaptured_moves`], for what reports the
/// design's placements). The placement index holds `pending_moves`, so this walks the path.
pub fn occurrence_placement_with(&self, path: &[u64], moves: &std::collections::BTreeMap<u64, Mat>) -> Result<OccurrencePlacement> {
if path.is_empty() || path.len() > 64 {
return Err(invalid("occurrence path must contain 1..64 ids"));
}
@@ -972,11 +972,12 @@ impl Session {
// A selected body that is gone cannot be deleted, measured or shown: drop it, so a
// later body taking its name is never deleted in its place (#9737).
if self.selection.iter().any(|s| s.body_name().is_some()) {
let world = self.world_state();
self.selection.retain(|s| match s.body_name() {
Some(name) => world.body(name).is_some(),
None => true,
});
if let Ok(world) = self.world_state() {
self.selection.retain(|s| match s.body_name() {
Some(name) => world.body(name).is_some(),
None => true,
});
}
}
let hidden = std::mem::take(&mut self.visibility.hidden_occurrences);
self.visibility.hidden_occurrences = hidden.into_iter().filter(|p| self.occurrence_placement(p).is_ok()).collect();
+2 -2
View File
@@ -27,7 +27,7 @@ fn a_sketch_on_either_end_of_a_rounded_loft_takes_the_whole_face() {
let mut s = Session::default();
rounded_loft(&mut s);
for up in [-1.0, 1.0] {
let world = s.world_state();
let world = s.world_state().unwrap();
let loft = world.body("Loft").unwrap();
let face = loft.body.faces(0.01).unwrap().into_iter().find(|f| f.plane_normal.is_some_and(|n| n.dot(Vec3::Z * up) > 0.999)).unwrap();
let pick = json!({"body": "Loft", "index": face.index, "face": [face.centroid.x, face.centroid.y, face.centroid.z]});
@@ -37,7 +37,7 @@ fn a_sketch_on_either_end_of_a_rounded_loft_takes_the_whole_face() {
run(&mut s2, "solid.extrude", json!({"sketch": id, "distance": 5, "operation": "new", "body_name": "Pad"}));
let state = s2.model.state();
let pad = state.bodies.iter().find(|b| b.name == "Pad").unwrap();
let v = pad.body.tessellate(1e-2).unwrap().measure().volume;
let v = pad.body.mesh(&crate::kernel::MeshTolerance::chord(1e-2)).unwrap().measure().volume;
// The projected outline fits the round with splines, so it is close to the face, not exact.
assert!(rel(v, face.area * 5.0) < 1e-2, "end {up}: {v} vs {}", face.area * 5.0);
}
+1 -1
View File
@@ -814,7 +814,7 @@ fn build_scene(
let fade = GridFade::new(cam.half_height(), slant, ext, far_reach);
// The grid's centre on each axis, from the plane's origin (the world's, or the sketch
// plane's): a level fading in near a switch of the spacing lies on its own places (#9570).
let origin = open_sketch_plane(s).map_or(Vec3::ZERO, |pl| pl.origin);
let origin = open_sketch_plane(s)?.map_or(Vec3::ZERO, |pl| pl.origin);
let (at_u, at_v) = ((c - origin).dot(u), (c - origin).dot(v));
// Two lines through the point `c + u * t`: one along v, one along u.
for (along, across, at) in [(v, u, at_u), (u, v, at_v)] {
+2 -2
View File
@@ -140,14 +140,14 @@ fn the_grid_cross_fades_between_levels_as_the_view_zooms() {
#[test]
fn the_headless_grid_draws_both_levels_at_a_switch() {
let s = Session::default();
let mut cam = home_camera(&s);
let mut cam = home_camera(&s).unwrap();
cam.pitch = std::f64::consts::FRAC_PI_2;
cam.target = Vec3::ZERO;
let strength = |hh: f64, x: f64| {
let mut cam = cam;
cam.distance *= hh / cam.half_height();
assert!((cam.half_height() - hh).abs() < 1e-6);
let scene = scene(&s, &cam);
let scene = scene(&s, &cam).unwrap();
assert!(scene.grid.iter().all(|l| [Palette::DEFAULT.grid, Palette::DEFAULT.grid_major].contains(&l.color)));
// The strongest piece of the line in each grid colour, added: a line two levels share
// with different kinds is drawn in both.
@@ -17,7 +17,7 @@ fn geometry_independent_moves_do_not_evaluate_the_timeline() {
let before = session.evaluations;
run(&mut session, "occurrence.move", json!({"occurrence":occurrence,"translate":[10,0,0],"capture":true}));
assert_eq!(session.evaluations - before, 0, "a placement-independent model must not enter timeline evaluation");
let world = session.world_state();
let world = session.world_state().unwrap();
let block = world.bodies.iter().find(|b| b.source_name.as_deref() == Some("Block")).unwrap();
assert!(block.mesh().bounds().min.dist(Vec3::new(10., 0., 0.)) < 1e-9);
assert!(Arc::ptr_eq(&definitions, &session.model.state()));
@@ -34,7 +34,7 @@ fn geometry_independent_moves_do_not_evaluate_the_timeline() {
assert_eq!(scratch.evaluations, 0);
assert!(Arc::ptr_eq(&definitions, &scratch.model.state()));
assert_fresh_world(&scratch);
assert!(session.world_state().bodies[0].mesh().bounds().min.dist(Vec3::new(10., 0., 0.)) < 1e-9);
assert!(session.world_state().unwrap().bodies[0].mesh().bounds().min.dist(Vec3::new(10., 0., 0.)) < 1e-9);
}
fn assert_fresh_world(session: &Session) {
@@ -42,8 +42,8 @@ fn assert_fresh_world(session: &Session) {
fresh.no_cache = true;
fresh.evaluate(&session.doc);
assert!(fresh.results.iter().all(|r| r.error.is_none()));
let expected = crate::doc::world_state(&session.doc, &fresh.state());
let actual = session.world_state();
let expected = crate::doc::world_state(&session.doc, &fresh.state()).unwrap();
let actual = session.world_state().unwrap();
assert_eq!(actual.bodies.len(), expected.bodies.len());
for (a, b) in actual.bodies.iter().zip(&expected.bodies) {
assert_eq!(a.name, b.name);
@@ -450,19 +450,19 @@ fn body_frames_are_kept_with_the_world_state() {
let first = run(&mut session, "component.create", json!({"name":"Part"}))["occurrence"].as_u64().unwrap();
run(&mut session, "solid.box", json!({"length":2,"width":3,"height":4,"body_name":"Block","operation":"new"}));
let second = run(&mut session, "occurrence.copy", json!({"occurrence":first}))["occurrence"].as_u64().unwrap();
let _ = session.world_state();
let _ = session.world_state().unwrap();
let frames = session.body_frames();
assert!(Arc::ptr_eq(&frames, &session.body_frames()), "kept while the world state is current");
let copy = session.occurrence_placement(&[second]).unwrap().bodies[0].world.clone();
assert!(frames.iter().any(|(world, own, _)| *world == copy && own == "Block"), "a copy maps back to its definition");
run(&mut session, "occurrence.move", json!({"occurrence":second,"translate":[10,0,0],"capture":false}));
let _ = session.world_state();
let _ = session.world_state().unwrap();
let moved = session.body_frames();
assert!(!Arc::ptr_eq(&frames, &moved), "a moved placement works them out again");
let at = |frames: &crate::BodyFrames| frames.iter().find(|(world, ..)| *world == copy).map(|(_, _, m)| m[3][0]);
assert_eq!(at(&moved), Some(10.0), "with the placement's new frame");
assert_eq!(at(&frames), Some(0.0));
run(&mut session, "solid.box", json!({"length":1,"width":1,"height":1,"body_name":"Other","operation":"new"}));
let _ = session.world_state();
let _ = session.world_state().unwrap();
assert!(session.body_frames().iter().any(|(world, ..)| world.starts_with("Other")), "a new body is in them");
}
+1 -1
View File
@@ -17,7 +17,7 @@ use solidcraft_render::{AnalysisMode, Camera, Rgb, SECTION_CAP, Scene, SceneMesh
use wgpu::util::DeviceExt;
use crate::view::{AccessMap, DrawStats, Frame, GpuScene, GpuTarget, Palette, PaletteIndex, Renderer, analysis_colors, analysis_uniform};
use crate::{GpuError, Result, caught, error_scopes, popped};
use crate::{GpuError, Result, caught, error_scopes};
/// The colour format pictures are drawn in: sRGB bytes as they are, like the viewport's usual
/// (non-sRGB) target.
+13 -10
View File
@@ -738,7 +738,7 @@ pub struct BlockBuilder {
hidden: Vec<u8>,
bodies: Vec<BodySpan>,
/// The body being written: its first vertex and the box so far.
open: Option<(u32, [[f32; 3]; 2])>,
spanning: Option<(u32, [[f32; 3]; 2])>,
}
impl BlockBuilder {
@@ -746,12 +746,12 @@ impl BlockBuilder {
/// it can be left out of a view it is not in).
pub fn begin_body(&mut self) {
let at = u32::try_from(self.vertices.len() / TRI_SIZE).unwrap_or(u32::MAX);
self.open = Some((at, [[f32::INFINITY; 3], [f32::NEG_INFINITY; 3]]));
self.spanning = Some((at, [[f32::INFINITY; 3], [f32::NEG_INFINITY; 3]]));
}
/// End the body [`BlockBuilder::begin_body`] began.
pub fn end_body(&mut self) {
let Some((start, bounds)) = self.open.take() else { return };
let Some((start, bounds)) = self.spanning.take() else { return };
let end = u32::try_from(self.vertices.len() / TRI_SIZE).unwrap_or(u32::MAX);
if end > start && bounds.iter().flatten().all(|x| x.is_finite()) {
self.bodies.push(BodySpan { vertices: start..end, bounds });
@@ -775,7 +775,7 @@ impl BlockBuilder {
}
}
self.vertices.extend_from_slice(&record);
if let Some((_, [lo, hi])) = &mut self.open {
if let Some((_, [lo, hi])) = &mut self.spanning {
for v in p {
for k in 0..3 {
lo[k] = lo[k].min(v[k]);
@@ -1489,20 +1489,20 @@ impl BlockGpu {
/// Draw `class`'s triangles with the pipeline already set (`tally`: what is counted).
fn draw(&self, pass: &mut wgpu::RenderPass<'_>, class: TriClass, tally: &Tally) {
self.draw_runs(pass, self.class(class), tally);
self.draw_runs(pass, self.class(class), self.culled.as_ref().and_then(|c| c.get(class as usize)), tally);
}
/// Draw the closed bodies' opaque triangles ([`BlockBytes::closed`]), as [`BlockGpu::draw`].
fn draw_closed(&self, pass: &mut wgpu::RenderPass<'_>, tally: &Tally) {
self.draw_runs(pass, self.closed.as_ref(), tally);
self.draw_runs(pass, self.closed.as_ref(), None, tally);
}
fn draw_runs(&self, pass: &mut wgpu::RenderPass<'_>, runs: Option<&ClassGpu>, tally: &Tally) {
fn draw_runs(&self, pass: &mut wgpu::RenderPass<'_>, runs: Option<&ClassGpu>, culled: Option<&Vec<std::ops::Range<u32>>>, tally: &Tally) {
match runs {
Some(ClassGpu::Runs(runs)) => {
let Some(vertices) = &self.vertices else { return };
pass.set_vertex_buffer(0, vertices.slice(..));
let runs = self.culled.as_ref().map_or(&runs[..], |c| &c[class as usize][..]);
let runs = culled.map_or(&runs[..], |c| &c[..]);
for r in runs.iter() {
pass.draw(r.clone(), 0..1);
tally.triangles(r.len() as u32, 1);
@@ -2816,10 +2816,13 @@ impl Renderer {
// one drawn paint the cut, other bodies behind it included (their colours and outlines
// showed through it, #6328).
let cap = |source: TriSource| {
cut_states.as_ref().map(|(_, _, _, cap)| solid_writes("sc_tris_cap", "fs_solid_cut", Some(wgpu::Face::Front), Some(cap.clone()), source, false))
cut_states
.as_ref()
.map(|(_, _, _, cap)| solid_writes("sc_tris_cap", "fs_solid_cut", Some(wgpu::Face::Front), Some(cap.clone()), source, false))
};
type Marks = (wgpu::RenderPipeline, wgpu::RenderPipeline, wgpu::RenderPipeline);
let cut = |marks: Option<Marks>, cap: Option<wgpu::RenderPipeline>| marks.zip(cap).map(|((count, fill, rest), cap)| Cut { count, fill, cap, rest });
let cut =
|marks: Option<Marks>, cap: Option<wgpu::RenderPipeline>| marks.zip(cap).map(|((count, fill, rest), cap)| Cut { count, fill, cap, rest });
let access_bind = access_texture(device, None, &access_bgl, 1, &[]);
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("sc_image"),
+4 -4
View File
@@ -129,8 +129,8 @@ fn the_renderer_counts_what_it_draws() {
fn pictures_drawn_at_once_each_count_their_own() {
let Some(c) = capturer() else { return };
let scene = |s: &Session| {
let cam = view::home_camera(s);
(view::scene_with_guides(s, &cam, None, Guides { grid: false, axes: false }), cam)
let cam = view::home_camera(s).unwrap();
(view::scene_with_guides(s, &cam, None, Guides { grid: false, axes: false }).unwrap(), cam)
};
let empty = scene(&Session::default());
let solid = scene(&session(&[("solid.box", json!({"length": 40, "width": 30, "height": 20}))]));
@@ -330,12 +330,12 @@ fn zebra_on_flat_faces_holds_still_while_the_view_turns() {
let Some(c) = capturer() else { return };
let mut s = session(&[("solid.box", json!({"length": 60, "width": 40, "height": 20}))]);
s.execute("inspect.zebra", &json!({})).unwrap();
let home = view::home_camera(&s);
let home = view::home_camera(&s).unwrap();
let whites: Vec<usize> = (0..24)
.map(|step| {
let mut cam = home;
cam.yaw += f64::from(step) * 0.11_f64.to_radians();
let img = c.rgba(&view::scene_with_guides(&s, &cam, None, Guides { grid: false, axes: false }), &cam, 320, 200).unwrap();
let img = c.rgba(&view::scene_with_guides(&s, &cam, None, Guides { grid: false, axes: false }).unwrap(), &cam, 320, 200).unwrap();
pixels(&img).filter(|p| p[0] > 225 && p[1] > 225 && p[2] > 225).count()
})
.collect();
+5 -4
View File
@@ -328,10 +328,11 @@ mod planar_tests {
(22.5, 10.0, None),
(25.0, 10.0, None),
] {
let a = crate::cylinder(Vec3::new(12.5, 5.0, 5.0) * k, Vec3::Z, 2.5 * k, 17.5 * k).unwrap();
let b = crate::box_solid(Vec3::new(10.0, 5.0, lo) * k, Vec3::new(22.5, 10.0, lo + height) * k).unwrap();
let a = crate::cylinder(solidcraft_geom::Vec3::new(12.5, 5.0, 5.0) * k, solidcraft_geom::Vec3::Z, 2.5 * k, 17.5 * k).unwrap();
let b =
crate::box_solid(solidcraft_geom::Vec3::new(10.0, 5.0, lo) * k, solidcraft_geom::Vec3::new(22.5, 10.0, lo + height) * k).unwrap();
for (lhs, rhs) in [(&a, &b), (&b, &a)] {
let result = simple_prism_boolean(lhs, rhs, BoolOp::Intersect).expect("the exact prism route applies").unwrap();
let result = crate::boolean(lhs, rhs, BoolOp::Intersect).unwrap();
if let Some((bottom, top)) = expected {
let body = result.expect("a positive-height half disc is a solid");
assert!(body.validity().is_empty());
@@ -339,7 +340,7 @@ mod planar_tests {
let expected = std::f64::consts::PI * (2.5 * k).powi(2) * (top - bottom) * k / 2.0;
let actual = crate::measure(&body).unwrap().volume;
assert!((actual - expected).abs() < expected * 2e-3, "{lo} {height} {k}: {actual} != {expected}");
let bounds = body.tessellate(k * 1e-3).unwrap().bounds();
let bounds = body.mesh(&crate::MeshTolerance::chord(k * 1e-3)).unwrap().bounds();
assert!((bounds.min.z - bottom * k).abs() < k * 1e-7);
assert!((bounds.max.z - top * k).abs() < k * 1e-7);
} else {
+2 -2
View File
@@ -98,7 +98,7 @@ impl Sketch {
/// `equation` kept beside it. Returns the curve's index.
pub fn add_equation_curve(&mut self, at: &[Vec2], closed: bool, mut equation: EquationCurve) -> Result<usize> {
let pts = self.equation_points(at, closed, None)?;
let curve = self.add_curve(CurveKind::Spline { pts, control: false, degree: 3, periodic: closed }, None)?;
let curve = self.add_curve(CurveKind::Spline { pts, control: false, degree: 3, periodic: closed, nurbs: None }, None)?;
equation.curve = self.curves.get(curve).map(|c| c.id.clone()).ok_or_else(|| SketchError::Invalid("the new curve is missing".into()))?;
self.equations.push(equation);
Ok(curve)
@@ -119,7 +119,7 @@ impl Sketch {
let pts = self.equation_points(at, closed, Some((first, keep_end)))?;
let unused: Vec<usize> = old.iter().copied().filter(|p| !pts.contains(p)).collect();
if let Some(c) = self.curves.get_mut(curve) {
c.kind = CurveKind::Spline { pts, control: false, degree: 3, periodic: closed };
c.kind = CurveKind::Spline { pts, control: false, degree: 3, periodic: closed, nurbs: None };
}
// The old points no other curve uses (the in-between points, and an end a closed curve
// no longer has) go, with the constraints on them.
+3 -3
View File
@@ -1112,7 +1112,7 @@ fn wave_spline(sk: &mut Sketch, periodic: bool) -> (Vec<usize>, usize) {
pts.pop();
pts.push(pts[0]);
}
let s = sk.add_curve(CurveKind::Spline { pts: pts.clone(), control: false, degree: 3, periodic }, None).unwrap();
let s = sk.add_curve(CurveKind::Spline { pts: pts.clone(), control: false, degree: 3, periodic, nurbs: None }, None).unwrap();
(pts, s)
}
@@ -1152,7 +1152,7 @@ fn a_fit_point_inserted_into_a_spline_keeps_the_curve_through_its_points() {
assert!(k.insert_spline_point(s, old[2]).is_err());
assert_eq!(k, sk, "a refused insertion changes nothing");
let cv: Vec<usize> = old.iter().map(|q| k.add_point(*q + v(0.0, 30.0), None).unwrap()).collect();
let c = k.add_curve(CurveKind::Spline { pts: cv, control: true, degree: 3, periodic: false }, None).unwrap();
let c = k.add_curve(CurveKind::Spline { pts: cv, control: true, degree: 3, periodic: false, nurbs: None }, None).unwrap();
assert!(k.insert_spline_point(c, v(20.0, 38.0)).is_err());
let l = k.add_line(v(0.0, -10.0), v(50.0, -10.0), None, None, None).unwrap();
assert!(k.insert_spline_point(l, v(20.0, -10.0)).is_err());
@@ -1502,7 +1502,7 @@ fn rounded_rectangles_sharing_their_sides_leave_every_profile_closed() {
}
let mut ids: Vec<usize> = pts.iter().map(|p| sk.add_point(*p, None).unwrap()).collect();
ids.push(ids[0]);
sk.add_curve(CurveKind::Spline { pts: ids, control: true, degree: 3, periodic: false }, None).unwrap();
sk.add_curve(CurveKind::Spline { pts: ids, control: true, degree: 3, periodic: false, nurbs: None }, None).unwrap();
}
let mut sk = Sketch::new();
rounded(&mut sk, 0.55);
+3 -3
View File
@@ -63,7 +63,7 @@ fn f2_on_a_clicked_browser_group_renames_the_group() {
.unwrap();
h.app.preferences.browser_bodies_folders = crate::prefs::BodiesFolders::Always;
h.settle();
let world_body = h.app.session.world_state().bodies[0].name.clone();
let world_body = crate::open_meshing::session_world(&h.app.session).bodies[0].name.clone();
h.app.run("select.set", json!({"items":[Sel::Body { name: world_body.clone() }]})).unwrap();
h.step(&json!({"click":{"handle":"row:Group (1)"}})).unwrap();
h.step(&json!({"key":"F2"})).unwrap();
@@ -81,7 +81,7 @@ fn f2_on_a_clicked_browser_group_renames_the_group() {
assert!(h.app.menu.rename.is_none());
let names: Vec<&str> = h.app.session.doc.browser_groups.iter().map(|g| g.name.as_str()).collect();
assert_eq!(names, ["Fasteners"], "component {inside_component}: Enter renames the group; status {:?}", h.app.status);
assert!(h.app.session.world_state().body(&world_body).is_some(), "the body keeps its name");
assert!(crate::open_meshing::session_world(&h.app.session).body(&world_body).is_some(), "the body keeps its name");
}
}
@@ -679,7 +679,7 @@ fn mesh_and_solid_body_rows_have_different_icons_and_tooltips() {
use solidcraft_engine::doc::{Document, FeatureKind, MeshData};
use solidcraft_engine::geom::Vec3;
let solid = solidcraft_engine::kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap();
let mesh = solid.tessellate(0.01).unwrap();
let mesh = solid.mesh(&solidcraft_engine::kernel::MeshTolerance::chord(0.01)).unwrap();
let mut doc = Document::default();
doc.add_feature(
FeatureKind::MeshImport {
+1 -1
View File
@@ -209,7 +209,7 @@ fn render_browser_swatch_resolves_face_body_component_then_physical_material() {
assert_eq!(swatch(&h).name, "ABS - Blue");
let point = solidcraft_engine::geom::Vec3::new(0.0, 5.0, 5.0);
h.app.run("appearance.assign", json!({"body":"Body1","faces":[point],"appearance":"ABS - Red"})).unwrap();
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let body = state.body("Body1").unwrap();
let face = solidcraft_engine::doc::appearance::face_index_at(body, point).unwrap();
h.app.run("select.set", json!({"items":[Sel::Face{body:"Body1".into(),index:face,point}]})).unwrap();
+13 -5
View File
@@ -2582,7 +2582,15 @@ mod tests {
let pc = h.app.run("sketch.dimension", json!({"entities":[c]})).unwrap()["param"].as_str().unwrap().to_owned();
let pl = h.app.run("sketch.dimension", json!({"entities":[l],"value":20})).unwrap()["param"].as_str().unwrap().to_owned();
let dim = |h: &crate::scenario::Harness, p: &str| {
h.app.session.world_state().sketch(sid).unwrap().sketch.constraints.iter().find(|c| c.param.as_deref() == Some(p)).unwrap().clone()
crate::open_meshing::session_world(&h.app.session)
.sketch(sid)
.unwrap()
.sketch
.constraints
.iter()
.find(|c| c.param.as_deref() == Some(p))
.unwrap()
.clone()
};
let did = dim(&h, &pc).id;
@@ -2771,12 +2779,12 @@ mod tests {
.to_owned();
let sid = h.app.session.active_sketch.unwrap();
let spot = |h: &crate::scenario::Harness, id: &str, piece: usize| {
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sk = &state.sketch(sid).unwrap().sketch;
sk.exact_segs(sk.curve_index(id).unwrap()).unwrap()[piece].point_at(0.5)
};
let count = |h: &crate::scenario::Harness, id: &str| {
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sk = &state.sketch(sid).unwrap().sketch;
sk.curves[sk.curve_index(id).unwrap()].kind.point_ids().len()
};
@@ -2792,7 +2800,7 @@ mod tests {
let item = menu.iter().find(|item| item.id == "sketch.spline.reverse").expect("a control point spline offers Reverse Spline Direction");
assert_eq!((item.label.as_str(), item.params.clone()), ("Reverse Spline Direction", Some(json!({"curve": control}))));
let points = |h: &crate::scenario::Harness, id: &str| {
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sk = &state.sketch(sid).unwrap().sketch;
sk.curves[sk.curve_index(id).unwrap()].kind.point_ids()
};
@@ -2815,7 +2823,7 @@ mod tests {
assert!(clicked.dist(at) < 0.5, "the item carries the clicked spot: {clicked:?} for {at:?}");
h.step(&json!({"click":{"handle":"menu:sketch.spline.insert_point"}})).unwrap();
assert_eq!(count(&h, &fit), 5);
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sk = &state.sketch(sid).unwrap().sketch;
let added = sk.point(sk.curves[sk.curve_index(&fit).unwrap()].kind.point_ids()[2]).unwrap();
assert!(added.dist(at) < 0.5, "the new fit point is where the menu was opened: {added:?}");
+3 -4
View File
@@ -1621,8 +1621,7 @@ impl Dialog {
{
return false;
}
let profiles: Vec<Sel> = s
.world_state()
let profiles: Vec<Sel> = crate::open_meshing::session_world(s)
.sketch(id)
.map(|ss| outermost_profiles(&ss.profiles).map(|index| Sel::Profile { sketch: id, index }).collect())
.unwrap_or_default();
@@ -2241,7 +2240,7 @@ fn sketch_curve_edge(s: &Session, sel: &Sel) -> Option<Sel> {
Sel::ExternalSketchCurve { sketch, id } => (*sketch, id),
_ => return None,
};
let st = s.world_state();
let st = crate::open_meshing::session_world(&s);
let ss = st.sketch(sketch)?;
let segs = ss.sketch.segs(ss.sketch.curve_index(id)?);
let (a, b) = (ss.plane.to_world(segs.first()?.start()), ss.plane.to_world(segs.last()?.end()));
@@ -2653,7 +2652,7 @@ fn placement_row(d: &mut Dialog, ui: &mut egui::Ui, app: &SolveApp) {
/// active a part inside a subassembly picks the subassembly; with the subassembly (or the part)
/// active it picks the part. A body outside the active component picks its top-level placement.
pub(crate) fn occurrence_path_of_body(s: &Session, body: &str) -> Option<Vec<u64>> {
let st = s.world_state();
let st = crate::open_meshing::session_world(&s);
let b = st.body(body)?;
if s.doc.model_body_component(b) == 0 {
return None;
+3 -3
View File
@@ -439,7 +439,7 @@ fn align_components_mode_switches_and_aligns_only_selected_instance() {
h.app.run(&command, params).unwrap();
}
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let b1 = st.body("Beam").unwrap().mesh().bounds();
assert!((b1.min.x - 0.0).abs() < 1e-5 && (b1.max.x - 10.0).abs() < 1e-5, "{b1:?}");
let b2 = st.body("Beam (Extrusion:2)").unwrap().mesh().bounds();
@@ -1018,7 +1018,7 @@ fn a_dialog_field_typed_as_a_definition_defines_the_parameter() {
let extrude = doc.features.iter().rev().find(|f| f.name.starts_with("Extrude")).unwrap();
assert!(extrude.kind.inputs().iter().any(|(label, e, _)| *label == "Distance" && e == "depth"), "{:?}", extrude.kind.inputs());
assert_eq!(h.app.session.undo.len(), undo + 1, "one undo step");
let bounds = h.app.session.world_state().bodies.first().unwrap().mesh().bounds();
let bounds = crate::open_meshing::session_world(&h.app.session).bodies.first().unwrap().mesh().bounds();
assert!((bounds.max.z - bounds.min.z - 30.0).abs() < 1e-7);
}
@@ -1119,7 +1119,7 @@ fn align_components_picks_the_placement_under_the_active_component_and_warns_whe
let bracket = h.app.run("component.create", json!({"name": "Bracket", "grounded": false})).unwrap()["occurrence"].as_u64().unwrap();
h.app.run("solid.box", json!({"length": 10, "width": 10, "height": 4, "body_name": "Plate"})).unwrap();
h.app.run("component.activate", json!({"component": "root"})).unwrap();
let plate = h.app.session.world_state().bodies.first().map(|b| b.name.clone()).unwrap();
let plate = crate::open_meshing::session_world(&h.app.session).bodies.first().map(|b| b.name.clone()).unwrap();
// With the design active, a click on the part inside Frame picks the Frame placement.
assert_eq!(occurrence_path_of_body(&h.app.session, &plate), Some(vec![a]));
+2 -2
View File
@@ -345,7 +345,7 @@ fn side_pick(app: &SolveApp, o: &JointOrigin) -> Option<Sel> {
let at =
if o.occurrence == 0 { solidcraft_engine::doc::IDENTITY } else { solidcraft_engine::doc::joints::occurrence_world(&s.doc, o.occurrence) };
let p = solidcraft_engine::doc::apply_point(&at, pick);
let st = s.world_state();
let st = crate::open_meshing::session_world(&s);
let own: Vec<&solidcraft_engine::doc::ModelBody> = st.bodies.iter().filter(|b| occurrence_of_body(s, &b.name) == Some(o.occurrence)).collect();
let named = match (face, edge) {
(Some(name), _) => own.iter().filter_map(|b| named_face(b, name, p)).min_by(|a, b| a.0.total_cmp(&b.0)),
@@ -390,7 +390,7 @@ fn named_edge(b: &solidcraft_engine::doc::ModelBody, name: &str, p: Vec3) -> Opt
let index = match names.iter().position(|n| n == name) {
Some(i) => i,
None => {
let (at, _) = solidcraft_engine::doc::naming::find_edge(b, name, p).ok()?;
let at = solidcraft_engine::doc::naming::find_edge(b, name, p).ok()?;
(0..m.edges.len())
.min_by(|x, y| edge_dist(&m, *x, at).unwrap_or(f64::INFINITY).total_cmp(&edge_dist(&m, *y, at).unwrap_or(f64::INFINITY)))?
}
+4 -4
View File
@@ -63,7 +63,7 @@ fn editing_a_move_in_a_turned_component_keeps_world_distances() {
h.app.run("occurrence.move", json!({"occurrence": "Part:1", "axis": [1, 0, 0], "angle": "90 deg", "translate": [50, 10, 20]})).unwrap();
h.app.run("component.activate", json!({"component": "root"})).unwrap();
let moved = h.app.run("solid.move", json!({"bodies": ["Body1"], "translate": ["0 mm", "-travel", "0 mm"]})).unwrap()["feature"].as_u64().unwrap();
let bounds = |h: &crate::scenario::Harness| h.app.session.world_state().body("Body1").unwrap().mesh().bounds();
let bounds = |h: &crate::scenario::Harness| crate::open_meshing::session_world(&h.app.session).body("Body1").unwrap().mesh().bounds();
let before = bounds(&h);
let mut d = crate::dialogs::for_feature(&h.app, moved, None).unwrap();
let Kind::Move { x, y, z, .. } = &d.kind else { panic!("not a move") };
@@ -504,7 +504,7 @@ fn translated_box() -> crate::scenario::Harness {
fn changing_the_move_type_keeps_the_move_made_so_far() {
let mut h = translated_box();
let corners = |b: solidcraft_engine::geom::Aabb3| [b.min, b.max];
let before = corners(h.app.session.world_state().body("Body1").unwrap().mesh().bounds());
let before = corners(crate::open_meshing::session_world(&h.app.session).body("Body1").unwrap().mesh().bounds());
let mut d = h.app.dialog.take().unwrap();
switch_type(&h.app, &mut d, ROTATE);
// No axis yet: the translation alone is made.
@@ -519,7 +519,7 @@ fn changing_the_move_type_keeps_the_move_made_so_far() {
for (command, params) in crate::dialogs::apply_commands(&h.app, &d).unwrap() {
h.app.run(&command, params).unwrap();
}
let after = h.app.session.world_state().body("Body1").unwrap().mesh().bounds();
let after = crate::open_meshing::session_world(&h.app.session).body("Body1").unwrap().mesh().bounds();
let turn = |p: Vec3| Vec3::new(-p.y, p.x + 10.0, p.z);
let mut want = solidcraft_engine::geom::Aabb3::EMPTY;
for c in before {
@@ -587,7 +587,7 @@ fn editing_a_free_move_shows_the_values_it_was_made_with() {
.unwrap()["feature"]
.as_u64()
.unwrap();
let bounds = |h: &crate::scenario::Harness| h.app.session.world_state().body("Body1").unwrap().mesh().bounds();
let bounds = |h: &crate::scenario::Harness| crate::open_meshing::session_world(&h.app.session).body("Body1").unwrap().mesh().bounds();
let before = bounds(&h);
let d = crate::dialogs::for_feature(&h.app, id, None).unwrap();
let Kind::Move { x, y, z, rx, ry, rz, .. } = &d.kind else { panic!("not a move") };
+1 -1
View File
@@ -403,7 +403,7 @@ mod tests {
fn part_size_without_a_part_explains_itself_and_standard_parts_opens_the_library() {
let mut app = SolveApp::new(Session::default(), crate::Services::default());
app.run("solid.box", json!({"length": 10, "width": 10, "height": 10})).unwrap();
let body = app.session.world_state().bodies.first().map(|b| b.name.clone()).unwrap();
let body = crate::open_meshing::session_world(&app.session).bodies.first().map(|b| b.name.clone()).unwrap();
for selection in [vec![], vec![Sel::Body { name: body }]] {
app.session.selection = selection;
app.status = None;
+1 -1
View File
@@ -316,7 +316,7 @@ fn paint_icon(ui: &egui::Ui, rect: egui::Rect, tex: Option<egui::TextureId>, nam
/// Every body of the design, in model order; with `visible`, only the bodies the viewport shows.
pub(crate) fn body_picks(app: &SolveApp, visible: bool) -> Vec<Sel> {
let st = app.session.world_state();
let st = crate::open_meshing::session_world(&app.session);
let shown = |name: &str| !visible || app.session.body_visible(name);
st.bodies.iter().filter(|b| shown(&b.name)).map(|b| Sel::Body { name: b.name.clone() }).collect()
}
+1 -1
View File
@@ -255,7 +255,7 @@ fn project_dialog_keeps_geometry_targets_and_can_project_whole_bodies_unlinked()
h.settle();
assert!(h.app.dialog.is_none());
let sketch = h.app.session.active_sketch.expect("Project leaves the sketch active");
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sketch = &state.sketch(sketch).unwrap().sketch;
assert!(sketch.links.is_empty());
assert!(!sketch.curves.is_empty());
+1 -1
View File
@@ -1402,7 +1402,7 @@ mod tests {
h.settle();
h.app.run("parameters.change", json!({"name":param,"expression":"30"})).unwrap();
h.settle();
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let solved = state.sketch(h.app.session.active_sketch.unwrap()).unwrap();
let c = solved.sketch.constraints.iter().find(|c| c.param.as_deref() == Some(param.as_str())).unwrap();
// The placed offset (15 above the measured line) is kept with the dimension.
@@ -588,7 +588,7 @@ fn a_driven_dimension_nothing_fixes_any_more_takes_a_value() {
h.settle();
let sketch = h.app.session.active_sketch.unwrap();
let dim = |h: &Harness| {
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sk = &state.sketch(sketch).unwrap().sketch;
sk.constraints
.iter()
@@ -641,7 +641,7 @@ fn a_driven_dimension_nothing_fixes_any_more_takes_a_value() {
assert!(!driving.driven && driving.param.as_deref() == Some(name.as_str()), "{driving:?}");
let value = driving.kind.value().unwrap();
assert!((value - 30.0).abs() < 1e-6 && (measured - 30.0).abs() > 1.0, "{measured} → {value}");
assert!(h.app.session.world_state().sketch(sketch).unwrap().report.ok());
assert!(crate::open_meshing::session_world(&h.app.session).sketch(sketch).unwrap().report.ok());
// One undo step puts the driven dimension back.
assert_eq!(h.app.session.undo.len(), undo + 1);
+3 -3
View File
@@ -324,7 +324,7 @@ pub fn copy(app: &mut SolveApp) -> bool {
let names: Vec<String> = placements.iter().filter_map(|p| app.session.occurrence_placement(p).ok()).map(|p| p.name).collect();
app.set_status(format!("Copied {} (Ctrl+V pastes an instance, Ctrl+Shift+V a new copy)", names.join(", ")), false);
leave_text(app, format!("SolidCraft components: {}", names.join(", ")));
app.foreign_copy = Some(std::sync::Arc::new(solidcraft_engine::ForeignCopy::of(&app.session, Vec::new(), placements.clone())));
app.foreign_copy = solidcraft_engine::ForeignCopy::of(&app.session, Vec::new(), placements.clone()).ok().map(std::sync::Arc::new);
app.copied_placements = placements;
return true;
}
@@ -334,7 +334,7 @@ pub fn copy(app: &mut SolveApp) -> bool {
replace_clipboard(app);
app.set_status(format!("Copied {}", bodies.join(", ")), false);
leave_text(app, format!("SolidCraft bodies: {}", bodies.join(", ")));
app.foreign_copy = Some(std::sync::Arc::new(solidcraft_engine::ForeignCopy::of(&app.session, bodies.clone(), Vec::new())));
app.foreign_copy = solidcraft_engine::ForeignCopy::of(&app.session, bodies.clone(), Vec::new()).ok().map(std::sync::Arc::new);
app.copied_bodies = bodies;
return true;
}
@@ -463,7 +463,7 @@ pub fn cut(app: &mut SolveApp) -> bool {
// design: a cut that holds any feature leaves another design nothing to paste.
if set.features.is_empty() {
app.foreign_copy =
Some(std::sync::Arc::new(solidcraft_engine::ForeignCopy::of(&app.session, set.bodies.clone(), set.placements.clone())));
solidcraft_engine::ForeignCopy::of(&app.session, set.bodies.clone(), set.placements.clone()).ok().map(std::sync::Arc::new);
}
app.cut = set;
// What is cut is the faded set; the selection that chose it would only muddy which that is.
+2 -2
View File
@@ -690,13 +690,13 @@ fn design_with_cube(name: &str) -> solidcraft_engine::Session {
#[test]
fn copied_bodies_paste_into_another_design_as_imported_bodies() {
let mut app = crate::SolveApp::new(design_with_cube("Bracket"), crate::Services::default());
let was = app.session.world_state().body("Cube").unwrap().mesh().bounds();
let was = crate::open_meshing::session_world(&app.session).body("Cube").unwrap().mesh().bounds();
select(&mut app, json!({"type": "body", "name": "Cube"}));
assert!(super::copy(&mut app));
crate::documents::open_tab(&mut app, design_with_cube("Frame")).unwrap();
assert!(super::can_paste(&app) && super::pasting_bodies(&app));
assert!(super::paste(&mut app, None));
let is = app.session.world_state().body("Cube (2)").unwrap().mesh().bounds();
let is = crate::open_meshing::session_world(&app.session).body("Cube (2)").unwrap().mesh().bounds();
assert!(was.min.dist(is.min) < 1e-6 && was.max.dist(is.max) < 1e-6, "{was:?} vs {is:?}");
let feature = app.session.doc.find_feature("Import1").expect("one Import feature");
assert!(matches!(feature.kind, solidcraft_engine::doc::FeatureKind::Import { .. }));
+2 -2
View File
@@ -101,7 +101,7 @@ pub(crate) fn double_click_face(app: &mut SolveApp, body: &str) -> bool {
if !app.preferences.double_click_components {
return false;
}
let Some(component) = app.session.world_state().body(body).map(|b| b.component).filter(|c| *c != 0) else { return false };
let Some(component) = crate::open_meshing::session_world(&app.session).body(body).map(|b| b.component).filter(|c| *c != 0) else { return false };
let placement = app
.session
.doc
@@ -165,7 +165,7 @@ fn working(app: &SolveApp) -> bool {
/// at `placement`: how many, their triangles and their extent. `None` when the placement is gone.
fn snapshot(session: &Session, component: u64, placement: &[u64]) -> Option<(usize, Vec<([f32; 3], [f32; 3])>, Aabb3)> {
let frame = session.occurrence_placement(placement).ok().and_then(|p| mat_inverse(&p.world))?;
let world = session.world_state();
let world = crate::open_meshing::session_world(&session);
let (mut bodies, mut triangles, mut bounds) = (0, Vec::new(), Aabb3::default());
for b in world.bodies.iter().filter(|b| b.component != component && session.body_visible(&b.name)) {
let (mesh, placed) = b.mesh_in_place();
@@ -5,6 +5,7 @@ use solidcraft_engine::Session;
fn volume_of(session: &Session, prefix: &str) -> f64 {
session
.world_state()
.unwrap()
.bodies
.iter()
.filter(|b| b.name.starts_with(prefix))
+4 -4
View File
@@ -49,7 +49,7 @@ fn undoing_the_edited_feature_closes_its_editor_and_preserves_redo() {
h.app.run("edit.redo", json!({})).unwrap();
assert!(h.app.session.doc.feature(feature).is_some());
assert!(h.app.dialog.is_none(), "redo does not resurrect the old editor");
assert_eq!(h.app.session.world_state().body("Main").unwrap().mesh().bounds().max.z, 10.0);
assert_eq!(crate::open_meshing::session_world(&h.app.session).body("Main").unwrap().mesh().bounds().max.z, 10.0);
open(&mut h);
h.step(&json!({"widget":"OK"})).unwrap();
assert!(h.app.dialog.is_none(), "the restored feature can be edited normally");
@@ -85,7 +85,7 @@ fn deleting_the_edited_feature_closes_its_editor_and_undo_restores_the_design()
assert_eq!((h.app.session.undo.len(), h.app.session.redo.len()), history);
h.app.run("edit.undo", json!({})).unwrap();
assert!(std::sync::Arc::ptr_eq(&original, &h.app.session.doc), "Undo restores the document before the temporary editor rollback");
assert_eq!(h.app.session.world_state().body("Main").unwrap().mesh().bounds().max.z, 10.0);
assert_eq!(crate::open_meshing::session_world(&h.app.session).body("Main").unwrap().mesh().bounds().max.z, 10.0);
}
#[test]
@@ -601,7 +601,7 @@ fn editing_face_extrusion_temporarily_shows_its_hidden_source_body() {
assert!(h.app.body_visible("Source"));
assert!(!h.app.session.body_visible("Source"), "the editor must not change persistent visibility");
assert!(h.app.body_visible("Other"));
assert!(h.app.session.world_state().body("Extension").is_none(), "the feature's result is rolled off");
assert!(crate::open_meshing::session_world(&h.app.session).body("Extension").is_none(), "the feature's result is rolled off");
assert_eq!(h.app.session.undo.len(), undo);
h.app.session.visibility.hidden_bodies.push("Other".into());
h.step(&json!({"widget":finish})).unwrap();
@@ -610,7 +610,7 @@ fn editing_face_extrusion_temporarily_shows_its_hidden_source_body() {
assert!(!h.app.body_visible("Other"), "unrelated visibility edits are preserved");
assert_eq!(h.app.session.visibility.hidden_sketches, visibility.hidden_sketches);
assert_eq!(h.app.session.visibility.shown_sketches, visibility.shown_sketches);
assert!(h.app.session.world_state().body("Extension").is_some());
assert!(crate::open_meshing::session_world(&h.app.session).body("Extension").is_some());
}
}
+1 -1
View File
@@ -464,7 +464,7 @@ pub(crate) fn track_and_show(app: &mut SolveApp, painter: &egui::Painter, proj:
if tracking.sketch != active || !tracking.document.ptr_eq(&document) {
*tracking = Tracking { sketch: active, document, ..Default::default() };
}
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
let Some((ss, cursor)) = active
.and_then(|id| state.sketch(id))
.zip(cursor)
+2 -2
View File
@@ -142,7 +142,7 @@ impl Mode {
.filter(|(_, _, _, path, _)| path.last() == Some(&self.occurrence))
.map(|(name, ..)| name)
.collect();
let world = app.session.world_state();
let world = crate::open_meshing::session_world(&app.session);
self.radii = self
.axes
.iter()
@@ -664,7 +664,7 @@ mod tests {
let joint =
h.app.run("joint.create", json!({"type":kind,"a":a,"b":b,"flip":true,"angle":0,"name":"Guide"})).unwrap()["joint"].as_u64().unwrap();
let component = h.app.session.doc.find_component("Arm").unwrap();
let world = h.app.session.world_state();
let world = crate::open_meshing::session_world(&h.app.session);
let body = world.bodies.iter().find(|b| h.app.session.doc.model_body_component(b) == component).unwrap().name.clone();
h.app.session.selection = vec![Sel::Face { body, index: 0, point: Vec3::new(30., 0., 10.) }];
h.call("ui.view", json!({"view":"top"}));
+2 -2
View File
@@ -2579,7 +2579,7 @@ impl SolveApp {
/// Move to `to` and frame the whole model from there at Home's zoom (animated): the
/// navigators' Home, faces, corners, arrows and axes (`navigator_fitted`).
pub fn animate_to_fitted(&mut self, to: Camera) {
let bounds = solidcraft_engine::view::bounds(&self.session);
let Ok(bounds) = solidcraft_engine::view::bounds(&self.session) else { return };
let shared = self.preferences.navigator_fit;
let framed = if shared { self.navigator_fitted(to) } else { self.fitted(to) };
self.animate_to(framed);
@@ -2660,7 +2660,7 @@ impl SolveApp {
let to = self.fitted_bounds(from, &bounds);
self.pending_fit = None;
self.animate_to(to);
self.pending_fit = Some((from, bounds, true));
self.pending_fit = Some((from, bounds, true, false));
}
/// Move the viewport to the camera a command asked for (`view.apply`): animated like a view
+6 -2
View File
@@ -446,7 +446,11 @@ mod tests {
let mut app = SolveApp::new(Session::default(), Default::default());
app.run("component.insert_design", json!({"path": file})).unwrap();
let volume = |app: &SolveApp| {
app.session.world_state().bodies.iter().map(|b| solidcraft_engine::kernel::measure(&b.body).unwrap().volume).sum::<f64>()
crate::open_meshing::session_world(&app.session)
.bodies
.iter()
.map(|b| solidcraft_engine::kernel::measure(&b.body).unwrap().volume)
.sum::<f64>()
};
let ctx = egui::Context::default();
let settle = |app: &mut SolveApp| {
@@ -539,7 +543,7 @@ mod tests {
}
fn volume(app: &SolveApp) -> f64 {
app.session.world_state().bodies.iter().map(|b| solidcraft_engine::kernel::measure(&b.body).unwrap().volume).sum()
crate::open_meshing::session_world(&app.session).bodies.iter().map(|b| solidcraft_engine::kernel::measure(&b.body).unwrap().volume).sum()
}
/// Away to the blank tab and back to the assembly.
+2 -2
View File
@@ -101,7 +101,7 @@ fn look_at_squares_a_vertical_sketch_to_the_screen() {
h.app.run("sketch.create", json!({"plane": "XZ", "name": "Wall"})).unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 20]})).unwrap();
crate::dialogs::look_at_sketch(&mut h.app);
let plane = h.app.session.world_state().sketch(h.app.session.active_sketch.unwrap()).unwrap().plane;
let plane = crate::open_meshing::session_world(&h.app.session).sketch(h.app.session.active_sketch.unwrap()).unwrap().plane;
let (right, up, back) = h.app.cam.basis();
assert!((back.cross(plane.normal())).len() < 1e-6, "facing the sketch: back={back:?} n={:?}", plane.normal());
assert!(right.dot(plane.x).abs() > 0.999, "sketch x to the right: right={right:?} x={:?}", plane.x);
@@ -125,7 +125,7 @@ fn editing_a_sketch_in_a_turned_component_faces_its_world_plane() {
h.app.run("occurrence.move", json!({"occurrence": occ.id, "axis": [1, 0, 0], "angle": "90 deg", "capture": true})).unwrap();
let id = h.app.session.doc.find_feature("Face").map(|f| f.id).unwrap();
h.app.edit_sketch(id);
let plane = h.app.session.world_state().sketch(id).unwrap().plane;
let plane = crate::open_meshing::session_world(&h.app.session).sketch(id).unwrap().plane;
let (right, up, back) = h.app.cam.basis();
assert!((back.cross(plane.normal())).len() < 1e-5, "facing world plane: back={back:?} n={:?}", plane.normal());
assert!(right.dot(plane.x).abs() > 0.99, "x right: {right:?} vs {:?}", plane.x);
+1 -1
View File
@@ -31,7 +31,7 @@ fn measure_starts_with_selected_vertices_edges_and_faces() {
.map(|point| match kind {
"vertex" => json!({"type":kind,"body":"Box","point":point}),
_ => {
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let mesh = st.body("Box").unwrap().mesh();
let point = solidcraft_engine::geom::Vec3::from(point);
let index = if kind == "edge" {
+2 -2
View File
@@ -253,13 +253,13 @@ mod tests {
let wider = gpu_clouds(&h.app, &h.ctx);
assert_eq!((wider[0].version, wider[0].style.range), (ramp[0].version, [0.0, 2.0]), "a new range uploads nothing");
// Hidden, alone or with its folder: nothing drawn; the view frames it while shown.
assert!(!solidcraft_engine::view::bounds(&h.app.session).is_empty());
assert!(!solidcraft_engine::view::bounds(&h.app.session).unwrap().is_empty());
let first_bounds = crate::viewport::model_bounds(&h.app);
assert!(!first_bounds.is_empty());
assert_eq!(crate::viewport::model_bounds(&h.app), first_bounds, "cached sketch bounds keep the point cloud extent");
h.app.run("pointcloud.edit", serde_json::json!({"cloud": id, "visible": false})).unwrap();
assert!(gpu_clouds(&h.app, &h.ctx).is_empty());
assert!(solidcraft_engine::view::bounds(&h.app.session).is_empty());
assert!(solidcraft_engine::view::bounds(&h.app.session).unwrap().is_empty());
assert!(crate::viewport::model_bounds(&h.app).is_empty());
let _ = std::fs::remove_dir_all(dir);
}
+2 -2
View File
@@ -6336,7 +6336,7 @@ mod tests {
let (t, white) = (Tokens::get(), egui::Color32::WHITE);
assert_eq!((t.viewport_top, t.viewport_bottom, Tokens::view().viewport_top), (white, white, white), "{theme}");
assert_eq!(t.panel, Tokens::themed(view_dark, ColorVision::Off).panel, "{theme}: the window");
let scene = crate::control::view_scene(&h.app, solidcraft_engine::view::Guides::default());
let scene = crate::control::view_scene(&h.app, solidcraft_engine::view::Guides::default()).unwrap();
let white = solidcraft_engine::render::Rgb(255, 255, 255);
assert_eq!(scene.background, Some((white, white)), "{theme}: a picture of the view");
}
@@ -9015,7 +9015,7 @@ mod tests {
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0));
app.fit_view();
let proj = crate::viewport::projection(&app, rect);
let st = app.session.world_state();
let st = crate::open_meshing::session_world(&app.session);
let m = st.bodies[0].mesh();
// A spot a fifth of the way along a visible edge, well away from its middle on screen.
let found = m.edges.iter().filter(|e| e.len() >= 2).find_map(|e| {
+12 -8
View File
@@ -172,7 +172,7 @@ fn a_body_mirror_reopens_with_its_bodies_and_operation() {
let mut app = app_with(json!([
{"command":"solid.box","params":{"corner":[40,0,0],"length":2,"width":2,"height":2,"name":"Base"}}
]));
let body = app.session.world_state().bodies[0].name.clone();
let body = crate::open_meshing::session_world(&app.session).bodies[0].name.clone();
app.session.execute("solid.mirror", &json!({"bodies":[body],"plane":"YZ","combine":combine})).unwrap();
let feature = app.session.doc.features.last().unwrap().clone();
app.session = Session::new(solidcraft_engine::doc::Document::from_json(&app.session.doc.to_json()).unwrap());
@@ -193,7 +193,7 @@ fn a_body_mirror_reopens_with_its_bodies_and_operation() {
let mut app = app_with(json!([
{"command":"solid.box","params":{"corner":[40,0,0],"length":2,"width":2,"height":2,"name":"Base"}}
]));
let body = app.session.world_state().bodies[0].name.clone();
let body = crate::open_meshing::session_world(&app.session).bodies[0].name.clone();
app.session.execute("solid.pattern.rectangular", &json!({"bodies":[body],"dir1":[1,0,0],"count1":3,"spacing1":5})).unwrap();
let feature = app.session.doc.features.last().unwrap().clone();
app.session.selection = vec![Sel::Feature { id: feature.id }];
@@ -1098,7 +1098,7 @@ fn path_pattern_editor_restores_sources_path_expressions_and_options() {
let original = h.app.session.doc.find_feature("Copies").unwrap().clone();
let seed = h.app.session.doc.find_feature("Seed").unwrap().id;
let path = h.app.session.doc.find_feature("Path").unwrap().id;
let bounds: Vec<_> = h.app.session.world_state().bodies.iter().map(|b| b.mesh().bounds()).collect();
let bounds: Vec<_> = crate::open_meshing::session_world(&h.app.session).bodies.iter().map(|b| b.mesh().bounds()).collect();
h.step(&json!({"call":"ui.editFeature","params":{"feature":"Copies"}})).unwrap();
h.frames(8);
assert!(!h.app.ui.palette_open, "Edit Feature must open the existing pattern dialog");
@@ -1117,7 +1117,7 @@ fn path_pattern_editor_restores_sources_path_expressions_and_options() {
assert_eq!(edited.id, original.id);
assert_eq!(edited.kind, original.kind);
assert_eq!(edited.param_names, original.param_names);
assert_eq!(h.app.session.world_state().bodies.iter().map(|b| b.mesh().bounds()).collect::<Vec<_>>(), bounds);
assert_eq!(crate::open_meshing::session_world(&h.app.session).bodies.iter().map(|b| b.mesh().bounds()).collect::<Vec<_>>(), bounds);
}
}
}
@@ -1148,15 +1148,19 @@ fn path_pattern_editor_applies_in_place_cancels_and_undoes() {
if cancel {
assert_eq!(edited.kind, original.kind);
assert_eq!(h.app.session.undo.len(), undo);
assert_eq!(h.app.session.world_state().bodies.len(), 3);
assert_eq!(crate::open_meshing::session_world(&h.app.session).bodies.len(), 3);
} else {
assert_eq!(h.app.session.world_state().bodies.len(), 5);
let max_x = h.app.session.world_state().bodies.iter().map(|b| b.mesh().bounds().max.x).fold(f64::NEG_INFINITY, f64::max);
assert_eq!(crate::open_meshing::session_world(&h.app.session).bodies.len(), 5);
let max_x = crate::open_meshing::session_world(&h.app.session)
.bodies
.iter()
.map(|b| b.mesh().bounds().max.x)
.fold(f64::NEG_INFINITY, f64::max);
let expected = if distance_key == "distance" { 16.0 } else { 52.0 };
assert!((max_x - expected).abs() < 1e-7, "five 4 mm bodies, {distance_key} 12 mm: {max_x}");
h.app.run("edit.undo", json!({})).unwrap();
assert_eq!(h.app.session.doc.find_feature("Copies").unwrap().kind, original.kind);
assert_eq!(h.app.session.world_state().bodies.len(), 3);
assert_eq!(crate::open_meshing::session_world(&h.app.session).bodies.len(), 3);
}
}
}
+3 -3
View File
@@ -9,14 +9,14 @@ use crate::SolveApp;
/// Whether face `index` of `body` (as the viewport names it) is that body's print face now.
pub(crate) fn is_print_face(app: &SolveApp, body: &str, index: usize) -> bool {
let world = app.session.world_state();
let world = crate::open_meshing::session_world(&app.session);
world.body(body).and_then(|b| app.session.doc.print_face(&world, b)).and_then(Result::ok).is_some_and(|f| f.index == index)
}
/// `body`'s print face as a face pick, for the Send to Slicer window; `None` when it has none,
/// or it can't be found.
pub(crate) fn current(app: &SolveApp, body: &str) -> Option<Sel> {
let world = app.session.world_state();
let world = crate::open_meshing::session_world(&app.session);
let b = world.body(body)?;
let face = app.session.doc.print_face(&world, b)?.ok()?;
let point = solidcraft_engine::doc::naming::face_spot(b, face.index).map_or(face.point, |spot| spot.point);
@@ -63,7 +63,7 @@ mod tests {
/// Select the face of Block at `point`; its index.
fn select_face(app: &mut crate::SolveApp, point: [f64; 3]) -> usize {
let world = app.session.world_state();
let world = crate::open_meshing::session_world(&app.session);
let body = world.body("Block").unwrap();
let p = solidcraft_engine::geom::Vec3::new(point[0], point[1], point[2]);
let index = solidcraft_engine::doc::appearance::face_index_at(body, p).unwrap();
+3 -3
View File
@@ -24,7 +24,7 @@ fn block() -> Harness {
}
fn edge(h: &Harness, p: Vec3) -> Sel {
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let m = st.body("Body1").unwrap().mesh();
let index = m.edges.iter().position(|e| e.windows(2).any(|w| p.dist_to_segment(w[0], w[1]) < 1e-9)).expect("an edge there");
Sel::Edge { body: "Body1".into(), index, point: p }
@@ -32,7 +32,7 @@ fn edge(h: &Harness, p: Vec3) -> Sel {
/// The face with a triangle wholly at height `z`.
fn face_at_height(h: &Harness, z: f64) -> Sel {
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let m = st.body("Body1").unwrap().mesh();
let (t, f) = m.triangles.iter().zip(&m.tri_face).find(|(t, _)| m.tri(t).is_some_and(|ps| ps.iter().all(|p| (p.z - z).abs() < 1e-9))).unwrap();
let ps = m.tri(t).unwrap();
@@ -73,7 +73,7 @@ fn a_round_edge_shows_its_diameter_radius_and_circumference() {
h.frames(2);
// The top circle, by a point between two of its polyline's points (its first is on the seam).
let on_rim = {
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let m = st.body("Body1").unwrap().mesh();
let rim = m.edges.iter().find(|e| e.len() > 2 && e.iter().all(|p| (p.z - 10.0).abs() < 1e-9)).expect("the top circle");
(rim[1] + rim[2]) * 0.5
+2 -2
View File
@@ -76,7 +76,7 @@ fn step_index(step: LookStep) -> usize {
}
pub(super) fn targets(app: &SolveApp, step: LookStep) -> Targets {
let mut targets = Targets::default();
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
for selection in &app.session.selection {
let body = match selection {
Sel::Body { name } | Sel::Face { body: name, .. } => state.body(name),
@@ -391,7 +391,7 @@ fn draw(app: &mut SolveApp, ui: &mut egui::Ui, view: &mut View, selected: &Targe
selected.items.get(index).filter(|_| !matches!(view.inspected, Some(LookStep::Component | LookStep::Material))).and_then(|item| {
match item {
Sel::Body { name } | Sel::Face { body: name, .. } => {
app.session.world_state().body(name).and_then(|body| app.session.doc.model_display_look(body))
crate::open_meshing::session_world(&app.session).body(name).and_then(|body| app.session.doc.model_display_look(body))
}
_ => None,
}
+2 -4
View File
@@ -42,7 +42,7 @@ fn component(app: &SolveApp, sel: &Sel) -> Option<u64> {
match sel {
Sel::Occurrence { path } => app.session.placed_document().path_placement(path).map(|(id, _)| id).filter(|id| *id != 0),
Sel::Body { name } | Sel::Face { body: name, .. } | Sel::Edge { body: name, .. } => {
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
state.body(name).map(|body| app.session.doc.model_body_component(body)).filter(|id| *id != 0)
}
_ => None,
@@ -253,9 +253,7 @@ fn drop_preview(app: &SolveApp, look: Value, hit: &Hit, sel: &Sel, mods: Modifie
}
let whole_component = mods.ctrl || mods.command || app.ui.pick_components;
if whole_component && let Some(id) = component(app, sel) {
let bodies = app
.session
.world_state()
let bodies = crate::open_meshing::session_world(&app.session)
.bodies
.iter()
.filter(|b| {
+5 -5
View File
@@ -25,7 +25,7 @@ pub(crate) fn decoration(app: &SolveApp, names: &[String]) -> Decoration {
if !super::active(app) {
return Decoration::default();
}
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
let bodies: Vec<_> = names.iter().filter_map(|name| state.body(name)).collect();
let Some(body) = bodies.first() else { return Decoration::default() };
let look = object_look(app, body);
@@ -51,7 +51,7 @@ fn object_look(app: &SolveApp, body: &ModelBody) -> Option<Look> {
}
pub(crate) fn same_look(app: &SolveApp, names: &[String]) -> bool {
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
let mut looks = names.iter().filter_map(|name| state.body(name)).map(|body| app.session.doc.model_display_look(body));
let Some(first) = looks.next() else { return false };
looks.all(|look| look == first)
@@ -92,7 +92,7 @@ fn section(app: &mut SolveApp, ui: &mut egui::Ui, key: &str) -> bool {
secondary: &match key {
"lights" => light_types(app).len(),
"shots" => app.session.doc.render.setups.len(),
_ => app.session.world_state().bodies.len(),
_ => crate::open_meshing::session_world(&app.session).bodies.len(),
}
.to_string(),
fold: Some(open),
@@ -353,7 +353,7 @@ pub(crate) struct LookGroup {
pub(crate) fn groups(app: &SolveApp) -> Vec<LookGroup> {
let mut groups: Vec<LookGroup> = Vec::new();
for body in &app.session.world_state().bodies {
for body in &crate::open_meshing::session_world(&app.session).bodies {
for look in app.session.doc.model_used_looks(body) {
if let Some(group) = groups.iter_mut().find(|g| g.look == look) {
group.objects.push(body.name.clone());
@@ -443,7 +443,7 @@ pub(crate) fn objects(app: &mut SolveApp, ui: &mut egui::Ui, hover_bodies: &mut
browser::toggle(app, &key, true);
}
let source = group.objects.first().and_then(|name| {
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
let body = state.body(name)?;
let face = if app.session.doc.model_display_look(body) == group.look {
None
+6 -2
View File
@@ -2054,8 +2054,12 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
app.session.visibility.hidden_bodies.hash(&mut visibility);
app.session.visibility.hidden_occurrences.hash(&mut visibility);
let budget = preferred_geometry_budget(app.render_workspace.geometry_budget, app.preferences.render_previous_geometry_budget);
let signature =
(app.session.revision, Arc::as_ptr(&crate::open_meshing::session_world(&app.session)) as usize, visibility.finish() ^ assign::preview_key(app), budget);
let signature = (
app.session.revision,
Arc::as_ptr(&crate::open_meshing::session_world(&app.session)) as usize,
visibility.finish() ^ assign::preview_key(app),
budget,
);
let preview = assign::preview(app);
let previous_key = app.render_workspace.key;
app.render_workspace.prepare_preview(signature, || {
+4 -4
View File
@@ -148,7 +148,7 @@ pub(crate) fn edited(app: &mut SolveApp, params: &Value, reply: &Value) {
}
fn product_centre(app: &SolveApp) -> [f64; 3] {
let bounds = solidcraft_engine::view::bounds(&app.session);
let bounds = solidcraft_engine::view::bounds(&app.session).unwrap_or(solidcraft_engine::geom::Aabb3::EMPTY);
let point = if bounds.is_empty() { app.cam.target } else { bounds.center() };
[point.x, point.y, point.z]
}
@@ -773,13 +773,13 @@ pub(super) fn editor(app: &mut SolveApp, ui: &mut egui::Ui, id: &str) {
let label =
app.tr(if aim == product_centre(app) { "render.light_product_centre" } else { "render.lighting_point" }).to_string();
ui_kit::dropdown_menu(ui, "render:light:aim", &label, |ui| {
let bounds = solidcraft_engine::view::bounds(&app.session);
let bounds = solidcraft_engine::view::bounds(&app.session).unwrap_or(solidcraft_engine::geom::Aabb3::EMPTY);
let mut targets = vec![
(app.tr("render.light_product_centre").to_string(), product_centre(app)),
(app.tr("render.light_world_origin").to_string(), [0.0; 3]),
];
if !bounds.is_empty() {
for body in &app.session.world_state().bodies {
for body in &crate::open_meshing::session_world(&app.session).bodies {
let bounds = body.mesh_bounds();
let c = bounds.center();
targets.push((body.name.clone(), [c.x, c.y, c.z]));
@@ -925,7 +925,7 @@ fn length([x, y, z]: [f64; 3]) -> f64 {
x.hypot(y).hypot(z)
}
fn subject_size(app: &SolveApp) -> f64 {
bounded(solidcraft_engine::view::bounds(&app.session).diagonal(), 1.0, 1e6)
bounded(solidcraft_engine::view::bounds(&app.session).unwrap_or(solidcraft_engine::geom::Aabb3::EMPTY).diagonal(), 1.0, 1e6)
}
fn note(app: &SolveApp, ui: &mut egui::Ui, key: &'static str) {
ui_kit::text(ui, app.tr(key), ui_kit::Text::Note(key));
+2 -2
View File
@@ -5166,7 +5166,7 @@ fn render_workspace_browser_semantic_focus_survives_clearing_object_picks() {
h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Object"})).unwrap();
enter(&mut h.app, true).unwrap();
let doc = h.app.session.doc.clone();
let evaluated = h.app.session.world_state();
let evaluated = crate::open_meshing::session_world(&h.app.session);
let before = (h.app.session.undo.len(), h.app.session.redo.len(), h.app.session.is_dirty());
for (request, expected) in [
(json!({"kind":"environment"}), RenderSelection::Environment),
@@ -5184,7 +5184,7 @@ fn render_workspace_browser_semantic_focus_survives_clearing_object_picks() {
assert!(h.app.session.selection.is_empty());
}
assert!(Arc::ptr_eq(&doc, &h.app.session.doc));
assert!(Arc::ptr_eq(&evaluated, &h.app.session.world_state()));
assert!(Arc::ptr_eq(&evaluated, &crate::open_meshing::session_world(&h.app.session)));
assert_eq!(before, (h.app.session.undo.len(), h.app.session.redo.len(), h.app.session.is_dirty()));
}
@@ -105,7 +105,7 @@ pub(super) fn refresh(app: &mut SolveApp, ctx: &egui::Context) {
let mut scene = std::collections::hash_map::DefaultHasher::new();
app.session.visibility.hidden_bodies.hash(&mut scene);
app.session.visibility.hidden_occurrences.hash(&mut scene);
(std::sync::Arc::as_ptr(&app.session.world_state()) as usize).hash(&mut scene);
(std::sync::Arc::as_ptr(&crate::open_meshing::session_world(&app.session)) as usize).hash(&mut scene);
let scene = scene.finish();
let mut keys = Vec::new();
for name in std::iter::once(&app.render_workspace.view.name)
+1 -1
View File
@@ -591,7 +591,7 @@ fn zoomed_view_frame_cost() {
(t[3], frames.stats().triangles)
};
let (whole, whole_tris) = still(&h, &mut frames);
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let b = st.bodies.iter().find(|b| b.component == 0).unwrap().mesh().bounds();
drop(st);
h.app.cam.target = b.center();
@@ -292,7 +292,7 @@ fn line3d_tool_draws_polyline_off_plane() {
h.step(&json!({"click": {"world": p}})).unwrap();
}
h.step(&json!({"key": "Escape"})).unwrap();
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let wires = &state.sketch(sketch).unwrap().sketch.wires;
assert_eq!(wires.len(), 2, "three off-plane clicks finish as two joined 3D segments");
let expected = [[Vec3::new(10.0, 0.0, 10.0), Vec3::new(20.0, 10.0, 10.0)], [Vec3::new(20.0, 10.0, 10.0), Vec3::new(10.0, 10.0, 10.0)]];
@@ -312,7 +312,7 @@ fn spline3d_tool_fit_points_off_plane() {
h.step(&json!({"click": {"world": p}})).unwrap();
}
h.step(&json!({"key": "Escape"})).unwrap();
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let wires = &state.sketch(sketch).unwrap().sketch.wires;
assert_eq!(wires.len(), 1, "three off-plane clicks finish as one 3D spline");
let w = &wires[0];
@@ -332,7 +332,7 @@ fn point3d_tool_places_points_and_stays() {
for p in [OFF_PLANE[0], OFF_PLANE[1]] {
h.step(&json!({"click": {"world": p}})).unwrap();
}
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let wires = &state.sketch(sketch).unwrap().sketch.wires;
assert_eq!(wires.len(), 2, "two clicks place two drawn 3D points");
assert!(wires.iter().all(|w| w.fit.len() == 1), "{:?}", wires.iter().map(|w| &w.fit).collect::<Vec<_>>());
+2 -2
View File
@@ -82,7 +82,7 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, hits: &[Hit], at: Option<(V
_ => None,
});
if let Some(id) = picked {
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
let valid = active.and_then(|sid| state.sketch(sid)).is_some_and(|ss| {
ss.sketch.curve_index(&id).and_then(|i| ss.sketch.curves.get(i)).is_some_and(|c| {
use solidcraft_engine::sketch::CurveKind;
@@ -133,7 +133,7 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
let Some(pointer) = &t.hover else { return };
let Some(mode) = construction_line_mode(t.cmd) else { return };
let Some(p) = params(app, t, pointer, true) else { return };
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
let Some(ss) = app.session.active_sketch.and_then(|sid| state.sketch(sid)) else { return };
let Ok(lines) = construction_line_solutions(&ss.sketch, &p, mode) else { return };
let chosen = construction_line_choice(&lines, &p).ok();
+2 -2
View File
@@ -224,7 +224,7 @@ fn shift_clicks_on_two_curves_extend_both_to_where_they_meet() {
shift_click(&mut h, &json!({"sketch": [12, 3]}));
assert_eq!(h.app.session.undo.len(), undo + 1, "the second runs the two-curve extend, in one step");
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let ss = st.sketch(h.app.session.active_sketch.unwrap()).unwrap();
for end in ["l1.end", "l2.end"] {
let p = ss.sketch.point(ss.sketch.resolve_point(end).unwrap()).unwrap();
@@ -948,7 +948,7 @@ fn corner_candidates_agree_with_engine_spatial_curve_filter() {
for cmd in ["sketch.fillet", "sketch.chamfer.equal_distance", "sketch.chamfer.two_distance", "sketch.chamfer.distance_angle"] {
h.app.start(cmd);
for id in [&line, &spline, &linked, &"side".to_owned()] {
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sk = &state.sketch(sketch).unwrap().sketch;
let hit = Hit::SketchCurve { sketch, id: id.clone(), straight: true, point: solidcraft_engine::geom::Vec3::ZERO };
let offered = candidate(h.app.dialog.as_ref().unwrap(), &h.app.session, &hit).is_some();
+7 -7
View File
@@ -373,7 +373,7 @@ fn a_line_end_on_a_spline_snaps_on_it_and_stays_on_it() {
h.settle();
let sid = h.app.session.active_sketch.unwrap();
let (spline, on) = {
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let sk = &st.sketch(sid).unwrap().sketch;
let ci = sk.curves.iter().position(|c| matches!(c.kind, CurveKind::Spline { .. })).unwrap();
let poly = sk.polyline(ci);
@@ -387,7 +387,7 @@ fn a_line_end_on_a_spline_snaps_on_it_and_stays_on_it() {
let hover = h.app.tool.as_ref().unwrap().hover.clone().unwrap();
assert!(hover.1.as_deref().is_some_and(|s| s.starts_with("on:")), "{hover:?}");
h.step(&json!({"click": {"world": [on.x, on.y, 0]}})).unwrap();
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let sk = &st.sketch(sid).unwrap().sketch;
let b = sk
.curves
@@ -599,7 +599,7 @@ fn polygon_inputs_clear_each_other_and_the_preview() {
let tool = h.app.tool.as_ref().unwrap();
let end = crate::sketch_dims::shown(&h.app, tool, tool.hover.as_ref().unwrap());
let outline = crate::tools::polygon_outline(tool, end, 6.0).unwrap();
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let plane = state.sketch(h.app.session.active_sketch.unwrap()).unwrap().plane;
let proj = crate::viewport::projection(&h.app, h.app.viewport.rect.unwrap());
let points: Vec<_> = outline.iter().map(|p| proj.to_screen(plane.to_world(*p)).unwrap()).collect();
@@ -614,7 +614,7 @@ fn polygon_inputs_clear_each_other_and_the_preview() {
assert_focus(&h, 1);
type_value(&mut h, "6");
h.step(&json!({"key":"Enter"})).unwrap();
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sketch = state.sketch(h.app.session.active_sketch.unwrap()).unwrap();
assert_eq!(sketch.sketch.curves.iter().filter(|c| !c.construction).count(), 6);
assert_eq!(sketch.profiles.len(), 1);
@@ -634,7 +634,7 @@ fn a_box_typed_name_equals_value_defines_the_parameter_driving_the_dimension() {
let tool = h.app.tool.as_ref().unwrap();
assert_eq!(crate::sketch_dims::locked_value(&h.app, tool, "Diameter"), Some(20.0), "the preview follows the typed size");
h.step(&json!({"key": "Enter"})).unwrap();
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sk = &state.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch;
assert_eq!(sk.curves.len(), 1, "{:?}", h.app.status);
assert!(sk.radius(0).is_some_and(|r| (r - 10.0).abs() < 1e-6), "{:?}", sk.radius(0));
@@ -708,7 +708,7 @@ fn a_second_circle_on_the_same_centre_keeps_its_diameter_box_after_release() {
h.ctx.memory(|m| m.focused()),
tool.hover
);
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sk = &state.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch;
assert_eq!(sk.curves.len(), 1, "{label}: unexpected curves {}", sk.curves.len());
}
@@ -757,7 +757,7 @@ fn a_definition_in_a_box_that_is_not_a_dimension_is_refused() {
}
type_value(&mut h, text);
h.step(&json!({"key": "Enter"})).unwrap();
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sk = &state.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch;
assert!(sk.curves.is_empty(), "{command} {text}: placed {:?}", h.app.status);
assert!(h.app.session.doc.params.iter().all(|p| p.model), "{command} {text}: no user parameter");
@@ -404,7 +404,7 @@ fn two_recent_midpoints_snap_where_their_guides_meet() {
step(&mut h, json!({"click":{"sketch":[0.4,3.3]}}));
let placed = h.app.tool.as_ref().unwrap().pts[0].0;
assert!(placed.dist(cross) < 1e-6, "{placed:?}");
let curves = h.app.session.world_state().sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch.curves.len();
let curves = crate::open_meshing::session_world(&h.app.session).sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch.curves.len();
assert_eq!(curves, 5, "the snap adds no construction geometry");
h.call("ui.prefs", json!({"sketch_tracking_guides":false}));
h.frames(2);
@@ -484,7 +484,7 @@ fn lining_up_fixture(points: &[(&str, Value)]) -> Harness {
/// constraints tie it to.
fn point_and_alignments(h: &Harness, at: [f64; 2]) -> Option<(String, Vec<(&'static str, String)>)> {
use solidcraft_engine::sketch::ConstraintKind::{HorizontalPoints, VerticalPoints};
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let sk = &st.sketch(h.app.session.active_sketch?)?.sketch;
let at = solidcraft_engine::geom::Vec2::new(at[0], at[1]);
let i = sk.points.iter().position(|p| p.pos.dist(at) < 1e-9)?;
@@ -562,7 +562,7 @@ fn a_horizontal_line_ends_plumb_with_a_point_above_and_keeps_both() {
step(&mut h, json!({"key":"Escape"}));
let (_, tied) = point_and_alignments(&h, [20.37, 0.0]).expect("the end is level with the start and plumb with the point");
assert_eq!(tied, [("Vertical", above)]);
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let sk = &st.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch;
assert!(
sk.constraints.iter().any(|c| matches!(c.kind, solidcraft_engine::sketch::ConstraintKind::Horizontal { .. })),
+2 -2
View File
@@ -64,7 +64,7 @@ pub fn follow_pointer(app: &mut SolveApp, ctx: &egui::Context, proj: &Proj, poin
return;
}
let screen = {
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
let plane = app.session.active_sketch.and_then(|s| state.sketch(s)).map(|ss| ss.plane);
let last = app.tool.as_ref().and_then(|t| t.pts.first()).map(|p| p.0);
plane.zip(last).and_then(|(plane, last)| proj.to_screen(plane.to_world(last)))
@@ -132,7 +132,7 @@ pub fn place_arc(app: &mut SolveApp, tool: &mut Tool, end: &(Vec2, Option<String
// On an existing point the arc ends on that point, so an outline closes. The other snaps
// (a midpoint, a place on a curve) name no point: their coordinates.
let on_point = |id: &&String| {
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
app.session.active_sketch.and_then(|s| state.sketch(s)).is_some_and(|ss| ss.sketch.resolve_point(id).is_some())
};
let to = end.1.as_ref().filter(on_point).map_or_else(|| json!([end.0.x, end.0.y]), |id| json!(id));
+7 -5
View File
@@ -282,7 +282,7 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
/// OK runs `sketch.equation_curve.edit` on it.
pub fn edit_equation(app: &mut SolveApp, id: &str) -> bool {
let Some(sid) = app.session.active_sketch else { return false };
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
let Some(eq) = state.sketch(sid).and_then(|s| s.sketch.equation_of(id)).cloned() else { return false };
let kind = Mod::Equation {
form: usize::from(eq.x.is_none()),
@@ -304,7 +304,7 @@ pub fn edit_equation(app: &mut SolveApp, id: &str) -> bool {
pub(crate) fn selected_equation(app: &SolveApp) -> Option<String> {
let [Sel::SketchCurve { id, .. }] = app.session.selection.as_slice() else { return None };
let sid = app.session.active_sketch?;
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
state.sketch(sid)?.sketch.equation_of(id).map(|e| e.curve.clone())
}
@@ -315,7 +315,7 @@ pub fn candidate(d: &Dialog, session: &solidcraft_engine::Session, hit: &Hit) ->
match hit {
Hit::SketchCurve { sketch, .. } | Hit::SketchPoint { sketch, .. } | Hit::Profile { sketch, .. } if *sketch != active => None,
Hit::SketchCurve { id, .. } if matches!(d.kind, Kind::SketchModify(Mod::Corner { .. })) => {
let state = session.world_state();
let state = crate::open_meshing::session_world(&session);
let sk = &state.sketch(active)?.sketch;
if sk.is_spatial_corner_curve(id) {
return None;
@@ -725,7 +725,9 @@ fn entity_ids(session: &solidcraft_engine::Session, inputs: &[SelInput], i: usiz
match x {
Sel::SketchCurve { id, .. } => out.push(id.clone()),
Sel::Profile { sketch, index } => {
if let Some(ids) = session.world_state().sketch(*sketch).and_then(|ss| ss.profiles.get(*index)).map(|p| p.curve_ids()) {
if let Some(ids) =
crate::open_meshing::session_world(&session).sketch(*sketch).and_then(|ss| ss.profiles.get(*index)).map(|p| p.curve_ids())
{
out.extend(ids);
}
}
@@ -937,7 +939,7 @@ pub fn commands(app: &SolveApp, k: &Mod, inputs: &[SelInput], extra: &Map<String
json!(axis)
} else {
let (origin, dir) = crate::dialogs::axis_of(app, sel).ok_or("Select an origin axis")?;
let st = app.session.world_state();
let st = crate::open_meshing::session_world(&app.session);
let plane =
st.sketch(app.session.active_sketch.ok_or("No sketch is being edited")?).ok_or("The sketch no longer exists")?.plane;
if plane.normal().dot(dir).abs() > 1e-9 || plane.normal().dot(origin - plane.origin).abs() > 1e-6 {
+5 -2
View File
@@ -694,7 +694,7 @@ mod tests {
let history = h.app.session.undo.len();
let r = h.app.run("sketch.project", json!({"refs":[{"sketch":1,"curve":"l1"},{"sketch":1,"curve":"c1"}]})).unwrap();
assert_eq!(r["links"].as_array().map(Vec::len), Some(2), "mode {m}: {r}");
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let sketch = &state.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch;
let kind = |line: bool| {
sketch
@@ -708,7 +708,10 @@ mod tests {
assert_eq!(kind(false), circle, "mode {m}: the circle");
assert_eq!(h.app.session.undo.len(), history + 1, "mode {m}: one undo step");
h.app.run("edit.undo", json!({})).unwrap();
assert!(h.app.session.world_state().sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch.curves.is_empty(), "mode {m}");
assert!(
crate::open_meshing::session_world(&h.app.session).sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch.curves.is_empty(),
"mode {m}"
);
set_mode(&mut h.app, 0);
}
}
+1 -1
View File
@@ -187,7 +187,7 @@ pub(crate) fn grab_inside(app: &SolveApp, proj: &Proj, pos: Pos2) -> Option<Hit>
return None;
}
let id = crate::sketch_text::selected_entity(app)?;
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
let ss = state.sketch(sketch)?;
let curve = ss.sketch.curves.iter().find(|c| c.id == id)?;
let straight = matches!(curve.kind, CurveKind::Line { .. });
+3 -3
View File
@@ -7,7 +7,7 @@ use crate::icons::markers::glyph_icon;
use crate::icons::markers::glyph_letter;
use std::cell::{Cell, RefCell};
use egui::{Color32, Pos2, Stroke};
use egui::{Pos2, Stroke};
use serde_json::{Value, json};
use solidcraft_engine::geom::{Plane, Vec2, Vec3};
use solidcraft_engine::kernel::Continuity;
@@ -544,7 +544,7 @@ pub(crate) fn lined_at(at: Vec2) -> Vec<Lined> {
/// The active sketch's point ids, to tell the points a command makes from those it had.
pub(crate) fn point_ids(app: &SolveApp) -> Vec<String> {
let st = app.session.world_state();
let st = crate::open_meshing::session_world(&app.session);
app.session.active_sketch.and_then(|id| st.sketch(id)).map(|ss| ss.sketch.points.iter().map(|p| p.id.clone()).collect()).unwrap_or_default()
}
@@ -554,7 +554,7 @@ pub(crate) fn point_ids(app: &SolveApp) -> Vec<String> {
pub(crate) fn constrain_lined(app: &mut SolveApp, tool: &mut Tool, placed: &[Vec2], before: &[String]) {
let lined = std::mem::take(&mut tool.lined);
let new: Vec<(String, Vec2)> = {
let st = app.session.world_state();
let st = crate::open_meshing::session_world(&app.session);
let Some(ss) = app.session.active_sketch.and_then(|id| st.sketch(id)) else { return };
ss.sketch.points.iter().filter(|p| !before.contains(&p.id)).map(|p| (p.id.clone(), p.pos)).collect()
};
+1 -1
View File
@@ -128,7 +128,7 @@ pub fn upright(z_down: f64, y_either: f64, size: [f64; 3]) -> Upright {
/// Which axis the bodies named `bodies` in the current design seem to have up. Looks at most
/// `MAX_BODIES` bodies and `MAX_TRIANGLES` triangles; a body that can't be meshed adds nothing.
pub fn estimate(app: &SolveApp, bodies: &[String]) -> Upright {
let world = app.session.world_state();
let world = crate::open_meshing::session_world(&app.session);
let mut areas = [0.0_f64; 3];
let mut bounds = solidcraft_engine::geom::Aabb3::EMPTY;
let mut budget = MAX_TRIANGLES;
+1 -1
View File
@@ -91,7 +91,7 @@ pub(crate) struct Contents {
}
pub(crate) fn contents(app: &SolveApp) -> Contents {
let st = app.session.world_state();
let st = crate::open_meshing::session_world(&app.session);
let sketch = app.session.active_sketch.and_then(|id| st.sketch(id));
let edited = app
.dialog
+2 -2
View File
@@ -56,7 +56,7 @@ pub(crate) fn transport_items(app: &SolveApp) -> Vec<crate::context_menu::Item>
/// that made the body.
fn geometry_feature(app: &SolveApp, body: &str, face: Option<usize>) -> Option<u64> {
face.and_then(|index| crate::context_menu::face_feature(app, body, index))
.or_else(|| app.session.world_state().body(body).map(|b| b.feature))
.or_else(|| crate::open_meshing::session_world(&app.session).body(body).map(|b| b.feature))
.filter(|id| app.session.doc.feature(*id).is_some())
}
@@ -1925,7 +1925,7 @@ mod tests {
let shown = |h: &Harness| h.app.shell.history.unwrap().contains(crate::scenario::handle_at("timeline:Hole").unwrap());
assert!(!shown(&h), "left={left}: the hole starts outside the timeline");
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
let body = state.bodies.first().unwrap();
let names = solidcraft_engine::doc::naming::face_names(body);
let index = names.iter().position(|name| solidcraft_engine::doc::persistent_name::made_by(name) == Some(hole)).unwrap();
+12 -14
View File
@@ -3261,7 +3261,7 @@ fn hit_candidate(app: &SolveApp, h: &Hit) -> Option<(Hit, Sel)> {
/// Root bodies have no occurrence row, so retain their body identity.
pub(crate) fn component_selection(app: &SolveApp, body: &str) -> Sel {
let doc = app.session.placed_document();
let state = app.session.world_state();
let state = crate::open_meshing::session_world(&app.session);
if let Some(b) = state.body(body) {
let component = doc.model_body_component(b);
let model = app.session.model.state();
@@ -3919,12 +3919,10 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
face_tris(&mut sc, &shown(b), *face, c4(t.error.gamma_multiply(0.35)), false);
let m = drawn_mesh(app, b);
let hidden = sc.color(c4(t.error.gamma_multiply(0.45)));
let colours = EdgeColours { halo: sc.color(c4(t.error.gamma_multiply(0.5))), core: sc.color(c4(t.error)), hidden };
for (ei, e) in m.edges.iter().enumerate() {
if m.edge_faces.get(ei).is_some_and(|f| f.iter().any(|x| *x as usize == *face)) && m.edge_pickable(ei) {
edge_lines(&mut sc, e, t.error, t.error.gamma_multiply(0.5), 3.0, &edge_normals(&m, e));
for p in e.windows(2) {
sc.hidden_line(p[0].to_f32(), p[1].to_f32(), hidden, 1.4);
}
edge_lines(&mut sc, e, colours, 3.0, &edge_normals(&m, e));
}
}
}
@@ -9845,7 +9843,7 @@ mod tests {
fn body_input_hover_lights_the_whole_body_and_refreshes_for_face_inputs() {
let mut app = cylinder_app();
app.set_origin_shown(false);
let body = app.session.world_state().bodies[0].clone();
let body = crate::open_meshing::session_world(&app.session).bodies[0].clone();
let hit = Hit::Face { body: body.name.clone(), index: 0, point: Vec3::ZERO };
app.start("file.save_mesh");
app.viewport.hover = Some(hit.clone());
@@ -11825,9 +11823,9 @@ mod tests {
let imported = h.app.run("sketch.insert_svg", json!({"text": svg})).unwrap();
let sketch = imported["sketch"].as_u64().unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
assert!(h.app.session.world_state().bodies.is_empty(), "the import leaves a sketch, not a body");
assert!(crate::open_meshing::session_world(&h.app.session).bodies.is_empty(), "the import leaves a sketch, not a body");
h.call("ui.view", json!({"view": "top"}));
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let ss = st.sketch(sketch).unwrap();
assert_eq!(ss.profiles.len(), 1, "the imported square is one region");
let (low, high) = ss.sketch.bounds().unwrap();
@@ -11848,7 +11846,7 @@ mod tests {
h.app.run("sketch.finish", json!({})).unwrap();
h.call("ui.view", json!({"view": "top"}));
h.call("ui.view", json!({"view": "fit"}));
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let ss = st.sketch(sketch).unwrap();
assert_eq!(ss.profiles.len(), 2, "the imported squares are two regions");
// The world box of each region, a little outside its edges.
@@ -11892,13 +11890,13 @@ mod tests {
let sketch = imported["sketch"].as_u64().unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.settle();
assert_eq!(h.app.session.world_state().bodies.len(), 1, "the design has a body and a sketch");
assert!(!h.app.session.world_state().bodies[0].mesh().edges.is_empty(), "the body is meshed");
assert_eq!(crate::open_meshing::session_world(&h.app.session).bodies.len(), 1, "the design has a body and a sketch");
assert!(!crate::open_meshing::session_world(&h.app.session).bodies[0].mesh().edges.is_empty(), "the body is meshed");
h.call("ui.view", json!({"view": "top"}));
h.settle();
h.call("ui.view", json!({"view": "fit"}));
h.settle();
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let ss = st.sketch(sketch).unwrap();
let (low, high) = ss.sketch.bounds().unwrap();
// The screen box of the body and of the region: the marquee covers both.
@@ -11961,7 +11959,7 @@ mod tests {
// The Patch dialog takes its boundary from profiles or edges.
h.step(&json!({"start": "surface.patch"})).unwrap();
assert!(h.app.dialog.is_some(), "the patch dialog is open");
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let ss = st.sketch(sketch).unwrap();
let (low, high) = ss.sketch.bounds().unwrap();
let a = ss.plane.to_world(Vec2::new(low.x - 5.0, low.y - 5.0));
@@ -12742,7 +12740,7 @@ mod grid_plane_tests {
assert_eq!(h.app.viewport.hover_plane.as_deref(), Some("XY"));
assert!(on(previewed_sketch_plane(&h.app), "XY"), "XY under the pointer in the browser");
h.step(&json!({"click":{"plane":"XZ"}})).unwrap();
let state = h.app.session.world_state();
let state = crate::open_meshing::session_world(&h.app.session);
assert!(on(state.sketches.first().map(|s| s.plane), "XZ"), "the click sketches on XZ");
assert!(previewed_sketch_plane(&h.app).is_none(), "the open sketch has its own grid");
}
@@ -207,7 +207,7 @@ fn a_material_colour_edit_its_undo_and_redo_reach_the_viewport() {
app.ui.ground_shadow = false;
app.set_origin_shown(false);
app.run("solid.box", json!({"length": 20, "width": 15, "height": 10})).unwrap();
let name = app.session.world_state().bodies[0].name.clone();
let name = crate::open_meshing::session_world(&app.session).bodies[0].name.clone();
app.run("material.create", json!({"name": "Wood dark", "density_g_cm3": 0.7, "color": "#c8a573"})).unwrap();
app.run("material.assign", json!({"bodies": [name], "material": "Wood dark"})).unwrap();
let colour = |app: &SolveApp| {
@@ -500,7 +500,7 @@ fn conflicting_curves_points_glyphs_and_dimensions_take_the_conflict_colour_unti
h.app.edit_sketch(reach);
h.settle();
assert_eq!(h.app.session.active_sketch, Some(reach));
let st = h.app.session.world_state();
let st = crate::open_meshing::session_world(&h.app.session);
let ss = st.sketch(reach).unwrap();
assert_eq!(!ss.report.ok(), conflicting, "w = {w}: {:?}", ss.report);
let conflict = super::Conflict::of(&h.app, &ss.sketch, &ss.report);
+2 -1
View File
@@ -282,7 +282,8 @@ pub(crate) fn legacy_legends(app: &SolveApp, ui: &mut egui::Ui, viewport: egui::
if (!app.preferences.viewport_analysis_status || !app.preferences.viewport_status_details) && !app.session.continuity.is_empty() {
let t = crate::theme::Tokens::get();
// Best to worst: the curvature-continuous orders share a colour, then a gap between faces.
let mut classes: Vec<Option<solidcraft_engine::kernel::Continuity>> = solidcraft_engine::kernel::Continuity::ALL.into_iter().rev().map(Some).collect();
let mut classes: Vec<Option<solidcraft_engine::kernel::Continuity>> =
solidcraft_engine::kernel::Continuity::ALL.into_iter().rev().map(Some).collect();
classes.push(None);
let key = classes
.into_iter()
@@ -1674,7 +1674,7 @@ fn selected_sketch_point_aliases_respect_owner_and_visibility() {
h.call("ui.view", json!({"view":"top"}));
h.settle();
let sid = h.app.session.active_sketch.unwrap();
let origin = h.app.session.world_state().sketch(sid).unwrap().sketch.points[0].id.clone();
let origin = crate::open_meshing::session_world(&h.app.session).sketch(sid).unwrap().sketch.points[0].id.clone();
let ids = [origin, "custom:point".into(), "l1.end".into(), format!("{sid}:l1.end"), format!("{}:l1.start", sid + 1)];
h.app.run("select.set", json!({"items":ids.iter().map(|id| json!({"type":"sketch_point","id":id})).collect::<Vec<_>>()})).unwrap();
let rings = point_selection_rings(&point_overlays(&h.app));
+1 -1
View File
@@ -26,7 +26,7 @@ fn bearing(kind: &str) -> (Harness, u64) {
h.app.run("joint.create", json!({"type":kind,"a":at("Housing"),"b":at("Bearing"),"flip":true,"angle":0,"name":"Guide"})).unwrap()["joint"]
.as_u64()
.unwrap();
let st = h.app.session.world_state();
let st = h.app.session.world_state().unwrap();
let body = st.bodies.iter().find(|b| h.app.session.doc.model_body_component(b) == part["component"].as_u64().unwrap()).unwrap();
let mesh = body.mesh();
let face = mesh