IGES reader checked against Fusion's IGES exports: 75 of 76 reference parts read back exactly; cone tips read whole
`cargo xtask step-corpus --iges` (and `solvecraft-cli step-corpus --iges`) opens every oracle part.igs as File → Open does and compares body count, volume, area and face count with Fusion's measurements, writing docs/iges-import.md. The run found faces closing at an apex (a drill point's cone, a sphere's pole: the seam runs out to the apex and straight back) kept whole and meshed wrongly; they are now split along the iso-line opposite the seam like bands are (18-hole-features and 40-box-sphere-torus pass). The one failure left, 50-sweep-tight-bend, is written by Fusion as five loose, partly degenerate surfaces that Fusion cannot read back as a solid either; documented in the report. Test: a drilled blind hole round-trips through IGES. Fixes #14 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
This commit is contained in:
1 parent
72bc6fb010
commit
a7fe0429f0
6 files changed
+214
-18
No files matched your search
+31
-13
@@ -23,9 +23,10 @@ commands:
|
||||
xtask/data/fusion-catalog.tsv from plan/fusion/menu-tree.json)
|
||||
oracle replay plan/fusion/oracle/*/recipe.json and compare with measure.json;
|
||||
writes docs/oracle.md
|
||||
step-corpus [DIR]
|
||||
step-corpus [--iges] [DIR]
|
||||
import every DIR/*/part.step (default plan/fusion/oracle) and compare
|
||||
with measure.json; writes docs/step-import.md
|
||||
with measure.json; writes docs/step-import.md (--iges: part.igs,
|
||||
docs/iges-import.md)
|
||||
book [--no-build]
|
||||
generate docs/book's command reference and screenshots, then mdbook build
|
||||
into target/book
|
||||
@@ -44,7 +45,7 @@ fn main() -> ExitCode {
|
||||
Some("assets") => assets::run(&root()),
|
||||
Some("parity") => parity::run(&root(), rest.contains(&"--refresh")),
|
||||
Some("oracle") => cmd_oracle(),
|
||||
Some("step-corpus") => cmd_step_corpus(rest.first().copied()),
|
||||
Some("step-corpus") => cmd_step_corpus(rest.iter().find(|a| !a.starts_with("--")).copied(), rest.contains(&"--iges")),
|
||||
Some("book") => book::run(&root(), !rest.contains(&"--no-build")),
|
||||
Some("licences") => licences::run(&root(), rest.contains(&"--check")),
|
||||
Some("ci") => cmd_ci(),
|
||||
@@ -216,20 +217,24 @@ fn cmd_wasm() -> Result<(), String> {
|
||||
|
||||
/// Import every oracle STEP file (exported by Fusion) and compare with Fusion's measurements;
|
||||
/// write docs/step-import.md.
|
||||
fn cmd_step_corpus(dir: Option<&str>) -> Result<(), String> {
|
||||
fn cmd_step_corpus(dir: Option<&str>, iges: bool) -> Result<(), String> {
|
||||
let dir = dir.map(PathBuf::from).unwrap_or_else(|| root().join("plan/fusion/oracle"));
|
||||
let file = if iges { "part.igs" } else { "part.step" };
|
||||
let mut cases: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
.map_err(|e| format!("{}: {e} (the oracle data is local, in plan/fusion/oracle)", dir.display()))?
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.join("part.step").is_file() && p.join("measure.json").is_file())
|
||||
.filter(|p| p.join(file).is_file() && p.join("measure.json").is_file())
|
||||
.collect();
|
||||
cases.sort();
|
||||
if cases.is_empty() {
|
||||
return Err(format!("no cases with part.step and measure.json in {}", dir.display()));
|
||||
return Err(format!("no cases with {file} and measure.json in {}", dir.display()));
|
||||
}
|
||||
let mut cmd = cargo();
|
||||
cmd.args(["run", "-q", "--release", "-p", "solvecraft-cli", "--", "step-corpus", "--json"]);
|
||||
if iges {
|
||||
cmd.arg("--iges");
|
||||
}
|
||||
for c in &cases {
|
||||
cmd.arg(c);
|
||||
}
|
||||
@@ -241,8 +246,19 @@ fn cmd_step_corpus(dir: Option<&str>) -> Result<(), String> {
|
||||
let check =
|
||||
|r: &serde_json::Value, name: &str| r["checks"].as_array().and_then(|c| c.iter().find(|c| c["check"] == name).cloned()).unwrap_or_default();
|
||||
let num = |v: &serde_json::Value| v.as_f64().map(|x| format!("{x:.3}")).unwrap_or_else(|| "–".into());
|
||||
let mut md = format!(
|
||||
"# STEP import\n\n\
|
||||
let mut md = if iges {
|
||||
format!(
|
||||
"# IGES import (Fusion corpus)\n\nGenerated by `cargo xtask step-corpus --iges`. Each case is a reference part exported from Fusion as IGES 5.3 \
|
||||
(kept locally in `plan/fusion/oracle/`, never committed). SolveCraft opens `part.igs` exactly as File → Open does and compares with Fusion's \
|
||||
measurements of the part it exported: body count, volume and area (relative tolerance 1e-3) and the number of faces read from the file. \
|
||||
How IGES is read: see [STEP import](step-import.md#iges). 50-sweep-tight-bend is written by Fusion as five loose, partly degenerate surfaces \
|
||||
(seams running into slits, a ring given twice); Fusion cannot read it back as a solid either.\n\n**{pass} / {} files pass.**\n\n\
|
||||
| Case | Result | Bodies | Volume mm³ (got / Fusion) | Area mm² (got / Fusion) | Faces (got / Fusion) | ms | Notes |\n|---|---|---|---|---|---|---|---|\n",
|
||||
list.len()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"# STEP import\n\n\
|
||||
SolveCraft reads STEP (ISO 10303-21; AP203, AP214 and AP242 geometry) with its own reader in `crates/kernel/src/step_in` \
|
||||
(written from the public standards; no third-party STEP code). A STEP file becomes an **Import** base feature on the \
|
||||
timeline: it holds the file's B-rep (the STEP text is kept in the design, so the design reopens without the file) and \
|
||||
@@ -270,8 +286,9 @@ units; colours (314 and the predefined numbers); body names (name properties, el
|
||||
offset surfaces, curves on surfaces given only in parameter space. Export (`file.export` to `.igs`) writes each body as \
|
||||
a manifold solid B-rep on the same surfaces a STEP export uses, with its name and colour.\n\n\
|
||||
## Fusion corpus\n\nGenerated by `cargo xtask step-corpus`. Each case is a part exported from Fusion as STEP (AP214; kept locally in `plan/fusion/oracle/`, never committed). SolveCraft opens `part.step` exactly as File → Open does (an Import base feature) and compares with Fusion's own measurements: body count, volume and area (relative tolerance 1e-3) and the number of faces read from the file.\n\n**{pass} / {} files pass.**\n\n| Case | Result | Bodies | Volume mm³ (got / Fusion) | Area mm² (got / Fusion) | Faces (got / Fusion) | ms | Notes |\n|---|---|---|---|---|---|---|---|\n",
|
||||
list.len()
|
||||
);
|
||||
list.len()
|
||||
)
|
||||
};
|
||||
for r in &list {
|
||||
let status = if r["pass"].as_bool().unwrap_or(false) { "pass" } else { "FAIL" };
|
||||
let (v, a, f) = (check(r, "volume_mm3"), check(r, "area_mm2"), check(r, "faces"));
|
||||
@@ -294,9 +311,10 @@ a manifold solid B-rep on the same surfaces a STEP export uses, with its name an
|
||||
notes.join("; ")
|
||||
);
|
||||
}
|
||||
std::fs::write(root().join("docs/step-import.md"), md).map_err(|e| format!("docs/step-import.md: {e}"))?;
|
||||
println!("step-corpus: {pass}/{} files pass → docs/step-import.md", list.len());
|
||||
if pass == list.len() { Ok(()) } else { Err(format!("{} STEP file(s) failed", list.len() - pass)) }
|
||||
let out_md = if iges { "docs/iges-import.md" } else { "docs/step-import.md" };
|
||||
std::fs::write(root().join(out_md), md).map_err(|e| format!("{out_md}: {e}"))?;
|
||||
println!("step-corpus: {pass}/{} files pass → {out_md}", list.len());
|
||||
if pass == list.len() { Ok(()) } else { Err(format!("{} file(s) failed", list.len() - pass)) }
|
||||
}
|
||||
|
||||
fn cmd_ci() -> Result<(), String> {
|
||||
|
||||
Reference in new issue
Block a user