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:
1 parent
27415e4dd7
commit
296683229d
464 files changed
+3408
-2956
No files matched your search
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solvecraft_geom::Vec3;
|
||||
use solidcraft_geom::Vec3;
|
||||
|
||||
use crate::Document;
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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,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};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
}
|
||||
|
||||
@@ -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.)));
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
|
||||
@@ -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
@@ -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
@@ -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] = §ions[..] 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] = ®ions[..] 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] = ®ions[..] 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 ®ions {
|
||||
made.push(kernel::patch_region(&ss.plane, r)?);
|
||||
}
|
||||
|
||||
@@ -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, ®ions, 0., 10.).unwrap();
|
||||
let raw = solidcraft_kernel::extrude(&solved.plane, ®ions, 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
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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, ¶meters);
|
||||
let expr = solidcraft_doc::clone_expression(&p.expr, ¶meters);
|
||||
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 } }
|
||||
|
||||
@@ -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!({}));
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, ¶ms).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);
|
||||
|
||||
@@ -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, ¶ms).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());
|
||||
|
||||
@@ -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,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};
|
||||
|
||||
@@ -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}));
|
||||
|
||||
@@ -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_};
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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())));
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)")));
|
||||
}
|
||||
|
||||
@@ -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, §ion_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, ®ions, if deboss { -d } else { d }).map_err(|e| bad(cmd, e.to_string()))?
|
||||
solidcraft_kernel::infer_cylinder_emboss(&raw.body, &plane, ®ions, 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)
|
||||
|
||||
@@ -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"}));
|
||||
|
||||
@@ -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:?}")}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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<_>>()}))
|
||||
}
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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!({}));
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()}));
|
||||
|
||||
@@ -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, ¶ms).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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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, ¶ms, &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}));
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
®ions,
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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()}))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)?)
|
||||
|
||||
@@ -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, ¶ms).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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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!({}));
|
||||
|
||||
@@ -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,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, ¶ms).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);
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, ¶ms).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,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
Reference in new issue
Block a user