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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
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

+4 -4
View File
@@ -1,5 +1,5 @@
[package]
name = "solvecraft-doc"
name = "solidcraft-doc"
description = "SolidCraft document model: parameters and expressions, sketches and features on a parametric timeline, incremental evaluation"
version.workspace = true
edition.workspace = true
@@ -9,9 +9,9 @@ license.workspace = true
workspace = true
[dependencies]
solvecraft-geom = { workspace = true }
solvecraft-sketch = { workspace = true }
solvecraft-kernel = { workspace = true }
solidcraft-geom = { workspace = true }
solidcraft-sketch = { workspace = true }
solidcraft-kernel = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
+1 -1
View File
@@ -7,7 +7,7 @@ pub fn state_in_face_anchor_frame<'a>(doc: &Document, state: &'a ModelState, anc
let raw = state.body(&anchor.target.body).ok_or_else(|| DocError::Unknown(format!("anchored body `{}`", anchor.target.body)))?;
let relative = anchor.source_to_author(doc, author, raw)?;
let mut output = crate::state_in_frame_at(doc, state, author, frame).into_owned();
let shape = if crate::is_identity(&relative) { raw.body.clone() } else { solvecraft_kernel::transform_matrix(&raw.body, relative)? };
let shape = if crate::is_identity(&relative) { raw.body.clone() } else { solidcraft_kernel::transform_matrix(&raw.body, relative)? };
let mut body = ModelBody::new(raw.name.clone(), shape, raw.feature);
body.component = raw.component;
body.transferred = raw.transferred;
+1 -1
View File
@@ -6,7 +6,7 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use crate::Document;
+1 -1
View File
@@ -9,7 +9,7 @@
//! and the engine maps a new feature's geometry into the active component's frame.
use serde::{Deserialize, Serialize};
use solvecraft_geom::{Plane, Vec3};
use solidcraft_geom::{Plane, Vec3};
use crate::{AxisRef, DocError, Document, FeatureKind, PatternKind, PlaneRef, Result};
+2 -2
View File
@@ -1,6 +1,6 @@
//! Recorded ownership changes are evaluated at their timeline position.
use crate::{DocError, Document, Mat, ModelBody, ModelState, Result};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use std::collections::BTreeSet;
pub fn valid_transfer_matrix(m: &Mat) -> bool {
@@ -39,7 +39,7 @@ pub(crate) fn evaluate(doc: &Document, st: &mut ModelState, bodies: &[String], f
if doc.model_body_component(old) != from {
return Err(DocError::Invalid(format!("body `{name}` no longer belongs to transfer source component {from}")));
}
let shape = if crate::is_identity(&transform) { old.body.clone() } else { solvecraft_kernel::transform_matrix(&old.body, transform)? };
let shape = if crate::is_identity(&transform) { old.body.clone() } else { solidcraft_kernel::transform_matrix(&old.body, transform)? };
let mut body = ModelBody::new(old.name.clone(), shape, old.feature);
body.component = to;
body.transferred = true;
+1 -1
View File
@@ -1,5 +1,5 @@
//! World-frame metadata at a timeline body ownership boundary.
use solvecraft_geom::Plane;
use solidcraft_geom::Plane;
use crate::{DocError, Mat, ModelState, Result, apply_point, apply_vector};
+1 -1
View File
@@ -2,7 +2,7 @@
//! The image bytes travel inside the design file (base64), so it reopens without the original.
use serde::{Deserialize, Serialize};
use solvecraft_geom::{Plane, Vec2, Vec3};
use solidcraft_geom::{Plane, Vec2, Vec3};
use crate::PlaneRef;
+4 -4
View File
@@ -202,13 +202,13 @@ fn remap_plane(
}
fn remap_link(
source: &mut solvecraft_sketch::LinkSource,
source: &mut solidcraft_sketch::LinkSource,
ids: &BTreeMap<u64, u64>,
names: &BTreeMap<String, String>,
bodies: &BTreeMap<String, String>,
depth: usize,
) {
use solvecraft_sketch::LinkSource;
use solidcraft_sketch::LinkSource;
if depth > 64 {
return;
}
@@ -441,11 +441,11 @@ fn checked_plane(p: &PlaneRef, depth: usize) -> crate::Result<()> {
}
Ok(())
}
fn checked_link(source: &solvecraft_sketch::LinkSource, depth: usize) -> crate::Result<()> {
fn checked_link(source: &solidcraft_sketch::LinkSource, depth: usize) -> crate::Result<()> {
if depth > 64 {
return Err(crate::DocError::Invalid("copy link nesting exceeds 64".into()));
}
if let solvecraft_sketch::LinkSource::Intersection { a, b } = source {
if let solidcraft_sketch::LinkSource::Intersection { a, b } = source {
checked_link(a, depth + 1)?;
checked_link(b, depth + 1)?;
}
+37 -37
View File
@@ -1,7 +1,7 @@
//! Certificates are producer identities, never a nearest-surface selection.
use crate::{DocError, Document, FeatureKind, ModelBody, Result, SolvedSketch};
use serde::{Deserialize, Serialize};
use solvecraft_geom::Seg2;
use solidcraft_geom::Seg2;
fn valid_signature(outer: &[String], holes: &[Vec<String>]) -> bool {
let valid = |ids: &[String]| {
@@ -68,11 +68,11 @@ pub(crate) fn cap_key(outer: &[String], holes: &[Vec<String>]) -> String {
}
format!("{:x}", hash.finalize())
}
fn exact_region_signature(curves: &ExactSketchCurves, region: &solvecraft_geom::Region2) -> Option<solvecraft_kernel::CapSignature> {
fn exact_region_signature(curves: &ExactSketchCurves, region: &solidcraft_geom::Region2) -> Option<solidcraft_kernel::CapSignature> {
if region.outer.segs.len() + region.holes.iter().map(|hole| hole.segs.len()).sum::<usize>() > 4096 || region.holes.len() > 128 {
return None;
}
let signature = |outline: &solvecraft_geom::Loop2| {
let signature = |outline: &solidcraft_geom::Loop2| {
let mut ids = outline.segs.iter().map(|segment| curves.find(segment)).collect::<Option<Vec<_>>>()?;
ids.sort();
ids.dedup();
@@ -85,9 +85,9 @@ fn exact_region_signature(curves: &ExactSketchCurves, region: &solvecraft_geom::
}
/// Charge all source-curve comparisons before matching; over-budget tools stay usable but uncertified.
type CapBindings = Vec<Vec<Option<solvecraft_kernel::CapSignature>>>;
pub(crate) fn extrude_cap_bindings(sketch: &SolvedSketch, bodies: &[solvecraft_kernel::Body], budget: usize) -> Option<CapBindings> {
use solvecraft_kernel::FaceSource;
type CapBindings = Vec<Vec<Option<solidcraft_kernel::CapSignature>>>;
pub(crate) fn extrude_cap_bindings(sketch: &SolvedSketch, bodies: &[solidcraft_kernel::Body], budget: usize) -> Option<CapBindings> {
use solidcraft_kernel::FaceSource;
if sketch.sketch.curves.len() > 2000 {
return None;
}
@@ -116,7 +116,7 @@ pub(crate) fn extrude_cap_bindings(sketch: &SolvedSketch, bodies: &[solvecraft_k
let mut counts = std::collections::BTreeMap::new();
let mut bindings = Vec::with_capacity(bodies.len());
for body in bodies {
let mut cached: Option<(&solvecraft_geom::Region2, Option<solvecraft_kernel::CapSignature>)> = None;
let mut cached: Option<(&solidcraft_geom::Region2, Option<solidcraft_kernel::CapSignature>)> = None;
let mut values = Vec::new();
for source in body.face_sources().into_iter().flatten() {
let signature = if let Some(FaceSource::ExtrudeCap { region, high, .. }) = source {
@@ -260,8 +260,8 @@ pub(crate) fn shell_certificate_role(source: &FaceCertificate, inner: bool) -> R
Ok(format!("shell:{}:{role}", if inner { "inner" } else { "outer" }))
}
pub(crate) fn flat_source_certificate(source: &solvecraft_kernel::FaceSource) -> Option<(u64, FaceCertificate)> {
use solvecraft_kernel::FaceSource;
pub(crate) fn flat_source_certificate(source: &solidcraft_kernel::FaceSource) -> Option<(u64, FaceCertificate)> {
use solidcraft_kernel::FaceSource;
match source {
FaceSource::Box { producer: Some(producer), axis, high, .. } => Some((*producer, FaceCertificate::Box { axis: *axis, high: *high })),
FaceSource::CertifiedExtrudeSide { producer, sketch, curve, .. } => {
@@ -277,8 +277,8 @@ pub(crate) fn flat_source_certificate(source: &solvecraft_kernel::FaceSource) ->
}
}
pub(crate) fn flat_source_matches(certificate: &FaceCertificate, source: &solvecraft_kernel::FaceSource, producer: u64) -> bool {
use solvecraft_kernel::FaceSource;
pub(crate) fn flat_source_matches(certificate: &FaceCertificate, source: &solidcraft_kernel::FaceSource, producer: u64) -> bool {
use solidcraft_kernel::FaceSource;
match (certificate, source) {
(FaceCertificate::Box { axis, high }, FaceSource::Box { producer: Some(current), axis: current_axis, high: current_high, .. }) => {
producer == *current && axis == current_axis && high == current_high
@@ -324,14 +324,14 @@ impl<'a> ExactSketchCurves<'a> {
if curve.construction
|| !matches!(
curve.kind,
solvecraft_sketch::CurveKind::Line { .. }
| solvecraft_sketch::CurveKind::Arc { .. }
| solvecraft_sketch::CurveKind::Circle { .. }
solidcraft_sketch::CurveKind::Line { .. }
| solidcraft_sketch::CurveKind::Arc { .. }
| solidcraft_sketch::CurveKind::Circle { .. }
)
{
continue;
}
let originals = if let solvecraft_sketch::CurveKind::Circle { c, r } = curve.kind {
let originals = if let solidcraft_sketch::CurveKind::Circle { c, r } = curve.kind {
sketch
.sketch
.point(c)
@@ -364,7 +364,7 @@ fn exact_segment_contains(original: &Seg2, generated: &Seg2) -> bool {
(Seg2::Line { a, b }, Seg2::Line { a: c, b: d }) => {
let direction = *b - *a;
let scale = direction.len().max((*d - *c).len()).max(1.);
let interval = |point: solvecraft_geom::Vec2| {
let interval = |point: solidcraft_geom::Vec2| {
let t = (point - *a).dot(direction) / direction.len2();
(-1e-10..=1. + 1e-10).contains(&t)
};
@@ -400,18 +400,18 @@ fn exact_segment_contains(original: &Seg2, generated: &Seg2) -> bool {
}
impl Document {
pub fn configuration_face_target(&self, body: &ModelBody, index: usize, point: solvecraft_geom::Vec3) -> Result<crate::config::ConfigFaceTarget> {
pub fn configuration_face_target(&self, body: &ModelBody, index: usize, point: solidcraft_geom::Vec3) -> Result<crate::config::ConfigFaceTarget> {
let names = crate::naming::face_names(body);
let mut name = names
.get(index)
.map(|name| crate::naming::strip_piece(name).to_string())
.ok_or_else(|| DocError::Unknown("face source name missing".into()))?;
let origin = match body.body.face_sources().and_then(|sources| sources.get(index)).and_then(Option::as_ref) {
Some(solvecraft_kernel::FaceSource::Imported { producer: Some(producer), .. })
| Some(solvecraft_kernel::FaceSource::CertifiedExtrudeSide { producer, .. })
| Some(solvecraft_kernel::FaceSource::CertifiedExtrudeCap { producer, .. })
| Some(solvecraft_kernel::FaceSource::ShellPlanar { producer: Some(producer), .. })
| Some(solvecraft_kernel::FaceSource::Box { producer: Some(producer), .. }) => *producer,
Some(solidcraft_kernel::FaceSource::Imported { producer: Some(producer), .. })
| Some(solidcraft_kernel::FaceSource::CertifiedExtrudeSide { producer, .. })
| Some(solidcraft_kernel::FaceSource::CertifiedExtrudeCap { producer, .. })
| Some(solidcraft_kernel::FaceSource::ShellPlanar { producer: Some(producer), .. })
| Some(solidcraft_kernel::FaceSource::Box { producer: Some(producer), .. }) => *producer,
_ => name
.strip_prefix('F')
.and_then(|name| name.split_once(':'))
@@ -439,7 +439,7 @@ impl Document {
let Some(source) = body.body.face_sources().and_then(|sources| sources.get(index)).and_then(Option::as_ref) else {
return Ok(None);
};
if let solvecraft_kernel::FaceSource::ShellPlanar { producer: Some(producer), source, inner, .. } = source {
if let solidcraft_kernel::FaceSource::ShellPlanar { producer: Some(producer), source, inner, .. } = source {
if !self.feature(*producer).is_some_and(|feature| matches!(feature.kind, FeatureKind::Shell { .. })) {
return Err(DocError::Invalid("shell source producer missing".into()));
}
@@ -452,7 +452,7 @@ impl Document {
certificate.validate()?;
return Ok(Some(certificate));
}
if let solvecraft_kernel::FaceSource::Imported { item, face, payload_sha256: Some(digest), producer: Some(producer) } = source {
if let solidcraft_kernel::FaceSource::Imported { item, face, payload_sha256: Some(digest), producer: Some(producer) } = source {
if !self.feature(*producer).is_some_and(|feature| matches!(feature.kind, FeatureKind::Import { .. })) {
return Err(DocError::Invalid("import source producer missing".into()));
}
@@ -461,7 +461,7 @@ impl Document {
return Ok(Some(certificate));
}
let certificate = match source {
solvecraft_kernel::FaceSource::CertifiedExtrudeSide { producer, sketch, curve, .. } => {
solidcraft_kernel::FaceSource::CertifiedExtrudeSide { producer, sketch, curve, .. } => {
if !self.feature(*producer).is_some_and(|feature| matches!(feature.kind, FeatureKind::Extrude { .. })) {
return Err(DocError::Invalid("side source producer missing".into()));
}
@@ -472,16 +472,16 @@ impl Document {
certificate.validate()?;
Ok(Some(certificate))
}
pub fn strong_face_target(&self, body: &ModelBody, index: usize, point: solvecraft_geom::Vec3) -> Result<crate::config::ConfigFaceTarget> {
pub fn strong_face_target(&self, body: &ModelBody, index: usize, point: solidcraft_geom::Vec3) -> Result<crate::config::ConfigFaceTarget> {
let mut target = self.configuration_face_target(body, index, point)?;
match body.body.face_sources().and_then(|sources| sources.get(index)).and_then(Option::as_ref) {
Some(solvecraft_kernel::FaceSource::Box { producer: Some(producer), axis, high, .. }) => {
Some(solidcraft_kernel::FaceSource::Box { producer: Some(producer), axis, high, .. }) => {
target.origin_feature = *producer;
let axis_name = ["x", "y", "z"].get(*axis as usize).ok_or_else(|| DocError::Invalid("invalid box face axis".into()))?;
target.name = format!("F{producer}:{}{axis_name}", if *high { "+" } else { "-" });
target.certificate = Some(FaceCertificate::Box { axis: *axis, high: *high });
}
Some(solvecraft_kernel::FaceSource::CertifiedExtrudeCap { producer, sketch, high, outer, holes, .. }) => {
Some(solidcraft_kernel::FaceSource::CertifiedExtrudeCap { producer, sketch, high, outer, holes, .. }) => {
target.origin_feature = *producer;
target.name = format!("F{producer}:cap:{}:{}", if *high { "high" } else { "low" }, cap_key(outer, holes));
target.certificate = Some(FaceCertificate::ExtrudeCap { sketch: *sketch, high: *high, outer: outer.clone(), holes: holes.clone() });
@@ -507,15 +507,15 @@ mod tests {
use super::*;
use serde_json::json;
fn solved(mut sketch: solvecraft_sketch::Sketch) -> SolvedSketch {
let report = solvecraft_sketch::solve(&mut sketch);
SolvedSketch { feature: 1, name: "sources".into(), plane: solvecraft_geom::Plane::XY, sketch, report, profiles: Vec::new() }
fn solved(mut sketch: solidcraft_sketch::Sketch) -> SolvedSketch {
let report = solidcraft_sketch::solve(&mut sketch);
SolvedSketch { feature: 1, name: "sources".into(), plane: solidcraft_geom::Plane::XY, sketch, report, profiles: Vec::new() }
}
#[test]
fn prepared_sources_keep_exact_identity_and_reject_ambiguity() {
use solvecraft_geom::Vec2;
let mut sketch = solvecraft_sketch::Sketch::new();
use solidcraft_geom::Vec2;
let mut sketch = solidcraft_sketch::Sketch::new();
sketch.add_line(Vec2::ZERO, Vec2::new(10., 0.), None, None, Some("line")).unwrap();
sketch.add_circle(Vec2::new(20., 0.), 2., None, Some("circle")).unwrap();
let construction = sketch.add_line(Vec2::ZERO, Vec2::new(10., 0.), None, None, Some("construction")).unwrap();
@@ -532,7 +532,7 @@ mod tests {
// A prepared pass borrows one immutable evaluated sketch, so an upstream edit
// necessarily prepares fresh sources rather than retaining stale coordinates.
let mut moved = sketch.clone();
moved.sketch = solvecraft_sketch::Sketch::new();
moved.sketch = solidcraft_sketch::Sketch::new();
moved.sketch.add_line(Vec2::new(0., 1.), Vec2::new(10., 1.), None, None, Some("line")).unwrap();
assert_eq!(ExactSketchCurves::new(&moved).find(&partial), None);
}
@@ -540,8 +540,8 @@ mod tests {
#[test]
#[ignore = "manual optimized certification microbenchmark; no timing assertion"]
fn benchmark_prepared_extrude_sources() {
use solvecraft_geom::Vec2;
let mut sketch = solvecraft_sketch::Sketch::new();
use solidcraft_geom::Vec2;
let mut sketch = solidcraft_sketch::Sketch::new();
for i in 0..1000 {
let y = i as f64;
sketch.add_line(Vec2::new(0., y), Vec2::new(10., y), None, None, None).unwrap();
@@ -593,7 +593,7 @@ mod tests {
#[test]
fn analytic_arc_interval_refuses_unsampled_gaps_and_accepts_wrap_reversal() {
let radians = |degrees: f64| degrees.to_radians();
let arc = |start, sweep| Seg2::Arc { center: solvecraft_geom::Vec2::default(), radius: 10., start: radians(start), sweep: radians(sweep) };
let arc = |start, sweep| Seg2::Arc { center: solidcraft_geom::Vec2::default(), radius: 10., start: radians(start), sweep: radians(sweep) };
assert!(!exact_segment_contains(&arc(10., 350.), &arc(20., 360.)), "five point probes missed the missing ten-degree gap");
assert!(!exact_segment_contains(&arc(10., 350.), &arc(20., -360.)));
assert!(exact_segment_contains(&arc(350., 40.), &arc(355., 20.)));
+2 -2
View File
@@ -142,7 +142,7 @@ mod tests {
body: "Old".into(),
origin_feature: 3,
name: "F3:+z".into(),
point: solvecraft_geom::Vec3::new(1., 1., 2.),
point: solidcraft_geom::Vec3::new(1., 1., 2.),
certificate: None,
}),
],
@@ -199,7 +199,7 @@ mod tests {
body: "Foreign".into(),
origin_feature: 3,
name: "F3:+z".into(),
point: solvecraft_geom::Vec3::ZERO,
point: solidcraft_geom::Vec3::ZERO,
certificate: None,
}),
],
+50 -50
View File
@@ -1,7 +1,7 @@
use super::*;
use crate::{FeatureKind, Model, PlaneRef};
use serde_json::json;
use solvecraft_geom::{Vec2, Vec3};
use solidcraft_geom::{Vec2, Vec3};
fn shell_document() -> (Document, u64, u64) {
let mut doc = Document::new("certified shell");
@@ -35,23 +35,23 @@ fn face_index(body: &ModelBody, inner: bool) -> usize {
.unwrap()
.iter()
.position(|source| {
matches!(source, Some(solvecraft_kernel::FaceSource::ShellPlanar { inner: current, source, .. })
if *current == inner && matches!(source.as_ref(), solvecraft_kernel::FaceSource::Box { axis:0,high:true,.. }))
matches!(source, Some(solidcraft_kernel::FaceSource::ShellPlanar { inner: current, source, .. })
if *current == inner && matches!(source.as_ref(), solidcraft_kernel::FaceSource::Box { axis:0,high:true,.. }))
})
.unwrap()
}
#[test]
fn shell_planar_raw_bounds_flat_identity_and_clone_remap() {
let plane = solvecraft_geom::Plane::XY;
let same = |planes: &[Option<solvecraft_geom::Plane>]| super::super::styles::shell_plane_family(planes.iter().copied());
let plane = solidcraft_geom::Plane::XY;
let same = |planes: &[Option<solidcraft_geom::Plane>]| super::super::styles::shell_plane_family(planes.iter().copied());
assert!(same(&[Some(plane), Some(plane)]));
assert!(!same(&[]));
assert!(!same(&[Some(plane), None]));
for different in [
solvecraft_geom::Plane { origin: Vec3::X, ..plane },
solvecraft_geom::Plane { x: -plane.x, y: -plane.y, ..plane },
solvecraft_geom::Plane { y: -plane.y, ..plane },
solidcraft_geom::Plane { origin: Vec3::X, ..plane },
solidcraft_geom::Plane { x: -plane.x, y: -plane.y, ..plane },
solidcraft_geom::Plane { y: -plane.y, ..plane },
] {
assert!(!same(&[Some(plane), Some(different)]));
}
@@ -82,15 +82,15 @@ fn shell_planar_anchor_rebuild_cold_geometry_and_missing_source_errors() {
let body = state.body("Case").unwrap();
let outer = doc.strong_face_target(body, face_index(body, false), Vec3::default()).unwrap();
let outer_matches: Vec<_> = body.body.face_sources().unwrap().iter().enumerate().filter_map(|(index, source)| {
matches!(source, Some(solvecraft_kernel::FaceSource::ShellPlanar { inner: false, source, .. }) if matches!(source.as_ref(), solvecraft_kernel::FaceSource::Box { axis: 0, high: true, .. })).then_some((index, body.body.face_plane(index)))
matches!(source, Some(solidcraft_kernel::FaceSource::ShellPlanar { inner: false, source, .. }) if matches!(source.as_ref(), solidcraft_kernel::FaceSource::Box { axis: 0, high: true, .. })).then_some((index, body.body.face_plane(index)))
}).collect();
assert_eq!(outer_matches.len(), 1, "outer support must be one physical face: {outer_matches:?}");
assert!(super::super::styles::shell_plane_family(outer_matches.iter().map(|(_, plane)| *plane)));
let outer_plane = body.body.face_plane(face_index(body, false)).unwrap();
let outer_anchor = doc.capture_face_anchor(body, face_index(body, false), &outer_plane, &[], 0, &[]).unwrap();
let projected = crate::project::resolve_anchored_face(&doc, &state, &outer_plane, solvecraft_sketch::LinkKind::Project, &outer_anchor).unwrap();
let mut linked = solvecraft_sketch::Sketch::new();
let link = linked.add_link(solvecraft_sketch::LinkKind::Project, projected.source, &projected.geom).unwrap();
let projected = crate::project::resolve_anchored_face(&doc, &state, &outer_plane, solidcraft_sketch::LinkKind::Project, &outer_anchor).unwrap();
let mut linked = solidcraft_sketch::Sketch::new();
let link = linked.add_link(solidcraft_sketch::LinkKind::Project, projected.source, &projected.geom).unwrap();
assert!(!linked.update_link(&link, &projected.geom).unwrap());
let inner_index = face_index(body, true);
let inner = doc.strong_face_target(body, inner_index, Vec3::default()).unwrap();
@@ -100,7 +100,7 @@ fn shell_planar_anchor_rebuild_cold_geometry_and_missing_source_errors() {
let picked = body.body.face_plane(inner_index).unwrap();
assert!((picked.origin.x - 18.).abs() < 1e-8);
let anchor = doc.capture_face_anchor(body, inner_index, &picked, &[], 0, &[]).unwrap();
let mut sketch = solvecraft_sketch::Sketch::new();
let mut sketch = solidcraft_sketch::Sketch::new();
let points: Vec<_> =
[(2., 2.), (4., 2.), (4., 4.), (2., 4.)].into_iter().map(|(x, y)| sketch.add_point(Vec2::new(x, y), None).unwrap()).collect();
for edge in 0..4 {
@@ -136,11 +136,11 @@ fn shell_planar_anchor_rebuild_cold_geometry_and_missing_source_errors() {
let plane = doc.resolve_face_anchor(&anchor, 0, &changed).unwrap();
assert!((plane.origin.x - 27.).abs() < 1e-8);
let after = changed.bodies.iter().find(|body| body.feature == boss).unwrap().mesh().bounds();
assert!((solvecraft_kernel::measure(&changed.bodies.iter().find(|body| body.feature == boss).unwrap().body).unwrap().volume - 8.).abs() < 1e-8);
assert!((solidcraft_kernel::measure(&changed.bodies.iter().find(|body| body.feature == boss).unwrap().body).unwrap().volume - 8.).abs() < 1e-8);
assert!((doc.resolve_face_anchor(&outer_anchor, 0, &changed).unwrap().origin.x - 30.).abs() < 1e-8);
let changed_outer_plane = doc.resolve_face_anchor(&outer_anchor, 0, &changed).unwrap();
let refreshed =
crate::project::resolve_anchored_face(&doc, &changed, &changed_outer_plane, solvecraft_sketch::LinkKind::Project, &outer_anchor).unwrap();
crate::project::resolve_anchored_face(&doc, &changed, &changed_outer_plane, solidcraft_sketch::LinkKind::Project, &outer_anchor).unwrap();
assert!(!linked.update_link(&link, &refreshed.geom).unwrap(), "same exterior boundary must retain projected IDs");
assert!((after.min.x - before.min.x - 9.).abs() < 1e-8, "{before:?} -> {after:?}");
assert!((after.max.x - before.max.x - 9.).abs() < 1e-8, "{before:?} -> {after:?}");
@@ -173,11 +173,11 @@ fn shell_planar_anchor_rebuild_cold_geometry_and_missing_source_errors() {
fn shell_planar_format13_keeps_v12_documents_and_refuses_future() {
let (doc, _, _) = shell_document();
let mut legacy: serde_json::Value = serde_json::from_str(&doc.to_json()).unwrap();
legacy["format"] = json!("solvecraft/12");
legacy["format"] = json!("solidcraft/12");
let loaded = crate::format::read(&legacy.to_string()).unwrap();
let mut current: serde_json::Value = serde_json::from_str(&loaded.to_json()).unwrap();
assert_eq!(current["format"], crate::format::format_string(crate::format::FORMAT));
current["format"] = json!("solvecraft/12");
current["format"] = json!("solidcraft/12");
crate::format::pieces_in_shell_order(&mut legacy["features"]);
assert_eq!(legacy, current, "the migration preserves every prior field");
legacy["format"] = json!(crate::format::format_string(crate::format::FORMAT + 1));
@@ -199,7 +199,7 @@ fn regularized_shell_keeps_legacy_and_explicit_single_face_projections_closed()
let body = state.body("Case").unwrap();
let index = face_index(body, false);
let mesh = body.mesh();
assert_eq!(body.body.face_sources().unwrap().iter().filter(|source|matches!(source,Some(solvecraft_kernel::FaceSource::ShellPlanar {inner:false,source,..}) if matches!(source.as_ref(),solvecraft_kernel::FaceSource::Box {axis:0,high:true,..}))).count(),1,"no physical outer-wall strip face may remain");
assert_eq!(body.body.face_sources().unwrap().iter().filter(|source|matches!(source,Some(solidcraft_kernel::FaceSource::ShellPlanar {inner:false,source,..}) if matches!(source.as_ref(),solidcraft_kernel::FaceSource::Box {axis:0,high:true,..}))).count(),1,"no physical outer-wall strip face may remain");
let plane = body.body.face_plane(index).unwrap();
let point = mesh
.triangles
@@ -208,14 +208,14 @@ fn regularized_shell_keeps_legacy_and_explicit_single_face_projections_closed()
.find_map(|(tri, face)| (*face as usize == index).then(|| mesh.tri(tri)).flatten())
.map(|[a, b, c]| (a + b + c) / 3.)
.unwrap();
let source = solvecraft_sketch::LinkSource::Face { body: "Case".into(), at: point };
let source = solidcraft_sketch::LinkSource::Face { body: "Case".into(), at: point };
let (values, _) = doc.param_values();
let legacy = crate::project::resolve(&doc, &values, &state, &plane, solvecraft_sketch::LinkKind::Project, &source).unwrap();
let explicit = crate::project::resolve_face_indices(&state, &plane, solvecraft_sketch::LinkKind::Project, "Case", &[index]).unwrap();
let legacy = crate::project::resolve(&doc, &values, &state, &plane, solidcraft_sketch::LinkKind::Project, &source).unwrap();
let explicit = crate::project::resolve_face_indices(&state, &plane, solidcraft_sketch::LinkKind::Project, "Case", &[index]).unwrap();
assert_eq!(legacy.geom, explicit.geom);
assert_eq!(explicit.geom.len(), 4, "raw planar piece keeps all four edges");
let mut sketch = solvecraft_sketch::Sketch::new();
let link = sketch.add_link(solvecraft_sketch::LinkKind::Project, source, &legacy.geom).unwrap();
let mut sketch = solidcraft_sketch::Sketch::new();
let link = sketch.add_link(solidcraft_sketch::LinkKind::Project, source, &legacy.geom).unwrap();
sketch.links[0].face_name = Some(crate::naming::face_names(body)[index].clone());
let warnings = crate::project::refresh_links(&doc, &values, &state, &plane, &mut sketch);
assert!(warnings.is_empty(), "{warnings:?}");
@@ -242,11 +242,11 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
// Legacy Shell did not propagate construction certificates. Omit transient
// metadata here; operand paint also disables new wall-run coalescence so
// this control retains the actual historical raw pieces.
let raw_source = solvecraft_kernel::box_solid(Vec3::ZERO, Vec3::new(20., 12., 10.)).unwrap().with_paint(Some(solvecraft_kernel::Paint {
let raw_source = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(20., 12., 10.)).unwrap().with_paint(Some(solidcraft_kernel::Paint {
opacity: 1.,
faces: vec![solvecraft_kernel::FacePaint { face: 0, color: [1., 0., 0.], opacity: 1. }],
faces: vec![solidcraft_kernel::FacePaint { face: 0, color: [1., 0., 0.], opacity: 1. }],
}));
let raw = solvecraft_kernel::shell(&raw_source, &[Vec3::new(5., 6., 10.)], 2.).unwrap();
let raw = solidcraft_kernel::shell(&raw_source, &[Vec3::new(5., 6., 10.)], 2.).unwrap();
let mut old_body = ModelBody::new("Case".into(), raw, shell);
old_body.names = crate::naming::NameCell::new(Some(std::sync::Arc::new(crate::naming::NamingOrigin {
feature: doc.feature(shell).unwrap().clone(),
@@ -258,13 +258,13 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
assert_eq!(crate::naming::parent_piece(&captured), Some(stem.as_str()), "actual strip capture records a piece name");
// What the builds before format 14 saved for that piece: its number by centroid order.
let former = format!("{stem}#1");
let mut sketch = solvecraft_sketch::Sketch::new();
let mut sketch = solidcraft_sketch::Sketch::new();
let corners = [Vec2::new(0., 0.), Vec2::new(12., 0.), Vec2::new(12., 2.), Vec2::new(0., 2.)];
let geometry: Vec<_> = (0..4).map(|i| solvecraft_sketch::LinkGeom::Line(corners[i], corners[(i + 1) % 4])).collect();
let geometry: Vec<_> = (0..4).map(|i| solidcraft_sketch::LinkGeom::Line(corners[i], corners[(i + 1) % 4])).collect();
sketch
.add_link(
solvecraft_sketch::LinkKind::Project,
solvecraft_sketch::LinkSource::Face { body: "Case".into(), at: Vec3::new(20., 6., 1.) },
solidcraft_sketch::LinkKind::Project,
solidcraft_sketch::LinkSource::Face { body: "Case".into(), at: Vec3::new(20., 6., 1.) },
&geometry,
)
.unwrap();
@@ -289,7 +289,7 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
)
.unwrap();
let mut legacy: serde_json::Value = serde_json::from_str(&doc.to_json()).unwrap();
legacy["format"] = json!("solvecraft/12");
legacy["format"] = json!("solidcraft/12");
let mut doc = crate::format::read(&legacy.to_string()).unwrap();
for width in [20., 30.] {
doc.set_param("width", &width.to_string(), Some("mm"), None).unwrap();
@@ -308,7 +308,7 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
assert_eq!(solved.sketch.points, original.points);
let material = state.bodies.iter().find(|body| body.feature == boss).unwrap();
assert!(
(solvecraft_kernel::measure(&material.body).unwrap().volume - 24.).abs() < 1e-8,
(solidcraft_kernel::measure(&material.body).unwrap().volume - 24.).abs() < 1e-8,
"old24mm3 profile must not silently become120mm3 wall profile"
);
let bounds = material.mesh().bounds();
@@ -320,8 +320,8 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
#[test]
fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
use solvecraft_kernel::{FacePaint, Paint};
use solvecraft_sketch::{LinkKind, LinkSource, Sketch};
use solidcraft_kernel::{FacePaint, Paint};
use solidcraft_sketch::{LinkKind, LinkSource, Sketch};
let mut doc = Document::new("saved curved strip");
doc.set_param("width", "80", Some("mm"), None).unwrap();
doc.set_param("wall", "2", Some("mm"), None).unwrap();
@@ -362,14 +362,14 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
.body
.clone()
.with_paint(Some(Paint { opacity: 1., faces: vec![FacePaint { face: 0, color: [1., 0., 0.], opacity: 1. }] }));
let cavity_box = solvecraft_kernel::box_solid(Vec3::new(-38., -28., 2.), Vec3::new(38., 28., 35.)).unwrap();
let cavity = solvecraft_kernel::fillet(
let cavity_box = solidcraft_kernel::box_solid(Vec3::new(-38., -28., 2.), Vec3::new(38., 28., 35.)).unwrap();
let cavity = solidcraft_kernel::fillet(
&cavity_box,
&[Vec3::new(38., 28., 15.), Vec3::new(-38., 28., 15.), Vec3::new(-38., -28., 15.), Vec3::new(38., -28., 15.)],
4.,
)
.unwrap();
let raw = solvecraft_kernel::boolean(&old_source, &cavity, solvecraft_kernel::BoolOp::Cut).unwrap().unwrap();
let raw = solidcraft_kernel::boolean(&old_source, &cavity, solidcraft_kernel::BoolOp::Cut).unwrap().unwrap();
let mut old_body = ModelBody::new("Case".into(), raw, shell);
old_body.names = crate::naming::NameCell::new(Some(std::sync::Arc::new(crate::naming::NamingOrigin {
feature: doc.feature(shell).unwrap().clone(),
@@ -403,18 +403,18 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
continue;
}
let points = &mesh.edges[index];
let a = solvecraft_geom::Plane::YZ.to_local(*points.first().unwrap());
let b = solvecraft_geom::Plane::YZ.to_local(*points.last().unwrap());
let a = solidcraft_geom::Plane::YZ.to_local(*points.first().unwrap());
let b = solidcraft_geom::Plane::YZ.to_local(*points.last().unwrap());
let (a, b) = (Vec2::new(a.x, a.y), Vec2::new(b.x, b.y));
assert!(a.dist(b) > 1e-6);
for point in points {
let q = solvecraft_geom::Plane::YZ.to_local(*point);
let q = solidcraft_geom::Plane::YZ.to_local(*point);
let q = Vec2::new(q.x, q.y);
let direction = b - a;
let t = ((q - a).dot(direction) / direction.dot(direction)).clamp(0., 1.);
assert!(q.dist(a + direction * t) < 1e-7, "exact cylindrical quadrant projects to a straight support");
}
geometry.push(solvecraft_sketch::LinkGeom::Line(a, b));
geometry.push(solidcraft_sketch::LinkGeom::Line(a, b));
}
assert_eq!(geometry.len(), 4, "historical curved strip has a complete four-edge rectangular projection");
let mut sketch = Sketch::new();
@@ -422,7 +422,7 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
sketch.links[0].face_name = Some(former);
let original = sketch.clone();
let sk = doc
.add_feature(FeatureKind::Sketch { plane: PlaneRef::Custom { plane: solvecraft_geom::Plane::YZ }, sketch }, Some("Saved corner strip"))
.add_feature(FeatureKind::Sketch { plane: PlaneRef::Custom { plane: solidcraft_geom::Plane::YZ }, sketch }, Some("Saved corner strip"))
.unwrap();
let boss = doc
.add_feature(
@@ -454,7 +454,7 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
assert_eq!(solved.sketch.curves, original.curves);
let material = state.bodies.iter().find(|body| body.feature == boss).unwrap();
assert!(
(solvecraft_kernel::measure(&material.body).unwrap().volume - 12.).abs() < 1e-7,
(solidcraft_kernel::measure(&material.body).unwrap().volume - 12.).abs() < 1e-7,
"two-mm strip cannot expand to a full thirty-mm wall"
);
}
@@ -463,8 +463,8 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
#[test]
fn saved_stepped_tab_cut_piece_projection_is_lost_without_expanding_material() {
use solvecraft_kernel::{BoolOp, FacePaint, Paint};
use solvecraft_sketch::{LinkKind, LinkSource, Sketch};
use solidcraft_kernel::{BoolOp, FacePaint, Paint};
use solidcraft_sketch::{LinkKind, LinkSource, Sketch};
let mut doc = Document::new("public stepped tab case");
doc.set_param("length", "40", Some("mm"), None).unwrap();
doc.set_param("wall", "2", Some("mm"), None).unwrap();
@@ -508,7 +508,7 @@ fn saved_stepped_tab_cut_piece_projection_is_lost_without_expanding_material() {
.body
.clone()
.with_paint(Some(Paint { opacity: 1., faces: vec![FacePaint { face: 0, color: [1., 0., 0.], opacity: 1. }] }));
let old_cut = solvecraft_kernel::boolean(&old_stock, &before.body("Pocket tool").unwrap().body, BoolOp::Cut).unwrap().unwrap();
let old_cut = solidcraft_kernel::boolean(&old_stock, &before.body("Pocket tool").unwrap().body, BoolOp::Cut).unwrap().unwrap();
let mut old_body = ModelBody::new("Case".into(), old_cut, cut);
old_body.names = crate::naming::NameCell::new(Some(std::sync::Arc::new(crate::naming::NamingOrigin {
feature: doc.feature(cut).unwrap().clone(),
@@ -522,7 +522,7 @@ fn saved_stepped_tab_cut_piece_projection_is_lost_without_expanding_material() {
let index = crate::naming::face_on_body(&old_body, &former, at).unwrap().0;
let mut old_state = crate::ModelState::default();
old_state.bodies.push(old_body);
let plane = solvecraft_geom::Plane::YZ;
let plane = solidcraft_geom::Plane::YZ;
let resolved = crate::project::resolve_face_indices(&old_state, &plane, LinkKind::Project, "Case", &[index]).unwrap();
let current_state = model.state();
let current = current_state.body("Case").unwrap();
@@ -569,15 +569,15 @@ fn saved_stepped_tab_cut_piece_projection_is_lost_without_expanding_material() {
assert_eq!(saved.sketch.curves, original.curves);
let stock = state.bodies.iter().find(|b| b.feature == material).unwrap();
assert!(
(solvecraft_kernel::measure(&stock.body).unwrap().volume - 200.).abs() < 1e-7,
(solidcraft_kernel::measure(&stock.body).unwrap().volume - 200.).abs() < 1e-7,
"saved10x20 patch must not become30x20 wall; actualvolume={}, bounds={:?}, originalpoints={:?}",
solvecraft_kernel::measure(&stock.body).unwrap().volume,
solidcraft_kernel::measure(&stock.body).unwrap().volume,
stock.mesh().bounds(),
original.points
);
let case = state.body("Case").unwrap();
let expected = length * 30. * 20. + 8. * 10. * 8. - (length - 2. * wall) * (30. - 2. * wall) * (20. - wall);
assert!((solvecraft_kernel::measure(&case.body).unwrap().volume - expected).abs() < 1e-6);
assert!((solidcraft_kernel::measure(&case.body).unwrap().volume - expected).abs() < 1e-6);
}
}
}
+9 -9
View File
@@ -3,9 +3,9 @@ use super::Column;
use crate::{DocError, Document, FeatureKind, ModelState, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
pub(crate) fn shell_plane_family(planes: impl IntoIterator<Item = Option<solvecraft_geom::Plane>>) -> bool {
pub(crate) fn shell_plane_family(planes: impl IntoIterator<Item = Option<solidcraft_geom::Plane>>) -> bool {
let mut planes = planes.into_iter();
let Some(Some(first)) = planes.next() else { return false };
first.origin.is_finite() && first.x.is_finite() && first.y.is_finite() && planes.all(|plane| plane == Some(first))
@@ -112,7 +112,7 @@ impl Document {
(FaceCertificate::ExtrudeCap { sketch, .. }, FeatureKind::Extrude { sketch: current, extent, operation: crate::Operation::NewBody, .. }) => sketch == current && extent.taper.is_none(),
(FaceCertificate::ExtrudeSide { sketch, curve }, FeatureKind::Extrude { sketch: current, extent, operation: crate::Operation::NewBody, .. }) => {
sketch == current && extent.taper.is_none()
&& self.feature(*sketch).is_some_and(|feature| matches!(&feature.kind, FeatureKind::Sketch { sketch, .. } if sketch.curves.iter().any(|candidate| candidate.id == *curve && !candidate.construction && matches!(candidate.kind, solvecraft_sketch::CurveKind::Line { .. } | solvecraft_sketch::CurveKind::Circle { .. } | solvecraft_sketch::CurveKind::Arc { .. }))))
&& self.feature(*sketch).is_some_and(|feature| matches!(&feature.kind, FeatureKind::Sketch { sketch, .. } if sketch.curves.iter().any(|candidate| candidate.id == *curve && !candidate.construction && matches!(candidate.kind, solidcraft_sketch::CurveKind::Line { .. } | solidcraft_sketch::CurveKind::Circle { .. } | solidcraft_sketch::CurveKind::Arc { .. }))))
}
(FaceCertificate::Imported { .. }, FeatureKind::Import { .. }) => true,
_ => false,
@@ -138,7 +138,7 @@ impl Document {
}
(super::FaceCertificate::ExtrudeSide { sketch, curve }, FeatureKind::Extrude { sketch: producer_sketch, extent, operation: crate::Operation::NewBody, .. }) => {
sketch == producer_sketch && extent.taper.is_none() && role == format!("side:{curve}")
&& self.feature(*sketch).is_some_and(|feature| matches!(&feature.kind, FeatureKind::Sketch { sketch, .. } if sketch.curves.iter().any(|candidate| candidate.id == *curve && !candidate.construction && matches!(candidate.kind, solvecraft_sketch::CurveKind::Line { .. } | solvecraft_sketch::CurveKind::Circle { .. } | solvecraft_sketch::CurveKind::Arc { .. }))))
&& self.feature(*sketch).is_some_and(|feature| matches!(&feature.kind, FeatureKind::Sketch { sketch, .. } if sketch.curves.iter().any(|candidate| candidate.id == *curve && !candidate.construction && matches!(candidate.kind, solidcraft_sketch::CurveKind::Line { .. } | solidcraft_sketch::CurveKind::Circle { .. } | solidcraft_sketch::CurveKind::Arc { .. }))))
}
(super::FaceCertificate::Imported { .. }, FeatureKind::Import { .. }) => role.strip_prefix("face:").is_some_and(|index| index.parse::<usize>().is_ok()),
(super::FaceCertificate::ShellPlanar { source_producer, source, inner }, FeatureKind::Shell { .. }) => {
@@ -183,7 +183,7 @@ impl Document {
}
if let Some(super::FaceCertificate::ShellPlanar { source_producer, source, inner }) = &target.certificate {
let indices: Vec<_> = body.body.face_sources().into_iter().flatten().enumerate().filter_map(|(index, current)| {
matches!(current, Some(solvecraft_kernel::FaceSource::ShellPlanar { producer: Some(producer), source: current_source, inner: current_inner, .. }) if *producer == target.origin_feature && inner == current_inner && super::flat_source_matches(source, current_source, *source_producer)).then_some(index)
matches!(current, Some(solidcraft_kernel::FaceSource::ShellPlanar { producer: Some(producer), source: current_source, inner: current_inner, .. }) if *producer == target.origin_feature && inner == current_inner && super::flat_source_matches(source, current_source, *source_producer)).then_some(index)
}).collect();
// A shell can split one original support into several pieces. All pieces
// must retain the same canonical frame, not merely lie on a common plane.
@@ -203,11 +203,11 @@ impl Document {
let matched = match (&target.certificate, source) {
(
Some(super::FaceCertificate::Box { axis, high }),
Some(solvecraft_kernel::FaceSource::Box { producer: Some(producer), axis: current_axis, high: current_high, .. }),
Some(solidcraft_kernel::FaceSource::Box { producer: Some(producer), axis: current_axis, high: current_high, .. }),
) => *producer == target.origin_feature && axis == current_axis && high == current_high,
(
Some(super::FaceCertificate::ExtrudeCap { sketch, high, outer, holes }),
Some(solvecraft_kernel::FaceSource::CertifiedExtrudeCap {
Some(solidcraft_kernel::FaceSource::CertifiedExtrudeCap {
producer,
sketch: current_sketch,
high: current_high,
@@ -234,7 +234,7 @@ impl Document {
}
if let Some(super::FaceCertificate::ExtrudeSide { sketch, curve }) = &target.certificate {
let indices: Vec<_> = body.body.face_sources().into_iter().flatten().enumerate().filter_map(|(index, source)| {
matches!(source, Some(solvecraft_kernel::FaceSource::CertifiedExtrudeSide { producer, sketch: current_sketch, curve: current_curve, .. }) if *producer == target.origin_feature && current_sketch == sketch && current_curve == curve).then_some(index)
matches!(source, Some(solidcraft_kernel::FaceSource::CertifiedExtrudeSide { producer, sketch: current_sketch, curve: current_curve, .. }) if *producer == target.origin_feature && current_sketch == sketch && current_curve == curve).then_some(index)
}).collect();
if indices.is_empty() {
return Err(DocError::Unknown("constructed side certificate no longer exists".into()));
@@ -243,7 +243,7 @@ impl Document {
}
if let Some(super::FaceCertificate::Imported { payload_sha256, item, face }) = &target.certificate {
let indices: Vec<_> = body.body.face_sources().into_iter().flatten().enumerate().filter_map(|(index, source)| {
matches!(source, Some(solvecraft_kernel::FaceSource::Imported { item: current_item, face: current_face, payload_sha256: Some(digest), producer: Some(producer) }) if current_item == item && current_face == face && digest == payload_sha256 && *producer == target.origin_feature).then_some(index)
matches!(source, Some(solidcraft_kernel::FaceSource::Imported { item: current_item, face: current_face, payload_sha256: Some(digest), producer: Some(producer) }) if current_item == item && current_face == face && digest == payload_sha256 && *producer == target.origin_feature).then_some(index)
}).collect();
if indices.is_empty() {
return Err(DocError::Unknown("immutable import face certificate no longer exists".into()));
+2 -2
View File
@@ -8,8 +8,8 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use solvecraft_geom::{Plane, Vec2, Vec3};
use solvecraft_kernel::{self as kernel, FaceSurface};
use solidcraft_geom::{Plane, Vec2, Vec3};
use solidcraft_kernel::{self as kernel, FaceSurface};
use crate::eval::{ModelBody, ModelState};
use crate::expr::{Kind, Value};
+11 -11
View File
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use solvecraft_geom::{Plane, Vec2, Vec3};
use solvecraft_sketch::Sketch;
use solidcraft_geom::{Plane, Vec2, Vec3};
use solidcraft_sketch::Sketch;
use crate::construct::{AxisDef, GeoRef, PointDef};
use crate::expr::{self, Kind};
@@ -884,7 +884,7 @@ pub enum FeatureKind {
step: String,
/// The file's product/assembly tree, kept for components.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
components: Vec<solvecraft_kernel::StepNode>,
components: Vec<solidcraft_kernel::StepNode>,
},
/// Rebuild an upstream constant-section mesh with analytic planar/cylindrical faces.
MeshConvert {
@@ -1697,7 +1697,7 @@ pub struct Document {
pub struct NamedView {
#[serde(default)]
pub name: String,
pub target: solvecraft_geom::Vec3,
pub target: solidcraft_geom::Vec3,
pub yaw: f64,
pub pitch: f64,
pub distance: f64,
@@ -1774,13 +1774,13 @@ impl Document {
let mut st = state.clone();
for b in &mut st.bodies {
let c = self.body_color(b);
let faces: Vec<solvecraft_kernel::FacePaint> = self
let faces: Vec<solidcraft_kernel::FacePaint> = self
.face_colors(b)
.into_iter()
.map(|(face, l)| solvecraft_kernel::FacePaint { face, color: l.rgb(), opacity: l.opacity as f32 })
.map(|(face, l)| solidcraft_kernel::FacePaint { face, color: l.rgb(), opacity: l.opacity as f32 })
.collect();
let opacity = self.model_body_opacity(b) as f32;
let paint = solvecraft_kernel::Paint { opacity, faces };
let paint = solidcraft_kernel::Paint { opacity, faces };
if c != b.body.color() || b.body.paint() != Some(&paint) {
b.body = b.body.clone().with_color(c).with_paint(Some(paint));
}
@@ -2214,14 +2214,14 @@ impl Document {
&self,
vals: &std::collections::BTreeMap<String, expr::Value>,
sk: &mut Sketch,
stored: &solvecraft_sketch::DrivingValues,
) -> Result<solvecraft_sketch::SolveReport> {
stored: &solidcraft_sketch::DrivingValues,
) -> Result<solidcraft_sketch::SolveReport> {
self.apply_dimension_values(vals, sk)?;
let mut report = solvecraft_sketch::solve_after_value_edit(sk, stored);
let mut report = solidcraft_sketch::solve_after_value_edit(sk, stored);
if !sk.offsets.is_empty() || !sk.freeform_offsets.is_empty() {
// Source dimensions may have moved curves during the solve.
sk.refresh_offsets()?;
report = if report.ok() { solvecraft_sketch::solve(sk) } else { solvecraft_sketch::solve_after_value_edit(sk, stored) };
report = if report.ok() { solidcraft_sketch::solve(sk) } else { solidcraft_sketch::solve_after_value_edit(sk, stored) };
}
Ok(report)
}
+38 -38
View File
@@ -5,9 +5,9 @@ use std::hash::{Hash, Hasher};
use std::sync::{Arc, OnceLock};
use serde::Serialize;
use solvecraft_geom::{Mesh, Plane, Region2, Vec2, Vec3};
use solvecraft_kernel::{self as kernel, Body, BoolOp};
use solvecraft_sketch::{CurveKind, Profile, SolveReport, find_profiles};
use solidcraft_geom::{Mesh, Plane, Region2, Vec2, Vec3};
use solidcraft_kernel::{self as kernel, Body, BoolOp};
use solidcraft_sketch::{CurveKind, Profile, SolveReport, find_profiles};
use crate::expr::{self, Kind, Value};
use crate::lineage::Traced;
@@ -79,7 +79,7 @@ pub struct SolvedSketch {
pub feature: u64,
pub name: String,
pub plane: Plane,
pub sketch: solvecraft_sketch::Sketch,
pub sketch: solidcraft_sketch::Sketch,
pub report: SolveReport,
pub profiles: Vec<Profile>,
}
@@ -217,7 +217,7 @@ fn thread_on(st: &ModelState, feature: u64, p: Vec3, designation: Option<&str>,
/// profile, 60° flanks): cut from the shaft (external) or the hole wall (internal), running from
/// `start` to `end` along the axis. `radius` is the face's own radius; `open` says which ends
/// of the threaded length are free ends of the face.
fn thread_groove(t: &ThreadInfo, radius: f64, open: (bool, bool)) -> Result<solvecraft_geom::Mesh> {
fn thread_groove(t: &ThreadInfo, radius: f64, open: (bool, bool)) -> Result<solidcraft_geom::Mesh> {
let (p, d) = (t.pitch, t.major_diameter);
let h = p * 3f64.sqrt() / 2.0;
let slope = (std::f64::consts::PI / 6.0).tan();
@@ -538,7 +538,7 @@ const QUANTUM: f64 = 1e-6;
/// Put what a solve writes into a sketch on the [`QUANTUM`] grid: positions, radii and the
/// measured values of driven dimensions.
fn quantize(sk: &mut solvecraft_sketch::Sketch) {
fn quantize(sk: &mut solidcraft_sketch::Sketch) {
let q = |v: f64| (v / QUANTUM).round() * QUANTUM;
for p in &mut sk.points {
p.pos = Vec2::new(q(p.pos.x), q(p.pos.y));
@@ -572,7 +572,7 @@ fn feature_json(f: &Feature) -> String {
/// constraints' values. Nothing else: what evaluation derives for its own copy (an offset's
/// upgraded topology, refreshed links) stays out of the stored sketch. A `solved` that does not
/// match `stored` entity for entity is not adopted.
fn adopt_solution(stored: &mut solvecraft_sketch::Sketch, solved: &solvecraft_sketch::Sketch) {
fn adopt_solution(stored: &mut solidcraft_sketch::Sketch, solved: &solidcraft_sketch::Sketch) {
if stored.points.len() != solved.points.len()
|| stored.curves.len() != solved.curves.len()
|| stored.constraints.len() != solved.constraints.len()
@@ -771,7 +771,7 @@ impl Model {
/// recomputed; the next [`Model::evaluate`] recomputes from the sketch on. Returns `false`,
/// changing nothing, when the sketch has no evaluated result or a feature before it is
/// already stale (only a full evaluation brings that up to date).
pub fn show_live_sketch(&mut self, feature: u64, sketch: solvecraft_sketch::Sketch, report: SolveReport) -> bool {
pub fn show_live_sketch(&mut self, feature: u64, sketch: solidcraft_sketch::Sketch, report: SolveReport) -> bool {
let Some(i) = self.results.iter().position(|r| r.id == feature) else { return false };
if self.results.iter().take(i).any(|r| r.stale) {
return false;
@@ -1042,7 +1042,7 @@ pub fn select_profiles(ss: &SolvedSketch, sel: &ProfileSel) -> Result<Vec<Region
// "All" means what was drawn: projected reference geometry (a face's auto-projected
// edges) only counts when the drawn curves alone make no profile.
ProfileSel::All => {
let drawn = solvecraft_sketch::find_drawn_profiles(&ss.sketch);
let drawn = solidcraft_sketch::find_drawn_profiles(&ss.sketch);
if drawn.is_empty() { ps.iter().map(|p| p.region.clone()).collect() } else { drawn.into_iter().map(|p| p.region).collect() }
}
ProfileSel::Indices { indices } => {
@@ -1066,7 +1066,7 @@ pub fn select_profiles(ss: &SolvedSketch, sel: &ProfileSel) -> Result<Vec<Region
let mut v = Vec::new();
for p in points {
// Innermost profile containing the point.
let found = solvecraft_sketch::profile_at_point(ps, *p).and_then(|i| ps.get(i));
let found = solidcraft_sketch::profile_at_point(ps, *p).and_then(|i| ps.get(i));
v.push(found.ok_or_else(|| DocError::Unknown(format!("profile at {:?} in {}", [p.x, p.y], ss.name)))?.region.clone());
}
v
@@ -1079,12 +1079,12 @@ pub fn select_profiles(ss: &SolvedSketch, sel: &ProfileSel) -> Result<Vec<Region
}
/// The error for a profile picked by curves that no longer names exactly one profile.
pub fn curve_pick_error(e: solvecraft_sketch::CurvePick, ids: &[String], sketch: &str) -> DocError {
pub fn curve_pick_error(e: solidcraft_sketch::CurvePick, ids: &[String], sketch: &str) -> DocError {
match e {
solvecraft_sketch::CurvePick::Missing => DocError::Invalid(format!(
solidcraft_sketch::CurvePick::Missing => DocError::Invalid(format!(
"no profile of {sketch} is bounded by exactly the curves {ids:?} (a curve was deleted, or another curve now cuts the region)"
)),
solvecraft_sketch::CurvePick::Ambiguous(n) => DocError::Invalid(format!(
solidcraft_sketch::CurvePick::Ambiguous(n) => DocError::Invalid(format!(
"{n} profiles of {sketch} are bounded by exactly the curves {ids:?}; add a curve that tells them apart, or pick by index or point"
)),
}
@@ -1389,7 +1389,7 @@ fn grow_profile_for_coplanar(st: &ModelState, r: &Region2, op: Operation, target
let delta = (size * 0.005).max(1e-3);
let lp = r.outer.ccw();
let lines: Option<Vec<(Vec2, Vec2)>> =
lp.segs.iter().map(|s| if let solvecraft_geom::Seg2::Line { a, b } = *s { Some((a, b)) } else { None }).collect();
lp.segs.iter().map(|s| if let solidcraft_geom::Seg2::Line { a, b } = *s { Some((a, b)) } else { None }).collect();
let Some(lines) = lines else { return r.clone() };
let inside_any = |p: Vec3| meshes.iter().any(|m| m.contains(p));
let mut grow = vec![0.0; lines.len()];
@@ -1436,7 +1436,7 @@ fn grow_profile_for_coplanar(st: &ModelState, r: &Region2, op: Operation, target
let t = (p1 - p0).cross(d1) / den;
pts.push(p0 + d0 * t);
}
Region2 { outer: solvecraft_geom::Loop2::polygon(&pts), holes: r.holes.clone() }
Region2 { outer: solidcraft_geom::Loop2::polygon(&pts), holes: r.holes.clone() }
}
/// Booleans struggle with coincident faces (a cut starting on the face it cuts, a boss
@@ -1771,7 +1771,7 @@ fn hole_tools(
let seam = std::cell::Cell::new(0.0);
let disc = |rad: f64| {
seam.set(seam.get() + 0.613);
Region2 { outer: solvecraft_geom::Loop2::circle_from(Vec2::ZERO, rad, seam.get()), holes: vec![] }
Region2 { outer: solidcraft_geom::Loop2::circle_from(Vec2::ZERO, rad, seam.get()), holes: vec![] }
};
let cyl = |rad: f64, lo: f64, hi: f64| -> Result<Body> {
kernel::extrude(&plane, &[disc(rad)], lo, hi)?.pop().ok_or_else(|| DocError::Invalid("hole tool".into()))
@@ -1794,7 +1794,7 @@ fn hole_tools(
let side = up.any_perp();
let rp = Plane::new(*position, side, up).ok_or_else(|| DocError::Invalid("hole axis".into()))?;
let pts = [Vec2::new(0.0, top), Vec2::new(r, top), Vec2::new(r, bottom), Vec2::new(0.0, bottom - tip)];
let region = Region2 { outer: solvecraft_geom::Loop2::polygon(&pts), holes: vec![] };
let region = Region2 { outer: solidcraft_geom::Loop2::polygon(&pts), holes: vec![] };
tools.extend(kernel::revolve(&rp, &[region], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU)?);
} else {
tools.push(cyl(r, bottom, top)?);
@@ -1987,7 +1987,7 @@ fn feature_tools_with_constructor(
match &f.kind {
FeatureKind::Extrude { sketch, profiles, extent, operation, targets } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?.clone();
let regions = solvecraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
// From / To Object: where the face or point lies along the normal, measured over the
// middle of the profiles.
let n = ss.plane.normal();
@@ -2130,7 +2130,7 @@ fn feature_tools_with_constructor(
}
FeatureKind::Revolve { sketch, profiles, axis, angle, angle2, symmetric, convention, to, .. } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?.clone();
let regions = solvecraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let (o, d) = revolve_axis(&ss, axis)?;
let angles = if to.is_some() {
(0.0, None)
@@ -2257,7 +2257,7 @@ fn feature_tools_with_constructor(
let [a, b] = &sections[..] else { return Err(DocError::Invalid("not supported yet: a point section in a loft of more than two".into())) };
let (prof, pt) = if a.point.is_some() { (b, a) } else { (a, b) };
let ss = st.sketch(prof.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", prof.sketch)))?;
let regions = solvecraft_sketch::merge_regions(&select_profiles(ss, &prof.profiles)?);
let regions = solidcraft_sketch::merge_regions(&select_profiles(ss, &prof.profiles)?);
let [r] = &regions[..] else { return Err(DocError::Invalid("each loft section must be one profile".into())) };
let ps = st.sketch(pt.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", pt.sketch)))?;
let id = pt.point.as_deref().unwrap_or_default();
@@ -2292,10 +2292,10 @@ fn feature_tools_with_constructor(
let plane = Plane { origin: *base, x, y: k };
let c = Vec2::new(r, 0.0);
let outer = match section {
crate::CoilSection::Circular => solvecraft_geom::Loop2::circle(c, s / 2.0),
crate::CoilSection::Circular => solidcraft_geom::Loop2::circle(c, s / 2.0),
crate::CoilSection::Square => {
let h = s / 2.0;
solvecraft_geom::Loop2::polygon(&[c + Vec2::new(-h, -h), c + Vec2::new(h, -h), c + Vec2::new(h, h), c + Vec2::new(-h, h)])
solidcraft_geom::Loop2::polygon(&[c + Vec2::new(-h, -h), c + Vec2::new(h, -h), c + Vec2::new(h, h), c + Vec2::new(-h, h)])
}
};
let region = Region2 { outer, holes: Vec::new() };
@@ -2310,7 +2310,7 @@ fn feature_tools_with_constructor(
let ss = st
.sketch(sec.sketch)
.ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sec.sketch)))?;
let regions = solvecraft_sketch::merge_regions(&select_profiles(ss, &sec.profiles)?);
let regions = solidcraft_sketch::merge_regions(&select_profiles(ss, &sec.profiles)?);
let [r] = &regions[..] else { return Err(DocError::Invalid("each loft section must be one profile".into())) };
secs.push((ss.plane, r.outer.clone()));
}
@@ -2332,7 +2332,7 @@ fn feature_tools_with_constructor(
};
let mapped = path_in_frame(doc, ps, f.component)?;
let ps = mapped.as_ref();
let regions = solvecraft_sketch::merge_regions(&select_profiles(ss, profiles)?);
let regions = solidcraft_sketch::merge_regions(&select_profiles(ss, profiles)?);
let start = regions.first().map(|r| ss.plane.to_world(r.centroid())).unwrap_or_default();
let tool = |r: &Region2, body: Body| crate::lineage::ProfileRoles::swept(Some(ss), Some(r)).tool(body, None);
// A path through 3D sketch curves follows them as points.
@@ -2407,14 +2407,14 @@ fn pipe_region(vals: &BTreeMap<String, Value>, r: f64, wall: &Option<String>) ->
if !(wt > 0.0 && wt < r) {
return Err(DocError::Invalid("the wall must be thinner than the radius".into()));
}
vec![solvecraft_geom::Loop2::circle(Vec2::ZERO, r - wt).reversed()]
vec![solidcraft_geom::Loop2::circle(Vec2::ZERO, r - wt).reversed()]
}
None => Vec::new(),
};
Ok(Region2 { outer: solvecraft_geom::Loop2::circle(Vec2::ZERO, r), holes })
Ok(Region2 { outer: solidcraft_geom::Loop2::circle(Vec2::ZERO, r), holes })
}
fn map_wire(wire: &mut solvecraft_sketch::Wire, matrix: &Mat) {
fn map_wire(wire: &mut solidcraft_sketch::Wire, matrix: &Mat) {
for point in wire.pts.iter_mut().chain(&mut wire.fit) {
*point = crate::apply_point(matrix, *point);
}
@@ -2453,7 +2453,7 @@ fn path_in_frame<'a>(doc: &Document, sketch: &'a SolvedSketch, consumer: u64) ->
)
.ok_or_else(|| DocError::Invalid("path sketch frame is invalid".into()))?;
for wire in &mut mapped.sketch.wires {
let matrix = if wire.space == solvecraft_sketch::WireSpace::Component { &relative } else { &inverse };
let matrix = if wire.space == solidcraft_sketch::WireSpace::Component { &relative } else { &inverse };
map_wire(wire, matrix);
}
Ok(Cow::Owned(mapped))
@@ -2530,7 +2530,7 @@ fn path_segments(ps: &SolvedSketch, ids: &[String], start: Vec3) -> Result<Vec<k
if let Some(pts) = path_points(ps, ids, start)? {
return Ok(pts.windows(2).map(|w| kernel::PathSeg::Line { a: w[0], b: w[1] }).collect());
}
let mut segs: Vec<solvecraft_geom::Seg2> = Vec::new();
let mut segs: Vec<solidcraft_geom::Seg2> = Vec::new();
for id in ids {
let ci = ps.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("path curve `{id}`")))?;
segs.extend(ps.sketch.segs(ci));
@@ -2564,12 +2564,12 @@ fn path_segments(ps: &SolvedSketch, ids: &[String], start: Vec3) -> Result<Vec<k
Ok(segs
.iter()
.flat_map(|s| match *s {
solvecraft_geom::Seg2::Line { a, b } => vec![kernel::PathSeg::Line { a: w(a), b: w(b) }],
solvecraft_geom::Seg2::Arc { center, sweep, .. } => {
solidcraft_geom::Seg2::Line { a, b } => vec![kernel::PathSeg::Line { a: w(a), b: w(b) }],
solidcraft_geom::Seg2::Arc { center, sweep, .. } => {
vec![kernel::PathSeg::Arc { a: w(s.start()), center: w(center), axis: n * sweep.signum(), angle: sweep.abs() }]
}
// Free-form path curves are followed as polylines.
solvecraft_geom::Seg2::Cubic { .. } | solvecraft_geom::Seg2::Conic { .. } | solvecraft_geom::Seg2::Bezier { .. } => {
solidcraft_geom::Seg2::Cubic { .. } | solidcraft_geom::Seg2::Conic { .. } | solidcraft_geom::Seg2::Bezier { .. } => {
s.polyline(1e-2).windows(2).map(|q| kernel::PathSeg::Line { a: w(q[0]), b: w(q[1]) }).collect()
}
})
@@ -3388,7 +3388,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
(None, Some(sid)) => {
let ss = st.sketch(*sid).ok_or_else(|| DocError::Unknown(format!("sketch {sid}")))?;
// Each curve's exact segments (lines and arcs split by their own plane and cylinder).
let curves: Vec<Vec<solvecraft_geom::Seg2>> = (0..ss.sketch.curves.len())
let curves: Vec<Vec<solidcraft_geom::Seg2>> = (0..ss.sketch.curves.len())
.filter(|ci| ss.sketch.curves.get(*ci).is_some_and(|c| !c.construction))
.map(|ci| ss.sketch.segs(ci))
.filter(|segs| !segs.is_empty())
@@ -3681,7 +3681,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::BoundingSolid { bodies, margin } => {
let m = val(vals, margin, Kind::Length)?;
let mut bb = solvecraft_geom::Aabb3::EMPTY;
let mut bb = solidcraft_geom::Aabb3::EMPTY;
for b in st.bodies.iter().filter(|b| bodies.is_empty() || bodies.contains(&b.name)) {
bb = bb.union(&b.mesh().bounds());
}
@@ -3812,7 +3812,7 @@ pub fn state_in_frame_at<'a>(doc: &Document, st: &'a ModelState, comp: u64, fram
.collect();
let sketch_m: Vec<Mat> = st.sketches.iter().map(|s| rel(comp_of_feature(s.feature), None)).collect();
let world_wires_need_mapping = !crate::is_identity(&inv)
&& st.sketches.iter().any(|sketch| sketch.sketch.wires.iter().any(|wire| wire.space == solvecraft_sketch::WireSpace::World));
&& st.sketches.iter().any(|sketch| sketch.sketch.wires.iter().any(|wire| wire.space == solidcraft_sketch::WireSpace::World));
if body_m.iter().chain(&sketch_m).all(crate::is_identity) && !world_wires_need_mapping {
return Cow::Borrowed(st);
}
@@ -3835,7 +3835,7 @@ pub fn state_in_frame_at<'a>(doc: &Document, st: &'a ModelState, comp: u64, fram
}
for (s, m) in out.sketches.iter_mut().zip(&sketch_m) {
// Only a sketch the frame moves is copied.
let maps_wire = |w: &solvecraft_sketch::Wire| !crate::is_identity(if w.space == solvecraft_sketch::WireSpace::Component { m } else { &inv });
let maps_wire = |w: &solidcraft_sketch::Wire| !crate::is_identity(if w.space == solidcraft_sketch::WireSpace::Component { m } else { &inv });
if crate::is_identity(m) && !s.sketch.wires.iter().any(maps_wire) {
continue;
}
@@ -3846,7 +3846,7 @@ pub fn state_in_frame_at<'a>(doc: &Document, st: &'a ModelState, comp: u64, fram
s.plane = np;
}
for wire in &mut s.sketch.wires {
let matrix = if wire.space == solvecraft_sketch::WireSpace::Component { m } else { &inv };
let matrix = if wire.space == solidcraft_sketch::WireSpace::Component { m } else { &inv };
map_wire(wire, matrix);
}
}
@@ -3919,7 +3919,7 @@ pub fn world_state(doc: &Document, st: &ModelState) -> ModelState {
}
}
for wire in &mut s2.sketch.wires {
if wire.space == solvecraft_sketch::WireSpace::Component {
if wire.space == solidcraft_sketch::WireSpace::Component {
map_wire(wire, &m);
}
}
+4 -4
View File
@@ -5,7 +5,7 @@ use super::*;
use crate::EmbossFace;
use crate::apply_point;
use crate::sheet;
use solvecraft_geom::{Loop2, Seg2};
use solidcraft_geom::{Loop2, Seg2};
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
match &f.kind {
@@ -357,7 +357,7 @@ pub(super) fn sheet_eval(
match &f.kind {
FeatureKind::SheetBase { sketch, profiles, rule, flip } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let regions = solvecraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let r = doc.sheet_rule(rule.as_deref());
let rv = doc.rule_values(vals, &r)?;
for (k, region) in regions.into_iter().enumerate() {
@@ -631,7 +631,7 @@ pub(super) fn sheet_cut(
return Ok(Vec::new());
}
let Some(ss) = st.sketch(*sketch).cloned() else { return Ok(Vec::new()) };
let regions = solvecraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let n = ss.plane.normal();
let mut work = sheet::FlatWork::new();
let faces = tools.iter().fold(0usize, |count, body| count.saturating_add(body.face_count()));
@@ -836,7 +836,7 @@ pub(super) fn sheet_cut(
/// Outline loops (in `plane` coordinates) of the mesh triangles lying in the plane at distance
/// `level` from `at` along `n`: the edges used by one of them only, chained.
fn face_loops(mesh: &solvecraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, level: f64, tol: f64) -> Vec<Vec<Vec2>> {
fn face_loops(mesh: &solidcraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, level: f64, tol: f64) -> Vec<Vec<Vec2>> {
let on: Vec<[u32; 3]> = mesh
.triangles
.iter()
+1 -1
View File
@@ -8,7 +8,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
let mut made = Vec::new();
if let Some(sk) = sketch {
let ss = st.sketch(*sk).ok_or_else(|| DocError::Unknown(format!("sketch {sk} (it must come earlier in the timeline)")))?;
let regions = solvecraft_sketch::merge_regions(&select_profiles(ss, profiles)?);
let regions = solidcraft_sketch::merge_regions(&select_profiles(ss, profiles)?);
for r in &regions {
made.push(kernel::patch_region(&ss.plane, r)?);
}
+15 -15
View File
@@ -3,7 +3,7 @@ use crate::config::ConfigFaceTarget;
use crate::{DocError, Document, EmbossFace, FeatureKind, Model, ModelBody, ModelState, PlaneRef, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use solvecraft_geom::Plane;
use solidcraft_geom::Plane;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
@@ -311,7 +311,7 @@ pub fn validate_face_anchors_json(value: &Value) -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
fn fixture() -> (Document, ModelBody, usize, Plane) {
let mut doc = Document::default();
let kind: FeatureKind =
@@ -319,7 +319,7 @@ mod tests {
let id = doc.add_feature(kind, Some("Base")).unwrap();
let body = ModelBody::new(
"Base".into(),
solvecraft_kernel::box_solid(Vec3::default(), Vec3::new(10., 10., 10.)).unwrap().with_box_certificate(id).unwrap(),
solidcraft_kernel::box_solid(Vec3::default(), Vec3::new(10., 10., 10.)).unwrap().with_box_certificate(id).unwrap(),
id,
);
let index = body
@@ -327,7 +327,7 @@ mod tests {
.face_sources()
.unwrap()
.iter()
.position(|source| matches!(source, Some(solvecraft_kernel::FaceSource::Box { axis: 2, high: true, .. })))
.position(|source| matches!(source, Some(solidcraft_kernel::FaceSource::Box { axis: 2, high: true, .. })))
.unwrap();
let canonical = body.body.face_plane(index).unwrap();
let picked = Plane::new(canonical.origin + canonical.x * 2. + canonical.y * 3., canonical.y, -canonical.x).unwrap();
@@ -350,7 +350,7 @@ mod tests {
assert!((initial.x - picked.x).len() < 1e-9);
state.bodies[0] = ModelBody::new(
"Base".into(),
solvecraft_kernel::box_solid(Vec3::default(), Vec3::new(10., 10., 20.))
solidcraft_kernel::box_solid(Vec3::default(), Vec3::new(10., 10., 20.))
.unwrap()
.with_box_certificate(anchor.target.origin_feature)
.unwrap(),
@@ -361,7 +361,7 @@ mod tests {
assert!((changed.origin.x - initial.origin.x).abs() < 1e-9);
state.bodies[0] = ModelBody::new(
"Control".into(),
solvecraft_kernel::box_solid(Vec3::default(), Vec3::new(10., 10., 30.))
solidcraft_kernel::box_solid(Vec3::default(), Vec3::new(10., 10., 30.))
.unwrap()
.with_box_certificate(anchor.target.origin_feature)
.unwrap(),
@@ -369,7 +369,7 @@ mod tests {
);
assert!(doc.resolve_face_anchor(&anchor, 0, &state).is_err());
state.bodies[0].name = "Base".into();
state.bodies[0].body = solvecraft_kernel::box_solid(Vec3::default(), Vec3::new(10., 10., 30.))
state.bodies[0].body = solidcraft_kernel::box_solid(Vec3::default(), Vec3::new(10., 10., 30.))
.unwrap()
.with_box_certificate(anchor.target.origin_feature + 1)
.unwrap();
@@ -400,8 +400,8 @@ mod tests {
}
#[test]
fn extrude_cap_profile_reordering_never_retargets_an_ordinal_body() {
use solvecraft_geom::Vec2;
let mut sketch = solvecraft_sketch::Sketch::new();
use solidcraft_geom::Vec2;
let mut sketch = solidcraft_sketch::Sketch::new();
for offset in [0., 30.] {
let points: Vec<_> = [(0., 0.), (10., 0.), (10., 10.), (0., 10.)]
.iter()
@@ -432,7 +432,7 @@ mod tests {
assert_eq!(state.bodies.len(), 2);
let solved = state.sketch(source).unwrap();
let regions = crate::select_profiles(solved, &crate::ProfileSel::All).unwrap();
let raw = solvecraft_kernel::extrude(&solved.plane, &regions, 0., 10.).unwrap();
let raw = solidcraft_kernel::extrude(&solved.plane, &regions, 0., 10.).unwrap();
assert!(
crate::config::extrude_cap_bindings(solved, &raw, 0).is_none(),
"exhausted certification work must leave the complete producer uncertified"
@@ -449,7 +449,7 @@ mod tests {
.face_sources()
.unwrap()
.iter()
.position(|source| matches!(source, Some(solvecraft_kernel::FaceSource::CertifiedExtrudeCap { high: true, .. })))
.position(|source| matches!(source, Some(solidcraft_kernel::FaceSource::CertifiedExtrudeCap { high: true, .. })))
.unwrap();
let picked = body.body.face_plane(index).unwrap();
let anchor = doc.capture_face_anchor(body, index, &picked, &[], 0, &[]).unwrap();
@@ -476,7 +476,7 @@ mod tests {
base: Box::new(PlaneRef::Face { plane: picked, at: picked.origin, name: None, anchor: None }),
distance: "2".into(),
},
sketch: solvecraft_sketch::Sketch::new(),
sketch: solidcraft_sketch::Sketch::new(),
},
None,
)
@@ -492,7 +492,7 @@ mod tests {
}
#[test]
fn warm_root_body_offset_edit_matches_cold_attached_sketch_and_downstream_bounds() {
use solvecraft_geom::Vec2;
use solidcraft_geom::Vec2;
let (mut doc, _, _, _) = fixture();
let mut warm = Model::new();
warm.evaluate(&doc);
@@ -503,12 +503,12 @@ mod tests {
.face_sources()
.unwrap()
.iter()
.position(|source| matches!(source, Some(solvecraft_kernel::FaceSource::Box { axis: 2, high: true, .. })))
.position(|source| matches!(source, Some(solidcraft_kernel::FaceSource::Box { axis: 2, high: true, .. })))
.unwrap();
let picked = body.body.face_plane(index).unwrap();
let anchor = doc.capture_face_anchor(body, index, &picked, &[], 0, &[]).unwrap();
let source_name = body.name.clone();
let mut sketch = solvecraft_sketch::Sketch::new();
let mut sketch = solidcraft_sketch::Sketch::new();
let points: Vec<_> =
[(2., 2.), (4., 2.), (4., 4.), (2., 4.)].iter().map(|(x, y)| sketch.add_point(Vec2::new(*x, *y), None).unwrap()).collect();
for edge in 0..4 {
+53 -46
View File
@@ -1,6 +1,6 @@
//! The design file format version and the migrations that bring older files up to date.
//!
//! A design file starts with `"format": "solvecraft/N"`. A file of an older format is read and
//! A design file starts with `"format": "solidcraft/N"`. A file of an older format is read and
//! upgraded step by step (N → N+1 → … → current): JSON-level steps first, for changes in the
//! file's shape, then document-level steps. A file of a newer format than this build knows is
//! refused with a message saying so, never guessed at.
@@ -55,12 +55,16 @@ use crate::{DocError, Document, Result};
pub const FORMAT: u32 = 14;
pub fn format_string(n: u32) -> String {
format!("solvecraft/{n}")
format!("solidcraft/{n}")
}
/// The version in a format string (`solvecraft/N`).
/// The format tag of designs saved before the rename. Compatibility shim (docs/compatibility.md):
/// read, never written.
pub const LEGACY_FORMAT_PREFIX: &str = "solvecraft/";
/// The version in a format string (`solidcraft/N`, or the legacy `solvecraft/N`).
pub fn version_of(s: &str) -> Option<u32> {
s.strip_prefix("solvecraft/")?.parse().ok()
s.strip_prefix("solidcraft/").or_else(|| s.strip_prefix(LEGACY_FORMAT_PREFIX))?.parse().ok()
}
/// JSON-level upgrades: entry `i` turns format `i + 1` into `i + 2`.
@@ -300,7 +304,7 @@ pub fn read(s: &str) -> Result<Document> {
if let Some(crate::Feature { kind: crate::FeatureKind::Sketch { sketch, .. }, .. }) = d.features.get_mut(feature_index) {
let mut spaces = std::collections::BTreeMap::new();
for link in &sketch.links {
spaces.entry(link.id.as_str()).or_insert_with(|| solvecraft_sketch::WireSpace::from_legacy_link(link));
spaces.entry(link.id.as_str()).or_insert_with(|| solidcraft_sketch::WireSpace::from_legacy_link(link));
}
for wire_index in wire_indices {
if let Some(wire) = sketch.wires.get_mut(wire_index) {
@@ -358,9 +362,9 @@ fn validate_sketch_sizes(value: &Value) -> Result<()> {
}
let Some(sketch) = feature.get("sketch") else { continue };
let len = |key: &str| sketch.get(key).and_then(Value::as_array).map_or(0, Vec::len);
if len("points") > solvecraft_sketch::MAX_POINTS
|| len("curves") > solvecraft_sketch::MAX_CURVES
|| len("constraints") > solvecraft_sketch::MAX_CONSTRAINTS
if len("points") > solidcraft_sketch::MAX_POINTS
|| len("curves") > solidcraft_sketch::MAX_CURVES
|| len("constraints") > solidcraft_sketch::MAX_CONSTRAINTS
{
return Err(invalid());
}
@@ -553,7 +557,7 @@ fn validate_versioned_geometry(value: &Value) -> Result<()> {
}
let indices = curve.get("pts").and_then(Value::as_array).ok_or_else(invalid)?;
work = work.saturating_add(indices.len());
if !(4..=solvecraft_sketch::MAX_SPLINE_POINTS).contains(&indices.len())
if !(4..=solidcraft_sketch::MAX_SPLINE_POINTS).contains(&indices.len())
|| work > 1_000_000
|| curve.get("control").is_some_and(|control| control.as_bool() != Some(false))
|| curve.get("degree").is_some_and(|degree| degree.as_u64() != Some(3))
@@ -630,7 +634,7 @@ fn validate_freeform_offsets(value: &Value) -> Result<()> {
if !(1..=256).contains(&divisions)
|| !divisions.is_power_of_two()
|| !(1..=256).contains(&pieces)
|| pieces * divisions >= solvecraft_sketch::MAX_SPLINE_POINTS as u64
|| pieces * divisions >= solidcraft_sketch::MAX_SPLINE_POINTS as u64
|| !distance.is_finite()
|| !(1e-9..=MAX_REAL).contains(&distance.abs())
|| !tolerance.is_finite()
@@ -727,7 +731,7 @@ mod tests {
#[test]
fn version_thirteen_names_keep_their_meaning_under_the_legacy_mark() {
let file = serde_json::json!({
"format": "solvecraft/13",
"format": "solidcraft/13",
"features": [
{"type": "fillet", "edge_names": ["F2:side:l2|F2:top#2~1", ""], "face_names": ["F2:top#1", ""]},
{"type": "sheet_flange", "edge_names": ["base:l1:top"]},
@@ -774,7 +778,7 @@ mod tests {
#[test]
fn version_thirteen_marking_targets_name_fields_not_key_names() {
let file = serde_json::json!({
"format": "solvecraft/13",
"format": "solidcraft/13",
"features": [
{"type": "extrude", "options": {"face": "flat", "face_name": "label", "face_names": ["a"]}},
{"type": "fillet", "settings": {"edge_names": ["kept"]}},
@@ -794,8 +798,8 @@ mod tests {
#[test]
fn version_nine_wire_spaces_preserve_saved_points_and_explicit_markers() {
use solvecraft_geom::Vec3;
use solvecraft_sketch::{LinkKind, LinkSource, Sketch, WireSpace};
use solidcraft_geom::Vec3;
use solidcraft_sketch::{LinkKind, LinkSource, Sketch, WireSpace};
let mut sketch = Sketch::new();
sketch.add_drawn_wire(&[Vec3::new(12.0, 3.0, 4.0)]).unwrap();
let link = sketch
@@ -810,7 +814,7 @@ mod tests {
let mut doc = Document::new("legacy wire spaces");
doc.add_feature(crate::FeatureKind::Sketch { plane: crate::PlaneRef::Origin { name: "XY".into() }, sketch }, None).unwrap();
let mut json: Value = serde_json::from_str(&doc.to_json()).unwrap();
json["format"] = Value::from("solvecraft/9");
json["format"] = Value::from("solidcraft/9");
for wire in json["features"][0]["sketch"]["wires"].as_array_mut().unwrap() {
wire.as_object_mut().unwrap().remove("space");
}
@@ -891,7 +895,7 @@ mod tests {
let mut document = Document::new("legacy named extent");
document.set_param("offset", "2", Some("mm"), None).unwrap();
let sketch = document
.add_feature(FeatureKind::Sketch { plane: PlaneRef::Origin { name: "XY".into() }, sketch: solvecraft_sketch::Sketch::new() }, None)
.add_feature(FeatureKind::Sketch { plane: PlaneRef::Origin { name: "XY".into() }, sketch: solidcraft_sketch::Sketch::new() }, None)
.unwrap();
document
.add_feature(
@@ -905,9 +909,9 @@ mod tests {
start_offset: None,
through_all: false,
taper: Some("0 deg".into()),
to: Some(solvecraft_geom::Vec3::new(0.0, 0.0, 10.0)),
to: Some(solidcraft_geom::Vec3::new(0.0, 0.0, 10.0)),
to_offset: Some("offset + 1 mm".into()),
from: Some(solvecraft_geom::Vec3::new(0.0, 0.0, 5.0)),
from: Some(solidcraft_geom::Vec3::new(0.0, 0.0, 5.0)),
},
operation: Operation::NewBody,
targets: vec![],
@@ -928,7 +932,7 @@ mod tests {
)
.unwrap();
let mut legacy: Value = serde_json::from_str(&document.to_json()).unwrap();
legacy["format"] = Value::from("solvecraft/8");
legacy["format"] = Value::from("solidcraft/8");
// Version 8 exposed distance and taper, but had no model slot for To Offset.
legacy["features"][1]["param_names"] = serde_json::json!(["d1", "d2"]);
legacy["features"][2]["param_names"] = serde_json::json!(["d3", "d4", "d5"]);
@@ -952,8 +956,8 @@ mod tests {
#[test]
fn saved_offset_cannot_alias_and_overwrite_its_source_geometry() {
use solvecraft_geom::Vec2;
use solvecraft_sketch::{CurveKind, Sketch};
use solidcraft_geom::Vec2;
use solidcraft_sketch::{CurveKind, Sketch};
let mut sketch = Sketch::new();
let center = sketch.add_point(Vec2::ZERO, None).unwrap();
let major = sketch.add_point(Vec2::new(10.0, 0.0), None).unwrap();
@@ -976,7 +980,7 @@ mod tests {
let mut document = Document::new("missing offset geometry");
document
.add_feature(
crate::FeatureKind::Sketch { plane: crate::PlaneRef::Origin { name: "XY".into() }, sketch: solvecraft_sketch::Sketch::new() },
crate::FeatureKind::Sketch { plane: crate::PlaneRef::Origin { name: "XY".into() }, sketch: solidcraft_sketch::Sketch::new() },
None,
)
.unwrap();
@@ -1052,7 +1056,7 @@ mod tests {
("degree", serde_json::json!(2)),
("pts", serde_json::json!([0, 1, 2])),
("pts", serde_json::json!([0, 1, 2, u64::MAX])),
("pts", serde_json::json!(vec![0; solvecraft_sketch::MAX_SPLINE_POINTS + 1])),
("pts", serde_json::json!(vec![0; solidcraft_sketch::MAX_SPLINE_POINTS + 1])),
] {
let mut damaged = curve.clone();
damaged[key] = malformed;
@@ -1091,8 +1095,8 @@ mod tests {
#[test]
fn periodic_file_validation_refuses_damaged_closed_geometry() {
use solvecraft_geom::Vec2;
use solvecraft_sketch::{CurveKind, Sketch};
use solidcraft_geom::Vec2;
use solidcraft_sketch::{CurveKind, Sketch};
let mut sketch = Sketch::new();
let mut pts = Vec::new();
for point in [Vec2::ZERO, Vec2::new(10.0, 0.0), Vec2::new(0.0, 10.0), Vec2::ZERO] {
@@ -1199,7 +1203,7 @@ mod tests {
];
d.configs.active = Some("Default".into());
let mut legacy: Value = serde_json::from_str(&d.to_json()).unwrap();
legacy["format"] = Value::from("solvecraft/6");
legacy["format"] = Value::from("solidcraft/6");
let original_table = legacy["configs"].clone();
let upgraded = read(&legacy.to_string()).unwrap();
d.features.iter_mut().for_each(|f| f.shell_piece_order = true);
@@ -1224,7 +1228,7 @@ mod tests {
}];
d.apply_configuration("Blue steel").unwrap();
let mut value: Value = serde_json::from_str(&d.to_json()).unwrap();
value["format"] = Value::from("solvecraft/7");
value["format"] = Value::from("solidcraft/7");
let original_table = value["configs"].clone();
let roundtrip = read(&value.to_string()).unwrap();
assert_eq!(roundtrip, d);
@@ -1241,8 +1245,8 @@ mod tests {
#[test]
fn version_seven_origin_sketch_keeps_geometry_dimension_ids_and_expressions() {
use solvecraft_geom::Vec2;
use solvecraft_sketch::{ConstraintKind, Sketch};
use solidcraft_geom::Vec2;
use solidcraft_sketch::{ConstraintKind, Sketch};
let mut d = Document::new("legacy origin dimensions");
d.set_param("reach", "25 mm", Some("mm"), None).unwrap();
let dim = d.new_model_param("reach", "mm");
@@ -1253,7 +1257,7 @@ mod tests {
sketch.add_constraint(ConstraintKind::Length { l: line, value: 25.0 }, Some(dim)).unwrap();
d.add_feature(crate::FeatureKind::Sketch { plane: crate::PlaneRef::Origin { name: "XY".into() }, sketch }, Some("Dimensioned")).unwrap();
let mut legacy: Value = serde_json::from_str(&d.to_json()).unwrap();
legacy["format"] = Value::from("solvecraft/7");
legacy["format"] = Value::from("solidcraft/7");
let mut features = legacy["features"].clone();
pieces_in_shell_order(&mut features);
let params = legacy["params"].clone();
@@ -1268,8 +1272,8 @@ mod tests {
#[test]
fn version_eight_persistent_angle_sweep_and_slot_dimensions_roundtrip() {
use solvecraft_geom::Vec2;
use solvecraft_sketch::{ConstraintKind, Sketch};
use solidcraft_geom::Vec2;
use solidcraft_sketch::{ConstraintKind, Sketch};
let mut d = Document::new("persistent typed dimensions");
let heading = d.new_model_param("45 deg", "deg");
let sweep = d.new_model_param("180 deg", "deg");
@@ -1358,8 +1362,11 @@ mod tests {
#[test]
fn versions() {
assert_eq!(version_of("solvecraft/1"), Some(1));
assert_eq!(version_of("solvecraft/x"), None);
assert_eq!(version_of("solidcraft/1"), Some(1));
assert_eq!(version_of("solidcraft/x"), None);
// Compatibility shim: files saved before the rename carry the legacy tag.
assert_eq!(version_of("solvecraft/13"), Some(13));
assert_eq!(version_of("solvecraftx/13"), None);
let d = Document::new("A");
assert_eq!(d.format, format_string(FORMAT));
let back = read(&d.to_json()).unwrap();
@@ -1378,44 +1385,44 @@ mod tests {
)
.unwrap();
let mut json: Value = serde_json::from_str(&doc.to_json()).unwrap();
json["format"] = Value::from("solvecraft/5");
json["format"] = Value::from("solidcraft/5");
let migrated = read(&json.to_string()).unwrap();
let crate::FeatureKind::ConstructionPlane { plane } = &migrated.feature(id).unwrap().kind else { panic!("plane") };
let (values, errors) = migrated.param_values();
assert!(errors.is_empty());
let p = migrated.resolve_plane(&values, plane, 0).unwrap();
assert_eq!(p.y, solvecraft_geom::Vec3::Z);
assert_eq!(p.normal(), -solvecraft_geom::Vec3::Y);
assert_eq!(p.origin, solvecraft_geom::Vec3::new(0.0, -7.0, 0.0));
assert_eq!(p.y, solidcraft_geom::Vec3::Z);
assert_eq!(p.normal(), -solidcraft_geom::Vec3::Y);
assert_eq!(p.origin, solidcraft_geom::Vec3::new(0.0, -7.0, 0.0));
assert_eq!(read(&migrated.to_json()).unwrap(), migrated);
let fresh = doc.resolve_plane(&values, &crate::PlaneRef::Origin { name: "XZ".into() }, 0).unwrap();
assert_eq!(fresh.normal(), solvecraft_geom::Vec3::Y);
assert_eq!(fresh.normal(), solidcraft_geom::Vec3::Y);
}
#[test]
fn older_upgraded_newer_refused_others_rejected() {
let mut v: Value = serde_json::from_str(&Document::new("A").to_json()).unwrap();
v["format"] = Value::from("solvecraft/1");
v["format"] = Value::from("solidcraft/1");
assert_eq!(read(&v.to_string()).unwrap().format, format_string(FORMAT));
v.as_object_mut().unwrap().remove("format");
assert!(read(&v.to_string()).is_ok(), "a file without a format is format 1");
v["format"] = Value::from(format_string(FORMAT + 1));
let e = read(&v.to_string()).unwrap_err().to_string();
assert!(e.contains("newer SolidCraft"), "{e}");
for bad in ["solvecraft/0", "acad/3", "solvecraft/-1", "solvecraft/99999999999999999999"] {
for bad in ["solidcraft/0", "acad/3", "solidcraft/-1", "solidcraft/99999999999999999999"] {
v["format"] = Value::from(bad);
assert!(read(&v.to_string()).is_err(), "{bad}");
}
assert!(read("[]").is_err());
// Format 2 body colours become body appearances.
let mut old: Value = serde_json::from_str(&Document::new("A").to_json()).unwrap();
old["format"] = Value::from("solvecraft/2");
old["format"] = Value::from("solidcraft/2");
old["appearances"] = serde_json::json!({"Body1": [200, 10, 20]});
let d = read(&old.to_string()).unwrap();
let l = d.appearances.bodies.get("Body1").unwrap();
// Format 3 joints (aligned unless flipped) keep their placement.
let mut j: Value = serde_json::from_str(&Document::new("A").to_json()).unwrap();
j["format"] = Value::from("solvecraft/3");
j["format"] = Value::from("solidcraft/3");
j["assembly"] = serde_json::json!({"joints": [{"id": 1, "name": "J", "kind": "rigid", "a": {"occurrence": 0, "snap": {"type": "point", "point": [0, 0, 0]}}, "b": {"occurrence": 0, "snap": {"type": "point", "point": [0, 0, 0]}}}]});
let dj = read(&j.to_string()).unwrap();
assert!(dj.assembly.joints[0].flip, "aligned before = not mated now");
@@ -1431,14 +1438,14 @@ mod tests {
/// file too, before the arrays are deserialised.
#[test]
fn oversized_sketches_are_refused_before_typed_decoding() {
use solvecraft_sketch::{MAX_CONSTRAINTS, MAX_CURVES, MAX_POINTS, Sketch};
use solidcraft_sketch::{MAX_CONSTRAINTS, MAX_CURVES, MAX_POINTS, Sketch};
// A sketch right at the point cap, chained by coincident constraints.
let mut sketch = Sketch::new();
let mut previous = 0;
while sketch.points.len() < MAX_POINTS {
let i = sketch.add_point(solvecraft_geom::Vec2::new(0.001 * sketch.points.len() as f64, 0.0), None).unwrap();
sketch.add_constraint(solvecraft_sketch::ConstraintKind::Coincident { p: previous, q: i }, None).unwrap();
let i = sketch.add_point(solidcraft_geom::Vec2::new(0.001 * sketch.points.len() as f64, 0.0), None).unwrap();
sketch.add_constraint(solidcraft_sketch::ConstraintKind::Coincident { p: previous, q: i }, None).unwrap();
previous = i;
}
let mut doc = Document::new("big sketch");
+8 -8
View File
@@ -380,11 +380,11 @@ mod tests {
#[test]
fn formats_one_through_eleven_keep_legacy_offset_geometry_and_ids() {
let mut sketch = solvecraft_sketch::Sketch::new();
let center = sketch.add_point(solvecraft_geom::Vec2::new(4., 7.), Some("center")).unwrap();
sketch.add_circle(solvecraft_geom::Vec2::new(4., 7.), 10., Some(center), Some("source")).unwrap();
sketch.add_circle(solvecraft_geom::Vec2::new(4., 7.), 9., Some(center), Some("offset")).unwrap();
sketch.offsets.push(solvecraft_sketch::OffsetChain {
let mut sketch = solidcraft_sketch::Sketch::new();
let center = sketch.add_point(solidcraft_geom::Vec2::new(4., 7.), Some("center")).unwrap();
sketch.add_circle(solidcraft_geom::Vec2::new(4., 7.), 10., Some(center), Some("source")).unwrap();
sketch.add_circle(solidcraft_geom::Vec2::new(4., 7.), 9., Some(center), Some("offset")).unwrap();
sketch.offsets.push(solidcraft_sketch::OffsetChain {
sources: vec![("source".into(), true)],
outputs: vec!["offset".into()],
distance: 1.,
@@ -409,15 +409,15 @@ mod tests {
let state = model.state();
let solved = state.sketch(sketch_id).unwrap();
let radius = solved.sketch.curves.iter().find(|curve| curve.id == "offset").map(|curve| &curve.kind);
assert!(matches!(radius,Some(solvecraft_sketch::CurveKind::Circle {r,..}) if *r ==9.));
assert!(matches!(radius,Some(solidcraft_sketch::CurveKind::Circle {r,..}) if *r ==9.));
}
}
#[test]
fn missing_boundary_or_defining_sketch_reopens_as_a_model_error_without_retarget() {
for missing_sketch in [false, true] {
let mut sketch = solvecraft_sketch::Sketch::new();
sketch.add_circle(solvecraft_geom::Vec2::new(0., 0.), 10., None, Some("unrelated-circle")).unwrap();
let mut sketch = solidcraft_sketch::Sketch::new();
sketch.add_circle(solidcraft_geom::Vec2::new(0., 0.), 10., None, Some("unrelated-circle")).unwrap();
let mut document = Document::new("repairable lost boundary");
let id = document.add_feature(FeatureKind::Sketch { plane: crate::PlaneRef::Origin { name: "XY".into() }, sketch }, None).unwrap();
document
+1 -1
View File
@@ -17,7 +17,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Arc, OnceLock};
use solvecraft_kernel::history::{FaceOrigin, FaceProv, History};
use solidcraft_kernel::history::{FaceOrigin, FaceProv, History};
use crate::ModelBody;
use crate::lineage::{Lineage, Operand};
+3 -3
View File
@@ -13,7 +13,7 @@
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use serde::{Deserialize, Serialize};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use crate::{DocError, Document, IDENTITY, Mat, ModelState, Result, apply_point, apply_vector, mat_inverse, mat_mul};
@@ -273,9 +273,9 @@ pub fn resolve_origin(doc: &Document, st: &ModelState, o: &JointOrigin) -> Resul
let comp = component_of(doc, o.occurrence)?;
let cyl = component_bodies(doc, st, comp)
.into_iter()
.filter_map(|b| solvecraft_kernel::cylinder_face_at(&b.body, *pick))
.filter_map(|b| solidcraft_kernel::cylinder_face_at(&b.body, *pick))
.min_by(|a, b| {
let off = |c: &solvecraft_kernel::CylinderFace| {
let off = |c: &solidcraft_kernel::CylinderFace| {
let v = *pick - c.axis_point;
((v - c.axis * v.dot(c.axis)).len() - c.radius).abs()
};
+5 -5
View File
@@ -1,4 +1,4 @@
//! The SolveCraft document: user and model parameters, and a parametric timeline of features
//! The SolidCraft document: user and model parameters, and a parametric timeline of features
//! (sketches, extrudes, revolves, fillets, primitives, combines…). Evaluating the timeline with
//! the kernel produces the model (bodies and solved sketches). Evaluation is incremental: a
//! feature is recomputed only when it, a parameter it uses, or an earlier feature changed.
@@ -47,8 +47,8 @@ pub use eval::{
state_in_frame_at, through_all_distance, world_state,
};
pub use params::{ParamRow, ParamUser, cycles};
pub use solvecraft_kernel as kernel;
pub use solvecraft_sketch as sketch;
pub use solidcraft_kernel as kernel;
pub use solidcraft_sketch as sketch;
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum DocError {
@@ -59,9 +59,9 @@ pub enum DocError {
#[error("unknown {0}")]
Unknown(String),
#[error(transparent)]
Sketch(#[from] solvecraft_sketch::SketchError),
Sketch(#[from] solidcraft_sketch::SketchError),
#[error(transparent)]
Kernel(#[from] solvecraft_kernel::KernelError),
Kernel(#[from] solidcraft_kernel::KernelError),
}
pub type Result<T> = std::result::Result<T, DocError>;
+5 -5
View File
@@ -11,9 +11,9 @@
use std::sync::{Arc, OnceLock};
use solvecraft_geom::Region2;
use solvecraft_kernel::Body;
use solvecraft_kernel::history::{Change, FaceOrigin, FaceProv, History};
use solidcraft_geom::Region2;
use solidcraft_kernel::Body;
use solidcraft_kernel::history::{Change, FaceOrigin, FaceProv, History};
use crate::SolvedSketch;
@@ -183,7 +183,7 @@ impl<'a> ProfileRoles<'a> {
if history.operands != 0 {
return None;
}
let segments: Vec<&solvecraft_geom::Seg2> =
let segments: Vec<&solidcraft_geom::Seg2> =
self.region.map(|r| std::iter::once(&r.outer).chain(&r.holes).flat_map(|l| &l.segs).collect()).unwrap_or_default();
let curves = self.sketch.map(crate::config::ExactSketchCurves::new);
let curve = |segment: usize| -> Option<String> { curves.as_ref()?.find(segments.get(segment)?).map(|id| format!("side:{id}")) };
@@ -235,7 +235,7 @@ pub(crate) fn box_tool(body: Body) -> Traced {
sources
.iter()
.map(|source| match source {
Some(solvecraft_kernel::FaceSource::Box { axis, high, .. }) => {
Some(solidcraft_kernel::FaceSource::Box { axis, high, .. }) => {
["x", "y", "z"].get(usize::from(*axis)).map(|a| vec![format!("{}{a}", if *high { "+" } else { "-" })]).unwrap_or_default()
}
_ => Vec::new(),
+1 -1
View File
@@ -3,7 +3,7 @@
//! shown without changing the design).
use serde::{Deserialize, Serialize};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
/// Occurrences whose bodies may touch but not pass through each other while joints move.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+1 -1
View File
@@ -8,7 +8,7 @@
//! was worked out from geometry, found where the reference's point no longer lies, is reported:
//! only geometry decided.
use solvecraft_geom::{Mesh, Vec3};
use solidcraft_geom::{Mesh, Vec3};
use crate::history_names::{Match, Names};
use crate::naming::{self, closest, edge_names, face_names, legacy_edge_names, legacy_face_names, strip_piece};
+3 -3
View File
@@ -1,11 +1,11 @@
use crate::{Document, ModelBody, ModelState};
use solvecraft_geom::{Plane, Vec3};
use solvecraft_sketch::{LinkKind, LinkSource, Sketch};
use solidcraft_geom::{Plane, Vec3};
use solidcraft_sketch::{LinkKind, LinkSource, Sketch};
use std::collections::BTreeMap;
use std::sync::Arc;
fn named_box(name: &str, width: f64, height: f64) -> ModelBody {
let body = solvecraft_kernel::box_solid(Vec3::ZERO, Vec3::new(width, width, height)).unwrap();
let body = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(width, width, height)).unwrap();
let mut names = vec!["unrelated".to_string(); body.faces(0.01).unwrap().len()];
let top = body.faces(0.01).unwrap().into_iter().find(|face| face.plane_normal.is_some_and(|normal| normal.z > 0.99)).unwrap().index;
names[top] = "shared:top".into();
+2 -2
View File
@@ -17,7 +17,7 @@
use std::sync::{Arc, OnceLock};
use solvecraft_geom::{Mesh, Seg2, Vec2, Vec3};
use solidcraft_geom::{Mesh, Seg2, Vec2, Vec3};
use crate::history_names::Names;
use crate::lineage::Lineage;
@@ -737,7 +737,7 @@ mod tests {
#[test]
fn merged_face_votes_collectively_for_pieces_of_one_source() {
let body = ModelBody::new("Input".into(), solvecraft_kernel::box_solid(Vec3::ZERO, Vec3::new(4., 1., 1.)).unwrap(), 1);
let body = ModelBody::new("Input".into(), solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(4., 1., 1.)).unwrap(), 1);
let mut mesh = Mesh::default();
let mut points = Vec::new();
for i in 0..3u32 {
+13 -13
View File
@@ -5,7 +5,7 @@ use crate::{DocError, ProfileSel, Result, SolvedSketch, WholeProfileBoundary};
const MAX_SELECTIONS: usize = 128;
fn whole_boundary(profile: &solvecraft_sketch::Profile) -> Option<WholeProfileBoundary> {
fn whole_boundary(profile: &solidcraft_sketch::Profile) -> Option<WholeProfileBoundary> {
let (outer, holes) = profile.whole_curve_boundaries()?;
Some(WholeProfileBoundary { outer, holes })
}
@@ -117,10 +117,10 @@ pub fn profile_selection_indices(ss: &SolvedSketch, selection: &ProfileSel) -> R
if curves.iter().collect::<BTreeSet<_>>().len() != curves.len()
&& !ss.profiles.iter().any(|profile| same_ids(curves, &profile.outer_curves))
{
return Err(crate::curve_pick_error(solvecraft_sketch::CurvePick::Missing, curves, &ss.name));
return Err(crate::curve_pick_error(solidcraft_sketch::CurvePick::Missing, curves, &ss.name));
}
indices.push(
solvecraft_sketch::profile_by_curves(&ss.profiles, curves)
solidcraft_sketch::profile_by_curves(&ss.profiles, curves)
.map_err(|error| crate::curve_pick_error(error, curves, &ss.name))?,
);
}
@@ -230,8 +230,8 @@ pub fn capture_profile_selection(ss: &SolvedSketch, indices: &[usize]) -> Result
mod tests {
#![allow(clippy::unwrap_used, clippy::panic)]
use super::*;
use solvecraft_geom::{Plane, Vec2};
use solvecraft_sketch::{Sketch, find_profiles, solve};
use solidcraft_geom::{Plane, Vec2};
use solidcraft_sketch::{Sketch, find_profiles, solve};
fn rounded_source(sketch: &mut Sketch, radius: f64) -> Vec<(String, bool)> {
let endpoints = [
@@ -328,13 +328,13 @@ mod tests {
)
.unwrap();
model.evaluate(&document);
let volume = solvecraft_kernel::measure(&model.state().bodies[0].body).unwrap().volume;
let volume = solidcraft_kernel::measure(&model.state().bodies[0].body).unwrap().volume;
assert!((volume - 2. * 73.2 * 63.2).abs() < 1e-6, "{volume}");
let reopened = crate::Document::from_json(&document.to_json()).unwrap();
assert_eq!(reopened, document);
let mut cold = crate::Model::new();
cold.evaluate(&reopened);
assert!((solvecraft_kernel::measure(&cold.state().bodies[0].body).unwrap().volume - volume).abs() < 1e-6);
assert!((solidcraft_kernel::measure(&cold.state().bodies[0].body).unwrap().volume - volume).abs() < 1e-6);
let crate::FeatureKind::Sketch { sketch, .. } = &mut document.features[0].kind else { panic!("sketch") };
set_source_radius(sketch, 2.);
model.evaluate(&document);
@@ -345,7 +345,7 @@ mod tests {
assert_eq!(active, original_roles);
let rounded_area = 75.2 * 65.2 - (4. - std::f64::consts::PI) * 0.6 * 0.6;
assert!((crate::select_profiles(solved, &selection).unwrap()[0].area() - rounded_area).abs() < 1e-7);
let volume = solvecraft_kernel::measure(&model.state().bodies[0].body).unwrap().volume;
let volume = solidcraft_kernel::measure(&model.state().bodies[0].body).unwrap().volume;
// Kernel volume integrates the curved display mesh; the analytic region above
// checks the exact rounded geometry independently of that tessellation.
assert!((volume - 2. * rounded_area).abs() < 0.002, "{volume}");
@@ -359,7 +359,7 @@ mod tests {
);
let reopened = crate::Document::from_json(&document.to_json()).unwrap();
cold.evaluate(&reopened);
assert!((solvecraft_kernel::measure(&cold.state().bodies[0].body).unwrap().volume - volume).abs() < 1e-5);
assert!((solidcraft_kernel::measure(&cold.state().bodies[0].body).unwrap().volume - volume).abs() < 1e-5);
}
#[test]
@@ -387,7 +387,7 @@ mod tests {
fn periodic_offset_ring_whole_source_survives_adaptive_spans_and_reopen() {
let mut sketch = Sketch::new();
let major = sketch.add_point(Vec2::new(10., 0.), None).unwrap();
let ellipse = sketch.add_curve(solvecraft_sketch::CurveKind::Ellipse { c: 0, m: major, r: 5. }, None).unwrap();
let ellipse = sketch.add_curve(solidcraft_sketch::CurveKind::Ellipse { c: 0, m: major, r: 5. }, None).unwrap();
let first = sketch.add_freeform_offset(ellipse, true, -1.).unwrap();
sketch.add_freeform_offset(first, true, -0.5).unwrap();
let mut document = crate::Document::new("periodic offset ring multiset");
@@ -453,7 +453,7 @@ mod tests {
)
.unwrap();
model.evaluate(&document);
let initial_volume = solvecraft_kernel::measure(&model.state().bodies[0].body).unwrap().volume;
let initial_volume = solidcraft_kernel::measure(&model.state().bodies[0].body).unwrap().volume;
// Kernel measurement integrates the curved mesh. Scale the established
// periodic-ring tolerance (0.05 mm³ at height3) to this height2 extrusion.
// Exact profile identity and cold/warm equality below remain strict.
@@ -481,7 +481,7 @@ mod tests {
assert!(updated_region.contains(ring_point));
assert!(!updated_region.contains(source_point + normal * 0.5));
assert!(!updated_region.contains(source_point + normal * 1.75));
let updated_volume = solvecraft_kernel::measure(&updated_state.bodies[0].body).unwrap().volume;
let updated_volume = solidcraft_kernel::measure(&updated_state.bodies[0].body).unwrap().volume;
assert!(updated_volume > initial_volume);
assert!(
(updated_volume - 2. * updated_region.area()).abs() < volume_tolerance,
@@ -491,7 +491,7 @@ mod tests {
let reopened_document = crate::Document::from_json(&document.to_json()).unwrap();
cold.evaluate(&reopened_document);
assert!(cold.results.iter().all(|result| result.error.is_none()));
assert!((solvecraft_kernel::measure(&cold.state().bodies[0].body).unwrap().volume - updated_volume).abs() < 1e-6);
assert!((solidcraft_kernel::measure(&cold.state().bodies[0].body).unwrap().volume - updated_volume).abs() < 1e-6);
// A new hole is a semantic boundary change, not the old ring. Whole-source
// identities include holes and must not silently absorb the new material cutout.
+9 -9
View File
@@ -8,8 +8,8 @@
use std::collections::BTreeMap;
use solvecraft_geom::{Mesh, Plane, Vec2, Vec3};
use solvecraft_sketch::{CurveKind, LinkGeom, LinkKind, LinkSource, Sketch};
use solidcraft_geom::{Mesh, Plane, Vec2, Vec3};
use solidcraft_sketch::{CurveKind, LinkGeom, LinkKind, LinkSource, Sketch};
use crate::eval::ModelState;
use crate::expr::Value;
@@ -259,7 +259,7 @@ fn resolve_uncached(
_ => vec![LinkGeom::Point(o)],
}
}
LinkSource::Text { text, at, height, angle } => solvecraft_sketch::text_geometry(text, *at, *height, *angle)?,
LinkSource::Text { text, at, height, angle } => solidcraft_sketch::text_geometry(text, *at, *height, *angle)?,
LinkSource::Plane { name } => {
let pr =
if Plane::named(name).is_some() { PlaneRef::Origin { name: name.clone() } } else { PlaneRef::Construction { name: name.clone() } };
@@ -310,7 +310,7 @@ fn projection_face_edges(body: &crate::ModelBody, mesh: &Mesh, index: u32, plane
.body
.face_sources()
.and_then(|sources| sources.get(index as usize))
.is_some_and(|source| matches!(source, Some(solvecraft_kernel::FaceSource::ShellPlanar { .. })));
.is_some_and(|source| matches!(source, Some(solidcraft_kernel::FaceSource::ShellPlanar { .. })));
if shell_plane { order::raw_face_edges(mesh, index, plane) } else { order::face_edges(mesh, index, plane) }
}
@@ -319,10 +319,10 @@ fn certified_planar_construction(body: &crate::ModelBody, index: usize) -> bool
matches!(
source,
Some(
solvecraft_kernel::FaceSource::ShellPlanar { producer: Some(_), .. }
| solvecraft_kernel::FaceSource::Box { producer: Some(_), .. }
| solvecraft_kernel::FaceSource::CertifiedExtrudeSide { plane: Some(_), .. }
| solvecraft_kernel::FaceSource::CertifiedExtrudeCap { .. }
solidcraft_kernel::FaceSource::ShellPlanar { producer: Some(_), .. }
| solidcraft_kernel::FaceSource::Box { producer: Some(_), .. }
| solidcraft_kernel::FaceSource::CertifiedExtrudeSide { plane: Some(_), .. }
| solidcraft_kernel::FaceSource::CertifiedExtrudeCap { .. }
)
)
})
@@ -785,7 +785,7 @@ fn as_spline(pts: &[Vec2], tol: f64) -> Option<LinkGeom> {
if fit.len() < 2 {
return None;
}
let poly = solvecraft_sketch::spline_polyline(&fit, false, 3);
let poly = solidcraft_sketch::spline_polyline(&fit, false, 3);
let ok = pts.iter().all(|q| {
poly.windows(2)
.map(|w| {
+7 -7
View File
@@ -4,8 +4,8 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use solvecraft_geom::{Mesh, Plane, Vec2, Vec3};
use solvecraft_sketch::LinkSource;
use solidcraft_geom::{Mesh, Plane, Vec2, Vec3};
use solidcraft_sketch::LinkSource;
use crate::eval::ModelState;
@@ -439,8 +439,8 @@ fn tri_dist(p: Vec3, t: &[Vec3; 3]) -> f64 {
}
/// Sample a fitted planar curve.
fn link_polyline(g: &solvecraft_sketch::LinkGeom) -> Vec<Vec2> {
use solvecraft_sketch::LinkGeom;
fn link_polyline(g: &solidcraft_sketch::LinkGeom) -> Vec<Vec2> {
use solidcraft_sketch::LinkGeom;
match *g {
LinkGeom::Line(a, b) => vec![a, b],
LinkGeom::Circle(c, r) => (0..=96).map(|i| c + Vec2::from_angle(i as f64 / 96.0 * std::f64::consts::TAU) * r).collect(),
@@ -452,11 +452,11 @@ fn link_polyline(g: &solvecraft_sketch::LinkGeom) -> Vec<Vec2> {
let r = c.dist(a);
(0..=48).map(|i| c + Vec2::from_angle(s0 + sw * i as f64 / 48.0) * r).collect()
}
LinkGeom::Conic { a, apex, b, rho } => (0..=32).map(|i| solvecraft_sketch::conic_point(a, apex, b, rho, i as f64 / 32.0)).collect(),
LinkGeom::Conic { a, apex, b, rho } => (0..=32).map(|i| solidcraft_sketch::conic_point(a, apex, b, rho, i as f64 / 32.0)).collect(),
LinkGeom::Ellipse { c, major, minor } => {
(0..=96).map(|i| solvecraft_sketch::ellipse_point(c, major, minor, i as f64 / 96.0 * std::f64::consts::TAU)).collect()
(0..=96).map(|i| solidcraft_sketch::ellipse_point(c, major, minor, i as f64 / 96.0 * std::f64::consts::TAU)).collect()
}
LinkGeom::Spline { ref pts, control, degree, periodic } => solvecraft_sketch::spline_polyline_mode(pts, control, degree, periodic),
LinkGeom::Spline { ref pts, control, degree, periodic } => solidcraft_sketch::spline_polyline_mode(pts, control, degree, periodic),
LinkGeom::Point(_) => Vec::new(),
}
}
+10 -10
View File
@@ -3,7 +3,7 @@
use std::collections::BTreeMap;
use solvecraft_geom::{Mesh, Plane, Vec3};
use solidcraft_geom::{Mesh, Plane, Vec3};
use crate::{DocError, Result};
@@ -60,7 +60,7 @@ fn boundary_edges(mesh: &Mesh, mut edges: Vec<usize>, plane: &Plane) -> Result<V
if edges.iter().any(|e| !mesh.edges.get(*e).is_some_and(|p| p.len() >= 2 && p.iter().all(|p| p.is_finite()))) {
return Err(DocError::Invalid("face projection has an invalid boundary edge".into()));
}
if edges.len() > solvecraft_sketch::MAX_LINK_ENTITIES {
if edges.len() > solidcraft_sketch::MAX_LINK_ENTITIES {
return Err(DocError::Invalid("face projection exceeds the boundary edge limit".into()));
}
let geometry_key = |e: usize| {
@@ -197,17 +197,17 @@ mod tests {
let edges = family_edges(&mesh, &[0, 1].into_iter().collect(), &Plane::XY).unwrap();
assert_eq!(edges.len(), 8);
let geom: Vec<_> = edges.iter().flat_map(|edge| super::super::fit_world(&Plane::XY, &mesh.edges[*edge])).collect();
let mut sk = solvecraft_sketch::Sketch::new();
let mut sk = solidcraft_sketch::Sketch::new();
let link = sk
.add_link(solvecraft_sketch::LinkKind::Project, solvecraft_sketch::LinkSource::Face { body: "ambiguous".into(), at: Vec3::ZERO }, &geom)
.add_link(solidcraft_sketch::LinkKind::Project, solidcraft_sketch::LinkSource::Face { body: "ambiguous".into(), at: Vec3::ZERO }, &geom)
.unwrap();
assert!(sk.update_link(&link, &geom).is_err());
}
#[test]
fn family_line_normalization_preserves_corners_arcs_branches_and_retracing() {
use solvecraft_geom::Vec2;
use solvecraft_sketch::LinkGeom;
use solidcraft_geom::Vec2;
use solidcraft_sketch::LinkGeom;
let line = |a, b| LinkGeom::Line(Vec2::new(a, 0.), Vec2::new(b, 0.));
assert_eq!(super::super::merge_family_lines(vec![line(0., 1.), line(2., 1.)]).unwrap(), vec![line(0., 2.)]);
for geometry in [
@@ -271,8 +271,8 @@ mod tests {
let normalized: Vec<_> = expected
.iter()
.map(|g| {
let solvecraft_sketch::LinkGeom::Line(a, b) = g[0] else { panic!("line") };
if a.x > b.x || a.y > b.y { vec![solvecraft_sketch::LinkGeom::Line(b, a)] } else { g.clone() }
let solidcraft_sketch::LinkGeom::Line(a, b) = g[0] else { panic!("line") };
if a.x > b.x || a.y > b.y { vec![solidcraft_sketch::LinkGeom::Line(b, a)] } else { g.clone() }
})
.collect();
assert_eq!(actual, normalized);
@@ -288,8 +288,8 @@ mod tests {
#[test]
fn rounded_boundary_arc_sweeps_survive_individual_edge_reversal() {
use solvecraft_geom::{Seg2, Vec2};
use solvecraft_sketch::LinkGeom;
use solidcraft_geom::{Seg2, Vec2};
use solidcraft_sketch::LinkGeom;
let mut mesh = Mesh::default();
for (extent, radius) in [(10.0, 3.0), (8.0, 1.0)] {
for k in 0..4 {
+2 -2
View File
@@ -10,8 +10,8 @@
//! together; the flat solid is the outline extruded through the thickness.
use serde::{Deserialize, Serialize};
use solvecraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
use solvecraft_kernel::{self as kernel, Body, EdgeSpec, FaceSpec, SurfSpec};
use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
use solidcraft_kernel::{self as kernel, Body, EdgeSpec, FaceSpec, SurfSpec};
use crate::expr::{self, Kind, Value};
use crate::{DocError, Document, IDENTITY, Mat, Result, apply_point, apply_vector, mat_inverse, mat_mul};
+4 -4
View File
@@ -1,6 +1,6 @@
use solvecraft_geom::{Vec2, Vec3};
use solvecraft_kernel::measure;
use solvecraft_sketch::{ConstraintKind, Sketch};
use solidcraft_geom::{Vec2, Vec3};
use solidcraft_kernel::measure;
use solidcraft_sketch::{ConstraintKind, Sketch};
use crate::*;
@@ -379,7 +379,7 @@ fn input_sketches_lists_every_variant_with_a_sketch_field() {
#[test]
fn timeline_clones_share_meshes_even_when_cloned_before_tessellation() {
let body = solvecraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap();
let body = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap();
let a = crate::eval::ModelBody::new("box".into(), body, 1);
let b = a.clone();
assert!(std::sync::Arc::ptr_eq(&a.mesh(), &b.mesh()));
+7 -7
View File
@@ -1,5 +1,5 @@
[package]
name = "solvecraft-engine"
name = "solidcraft-engine"
description = "SolidCraft engine: the session, the command registry (everything is a command), undo/redo and selection"
version.workspace = true
edition.workspace = true
@@ -9,12 +9,12 @@ license.workspace = true
workspace = true
[dependencies]
solvecraft-geom = { workspace = true }
solvecraft-sketch = { workspace = true }
solvecraft-kernel = { workspace = true }
solvecraft-doc = { workspace = true }
solvecraft-io = { workspace = true }
solvecraft-render = { workspace = true }
solidcraft-geom = { workspace = true }
solidcraft-sketch = { workspace = true }
solidcraft-kernel = { workspace = true }
solidcraft-doc = { workspace = true }
solidcraft-io = { workspace = true }
solidcraft-render = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
@@ -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)?);
Loaded 100 of 464 files, more files were not shown because too many files have changed in this diff. Show more