STEP export: AP242 file of our own, names, colours, components as assemblies
Our writer assembles the file: AP242 header and contexts, a product per part (or per component, with NEXT_ASSEMBLY_USAGE_OCCURRENCE and item-defined placements per occurrence), named solids, surface colours as styled items, ADVANCED_FACE for advanced breps. truck now writes only each body's B-rep, which we copy in; identical geometric entities are written once and shared. Designs with components export as assemblies (bodies in component frames). step_validate also rejects unreferenced non-root entities. Tests: the sample plate and a component assembly export, validate and read back with no warnings and the same volume, area and face counts; the oracle replay checks area and faces too (39 rebuildable cases pass). An independent Part 21 parser (step-p21, run outside the repo) accepts our exports. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
dd7485503a
commit
9330ee5698
10 files changed
+682
-14
No files matched your search
@@ -160,7 +160,12 @@ fn save_as(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn export_to(s: &Session, path: &str, format: Format, bodies: &[String]) -> Result<Value> {
|
||||
let name = std::path::Path::new(path).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_else(|| s.doc.name.clone());
|
||||
let bytes = solvecraft_io::export(&s.world_state(), bodies, format, &name)?;
|
||||
// A design with components exports as a STEP assembly; otherwise bodies as placed.
|
||||
let bytes = if format == Format::Step && bodies.is_empty() && !s.doc.occurrences.is_empty() {
|
||||
solvecraft_io::step_assembly(&s.doc, &s.model.state(), &name)?
|
||||
} else {
|
||||
solvecraft_io::export(&s.world_state(), bodies, format, &name)?
|
||||
};
|
||||
std::fs::write(path, &bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
Ok(json!({"path": path, "bytes": bytes.len(), "format": format!("{format:?}")}))
|
||||
}
|
||||
|
||||
@@ -900,3 +900,60 @@ fn rolling_back_and_forward_leaves_no_projection_warnings() {
|
||||
assert!(warnings(&s).is_empty(), "after rolling to {pos}: {:?}", warnings(&s));
|
||||
}
|
||||
}
|
||||
|
||||
/// Export STEP, validate the file, read it back: no warnings, same volume, area and faces.
|
||||
fn step_round_trip(s: &mut Session, tag: &str) -> solvecraft_kernel::StepImport {
|
||||
let p = std::env::temp_dir().join(format!("solvecraft-steprt-{tag}-{}.step", std::process::id()));
|
||||
run(s, "ExportCommand", json!({"path": p.to_string_lossy()}));
|
||||
let text = std::fs::read_to_string(&p).unwrap();
|
||||
let _ = std::fs::remove_file(&p);
|
||||
solvecraft_kernel::step_validate(&text).unwrap();
|
||||
assert!(text.contains("AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF"));
|
||||
let imp = solvecraft_kernel::step_import(&text).unwrap();
|
||||
assert!(imp.warnings.is_empty(), "{:?}", imp.warnings);
|
||||
imp
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_plate_step_round_trip() {
|
||||
let mut s = Session::default();
|
||||
s.run_script(&crate::sample::script()).unwrap();
|
||||
let imp = step_round_trip(&mut s, "sample");
|
||||
let st = s.world_state();
|
||||
assert_eq!(imp.bodies.len(), st.bodies.len());
|
||||
for (a, b) in st.bodies.iter().zip(&imp.bodies) {
|
||||
assert_eq!(a.name, b.name);
|
||||
let (ma, mb) = (solvecraft_kernel::measure(&a.body).unwrap(), solvecraft_kernel::measure(&b.body).unwrap());
|
||||
assert!(rel(mb.volume, ma.volume) < 1e-4, "{} vs {}", mb.volume, ma.volume);
|
||||
assert!(rel(mb.area, ma.area) < 1e-4, "{} vs {}", mb.area, ma.area);
|
||||
assert_eq!(b.file_faces, a.body.face_count());
|
||||
assert_eq!(mb.merged.faces, ma.merged.faces);
|
||||
}
|
||||
}
|
||||
|
||||
/// Components export as a STEP assembly: products, occurrences, placements.
|
||||
#[test]
|
||||
fn components_export_as_step_assembly() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 5, "name": "Plate"}));
|
||||
run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "Pin"}));
|
||||
run(&mut s, "PrimitiveCylinder", json!({"radius": 2, "height": 10, "base": [5, 5, 5]}));
|
||||
run(&mut s, "occurrence.copy", json!({"component": "Pin", "translate": [20, 0, 0]}));
|
||||
let want: f64 = s.world_state().bodies.iter().map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).sum();
|
||||
let imp = step_round_trip(&mut s, "asm");
|
||||
assert_eq!(imp.bodies.len(), 3, "{:?}", imp.bodies.iter().map(|b| &b.name).collect::<Vec<_>>());
|
||||
let got: f64 = imp.bodies.iter().map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).sum();
|
||||
assert!(rel(got, want) < 1e-6, "{got} vs {want}");
|
||||
let root = &imp.tree[0];
|
||||
assert_eq!(root.children.len(), 2);
|
||||
assert!(root.children.iter().all(|c| c.name == "Pin"));
|
||||
// The second pin sits 20 mm along X from the first.
|
||||
let xs: Vec<f64> = imp
|
||||
.bodies
|
||||
.iter()
|
||||
.filter(|b| b.path.last().is_some_and(|p| p == "Pin"))
|
||||
.map(|b| solvecraft_kernel::measure(&b.body).unwrap().centroid.x)
|
||||
.collect();
|
||||
assert_eq!(xs.len(), 2);
|
||||
assert!(((xs[0] - xs[1]).abs() - 20.0).abs() < 1e-6, "{xs:?}");
|
||||
}
|
||||
Reference in new issue
Block a user