Open .solidcraft designs: rebuild, persistent face names, geometry commands (#4)

* geometry: open .solidcraft designs, rebuild them and keep persistent face names

open_design(bytes) reads a design (container or JSON) with solidcraft-io, rebuilds its
timeline with solidcraft-doc and returns its bodies; each Shape carries SolidCraft's
persistent face names by B-rep face index, so a face keeps its name through a parameter
edit. sample::plate_design builds a parametric test plate in code.

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

* engine: geometry.open, geometry.bodies and geometry.tessellate

The session holds the open design (shared, restored on a failed command, journalled when
it changes). Hostile-params coverage includes non-files, missing files, oversized paths and
extreme tolerances against an open design.

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

* docs: opening SolidCraft designs in the architecture and roadmap

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

* geometry: open_design_file reads through SolidCraft's bounded file access; simplify

The engine's hand-rolled capped read is replaced by open_design_file, which uses
solidcraft_io::vfs::read (refuses non-regular files before opening, works on the browser's
in-memory files). Drops the duplicate size check, simplifies the sample sketch and the
design-change test in Session::execute.

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

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 14:41:05 -04:00
1 parent 620ef6bc64
commit f36b1d9a69
13 files changed
+6290 -19

No files matched your search

Generated
+229 -1
View File
@@ -26,6 +26,15 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "arbitrary"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "array-macro" name = "array-macro"
version = "2.1.8" version = "2.1.8"
@@ -44,6 +53,15 @@ version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "branches" name = "branches"
version = "0.3.0" version = "0.3.0"
@@ -131,6 +149,24 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.8" version = "0.8.8"
@@ -156,6 +192,27 @@ version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "derive_arbitrary"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.20" version = "0.99.20"
@@ -169,6 +226,16 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.19.0" version = "1.19.0"
@@ -187,6 +254,15 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "flate2"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
dependencies = [
"zlib-rs",
]
[[package]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.2.0" version = "0.2.0"
@@ -217,6 +293,16 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.17" version = "0.2.17"
@@ -241,6 +327,12 @@ dependencies = [
"foldhash", "foldhash",
] ]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.65" version = "0.1.65"
@@ -265,6 +357,16 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.13.0"
@@ -352,6 +454,7 @@ name = "modelcraft-engine"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"log", "log",
"modelcraft-geometry",
"modelcraft-model", "modelcraft-model",
"serde", "serde",
"serde_json", "serde_json",
@@ -362,7 +465,9 @@ dependencies = [
name = "modelcraft-geometry" name = "modelcraft-geometry"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"solidcraft-doc",
"solidcraft-geom", "solidcraft-geom",
"solidcraft-io",
"solidcraft-kernel", "solidcraft-kernel",
"thiserror 2.0.21", "thiserror 2.0.21",
] ]
@@ -479,6 +584,15 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "quick-xml"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.47" version = "1.0.47"
@@ -638,12 +752,29 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "2.0.1" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "simd-adler32"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.12"
@@ -656,6 +787,22 @@ version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
[[package]]
name = "solidcraft-doc"
version = "0.8.0"
source = "git+https://github.com/bherbruck/solidcraft?rev=de1479029dd6821cffd28b70688a58e7f61f0cfd#de1479029dd6821cffd28b70688a58e7f61f0cfd"
dependencies = [
"log",
"serde",
"serde_json",
"sha2",
"solidcraft-geom",
"solidcraft-kernel",
"solidcraft-sketch",
"thiserror 2.0.21",
"web-time",
]
[[package]] [[package]]
name = "solidcraft-geom" name = "solidcraft-geom"
version = "0.8.0" version = "0.8.0"
@@ -665,6 +812,22 @@ dependencies = [
"thiserror 2.0.21", "thiserror 2.0.21",
] ]
[[package]]
name = "solidcraft-io"
version = "0.8.0"
source = "git+https://github.com/bherbruck/solidcraft?rev=de1479029dd6821cffd28b70688a58e7f61f0cfd#de1479029dd6821cffd28b70688a58e7f61f0cfd"
dependencies = [
"quick-xml",
"serde",
"serde_json",
"sha2",
"solidcraft-doc",
"solidcraft-geom",
"solidcraft-kernel",
"thiserror 2.0.21",
"zip",
]
[[package]] [[package]]
name = "solidcraft-kernel" name = "solidcraft-kernel"
version = "0.8.0" version = "0.8.0"
@@ -682,13 +845,24 @@ dependencies = [
"truck-topology", "truck-topology",
] ]
[[package]]
name = "solidcraft-sketch"
version = "0.8.0"
source = "git+https://github.com/bherbruck/solidcraft?rev=de1479029dd6821cffd28b70688a58e7f61f0cfd#de1479029dd6821cffd28b70688a58e7f61f0cfd"
dependencies = [
"serde",
"solidcraft-geom",
"thiserror 2.0.21",
"ttf-parser",
]
[[package]] [[package]]
name = "spade" name = "spade"
version = "2.15.1" version = "2.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa" checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa"
dependencies = [ dependencies = [
"hashbrown", "hashbrown 0.16.1",
"num-traits", "num-traits",
"robust", "robust",
"smallvec", "smallvec",
@@ -908,6 +1082,18 @@ dependencies = [
"truck-geotrait", "truck-geotrait",
] ]
[[package]]
name = "ttf-parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.27" version = "1.0.27"
@@ -971,6 +1157,16 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.62.2" version = "0.62.2"
@@ -1057,8 +1253,40 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "zip"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
dependencies = [
"arbitrary",
"crc32fast",
"flate2",
"indexmap",
"memchr",
"zopfli",
]
[[package]]
name = "zlib-rs"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.23" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]]
name = "zopfli"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
+2
View File
@@ -27,6 +27,8 @@ log = "0.4"
# (plan/adr/0001-libraries.md). # (plan/adr/0001-libraries.md).
solidcraft-geom = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" } solidcraft-geom = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
solidcraft-kernel = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" } solidcraft-kernel = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
solidcraft-doc = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
solidcraft-io = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
[workspace.lints.rust] [workspace.lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"
+1 -1
View File
@@ -6,7 +6,7 @@ LS-PrePost, with hexahedral meshing as the centre of gravity, plus its own linea
| Milestone | Scope | Status | | Milestone | Scope | Status |
|---|---|---| |---|---|---|
| M0 Foundation | Workspace, quality gates (`cargo xtask ci`), FE model, command engine with rollback and panic guard, CLI, geometry boundary on SolidCraft's kernel and tessellator | done | | M0 Foundation | Workspace, quality gates (`cargo xtask ci`), FE model, command engine with rollback and panic guard, CLI, geometry boundary on SolidCraft's kernel and tessellator | done |
| M1 Surface preparation | Robust adaptive-precision predicates; weld, closed/manifold/orientation checks and feature edges on the tessellated boundary; mesh quality metrics | next | | M1 Surface preparation | Geometry in: open `.solidcraft` designs, rebuilt by SolidCraft, with persistent face names (done); robust adaptive-precision predicates; weld, closed/manifold/orientation checks and feature edges on the tessellated boundary; mesh quality metrics | next |
| M2 Tetrahedral meshing | Bowyer–Watson Delaunay, robust constrained Delaunay boundary recovery (Diazzi et al. 2023), Delaunay refinement to a size field, ODT smoothing, Klingner–Shewchuk sliver removal; linear and quadratic tets | planned | | M2 Tetrahedral meshing | Bowyer–Watson Delaunay, robust constrained Delaunay boundary recovery (Diazzi et al. 2023), Delaunay refinement to a size field, ODT smoothing, Klingner–Shewchuk sliver removal; linear and quadratic tets | planned |
| M3 Hexahedral meshing (core) | Octree/grid dual all-hex (valid on any input), then hex-dominant by tet recombination, then polycube for blocky parts; see docs/meshing/literature.md | planned | | M3 Hexahedral meshing (core) | Octree/grid dual all-hex (valid on any input), then hex-dominant by tet recombination, then polycube for blocky parts; see docs/meshing/literature.md | planned |
| M4 Model definition | Materials, sections, sets and surfaces, boundary conditions, loads, steps, output requests | planned | | M4 Model definition | Materials, sections, sets and surfaces, boundary conditions, loads, steps, output requests | planned |
+5535 -9
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -10,6 +10,7 @@ workspace = true
[dependencies] [dependencies]
modelcraft-model = { workspace = true } modelcraft-model = { workspace = true }
modelcraft-geometry = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
+79
View File
@@ -0,0 +1,79 @@
use std::sync::Arc;
use modelcraft_geometry::{Shape, open_design_file};
use serde_json::{Value, json};
use crate::{CommandSpec, EngineError, Result, Session};
pub const OPEN: CommandSpec = CommandSpec {
id: "geometry.open",
label: "Open Design",
params: r#"{"path": "part.solidcraft"} — rebuilds the SolidCraft design and makes its bodies the geometry; opening it again after an edit picks up the change"#,
run: open,
};
pub const BODIES: CommandSpec = CommandSpec {
id: "geometry.bodies",
label: "Bodies",
params: "{} — the open design's bodies: measures and persistent face names (by face index)",
run: bodies,
};
pub const TESSELLATE: CommandSpec = CommandSpec {
id: "geometry.tessellate",
label: "Tessellate Body",
params: r#"{"body": "Body1", "tolerance": 0.05} — triangulates the body's boundary within the chordal tolerance (mm); reports triangles per face"#,
run: tessellate,
};
fn open(s: &mut Session, p: &Value) -> Result<Value> {
let path = p
.get("path")
.and_then(Value::as_str)
.filter(|v| !v.is_empty())
.ok_or_else(|| EngineError::Params("`path` must be the design file's path".into()))?;
let design = open_design_file(path)?;
let out = json!({
"name": design.name,
"bodies": design.bodies.iter().map(|b| b.name.as_str()).collect::<Vec<_>>(),
"errors": design.errors.iter().map(|e| json!({"feature": e.feature, "message": e.message})).collect::<Vec<_>>(),
});
s.design = Some(Arc::new(design));
Ok(out)
}
fn bodies(s: &mut Session, _: &Value) -> Result<Value> {
let Some(design) = &s.design else { return Ok(json!([])) };
let mut out = Vec::with_capacity(design.bodies.len());
for b in &design.bodies {
let m = b.measure()?;
out.push(json!({
"name": b.name,
"volume": m.volume,
"area": m.area,
"centroid": m.centroid,
"bbox": [m.bbox_min, m.bbox_max],
"faces": b.faces().iter().map(|f| json!({"name": f.name, "exact": f.exact})).collect::<Vec<_>>(),
}));
}
Ok(Value::Array(out))
}
fn body<'a>(s: &'a Session, p: &Value) -> Result<&'a Shape> {
let name = p.get("body").and_then(Value::as_str).ok_or_else(|| EngineError::Params("`body` must be a body name".into()))?;
s.design.as_deref().and_then(|d| d.body(name)).ok_or_else(|| EngineError::Params(format!("no body `{name}` (open a design first)")))
}
fn tessellate(s: &mut Session, p: &Value) -> Result<Value> {
let shape = body(s, p)?;
let tolerance = p.get("tolerance").and_then(Value::as_f64).ok_or_else(|| EngineError::Params("`tolerance` must be a number (mm)".into()))?;
let mesh = shape.tessellate(tolerance)?;
let mut per_face = vec![0usize; shape.faces().len()];
for &f in &mesh.tri_face {
if let Some(n) = per_face.get_mut(f as usize) {
*n += 1;
}
}
let faces: Vec<Value> = shape.faces().iter().zip(per_face).map(|(f, n)| json!({"name": f.name, "triangles": n})).collect();
Ok(json!({"body": shape.name, "vertices": mesh.positions.len(), "triangles": mesh.triangles.len(), "faces": faces}))
}
+11 -1
View File
@@ -1,5 +1,6 @@
//! The command registry. Each area has its own module; [`ALL`] lists every command. //! The command registry. Each area has its own module; [`ALL`] lists every command.
mod geometry;
mod mesh; mod mesh;
mod model; mod model;
@@ -7,7 +8,16 @@ use serde_json::Value;
use crate::{CommandSpec, EngineError, Result}; use crate::{CommandSpec, EngineError, Result};
pub static ALL: &[CommandSpec] = &[model::INFO, mesh::NODE_ADD, mesh::NODE_REMOVE, mesh::ELEMENT_ADD, mesh::ELEMENT_REMOVE]; pub static ALL: &[CommandSpec] = &[
model::INFO,
geometry::OPEN,
geometry::BODIES,
geometry::TESSELLATE,
mesh::NODE_ADD,
mesh::NODE_REMOVE,
mesh::ELEMENT_ADD,
mesh::ELEMENT_REMOVE,
];
/// A required label parameter (1..=u32::MAX; the model checks its own range). /// A required label parameter (1..=u32::MAX; the model checks its own range).
pub(crate) fn label(p: &Value, key: &str) -> Result<u32> { pub(crate) fn label(p: &Value, key: &str) -> Result<u32> {
+103 -4
View File
@@ -9,6 +9,9 @@
pub mod cmd; pub mod cmd;
use std::sync::Arc;
use modelcraft_geometry::Design;
use modelcraft_model::Model; use modelcraft_model::Model;
use serde_json::Value; use serde_json::Value;
@@ -20,6 +23,8 @@ pub enum EngineError {
Params(String), Params(String),
#[error(transparent)] #[error(transparent)]
Model(#[from] modelcraft_model::Error), Model(#[from] modelcraft_model::Error),
#[error(transparent)]
Geometry(#[from] modelcraft_geometry::Error),
/// A command panicked; the model is unchanged. /// A command panicked; the model is unchanged.
#[error("internal error in {command}: {message}")] #[error("internal error in {command}: {message}")]
Internal { command: String, message: String }, Internal { command: String, message: String },
@@ -46,19 +51,22 @@ pub fn find_command(id: &str) -> Option<&'static CommandSpec> {
commands().iter().find(|c| c.id == id) commands().iter().find(|c| c.id == id)
} }
/// The model being edited and the commands that built it. /// The model being edited, the geometry it is built on and the commands that built it.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Session { pub struct Session {
pub model: Model, pub model: Model,
/// The open SolidCraft design (`geometry.open`); shared, so a snapshot costs nothing.
pub design: Option<Arc<Design>>,
/// Commands that changed the model, in order (a script that rebuilds it). /// Commands that changed the model, in order (a script that rebuilds it).
pub journal: Vec<(String, Value)>, pub journal: Vec<(String, Value)>,
} }
impl Session { impl Session {
/// Run a command. On an error or a panic the model is left as it was. /// Run a command. On an error or a panic the model and the design are left as they were.
pub fn execute(&mut self, id: &str, params: &Value) -> Result<Value> { pub fn execute(&mut self, id: &str, params: &Value) -> Result<Value> {
let spec = find_command(id).ok_or_else(|| EngineError::UnknownCommand(id.to_string()))?; let spec = find_command(id).ok_or_else(|| EngineError::UnknownCommand(id.to_string()))?;
let before = self.model.clone(); let before = self.model.clone();
let design_before = self.design.clone();
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (spec.run)(self, params))); let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (spec.run)(self, params)));
let result = match outcome { let result = match outcome {
Ok(r) => r, Ok(r) => r,
@@ -68,10 +76,14 @@ impl Session {
Err(EngineError::Internal { command: id.to_string(), message }) Err(EngineError::Internal { command: id.to_string(), message })
} }
}; };
let design_changed = self.design.as_ref().map(Arc::as_ptr) != design_before.as_ref().map(Arc::as_ptr);
match &result { match &result {
Ok(_) if self.model != before => self.journal.push((id.to_string(), params.clone())), Ok(_) if self.model != before || design_changed => self.journal.push((id.to_string(), params.clone())),
Ok(_) => {} Ok(_) => {}
Err(_) => self.model = before, Err(_) => {
self.model = before;
self.design = design_before;
}
} }
result result
} }
@@ -91,6 +103,7 @@ impl Session {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use modelcraft_geometry::sample::{Plate, plate_design};
use serde_json::json; use serde_json::json;
#[test] #[test]
@@ -143,6 +156,14 @@ mod tests {
json!({"label": 1, "kind": "nope", "nodes": [1]}), json!({"label": 1, "kind": "nope", "nodes": [1]}),
json!({"label": 1, "kind": "tet4", "nodes": [1, 1, 1, 1]}), json!({"label": 1, "kind": "tet4", "nodes": [1, 1, 1, 1]}),
json!({"label": 1, "kind": "hex20", "nodes": vec![u32::MAX; 20]}), json!({"label": 1, "kind": "hex20", "nodes": vec![u32::MAX; 20]}),
json!({"path": ""}),
json!({"path": "/"}),
json!({"path": "/dev/zero"}),
json!({"path": "/no/such/design.solidcraft"}),
json!({"path": "x".repeat(100_000)}),
json!({"path": 7}),
json!({"body": "Body1", "tolerance": 0.05}),
json!({"body": null, "tolerance": "fine"}),
]; ];
for spec in commands() { for spec in commands() {
for p in &hostile { for p in &hostile {
@@ -151,5 +172,83 @@ mod tests {
} }
} }
assert!(Session::default().execute("no.such", &json!({})).is_err()); assert!(Session::default().execute("no.such", &json!({})).is_err());
// The same against an open design.
let (_dir, path) = sample_design_file(&Plate::default());
let mut opened = Session::default();
opened.execute("geometry.open", &json!({"path": path})).unwrap();
let tess = [
json!({"body": "Body1"}),
json!({"body": "Body1", "tolerance": 0}),
json!({"body": "Body1", "tolerance": -1}),
json!({"body": "Body1", "tolerance": 1e-300}),
json!({"body": "Body1", "tolerance": 1e300}),
json!({"body": "Body2", "tolerance": 0.1}),
];
for p in tess.iter().chain(&hostile) {
for spec in commands() {
let mut s = opened.clone();
let _ = s.execute(spec.id, p);
}
}
}
/// A design file in a fresh temporary directory (removed when the guard drops).
fn sample_design_file(plate: &Plate) -> (TempDir, String) {
let dir = TempDir::new();
let path = dir.0.join("plate.solidcraft");
std::fs::write(&path, plate_design(plate).unwrap()).unwrap();
(dir, path.to_string_lossy().into_owned())
}
struct TempDir(std::path::PathBuf);
impl TempDir {
fn new() -> Self {
static N: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("modelcraft-engine-{}-{n}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
TempDir(dir)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn open_a_design_list_its_bodies_and_tessellate_one() {
let (_dir, path) = sample_design_file(&Plate::default());
let mut s = Session::default();
let opened = s.execute("geometry.open", &json!({"path": path})).unwrap();
assert_eq!(opened["bodies"], json!(["Body1"]));
assert_eq!(opened["errors"], json!([]));
assert_eq!(s.journal.len(), 1);
let bodies = s.execute("geometry.bodies", &json!({})).unwrap();
let volume = bodies[0]["volume"].as_f64().unwrap();
let expect = (40.0 * 30.0 - std::f64::consts::PI * 25.0) * 10.0;
assert!((volume - expect).abs() < 1e-3 * expect, "{volume}");
let faces = bodies[0]["faces"].as_array().unwrap();
assert_eq!(faces.len(), 8);
let t = s.execute("geometry.tessellate", &json!({"body": "Body1", "tolerance": 0.05})).unwrap();
let per_face: Vec<u64> = t["faces"].as_array().unwrap().iter().map(|f| f["triangles"].as_u64().unwrap()).collect();
assert!(per_face.iter().all(|&n| n > 0), "{t}");
assert_eq!(per_face.iter().sum::<u64>(), t["triangles"].as_u64().unwrap());
// A failed open keeps the design that was open.
assert!(s.execute("geometry.open", &json!({"path": "/no/such/design.solidcraft"})).is_err());
assert!(s.design.is_some());
assert_eq!(s.journal.len(), 1);
// Re-opening the edited design rebuilds it with the same face names.
std::fs::write(&path, plate_design(&Plate { width: 60.0, ..Plate::default() }).unwrap()).unwrap();
s.execute("geometry.open", &json!({"path": path})).unwrap();
let wider = s.execute("geometry.bodies", &json!({})).unwrap();
assert!(wider[0]["volume"].as_f64().unwrap() > volume);
assert_eq!(wider[0]["faces"], bodies[0]["faces"]);
assert_eq!(s.journal.len(), 2);
} }
} }
+2
View File
@@ -11,4 +11,6 @@ workspace = true
[dependencies] [dependencies]
solidcraft-geom = { workspace = true } solidcraft-geom = { workspace = true }
solidcraft-kernel = { workspace = true } solidcraft-kernel = { workspace = true }
solidcraft-doc = { workspace = true }
solidcraft-io = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
+170
View File
@@ -0,0 +1,170 @@
//! Opening SolidCraft designs (`.solidcraft`): read the design, rebuild its parametric timeline
//! with SolidCraft, and hand back its bodies with persistent face names.
//!
//! The design is rebuilt from its authoritative inputs (sketches, features, parameters); cached
//! display meshes in the file are never used. [`open_design`] takes bytes; [`open_design_file`]
//! reads a path through SolidCraft's file access, which in the browser is the in-memory files the
//! web host loaded.
use solidcraft_doc::{Document, Model, naming};
use solidcraft_kernel::{KernelError, guard};
use crate::{Error, Face, Result, Shape};
/// Largest design file opened (SolidCraft's own limit).
pub const MAX_DESIGN_BYTES: usize = solidcraft_io::MAX_DESIGN_BYTES;
/// A rebuilt design.
#[derive(Debug, Clone)]
pub struct Design {
pub name: String,
/// The bodies at the end of the timeline, in the design's order.
pub bodies: Vec<Shape>,
/// Features that failed to rebuild. The design still opens with the bodies the rest made,
/// as it does in SolidCraft.
pub errors: Vec<FeatureError>,
}
/// A feature of the design that failed to rebuild.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FeatureError {
pub feature: String,
pub message: String,
}
impl Design {
pub fn body(&self, name: &str) -> Option<&Shape> {
self.bodies.iter().find(|b| b.name == name)
}
}
/// Open a design from its file bytes (a `.solidcraft` container or a JSON design) and rebuild it.
pub fn open_design(bytes: &[u8]) -> Result<Design> {
let doc = guard("read design", || solidcraft_io::read_design(bytes).map_err(|e| KernelError::Invalid(format!("not a readable design: {e}"))))?;
rebuild(&doc)
}
/// Read a design file (a regular file of at most [`MAX_DESIGN_BYTES`]) and rebuild it.
pub fn open_design_file(path: &str) -> Result<Design> {
let bytes = solidcraft_io::vfs::read(path, MAX_DESIGN_BYTES).map_err(|e| Error::Invalid(format!("cannot read `{path}`: {e}")))?;
open_design(&bytes)
}
/// Rebuild an in-memory document (the design half of [`open_design`]).
pub(crate) fn rebuild(doc: &Document) -> Result<Design> {
let model = guard("rebuild design", || {
let mut m = Model::new();
m.evaluate(doc);
Ok(m)
})?;
let errors = model
.results
.iter()
.filter(|r| !r.skipped)
.filter_map(|r| r.error.as_ref().map(|e| FeatureError { feature: r.name.clone(), message: e.clone() }))
.collect();
let state = model.state();
let mut bodies = Vec::with_capacity(state.bodies.len());
for b in &state.bodies {
let faces = guard("name faces", || {
let names = naming::face_names(b);
Ok(names.iter().enumerate().map(|(i, name)| Face { name: name.clone(), exact: naming::face_name_exact(b, i) }).collect())
})?;
bodies.push(Shape { name: b.name.clone(), body: b.body.clone(), faces });
}
Ok(Design { name: doc.name.clone(), bodies, errors })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sample::{self, Plate};
fn face_areas(shape: &Shape) -> Vec<f64> {
let mesh = shape.tessellate(0.01).unwrap();
let mut areas = vec![0.0; shape.faces().len()];
for (t, f) in mesh.triangles.iter().zip(&mesh.tri_face) {
let [a, b, c] = t.map(|i| {
let [x, y, z] = mesh.positions[i as usize];
solidcraft_geom::Vec3 { x, y, z }
});
areas[*f as usize] += 0.5 * (b - a).cross(c - a).len();
}
areas
}
#[test]
fn opens_rebuilds_and_tessellates_a_design() {
let plate = Plate::default();
let design = open_design(&sample::plate_design(&plate).unwrap()).unwrap();
assert!(design.errors.is_empty(), "{:?}", design.errors);
assert_eq!(design.bodies.len(), 1);
let body = &design.bodies[0];
let m = body.measure().unwrap();
let r = plate.hole_diameter / 2.0;
let expect = (plate.width * plate.depth - std::f64::consts::PI * r * r) * plate.thickness;
assert!((m.volume - expect).abs() < 1e-3 * expect, "{} vs {expect}", m.volume);
// Four sides, top, bottom and the hole (a cylinder is two half faces).
assert_eq!(m.faces, 8);
assert_eq!(body.faces().len(), 8);
let mesh = body.tessellate(0.01).unwrap();
let mut tags = mesh.tri_face.clone();
tags.sort_unstable();
tags.dedup();
assert_eq!(tags.len(), 8);
assert!(tags.iter().all(|&t| (t as usize) < body.faces().len()));
// Every face has its own name.
let mut names: Vec<&str> = body.faces().iter().map(|f| f.name.as_str()).collect();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), 8, "{names:?}");
}
#[test]
fn face_names_survive_a_parameter_change() {
let narrow = Plate::default();
let wide = Plate { width: 70.0, thickness: 15.0, ..Plate::default() };
let a = open_design(&sample::plate_design(&narrow).unwrap()).unwrap();
let b = open_design(&sample::plate_design(&wide).unwrap()).unwrap();
let (a, b) = (&a.bodies[0], &b.bodies[0]);
let (area_a, area_b) = (face_areas(a), face_areas(b));
let pi = std::f64::consts::PI;
// Each half of the hole.
let hole = |p: &Plate| pi * p.hole_diameter * p.thickness / 2.0;
let cap = |p: &Plate| p.width * p.depth - pi * p.hole_diameter * p.hole_diameter / 4.0;
for face in a.faces() {
let i = a.face_index(&face.name).unwrap() as usize;
let j = b.face_index(&face.name).unwrap_or_else(|| panic!("{} is gone after the edit", face.name)) as usize;
// The same face by name: the hole stays the hole, a cap stays a cap, and a side keeps
// its own dimension (depth sides grow with the thickness only, width sides with both).
let (x, y) = (area_a[i], area_b[j]);
let close = |v: f64, e: f64| (v - e).abs() < 0.02 * e;
let kind = if close(x, hole(&narrow)) {
assert!(close(y, hole(&wide)), "{}: {y}", face.name);
"hole"
} else if close(x, cap(&narrow)) {
assert!(close(y, cap(&wide)), "{}: {y}", face.name);
"cap"
} else if close(x, narrow.width * narrow.thickness) {
assert!(close(y, wide.width * wide.thickness), "{}: {y}", face.name);
"width side"
} else {
assert!(close(x, narrow.depth * narrow.thickness), "{}: {x}", face.name);
assert!(close(y, wide.depth * wide.thickness), "{}: {y}", face.name);
"depth side"
};
assert!(face.exact, "{} ({kind}) is named from geometry", face.name);
}
}
#[test]
fn hostile_bytes_are_an_error() {
for bytes in [&b""[..], b"PK\x03\x04", b"{", b"{}", b"\xff\xfe\x00", b"[1,2,3]", br#"{"format":"solidcraft/9999","name":"x"}"#] {
assert!(open_design(bytes).is_err(), "{bytes:?}");
}
assert!(open_design(&vec![b' '; MAX_DESIGN_BYTES + 1]).is_err());
for path in ["", "/", "/dev/zero", "/no/such/design.solidcraft"] {
assert!(open_design_file(path).is_err(), "{path}");
}
}
}
+34 -1
View File
@@ -8,10 +8,19 @@
//! be replaced without touching the rest of the workspace. Only this crate may depend on //! be replaced without touching the rest of the workspace. Only this crate may depend on
//! `solidcraft-*` (`cargo xtask layers` enforces it). //! `solidcraft-*` (`cargo xtask layers` enforces it).
//! //!
//! Geometry enters two ways: shapes built in code (such as [`block`]) and SolidCraft designs
//! opened with [`open_design`], which rebuilds the design's parametric timeline and gives its
//! bodies with persistent face names (a boundary condition on a named face follows that face
//! through an edit of the design).
//!
//! Lengths are millimetres. //! Lengths are millimetres.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
mod design;
pub mod sample;
pub use design::{Design, FeatureError, MAX_DESIGN_BYTES, open_design, open_design_file};
use solidcraft_kernel::{Body, KernelError, guard}; use solidcraft_kernel::{Body, KernelError, guard};
#[derive(Debug, Clone, PartialEq, thiserror::Error)] #[derive(Debug, Clone, PartialEq, thiserror::Error)]
@@ -38,6 +47,19 @@ impl From<KernelError> for Error {
pub struct Shape { pub struct Shape {
pub name: String, pub name: String,
body: Body, body: Body,
/// By B-rep face index (the [`SurfaceMesh::tri_face`] tags); empty for shapes built in code.
faces: Vec<Face>,
}
/// A B-rep face's persistent identity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Face {
/// Stable across rebuilds of the design: named after the feature and sketch curve that made
/// the face (`F2:side:l4`, `F2:top`, …), not after its position in the body.
pub name: String,
/// The name follows the history SolidCraft recorded; `false` when it was worked out from the
/// geometry, which an upstream edit can fool.
pub exact: bool,
} }
/// Mass properties and extent of a [`Shape`]. /// Mass properties and extent of a [`Shape`].
@@ -69,10 +91,21 @@ fn xyz(v: solidcraft_geom::Vec3) -> [f64; 3] {
pub fn block(a: [f64; 3], b: [f64; 3]) -> Result<Shape> { pub fn block(a: [f64; 3], b: [f64; 3]) -> Result<Shape> {
let v = |p: [f64; 3]| solidcraft_geom::Vec3 { x: p[0], y: p[1], z: p[2] }; let v = |p: [f64; 3]| solidcraft_geom::Vec3 { x: p[0], y: p[1], z: p[2] };
let body = guard("block", || solidcraft_kernel::box_solid(v(a), v(b)))?; let body = guard("block", || solidcraft_kernel::box_solid(v(a), v(b)))?;
Ok(Shape { name: "Block".into(), body }) Ok(Shape { name: "Block".into(), body, faces: Vec::new() })
} }
impl Shape { impl Shape {
/// The persistent identities of the faces, by B-rep face index (empty for shapes built in code).
pub fn faces(&self) -> &[Face] {
&self.faces
}
/// The index of the face with this persistent name: the [`SurfaceMesh::tri_face`] tag its
/// triangles carry.
pub fn face_index(&self, name: &str) -> Option<u32> {
self.faces.iter().position(|f| f.name == name).and_then(|i| u32::try_from(i).ok())
}
pub fn measure(&self) -> Result<Measure> { pub fn measure(&self) -> Result<Measure> {
let m = guard("measure", || solidcraft_kernel::measure(&self.body))?; let m = guard("measure", || solidcraft_kernel::measure(&self.body))?;
Ok(Measure { Ok(Measure {
+119
View File
@@ -0,0 +1,119 @@
//! Sample SolidCraft designs built in code: test parts whose recipe is this source, so no design
//! file is committed (docs/adr/0001-libraries.md, test geometry).
use solidcraft_doc::sketch::{ConstraintKind, Sketch};
use solidcraft_doc::{Direction, Document, Extent, FeatureKind, Operation, PlaneRef, ProfileSel};
use solidcraft_geom::Vec2;
use crate::{Error, Result};
/// A rectangular plate with a round through hole, as a parametric design: the sketch's width is
/// driven by the design parameter `width` and the extrusion by `thickness`, so editing either
/// rebuilds the same faces at a new size.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Plate {
/// Along X, mm. At least `depth`.
pub width: f64,
/// Along Y, mm.
pub depth: f64,
/// Along Z, mm.
pub thickness: f64,
/// The hole is centred `depth / 2` from the corner at the origin, so it needs to be smaller
/// than `depth`.
pub hole_diameter: f64,
}
impl Default for Plate {
fn default() -> Self {
Plate { width: 40.0, depth: 30.0, thickness: 10.0, hole_diameter: 10.0 }
}
}
fn failed(e: impl std::fmt::Display) -> Error {
Error::Failed(format!("sample design: {e}"))
}
fn extrude(sketch: u64, distance: &str, operation: Operation) -> FeatureKind {
FeatureKind::Extrude {
sketch,
profiles: ProfileSel::All,
extent: Extent {
distance: distance.into(),
direction: Direction::Positive,
distance2: None,
start_offset: None,
through_all: false,
taper: None,
to: None,
to_offset: None,
from: None,
},
operation,
targets: vec![],
}
}
fn sketch_on_xy(doc: &mut Document, sketch: Sketch, name: &str) -> Result<u64> {
doc.add_feature(FeatureKind::Sketch { plane: PlaneRef::Origin { name: "XY".into() }, sketch }, Some(name)).map_err(failed)
}
/// The plate as `.solidcraft` file bytes.
pub fn plate_design(p: &Plate) -> Result<Vec<u8>> {
let ok = |v: f64| v.is_finite() && v > 0.0 && v <= 1e6;
if !(ok(p.width) && ok(p.depth) && ok(p.thickness) && ok(p.hole_diameter)) || p.width < p.depth || p.hole_diameter >= p.depth {
return Err(Error::Invalid(format!("plate dimensions out of range: {p:?}")));
}
let mut doc = Document::new("Plate");
doc.set_param("width", &format!("{} mm", p.width), Some("mm"), None).map_err(failed)?;
doc.set_param("thickness", &format!("{} mm", p.thickness), Some("mm"), None).map_err(failed)?;
let mut sk = Sketch::new();
let corners = [Vec2::new(0.0, 0.0), Vec2::new(p.width, 0.0), Vec2::new(p.width, p.depth), Vec2::new(0.0, p.depth)];
let [p0, p1, p2, p3] = corners.map(|c| sk.add_point(c, None));
let (p0, p1, p2, p3) = (p0.map_err(failed)?, p1.map_err(failed)?, p2.map_err(failed)?, p3.map_err(failed)?);
let l0 = sk.add_line_pts(p0, p1, None).map_err(failed)?;
let l1 = sk.add_line_pts(p1, p2, None).map_err(failed)?;
let l2 = sk.add_line_pts(p2, p3, None).map_err(failed)?;
let l3 = sk.add_line_pts(p3, p0, None).map_err(failed)?;
use ConstraintKind::{Coincident, Horizontal, Vertical};
// Point 0 of a sketch is its origin.
for c in [Coincident { p: p0, q: 0 }, Horizontal { l: l0 }, Horizontal { l: l2 }, Vertical { l: l1 }, Vertical { l: l3 }] {
sk.add_constraint(c, None).map_err(failed)?;
}
let width = doc.new_model_param("width", "mm");
sk.add_constraint(ConstraintKind::Length { l: l0, value: p.width }, Some(width)).map_err(failed)?;
let depth = doc.new_model_param(&format!("{} mm", p.depth), "mm");
sk.add_constraint(ConstraintKind::Length { l: l1, value: p.depth }, Some(depth)).map_err(failed)?;
let outline = sketch_on_xy(&mut doc, sk, "Outline")?;
doc.add_feature(extrude(outline, "thickness", Operation::NewBody), Some("Plate")).map_err(failed)?;
let mut sk = Sketch::new();
let c = p.depth / 2.0;
sk.add_circle(Vec2::new(c, c), p.hole_diameter / 2.0, None, None).map_err(failed)?;
let hole = sketch_on_xy(&mut doc, sk, "Hole sketch")?;
// Past the top face, so the cut never ends on it.
doc.add_feature(extrude(hole, "thickness + 1 mm", Operation::Cut), Some("Hole")).map_err(failed)?;
solidcraft_io::write_design_container(&doc).map_err(failed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hostile_dimensions_are_an_error() {
let base = Plate::default();
for p in [
Plate { width: f64::NAN, ..base },
Plate { width: -1.0, ..base },
Plate { thickness: 0.0, ..base },
Plate { thickness: f64::INFINITY, ..base },
Plate { width: 10.0, ..base },
Plate { hole_diameter: 30.0, ..base },
Plate { depth: 1e300, width: 1e300, ..base },
] {
assert!(plate_design(&p).is_err(), "{p:?}");
}
}
}
+4 -2
View File
@@ -19,7 +19,7 @@ crates in strictly lower layers.
| Layer | Crate | Role | Status | | Layer | Crate | Role | Status |
|---|---|---|---| |---|---|---|---|
| L0 | `modelcraft-model` | Solver-neutral FE model: nodes, elements by label; materials, sections, sets, BCs, loads and steps as they land. Knows no file format. | started | | L0 | `modelcraft-model` | Solver-neutral FE model: nodes, elements by label; materials, sections, sets, BCs, loads and steps as they land. Knows no file format. | started |
| L1 | `modelcraft-geometry` | The only door to SolidCraft: B-rep bodies, measures, tessellation into a face-tagged `SurfaceMesh`. No SolidCraft type in its API. | started | | L1 | `modelcraft-geometry` | The only door to SolidCraft: opens `.solidcraft` designs (rebuilt from their timeline, bodies with persistent face names), B-rep bodies, measures, tessellation into a face-tagged `SurfaceMesh`. No SolidCraft type in its API. | started |
| L1 | `modelcraft-mesh` | Surface preparation (weld, watertightness, feature edges), robust predicates, Delaunay tetrahedralization and refinement, Voronoi-based optimization, hex meshing, quality metrics. | started | | L1 | `modelcraft-mesh` | Surface preparation (weld, watertightness, feature edges), robust predicates, Delaunay tetrahedralization and refinement, Voronoi-based optimization, hex meshing, quality metrics. | started |
| L1 | `modelcraft-solver` | Element formulations, assembly, sparse linear static and modal solution. | planned | | L1 | `modelcraft-solver` | Element formulations, assembly, sparse linear static and modal solution. | planned |
| L1 | `modelcraft-decks` | Abaqus input, Nastran bulk data and LS-DYNA keyword readers and writers, to and from `model`. | planned | | L1 | `modelcraft-decks` | Abaqus input, Nastran bulk data and LS-DYNA keyword readers and writers, to and from `model`. | planned |
@@ -40,7 +40,9 @@ Rules beyond the layer order:
## Meshing pipeline ## Meshing pipeline
1. **Boundary surface.** SolidCraft tessellates each body; every triangle carries its B-rep face, 1. **Boundary surface.** SolidCraft tessellates each body; every triangle carries its B-rep face,
so feature edges and face-based loads and BCs survive meshing. so feature edges and face-based loads and BCs survive meshing. Faces of an opened design also
carry SolidCraft's persistent names (`F2:side:l4`, from the feature and sketch curve that made
them), so a load on a named face follows it when the design is edited and opened again.
2. **Surface preparation.** Weld coincident vertices, check the surface is closed, manifold and 2. **Surface preparation.** Weld coincident vertices, check the surface is closed, manifold and
consistently oriented, find feature edges (face boundaries, sharp dihedral angles). consistently oriented, find feature edges (face boundaries, sharp dihedral angles).
3. **Tetrahedral meshing.** Bowyer–Watson Delaunay on Shewchuk's adaptive predicates (symbolic 3. **Tetrahedral meshing.** Bowyer–Watson Delaunay on Shewchuk's adaptive predicates (symbolic