Rename SolveCraft to SolidCraft everywhere, with compatibility shims (#389)

* WIP deep rename (not for push)

* WIP2

* WIP3

* WIP4 wxs fix

* rustfmt

* Rename the remaining identifiers in merged crash-report code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-12 17:17:01 -04:00
1 parent 27415e4dd7
commit 296683229d
464 files changed
+3408 -2956

No files matched your search

@@ -13,7 +13,7 @@ fn replay(script: &str) -> Session {
}
fn volume(s: &Session, body: &str) -> f64 {
s.model.state().body(body).map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).unwrap_or(0.0)
s.model.state().body(body).map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).unwrap_or(0.0)
}
/// Slab less a 557.68 × 100 × 120 pocket and the loft below its floor (556.68 × 99 down 4 mm to
+12 -12
View File
@@ -6,7 +6,7 @@
use serde_json::{Value, json};
use solvecraft_doc::FeatureKind;
use solidcraft_doc::FeatureKind;
use super::CommandSpec;
use crate::params::{bad, bool_, str_};
@@ -92,7 +92,7 @@ fn group(s: &mut Session, p: &Value) -> Result<Value> {
for g in d.browser_groups.iter_mut().filter(|g| g.component == component && g.folder == folder) {
g.items.retain(|k| !items.contains(k));
}
d.browser_groups.push(solvecraft_doc::BrowserGroup { id, name: name.clone(), component, folder, items });
d.browser_groups.push(solidcraft_doc::BrowserGroup { id, name: name.clone(), component, folder, items });
Ok(json!({ "group": id, "name": name }))
}
@@ -326,7 +326,7 @@ fn document_units(s: &mut Session, p: &Value) -> Result<Value> {
.doc
.params
.iter()
.filter(|p| !p.model && p.unit.is_empty() && solvecraft_doc::expr::is_literal(&p.expr) && p.expr.trim().parse::<f64>().is_ok())
.filter(|p| !p.model && p.unit.is_empty() && solidcraft_doc::expr::is_literal(&p.expr) && p.expr.trim().parse::<f64>().is_ok())
.map(|p| (p.name.clone(), p.expr.trim().to_string()))
.collect();
for (name, e) in bare {
@@ -354,7 +354,7 @@ fn document_units(s: &mut Session, p: &Value) -> Result<Value> {
fn view_save(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "view.save";
let c = p.get("camera").ok_or_else(|| bad(cmd, "`camera` is required"))?;
let mut v: solvecraft_doc::NamedView = serde_json::from_value(c.clone()).map_err(|e| bad(cmd, format!("bad camera: {e}")))?;
let mut v: solidcraft_doc::NamedView = serde_json::from_value(c.clone()).map_err(|e| bad(cmd, format!("bad camera: {e}")))?;
let ok = [v.target.x, v.target.y, v.target.z, v.yaw, v.pitch, v.distance, v.fov].iter().all(|x| x.is_finite()) && v.distance > 0.0;
if !ok {
return Err(bad(cmd, "the camera must be finite with a positive distance"));
@@ -405,7 +405,7 @@ fn view_list(s: &mut Session, _p: &Value) -> Result<Value> {
}
/// Rename an item key in the browser's groups and orders of a folder.
fn rename_key(d: &mut solvecraft_doc::Document, folder: &str, old: &str, new: &str) {
fn rename_key(d: &mut solidcraft_doc::Document, folder: &str, old: &str, new: &str) {
for g in d.browser_groups.iter_mut().filter(|g| g.folder == folder) {
for k in g.items.iter_mut().filter(|k| *k == old) {
*k = new.to_string();
@@ -472,7 +472,7 @@ fn rename_body(s: &mut Session, p: &Value) -> Result<Value> {
let bodies = std::collections::BTreeMap::from([(old.clone(), name.to_string())]);
anchor.remap_target(&ids, &bodies)?;
for link in &mut sketch.links {
if link.auto_face && matches!(&link.source,solvecraft_sketch::LinkSource::Face {body,..} if *body==old) {
if link.auto_face && matches!(&link.source,solidcraft_sketch::LinkSource::Face {body,..} if *body==old) {
link.face_name = Some(anchor.target.name.clone());
}
}
@@ -499,10 +499,10 @@ fn rename_body(s: &mut Session, p: &Value) -> Result<Value> {
}
for column in &mut d.configs.columns {
match column {
solvecraft_doc::config::Column::Material(body) | solvecraft_doc::config::Column::Appearance(body) if *body == old => {
solidcraft_doc::config::Column::Material(body) | solidcraft_doc::config::Column::Appearance(body) if *body == old => {
*body = name.to_string();
}
solvecraft_doc::config::Column::FaceAppearance(target) if target.body == old => {
solidcraft_doc::config::Column::FaceAppearance(target) if target.body == old => {
target.body = name.to_string();
}
_ => {}
@@ -592,7 +592,7 @@ fn redefine_sketch(s: &mut Session, p: &Value) -> Result<Value> {
.filter(|link| {
link.auto_face
&& link.face_name.as_ref() == Some(&anchor.target.name)
&& matches!(&link.source,solvecraft_sketch::LinkSource::Face {body,..} if *body==anchor.target.body)
&& matches!(&link.source,solidcraft_sketch::LinkSource::Face {body,..} if *body==anchor.target.body)
})
.map(|link| link.id.clone())
.collect();
@@ -723,7 +723,7 @@ mod tests {
run(&mut s, "view.save", json!({"camera": cam, "name": "Detail"}));
run(&mut s, "view.rename", json!({"view": "Named View1", "name": "Overview"}));
assert!(s.execute("view.rename", &json!({"view": "Overview", "name": "Detail"})).is_err());
let back: solvecraft_doc::Document = serde_json::from_str(&serde_json::to_string(&*s.doc).unwrap()).unwrap();
let back: solidcraft_doc::Document = serde_json::from_str(&serde_json::to_string(&*s.doc).unwrap()).unwrap();
assert_eq!(back.named_views.len(), 2);
assert_eq!(back.named_views[0].target.z, 3.0);
run(&mut s, "view.delete", json!({"view": "Detail"}));
@@ -748,7 +748,7 @@ mod tests {
run(&mut s, "construct.plane.offset", json!({"base": "XY", "offset": 5, "name": "Floor"}));
let body = s.model.state().bodies[0].name.clone();
let drain_name = s.doc.feature(drain).unwrap().name.clone();
let cam = solvecraft_render::Camera::default();
let cam = solidcraft_render::Camera::default();
let lines = |s: &Session| crate::view::scene(s, &cam).lines.len();
let shown = |s: &mut Session| {
let d = run(s, "document.inspect", json!({}));
@@ -806,7 +806,7 @@ mod tests {
run(&mut s, "browser.move", json!({"folder": "bodies", "items": [b.clone()]}));
assert_eq!(s.doc.browser_groups[0].items, vec!["Bolt".to_string()]);
run(&mut s, "browser.order", json!({"folder": "bodies", "order": [b.clone(), "Bolt"]}));
let saved: solvecraft_doc::Document = serde_json::from_str(&serde_json::to_string(&*s.doc).unwrap()).unwrap();
let saved: solidcraft_doc::Document = serde_json::from_str(&serde_json::to_string(&*s.doc).unwrap()).unwrap();
assert_eq!(saved.browser_groups, s.doc.browser_groups);
assert_eq!(saved.browser_order["0/bodies"], vec![b, "Bolt".to_string()]);
run(&mut s, "browser.ungroup", json!({"group": g}));
+6 -6
View File
@@ -5,7 +5,7 @@
use std::collections::BTreeMap;
use serde_json::{Value, json};
use solvecraft_doc::{FeatureKind, PlaneRef};
use solidcraft_doc::{FeatureKind, PlaneRef};
use super::CommandSpec;
use crate::params::{bad, string_list, vec3};
@@ -31,7 +31,7 @@ fn copy(s: &mut Session, p: &Value) -> Result<Value> {
ids.push(s.doc.find_feature(k).map(|f| f.id).ok_or_else(|| bad(cmd, format!("no feature `{k}`")))?);
}
// Keep timeline order so sketches come before the features that use them.
let copied: Vec<solvecraft_doc::Feature> = s.doc.features.iter().filter(|f| ids.contains(&f.id)).cloned().collect();
let copied: Vec<solidcraft_doc::Feature> = s.doc.features.iter().filter(|f| ids.contains(&f.id)).cloned().collect();
let names: Vec<String> = copied.iter().map(|f| f.name.clone()).collect();
s.clipboard = copied;
Ok(json!({"copied": names}))
@@ -41,7 +41,7 @@ fn copy(s: &mut Session, p: &Value) -> Result<Value> {
fn explicit_planes(p: &mut PlaneRef) {
match p {
PlaneRef::Origin { name } => {
if let Some(plane) = solvecraft_geom::Plane::named(name) {
if let Some(plane) = solidcraft_geom::Plane::named(name) {
*p = PlaneRef::Custom { plane };
}
}
@@ -66,7 +66,7 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
let mut used: std::collections::BTreeSet<String> = s.model.state().bodies.iter().map(|body| body.name.clone()).collect();
let mut notices = Vec::new();
let author_path = s.active_occurrence_path.clone().unwrap_or_else(|| s.doc.first_path(comp));
solvecraft_doc::EmbossFace::component_frame(&s.placed_document(), comp, &author_path)?;
solidcraft_doc::EmbossFace::component_frame(&s.placed_document(), comp, &author_path)?;
let mut names: BTreeMap<String, String> = BTreeMap::new();
let mut made = Vec::new();
for f in feats {
@@ -92,7 +92,7 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
if let FeatureKind::Sketch { sketch, .. } = &mut kind {
for link in &mut sketch.links {
if link.auto_face {
if let solvecraft_sketch::LinkSource::Face { body, .. } = &mut link.source
if let solidcraft_sketch::LinkSource::Face { body, .. } = &mut link.source
&& let Some(mapped) = bodies.get(body)
{
*body = mapped.clone();
@@ -138,7 +138,7 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
if let FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } = &mut kind {
explicit_planes(plane);
}
let mut m = solvecraft_doc::IDENTITY;
let mut m = solidcraft_doc::IDENTITY;
m[3][0] = t.x;
m[3][1] = t.y;
m[3][2] = t.z;
+15 -15
View File
@@ -1,11 +1,11 @@
//! Components and occurrences: the design as a tree (see `solvecraft_doc` assembly docs).
//! Components and occurrences: the design as a tree (see `solidcraft_doc` assembly docs).
//! New sketches and features go into the active component, authored in its frame; bodies
//! belong to their feature's component unless moved; occurrences place components (instances
//! share contents, "paste new" copies them) and can be grounded and moved.
use serde_json::{Value, json};
use solvecraft_doc::{FeatureKind, Mat, Occurrence};
use solvecraft_geom::Vec3;
use solidcraft_doc::{FeatureKind, Mat, Occurrence};
use solidcraft_geom::Vec3;
use super::CommandSpec;
use crate::params::{bad, bool_, str_, string_list, vec3};
@@ -70,7 +70,7 @@ fn occurrence(s: &Session, p: &Value, cmd: &str) -> Result<u64> {
}
fn translation(t: Vec3) -> Mat {
solvecraft_doc::rigid(t, Vec3::ZERO, Vec3::Z, 0.0)
solidcraft_doc::rigid(t, Vec3::ZERO, Vec3::Z, 0.0)
}
fn move_vector(p: &Value, key: &str, default: Vec3, coordinate: bool, cmd: &str) -> Result<Vec3> {
@@ -285,7 +285,7 @@ fn copy_occurrence(s: &mut Session, p: &Value) -> Result<Value> {
let parent = if p.get("parent").is_some() { component(s, p, "parent", cmd)? } else { src.parent };
let t = move_vector(p, "translate", Vec3::ZERO, true, cmd)?;
let base = s.pending_moves.get(&src.id).unwrap_or(&src.transform);
let m = solvecraft_doc::mat_mul(&translation(t), base);
let m = solidcraft_doc::mat_mul(&translation(t), base);
if !valid_position(&m) {
return Err(bad(cmd, "copy transform exceeds supported numeric range"));
}
@@ -314,8 +314,8 @@ fn paste_new(s: &mut Session, p: &Value) -> Result<Value> {
s.doc.occurrence_of(comp).cloned()
};
let t = move_vector(p, "translate", Vec3::ZERO, true, cmd)?;
let base = occ.as_ref().map(|o| s.pending_moves.get(&o.id).copied().unwrap_or(o.transform)).unwrap_or(solvecraft_doc::IDENTITY);
let transform = solvecraft_doc::mat_mul(&translation(t), &base);
let base = occ.as_ref().map(|o| s.pending_moves.get(&o.id).copied().unwrap_or(o.transform)).unwrap_or(solidcraft_doc::IDENTITY);
let transform = solidcraft_doc::mat_mul(&translation(t), &base);
if !valid_position(&transform) {
return Err(bad(cmd, "copy transform exceeds supported numeric range"));
}
@@ -361,7 +361,7 @@ fn move_occurrence(s: &mut Session, p: &Value) -> Result<Value> {
let t = move_vector(p, "translate", Vec3::ZERO, true, cmd)?;
let axis = move_vector(p, "axis", Vec3::Z, false, cmd)?;
let angle = match p.get("angle") {
Some(Value::String(e)) => s.doc.eval(e, solvecraft_doc::expr::Kind::Angle).map_err(|e| bad(cmd, e.to_string()))?,
Some(Value::String(e)) => s.doc.eval(e, solidcraft_doc::expr::Kind::Angle).map_err(|e| bad(cmd, e.to_string()))?,
None => 0.0,
Some(value) => value.as_f64().ok_or_else(|| bad(cmd, "angle must be a number or angle expression"))?,
};
@@ -372,7 +372,7 @@ fn move_occurrence(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, "rotation axis must have a finite nonzero length"));
}
let origin = move_vector(p, "origin", Vec3::ZERO, true, cmd)?;
let delta = solvecraft_doc::rigid(t, origin, axis, angle);
let delta = solidcraft_doc::rigid(t, origin, axis, angle);
let space = match p.get("space") {
None => "local",
Some(Value::String(space)) => space.as_str(),
@@ -384,8 +384,8 @@ fn move_occurrence(s: &mut Session, p: &Value) -> Result<Value> {
let path = if p.get("path").is_some() || space == "world" { Some(occurrence_path(s, p, id, cmd)?) } else { None };
let delta = if space == "world" {
let placement = s.occurrence_placement(path.as_deref().unwrap_or(&[]))?;
let inverse = solvecraft_doc::mat_inverse(&placement.parent_world).ok_or_else(|| bad(cmd, "parent frame is singular"))?;
solvecraft_doc::mat_mul(&solvecraft_doc::mat_mul(&inverse, &delta), &placement.parent_world)
let inverse = solidcraft_doc::mat_inverse(&placement.parent_world).ok_or_else(|| bad(cmd, "parent frame is singular"))?;
solidcraft_doc::mat_mul(&solidcraft_doc::mat_mul(&inverse, &delta), &placement.parent_world)
} else {
delta
};
@@ -396,7 +396,7 @@ fn move_occurrence(s: &mut Session, p: &Value) -> Result<Value> {
|| base[1][3].abs() > 1e-12
|| base[2][3].abs() > 1e-12
|| (base[3][3] - 1.0).abs() > 1e-12
|| solvecraft_doc::mat_inverse(&base).is_none()
|| solidcraft_doc::mat_inverse(&base).is_none()
{
return Err(bad(cmd, "base_transform must be finite, affine and invertible"));
}
@@ -404,7 +404,7 @@ fn move_occurrence(s: &mut Session, p: &Value) -> Result<Value> {
} else {
s.pending_moves.get(&id).copied().unwrap_or(o.transform)
};
let next = solvecraft_doc::mat_mul(&delta, &cur);
let next = solidcraft_doc::mat_mul(&delta, &cur);
if !valid_position(&next) {
return Err(bad(cmd, "move transform exceeds supported numeric range"));
}
@@ -474,10 +474,10 @@ pub fn to_active_frame(s: &Session, kind: &mut FeatureKind) {
return;
}
let m = s.active_component_frame();
if solvecraft_doc::is_identity(&m) {
if solidcraft_doc::is_identity(&m) {
return;
}
if let Some(inv) = solvecraft_doc::mat_inverse(&m) {
if let Some(inv) = solidcraft_doc::mat_inverse(&m) {
kind.transform_geometry(&inv);
}
}
+24 -24
View File
@@ -4,12 +4,12 @@ use std::collections::{BTreeMap, BTreeSet};
use crate::params::bad;
use crate::{Result, Sel, Session};
use serde_json::{Value, json};
use solvecraft_doc::{Document, Feature, FeatureKind, Mat, Occurrence};
use solidcraft_doc::{Document, Feature, FeatureKind, Mat, Occurrence};
const CMD: &str = "component.paste_new";
const MAX_WORK: usize = 10_000;
fn mesh_distance(mesh: &solvecraft_geom::Mesh, point: solvecraft_geom::Vec3) -> f64 {
fn mesh_distance(mesh: &solidcraft_geom::Mesh, point: solidcraft_geom::Vec3) -> f64 {
mesh.triangles
.iter()
.filter_map(|triangle| mesh.tri(triangle))
@@ -32,7 +32,7 @@ fn mesh_distance(mesh: &solvecraft_geom::Mesh, point: solvecraft_geom::Vec3) ->
.fold(f64::INFINITY, f64::min)
}
fn material_probe_matches(a: &solvecraft_geom::Mesh, b: &solvecraft_geom::Mesh, point: solvecraft_geom::Vec3, tol_a: f64, tol_b: f64) -> bool {
fn material_probe_matches(a: &solidcraft_geom::Mesh, b: &solidcraft_geom::Mesh, point: solidcraft_geom::Vec3, tol_a: f64, tol_b: f64) -> bool {
if !point.is_finite() || !tol_a.is_finite() || !tol_b.is_finite() || tol_a <= 0. || tol_b <= 0. {
return false;
}
@@ -47,7 +47,7 @@ fn material_probe_matches(a: &solvecraft_geom::Mesh, b: &solvecraft_geom::Mesh,
let offset = 4. * tol_a.max(tol_b);
let mut material = false;
let mut void = false;
for axis in [solvecraft_geom::Vec3::X, solvecraft_geom::Vec3::Y, solvecraft_geom::Vec3::Z] {
for axis in [solidcraft_geom::Vec3::X, solidcraft_geom::Vec3::Y, solidcraft_geom::Vec3::Z] {
for sign in [-1., 1.] {
let q = point + axis * (sign * offset);
if !q.is_finite() {
@@ -294,7 +294,7 @@ pub(super) fn paste(
if body.body.is_mesh() {
return Err(bad(CMD, "Paste New of a reassigned mesh body is not supported yet"));
}
let offset = source.model_body_offset(body).copied().unwrap_or(solvecraft_doc::IDENTITY);
let offset = source.model_body_offset(body).copied().unwrap_or(solidcraft_doc::IDENTITY);
// An external transfer establishes the local baseline in this graph.
// Freeze AFTER that boundary, then replay it as an identity ownership event.
let entry = feats.iter().find(|f| {
@@ -313,8 +313,8 @@ pub(super) fn paste(
let entered = entry.and_then(|f| s.model.result(f.id)).map(|r| r.state.clone());
let base = entered.as_ref().and_then(|st| st.body(&body.name)).or_else(|| before.as_ref().and_then(|st| st.body(&body.name))).unwrap_or(body);
let baseline_component = source.model_body_component(base);
let geometry = solvecraft_kernel::transform_matrix(&base.body, offset)?;
let step = solvecraft_kernel::step_export(&[&geometry], "SolidCraft independent reassigned body")?;
let geometry = solidcraft_kernel::transform_matrix(&base.body, offset)?;
let step = solidcraft_kernel::step_export(&[&geometry], "SolidCraft independent reassigned body")?;
let new_name = unique(&body.name, &mut used);
bodies.insert(body.name.clone(), new_name.clone());
let feature_name = unique(&format!("{} Base", body.name), &mut used);
@@ -351,7 +351,7 @@ pub(super) fn paste(
&& let Some(p) = source_parameters.get(name.as_str())
&& p.model
{
if scratch.doc.params.len() >= solvecraft_doc::MAX_PARAMS {
if scratch.doc.params.len() >= solidcraft_doc::MAX_PARAMS {
return Err(bad(CMD, "too many copied parameters"));
}
let new = scratch.doc_mut().new_model_param(&p.expr, &p.unit);
@@ -367,7 +367,7 @@ pub(super) fn paste(
if let Some(p) = source_parameters.get(old.as_str())
&& p.model
{
let expr = solvecraft_doc::clone_expression(&p.expr, &parameters);
let expr = solidcraft_doc::clone_expression(&p.expr, &parameters);
if let Some(p) = scratch.doc_mut().params.iter_mut().find(|p| p.name == *new) {
p.expr = expr;
p.comment = source_parameters.get(old.as_str()).map(|p| p.comment.clone()).unwrap_or_default();
@@ -389,7 +389,7 @@ pub(super) fn paste(
*from = *mapped;
} else {
*from = *to;
*transform = solvecraft_doc::IDENTITY;
*transform = solidcraft_doc::IDENTITY;
}
}
if let FeatureKind::Emboss { face: Some(face), .. } = &mut f.kind
@@ -466,15 +466,15 @@ pub(super) fn paste(
return Err(bad(CMD, "cannot copy strict face paint through uncertified foreign re-export"));
}
if let Some(name) = &face.name {
face.point = solvecraft_doc::naming::point_on_face(&state, name, face.point)
face.point = solidcraft_doc::naming::point_on_face(&state, name, face.point)
.map(|(point, _)| point)
.ok_or_else(|| bad(CMD, "source frozen face appearance lost its identity"))?;
}
let source_body = state.body(&face.body).ok_or_else(|| bad(CMD, "source frozen face appearance lost its body"))?;
face.point = solvecraft_doc::apply_point(source.model_body_offset(source_body).unwrap_or(&solvecraft_doc::IDENTITY), face.point);
face.point = solidcraft_doc::apply_point(source.model_body_offset(source_body).unwrap_or(&solidcraft_doc::IDENTITY), face.point);
face.name = None;
} else if let Some(name) = &mut face.name {
*name = solvecraft_doc::remap_topology(name, &features, &bodies);
*name = solidcraft_doc::remap_topology(name, &features, &bodies);
}
face.body = new.clone();
if let Some(certificate) = &mut face.certificate {
@@ -484,7 +484,7 @@ pub(super) fn paste(
}
}
// Typed configuration and assembly helpers preserve original targets.
scratch.doc_mut().configs.clone_targets(&solvecraft_doc::config::ConfigCloneMaps {
scratch.doc_mut().configs.clone_targets(&solidcraft_doc::config::ConfigCloneMaps {
features: features.clone(),
components: components.clone(),
bodies: bodies.clone(),
@@ -500,7 +500,7 @@ pub(super) fn paste(
notices.push(format!("{} retains external selected occurrence path {:?}", feature.name, face.path));
}
if let FeatureKind::Sketch { sketch, .. } = &feature.kind {
use solvecraft_sketch::LinkSource as L;
use solidcraft_sketch::LinkSource as L;
let mut pending: Vec<_> = sketch.links.iter().map(|l| &l.source).collect();
let mut work = 0;
while let Some(link) = pending.pop() {
@@ -563,7 +563,7 @@ pub(super) fn paste(
}
}
for expression in feature.kind.expressions() {
for name in solvecraft_doc::expr::references(expression) {
for name in solidcraft_doc::expr::references(expression) {
reference_work += 1;
if reference_work > 100_000 {
return Err(bad(CMD, "expression references exceed copy work limit"));
@@ -590,8 +590,8 @@ pub(super) fn paste(
if bodies.iter().any(|(old, new)| frozen.contains(old) && *new == face.body) {
let body = resolved.body(&face.body).ok_or_else(|| bad(CMD, "missing frozen face body"))?;
let face_index =
solvecraft_doc::appearance::face_index_at(body, face.point).ok_or_else(|| bad(CMD, "cannot reproduce frozen face appearance"))?;
let name = solvecraft_doc::naming::face_names(body).get(face_index).cloned().ok_or_else(|| bad(CMD, "missing frozen face identity"))?;
solidcraft_doc::appearance::face_index_at(body, face.point).ok_or_else(|| bad(CMD, "cannot reproduce frozen face appearance"))?;
let name = solidcraft_doc::naming::face_names(body).get(face_index).cloned().ok_or_else(|| bad(CMD, "missing frozen face identity"))?;
face_updates.push((index, name));
}
}
@@ -638,10 +638,10 @@ pub(super) fn paste(
for body in state.bodies.iter().filter(|b| selected.contains(&source.model_body_component(b))) {
let new = bodies.get(&body.name).ok_or_else(|| bad(CMD, format!("body {} has no clone identity", body.name)))?;
let cloned = copied_bodies.get(new.as_str()).ok_or_else(|| bad(CMD, format!("copy did not reproduce body {}", body.name)))?;
let a = solvecraft_kernel::transform_matrix(&body.body, *source.model_body_offset(body).unwrap_or(&solvecraft_doc::IDENTITY))?;
let b = solvecraft_kernel::transform_matrix(&cloned.body, *scratch.doc.model_body_offset(cloned).unwrap_or(&solvecraft_doc::IDENTITY))?;
let ma = solvecraft_kernel::measure(&a)?;
let mb = solvecraft_kernel::measure(&b)?;
let a = solidcraft_kernel::transform_matrix(&body.body, *source.model_body_offset(body).unwrap_or(&solidcraft_doc::IDENTITY))?;
let b = solidcraft_kernel::transform_matrix(&cloned.body, *scratch.doc.model_body_offset(cloned).unwrap_or(&solidcraft_doc::IDENTITY))?;
let ma = solidcraft_kernel::measure(&a)?;
let mb = solidcraft_kernel::measure(&b)?;
let tolerance = 1e-5 * (1.0 + (ma.bbox.max - ma.bbox.min).len()) + 64.0 * f64::EPSILON * (ma.bbox.min.len() + ma.bbox.max.len());
let mut material_matches = true;
let tol_a = (a.size() * 1e-3).max(1e-3).max(a.size() * 1e-6);
@@ -658,7 +658,7 @@ pub(super) fn paste(
for y in [0.2, 0.5, 0.8] {
for z in [0.2, 0.5, 0.8] {
let d = ma.bbox.max - ma.bbox.min;
let p = ma.bbox.min + solvecraft_geom::Vec3::new(d.x * x, d.y * y, d.z * z);
let p = ma.bbox.min + solidcraft_geom::Vec3::new(d.x * x, d.y * y, d.z * z);
let roundoff = 64. * f64::EPSILON * (1. + p.len());
material_matches &= material_probe_matches(&mesh_a, &mesh_b, p, tol_a + roundoff, tol_b + roundoff);
}
@@ -668,7 +668,7 @@ pub(super) fn paste(
// chord-volume biases after STEP. Only independently certified current
// sections may replace that volume comparison; shape mismatches still
// refuse even when their volumes happen to agree.
let volume_matches = match solvecraft_kernel::certified_prism_equivalent(&a, &b)? {
let volume_matches = match solidcraft_kernel::certified_prism_equivalent(&a, &b)? {
Some(same) => same,
None => (ma.volume - mb.volume).abs() <= 1e-5 * (1.0 + ma.volume.abs()),
};
@@ -2,8 +2,8 @@
use std::collections::{BTreeMap, BTreeSet};
use solvecraft_doc::Document;
use solvecraft_doc::joints::{Assembly, JointOrigin, Snap};
use solidcraft_doc::Document;
use solidcraft_doc::joints::{Assembly, JointOrigin, Snap};
use crate::{EngineError, Result};
@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used, clippy::panic)]
use super::*;
use solvecraft_doc::joints::{Joint, JointKind, MotionLink, NamedOrigin};
use solvecraft_doc::motion::{ContactSet, ExplodedView, MotionStudy, StudyKey};
use solvecraft_doc::{IDENTITY, Occurrence};
use solvecraft_geom::Vec3;
use solidcraft_doc::joints::{Joint, JointKind, MotionLink, NamedOrigin};
use solidcraft_doc::motion::{ContactSet, ExplodedView, MotionStudy, StudyKey};
use solidcraft_doc::{IDENTITY, Occurrence};
use solidcraft_geom::Vec3;
fn at(occurrence: u64) -> JointOrigin {
JointOrigin { occurrence, snap: Snap::Point { point: Vec3::ZERO, z: None } }
+43 -43
View File
@@ -28,8 +28,8 @@ fn coincident_independent_primitives_keep_their_own_semantic_faces() {
let state = s.model.state();
let a = state.body("Cube").unwrap();
let b = state.body("Other").unwrap();
let a_names = solvecraft_doc::naming::face_names(a);
let b_names = solvecraft_doc::naming::face_names(b);
let a_names = solidcraft_doc::naming::face_names(a);
let b_names = solidcraft_doc::naming::face_names(b);
assert!(a_names.contains(&format!("F{first}:+z")));
assert!(b_names.contains(&format!("F{second}:+z")));
assert!(!b_names.contains(&format!("F{first}:+z")));
@@ -55,7 +55,7 @@ fn nested_definition_dag_and_histories_are_independent() {
near(volume(&mut s, parent), 24.0);
near(volume(&mut s, new), 16.0);
let saved = s.doc.to_json();
let reopened = solvecraft_doc::Document::from_json(&saved).unwrap();
let reopened = solidcraft_doc::Document::from_json(&saved).unwrap();
assert_eq!(reopened.occurrences.iter().filter(|o| o.parent == new).count(), 2);
}
@@ -144,7 +144,7 @@ fn generated_feature_parameter_dependencies_are_independent() {
.doc
.features
.iter()
.find(|f| f.component == clone && matches!(f.kind, solvecraft_doc::FeatureKind::Box { .. }))
.find(|f| f.component == clone && matches!(f.kind, solidcraft_doc::FeatureKind::Box { .. }))
.unwrap()
.param_names
.clone();
@@ -195,12 +195,12 @@ fn copied_sheet_base_targets_its_own_sketch_history() {
let copied = copy(&mut s, part);
let new = copied["component"].as_u64().unwrap();
near(volume(&mut s, new), source_volume);
let clone_sketch = s.doc.features.iter().find(|f| f.component == new && matches!(f.kind, solvecraft_doc::FeatureKind::Sketch { .. })).unwrap().id;
let clone_sketch = s.doc.features.iter().find(|f| f.component == new && matches!(f.kind, solidcraft_doc::FeatureKind::Sketch { .. })).unwrap().id;
assert_ne!(clone_sketch, sketch);
let base = s.doc.features.iter().find(|f| f.component == new && matches!(f.kind, solvecraft_doc::FeatureKind::SheetBase { .. })).unwrap();
let base = s.doc.features.iter().find(|f| f.component == new && matches!(f.kind, solidcraft_doc::FeatureKind::SheetBase { .. })).unwrap();
assert_eq!(base.kind.input_sketches(), vec![clone_sketch]);
// Mutate only source geometry, then verify the copied base remains unchanged.
if let solvecraft_doc::FeatureKind::Sketch { sketch, .. } = &mut s.doc_mut().feature_mut(sketch).unwrap().kind {
if let solidcraft_doc::FeatureKind::Sketch { sketch, .. } = &mut s.doc_mut().feature_mut(sketch).unwrap().kind {
for p in &mut sketch.points {
p.pos.x *= 2.0;
}
@@ -223,8 +223,8 @@ fn renamed_pattern_outputs_and_later_body_operands_follow_clone() {
near(volume(&mut s, new), 24.0);
let state = s.model.state();
let target = state.bodies.iter().find(|b| b.name == "Custom Output (copy)").unwrap();
near(solvecraft_kernel::measure(&target.body).unwrap().centroid.y, 6.0);
assert_eq!(s.doc.features.iter().filter(|f| f.component == new && matches!(f.kind, solvecraft_doc::FeatureKind::Move { .. })).count(), 1);
near(solidcraft_kernel::measure(&target.body).unwrap().centroid.y, 6.0);
assert_eq!(s.doc.features.iter().filter(|f| f.component == new && matches!(f.kind, solidcraft_doc::FeatureKind::Move { .. })).count(), 1);
}
#[test]
@@ -233,15 +233,15 @@ fn configuration_rows_follow_copied_parameters_material_and_strict_face_lineage(
let part = component(&mut s, "Configured");
let source = box2(&mut s);
let length = s.doc.feature(source).unwrap().param_names[0].clone();
let target = solvecraft_doc::config::ConfigFaceTarget {
let target = solidcraft_doc::config::ConfigFaceTarget {
body: "Cube".into(),
origin_feature: source,
name: format!("F{source}:+z"),
point: solvecraft_geom::Vec3::new(1.0, 1.0, 2.0),
point: solidcraft_geom::Vec3::new(1.0, 1.0, 2.0),
certificate: None,
};
run(&mut s, "config.configure", json!({"params":[length],"materials":["Cube"],"component_appearances":[part],"face_appearances":[target]}));
let face_key = s.doc.config_key(&solvecraft_doc::config::Column::FaceAppearance(target.clone()));
let face_key = s.doc.config_key(&solidcraft_doc::config::Column::FaceAppearance(target.clone()));
run(
&mut s,
"config.row",
@@ -271,26 +271,26 @@ fn configured_extrude_cap_copy_keeps_independent_semantic_lineage() {
run(&mut s, "sketch.finish", json!({}));
let feature = run(&mut s, "solid.extrude", json!({"distance":2}))["feature"].as_u64().unwrap();
let body_name = s.model.state().bodies.last().unwrap().name.clone();
let target = solvecraft_doc::config::ConfigFaceTarget {
let target = solidcraft_doc::config::ConfigFaceTarget {
body: body_name,
origin_feature: feature,
name: format!("F{feature}:top"),
point: solvecraft_geom::Vec3::new(1.0, 1.0, 2.0),
point: solidcraft_geom::Vec3::new(1.0, 1.0, 2.0),
certificate: None,
};
run(&mut s, "config.configure", json!({"face_appearances":[target]}));
let key = s.doc.config_key(&solvecraft_doc::config::Column::FaceAppearance(target));
let key = s.doc.config_key(&solidcraft_doc::config::Column::FaceAppearance(target));
run(&mut s, "config.row", json!({"name":"Painted","values":{key:{"name":"Custom","color":[0,255,0],"opacity":1.0}}}));
let copied = copy(&mut s, part)["component"].as_u64().unwrap();
run(&mut s, "config.activate", json!({"row":"Painted"}));
let state = s.model.state();
let body = state.bodies.iter().find(|b| b.component == copied).unwrap();
assert!(solvecraft_doc::naming::face_names(body).contains(&format!("F{}:top", body.feature)));
assert!(solidcraft_doc::naming::face_names(body).contains(&format!("F{}:top", body.feature)));
assert!(s.doc.face_colors(body).iter().any(|(_, look)| look.color == [0, 255, 0]));
assert!(s.doc.configuration_style_status(&state).is_ok());
let saved = s.doc.to_json();
let reopened = solvecraft_doc::Document::from_json(&saved).unwrap();
let mut model = solvecraft_doc::Model::default();
let reopened = solidcraft_doc::Document::from_json(&saved).unwrap();
let mut model = solidcraft_doc::Model::default();
model.evaluate(&reopened);
assert!(reopened.configuration_style_status(&model.state()).is_ok());
}
@@ -310,8 +310,8 @@ fn selected_second_occurrence_emboss_copy_remaps_target_and_author_paths_and_reo
let copied = run(&mut s, "component.paste_new", json!({"component":part,"occurrence":second,"path":[second],"translate":[20,0,0]}));
let new = copied["component"].as_u64().unwrap();
let placement = copied["occurrence"].as_u64().unwrap();
let emboss = s.doc.features.iter().find(|f| f.component == new && matches!(f.kind, solvecraft_doc::FeatureKind::Emboss { .. })).unwrap();
let solvecraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &emboss.kind else { panic!("missing selected face") };
let emboss = s.doc.features.iter().find(|f| f.component == new && matches!(f.kind, solidcraft_doc::FeatureKind::Emboss { .. })).unwrap();
let solidcraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &emboss.kind else { panic!("missing selected face") };
assert_eq!(face.body, "Sleeve (copy)");
assert_eq!(face.path, vec![placement]);
assert_eq!(face.author_path, vec![placement]);
@@ -321,7 +321,7 @@ fn selected_second_occurrence_emboss_copy_remaps_target_and_author_paths_and_reo
run(&mut s, "parameters.change", json!({"name":height,"expression":"30 mm"}));
near(volume(&mut s, new), clone_volume);
assert!(volume(&mut s, part) > original_volume);
let mut reopened = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!(reopened.doc.features.iter().all(|f| reopened.model.result(f.id).is_some_and(|r| r.error.is_none())));
near(volume(&mut reopened, new), clone_volume);
@@ -339,7 +339,7 @@ fn reassigned_sample_plate_paste_new_preserves_complex_frozen_geometry() {
run(&mut s, "occurrence.ground", json!({"occurrence":occurrence,"grounded":true}));
let original_volume = volume(&mut s, part);
let source_mesh = s.model.state().body(&body).unwrap().mesh();
let boss_probe = solvecraft_geom::Vec3::new(40.0, 35.0, 12.0);
let boss_probe = solidcraft_geom::Vec3::new(40.0, 35.0, 12.0);
let source_boss = source_mesh.contains(boss_probe);
let copied = run(&mut s, "component.paste_new", json!({"component":part,"translate":[10,10,0]}));
assert_eq!(s.doc.components.len(), 2);
@@ -354,11 +354,11 @@ fn reassigned_sample_plate_paste_new_preserves_complex_frozen_geometry() {
for (point, expected) in
[([5.0, 25.0, 4.0], true), ([12.0, 25.0, 4.0], false), ([68.0, 25.0, 4.0], false), ([40.0, 25.0, 12.0], false), ([0.0, 0.0, 4.0], false)]
{
assert_eq!(mesh.contains(solvecraft_geom::Vec3::new(point[0], point[1], point[2])), expected, "{point:?}");
assert_eq!(mesh.contains(solidcraft_geom::Vec3::new(point[0], point[1], point[2])), expected, "{point:?}");
}
assert_eq!(mesh.contains(boss_probe), source_boss);
assert_eq!(state.body(&body).unwrap().mesh().contains(boss_probe), source_boss);
let mut reopened = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!((volume(&mut reopened, new) - original_volume).abs() < 1e-5 * original_volume);
}
@@ -375,7 +375,7 @@ fn generated_copy_parameters_cannot_heal_an_unrelated_missing_reference() {
let missing = probe.new_model_param("2 mm", "mm");
let feature = s.doc_mut().feature_mut(broken).unwrap();
feature.component = 0;
if let solvecraft_doc::FeatureKind::Box { width, .. } = &mut feature.kind {
if let solidcraft_doc::FeatureKind::Box { width, .. } = &mut feature.kind {
*width = missing;
}
s.refresh();
@@ -418,7 +418,7 @@ fn external_transfer_entry_and_internal_rotated_transfer_clone_once() {
assert!(before.min.dist(after.min) < 1e-6);
assert!(before.max.dist(after.max) < 1e-6);
assert!(body.transferred);
let mut reopened = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!(reopened.model.state().body("Foreign (copy)").unwrap().mesh().bounds().min.dist(before.min) < 1e-6);
}
@@ -433,7 +433,7 @@ fn suppressed_external_entry_does_not_supply_clone_baseline() {
let skipped = s
.doc_mut()
.add_feature(
solvecraft_doc::FeatureKind::BodyTransfer { bodies: vec!["Foreign".into()], from: 0, to: part, transform: solvecraft_doc::IDENTITY },
solidcraft_doc::FeatureKind::BodyTransfer { bodies: vec!["Foreign".into()], from: 0, to: part, transform: solidcraft_doc::IDENTITY },
None,
)
.unwrap();
@@ -454,13 +454,13 @@ fn transferred_frozen_face_paint_ignores_consumed_legacy_offset() {
run(&mut s, "appearance.assign", json!({"body":"Foreign","faces":[[1,1,2]],"color":"#00ff00"}));
s.doc_mut().body_offsets.insert(
"Foreign".into(),
solvecraft_doc::rigid(solvecraft_geom::Vec3::new(7.0, 0.0, 0.0), solvecraft_geom::Vec3::ZERO, solvecraft_geom::Vec3::Z, 0.0),
solidcraft_doc::rigid(solidcraft_geom::Vec3::new(7.0, 0.0, 0.0), solidcraft_geom::Vec3::ZERO, solidcraft_geom::Vec3::Z, 0.0),
);
s.refresh();
let part = component(&mut s, "Part");
run(&mut s, "component.move_bodies", json!({"component":part,"bodies":["Foreign"]}));
let original = s.doc.appearances.faces.iter().find(|face| face.body == "Foreign").unwrap();
let expected = solvecraft_doc::naming::point_on_face(&s.model.state(), original.name.as_ref().unwrap(), original.point).unwrap().0;
let expected = solidcraft_doc::naming::point_on_face(&s.model.state(), original.name.as_ref().unwrap(), original.point).unwrap().0;
copy(&mut s, part);
let paint = s.doc.appearances.faces.iter().find(|face| face.body == "Foreign (copy)").unwrap();
assert!(paint.point.dist(expected) < 1e-6, "{:?} versus {expected:?}", paint.point);
@@ -468,18 +468,18 @@ fn transferred_frozen_face_paint_ignores_consumed_legacy_offset() {
#[test]
fn boundary_material_proof_requires_resolvable_stock_and_void() {
use solvecraft_geom::Vec3;
let a = solvecraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap().tessellate(1e-3).unwrap();
let b = solvecraft_kernel::box_solid(Vec3::new(1e-5, 0., 0.), Vec3::new(10.00001, 10., 10.)).unwrap().tessellate(1e-3).unwrap();
use solidcraft_geom::Vec3;
let a = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap().tessellate(1e-3).unwrap();
let b = solidcraft_kernel::box_solid(Vec3::new(1e-5, 0., 0.), Vec3::new(10.00001, 10., 10.)).unwrap().tessellate(1e-3).unwrap();
let p = Vec3::new(5e-6, 5., 5.);
assert_ne!(a.contains(p), b.contains(p));
assert!((super::mesh_distance(&a, p) - 5e-6).abs() < 1e-12);
assert!((super::mesh_distance(&b, p) - 5e-6).abs() < 1e-12);
assert!(super::material_probe_matches(&a, &b, p, 1e-3, 1e-3));
let shifted = solvecraft_kernel::box_solid(Vec3::new(1., 0., 0.), Vec3::new(11., 10., 10.)).unwrap().tessellate(1e-3).unwrap();
let shifted = solidcraft_kernel::box_solid(Vec3::new(1., 0., 0.), Vec3::new(11., 10., 10.)).unwrap().tessellate(1e-3).unwrap();
assert!(!super::material_probe_matches(&a, &shifted, p, 1e-3, 1e-3));
let thin = solvecraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 1e-4)).unwrap().tessellate(1e-6).unwrap();
let thin_shifted = solvecraft_kernel::box_solid(Vec3::new(0., 0., 1e-4), Vec3::new(10., 10., 2e-4)).unwrap().tessellate(1e-6).unwrap();
let thin = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 1e-4)).unwrap().tessellate(1e-6).unwrap();
let thin_shifted = solidcraft_kernel::box_solid(Vec3::new(0., 0., 1e-4), Vec3::new(10., 10., 2e-4)).unwrap().tessellate(1e-6).unwrap();
// Every resolvable replacement is void: thin ambiguous material is refused.
assert!(!super::material_probe_matches(&thin, &thin_shifted, Vec3::new(5., 5., 5e-5), 1e-3, 1e-3));
assert!(!super::material_probe_matches(&a, &b, p, f64::INFINITY, 1e-3));
@@ -487,7 +487,7 @@ fn boundary_material_proof_requires_resolvable_stock_and_void() {
#[test]
fn frozen_bore_boundary_copy_preserves_stock_void_and_reopen() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
let bore = |center: f64| {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length":30,"width":20,"height":10,"body_name":"Drilled"}));
@@ -500,7 +500,7 @@ fn frozen_bore_boundary_copy_preserves_stock_void_and_reopen() {
// cardinal angle, so its distance is bounded by the requested sag tolerance.
assert!(super::mesh_distance(&source, Vec3::new(6., 10., 5.)) <= 1e-3);
let shifted = bore(11.).model.state().body("Drilled").unwrap().body.tessellate(1e-3).unwrap();
let lost = solvecraft_kernel::box_solid(Vec3::ZERO, Vec3::new(30., 20., 10.)).unwrap().tessellate(1e-3).unwrap();
let lost = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(30., 20., 10.)).unwrap().tessellate(1e-3).unwrap();
assert!(!super::material_probe_matches(&source, &shifted, Vec3::new(6.5, 10., 5.), 1e-3, 1e-3));
assert!(!super::material_probe_matches(&source, &lost, Vec3::new(10., 10., 5.), 1e-3, 1e-3));
let created = run(&mut s, "component.from_bodies", json!({"bodies":["Drilled"]}));
@@ -517,17 +517,17 @@ fn frozen_bore_boundary_copy_preserves_stock_void_and_reopen() {
}
};
check(&s);
check(&Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
check(&Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
}
#[test]
fn frozen_r6_bore_copy_uses_current_geometry_despite_mesh_measure_bias() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length":20,"width":20,"height":10,"body_name":"Drilled"}));
run(&mut s, "solid.cylinder", json!({"base":[10,10,0],"radius":6,"height":10,"operation":"cut"}));
let source = s.model.state().body("Drilled").unwrap().body.clone();
let original = solvecraft_kernel::step_export(&[&source], "source").unwrap();
let original = solidcraft_kernel::step_export(&[&source], "source").unwrap();
let created = run(&mut s, "component.from_bodies", json!({"bodies":["Drilled"]}));
let part = created["components"][0]["component"].as_u64().unwrap();
let before = s.doc.to_json();
@@ -535,7 +535,7 @@ fn frozen_r6_bore_copy_uses_current_geometry_despite_mesh_measure_bias() {
let check = |session: &Session| {
let state = session.model.state();
let body = state.bodies.iter().find(|b| session.doc.model_body_component(b) == copied).unwrap();
assert_eq!(solvecraft_kernel::certified_prism_equivalent(&source, &body.body).unwrap(), Some(true));
assert_eq!(solidcraft_kernel::certified_prism_equivalent(&source, &body.body).unwrap(), Some(true));
let mesh = body.body.tessellate(1e-3).unwrap();
for (point, material) in
[(Vec3::new(3., 10., 5.), true), (Vec3::new(5., 10., 5.), false), (Vec3::new(10., 10., 5.), false), (Vec3::new(18., 10., 5.), true)]
@@ -543,10 +543,10 @@ fn frozen_r6_bore_copy_uses_current_geometry_despite_mesh_measure_bias() {
assert_eq!(mesh.contains(point), material, "{point:?}");
}
let original_body = state.body("Drilled").unwrap();
assert_eq!(solvecraft_kernel::step_export(&[&original_body.body], "source").unwrap(), original);
assert_eq!(solidcraft_kernel::step_export(&[&original_body.body], "source").unwrap(), original);
};
check(&s);
check(&Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
check(&Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.to_json(), before);
run(&mut s, "edit.redo", json!({}));
+10 -10
View File
@@ -1,7 +1,7 @@
//! Physical properties of an assembly placement, with original body material assignments.
use serde_json::{Value, json};
use solvecraft_doc::{Document, Mat};
use solvecraft_geom::Vec3;
use solidcraft_doc::{Document, Mat};
use solidcraft_geom::Vec3;
use crate::params::bad;
use crate::{Result, Session};
@@ -27,8 +27,8 @@ fn placed_doc(s: &Session) -> Document {
/// A property total must cover the entire assembly. The display traversal intentionally
/// truncates hostile designs, so use an explicit error instead of accepting its prefix.
pub(in crate::cmd) fn placements(doc: &Document, cmd: &str) -> Result<Vec<(u64, Vec<u64>, Mat)>> {
let mut out = vec![(0, Vec::new(), solvecraft_doc::IDENTITY)];
let mut stack = vec![(0, Vec::<u64>::new(), solvecraft_doc::IDENTITY)];
let mut out = vec![(0, Vec::new(), solidcraft_doc::IDENTITY)];
let mut stack = vec![(0, Vec::<u64>::new(), solidcraft_doc::IDENTITY)];
let mut children: std::collections::BTreeMap<u64, Vec<_>> = std::collections::BTreeMap::new();
for occurrence in &doc.occurrences {
children.entry(occurrence.parent).or_default().push(occurrence);
@@ -40,7 +40,7 @@ pub(in crate::cmd) fn placements(doc: &Document, cmd: &str) -> Result<Vec<(u64,
}
let mut child_path = path.clone();
child_path.push(occurrence.id);
let world = solvecraft_doc::mat_mul(&transform, &occurrence.transform);
let world = solidcraft_doc::mat_mul(&transform, &occurrence.transform);
out.push((occurrence.component, child_path.clone(), world));
stack.push((occurrence.component, child_path, world));
}
@@ -95,7 +95,7 @@ fn properties(s: &Session, doc: &Document, selected: &[u64], descendants: bool,
let (mut faces, mut edges, mut vertices) = (0, 0, 0);
let mut placement_count = 0;
let selected_placements = if selected.is_empty() {
if descendants { placements(doc, cmd)? } else { vec![(0, Vec::new(), solvecraft_doc::IDENTITY)] }
if descendants { placements(doc, cmd)? } else { vec![(0, Vec::new(), solidcraft_doc::IDENTITY)] }
} else {
let placement = s.occurrence_placement(selected).map_err(|e| bad(cmd, e.to_string()))?;
let mut out = vec![(placement.component, selected.to_vec(), placement.world)];
@@ -107,7 +107,7 @@ fn properties(s: &Session, doc: &Document, selected: &[u64], descendants: bool,
}
let mut child = path.clone();
child.push(occurrence.id);
let world = solvecraft_doc::mat_mul(&transform, &occurrence.transform);
let world = solidcraft_doc::mat_mul(&transform, &occurrence.transform);
out.push((occurrence.component, child.clone(), world));
stack.push((occurrence.component, child, world));
}
@@ -117,9 +117,9 @@ fn properties(s: &Session, doc: &Document, selected: &[u64], descendants: bool,
for (component, path, transform) in selected_placements {
placement_count += 1;
for body in state.bodies.iter().filter(|b| doc.model_body_component(b) == component) {
let transform: Mat = doc.model_body_offset(body).map(|offset| solvecraft_doc::mat_mul(&transform, offset)).unwrap_or(transform);
let placed = solvecraft_kernel::transform_matrix(&body.body, transform)?;
let measured = solvecraft_kernel::measure(&placed)?;
let transform: Mat = doc.model_body_offset(body).map(|offset| solidcraft_doc::mat_mul(&transform, offset)).unwrap_or(transform);
let placed = solidcraft_kernel::transform_matrix(&body.body, transform)?;
let measured = solidcraft_kernel::measure(&placed)?;
let density = doc.density(&body.name);
let body_mass = measured.volume * density / 1000.0;
volume += measured.volume;
@@ -108,7 +108,7 @@ fn compact_exponentially_instanced_assembly_refuses_partial_totals() {
for _ in 0..17 {
let parent = s.active_component;
let component = s.doc_mut().add_component(None, parent).unwrap();
last_occurrence = s.doc_mut().add_occurrence(component, parent, solvecraft_doc::IDENTITY).unwrap();
last_occurrence = s.doc_mut().add_occurrence(component, parent, solidcraft_doc::IDENTITY).unwrap();
s.active_component = component;
}
for command in ["component.properties", "occurrence.properties"] {
+8 -8
View File
@@ -10,8 +10,8 @@ fn unique_frames(s: &Session, cmd: &str) -> Result<UniqueFrames> {
for occurrence in &doc.occurrences {
children.entry(occurrence.parent).or_default().push(occurrence);
}
let mut frames = BTreeMap::from([(0, Some((Vec::new(), solvecraft_doc::IDENTITY)))]);
let mut stack = vec![(0, Vec::new(), solvecraft_doc::IDENTITY)];
let mut frames = BTreeMap::from([(0, Some((Vec::new(), solidcraft_doc::IDENTITY)))]);
let mut stack = vec![(0, Vec::new(), solidcraft_doc::IDENTITY)];
let mut work = 0usize;
while let Some((component, path, matrix)) = stack.pop() {
for occurrence in children.get(&component).into_iter().flatten() {
@@ -21,7 +21,7 @@ fn unique_frames(s: &Session, cmd: &str) -> Result<UniqueFrames> {
}
let mut child_path = path.clone();
child_path.push(occurrence.id);
let frame = solvecraft_doc::mat_mul(&matrix, &occurrence.transform);
let frame = solidcraft_doc::mat_mul(&matrix, &occurrence.transform);
frames.entry(occurrence.component).and_modify(|entry| *entry = None).or_insert_with(|| Some((child_path.clone(), frame)));
stack.push((occurrence.component, child_path, frame));
}
@@ -61,16 +61,16 @@ fn transfer(s: &mut Session, name: &str, to: u64, frames: &UniqueFrames, cmd: &s
};
let source = unique(from)?;
let destination = unique(to)?;
let old = doc.model_body_offset(body).map(|offset| solvecraft_doc::mat_mul(&source.1, offset)).unwrap_or(source.1);
let inv = solvecraft_doc::mat_inverse(&destination.1).ok_or_else(|| bad(cmd, "destination frame is not invertible"))?;
let transform = solvecraft_doc::mat_mul(&inv, &old);
if !solvecraft_doc::valid_transfer_matrix(&transform) {
let old = doc.model_body_offset(body).map(|offset| solidcraft_doc::mat_mul(&source.1, offset)).unwrap_or(source.1);
let inv = solidcraft_doc::mat_inverse(&destination.1).ok_or_else(|| bad(cmd, "destination frame is not invertible"))?;
let transform = solidcraft_doc::mat_mul(&inv, &old);
if !solidcraft_doc::valid_transfer_matrix(&transform) {
return Err(bad(cmd, "body transfer exceeds supported rigid coordinate range"));
}
let inherited = (!s.doc.appearances.bodies.contains_key(name)).then(|| s.doc.appearances.components.get(&from).cloned()).flatten();
let paths: BTreeSet<u64> = source.0.iter().chain(&destination.0).copied().collect();
let captures: Vec<_> = paths.into_iter().filter_map(|id| s.pending_moves.get(&id).map(|transform| (id, *transform))).collect();
if captures.iter().any(|(_, transform)| !solvecraft_doc::valid_transfer_matrix(transform)) {
if captures.iter().any(|(_, transform)| !solidcraft_doc::valid_transfer_matrix(transform)) {
return Err(bad(cmd, "pending transfer position is not a supported rigid transform"));
}
drop(doc);
@@ -10,7 +10,7 @@ fn recorded_assignment_preserves_sample_joined_material_and_timeline() {
s.run_script(&crate::sample::script()).unwrap();
let before = s.model.state();
assert_eq!(before.bodies.len(), 1);
let volume = solvecraft_kernel::measure(&before.body("Plate").unwrap().body).unwrap().volume;
let volume = solidcraft_kernel::measure(&before.body("Plate").unwrap().body).unwrap().volume;
let created = run(&mut s, "component.from_bodies", json!({"bodies":["Plate"]}));
let to = created["components"][0]["component"].as_u64().unwrap();
let feature = created["components"][0]["feature"].as_u64().unwrap();
@@ -19,7 +19,7 @@ fn recorded_assignment_preserves_sample_joined_material_and_timeline() {
let body = after.body("Plate").unwrap();
assert_eq!(body.component, to);
assert!(body.transferred);
assert!((solvecraft_kernel::measure(&body.body).unwrap().volume - volume).abs() < 1e-6);
assert!((solidcraft_kernel::measure(&body.body).unwrap().volume - volume).abs() < 1e-6);
assert!(body.mesh().contains(Vec3::new(40.0, 35.0, 12.0)));
let index = s.doc.feature_index(feature).unwrap();
s.doc_mut().marker = Some(index);
@@ -44,12 +44,12 @@ fn transformed_pending_destination_repeated_transfer_preserves_world_pose() {
assert!(bounds.min.dist(Vec3::ZERO) < 1e-6);
assert!(bounds.max.dist(Vec3::new(10.0, 10.0, 10.0)) < 1e-6);
assert!(!s.pending_moves.contains_key(&occurrence));
let mut destination_reopened = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
let mut destination_reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
destination_reopened.refresh();
assert!(destination_reopened.world_state().body("Block").unwrap().mesh().bounds().min.dist(Vec3::ZERO) < 1e-6);
run(&mut s, "component.move_bodies", json!({"component":0,"bodies":["Block"]}));
assert!(s.world_state().body("Block").unwrap().mesh().bounds().min.dist(Vec3::ZERO) < 1e-6);
let mut reopened = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!(reopened.world_state().body("Block").unwrap().mesh().bounds().min.dist(Vec3::ZERO) < 1e-6);
}
@@ -75,7 +75,7 @@ fn historical_join_cut_and_new_destination_modeling_keep_material() {
run(&mut s, "solid.box", json!({"corner":[2,2,0],"length":2,"width":2,"height":10,"operation":"cut"}));
let original = s.model.state();
assert_eq!(original.bodies.len(), 1);
let v = solvecraft_kernel::measure(&original.body("Block").unwrap().body).unwrap().volume;
let v = solidcraft_kernel::measure(&original.body("Block").unwrap().body).unwrap().volume;
assert!((v - 1760.0).abs() < 1e-6);
run(&mut s, "material.assign", json!({"bodies":["Block"],"material":"Brass"}));
run(&mut s, "appearance.assign", json!({"bodies":["Block"],"color":"#bc8537"}));
@@ -87,15 +87,15 @@ fn historical_join_cut_and_new_destination_modeling_keep_material() {
run(&mut s, "solid.box", json!({"corner":[18,0,0],"length":2,"width":10,"height":10,"operation":"join"}));
assert_eq!(s.model.state().bodies.len(), 1);
assert_eq!(s.model.state().body("Block").unwrap().component, c);
let final_volume = solvecraft_kernel::measure(&s.model.state().body("Block").unwrap().body).unwrap().volume;
let final_volume = solidcraft_kernel::measure(&s.model.state().body("Block").unwrap().body).unwrap().volume;
assert!((final_volume - 1960.0).abs() < 1e-6);
run(&mut s, "component.move_bodies", json!({"component":0,"bodies":["Block"]}));
assert_eq!(s.model.state().body("Block").unwrap().component, 0);
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.model.state().body("Block").unwrap().component, c);
let mut reopened = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!((solvecraft_kernel::measure(&reopened.model.state().body("Block").unwrap().body).unwrap().volume - final_volume).abs() < 1e-6);
assert!((solidcraft_kernel::measure(&reopened.model.state().body("Block").unwrap().body).unwrap().volume - final_volume).abs() < 1e-6);
}
#[test]
@@ -107,7 +107,7 @@ fn ownership_only_boundary_invalidates_old_boolean_cache() {
let c = s.doc_mut().add_component(Some("Destination"), 0).unwrap();
let id = s
.doc_mut()
.add_feature(FeatureKind::BodyTransfer { bodies: vec!["Block".into()], from: 0, to: c, transform: solvecraft_doc::IDENTITY }, None)
.add_feature(FeatureKind::BodyTransfer { bodies: vec!["Block".into()], from: 0, to: c, transform: solidcraft_doc::IDENTITY }, None)
.unwrap();
let index = s.doc.feature_index(id).unwrap();
let transfer = s.doc_mut().features.remove(index);
@@ -115,7 +115,7 @@ fn ownership_only_boundary_invalidates_old_boolean_cache() {
s.refresh();
assert_eq!(s.model.state().bodies.len(), 2);
assert_eq!(s.model.state().body("Block").unwrap().component, c);
let mut reopened = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert_eq!(reopened.model.state().bodies.len(), 2);
assert_eq!(reopened.model.state().body("Block").unwrap().component, c);
@@ -128,7 +128,7 @@ fn failed_transfer_cannot_change_evaluated_world_owner() {
let c = s.doc_mut().add_component(Some("Destination"), 0).unwrap();
let id = s
.doc_mut()
.add_feature(FeatureKind::BodyTransfer { bodies: vec!["Block".into()], from: c, to: c, transform: solvecraft_doc::IDENTITY }, None)
.add_feature(FeatureKind::BodyTransfer { bodies: vec!["Block".into()], from: c, to: c, transform: solidcraft_doc::IDENTITY }, None)
.unwrap();
s.refresh();
assert!(s.model.result(id).unwrap().error.is_some());
@@ -149,7 +149,7 @@ fn transferred_thread_follows_body_frame_without_changing_world_axis() {
let after = s.world_state().threads[0].clone();
assert!(after.axis_point.dist(before.axis_point) < 1e-6);
assert!(after.axis.dist(before.axis) < 1e-6);
let mut reopened = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!(reopened.world_state().threads[0].axis.dist(before.axis) < 1e-6);
}
@@ -159,7 +159,7 @@ fn inherited_component_appearance_and_legacy_assignment_survive_transfer() {
let mut s = Session::default();
let source = run(&mut s, "component.create", json!({"name":"Source"}))["component"].as_u64().unwrap();
run(&mut s, "solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Block"}));
s.doc_mut().appearances.components.insert(source, solvecraft_doc::appearance::Look { name: "Green".into(), color: [0, 150, 0], opacity: 0.7 });
s.doc_mut().appearances.components.insert(source, solidcraft_doc::appearance::Look { name: "Green".into(), color: [0, 150, 0], opacity: 0.7 });
s.refresh();
let look = s.doc.model_body_look(s.model.state().body("Block").unwrap()).unwrap();
run(&mut s, "component.move_bodies", json!({"component":0,"bodies":["Block"]}));
@@ -169,12 +169,12 @@ fn inherited_component_appearance_and_legacy_assignment_survive_transfer() {
run(&mut legacy, "solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Old"}));
let c = legacy.doc_mut().add_component(Some("Legacy"), 0).unwrap();
legacy.doc_mut().body_components.insert("Old".into(), c);
legacy.doc_mut().body_offsets.insert("Old".into(), solvecraft_doc::rigid(Vec3::new(7.0, 0.0, 0.0), Vec3::ZERO, Vec3::Z, 0.0));
legacy.doc_mut().body_offsets.insert("Old".into(), solidcraft_doc::rigid(Vec3::new(7.0, 0.0, 0.0), Vec3::ZERO, Vec3::Z, 0.0));
legacy.refresh();
assert!(!legacy.model.state().body("Old").unwrap().transferred);
run(&mut legacy, "component.move_bodies", json!({"component":0,"bodies":["Old"]}));
assert!((legacy.world_state().body("Old").unwrap().mesh().bounds().min.x - 7.0).abs() < 1e-6);
let mut reopened = Session { doc: solvecraft_doc::Document::from_json(&legacy.doc.to_json()).unwrap().into(), ..Session::default() };
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&legacy.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!((reopened.world_state().body("Old").unwrap().mesh().bounds().min.x - 7.0).abs() < 1e-6);
}
@@ -190,7 +190,7 @@ fn deleting_external_transfer_source_refuses_without_dangling_archive() {
let document = s.doc.to_json();
assert!(s.execute("component.delete", &json!({"component":a})).is_err());
assert_eq!(s.doc.to_json(), document);
assert!(solvecraft_doc::Document::from_json(&document).is_ok());
assert!(solidcraft_doc::Document::from_json(&document).is_ok());
}
#[test]
@@ -204,7 +204,7 @@ fn earlier_selected_cylinder_emboss_keeps_source_frame_after_transfer_and_edit()
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[-2,5],"p1":[2,10]}));
run(&mut s, "sketch.finish", json!({}));
let emboss = run(&mut s, "solid.emboss", json!({"sketch":"Badge","face":[30,10,7.5],"depth":1}))["feature"].as_u64().unwrap();
let before = solvecraft_kernel::measure(&s.world_state().body("Sleeve").unwrap().body).unwrap().volume;
let before = solidcraft_kernel::measure(&s.world_state().body("Sleeve").unwrap().body).unwrap().volume;
run(&mut s, "component.activate", json!({"component":0}));
let destination = run(&mut s, "component.create", json!({"name":"Destination"}));
let to = destination["component"].as_u64().unwrap();
@@ -212,13 +212,13 @@ fn earlier_selected_cylinder_emboss_keeps_source_frame_after_transfer_and_edit()
run(&mut s, "occurrence.move", json!({"occurrence":destination_occurrence,"translate":[0,40,0],"axis":[1,0,0],"angle":90,"capture":true}));
run(&mut s, "component.move_bodies", json!({"component":to,"bodies":["Sleeve"]}));
assert!(s.doc.features.iter().all(|feature| s.model.result(feature.id).is_some_and(|result| result.error.is_none())));
let mut reopened = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert!(reopened.doc.features.iter().all(|feature| reopened.model.result(feature.id).is_some_and(|result| result.error.is_none())));
assert!((solvecraft_kernel::measure(&reopened.world_state().body("Sleeve").unwrap().body).unwrap().volume - before).abs() < 1e-5);
assert!((solidcraft_kernel::measure(&reopened.world_state().body("Sleeve").unwrap().body).unwrap().volume - before).abs() < 1e-5);
let depth = reopened.doc.feature(emboss).unwrap().param_names[0].clone();
run(&mut reopened, "parameters.change", json!({"name":depth,"expression":"2 mm"}));
assert!(solvecraft_kernel::measure(&reopened.world_state().body("Sleeve").unwrap().body).unwrap().volume > before);
assert!(solidcraft_kernel::measure(&reopened.world_state().body("Sleeve").unwrap().body).unwrap().volume > before);
}
#[test]
@@ -280,7 +280,7 @@ fn edited_transfer_destination_updates_history_owner_copy_and_reopen() {
assert_eq!(s.pending_moves, pending);
let copied = run(&mut s, "component.paste_new", json!({"component":b}))["component"].as_u64().unwrap();
assert_eq!(s.model.state().body("Block (copy)").unwrap().component, copied);
let mut reopened = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
reopened.refresh();
assert_eq!(reopened.model.state().body("Block").unwrap().component, b);
assert_eq!(reopened.model.state().body("Block (copy)").unwrap().component, copied);
+14 -14
View File
@@ -2,7 +2,7 @@
//! suppressions) and switching between its rows.
use serde_json::{Value, json};
use solvecraft_doc::config::{Column, ConfigRow, MAX_COLUMNS, MAX_ROWS};
use solidcraft_doc::config::{Column, ConfigRow, MAX_COLUMNS, MAX_ROWS};
use super::CommandSpec;
use crate::params::{bad, bool_, str_, string_list};
@@ -64,8 +64,8 @@ fn column_of(s: &Session, p: &Value, cmd: &str) -> Result<Column> {
.ok_or_else(|| bad(cmd, "no face column"));
}
if let Some(target) = value.get("target") {
solvecraft_doc::config::validate_face_target_json(target).map_err(|error| bad(cmd, error.to_string()))?;
let target: solvecraft_doc::config::ConfigFaceTarget =
solidcraft_doc::config::validate_face_target_json(target).map_err(|error| bad(cmd, error.to_string()))?;
let target: solidcraft_doc::config::ConfigFaceTarget =
serde_json::from_value(target.clone()).map_err(|error| bad(cmd, error.to_string()))?;
let state = s.model.state();
let body = state.body(&target.body).ok_or_else(|| bad(cmd, "no face definition"))?;
@@ -79,7 +79,7 @@ fn column_of(s: &Session, p: &Value, cmd: &str) -> Result<Column> {
if body.body.is_mesh() {
return Err(bad(cmd, "mesh faces have no supported semantic lineage"));
}
let index = solvecraft_doc::appearance::face_index_at(body, point).ok_or_else(|| bad(cmd, "no face at picked point"))?;
let index = solidcraft_doc::appearance::face_index_at(body, point).ok_or_else(|| bad(cmd, "no face at picked point"))?;
let target = s.doc.configuration_face_target(body, index, point).map_err(|error| bad(cmd, error.to_string()))?;
s.doc.certify_config_face(&target).map_err(|error| bad(cmd, error.to_string()))?;
s.doc.configuration_face_indices(body, &target).map_err(|error| bad(cmd, error.to_string()))?;
@@ -265,10 +265,10 @@ fn row(s: &mut Session, p: &Value) -> Result<Value> {
.ok_or_else(|| bad(cmd, format!("`{k}` is not a column (config.column adds one)")))?;
match s.doc.configs.columns.get(col) {
Some(Column::Material(_)) => {
text = solvecraft_doc::config::material_cell(&text).map_err(|e| bad(cmd, e.to_string()))?.unwrap_or_else(|| "Default".into());
text = solidcraft_doc::config::material_cell(&text).map_err(|e| bad(cmd, e.to_string()))?.unwrap_or_else(|| "Default".into());
}
Some(Column::Appearance(_) | Column::ComponentAppearance(_) | Column::FaceAppearance(_)) => {
text = match solvecraft_doc::config::appearance_cell(&text).map_err(|e| bad(cmd, e.to_string()))? {
text = match solidcraft_doc::config::appearance_cell(&text).map_err(|e| bad(cmd, e.to_string()))? {
Some(look) => serde_json::to_string(&look).map_err(|e| bad(cmd, e.to_string()))?,
None => String::new(),
};
@@ -329,8 +329,8 @@ fn validate_rows(s: &Session, cmd: &str) -> Result<()> {
fn rule(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "config.rule";
if let Some(value) = p.get("rule") {
solvecraft_doc::config::validate_rules_json(&json!({"configs":{"rules":[value]}})).map_err(|e| bad(cmd, e.to_string()))?;
let mut rule: solvecraft_doc::config::ConfigRule = serde_json::from_value(value.clone()).map_err(|e| bad(cmd, e.to_string()))?;
solidcraft_doc::config::validate_rules_json(&json!({"configs":{"rules":[value]}})).map_err(|e| bad(cmd, e.to_string()))?;
let mut rule: solidcraft_doc::config::ConfigRule = serde_json::from_value(value.clone()).map_err(|e| bad(cmd, e.to_string()))?;
if rule.length_unit.is_none() {
rule.length_unit = s
.doc
@@ -345,7 +345,7 @@ fn rule(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(index) = rules.iter().position(|r| r.id == rule.id) {
rules[index] = rule;
} else {
if rules.len() >= solvecraft_doc::config::MAX_RULES {
if rules.len() >= solidcraft_doc::config::MAX_RULES {
return Err(bad(cmd, "too many rules"));
}
rules.push(rule);
@@ -541,7 +541,7 @@ mod tests {
run(&mut s, "config.rule", json!({"id":"limit","delete":true}));
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc, before);
let reopened = solvecraft_doc::format::read(&s.doc.to_json()).unwrap();
let reopened = solidcraft_doc::format::read(&s.doc.to_json()).unwrap();
assert_eq!(reopened.configs.rules, s.doc.configs.rules);
}
@@ -580,7 +580,7 @@ mod tests {
assert!((volume(&mut s) - 1000.0).abs() < 1e-6, "captured Default preserves geometry in an inch design");
run(&mut s, "config.row", json!({"name":"Inches", "values":{length.clone():"2", format!("suppress:{feature}"):"NO"}}));
run(&mut s, "config.activate", json!({"row":"Inches"}));
assert_eq!(s.doc.config_current(&solvecraft_doc::config::Column::Param(length.clone())).as_deref(), Some("2 in"));
assert_eq!(s.doc.config_current(&solidcraft_doc::config::Column::Param(length.clone())).as_deref(), Some("2 in"));
assert!(s.doc.configuration_matches());
assert!((volume(&mut s) - 5080.0).abs() < 1e-6);
let feature_id = s.doc.features[0].id;
@@ -682,7 +682,7 @@ mod tests {
let mass = run(&mut s, "inspect.measure", json!({}));
assert!((mass["bodies"][0]["mass_g"].as_f64().unwrap() - 2.7).abs() < 1e-6);
assert_eq!(s.doc.appearances.bodies.get(&body).unwrap().opacity, 0.4);
let reopened = solvecraft_doc::format::read(&s.doc.to_json()).unwrap();
let reopened = solidcraft_doc::format::read(&s.doc.to_json()).unwrap();
assert_eq!(reopened, *s.doc);
assert!(reopened.configuration_matches());
run(&mut s, "edit.undo", json!({}));
@@ -740,7 +740,7 @@ mod tests {
run(&mut s, "config.activate", json!({"row": "Default"}));
assert_eq!(s.doc.appearances.bodies.get(&body).unwrap().name, name);
assert!(s.doc.configuration_matches());
let reopened = solvecraft_doc::format::read(&s.doc.to_json()).unwrap();
let reopened = solidcraft_doc::format::read(&s.doc.to_json()).unwrap();
assert!(reopened.configuration_matches());
}
@@ -775,7 +775,7 @@ mod tests {
run(&mut s, "edit.undo", json!({}));
assert_eq!(run(&mut s, "config.table", json!({}))["columns"][2], "material:Block");
run(&mut s, "edit.redo", json!({}));
let mut reopened = Session::new(solvecraft_doc::format::read(&s.doc.to_json()).unwrap());
let mut reopened = Session::new(solidcraft_doc::format::read(&s.doc.to_json()).unwrap());
run(&mut reopened, "config.activate", json!({"row": "Default"}));
assert!(!reopened.doc.materials.contains_key("Housing"));
assert!(!reopened.doc.appearances.bodies.contains_key("Housing"));
@@ -1,10 +1,10 @@
use crate::Session;
use serde_json::{Value, json};
use solvecraft_doc::{
use solidcraft_doc::{
FeatureKind,
config::{Column, FaceCertificate},
};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
fn run(session: &mut Session, command: &str, params: Value) -> Value {
session.execute(command, &params).unwrap_or_else(|error| panic!("{command} {params}: {error}"))
@@ -21,11 +21,11 @@ fn painted_radius(session: &Session, name: &str, radius: f64) -> usize {
state.body(name).unwrap_or_else(|| panic!("missing {name}; bodies {:?}", state.bodies.iter().map(|body| &body.name).collect::<Vec<_>>()));
let painted = session.doc.face_colors(body);
let mesh = body.body.tessellate(0.001).unwrap();
let inverse = solvecraft_doc::mat_inverse(&body.source_transform).unwrap();
let inverse = solidcraft_doc::mat_inverse(&body.source_transform).unwrap();
for (triangle, face) in mesh.triangles.iter().zip(&mesh.tri_face) {
if painted.iter().any(|(index, _)| *index == *face as usize) {
for vertex in mesh.tri(triangle).unwrap() {
let vertex = solvecraft_doc::apply_point(&inverse, vertex);
let vertex = solidcraft_doc::apply_point(&inverse, vertex);
assert!((vertex.x.hypot(vertex.y) - radius).abs() < 1e-7, "{name}: {vertex:?}");
}
}
@@ -60,7 +60,7 @@ fn certified_annulus_side_rows_follow_radius_edits_and_clone() {
assert_eq!(s.doc.configs.columns.len(), 2);
run(&mut s, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&s, "Ring (copy)", 12.), 2);
let reopened = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let reopened = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let mut reopened = Session::new(reopened);
run(&mut reopened, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&reopened, "Ring", 12.), 2);
@@ -83,19 +83,19 @@ fn imported(session: &mut Session, step: String, name: &str) -> u64 {
#[test]
fn legacy_v9_imported_emboss_anchor_and_ordinary_face_paint_replay_without_renaming() {
let mut s = Session::default();
let cylinder = solvecraft_kernel::cylinder(Vec3::ZERO, Vec3::Z, 10., 20.).unwrap();
let step = solvecraft_kernel::step_export(&[&cylinder], "legacy anchored cylinder").unwrap();
let cylinder = solidcraft_kernel::cylinder(Vec3::ZERO, Vec3::Z, 10., 20.).unwrap();
let step = solidcraft_kernel::step_export(&[&cylinder], "legacy anchored cylinder").unwrap();
let source = imported(&mut s, step, "Sleeve");
run(&mut s, "material.assign", json!({"bodies":["Sleeve"],"material":"Steel"}));
let before_volume = s.model.state().body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume;
let legacy_names = solvecraft_doc::naming::face_names(s.model.state().body("Sleeve").unwrap());
let legacy_names = solidcraft_doc::naming::face_names(s.model.state().body("Sleeve").unwrap());
assert!(legacy_names.iter().all(|name| name.starts_with(&format!("F{source}:face:"))));
run(&mut s, "appearance.assign", json!({"faces":[{"body":"Sleeve","point":[6,0,20]}],"appearance":"ABS - Blue"}));
assert!(s.doc.appearances.faces[0].certificate.is_none());
let badge = run(&mut s, "sketch.create", json!({"plane":{"face":[0,0,20]},"name":"Badge","project_edges":false}))["sketch"].as_u64().unwrap();
let FeatureKind::Sketch { plane: saved_plane, .. } = &s.doc.feature(badge).unwrap().kind else { panic!() };
let saved_plane = saved_plane.clone();
assert!(matches!(&saved_plane, solvecraft_doc::PlaneRef::Face { name: Some(name), .. } if legacy_names.contains(name)));
assert!(matches!(&saved_plane, solidcraft_doc::PlaneRef::Face { name: Some(name), .. } if legacy_names.contains(name)));
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[-2,-2],"p1":[2,2]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.emboss", json!({"sketch":"Badge","profiles":"all","depth":1,"targets":["Sleeve"],"name":"Raised Badge"}));
@@ -106,16 +106,16 @@ fn legacy_v9_imported_emboss_anchor_and_ordinary_face_paint_replay_without_renam
assert!((volume - before_volume - 16.).abs() < 0.01, "addedvolume {} baseline {before_volume}", volume - before_volume);
let paint = s.doc.appearances.faces.clone();
let mut legacy: Value = serde_json::from_str(&s.doc.to_json()).unwrap();
legacy["format"] = json!("solvecraft/9");
let reopened = Session::new(solvecraft_doc::Document::from_json(&legacy.to_string()).unwrap());
legacy["format"] = json!("solidcraft/9");
let reopened = Session::new(solidcraft_doc::Document::from_json(&legacy.to_string()).unwrap());
// Saved before format 14, it keeps naming the pieces it makes in the kernel's shell order.
let anchored = solvecraft_doc::Feature { shell_piece_order: true, ..anchored };
let anchored = solidcraft_doc::Feature { shell_piece_order: true, ..anchored };
assert_eq!(reopened.doc.feature(anchored.id).unwrap(), &anchored);
// Names saved before format 14 keep naming the face they named, marked so they are found as
// the build that saved them found them.
let mark = |name: &Option<String>| name.as_ref().map(|n| format!("{}{n}", solvecraft_doc::naming::LEGACY_MARK));
let mark = |name: &Option<String>| name.as_ref().map(|n| format!("{}{n}", solidcraft_doc::naming::LEGACY_MARK));
let mut saved_plane = saved_plane;
if let solvecraft_doc::PlaneRef::Face { name, .. } = &mut saved_plane {
if let solidcraft_doc::PlaneRef::Face { name, .. } = &mut saved_plane {
*name = mark(name);
}
let paint: Vec<_> = paint
@@ -173,10 +173,10 @@ fn legacy_v9_annulus_outer_side_emboss_keeps_its_original_heuristic_anchor() {
run(&mut s, "sketch.circle.center", json!({"center":[0,0],"radius":9,"id":"inner"}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"distance":20,"profiles":[{"point":[9.5,0]}],"body_name":"Sleeve"}));
let names = solvecraft_doc::naming::face_names(s.model.state().body("Sleeve").unwrap());
let names = solidcraft_doc::naming::face_names(s.model.state().body("Sleeve").unwrap());
// The outer wall is named after its own sketch curve: its history says so. (Builds before
// format 14 named it after the curve nearest its centre, the inner circle; see below.)
let name = solvecraft_doc::naming::face_names_at(&s.model.state(), &[Vec3::new(0., 10., 7.5)]).remove(0);
let name = solidcraft_doc::naming::face_names_at(&s.model.state(), &[Vec3::new(0., 10., 7.5)]).remove(0);
assert!(name.contains("side:outer"));
run(&mut s, "sketch.create", json!({"plane":{"origin":[0,10,0],"x_dir":[-1,0,0],"y_dir":[0,0,1]},"name":"Badge"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[-2,5],"p1":[2,10]}));
@@ -189,7 +189,7 @@ fn legacy_v9_annulus_outer_side_emboss_keeps_its_original_heuristic_anchor() {
let volume = s.model.state().body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume;
assert!((volume - std::f64::consts::PI * 380. - 21.).abs() < 0.2, "volume {volume}");
let mut legacy: Value = serde_json::from_str(&s.doc.to_json()).unwrap();
legacy["format"] = json!("solvecraft/9");
legacy["format"] = json!("solidcraft/9");
// What a version 9 design stored for that pick: the centroid-family name of the outer wall
// piece holding the point. It keeps finding that piece, as it did when saved.
for feature in legacy["features"].as_array_mut().unwrap() {
@@ -197,7 +197,7 @@ fn legacy_v9_annulus_outer_side_emboss_keeps_its_original_heuristic_anchor() {
feature["face_names"] = json!(["F2:side:inner#4"]);
}
}
let reopened = Session::new(solvecraft_doc::Document::from_json(&legacy.to_string()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&legacy.to_string()).unwrap());
assert!(reopened.model.result(feature_id).unwrap().error.is_none());
assert!((reopened.model.state().body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume - volume).abs() < 1e-8);
}
@@ -218,7 +218,7 @@ fn certified_side_transfer_preserves_geometry_and_unsupported_taper_refuses_atom
run(&mut s, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&s, "Round", 10.), 2);
let before = s.world_state().body("Round").unwrap().mesh().bounds();
let mut s = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let mut s = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
run(&mut s, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&s, "Round", 10.), 2);
assert!(
@@ -242,8 +242,8 @@ fn certified_side_transfer_preserves_geometry_and_unsupported_taper_refuses_atom
#[test]
fn certified_import_payload_binding_roundtrip_clone_and_replacement_refusal() {
let geometry = solvecraft_kernel::cylinder(Vec3::ZERO, Vec3::Z, 10., 10.).unwrap();
let step = solvecraft_kernel::step_export(&[&geometry], "certificate fixture").unwrap();
let geometry = solidcraft_kernel::cylinder(Vec3::ZERO, Vec3::Z, 10., 10.).unwrap();
let step = solidcraft_kernel::step_export(&[&geometry], "certificate fixture").unwrap();
let mut s = Session::default();
let part = run(&mut s, "component.create", json!({"name":"ImportedPart"}))["component"].as_u64().unwrap();
let source = imported(&mut s, step.clone(), "Imported");
@@ -256,7 +256,7 @@ fn certified_import_payload_binding_roundtrip_clone_and_replacement_refusal() {
run(&mut s, "component.paste_new", json!({"component":part,"translate":[30,0,0]}));
run(&mut s, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&s, "Imported (copy)", 10.), pieces);
let reopened = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let reopened = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let mut s = Session::new(reopened);
run(&mut s, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&s, "Imported", 10.), pieces);
+14 -14
View File
@@ -1,6 +1,6 @@
use crate::Session;
use serde_json::{Value, json};
use solvecraft_doc::config::{Column, ConfigFaceTarget};
use solidcraft_doc::config::{Column, ConfigFaceTarget};
fn run(s: &mut Session, command: &str, params: Value) -> Value {
s.execute(command, &params).unwrap_or_else(|error| panic!("{command} {params}: {error}"))
}
@@ -63,7 +63,7 @@ fn strict_face_rows_resize_rename_preview_suppress_and_roundtrip() {
let mb = state.body(&body).unwrap();
let colors = s.doc.face_colors(mb);
assert_eq!(colors.len(), 1);
let names = solvecraft_doc::naming::face_names(mb);
let names = solidcraft_doc::naming::face_names(mb);
assert!(names[colors[0].0].ends_with(":+z"));
assert!(s.doc.appearances.faces[0].strict_name);
run(&mut s, "appearance.assign", json!({"faces":[{"body":body,"point":[5,5,20]}]}));
@@ -76,7 +76,7 @@ fn strict_face_rows_resize_rename_preview_suppress_and_roundtrip() {
let hidden = run(&mut s, "config.activate", json!({"row":"Hidden"}));
assert_eq!(hidden["styles"][0]["dormant"], true);
assert!(s.model.state().bodies.is_empty());
let reopened = solvecraft_doc::format::read(&s.doc.to_json()).unwrap();
let reopened = solidcraft_doc::format::read(&s.doc.to_json()).unwrap();
assert_eq!(reopened, *s.doc);
run(&mut s, "config.activate", json!({"row":"Tall"}));
assert_eq!(s.doc.face_colors(s.model.state().body("Housing").unwrap())[0].1.name, "ABS - Red");
@@ -103,9 +103,9 @@ fn strict_face_split_pieces_share_lineage_without_index_targets() {
let state = s.model.state();
let body = state.body(&body).unwrap();
let colors = s.doc.face_colors(body);
let names = solvecraft_doc::naming::face_names(body);
let names = solidcraft_doc::naming::face_names(body);
assert!(colors.len() >= 2, "split should create multiple painted pieces: {names:?}");
assert!(colors.iter().all(|(index, look)| solvecraft_doc::naming::strip_piece(&names[*index]).ends_with(":+z") && look.name == "ABS - Red"));
assert!(colors.iter().all(|(index, look)| solidcraft_doc::naming::strip_piece(&names[*index]).ends_with(":+z") && look.name == "ABS - Red"));
}
#[test]
@@ -127,10 +127,10 @@ fn strict_face_missing_lineage_never_retargets_and_rolls_back() {
assert_eq!(s.doc, before);
assert_eq!(s.revision, revision);
assert_eq!(s.undo.len(), history);
let look = solvecraft_doc::appearance::library("ABS - Red").unwrap();
s.doc_mut().appearances.faces.push(solvecraft_doc::appearance::FaceLook {
let look = solidcraft_doc::appearance::library("ABS - Red").unwrap();
s.doc_mut().appearances.faces.push(solidcraft_doc::appearance::FaceLook {
body: body.clone(),
point: solvecraft_geom::Vec3::new(5., 5., 10.),
point: solidcraft_geom::Vec3::new(5., 5., 10.),
name: Some(format!("F{other}:+z")),
strict_name: true,
certificate: None,
@@ -150,11 +150,11 @@ fn face_occurrence_pick_uses_definition_identity_and_coordinates() {
let world = s.world_state();
let placed = world.bodies.iter().find(|body| body.name != body.definition_name()).unwrap();
let placed_name = placed.name.clone();
let point = solvecraft_doc::apply_point(&placed.source_transform, solvecraft_geom::Vec3::new(5., 5., 10.));
let point = solidcraft_doc::apply_point(&placed.source_transform, solidcraft_geom::Vec3::new(5., 5., 10.));
let key = face_column(&mut s, &placed_name, json!(point));
let Column::FaceAppearance(target) = &s.doc.configs.columns[0] else { panic!() };
assert_eq!(target.body, body);
assert!(target.point.dist(solvecraft_geom::Vec3::new(5., 5., 10.)) < 1e-8);
assert!(target.point.dist(solidcraft_geom::Vec3::new(5., 5., 10.)) < 1e-8);
run(&mut s, "config.row", json!({"name":"Paint","values":{key:"ABS - Blue"}}));
run(&mut s, "config.activate", json!({"row":"Paint"}));
assert!(s.world_state().bodies.iter().all(|body| s.doc.face_colors(body).len() == 1));
@@ -171,10 +171,10 @@ fn extrude_cap_certification_follows_actual_positive_negative_and_symmetric_name
let state = s.model.state();
let body = state.body("Pad").unwrap();
let faces = body.body.faces(0.01).unwrap();
let names = solvecraft_doc::naming::face_names(body);
let names = solidcraft_doc::naming::face_names(body);
let mut caps = 0;
for face in faces {
let name = solvecraft_doc::naming::strip_piece(&names[face.index]);
let name = solidcraft_doc::naming::strip_piece(&names[face.index]);
let origin = name.strip_prefix('F').unwrap().split_once(':').unwrap().0.parse().unwrap();
let target = ConfigFaceTarget { body: "Pad".into(), origin_feature: origin, name: name.into(), point: face.centroid, certificate: None };
if name.ends_with(":start") || name.ends_with(":top") || name.ends_with(":bottom") {
@@ -216,7 +216,7 @@ fn hostile_and_uncertified_face_targets_are_rejected() {
body: body.clone(),
origin_feature: id,
name: format!("F{id}:face:0"),
point: solvecraft_geom::Vec3::ZERO,
point: solidcraft_geom::Vec3::ZERO,
certificate: None,
};
assert!(s.doc.certify_config_face(&target).is_err());
@@ -225,7 +225,7 @@ fn hostile_and_uncertified_face_targets_are_rejected() {
json!({"body":body,"origin_feature":id,"name":format!("F{id}:+z"),"point":[0,0,1e20]}),
json!({"body":body,"origin_feature":id,"name":format!("F{id}:+z"),"point":[0,0,0],"extra":[[]]}),
] {
assert!(solvecraft_doc::config::validate_styles_json(&json!({"configs":{"columns":[{"face_appearance":value}]}})).is_err());
assert!(solidcraft_doc::config::validate_styles_json(&json!({"configs":{"columns":[{"face_appearance":value}]}})).is_err());
}
assert!(s.execute("config.column", &json!({"component_appearance":0})).is_err());
assert!(s.execute("config.column", &json!({"face_appearance":{"body":body,"point":[0,0,1e20]}})).is_err());
+3 -3
View File
@@ -7,8 +7,8 @@
//! persistent names.
use serde_json::Value;
use solvecraft_doc::construct::{AxisDef, GeoRef, PointDef};
use solvecraft_doc::{FeatureKind, PlaneRef};
use solidcraft_doc::construct::{AxisDef, GeoRef, PointDef};
use solidcraft_doc::{FeatureKind, PlaneRef};
use super::CommandSpec;
use super::features::{add_feature, check_expr, plane_param, sketch_id};
@@ -137,7 +137,7 @@ fn geo(s: &Session, v: Option<&Value>, cmd: &str, what: &str) -> Result<GeoRef>
fn t_param(s: &Session, p: &Value, cmd: &str) -> Result<String> {
let t = if p.get("t").is_some() { req_expr(cmd, p, "t")? } else { "0.5".to_string() };
check_expr(s, &t, solvecraft_doc::expr::Kind::Unitless, cmd, "t")?;
check_expr(s, &t, solidcraft_doc::expr::Kind::Unitless, cmd, "t")?;
Ok(t)
}
+3 -3
View File
@@ -3,9 +3,9 @@
use std::collections::{BTreeSet, HashMap};
use std::sync::Arc;
use solvecraft_doc::joints;
use solvecraft_doc::{IDENTITY, Mat, apply_point, mat_mul};
use solvecraft_geom::{Aabb3, Mesh, Vec3};
use solidcraft_doc::joints;
use solidcraft_doc::{IDENTITY, Mat, apply_point, mat_mul};
use solidcraft_geom::{Aabb3, Mesh, Vec3};
use crate::params::bad;
use crate::{Result, Session};
+3 -3
View File
@@ -1,6 +1,6 @@
use crate::Session;
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
fn run(s: &mut Session, command: &str, p: Value) -> Value {
s.execute(command, &p).unwrap()
@@ -57,7 +57,7 @@ fn revolute_cannot_cross_a_blocker_between_clear_endpoints() {
assert!(result["stopped_by_contact"].is_object(), "{result}");
let angle = s.doc.assembly.joints.iter().find(|j| j.name == "Pivot").unwrap().values[0];
assert!(angle > 60_f64.to_radians() && angle < 90_f64.to_radians(), "angle={angle}");
let arm = solvecraft_doc::apply_point(&solvecraft_doc::joints::occurrence_world(&s.doc, occurrence(&s, "Arm")), Vec3::new(22.0, 0.0, 0.0));
let arm = solidcraft_doc::apply_point(&solidcraft_doc::joints::occurrence_world(&s.doc, occurrence(&s, "Arm")), Vec3::new(22.0, 0.0, 0.0));
assert!(arm.y > 20.0 && arm.x > 2.0, "{arm:?}");
}
@@ -105,7 +105,7 @@ fn over_budget_sweep_errors_and_restores_document() {
#[test]
fn thin_tilted_plates_cannot_cross_between_axis_bound_samples() {
let mut s = Session::default();
let normal = solvecraft_doc::apply_vector(&solvecraft_doc::rigid(Vec3::ZERO, Vec3::ZERO, Vec3::new(1.0, 1.0, 0.0), 55_f64.to_radians()), Vec3::Z);
let normal = solidcraft_doc::apply_vector(&solidcraft_doc::rigid(Vec3::ZERO, Vec3::ZERO, Vec3::new(1.0, 1.0, 0.0), 55_f64.to_radians()), Vec3::Z);
for name in ["Plate A", "Plate B"] {
run(&mut s, "component.activate", json!({"component": 0}));
run(&mut s, "component.create", json!({"name": name}));
+2 -2
View File
@@ -5,8 +5,8 @@
//! changing anything, so the UI can ask first.
use serde_json::{Value, json};
use solvecraft_doc::FeatureKind;
use solvecraft_geom::Vec3;
use solidcraft_doc::FeatureKind;
use solidcraft_geom::Vec3;
use super::CommandSpec;
use crate::params::{bad, bool_};
+4 -4
View File
@@ -24,7 +24,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("diagnostics.crash_test", "Crash Test", crash_test).noundo().enabled(crash_test_enabled).params(
"mode: panic | kernel | abort. Checks that crash reports work: panic = an internal error in this command (caught; \
the design is unchanged), kernel = one inside the geometry kernel (caught), abort = the app ends at once (reported \
at the next start). Only when the SOLVECRAFT_CRASH_TEST environment variable is set",
at the next start). Only when the SOLIDCRAFT_CRASH_TEST environment variable is set",
),
];
@@ -52,18 +52,18 @@ fn clear(_: &mut Session, p: &Value) -> Result<Value> {
}
fn crash_test_enabled(_: &Session) -> std::result::Result<(), String> {
if std::env::var_os("SOLVECRAFT_CRASH_TEST").is_some() { Ok(()) } else { Err("set SOLVECRAFT_CRASH_TEST to use the crash test".into()) }
if solidcraft_geom::env::var_os("CRASH_TEST").is_some() { Ok(()) } else { Err("set SOLIDCRAFT_CRASH_TEST to use the crash test".into()) }
}
// The one deliberate panic: it proves the crash reports work, and runs only when asked for with
// SOLVECRAFT_CRASH_TEST.
// SOLIDCRAFT_CRASH_TEST.
#[allow(clippy::panic)]
fn crash_test(_: &mut Session, p: &Value) -> Result<Value> {
let cmd = "diagnostics.crash_test";
match str_(p, "mode") {
Some("panic") => std::panic::panic_any(String::from("crash test: an internal error in a command")),
Some("kernel") => {
solvecraft_kernel::guard("crash test", || -> solvecraft_kernel::Result<()> {
solidcraft_kernel::guard("crash test", || -> solidcraft_kernel::Result<()> {
std::panic::panic_any(String::from("an internal error in the kernel"))
})?;
Err(EngineError::Other("crash test: the kernel error was not caught".into()))
+11 -11
View File
@@ -1,7 +1,7 @@
//! Undo/redo, the timeline, deletion and selection.
use serde_json::{Value, json};
use solvecraft_doc::{Feature, FeatureKind};
use solidcraft_doc::{Feature, FeatureKind};
use super::CommandSpec;
use crate::params::{bad, bool_, num, str_, string_list};
@@ -96,7 +96,7 @@ fn restore_checkpoint(s: &mut Session, p: &Value) -> Result<Value> {
}
fn compute_all(s: &mut Session, _p: &Value) -> Result<Value> {
s.model = solvecraft_doc::Model::new();
s.model = solidcraft_doc::Model::new();
s.refresh();
let errors = s.model.results.iter().filter(|r| r.error.is_some()).count();
Ok(json!({"features": s.model.results.len(), "errors": errors}))
@@ -303,7 +303,7 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, "the feature type cannot change"));
}
if let FeatureKind::BodyTransfer { bodies, from, to, transform } = &nf.kind {
solvecraft_doc::validate_body_transfer(&s.doc, bodies, *from, *to, transform)?;
solidcraft_doc::validate_body_transfer(&s.doc, bodies, *from, *to, transform)?;
nf.component = *to;
}
if let (FeatureKind::Sketch { sketch: a, .. }, FeatureKind::Sketch { sketch: b, .. }) = (&nf.kind, &f.kind)
@@ -319,8 +319,8 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result<Value> {
&& a != b
{
let before = s.model.state_before(id);
nf.edge_refs = solvecraft_doc::edge_refs(&before, body, a);
nf.edge_names = solvecraft_doc::edge_names_for(&before, body, a);
nf.edge_refs = solidcraft_doc::edge_refs(&before, body, a);
nf.edge_names = solidcraft_doc::edge_names_for(&before, body, a);
}
// Profiles picked by index or point are kept by their curves, as when the feature was made.
if nf.kind.clone().profile_sels_mut() != f.kind.clone().profile_sels_mut() {
@@ -328,7 +328,7 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result<Value> {
}
// New face points: their faces' names now.
if nf.kind.face_points() != f.kind.face_points() {
nf.face_names = solvecraft_doc::naming::face_names_at(&s.model.state_before(id), &nf.kind.face_points());
nf.face_names = solidcraft_doc::naming::face_names_at(&s.model.state_before(id), &nf.kind.face_points());
}
if let Some(slot) = s.doc_mut().feature_mut(id) {
*slot = nf;
@@ -372,7 +372,7 @@ fn select_clear(s: &mut Session, _p: &Value) -> Result<Value> {
}
/// Features that worked in `before` and fail in `after`: (name, error).
fn newly_broken(before: &solvecraft_doc::Model, after: &solvecraft_doc::Model) -> Vec<(String, String)> {
fn newly_broken(before: &solidcraft_doc::Model, after: &solidcraft_doc::Model) -> Vec<(String, String)> {
after
.results
.iter()
@@ -398,7 +398,7 @@ fn reorder(s: &mut Session, p: &Value) -> Result<Value> {
let name = f.name.clone();
next.features.insert(to, f);
// Everything that evaluated before must still evaluate.
let mut after = solvecraft_doc::Model::new();
let mut after = solidcraft_doc::Model::new();
after.evaluate(&next);
let broken = newly_broken(&s.model, &after);
if !broken.is_empty() {
@@ -409,13 +409,13 @@ fn reorder(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"feature": id, "position": to}))
}
fn input_keys(kind: &FeatureKind) -> Vec<(String, solvecraft_doc::expr::Kind)> {
fn input_keys(kind: &FeatureKind) -> Vec<(String, solidcraft_doc::expr::Kind)> {
let mut keys: Vec<_> = kind.inputs().into_iter().map(|(label, _, unit)| (label.to_string(), unit)).collect();
if let FeatureKind::Fillet { style, .. } = kind {
if let Some((key, _)) = keys.first_mut() {
*key = "fillet.radius".into();
}
if let solvecraft_doc::FilletStyle::Variable { mid, .. } = style {
if let solidcraft_doc::FilletStyle::Variable { mid, .. } = style {
for ((key, _), (position, _)) in keys.iter_mut().skip(2).zip(mid) {
*key = format!("fillet.mid.{}", position.to_bits());
}
@@ -583,7 +583,7 @@ fn dependents(s: &mut Session, p: &Value) -> Result<Value> {
let id = feature_id(s, p.get("feature"), cmd)?;
let mut next = (*s.doc).clone();
let gone = next.delete_feature(id)?;
let mut after = solvecraft_doc::Model::new();
let mut after = solidcraft_doc::Model::new();
after.evaluate(&next);
let names = |ids: &[u64]| -> Vec<String> { ids.iter().filter(|x| **x != id).filter_map(|x| s.doc.feature(*x).map(|f| f.name.clone())).collect() };
let broken: Vec<String> = newly_broken(&s.model, &after).into_iter().map(|(n, _)| n).collect();
+7 -7
View File
@@ -135,7 +135,7 @@ fn redefine_rejects_sheet_exports_before_they_touch_any_files() {
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "sheet.flange", json!({"name": "Plate"}));
let doc = s.doc.clone();
let base = std::env::temp_dir().join(format!("solvecraft-redefine-export-{}", std::process::id()));
let base = std::env::temp_dir().join(format!("solidcraft-redefine-export-{}", std::process::id()));
std::fs::create_dir_all(&base).unwrap();
for command in ["sheet.export_dxf", "sheet.export_bend_table"] {
for existing in [false, true] {
@@ -182,9 +182,9 @@ fn redefine_keeps_allocator_above_component_and_occurrence_ids() {
assert_eq!(s.doc.find_feature("Join").unwrap().id, original_feature);
let combine_index = s.doc.feature_index(original_feature).unwrap();
assert!(
matches!(&s.doc.features[combine_index + 1].kind, solvecraft_doc::FeatureKind::BodyTransfer { bodies, to, .. } if bodies == &["Base"] && *to == destination)
matches!(&s.doc.features[combine_index + 1].kind, solidcraft_doc::FeatureKind::BodyTransfer { bodies, to, .. } if bodies == &["Base"] && *to == destination)
);
assert!((solvecraft_kernel::measure(&s.world_state().body("Base").unwrap().body).unwrap().volume - 6000.0).abs() < 1e-6);
assert!((solidcraft_kernel::measure(&s.world_state().body("Base").unwrap().body).unwrap().volume - 6000.0).abs() < 1e-6);
assert_eq!(s.world_state().bodies.len(), 1);
run(&mut s, "timeline.rollback", json!({"position":combine_index + 1}));
assert_eq!(s.world_state().body("Base").unwrap().component, 0, "the result transfers only AFTER Combine");
@@ -192,9 +192,9 @@ fn redefine_keeps_allocator_above_component_and_occurrence_ids() {
run(&mut s, "edit.undo", json!({}));
assert_eq!(*s.doc, *original);
run(&mut s, "edit.redo", json!({}));
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert_eq!(reopened.world_state().body("Base").unwrap().component, destination);
assert!((solvecraft_kernel::measure(&reopened.world_state().body("Base").unwrap().body).unwrap().volume - 6000.0).abs() < 1e-6);
assert!((solidcraft_kernel::measure(&reopened.world_state().body("Base").unwrap().body).unwrap().volume - 6000.0).abs() < 1e-6);
let max_id =
s.doc.components.iter().map(|c| c.id).chain(s.doc.occurrences.iter().map(|o| o.id)).chain(s.doc.features.iter().map(|f| f.id)).max().unwrap();
assert!(s.doc.next_id > max_id);
@@ -215,7 +215,7 @@ fn redefine_cannot_publish_a_second_transfer_that_breaks_existing_ownership() {
let full = s.doc.to_json();
let join_index = s.doc.features.iter().position(|feature| feature.name == "Join").unwrap();
for marker in [None, Some(join_index), Some(join_index + 1)] {
let mut s = Session::new(solvecraft_doc::Document::from_json(&full).unwrap());
let mut s = Session::new(solidcraft_doc::Document::from_json(&full).unwrap());
s.doc_mut().marker = marker;
s.refresh();
let before = s.doc.to_json();
@@ -225,7 +225,7 @@ fn redefine_cannot_publish_a_second_transfer_that_breaks_existing_ownership() {
assert_eq!(s.doc.to_json(), before);
assert_eq!(s.pending_moves, pending);
}
let reopened = Session::new(solvecraft_doc::Document::from_json(&full).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&full).unwrap());
assert_eq!(reopened.world_state().body("Base").unwrap().component, reopened.doc.find_component("Original").unwrap());
assert!(reopened.doc.features.iter().all(|feature| reopened.model.result(feature.id).is_some_and(|result| result.error.is_none())));
}
+2 -2
View File
@@ -3,8 +3,8 @@
//! edges become lines, circular edges circles or arcs, anything else a chain of lines.
use serde_json::{Value, json};
use solvecraft_doc::{EmbossFace, apply_point, mat_inverse, mat_mul};
use solvecraft_geom::{Mesh, Vec2, Vec3};
use solidcraft_doc::{EmbossFace, apply_point, mat_inverse, mat_mul};
use solidcraft_geom::{Mesh, Vec2, Vec3};
use crate::params::bad;
use crate::{Result, Session};
+8 -8
View File
@@ -1,8 +1,8 @@
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use crate::Session;
use serde_json::{Value, json};
use solvecraft_doc::{Document, FeatureKind, PlaneRef};
use solvecraft_geom::Vec3;
use solidcraft_doc::{Document, FeatureKind, PlaneRef};
use solidcraft_geom::Vec3;
use std::f64::consts::PI;
fn run(s: &mut Session, command: &str, params: Value) -> Value {
@@ -12,7 +12,7 @@ fn check(s: &mut Session, want: f64, hole: bool, height: f64) {
s.refresh();
assert!(s.model.results.iter().all(|result| result.error.is_none() && result.warning.is_none()), "{:?}", s.model.results);
let state = s.model.state();
let got: f64 = state.bodies.iter().map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).sum();
let got: f64 = state.bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).sum();
// Legacy linked outlines fit tessellated curves; enforce the existing 1e-4 material envelope.
assert!((got - want).abs() < want * 1e-4, "{got} vs {want}");
assert!(state.bodies.iter().any(|b| b.body.contains(Vec3::new(5.0, 5.0, height - 1.0))));
@@ -39,7 +39,7 @@ fn generated_face_profiles_keep_exact_links_live_after_edit_and_reopen() {
let FeatureKind::Sketch { plane, sketch: profile } = &generated.kind else { panic!("sketch") };
let PlaneRef::Face { anchor: Some(anchor), .. } = plane else { panic!("the original Box cap remains exact after a hole cut") };
assert_eq!(anchor.target.origin_feature, base);
assert!(matches!(anchor.target.certificate, Some(solvecraft_doc::config::FaceCertificate::Box { axis: 2, high: true })));
assert!(matches!(anchor.target.certificate, Some(solidcraft_doc::config::FaceCertificate::Box { axis: 2, high: true })));
assert!(!profile.links.is_empty());
assert!(profile.links.iter().all(|link| link.auto_face && !link.lost));
let area = 1200.0 - if hole { 25.0 * PI } else { 0.0 };
@@ -54,12 +54,12 @@ fn generated_face_profiles_keep_exact_links_live_after_edit_and_reopen() {
let solved = edited.sketch(sketch).unwrap().plane;
assert!((solved.origin.z - 25.0).abs() < 1e-6);
let before = s.model.state();
let before_volume: f64 = before.bodies.iter().map(|body| solvecraft_kernel::measure(&body.body).unwrap().volume).sum();
let before_volume: f64 = before.bodies.iter().map(|body| solidcraft_kernel::measure(&body.body).unwrap().volume).sum();
let before_bounds: Vec<_> = before.bodies.iter().map(|body| (body.name.clone(), body.mesh().bounds())).collect();
let mut reopened = Session::new(Document::from_json(&s.doc.to_json()).unwrap());
check(&mut reopened, area * 35.0, hole, 35.0);
let after = reopened.model.state();
let after_volume: f64 = after.bodies.iter().map(|body| solvecraft_kernel::measure(&body.body).unwrap().volume).sum();
let after_volume: f64 = after.bodies.iter().map(|body| solidcraft_kernel::measure(&body.body).unwrap().volume).sum();
assert!((before_volume - after_volume).abs() < 1e-6);
for (name, bounds) in before_bounds {
let cold = after.body(&name).unwrap().mesh().bounds();
@@ -121,12 +121,12 @@ fn generated_profiles_and_press_pull_convert_rotated_occurrence_picks_once() {
if hole {
assert!(!world.bodies.iter().any(|body| body.body.contains(Vec3::new(50.0, 21.0 - height, 25.0))));
}
let before_volume: f64 = world.bodies.iter().map(|body| solvecraft_kernel::measure(&body.body).unwrap().volume).sum();
let before_volume: f64 = world.bodies.iter().map(|body| solidcraft_kernel::measure(&body.body).unwrap().volume).sum();
let before_bounds: Vec<_> = world.bodies.iter().map(|body| (body.name.clone(), body.mesh().bounds())).collect();
let reopened = Session::new(Document::from_json(&s.doc.to_json()).unwrap());
let after = reopened.world_state();
assert!(after.bodies.iter().any(|body| body.body.contains(occupied)));
let v: f64 = after.bodies.iter().map(|body| solvecraft_kernel::measure(&body.body).unwrap().volume).sum();
let v: f64 = after.bodies.iter().map(|body| solidcraft_kernel::measure(&body.body).unwrap().volume).sum();
assert!((v - area * height).abs() < area * height * 1e-4);
assert!((v - before_volume).abs() < 1e-6);
for (name, bounds) in before_bounds {
+59 -59
View File
@@ -1,8 +1,8 @@
//! Solid feature commands: extrude, revolve, fillet, chamfer, primitives, combine, move.
use serde_json::{Value, json};
use solvecraft_doc::{AxisRef, Direction, Extent, FeatureKind, Operation, PointExpr, ProfileSel, expr::Kind, point_expr};
use solvecraft_geom::Vec3;
use solidcraft_doc::{AxisRef, Direction, Extent, FeatureKind, Operation, PointExpr, ProfileSel, expr::Kind, point_expr};
use solidcraft_geom::Vec3;
use super::CommandSpec;
use crate::params::{bad, bool_, expr, req_expr, str_, string_list, vec2, vec3};
@@ -125,7 +125,7 @@ fn extrude_base(s: &Session, p: &Value) -> Option<(Vec3, Vec3)> {
ProfileSel::Indices { indices } => ps.get(*indices.first()?)?.region.interior_point(),
ProfileSel::Points { points } => *points.first()?,
selection @ (ProfileSel::Curves { .. } | ProfileSel::OffsetBoundaries { .. } | ProfileSel::WholeBoundaries { .. }) => {
let indices = solvecraft_doc::profile_selection_indices(ss, &selection).ok()?;
let indices = solidcraft_doc::profile_selection_indices(ss, &selection).ok()?;
ps.get(*indices.first()?)?.region.interior_point()
}
};
@@ -210,12 +210,12 @@ pub(super) fn feature_sketch(s: &Session, p: &Value, cmd: &str) -> Result<u64> {
/// curves) stays as given. Curve picks are checked: one that names no profile, or several, is
/// refused now rather than failing later.
pub(super) fn stable_profiles(s: &Session, kind: &mut FeatureKind) -> Result<()> {
use solvecraft_doc::sketch::profile_at_point;
use solidcraft_doc::sketch::profile_at_point;
let st = s.model.state();
for (sketch, sel) in kind.profile_sels_mut() {
let Some(ss) = st.sketch(sketch) else { continue };
let indices = match sel {
ProfileSel::Indices { .. } => solvecraft_doc::profile_selection_indices(ss, sel)?,
ProfileSel::Indices { .. } => solidcraft_doc::profile_selection_indices(ss, sel)?,
ProfileSel::Points { points } => {
let Some(indices) = points.iter().map(|point| profile_at_point(&ss.profiles, *point)).collect::<Option<Vec<_>>>() else { continue };
indices
@@ -240,19 +240,19 @@ pub(super) fn stable_profiles(s: &Session, kind: &mut FeatureKind) -> Result<()>
}
}
let selection = ProfileSel::Curves { loops: normalized };
solvecraft_doc::profile_selection_indices(ss, &selection)?;
solidcraft_doc::profile_selection_indices(ss, &selection)?;
*sel = selection;
continue;
}
ProfileSel::OffsetBoundaries { .. } | ProfileSel::WholeBoundaries { .. } => {
// Explicit identities retain the caller's semantics. Validate through the
// same resolver as recomputation, including reserved-role precedence.
solvecraft_doc::profile_selection_indices(ss, sel)?;
solidcraft_doc::profile_selection_indices(ss, sel)?;
continue;
}
ProfileSel::All => continue,
};
let captured = solvecraft_doc::capture_profile_selection(ss, &indices)?;
let captured = solidcraft_doc::capture_profile_selection(ss, &indices)?;
*sel = match captured {
ProfileSel::OffsetBoundaries { associations }
if associations.iter().any(|id| {
@@ -288,7 +288,7 @@ pub(super) fn profiles(p: &Value, cmd: &str) -> Result<ProfileSel> {
match v {
Value::Object(fields) => {
if fields.get("type").and_then(Value::as_str) == Some("whole_boundaries") {
return solvecraft_doc::whole_profile_selection_from_value(v).map_err(|error| bad(cmd, error.to_string()));
return solidcraft_doc::whole_profile_selection_from_value(v).map_err(|error| bad(cmd, error.to_string()));
}
let associations = fields
.get("offset_boundaries")
@@ -384,11 +384,11 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
let (refs, names) = match &kind {
FeatureKind::Fillet { edges, body, .. } | FeatureKind::Chamfer { edges, body, .. } => {
let st = s.model.state();
(solvecraft_doc::edge_refs(&st, body, edges), solvecraft_doc::edge_names_for(&st, body, edges))
(solidcraft_doc::edge_refs(&st, body, edges), solidcraft_doc::edge_names_for(&st, body, edges))
}
// Sheet metal edges are named by the sheet model.
FeatureKind::SheetFlange { edges, .. } | FeatureKind::SheetHem { edges, .. } => {
(Vec::new(), solvecraft_doc::sheet::sheet_edge_names(&s.model.state(), edges))
(Vec::new(), solidcraft_doc::sheet::sheet_edge_names(&s.model.state(), edges))
}
_ => (Vec::new(), Vec::new()),
};
@@ -398,11 +398,11 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
let state = s.model.state();
let body = state.body(&face.body).ok_or_else(|| EngineError::Other("selected cylinder body no longer exists".into()))?;
let frame = face.body_frame(&s.doc, body).map_err(|e| EngineError::Other(e.to_string()))?;
let inverse = solvecraft_doc::mat_inverse(&frame).ok_or_else(|| EngineError::Other("invalid cylinder placement".into()))?;
let at = solvecraft_doc::apply_point(&solvecraft_doc::mat_mul(&inverse, &s.active_component_frame()), face.at);
let mut scoped = solvecraft_doc::ModelState::default();
let inverse = solidcraft_doc::mat_inverse(&frame).ok_or_else(|| EngineError::Other("invalid cylinder placement".into()))?;
let at = solidcraft_doc::apply_point(&solidcraft_doc::mat_mul(&inverse, &s.active_component_frame()), face.at);
let mut scoped = solidcraft_doc::ModelState::default();
scoped.bodies.push(body.clone());
let names = solvecraft_doc::naming::face_names_at(&scoped, &[at]);
let names = solidcraft_doc::naming::face_names_at(&scoped, &[at]);
if names.len() != 1 || names.first().is_none_or(|name| name.is_empty()) {
return Err(EngineError::Other("selected cylinder face has no persistent identity".into()));
}
@@ -410,7 +410,7 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
} else if pts.is_empty() {
Vec::new()
} else {
solvecraft_doc::naming::face_names_at(&s.model.state(), &pts)
solidcraft_doc::naming::face_names_at(&s.model.state(), &pts)
}
};
// Primitives have no `targets` of their own: their `targets` are the bodies taking part (#259).
@@ -578,7 +578,7 @@ fn revolve(s: &mut Session, p: &Value) -> Result<Value> {
let symmetric =
matches!(str_(p, "direction").map(str::to_ascii_lowercase).as_deref(), Some("symmetric")) || bool_(p, "symmetric").unwrap_or(false);
let convention = match p.get("convention") {
None => solvecraft_doc::RevolveConvention::SketchNormal,
None => solidcraft_doc::RevolveConvention::SketchNormal,
Some(value) => serde_json::from_value(value.clone()).map_err(|_| bad(cmd, "`convention` must be sketch_normal or legacy_axis_start"))?,
};
let kind = FeatureKind::Revolve {
@@ -729,7 +729,7 @@ fn press_pull(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(fp) = vec3(face)
&& super::face::face_is_planar(s, fp) == Some(false)
{
let name = solvecraft_doc::naming::face_names_at(&s.model.state(), &[fp]).into_iter().next().unwrap_or_default();
let name = solidcraft_doc::naming::face_names_at(&s.model.state(), &[fp]).into_iter().next().unwrap_or_default();
let fid = name.strip_prefix('F').and_then(|r| r.split(':').next()).and_then(|x| x.parse::<u64>().ok());
if name.contains(":blend") {
let f = fid.and_then(|id| s.doc.feature(id)).cloned().ok_or_else(|| bad(cmd, "the blend's feature is gone"))?;
@@ -757,8 +757,8 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
let radius = req_expr(cmd, p, "radius")?;
check_expr(s, &radius, Kind::Length, cmd, "radius")?;
let style = match str_(p, "type").map(str::to_ascii_lowercase).as_deref() {
None | Some("constant") | Some("rolling_ball") => solvecraft_doc::FilletStyle::Constant,
Some("chord") | Some("chord_length") => solvecraft_doc::FilletStyle::Chord,
None | Some("constant") | Some("rolling_ball") => solidcraft_doc::FilletStyle::Constant,
Some("chord") | Some("chord_length") => solidcraft_doc::FilletStyle::Chord,
Some("variable") => {
let radius2 = req_expr(cmd, p, "radius2")?;
check_expr(s, &radius2, Kind::Length, cmd, "radius2")?;
@@ -781,13 +781,13 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
mid.push((t, r));
}
let smooth = p.get("smooth").and_then(Value::as_bool).unwrap_or(false);
solvecraft_doc::FilletStyle::Variable { radius2, start, mid, smooth }
solidcraft_doc::FilletStyle::Variable { radius2, start, mid, smooth }
}
Some(o) => return Err(bad(cmd, format!("unknown fillet type `{o}` (constant, chord or variable)"))),
};
let fit = match str_(p, "fit").map(str::to_ascii_lowercase).as_deref() {
None | Some("exact") => solvecraft_doc::FilletFit::Exact,
Some("reduce") => solvecraft_doc::FilletFit::Reduce,
None | Some("exact") => solidcraft_doc::FilletFit::Exact,
Some("reduce") => solidcraft_doc::FilletFit::Reduce,
Some(o) => return Err(bad(cmd, format!("unknown fit `{o}` (exact or reduce)"))),
};
let mut out = add_feature(s, p, FeatureKind::Fillet { edges, radius, body: str_(p, "body").map(str::to_string), style, fit })?;
@@ -909,19 +909,19 @@ fn axis_line(p: &Value, cmd: &str) -> Result<(Vec3, Vec3)> {
}
/// A pattern feature, or for `components`, new occurrences of them at each placement.
fn finish_pattern(s: &mut Session, p: &Value, features: Vec<String>, pattern: solvecraft_doc::PatternKind, bodies: Vec<String>) -> Result<Value> {
fn finish_pattern(s: &mut Session, p: &Value, features: Vec<String>, pattern: solidcraft_doc::PatternKind, bodies: Vec<String>) -> Result<Value> {
let comps = string_list(p, "components");
if comps.is_empty() {
return add_feature(s, p, FeatureKind::Pattern { features, pattern, bodies });
}
let cmd = "pattern (components)";
let mats = solvecraft_doc::pattern_matrices(&s.doc, &s.model.state(), &pattern)?;
let mats = solidcraft_doc::pattern_matrices(&s.doc, &s.model.state(), &pattern)?;
let mut made = Vec::new();
for c in &comps {
let id = s.doc.find_component(c).filter(|id| *id != 0).ok_or_else(|| bad(cmd, format!("no component `{c}`")))?;
let src = s.doc.occurrence_of(id).cloned().ok_or_else(|| bad(cmd, format!("component `{c}` has no occurrence")))?;
for m in &mats {
made.push(s.doc_mut().add_occurrence(id, src.parent, solvecraft_doc::mat_mul(m, &src.transform))?);
made.push(s.doc_mut().add_occurrence(id, src.parent, solidcraft_doc::mat_mul(m, &src.transform))?);
}
}
Ok(json!({"occurrences": made}))
@@ -960,7 +960,7 @@ fn pattern_rect(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(c) = &spacing2 {
check_expr(s, c, Kind::Length, cmd, "spacing2")?;
}
let pattern = solvecraft_doc::PatternKind::Rectangular { dir1, count1, spacing1, dir2, count2, spacing2 };
let pattern = solidcraft_doc::PatternKind::Rectangular { dir1, count1, spacing1, dir2, count2, spacing2 };
finish_pattern(s, p, features, pattern, bodies)
}
@@ -972,7 +972,7 @@ fn pattern_circ(s: &mut Session, p: &Value) -> Result<Value> {
check_expr(s, &count, Kind::Unitless, cmd, "count")?;
let angle = expr(p, "angle").unwrap_or_else(|| "360 deg".into());
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
let pattern = solvecraft_doc::PatternKind::Circular { origin, axis, count, angle };
let pattern = solidcraft_doc::PatternKind::Circular { origin, axis, count, angle };
finish_pattern(s, p, features, pattern, bodies)
}
@@ -998,7 +998,7 @@ fn pattern_path(s: &mut Session, p: &Value) -> Result<Value> {
Some(o) => return Err(bad(cmd, format!("unknown orientation `{o}` (identical or path)"))),
};
let flip = bool_(p, "flip").unwrap_or(false);
let pattern = solvecraft_doc::PatternKind::Path { path_sketch, path, count, spacing, extent, flip, orient };
let pattern = solidcraft_doc::PatternKind::Path { path_sketch, path, count, spacing, extent, flip, orient };
finish_pattern(s, p, features, pattern, bodies)
}
@@ -1027,7 +1027,7 @@ pub(super) fn origin_plane_alias(name: &str) -> Option<&'static str> {
/// A construction plane may not take an origin plane's name (`XY`, `Top`...): a plane
/// parameter could then mean either.
pub(super) fn check_plane_name(name: &str, cmd: &str) -> Result<()> {
if solvecraft_geom::Plane::named(name.trim()).is_some() {
if solidcraft_geom::Plane::named(name.trim()).is_some() {
return Err(bad(
cmd,
format!("`{}` is reserved for an origin plane (XY, XZ, YZ, Top, Front, Right); give the construction plane another name", name.trim()),
@@ -1040,8 +1040,8 @@ pub(super) fn check_plane_name(name: &str, cmd: &str) -> Result<()> {
/// feature id; kept by name, so the feature follows the plane when it changes), or an
/// explicit plane `{origin, x_dir, y_dir}` / `{origin, normal}`. Every command that takes a
/// plane reads it here.
pub(super) fn plane_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result<solvecraft_doc::PlaneRef> {
use solvecraft_doc::PlaneRef;
pub(super) fn plane_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result<solidcraft_doc::PlaneRef> {
use solidcraft_doc::PlaneRef;
const WANT: &str = "XY, XZ, YZ, a construction plane name or id, or {origin, normal}";
let construction = |r: &str| match s.doc.find_feature(r) {
Some(f) if matches!(f.kind, FeatureKind::ConstructionPlane { .. }) => Ok(PlaneRef::Construction { name: f.name.clone() }),
@@ -1049,7 +1049,7 @@ pub(super) fn plane_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result<s
None => Err(bad(cmd, format!("no plane `{r}` (a plane is {WANT})"))),
};
match v {
Some(Value::String(n)) if solvecraft_geom::Plane::named(n.trim()).is_some() => {
Some(Value::String(n)) if solidcraft_geom::Plane::named(n.trim()).is_some() => {
if let Some(f) = s.doc.find_feature(n.trim())
&& matches!(f.kind, FeatureKind::ConstructionPlane { .. })
{
@@ -1076,8 +1076,8 @@ pub(super) fn plane_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result<s
Some(x) => vec3(x).ok_or_else(|| bad(cmd, "the plane's `origin` must be a point [x, y, z]"))?,
};
let pl = match (o.get("x_dir").and_then(vec3), o.get("y_dir").and_then(vec3), o.get("normal").and_then(vec3)) {
(Some(x), Some(y), _) => solvecraft_geom::Plane::new(origin, x, y),
(_, _, Some(n)) => solvecraft_geom::Plane::from_normal(origin, n),
(Some(x), Some(y), _) => solidcraft_geom::Plane::new(origin, x, y),
(_, _, Some(n)) => solidcraft_geom::Plane::from_normal(origin, n),
_ => None,
};
Ok(PlaneRef::Custom { plane: pl.ok_or_else(|| bad(cmd, "plane needs x_dir and y_dir, or normal"))? })
@@ -1094,12 +1094,12 @@ fn plane_offset(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(face) = p.get("base").filter(|b| b.get("face").is_some()) {
let base = super::sketch::face::capture(s, face, cmd)?;
super::sketch::face::capture_positions(s, &base)?;
let kind = FeatureKind::ConstructionPlane { plane: solvecraft_doc::PlaneRef::Offset { base: Box::new(base), distance: d } };
let kind = FeatureKind::ConstructionPlane { plane: solidcraft_doc::PlaneRef::Offset { base: Box::new(base), distance: d } };
// The capture is already in the authoring frame.
return add_feature_in(s, p, kind, false);
}
let base = plane_param(s, p.get("base"), cmd)?;
add_feature(s, p, FeatureKind::ConstructionPlane { plane: solvecraft_doc::PlaneRef::Offset { base: Box::new(base), distance: d } })
add_feature(s, p, FeatureKind::ConstructionPlane { plane: solidcraft_doc::PlaneRef::Offset { base: Box::new(base), distance: d } })
}
fn plane_angle(s: &mut Session, p: &Value) -> Result<Value> {
@@ -1111,7 +1111,7 @@ fn plane_angle(s: &mut Session, p: &Value) -> Result<Value> {
add_feature(
s,
p,
FeatureKind::ConstructionPlane { plane: solvecraft_doc::PlaneRef::AtAngle { base: Box::new(base), axis_origin, axis_dir, angle } },
FeatureKind::ConstructionPlane { plane: solidcraft_doc::PlaneRef::AtAngle { base: Box::new(base), axis_origin, axis_dir, angle } },
)
}
@@ -1119,11 +1119,11 @@ fn split_face(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "solid.split_face";
let faces = face_points(p, cmd)?;
let body = str_(p, "body").map(str::to_string);
let xy = || solvecraft_doc::PlaneRef::Origin { name: "XY".into() };
let xy = || solidcraft_doc::PlaneRef::Origin { name: "XY".into() };
let kind = if let Some(t) = p.get("tool").filter(|t| !t.is_null()) {
let tb = t.get("body").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`tool.body` must name a body"))?.to_string();
let point = t.get("point").and_then(crate::params::vec3).ok_or_else(|| bad(cmd, "`tool.point` must be [x, y, z] on the tool face"))?;
FeatureKind::SplitFace { faces, body, plane: xy(), tool: Some(solvecraft_doc::FaceAt { body: tb, point }), sketch: None }
FeatureKind::SplitFace { faces, body, plane: xy(), tool: Some(solidcraft_doc::FaceAt { body: tb, point }), sketch: None }
} else if p.get("sketch").is_some_and(|v| !v.is_null()) {
let sketch = sketch_id(s, p.get("sketch"), cmd, "sketch")?;
FeatureKind::SplitFace { faces, body, plane: xy(), tool: None, sketch: Some(sketch) }
@@ -1146,7 +1146,7 @@ fn split_body(s: &mut Session, p: &Value) -> Result<Value> {
let point = t.get("point").and_then(crate::params::vec3).ok_or_else(|| bad(cmd, "`tool.point` must be [x, y, z] on the tool face"))?;
let st = s.model.state();
let tbody = st.body(&tb).ok_or_else(|| bad(cmd, format!("no body `{tb}`")))?;
if solvecraft_doc::appearance::face_index_at(tbody, point).is_none() {
if solidcraft_doc::appearance::face_index_at(tbody, point).is_none() {
return Err(bad(cmd, "`tool.point` is not on a face of the tool body"));
}
return add_feature(
@@ -1154,8 +1154,8 @@ fn split_body(s: &mut Session, p: &Value) -> Result<Value> {
p,
FeatureKind::Split {
body,
plane: solvecraft_doc::PlaneRef::Origin { name: "XY".into() },
tool: Some(solvecraft_doc::FaceAt { body: tb, point }),
plane: solidcraft_doc::PlaneRef::Origin { name: "XY".into() },
tool: Some(solidcraft_doc::FaceAt { body: tb, point }),
},
);
}
@@ -1221,13 +1221,13 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
let n = ss.plane.normal();
let first =
ids.first().and_then(|id| ss.sketch.points.iter().find(|q| &q.id == id)).map(|q| ss.plane.to_world(q.pos)).unwrap_or_default();
Some((solvecraft_doc::SketchPoints { sketch, ids }, first, -n))
Some((solidcraft_doc::SketchPoints { sketch, ids }, first, -n))
}
_ => None,
};
let thread = str_(p, "thread").map(|t| t.trim().to_string()).filter(|t| !t.is_empty());
if let Some(t) = &thread
&& solvecraft_doc::parse_metric_thread(t).is_none()
&& solidcraft_doc::parse_metric_thread(t).is_none()
{
return Err(bad(cmd, format!("unknown thread `{t}` (ISO metric, like M6 or M6x0.75)")));
}
@@ -1268,7 +1268,7 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
format!("{d} mm")
}
(None, Some(t), None) => {
let (major, pitch) = solvecraft_doc::parse_metric_thread(t).ok_or_else(|| bad(cmd, "thread"))?;
let (major, pitch) = solidcraft_doc::parse_metric_thread(t).ok_or_else(|| bad(cmd, "thread"))?;
format!("{} mm", ((major - pitch) * 1000.0).round() / 1000.0)
}
(None, None, None) => return Err(bad(cmd, "needs `diameter`, `thread` (a tapped hole) or `clearance` (a bolt size)")),
@@ -1284,12 +1284,12 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
};
let kind = match str_(p, "type").unwrap_or("simple") {
"simple" => match expr(p, "tip_angle") {
Some(t) if depth.is_some() => solvecraft_doc::HoleKind::Drilled { tip_angle: t },
_ => solvecraft_doc::HoleKind::Simple,
Some(t) if depth.is_some() => solidcraft_doc::HoleKind::Drilled { tip_angle: t },
_ => solidcraft_doc::HoleKind::Simple,
},
"drilled" => solvecraft_doc::HoleKind::Drilled { tip_angle: expr(p, "tip_angle").unwrap_or_else(|| "118 deg".into()) },
"counterbore" => solvecraft_doc::HoleKind::Counterbore { cb_diameter: get("cb_diameter")?, cb_depth: get("cb_depth")? },
"countersink" => solvecraft_doc::HoleKind::Countersink {
"drilled" => solidcraft_doc::HoleKind::Drilled { tip_angle: expr(p, "tip_angle").unwrap_or_else(|| "118 deg".into()) },
"counterbore" => solidcraft_doc::HoleKind::Counterbore { cb_diameter: get("cb_diameter")?, cb_depth: get("cb_depth")? },
"countersink" => solidcraft_doc::HoleKind::Countersink {
cs_diameter: get("cs_diameter")?,
cs_angle: expr(p, "cs_angle").unwrap_or_else(|| "90 deg".into()),
},
@@ -1309,11 +1309,11 @@ fn sketch_hole_points(s: &Session, sketch: u64) -> Vec<String> {
let mut ends: Vec<usize> = Vec::new();
for c in &ss.sketch.curves {
match &c.kind {
solvecraft_sketch::CurveKind::Circle { .. } => {}
solvecraft_sketch::CurveKind::Arc { a, b, .. } | solvecraft_sketch::CurveKind::Line { a, b } => ends.extend([*a, *b]),
solvecraft_sketch::CurveKind::Ellipse { m, .. } => ends.push(*m),
solvecraft_sketch::CurveKind::Spline { pts, .. } => ends.extend(pts.iter().copied()),
solvecraft_sketch::CurveKind::Conic { a, b, apex, .. } => ends.extend([*a, *b, *apex]),
solidcraft_sketch::CurveKind::Circle { .. } => {}
solidcraft_sketch::CurveKind::Arc { a, b, .. } | solidcraft_sketch::CurveKind::Line { a, b } => ends.extend([*a, *b]),
solidcraft_sketch::CurveKind::Ellipse { m, .. } => ends.push(*m),
solidcraft_sketch::CurveKind::Spline { pts, .. } => ends.extend(pts.iter().copied()),
solidcraft_sketch::CurveKind::Conic { a, b, apex, .. } => ends.extend([*a, *b, *apex]),
}
}
ss.sketch.points.iter().enumerate().filter(|(i, p)| p.id != "origin" && !ends.contains(i)).map(|(_, p)| p.id.clone()).collect()
@@ -1410,10 +1410,10 @@ fn loft(s: &mut Session, p: &Value) -> Result<Value> {
if !st.sketch(sketch).is_some_and(|ss| ss.sketch.points.iter().any(|q| q.id == pt)) {
return Err(bad(cmd, format!("no point `{pt}` in the section's sketch")));
}
sections.push(solvecraft_doc::LoftSection { sketch, profiles: solvecraft_doc::ProfileSel::All, point: Some(pt.to_string()) });
sections.push(solidcraft_doc::LoftSection { sketch, profiles: solidcraft_doc::ProfileSel::All, point: Some(pt.to_string()) });
continue;
}
sections.push(solvecraft_doc::LoftSection { sketch, profiles: profiles(sec, cmd)?, point: None });
sections.push(solidcraft_doc::LoftSection { sketch, profiles: profiles(sec, cmd)?, point: None });
}
add_feature(s, p, FeatureKind::Loft { sections, operation: operation(p, cmd)?, targets: string_list(p, "targets") })
}
@@ -1426,7 +1426,7 @@ fn thread(s: &mut Session, p: &Value) -> Result<Value> {
.ok_or_else(|| bad(cmd, "`face` must be a point [x, y, z] on a cylindrical face"))?;
let designation = str_(p, "designation").map(|t| t.trim().to_string()).filter(|t| !t.is_empty());
if let Some(t) = &designation
&& solvecraft_doc::parse_metric_thread(t).is_none()
&& solidcraft_doc::parse_metric_thread(t).is_none()
{
return Err(bad(cmd, format!("unknown thread `{t}` (ISO metric, like M8 or M8x1)")));
}
+32 -32
View File
@@ -1,9 +1,9 @@
//! Rib, Web, Emboss, Replace Face, Align and Remove.
use serde_json::Value;
use solvecraft_doc::FeatureKind;
use solvecraft_doc::expr::Kind;
use solvecraft_geom::Vec3;
use solidcraft_doc::FeatureKind;
use solidcraft_doc::expr::Kind;
use solidcraft_geom::Vec3;
use super::CommandSpec;
use super::features::{add_feature, axis_dir_param, check_expr, feature_sketch, plane_param, point_param, profiles};
@@ -43,7 +43,7 @@ pub static COMMANDS: &[CommandSpec] = &[
fn coil(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "solid.coil";
let base = point_param(s, p, "base", cmd)?.unwrap_or_else(|| solvecraft_doc::point_expr(Vec3::ZERO));
let base = point_param(s, p, "base", cmd)?.unwrap_or_else(|| solidcraft_doc::point_expr(Vec3::ZERO));
let axis = axis_dir_param(p, cmd)?;
let diameter = req_expr(cmd, p, "diameter")?;
check_expr(s, &diameter, Kind::Length, cmd, "diameter")?;
@@ -60,8 +60,8 @@ fn coil(s: &mut Session, p: &Value) -> Result<Value> {
let section_size = req_expr(cmd, p, "section_size")?;
check_expr(s, &section_size, Kind::Length, cmd, "section_size")?;
let section = match str_(p, "section").map(str::to_ascii_lowercase).as_deref() {
None | Some("circular" | "circle") => solvecraft_doc::CoilSection::Circular,
Some("square") => solvecraft_doc::CoilSection::Square,
None | Some("circular" | "circle") => solidcraft_doc::CoilSection::Circular,
Some("square") => solidcraft_doc::CoilSection::Square,
Some(o) => return Err(bad(cmd, format!("unknown section `{o}` (circular or square)"))),
};
let position = match str_(p, "section_position").map(str::to_ascii_lowercase).as_deref() {
@@ -146,7 +146,7 @@ fn emboss(s: &mut Session, p: &Value) -> Result<Value> {
let mut targets = string_list(p, "targets");
let face = if let Some(value) = p.get("face") {
let local = vec3(value).ok_or_else(|| bad(cmd, "`face` must be a finite point [x,y,z]"))?;
let at = solvecraft_doc::apply_point(&s.active_component_frame(), local);
let at = solidcraft_doc::apply_point(&s.active_component_frame(), local);
let state = s.world_state();
let mut candidates = Vec::new();
for body in &state.bodies {
@@ -179,19 +179,19 @@ fn emboss(s: &mut Session, p: &Value) -> Result<Value> {
let (selected, index) = candidates.first().copied().ok_or_else(|| bad(cmd, "no target face"))?;
let name = selected.definition_name();
let body = &selected.body;
match solvecraft_kernel::face_surface(body, index) {
Some(solvecraft_kernel::FaceSurface::Plane { .. }) => {
match solidcraft_kernel::face_surface(body, index) {
Some(solidcraft_kernel::FaceSurface::Plane { .. }) => {
targets = vec![name.to_owned()];
None
}
Some(solvecraft_kernel::FaceSurface::Cylinder { .. }) => {
Some(solidcraft_kernel::FaceSurface::Cylinder { .. }) => {
if targets.len() > 1 {
return Err(bad(cmd, "cylinder emboss needs one selected target body"));
}
// Viewport picks lie on tessellated chords. Keep their axial and
// angular position, and certify their distance before storing an
// exact surface anchor for later replay.
let at = solvecraft_kernel::cylinder_emboss_anchor(body, index, at).map_err(|e| bad(cmd, e.to_string()))?;
let at = solidcraft_kernel::cylinder_emboss_anchor(body, index, at).map_err(|e| bad(cmd, e.to_string()))?;
let face = emboss_selection(s, selected, at)?;
check_emboss_positions(s, sketch, selected, &face)?;
targets = vec![name.to_owned()];
@@ -209,7 +209,7 @@ fn emboss(s: &mut Session, p: &Value) -> Result<Value> {
add_feature(s, p, FeatureKind::Emboss { sketch, profiles, depth, deboss, targets, face })
}
fn emboss_selection(s: &Session, selected: &solvecraft_doc::ModelBody, at: Vec3) -> Result<solvecraft_doc::EmbossFace> {
fn emboss_selection(s: &Session, selected: &solidcraft_doc::ModelBody, at: Vec3) -> Result<solidcraft_doc::EmbossFace> {
let cmd = "solid.emboss";
let name = selected.definition_name();
let original = s.model.state();
@@ -229,11 +229,11 @@ fn emboss_selection(s: &Session, selected: &solvecraft_doc::ModelBody, at: Vec3)
if path.len() > 64 || author_path.len() > 64 {
return Err(bad(cmd, "cylinder placement is too deep"));
}
let inverse = solvecraft_doc::mat_inverse(&s.active_component_frame()).ok_or_else(|| bad(cmd, "invalid active frame"))?;
Ok(solvecraft_doc::EmbossFace { body: name.to_owned(), at: solvecraft_doc::apply_point(&inverse, at), path, author_path })
let inverse = solidcraft_doc::mat_inverse(&s.active_component_frame()).ok_or_else(|| bad(cmd, "invalid active frame"))?;
Ok(solidcraft_doc::EmbossFace { body: name.to_owned(), at: solidcraft_doc::apply_point(&inverse, at), path, author_path })
}
fn check_emboss_positions(s: &Session, sketch: u64, body: &solvecraft_doc::ModelBody, face: &solvecraft_doc::EmbossFace) -> Result<()> {
fn check_emboss_positions(s: &Session, sketch: u64, body: &solidcraft_doc::ModelBody, face: &solidcraft_doc::EmbossFace) -> Result<()> {
let cmd = "solid.emboss";
let owner = s.doc.model_body_component(body);
let sketch_owner = s.doc.feature(sketch).map(|f| f.component).ok_or_else(|| bad(cmd, "profile sketch no longer exists"))?;
@@ -253,15 +253,15 @@ fn check_emboss_positions(s: &Session, sketch: u64, body: &solvecraft_doc::Model
fn infer_emboss_selection(
s: &Session,
sketch: u64,
profiles: &solvecraft_doc::ProfileSel,
profiles: &solidcraft_doc::ProfileSel,
depth: &str,
deboss: bool,
targets: &[String],
) -> Result<Option<solvecraft_doc::EmbossFace>> {
) -> Result<Option<solidcraft_doc::EmbossFace>> {
let cmd = "solid.emboss";
let original = s.model.state();
let ss = original.sketch(sketch).ok_or_else(|| bad(cmd, "the sketch is not evaluated"))?;
let regions = solvecraft_doc::select_profiles(ss, profiles).map_err(|e| bad(cmd, e.to_string()))?;
let regions = solidcraft_doc::select_profiles(ss, profiles).map_err(|e| bad(cmd, e.to_string()))?;
let sketch_owner = s.doc.feature(sketch).map(|f| f.component).ok_or_else(|| bad(cmd, "profile sketch no longer exists"))?;
let d = s.doc.eval(&s.doc.with_design_unit(depth, Kind::Length), Kind::Length).map_err(|e| bad(cmd, e.to_string()))?;
let world = s.world_state();
@@ -278,26 +278,26 @@ fn infer_emboss_selection(
if faces > 50_000 {
return Err(bad(cmd, "cylinder target search exceeds face work limit"));
}
if !solvecraft_kernel::has_cylinder_emboss_wall(&raw.body).map_err(|e| bad(cmd, e.to_string()))? {
if !solidcraft_kernel::has_cylinder_emboss_wall(&raw.body).map_err(|e| bad(cmd, e.to_string()))? {
continue;
}
let mut face = emboss_selection(s, selected, Vec3::ZERO)?;
let body_frame = face.body_frame(&placed, raw).map_err(|e| bad(cmd, e.to_string()))?;
let inverse = solvecraft_doc::mat_inverse(&body_frame).ok_or_else(|| bad(cmd, "invalid cylinder body frame"))?;
let inverse = solidcraft_doc::mat_inverse(&body_frame).ok_or_else(|| bad(cmd, "invalid cylinder body frame"))?;
let displayed = world.sketch(sketch).ok_or_else(|| bad(cmd, "the profile sketch is not displayed"))?;
let plane = solvecraft_geom::Plane::new(
solvecraft_doc::apply_point(&inverse, displayed.plane.origin),
solvecraft_doc::apply_vector(&inverse, displayed.plane.x),
solvecraft_doc::apply_vector(&inverse, displayed.plane.y),
let plane = solidcraft_geom::Plane::new(
solidcraft_doc::apply_point(&inverse, displayed.plane.origin),
solidcraft_doc::apply_vector(&inverse, displayed.plane.x),
solidcraft_doc::apply_vector(&inverse, displayed.plane.y),
)
.ok_or_else(|| bad(cmd, "invalid cylinder sketch frame"))?;
if let Some((_, at)) =
solvecraft_kernel::infer_cylinder_emboss(&raw.body, &plane, &regions, if deboss { -d } else { d }).map_err(|e| bad(cmd, e.to_string()))?
solidcraft_kernel::infer_cylinder_emboss(&raw.body, &plane, &regions, if deboss { -d } else { d }).map_err(|e| bad(cmd, e.to_string()))?
{
face.sketch_frame(&placed, s.active_component, sketch_owner, raw).map_err(|e| bad(cmd, e.to_string()))?;
check_emboss_positions(s, sketch, selected, &face)?;
let author_inverse = solvecraft_doc::mat_inverse(&s.active_component_frame()).ok_or_else(|| bad(cmd, "invalid author frame"))?;
face.at = solvecraft_doc::apply_point(&solvecraft_doc::mat_mul(&author_inverse, &body_frame), at);
let author_inverse = solidcraft_doc::mat_inverse(&s.active_component_frame()).ok_or_else(|| bad(cmd, "invalid author frame"))?;
face.at = solidcraft_doc::apply_point(&solidcraft_doc::mat_mul(&author_inverse, &body_frame), at);
if chosen.is_some() {
return Err(bad(cmd, "multiple tangent cylinders match the selected profiles; select a target face or body"));
}
@@ -325,8 +325,8 @@ fn replace_face(s: &mut Session, p: &Value) -> Result<Value> {
let target = match p.get("target_face").and_then(vec3) {
Some(tp) => {
let n = super::face::face_normal(s, tp).ok_or_else(|| bad(cmd, "the target face must be planar"))?;
let plane = solvecraft_geom::Plane::from_normal(tp, n).ok_or_else(|| bad(cmd, "the target face's plane"))?;
solvecraft_doc::PlaneRef::Custom { plane }
let plane = solidcraft_geom::Plane::from_normal(tp, n).ok_or_else(|| bad(cmd, "the target face's plane"))?;
solidcraft_doc::PlaneRef::Custom { plane }
}
None => plane_param(s, p.get("target"), cmd)?,
};
@@ -399,7 +399,7 @@ pub(super) fn snap_point(s: &Session, kind: &str, at: Vec3) -> Option<Vec3> {
.collect(),
),
"edge_mid" => {
let d = |e: &solvecraft_doc::kernel::EdgeInfo| e.points.windows(2).map(|w| at.dist_to_segment(w[0], w[1])).fold(f64::MAX, f64::min);
let d = |e: &solidcraft_doc::kernel::EdgeInfo| e.points.windows(2).map(|w| at.dist_to_segment(w[0], w[1])).fold(f64::MAX, f64::min);
st.bodies
.iter()
.filter_map(|b| b.body.edges((b.body.size() * 2e-3).max(1e-3)).ok())
@@ -408,11 +408,11 @@ pub(super) fn snap_point(s: &Session, kind: &str, at: Vec3) -> Option<Vec3> {
.map(|e| e.mid)
}
"face_center" => st.bodies.iter().find_map(|b| {
let i = solvecraft_doc::appearance::face_index_at(b, at)?;
let i = solidcraft_doc::appearance::face_index_at(b, at)?;
b.body.faces((b.body.size() * 2e-3).max(1e-3)).ok()?.into_iter().find(|f| f.index == i).map(|f| f.centroid)
}),
"circle_center" => st.bodies.iter().find_map(|b| {
let c = solvecraft_doc::kernel::cylinder_face_at(&b.body, at)?;
let c = solidcraft_doc::kernel::cylinder_face_at(&b.body, at)?;
let along = (at - c.axis_point).dot(c.axis);
let end = if (along - c.start).abs() <= (c.end - along).abs() { c.start } else { c.end };
Some(c.axis_point + c.axis * end)
+18 -18
View File
@@ -108,7 +108,7 @@ fn exact_cylinder_volume(s: &Session) -> f64 {
#[test]
fn cylinder_emboss_persists_selection_and_replays_radial_depth() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
let mut s = cylinder_badge();
let before = exact_cylinder_volume(&s);
run(
@@ -124,10 +124,10 @@ fn cylinder_emboss_persists_selection_and_replays_radial_depth() {
assert!(mesh.contains(Vec3::new(-10.5 * 0.15_f64.sin(), 10.5 * 0.15_f64.cos(), 7.5)));
let feature = s.doc.find_feature("Raised Badge").unwrap();
assert!(
matches!(&feature.kind,solvecraft_doc::FeatureKind::Emboss{face:Some(face),..} if face.body=="Sleeve"&&face.at.dist(Vec3::new(0.,10.,7.5))<1e-10)
matches!(&feature.kind,solidcraft_doc::FeatureKind::Emboss{face:Some(face),..} if face.body=="Sleeve"&&face.at.dist(Vec3::new(0.,10.,7.5))<1e-10)
);
let saved = s.doc.to_json();
let mut loaded = Session { doc: solvecraft_doc::Document::from_json(&saved).unwrap().into(), ..Default::default() };
let mut loaded = Session { doc: solidcraft_doc::Document::from_json(&saved).unwrap().into(), ..Default::default() };
loaded.refresh();
assert!(loaded.model.results.iter().all(|f| f.error.is_none()), "{:?}", loaded.model.results);
assert!((exact_cylinder_volume(&loaded) - before - 21.0).abs() < 0.02);
@@ -171,7 +171,7 @@ fn cylinder_small_chord_pick_is_saved_on_the_exact_surface() {
.unwrap();
assert!(((pick.x * pick.x + pick.y * pick.y).sqrt() - 0.1).abs() > 1e-7);
run(&mut s, "solid.emboss", json!({"sketch":"Badge","profiles":"all","face":[pick.x,pick.y,pick.z],"depth":"0.01mm","targets":["Tiny"]}));
let solvecraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &s.doc.features.last().unwrap().kind else {
let solidcraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &s.doc.features.last().unwrap().kind else {
panic!("missing face");
};
assert!(((face.at.x * face.at.x + face.at.y * face.at.y).sqrt() - 0.1).abs() < 1e-12);
@@ -186,7 +186,7 @@ fn cylinder_cross_component_emboss_keeps_source_owner_and_placements() {
let a = run(&mut s, "component.create", json!({"name":"Source"}))["component"].as_u64().unwrap();
run(&mut s, "solid.cylinder", json!({"radius":10,"height":20,"body_name":"Sleeve"}));
let source_feature = s.model.state().bodies.first().unwrap().feature;
let source_names = solvecraft_doc::naming::face_names(s.model.state().body("Sleeve").unwrap());
let source_names = solidcraft_doc::naming::face_names(s.model.state().body("Sleeve").unwrap());
let before = exact_cylinder_volume(&s);
run(&mut s, "occurrence.move", json!({"occurrence":"Source:1","axis":[0,0,1],"angle":"90 deg","translate":[50,0,0]}));
let second = run(&mut s, "occurrence.copy", json!({"component":a,"translate":[80,0,0]}))["occurrence"].as_u64().unwrap();
@@ -204,7 +204,7 @@ fn cylinder_cross_component_emboss_keeps_source_owner_and_placements() {
.world_state()
.bodies
.iter()
.find(|b| b.body.tessellate_with(0.001, true).unwrap().contains(solvecraft_geom::Vec3::new(40.5 + pick_offset, 0., 7.5)))
.find(|b| b.body.tessellate_with(0.001, true).unwrap().contains(solidcraft_geom::Vec3::new(40.5 + pick_offset, 0., 7.5)))
.unwrap()
.name
.clone();
@@ -213,7 +213,7 @@ fn cylinder_cross_component_emboss_keeps_source_owner_and_placements() {
"solid.emboss",
json!({"sketch":"Badge","profiles":"all","face":[40.+pick_offset,0,7.5],"depth":"badge_depth","targets":[target.clone()]}),
);
let solvecraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &s.doc.features.last().unwrap().kind else {
let solidcraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &s.doc.features.last().unwrap().kind else {
panic!("missing selection");
};
assert_eq!(face.path, if pick_offset == 0. { s.doc.first_path(a) } else { vec![second] });
@@ -228,9 +228,9 @@ fn cylinder_cross_component_emboss_keeps_source_owner_and_placements() {
assert!((exact_cylinder_volume(&s) - before - 21.).abs() < 0.02);
let world = s.world_state();
for x in [39.5, 119.5] {
assert!(world.bodies.iter().any(|b| b.body.tessellate_with(0.001, true).unwrap().contains(solvecraft_geom::Vec3::new(x, 0., 7.5))));
assert!(world.bodies.iter().any(|b| b.body.tessellate_with(0.001, true).unwrap().contains(solidcraft_geom::Vec3::new(x, 0., 7.5))));
}
let mut loaded = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Default::default() };
let mut loaded = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Default::default() };
loaded.refresh();
assert!(loaded.model.results.iter().all(|r| r.error.is_none()), "{:?}", loaded.model.results);
run(&mut loaded, "parameters.change", json!({"name":"badge_depth","expression":"2mm"}));
@@ -254,7 +254,7 @@ fn cylinder_cross_component_emboss_keeps_source_owner_and_placements() {
assert!((exact_cylinder_volume(&loaded) - before - 69.).abs() < 0.02);
let mut broken = loaded.doc.as_ref().clone();
let id = broken.features.last().unwrap().id;
let solvecraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &mut broken.feature_mut(id).unwrap().kind else {
let solidcraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &mut broken.feature_mut(id).unwrap().kind else {
panic!("missing selection");
};
face.path = vec![u64::MAX];
@@ -283,12 +283,12 @@ fn check_active_second_source(auto: bool) {
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.emboss", json!({"sketch":"Badge","profiles":"all","face":[80,10,7.5],"depth":1}));
assert!((exact_cylinder_volume(&s) - before - 21.).abs() < 0.02);
let solvecraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &s.doc.features.last().unwrap().kind else {
let solidcraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &s.doc.features.last().unwrap().kind else {
panic!("missing selection");
};
assert_eq!(face.path, vec![second]);
assert_eq!(face.author_path, vec![second]);
let mut loaded = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Default::default() };
let mut loaded = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Default::default() };
loaded.refresh();
assert!(loaded.model.results.iter().all(|r| r.error.is_none()), "{:?}", loaded.model.results);
assert!((exact_cylinder_volume(&loaded) - before - 21.).abs() < 0.02);
@@ -376,7 +376,7 @@ fn check_body_offset(auto: bool) {
assert!((exact_cylinder_volume(&s) - before - 21.).abs() < 0.02);
assert_eq!(s.doc.body_offsets["Sleeve"], offset);
assert_eq!(s.doc.body_component("Sleeve", s.model.state().body("Sleeve").unwrap().feature), holder);
assert!(s.world_state().body("Sleeve").unwrap().body.tessellate_with(0.001, true).unwrap().contains(solvecraft_geom::Vec3::new(0., 10.5, 7.5)));
assert!(s.world_state().body("Sleeve").unwrap().body.tessellate_with(0.001, true).unwrap().contains(solidcraft_geom::Vec3::new(0., 10.5, 7.5)));
}
#[test]
@@ -486,14 +486,14 @@ fn loft_cut_through_a_shelled_basin_in_either_order() {
#[test]
fn cylinder_auto_inference_saves_a_profile_anchor_and_replays_both_depth_signs() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
for mode in ["emboss", "deboss"] {
let mut s = cylinder_badge();
let before = exact_cylinder_volume(&s);
let initial = s.doc.to_json();
run(&mut s, "solid.emboss", json!({"sketch":"Badge","profiles":"all","depth":"badge_depth","mode":mode,"name":"Auto Badge"}));
let feature = s.doc.find_feature("Auto Badge").unwrap().clone();
let solvecraft_doc::FeatureKind::Emboss { face: Some(face), targets, .. } = &feature.kind else {
let solidcraft_doc::FeatureKind::Emboss { face: Some(face), targets, .. } = &feature.kind else {
panic!("missing inferred association");
};
assert_eq!(targets, &["Sleeve"]);
@@ -514,7 +514,7 @@ fn cylinder_auto_inference_saves_a_profile_anchor_and_replays_both_depth_signs()
assert_eq!(s.doc.to_json(), initial);
run(&mut s, "edit.redo", json!({}));
assert_eq!(s.doc.find_feature("Auto Badge").unwrap().kind, feature.kind);
let mut loaded = Session { doc: solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Default::default() };
let mut loaded = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Default::default() };
loaded.refresh();
assert!(loaded.model.results.iter().all(|r| r.error.is_none()), "{:?}", loaded.model.results);
run(
@@ -551,7 +551,7 @@ fn cylinder_auto_inference_ignores_remote_finite_tangent_walls_in_each_body_orde
#[test]
fn cylinder_auto_inference_retains_selected_holes_and_nested_islands() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
for mode in ["emboss", "deboss"] {
let mut s = cylinder_badge();
run(&mut s, "sketch.edit", json!({"sketch":"Badge"}));
@@ -584,7 +584,7 @@ fn cylinder_auto_inference_refuses_ambiguity_curves_and_seam_crossing_atomically
assert!(error.contains("multiple tangent cylinders"), "{error}");
assert_eq!(s.doc.to_json(), before);
run(&mut s, "solid.emboss", json!({"sketch":"Badge","depth":1,"targets":["Sleeve"]}));
assert!(matches!(&s.doc.features.last().unwrap().kind,solvecraft_doc::FeatureKind::Emboss{face:Some(face),..} if face.body=="Sleeve"));
assert!(matches!(&s.doc.features.last().unwrap().kind,solidcraft_doc::FeatureKind::Emboss{face:Some(face),..} if face.body=="Sleeve"));
for circle in [false, true] {
let mut s = cylinder_badge();
run(&mut s, "sketch.create", json!({"plane":{"origin":[0,10,0],"x_dir":[-1,0,0],"y_dir":[0,0,1]},"name":"Unsupported"}));
+19 -19
View File
@@ -1,8 +1,8 @@
//! Documents and files: new, open (designs and STEP), insert STEP, save, export.
use serde_json::{Value, json};
use solvecraft_doc::Document;
use solvecraft_io::Format;
use solidcraft_doc::Document;
use solidcraft_io::Format;
use super::CommandSpec;
use crate::params::{bad, bool_, str_, string_list};
@@ -14,7 +14,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.icon("open")
.key("Ctrl+O")
.noundo()
.params("path: .solvecraft design, or a .step/.stp file (opens as a new design)"),
.params("path: .solidcraft design, or a .step/.stp file (opens as a new design)"),
CommandSpec::new("file.insert_step", "Insert STEP", insert_step)
.at("SOLID", "INSERT")
.icon("import")
@@ -50,9 +50,9 @@ fn path_arg<'a>(p: &'a Value, cmd: &str) -> Result<&'a str> {
}
/// Read a STEP file as an Import feature.
fn read_step(path: &str) -> Result<solvecraft_io::StepFeature> {
let bytes = solvecraft_io::vfs::read(path, solvecraft_io::MAX_STEP_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
solvecraft_io::step_import_feature(&bytes, path).map_err(|e| EngineError::Other(format!("{path}: {e}")))
fn read_step(path: &str) -> Result<solidcraft_io::StepFeature> {
let bytes = solidcraft_io::vfs::read(path, solidcraft_io::MAX_STEP_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
solidcraft_io::step_import_feature(&bytes, path).map_err(|e| EngineError::Other(format!("{path}: {e}")))
}
/// Add an Import feature for a STEP file to the session's design.
@@ -76,8 +76,8 @@ fn add_step(s: &mut Session, path: &str, name: Option<&str>) -> Result<Value> {
/// Add a MeshImport feature for a 3MF or STL file.
fn add_mesh(s: &mut Session, path: &str, name: Option<&str>) -> Result<Value> {
let bytes = solvecraft_io::vfs::read(path, solvecraft_io::MAX_3MF_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let mf = solvecraft_io::mesh_import_feature(&bytes, path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let bytes = solidcraft_io::vfs::read(path, solidcraft_io::MAX_3MF_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let mf = solidcraft_io::mesh_import_feature(&bytes, path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let fname = name.filter(|n| !n.trim().is_empty()).unwrap_or(&mf.name).to_string();
let id = s.doc_mut().add_feature(mf.kind, Some(&fname))?;
if let Some(f) = s.doc_mut().feature_mut(id) {
@@ -97,7 +97,7 @@ fn add_mesh(s: &mut Session, path: &str, name: Option<&str>) -> Result<Value> {
fn insert_mesh(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "file.insert_mesh";
let path = path_arg(p, cmd)?;
if !solvecraft_io::is_mesh_path(path) {
if !solidcraft_io::is_mesh_path(path) {
return Err(bad(cmd, "only 3MF and STL files (.3mf, .stl) can be inserted as meshes"));
}
add_mesh(s, path, str_(p, "name"))
@@ -106,7 +106,7 @@ fn insert_mesh(s: &mut Session, p: &Value) -> Result<Value> {
fn insert_step(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "file.insert_step";
let path = path_arg(p, cmd)?;
if !solvecraft_io::is_step_path(path) {
if !solidcraft_io::is_step_path(path) {
return Err(bad(cmd, "only STEP files (.step, .stp) can be inserted"));
}
add_step(s, path, str_(p, "name"))
@@ -114,18 +114,18 @@ fn insert_step(s: &mut Session, p: &Value) -> Result<Value> {
fn open(s: &mut Session, p: &Value) -> Result<Value> {
let path = path_arg(p, "doc.open")?;
if solvecraft_io::is_step_path(path) || solvecraft_io::is_mesh_path(path) {
if solidcraft_io::is_step_path(path) || solidcraft_io::is_mesh_path(path) {
// A new, unsaved design holding the file's bodies.
let stem = std::path::Path::new(path).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_else(|| "Untitled".into());
let mut fresh = Session::new(Document::new(&stem));
let r = if solvecraft_io::is_step_path(path) { add_step(&mut fresh, path, None)? } else { add_mesh(&mut fresh, path, None)? };
let r = if solidcraft_io::is_step_path(path) { add_step(&mut fresh, path, None)? } else { add_mesh(&mut fresh, path, None)? };
fresh.undo.clear();
*s = fresh;
let errors = s.model.results.iter().filter(|r| r.error.is_some()).count();
return Ok(json!({"path": path, "features": s.doc.features.len(), "errors": errors, "bodies": r["bodies"], "warnings": r["warnings"]}));
}
let bytes = solvecraft_io::vfs::read(path, solvecraft_io::MAX_DESIGN_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let doc = solvecraft_io::read_design(&bytes)?;
let bytes = solidcraft_io::vfs::read(path, solidcraft_io::MAX_DESIGN_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let doc = solidcraft_io::read_design(&bytes)?;
*s = Session::new(doc);
s.path = Some(path.to_string());
let errors = s.model.results.iter().filter(|r| r.error.is_some()).count();
@@ -133,9 +133,9 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
}
fn write(s: &mut Session, path: &str) -> Result<Value> {
let bytes = solvecraft_io::write_design(&s.doc);
let bytes = solidcraft_io::write_design(&s.doc);
// Atomic, keeping the version being replaced as `<file>.bak`.
solvecraft_io::write_atomic(std::path::Path::new(path), &bytes, true).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
solidcraft_io::write_atomic(std::path::Path::new(path), &bytes, true).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
s.path = Some(path.to_string());
s.mark_saved();
Ok(json!({"path": path, "bytes": bytes.len()}))
@@ -158,12 +158,12 @@ fn export_to(s: &Session, path: &str, format: Format, bodies: &[String]) -> Resu
let name = std::path::Path::new(path).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_else(|| s.doc.name.clone());
// A design with components exports as a STEP assembly; otherwise bodies as placed.
let bytes = if format == Format::Step && bodies.is_empty() && !s.doc.occurrences.is_empty() {
solvecraft_io::step_assembly(&s.doc, &s.doc.painted(&s.model.state()), &name)?
solidcraft_io::step_assembly(&s.doc, &s.doc.painted(&s.model.state()), &name)?
} else {
// Bodies carry the colour they are shown in (appearance, material or imported).
solvecraft_io::export(&s.doc.painted(&s.world_state()), bodies, format, &name)?
solidcraft_io::export(&s.doc.painted(&s.world_state()), bodies, format, &name)?
};
solvecraft_io::vfs::write(path, &bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
solidcraft_io::vfs::write(path, &bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
Ok(json!({"path": path, "bytes": bytes.len(), "format": format!("{format:?}")}))
}
+23 -23
View File
@@ -1,7 +1,7 @@
//! Inspection: measure bodies, describe the document, sketches and model topology.
use serde_json::{Value, json};
use solvecraft_sketch::CurveKind;
use solidcraft_sketch::CurveKind;
use super::CommandSpec;
use crate::params::{bad, bool_, str_, string_list};
@@ -38,8 +38,8 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("engine.commands", "List Commands", commands).noundo(),
];
pub fn measure_json(b: &solvecraft_doc::ModelBody) -> Value {
match solvecraft_kernel::measure(&b.body) {
pub fn measure_json(b: &solidcraft_doc::ModelBody) -> Value {
match solidcraft_kernel::measure(&b.body) {
Ok(m) => json!({
"name": b.name,
"volume_mm3": m.volume,
@@ -234,7 +234,7 @@ fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
}))
}
fn body_of<'a>(st: &'a solvecraft_doc::ModelState, p: &Value, cmd: &str) -> Result<&'a solvecraft_doc::ModelBody> {
fn body_of<'a>(st: &'a solidcraft_doc::ModelState, p: &Value, cmd: &str) -> Result<&'a solidcraft_doc::ModelBody> {
let n = str_(p, "body").ok_or_else(|| bad(cmd, "`body` is required"))?;
st.body(n).ok_or_else(|| bad(cmd, format!("no body `{n}`")))
}
@@ -243,7 +243,7 @@ fn model_edges(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.world_state();
let b = body_of(&st, p, "model.edges")?;
let tol = (b.body.size() * 2e-3).max(1e-3);
let names = solvecraft_doc::naming::edge_names(b);
let names = solidcraft_doc::naming::edge_names(b);
let edges: Vec<Value> = b
.body
.edges(tol)?
@@ -256,14 +256,14 @@ fn model_edges(s: &mut Session, p: &Value) -> Result<Value> {
fn model_faces(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.world_state();
let b = body_of(&st, p, "model.faces")?;
let names = solvecraft_doc::naming::face_names(b);
let names = solidcraft_doc::naming::face_names(b);
let tol = (b.body.size() * 2e-3).max(1e-3);
let faces: Vec<Value> = b
.body
.faces(tol)?
.iter()
.map(|f| {
json!({"index": f.index, "name": names.get(f.index), "exact": solvecraft_doc::naming::face_name_exact(b, f.index),
json!({"index": f.index, "name": names.get(f.index), "exact": solidcraft_doc::naming::face_name_exact(b, f.index),
"area": f.area, "centroid": f.centroid, "plane_normal": f.plane_normal})
})
.collect();
@@ -283,7 +283,7 @@ fn physical_material(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "material.assign";
let bodies = string_list(p, "bodies");
let material = str_(p, "material").ok_or_else(|| bad(cmd, "`material` must be a material name"))?;
let canonical = solvecraft_doc::MATERIALS
let canonical = solidcraft_doc::MATERIALS
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(material.trim()))
.map(|(n, _)| n.to_string())
@@ -318,7 +318,7 @@ fn color_param(v: &Value) -> Option<[u8; 3]> {
}
fn appearance(s: &mut Session, p: &Value) -> Result<Value> {
use solvecraft_doc::appearance::{FaceLook, Look, library};
use solidcraft_doc::appearance::{FaceLook, Look, library};
let cmd = "appearance.assign";
let look: Option<Look> = match (str_(p, "appearance"), p.get("color")) {
// A colour with a name: that colour, called that (a name not in the library is fine).
@@ -352,14 +352,14 @@ fn appearance(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, "`bodies` must list existing bodies"));
}
// Faces: a point on each, on the named body or the nearest one.
let mut faces: Vec<(String, solvecraft_geom::Vec3)> = Vec::new();
let mut faces: Vec<(String, solidcraft_geom::Vec3)> = Vec::new();
if let Some(list) = p.get("faces") {
let list = list.as_array().filter(|a| a.len() <= 10_000).ok_or_else(|| bad(cmd, "`faces` must list points [x, y, z]"))?;
for v in list {
let pt = crate::params::vec3(v)
.or_else(|| v.get("point").and_then(crate::params::vec3))
.ok_or_else(|| bad(cmd, "`faces` must list points [x, y, z]"))?;
let on = |b: &solvecraft_doc::ModelBody| solvecraft_doc::appearance::face_index_at(b, pt).is_some();
let on = |b: &solidcraft_doc::ModelBody| solidcraft_doc::appearance::face_index_at(b, pt).is_some();
let body = match v.get("body").and_then(Value::as_str).or_else(|| str_(p, "body")) {
Some(n) => st.body(n).filter(|b| on(b)).map(|b| b.name.clone()),
None => st.bodies.iter().filter(|b| on(b)).min_by(|a, b| dist_to(a, pt).total_cmp(&dist_to(b, pt))).map(|b| b.name.clone()),
@@ -381,11 +381,11 @@ fn appearance(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, "give `bodies`, `faces` or `components`"));
}
// Persistent names of the picked faces.
let st_names: Vec<(String, solvecraft_geom::Vec3, Option<String>)> = faces
let st_names: Vec<(String, solidcraft_geom::Vec3, Option<String>)> = faces
.iter()
.map(|(b, pt)| {
let n = st.body(b).and_then(|mb| {
solvecraft_doc::appearance::face_index_at(mb, *pt).and_then(|i| solvecraft_doc::naming::face_names(mb).get(i).cloned())
solidcraft_doc::appearance::face_index_at(mb, *pt).and_then(|i| solidcraft_doc::naming::face_names(mb).get(i).cloned())
});
(b.clone(), *pt, n)
})
@@ -423,20 +423,20 @@ fn appearance(s: &mut Session, p: &Value) -> Result<Value> {
.map(|(b, pt)| {
(
b.clone(),
st.body(b).and_then(|mb| solvecraft_doc::appearance::face_index_at(mb, *pt)),
st.body(b).and_then(|mb| solidcraft_doc::appearance::face_index_at(mb, *pt)),
st_names.iter().find(|(name, p, _)| name == b && p.dist(*pt) < 1e-12).and_then(|(_, _, name)| name.clone()),
)
})
.collect();
doc.appearances.faces.retain(|f| {
let idx = st.body(&f.body).and_then(|mb| solvecraft_doc::appearance::face_index_at(mb, f.point));
let idx = st.body(&f.body).and_then(|mb| solidcraft_doc::appearance::face_index_at(mb, f.point));
!picked.iter().any(|(b, i, name)| {
*b == f.body
&& (f
.name
.as_ref()
.zip(name.as_ref())
.is_some_and(|(a, b)| solvecraft_doc::naming::strip_piece(a) == solvecraft_doc::naming::strip_piece(b))
.is_some_and(|(a, b)| solidcraft_doc::naming::strip_piece(a) == solidcraft_doc::naming::strip_piece(b))
|| (!f.strict_name && i.is_some() && *i == idx))
})
});
@@ -449,18 +449,18 @@ fn appearance(s: &mut Session, p: &Value) -> Result<Value> {
}))
}
fn dist_to(b: &solvecraft_doc::ModelBody, p: solvecraft_geom::Vec3) -> f64 {
fn dist_to(b: &solidcraft_doc::ModelBody, p: solidcraft_geom::Vec3) -> f64 {
let bb = b.mesh().bounds();
let c = solvecraft_geom::Vec3::new(p.x.clamp(bb.min.x, bb.max.x), p.y.clamp(bb.min.y, bb.max.y), p.z.clamp(bb.min.z, bb.max.z));
let c = solidcraft_geom::Vec3::new(p.x.clamp(bb.min.x, bb.max.x), p.y.clamp(bb.min.y, bb.max.y), p.z.clamp(bb.min.z, bb.max.z));
c.dist(p)
}
fn look_json(l: &solvecraft_doc::appearance::Look) -> Value {
fn look_json(l: &solidcraft_doc::appearance::Look) -> Value {
json!({"name": l.name, "color": l.hex(), "opacity": l.opacity})
}
fn appearance_library(_s: &mut Session, _p: &Value) -> Result<Value> {
let list: Vec<Value> = solvecraft_doc::appearance::LIBRARY
let list: Vec<Value> = solidcraft_doc::appearance::LIBRARY
.iter()
.map(|(n, c, o)| {
let category = n.split(" - ").next().unwrap_or(n);
@@ -473,8 +473,8 @@ fn appearance_library(_s: &mut Session, _p: &Value) -> Result<Value> {
fn appearance_list(s: &mut Session, p: &Value) -> Result<Value> {
let a = &s.doc.appearances;
// "In this design": each look used, with where.
let mut used: Vec<(solvecraft_doc::appearance::Look, Vec<Value>, Vec<Value>, Vec<Value>)> = Vec::new();
fn slot(used: &mut Vec<(solvecraft_doc::appearance::Look, Vec<Value>, Vec<Value>, Vec<Value>)>, l: &solvecraft_doc::appearance::Look) -> usize {
let mut used: Vec<(solidcraft_doc::appearance::Look, Vec<Value>, Vec<Value>, Vec<Value>)> = Vec::new();
fn slot(used: &mut Vec<(solidcraft_doc::appearance::Look, Vec<Value>, Vec<Value>, Vec<Value>)>, l: &solidcraft_doc::appearance::Look) -> usize {
match used.iter().position(|(u, ..)| u == l) {
Some(i) => i,
None => {
@@ -525,5 +525,5 @@ fn appearance_list(s: &mut Session, p: &Value) -> Result<Value> {
}
fn material_list(_s: &mut Session, _p: &Value) -> Result<Value> {
Ok(json!({"materials": solvecraft_doc::MATERIALS.iter().map(|(n, d)| json!({"name": n, "density_g_cm3": d})).collect::<Vec<_>>()}))
Ok(json!({"materials": solidcraft_doc::MATERIALS.iter().map(|(n, d)| json!({"name": n, "density_g_cm3": d})).collect::<Vec<_>>()}))
}
+17 -17
View File
@@ -1,12 +1,12 @@
//! Joints between occurrences (`solvecraft_doc::joints`): Joint Origin, Joint, As-built Joint,
//! Joints between occurrences (`solidcraft_doc::joints`): Joint Origin, Joint, As-built Joint,
//! Rigid Group, Drive Joints, Motion Link, limits, and Interference. Every change re-solves the
//! joint graph and moves the occurrences it places; the session re-solves after any edit too,
//! so joints follow the geometry their origins snap to.
use serde_json::{Value, json};
use solvecraft_doc::expr::Kind;
use solvecraft_doc::joints::{self, Joint, JointKind, JointOrigin, MotionLink, NamedOrigin, Snap};
use solvecraft_geom::Vec3;
use solidcraft_doc::expr::Kind;
use solidcraft_doc::joints::{self, Joint, JointKind, JointOrigin, MotionLink, NamedOrigin, Snap};
use solidcraft_geom::Vec3;
use super::CommandSpec;
use crate::params::{bad, bool_, num, str_, string_list, vec3};
@@ -88,9 +88,9 @@ pub(crate) fn occurrence_at(s: &Session, p: Vec3) -> Option<u64> {
let comp = s.doc.model_body_component(b);
let occs: Vec<u64> = if comp == 0 { vec![0] } else { s.doc.occurrences.iter().filter(|o| o.component == comp).map(|o| o.id).collect() };
for occ in occs {
let t = if occ == 0 { solvecraft_doc::IDENTITY } else { joints::occurrence_world(&s.doc, occ) };
let inv = solvecraft_doc::mat_inverse(&t)?;
let q = solvecraft_doc::apply_point(&inv, p);
let t = if occ == 0 { solidcraft_doc::IDENTITY } else { joints::occurrence_world(&s.doc, occ) };
let inv = solidcraft_doc::mat_inverse(&t)?;
let q = solidcraft_doc::apply_point(&inv, p);
let bb = b.mesh().bounds();
let c = Vec3::new(q.x.clamp(bb.min.x, bb.max.x), q.y.clamp(bb.min.y, bb.max.y), q.z.clamp(bb.min.z, bb.max.z));
let d = c.dist(q);
@@ -126,10 +126,10 @@ fn origin_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result<JointOrigin
None => occurrence_at(s, world).ok_or_else(|| bad(cmd, "no body there"))?,
};
// World → the component's own coordinates.
let t = if occurrence == 0 { solvecraft_doc::IDENTITY } else { joints::occurrence_world(&s.doc, occurrence) };
let inv = solvecraft_doc::mat_inverse(&t).ok_or_else(|| bad(cmd, "occurrence transform"))?;
let local = |p: Vec3| solvecraft_doc::apply_point(&inv, p);
let local_dir = |d: Vec3| solvecraft_doc::apply_vector(&inv, d);
let t = if occurrence == 0 { solidcraft_doc::IDENTITY } else { joints::occurrence_world(&s.doc, occurrence) };
let inv = solidcraft_doc::mat_inverse(&t).ok_or_else(|| bad(cmd, "occurrence transform"))?;
let local = |p: Vec3| solidcraft_doc::apply_point(&inv, p);
let local_dir = |d: Vec3| solidcraft_doc::apply_vector(&inv, d);
let snap = match what {
"face" | "circle" => {
let pick = local(world);
@@ -139,7 +139,7 @@ fn origin_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result<JointOrigin
} else {
pick
};
let face = solvecraft_doc::naming::face_names_at(&s.model.state(), &[named_at]).into_iter().next().filter(|n| !n.is_empty());
let face = solidcraft_doc::naming::face_names_at(&s.model.state(), &[named_at]).into_iter().next().filter(|n| !n.is_empty());
if what == "face" { Snap::FaceCenter { pick, face } } else { Snap::CircleCenter { pick, face } }
}
"point" => Snap::Point { point: local(world), z: v.get("z").and_then(vec3).map(local_dir) },
@@ -526,7 +526,7 @@ fn interference(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "inspect.interference";
let want = string_list(p, "bodies");
let st = s.world_state();
let bodies: Vec<&solvecraft_doc::ModelBody> =
let bodies: Vec<&solidcraft_doc::ModelBody> =
st.bodies.iter().filter(|b| want.is_empty() || want.contains(&b.name)).filter(|b| !b.body.is_mesh()).collect();
if bodies.len() > 200 {
return Err(bad(cmd, "too many bodies (200 at most)"));
@@ -544,8 +544,8 @@ fn interference(s: &mut Session, p: &Value) -> Result<Value> {
if apart {
continue;
}
let Ok(Some(o)) = solvecraft_kernel::boolean(&a.body, &b.body, solvecraft_kernel::BoolOp::Intersect) else { continue };
let Ok(m) = solvecraft_kernel::measure(&o) else { continue };
let Ok(Some(o)) = solidcraft_kernel::boolean(&a.body, &b.body, solidcraft_kernel::BoolOp::Intersect) else { continue };
let Ok(m) = solidcraft_kernel::measure(&o) else { continue };
if m.volume > 1e-9 * a.body.size().max(1.0).powi(3) {
hits.push(json!({"a": a.name, "b": b.name, "volume_mm3": m.volume, "area_mm2": m.area, "center": m.centroid, "bbox": {"min": m.bbox.min, "max": m.bbox.max}}));
}
@@ -559,8 +559,8 @@ fn interference(s: &mut Session, p: &Value) -> Result<Value> {
mod tests;
/// A point in the middle of the cylindrical face at `pick` (on its wall, halfway along).
fn cylinder_wall_point(b: &solvecraft_doc::kernel::Body, pick: Vec3) -> Option<Vec3> {
let c = solvecraft_doc::kernel::cylinder_face_at(b, pick)?;
fn cylinder_wall_point(b: &solidcraft_doc::kernel::Body, pick: Vec3) -> Option<Vec3> {
let c = solidcraft_doc::kernel::cylinder_face_at(b, pick)?;
let v = pick - c.axis_point;
let along = v.dot(c.axis);
let radial = (v - c.axis * along).normalized()?;
+6 -6
View File
@@ -1,7 +1,7 @@
use std::f64::consts::FRAC_PI_2;
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use crate::Session;
@@ -18,7 +18,7 @@ fn occ(s: &Session, component: &str) -> u64 {
}
fn place(s: &Session, component: &str, p: Vec3) -> Vec3 {
solvecraft_doc::apply_point(&solvecraft_doc::joints::occurrence_world(&s.doc, occ(s, component)), p)
solidcraft_doc::apply_point(&solidcraft_doc::joints::occurrence_world(&s.doc, occ(s, component)), p)
}
fn near(a: Vec3, b: Vec3) -> bool {
@@ -55,9 +55,9 @@ fn revolute_hinge_turns_about_the_knuckle() {
assert_eq!(dof, 1, "{l}");
// Drive 90°: B turns about the knuckle axis (through (0, 33, 4), along the frame's z = −X).
run(&mut s, "joint.drive", json!({"joint": "Hinge", "value": 90}));
let want = solvecraft_doc::rigid(Vec3::ZERO, Vec3::new(0.0, 33.0, 4.0), Vec3::new(-1.0, 0.0, 0.0), FRAC_PI_2);
let want = solidcraft_doc::rigid(Vec3::ZERO, Vec3::new(0.0, 33.0, 4.0), Vec3::new(-1.0, 0.0, 0.0), FRAC_PI_2);
let p = Vec3::new(20.0, 0.0, 0.0);
assert!(near(place(&s, "LeafB", p), solvecraft_doc::apply_point(&want, p)), "{:?}", place(&s, "LeafB", p));
assert!(near(place(&s, "LeafB", p), solidcraft_doc::apply_point(&want, p)), "{:?}", place(&s, "LeafB", p));
assert!(near(place(&s, "LeafB", p), Vec3::new(20.0, 29.0, 37.0)));
// LeafA did not move; the knuckle axis is fixed.
assert!(near(place(&s, "LeafA", p), p));
@@ -108,7 +108,7 @@ fn slider_limits_clamp() {
.doc
.features
.iter()
.find(|f| matches!(f.kind, solvecraft_doc::FeatureKind::Box { .. }))
.find(|f| matches!(f.kind, solidcraft_doc::FeatureKind::Box { .. }))
.and_then(|f| f.param_names.get(2).cloned())
.unwrap_or_default();
run(&mut s, "parameters.change", json!({"name": h, "expression": "15"}));
@@ -176,7 +176,7 @@ fn motion_links_and_interference() {
run(&mut s, "joint.drive", json!({"joint": "Screw", "values": [0, 3]}));
assert!(near(place(&s, "D", Vec3::new(65.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 16.0)), "{:?}", place(&s, "D", Vec3::new(65.0, 5.0, 0.0)));
// Saved and reopened, joints come back.
let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let back = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(back.assembly, s.doc.assembly);
let mut t = Session::new(back);
let l = run(&mut t, "joint.list", json!({}));
+1 -1
View File
@@ -3,7 +3,7 @@
//! points) and, where both have a direction (planar faces, straight edges), the angle.
use serde_json::{Value, json};
use solvecraft_geom::{Mesh, Vec3};
use solidcraft_geom::{Mesh, Vec3};
use crate::params::bad;
use crate::{Result, Sel, Session};
+2 -2
View File
@@ -1,8 +1,8 @@
//! Parametric reconstruction of imported mechanical mesh bodies.
use serde_json::Value;
use solvecraft_doc::FeatureKind;
use solvecraft_doc::expr::Kind;
use solidcraft_doc::FeatureKind;
use solidcraft_doc::expr::Kind;
use super::{
CommandSpec,
+1 -1
View File
@@ -1,4 +1,4 @@
//! The command registry. Ids are SolveCraft's own dotted ids (`solid.extrude`,
//! The command registry. Ids are SolidCraft's own dotted ids (`solid.extrude`,
//! `sketch.rectangle.two_point`); the toolbar placement (`tab`, `panel`) follows the familiar
//! Design workspace layout.
+12 -12
View File
@@ -1,11 +1,11 @@
//! Contact Sets, Motion Study and Exploded views.
use serde_json::{Value, json};
use solvecraft_doc::Mat;
use solvecraft_doc::joints::{self, JointKind};
use solvecraft_doc::motion::{ContactSet, ExplodedView, MotionStudy, StudyKey};
use solvecraft_geom::Vec3;
use solvecraft_kernel::Body;
use solidcraft_doc::Mat;
use solidcraft_doc::joints::{self, JointKind};
use solidcraft_doc::motion::{ContactSet, ExplodedView, MotionStudy, StudyKey};
use solidcraft_geom::Vec3;
use solidcraft_kernel::Body;
use super::CommandSpec;
use super::joints::{joint_id, occurrence_id, resolve, value};
@@ -93,8 +93,8 @@ fn placed(s: &Session) -> Vec<(Vec<u64>, Body)> {
// Moving a body into another component preserves its world placement using an
// offset from its authoring frame. Contact and explode centres must use that
// same offset as display and physical properties.
let frame = s.doc.model_body_offset(b).map(|offset| solvecraft_doc::mat_mul(&m, offset)).unwrap_or(m);
if let Ok(t) = solvecraft_kernel::transform_matrix(&b.body, frame) {
let frame = s.doc.model_body_offset(b).map(|offset| solidcraft_doc::mat_mul(&m, offset)).unwrap_or(m);
if let Ok(t) = solidcraft_kernel::transform_matrix(&b.body, frame) {
out.push((path.clone(), t));
}
}
@@ -141,7 +141,7 @@ fn overlap(a: &Body, b: &Body) -> Option<f64> {
mesh_overlap(&ma, &mb, 1e-4 * a.size().max(b.size()).max(1.0))
}
fn mesh_overlap(ma: &solvecraft_geom::Mesh, mb: &solvecraft_geom::Mesh, tol: f64) -> Option<f64> {
fn mesh_overlap(ma: &solidcraft_geom::Mesh, mb: &solidcraft_geom::Mesh, tol: f64) -> Option<f64> {
let (ba, bb) = (ma.bounds(), mb.bounds());
if ba.max.x < bb.min.x + tol
|| bb.max.x < ba.min.x + tol
@@ -156,12 +156,12 @@ fn mesh_overlap(ma: &solvecraft_geom::Mesh, mb: &solvecraft_geom::Mesh, tol: f64
(depth > tol).then_some(depth)
}
fn tris(m: &solvecraft_geom::Mesh) -> Vec<[Vec3; 3]> {
fn tris(m: &solidcraft_geom::Mesh) -> Vec<[Vec3; 3]> {
m.triangles.iter().filter_map(|t| m.tri(t)).collect()
}
/// The deepest any sample point of `a` lies inside `b` (0 if none).
fn deepest(a: &solvecraft_geom::Mesh, b: &solvecraft_geom::Mesh, tol: f64) -> f64 {
fn deepest(a: &solidcraft_geom::Mesh, b: &solidcraft_geom::Mesh, tol: f64) -> f64 {
let tb = tris(b);
let bx = b.bounds();
let mut pts: Vec<Vec3> = a.positions.clone();
@@ -589,7 +589,7 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
} else {
serde_json::to_string_pretty(&out).unwrap_or_default()
};
solvecraft_io::write_atomic(std::path::Path::new(path), text.as_bytes(), false).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
solidcraft_io::write_atomic(std::path::Path::new(path), text.as_bytes(), false).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
return Ok(json!({"path": path, "samples": rows.len()}));
}
Ok(out)
@@ -715,7 +715,7 @@ fn explode_delete(s: &mut Session, p: &Value) -> Result<Value> {
/// World position of a point of an occurrence (tests).
#[cfg(test)]
pub fn world_point(s: &Session, occurrence: u64, p: Vec3) -> Vec3 {
solvecraft_doc::apply_point(&joints::occurrence_world(&s.doc, occurrence), p)
solidcraft_doc::apply_point(&joints::occurrence_world(&s.doc, occurrence), p)
}
#[cfg(test)]
+2 -2
View File
@@ -1,5 +1,5 @@
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use crate::Session;
@@ -130,7 +130,7 @@ fn motion_study_plays_and_exports() {
// The B occurrence's origin moves by the slider value (it starts at 0 offset with value 0 at 10 mm… so 10 → −5).
assert!((bx(&rows[1]) - bx(&rows[0]) + 3.75).abs() < 1e-6, "{e}");
assert!(rows[4]["stopped_by_contact"].is_object());
let dir = std::env::temp_dir().join(format!("solvecraft-motion-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-motion-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let csv = dir.join("push.csv");
run(&mut s, "motion.export", json!({"study": "Push", "samples": 3, "path": csv.to_string_lossy()}));
+8 -8
View File
@@ -190,8 +190,8 @@ fn duplicate_nested_labels_use_full_path_and_reopen_undo_preserve_placements() {
let (parent_component, parent) = created(&mut s, "Parent");
let (_, child) = created(&mut s, "Child");
run(&mut s, "solid.box", json!({"length":1,"width":1,"height":1}));
let parent2 = s.doc_mut().add_occurrence(parent_component, 0, solvecraft_doc::IDENTITY).unwrap();
let parent3 = s.doc_mut().add_occurrence(parent_component, 0, solvecraft_doc::IDENTITY).unwrap();
let parent2 = s.doc_mut().add_occurrence(parent_component, 0, solidcraft_doc::IDENTITY).unwrap();
let parent3 = s.doc_mut().add_occurrence(parent_component, 0, solidcraft_doc::IDENTITY).unwrap();
for o in s.doc_mut().occurrences.iter_mut().filter(|o| o.id == parent2 || o.id == parent3) {
o.name = "Same".into();
}
@@ -199,7 +199,7 @@ fn duplicate_nested_labels_use_full_path_and_reopen_undo_preserve_placements() {
let a = s.occurrence_bodies(&[parent2, child], false).unwrap()[0].clone();
let b = s.occurrence_bodies(&[parent3, child], false).unwrap()[0].clone();
assert_ne!(a, b);
let mut reopened = Session::new(solvecraft_doc::format::read(&s.doc.to_json()).unwrap());
let mut reopened = Session::new(solidcraft_doc::format::read(&s.doc.to_json()).unwrap());
assert_eq!(reopened.occurrence_bodies(&[parent2, child], false).unwrap()[0], a);
assert_eq!(reopened.occurrence_bodies(&[parent3, child], false).unwrap()[0], b);
run(&mut reopened, "selection.delete", json!({"occurrences":[parent3]}));
@@ -218,7 +218,7 @@ fn compact_duplicated_assembly_exact_resolution_and_expansion_limit() {
let component = s.doc_mut().add_component(Some(&format!("Level{i}")), parent).unwrap();
let occurrence = s.doc.occurrence_of(component).unwrap().id;
path.push(occurrence);
s.doc_mut().add_occurrence(component, parent, solvecraft_doc::IDENTITY).unwrap();
s.doc_mut().add_occurrence(component, parent, solidcraft_doc::IDENTITY).unwrap();
parent = component;
}
assert!(s.occurrence_placement(&path).is_ok());
@@ -326,7 +326,7 @@ fn isolate_nested_child_hides_ancestor_material_and_sibling() {
.add_occurrence(
child_component,
parent_component,
solvecraft_doc::rigid(crate::geom::Vec3::new(20.0, 0.0, 0.0), crate::geom::Vec3::ZERO, crate::geom::Vec3::Z, 0.0),
solidcraft_doc::rigid(crate::geom::Vec3::new(20.0, 0.0, 0.0), crate::geom::Vec3::ZERO, crate::geom::Vec3::Z, 0.0),
)
.unwrap();
s.refresh();
@@ -412,14 +412,14 @@ fn instance_styles_follow_source_identity_after_collision_rename_and_pending_mov
assert!((measured["bodies"][0]["mass_g"].as_f64().unwrap() - 0.068).abs() < 1e-8);
}
let definition = s.model.state();
assert_eq!(solvecraft_kernel::measure(&definition.body("Shared Renamed").unwrap().body).unwrap().bbox.min, crate::geom::Vec3::ZERO);
assert_eq!(solidcraft_kernel::measure(&definition.body("Shared Renamed").unwrap().body).unwrap().bbox.min, crate::geom::Vec3::ZERO);
let painted = s.doc.painted(&world);
let secondname = s.occurrence_bodies(&[second], false).unwrap()[0].clone();
let body = painted.body(&secondname).unwrap();
assert_eq!(body.body.color(), Some([1.0, 0.0, 0.0]));
assert!(body.body.paint().unwrap().faces.iter().any(|f| f.color == [0.0, 1.0, 0.0]));
let bytes = solvecraft_io::export(&painted, &[secondname], solvecraft_io::Format::Step, "InstanceStyles").unwrap();
let imported = solvecraft_kernel::step_import(&String::from_utf8(bytes).unwrap()).unwrap();
let bytes = solidcraft_io::export(&painted, &[secondname], solidcraft_io::Format::Step, "InstanceStyles").unwrap();
let imported = solidcraft_kernel::step_import(&String::from_utf8(bytes).unwrap()).unwrap();
assert_eq!(imported.bodies.len(), 1);
assert_eq!(imported.bodies[0].body.color(), Some([1.0, 0.0, 0.0]));
assert!(imported.bodies[0].body.paint().unwrap().faces.iter().any(|f| f.color == [0.0, 1.0, 0.0]));
@@ -1,8 +1,8 @@
#![allow(clippy::unwrap_used, clippy::panic)]
use serde_json::{Value, json};
use solvecraft_doc::{Document, FeatureKind, ProfileSel};
use solvecraft_geom::Vec2;
use solvecraft_sketch::{CurveKind, Sketch};
use solidcraft_doc::{Document, FeatureKind, ProfileSel};
use solidcraft_geom::Vec2;
use solidcraft_sketch::{CurveKind, Sketch};
use crate::{Session, Snapshot};
@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used, clippy::panic)]
use serde_json::{Value, json};
use solvecraft_doc::{Document, FeatureKind, ProfileSel};
use solidcraft_doc::{Document, FeatureKind, ProfileSel};
use crate::Session;
@@ -26,7 +26,7 @@ fn rounded_offset(radius: f64) -> (Session, u64, String, String) {
fn volume(session: &Session, feature: u64) -> f64 {
let state = session.model.state();
let body = state.bodies.iter().find(|body| body.feature == feature).unwrap();
solvecraft_kernel::measure(&body.body).unwrap().volume
solidcraft_kernel::measure(&body.body).unwrap().volume
}
fn healthy(session: &Session) {
@@ -41,13 +41,13 @@ fn legacy_offset_capture_persists_reserved_curves_without_a_transient_associatio
let reserved = raw.offsets[0].outputs.clone();
assert_eq!(reserved.len(), 8);
raw.offsets[0].topology = None;
document.format = "solvecraft/11".into();
document.format = "solidcraft/11".into();
let mut session = Session::new(Document::from_json(&document.to_json()).unwrap());
assert!(session.doc.sketch(sketch).unwrap().offsets[0].topology.is_none());
let state = session.model.state();
let solved = state.sketch(sketch).unwrap();
assert!(solved.sketch.offsets[0].topology.is_some(), "legacy upgrade belongs only to the evaluated copy");
let index = solvecraft_doc::profile_selection_indices(solved, &ProfileSel::Curves { loops: vec![reserved.clone()] }).unwrap()[0];
let index = solidcraft_doc::profile_selection_indices(solved, &ProfileSel::Curves { loops: vec![reserved.clone()] }).unwrap()[0];
let extrusion = run(&mut session, "solid.extrude", json!({"sketch":sketch,"profiles":[index],"distance":2,"name":"Legacy inner"}))["feature"]
.as_u64()
.unwrap();
@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use crate::Session;
use serde_json::{Value, json};
use solvecraft_doc::construct::ConstructGeom;
use solvecraft_doc::{Document, FeatureKind, PlaneRef};
use solvecraft_geom::Plane;
use solidcraft_doc::construct::ConstructGeom;
use solidcraft_doc::{Document, FeatureKind, PlaneRef};
use solidcraft_geom::Plane;
fn run(s: &mut Session, command: &str, params: Value) -> Value {
s.execute(command, &params).unwrap_or_else(|e| panic!("{command} {params}: {e}"))
@@ -18,7 +18,7 @@ fn plane(s: &Session, name: &str) -> Plane {
}
fn volume(s: &Session) -> f64 {
s.model.state().bodies.iter().map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).sum()
s.model.state().bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).sum()
}
#[test]
@@ -1,7 +1,7 @@
#![allow(clippy::unwrap_used, clippy::panic)]
use serde_json::{Value, json};
use solvecraft_doc::{FeatureKind, ProfileSel};
use solvecraft_geom::Vec3;
use solidcraft_doc::{FeatureKind, ProfileSel};
use solidcraft_geom::Vec3;
use crate::Session;
@@ -37,7 +37,7 @@ fn breaking_offset_preserves_downstream_material_history_and_source_independence
run(&mut session, "parameters.change", json!({"name":radius,"expression":"2 mm"}));
assert!(session.model.results.iter().all(|result| result.error.is_none()));
assert!((session.world_state().bodies.last().unwrap().mesh().measure().volume.abs() - before_volume).abs() < 1e-6);
let reopened = Session::new(solvecraft_doc::Document::from_json(&session.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&session.doc.to_json()).unwrap());
assert!(reopened.model.results.iter().all(|result| result.error.is_none()));
assert!((reopened.world_state().bodies.last().unwrap().mesh().measure().volume.abs() - before_volume).abs() < 1e-6);
}
@@ -87,7 +87,7 @@ fn inset_past_corner_radius_creates_a_real_boundary_and_saved_profile_identity()
};
check(&session);
let saved = session.doc.to_json();
let reopened = Session::new(solvecraft_doc::Document::from_json(&saved).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&saved).unwrap());
check(&reopened);
assert_eq!(reopened.doc.to_json(), saved);
let undo = session.undo.len();
+9 -9
View File
@@ -3,7 +3,7 @@
//! graph, CSV/JSON import and export, expression evaluation and autocomplete.
use serde_json::{Value, json};
use solvecraft_doc::expr::{self, Kind};
use solidcraft_doc::expr::{self, Kind};
use super::CommandSpec;
use crate::params::{bad, bool_, str_};
@@ -123,7 +123,7 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
(_, Some(f)) => f.to_ascii_lowercase(),
_ => "all".to_string(),
};
let mut rows: Vec<solvecraft_doc::ParamRow> = s
let mut rows: Vec<solidcraft_doc::ParamRow> = s
.doc
.param_rows()
.into_iter()
@@ -136,7 +136,7 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
.collect();
// Favourites first; otherwise the table order (user, sketch, feature).
rows.sort_by_key(|r| !r.favorite);
let cycles = solvecraft_doc::cycles(&s.doc.all_param_exprs());
let cycles = solidcraft_doc::cycles(&s.doc.all_param_exprs());
Ok(json!({"parameters": rows, "count": rows.len(), "cycles": cycles}))
}
@@ -203,7 +203,7 @@ fn graph(s: &mut Session, _p: &Value) -> Result<Value> {
let (_, errs) = s.doc.param_values();
let nodes: Vec<String> = s.doc.param_defs().into_iter().map(|d| d.name).collect();
let edges: Vec<[String; 2]> = s.doc.param_graph().into_iter().map(|(a, b)| [a, b]).collect();
let cycles = solvecraft_doc::cycles(&s.doc.all_param_exprs());
let cycles = solidcraft_doc::cycles(&s.doc.all_param_exprs());
Ok(json!({"nodes": nodes, "edges": edges, "cycles": cycles, "errors": errs}))
}
@@ -229,7 +229,7 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
other => return Err(bad(cmd, format!("unknown format `{other}` (csv or json)"))),
};
if let Some(path) = path {
solvecraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
solidcraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
}
Ok(json!({"format": format, "path": path, "text": text}))
}
@@ -242,7 +242,7 @@ fn import(s: &mut Session, p: &Value) -> Result<Value> {
let text = match (str_(p, "text"), str_(p, "path")) {
(Some(t), _) => t.to_string(),
(None, Some(path)) => {
solvecraft_io::vfs::read_to_string(path, MAX_PARAMETER_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?
solidcraft_io::vfs::read_to_string(path, MAX_PARAMETER_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?
}
_ => return Err(bad(cmd, "give `text` or `path`")),
};
@@ -272,10 +272,10 @@ fn evaluate(s: &mut Session, p: &Value) -> Result<Value> {
let (vals, _) = s.doc.param_values();
let defaults = expr::Defaults::for_unit(unit.unwrap_or(""), 1.0);
let refs = expr::references(&e);
let lookup = |n: &str| vals.get(n).copied().ok_or_else(|| solvecraft_doc::DocError::Expr(format!("unknown parameter `{n}`")));
let lookup = |n: &str| vals.get(n).copied().ok_or_else(|| solidcraft_doc::DocError::Expr(format!("unknown parameter `{n}`")));
let r = expr::eval_with_defaults(&e, &lookup, defaults).and_then(|v| match kind {
Some(k) => v.to_kind_in(k, defaults).map(|x| (x, k)),
None => v.kind().map(|k| (v.v, k)).ok_or_else(|| solvecraft_doc::DocError::Expr(format!("the result is {}", v.describe()))),
None => v.kind().map(|k| (v.v, k)).ok_or_else(|| solidcraft_doc::DocError::Expr(format!("the result is {}", v.describe()))),
});
Ok(match r {
Ok((v, k)) => {
@@ -301,7 +301,7 @@ fn complete(s: &mut Session, p: &Value) -> Result<Value> {
let word: String = text.chars().rev().take_while(|c| c.is_alphanumeric() || *c == '_').collect::<Vec<_>>().into_iter().rev().collect();
let starts = |n: &str| word.is_empty() || n.to_ascii_lowercase().starts_with(&word.to_ascii_lowercase());
let mut out: Vec<Value> = Vec::new();
let mut rows: Vec<solvecraft_doc::ParamRow> = s.doc.param_rows().into_iter().filter(|r| starts(&r.name)).collect();
let mut rows: Vec<solidcraft_doc::ParamRow> = s.doc.param_rows().into_iter().filter(|r| starts(&r.name)).collect();
// Favourites, then user parameters, then the rest.
rows.sort_by_key(|r| (!r.favorite, r.source != "user"));
for r in rows.into_iter().take(200) {
+9 -9
View File
@@ -157,7 +157,7 @@ fn rename_updates_every_reference() {
assert!(rel(volume(&mut s), 40.0 * 40.0 * 10.0) < 1e-9);
// Model and feature parameters can be renamed too: dimensions and features follow.
let dim = s.doc.features.first().and_then(|f| match &f.kind {
solvecraft_doc::FeatureKind::Sketch { sketch, .. } => sketch.constraints.iter().filter_map(|c| c.param.clone()).next(),
solidcraft_doc::FeatureKind::Sketch { sketch, .. } => sketch.constraints.iter().filter_map(|c| c.param.clone()).next(),
_ => None,
});
let dim = dim.unwrap_or_default();
@@ -235,7 +235,7 @@ fn csv_and_json_round_trip() {
assert_eq!(s.doc.param("len").map(|p| p.expr.as_str()), Some("80 mm"));
assert!(matches!(s.execute("parameters.import", &json!({})), Err(EngineError::BadParams { .. })));
let dir = std::env::temp_dir().join(format!("solvecraft-params-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-params-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("p.json");
let r = run(&mut s, "parameters.export", json!({"path": path.to_string_lossy()}));
@@ -307,7 +307,7 @@ fn dimension_edits_of_a_triconnected_linkage_continue_and_store_the_solved_sketc
let solved = s.model.state().sketch(feature).unwrap().report.clone();
assert!(solved.ok(), "{solved:?}");
assert_eq!(linkage_turns(&s, feature), before);
let mut fresh = solvecraft_doc::Model::default();
let mut fresh = solidcraft_doc::Model::default();
fresh.evaluate(&s.doc);
let again = fresh.state().sketch(feature).unwrap().report.clone();
assert!(again.ok() && again.continuations == 0, "{again:?}");
@@ -348,7 +348,7 @@ fn a_failed_edit_keeps_the_stored_sketch_solved_for_its_values() {
run(&mut s, "sketch.point", json!({"point": [200, 200]}));
let stored = s.doc.sketch(feature).unwrap();
for c in stored.constraints.iter().filter(|c| c.param.is_some()) {
let measured = solvecraft_sketch::measure_dimension(stored, &c.kind).unwrap();
let measured = solidcraft_sketch::measure_dimension(stored, &c.kind).unwrap();
assert!((measured - c.kind.value().unwrap()).abs() < 1e-6, "{}: {measured} vs {:?}", c.id, c.kind.value());
}
}
@@ -396,7 +396,7 @@ fn a_design_stored_with_other_values_still_rebuilds() {
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch": "Linkage", "distance": 5}));
set_params(&mut s, &params, &FLIPPING);
let s = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let s = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let state = s.model.state();
let solved = state.sketch(feature).unwrap();
assert!(solved.report.ok() && !solved.report.folded.is_empty(), "{:?}", solved.report);
@@ -450,7 +450,7 @@ fn a_failed_auto_constrain_keeps_the_stored_sketch_consistent() {
let consistent = |s: &Session, feature: u64| {
let stored = s.doc.sketch(feature).unwrap();
stored.constraints.iter().filter(|c| c.param.is_some()).all(|c| {
let measured = solvecraft_sketch::measure_dimension(stored, &c.kind).unwrap();
let measured = solidcraft_sketch::measure_dimension(stored, &c.kind).unwrap();
(measured - c.kind.value().unwrap()).abs() < 1e-6
})
};
@@ -473,7 +473,7 @@ fn a_failed_auto_constrain_keeps_the_stored_sketch_consistent() {
}
/// The stored sketch's points, by id.
fn stored_points(s: &Session, feature: u64) -> Vec<solvecraft_geom::Vec2> {
fn stored_points(s: &Session, feature: u64) -> Vec<solidcraft_geom::Vec2> {
s.doc.sketch(feature).unwrap().points.iter().map(|p| p.pos).collect()
}
@@ -504,7 +504,7 @@ fn an_unsolvable_value_edit_is_refused_and_changes_nothing() {
fn value_edits_continue_from_the_last_solved_state() {
// A → B → C, each a small edit solved in one jump; C is solved from B (the stored state),
// and setting both values back reproduces A (the linkage is fully constrained).
let close = |a: &[solvecraft_geom::Vec2], b: &[solvecraft_geom::Vec2], tol: f64| a.iter().zip(b).all(|(p, q)| p.dist(*q) < tol);
let close = |a: &[solidcraft_geom::Vec2], b: &[solidcraft_geom::Vec2], tol: f64| a.iter().zip(b).all(|(p, q)| p.dist(*q) < tol);
let (mut s, feature, params) = linkage_session();
run(&mut s, "sketch.finish", json!({}));
let a = stored_points(&s, feature);
@@ -520,7 +520,7 @@ fn value_edits_continue_from_the_last_solved_state() {
let stored = from_b.driving_values();
let (vals, _) = s.doc.param_values();
s.doc.apply_dimension_values(&vals, &mut from_b).unwrap();
assert!(solvecraft_sketch::solve_after_value_edit(&mut from_b, &stored).ok());
assert!(solidcraft_sketch::solve_after_value_edit(&mut from_b, &stored).ok());
assert!(close(&c, &from_b.points.iter().map(|p| p.pos).collect::<Vec<_>>(), 1e-6) && !close(&c, &b, 1e-3));
run(&mut s, "parameters.change", json!({"name": params[7], "expression": p7}));
run(&mut s, "parameters.change", json!({"name": params[8], "expression": p8}));
+9 -9
View File
@@ -8,7 +8,7 @@
//! washer, the top of a set screw or pin); the shank runs down −z.
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use super::CommandSpec;
use crate::params::{bad, num, str_, vec3};
@@ -355,7 +355,7 @@ fn insert(s: &mut Session, p: &Value) -> Result<Value> {
let c = run(s, "component.create", json!({"name": name}))?;
let (comp, occ) = (c["component"].as_u64().unwrap_or(0), c["occurrence"].as_u64().unwrap_or(0));
build(s, &sh)?;
s.doc_mut().parts.insert(comp, solvecraft_doc::StandardPart { family: family.clone(), size: size.clone(), length: shape_len(&sh) });
s.doc_mut().parts.insert(comp, solidcraft_doc::StandardPart { family: family.clone(), size: size.clone(), length: shape_len(&sh) });
run(s, "component.activate", json!({"component": active}))?;
let mut out = json!({"component": comp, "occurrence": occ, "name": name});
if let (Some(a), Some(h)) = (at, host) {
@@ -370,7 +370,7 @@ fn insert(s: &mut Session, p: &Value) -> Result<Value> {
} else if let Some(pt) = p.get("point").and_then(vec3) {
// Placed at a point, its +z along `direction`.
let dir = p.get("direction").and_then(vec3).and_then(|d| d.normalized()).unwrap_or(Vec3::Z);
let m = solvecraft_doc::rigid(
let m = solidcraft_doc::rigid(
Vec3::ZERO,
Vec3::ZERO,
Vec3::Z.cross(dir).normalized().unwrap_or(Vec3::X),
@@ -393,7 +393,7 @@ fn hole_wall(s: &Session, a: Vec3) -> Option<Vec3> {
for b in &st.bodies {
for d in dirs {
let q = a + d * step;
if let Some(c) = solvecraft_doc::kernel::cylinder_face_at(&b.body, q) {
if let Some(c) = solidcraft_doc::kernel::cylinder_face_at(&b.body, q) {
let v = q - c.axis_point;
let along = v.dot(c.axis);
let on_wall = ((v - c.axis * along).len() - c.radius).abs() < 1e-3 && along > c.start && along < c.end;
@@ -435,7 +435,7 @@ fn resize(s: &mut Session, p: &Value) -> Result<Value> {
run(s, "component.activate", json!({"component": comp}))?;
build(s, &sh)?;
let name = part_name(&part.family, &size, &sh);
s.doc_mut().parts.insert(comp, solvecraft_doc::StandardPart { family: part.family.clone(), size: size.clone(), length: shape_len(&sh) });
s.doc_mut().parts.insert(comp, solidcraft_doc::StandardPart { family: part.family.clone(), size: size.clone(), length: shape_len(&sh) });
if let Some(c) = s.doc_mut().components.iter_mut().find(|c| c.id == comp) {
c.name = name.clone();
}
@@ -460,14 +460,14 @@ mod tests {
}
}
fn bbox(s: &Session, prefix: &str) -> (solvecraft_geom::Vec3, solvecraft_geom::Vec3) {
fn bbox(s: &Session, prefix: &str) -> (solidcraft_geom::Vec3, solidcraft_geom::Vec3) {
let st = s.world_state();
let comp = s.doc.components.iter().find(|c| c.name.starts_with(prefix)).map(|c| c.id).unwrap_or(0);
let (mut lo, mut hi) = (solvecraft_geom::Vec3::new(f64::MAX, f64::MAX, f64::MAX), solvecraft_geom::Vec3::new(f64::MIN, f64::MIN, f64::MIN));
let (mut lo, mut hi) = (solidcraft_geom::Vec3::new(f64::MAX, f64::MAX, f64::MAX), solidcraft_geom::Vec3::new(f64::MIN, f64::MIN, f64::MIN));
for b in st.bodies.iter().filter(|b| s.doc.model_body_component(b) == comp) {
let m = b.mesh().bounds();
lo = solvecraft_geom::Vec3::new(lo.x.min(m.min.x), lo.y.min(m.min.y), lo.z.min(m.min.z));
hi = solvecraft_geom::Vec3::new(hi.x.max(m.max.x), hi.y.max(m.max.y), hi.z.max(m.max.z));
lo = solidcraft_geom::Vec3::new(lo.x.min(m.min.x), lo.y.min(m.min.y), lo.z.min(m.min.z));
hi = solidcraft_geom::Vec3::new(hi.x.max(m.max.x), hi.y.max(m.max.y), hi.z.max(m.max.z));
}
(lo, hi)
}
+2 -2
View File
@@ -85,7 +85,7 @@ fn circular_numeric_axis_origin_preserves_hole_geometry_and_reopens() {
assert_eq!(s.doc.to_json(), before);
run(&mut s, "edit.redo", json!({}));
assert_eq!(s.doc.to_json(), saved);
let mut reopened = Session::new(solvecraft_doc::Document::from_json(&saved).unwrap());
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&saved).unwrap());
assert!(rel(volume(&mut reopened), expected) < 1e-4);
}
// The documented omission of origin still means zero.
@@ -94,7 +94,7 @@ fn circular_numeric_axis_origin_preserves_hole_geometry_and_reopens() {
let before = s.doc.to_json();
run(&mut s, "solid.pattern.circular", json!({"bodies":["Cube"],"axis":{"dir":[0,0,1]},"count":4}));
let implicit = s.doc.to_json();
let mut explicit = Session::new(solvecraft_doc::Document::from_json(&before).unwrap());
let mut explicit = Session::new(solidcraft_doc::Document::from_json(&before).unwrap());
run(&mut explicit, "solid.pattern.circular", json!({"bodies":["Cube"],"axis":{"origin":[0,0,0],"dir":[0,0,1]},"count":4}));
assert_eq!(implicit, explicit.doc.to_json());
}
+8 -8
View File
@@ -1,10 +1,10 @@
//! Plastic (enclosure) features: Boss, Lip / Groove, Snap Fit and Rest, and plastic rules.
use serde_json::{Value, json};
use solvecraft_doc::FeatureKind;
use solvecraft_doc::expr::Kind;
use solvecraft_doc::plastic::PlasticRule;
use solvecraft_geom::Vec3;
use solidcraft_doc::FeatureKind;
use solidcraft_doc::expr::Kind;
use solidcraft_doc::plastic::PlasticRule;
use solidcraft_geom::Vec3;
use super::CommandSpec;
use super::features::{add_feature, check_expr};
@@ -267,7 +267,7 @@ fn snap_fit(s: &mut Session, p: &Value) -> Result<Value> {
#[cfg(test)]
mod tests {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use std::f64::consts::PI;
use serde_json::{Value, json};
@@ -362,7 +362,7 @@ mod tests {
run(&mut s, "parameters.change", json!({"name":"board_d","expression":"50 mm"}));
let before = s.model.state_before(feature.id);
let name = feature.face_names.first().unwrap();
let point = solvecraft_doc::naming::point_on_face(&before, name, Vec3::new(0., -28., 20.))
let point = solidcraft_doc::naming::point_on_face(&before, name, Vec3::new(0., -28., 20.))
.expect("the original wall identity survives merged pieces")
.0;
assert!((point - Vec3::new(0., -33., 20.)).len() < 1e-8, "{point:?}");
@@ -372,7 +372,7 @@ mod tests {
let after = &session.model.result(feature.id).unwrap().state;
let old = before.body("Case").unwrap();
let body = after.body("Case").unwrap();
let added = solvecraft_kernel::measure(&body.body).unwrap().volume - solvecraft_kernel::measure(&old.body).unwrap().volume;
let added = solidcraft_kernel::measure(&body.body).unwrap().volume - solidcraft_kernel::measure(&old.body).unwrap().volume;
// Rectangular arm + triangular catch; the small attachment overlap lies inside the wall.
assert!((added - (8. * 1.2 + 0.8 * 1.5 / 2.) * 6.).abs() < 0.05, "{added}");
let mesh = body.mesh();
@@ -382,7 +382,7 @@ mod tests {
assert!(!mesh.contains(Vec3::new(0., -24., 19.4)), "the arm ends at its specified length");
};
check(&s);
let cold = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(cold.model.results.iter().all(|result| result.error.is_none()));
check(&cold);
}
@@ -2,8 +2,8 @@
use crate::Session;
use serde_json::{Value, json};
use solvecraft_doc::{Document, FeatureKind, RevolveConvention};
use solvecraft_geom::Vec3;
use solidcraft_doc::{Document, FeatureKind, RevolveConvention};
use solidcraft_geom::Vec3;
use std::f64::consts::{FRAC_PI_2, PI, SQRT_2};
fn run(s: &mut Session, cmd: &str, p: Value) -> Value {
@@ -28,7 +28,7 @@ fn turn(s: &mut Session, sign: f64, settings: Value) {
fn check_centered(s: &Session, opposed: bool) {
let state = s.model.state();
let expected = 30.0 * PI * if opposed { 2.0 } else { 1.0 };
let volume: f64 = state.bodies.iter().map(|mb| solvecraft_kernel::measure(&mb.body).unwrap().volume).sum();
let volume: f64 = state.bodies.iter().map(|mb| solidcraft_kernel::measure(&mb.body).unwrap().volume).sum();
assert!((volume - expected).abs() < 0.03, "{volume} vs {expected}");
for sign in if opposed { vec![1.0, -1.0] } else { vec![1.0] } {
let body = &state.bodies.iter().find(|mb| mb.body.contains(Vec3::new(sign * 3.0, 5.0, 0.0))).unwrap().body;
@@ -40,7 +40,7 @@ fn check_centered(s: &Session, opposed: bool) {
assert!((bounds.max.z - 2.0 * SQRT_2).abs() < 1e-6, "{bounds:?}");
let inner_x = if sign > 0.0 { bounds.min.x } else { -bounds.max.x };
assert!((inner_x - SQRT_2).abs() < 1e-6, "{bounds:?}");
let centroid = solvecraft_kernel::measure(body).unwrap().centroid;
let centroid = solidcraft_kernel::measure(body).unwrap().centroid;
let radial = (2.0 / 3.0) * (64.0 - 8.0) / (16.0 - 4.0);
let want_x = radial * (PI / 4.0).sin() / (PI / 4.0);
assert!((centroid.x - sign * want_x).abs() < 0.005, "{centroid:?}");
@@ -87,7 +87,7 @@ fn two_sided_revolves_use_the_canonical_negative_and_positive_limits() {
assert!((bounds.max.z - 2.0 * 3.0f64.sqrt()).abs() < 1e-6);
assert!(body.contains(Vec3::new(3.0, 5.0, 0.0)));
assert!(!body.contains(Vec3::new(0.0, 5.0, 3.0)));
assert!((solvecraft_kernel::measure(body).unwrap().volume - 30.0 * PI).abs() < 0.03);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 30.0 * PI).abs() < 0.03);
}
}
@@ -97,7 +97,7 @@ fn legacy_missing_convention_keeps_geometry_through_edit_redefine_undo_and_reope
profile(&mut s, false);
turn(&mut s, -1.0, json!({"convention":"legacy_axis_start"}));
let mut saved: Value = serde_json::from_str(&s.doc.to_json()).unwrap();
saved["format"] = json!("solvecraft/10");
saved["format"] = json!("solidcraft/10");
for f in saved["features"].as_array_mut().unwrap() {
f.as_object_mut().unwrap().remove("convention");
}
@@ -149,7 +149,7 @@ fn extent_handle_angles_match_new_and_legacy_material() {
let ss = &state.sketches[0];
let regions = vec![ss.profiles[0].region.clone()];
for convention in [RevolveConvention::SketchNormal, RevolveConvention::LegacyAxisStart] {
let extent = solvecraft_doc::revolve_extent_with_convention(
let extent = solidcraft_doc::revolve_extent_with_convention(
&state,
&ss.plane,
&regions,
@@ -180,7 +180,7 @@ fn symmetric_revolve_preserves_a_translated_rotated_sketch_frame() {
assert!(bounds.max.dist(Vec3::new(20.0, 20.0 + 2.0 * SQRT_2, 34.0)) < 1e-4, "{bounds:?}");
assert!(body.contains(Vec3::new(15.0, 20.0, 33.0)));
assert!(!body.contains(Vec3::new(15.0, 23.0, 30.0)));
assert!((solvecraft_kernel::measure(body).unwrap().volume - 30.0 * PI).abs() < 0.03);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 30.0 * PI).abs() < 0.03);
}
#[test]
@@ -217,7 +217,7 @@ fn centered_coplanar_cut_uses_the_actual_angular_interval() {
// Removed sector clipped by x>=2: 10∫[-π/4,π/4](16−4sec²θ)/2 dθ.
// The sector crosses the whole stock at z=0, leaving two disconnected solids.
let expected = 200.0 - 40.0 * PI;
let volumes: Vec<_> = state.bodies.iter().map(|mb| solvecraft_kernel::measure(&mb.body).unwrap().volume).collect();
let volumes: Vec<_> = state.bodies.iter().map(|mb| solidcraft_kernel::measure(&mb.body).unwrap().volume).collect();
let total: f64 = volumes.iter().sum();
assert!((total - expected).abs() < 0.05, "{volumes:?}: total{total}, expected{expected}");
assert_eq!(state.bodies.len(), 2);
@@ -241,7 +241,7 @@ fn one_sided_and_full_revolves_keep_the_existing_material() {
assert!(body.contains(Vec3::new(3.0 / SQRT_2, 5.0, 3.0 / SQRT_2)));
assert_eq!(body.contains(Vec3::new(-3.0 / SQRT_2, 5.0, -3.0 / SQRT_2)), angle == "360 deg");
let expected = if angle == "360 deg" { 120.0 * PI } else { 30.0 * PI };
assert!((solvecraft_kernel::measure(body).unwrap().volume - expected).abs() < 0.08);
assert!((solidcraft_kernel::measure(body).unwrap().volume - expected).abs() < 0.08);
}
}
}
@@ -256,7 +256,7 @@ fn point_targets_use_each_profiles_canonical_axis_for_both_axis_signs() {
turn(&mut s, sign, json!({"to":[0,5,3]}));
let state = s.model.state();
let expected = 30.0 * PI * if opposed { 2.0 } else { 1.0 };
let volume: f64 = state.bodies.iter().map(|mb| solvecraft_kernel::measure(&mb.body).unwrap().volume).sum();
let volume: f64 = state.bodies.iter().map(|mb| solidcraft_kernel::measure(&mb.body).unwrap().volume).sum();
assert!((volume - expected).abs() < 0.05, "{volume}");
assert!(state.bodies.iter().any(|mb| mb.body.contains(Vec3::new(3.0 / SQRT_2, 5.0, 3.0 / SQRT_2))));
assert!(!state.bodies.iter().any(|mb| mb.body.contains(Vec3::new(3.0 / SQRT_2, 5.0, -3.0 / SQRT_2))));
@@ -299,10 +299,10 @@ fn curved_targets_keep_only_the_material_before_the_post_for_both_axis_signs() {
assert!(!arc.contains(Vec3::new(-side * 12.0 * (PI / 6.0).cos(), 6.0, -2.5)));
assert!(!arc.contains(Vec3::new(side * 12.0 * (PI / 6.0).cos(), -6.0, -2.5)));
let full = PI * (225.0 - 100.0) * 5.0;
let angle = solvecraft_kernel::measure(arc).unwrap().volume / full * 360.0;
let angle = solidcraft_kernel::measure(arc).unwrap().volume / full * 360.0;
assert!(angle > 60.0 && angle < 90.0, "{angle}");
let overlap = solvecraft_kernel::boolean(arc, post, solvecraft_kernel::BoolOp::Intersect).unwrap();
assert!(overlap.is_none_or(|b| solvecraft_kernel::measure(&b).unwrap().volume < 1e-3));
let overlap = solidcraft_kernel::boolean(arc, post, solidcraft_kernel::BoolOp::Intersect).unwrap();
assert!(overlap.is_none_or(|b| solidcraft_kernel::measure(&b).unwrap().volume < 1e-3));
}
}
}
+4 -4
View File
@@ -2,7 +2,7 @@
//! positive side is hidden). It changes no geometry and is not an undo step.
use serde_json::{Value, json};
use solvecraft_geom::{Plane, Vec3};
use solidcraft_geom::{Plane, Vec3};
use super::CommandSpec;
use crate::params::{bad, bool_, expr, vec3};
@@ -35,13 +35,13 @@ fn section(s: &mut Session, p: &Value) -> Result<Value> {
// A construction plane (name or id): where it is now.
Some(v @ (Value::String(_) | Value::Number(_))) => {
let r = super::features::plane_param(s, Some(v), cmd)?;
let solvecraft_doc::PlaneRef::Construction { name } = &r else { return Err(bad(cmd, "`plane` must be a plane")) };
let solidcraft_doc::PlaneRef::Construction { name } = &r else { return Err(bad(cmd, "`plane` must be a plane")) };
let st = s.model.state();
let pl = st
.construct
.iter()
.find(|c| &c.name == name)
.and_then(|c| if let solvecraft_doc::construct::ConstructGeom::Plane(pl) = &c.geom { Some(*pl) } else { None })
.and_then(|c| if let solidcraft_doc::construct::ConstructGeom::Plane(pl) = &c.geom { Some(*pl) } else { None })
.ok_or_else(|| bad(cmd, format!("construction plane `{name}` is not built (rolled back, suppressed or failed)")))?;
(pl.origin, pl.normal(), None)
}
@@ -54,7 +54,7 @@ fn section(s: &mut Session, p: &Value) -> Result<Value> {
};
let length = |key: &str| -> Result<Option<f64>> {
match expr(p, key) {
Some(e) => s.doc.eval(&e, solvecraft_doc::expr::Kind::Length).map(Some).map_err(|e| bad(cmd, format!("{key}: {e}"))),
Some(e) => s.doc.eval(&e, solidcraft_doc::expr::Kind::Length).map(Some).map_err(|e| bad(cmd, format!("{key}: {e}"))),
None => Ok(None),
}
};
+12 -12
View File
@@ -2,10 +2,10 @@
//! the flat pattern (outline, bend lines, bend table) and its DXF export.
use serde_json::{Value, json};
use solvecraft_doc::FeatureKind;
use solvecraft_doc::expr::Kind;
use solvecraft_doc::sheet::{SheetBody, SheetRule};
use solvecraft_geom::{Vec2, Vec3};
use solidcraft_doc::FeatureKind;
use solidcraft_doc::expr::Kind;
use solidcraft_doc::sheet::{SheetBody, SheetRule};
use solidcraft_geom::{Vec2, Vec3};
use super::CommandSpec;
use super::features::{add_feature, check_expr, feature_sketch, profiles, sketch_id};
@@ -157,7 +157,7 @@ fn fold(s: &mut Session, p: &Value) -> Result<Value> {
let ss = st.sketch(sk).ok_or_else(|| bad(cmd, "no such sketch"))?;
let ci = ss.sketch.curve_index(&c).ok_or_else(|| bad(cmd, format!("no curve `{c}` in the sketch")))?;
let (a, b) = match ss.sketch.segs(ci).as_slice() {
[solvecraft_geom::Seg2::Line { a, b }] => (ss.plane.to_world(*a), ss.plane.to_world(*b)),
[solidcraft_geom::Seg2::Line { a, b }] => (ss.plane.to_world(*a), ss.plane.to_world(*b)),
_ => return Err(bad(cmd, "the fold line must be a sketch line")),
};
(Some(sk), Some(c), a, b)
@@ -323,7 +323,7 @@ fn flat_pattern(s: &mut Session, p: &Value) -> Result<Value> {
pub fn flat_dxf(sh: &SheetBody) -> Result<String> {
let regions = sh.flat_regions()?;
let pockets = sh.machining()?;
let pocket_layer = |p: &solvecraft_doc::sheet::SheetPocket| {
let pocket_layer = |p: &solidcraft_doc::sheet::SheetPocket| {
let side = if p.z0 == 0.0 {
"BOTTOM"
} else if p.z1 == sh.t {
@@ -384,9 +384,9 @@ pub fn flat_dxf(sh: &SheetBody) -> Result<String> {
for (h, layer) in loops {
// A circle is often represented by two semicircles. Keep the whole analytic
// boundary as one CIRCLE rather than approximating outer boundaries with chords.
if let Some(solvecraft_geom::Seg2::Arc { center, radius, sweep, .. }) = h.segs.first() {
let total: f64 = h.segs.iter().filter_map(|s| if let solvecraft_geom::Seg2::Arc { sweep, .. } = s { Some(*sweep) } else { None }).sum();
let circular = h.segs.iter().all(|s| matches!(s, solvecraft_geom::Seg2::Arc { center: c, radius: r, sweep: a, .. } if c.dist(*center) < 1e-9 && (*r-*radius).abs() < 1e-9 && a.signum() == sweep.signum()));
if let Some(solidcraft_geom::Seg2::Arc { center, radius, sweep, .. }) = h.segs.first() {
let total: f64 = h.segs.iter().filter_map(|s| if let solidcraft_geom::Seg2::Arc { sweep, .. } = s { Some(*sweep) } else { None }).sum();
let circular = h.segs.iter().all(|s| matches!(s, solidcraft_geom::Seg2::Arc { center: c, radius: r, sweep: a, .. } if c.dist(*center) < 1e-9 && (*r-*radius).abs() < 1e-9 && a.signum() == sweep.signum()));
if circular && (total.abs() - std::f64::consts::TAU).abs() < 1e-9 {
w(0, "CIRCLE");
w(8, &layer);
@@ -398,7 +398,7 @@ pub fn flat_dxf(sh: &SheetBody) -> Result<String> {
}
for seg in &h.segs {
match *seg {
solvecraft_geom::Seg2::Arc { center, radius, start, sweep } => {
solidcraft_geom::Seg2::Arc { center, radius, start, sweep } => {
if (sweep.abs() - std::f64::consts::TAU).abs() < 1e-9 {
w(0, "CIRCLE");
w(8, &layer);
@@ -438,7 +438,7 @@ fn export_dxf(s: &mut Session, p: &Value) -> Result<Value> {
let sh = sheet_of(s, p, cmd)?;
let text = flat_dxf(&sh)?;
if let Some(path) = str_(p, "path").filter(|x| !x.trim().is_empty()) {
solvecraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
solidcraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
return Ok(json!({"path": path, "bends": sh.bends().len()}));
}
Ok(json!({"dxf": text, "bends": sh.bends().len()}))
@@ -482,7 +482,7 @@ fn export_bend_table(s: &mut Session, p: &Value) -> Result<Value> {
csv
};
if let Some(path) = str_(p, "path").filter(|x| !x.trim().is_empty()) {
solvecraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
solidcraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
return Ok(json!({"path": path, "format": format, "bends": sh.bends().len()}));
}
Ok(json!({"format": format, "text": text, "bends": sh.bends().len()}))
+79 -79
View File
@@ -50,7 +50,7 @@ fn shallow_sheet_pocket_preserves_material_depth_export_undo_and_reopen() {
assert_eq!(f["pockets"][0]["depth_mm"], 1.0);
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap().to_string();
assert!(dxf.contains("POCKET_BOTTOM_Z0.000000_TO_1.000000_MM"));
assert_eq!(solvecraft_io::read_dxf(&dxf).unwrap().len(), 8);
assert_eq!(solidcraft_io::read_dxf(&dxf).unwrap().len(), 8);
run(&mut s, "edit.undo", json!({}));
assert!((volume(&mut s) - 15000.0).abs() < 1e-6);
run(&mut s, "edit.redo", json!({}));
@@ -59,10 +59,10 @@ fn shallow_sheet_pocket_preserves_material_depth_export_undo_and_reopen() {
assert!((volume(&mut s) - 14900.0).abs() < 1e-6);
let state = s.model.state();
let body = &state.bodies[0].body;
assert!(!body.contains(solvecraft_geom::Vec3::new(25.0, 25.0, 0.5)));
assert!(body.contains(solvecraft_geom::Vec3::new(25.0, 25.0, 1.5)));
assert!(!body.contains(solidcraft_geom::Vec3::new(25.0, 25.0, 0.5)));
assert!(body.contains(solidcraft_geom::Vec3::new(25.0, 25.0, 1.5)));
}
let mut reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!((volume(&mut reopened) - 14900.0).abs() < 1e-6);
assert_eq!(flat(&mut reopened)["pockets"], f["pockets"]);
}
@@ -81,7 +81,7 @@ fn planar_sheet_intersection_retains_exact_outline_and_islands() {
assert_eq!(flat(&mut s)["flat_size_mm"], json!([10.0, 10.0, 2.5]));
}
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap().to_string();
assert_eq!(solvecraft_io::read_dxf(&dxf).unwrap().len(), 4);
assert_eq!(solidcraft_io::read_dxf(&dxf).unwrap().len(), 4);
let mut imported = Session::default();
run(&mut imported, "sketch.insert_dxf", json!({"name":"Retained","text":dxf}));
run(&mut imported, "sketch.finish", json!({}));
@@ -212,26 +212,26 @@ fn intersect_preserves_both_material_regions_of_a_logical_ring_sheet() {
let area: f64 = regions.iter().map(|r| r.area()).sum();
assert!((area - (900.0 - PI * 75.0)).abs() < 1e-8, "area={area}");
let body = &state.bodies[0].body;
assert!(body.contains(solvecraft_geom::Vec3::new(50.0, 30.0, 1.0)));
assert!(!body.contains(solvecraft_geom::Vec3::new(57.0, 30.0, 1.0)));
assert!(body.contains(solvecraft_geom::Vec3::new(64.0, 30.0, 1.0)));
assert!(!body.contains(solvecraft_geom::Vec3::new(70.0, 30.0, 1.0)));
assert!(body.contains(solidcraft_geom::Vec3::new(50.0, 30.0, 1.0)));
assert!(!body.contains(solidcraft_geom::Vec3::new(57.0, 30.0, 1.0)));
assert!(body.contains(solidcraft_geom::Vec3::new(64.0, 30.0, 1.0)));
assert!(!body.contains(solidcraft_geom::Vec3::new(70.0, 30.0, 1.0)));
assert!((volume(&mut s) - (900.0 - PI * 75.0) * 2.5).abs() < 0.2);
}
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap().to_string();
assert_eq!(solvecraft_io::read_dxf(&dxf).unwrap().len(), 6);
assert_eq!(solidcraft_io::read_dxf(&dxf).unwrap().len(), 6);
}
#[test]
fn machining_a_folded_parallel_panel_preserves_its_pocket_on_unfold() {
use solvecraft_doc::{apply_point, apply_vector};
use solvecraft_geom::Vec3;
use solidcraft_doc::{apply_point, apply_vector};
use solidcraft_geom::Vec3;
let mut s = Session::default();
base(&mut s, 100.0, 60.0);
run(&mut s, "sheet.flange", json!({"edges":[[50,0,2.5]],"height":20}));
let sh = s.model.state().sheets[0].clone();
let points = sh.panel(&sh.flanges[0]).outer.polyline(1e-3);
let c = points.iter().copied().fold(solvecraft_geom::Vec2::ZERO, |sum, p| sum + p) / (points.len() as f64);
let c = points.iter().copied().fold(solidcraft_geom::Vec2::ZERO, |sum, p| sum + p) / (points.len() as f64);
let w = sh.world(Some(0));
let origin = apply_point(&w, Vec3::ZERO);
let x = apply_vector(&w, Vec3::X);
@@ -268,8 +268,8 @@ fn flipped_sheet_pocket_keeps_depth_in_the_local_thickness_direction() {
}
assert!((volume(&mut s) - 14900.0).abs() < 1e-6);
let state = s.model.state();
assert!(!state.bodies[0].body.contains(solvecraft_geom::Vec3::new(25.0, 25.0, -0.5)));
assert!(state.bodies[0].body.contains(solvecraft_geom::Vec3::new(25.0, 25.0, -1.5)));
assert!(!state.bodies[0].body.contains(solidcraft_geom::Vec3::new(25.0, 25.0, -0.5)));
assert!(state.bodies[0].body.contains(solidcraft_geom::Vec3::new(25.0, 25.0, -1.5)));
}
}
@@ -300,10 +300,10 @@ fn multiple_profile_intersect_keeps_tool_union_for_solids_and_sheet_groups() {
}
let state = s.model.state();
for x in [25.0, 65.0] {
assert!(state.bodies.iter().any(|b| b.body.contains(solvecraft_geom::Vec3::new(x, 25.0, 1.0))));
assert!(state.bodies.iter().any(|b| b.body.contains(solidcraft_geom::Vec3::new(x, 25.0, 1.0))));
}
assert!(!state.bodies.iter().any(|b| b.body.contains(solvecraft_geom::Vec3::new(50.0, 25.0, 1.0))));
assert!(state.bodies.iter().any(|b| b.name == "Control" && b.body.contains(solvecraft_geom::Vec3::new(205.0, 5.0, 5.0))));
assert!(!state.bodies.iter().any(|b| b.body.contains(solidcraft_geom::Vec3::new(50.0, 25.0, 1.0))));
assert!(state.bodies.iter().any(|b| b.name == "Control" && b.body.contains(solidcraft_geom::Vec3::new(205.0, 5.0, 5.0))));
}
}
@@ -322,14 +322,14 @@ fn deleting_sheet_material_prunes_cosmetic_threads_and_undo_restores_them() {
assert!(s.model.state().bodies.is_empty());
assert!(s.model.state().sheets.is_empty());
assert!(s.model.state().threads.is_empty());
assert!(solvecraft_doc::world_state(&s.doc, &s.model.state()).threads.is_empty());
assert!(solidcraft_doc::world_state(&s.doc, &s.model.state()).threads.is_empty());
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.model.state().threads.len(), 1);
}
#[test]
fn sheet_pocket_transfer_keeps_local_depth_world_pose_and_history() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
fn check(s: &Session, name: &str, owner: u64, local_origin: Vec3, local_y: Vec3, transferred: bool) {
let state = s.model.state();
let mb = state.body(name).unwrap();
@@ -341,8 +341,8 @@ fn sheet_pocket_transfer_keeps_local_depth_world_pose_and_history() {
assert!(sh.frame.y.dist(local_y) < 1e-8);
assert_eq!(sh.pockets.len(), 1);
assert_eq!((sh.pockets[0].z0, sh.pockets[0].z1), (0.0, 1.0));
let cut = sh.frame.to_world(solvecraft_geom::Vec2::new(25.0, 25.0)) + sh.frame.normal() * 0.5;
let floor = sh.frame.to_world(solvecraft_geom::Vec2::new(25.0, 25.0)) + sh.frame.normal() * 1.5;
let cut = sh.frame.to_world(solidcraft_geom::Vec2::new(25.0, 25.0)) + sh.frame.normal() * 0.5;
let floor = sh.frame.to_world(solidcraft_geom::Vec2::new(25.0, 25.0)) + sh.frame.normal() * 1.5;
assert!(!mb.body.contains(cut));
assert!(mb.body.contains(floor));
let world = s.world_state();
@@ -352,7 +352,7 @@ fn sheet_pocket_transfer_keeps_local_depth_world_pose_and_history() {
assert!(bounds.max.dist(Vec3::new(100.0, 60.0, 2.5)) < 1e-6, "{bounds:?}");
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.5)));
assert!(body.contains(Vec3::new(25.0, 25.0, 1.5)));
assert!((solvecraft_kernel::measure(body).unwrap().volume - 14900.0).abs() < 1e-6);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 14900.0).abs() < 1e-6);
}
let mut s = Session::default();
base(&mut s, 100.0, 60.0);
@@ -381,7 +381,7 @@ fn sheet_pocket_transfer_keeps_local_depth_world_pose_and_history() {
assert_eq!(f["pockets"][0]["side"], "bottom");
assert_eq!(f["pockets"][0]["depth_mm"], 1.0);
}
let mut reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
check(&reopened, &name, component, origin, y, true);
run(&mut reopened, "component.activate", json!({"component":component}));
run(&mut reopened, "sheet.unfold", json!({"body":name}));
@@ -789,7 +789,7 @@ fn malformed_fold_points_are_rejected_atomically() {
#[test]
fn flat_dxf_preserves_exact_cutout_entities() {
use solvecraft_geom::{Loop2, Region2, Seg2, Vec2};
use solidcraft_geom::{Loop2, Region2, Seg2, Vec2};
let mut s = Session::default();
base(&mut s, 100.0, 60.0);
let mut sh = s.model.state().sheets[0].clone();
@@ -800,9 +800,9 @@ fn flat_dxf_preserves_exact_cutout_entities() {
let dxf = super::flat_dxf(&sh).unwrap();
assert_eq!(dxf.matches("0\nCIRCLE\n").count(), 1, "{dxf}");
assert!(!dxf.contains("0\nARC\n"), "a full circle must not become a zero-length ARC");
let read = solvecraft_io::read_dxf(&dxf).unwrap();
let read = solidcraft_io::read_dxf(&dxf).unwrap();
assert_eq!(read.len(), 5);
assert!(read.iter().any(|g| matches!(g, solvecraft_io::Geom2::Circle(c, r) if *c == Vec2::new(40.0, 30.0) && (*r - 5.0).abs() < 1e-9)));
assert!(read.iter().any(|g| matches!(g, solidcraft_io::Geom2::Circle(c, r) if *c == Vec2::new(40.0, 30.0) && (*r - 5.0).abs() < 1e-9)));
sh.holes[0].outer = Loop2 {
segs: vec![
Seg2::Line { a: Vec2::new(10.0, 10.0), b: Vec2::new(30.0, 10.0) },
@@ -813,9 +813,9 @@ fn flat_dxf_preserves_exact_cutout_entities() {
assert_eq!(dxf.matches("0\nLINE\n").count(), 5, "four outline lines and one exact cutout line");
assert_eq!(dxf.matches("0\nARC\n").count(), 1);
assert!(dxf.contains("50\n0.000000\n51\n180.000000\n"));
let read = solvecraft_io::read_dxf(&dxf).unwrap();
let read = solidcraft_io::read_dxf(&dxf).unwrap();
assert_eq!(read.len(), 6);
assert!(read.iter().any(|g| matches!(g, solvecraft_io::Geom2::Arc { c, a, b } if *c == Vec2::new(20.0, 10.0) && a.dist(Vec2::new(30.0, 10.0)) < 1e-9 && b.dist(Vec2::new(10.0, 10.0)) < 1e-9)));
assert!(read.iter().any(|g| matches!(g, solidcraft_io::Geom2::Arc { c, a, b } if *c == Vec2::new(20.0, 10.0) && a.dist(Vec2::new(30.0, 10.0)) < 1e-9 && b.dist(Vec2::new(10.0, 10.0)) < 1e-9)));
}
// A profile chosen by a point outside its inner loop must keep that inner loop through
@@ -833,7 +833,7 @@ fn inherited_base_profile_holes_survive_every_sheet_state() {
let f = flat(&mut s);
assert_eq!(f["cutouts"].as_array().map(Vec::len), Some(1));
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap().to_string();
assert_eq!(solvecraft_io::read_dxf(&dxf).unwrap().len(), 8);
assert_eq!(solidcraft_io::read_dxf(&dxf).unwrap().len(), 8);
let mut reopened = Session::default();
run(&mut reopened, "sketch.insert_dxf", json!({"text": dxf, "name": "Blank"}));
run(&mut reopened, "sketch.finish", json!({}));
@@ -861,7 +861,7 @@ fn edge_notch_uses_the_same_material_boundary_in_solid_flat_and_dxf() {
assert_eq!(f["outline"][0].as_array().map(Vec::len), Some(8));
assert_eq!(f["cutouts"].as_array().map(Vec::len), Some(0));
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap().to_string();
assert_eq!(solvecraft_io::read_dxf(&dxf).unwrap().len(), 8);
assert_eq!(solidcraft_io::read_dxf(&dxf).unwrap().len(), 8);
let mut reopened = Session::default();
run(&mut reopened, "sketch.insert_dxf", json!({"text": dxf, "name": "Blank"}));
run(&mut reopened, "sketch.finish", json!({}));
@@ -890,8 +890,8 @@ fn inherited_round_hole_survives_flange_and_unfold_in_flipped_frames() {
let sh = s.model.state().sheets[0].clone();
let regions = sh.flat_regions().unwrap();
assert_eq!(regions[0].holes[0].segs.len(), 2);
assert!(regions[0].holes[0].segs.iter().all(|s| matches!(s, solvecraft_geom::Seg2::Arc { radius, .. } if (*radius - 5.0).abs() < 1e-9)));
let expected = regions.iter().map(solvecraft_geom::Region2::area).sum::<f64>() * 2.5;
assert!(regions[0].holes[0].segs.iter().all(|s| matches!(s, solidcraft_geom::Seg2::Arc { radius, .. } if (*radius - 5.0).abs() < 1e-9)));
let expected = regions.iter().map(solidcraft_geom::Region2::area).sum::<f64>() * 2.5;
run(&mut s, "sheet.unfold", json!({}));
assert!((volume(&mut s) - expected).abs() < 0.1);
run(&mut s, "sheet.refold", json!({}));
@@ -1028,7 +1028,7 @@ fn tiny_cut_inside_exact_circle_survives_unfold_refold_and_dxf() {
assert!((regions[0].holes[0].signed_area().abs() - 2.5e-9).abs() < 1e-13);
assert_eq!(s.model.state().bodies.len(), 1);
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap().to_string();
assert_eq!(solvecraft_io::read_dxf(&dxf).unwrap().len(), 5);
assert_eq!(solidcraft_io::read_dxf(&dxf).unwrap().len(), 5);
if stage == "sheet.unfold" && initially_flat {
run(&mut s, "sheet.refold", json!({}));
}
@@ -1070,8 +1070,8 @@ fn unrelated_box_cut_preserves_logical_ring_sheet_compound() {
assert_eq!(s.model.state().bodies.len(), 2);
let state = s.model.state();
let body = &state.bodies.iter().find(|b| b.name == sheet_name).unwrap().body;
assert!(body.contains(solvecraft_geom::Vec3::new(50.0, 30.0, 1.25)));
assert!(!body.contains(solvecraft_geom::Vec3::new(57.0, 30.0, 1.25)));
assert!(body.contains(solidcraft_geom::Vec3::new(50.0, 30.0, 1.25)));
assert!(!body.contains(solidcraft_geom::Vec3::new(57.0, 30.0, 1.25)));
}
}
@@ -1106,9 +1106,9 @@ fn specialized_sheet_cuts_respect_component_and_explicit_participants() {
fn ring_cut_preserves_retained_island_through_unfold_and_refold() {
fn material_probes(s: &Session) {
let body = &s.model.state().bodies[0].body;
assert!(body.contains(solvecraft_geom::Vec3::new(50.0, 30.0, 1.25)));
assert!(!body.contains(solvecraft_geom::Vec3::new(57.0, 30.0, 1.25)));
assert!(body.contains(solvecraft_geom::Vec3::new(65.0, 30.0, 1.25)));
assert!(body.contains(solidcraft_geom::Vec3::new(50.0, 30.0, 1.25)));
assert!(!body.contains(solidcraft_geom::Vec3::new(57.0, 30.0, 1.25)));
assert!(body.contains(solidcraft_geom::Vec3::new(65.0, 30.0, 1.25)));
}
let mut s = Session::default();
base(&mut s, 100.0, 60.0);
@@ -1126,9 +1126,9 @@ fn ring_cut_preserves_retained_island_through_unfold_and_refold() {
material_probes(&s);
assert_eq!(flat(&mut s)["outline"].as_array().map(Vec::len), Some(2));
let regions = s.model.state().sheets[0].flat_regions().unwrap();
assert!(regions.iter().any(|r| r.contains(solvecraft_geom::Vec2::new(50.0, 30.0))));
assert!(!regions.iter().any(|r| r.contains(solvecraft_geom::Vec2::new(57.0, 30.0))));
assert!(regions.iter().any(|r| r.contains(solvecraft_geom::Vec2::new(65.0, 30.0))));
assert!(regions.iter().any(|r| r.contains(solidcraft_geom::Vec2::new(50.0, 30.0))));
assert!(!regions.iter().any(|r| r.contains(solidcraft_geom::Vec2::new(57.0, 30.0))));
assert!(regions.iter().any(|r| r.contains(solidcraft_geom::Vec2::new(65.0, 30.0))));
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap().to_string();
let mut reopened = Session::default();
run(&mut reopened, "sketch.insert_dxf", json!({"text": dxf, "name": "RingBlank"}));
@@ -1144,7 +1144,7 @@ fn ring_cut_preserves_retained_island_through_unfold_and_refold() {
}
fn retained_depth_check(s: &mut Session, want: f64, z0: f64, z1: f64) {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
assert!((volume(s) - want).abs() < 1e-6, "{} vs {want}", volume(s));
let state = s.model.state();
assert_eq!(state.sheets.len(), 1);
@@ -1157,8 +1157,8 @@ fn retained_depth_check(s: &mut Session, want: f64, z0: f64, z1: f64) {
assert!((bounds.min.z - z0).abs() < 1e-8 && (bounds.max.z - z1).abs() < 1e-8, "{bounds:?}");
}
fn depth_step_reopen(s: &mut Session, suffix: &str, want: f64, inside: solvecraft_geom::Vec3, outside: solvecraft_geom::Vec3) {
let path = std::env::temp_dir().join(format!("solvecraft-depth-intersect-{suffix}-{}.step", std::process::id()));
fn depth_step_reopen(s: &mut Session, suffix: &str, want: f64, inside: solidcraft_geom::Vec3, outside: solidcraft_geom::Vec3) {
let path = std::env::temp_dir().join(format!("solidcraft-depth-intersect-{suffix}-{}.step", std::process::id()));
run(s, "file.export", json!({"path":path.to_string_lossy()}));
let mut imported = Session::default();
run(&mut imported, "doc.open", json!({"path":path.to_string_lossy()}));
@@ -1192,7 +1192,7 @@ fn dxf_outline_only(text: &str) -> String {
#[test]
fn partial_depth_intersect_retains_bottom_top_and_middle_material_through_history_and_exports() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
for (offset, z0, z1, layers) in [
(0.0, 0.0, 1.0, vec![("top", 1.0, 2.5)]),
(1.5, 1.5, 2.5, vec![("bottom", 0.0, 1.5)]),
@@ -1218,7 +1218,7 @@ fn partial_depth_intersect_retains_bottom_top_and_middle_material_through_histor
assert!(dxf.contains(&format!("POCKET_{}_Z{a:.6}_TO_{b:.6}_MM", side.to_uppercase())));
}
let outline = dxf_outline_only(&dxf);
assert_eq!(solvecraft_io::read_dxf(&outline).unwrap().len(), 4);
assert_eq!(solidcraft_io::read_dxf(&outline).unwrap().len(), 4);
let mut outline_import = Session::default();
run(&mut outline_import, "sketch.insert_dxf", json!({"text":outline,"name":"Outline"}));
run(&mut outline_import, "sketch.finish", json!({}));
@@ -1232,7 +1232,7 @@ fn partial_depth_intersect_retains_bottom_top_and_middle_material_through_histor
retained_depth_check(&mut s, 100.0, z0, z1);
assert_eq!(flat(&mut s)["pockets"], exported["pockets"]);
}
let mut reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
retained_depth_check(&mut reopened, 100.0, z0, z1);
depth_step_reopen(&mut s, &format!("offset{offset}"), 100.0, Vec3::new(25.0, 25.0, (z0 + z1) * 0.5), Vec3::new(25.0, 25.0, z1 + 0.1));
}
@@ -1240,7 +1240,7 @@ fn partial_depth_intersect_retains_bottom_top_and_middle_material_through_histor
#[test]
fn partial_depth_intersect_preserves_prior_pockets_and_actual_interval_intersections() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
let mut s = Session::default();
base(&mut s, 100.0, 60.0);
rectangle_tool(&mut s, "OldPocket", [20.0, 20.0], [25.0, 30.0]);
@@ -1267,14 +1267,14 @@ fn partial_depth_intersect_preserves_prior_pockets_and_actual_interval_intersect
assert!((volume(&mut s) - 25.0).abs() < 1e-6);
run(&mut s, "sheet.unfold", json!({}));
assert!((volume(&mut s) - 25.0).abs() < 1e-6);
let mut cold = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let mut cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!((volume(&mut cold) - 25.0).abs() < 1e-6);
depth_step_reopen(&mut s, "prior", 25.0, Vec3::new(22.0, 25.0, 1.125), Vec3::new(22.0, 25.0, 0.75));
}
#[test]
fn partial_depth_intersect_keeps_disconnected_tool_islands_and_annular_curves() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
let mut s = Session::default();
base(&mut s, 100.0, 60.0);
rectangle_tool(&mut s, "Islands", [20.0, 20.0], [30.0, 30.0]);
@@ -1316,7 +1316,7 @@ fn partial_depth_intersect_keeps_disconnected_tool_islands_and_annular_curves()
}
let dxf = run(&mut ring, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap().to_string();
assert!(dxf.contains("POCKET_TOP_Z1.000000_TO_2.500000_MM"));
assert_eq!(solvecraft_io::read_dxf(&dxf_outline_only(&dxf)).unwrap().len(), 2);
assert_eq!(solidcraft_io::read_dxf(&dxf_outline_only(&dxf)).unwrap().len(), 2);
}
#[test]
@@ -1336,7 +1336,7 @@ fn partial_depth_intersect_deletes_material_already_removed_by_a_pocket_atomical
#[test]
fn partial_depth_intersect_flipped_tool_and_transferred_sheet_keep_depth_side_and_world_pose() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
let mut flipped = Session::default();
base(&mut flipped, 100.0, 60.0);
run(&mut flipped, "sketch.create", json!({"name":"FlippedTool","plane":{"origin":[0,0,2.5],"x_dir":[1,0,0],"y_dir":[0,-1,0]}}));
@@ -1376,24 +1376,24 @@ fn partial_depth_intersect_flipped_tool_and_transferred_sheet_keep_depth_side_an
assert!(!body.contains(Vec3::new(25.0, 25.0, 2.0)));
let bounds = world.body(&name).unwrap().mesh().bounds();
assert!(before.min.dist(bounds.min) < 1e-8 && before.max.dist(bounds.max) < 1e-8);
assert!((solvecraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-6);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-6);
}
let mut reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened_world = reopened.world_state();
let body = &reopened_world.body(&name).unwrap().body;
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
assert!((solvecraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-6);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-6);
run(&mut reopened, "timeline.edit", json!({"feature":"RetainSlab","set":{"extent":{"distance":"1.5"}}}));
let world = reopened.world_state();
let body = &world.body(&name).unwrap().body;
assert!((solvecraft_kernel::measure(body).unwrap().volume - 150.0).abs() < 1e-6);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 150.0).abs() < 1e-6);
assert!(body.contains(Vec3::new(25.0, 25.0, 1.75)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 2.25)));
}
#[test]
fn partial_depth_intersect_full_gauge_in_rotated_frames_does_not_invent_pockets() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
for (angle, translate) in [(37.0, [30.0, 20.0, 10.0]), (121.0, [12345.678, 98765.432, 4321.123])] {
let mut s = Session::default();
base(&mut s, 100.0, 60.0);
@@ -1410,7 +1410,7 @@ fn partial_depth_intersect_full_gauge_in_rotated_frames_does_not_invent_pockets(
run(&mut s, "sketch.create", json!({"name":"WorldTool","plane":{"origin":[0,0,0],"x_dir":[1,0,0],"y_dir":[0,1,0]}}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[20,20],"p1":[30,30]}));
run(&mut s, "sketch.finish", json!({}));
let mut tool_session = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let mut tool_session = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
run(&mut tool_session, "solid.extrude", json!({"sketch":"WorldTool","distance":2.5,"body_name":"CapWitness"}));
let witness_state = tool_session.model.state();
let witness = &witness_state.body("CapWitness").unwrap().body;
@@ -1419,7 +1419,7 @@ fn partial_depth_intersect_full_gauge_in_rotated_frames_does_not_invent_pockets(
assert_eq!(sources.len(), witness.face_count());
let mut cap_count = 0;
for (index, source) in sources.iter().enumerate() {
if let Some(solvecraft_kernel::FaceSource::CertifiedExtrudeCap { high, plane, .. }) = source {
if let Some(solidcraft_kernel::FaceSource::CertifiedExtrudeCap { high, plane, .. }) = source {
cap_count += 1;
let current = witness.planar_face(index).unwrap();
assert!(current.normal().cross(tool_plane.normal()).len() < 1e-10);
@@ -1436,12 +1436,12 @@ fn partial_depth_intersect_full_gauge_in_rotated_frames_does_not_invent_pockets(
assert!(s.model.state().sheets[0].pockets.is_empty(), "full gauge must not acquire roundoff-depth pockets");
let world = s.world_state();
let body = &world.body(&name).unwrap().body;
assert!((solvecraft_kernel::measure(body).unwrap().volume - 250.0).abs() < 1e-5);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 250.0).abs() < 1e-5);
assert!(body.contains(Vec3::new(25.0, 25.0, 0.1)));
assert!(body.contains(Vec3::new(25.0, 25.0, 2.4)));
assert!(!body.contains(Vec3::new(10.0, 25.0, 1.0)));
}
let mut cold = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let mut cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(cold.model.state().sheets[0].pockets.is_empty());
assert!(flat(&mut cold)["pockets"].as_array().unwrap().is_empty());
run(&mut s, "timeline.edit", json!({"feature":"PostTransferRetain","set":{"extent":{"distance":"1","start_offset":"0.5"}}}));
@@ -1452,16 +1452,16 @@ fn partial_depth_intersect_full_gauge_in_rotated_frames_does_not_invent_pockets(
assert_eq!(s.model.state().sheets[0].pockets.len(), 2);
let world = s.world_state();
let body = &world.body(&name).unwrap().body;
let measured = solvecraft_kernel::measure(body).unwrap().volume;
let measured = solidcraft_kernel::measure(body).unwrap().volume;
assert!((measured - 100.0).abs() < 1e-5, "retained volume {measured}");
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.1)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 2.4)));
}
let cold = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let world = cold.world_state();
let body = &world.body(&name).unwrap().body;
assert!((solvecraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-5);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-5);
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.1)));
}
@@ -1469,7 +1469,7 @@ fn partial_depth_intersect_full_gauge_in_rotated_frames_does_not_invent_pockets(
#[test]
fn partial_depth_intersect_planar_to_object_retains_current_target_depth() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
let mut s = Session::default();
base(&mut s, 100.0, 60.0);
let name = s.model.state().sheets[0].body.clone();
@@ -1482,15 +1482,15 @@ fn partial_depth_intersect_planar_to_object_retains_current_target_depth() {
}
let state = s.model.state();
let body = &state.body(&name).unwrap().body;
assert!((solvecraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-6);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-6);
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.1)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 2.0)));
assert_eq!(state.sheets[0].pockets.len(), 2);
assert!((solvecraft_kernel::measure(&state.body("Roof").unwrap().body).unwrap().volume - 100.0).abs() < 1e-6);
assert!((solidcraft_kernel::measure(&state.body("Roof").unwrap().body).unwrap().volume - 100.0).abs() < 1e-6);
}
let cold = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!((solvecraft_kernel::measure(&cold.model.state().body(&name).unwrap().body).unwrap().volume - 100.0).abs() < 1e-6);
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!((solidcraft_kernel::measure(&cold.model.state().body(&name).unwrap().body).unwrap().volume - 100.0).abs() < 1e-6);
}
#[test]
@@ -1510,14 +1510,14 @@ fn partial_depth_intersect_curved_to_object_refuses_unrepresentable_depth_atomic
run(&mut s, command, json!({"body":name}));
}
let state = s.model.state();
assert!((solvecraft_kernel::measure(&state.body(&name).unwrap().body).unwrap().volume - 15000.0).abs() < 1e-6);
assert!((solidcraft_kernel::measure(&state.body(&name).unwrap().body).unwrap().volume - 15000.0).abs() < 1e-6);
assert!(state.sheets[0].pockets.is_empty());
}
}
#[test]
fn partial_depth_intersect_curved_to_object_preserves_current_trimmed_cap_footprint() {
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
let mut s = Session::default();
base(&mut s, 100.0, 60.0);
let name = s.model.state().sheets[0].body.clone();
@@ -1525,11 +1525,11 @@ fn partial_depth_intersect_curved_to_object_preserves_current_trimmed_cap_footpr
run(&mut s, "solid.box", json!({"corner":[19,19,1],"length":12,"width":12,"height":1,"body_name":"StopPlate"}));
run(&mut s, "solid.combine", json!({"target":"CurvedRoof","tools":["StopPlate"],"operation":"join"}));
rectangle_tool(&mut s, "Tool", [20.0, 20.0], [30.0, 30.0]);
let mut tool_session = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let mut tool_session = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
run(&mut tool_session, "solid.extrude", json!({"sketch":"Tool","to":[27,25,10],"body_name":"TrimWitness"}));
let ts = tool_session.model.state();
let witness = &ts.body("TrimWitness").unwrap().body;
assert!((solvecraft_kernel::measure(witness).unwrap().volume - (100.0 - 4.0 * std::f64::consts::PI)).abs() < 0.01);
assert!((solidcraft_kernel::measure(witness).unwrap().volume - (100.0 - 4.0 * std::f64::consts::PI)).abs() < 0.01);
assert!(witness.contains(Vec3::new(21.0, 21.0, 0.5)));
assert!(!witness.contains(Vec3::new(25.0, 25.0, 0.5)));
run(&mut s, "solid.extrude", json!({"sketch":"Tool","to":[27,25,10],"operation":"intersect","participants":[name]}));
@@ -1539,7 +1539,7 @@ fn partial_depth_intersect_curved_to_object_preserves_current_trimmed_cap_footpr
}
let state = s.model.state();
let body = &state.body(&name).unwrap().body;
let measured = solvecraft_kernel::measure(body).unwrap().volume;
let measured = solidcraft_kernel::measure(body).unwrap().volume;
assert!((measured - (100.0 - 4.0 * std::f64::consts::PI)).abs() < 0.01);
let area: f64 = state.sheets[0].flat_regions().unwrap().iter().map(|r| r.area()).sum();
assert!((area - (100.0 - 4.0 * std::f64::consts::PI)).abs() < 1e-8);
@@ -1547,19 +1547,19 @@ fn partial_depth_intersect_curved_to_object_preserves_current_trimmed_cap_footpr
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.5)));
assert!(!body.contains(Vec3::new(21.0, 21.0, 1.5)));
}
let cold = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(!cold.model.state().body(&name).unwrap().body.contains(Vec3::new(25.0, 25.0, 0.5)));
let dxf = run(&mut s, "sheet.export_dxf", json!({"body":name}))["dxf"].as_str().unwrap().to_string();
assert!(dxf.contains("POCKET_TOP_Z1.000000_TO_2.500000_MM"));
let outline = dxf_outline_only(&dxf);
assert_eq!(solvecraft_io::read_dxf(&outline).unwrap().len(), 5);
assert_eq!(solidcraft_io::read_dxf(&outline).unwrap().len(), 5);
let mut outline_import = Session::default();
run(&mut outline_import, "sketch.insert_dxf", json!({"text":outline,"name":"Outline"}));
run(&mut outline_import, "sketch.finish", json!({}));
run(&mut outline_import, "solid.extrude", json!({"distance":1,"profiles":[{"point":[21,21]}]}));
assert!((volume(&mut outline_import) - (100.0 - 4.0 * std::f64::consts::PI)).abs() < 0.01);
assert!(!outline_import.model.state().bodies[0].body.contains(Vec3::new(25.0, 25.0, 0.5)));
let path = std::env::temp_dir().join(format!("solvecraft-current-trim-cap-{}.step", std::process::id()));
let path = std::env::temp_dir().join(format!("solidcraft-current-trim-cap-{}.step", std::process::id()));
run(&mut s, "file.export", json!({"path":path.to_string_lossy(),"bodies":[name]}));
let mut imported = Session::default();
run(&mut imported, "doc.open", json!({"path":path.to_string_lossy()}));
@@ -1594,7 +1594,7 @@ fn partial_depth_intersect_sub_extrusion_resolution_layers_refuse_atomically() {
assert!(error.contains("resolution") || error.contains("distance is zero") || error.contains("sheet machining"), "{error}");
assert_eq!(s.doc.to_json(), before);
assert!(s.model.state().sheets[0].pockets.is_empty());
assert!((solvecraft_kernel::measure(&s.world_state().body(&name).unwrap().body).unwrap().volume - 15000.0).abs() < 1e-5);
assert!((solidcraft_kernel::measure(&s.world_state().body(&name).unwrap().body).unwrap().volume - 15000.0).abs() < 1e-5);
}
}
+21 -21
View File
@@ -1,9 +1,9 @@
//! Sketch commands: create/edit/finish sketches, draw curves, constraints and dimensions.
use serde_json::{Value, json};
use solvecraft_doc::{FeatureKind, PlaneRef, expr::Kind};
use solvecraft_geom::{Plane, Vec2, Vec3};
use solvecraft_sketch::{Analysis, Budget, ConstraintKind, CurveKind, Hold, Sketch, SolveReport, solve, solve_with};
use solidcraft_doc::{FeatureKind, PlaneRef, expr::Kind};
use solidcraft_geom::{Plane, Vec2, Vec3};
use solidcraft_sketch::{Analysis, Budget, ConstraintKind, CurveKind, Hold, Sketch, SolveReport, solve, solve_with};
use super::{CommandSpec, in_sketch};
use crate::params::{bad, bool_, expr, num, req_vec2, str_, string_list, vec2, vec3};
@@ -222,7 +222,7 @@ pub(super) fn edit<T>(
p: &Value,
cmd: &str,
strict: bool,
f: impl FnOnce(&mut Sketch, &mut solvecraft_doc::Document) -> Result<T>,
f: impl FnOnce(&mut Sketch, &mut solidcraft_doc::Document) -> Result<T>,
) -> Result<(T, Value)> {
let id = target_sketch(s, p, cmd)?;
let (out, rep) = edit_solved(s, id, strict, |sk, doc| Ok((f(sk, doc)?, Left::Unsolved)))?;
@@ -262,7 +262,7 @@ pub(super) fn edit_solved<T>(
s: &mut Session,
id: u64,
strict: bool,
f: impl FnOnce(&mut Sketch, &mut solvecraft_doc::Document) -> Result<(T, Left)>,
f: impl FnOnce(&mut Sketch, &mut solidcraft_doc::Document) -> Result<(T, Left)>,
) -> Result<(T, Option<SolveReport>)> {
let mut doc = (*s.doc).clone();
let mut sk = doc.sketch(id)?.clone();
@@ -333,7 +333,7 @@ pub(super) fn counted_solve(
/// Inside an `edit` closure: solve the sketch for the document's parameter values from its
/// stored solved state, as the edit's own solve does, for commands that work on the current
/// geometry.
pub(super) fn solve_current(sk: &mut Sketch, doc: &solvecraft_doc::Document) -> Result<SolveReport> {
pub(super) fn solve_current(sk: &mut Sketch, doc: &solidcraft_doc::Document) -> Result<SolveReport> {
let stored = sk.driving_values();
let (values, _) = doc.param_values();
Ok(doc.solve_sketch(&values, sk, &stored)?)
@@ -345,7 +345,7 @@ pub(super) fn solve_current(sk: &mut Sketch, doc: &solvecraft_doc::Document) ->
pub(super) mod solve_counts {
use std::cell::Cell;
use solvecraft_sketch::Analysis;
use solidcraft_sketch::Analysis;
thread_local! {
static COUNTS: Cell<(usize, usize)> = const { Cell::new((0, 0)) };
@@ -395,7 +395,7 @@ pub(super) fn draw(
s: &mut Session,
p: &Value,
cmd: &str,
f: impl FnOnce(&mut Sketch, &mut solvecraft_doc::Document) -> Result<(Vec<String>, Vec<String>)>,
f: impl FnOnce(&mut Sketch, &mut solidcraft_doc::Document) -> Result<(Vec<String>, Vec<String>)>,
) -> Result<((Vec<String>, Vec<String>), Value)> {
edit(s, p, cmd, false, |sk, doc| {
let first = sk.points.len();
@@ -497,7 +497,7 @@ pub(crate) fn created(before: Option<&Sketch>, after: &Sketch) -> Value {
let points: Vec<&str> = after.points.iter().map(|x| x.id.as_str()).filter(new).collect();
let curves: Vec<&str> = after.curves.iter().map(|x| x.id.as_str()).filter(new).collect();
let (dims, cons): (Vec<_>, Vec<_>) = after.constraints.iter().filter(|c| new(&c.id.as_str())).partition(|c| c.kind.is_dimension());
let ids = |v: Vec<&solvecraft_sketch::Constraint>| v.into_iter().map(|c| c.id.clone()).collect::<Vec<_>>();
let ids = |v: Vec<&solidcraft_sketch::Constraint>| v.into_iter().map(|c| c.id.clone()).collect::<Vec<_>>();
let mut out = json!({"points": points, "curves": curves, "constraints": ids(cons), "dimensions": ids(dims)});
let wires: Vec<&str> = after.wires.iter().map(|x| x.id.as_str()).filter(new).collect();
if !wires.is_empty()
@@ -515,7 +515,7 @@ pub(super) fn result(sk_out: (Vec<String>, Vec<String>), info: Value) -> Value {
// ---------------------------------------------------------------------------------------------
// Sketch lifecycle
pub(crate) fn author_path_for_rebind(s: &Session, feature: &solvecraft_doc::Feature, p: &Value, cmd: &str) -> Result<Vec<u64>> {
pub(crate) fn author_path_for_rebind(s: &Session, feature: &solidcraft_doc::Feature, p: &Value, cmd: &str) -> Result<Vec<u64>> {
let mut saved = None;
if let FeatureKind::Sketch { plane, .. } = &feature.kind {
let mut plane = plane;
@@ -553,7 +553,7 @@ pub(crate) fn author_path_for_rebind(s: &Session, feature: &solvecraft_doc::Feat
if feature.component != 0 && path.is_empty() {
return Err(bad(cmd, "a component sketch needs an explicit complete author_path"));
}
solvecraft_doc::EmbossFace::component_frame(&s.placed_document(), feature.component, &path)?;
solidcraft_doc::EmbossFace::component_frame(&s.placed_document(), feature.component, &path)?;
Ok(path)
}
@@ -581,7 +581,7 @@ pub(super) fn plane_ref(s: &Session, p: &Value, cmd: &str) -> Result<PlaneRef> {
}
/// Does triangle set of face `fi` contain point `p` (within `tol` of the face plane)?
pub(super) fn face_contains(m: &solvecraft_geom::Mesh, fi: usize, p: Vec3, tol: f64) -> bool {
pub(super) fn face_contains(m: &solidcraft_geom::Mesh, fi: usize, p: Vec3, tol: f64) -> bool {
m.triangles.iter().zip(&m.tri_face).filter(|(_, f)| **f as usize == fi).any(|(t, _)| {
let Some([a, b, c]) = m.tri(t) else { return false };
let n = (b - a).cross(c - a);
@@ -672,7 +672,7 @@ fn edit_sketch(s: &mut Session, p: &Value) -> Result<Value> {
_ => None,
};
if let Some(path) = authored_path {
solvecraft_doc::EmbossFace::component_frame(&s.placed_document(), component, &path)
solidcraft_doc::EmbossFace::component_frame(&s.placed_document(), component, &path)
.map_err(|error| bad("sketch.edit", format!("{error}; explicitly rebind the author placement before editing")))?;
s.active_component = component;
s.active_occurrence_path = (!path.is_empty()).then_some(path);
@@ -1282,7 +1282,7 @@ pub(super) fn apply_in_order(sk: &mut Sketch, rep: &SolveReport, first: &[Ent],
const TEMP: &str = "__keep_size";
for (h, extra) in tries {
let mut trial = sk.clone();
trial.constraints.extend(extra.into_iter().map(|kind| solvecraft_sketch::Constraint {
trial.constraints.extend(extra.into_iter().map(|kind| solidcraft_sketch::Constraint {
id: TEMP.into(),
kind,
param: None,
@@ -1764,7 +1764,7 @@ fn measured(is_angle: bool, v: f64) -> (f64, &'static str) {
fn solved_measure(s: &Session, p: &Value, cmd: &str) -> Option<(f64, &'static str)> {
let sk = s.doc.sketch(target_sketch(s, p, cmd).ok()?).ok()?;
let k = &sk.constraints.last()?.kind;
Some(measured(k.is_angle(), solvecraft_sketch::measure_dimension(sk, k)?))
Some(measured(k.is_angle(), solidcraft_sketch::measure_dimension(sk, k)?))
}
/// The type of a dimension between `a` and `b` (two points or a line's ends). `auto` takes it
@@ -1793,8 +1793,8 @@ fn auto_orientation(ty: &str, a: Vec2, b: Vec2, text_at: Option<Vec2>) -> &str {
/// dimension so it follows the geometry.
fn place_text(sk: &mut Sketch, id: &str, at: Option<Vec2>) {
let Some(i) = sk.constraints.iter().position(|c| c.id == id) else { return };
let text = match (at, sk.constraints.get(i).and_then(|c| solvecraft_sketch::dim_frame(sk, &c.kind))) {
(Some(at), Some(f)) => Some(solvecraft_sketch::encode_text(&f, at)),
let text = match (at, sk.constraints.get(i).and_then(|c| solidcraft_sketch::dim_frame(sk, &c.kind))) {
(Some(at), Some(f)) => Some(solidcraft_sketch::encode_text(&f, at)),
_ => None,
};
if let Some(c) = sk.constraints.get_mut(i) {
@@ -1827,7 +1827,7 @@ fn diameter_about(sk: &Sketch, l: usize, ty: &str) -> bool {
fn arc_len(sk: &Sketch, c: usize) -> Option<f64> {
match sk.segs(c).first()? {
solvecraft_geom::Seg2::Arc { radius, sweep, .. } if matches!(sk.curves.get(c)?.kind, CurveKind::Arc { .. }) => Some(radius * sweep),
solidcraft_geom::Seg2::Arc { radius, sweep, .. } if matches!(sk.curves.get(c)?.kind, CurveKind::Arc { .. }) => Some(radius * sweep),
_ => None,
}
}
@@ -1948,7 +1948,7 @@ fn sketch_delete(s: &mut Session, p: &Value) -> Result<Value> {
&& topology
.roles
.iter()
.any(|role| !role.retired && role.kind == solvecraft_sketch::OffsetRoleKind::Arc && !removed.contains(role.output.as_str()))
.any(|role| !role.retired && role.kind == solidcraft_sketch::OffsetRoleKind::Arc && !removed.contains(role.output.as_str()))
{
return Err(bad(cmd, "break the offset association before deleting only part of its curved boundary"));
}
@@ -2011,7 +2011,7 @@ fn drag_frame(s: &mut Session, p: &Value, live: bool) -> Result<(Value, Option<c
let sk = s.doc.sketch(id)?;
let moves: Vec<Vec2> = d.moving.iter().filter_map(|&(q, was)| Some(sk.point(q)? - was)).collect();
let at = from + moves.iter().fold(Vec2::ZERO, |a, m| a + *m) * (1.0 / moves.len().max(1) as f64);
let rep = rep.or_else(|| last.map(|last| SolveReport { status: solvecraft_sketch::SolveStatus::Solved, failing: Vec::new(), ..last }));
let rep = rep.or_else(|| last.map(|last| SolveReport { status: solidcraft_sketch::SolveStatus::Solved, failing: Vec::new(), ..last }));
let out = json!({"sketch": rep.as_ref().map(report_json), "reached": d.reached, "at": [at.x, at.y]});
Ok((out, rep.filter(|_| live).map(|report| crate::LiveEdit { sketch: id, report })))
}
@@ -2048,7 +2048,7 @@ fn drag_end(s: &mut Session, p: &Value) -> Result<Value> {
}
/// The Sketch Palette options of a sketch, by their palette names (true = shown / on).
fn options_json(v: &solvecraft_sketch::SketchView) -> Value {
fn options_json(v: &solidcraft_sketch::SketchView) -> Value {
json!({
"show_profile": !v.hide_profile,
"show_points": !v.hide_points,
+14 -14
View File
@@ -3,8 +3,8 @@
//! drawn and snapped to, movable point by point, and usable as sweep and pipe paths.
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solvecraft_sketch::WireSpace;
use solidcraft_geom::Vec3;
use solidcraft_sketch::WireSpace;
use super::sketch::{edit, target_sketch};
use super::{CommandSpec, in_sketch};
@@ -29,26 +29,26 @@ pub static COMMANDS: &[CommandSpec] = &[
fn points(cmd: &str, p: &Value, min: usize) -> Result<Vec<Vec3>> {
let a = p.get("points").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`points` must list [x,y,z] points"))?;
if a.len() < min || a.len() > solvecraft_sketch::MAX_FIT_POINTS {
return Err(bad(cmd, format!("`points` needs {min} to {} points", solvecraft_sketch::MAX_FIT_POINTS)));
if a.len() < min || a.len() > solidcraft_sketch::MAX_FIT_POINTS {
return Err(bad(cmd, format!("`points` needs {min} to {} points", solidcraft_sketch::MAX_FIT_POINTS)));
}
a.iter().map(|v| vec3(v).ok_or_else(|| bad(cmd, "a point must be [x, y, z]"))).collect()
}
fn needs_3d(sk: &solvecraft_sketch::Sketch, cmd: &str) -> Result<()> {
fn needs_3d(sk: &solidcraft_sketch::Sketch, cmd: &str) -> Result<()> {
if sk.view.three_d { Ok(()) } else { Err(bad(cmd, "turn on 3D Sketch (sketch.options sketch_3d: true) to draw off the sketch plane")) }
}
/// World command inputs belong to the target sketch's owner, which may differ from the
/// active component when a script supplies an explicit sketch.
fn inverse_owner_frame(s: &Session, p: &Value, cmd: &str) -> Result<solvecraft_doc::Mat> {
fn inverse_owner_frame(s: &Session, p: &Value, cmd: &str) -> Result<solidcraft_doc::Mat> {
let id = target_sketch(s, p, cmd)?;
let owner = s.doc.feature(id).map(|f| f.component).ok_or_else(|| bad(cmd, "target sketch is missing"))?;
if owner != 0 && owner != s.active_component {
if s.doc.occurrences.len() > 100_000 {
return Err(bad(cmd, "sketch owner placement expansion exceeds supported bounds"));
}
let mut children: std::collections::BTreeMap<u64, Vec<&solvecraft_doc::Occurrence>> = std::collections::BTreeMap::new();
let mut children: std::collections::BTreeMap<u64, Vec<&solidcraft_doc::Occurrence>> = std::collections::BTreeMap::new();
for occurrence in &s.doc.occurrences {
children.entry(occurrence.parent).or_default().push(occurrence);
}
@@ -72,13 +72,13 @@ fn inverse_owner_frame(s: &Session, p: &Value, cmd: &str) -> Result<solvecraft_d
}
}
let frame = if owner == s.active_component { s.active_component_frame() } else { s.placed_document().component_transform(owner) };
solvecraft_doc::mat_inverse(&frame).ok_or_else(|| bad(cmd, "the sketch owner placement is not invertible"))
solidcraft_doc::mat_inverse(&frame).ok_or_else(|| bad(cmd, "the sketch owner placement is not invertible"))
}
fn line3d(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.line3d";
let inverse = inverse_owner_frame(s, p, cmd)?;
let pts: Vec<Vec3> = points(cmd, p, 2)?.into_iter().map(|at| solvecraft_doc::apply_point(&inverse, at)).collect();
let pts: Vec<Vec3> = points(cmd, p, 2)?.into_iter().map(|at| solidcraft_doc::apply_point(&inverse, at)).collect();
let (ids, info) = edit(s, p, cmd, false, |sk, _| {
needs_3d(sk, cmd)?;
pts.windows(2).map(|w| sk.add_drawn_wire_in(w, WireSpace::Component).map_err(Into::into)).collect::<Result<Vec<String>>>()
@@ -89,7 +89,7 @@ fn line3d(s: &mut Session, p: &Value) -> Result<Value> {
fn spline3d(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.spline3d";
let inverse = inverse_owner_frame(s, p, cmd)?;
let pts: Vec<Vec3> = points(cmd, p, 2)?.into_iter().map(|at| solvecraft_doc::apply_point(&inverse, at)).collect();
let pts: Vec<Vec3> = points(cmd, p, 2)?.into_iter().map(|at| solidcraft_doc::apply_point(&inverse, at)).collect();
let (id, info) = edit(s, p, cmd, false, |sk, _| {
needs_3d(sk, cmd)?;
Ok(sk.add_drawn_wire_in(&pts, WireSpace::Component)?)
@@ -100,7 +100,7 @@ fn spline3d(s: &mut Session, p: &Value) -> Result<Value> {
fn point3d(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.point3d";
let inverse = inverse_owner_frame(s, p, cmd)?;
let at = solvecraft_doc::apply_point(&inverse, req_vec3(cmd, p, "point")?);
let at = solidcraft_doc::apply_point(&inverse, req_vec3(cmd, p, "point")?);
let (id, info) = edit(s, p, cmd, false, |sk, _| {
needs_3d(sk, cmd)?;
Ok(sk.add_drawn_wire_in(&[at], WireSpace::Component)?)
@@ -116,7 +116,7 @@ fn move3d(s: &mut Session, p: &Value) -> Result<Value> {
Some(v) => v
.as_u64()
.and_then(|v| usize::try_from(v).ok())
.filter(|v| *v < solvecraft_sketch::MAX_FIT_POINTS)
.filter(|v| *v < solidcraft_sketch::MAX_FIT_POINTS)
.ok_or_else(|| bad(cmd, "`index` must be a fit point index below 500"))?,
};
let parse = |name| p.get(name).map(|v| vec3(v).ok_or_else(|| bad(cmd, format!("`{name}` must be [x,y,z]")))).transpose();
@@ -133,12 +133,12 @@ fn move3d(s: &mut Session, p: &Value) -> Result<Value> {
let to = match (to, by) {
(Some(t), _) => {
if drawn.space == WireSpace::Component {
solvecraft_doc::apply_point(&inverse, t)
solidcraft_doc::apply_point(&inverse, t)
} else {
t
}
}
(None, Some(d)) => from + if drawn.space == WireSpace::Component { solvecraft_doc::apply_vector(&inverse, d) } else { d },
(None, Some(d)) => from + if drawn.space == WireSpace::Component { solidcraft_doc::apply_vector(&inverse, d) } else { d },
_ => return Err(bad(cmd, "give `to` or `by`")),
};
Ok(sk.move_drawn_point(&wire, index, to)?)
+15 -15
View File
@@ -1,7 +1,7 @@
use crate::Session;
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solvecraft_sketch::WireSpace;
use solidcraft_geom::Vec3;
use solidcraft_sketch::WireSpace;
fn run(s: &mut Session, cmd: &str, params: Value) -> Value {
s.execute(cmd, &params).unwrap_or_else(|e| panic!("{cmd}: {e}"))
@@ -57,7 +57,7 @@ fn wire_frame_second_occurrence_world_inputs_move_and_reopen() {
run(&mut s, "edit.redo", json!({}));
run(&mut s, "sketch.move3d", json!({"wire":point,"to":[18,33,46]}));
near(s.doc.sketch(sketch).unwrap().wires.iter().find(|w| w.id == point).unwrap().fit[0], Vec3::new(3.0, 2.0, 6.0));
let restored = solvecraft_doc::format::read(&s.doc.to_json()).unwrap();
let restored = solidcraft_doc::format::read(&s.doc.to_json()).unwrap();
assert_eq!(restored.sketch(sketch).unwrap().wires, s.doc.sketch(sketch).unwrap().wires);
// Same definition at the first placement has the local geometry; selected second is exact.
run(&mut s, "component.activate", json!({"component":component,"occurrence":first,"path":[first]}));
@@ -144,18 +144,18 @@ fn wire_frame_include_other_component_refresh_break_lost_and_reopen() {
run(&mut s, "sketch.break_link", json!({"link":link}));
assert_eq!(s.doc.sketch(sketch).unwrap().wires[0].space, WireSpace::Component);
assert!(s.doc.sketch(sketch).unwrap().wires[0].link.is_none());
let restored = solvecraft_doc::format::read(&s.doc.to_json()).unwrap();
let restored = solidcraft_doc::format::read(&s.doc.to_json()).unwrap();
assert_eq!(restored.sketch(sketch).unwrap().wires[0], s.doc.sketch(sketch).unwrap().wires[0]);
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.to_json(), before);
// Lost references retain the last coordinates and explicit marker, with no guessing.
let sk = s.doc_mut().sketch_mut(sketch).unwrap();
sk.links.iter_mut().find(|l| l.id == link).unwrap().source = solvecraft_sketch::LinkSource::Edge { body: "Missing".into(), at: Vec3::ZERO };
sk.links.iter_mut().find(|l| l.id == link).unwrap().source = solidcraft_sketch::LinkSource::Edge { body: "Missing".into(), at: Vec3::ZERO };
run(&mut s, "sketch.line", json!({"points":[[2,0],[3,0]]}));
let sk = s.doc.sketch(sketch).unwrap();
assert!(sk.link(&link).unwrap().lost);
assert_eq!(sk.wires[0], initial);
let restored = solvecraft_doc::format::read(&s.doc.to_json()).unwrap();
let restored = solidcraft_doc::format::read(&s.doc.to_json()).unwrap();
assert_eq!(restored.sketch(sketch).unwrap().wires[0], initial);
}
@@ -180,7 +180,7 @@ fn wire_frame_pipe_component_and_world_paths_have_analytic_volume_and_position()
let body = world.bodies.last().unwrap();
// Property measurement uses fine adaptive tessellation; the viewport mesh alone
// undershoots a small circular profile by about 0.64% at its display chord tolerance.
let measure = solvecraft_kernel::measure(&body.body).unwrap();
let measure = solidcraft_kernel::measure(&body.body).unwrap();
assert!((measure.volume - 40.0 * std::f64::consts::PI).abs() < 0.05, "{}", measure.volume);
let bounds = body.mesh().bounds();
near((bounds.min + bounds.max) / 2.0, Vec3::new(17.0, 32.0, 50.0));
@@ -212,17 +212,17 @@ fn wire_frame_wide_owner_graph_has_bounded_indexed_traversal() {
// A legal wide assembly used to force one whole occurrence scan for each leaf.
let doc = s.doc_mut();
for i in 0..9998u64 {
doc.components.push(solvecraft_doc::Component { id: 10_000 + i, name: format!("Sibling{i}"), parent: 0 });
doc.occurrences.push(solvecraft_doc::Occurrence {
doc.components.push(solidcraft_doc::Component { id: 10_000 + i, name: format!("Sibling{i}"), parent: 0 });
doc.occurrences.push(solidcraft_doc::Occurrence {
id: 100_000 + i,
component: 10_000 + i,
parent: 0,
name: format!("Sibling{i}"),
transform: solvecraft_doc::IDENTITY,
transform: solidcraft_doc::IDENTITY,
grounded: false,
});
}
assert_eq!(super::inverse_owner_frame(&s, &json!({"sketch":target}), "sketch.point3d").unwrap(), solvecraft_doc::IDENTITY);
assert_eq!(super::inverse_owner_frame(&s, &json!({"sketch":target}), "sketch.point3d").unwrap(), solidcraft_doc::IDENTITY);
}
#[test]
@@ -241,7 +241,7 @@ fn wire_frame_cross_owner_sweep_follows_world_path_edit_undo_and_reopen() {
let check = |s: &Session, length: f64| {
let world = s.world_state();
let body = world.bodies.last().unwrap();
let measure = solvecraft_kernel::measure(&body.body).unwrap();
let measure = solidcraft_kernel::measure(&body.body).unwrap();
assert!((measure.volume - 4.0 * std::f64::consts::PI * length).abs() < 0.05, "{}", measure.volume);
near(measure.centroid, Vec3::new(17.0, 32.0, 45.0 + length / 2.0));
};
@@ -253,7 +253,7 @@ fn wire_frame_cross_owner_sweep_follows_world_path_edit_undo_and_reopen() {
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.to_json(), saved);
check(&s, 10.0);
let mut reopened = Session { doc: solvecraft_doc::format::read(&saved).unwrap().into(), ..Default::default() };
let mut reopened = Session { doc: solidcraft_doc::format::read(&saved).unwrap().into(), ..Default::default() };
reopened.refresh();
check(&reopened, 10.0);
}
@@ -284,7 +284,7 @@ fn wire_frame_three_owner_sweep_profile_move_invalidates_warm_cache() {
let state = session.world_state();
let body = state.bodies.last().unwrap();
assert_eq!(body.component, consumer["component"].as_u64().unwrap());
let result = solvecraft_kernel::measure(&body.body).unwrap();
let result = solidcraft_kernel::measure(&body.body).unwrap();
assert!((result.volume - 40.0 * std::f64::consts::PI).abs() < 0.05, "{}", result.volume);
near(result.centroid, Vec3::new(x, 7.0, 10.0));
result
@@ -301,7 +301,7 @@ fn wire_frame_three_owner_sweep_profile_move_invalidates_warm_cache() {
assert_eq!(s.doc.component_transform(path_owner["component"].as_u64().unwrap()), fixed_path);
assert_eq!(s.doc.component_transform(consumer["component"].as_u64().unwrap()), fixed_consumer);
let warm = measure(&s, 7.0);
let mut cold = Session { doc: solvecraft_doc::format::read(&s.doc.to_json()).unwrap().into(), ..Default::default() };
let mut cold = Session { doc: solidcraft_doc::format::read(&s.doc.to_json()).unwrap().into(), ..Default::default() };
cold.refresh();
let reopened = measure(&cold, 7.0);
near(warm.centroid, reopened.centroid);
+5 -5
View File
@@ -1,8 +1,8 @@
//! Canvases (reference images on planes) and decals (images on planar faces).
use serde_json::{Value, json};
use solvecraft_doc::canvas::{Canvas, MAX_CANVAS_BYTES, base64_decode, base64_encode, image_info};
use solvecraft_geom::Vec2;
use solidcraft_doc::canvas::{Canvas, MAX_CANVAS_BYTES, base64_decode, base64_encode, image_info};
use solidcraft_geom::Vec2;
use super::CommandSpec;
use crate::params::{bad, bool_, num, str_, vec2};
@@ -31,7 +31,7 @@ fn image_bytes(p: &Value, cmd: &str) -> Result<Vec<u8>> {
base64_decode(d).ok_or_else(|| bad(cmd, "`data` is not base64"))?
} else {
let path = str_(p, "path").filter(|x| !x.trim().is_empty() && x.len() < 4096).ok_or_else(|| bad(cmd, "give `path` or `data`"))?;
solvecraft_io::vfs::read(path, MAX_CANVAS_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?
solidcraft_io::vfs::read(path, MAX_CANVAS_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?
};
if b.len() > MAX_CANVAS_BYTES {
return Err(bad(cmd, "the image is too large"));
@@ -158,7 +158,7 @@ fn delete_canvas(s: &mut Session, p: &Value) -> Result<Value> {
#[cfg(test)]
mod tests {
use serde_json::json;
use solvecraft_doc::canvas::base64_encode;
use solidcraft_doc::canvas::base64_encode;
use crate::Session;
@@ -183,7 +183,7 @@ mod tests {
assert!((s.doc.canvases[0].width - 200.0).abs() < 1e-9);
s.execute("canvas.edit", &json!({"canvas": "Canvas1", "opacity": 0.8, "angle": 90})).unwrap();
// Undo-able and saved with the design.
let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let back = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(back.canvases, s.doc.canvases);
s.execute("canvas.delete", &json!({"canvas": id})).unwrap();
assert!(s.doc.canvases.is_empty());
+8 -8
View File
@@ -2,8 +2,8 @@
//! inference for interactive tools.
use serde_json::{Value, json};
use solvecraft_geom::Vec2;
use solvecraft_sketch::{ConstraintKind, CurveKind, Sketch};
use solidcraft_geom::Vec2;
use solidcraft_sketch::{ConstraintKind, CurveKind, Sketch};
use super::sketch::{add_c, dof_now, edit, reject_redundant};
use super::{CommandSpec, in_sketch};
@@ -120,7 +120,7 @@ const MAX_TRIALS: usize = 2000;
/// Add `k` if it removes a degree of freedom and the sketch still solves; else leave the
/// sketch as it was. Returns the new dof when kept.
fn try_add(sk: &mut Sketch, doc: &mut solvecraft_doc::Document, k: ConstraintKind, dof: usize, dim: bool) -> Option<usize> {
fn try_add(sk: &mut Sketch, doc: &mut solidcraft_doc::Document, k: ConstraintKind, dof: usize, dim: bool) -> Option<usize> {
let saved = sk.clone();
let param = if dim {
let v = k.value()?;
@@ -136,7 +136,7 @@ fn try_add(sk: &mut Sketch, doc: &mut solvecraft_doc::Document, k: ConstraintKin
return None;
}
let mut trial = sk.clone();
let rep = solvecraft_sketch::solve(&mut trial);
let rep = solidcraft_sketch::solve(&mut trial);
if rep.ok() && rep.dof < dof {
*sk = trial;
return Some(rep.dof);
@@ -148,7 +148,7 @@ fn try_add(sk: &mut Sketch, doc: &mut solvecraft_doc::Document, k: ConstraintKin
None
}
fn run_auto(sk: &mut Sketch, doc: &mut solvecraft_doc::Document, p: &Value, datum: usize) -> (Vec<String>, usize) {
fn run_auto(sk: &mut Sketch, doc: &mut solidcraft_doc::Document, p: &Value, datum: usize) -> (Vec<String>, usize) {
use ConstraintKind::*;
let tol = num(p, "tolerance").filter(|t| *t > 0.0).unwrap_or(0.01);
let atol = num(p, "angle_tolerance").filter(|t| *t > 0.0).unwrap_or(1.0).to_radians();
@@ -156,7 +156,7 @@ fn run_auto(sk: &mut Sketch, doc: &mut solvecraft_doc::Document, p: &Value, datu
let n0 = sk.constraints.len();
let mut dof = dof_now(sk);
let mut trials = 0;
let mut attempt = |sk: &mut Sketch, doc: &mut solvecraft_doc::Document, k: ConstraintKind, dim: bool, dof: &mut usize| {
let mut attempt = |sk: &mut Sketch, doc: &mut solidcraft_doc::Document, k: ConstraintKind, dim: bool, dof: &mut usize| {
if *dof == 0 || trials >= MAX_TRIALS {
return;
}
@@ -284,7 +284,7 @@ fn guess(sk: &Sketch, ents: &[String], cmd: &str) -> Result<ConstraintKind> {
sk.resolve_point(r).map(|p| (Some(p), None)).ok_or_else(|| bad(cmd, format!("unknown sketch entity `{r}`")))
};
let line_dir = |c: usize| match sk.shape(c) {
Some(solvecraft_sketch::Shape::Line { a, b }) => (b - a).normalized(),
Some(solidcraft_sketch::Shape::Line { a, b }) => (b - a).normalized(),
_ => None,
};
let round = |c: usize| matches!(sk.curves.get(c).map(|c| &c.kind), Some(CurveKind::Circle { .. } | CurveKind::Arc { .. }));
@@ -313,7 +313,7 @@ fn guess(sk: &Sketch, ents: &[String], cmd: &str) -> Result<ConstraintKind> {
if cross < 0.2 {
// Parallel; on one line → collinear.
let off = match (sk.shape(x), sk.shape(y)) {
(Some(solvecraft_sketch::Shape::Line { a, .. }), Some(solvecraft_sketch::Shape::Line { a: b0, .. })) => {
(Some(solidcraft_sketch::Shape::Line { a, .. }), Some(solidcraft_sketch::Shape::Line { a: b0, .. })) => {
u.cross(b0 - a).abs()
}
_ => f64::INFINITY,
@@ -1,5 +1,5 @@
use serde_json::{Value, json};
use solvecraft_geom::Vec2;
use solidcraft_geom::Vec2;
use crate::Session;
@@ -20,7 +20,7 @@ fn new_sketch() -> Session {
s
}
fn sketch(s: &Session) -> solvecraft_sketch::Sketch {
fn sketch(s: &Session) -> solidcraft_sketch::Sketch {
s.model.state().sketch(s.active_sketch.unwrap()).unwrap().sketch.clone()
}
@@ -97,7 +97,7 @@ fn curvature_polygon_arc_length_linear_diameter() {
let sk = sketch(&s);
let ai = sk.curve_index(&a).unwrap();
let len = match sk.segs(ai)[0] {
solvecraft_geom::Seg2::Arc { radius, sweep, .. } => radius * sweep,
solidcraft_geom::Seg2::Arc { radius, sweep, .. } => radius * sweep,
_ => 0.0,
};
assert!((len - 20.0).abs() < 1e-6, "{len}");
+4 -4
View File
@@ -23,7 +23,7 @@ fn curves(r: &Value) -> Vec<String> {
r["curves"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default()
}
fn sk(s: &Session) -> solvecraft_sketch::Sketch {
fn sk(s: &Session) -> solidcraft_sketch::Sketch {
s.model.state().sketch(s.active_sketch.unwrap()).unwrap().sketch.clone()
}
@@ -132,9 +132,9 @@ fn a_fillet_keeps_the_dimensions_of_its_corner_on_a_virtual_sharp() {
// The virtual corner sits where the corner was, and the part keeps its size.
let k = sk(&s);
let vc = k.point(k.point_index(&corner).unwrap()).unwrap();
assert!(vc.dist(solvecraft_geom::Vec2::new(0.0, 100.0)) < 1e-6, "{vc:?}");
assert!(vc.dist(solidcraft_geom::Vec2::new(0.0, 100.0)) < 1e-6, "{vc:?}");
let top_end = k.point(k.resolve_point("top.end").unwrap()).unwrap();
assert!(top_end.dist(solvecraft_geom::Vec2::new(-80.0, 100.0)) < 1e-6, "{top_end:?}");
assert!(top_end.dist(solidcraft_geom::Vec2::new(-80.0, 100.0)) < 1e-6, "{top_end:?}");
let (lo, hi) = k.bounds().unwrap();
assert!((hi.x - lo.x - 80.0).abs() < 1e-6 && (hi.y - lo.y - 100.0).abs() < 1e-6, "{lo:?} {hi:?}");
// It is drawn with extension lines from both lines' ends to it.
@@ -150,7 +150,7 @@ fn a_fillet_keeps_the_dimensions_of_its_corner_on_a_virtual_sharp() {
assert!((hi.x - lo.x - 90.0).abs() < 1e-6 && (hi.y - lo.y - 120.0).abs() < 1e-6, "{lo:?} {hi:?}");
// It is kept in the design file, and undo and redo take it away and bring it back.
let id = s.active_sketch.unwrap();
let doc = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let doc = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(doc.sketch(id).unwrap(), s.doc.sketch(id).unwrap());
run(&mut s, "edit.undo", json!({}));
run(&mut s, "edit.undo", json!({}));
+9 -9
View File
@@ -2,8 +2,8 @@
//! arc slots.
use serde_json::{Value, json};
use solvecraft_geom::Vec2;
use solvecraft_sketch::{ConstraintKind, CurveKind, LinkKind, LinkSource, Sketch};
use solidcraft_geom::Vec2;
use solidcraft_sketch::{ConstraintKind, CurveKind, LinkKind, LinkSource, Sketch};
use super::sketch::{add_c, circumcircle, edit, ids_of, mark_construction, req_parg, result, slot};
use super::{CommandSpec, in_sketch};
@@ -92,7 +92,7 @@ fn text_source(p: &Value, cmd: &str, old: Option<(String, Vec2, f64, f64)>) -> R
if text.trim().is_empty() {
return Err(bad(cmd, "`text` must not be empty"));
}
if text.chars().count() > solvecraft_sketch::MAX_TEXT_CHARS {
if text.chars().count() > solidcraft_sketch::MAX_TEXT_CHARS {
return Err(bad(cmd, "text too long"));
}
let at = p.get("at").and_then(crate::params::vec2).unwrap_or(at0);
@@ -136,7 +136,7 @@ fn edit_text(s: &mut Session, p: &Value) -> Result<Value> {
};
let src = text_source(p, cmd, Some(old))?;
let LinkSource::Text { text, at, height, angle } = &src else { return Err(bad(cmd, "text")) };
let geom = solvecraft_sketch::text_geometry(text, *at, *height, *angle)?;
let geom = solidcraft_sketch::text_geometry(text, *at, *height, *angle)?;
sk.set_link_source(&l, src.clone());
sk.update_link(&l, &geom)?;
let curves = ids_of(&sk, &sk.link_curves(&l));
@@ -231,7 +231,7 @@ fn ellipse(s: &mut Session, p: &Value) -> Result<Value> {
fn point_list(sk: &mut Sketch, p: &Value, cmd: &str) -> Result<Vec<usize>> {
let list = p.get("points").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`points` must be a list"))?;
if list.len() < 2 || list.len() > solvecraft_sketch::MAX_SPLINE_POINTS {
if list.len() < 2 || list.len() > solidcraft_sketch::MAX_SPLINE_POINTS {
return Err(bad(cmd, "a spline needs 2 to 500 points"));
}
let mut out = Vec::new();
@@ -359,9 +359,9 @@ fn leaving_dir(sk: &Sketch, ci: usize, pi: usize) -> Option<Vec2> {
CurveKind::Spline { ref pts, control, periodic, .. } => {
let q: Vec<Vec2> = pts.iter().filter_map(|i| sk.point(*i)).collect();
if pts.last() == Some(&pi) {
solvecraft_sketch::spline_end_tangent_mode(&q, control, periodic, true)
solidcraft_sketch::spline_end_tangent_mode(&q, control, periodic, true)
} else if pts.first() == Some(&pi) {
solvecraft_sketch::spline_end_tangent_mode(&q, control, periodic, false).map(|t| -t)
solidcraft_sketch::spline_end_tangent_mode(&q, control, periodic, false).map(|t| -t)
} else {
None
}
@@ -441,7 +441,7 @@ fn tangent_circle(s: &mut Session, p: &Value, cmd: &str, n: usize, radius: Optio
let (out, info) = edit(s, p, cmd, true, |sk, doc| {
let cs = tangent_curves(sk, p, cmd, n)?;
let r0 = match &radius {
Some(e) => doc.eval(e, solvecraft_doc::expr::Kind::Length).map_err(|e| bad(cmd, format!("radius: {e}")))?,
Some(e) => doc.eval(e, solidcraft_doc::expr::Kind::Length).map_err(|e| bad(cmd, format!("radius: {e}")))?,
None => guess_radius(sk, &cs, near),
};
if !(r0 > 1e-9 && r0 < 1e8) {
@@ -564,7 +564,7 @@ pub(super) fn solve_new_only(sk: &mut Sketch, first_new: usize) {
p.fixed = true;
}
}
if solvecraft_sketch::solve(&mut trial).ok() {
if solidcraft_sketch::solve(&mut trial).ok() {
for (p, t) in sk.points.iter_mut().zip(&trial.points).skip(first_new) {
p.pos = t.pos;
}
+1 -1
View File
@@ -1,6 +1,6 @@
//! Shape creation and its typed driving dimensions form one command and one undo step.
use serde_json::{Value, json};
use solvecraft_doc::expr::Kind;
use solidcraft_doc::expr::Kind;
use crate::{
Result, Session,
@@ -1,6 +1,6 @@
use crate::Session;
use serde_json::{Value, json};
use solvecraft_sketch::ConstraintKind;
use solidcraft_sketch::ConstraintKind;
fn run(s: &mut Session, command: &str, params: Value) -> Value {
s.execute(command, &params).unwrap_or_else(|error| panic!("{command} {params}: {error}"))
@@ -10,7 +10,7 @@ fn session() -> Session {
run(&mut s, "sketch.create", json!({"plane":"XY"}));
s
}
fn sketch(s: &Session) -> solvecraft_sketch::Sketch {
fn sketch(s: &Session) -> solidcraft_sketch::Sketch {
s.model.state().sketch(s.active_sketch.unwrap()).unwrap().sketch.clone()
}
fn draw(s: &mut Session, command: &str, params: Value, dimensions: Value) -> Value {
@@ -22,7 +22,7 @@ fn close(a: f64, b: f64) {
fn measure(s: &Session, kind: fn(&ConstraintKind) -> bool) -> f64 {
let sk = sketch(s);
let c = sk.constraints.iter().find(|c| kind(&c.kind)).unwrap();
solvecraft_sketch::measure_dimension(&sk, &c.kind).unwrap()
solidcraft_sketch::measure_dimension(&sk, &c.kind).unwrap()
}
#[test]
@@ -41,7 +41,7 @@ fn typed_line_angle_survives_quadrants_move_reopen_and_one_undo() {
close(measure(&s, |k| matches!(k, ConstraintKind::Length { .. })), 10.0);
}
let encoded = serde_json::to_value(&*s.doc).unwrap();
let doc: solvecraft_doc::Document = serde_json::from_value(encoded).unwrap();
let doc: solidcraft_doc::Document = serde_json::from_value(encoded).unwrap();
let mut reopened = Session { doc: std::sync::Arc::new(doc), active_sketch: s.active_sketch, ..Session::default() };
reopened.refresh();
close(measure(&reopened, |k| matches!(k, ConstraintKind::LineAngle { .. })), 90f64.to_radians());
@@ -161,7 +161,7 @@ fn dimensioned_authoring_uses_selected_occurrence_and_keeps_local_geometry() {
assert_eq!(s.active_occurrence_path, Some(vec![second]));
let sk = sketch(&s);
let line = sk.curve_index(out["curves"][0].as_str().unwrap()).unwrap();
let solvecraft_sketch::CurveKind::Line { a, b } = sk.curves[line].kind else { panic!("line") };
let solidcraft_sketch::CurveKind::Line { a, b } = sk.curves[line].kind else { panic!("line") };
close(sk.points[a].pos.x, 0.0);
close(sk.points[b].pos.x, 5.0 / std::f64::consts::SQRT_2);
}
+2 -2
View File
@@ -14,8 +14,8 @@
use std::f64::consts::{PI, TAU};
use solvecraft_geom::Vec2;
use solvecraft_sketch::{Analysis, Budget, ConstraintKind, CurveKind, Hold, Sketch};
use solidcraft_geom::Vec2;
use solidcraft_sketch::{Analysis, Budget, ConstraintKind, CurveKind, Hold, Sketch};
use super::{Ent, Left, counted_solve};
+11 -11
View File
@@ -2,7 +2,7 @@
//! rank analysis, and evaluates nothing after it; the gesture's end evaluates the model once.
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use super::solve_counts;
use crate::Session;
@@ -49,12 +49,12 @@ fn body_contains(s: &Session, p: Vec3) -> bool {
s.model.state().bodies.first().expect("a body").body.contains(p)
}
fn pt(s: &Session, id: &str) -> solvecraft_geom::Vec2 {
fn pt(s: &Session, id: &str) -> solidcraft_geom::Vec2 {
let sk = s.active_sketch.and_then(|i| s.doc.sketch(i).ok()).expect("sketch mode");
sk.resolve_point(id).and_then(|i| sk.point(i)).expect("the point")
}
fn corner(s: &Session) -> solvecraft_geom::Vec2 {
fn corner(s: &Session) -> solidcraft_geom::Vec2 {
let id = s.active_sketch.expect("sketch mode");
let st = s.world_state();
let sk = &st.sketch(id).expect("the shown sketch").sketch;
@@ -73,7 +73,7 @@ fn a_live_frame_solves_once_and_leaves_the_features_after_the_sketch_stale() {
assert_eq!(r["sketch"]["solved"], json!(true));
assert_eq!(r["sketch"]["dof"], json!(2), "the degrees of freedom from before the drag");
// The sketch shows its new place; the extrude keeps its last result.
assert!(corner(&s).dist(solvecraft_geom::Vec2::new(4.0, 5.0)) < 1e-9);
assert!(corner(&s).dist(solidcraft_geom::Vec2::new(4.0, 5.0)) < 1e-9);
let result = s.model.result(extrude).expect("the extrude's result");
assert!(result.stale);
assert!(s.model.state().bodies.first().expect("a body").body.same(&body), "the body is not rebuilt");
@@ -100,7 +100,7 @@ fn the_drag_end_evaluates_once_to_the_scripted_drags_result() {
assert!(live.model.result(extrude).is_some_and(|r| !r.stale && r.error.is_none()));
// One undo step for the gesture is the UI's business; each frame is still undoable.
run(&mut live, "edit.undo", json!({}));
assert!(corner(&live).dist(solvecraft_geom::Vec2::new(3.0, 5.0)) < 1e-9);
assert!(corner(&live).dist(solidcraft_geom::Vec2::new(3.0, 5.0)) < 1e-9);
}
#[test]
@@ -150,7 +150,7 @@ fn a_drag_reports_how_far_the_geometry_followed() {
assert_eq!(r["reached"], json!(0.0), "{r}");
let moved = pt(&s, "l1.end") - before;
let at = crate::params::vec2(&r["at"]).expect("`at`");
assert!(at.dist(solvecraft_geom::Vec2::new(25.0, 10.0) + moved) < 1e-9 && at.y < 20.0, "{r}");
assert!(at.dist(solidcraft_geom::Vec2::new(25.0, 10.0) + moved) < 1e-9 && at.y < 20.0, "{r}");
let to = 40 + 10 * usize::from(live);
let r = run(&mut s, "sketch.drag", json!({"entity": "l2", "from": [25, to - 10], "to": [25, to]}));
assert_eq!((&r["reached"], &r["at"]), (&json!(1.0), &json!([25.0, to as f64])), "{r}");
@@ -170,7 +170,7 @@ fn a_live_drag_of_a_lone_sketch_shows_it_without_evaluating() {
assert_eq!(s.evaluations, 0);
assert_eq!(solve_counts::take(), (2, 0));
// The free rectangle's corner moves alone; its profile follows.
assert!(corner(&s).dist(solvecraft_geom::Vec2::new(3.0, 5.0)) < 1e-9);
assert!(corner(&s).dist(solidcraft_geom::Vec2::new(3.0, 5.0)) < 1e-9);
let st = s.model.state();
let profile = st.sketches[0].profiles.first().expect("a profile");
assert!((profile.area - 22.0 * 15.0).abs() < 1e-9, "{}", profile.area);
@@ -199,8 +199,8 @@ fn the_grab_follows_the_points_that_move() {
run(&mut s, "sketch.line", json!({"points": [[10, 10], [30, 10]]}));
run(&mut s, "sketch.constraint.fix", json!({"entities": ["l1.start"]}));
ui_frames(&mut s, "l1", [20.0, 10.0], &[[0.0, 1.0]; 6]);
assert!(pt(&s, "l1.end").dist(solvecraft_geom::Vec2::new(30.0, 16.0)) < 1e-9, "{:?}", pt(&s, "l1.end"));
assert!(pt(&s, "l1.start").dist(solvecraft_geom::Vec2::new(10.0, 10.0)) < 1e-12);
assert!(pt(&s, "l1.end").dist(solidcraft_geom::Vec2::new(30.0, 16.0)) < 1e-9, "{:?}", pt(&s, "l1.end"));
assert!(pt(&s, "l1.start").dist(solidcraft_geom::Vec2::new(10.0, 10.0)) < 1e-12);
}
#[test]
@@ -219,7 +219,7 @@ fn a_query_during_a_live_drag_does_not_evaluate_the_model() {
assert_eq!(s.evaluations, 0);
}
fn sketch_of(s: &Session) -> solvecraft_sketch::Sketch {
fn sketch_of(s: &Session) -> solidcraft_sketch::Sketch {
s.active_sketch.and_then(|i| s.doc.sketch(i).ok()).cloned().expect("sketch mode")
}
@@ -232,7 +232,7 @@ fn a_live_frame_solves_the_dimensions_current_values() {
let param = sk
.constraints
.iter()
.find(|c| matches!(c.kind, solvecraft_sketch::ConstraintKind::Length { l, .. } if l == l1))
.find(|c| matches!(c.kind, solidcraft_sketch::ConstraintKind::Length { l, .. } if l == l1))
.and_then(|c| c.param.clone())
.expect("l1's dimension");
if let Some(p) = s.doc_mut().params.iter_mut().find(|p| p.name == param) {
+6 -6
View File
@@ -2,8 +2,8 @@
use crate::params::{bad, vec3};
use crate::{Result, Session};
use serde_json::Value;
use solvecraft_doc::{EmbossFace, PlaneRef, apply_point, apply_vector, mat_inverse, mat_mul};
use solvecraft_geom::Plane;
use solidcraft_doc::{EmbossFace, PlaneRef, apply_point, apply_vector, mat_inverse, mat_mul};
use solidcraft_geom::Plane;
pub(in crate::cmd) fn is_face(p: &Value) -> bool {
p.get("plane").and_then(|plane| plane.get("face")).is_some()
@@ -144,10 +144,10 @@ fn capture_impl(s: &Session, value: &Value, cmd: &str, internal: bool) -> Result
return Ok(PlaneRef::Custom { plane });
}
let at = apply_point(&author_inverse, point);
let name = solvecraft_doc::naming::face_names(body).get(index).cloned();
let name = solidcraft_doc::naming::face_names(body).get(index).cloned();
let anchor = match doc.capture_face_anchor(body, index, &plane, &path, s.active_component, &author_path) {
Ok(anchor) => Some(Box::new(anchor)),
Err(solvecraft_doc::DocError::Invalid(message))
Err(solidcraft_doc::DocError::Invalid(message))
if internal
&& matches!(
message.as_str(),
@@ -166,7 +166,7 @@ fn capture_impl(s: &Session, value: &Value, cmd: &str, internal: bool) -> Result
}
/// The sole durable face attachment beneath an offset/angle wrapper.
pub(in crate::cmd) fn anchor(mut plane: &PlaneRef) -> Option<&solvecraft_doc::FaceAnchor> {
pub(in crate::cmd) fn anchor(mut plane: &PlaneRef) -> Option<&solidcraft_doc::FaceAnchor> {
loop {
match plane {
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => plane = base,
@@ -176,7 +176,7 @@ pub(in crate::cmd) fn anchor(mut plane: &PlaneRef) -> Option<&solvecraft_doc::Fa
}
}
pub(in crate::cmd) fn anchor_mut(mut plane: &mut PlaneRef) -> Option<&mut solvecraft_doc::FaceAnchor> {
pub(in crate::cmd) fn anchor_mut(mut plane: &mut PlaneRef) -> Option<&mut solidcraft_doc::FaceAnchor> {
loop {
match plane {
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => plane = base,
+28 -28
View File
@@ -1,6 +1,6 @@
use crate::Session;
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
fn run(s: &mut Session, command: &str, params: Value) -> Value {
s.execute(command, &params).unwrap_or_else(|error| panic!("{command} {params}: {error}"))
@@ -41,7 +41,7 @@ fn exact_selected_base_follows_height_and_never_retargets_control() {
assert!(s.model.state().body("Badge").is_none());
assert!((s.world_state().body("Control").unwrap().mesh().bounds().max.z - 30.).abs() < 1e-6);
let before = s.doc.to_json();
let reopened = Session::new(solvecraft_doc::Document::from_json(&before).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&before).unwrap());
assert!(reopened.model.result(sketch).unwrap().error.is_some());
}
#[test]
@@ -69,7 +69,7 @@ fn explicit_rebind_preserves_geometry_dimensions_context_and_one_undo() {
assert_eq!(s.doc.to_json(), before);
assert!(s.model.result(id).unwrap().error.is_some());
run(&mut s, "edit.redo", json!({}));
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!((reopened.model.state().sketch(id).unwrap().plane.origin.z - 30.).abs() < 1e-6);
}
#[test]
@@ -84,7 +84,7 @@ fn unsupported_face_has_explicit_fixed_alternative_and_no_live_projection() {
assert_eq!(s.doc.to_json(), before);
pick["fixed"] = json!(true);
let id = run(&mut s, "sketch.create", json!({"plane":pick}))["sketch"].as_u64().unwrap();
assert!(matches!(s.doc.feature(id).unwrap().kind, solvecraft_doc::FeatureKind::Sketch { plane: solvecraft_doc::PlaneRef::Custom { .. }, .. }));
assert!(matches!(s.doc.feature(id).unwrap().kind, solidcraft_doc::FeatureKind::Sketch { plane: solidcraft_doc::PlaneRef::Custom { .. }, .. }));
assert!(s.doc.sketch(id).unwrap().links.is_empty());
}
#[test]
@@ -130,7 +130,7 @@ fn default_certified_borders_follow_width_and_height_with_stable_entities() {
let max_y = sketch.sketch.points.iter().map(|point| point.pos.y).fold(f64::NEG_INFINITY, f64::max);
let min_y = sketch.sketch.points.iter().map(|point| point.pos.y).fold(f64::INFINITY, f64::min);
assert!(((max_x - min_x) * (max_y - min_y) - 160.).abs() < 1e-5);
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(reopened.model.state().sketch(id).unwrap().sketch.links.iter().all(|link| !link.lost));
}
@@ -175,7 +175,7 @@ fn exact_nested_source_and_cross_owner_pending_paths_capture_only_involved_posit
let before = s.doc.to_json();
let pending = s.pending_moves.clone();
let id = run(&mut s, "sketch.create", json!({"plane":pick,"project_edges":false}))["sketch"].as_u64().unwrap();
let solvecraft_doc::FeatureKind::Sketch { plane, .. } = &s.doc.feature(id).unwrap().kind else { panic!("not sketch") };
let solidcraft_doc::FeatureKind::Sketch { plane, .. } = &s.doc.feature(id).unwrap().kind else { panic!("not sketch") };
let anchor = super::anchor(plane).unwrap();
assert_eq!(anchor.path, vec![second, child_occ]);
assert_eq!(anchor.author_path, vec![author["occurrence"].as_u64().unwrap()]);
@@ -186,7 +186,7 @@ fn exact_nested_source_and_cross_owner_pending_paths_capture_only_involved_posit
assert_eq!(s.doc.to_json(), before);
assert_eq!(s.pending_moves, pending);
run(&mut s, "edit.redo", json!({}));
let mut reopened = Session::new(solvecraft_doc::Document::from_json(&saved).unwrap());
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&saved).unwrap());
assert!(reopened.model.result(id).unwrap().error.is_none());
run(&mut reopened, "component.activate", json!({"component":0}));
let original = reopened.doc.to_json();
@@ -225,7 +225,7 @@ fn fixed_rebind_freezes_current_resized_borders_and_only_detaches_auto_links() {
visible.curves.iter().map(|curve| curve.id.clone()).collect::<Vec<_>>()
);
assert_eq!(fixed.points.iter().map(|point| point.pos).collect::<Vec<_>>(), visible.points.iter().map(|point| point.pos).collect::<Vec<_>>());
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(reopened.model.result(id).unwrap().error.is_none());
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.to_json(), before);
@@ -249,13 +249,13 @@ fn strict_anchor_timeline_copy_remaps_producer_and_refuses_external_translation_
let pasted = run(&mut s, "timeline.paste", json!({"translate":[30,0,0]}));
assert!(pasted["errors"].as_array().unwrap().is_empty(), "{pasted}");
let new_id = pasted["pasted"][1]["id"].as_u64().unwrap();
let solvecraft_doc::FeatureKind::Sketch { plane, .. } = &s.doc.feature(new_id).unwrap().kind else { panic!("not sketch") };
let solidcraft_doc::FeatureKind::Sketch { plane, .. } = &s.doc.feature(new_id).unwrap().kind else { panic!("not sketch") };
let anchor = super::anchor(plane).unwrap();
assert_ne!(anchor.target.origin_feature, s.doc.find_feature("BaseFeature").unwrap().id);
assert_ne!(anchor.target.body, "Base");
assert!(s.model.state().sketch(new_id).unwrap().sketch.links.iter().all(|link| !link.lost));
assert!(s.model.state().sketch(id).unwrap().sketch.links.iter().all(|link| !link.lost));
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(reopened.model.result(new_id).unwrap().error.is_none());
}
@@ -274,10 +274,10 @@ fn strict_anchor_deep_clone_is_independent_and_rename_preserves_certified_projec
.doc
.features
.iter()
.find(|feature| feature.component == owner && matches!(feature.kind, solvecraft_doc::FeatureKind::Sketch { .. }))
.find(|feature| feature.component == owner && matches!(feature.kind, solidcraft_doc::FeatureKind::Sketch { .. }))
.unwrap();
let new_id = new_feature.id;
let solvecraft_doc::FeatureKind::Sketch { plane, .. } = &new_feature.kind else { panic!("not sketch") };
let solidcraft_doc::FeatureKind::Sketch { plane, .. } = &new_feature.kind else { panic!("not sketch") };
let anchor = super::anchor(plane).unwrap();
let body = anchor.target.body.clone();
assert_eq!(anchor.component, owner);
@@ -287,7 +287,7 @@ fn strict_anchor_deep_clone_is_independent_and_rename_preserves_certified_projec
run(&mut s, "body.rename", json!({"body":body,"name":"Independent"}));
assert!(s.model.result(new_id).unwrap().error.is_none());
assert!(s.model.state().sketch(new_id).unwrap().sketch.links.iter().all(|link| !link.lost));
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(reopened.model.result(new_id).unwrap().error.is_none());
}
@@ -308,7 +308,7 @@ fn legacy_or_fixed_rebind_needs_explicit_author_path_for_repeated_owner() {
assert!(error.to_string().contains("ambiguous"), "{error}");
assert_eq!(s.doc.to_json(), before);
run(&mut s, "sketch.redefine", json!({"sketch":id,"plane":pick,"author_path":[second]}));
let solvecraft_doc::FeatureKind::Sketch { plane, .. } = &s.doc.feature(id).unwrap().kind else { panic!("not sketch") };
let solidcraft_doc::FeatureKind::Sketch { plane, .. } = &s.doc.feature(id).unwrap().kind else { panic!("not sketch") };
assert_eq!(super::anchor(plane).unwrap().author_path, vec![second]);
assert_eq!(s.active_component, 0);
}
@@ -370,12 +370,12 @@ fn freezing_rebuilt_arc_borders_keeps_authored_dimension_entity_indices() {
let dimension = frozen
.constraints
.iter()
.find(|constraint| matches!(constraint.kind, solvecraft_sketch::ConstraintKind::Radius { c, .. } if c == circle_index))
.find(|constraint| matches!(constraint.kind, solidcraft_sketch::ConstraintKind::Radius { c, .. } if c == circle_index))
.unwrap();
assert!((solvecraft_sketch::measure_dimension(frozen, &dimension.kind).unwrap() - 2.).abs() < 1e-6);
assert!((solidcraft_sketch::measure_dimension(frozen, &dimension.kind).unwrap() - 2.).abs() < 1e-6);
assert_eq!(frozen.curves.iter().map(|curve| &curve.id).collect::<Vec<_>>(), visible.curves.iter().map(|curve| &curve.id).collect::<Vec<_>>());
assert!(s.model.result(id).unwrap().error.is_none());
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(reopened.model.result(id).unwrap().error.is_none());
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.to_json(), before);
@@ -388,21 +388,21 @@ fn angled_attachment_tracks_source_plane_and_reopens() {
let pick = top_pick(&s, "Base");
let id = run(&mut s, "sketch.create", json!({"plane":pick,"project_edges":false}))["sketch"].as_u64().unwrap();
run(&mut s, "sketch.finish", json!({}));
let solvecraft_doc::FeatureKind::Sketch { plane, .. } = &mut s.doc_mut().feature_mut(id).unwrap().kind else { panic!("not sketch") };
*plane = solvecraft_doc::PlaneRef::AtAngle { base: Box::new(plane.clone()), axis_origin: Vec3::ZERO, axis_dir: Vec3::X, angle: "90 deg".into() };
let solidcraft_doc::FeatureKind::Sketch { plane, .. } = &mut s.doc_mut().feature_mut(id).unwrap().kind else { panic!("not sketch") };
*plane = solidcraft_doc::PlaneRef::AtAngle { base: Box::new(plane.clone()), axis_origin: Vec3::ZERO, axis_dir: Vec3::X, angle: "90 deg".into() };
s.refresh();
let height = s.doc.find_feature("BaseFeature").unwrap().param_names[2].clone();
run(&mut s, "parameters.change", json!({"name":height,"expression":"20 mm"}));
let placed = s.model.state().sketch(id).unwrap().plane;
assert!((placed.origin.y + 20.).abs() < 1e-6);
assert!(placed.origin.z.abs() < 1e-6);
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert_eq!(reopened.model.state().sketch(id).unwrap().plane, placed);
}
#[test]
fn rounded_enclosure_shell_side_and_floor_attach_through_width_wall_and_reopen() {
use solvecraft_doc::{FeatureKind, config::FaceCertificate, face_anchor::plane_face_anchor};
use solidcraft_doc::{FeatureKind, config::FaceCertificate, face_anchor::plane_face_anchor};
let mut s = Session::default();
for (name, expression) in [("width", "80 mm"), ("depth", "60 mm"), ("height", "30 mm"), ("wall", "2 mm")] {
run(&mut s, "parameters.add", json!({"name":name,"expression":expression}));
@@ -463,7 +463,7 @@ fn rounded_enclosure_shell_side_and_floor_attach_through_width_wall_and_reopen()
}
run(&mut s, "parameters.change", json!({"name":"width","expression":"100 mm"}));
run(&mut s, "parameters.change", json!({"name":"wall","expression":"3 mm"}));
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
for session in [&s, &reopened] {
for id in [side, floor] {
let result = session.model.result(id).unwrap();
@@ -481,7 +481,7 @@ fn rounded_enclosure_shell_side_and_floor_attach_through_width_wall_and_reopen()
assert!(state.sketch(floor).unwrap().plane.normal().dot(Vec3::Z) > 0.999);
let mesh = state.body("Case").unwrap().body.tessellate_with(1e-4, true).unwrap();
let case = &state.body("Case").unwrap().body;
let corners: Vec<_> = (0..case.face_count()).filter(|i| matches!(solvecraft_kernel::face_surface(case, *i), Some(solvecraft_kernel::FaceSurface::Cylinder { radius, .. }) if (radius - 6.).abs() < 1e-6)).collect();
let corners: Vec<_> = (0..case.face_count()).filter(|i| matches!(solidcraft_kernel::face_surface(case, *i), Some(solidcraft_kernel::FaceSurface::Cylinder { radius, .. }) if (radius - 6.).abs() < 1e-6)).collect();
assert_eq!(corners.len(), 4, "later port/boss features must not subdivide untouched outer corners");
for index in corners {
let points: Vec<_> = mesh
@@ -498,7 +498,7 @@ fn rounded_enclosure_shell_side_and_floor_attach_through_width_wall_and_reopen()
assert!(!mesh.contains(Vec3::new(49., 0., 12.)), "USB must remain open after resize/reopen");
assert!(mesh.contains(Vec3::new(49., 15., 12.)), "adjacent right wall remains solid");
assert!(mesh.contains(Vec3::new(-49., 0., 12.)), "opposite wall must stay solid");
let seed = state.sketch(floor).unwrap().plane.to_world(solvecraft_geom::Vec2::new(40., 20.)) + Vec3::Z * 6.;
let seed = state.sketch(floor).unwrap().plane.to_world(solidcraft_geom::Vec2::new(40., 20.)) + Vec3::Z * 6.;
assert!(mesh.contains(seed), "seed boss starts on current floor");
for shift in [Vec3::ZERO, Vec3::new(-80., 0., 0.), Vec3::new(0., -40., 0.), Vec3::new(-80., -40., 0.)] {
assert!(mesh.contains(seed + shift), "each patterned boss must survive resize/reopen at {:?}", seed + shift);
@@ -525,14 +525,14 @@ fn offset_rectangle_pocket_has_one_logical_outer_wall_and_closed_face_projection
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch":control_sketch,"distance":10,"operation":"new","body_name":"Control"}));
let untouched = s.model.state().body("Control").unwrap().body.clone();
let before_control = solvecraft_kernel::step_export(&[&untouched], "control snapshot").unwrap();
let before_control = solidcraft_kernel::step_export(&[&untouched], "control snapshot").unwrap();
let pick = top_pick(&s, "Stock");
let pocket = run(&mut s, "sketch.create", json!({"plane":pick}))["sketch"].as_u64().unwrap();
let side = {
let state = s.model.state();
let sk = &state.sketch(pocket).unwrap().sketch;
let curve = sk.curves.iter().find(|c| c.id == "l1").unwrap();
let solvecraft_sketch::CurveKind::Line { a, b } = curve.kind else { panic!() };
let solidcraft_sketch::CurveKind::Line { a, b } = curve.kind else { panic!() };
sk.point(a).unwrap() + sk.point(b).unwrap()
};
let outer = run(&mut s, "sketch.offset", json!({"curves":["l1"],"distance":0.3425231845240546,"side":[side.x,side.y]}))["curves"].clone();
@@ -541,7 +541,7 @@ fn offset_rectangle_pocket_has_one_logical_outer_wall_and_closed_face_projection
run(&mut s, "solid.extrude", json!({"sketch":pocket,"profiles":[{"curves":inner}],"distance":-9,"operation":"cut"}));
let control = s.model.state().body("Control").unwrap().body.clone();
assert!(control.same(&untouched), "a separate body crossing the cutter end plane remains the exact same solid");
assert_eq!(solvecraft_kernel::step_export(&[&control], "control snapshot").unwrap(), before_control);
assert_eq!(solidcraft_kernel::step_export(&[&control], "control snapshot").unwrap(), before_control);
let state = s.model.state();
let body = state.body("Stock").unwrap();
let display = body.body.display_mesh(0.01).unwrap();
@@ -556,7 +556,7 @@ fn offset_rectangle_pocket_has_one_logical_outer_wall_and_closed_face_projection
assert_eq!(display.surface_patch(face.index).len(), 1);
let attached = run(&mut s, "sketch.create", json!({"plane":{"body":"Stock","index":face.index,"face":[30,0,0.5]}}))["sketch"].as_u64().unwrap();
run(&mut s, "sketch.finish", json!({}));
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
for session in [&s, &reopened] {
let result = session.model.result(attached).unwrap();
assert!(result.error.is_none(), "{:?}", result.error);
+3 -3
View File
@@ -3,8 +3,8 @@
//! stays exact; the pieces replace the original curve.
use serde_json::{Value, json};
use solvecraft_geom::{Seg2, Vec2};
use solvecraft_sketch::{ConstraintKind, CurveKind, Sketch};
use solidcraft_geom::{Seg2, Vec2};
use solidcraft_sketch::{ConstraintKind, CurveKind, Sketch};
use super::sketch::add_c;
use crate::Result;
@@ -163,7 +163,7 @@ fn add_pieces(sk: &mut Sketch, segs: &[Seg2], start: Option<usize>, end: Option<
CurveKind::Conic { a: prev, b: to, apex: x, rho: w / (1.0 + w) }
}
Seg2::Bezier { n, p } => {
let k = (n as usize).clamp(1, solvecraft_geom::MAX_BEZIER_DEGREE);
let k = (n as usize).clamp(1, solidcraft_geom::MAX_BEZIER_DEGREE);
let mut pts = vec![prev];
for q in p.iter().take(k).skip(1) {
pts.push(sk.add_point(*q, None)?);
+1 -1
View File
@@ -94,7 +94,7 @@ fn random_sketch_editing_never_breaks() {
}
}
// The design round-trips.
let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let back = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(back, *s.doc, "seed {seed}");
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
//! new geometry right away; ids are never reused after a delete.
use serde_json::{Value, json};
use solvecraft_sketch::{CurveKind, Sketch};
use solidcraft_sketch::{CurveKind, Sketch};
use crate::Session;
+8 -8
View File
@@ -3,9 +3,9 @@
use std::collections::HashMap;
use serde_json::{Value, json};
use solvecraft_geom::Vec2;
use solvecraft_io::{Geom2, MAX_DRAWING_BYTES};
use solvecraft_sketch::{CurveKind, LinkKind, LinkSource, Sketch};
use solidcraft_geom::Vec2;
use solidcraft_io::{Geom2, MAX_DRAWING_BYTES};
use solidcraft_sketch::{CurveKind, LinkKind, LinkSource, Sketch};
use super::CommandSpec;
use super::sketch::{edit, ids_of};
@@ -34,7 +34,7 @@ fn source_text(p: &Value, cmd: &str) -> Result<String> {
return Ok(t.to_string());
}
let path = str_(p, "path").filter(|x| !x.trim().is_empty() && x.len() < 4096).ok_or_else(|| bad(cmd, "give `path` or `text`"))?;
let bytes = solvecraft_io::vfs::read(path, MAX_DRAWING_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let bytes = solidcraft_io::vfs::read(path, MAX_DRAWING_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
Ok(String::from_utf8_lossy(&bytes).to_string())
}
@@ -79,7 +79,7 @@ fn add_geometry(sk: &mut Sketch, geom: &[Geom2], f: &dyn Fn(Vec2) -> Vec2, k: f6
Geom2::Ellipse { c, major, minor } => Some(CurveKind::Ellipse { c: pt(sk, *c)?, m: pt(sk, *major)?, r: minor * k }),
Geom2::Spline { pts, control, degree } => {
let mut idx = Vec::new();
for p in pts.iter().take(solvecraft_sketch::MAX_SPLINE_POINTS) {
for p in pts.iter().take(solidcraft_sketch::MAX_SPLINE_POINTS) {
let i = pt(sk, *p)?;
if idx.last() != Some(&i) {
idx.push(i);
@@ -107,7 +107,7 @@ fn add_geometry(sk: &mut Sketch, geom: &[Geom2], f: &dyn Fn(Vec2) -> Vec2, k: f6
fn insert(s: &mut Session, p: &Value, cmd: &str, svg: bool) -> Result<Value> {
let text = source_text(p, cmd)?;
let geom = if svg { solvecraft_io::read_svg(&text) } else { solvecraft_io::read_dxf(&text) }.map_err(|e| bad(cmd, e.to_string()))?;
let geom = if svg { solidcraft_io::read_svg(&text) } else { solidcraft_io::read_dxf(&text) }.map_err(|e| bad(cmd, e.to_string()))?;
if geom.is_empty() {
return Err(bad(cmd, "the drawing has no geometry SolidCraft reads"));
}
@@ -180,9 +180,9 @@ fn export_dxf(s: &mut Session, p: &Value) -> Result<Value> {
}
}
}
let text = solvecraft_io::write_dxf(&geom);
let text = solidcraft_io::write_dxf(&geom);
if let Some(path) = str_(p, "path").filter(|x| !x.trim().is_empty()) {
solvecraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
solidcraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
return Ok(json!({"path": path, "entities": geom.len()}));
}
Ok(json!({"dxf": text, "entities": geom.len()}))
+4 -4
View File
@@ -2,7 +2,7 @@
//! centre of mass of bodies.
use serde_json::{Value, json};
use solvecraft_geom::{Seg2, Vec2, Vec3};
use solidcraft_geom::{Seg2, Vec2, Vec3};
use super::CommandSpec;
use crate::params::{bad, num, string_list, vec3};
@@ -147,7 +147,7 @@ fn min_of(teeth: &[(Vec3, f64, Vec3)]) -> Option<(f64, Vec3)> {
}
/// The face of a body at `at` (nearest triangle): (body index, face index).
fn face_at(st: &solvecraft_doc::ModelState, at: Vec3) -> Option<(usize, u32)> {
fn face_at(st: &solidcraft_doc::ModelState, at: Vec3) -> Option<(usize, u32)> {
let mut best: Option<(f64, usize, u32)> = None;
for (bi, b) in st.bodies.iter().enumerate() {
let m = b.mesh();
@@ -184,7 +184,7 @@ fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
/// Smallest radius of curvature over a face of a mesh, from the turn of the vertex normals
/// along each triangle side: (radius, where).
fn face_min_radius(m: &solvecraft_geom::Mesh, face: u32) -> Option<(f64, Vec3)> {
fn face_min_radius(m: &solidcraft_geom::Mesh, face: u32) -> Option<(f64, Vec3)> {
let mut best: Option<(f64, Vec3)> = None;
for (ti, t) in m.triangles.iter().enumerate() {
if m.tri_face.get(ti) != Some(&face) {
@@ -294,7 +294,7 @@ fn isocurves(s: &mut Session, p: &Value) -> Result<Value> {
let at = vec3(v).ok_or_else(|| bad(cmd, "`faces` must list points [x,y,z]"))?;
let (bi, f) = face_at(&st, at).ok_or_else(|| bad(cmd, "no face there"))?;
let m = st.bodies.get(bi).map(|b| b.mesh()).ok_or_else(|| bad(cmd, "body"))?;
for w in solvecraft_doc::iso_grid(&m, f, at, count) {
for w in solidcraft_doc::iso_grid(&m, f, at, count) {
out.push(w.iter().map(|q| [q.x, q.y, q.z]).collect::<Vec<_>>());
}
}
+10 -10
View File
@@ -4,9 +4,9 @@
use std::f64::consts::TAU;
use serde_json::{Value, json};
use solvecraft_doc::ProfileSel;
use solvecraft_geom::Vec2;
use solvecraft_sketch::{ConstraintKind, Curve, CurveKind, Shape, Sketch, intersections};
use solidcraft_doc::ProfileSel;
use solidcraft_geom::Vec2;
use solidcraft_sketch::{ConstraintKind, Curve, CurveKind, Shape, Sketch, intersections};
use super::sketch::{add_c, edit, ids_of, solve_current};
use super::{CommandSpec, in_sketch};
@@ -451,8 +451,8 @@ fn extend(s: &mut Session, p: &Value) -> Result<Value> {
}
pin(sk, end, oi)?;
let before = sk.points.iter().map(|p| p.pos).collect::<Vec<_>>();
let hold = solvecraft_sketch::Hold { points: (0..sk.points.len()).filter(|i| *i != end).collect(), radii: (0..sk.curves.len()).collect() };
if !solvecraft_sketch::solve_with(sk, &hold, None, solvecraft_sketch::Analysis::Full, None).ok()
let hold = solidcraft_sketch::Hold { points: (0..sk.points.len()).filter(|i| *i != end).collect(), radii: (0..sk.curves.len()).collect() };
if !solidcraft_sketch::solve_with(sk, &hold, None, solidcraft_sketch::Analysis::Full, None).ok()
|| sk.point(end).is_none_or(|point| point.dist(q) > TOL)
|| sk.points.iter().enumerate().any(|(i, point)| i != end && point.pos.dist(before[i]) > TOL)
{
@@ -602,7 +602,7 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
None => vec![p.clone()],
};
let (out, info) = edit(s, p, cmd, false, |sk, doc| {
let r = doc.eval(&r_expr, solvecraft_doc::expr::Kind::Length).map_err(|e| bad(cmd, format!("radius: {e}")))?;
let r = doc.eval(&r_expr, solidcraft_doc::expr::Kind::Length).map_err(|e| bad(cmd, format!("radius: {e}")))?;
if !(r > 1e-9 && r < 1e8) {
return Err(bad(cmd, "radius must be positive"));
}
@@ -958,7 +958,7 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
}
let ci = sk.add_circle(cc, nr, Some(c), None)?;
made.push(ci);
sk.offsets.push(solvecraft_sketch::OffsetChain {
sk.offsets.push(solidcraft_sketch::OffsetChain {
sources: vec![(ids[0].clone(), true)],
outputs: ids_of(sk, &made),
distance: r - nr,
@@ -1014,7 +1014,7 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
.map(|topology| topology.roles.clone())
.ok_or_else(|| bad(cmd, "new offset association is missing"))?;
for (source, role) in sources.iter().zip(&roles) {
if role.kind == solvecraft_sketch::OffsetRoleKind::Line {
if role.kind == solidcraft_sketch::OffsetRoleKind::Line {
let a = sk.curve_index(&source.0).ok_or_else(|| bad(cmd, "offset source line vanished"))?;
let b = sk.curve_index(&role.output).ok_or_else(|| bad(cmd, "offset output line vanished"))?;
cons.push(add_c(sk, ConstraintKind::Parallel { a, b })?);
@@ -1106,7 +1106,7 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
let outputs = ids_of(sk, &made);
if pieces.iter().all(|p| !matches!(p, Piece::Free(_))) {
let sources = order.iter().filter_map(|(i, forward)| sk.curves.get(*i).map(|c| (c.id.clone(), *forward))).collect();
sk.offsets.push(solvecraft_sketch::OffsetChain { sources, outputs: outputs.clone(), distance: d, closed, topology: None });
sk.offsets.push(solidcraft_sketch::OffsetChain { sources, outputs: outputs.clone(), distance: d, closed, topology: None });
sk.refresh_offsets()?;
}
Ok((outputs, cons, None))
@@ -1172,7 +1172,7 @@ fn break_offset(s: &mut Session, p: &Value) -> Result<Value> {
/// A broken logical boundary becomes ordinary geometry. Keep downstream features
/// on that exact current boundary while other associations retain their full roles.
fn freeze_offset_profiles(doc: &mut solvecraft_doc::Document, sketch: u64, boundary: &Sketch, association: &str) -> Result<()> {
fn freeze_offset_profiles(doc: &mut solidcraft_doc::Document, sketch: u64, boundary: &Sketch, association: &str) -> Result<()> {
let cmd = "sketch.offset.break";
let mut groups = std::collections::BTreeMap::new();
let mut work = 0usize;
+22 -22
View File
@@ -1,6 +1,6 @@
use serde_json::{Value, json};
use solvecraft_geom::Vec2;
use solvecraft_sketch::CurveKind;
use solidcraft_geom::Vec2;
use solidcraft_sketch::CurveKind;
use crate::Session;
@@ -15,7 +15,7 @@ fn ids(v: &Value) -> Vec<String> {
v.as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default()
}
fn sketch(s: &Session) -> solvecraft_sketch::Sketch {
fn sketch(s: &Session) -> solidcraft_sketch::Sketch {
let id = s.active_sketch.unwrap();
s.model.state().sketch(id).unwrap().sketch.clone()
}
@@ -63,7 +63,7 @@ fn explicit_freeform_break_preserves_exact_geometry_and_undo() {
}
}
let encoded = serde_json::to_value(&after).unwrap();
let restored: solvecraft_sketch::Sketch = serde_json::from_value(encoded).unwrap();
let restored: solidcraft_sketch::Sketch = serde_json::from_value(encoded).unwrap();
assert_eq!(restored.curves.len(), after.curves.len());
run(&mut s, "edit.undo", json!({}));
assert!(sketch(&s).curve_index(&id).is_some());
@@ -120,7 +120,7 @@ fn analytic_trim_and_break_use_freeform_cutters() {
let sk = sketch(&s);
for id in ids(&out["curves"]) {
let shape = sk.shape(sk.curve_index(&id).unwrap()).unwrap();
assert!(matches!(shape, solvecraft_sketch::Shape::Line { a, b } if (a.y - 5.0).abs() < 1e-6 && (b.y - 5.0).abs() < 1e-6));
assert!(matches!(shape, solidcraft_sketch::Shape::Line { a, b } if (a.y - 5.0).abs() < 1e-6 && (b.y - 5.0).abs() < 1e-6));
}
let mut s = new_sketch();
@@ -144,7 +144,7 @@ fn trim_extend_break() {
let sk = sketch(&s);
let sh = sk.shape(sk.curve_index(&h).unwrap()).unwrap();
assert!(
matches!(sh, solvecraft_sketch::Shape::Line { a, b } if a.dist(Vec2::new(0.0, 0.0)) < 1e-9 && b.dist(Vec2::new(10.0, 0.0)) < 1e-9),
matches!(sh, solidcraft_sketch::Shape::Line { a, b } if a.dist(Vec2::new(0.0, 0.0)) < 1e-9 && b.dist(Vec2::new(10.0, 0.0)) < 1e-9),
"{sh:?}"
);
// Trim the lower half of the vertical line: the original id survives on the upper piece.
@@ -198,7 +198,7 @@ fn exact_extend_ellipse_preserves_support_target_and_history() {
assert!(end.dist(Vec2::new(expected_x, y)) < 1e-7, "exact ellipse contact {end:?}");
assert_eq!(sk.exact_segs(0).unwrap(), target);
let persisted = s.doc.to_json();
let reopened = solvecraft_doc::format::read(&persisted).unwrap();
let reopened = solidcraft_doc::format::read(&persisted).unwrap();
assert_eq!(reopened.to_json(), persisted);
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.to_json(), before);
@@ -645,7 +645,7 @@ fn certified_offset_follows_dimension_drag_reopen_break_and_undo() {
run(&mut s, "sketch.drag", json!({"entity":ellipse,"from":[0,0],"to":[4,3]}));
verify(&s);
let mut reopened = Session {
doc: std::sync::Arc::new(solvecraft_doc::format::read(&s.doc.to_json()).unwrap()),
doc: std::sync::Arc::new(solidcraft_doc::format::read(&s.doc.to_json()).unwrap()),
active_sketch: s.active_sketch,
..Session::default()
};
@@ -677,7 +677,7 @@ fn certified_offset_delete_output_undo_reopen_and_refuse_dependent_source() {
assert!(sk.curve_index(&id).is_none());
assert!(sk.curve_index("e1").is_some());
let deleted = s.doc.to_json();
let reopened = solvecraft_doc::format::read(&deleted).unwrap();
let reopened = solidcraft_doc::format::read(&deleted).unwrap();
assert_eq!(reopened.to_json(), deleted);
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.to_json(), attached);
@@ -706,7 +706,7 @@ fn periodic_closed_offset_chain_break_drag_reopen_and_material_volume() {
let check = |session: &Session| {
let sk = sketch(session);
assert_eq!(sk.freeform_offsets.iter().map(|r| r.certificate).collect::<Vec<_>>(), [2, 2]);
assert_eq!(solvecraft_sketch::find_profiles(&sk).len(), 3);
assert_eq!(solidcraft_sketch::find_profiles(&sk).len(), 3);
for id in [&a, &b] {
assert!(matches!(sk.curves[sk.curve_index(id).unwrap()].kind, CurveKind::Spline { periodic: true, .. }));
}
@@ -719,7 +719,7 @@ fn periodic_closed_offset_chain_break_drag_reopen_and_material_volume() {
run(&mut s, "parameters.change", json!({"name":"d1","expression":12}));
check(&s);
let mut reopened =
Session { doc: solvecraft_doc::format::read(&s.doc.to_json()).unwrap().into(), active_sketch: s.active_sketch, ..Default::default() };
Session { doc: solidcraft_doc::format::read(&s.doc.to_json()).unwrap().into(), active_sketch: s.active_sketch, ..Default::default() };
reopened.refresh();
check(&reopened);
let attached = reopened.doc.to_json();
@@ -736,12 +736,12 @@ fn periodic_closed_offset_chain_break_drag_reopen_and_material_volume() {
let indices = moved.curves[moved.curve_index(&b).unwrap()].kind.point_ids();
assert_eq!(moved.point(indices[0]), moved.point(*indices.last().unwrap()));
assert!(moved.point(indices[0]).unwrap().dist(from) > 0.1);
assert_eq!(solvecraft_doc::format::read(&reopened.doc.to_json()).unwrap().to_json(), reopened.doc.to_json());
assert_eq!(solidcraft_doc::format::read(&reopened.doc.to_json()).unwrap().to_json(), reopened.doc.to_json());
run(&mut reopened, "edit.undo", json!({}));
run(&mut reopened, "edit.undo", json!({}));
assert_eq!(reopened.doc.to_json(), attached);
// An independent ring area multiplied by height probes actual profile material.
let regions = solvecraft_sketch::find_profiles(&sketch(&s));
let regions = solidcraft_sketch::find_profiles(&sketch(&s));
let selected = regions.iter().enumerate().min_by(|(_, x), (_, y)| x.area.total_cmp(&y.area)).unwrap();
let n = 4096;
let h = std::f64::consts::TAU / n as f64;
@@ -767,7 +767,7 @@ fn periodic_closed_offset_chain_break_drag_reopen_and_material_volume() {
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch":sketch_id,"profiles":[selected.0],"distance":3,"operation":"new"}));
let state = s.world_state();
let volume = solvecraft_kernel::measure(&state.bodies.last().unwrap().body).unwrap().volume;
let volume = solidcraft_kernel::measure(&state.bodies.last().unwrap().body).unwrap().volume;
assert!((volume - expected).abs() < 0.05, "{volume} vs {expected}");
}
@@ -780,7 +780,7 @@ fn periodic_fit_creator_keeps_legacy_and_refuses_hostile_intent_atomically() {
let i = sk.curve_index(&id).unwrap();
assert!(matches!(sk.curves[i].kind, CurveKind::Spline { periodic: true, .. }));
sk.validate_periodic_splines().unwrap();
assert_eq!(solvecraft_sketch::find_profiles(&sk).len(), 1);
assert_eq!(solidcraft_sketch::find_profiles(&sk).len(), 1);
let before = s.doc.to_json();
let history = s.undo.len();
for (cmd, p) in [
@@ -822,7 +822,7 @@ fn analytic_offset_partial_delete_releases_survivors_preserves_source_and_undo()
}
assert!(sk.curve_index("l1").is_some());
let saved = s.doc.to_json();
assert_eq!(solvecraft_doc::format::read(&saved).unwrap().to_json(), saved);
assert_eq!(solidcraft_doc::format::read(&saved).unwrap().to_json(), saved);
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.to_json(), before);
}
@@ -835,7 +835,7 @@ fn periodic_offset_extrusion_survives_post_extrude_span_changes_and_cold_reopen(
let a = run(&mut s, "sketch.offset", json!({"curves":["e1"],"distance":-1,"chain":false}))["curves"][0].as_str().unwrap().to_string();
run(&mut s, "sketch.offset", json!({"curves":[a],"distance":-0.5,"chain":false}));
let sk_id = s.active_sketch.unwrap();
let initial = solvecraft_sketch::find_profiles(&sketch(&s));
let initial = solidcraft_sketch::find_profiles(&sketch(&s));
let (index, ring) = initial.iter().enumerate().min_by(|(_, a), (_, b)| a.area.total_cmp(&b.area)).unwrap();
let initial_count = ring.outer_curves.len();
let boundary = ring.whole_curve_boundaries().unwrap();
@@ -887,21 +887,21 @@ fn periodic_offset_extrusion_survives_post_extrude_span_changes_and_cold_reopen(
let body = &state.bodies[0].body;
for (distance, material) in [(1.25, true), (0.5, false), (1.75, false)] {
let point = ellipse + normal * distance;
assert_eq!(body.contains(solvecraft_geom::Vec3::new(point.x, point.y, 1.5)), material);
assert_eq!(body.contains(solidcraft_geom::Vec3::new(point.x, point.y, 1.5)), material);
}
// Repeat at the exact major-axis seam as a physical material control.
for (distance, material) in [(1.25, true), (0.5, false), (1.75, false)] {
let point = centre + u * (major + distance);
assert_eq!(ring.region.contains(point), material);
assert_eq!(body.contains(solvecraft_geom::Vec3::new(point.x, point.y, 1.5)), material);
assert_eq!(body.contains(solidcraft_geom::Vec3::new(point.x, point.y, 1.5)), material);
}
let volume = solvecraft_kernel::measure(body).unwrap().volume;
let volume = solidcraft_kernel::measure(body).unwrap().volume;
assert!((volume - expected).abs() < 0.05, "{major}: {volume} vs independent {expected}");
let mut cold = Session { doc: solvecraft_doc::format::read(&s.doc.to_json()).unwrap().into(), ..Default::default() };
let mut cold = Session { doc: solidcraft_doc::format::read(&s.doc.to_json()).unwrap().into(), ..Default::default() };
cold.refresh();
let cold_state = cold.world_state();
assert_eq!(cold_state.bodies.len(), 1);
let cold_volume = solvecraft_kernel::measure(&cold_state.bodies[0].body).unwrap().volume;
let cold_volume = solidcraft_kernel::measure(&cold_state.bodies[0].body).unwrap().volume;
assert!((cold_volume - volume).abs() < 1e-8);
let edited = s.doc.to_json();
run(&mut s, "edit.undo", json!({}));
+5 -5
View File
@@ -1,4 +1,4 @@
//! Timings for large sketches (run with `cargo test -p solvecraft-engine --release sketch_perf -- --ignored --nocapture`;
//! Timings for large sketches (run with `cargo test -p solidcraft-engine --release sketch_perf -- --ignored --nocapture`;
//! numbers are recorded in docs/perf.md).
use std::time::Instant;
@@ -19,8 +19,8 @@ fn big(n: usize) -> Session {
let id = s.active_sketch.unwrap();
let mut doc = (*s.doc).clone();
let sk = doc.sketch_mut(id).unwrap();
use solvecraft_geom::Vec2;
use solvecraft_sketch::ConstraintKind::*;
use solidcraft_geom::Vec2;
use solidcraft_sketch::ConstraintKind::*;
let side = (n as f64).sqrt().ceil() as usize;
for k in 0..n {
let (x, y) = ((k % side) as f64 * 30.0, (k / side) as f64 * 30.0);
@@ -54,10 +54,10 @@ fn sketch_perf() {
let entities = sk.curves.len() + sk.points.len();
let t = Instant::now();
let mut k = sk.clone();
let rep = solvecraft_sketch::solve(&mut k);
let rep = solidcraft_sketch::solve(&mut k);
let solve = ms(t);
let t = Instant::now();
let prof = solvecraft_sketch::find_profiles(&k);
let prof = solidcraft_sketch::find_profiles(&k);
let profiles = ms(t);
let ss = s.model.state().sketch(id).unwrap().clone();
let t = Instant::now();
+24 -24
View File
@@ -4,10 +4,10 @@
//! normal geometry.
use serde_json::{Value, json};
use solvecraft_doc::project::resolve;
use solvecraft_doc::{Document, FeatureKind};
use solvecraft_geom::{Plane, Vec3};
use solvecraft_sketch::{ConstraintKind, LinkKind, LinkSource, Sketch};
use solidcraft_doc::project::resolve;
use solidcraft_doc::{Document, FeatureKind};
use solidcraft_geom::{Plane, Vec3};
use solidcraft_sketch::{ConstraintKind, LinkKind, LinkSource, Sketch};
use super::{CommandSpec, in_sketch};
use crate::params::{bad, bool_, str_, vec3};
@@ -141,7 +141,7 @@ fn placed_candidate<'a>(s: &Session, doc: &'a Document) -> std::borrow::Cow<'a,
std::borrow::Cow::Owned(placed)
}
fn owner_frame(s: &Session, doc: &Document, comp: u64) -> solvecraft_doc::Mat {
fn owner_frame(s: &Session, doc: &Document, comp: u64) -> solidcraft_doc::Mat {
if comp == s.active_component { s.active_component_frame() } else { doc.component_transform(comp) }
}
@@ -157,19 +157,19 @@ pub(super) fn refresh(s: &Session, doc: &Document, id: u64, sk: &mut Sketch) ->
let st = s.model.state_before(id);
let comp = doc.feature(id).map(|f| f.component).unwrap_or(0);
let attachment = doc.feature(id).and_then(|feature| match &feature.kind {
solvecraft_doc::FeatureKind::Sketch { plane, .. } => super::sketch::face::anchor(plane),
solidcraft_doc::FeatureKind::Sketch { plane, .. } => super::sketch::face::anchor(plane),
_ => None,
});
let st = match attachment {
Some(anchor) => solvecraft_doc::state_in_face_anchor_frame(&doc, &st, anchor, comp),
None => Ok(solvecraft_doc::state_in_frame_at(&doc, &st, comp, owner_frame(s, &doc, comp))),
Some(anchor) => solidcraft_doc::state_in_face_anchor_frame(&doc, &st, anchor, comp),
None => Ok(solidcraft_doc::state_in_frame_at(&doc, &st, comp, owner_frame(s, &doc, comp))),
};
let Ok(st) = st else {
return vec!["lost face attachment; reattach explicitly".into()];
};
match attachment {
Some(anchor) => solvecraft_doc::project::refresh_face_anchor_links(&doc, &vals, &st, &plane, sk, anchor),
None => solvecraft_doc::project::refresh_links(&doc, &vals, &st, &plane, sk),
Some(anchor) => solidcraft_doc::project::refresh_face_anchor_links(&doc, &vals, &st, &plane, sk, anchor),
None => solidcraft_doc::project::refresh_links(&doc, &vals, &st, &plane, sk),
}
}
@@ -182,21 +182,21 @@ pub(super) fn add_link(s: &Session, doc: &Document, id: u64, sk: &mut Sketch, ki
// The model as the sketch's component sees it.
let comp = doc.feature(id).map(|f| f.component).unwrap_or(0);
let attachment = doc.feature(id).and_then(|feature| match &feature.kind {
solvecraft_doc::FeatureKind::Sketch { plane, .. } => super::sketch::face::anchor(plane),
solidcraft_doc::FeatureKind::Sketch { plane, .. } => super::sketch::face::anchor(plane),
_ => None,
});
let st = match attachment {
Some(anchor) => solvecraft_doc::state_in_face_anchor_frame(&doc, &st, anchor, comp),
None => Ok(solvecraft_doc::state_in_frame_at(&doc, &st, comp, owner_frame(s, &doc, comp))),
Some(anchor) => solidcraft_doc::state_in_face_anchor_frame(&doc, &st, anchor, comp),
None => Ok(solidcraft_doc::state_in_frame_at(&doc, &st, comp, owner_frame(s, &doc, comp))),
};
let st = st?;
let r = resolve(&doc, &vals, &st, &plane, kind, &src)?;
let name = if let LinkSource::Face { body, at } = &r.source {
st.body(body).and_then(|body| {
if let Some(index) = r.face_index {
solvecraft_doc::naming::face_names(body).get(index).filter(|name| !name.is_empty()).cloned()
solidcraft_doc::naming::face_names(body).get(index).filter(|name| !name.is_empty()).cloned()
} else {
solvecraft_doc::naming::face_name_at_body(body, *at)
solidcraft_doc::naming::face_name_at_body(body, *at)
}
})
} else {
@@ -312,7 +312,7 @@ fn spun_profile(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.model.state();
let ss = st.sketch(id).ok_or_else(|| EngineError::Other("the sketch is not evaluated".into()))?;
let ci = ss.sketch.curve_index(axis).ok_or_else(|| bad(cmd, format!("unknown curve `{axis}`")))?;
let Some(solvecraft_sketch::Shape::Line { a, b }) = ss.sketch.shape(ci) else { return Err(bad(cmd, "the axis must be a line")) };
let Some(solidcraft_sketch::Shape::Line { a, b }) = ss.sketch.shape(ci) else { return Err(bad(cmd, "the axis must be a line")) };
let (wa, wb) = (ss.plane.to_world(a), ss.plane.to_world(b));
(wa, wb - wa)
}
@@ -417,7 +417,7 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value
let (c, xy) = t.rsplit_once(':')?;
let n: Vec<f64> = xy.split(',').filter_map(|x| x.trim().parse().ok()).collect();
match n[..] {
[x, y] if x.is_finite() && y.is_finite() => Some((c.to_string(), solvecraft_geom::Vec2::new(x, y))),
[x, y] if x.is_finite() && y.is_finite() => Some((c.to_string(), solidcraft_geom::Vec2::new(x, y))),
_ => None,
}
});
@@ -444,7 +444,7 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value
} else if let Some(line) = mid {
// "mid:<line>": a new point at the line's midpoint, kept there.
let l = sk.curve_index(&line).ok_or_else(|| bad(cmd, format!("unknown curve `{line}`")))?;
let Some(solvecraft_sketch::Shape::Line { a, b }) = sk.shape(l) else { return Err(bad(cmd, format!("`{line}` is not a line"))) };
let Some(solidcraft_sketch::Shape::Line { a, b }) = sk.shape(l) else { return Err(bad(cmd, format!("`{line}` is not a line"))) };
let pi = sk.add_point((a + b) * 0.5, None)?;
sk.add_constraint(ConstraintKind::Midpoint { p: pi, l }, None)?;
sk.points.get(pi).map(|q| q.id.clone()).unwrap_or_default()
@@ -493,9 +493,9 @@ pub(super) fn project_internal_face(s: &mut Session, id: u64, body: &str, index:
let plane = sketch_plane(&doc, id)?;
let raw = s.model.state_before(id);
let component = doc.feature(id).map(|feature| feature.component).unwrap_or(0);
let state = solvecraft_doc::state_in_frame_at(&doc, &raw, component, owner_frame(s, &doc, component));
let resolved = solvecraft_doc::project::resolve_face_indices(&state, &plane, LinkKind::Project, body, &[index])?;
let face_name = state.body(body).and_then(|body| solvecraft_doc::naming::face_names(body).get(index).cloned());
let state = solidcraft_doc::state_in_frame_at(&doc, &raw, component, owner_frame(s, &doc, component));
let resolved = solidcraft_doc::project::resolve_face_indices(&state, &plane, LinkKind::Project, body, &[index])?;
let face_name = state.body(body).and_then(|body| solidcraft_doc::naming::face_names(body).get(index).cloned());
let mut sk = doc.sketch(id)?.clone();
let link = sk.add_link_with_wires(LinkKind::Project, resolved.source, &resolved.geom, &resolved.wires)?;
if let Some(entry) = sk.links.iter_mut().find(|entry| entry.id == link) {
@@ -512,7 +512,7 @@ pub(super) fn auto_project_face(s: &mut Session, id: u64, _at: Vec3) -> Result<O
let Some(anchor) = doc
.feature(id)
.and_then(|feature| match &feature.kind {
solvecraft_doc::FeatureKind::Sketch { plane, .. } => super::sketch::face::anchor(plane),
solidcraft_doc::FeatureKind::Sketch { plane, .. } => super::sketch::face::anchor(plane),
_ => None,
})
.cloned()
@@ -521,9 +521,9 @@ pub(super) fn auto_project_face(s: &mut Session, id: u64, _at: Vec3) -> Result<O
};
let raw = s.model.state_before(id);
let author = doc.feature(id).map(|feature| feature.component).unwrap_or(0);
let state = solvecraft_doc::state_in_face_anchor_frame(&doc, &raw, &anchor, author)?;
let state = solidcraft_doc::state_in_face_anchor_frame(&doc, &raw, &anchor, author)?;
let plane = doc.resolve_face_anchor(&anchor, author, &raw)?;
let resolved = solvecraft_doc::project::resolve_anchored_face(&doc, &state, &plane, LinkKind::Project, &anchor)?;
let resolved = solidcraft_doc::project::resolve_anchored_face(&doc, &state, &plane, LinkKind::Project, &anchor)?;
let mut sk = doc.sketch(id)?.clone();
let link = sk.add_link_with_wires(LinkKind::Project, resolved.source, &resolved.geom, &resolved.wires)?;
if let Some(link) = sk.links.iter_mut().find(|entry| entry.id == link) {
+13 -13
View File
@@ -33,13 +33,13 @@ fn manual_projection_captures_the_requested_coincident_body_producer() {
let sk = &state.sketch(id).unwrap().sketch;
assert!(!sk.links[0].lost);
assert_eq!(sk.links[0].face_name, Some(format!("F{producer}:+z")));
assert!(matches!(&sk.links[0].source,solvecraft_sketch::LinkSource::Face{body,at} if body=="Target" && (at.z-20.).abs()<1e-6));
assert!(matches!(&sk.links[0].source,solidcraft_sketch::LinkSource::Face{body,at} if body=="Target" && (at.z-20.).abs()<1e-6));
assert!(sk.points.iter().any(|point| point.pos.x > 19.));
assert!(sk.points.iter().any(|point| point.pos.y > 19.));
assert!((session.world_state().body("Control").unwrap().mesh().bounds().max.z - 10.).abs() < 1e-6);
};
verify(&s);
verify(&Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
verify(&Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
}
#[test]
@@ -48,19 +48,19 @@ fn initial_projection_retains_selected_face_when_closest_point_lands_on_shared_e
run(&mut s, "solid.box", json!({"length":20,"width":20,"height":10,"body_name":"Target"}));
let state = s.model.state();
let body = state.body("Target").unwrap();
let source = solvecraft_sketch::LinkSource::Face { body: "Target".into(), at: solvecraft_geom::Vec3::new(-0.1, 5., 10.5) };
let resolved = solvecraft_doc::project::resolve(
let source = solidcraft_sketch::LinkSource::Face { body: "Target".into(), at: solidcraft_geom::Vec3::new(-0.1, 5., 10.5) };
let resolved = solidcraft_doc::project::resolve(
&s.doc,
&std::collections::BTreeMap::new(),
&state,
&solvecraft_geom::Plane::XY,
solvecraft_sketch::LinkKind::Project,
&solidcraft_geom::Plane::XY,
solidcraft_sketch::LinkKind::Project,
&source,
)
.unwrap();
let index = resolved.face_index.unwrap();
let expected = solvecraft_doc::naming::face_names(body)[index].clone();
let solvecraft_sketch::LinkSource::Face { at, .. } = resolved.source else { panic!("not face") };
let expected = solidcraft_doc::naming::face_names(body)[index].clone();
let solidcraft_sketch::LinkSource::Face { at, .. } = resolved.source else { panic!("not face") };
assert!(at.x.abs() < 1e-6 && (at.z - 10.).abs() < 1e-6, "closestpoint must be on shared top/wall edge: {at:?}");
let plane = body.body.face_plane(index).unwrap();
let id = run(&mut s, "sketch.create", json!({"plane":{"origin":plane.origin,"x_dir":plane.x,"y_dir":plane.y}}))["sketch"].as_u64().unwrap();
@@ -71,7 +71,7 @@ fn initial_projection_retains_selected_face_when_closest_point_lands_on_shared_e
assert_eq!(sk.links[0].face_name, Some(expected));
assert!(!sk.links[0].lost);
assert_eq!(sk.curves.len(), 4);
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert_eq!(reopened.model.state().sketch(id).unwrap().sketch.curves, sk.curves);
}
@@ -83,7 +83,7 @@ fn box_part(s: &mut Session) {
let l = ids(&r["curves"]);
let sk = s.doc.sketch(s.active_sketch.unwrap()).unwrap();
let start = match sk.curves[0].kind {
solvecraft_sketch::CurveKind::Line { a, .. } => sk.points[a].id.clone(),
solidcraft_sketch::CurveKind::Line { a, .. } => sk.points[a].id.clone(),
_ => panic!(),
};
run(s, "sketch.constraint.coincident", json!({"a": start, "b": "origin"}));
@@ -446,7 +446,7 @@ fn sweeps_and_pipes_follow_3d_sketch_curves() {
// A sweep of a square profile drawn square to the curve at its start.
let (a, b) = (pts[0], pts[1]);
let t = (b - a).normalized().unwrap();
let x = t.cross(solvecraft_geom::Vec3::Z).normalized().unwrap();
let x = t.cross(solidcraft_geom::Vec3::Z).normalized().unwrap();
let y = t.cross(x);
run(&mut s, "sketch.create", json!({"plane": {"origin": [a.x, a.y, a.z], "x_dir": [x.x, x.y, x.z], "y_dir": [y.x, y.y, y.z]}}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [-1, -1], "p1": [1, 1]}));
@@ -488,11 +488,11 @@ fn project_and_include_from_other_bodies() {
run(&mut s, "sketch.finish", json!({}));
// Widen the second box: the projection follows (its far edge is at 70).
run(&mut s, "parameters.change", json!({"name": "w", "expression": "20 mm"}));
let id = s.doc.features.iter().find(|f| matches!(f.kind, solvecraft_doc::FeatureKind::Sketch { .. })).unwrap().id;
let id = s.doc.features.iter().find(|f| matches!(f.kind, solidcraft_doc::FeatureKind::Sketch { .. })).unwrap().id;
let si = run(&mut s, "sketch.inspect", json!({"sketch": id}));
assert!(xs(&si).iter().any(|x| (x - 70.0).abs() < 1e-6), "{:?}", xs(&si));
// Remove the cylinder: its silhouette and edge are lost, with a warning and no error.
let cyl = s.doc.features.iter().find(|f| matches!(f.kind, solvecraft_doc::FeatureKind::Cylinder { .. })).unwrap().id;
let cyl = s.doc.features.iter().find(|f| matches!(f.kind, solidcraft_doc::FeatureKind::Cylinder { .. })).unwrap().id;
run(&mut s, "timeline.delete", json!({"feature": cyl}));
let t = run(&mut s, "document.inspect", json!({}));
let row = t["timeline"].as_array().unwrap().iter().find(|r| r["id"] == json!(id)).cloned().unwrap();
+2 -2
View File
@@ -1,7 +1,7 @@
//! Bounded roots on exact curve pieces against an infinite line or complete circle.
//! Rational pieces are intersected in homogeneous coordinates, without sampled chords.
use solvecraft_geom::{Seg2, Vec2};
use solvecraft_sketch::{Shape, intersections};
use solidcraft_geom::{Seg2, Vec2};
use solidcraft_sketch::{Shape, intersections};
use crate::{Result, params::bad};
+24 -24
View File
@@ -2,7 +2,7 @@
//! which entity moves when a two-entity constraint is applied.
use serde_json::{Value, json};
use solvecraft_sketch::{DimFrame, Sketch, dim_frame, dim_layout};
use solidcraft_sketch::{DimFrame, Sketch, dim_frame, dim_layout};
use crate::Session;
@@ -53,7 +53,7 @@ fn every_dimension_kind_has_a_drawable_frame() {
let mut kinds = Vec::new();
for c in dims {
let f = dim_frame(&sk, &c.kind).unwrap_or_else(|| panic!("no frame for {c:?}"));
let l = dim_layout(&f, None, 0.1, solvecraft_geom::Vec2::new(1.0, 0.5));
let l = dim_layout(&f, None, 0.1, solidcraft_geom::Vec2::new(1.0, 0.5));
assert!(!l.lines.is_empty() && !l.arrows.is_empty(), "{c:?}");
assert!(l.text.is_finite());
kinds.push(std::mem::discriminant(&f));
@@ -80,10 +80,10 @@ fn dimension_text_is_stored_relative_to_the_dimension_and_follows_it() {
let f = dim_frame(&sk, &c.kind).expect("frame");
dim_layout(&f, c.text, 0.1, Default::default()).text
};
assert!(text(&s).dist(solvecraft_geom::Vec2::new(20.0, -8.0)) < 1e-9);
assert!(text(&s).dist(solidcraft_geom::Vec2::new(20.0, -8.0)) < 1e-9);
// Moved by the parameter name; then the line moves and the text goes with it.
run(&mut s, "sketch.dimension_text", json!({"dimension": param, "at": [30, 12]}));
assert!(text(&s).dist(solvecraft_geom::Vec2::new(30.0, 12.0)) < 1e-9);
assert!(text(&s).dist(solidcraft_geom::Vec2::new(30.0, 12.0)) < 1e-9);
run(&mut s, "sketch.move_point", json!({"point": "l1.start", "to": [0, 10]}));
run(&mut s, "sketch.move_point", json!({"point": "l1.end", "to": [40, 10]}));
// Still 10 along and 12 across from the middle of the (moved) line.
@@ -114,7 +114,7 @@ fn deleting_a_dimension_frees_its_parameter() {
// ---------------------------------------------------------------------------------------------
// Fix / Unfix
fn pt(s: &Session, id: &str) -> solvecraft_geom::Vec2 {
fn pt(s: &Session, id: &str) -> solidcraft_geom::Vec2 {
let sk = sketch(s);
sk.resolve_point(id).and_then(|i| sk.point(i)).unwrap_or_else(|| panic!("no point {id}"))
}
@@ -163,7 +163,7 @@ fn fixing_curves_holds_their_points_and_radius() {
assert!(si["curves"].as_array().is_some_and(|c| c.iter().all(|c| c["fixed"] == true)), "{si}");
// A fixed circle keeps centre and radius; a dimension on it conflicts.
run(&mut s, "sketch.move_point", json!({"point": "c1.center", "to": [0, 0]}));
assert_eq!(pt(&s, "c1.center"), solvecraft_geom::Vec2::new(10.0, 10.0));
assert_eq!(pt(&s, "c1.center"), solidcraft_geom::Vec2::new(10.0, 10.0));
assert!(s.execute("sketch.dimension", &json!({"entities": ["c1"], "value": 20})).is_err());
let r0 = sketch(&s).radius(0);
assert_eq!(r0, Some(5.0));
@@ -272,7 +272,7 @@ fn projected_geometry_can_become_construction() {
// Two-entity constraints: the first pick stays, the second moves
/// Positions of an entity's points (and its radius).
fn snap(s: &Session, id: &str) -> (Vec<solvecraft_geom::Vec2>, Option<f64>) {
fn snap(s: &Session, id: &str) -> (Vec<solidcraft_geom::Vec2>, Option<f64>) {
let sk = sketch(s);
if let Some(i) = sk.curve_index(id) {
let pts = sk.curves[i].kind.point_ids().into_iter().filter_map(|p| sk.point(p)).collect();
@@ -281,7 +281,7 @@ fn snap(s: &Session, id: &str) -> (Vec<solvecraft_geom::Vec2>, Option<f64>) {
(vec![pt(s, id)], None)
}
fn same(a: &(Vec<solvecraft_geom::Vec2>, Option<f64>), b: &(Vec<solvecraft_geom::Vec2>, Option<f64>)) -> bool {
fn same(a: &(Vec<solidcraft_geom::Vec2>, Option<f64>), b: &(Vec<solidcraft_geom::Vec2>, Option<f64>)) -> bool {
a.0.len() == b.0.len() && a.0.iter().zip(&b.0).all(|(p, q)| p.dist(*q) < 1e-9) && a.1.zip(b.1).is_none_or(|(x, y)| (x - y).abs() < 1e-9)
}
@@ -413,7 +413,7 @@ fn point_constraints_keep_the_first_pick() {
let (a0, l0) = (snap(&s, "p1"), snap(&s, "l1"));
run(&mut s, "sketch.constraint.symmetry", json!({"a": "p1", "b": "p2", "line": "l1"}));
assert!(same(&snap(&s, "p1"), &a0) && same(&snap(&s, "l1"), &l0));
assert!(pt(&s, "p2").dist(solvecraft_geom::Vec2::new(3.0, -7.0)) < 1e-6, "{:?}", pt(&s, "p2"));
assert!(pt(&s, "p2").dist(solidcraft_geom::Vec2::new(3.0, -7.0)) < 1e-6, "{:?}", pt(&s, "p2"));
}
#[test]
@@ -468,7 +468,7 @@ fn palette_options_are_kept_with_each_sketch() {
let first = s.active_sketch.expect("sketch");
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "sketch.create", json!({"plane": "XZ"}));
assert_eq!(sketch(&s).view, solvecraft_sketch::SketchView::default());
assert_eq!(sketch(&s).view, solidcraft_sketch::SketchView::default());
let o = run(&mut s, "sketch.options", json!({"sketch": first}));
assert_eq!(o["options"]["show_dimensions"], false);
// Saved with the design.
@@ -489,8 +489,8 @@ fn drag(s: &mut Session, e: &str, from: [f64; 2], to: [f64; 2]) {
run(s, "sketch.drag", json!({"entity": e, "from": from, "to": to}));
}
fn v(x: f64, y: f64) -> solvecraft_geom::Vec2 {
solvecraft_geom::Vec2::new(x, y)
fn v(x: f64, y: f64) -> solidcraft_geom::Vec2 {
solidcraft_geom::Vec2::new(x, y)
}
#[test]
@@ -610,7 +610,7 @@ fn arm_drags(links: Links) -> (usize, usize) {
let v3 = v2 + v(b * (th1 + bend).cos(), b * (th1 + bend).sin());
let (tr, tt) = (lo + rnd() * (hi - lo), rnd() * TAU);
let to = o + v(tr * tt.cos(), tr * tt.sin());
let clear = |p: solvecraft_geom::Vec2| (lo..=hi).contains(&p.dist(o));
let clear = |p: solidcraft_geom::Vec2| (lo..=hi).contains(&p.dist(o));
if !(0..=100).all(|i| clear(v3 + (to - v3) * (i as f64 / 100.0))) {
continue;
}
@@ -697,13 +697,13 @@ fn hanging_triangle_flips(n: usize) -> usize {
let v3 = v2 + v(b * (th1 + bend).cos(), b * (th1 + bend).sin());
let (tr, tt) = (lo + rnd() * (hi - lo), rnd() * TAU);
let to = o + v(tr * tt.cos(), tr * tt.sin());
let clear = |p: solvecraft_geom::Vec2| (lo..=hi).contains(&p.dist(o));
let clear = |p: solidcraft_geom::Vec2| (lo..=hi).contains(&p.dist(o));
if !(0..=100).all(|i| clear(v3 + (to - v3) * (i as f64 / 100.0))) {
continue;
}
kept += 1;
let turn = rnd() * TAU;
let u = solvecraft_geom::Vec2::from_angle(turn);
let u = solidcraft_geom::Vec2::from_angle(turn);
let normal = u.perp() * if rnd() < 0.5 { 1.0 } else { -1.0 };
let (tq, tr) = (v3 + u * 3.9, v3 + u * 1.95 + normal * (4.0f64 - 1.95 * 1.95).sqrt());
{
@@ -821,7 +821,7 @@ fn the_step_cap_leaves_the_plan_and_is_restored() {
/// Stored (counter-clockwise) sweep of an arc, degrees in [0, 360).
fn sweep_deg(s: &Session, arc: &str) -> f64 {
let sk = sketch(s);
let Some(solvecraft_sketch::CurveKind::Arc { c, a, b }) = sk.curve_index(arc).map(|i| sk.curves[i].kind.clone()) else { panic!("no arc {arc}") };
let Some(solidcraft_sketch::CurveKind::Arc { c, a, b }) = sk.curve_index(arc).map(|i| sk.curves[i].kind.clone()) else { panic!("no arc {arc}") };
let (c, a, b) = (sk.points[c].pos, sk.points[a].pos, sk.points[b].pos);
((b - c).angle() - (a - c).angle()).rem_euclid(std::f64::consts::TAU).to_degrees()
}
@@ -893,11 +893,11 @@ fn a_dragged_fillet_stays_in_its_corner() {
run(&mut s, "sketch.constraint.fix", json!({"entities": ["l1.start"]}));
run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "l1", "mode": "horizontal"}));
let sk0 = sketch(&s);
let arc = sk0.curves.iter().position(|c| matches!(c.kind, solvecraft_sketch::CurveKind::Arc { .. })).expect("fillet arc");
let arc = sk0.curves.iter().position(|c| matches!(c.kind, solidcraft_sketch::CurveKind::Arc { .. })).expect("fillet arc");
let arc_id = sk0.curves[arc].id.clone();
let inside = |s: &Session| {
let sk = sketch(s);
let side = |p: solvecraft_geom::Vec2, a: solvecraft_geom::Vec2, b: solvecraft_geom::Vec2| (b - a).cross(p - a).signum();
let side = |p: solidcraft_geom::Vec2, a: solidcraft_geom::Vec2, b: solidcraft_geom::Vec2| (b - a).cross(p - a).signum();
let c = sk.center(arc).expect("centre");
let (p0, k1, k2, p3) = (pt(s, "l1.start"), pt(s, "l1.end"), pt(s, "l2.start"), pt(s, "l2.end"));
let r = sk.radius(arc).unwrap_or(0.0);
@@ -1029,7 +1029,7 @@ fn the_drag_step_counts_every_held_size_but_the_dragged_length() {
run(&mut s, "sketch.line", json!({"points": [[100, 100], [100, 110]], "ids": ["la"]}));
run(&mut s, "sketch.line", json!({"points": [[100.05, 120], [110, 120]], "ids": ["lb"], "connect": false}));
run(&mut s, "sketch.dimension", json!({"entities": ["la.start", "lb.start"], "type": "horizontal", "value": 0.05}));
assert!(sketch(&s).constraints.iter().any(|c| matches!(c.kind, solvecraft_sketch::ConstraintKind::DistanceX { .. })));
assert!(sketch(&s).constraints.iter().any(|c| matches!(c.kind, solidcraft_sketch::ConstraintKind::DistanceX { .. })));
let steps = drag_steps(&s, "la.start", [100.0, 100.0], [101.0, 100.0]);
drag(&mut s, "la.start", [100.0, 100.0], [101.0, 100.0]);
let (a, b) = (pt(&s, "la.start"), pt(&s, "lb.start"));
@@ -1047,7 +1047,7 @@ fn a_circle_centred_on_a_dragged_end_does_not_set_the_step() {
run(&mut s, "sketch.constraint.fix", json!({"entities": ["la.start"]}));
run(&mut s, "sketch.dimension", json!({"entities": ["la"], "value": 20}));
run(&mut s, "sketch.circle.center", json!({"center": [120, 100], "radius": 0.01}));
assert!(sketch(&s).constraints.iter().any(|c| matches!(c.kind, solvecraft_sketch::ConstraintKind::Coincident { .. })));
assert!(sketch(&s).constraints.iter().any(|c| matches!(c.kind, solidcraft_sketch::ConstraintKind::Coincident { .. })));
let steps = drag_steps(&s, "la.end", [120.0, 100.0], [120.0, 101.0]);
eprintln!("1 mm drag next to a 0.01 mm circle on the end: {steps} steps");
assert_eq!(steps, 1);
@@ -1208,8 +1208,8 @@ fn three_d_curves_need_the_option_and_move_joined() {
let sk = sketch(&s);
let w = &sk.wires[sk.wire_index(&sid).expect("spline")];
assert!(w.pts.len() > 10);
assert!(w.pts.first().expect("p").dist(solvecraft_geom::Vec3::new(10.0, 0.0, 10.0)) < 1e-12);
assert!(w.pts.last().expect("p").dist(solvecraft_geom::Vec3::new(20.0, 0.0, 20.0)) < 1e-12);
assert!(w.pts.first().expect("p").dist(solidcraft_geom::Vec3::new(10.0, 0.0, 10.0)) < 1e-12);
assert!(w.pts.last().expect("p").dist(solidcraft_geom::Vec3::new(20.0, 0.0, 20.0)) < 1e-12);
// The shared corner moves for both lines (and the spline that starts there).
let r = run(&mut s, "sketch.move3d", json!({"wire": ids[1], "index": 1, "by": [0, 0, 5]}));
assert_eq!(r["moved"], 2);
@@ -1253,7 +1253,7 @@ fn pipe_and_sweep_follow_3d_sketch_curves() {
/// past that above or below them the horizontal distance, beside them the vertical one (#257).
#[test]
fn auto_dimension_orientation_follows_where_its_text_is_placed() {
use solvecraft_sketch::ConstraintKind;
use solidcraft_sketch::ConstraintKind;
// The ends of a 30 × 40 line: 50 long, and the slope keeps "across" and "past" distinct.
let dimension = |end: [f64; 2], entities: Value, extra: Value| {
let mut s = new_sketch();
@@ -1331,7 +1331,7 @@ fn hung_elbow_side(s: &Session) -> f64 {
}
/// The hand of `hung_arm` swung by `angle` about the shoulder.
fn hung_hand_target(s: &Session, angle: f64, scale: f64) -> solvecraft_geom::Vec2 {
fn hung_hand_target(s: &Session, angle: f64, scale: f64) -> solidcraft_geom::Vec2 {
let (b, h) = (pt(s, "ab.end"), pt(s, "eh.end"));
let r = h - b;
let (c, si) = (angle.cos(), angle.sin());
+4 -4
View File
@@ -2,7 +2,7 @@
//! solids).
use serde_json::Value;
use solvecraft_doc::{FaceAt, FeatureKind, PlaneRef, ProfileSel, expr::Kind};
use solidcraft_doc::{FaceAt, FeatureKind, PlaneRef, ProfileSel, expr::Kind};
use super::CommandSpec;
use super::features::{add_feature, check_expr, feature_sketch, operation, plane_param, profiles};
@@ -34,7 +34,7 @@ pub static COMMANDS: &[CommandSpec] = &[
fn patch(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "surface.patch";
let edges: Vec<solvecraft_geom::Vec3> = p.get("edges").and_then(Value::as_array).map(|a| a.iter().filter_map(vec3).collect()).unwrap_or_default();
let edges: Vec<solidcraft_geom::Vec3> = p.get("edges").and_then(Value::as_array).map(|a| a.iter().filter_map(vec3).collect()).unwrap_or_default();
if !edges.is_empty() {
if edges.len() > 10_000 {
return Err(bad(cmd, "too many edges"));
@@ -88,7 +88,7 @@ fn extend(s: &mut Session, p: &Value) -> Result<Value> {
if s.model.state().body(&body).is_none() {
return Err(bad(cmd, format!("no body `{body}`")));
}
let edges: Vec<solvecraft_geom::Vec3> = p.get("edges").and_then(Value::as_array).map(|a| a.iter().filter_map(vec3).collect()).unwrap_or_default();
let edges: Vec<solidcraft_geom::Vec3> = p.get("edges").and_then(Value::as_array).map(|a| a.iter().filter_map(vec3).collect()).unwrap_or_default();
if edges.is_empty() || edges.len() > 10_000 {
return Err(bad(cmd, "`edges` must list points on the edges to extend"));
}
@@ -126,7 +126,7 @@ mod tests {
}
fn volume(s: &Session, name: &str) -> f64 {
let st = s.world_state();
solvecraft_kernel::measure(&st.body(name).unwrap().body).unwrap().volume
solidcraft_kernel::measure(&st.body(name).unwrap().body).unwrap().volume
}
/// A rectangle patched, trimmed by a plane and thickened; a box's faces stitched back.
+2 -2
View File
@@ -2,7 +2,7 @@
//! View state only: no geometry changes and no undo step.
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use super::CommandSpec;
use crate::params::{bad, bool_, num, vec3};
@@ -109,7 +109,7 @@ mod tests {
s.execute("inspect.environment_map", &json!({})).unwrap();
assert_eq!(s.analysis, Some(SurfaceAnalysis::Environment));
s.execute("inspect.accessibility", &json!({"direction": [0, 0, -3]})).unwrap();
assert_eq!(s.analysis, Some(SurfaceAnalysis::Access { dir: solvecraft_geom::Vec3::new(0.0, 0.0, -1.0) }));
assert_eq!(s.analysis, Some(SurfaceAnalysis::Access { dir: solidcraft_geom::Vec3::new(0.0, 0.0, -1.0) }));
s.execute("inspect.curvature_map", &json!({"clear": true})).unwrap();
assert_eq!(s.analysis, None);
assert_eq!(s.undo.len(), undo, "no undo steps");
+4 -4
View File
@@ -2,7 +2,7 @@
//! 40 × 30 × 20 box at the origin, a cylinder, a sphere and a torus.
use serde_json::{Value, json};
use solvecraft_geom::{Plane, Vec3};
use solidcraft_geom::{Plane, Vec3};
use crate::Session;
use crate::view::{construction_axes, construction_planes, construction_points};
@@ -236,7 +236,7 @@ fn construction_on_a_3d_wire_ignores_its_sketch_plane_and_follows_edits() {
let updated = plane(&s, "BridgePlane");
assert!(near(updated.origin, Vec3::new(30.0, 27.5, 25.0)), "{updated:?}");
assert!(parallel(updated.normal(), Vec3::new(40.0, 15.0, 30.0)));
let restored = solvecraft_doc::format::read(&s.doc.to_json()).unwrap();
let restored = solidcraft_doc::format::read(&s.doc.to_json()).unwrap();
let mut cold = Session { doc: restored.into(), ..Default::default() };
cold.refresh();
assert!(near(plane(&cold, "BridgePlane").origin, updated.origin));
@@ -349,7 +349,7 @@ fn ambiguous_plane_aliases_refuse_without_document_or_history_changes() {
let cut = run(&mut s, "inspect.section", json!({"plane":id}));
assert_eq!(cut["section"]["origin"], json!([0.0, 0.0, 19.05]));
run(&mut s, "parameters.change", json!({"name":"height","expression":"25.4 mm"}));
let mut reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let old = run(&mut reopened, "sketch.inspect", json!({"sketch":"Sketch1"}));
assert_eq!(old["plane"]["origin"], json!([0.0, 0.0, 0.0]), "saved origin reference is not reinterpreted");
let linked = run(&mut reopened, "sketch.inspect", json!({"sketch":"Sketch2"}));
@@ -380,7 +380,7 @@ fn disambiguated_top_plane_pocket_survives_undo_and_cold_reopen() {
assert_eq!(s.doc.to_json(), before);
run(&mut s, "edit.redo", json!({}));
assert_eq!(s.doc.to_json(), saved);
let mut reopened = Session::new(solvecraft_doc::Document::from_json(&saved).unwrap());
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&saved).unwrap());
let measured = run(&mut reopened, "inspect.measure", json!({}));
assert!((measured["total"]["volume_mm3"].as_f64().unwrap() - expected).abs() < 1e-6, "{measured}");
}
+17 -17
View File
@@ -3,8 +3,8 @@
//!
//! - **Panics** are caught by a hook. One inside a command becomes a report with the design as
//! it was before the command and the command itself, so `solidcraft-cli run replay.json --in
//! design.solvecraft` reproduces it. Panics the third-party kernel raises (and
//! `solvecraft_kernel::guard` turns into errors) are reported the same way, marked recovered.
//! design.solidcraft` reproduces it. Panics the third-party kernel raises (and
//! `solidcraft_kernel::guard` turns into errors) are reported the same way, marked recovered.
//! - **Crashes no hook sees** (an abort, a stack overflow, a segfault, the process being killed)
//! are found at the next start: every running app keeps a small, locked command journal; a
//! journal nobody holds any more belongs to an app that died. Its report carries the design
@@ -18,7 +18,7 @@
//! reports, [`MAX_TOTAL_BYTES`] on disk, nothing older than [`MAX_AGE_DAYS`] days.
//!
//! Reports are folders `crashes/<time>-<signature>/` holding `report.json`, and when known
//! `design.solvecraft` and `replay.json`. Nothing is ever sent anywhere.
//! `design.solidcraft` and `replay.json`. Nothing is ever sent anywhere.
use std::backtrace::Backtrace;
use std::cell::{Cell, RefCell};
@@ -30,7 +30,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock};
use serde_json::{Value, json};
use solvecraft_doc::Document;
use solidcraft_doc::Document;
use crate::Session;
@@ -53,7 +53,7 @@ const RECENT: usize = 32;
const MAX_FRAMES: usize = 60;
const MAX_MESSAGE: usize = 2000;
/// The report format.
pub const FORMAT: &str = "solvecraft-crash/1";
pub const FORMAT: &str = "solidcraft-crash/1";
/// What the app is (shown in every report).
#[derive(Clone, Debug, Default)]
@@ -79,7 +79,7 @@ pub struct Summary {
pub version: String,
/// The report's folder.
pub path: String,
/// `replay.json` and `design.solvecraft` are there: the crash can be replayed.
/// `replay.json` and `design.solidcraft` are there: the crash can be replayed.
pub replayable: bool,
}
@@ -114,16 +114,16 @@ pub fn enabled() -> bool {
STATE.get().is_some()
}
/// The crash folder, next to the recovery folder. `SOLVECRAFT_CRASH_DIR` overrides it.
/// The crash folder, next to the recovery folder. `SOLIDCRAFT_CRASH_DIR` overrides it.
pub fn default_dir() -> Option<PathBuf> {
if let Some(s) = STATE.get() {
return Some(s.dir.clone());
}
// Tests never touch the user's real crash folder.
if cfg!(test) {
return Some(std::env::temp_dir().join(format!("solvecraft-test-crashes-{}", std::process::id())));
return Some(std::env::temp_dir().join(format!("solidcraft-test-crashes-{}", std::process::id())));
}
if let Some(d) = std::env::var_os("SOLVECRAFT_CRASH_DIR").filter(|d| !d.is_empty()) {
if let Some(d) = solidcraft_geom::env::var_os("CRASH_DIR").filter(|d| !d.is_empty()) {
return Some(PathBuf::from(d));
}
crate::recovery::default_dir().and_then(|d| d.parent().map(|p| p.join("crashes")))
@@ -355,7 +355,7 @@ fn hook(info: &std::panic::PanicHookInfo<'_>) -> bool {
let message = limited(info.payload_as_str().unwrap_or("(not a text message)"), MAX_MESSAGE);
let location = info.location().map_or_else(|| "(unknown)".to_string(), |l| format!("{}:{}", l.file(), l.line()));
let sig = signature(&location, &message);
let recovered = solvecraft_kernel::guarded();
let recovered = solidcraft_kernel::guarded();
let in_command = IN_COMMAND.with(Cell::get) > 0;
// Already reported (this run or before): count it, nothing else.
let repeat = match state.known.lock() {
@@ -467,7 +467,7 @@ pub(crate) fn command_done(
let pending: Vec<Panic> = PENDING.with(|q| std::mem::take(&mut *q.borrow_mut()));
for p in pending {
let kind = if p.recovered { "kernel_panic_recovered" } else { "command_panic" };
let design = solvecraft_io::write_design(before);
let design = solidcraft_io::write_design(before);
let replay = json!({ "commands": [{ "command": id, "params": params }] });
let recent = if journaled { state.journal_path.as_deref().map(|p| read_journal(p).recent).unwrap_or_default() } else { Vec::new() };
let extra = Extra {
@@ -786,8 +786,8 @@ fn write_report(state: &State, kind: &str, p: Option<&Panic>, extra: Extra) -> O
let mut notes = extra.notes;
if let Some(d) = &extra.design {
if d.len() <= MAX_DESIGN_BYTES {
if solvecraft_io::write_atomic(&folder.join("design.solvecraft"), d, false).is_ok() {
files.insert("design".into(), json!("design.solvecraft"));
if solidcraft_io::write_atomic(&folder.join("design.solidcraft"), d, false).is_ok() {
files.insert("design".into(), json!("design.solidcraft"));
}
} else {
notes.push(format!("the design is larger than {} MB and was not copied", MAX_DESIGN_BYTES >> 20));
@@ -795,7 +795,7 @@ fn write_report(state: &State, kind: &str, p: Option<&Panic>, extra: Extra) -> O
}
if let Some(r) = &extra.replay
&& let Ok(b) = serde_json::to_vec_pretty(r)
&& solvecraft_io::write_atomic(&folder.join("replay.json"), &b, false).is_ok()
&& solidcraft_io::write_atomic(&folder.join("replay.json"), &b, false).is_ok()
{
files.insert("replay".into(), json!("replay.json"));
}
@@ -828,7 +828,7 @@ fn write_report(state: &State, kind: &str, p: Option<&Panic>, extra: Extra) -> O
"notes": notes,
});
let bytes = serde_json::to_vec_pretty(&report).ok()?;
solvecraft_io::write_atomic(&folder.join("report.json"), &bytes, false).ok()?;
solidcraft_io::write_atomic(&folder.join("report.json"), &bytes, false).ok()?;
prune(&state.dir, t);
return Some(id);
}
@@ -846,7 +846,7 @@ fn bump(dir: &Path, id: &str, n: u64) {
v["count"] = json!(count);
v["last_seen"] = json!(utc(now()));
if let Ok(b) = serde_json::to_vec_pretty(&v) {
let _ = solvecraft_io::write_atomic(&path, &b, false);
let _ = solidcraft_io::write_atomic(&path, &b, false);
}
}
@@ -951,7 +951,7 @@ pub fn read(dir: &Path, id: &str) -> std::result::Result<Value, String> {
let folder = dir.join(id);
let mut v = read_report_file(&folder.join("report.json")).ok_or_else(|| format!("no crash report `{id}`"))?;
v["path"] = json!(folder.to_string_lossy());
for (k, f) in [("design_path", "design.solvecraft"), ("replay_path", "replay.json")] {
for (k, f) in [("design_path", "design.solidcraft"), ("replay_path", "replay.json")] {
if folder.join(f).is_file() {
v[k] = json!(folder.join(f).to_string_lossy());
}
+11 -11
View File
@@ -2,7 +2,7 @@ use super::*;
use crate::Session;
fn fresh_dir(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("solvecraft-crash-test-{name}-{}", std::process::id()));
let d = std::env::temp_dir().join(format!("solidcraft-crash-test-{name}-{}", std::process::id()));
let _ = fs::remove_dir_all(&d);
d
}
@@ -12,10 +12,10 @@ fn build() -> Build {
}
/// The child half of the process tests: does nothing unless a parent asks, then installs the
/// hook and crashes the way `SOLVECRAFT_CRASH_CHILD` says.
/// hook and crashes the way `SOLIDCRAFT_CRASH_CHILD` says.
#[test]
fn child() {
let (Some(dir), Some(mode)) = (std::env::var_os("SOLVECRAFT_CRASH_CHILD_DIR"), std::env::var("SOLVECRAFT_CRASH_CHILD").ok()) else {
let (Some(dir), Some(mode)) = (solidcraft_geom::env::var_os("CRASH_CHILD_DIR"), solidcraft_geom::env::var("CRASH_CHILD")) else {
return;
};
let fresh = install(PathBuf::from(dir), build());
@@ -58,9 +58,9 @@ fn child() {
fn run_child(dir: &Path, mode: &str) -> (String, bool) {
let out = std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", "crash::tests::child", "--test-threads=1", "--nocapture"])
.env("SOLVECRAFT_CRASH_CHILD_DIR", dir)
.env("SOLVECRAFT_CRASH_CHILD", mode)
.env("SOLVECRAFT_CRASH_TEST", "1")
.env("SOLIDCRAFT_CRASH_CHILD_DIR", dir)
.env("SOLIDCRAFT_CRASH_CHILD", mode)
.env("SOLIDCRAFT_CRASH_TEST", "1")
.env("RUST_BACKTRACE", "0")
.output()
.unwrap();
@@ -96,7 +96,7 @@ fn a_command_panic_is_reported_once_with_the_design_and_the_command() {
assert!(!bt.iter().any(|f| f.as_str().unwrap().contains("crash::hook")), "the hook's own frames are dropped: {bt:?}");
assert_eq!(v["recent_commands"][0]["command"], "solid.box");
// The design from before the command, and the command: together they reproduce the crash.
let design = solvecraft_io::read_design(&fs::read(v["design_path"].as_str().unwrap()).unwrap()).unwrap();
let design = solidcraft_io::read_design(&fs::read(v["design_path"].as_str().unwrap()).unwrap()).unwrap();
assert_eq!(design.features.len(), 1);
let replay: Value = serde_json::from_slice(&fs::read(v["replay_path"].as_str().unwrap()).unwrap()).unwrap();
assert_eq!(replay["commands"], json!([{"command": "diagnostics.crash_test", "params": {"mode": "panic"}}]));
@@ -167,7 +167,7 @@ fn a_journal_reads_back_its_base_commands_and_the_one_in_flight() {
r#"{"base":"new"}"#,
r#"{"run":"solid.box","params":{"length":1}}"#,
r#"{"ok":true}"#,
r#"{"base":{"autosave":"/tmp/a.solvecraft"}}"#,
r#"{"base":{"autosave":"/tmp/a.solidcraft"}}"#,
r#"{"run":"solid.fillet","params":{"radius":1}}"#,
r#"{"ok":false,"error":"no edges"}"#,
"not json",
@@ -178,7 +178,7 @@ fn a_journal_reads_back_its_base_commands_and_the_one_in_flight() {
fs::write(&p, lines.join("\n")).unwrap();
let j = read_journal(&p);
assert_eq!(j.header["version"], "1.0");
assert_eq!(j.base, json!({"autosave": "/tmp/a.solvecraft"}));
assert_eq!(j.base, json!({"autosave": "/tmp/a.solidcraft"}));
assert_eq!(j.replay, vec![json!({"command": "solid.shell", "params": {"thickness": 1}})], "failed commands change nothing");
assert_eq!(j.recent.len(), 2);
assert_eq!(j.recent[0]["error"], "no edges");
@@ -274,9 +274,9 @@ fn dates_are_formatted_in_utc() {
fn backtraces_lose_the_panic_machinery_and_the_home_folder() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/u".into());
let bt = format!(
" 0: std::backtrace::Backtrace::force_capture\n at /rustc/abc123/library/std/src/backtrace.rs:312:13\n 1: hook\n 1: {{closure#3}}\n 1: <alloc::boxed::Box<F,A> as core::ops::function::Fn<Args>>::call\n 2: std::panicking::rust_panic_with_hook\n 3: core::panicking::panic_fmt\n 4: solvecraft_kernel::fillet::blend\n at {home}/src/kernel/fillet.rs:10:5\n 5: do_call<F>\n at /rustc/abc/library/std/src/panicking.rs:1:1\n 5: std::sys::backtrace::__rust_begin_short_backtrace\n 6: main"
" 0: std::backtrace::Backtrace::force_capture\n at /rustc/abc123/library/std/src/backtrace.rs:312:13\n 1: hook\n 1: {{closure#3}}\n 1: <alloc::boxed::Box<F,A> as core::ops::function::Fn<Args>>::call\n 2: std::panicking::rust_panic_with_hook\n 3: core::panicking::panic_fmt\n 4: solidcraft_kernel::fillet::blend\n at {home}/src/kernel/fillet.rs:10:5\n 5: do_call<F>\n at /rustc/abc/library/std/src/panicking.rs:1:1\n 5: std::sys::backtrace::__rust_begin_short_backtrace\n 6: main"
);
assert_eq!(frames(&bt), ["solvecraft_kernel::fillet::blend (~/src/kernel/fillet.rs:10:5)"]);
assert_eq!(frames(&bt), ["solidcraft_kernel::fillet::blend (~/src/kernel/fillet.rs:10:5)"]);
}
#[test]
+3 -3
View File
@@ -87,13 +87,13 @@ fn revolve_to_a_cylinder_follows_it() {
}
let st = s.model.state();
let (arc, post) = (st.body("Arc").unwrap(), st.body("Post").unwrap());
let overlap = solvecraft_kernel::boolean(&arc.body, &post.body, solvecraft_kernel::BoolOp::Intersect)
let overlap = solidcraft_kernel::boolean(&arc.body, &post.body, solidcraft_kernel::BoolOp::Intersect)
.ok()
.flatten()
.map(|b| solvecraft_kernel::measure(&b).map(|m| m.volume).unwrap_or(0.0))
.map(|b| solidcraft_kernel::measure(&b).map(|m| m.volume).unwrap_or(0.0))
.unwrap_or(0.0);
assert!(overlap < 1e-3, "no overlap: {overlap}");
let v = solvecraft_kernel::measure(&arc.body).unwrap().volume;
let v = solidcraft_kernel::measure(&arc.body).unwrap().volume;
let full = PI * (225.0 - 100.0) * 5.0;
// The post is at a quarter turn one way (three quarters the other): the ring stops
// short of the post's centre line, within its half-width of it.
+7 -7
View File
@@ -10,8 +10,8 @@
//! without moved components).
use serde_json::{Map, Value};
use solvecraft_doc::{Mat, apply_point, apply_vector, is_identity, map_point_expr, mat_inverse};
use solvecraft_geom::Vec3;
use solidcraft_doc::{Mat, apply_point, apply_vector, is_identity, map_point_expr, mat_inverse};
use solidcraft_geom::Vec3;
use crate::Session;
@@ -337,14 +337,14 @@ pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
// component is active. Body-relative picks still use the picked body's frame.
let mut context = s.scratch();
context.active_component = feature.component;
if let solvecraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &feature.kind {
if let solidcraft_doc::FeatureKind::Emboss { face: Some(face), .. } = &feature.kind {
context.active_occurrence_path = (!face.author_path.is_empty()).then(|| face.author_path.clone());
}
let mut inner = to_local(&context, cmd, params.get("params")?)?;
// An untouched explicit axis is displayed in world coordinates by Edit
// Feature. Keep its exact saved values instead of an affine round trip.
if cmd == "solid.revolve"
&& let solvecraft_doc::FeatureKind::Revolve { axis: solvecraft_doc::AxisRef::Line { origin, dir }, .. } = &feature.kind
&& let solidcraft_doc::FeatureKind::Revolve { axis: solidcraft_doc::AxisRef::Line { origin, dir }, .. } = &feature.kind
{
let frame = context.active_component_frame();
let displayed = serde_json::json!({"origin":pt(apply_point(&frame,*origin)),"dir":pt(apply_vector(&frame,*dir))});
@@ -353,8 +353,8 @@ pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
}
}
let points = match (&feature.kind, cmd) {
(solvecraft_doc::FeatureKind::Revolve { to, .. }, "solid.revolve") => [("to", *to), ("from", None)],
(solvecraft_doc::FeatureKind::Extrude { extent, .. }, "solid.extrude") => [("to", extent.to), ("from", extent.from)],
(solidcraft_doc::FeatureKind::Revolve { to, .. }, "solid.revolve") => [("to", *to), ("from", None)],
(solidcraft_doc::FeatureKind::Extrude { extent, .. }, "solid.extrude") => [("to", extent.to), ("from", extent.from)],
_ => [("to", None), ("from", None)],
};
for (key, point) in points {
@@ -383,7 +383,7 @@ pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
let key = params.get("sketch").and_then(|value| value.as_str().map(str::to_string).or_else(|| value.as_u64().map(|id| id.to_string())))?;
let feature = s.doc.find_feature(&key)?;
let path = crate::cmd::author_path_for_rebind(s, feature, params, id).ok()?;
let frame = solvecraft_doc::EmbossFace::component_frame(&s.placed_document(), feature.component, &path).ok()?;
let frame = solidcraft_doc::EmbossFace::component_frame(&s.placed_document(), feature.component, &path).ok()?;
command_frames.active = frame;
command_frames.sketch = frame;
}
+2 -2
View File
@@ -2,7 +2,7 @@
//! world land in the component's own frame (user bug #4).
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use crate::Session;
@@ -190,7 +190,7 @@ fn canvases_move_with_their_component() {
let root = s.doc.canvases.iter().find(|c| c.name == "Root").unwrap();
assert!(near(s.doc.canvas_plane(&vals, root).unwrap().origin, Vec3::ZERO));
// The component survives a save and reopen.
let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let back = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(back.canvases, s.doc.canvases);
}
+7 -7
View File
@@ -1,4 +1,4 @@
//! Random modelling sessions for robustness testing (`solvecraft-cli fuzz`, docs/robustness.md).
//! Random modelling sessions for robustness testing (`solidcraft-cli fuzz`, docs/robustness.md).
//!
//! A seeded generator builds realistic feature sequences: a base (box, extruded sketch profile
//! with an arc, revolved section), then joins and cuts of boxes, cylinders and spheres placed on
@@ -8,9 +8,9 @@
use serde::Serialize;
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solvecraft_kernel::fuzz::Rng;
pub use solvecraft_kernel::fuzz::{BoolCase, boolean_case, boolean_shapes};
use solidcraft_geom::Vec3;
use solidcraft_kernel::fuzz::Rng;
pub use solidcraft_kernel::fuzz::{BoolCase, boolean_case, boolean_shapes};
use crate::{EngineError, Session};
@@ -64,19 +64,19 @@ pub struct Run {
pub unsupported: Vec<(String, String)>,
/// Steps whose generated parameters a command refused.
pub rejected: usize,
pub boolean_fallbacks: std::collections::BTreeMap<solvecraft_kernel::BooleanStrategy, usize>,
pub boolean_fallbacks: std::collections::BTreeMap<solidcraft_kernel::BooleanStrategy, usize>,
pub boolean_reports_truncated: bool,
}
impl Run {
/// The commands as a script for `solvecraft-cli run`.
/// The commands as a script for `solidcraft-cli run`.
pub fn script(&self) -> Value {
Value::Array(self.steps.iter().flat_map(|s| s.commands.iter().map(|(c, p)| json!({"command": c, "params": p}))).collect())
}
}
fn total_volume(s: &Session) -> f64 {
s.model.state().bodies.iter().filter_map(|b| solvecraft_kernel::measure(&b.body).ok()).map(|m| m.volume).sum()
s.model.state().bodies.iter().filter_map(|b| solidcraft_kernel::measure(&b.body).ok()).map(|m| m.volume).sum()
}
fn classify(e: &EngineError) -> Outcome {
+39 -39
View File
@@ -1,4 +1,4 @@
//! The SolveCraft engine.
//! The SolidCraft engine.
//!
//! Every user-visible action is a command with a stable, dotted, lower-case id
//! (`solid.extrude`, `sketch.create`, `solid.fillet`, `timeline.rollback`). Commands take JSON
@@ -22,16 +22,16 @@ use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use solvecraft_doc::{Document, Model, ModelState};
use solvecraft_geom::Vec3;
use solidcraft_doc::{Document, Model, ModelState};
use solidcraft_geom::Vec3;
pub use cmd::{CommandInfo, CommandSpec, auto_operation, command_specs, find_command};
pub use solvecraft_doc as doc;
pub use solvecraft_geom as geom;
pub use solvecraft_io as io;
pub use solvecraft_kernel as kernel;
pub use solvecraft_render as render;
pub use solvecraft_sketch as sketch;
pub use solidcraft_doc as doc;
pub use solidcraft_geom as geom;
pub use solidcraft_io as io;
pub use solidcraft_kernel as kernel;
pub use solidcraft_render as render;
pub use solidcraft_sketch as sketch;
/// Longest accepted request line on the JSON-lines channels (MCP over stdio and the control
/// server over loopback), so both refuse runaway input at the same size.
@@ -52,23 +52,23 @@ pub enum EngineError {
Internal(String, String),
}
impl From<solvecraft_doc::DocError> for EngineError {
fn from(e: solvecraft_doc::DocError) -> Self {
impl From<solidcraft_doc::DocError> for EngineError {
fn from(e: solidcraft_doc::DocError) -> Self {
EngineError::Other(e.to_string())
}
}
impl From<solvecraft_sketch::SketchError> for EngineError {
fn from(e: solvecraft_sketch::SketchError) -> Self {
impl From<solidcraft_sketch::SketchError> for EngineError {
fn from(e: solidcraft_sketch::SketchError) -> Self {
EngineError::Other(e.to_string())
}
}
impl From<solvecraft_kernel::KernelError> for EngineError {
fn from(e: solvecraft_kernel::KernelError) -> Self {
impl From<solidcraft_kernel::KernelError> for EngineError {
fn from(e: solidcraft_kernel::KernelError) -> Self {
EngineError::Other(e.to_string())
}
}
impl From<solvecraft_io::IoError> for EngineError {
fn from(e: solvecraft_io::IoError) -> Self {
impl From<solidcraft_io::IoError> for EngineError {
fn from(e: solidcraft_io::IoError) -> Self {
EngineError::Other(e.to_string())
}
}
@@ -146,7 +146,7 @@ pub struct Snapshot {
pub label: String,
pub doc: Arc<Document>,
pub active_sketch: Option<u64>,
pub pending_moves: std::collections::BTreeMap<u64, solvecraft_doc::Mat>,
pub pending_moves: std::collections::BTreeMap<u64, solidcraft_doc::Mat>,
}
/// What some commands would make, evaluated on a scratch copy of a session
@@ -164,15 +164,15 @@ pub struct Preview {
#[derive(Clone)]
struct WorldCache {
doc: Arc<Document>,
model: Arc<solvecraft_doc::ModelState>,
pending_moves: std::collections::BTreeMap<u64, solvecraft_doc::Mat>,
model: Arc<solidcraft_doc::ModelState>,
pending_moves: std::collections::BTreeMap<u64, solidcraft_doc::Mat>,
active_component: u64,
active_occurrence_path: Option<Vec<u64>>,
world: Arc<solvecraft_doc::ModelState>,
world: Arc<solidcraft_doc::ModelState>,
}
impl WorldCache {
fn matches(&self, session: &Session, model: &Arc<solvecraft_doc::ModelState>) -> bool {
fn matches(&self, session: &Session, model: &Arc<solidcraft_doc::ModelState>) -> bool {
Arc::ptr_eq(&self.doc, &session.doc)
&& Arc::ptr_eq(&self.model, model)
&& self.pending_moves == session.pending_moves
@@ -203,11 +203,11 @@ pub struct Session {
/// The placement used when authoring shared component geometry.
pub active_occurrence_path: Option<Vec<u64>>,
/// Occurrence moves not captured yet (Capture Position keeps them, a recompute drops them).
pub pending_moves: std::collections::BTreeMap<u64, solvecraft_doc::Mat>,
pub pending_moves: std::collections::BTreeMap<u64, solidcraft_doc::Mat>,
world_cache: std::sync::Mutex<Option<WorldCache>>,
occurrence_visibility_cache: std::sync::Mutex<Option<occurrences::VisibilityCache>>,
/// Features copied with `timeline.copy` (pasted by `timeline.paste`).
pub clipboard: Vec<solvecraft_doc::Feature>,
pub clipboard: Vec<solidcraft_doc::Feature>,
/// Section Analysis: a view cut by a plane (origin, unit normal; the normal side is hidden).
pub section: Option<(Vec3, Vec3)>,
/// Surface analysis shading the model (zebra, draft, curvature map).
@@ -233,7 +233,7 @@ pub struct LiveEdit {
/// What the frame's solve found: a full solve's report, or, when the sketch was solved
/// without a rank analysis, its last report's degrees of freedom and determined flags (a
/// drag does not change them; the evaluation at the gesture's end brings them up to date).
pub report: solvecraft_sketch::SolveReport,
pub report: solidcraft_sketch::SolveReport,
}
/// The design, active sketch, pending positions and undo history at an `edit.checkpoint`.
@@ -242,7 +242,7 @@ pub(crate) struct Checkpoint {
pub id: u64,
pub doc: Arc<Document>,
pub active_sketch: Option<u64>,
pub pending_moves: std::collections::BTreeMap<u64, solvecraft_doc::Mat>,
pub pending_moves: std::collections::BTreeMap<u64, solidcraft_doc::Mat>,
pub undo: Vec<Snapshot>,
pub redo: Vec<Snapshot>,
}
@@ -352,7 +352,7 @@ impl Session {
/// Re-evaluate the model after a document change.
/// The model placed in the world through component occurrences (and uncaptured moves): what
/// to display, measure, pick and export. The same as `model.state()` without components.
pub fn world_state(&self) -> Arc<solvecraft_doc::ModelState> {
pub fn world_state(&self) -> Arc<solidcraft_doc::ModelState> {
let st = self.model.state();
if self.doc.occurrences.is_empty() {
return st;
@@ -364,7 +364,7 @@ impl Session {
return cached.world.clone();
}
let mut w = if self.pending_moves.is_empty() {
solvecraft_doc::world_state(&self.doc, &st)
solidcraft_doc::world_state(&self.doc, &st)
} else {
let mut d = (*self.doc).clone();
for o in &mut d.occurrences {
@@ -372,27 +372,27 @@ impl Session {
o.transform = *m;
}
}
solvecraft_doc::world_state(&d, &st)
solidcraft_doc::world_state(&d, &st)
};
// Sketch definitions are displayed once, at the placement currently being authored.
if self.active_occurrence_path.is_some() && self.active_component != 0 {
let first = self.occurrence_placement(&self.doc.first_path(self.active_component)).map(|p| p.world).unwrap_or(solvecraft_doc::IDENTITY);
if let Some(inverse) = solvecraft_doc::mat_inverse(&first) {
let delta = solvecraft_doc::mat_mul(&self.active_component_frame(), &inverse);
let first = self.occurrence_placement(&self.doc.first_path(self.active_component)).map(|p| p.world).unwrap_or(solidcraft_doc::IDENTITY);
if let Some(inverse) = solidcraft_doc::mat_inverse(&first) {
let delta = solidcraft_doc::mat_mul(&self.active_component_frame(), &inverse);
for sketch in w.sketches.iter_mut().filter(|sk| self.doc.feature(sk.feature).is_some_and(|f| f.component == self.active_component)) {
let sketch = Arc::make_mut(sketch);
let plane = sketch.plane;
if let Some(mapped) = solvecraft_geom::Plane::new(
solvecraft_doc::apply_point(&delta, plane.origin),
solvecraft_doc::apply_vector(&delta, plane.x),
solvecraft_doc::apply_vector(&delta, plane.y),
if let Some(mapped) = solidcraft_geom::Plane::new(
solidcraft_doc::apply_point(&delta, plane.origin),
solidcraft_doc::apply_vector(&delta, plane.x),
solidcraft_doc::apply_vector(&delta, plane.y),
) {
sketch.plane = mapped;
}
for wire in &mut sketch.sketch.wires {
if wire.space == solvecraft_doc::sketch::WireSpace::Component {
if wire.space == solidcraft_doc::sketch::WireSpace::Component {
for point in wire.pts.iter_mut().chain(&mut wire.fit) {
*point = solvecraft_doc::apply_point(&delta, *point);
*point = solidcraft_doc::apply_point(&delta, *point);
}
}
}
@@ -798,6 +798,6 @@ pub(crate) type SketchValues = std::collections::BTreeMap<u64, std::collections:
/// The values of a sketch's driving dimensions that come from parameters: what a value edit
/// changes in it (as opposed to geometry it projects or links from upstream).
fn own_values(sk: &solvecraft_sketch::Sketch) -> std::collections::BTreeMap<String, f64> {
fn own_values(sk: &solidcraft_sketch::Sketch) -> std::collections::BTreeMap<String, f64> {
sk.constraints.iter().filter(|c| c.param.is_some() && !c.driven).filter_map(|c| Some((c.id.clone(), c.kind.value()?))).collect()
}
+2 -2
View File
@@ -1,5 +1,5 @@
//! SolveCraft's licence texts and the third-party licences, built into every program so a single
//! downloaded executable carries them (Help ▸ About ▸ Licences, `solvecraft-cli licences`).
//! SolidCraft's licence texts and the third-party licences, built into every program so a single
//! downloaded executable carries them (Help ▸ About ▸ Licences, `solidcraft-cli licences`).
//! THIRD-PARTY-LICENSES.txt is written by `cargo xtask licences` and checked by `cargo xtask ci`.
pub const LICENSE_MIT: &str = include_str!("../../../LICENSE-MIT");
+5 -5
View File
@@ -4,7 +4,7 @@
//! names again by sampling geometry could do.
use serde_json::{Value, json};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use crate::Session;
@@ -40,8 +40,8 @@ fn body_at(s: &Session, p: [f64; 3]) -> Option<String> {
fn names(s: &Session, body: &str) -> Vec<(String, bool)> {
let state = s.model.state();
let b = state.body(body).unwrap_or_else(|| panic!("body {body}"));
let names = solvecraft_doc::naming::face_names(b);
names.iter().enumerate().map(|(i, n)| (n.clone(), solvecraft_doc::naming::face_name_exact(b, i))).collect()
let names = solidcraft_doc::naming::face_names(b);
names.iter().enumerate().map(|(i, n)| (n.clone(), solidcraft_doc::naming::face_name_exact(b, i))).collect()
}
/// A 40 x 30 x 10 block ("Block": sketch lines l1 bottom, l2 right, l3 top, l4 left).
@@ -230,7 +230,7 @@ fn a_name_of_a_body_without_history_is_found_by_geometry_and_says_so_once_it_mov
run(&mut s, "solid.replace_face", json!({"faces": [[20, 15, 10]], "target": {"origin": [0, 0, 12], "normal": [0, 0, 1]}, "name": "Lift"}));
let state = s.model.state();
let body = state.bodies.first().unwrap();
assert!((0..solvecraft_doc::naming::face_names(body).len()).all(|i| !solvecraft_doc::naming::face_name_exact(body, i)));
assert!((0..solidcraft_doc::naming::face_names(body).len()).all(|i| !solidcraft_doc::naming::face_name_exact(body, i)));
run(&mut s, "solid.fillet", json!({"edges": [[40, 15, 12]], "radius": 2, "name": "Round"}));
// Where it was picked, the pick and the name agree: nothing to say.
assert_eq!(warning(&s, "Round"), None);
@@ -263,7 +263,7 @@ fn a_sketch_on_a_certified_face_survives_a_fillet_and_a_cut_upstream_of_it() {
assert!((top(&s) - 14.0).abs() < 1e-6);
let state = s.model.state();
let body = state.bodies.first().unwrap();
assert!((0..solvecraft_doc::naming::face_names(body).len()).all(|i| solvecraft_doc::naming::face_name_exact(body, i)));
assert!((0..solidcraft_doc::naming::face_names(body).len()).all(|i| solidcraft_doc::naming::face_name_exact(body, i)));
change(&mut s, "h", "16 mm");
for f in ["Corner", "Hole", "Top", "Peg"] {
assert_eq!(error(&s, f), None, "{f}");
+2 -2
View File
@@ -1,6 +1,6 @@
//! Exact placement identity and session view/authoring state.
use crate::{EngineError, Result, Sel, Session};
use solvecraft_doc::{IDENTITY, Mat, mat_mul};
use solidcraft_doc::{IDENTITY, Mat, mat_mul};
#[derive(Clone, Debug)]
pub struct OccurrenceBody {
@@ -33,7 +33,7 @@ pub(crate) struct VisibilityCache {
impl Session {
/// Document with uncaptured occurrence positions applied, without changing saved geometry.
pub fn placed_document(&self) -> std::borrow::Cow<'_, solvecraft_doc::Document> {
pub fn placed_document(&self) -> std::borrow::Cow<'_, solidcraft_doc::Document> {
if self.pending_moves.is_empty() {
return std::borrow::Cow::Borrowed(&self.doc);
}
+1 -1
View File
@@ -1,7 +1,7 @@
//! Helpers to read hostile JSON parameters.
use serde_json::Value;
use solvecraft_geom::{Vec2, Vec3};
use solidcraft_geom::{Vec2, Vec3};
use crate::{EngineError, Result};
+23 -23
View File
@@ -13,7 +13,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value {
}
fn volume(s: &Session, body: &str) -> f64 {
s.model.state().body(body).map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).unwrap_or(0.0)
s.model.state().body(body).map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).unwrap_or(0.0)
}
#[test]
@@ -45,22 +45,22 @@ fn enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs() {
assert!(s.model.results.iter().all(|result| result.error.is_none()));
for (name, z) in [("PCB standoff", 2.5), ("Lid boss", 2.5), ("Lid lip", 30.), ("Lid screw", 34.)] {
let feature =
s.doc.features.iter().find(|feature| feature.name == name && matches!(feature.kind, solvecraft_doc::FeatureKind::Sketch { .. })).unwrap();
s.doc.features.iter().find(|feature| feature.name == name && matches!(feature.kind, solidcraft_doc::FeatureKind::Sketch { .. })).unwrap();
assert!((s.model.state().sketch(feature.id).unwrap().plane.origin.z - z).abs() < 1e-6);
}
let usb =
s.doc.features.iter().find(|feature| feature.name == "USB" && matches!(feature.kind, solvecraft_doc::FeatureKind::Sketch { .. })).unwrap();
s.doc.features.iter().find(|feature| feature.name == "USB" && matches!(feature.kind, solidcraft_doc::FeatureKind::Sketch { .. })).unwrap();
assert!((s.model.state().sketch(usb.id).unwrap().plane.origin.y + 35.5).abs() < 1e-6);
let assert_material = |session: &Session| {
let state = session.world_state();
let case = state.body("Case").unwrap().mesh();
let lid = state.body("Lid").unwrap().mesh();
assert!(case.contains(solvecraft_geom::Vec3::new(-24.5, -21.5, 7.)));
assert!(!case.contains(solvecraft_geom::Vec3::new(-26.5, -21.5, 7.)));
assert!(!case.contains(solvecraft_geom::Vec3::new(-10., -35.25, 11.35)));
assert!(case.contains(solvecraft_geom::Vec3::new(-10., -35.25, 15.)));
assert!(case.contains(solidcraft_geom::Vec3::new(-24.5, -21.5, 7.)));
assert!(!case.contains(solidcraft_geom::Vec3::new(-26.5, -21.5, 7.)));
assert!(!case.contains(solidcraft_geom::Vec3::new(-10., -35.25, 11.35)));
assert!(case.contains(solidcraft_geom::Vec3::new(-10., -35.25, 15.)));
assert!(
lid.contains(solvecraft_geom::Vec3::new(0., 32.2, 29.)),
lid.contains(solidcraft_geom::Vec3::new(0., 32.2, 29.)),
"lid bounds {:?}; lip {:?}; probes {:?}",
lid.bounds(),
session
@@ -71,30 +71,30 @@ fn enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs() {
.doc
.features
.iter()
.find(|feature| feature.name == "Lid lip" && matches!(feature.kind, solvecraft_doc::FeatureKind::Sketch { .. }))
.find(|feature| feature.name == "Lid lip" && matches!(feature.kind, solidcraft_doc::FeatureKind::Sketch { .. }))
.unwrap()
.id
)
.unwrap()
.sketch
.links,
(20..35).map(|y| (y, lid.contains(solvecraft_geom::Vec3::new(0., f64::from(y) + 0.2, 29.)))).collect::<Vec<_>>()
(20..35).map(|y| (y, lid.contains(solidcraft_geom::Vec3::new(0., f64::from(y) + 0.2, 29.)))).collect::<Vec<_>>()
);
assert!(!lid.contains(solvecraft_geom::Vec3::new(0., 32.9, 29.)));
assert!(!lid.contains(solvecraft_geom::Vec3::new(-33.5, -28.5, 32.)));
assert!(lid.contains(solvecraft_geom::Vec3::new(-31.5, -28.5, 32.)));
assert!(!lid.contains(solidcraft_geom::Vec3::new(0., 32.9, 29.)));
assert!(!lid.contains(solidcraft_geom::Vec3::new(-33.5, -28.5, 32.)));
assert!(lid.contains(solidcraft_geom::Vec3::new(-31.5, -28.5, 32.)));
};
assert_material(&s);
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(reopened.model.results.iter().all(|result| result.error.is_none()));
assert_material(&reopened);
}
#[test]
fn enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen() {
use solvecraft_doc::FeatureKind;
use solvecraft_geom::{Vec2, Vec3};
use solvecraft_sketch::CurveKind;
use solidcraft_doc::FeatureKind;
use solidcraft_geom::{Vec2, Vec3};
use solidcraft_sketch::CurveKind;
let mut s = Session::default();
let script: Value = serde_json::from_str(include_str!("../../../examples/acceptance/sensor_enclosure.json")).unwrap();
@@ -139,10 +139,10 @@ fn enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen
}
}
assert_eq!(corners.len(), 4);
let regions = solvecraft_doc::select_profiles(solved, profiles).unwrap();
let regions = solidcraft_doc::select_profiles(solved, profiles).unwrap();
assert_eq!(regions.len(), 1);
assert!((regions[0].area() - 332.570619298295).abs() < 1e-7, "area {}", regions[0].area());
let ring = solvecraft_kernel::extrude(&solved.plane, &regions, 0., 2.).unwrap();
let ring = solidcraft_kernel::extrude(&solved.plane, &regions, 0., 2.).unwrap();
assert_eq!(ring.len(), 1);
let precise_ring = ring[0].tessellate(1e-6).unwrap();
let bounds = precise_ring.bounds();
@@ -160,14 +160,14 @@ fn enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen
.segs
.iter()
.map(|segment| match segment {
solvecraft_geom::Seg2::Arc { radius, sweep, .. } => radius.abs() * sweep.abs(),
solvecraft_geom::Seg2::Line { .. } => 0.,
solidcraft_geom::Seg2::Arc { radius, sweep, .. } => radius.abs() * sweep.abs(),
solidcraft_geom::Seg2::Line { .. } => 0.,
other => panic!("unexpected ring boundary {other:?}"),
})
.sum::<f64>();
assert!((arc_perimeter - std::f64::consts::TAU * 0.8).abs() < 1e-8);
assert_eq!(regions[0].holes.len(), 1);
assert!(regions[0].holes[0].segs.iter().all(|segment| matches!(segment, solvecraft_geom::Seg2::Line { .. })));
assert!(regions[0].holes[0].segs.iter().all(|segment| matches!(segment, solidcraft_geom::Seg2::Line { .. })));
// Body::tessellate floors the requested sag tolerance at size * 1e-6.
// Only the outer arcs incur chord error: their tolerance-wide boundary
// strip bounds the volume error without changing CAD or export tolerances.
@@ -199,7 +199,7 @@ fn enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen
};
check(&s);
let saved = s.doc.to_json();
let reopened = Session::new(solvecraft_doc::Document::from_json(&saved).unwrap());
let reopened = Session::new(solidcraft_doc::Document::from_json(&saved).unwrap());
check(&reopened);
assert_eq!(reopened.doc.to_json(), saved);
}
+6 -6
View File
@@ -4,7 +4,7 @@
use serde_json::{Value, json};
use crate::Session;
use solvecraft_doc::{FeatureKind, ProfileSel};
use solidcraft_doc::{FeatureKind, ProfileSel};
fn run(s: &mut Session, id: &str, p: Value) -> Value {
match s.execute(id, &p) {
@@ -68,7 +68,7 @@ fn a_profile_picked_by_curves_follows_a_parameter_change() {
if let Some(f) = s.doc_mut().feature_mut(id)
&& let FeatureKind::Extrude { profiles, .. } = &mut f.kind
{
*profiles = ProfileSel::Points { points: vec![solvecraft_geom::Vec2::new(25.0, 10.0)] };
*profiles = ProfileSel::Points { points: vec![solidcraft_geom::Vec2::new(25.0, 10.0)] };
}
s.refresh();
assert!((volumes(&s)[0] - 6000.0).abs() < 1.0);
@@ -99,7 +99,7 @@ fn explicit_curve_pick_normalizes_repeats_before_save_and_parameter_edit() {
run(&mut s, "solid.extrude", json!({"sketch":"Tray","profiles":[{"curves":["top","bottom","div","left","bottom","top"]}],"distance":10}));
let ProfileSel::Curves { loops } = extrude_kind(&s) else { panic!("{:?}", extrude_kind(&s)) };
assert_eq!(loops, vec![vec!["top", "bottom", "div", "left"]]);
let mut reopened = Session::new(solvecraft_doc::format::read(&s.doc.to_json()).unwrap());
let mut reopened = Session::new(solidcraft_doc::format::read(&s.doc.to_json()).unwrap());
run(&mut reopened, "parameters.change", json!({"name":"split","expression":"20 mm"}));
assert_eq!(last_error(&reopened), None);
assert!((volumes(&reopened)[0] - 4000.0).abs() < 1.0);
@@ -116,7 +116,7 @@ fn repeated_explicit_curve_ids_do_not_count_split_graph_spans() {
run(&mut s, "solid.extrude", json!({"sketch":"S","profiles":[{"curves":["d","a","a","a","c","b"]}],"distance":2}));
let ProfileSel::Curves { loops } = extrude_kind(&s) else { panic!("{:?}", extrude_kind(&s)) };
assert_eq!(loops, vec![vec!["d", "a", "c", "b"]]);
let reopened = Session::new(solvecraft_doc::format::read(&s.doc.to_json()).unwrap());
let reopened = Session::new(solidcraft_doc::format::read(&s.doc.to_json()).unwrap());
assert_eq!(last_error(&reopened), None);
assert!((volumes(&reopened)[0] - 200.0).abs() < 1e-3);
}
@@ -187,7 +187,7 @@ fn curve_picks_save_load_and_edit() {
json!({"sketch": "Tray", "profiles": [{"curves": ["left", "bottom", "div", "top"]}], "distance": 10, "name": "Bay"}),
);
let text = serde_json::to_string(&*s.doc).unwrap();
let doc = solvecraft_doc::format::read(&text).unwrap();
let doc = solidcraft_doc::format::read(&text).unwrap();
let mut t = Session::new(doc);
assert_eq!(extrude_kind(&t), extrude_kind(&s));
run(&mut t, "parameters.change", json!({"name": "split", "expression": "20 mm"}));
@@ -201,6 +201,6 @@ fn curve_picks_save_load_and_edit() {
// An old design's point pick still loads and evaluates.
let old = text.replace(r#"{"type":"curves","loops":[["left","bottom","div","top"]]}"#, r#"{"type":"points","points":[[25.0,10.0]]}"#);
assert_ne!(old, text);
let t = Session::new(solvecraft_doc::format::read(&old).unwrap());
let t = Session::new(solidcraft_doc::format::read(&old).unwrap());
assert!((volumes(&t)[0] - 6000.0).abs() < 1.0, "{:?}", volumes(&t));
}
+8 -8
View File
@@ -48,8 +48,8 @@ fn bodies(s: &Session) -> Vec<(String, f64)> {
fn a_port_picked_by_index_stays_a_cut_after_a_width_change() {
let mut s = enclosure();
let f = s.doc.features.last().unwrap();
let solvecraft_doc::FeatureKind::Extrude { profiles, .. } = &f.kind else { panic!("{:?}", f.kind) };
assert!(matches!(profiles, solvecraft_doc::ProfileSel::Curves { .. }), "stored by curves: {profiles:?}");
let solidcraft_doc::FeatureKind::Extrude { profiles, .. } = &f.kind else { panic!("{:?}", f.kind) };
assert!(matches!(profiles, solidcraft_doc::ProfileSel::Curves { .. }), "stored by curves: {profiles:?}");
let before = bodies(&s);
assert_eq!(before.len(), 1, "{before:?}");
run(&mut s, "parameters.change", json!({"name": "width", "expression": "100 mm"}));
@@ -63,14 +63,14 @@ fn a_port_picked_by_index_stays_a_cut_after_a_width_change() {
assert!((s.model.state().sketch(port).unwrap().plane.origin.x - 50.).abs() < 1e-6);
let world = s.world_state();
let mesh = world.body("Enclosure").unwrap().mesh();
assert!(!mesh.contains(solvecraft_geom::Vec3::new(49., 0., 12.)));
assert!(mesh.contains(solvecraft_geom::Vec3::new(49., 0., 20.)));
let reopened = Session::new(solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(!mesh.contains(solidcraft_geom::Vec3::new(49., 0., 12.)));
assert!(mesh.contains(solidcraft_geom::Vec3::new(49., 0., 20.)));
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(reopened.model.result(id).unwrap().error.is_none());
let world = reopened.world_state();
let mesh = world.body("Enclosure").unwrap().mesh();
assert!(!mesh.contains(solvecraft_geom::Vec3::new(49., 0., 12.)));
assert!(mesh.contains(solvecraft_geom::Vec3::new(49., 0., 20.)));
assert!(!mesh.contains(solidcraft_geom::Vec3::new(49., 0., 12.)));
assert!(mesh.contains(solidcraft_geom::Vec3::new(49., 0., 20.)));
}
/// A pick the sketch no longer has is an error on the feature, never a different profile.
@@ -88,7 +88,7 @@ fn a_lost_profile_is_an_error_not_another_pick() {
// Delete the picked circle: the extrude fails with a message instead of taking the square.
let sk = s.doc.find_feature("S").unwrap().id;
let picked = match &s.doc.features.last().unwrap().kind {
solvecraft_doc::FeatureKind::Extrude { profiles: solvecraft_doc::ProfileSel::Curves { loops }, .. } => loops[0].clone(),
solidcraft_doc::FeatureKind::Extrude { profiles: solidcraft_doc::ProfileSel::Curves { loops }, .. } => loops[0].clone(),
k => panic!("{k:?}"),
};
run(&mut s, "sketch.edit", json!({"sketch": sk}));
+68 -21
View File
@@ -1,6 +1,6 @@
//! Autosave and crash recovery.
//!
//! A running app keeps one recovery entry in the recovery folder: `<id>.solvecraft` (the design
//! A running app keeps one recovery entry in the recovery folder: `<id>.solidcraft` (the design
//! as of the last autosave), `<id>.json` (its file path, name and time) and `<id>.lock`, which
//! the app holds locked while it runs. Autosaves are written atomically, only when the design
//! has unsaved changes it hasn't written yet; a design with nothing unsaved has no entry. An
@@ -13,28 +13,41 @@ use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use solvecraft_doc::Document;
use solidcraft_doc::Document;
use crate::{EngineError, Result, Session};
/// The recovery folder: `SOLVECRAFT_RECOVERY_DIR`, else the platform's per-user data folder.
/// The recovery folder: `SOLIDCRAFT_RECOVERY_DIR`, else the platform's per-user data folder.
pub fn default_dir() -> Option<PathBuf> {
// Tests never touch the user's real recovery folder.
if cfg!(test) {
return Some(std::env::temp_dir().join(format!("solvecraft-test-recovery-{}", std::process::id())));
return Some(std::env::temp_dir().join(format!("solidcraft-test-recovery-{}", std::process::id())));
}
if let Some(d) = std::env::var_os("SOLVECRAFT_RECOVERY_DIR").filter(|d| !d.is_empty()) {
if let Some(d) = solidcraft_geom::env::var_os("RECOVERY_DIR").filter(|d| !d.is_empty()) {
return Some(PathBuf::from(d));
}
let env = |k: &str| std::env::var_os(k).filter(|d| !d.is_empty()).map(PathBuf::from);
let base = if cfg!(windows) {
env("LOCALAPPDATA")?.join("SolveCraft")
let root = if cfg!(windows) {
env("LOCALAPPDATA")?
} else if cfg!(target_os = "macos") {
env("HOME")?.join("Library/Application Support/SolveCraft")
env("HOME")?.join("Library/Application Support")
} else {
env("XDG_DATA_HOME").or_else(|| env("HOME").map(|h| h.join(".local/share")))?.join("solvecraft")
env("XDG_DATA_HOME").or_else(|| env("HOME").map(|h| h.join(".local/share")))?
};
Some(base.join("recovery"))
let (dir, legacy) = dirs_under(&root, cfg!(windows) || cfg!(target_os = "macos"));
// Compatibility shim (docs/compatibility.md): autosaves made before the rename are copied
// to the new folder on first launch; the old folder is left as it was.
if let Err(e) = solidcraft_io::adopt_legacy_dir(&dir, &legacy) {
log::warn!("recovery: could not copy {} to {}: {e}", legacy.display(), dir.display());
}
Some(dir)
}
/// The recovery folder under the per-user data `root`, and the folder earlier releases used.
/// Windows and macOS name the app's folder in title case, other systems in lower case.
fn dirs_under(root: &Path, title_case: bool) -> (PathBuf, PathBuf) {
let (name, legacy) = if title_case { ("SolidCraft", "SolveCraft") } else { ("solidcraft", "solvecraft") };
(root.join(name).join("recovery"), root.join(legacy).join("recovery"))
}
/// What an entry's `<id>.json` holds.
@@ -57,6 +70,14 @@ fn valid_id(id: &str) -> bool {
!id.is_empty() && id.len() <= 64 && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
}
/// An entry's design: `<id>.solidcraft`, or `<id>.solvecraft` for an autosave copied from a
/// pre-rename recovery folder (compatibility shim, docs/compatibility.md).
fn design_file(dir: &Path, id: &str) -> PathBuf {
let current = file(dir, id, solidcraft_io::DESIGN_EXT);
let legacy = file(dir, id, solidcraft_io::LEGACY_DESIGN_EXT);
if !current.is_file() && legacy.is_file() { legacy } else { current }
}
fn file(dir: &Path, id: &str, ext: &str) -> PathBuf {
dir.join(format!("{id}.{ext}"))
}
@@ -73,7 +94,7 @@ pub fn orphans(dir: &Path) -> Vec<Entry> {
return None;
}
let entry: Entry = serde_json::from_slice(&fs::read(&p).ok()?).ok()?;
if !valid_id(&entry.id) || !file(dir, &entry.id, "solvecraft").is_file() {
if !valid_id(&entry.id) || !design_file(dir, &entry.id).is_file() {
return None;
}
// Held by a running app: not an orphan.
@@ -100,16 +121,16 @@ pub fn load(dir: &Path, id: &str) -> Result<(Document, Entry)> {
.ok()
.and_then(|b| serde_json::from_slice(&b).ok())
.ok_or_else(|| EngineError::Other(format!("no recovered design `{id}`")))?;
let path = file(dir, id, "solvecraft");
let bytes = solvecraft_io::vfs::read(&path.to_string_lossy(), solvecraft_io::MAX_DESIGN_BYTES)
let path = design_file(dir, id);
let bytes = solidcraft_io::vfs::read(&path.to_string_lossy(), solidcraft_io::MAX_DESIGN_BYTES)
.map_err(|e| EngineError::Other(format!("recovered design `{id}`: {e}")))?;
Ok((solvecraft_io::read_design(&bytes)?, entry))
Ok((solidcraft_io::read_design(&bytes)?, entry))
}
/// Delete an entry.
pub fn remove(dir: &Path, id: &str) {
if valid_id(id) {
for ext in ["json", "solvecraft", "lock"] {
for ext in ["json", solidcraft_io::DESIGN_EXT, solidcraft_io::LEGACY_DESIGN_EXT, "lock"] {
let _ = fs::remove_file(file(dir, id, ext));
}
}
@@ -158,7 +179,7 @@ impl Autosaver {
self.last_at = Instant::now();
if !s.is_dirty() {
if self.has_entry {
for ext in ["json", "solvecraft"] {
for ext in ["json", "solidcraft"] {
let _ = fs::remove_file(file(&self.dir, &self.id, ext));
}
self.has_entry = false;
@@ -172,15 +193,15 @@ impl Autosaver {
return Ok(false);
}
let io = |e: std::io::Error| EngineError::Other(format!("autosave: {e}"));
solvecraft_io::write_atomic(&file(&self.dir, &self.id, "solvecraft"), &solvecraft_io::write_design(&s.doc), false).map_err(io)?;
solidcraft_io::write_atomic(&file(&self.dir, &self.id, "solidcraft"), &solidcraft_io::write_design(&s.doc), false).map_err(io)?;
let entry =
Entry { id: self.id.clone(), path: s.path.clone(), name: s.doc.name.clone(), saved_at: unix_now(), features: s.doc.features.len() };
let meta = serde_json::to_vec_pretty(&entry).map_err(|e| EngineError::Other(e.to_string()))?;
solvecraft_io::write_atomic(&file(&self.dir, &self.id, "json"), &meta, false).map_err(io)?;
solidcraft_io::write_atomic(&file(&self.dir, &self.id, "json"), &meta, false).map_err(io)?;
self.last = Some(s.doc.clone());
self.has_entry = true;
// Crash replays start from this autosave from here.
crate::crash::rebased(s, Some(&file(&self.dir, &self.id, "solvecraft")), true);
crate::crash::rebased(s, Some(&file(&self.dir, &self.id, solidcraft_io::DESIGN_EXT)), true);
Ok(true)
}
@@ -202,11 +223,37 @@ impl Autosaver {
#[cfg(test)]
mod tests {
#[test]
fn autosave_from_a_legacy_folder_is_listed_and_loads() {
let d = std::env::temp_dir().join(format!("solidcraft-recovery-legacy-{}", std::process::id()));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
let id = "1-2-3";
let doc = Document::default();
fs::write(d.join(format!("{id}.solvecraft")), solidcraft_io::write_design(&doc)).unwrap();
let entry = Entry { id: id.into(), path: Some("/d/part.solvecraft".into()), name: "part".into(), saved_at: 1, features: 0 };
fs::write(d.join(format!("{id}.json")), serde_json::to_vec(&entry).unwrap()).unwrap();
assert_eq!(orphans(&d).iter().map(|e| e.id.as_str()).collect::<Vec<_>>(), [id]);
let (_, loaded) = load(&d, id).unwrap();
assert_eq!(loaded, entry);
remove(&d, id);
assert!(orphans(&d).is_empty() && !d.join(format!("{id}.solvecraft")).exists());
let _ = fs::remove_dir_all(&d);
}
#[test]
fn recovery_dirs_and_their_legacy_names() {
let (dir, legacy) = super::dirs_under(Path::new("/data"), false);
assert_eq!((dir.as_path(), legacy.as_path()), (Path::new("/data/solidcraft/recovery"), Path::new("/data/solvecraft/recovery")));
let (dir, legacy) = super::dirs_under(Path::new("/data"), true);
assert_eq!((dir.as_path(), legacy.as_path()), (Path::new("/data/SolidCraft/recovery"), Path::new("/data/SolveCraft/recovery")));
}
use super::*;
use serde_json::json;
fn dir(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("solvecraft-recovery-{tag}-{}", std::process::id()));
let d = std::env::temp_dir().join(format!("solidcraft-recovery-{tag}-{}", std::process::id()));
let _ = fs::remove_dir_all(&d);
d
}
@@ -259,7 +306,7 @@ mod tests {
assert!(a.tick(&s, false).unwrap());
s.mark_saved();
a.tick(&s, false).unwrap();
assert!(!file(&d, &a.id, "solvecraft").exists());
assert!(!file(&d, &a.id, "solidcraft").exists());
a.close(&s);
assert_eq!(fs::read_dir(&d).unwrap().count(), 0);
// Closing with unsaved changes keeps them for the next launch.
+11 -11
View File
@@ -1,4 +1,4 @@
//! A sample design (used by `solvecraft --sample` and demos): a parametric plate with rounded
//! A sample design (used by `solidcraft --sample` and demos): a parametric plate with rounded
//! corners, two holes and a boss.
use serde_json::{Value, json};
@@ -147,14 +147,14 @@ mod tests {
.doc
.features
.iter()
.find(|feature| feature.name == "Boss" && matches!(feature.kind, solvecraft_doc::FeatureKind::Sketch { .. }))
.find(|feature| feature.name == "Boss" && matches!(feature.kind, solidcraft_doc::FeatureKind::Sketch { .. }))
.unwrap()
.id;
let bore = session
.doc
.features
.iter()
.find(|feature| feature.name == "Bore" && matches!(feature.kind, solvecraft_doc::FeatureKind::Sketch { .. }))
.find(|feature| feature.name == "Bore" && matches!(feature.kind, solidcraft_doc::FeatureKind::Sketch { .. }))
.unwrap()
.id;
session.execute("parameters.change", &serde_json::json!({"name":"thickness","expression":"12 mm"})).unwrap();
@@ -164,17 +164,17 @@ mod tests {
let world = session.world_state();
let plate = world.body("Plate").unwrap().mesh();
assert!((plate.bounds().max.z - 26.).abs() < 1e-6);
assert!(plate.contains(solvecraft_geom::Vec3::new(5., 25., 10.)));
assert!(plate.contains(solvecraft_geom::Vec3::new(40., 35., 24.)));
assert!(!plate.contains(solvecraft_geom::Vec3::new(40., 25., 24.)));
assert!(!plate.contains(solvecraft_geom::Vec3::new(40., 25., 2.)));
assert!(plate.contains(solvecraft_geom::Vec3::new(40., 35., 2.)));
let reopened = crate::Session::new(solvecraft_doc::Document::from_json(&session.doc.to_json()).unwrap());
assert!(plate.contains(solidcraft_geom::Vec3::new(5., 25., 10.)));
assert!(plate.contains(solidcraft_geom::Vec3::new(40., 35., 24.)));
assert!(!plate.contains(solidcraft_geom::Vec3::new(40., 25., 24.)));
assert!(!plate.contains(solidcraft_geom::Vec3::new(40., 25., 2.)));
assert!(plate.contains(solidcraft_geom::Vec3::new(40., 35., 2.)));
let reopened = crate::Session::new(solidcraft_doc::Document::from_json(&session.doc.to_json()).unwrap());
assert!((reopened.model.state().sketch(bore).unwrap().plane.origin.z - 26.).abs() < 1e-6);
let reopened_world = reopened.world_state();
let reopened_plate = reopened_world.body("Plate").unwrap().mesh();
assert!(!reopened_plate.contains(solvecraft_geom::Vec3::new(40., 25., 2.)));
assert!(reopened_plate.contains(solvecraft_geom::Vec3::new(40., 35., 2.)));
assert!(!reopened_plate.contains(solidcraft_geom::Vec3::new(40., 25., 2.)));
assert!(reopened_plate.contains(solidcraft_geom::Vec3::new(40., 35., 2.)));
}
/// Every built-in sample builds without a failing feature or joint.
+1 -1
View File
@@ -71,7 +71,7 @@ fn export_keeps_split_faces_through_later_features() {
run(&mut s, "solid.fillet", json!({"edges": [[20, 0, 0]], "radius": 2}));
let (f, _) = faces_volume(&mut s, "B");
assert_eq!(f, 8);
let dir = std::env::temp_dir().join(format!("solvecraft-split-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-split-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let step = dir.join("split.step");
run(&mut s, "file.export", json!({"path": step.to_string_lossy()}));
+67 -67
View File
@@ -55,13 +55,13 @@ fn join_extrude_from_a_chamfered_top_face() {
run(&mut s, "solid.chamfer", json!({"edges": [[10, 0, 20]], "distance": "bevel"}));
let base = volume(&mut s);
let id = run(&mut s, "sketch.create", json!({"plane": {"face": [0, 0, 20]}}))["sketch"].as_u64().unwrap();
let solvecraft_doc::FeatureKind::Sketch { plane: solvecraft_doc::PlaneRef::Face { anchor: Some(anchor), .. }, sketch } =
let solidcraft_doc::FeatureKind::Sketch { plane: solidcraft_doc::PlaneRef::Face { anchor: Some(anchor), .. }, sketch } =
&s.doc.feature(id).unwrap().kind
else {
panic!("the chamfered cap retains its exact original construction support");
};
assert_eq!(anchor.target.origin_feature, source);
assert!(matches!(anchor.target.certificate, Some(solvecraft_doc::config::FaceCertificate::ExtrudeCap { high: true, .. })));
assert!(matches!(anchor.target.certificate, Some(solidcraft_doc::config::FaceCertificate::ExtrudeCap { high: true, .. })));
assert!(!sketch.links.is_empty());
assert!(sketch.links.iter().all(|link| link.auto_face && !link.lost));
// The default linked cap outline is radius8; no explicit fixed plane or
@@ -74,7 +74,7 @@ fn join_extrude_from_a_chamfered_top_face() {
run(&mut s, "parameters.change", json!({"name":"height","expression":format!("{height} mm")}));
run(&mut s, "parameters.change", json!({"name":"bevel","expression":format!("{bevel} mm")}));
let expected = PI * (100. * height - (10. * bevel * bevel - bevel * bevel * bevel / 3.) + (10. - bevel) * (10. - bevel) * 5.);
let loaded = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let loaded = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let mut cold = Session::new(loaded);
let warm_volume = volume(&mut s);
let cold_volume = volume(&mut cold);
@@ -88,10 +88,10 @@ fn join_extrude_from_a_chamfered_top_face() {
assert!((solved.plane.origin.z - height).abs() < 1e-8);
assert!(solved.sketch.links.iter().all(|link| link.auto_face && !link.lost));
let body = &state.bodies[0];
assert!(body.body.contains(solvecraft_geom::Vec3::new(0., 0., height + 4.)));
assert!(!body.body.contains(solvecraft_geom::Vec3::new(0., 0., height + 6.)));
assert!(body.body.contains(solvecraft_geom::Vec3::new(10. - bevel - 0.5, 0., height + 4.)));
assert!(!body.body.contains(solvecraft_geom::Vec3::new(10. - bevel + 0.5, 0., height + 4.)));
assert!(body.body.contains(solidcraft_geom::Vec3::new(0., 0., height + 4.)));
assert!(!body.body.contains(solidcraft_geom::Vec3::new(0., 0., height + 6.)));
assert!(body.body.contains(solidcraft_geom::Vec3::new(10. - bevel - 0.5, 0., height + 4.)));
assert!(!body.body.contains(solidcraft_geom::Vec3::new(10. - bevel + 0.5, 0., height + 4.)));
}
}
}
@@ -256,7 +256,7 @@ fn box_fillet_cut_part() {
let l: Vec<String> = r["curves"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_string()).collect();
let corner = s.doc.sketch(s.active_sketch.unwrap()).unwrap().curves[0].clone();
let start = match corner.kind {
solvecraft_sketch::CurveKind::Line { a, .. } => s.doc.sketch(s.active_sketch.unwrap()).unwrap().points[a].id.clone(),
solidcraft_sketch::CurveKind::Line { a, .. } => s.doc.sketch(s.active_sketch.unwrap()).unwrap().points[a].id.clone(),
_ => panic!(),
};
run(&mut s, "sketch.constraint.coincident", json!({"a": start, "b": "origin"}));
@@ -293,7 +293,7 @@ fn box_fillet_cut_part() {
assert!(rel(volume(&mut s), expect2) < 5e-4);
// Export STL and STEP.
let dir = std::env::temp_dir().join(format!("solvecraft-test-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let stl = dir.join("part.stl");
run(&mut s, "file.export", json!({"path": stl.to_string_lossy()}));
@@ -301,7 +301,7 @@ fn box_fillet_cut_part() {
let step = dir.join("part.step");
run(&mut s, "file.export", json!({"path": step.to_string_lossy()}));
assert!(std::fs::read_to_string(&step).unwrap().contains("ISO-10303-21"));
let design = dir.join("part.solvecraft");
let design = dir.join("part.solidcraft");
run(&mut s, "file.save_as", json!({"path": design.to_string_lossy()}));
let mut s2 = Session::default();
run(&mut s2, "doc.open", json!({"path": design.to_string_lossy()}));
@@ -610,7 +610,7 @@ fn sample_edges_chain_and_faces() {
let chain = m.tangent_chain(e, 2f64.to_radians());
// The plate's top outline: 4 lines and 4 corner arcs (a boolean may leave an edge twice,
// once per face; count distinct ones).
let mut mids: Vec<solvecraft_geom::Vec3> = Vec::new();
let mut mids: Vec<solidcraft_geom::Vec3> = Vec::new();
for i in &chain {
let p = &m.edges[*i];
let mid = p[p.len() / 2];
@@ -707,7 +707,7 @@ fn timeline_edits_reresolve_references() {
/// A 40 × 30 × 20 box exported to a STEP file in a fresh temp directory.
fn step_box_file(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("solvecraft-step-{tag}-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-step-{tag}-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20}));
@@ -756,7 +756,7 @@ fn step_open_then_model_on_it() {
// The design saves and reopens without the STEP file.
let before = volume(&mut s);
let design = p.with_extension("solvecraft");
let design = p.with_extension("solidcraft");
run(&mut s, "file.save_as", json!({"path": design.to_string_lossy()}));
std::fs::remove_file(&p).unwrap();
let mut s2 = Session::default();
@@ -802,8 +802,8 @@ fn step_insert_shell_and_errors() {
assert_eq!(*d.doc, *before);
}
fn body_volume(st: &solvecraft_doc::ModelState) -> f64 {
st.bodies.iter().map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).sum()
fn body_volume(st: &solidcraft_doc::ModelState) -> f64 {
st.bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).sum()
}
/// Previews evaluate on a scratch copy: the document, undo history and model are untouched.
@@ -1048,7 +1048,7 @@ fn extrude_a_body_face() {
/// solid features.
#[test]
fn threemf_export_open_insert() {
let dir = std::env::temp_dir().join(format!("solvecraft-3mf-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-3mf-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20}));
@@ -1124,12 +1124,12 @@ fn holes_at_sketch_points_threads_and_components() {
assert_eq!(r["components"].as_array().map(|a| a.len()), Some(1));
// Saved and loaded with the components.
let text = s.doc.to_json();
let back = solvecraft_doc::Document::from_json(&text).unwrap();
let back = solidcraft_doc::Document::from_json(&text).unwrap();
assert_eq!(back.components.len(), 2);
let assigned = r["components"][0]["component"].as_u64().unwrap();
let transfer = r["components"][0]["feature"].as_u64().unwrap();
assert!(
matches!(&back.feature(transfer).unwrap().kind, solvecraft_doc::FeatureKind::BodyTransfer { bodies, to, .. } if bodies == &["Shaft"] && *to == assigned)
matches!(&back.feature(transfer).unwrap().kind, solidcraft_doc::FeatureKind::BodyTransfer { bodies, to, .. } if bodies == &["Shaft"] && *to == assigned)
);
let reopened = Session::new(back);
assert_eq!(reopened.model.state().body("Shaft").unwrap().component, assigned);
@@ -1284,11 +1284,11 @@ fn components_occurrences_and_world_placement() {
assert_eq!(s.world_state().body("Base").unwrap().component, 0);
run(&mut s, "timeline.roll_to", json!({}));
// Saved and loaded; older designs get occurrences.
let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let back = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(back.occurrences.len(), s.doc.occurrences.len());
let mut old: serde_json::Value = serde_json::from_str(&s.doc.to_json()).unwrap();
old.as_object_mut().unwrap().remove("occurrences");
let migrated = solvecraft_doc::Document::from_json(&old.to_string()).unwrap();
let migrated = solidcraft_doc::Document::from_json(&old.to_string()).unwrap();
assert_eq!(migrated.occurrences.len(), migrated.components.len());
}
@@ -1325,7 +1325,7 @@ fn feature_inputs_are_parameters() {
assert!(rel(volume(&mut s), 24.0 * 12.0 * 25.4) < 1e-9);
assert!(s.execute("parameters.import", &json!({"text": "name,expression\nw,1/"})).is_err());
// Saved and reloaded, the names stay.
let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let back = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(back.features[0].param_names, s.doc.features[0].param_names);
}
@@ -1345,15 +1345,15 @@ fn rolling_back_and_forward_leaves_no_projection_warnings() {
}
/// Export STEP, validate the file, read it back: no warnings, same volume, area and faces.
fn step_round_trip(s: &mut Session, tag: &str) -> solvecraft_kernel::StepImport {
let p = std::env::temp_dir().join(format!("solvecraft-steprt-{tag}-{}.step", std::process::id()));
fn step_round_trip(s: &mut Session, tag: &str) -> solidcraft_kernel::StepImport {
let p = std::env::temp_dir().join(format!("solidcraft-steprt-{tag}-{}.step", std::process::id()));
run(s, "file.export", json!({"path": p.to_string_lossy()}));
let text = std::fs::read_to_string(&p).unwrap();
let _ = std::fs::remove_file(&p);
solvecraft_kernel::step_validate(&text).unwrap();
assert_eq!(solvecraft_kernel::step_orientation_errors(&text).unwrap(), Vec::<String>::new());
solidcraft_kernel::step_validate(&text).unwrap();
assert_eq!(solidcraft_kernel::step_orientation_errors(&text).unwrap(), Vec::<String>::new());
assert!(text.contains("AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF"));
let imp = solvecraft_kernel::step_import(&text).unwrap();
let imp = solidcraft_kernel::step_import(&text).unwrap();
assert!(imp.warnings.is_empty(), "{:?}", imp.warnings);
imp
}
@@ -1367,7 +1367,7 @@ fn sample_plate_step_round_trip() {
assert_eq!(imp.bodies.len(), st.bodies.len());
for (a, b) in st.bodies.iter().zip(&imp.bodies) {
assert_eq!(a.name, b.name);
let (ma, mb) = (solvecraft_kernel::measure(&a.body).unwrap(), solvecraft_kernel::measure(&b.body).unwrap());
let (ma, mb) = (solidcraft_kernel::measure(&a.body).unwrap(), solidcraft_kernel::measure(&b.body).unwrap());
assert!(rel(mb.volume, ma.volume) < 1e-4, "{} vs {}", mb.volume, ma.volume);
assert!(rel(mb.area, ma.area) < 1e-4, "{} vs {}", mb.area, ma.area);
assert!(b.file_faces <= a.body.face_count() && b.file_faces >= ma.merged.faces.min(a.body.face_count()));
@@ -1389,13 +1389,13 @@ fn material_and_appearance_colours_export_to_step() {
assert!(s.execute("appearance.assign", &json!({"bodies": ["RedAbs"], "color": "#12"})).is_err());
let rgb = |c: [u8; 3]| c.map(|x| x as f32 / 255.0);
let want =
[("BrassBox", Some(rgb(solvecraft_doc::material_color("Brass").unwrap()))), ("RedAbs", Some(rgb([0xd0, 0x1c, 0x1c]))), ("Plain", None)];
[("BrassBox", Some(rgb(solidcraft_doc::material_color("Brass").unwrap()))), ("RedAbs", Some(rgb([0xd0, 0x1c, 0x1c]))), ("Plain", None)];
let same = |a: Option<[f32; 3]>, b: Option<[f32; 3]>| match (a, b) {
(Some(a), Some(b)) => a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-3),
(None, None) => true,
_ => false,
};
let check = |imp: &solvecraft_kernel::StepImport| {
let check = |imp: &solidcraft_kernel::StepImport| {
for (name, c) in want {
let b = imp.bodies.iter().find(|b| b.name == name).unwrap_or_else(|| panic!("no body {name}"));
assert!(same(b.body.color(), c), "{name}: {:?} vs {c:?}", b.body.color());
@@ -1403,7 +1403,7 @@ fn material_and_appearance_colours_export_to_step() {
};
check(&step_round_trip(&mut s, "colours"));
// Open the export and export again: the file's colours are kept.
let p = std::env::temp_dir().join(format!("solvecraft-colours-{}.step", std::process::id()));
let p = std::env::temp_dir().join(format!("solidcraft-colours-{}.step", std::process::id()));
run(&mut s, "file.export", json!({"path": p.to_string_lossy()}));
let mut s2 = Session::default();
run(&mut s2, "doc.open", json!({"path": p.to_string_lossy()}));
@@ -1429,7 +1429,7 @@ fn face_colours_and_opacity_export() {
assert_eq!(paint.faces[0].face, top);
assert!((paint.faces[0].color[0] - 1.0).abs() < 1e-3 && paint.faces[0].color[1].abs() < 1e-3, "{paint:?}");
// Opened again, the face keeps its colour (shown and exported).
let p = std::env::temp_dir().join(format!("solvecraft-facecol-{}.step", std::process::id()));
let p = std::env::temp_dir().join(format!("solidcraft-facecol-{}.step", std::process::id()));
run(&mut s, "file.export", json!({"path": p.to_string_lossy()}));
let mut s2 = Session::default();
run(&mut s2, "doc.open", json!({"path": p.to_string_lossy()}));
@@ -1448,8 +1448,8 @@ fn iges_export_and_open() {
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 5, "body_name": "Plate"}));
run(&mut s, "solid.cylinder", json!({"radius": 4, "height": 20, "base": [10, 10, 5], "body_name": "Pin"}));
run(&mut s, "appearance.assign", json!({"bodies": ["Pin"], "color": "#ff0000"}));
let want: Vec<f64> = s.world_state().bodies.iter().map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).collect();
let p = std::env::temp_dir().join(format!("solvecraft-iges-{}.igs", std::process::id()));
let want: Vec<f64> = s.world_state().bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).collect();
let p = std::env::temp_dir().join(format!("solidcraft-iges-{}.igs", std::process::id()));
run(&mut s, "file.export", json!({"path": p.to_string_lossy()}));
let mut s2 = Session::default();
run(&mut s2, "doc.open", json!({"path": p.to_string_lossy()}));
@@ -1458,7 +1458,7 @@ fn iges_export_and_open() {
let names: Vec<&str> = st.bodies.iter().map(|b| b.name.as_str()).collect();
assert_eq!(names, ["Plate", "Pin"]);
for (b, v) in st.bodies.iter().zip(&want) {
assert!(rel(solvecraft_kernel::measure(&b.body).unwrap().volume, *v) < 1e-4);
assert!(rel(solidcraft_kernel::measure(&b.body).unwrap().volume, *v) < 1e-4);
}
assert_eq!(st.bodies[1].body.color(), Some([1.0, 0.0, 0.0]));
}
@@ -1473,7 +1473,7 @@ fn split_body_by_a_face() {
run(&mut s, "solid.split_body", json!({"body": "Block", "tool": {"body": "Tool", "point": [9, 10, 10]}}));
let st = s.world_state();
let mut v: Vec<f64> =
st.bodies.iter().filter(|b| b.name.starts_with("Block")).map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).collect();
st.bodies.iter().filter(|b| b.name.starts_with("Block")).map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).collect();
v.sort_by(f64::total_cmp);
let core = PI * 16.0 * 30.0;
assert!(v.len() == 2 && rel(v[0], core) < 1e-3 && rel(v[1], 6000.0 - core) < 1e-3, "{v:?}");
@@ -1488,12 +1488,12 @@ fn chord_and_variable_fillets() {
run(&mut s, "solid.box", json!({"length": 10, "width": 20, "height": 30, "body_name": "B"}));
run(&mut s, "solid.fillet", json!({"edges": [[10, 10, 30]], "radius": 4, "type": "chord"}));
let r = 4.0 / 2f64.sqrt();
let v = solvecraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume;
let v = solidcraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume;
assert!(rel(v, 6000.0 - (1.0 - PI / 4.0) * r * r * 20.0) < 1e-4, "{v}");
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 10, "width": 20, "height": 30, "body_name": "B"}));
run(&mut s, "solid.fillet", json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable", "radius2": 5, "start": [10, 0, 30]}));
let v = solvecraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume;
let v = solidcraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume;
let removed = (1.0 - PI / 4.0) * 20.0 * (4.0 + 10.0 + 25.0) / 3.0;
assert!(rel(v, 6000.0 - removed) < 1e-4, "{v}");
assert!(s.execute("solid.fillet", &json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable"})).is_err());
@@ -1506,7 +1506,7 @@ fn chord_and_variable_fillets() {
"solid.fillet",
json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable", "radius2": 2, "start": [10, 0, 30], "mid": [[0.5, "waist"]]}),
);
let vol = |s: &Session| solvecraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume;
let vol = |s: &Session| solidcraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume;
let removed = 2.0 * (1.0 - PI / 4.0) * 10.0 * (4.0 + 8.0 + 16.0) / 3.0;
assert!(rel(vol(&s), 6000.0 - removed) < 1e-4, "{}", vol(&s));
// The middle radius follows its parameter.
@@ -1530,10 +1530,10 @@ fn components_export_as_step_assembly() {
run(&mut s, "component.create", json!({"name": "Pin"}));
run(&mut s, "solid.cylinder", json!({"radius": 2, "height": 10, "base": [5, 5, 5]}));
run(&mut s, "occurrence.copy", json!({"component": "Pin", "translate": [20, 0, 0]}));
let want: f64 = s.world_state().bodies.iter().map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).sum();
let want: f64 = s.world_state().bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).sum();
let imp = step_round_trip(&mut s, "asm");
assert_eq!(imp.bodies.len(), 3, "{:?}", imp.bodies.iter().map(|b| &b.name).collect::<Vec<_>>());
let got: f64 = imp.bodies.iter().map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).sum();
let got: f64 = imp.bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).sum();
assert!(rel(got, want) < 1e-4, "{got} vs {want}");
let root = &imp.tree[0];
assert_eq!(root.children.len(), 2);
@@ -1543,7 +1543,7 @@ fn components_export_as_step_assembly() {
.bodies
.iter()
.filter(|b| b.path.last().is_some_and(|p| p == "Pin"))
.map(|b| solvecraft_kernel::measure(&b.body).unwrap().centroid.x)
.map(|b| solidcraft_kernel::measure(&b.body).unwrap().centroid.x)
.collect();
assert_eq!(xs.len(), 2);
assert!(((xs[0] - xs[1]).abs() - 20.0).abs() < 1e-6, "{xs:?}");
@@ -1607,7 +1607,7 @@ fn appearances_by_face_body_and_component() {
assert_eq!(look(&mut s, "Shell")["appearance"]["name"], "Brass");
// Saved and opened again.
run(&mut s, "appearance.assign", json!({"faces": [[5, 5, 10]], "color": "#00ff00"}));
let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let back = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(back.appearances, s.doc.appearances);
assert!(s.execute("appearance.assign", &json!({"bodies": ["Shell"], "appearance": "Unobtainium"})).is_err());
assert!(s.execute("appearance.assign", &json!({"color": "#ffffff"})).is_err(), "no target");
@@ -1627,16 +1627,16 @@ fn cached_evaluation_matches_from_scratch() {
);
run(&mut s, "solid.box", json!({"corner": [-50, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "Side"}));
let same = |s: &Session, what: &str| {
let mut fresh = solvecraft_doc::Model::new();
let mut fresh = solidcraft_doc::Model::new();
fresh.no_cache = true;
fresh.evaluate(&s.doc);
let errs = |m: &solvecraft_doc::Model| m.results.iter().map(|r| (r.id, r.error.clone(), r.skipped)).collect::<Vec<_>>();
let errs = |m: &solidcraft_doc::Model| m.results.iter().map(|r| (r.id, r.error.clone(), r.skipped)).collect::<Vec<_>>();
assert_eq!(errs(&s.model), errs(&fresh), "{what}: feature results");
let shape = |st: &solvecraft_doc::ModelState| {
let shape = |st: &solidcraft_doc::ModelState| {
st.bodies
.iter()
.map(|b| {
let m = solvecraft_kernel::measure(&b.body).unwrap();
let m = solidcraft_kernel::measure(&b.body).unwrap();
// Volume and area to 1e-9: two evaluations from scratch differ in the last
// bits too (the kernel's tessellation is not bit-for-bit repeatable).
(b.name.clone(), b.feature, (m.volume * 1e6).round(), (m.area * 1e6).round(), m.faces, m.edges, m.vertices, m.merged.faces)
@@ -1647,14 +1647,14 @@ fn cached_evaluation_matches_from_scratch() {
assert_eq!(s.model.state().sketches.len(), fresh.state().sketches.len(), "{what}: sketches");
};
// Two evaluations from scratch agree to the same precision.
let mut a = solvecraft_doc::Model::new();
let mut a = solidcraft_doc::Model::new();
a.no_cache = true;
a.evaluate(&s.doc);
let mut b = solvecraft_doc::Model::new();
let mut b = solidcraft_doc::Model::new();
b.no_cache = true;
b.evaluate(&s.doc);
let vols = |m: &solvecraft_doc::Model| {
m.state().bodies.iter().map(|x| (solvecraft_kernel::measure(&x.body).unwrap().volume * 1e6).round()).collect::<Vec<_>>()
let vols = |m: &solidcraft_doc::Model| {
m.state().bodies.iter().map(|x| (solidcraft_kernel::measure(&x.body).unwrap().volume * 1e6).round()).collect::<Vec<_>>()
};
assert_eq!(vols(&a), vols(&b));
same(&s, "built");
@@ -1859,14 +1859,14 @@ mod naming {
run(&mut s, "edit.undo", json!({}));
assert_eq!(attached(&s), Some(10.));
assert!((top(&mut s) - 15.).abs() < 1e-6);
let reopened = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let mut model = solvecraft_doc::Model::new();
let reopened = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let mut model = solidcraft_doc::Model::new();
model.evaluate(&reopened);
assert_eq!(model.state().sketch(reopened.find_feature("Top").unwrap().id).unwrap().plane.origin.z, 10.);
assert!((model.state().bodies[0].mesh().bounds().max.z - 15.).abs() < 1e-6);
// A historical format-10 attachment uses its saved generic face name and no strong anchor.
let mut old = (*s.doc).clone();
let solvecraft_doc::FeatureKind::Sketch { plane: solvecraft_doc::PlaneRef::Face { anchor, name, .. }, sketch } =
let solidcraft_doc::FeatureKind::Sketch { plane: solidcraft_doc::PlaneRef::Face { anchor, name, .. }, sketch } =
&mut old.features.iter_mut().find(|f| f.name == "Top").unwrap().kind
else {
panic!("fixture must contain a face sketch");
@@ -1878,8 +1878,8 @@ mod naming {
link.face_name = name.clone();
}
}
old.format = "solvecraft/10".into();
let mut legacy = Session::new(solvecraft_doc::Document::from_json(&old.to_json()).unwrap());
old.format = "solidcraft/10".into();
let mut legacy = Session::new(solidcraft_doc::Document::from_json(&old.to_json()).unwrap());
assert_eq!(attached(&legacy), Some(10.));
assert!((top(&mut legacy) - 15.).abs() < 1e-6);
// The upstream join consumes the certified body; new attachments cannot follow its substitute roof.
@@ -1893,10 +1893,10 @@ mod naming {
}
assert_eq!(attached(&legacy), Some(10.), "historical weak naming retains its saved geometry behavior");
assert!((top(&mut legacy) - 20.).abs() < 1e-6);
let legacy_reopened = Session::new(solvecraft_doc::Document::from_json(&legacy.doc.to_json()).unwrap());
let legacy_reopened = Session::new(solidcraft_doc::Document::from_json(&legacy.doc.to_json()).unwrap());
assert_eq!(attached(&legacy_reopened), Some(10.));
assert!((legacy_reopened.model.state().bodies[0].mesh().bounds().max.z - 20.).abs() < 1e-6);
let solvecraft_doc::FeatureKind::Sketch { plane: solvecraft_doc::PlaneRef::Face { anchor, .. }, .. } =
let solidcraft_doc::FeatureKind::Sketch { plane: solidcraft_doc::PlaneRef::Face { anchor, .. }, .. } =
&legacy_reopened.doc.find_feature("Top").unwrap().kind
else {
panic!("saved historical face reference must remain present");
@@ -2171,7 +2171,7 @@ fn appearances_survive_later_features() {
// Exported with a body colour and the red face, then opened: the colours come in as the
// body's own; a hole afterwards keeps them.
run(&mut s, "appearance.assign", json!({"bodies": ["B"], "color": "#2050c0"}));
let dir = std::env::temp_dir().join(format!("solvecraft-paint-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-paint-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let step = dir.join("painted.step");
run(&mut s, "file.export", json!({"path": step.to_string_lossy()}));
@@ -2192,10 +2192,10 @@ fn appearances_survive_later_features() {
/// The original #2 RGB import acceptance, including later topology and placement edits.
#[test]
fn imported_rgb_appearances_survive_edits_reopen_and_step_export() {
let dir = std::env::temp_dir().join(format!("solvecraft-imported-rgb-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-imported-rgb-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let input = dir.join("rgb.step");
let saved = dir.join("edited.solvecraft");
let saved = dir.join("edited.solidcraft");
let output = dir.join("edited.step");
let mut source = Session::default();
for (name, x, color) in [("Red", 0, "#ff0000"), ("Green", 40, "#00ff00"), ("Blue", 80, "#0000ff")] {
@@ -2380,7 +2380,7 @@ fn combine_keep_tools_and_new_component() {
assert!(st.body("B").is_some(), "the tool is kept");
let a = st.body("A").unwrap();
assert_eq!(s.doc.body_component("A", a.feature), comp);
assert!(rel(solvecraft_kernel::measure(&a.body).unwrap().volume, 4000.0) < 1e-9);
assert!(rel(solidcraft_kernel::measure(&a.body).unwrap().volume, 4000.0) < 1e-9);
}
/// Align with snap points: a box's vertex onto another's face centre, an edge midpoint onto a
@@ -2491,13 +2491,13 @@ fn drawing_on_existing_points_connects_to_them() {
fn computed_import_preserves_file_and_earlier_undo_history() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
s.path = Some("existing.solvecraft".into());
s.path = Some("existing.solidcraft".into());
s.mark_saved();
let saved = s.doc.clone();
let mut computed = s.scratch();
run(&mut computed, "solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [30, 0, 0]}));
s.accept_computed(computed);
assert_eq!(s.path.as_deref(), Some("existing.solvecraft"));
assert_eq!(s.path.as_deref(), Some("existing.solidcraft"));
assert!(s.is_dirty());
assert_eq!(s.model.state().bodies.len(), 2);
run(&mut s, "edit.undo", json!({}));
@@ -2536,7 +2536,7 @@ fn special_files_are_refused_by_every_file_command() {
assert!(r.as_ref().is_err_and(|e| e.to_string().contains("not a regular file")), "{cmd}: {r:?}");
}
// The commands that check the extension first need a special file with the right name.
let dir = std::env::temp_dir().join(format!("solvecraft-special-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-special-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
for (cmd, name) in [("file.insert_step", "x.step"), ("file.insert_mesh", "x.3mf"), ("sketch.insert_dxf", "x.dxf")] {
let p = dir.join(name);
@@ -2777,12 +2777,12 @@ fn a_design_saved_before_format_14_keeps_generic_names_past_body_name() {
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch": "R", "distance": "10", "operation": "new_body", "body_name": "Ribs"}));
let mut legacy: Value = serde_json::from_str(&s.doc.to_json()).unwrap();
legacy["format"] = json!("solvecraft/13");
legacy["format"] = json!("solidcraft/13");
let extrude = legacy["features"].as_array_mut().unwrap().iter_mut().find(|f| f["type"] == "extrude").unwrap();
extrude["body_names"] = json!(["Ribs"]);
let dir = std::env::temp_dir().join(format!("solvecraft-373-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-373-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let design = dir.join("ribs.solvecraft");
let design = dir.join("ribs.solidcraft");
std::fs::write(&design, legacy.to_string()).unwrap();
let mut old = Session::default();
run(&mut old, "doc.open", json!({"path": design.to_string_lossy()}));
@@ -1,5 +1,5 @@
use super::*;
use solvecraft_kernel::BooleanStrategy;
use solidcraft_kernel::BooleanStrategy;
#[test]
fn boolean_retry_warning_and_details_survive_cache_undo_and_reopen() {
@@ -41,7 +41,7 @@ fn boolean_retry_warning_and_details_survive_cache_undo_and_reopen() {
run(&mut session, "edit.undo", json!({}));
run(&mut session, "edit.redo", json!({}));
assert_eq!(check(&session), original);
let reopened_doc = solvecraft_doc::Document::from_json(&session.doc.to_json()).unwrap();
let reopened_doc = solidcraft_doc::Document::from_json(&session.doc.to_json()).unwrap();
let reopened = Session::new(reopened_doc);
assert_eq!(check(&reopened), original);
// A later independent feature must not inherit the earlier retry's warning.
+5 -5
View File
@@ -1,9 +1,9 @@
use super::*;
use solvecraft_doc::{Document, FeatureKind, MeshData};
use solvecraft_geom::Vec3;
use solidcraft_doc::{Document, FeatureKind, MeshData};
use solidcraft_geom::Vec3;
fn imported_box() -> Session {
let body = solvecraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 20., 30.)).unwrap();
let body = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 20., 30.)).unwrap();
let mesh = body.tessellate(0.01).unwrap();
let mut doc = Document::default();
doc.add_feature(
@@ -78,7 +78,7 @@ fn exported_binary_stl_with_a_bore_reconstructs_analytic_faces() {
run(&mut s, "sketch.circle.center", json!({"center":[10,10],"radius":4}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch":"Section","profiles":[{"point":[30,10]}],"distance":15,"body_name":"Bracket"}));
let path = std::env::temp_dir().join(format!("solvecraft-mesh-convert-{}.stl", std::process::id()));
let path = std::env::temp_dir().join(format!("solidcraft-mesh-convert-{}.stl", std::process::id()));
run(&mut s, "file.save_mesh", json!({"path":path}));
run(&mut s, "doc.open", json!({"path":path}));
let name = s.model.state().bodies[0].name.clone();
@@ -87,7 +87,7 @@ fn exported_binary_stl_with_a_bore_reconstructs_analytic_faces() {
let body = &state.bodies[0].body;
assert!(!body.is_mesh());
assert!(body.validity().is_empty());
assert_eq!(solvecraft_kernel::measure(body).unwrap().merged.face_types.get("cylinder"), Some(&1));
assert_eq!(solidcraft_kernel::measure(body).unwrap().merged.face_types.get("cylinder"), Some(&1));
assert_eq!(body.faces(0.01).unwrap().len(), 10);
std::fs::remove_file(path).unwrap();
}
+1 -1
View File
@@ -9,7 +9,7 @@ use crate::*;
fn min_x(s: &Session, body: &str) -> f64 {
let b = s.model.state().body(body).map(|b| b.body.clone()).unwrap();
solvecraft_kernel::measure(&b).unwrap().bbox.min.x
solidcraft_kernel::measure(&b).unwrap().bbox.min.x
}
#[test]
Loaded 100 of 464 files, more files were not shown because too many files have changed in this diff. Show more