forked from 1b-agent/fieldcraft-upstream
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:
13 files changed
+6290
-19
No files matched your search
Generated
+229
-1
@@ -26,6 +26,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "array-macro"
|
||||
version = "2.1.8"
|
||||
@@ -44,6 +53,15 @@ version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "branches"
|
||||
version = "0.3.0"
|
||||
@@ -131,6 +149,24 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.8"
|
||||
@@ -156,6 +192,27 @@ version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
@@ -169,6 +226,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "either"
|
||||
version = "1.19.0"
|
||||
@@ -187,6 +254,15 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
@@ -217,6 +293,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@@ -241,6 +327,12 @@ dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
@@ -265,6 +357,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@@ -352,6 +454,7 @@ name = "modelcraft-engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"modelcraft-geometry",
|
||||
"modelcraft-model",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -362,7 +465,9 @@ dependencies = [
|
||||
name = "modelcraft-geometry"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"solidcraft-doc",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-io",
|
||||
"solidcraft-kernel",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
@@ -479,6 +584,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -638,12 +752,29 @@ dependencies = [
|
||||
"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]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -656,6 +787,22 @@ version = "1.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.8.0"
|
||||
@@ -665,6 +812,22 @@ dependencies = [
|
||||
"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]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.8.0"
|
||||
@@ -682,13 +845,24 @@ dependencies = [
|
||||
"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]]
|
||||
name = "spade"
|
||||
version = "2.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.16.1",
|
||||
"num-traits",
|
||||
"robust",
|
||||
"smallvec",
|
||||
@@ -908,6 +1082,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
@@ -971,6 +1157,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
@@ -1057,8 +1253,40 @@ dependencies = [
|
||||
"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]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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",
|
||||
]
|
||||
@@ -27,6 +27,8 @@ log = "0.4"
|
||||
# (plan/adr/0001-libraries.md).
|
||||
solidcraft-geom = { 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]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ LS-PrePost, with hexahedral meshing as the centre of gravity, plus its own linea
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
+5535
-9
File diff suppressed because it is too large.
Load diff
@@ -10,6 +10,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
modelcraft-model = { workspace = true }
|
||||
modelcraft-geometry = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -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}))
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! The command registry. Each area has its own module; [`ALL`] lists every command.
|
||||
|
||||
mod geometry;
|
||||
mod mesh;
|
||||
mod model;
|
||||
|
||||
@@ -7,7 +8,16 @@ use serde_json::Value;
|
||||
|
||||
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).
|
||||
pub(crate) fn label(p: &Value, key: &str) -> Result<u32> {
|
||||
|
||||
+103
-4
@@ -9,6 +9,9 @@
|
||||
|
||||
pub mod cmd;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use modelcraft_geometry::Design;
|
||||
use modelcraft_model::Model;
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -20,6 +23,8 @@ pub enum EngineError {
|
||||
Params(String),
|
||||
#[error(transparent)]
|
||||
Model(#[from] modelcraft_model::Error),
|
||||
#[error(transparent)]
|
||||
Geometry(#[from] modelcraft_geometry::Error),
|
||||
/// A command panicked; the model is unchanged.
|
||||
#[error("internal error in {command}: {message}")]
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
pub struct Session {
|
||||
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).
|
||||
pub journal: Vec<(String, Value)>,
|
||||
}
|
||||
|
||||
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> {
|
||||
let spec = find_command(id).ok_or_else(|| EngineError::UnknownCommand(id.to_string()))?;
|
||||
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 result = match outcome {
|
||||
Ok(r) => r,
|
||||
@@ -68,10 +76,14 @@ impl Session {
|
||||
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 {
|
||||
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(_) => {}
|
||||
Err(_) => self.model = before,
|
||||
Err(_) => {
|
||||
self.model = before;
|
||||
self.design = design_before;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -91,6 +103,7 @@ impl Session {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
@@ -143,6 +156,14 @@ mod tests {
|
||||
json!({"label": 1, "kind": "nope", "nodes": [1]}),
|
||||
json!({"label": 1, "kind": "tet4", "nodes": [1, 1, 1, 1]}),
|
||||
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 p in &hostile {
|
||||
@@ -151,5 +172,83 @@ mod tests {
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,6 @@ workspace = true
|
||||
[dependencies]
|
||||
solidcraft-geom = { workspace = true }
|
||||
solidcraft-kernel = { workspace = true }
|
||||
solidcraft-doc = { workspace = true }
|
||||
solidcraft-io = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,19 @@
|
||||
//! be replaced without touching the rest of the workspace. Only this crate may depend on
|
||||
//! `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.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![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};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
@@ -38,6 +47,19 @@ impl From<KernelError> for Error {
|
||||
pub struct Shape {
|
||||
pub name: String,
|
||||
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`].
|
||||
@@ -69,10 +91,21 @@ fn xyz(v: solidcraft_geom::Vec3) -> [f64; 3] {
|
||||
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 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 {
|
||||
/// 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> {
|
||||
let m = guard("measure", || solidcraft_kernel::measure(&self.body))?;
|
||||
Ok(Measure {
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ crates in strictly lower layers.
|
||||
| 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 |
|
||||
| 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-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 |
|
||||
@@ -40,7 +40,9 @@ Rules beyond the layer order:
|
||||
## Meshing pipeline
|
||||
|
||||
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
|
||||
consistently oriented, find feature edges (face boundaries, sharp dihedral angles).
|
||||
3. **Tetrahedral meshing.** Bowyer–Watson Delaunay on Shewchuk's adaptive predicates (symbolic
|
||||
|
||||
Reference in new issue
Block a user