diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 9043a531..8de28a94 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2051,6 +2051,67 @@ fn appearances_survive_later_features() { let _ = std::fs::remove_dir_all(&dir); } +/// The original #2 RGB import acceptance, including later topology and placement edits. +#[test] +fn imported_rgb_appearances_survive_edits_reopen_and_step_export() { + let dir = std::env::temp_dir().join(format!("solvecraft-imported-rgb-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let input = dir.join("rgb.step"); + let saved = dir.join("edited.solvecraft"); + let output = dir.join("edited.step"); + let mut source = Session::default(); + for (name, x, color) in [("Red", 0, "#ff0000"), ("Green", 40, "#00ff00"), ("Blue", 80, "#0000ff")] { + run(&mut source, "solid.box", json!({"corner":[x,0,0],"length":30,"width":20,"height":20,"body_name":name})); + run(&mut source, "appearance.assign", json!({"bodies":[name],"color":color})); + } + run(&mut source, "appearance.assign", json!({"faces":[[15,10,20]],"color":"#ffff00"})); + run(&mut source, "file.export", json!({"path":input})); + let check = |session: &Session| { + let state = session.model.state(); + assert_eq!(state.bodies.len(), 3); + for (name, color) in [("Red", [1., 0., 0.]), ("Green", [0., 1., 0.]), ("Blue", [0., 0., 1.])] { + let body = state.body(name).unwrap(); + assert_eq!(session.doc.body_color(body), Some(color), "{name} retains its imported body colour"); + } + let red = state.body("Red").unwrap(); + assert!(session.doc.face_colors(red).iter().any(|(_, look)| look.color == [255, 255, 0]), "Red retains its yellow top face"); + assert!(session.model.results.iter().all(|result| result.error.is_none())); + }; + let mut edited = Session::default(); + run(&mut edited, "doc.open", json!({"path":input})); + check(&edited); + let initial_volume = volume(&mut edited); + run(&mut edited, "solid.fillet", json!({"bodies":["Red"],"edges":[[0,10,20]],"radius":2})); + check(&edited); + run(&mut edited, "solid.move", json!({"bodies":["Red"],"translate":[0,0,5]})); + check(&edited); + run(&mut edited, "solid.box", json!({"corner":[85,5,15],"length":10,"width":10,"height":10,"body_name":"Cutter"})); + run(&mut edited, "solid.combine", json!({"target":"Blue","tools":["Cutter"],"operation":"cut"})); + check(&edited); + run(&mut edited, "solid.box", json!({"corner":[65,0,0],"length":10,"width":20,"height":20,"body_name":"Joiner"})); + run(&mut edited, "solid.combine", json!({"target":"Green","tools":["Joiner"],"operation":"join"})); + check(&edited); + let edited_volume = volume(&mut edited); + // The join adds 5*20*20, the pocket removes 10*10*5, and the + // radius-2 quarter-round replaces a 2*2 square along a 20 mm edge. + let expected_volume = initial_volume + 2000. - 500. - (4. - PI) * 20.; + assert!((edited_volume - expected_volume).abs() < 0.1, "{edited_volume} vs {expected_volume}"); + run(&mut edited, "edit.undo", json!({})); + run(&mut edited, "edit.redo", json!({})); + check(&edited); + run(&mut edited, "file.save_as", json!({"path":saved})); + let mut reopened = Session::default(); + run(&mut reopened, "doc.open", json!({"path":saved})); + check(&reopened); + assert!(rel(volume(&mut reopened), edited_volume) < 1e-8); + run(&mut reopened, "file.export", json!({"path":output})); + let mut roundtrip = Session::default(); + run(&mut roundtrip, "doc.open", json!({"path":output})); + check(&roundtrip); + assert!(rel(volume(&mut roundtrip), edited_volume) < 1e-3); + let _ = std::fs::remove_dir_all(dir); +} + /// Extrude To Object (a parallel face, with an offset; a slanted face) and From Object. #[test] fn extrude_to_and_from_objects() { diff --git a/docs/step-import.md b/docs/step-import.md index 6ab12a5f..236cce3d 100644 --- a/docs/step-import.md +++ b/docs/step-import.md @@ -8,6 +8,19 @@ SolidCraft reads STEP (ISO 10303-21; AP203, AP214 and AP242 geometry) with its o Read: manifold solid breps (with voids), faceted breps, shell-based surface models; planes, cylinders, cones, spheres, tori, B-spline and rational B-spline surfaces (with knots, Bézier, uniform, quasi-uniform), surfaces of revolution and linear extrusion, trimmed surfaces; lines, circles, ellipses, B-spline curves, polylines, trimmed/surface/seam curves; length and angle units (SI prefixes, conversion-based units such as inches); product names (body names come from the solid's name, else the product's); surface colours; assemblies (next-assembly-usage occurrences placed by item-defined transformations, and mapped items) — the product tree is kept in the feature for the components milestone. Files are untrusted: sizes, entity counts and nesting are capped and every kernel call is guarded. What cannot be read becomes a warning on the feature (a face left out gives an open body); not read yet: offset surfaces, pcurve-only edges. +## Imported appearances + +Imported body colours and face colours are retained through later features. Body +colours remain the fallback under explicit design appearances and materials; face +colours follow persistent face names. A removed face has no colour assignment to +display on the resulting body. + +The RGB import regression in the engine checks a STEP file with separate red, +green and blue bodies: fillet and move the red body, cut the blue body, join an +extension to the green body, undo/redo, +save and reopen the design, then export and reopen STEP. It checks all three body +colours and the red body's separately coloured top face at each stage. + ## IGES IGES 5.3 files (`.igs`/`.iges`) open, insert and drop the same way: `crates/kernel/src/iges_in.rs` (written from the published specification) restates the file as STEP and the STEP reader reads it, so the feature holds STEP text. Read: manifold solid B-reps (186) with their shells, faces, loops, edge and vertex lists; trimmed (144) and bounded (143) surfaces, and bare B-spline patches, sewn into a solid by their shared edges (an open body when they do not close); lines, circular arcs, ellipses, composite curves, rational B-spline curves; planes (108, 190), cylinders, cones, spheres, tori, rational B-spline surfaces, surfaces of revolution and tabulated cylinders; transformation matrices; units; colours (314 and the predefined numbers); body names (name properties, else labels). Not read yet: ruled and 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.