Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

test(interop): cover step import round-trips & schema gat

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (Jun 8, 2026, 12:49 PM +0300) commit 079ac7b2 parent bbc2ed48 change-id mmuunmmq
+994 -47
+179
crates/bone-interop/tests/goldens/cube.step
··· 1 + ISO-10303-21; 2 + HEADER; 3 + FILE_DESCRIPTION(('Bone geometry'),'2;1'); 4 + FILE_NAME('cube','1970-01-01T00:00:00',(''),(''),'','Bone 0.0.0',''); 5 + FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }')); 6 + ENDSEC; 7 + DATA; 8 + #1 = APPLICATION_PROTOCOL_DEFINITION('international standard', 'automotive_design', 2000, #2); 9 + #2 = APPLICATION_CONTEXT('core data for automotive mechanical design processes'); 10 + #3 = SHAPE_DEFINITION_REPRESENTATION(#4, #10); 11 + #4 = PRODUCT_DEFINITION_SHAPE('','', #5); 12 + #5 = PRODUCT_DEFINITION('design','', #6, #9); 13 + #6 = PRODUCT_DEFINITION_FORMATION('','', #7); 14 + #7 = PRODUCT('','','', (#8)); 15 + #8 = PRODUCT_CONTEXT('', #2, 'mechanical'); 16 + #9 = PRODUCT_DEFINITION_CONTEXT('part definition', #2, 'design'); 17 + #10 = ADVANCED_BREP_SHAPE_REPRESENTATION('', (#16), #11); 18 + #11 = ( 19 + GEOMETRIC_REPRESENTATION_CONTEXT(3) 20 + GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#15)) 21 + GLOBAL_UNIT_ASSIGNED_CONTEXT((#12, #13, #14)) 22 + REPRESENTATION_CONTEXT('Context #1', '3D Context with UNIT and UNCERTAINTY') 23 + ); 24 + #12 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); 25 + #13 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); 26 + #14 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); 27 + #15 = UNCERTAINTY_MEASURE_WITH_UNIT(1.0E-6, #12, 'distance_accuracy_value','confusion accuracy'); 28 + #16 = MANIFOLD_SOLID_BREP('', #17); 29 + #17 = CLOSED_SHELL('', (#18, #25, #32, #39, #46, #53)); 30 + #18 = FACE_SURFACE('', (#19), #80, .F.); 31 + #19 = FACE_BOUND('', #20, .F.); 32 + #20 = EDGE_LOOP('', (#21, #22, #23, #24)); 33 + #21 = ORIENTED_EDGE('', *, *, #60, .T.); 34 + #22 = ORIENTED_EDGE('', *, *, #61, .T.); 35 + #23 = ORIENTED_EDGE('', *, *, #62, .T.); 36 + #24 = ORIENTED_EDGE('', *, *, #63, .T.); 37 + #25 = FACE_SURFACE('', (#26), #85, .T.); 38 + #26 = FACE_BOUND('', #27, .T.); 39 + #27 = EDGE_LOOP('', (#28, #29, #30, #31)); 40 + #28 = ORIENTED_EDGE('', *, *, #60, .T.); 41 + #29 = ORIENTED_EDGE('', *, *, #64, .T.); 42 + #30 = ORIENTED_EDGE('', *, *, #65, .F.); 43 + #31 = ORIENTED_EDGE('', *, *, #66, .F.); 44 + #32 = FACE_SURFACE('', (#33), #90, .T.); 45 + #33 = FACE_BOUND('', #34, .T.); 46 + #34 = EDGE_LOOP('', (#35, #36, #37, #38)); 47 + #35 = ORIENTED_EDGE('', *, *, #61, .T.); 48 + #36 = ORIENTED_EDGE('', *, *, #67, .T.); 49 + #37 = ORIENTED_EDGE('', *, *, #68, .F.); 50 + #38 = ORIENTED_EDGE('', *, *, #64, .F.); 51 + #39 = FACE_SURFACE('', (#40), #95, .T.); 52 + #40 = FACE_BOUND('', #41, .T.); 53 + #41 = EDGE_LOOP('', (#42, #43, #44, #45)); 54 + #42 = ORIENTED_EDGE('', *, *, #62, .T.); 55 + #43 = ORIENTED_EDGE('', *, *, #69, .T.); 56 + #44 = ORIENTED_EDGE('', *, *, #70, .F.); 57 + #45 = ORIENTED_EDGE('', *, *, #67, .F.); 58 + #46 = FACE_SURFACE('', (#47), #100, .T.); 59 + #47 = FACE_BOUND('', #48, .T.); 60 + #48 = EDGE_LOOP('', (#49, #50, #51, #52)); 61 + #49 = ORIENTED_EDGE('', *, *, #63, .T.); 62 + #50 = ORIENTED_EDGE('', *, *, #66, .T.); 63 + #51 = ORIENTED_EDGE('', *, *, #71, .F.); 64 + #52 = ORIENTED_EDGE('', *, *, #69, .F.); 65 + #53 = FACE_SURFACE('', (#54), #105, .T.); 66 + #54 = FACE_BOUND('', #55, .T.); 67 + #55 = EDGE_LOOP('', (#56, #57, #58, #59)); 68 + #56 = ORIENTED_EDGE('', *, *, #65, .T.); 69 + #57 = ORIENTED_EDGE('', *, *, #68, .T.); 70 + #58 = ORIENTED_EDGE('', *, *, #70, .T.); 71 + #59 = ORIENTED_EDGE('', *, *, #71, .T.); 72 + #60 = EDGE_CURVE('', #72, #73, #110, .T.); 73 + #61 = EDGE_CURVE('', #73, #74, #114, .T.); 74 + #62 = EDGE_CURVE('', #74, #75, #118, .T.); 75 + #63 = EDGE_CURVE('', #75, #72, #122, .T.); 76 + #64 = EDGE_CURVE('', #73, #76, #126, .T.); 77 + #65 = EDGE_CURVE('', #77, #76, #130, .T.); 78 + #66 = EDGE_CURVE('', #72, #77, #134, .T.); 79 + #67 = EDGE_CURVE('', #74, #78, #138, .T.); 80 + #68 = EDGE_CURVE('', #76, #78, #142, .T.); 81 + #69 = EDGE_CURVE('', #75, #79, #146, .T.); 82 + #70 = EDGE_CURVE('', #78, #79, #150, .T.); 83 + #71 = EDGE_CURVE('', #79, #77, #154, .T.); 84 + #72 = VERTEX_POINT('', #158); 85 + #73 = VERTEX_POINT('', #159); 86 + #74 = VERTEX_POINT('', #160); 87 + #75 = VERTEX_POINT('', #161); 88 + #76 = VERTEX_POINT('', #162); 89 + #77 = VERTEX_POINT('', #163); 90 + #78 = VERTEX_POINT('', #164); 91 + #79 = VERTEX_POINT('', #165); 92 + #80 = PLANE('', #81); 93 + #81 = AXIS2_PLACEMENT_3D('', #82, #83, #84); 94 + #82 = CARTESIAN_POINT('', (4.0, 2.0, 0.0)); 95 + #83 = DIRECTION('', (0.0, 0.0, 1.0)); 96 + #84 = DIRECTION('', (-1.0, 0.0, 0.0)); 97 + #85 = PLANE('', #86); 98 + #86 = AXIS2_PLACEMENT_3D('', #87, #88, #89); 99 + #87 = CARTESIAN_POINT('', (0.0, 0.0, 0.0)); 100 + #88 = DIRECTION('', (0.0, -1.0, 0.0)); 101 + #89 = DIRECTION('', (1.0, 0.0, 0.0)); 102 + #90 = PLANE('', #91); 103 + #91 = AXIS2_PLACEMENT_3D('', #92, #93, #94); 104 + #92 = CARTESIAN_POINT('', (4.0, 0.0, 0.0)); 105 + #93 = DIRECTION('', (1.0, 0.0, 0.0)); 106 + #94 = DIRECTION('', (0.0, 1.0, 0.0)); 107 + #95 = PLANE('', #96); 108 + #96 = AXIS2_PLACEMENT_3D('', #97, #98, #99); 109 + #97 = CARTESIAN_POINT('', (4.0, 2.0, 0.0)); 110 + #98 = DIRECTION('', (0.0, 1.0, -0.0)); 111 + #99 = DIRECTION('', (-1.0, 0.0, 0.0)); 112 + #100 = PLANE('', #101); 113 + #101 = AXIS2_PLACEMENT_3D('', #102, #103, #104); 114 + #102 = CARTESIAN_POINT('', (0.0, 2.0, 0.0)); 115 + #103 = DIRECTION('', (-1.0, 0.0, 0.0)); 116 + #104 = DIRECTION('', (0.0, -1.0, 0.0)); 117 + #105 = PLANE('', #106); 118 + #106 = AXIS2_PLACEMENT_3D('', #107, #108, #109); 119 + #107 = CARTESIAN_POINT('', (4.0, 2.0, 4.0)); 120 + #108 = DIRECTION('', (0.0, 0.0, 1.0)); 121 + #109 = DIRECTION('', (-1.0, 0.0, 0.0)); 122 + #110 = LINE('', #111, #112); 123 + #111 = CARTESIAN_POINT('', (0.0, 0.0, 0.0)); 124 + #112 = VECTOR('', #113, 4.0); 125 + #113 = DIRECTION('', (1.0, 0.0, 0.0)); 126 + #114 = LINE('', #115, #116); 127 + #115 = CARTESIAN_POINT('', (4.0, 0.0, 0.0)); 128 + #116 = VECTOR('', #117, 2.0); 129 + #117 = DIRECTION('', (0.0, 1.0, 0.0)); 130 + #118 = LINE('', #119, #120); 131 + #119 = CARTESIAN_POINT('', (4.0, 2.0, 0.0)); 132 + #120 = VECTOR('', #121, 4.0); 133 + #121 = DIRECTION('', (-1.0, 0.0, 0.0)); 134 + #122 = LINE('', #123, #124); 135 + #123 = CARTESIAN_POINT('', (0.0, 2.0, 0.0)); 136 + #124 = VECTOR('', #125, 2.0); 137 + #125 = DIRECTION('', (0.0, -1.0, 0.0)); 138 + #126 = LINE('', #127, #128); 139 + #127 = CARTESIAN_POINT('', (4.0, 0.0, 0.0)); 140 + #128 = VECTOR('', #129, 4.0); 141 + #129 = DIRECTION('', (0.0, 0.0, 1.0)); 142 + #130 = LINE('', #131, #132); 143 + #131 = CARTESIAN_POINT('', (0.0, 0.0, 4.0)); 144 + #132 = VECTOR('', #133, 4.0); 145 + #133 = DIRECTION('', (1.0, 0.0, 0.0)); 146 + #134 = LINE('', #135, #136); 147 + #135 = CARTESIAN_POINT('', (0.0, 0.0, 0.0)); 148 + #136 = VECTOR('', #137, 4.0); 149 + #137 = DIRECTION('', (0.0, 0.0, 1.0)); 150 + #138 = LINE('', #139, #140); 151 + #139 = CARTESIAN_POINT('', (4.0, 2.0, 0.0)); 152 + #140 = VECTOR('', #141, 4.0); 153 + #141 = DIRECTION('', (0.0, 0.0, 1.0)); 154 + #142 = LINE('', #143, #144); 155 + #143 = CARTESIAN_POINT('', (4.0, 0.0, 4.0)); 156 + #144 = VECTOR('', #145, 2.0); 157 + #145 = DIRECTION('', (0.0, 1.0, 0.0)); 158 + #146 = LINE('', #147, #148); 159 + #147 = CARTESIAN_POINT('', (0.0, 2.0, 0.0)); 160 + #148 = VECTOR('', #149, 4.0); 161 + #149 = DIRECTION('', (0.0, 0.0, 1.0)); 162 + #150 = LINE('', #151, #152); 163 + #151 = CARTESIAN_POINT('', (4.0, 2.0, 4.0)); 164 + #152 = VECTOR('', #153, 4.0); 165 + #153 = DIRECTION('', (-1.0, 0.0, 0.0)); 166 + #154 = LINE('', #155, #156); 167 + #155 = CARTESIAN_POINT('', (0.0, 2.0, 4.0)); 168 + #156 = VECTOR('', #157, 2.0); 169 + #157 = DIRECTION('', (0.0, -1.0, 0.0)); 170 + #158 = CARTESIAN_POINT('', (0.0, 0.0, 0.0)); 171 + #159 = CARTESIAN_POINT('', (4.0, 0.0, 0.0)); 172 + #160 = CARTESIAN_POINT('', (4.0, 2.0, 0.0)); 173 + #161 = CARTESIAN_POINT('', (0.0, 2.0, 0.0)); 174 + #162 = CARTESIAN_POINT('', (4.0, 0.0, 4.0)); 175 + #163 = CARTESIAN_POINT('', (0.0, 0.0, 4.0)); 176 + #164 = CARTESIAN_POINT('', (4.0, 2.0, 4.0)); 177 + #165 = CARTESIAN_POINT('', (0.0, 2.0, 4.0)); 178 + ENDSEC; 179 + END-ISO-10303-21;
+113
crates/bone-interop/tests/goldens/inbound/wedge.step
··· 1 + ISO-10303-21; 2 + HEADER; 3 + FILE_DESCRIPTION(('synthetic analytic quarter cylinder'),'2;1'); 4 + FILE_NAME('wedge','1970-01-01T00:00:00',(''),(''),'','synthetic foreign tool',''); 5 + FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }')); 6 + ENDSEC; 7 + DATA; 8 + #1 = MANIFOLD_SOLID_BREP('wedge', #2); 9 + #2 = CLOSED_SHELL('', (#10, #20, #30, #40, #50)); 10 + #10 = ADVANCED_FACE('bottom', (#11), #200, .F.); 11 + #11 = FACE_OUTER_BOUND('', #12, .T.); 12 + #12 = EDGE_LOOP('', (#13, #14, #15)); 13 + #13 = ORIENTED_EDGE('', *, *, #111, .T.); 14 + #14 = ORIENTED_EDGE('', *, *, #117, .F.); 15 + #15 = ORIENTED_EDGE('', *, *, #110, .F.); 16 + #20 = ADVANCED_FACE('top', (#21), #210, .T.); 17 + #21 = FACE_OUTER_BOUND('', #22, .T.); 18 + #22 = EDGE_LOOP('', (#23, #24, #25)); 19 + #23 = ORIENTED_EDGE('', *, *, #115, .T.); 20 + #24 = ORIENTED_EDGE('', *, *, #118, .T.); 21 + #25 = ORIENTED_EDGE('', *, *, #116, .F.); 22 + #30 = ADVANCED_FACE('side_x', (#31), #220, .T.); 23 + #31 = FACE_OUTER_BOUND('', #32, .T.); 24 + #32 = EDGE_LOOP('', (#33, #34, #35, #36)); 25 + #33 = ORIENTED_EDGE('', *, *, #110, .T.); 26 + #34 = ORIENTED_EDGE('', *, *, #113, .T.); 27 + #35 = ORIENTED_EDGE('', *, *, #115, .F.); 28 + #36 = ORIENTED_EDGE('', *, *, #112, .F.); 29 + #40 = ADVANCED_FACE('side_y', (#41), #230, .T.); 30 + #41 = FACE_OUTER_BOUND('', #42, .T.); 31 + #42 = EDGE_LOOP('', (#43, #44, #45, #46)); 32 + #43 = ORIENTED_EDGE('', *, *, #112, .T.); 33 + #44 = ORIENTED_EDGE('', *, *, #116, .T.); 34 + #45 = ORIENTED_EDGE('', *, *, #114, .F.); 35 + #46 = ORIENTED_EDGE('', *, *, #111, .F.); 36 + #50 = ADVANCED_FACE('wall', (#51), #240, .T.); 37 + #51 = FACE_OUTER_BOUND('', #52, .T.); 38 + #52 = EDGE_LOOP('', (#53, #54, #55, #56)); 39 + #53 = ORIENTED_EDGE('', *, *, #117, .T.); 40 + #54 = ORIENTED_EDGE('', *, *, #114, .T.); 41 + #55 = ORIENTED_EDGE('', *, *, #118, .F.); 42 + #56 = ORIENTED_EDGE('', *, *, #113, .F.); 43 + #100 = VERTEX_POINT('', #150); 44 + #101 = VERTEX_POINT('', #151); 45 + #102 = VERTEX_POINT('', #152); 46 + #103 = VERTEX_POINT('', #153); 47 + #104 = VERTEX_POINT('', #154); 48 + #105 = VERTEX_POINT('', #155); 49 + #150 = CARTESIAN_POINT('', (0.0, 0.0, 0.0)); 50 + #151 = CARTESIAN_POINT('', (5.0, 0.0, 0.0)); 51 + #152 = CARTESIAN_POINT('', (0.0, 5.0, 0.0)); 52 + #153 = CARTESIAN_POINT('', (0.0, 0.0, 8.0)); 53 + #154 = CARTESIAN_POINT('', (5.0, 0.0, 8.0)); 54 + #155 = CARTESIAN_POINT('', (0.0, 5.0, 8.0)); 55 + #110 = EDGE_CURVE('', #100, #101, #160, .T.); 56 + #111 = EDGE_CURVE('', #100, #102, #161, .T.); 57 + #112 = EDGE_CURVE('', #100, #103, #162, .T.); 58 + #113 = EDGE_CURVE('', #101, #104, #163, .T.); 59 + #114 = EDGE_CURVE('', #102, #105, #164, .T.); 60 + #115 = EDGE_CURVE('', #103, #104, #165, .T.); 61 + #116 = EDGE_CURVE('', #103, #105, #166, .T.); 62 + #117 = EDGE_CURVE('', #101, #102, #170, .T.); 63 + #118 = EDGE_CURVE('', #104, #105, #171, .T.); 64 + #160 = LINE('', #150, #180); 65 + #161 = LINE('', #150, #181); 66 + #162 = LINE('', #150, #182); 67 + #163 = LINE('', #151, #182); 68 + #164 = LINE('', #152, #182); 69 + #165 = LINE('', #153, #180); 70 + #166 = LINE('', #153, #181); 71 + #180 = VECTOR('', #190, 1.0); 72 + #181 = VECTOR('', #191, 1.0); 73 + #182 = VECTOR('', #192, 1.0); 74 + #190 = DIRECTION('', (1.0, 0.0, 0.0)); 75 + #191 = DIRECTION('', (0.0, 1.0, 0.0)); 76 + #192 = DIRECTION('', (0.0, 0.0, 1.0)); 77 + #170 = CIRCLE('', #300, 5.0); 78 + #171 = CIRCLE('', #310, 5.0); 79 + #300 = AXIS2_PLACEMENT_3D('', #301, #302, #303); 80 + #301 = CARTESIAN_POINT('', (0.0, 0.0, 0.0)); 81 + #302 = DIRECTION('', (0.0, 0.0, 1.0)); 82 + #303 = DIRECTION('', (1.0, 0.0, 0.0)); 83 + #310 = AXIS2_PLACEMENT_3D('', #311, #312, #313); 84 + #311 = CARTESIAN_POINT('', (0.0, 0.0, 8.0)); 85 + #312 = DIRECTION('', (0.0, 0.0, 1.0)); 86 + #313 = DIRECTION('', (1.0, 0.0, 0.0)); 87 + #200 = PLANE('', #320); 88 + #320 = AXIS2_PLACEMENT_3D('', #321, #322, #323); 89 + #321 = CARTESIAN_POINT('', (0.0, 0.0, 0.0)); 90 + #322 = DIRECTION('', (0.0, 0.0, 1.0)); 91 + #323 = DIRECTION('', (1.0, 0.0, 0.0)); 92 + #210 = PLANE('', #330); 93 + #330 = AXIS2_PLACEMENT_3D('', #331, #332, #333); 94 + #331 = CARTESIAN_POINT('', (0.0, 0.0, 8.0)); 95 + #332 = DIRECTION('', (0.0, 0.0, 1.0)); 96 + #333 = DIRECTION('', (1.0, 0.0, 0.0)); 97 + #220 = PLANE('', #340); 98 + #340 = AXIS2_PLACEMENT_3D('', #341, #342, #343); 99 + #341 = CARTESIAN_POINT('', (0.0, 0.0, 0.0)); 100 + #342 = DIRECTION('', (0.0, -1.0, 0.0)); 101 + #343 = DIRECTION('', (1.0, 0.0, 0.0)); 102 + #230 = PLANE('', #350); 103 + #350 = AXIS2_PLACEMENT_3D('', #351, #352, #353); 104 + #351 = CARTESIAN_POINT('', (0.0, 0.0, 0.0)); 105 + #352 = DIRECTION('', (-1.0, 0.0, 0.0)); 106 + #353 = DIRECTION('', (0.0, 1.0, 0.0)); 107 + #240 = CYLINDRICAL_SURFACE('', #360, 5.0); 108 + #360 = AXIS2_PLACEMENT_3D('', #361, #362, #363); 109 + #361 = CARTESIAN_POINT('', (0.0, 0.0, 0.0)); 110 + #362 = DIRECTION('', (0.0, 0.0, 1.0)); 111 + #363 = DIRECTION('', (1.0, 0.0, 0.0)); 112 + ENDSEC; 113 + END-ISO-10303-21;
+702 -47
crates/bone-interop/tests/step.rs
··· 1 + use std::path::{Path, PathBuf}; 2 + 1 3 use bone_document::{Document, EditOutcome, Sketch, SketchEdit, SketchEntity}; 2 - use bone_interop::{StepError, body_of, read, write}; 4 + use bone_interop::{HeaderDefect, StepError, body_of, read, write}; 3 5 use bone_kernel::{ 4 6 BrepFace, BrepSolid, ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, 5 7 MergeResult, 6 8 }; 7 9 use bone_types::{ 8 - DocumentId, ExtrudeId, FaceLabel, FeatureId, Length, Point2, Point3, PositiveLength, 9 - SketchEntityId, SketchId, SketchPlaneBasis, StepSchema, Tolerance, UnitVec3, millimeter, 10 + Aabb3, DocumentId, ExtrudeId, FaceLabel, FaceRole, Length, Point2, Point3, PositiveLength, 11 + SketchEntityId, SketchId, SketchPlaneBasis, StepEntityKind, StepSchema, Tolerance, UnitVec3, 12 + millimeter, 10 13 }; 11 14 use slotmap::KeyData; 12 15 13 16 const TOL: Tolerance = Tolerance::new(1.0e-9); 17 + const UPDATE_ENV: &str = "BONE_UPDATE_STEP_GOLDENS"; 14 18 15 19 fn xy_basis() -> SketchPlaneBasis { 16 20 let Ok(basis) = SketchPlaneBasis::new( ··· 34 38 35 39 fn extrude_id(n: u64) -> ExtrudeId { 36 40 ExtrudeId::from(ffi_key(n)) 37 - } 38 - 39 - fn import_feature() -> FeatureId { 40 - FeatureId::from(ffi_key(99)) 41 41 } 42 42 43 43 fn add_point(sketch: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) { ··· 117 117 solid.iter_faces().map(BrepFace::label).collect() 118 118 } 119 119 120 - fn step_round_trip_keeps_labels(document: &Document) { 121 - let Ok(expected) = body_of(document) else { 120 + fn is_dumb(solid: &BrepSolid) -> bool { 121 + solid 122 + .iter_faces() 123 + .all(|face| matches!(face.label().role, FaceRole::Imported { .. })) 124 + } 125 + 126 + fn write_to_temp(document: &Document, dir: &Path, name: &str) -> PathBuf { 127 + let path = dir.join(name); 128 + let Ok(()) = write(document, &path, StepSchema::Ap214) else { 129 + panic!("write step"); 130 + }; 131 + path 132 + } 133 + 134 + fn check_step_golden(text: &str, golden_rel: &str) { 135 + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(golden_rel); 136 + if std::env::var(UPDATE_ENV).is_ok() { 137 + if let Some(parent) = path.parent() { 138 + let Ok(()) = std::fs::create_dir_all(parent) else { 139 + panic!("create goldens dir"); 140 + }; 141 + } 142 + let Ok(()) = std::fs::write(&path, text.as_bytes()) else { 143 + panic!("write golden {}", path.display()); 144 + }; 145 + return; 146 + } 147 + let Ok(golden) = std::fs::read_to_string(&path) else { 148 + panic!( 149 + "golden missing at {}: rerun with {UPDATE_ENV}=1 to generate", 150 + path.display() 151 + ); 152 + }; 153 + assert_eq!(text, golden, "step output drifted from golden {golden_rel}"); 154 + } 155 + 156 + fn matches_golden(name: &str, sketch: Sketch, depth_mm: f64) { 157 + let document = document(name, sketch, depth_mm); 158 + let Ok(dir) = tempfile::tempdir() else { 159 + panic!("temp dir"); 160 + }; 161 + let path = write_to_temp(&document, dir.path(), &format!("{name}.step")); 162 + let Ok(text) = std::fs::read_to_string(&path) else { 163 + panic!("read back"); 164 + }; 165 + check_step_golden(&text, &format!("tests/goldens/{name}.step")); 166 + } 167 + 168 + #[test] 169 + fn cube_step_matches_golden() { 170 + matches_golden("cube", rectangle_sketch(), 4.0); 171 + } 172 + 173 + fn evaluates_within_bounds(seed: &Document, min_mm: [f64; 3], max_mm: [f64; 3], label: &str) { 174 + let Ok(solid) = body_of(seed) else { 175 + panic!("{label} evaluates to one body"); 176 + }; 177 + assert_bounds(&solid_bounds(&solid), min_mm, max_mm, label); 178 + } 179 + 180 + #[test] 181 + fn cylinder_evaluates_within_bounds() { 182 + evaluates_within_bounds( 183 + &document("cylinder", circle_sketch(5.0), 8.0), 184 + [-5.0, -5.0, 0.0], 185 + [5.0, 5.0, 8.0], 186 + "cylinder", 187 + ); 188 + } 189 + 190 + #[test] 191 + fn donut_evaluates_within_bounds() { 192 + evaluates_within_bounds( 193 + &document("donut", donut_sketch(), 6.0), 194 + [-10.0, -10.0, 0.0], 195 + [10.0, 10.0, 6.0], 196 + "donut", 197 + ); 198 + } 199 + 200 + fn import_round_trips_labels(seed: &Document) { 201 + let Ok(expected) = body_of(seed) else { 122 202 panic!("document evaluates to one body"); 123 203 }; 124 204 let Ok(dir) = tempfile::tempdir() else { 125 205 panic!("temp dir"); 126 206 }; 127 - let path = dir.path().join("part.step"); 128 - let Ok(()) = write(document, &path, StepSchema::Ap214) else { 129 - panic!("write step"); 207 + let path = write_to_temp(seed, dir.path(), "part.step"); 208 + let Ok(imported) = read(&path) else { 209 + panic!("read step"); 210 + }; 211 + let Ok(solid) = body_of(&imported) else { 212 + panic!("imported document carries one body"); 213 + }; 214 + assert_eq!( 215 + face_labels(&expected), 216 + face_labels(&solid), 217 + "matching sidecar restores labels" 218 + ); 219 + assert!(!is_dumb(&solid), "a matched sidecar yields a labeled body"); 220 + assert!(solid.validate(TOL).is_ok()); 221 + } 222 + 223 + #[test] 224 + fn cube_import_round_trips_labels() { 225 + import_round_trips_labels(&document("cube", rectangle_sketch(), 4.0)); 226 + } 227 + 228 + #[test] 229 + fn cylinder_import_round_trips_labels() { 230 + import_round_trips_labels(&document("cylinder", circle_sketch(5.0), 8.0)); 231 + } 232 + 233 + #[test] 234 + fn donut_import_round_trips_labels() { 235 + import_round_trips_labels(&document("donut", donut_sketch(), 6.0)); 236 + } 237 + 238 + fn imported_document(seed: &Document) -> Document { 239 + let Ok(dir) = tempfile::tempdir() else { 240 + panic!("temp dir"); 130 241 }; 131 - let Ok(outcome) = read(&path, import_feature()) else { 242 + let path = write_to_temp(seed, dir.path(), "part.step"); 243 + let Ok(imported) = read(&path) else { 132 244 panic!("read step"); 133 245 }; 134 - assert!(outcome.is_labeled(), "matching sidecar restores labels"); 135 - assert_eq!(face_labels(&expected), face_labels(outcome.solid())); 136 - assert!(outcome.solid().validate(TOL).is_ok()); 246 + imported 247 + } 248 + 249 + fn document_round_trips(seed: &Document) { 250 + let imported = imported_document(seed); 251 + let Ok(dir) = tempfile::tempdir() else { 252 + panic!("temp dir"); 253 + }; 254 + let path = write_to_temp(&imported, dir.path(), "part.step"); 255 + let Ok(round) = read(&path) else { 256 + panic!("re-read step"); 257 + }; 258 + assert_eq!( 259 + imported, round, 260 + "read(write(d)) preserves an imported-body document" 261 + ); 137 262 } 138 263 139 264 #[test] 140 - fn cube_step_round_trip_keeps_labels() { 141 - step_round_trip_keeps_labels(&document("cube", rectangle_sketch(), 4.0)); 265 + fn cube_document_round_trips() { 266 + document_round_trips(&document("cube", rectangle_sketch(), 4.0)); 267 + } 268 + 269 + #[test] 270 + fn cylinder_document_round_trips() { 271 + document_round_trips(&document("cylinder", circle_sketch(5.0), 8.0)); 142 272 } 143 273 144 274 #[test] 145 - fn cylinder_step_round_trip_keeps_labels() { 146 - step_round_trip_keeps_labels(&document("cylinder", circle_sketch(5.0), 8.0)); 275 + fn donut_document_round_trips() { 276 + document_round_trips(&document("donut", donut_sketch(), 6.0)); 147 277 } 148 278 149 279 #[test] 150 - fn donut_step_round_trip_keeps_labels() { 151 - step_round_trip_keeps_labels(&document("donut", donut_sketch(), 6.0)); 280 + fn import_enters_one_importable_body() { 281 + let imported = imported_document(&document("cube", rectangle_sketch(), 4.0)); 282 + assert_eq!(imported.imported_bodies().count(), 1); 283 + assert!(body_of(&imported).is_ok()); 152 284 } 153 285 154 286 #[test] ··· 157 289 let Ok(dir) = tempfile::tempdir() else { 158 290 panic!("temp dir"); 159 291 }; 160 - let path = dir.path().join("export.step"); 161 - let Ok(()) = write(&document, &path, StepSchema::Ap214) else { 162 - panic!("write step"); 163 - }; 292 + let path = write_to_temp(&document, dir.path(), "export.step"); 164 293 let Ok(text) = std::fs::read_to_string(&path) else { 165 294 panic!("read back"); 166 295 }; ··· 171 300 } 172 301 173 302 #[test] 303 + fn imported_document_takes_its_name_from_the_file_stem() { 304 + let document = document("bracket", rectangle_sketch(), 4.0); 305 + let Ok(dir) = tempfile::tempdir() else { 306 + panic!("temp dir"); 307 + }; 308 + let path = write_to_temp(&document, dir.path(), "widget.step"); 309 + let Ok(imported) = read(&path) else { 310 + panic!("read step"); 311 + }; 312 + assert_eq!(imported.name(), "widget"); 313 + } 314 + 315 + #[test] 174 316 fn apostrophe_in_document_name_round_trips() { 175 317 let document = document("nel's bracket", rectangle_sketch(), 4.0); 176 318 let Ok(dir) = tempfile::tempdir() else { 177 319 panic!("temp dir"); 178 320 }; 179 - let path = dir.path().join("part.step"); 180 - let Ok(()) = write(&document, &path, StepSchema::Ap214) else { 181 - panic!("an apostrophe in the document name must not break the header"); 182 - }; 321 + let path = write_to_temp(&document, dir.path(), "part.step"); 183 322 let Ok(text) = std::fs::read_to_string(&path) else { 184 323 panic!("read back"); 185 324 }; ··· 187 326 text.contains("FILE_NAME('nel''s bracket'"), 188 327 "an apostrophe is doubled per ISO 10303-21" 189 328 ); 190 - let Ok(outcome) = read(&path, import_feature()) else { 329 + let Ok(imported) = read(&path) else { 191 330 panic!("read step"); 192 331 }; 193 - assert!(outcome.is_labeled()); 332 + let Ok(solid) = body_of(&imported) else { 333 + panic!("one body"); 334 + }; 335 + assert!(!is_dumb(&solid)); 194 336 } 195 337 196 338 #[test] ··· 224 366 let Ok(dir) = tempfile::tempdir() else { 225 367 panic!("temp dir"); 226 368 }; 227 - let path = dir.path().join("part.step"); 228 - let Ok(()) = write(&document, &path, StepSchema::Ap214) else { 229 - panic!("write step"); 230 - }; 369 + let path = write_to_temp(&document, dir.path(), "part.step"); 231 370 let mut labels = path.clone().into_os_string(); 232 371 labels.push(".labels"); 233 - let Ok(()) = std::fs::remove_file(std::path::PathBuf::from(labels)) else { 372 + let Ok(()) = std::fs::remove_file(PathBuf::from(labels)) else { 234 373 panic!("remove sidecar"); 235 374 }; 236 - let Ok(outcome) = read(&path, import_feature()) else { 375 + let Ok(imported) = read(&path) else { 237 376 panic!("read step"); 238 377 }; 239 - assert!(!outcome.is_labeled(), "no sidecar yields a dumb body"); 378 + let Ok(solid) = body_of(&imported) else { 379 + panic!("one body"); 380 + }; 381 + assert!(is_dumb(&solid), "no sidecar yields a dumb body"); 240 382 assert_eq!( 241 - outcome.solid().iter_faces().count(), 242 - expected.iter_faces().count() 383 + solid.iter_faces().count(), 384 + expected.iter_faces().count(), 385 + "geometry survives even without labels" 386 + ); 387 + } 388 + 389 + #[test] 390 + fn ap242_header_reports_schema_mismatch() { 391 + let document = document("cube", rectangle_sketch(), 4.0); 392 + let Ok(dir) = tempfile::tempdir() else { 393 + panic!("temp dir"); 394 + }; 395 + let path = write_to_temp(&document, dir.path(), "cube.step"); 396 + let Ok(text) = std::fs::read_to_string(&path) else { 397 + panic!("read back"); 398 + }; 399 + let swapped = text.replace( 400 + "AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }", 401 + "AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 3 1 4 }", 402 + ); 403 + let foreign = dir.path().join("cube242.step"); 404 + let Ok(()) = std::fs::write(&foreign, swapped.as_bytes()) else { 405 + panic!("write swapped"); 406 + }; 407 + assert!(matches!( 408 + read(&foreign), 409 + Err(StepError::SchemaMismatch { 410 + found: StepSchema::Ap242E2, 411 + .. 412 + }) 413 + )); 414 + } 415 + 416 + #[test] 417 + fn ap203_header_imports_best_effort() { 418 + let document = document("cube", rectangle_sketch(), 4.0); 419 + let Ok(dir) = tempfile::tempdir() else { 420 + panic!("temp dir"); 421 + }; 422 + let path = write_to_temp(&document, dir.path(), "cube.step"); 423 + let Ok(text) = std::fs::read_to_string(&path) else { 424 + panic!("read back"); 425 + }; 426 + let swapped = text.replace( 427 + "AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }", 428 + "CONFIG_CONTROL_DESIGN { 1 0 10303 203 1 1 1 1 }", 429 + ); 430 + let foreign = dir.path().join("cube203.step"); 431 + let Ok(()) = std::fs::write(&foreign, swapped.as_bytes()) else { 432 + panic!("write swapped"); 433 + }; 434 + assert!( 435 + read(&foreign).is_ok(), 436 + "an unmodeled schema token still attempts a best-effort import" 437 + ); 438 + } 439 + 440 + #[test] 441 + fn text_without_header_reports_malformed_header() { 442 + let Ok(dir) = tempfile::tempdir() else { 443 + panic!("temp dir"); 444 + }; 445 + let path = dir.path().join("bad.step"); 446 + let Ok(()) = std::fs::write(&path, b"this is not a step file") else { 447 + panic!("write"); 448 + }; 449 + assert!(matches!( 450 + read(&path), 451 + Err(StepError::MalformedHeader { 452 + reason: HeaderDefect::NoHeaderSection 453 + }) 454 + )); 455 + } 456 + 457 + #[test] 458 + fn header_without_file_schema_reports_malformed_header() { 459 + let text = "ISO-10303-21;\nHEADER;\nFILE_NAME('x','1970-01-01T00:00:00',(''),(''),'','','');\nENDSEC;\nDATA;\nENDSEC;\nEND-ISO-10303-21;\n"; 460 + let Ok(dir) = tempfile::tempdir() else { 461 + panic!("temp dir"); 462 + }; 463 + let path = dir.path().join("noschema.step"); 464 + let Ok(()) = std::fs::write(&path, text.as_bytes()) else { 465 + panic!("write"); 466 + }; 467 + assert!(matches!( 468 + read(&path), 469 + Err(StepError::MalformedHeader { 470 + reason: HeaderDefect::NoFileSchema 471 + }) 472 + )); 473 + } 474 + 475 + #[test] 476 + fn header_with_empty_data_section_is_incomplete() { 477 + let text = "ISO-10303-21;\nHEADER;\nFILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));\nENDSEC;\nDATA;\nENDSEC;\nEND-ISO-10303-21;\n"; 478 + let Ok(dir) = tempfile::tempdir() else { 479 + panic!("temp dir"); 480 + }; 481 + let path = dir.path().join("empty.step"); 482 + let Ok(()) = std::fs::write(&path, text.as_bytes()) else { 483 + panic!("write"); 484 + }; 485 + assert!(matches!(read(&path), Err(StepError::IncompleteFile))); 486 + } 487 + 488 + #[test] 489 + fn header_comment_with_an_apostrophe_does_not_break_the_scan() { 490 + let document = document("cube", rectangle_sketch(), 4.0); 491 + let Ok(dir) = tempfile::tempdir() else { 492 + panic!("temp dir"); 493 + }; 494 + let path = write_to_temp(&document, dir.path(), "part.step"); 495 + let Ok(text) = std::fs::read_to_string(&path) else { 496 + panic!("read back"); 497 + }; 498 + let commented = text.replace( 499 + "FILE_SCHEMA(", 500 + "/* nel's draft, hand-edited */\nFILE_SCHEMA(", 501 + ); 502 + let Ok(()) = std::fs::write(&path, commented.as_bytes()) else { 503 + panic!("rewrite with a header comment"); 504 + }; 505 + let Ok(imported) = read(&path) else { 506 + panic!("a lone apostrophe inside a header comment must not desync the scan"); 507 + }; 508 + let Ok(solid) = body_of(&imported) else { 509 + panic!("one body"); 510 + }; 511 + assert!( 512 + !is_dumb(&solid), 513 + "a header comment does not block sidecar reattach" 514 + ); 515 + } 516 + 517 + #[test] 518 + fn header_comment_carrying_a_foreign_schema_statement_is_ignored() { 519 + let document = document("cube", rectangle_sketch(), 4.0); 520 + let Ok(dir) = tempfile::tempdir() else { 521 + panic!("temp dir"); 522 + }; 523 + let path = write_to_temp(&document, dir.path(), "part.step"); 524 + let Ok(text) = std::fs::read_to_string(&path) else { 525 + panic!("read back"); 526 + }; 527 + let commented = text.replace( 528 + "FILE_SCHEMA(", 529 + "/* legacy: FILE_SCHEMA(('AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 3 1 4 }')); */\nFILE_SCHEMA(", 530 + ); 531 + let Ok(()) = std::fs::write(&path, commented.as_bytes()) else { 532 + panic!("rewrite with a header comment"); 533 + }; 534 + let Ok(imported) = read(&path) else { 535 + panic!("a FILE_SCHEMA statement buried in a comment must not classify the file"); 536 + }; 537 + let Ok(solid) = body_of(&imported) else { 538 + panic!("one body"); 539 + }; 540 + assert!( 541 + !is_dumb(&solid), 542 + "the real AP214 schema still yields a labeled import" 243 543 ); 244 544 } 245 545 ··· 328 628 let path = dir.path().join("part.step"); 329 629 let mut blocker = path.clone().into_os_string(); 330 630 blocker.push(".labels"); 331 - let Ok(()) = std::fs::create_dir(std::path::PathBuf::from(blocker)) else { 631 + let Ok(()) = std::fs::create_dir(PathBuf::from(blocker)) else { 332 632 panic!("block the sidecar path with a directory"); 333 633 }; 334 634 assert!(write(&document, &path, StepSchema::Ap214).is_err()); ··· 338 638 ); 339 639 } 340 640 641 + fn solid_bounds(solid: &BrepSolid) -> Aabb3 { 642 + let Some(bbox) = solid.bounding_box() else { 643 + panic!("solid has a bounding box"); 644 + }; 645 + bbox 646 + } 647 + 648 + fn near_mm(value: Length, mm: f64) -> bool { 649 + (value.get::<millimeter>() - mm).abs() < 0.2 650 + } 651 + 652 + const INBOUND_BOUNDS: &[(&str, [f64; 3], [f64; 3])] = 653 + &[("wedge.step", [0.0, 0.0, 0.0], [5.0, 5.0, 8.0])]; 654 + 655 + fn inbound_step_files() -> Vec<PathBuf> { 656 + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/goldens/inbound"); 657 + let Ok(entries) = std::fs::read_dir(&dir) else { 658 + panic!("inbound goldens directory exists"); 659 + }; 660 + let mut files: Vec<PathBuf> = entries 661 + .filter_map(Result::ok) 662 + .map(|entry| entry.path()) 663 + .filter(|path| { 664 + path.extension() 665 + .is_some_and(|ext| ext.eq_ignore_ascii_case("step")) 666 + }) 667 + .collect(); 668 + files.sort(); 669 + files 670 + } 671 + 672 + fn assert_bounds(bbox: &Aabb3, min_mm: [f64; 3], max_mm: [f64; 3], name: &str) { 673 + let (min, max) = (bbox.min(), bbox.max()); 674 + assert!( 675 + near_mm(min.x(), min_mm[0]) && near_mm(min.y(), min_mm[1]) && near_mm(min.z(), min_mm[2]), 676 + "inbound {name} lower bound {min:?} matches {min_mm:?} mm" 677 + ); 678 + assert!( 679 + near_mm(max.x(), max_mm[0]) && near_mm(max.y(), max_mm[1]) && near_mm(max.z(), max_mm[2]), 680 + "inbound {name} upper bound {max:?} matches {max_mm:?} mm" 681 + ); 682 + } 683 + 684 + fn is_non_degenerate(bbox: &Aabb3) -> bool { 685 + let (min, max) = (bbox.min(), bbox.max()); 686 + let span = |hi: Length, lo: Length| (hi - lo).get::<millimeter>() > 0.1; 687 + span(max.x(), min.x()) && span(max.y(), min.y()) && span(max.z(), min.z()) 688 + } 689 + 690 + #[test] 691 + fn inbound_fixtures_import_and_match_bounds() { 692 + let files = inbound_step_files(); 693 + assert!( 694 + !files.is_empty(), 695 + "at least one inbound cross-tool fixture is present" 696 + ); 697 + files.iter().for_each(|path| { 698 + let label = path.display(); 699 + let document = match read(path) { 700 + Ok(document) => document, 701 + Err(error) => panic!("inbound {label} imports: {error}"), 702 + }; 703 + let Ok(solid) = body_of(&document) else { 704 + panic!("inbound {label} carries one body"); 705 + }; 706 + let bbox = solid_bounds(&solid); 707 + let name = path 708 + .file_name() 709 + .and_then(|n| n.to_str()) 710 + .unwrap_or_default(); 711 + match INBOUND_BOUNDS.iter().find(|(file, ..)| *file == name) { 712 + Some((_, min, max)) => assert_bounds(&bbox, *min, *max, name), 713 + None => assert!( 714 + is_non_degenerate(&bbox), 715 + "inbound {name} imports and tessellates to a non-degenerate box; \ 716 + register its design extent in INBOUND_BOUNDS to pin the match" 717 + ), 718 + } 719 + }); 720 + } 721 + 722 + fn wedge_text() -> String { 723 + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/goldens/inbound/wedge.step"); 724 + let Ok(text) = std::fs::read_to_string(&path) else { 725 + panic!("read wedge golden at {}", path.display()); 726 + }; 727 + text 728 + } 729 + 730 + fn read_step_text(text: &str, name: &str) -> Result<Document, StepError> { 731 + let Ok(dir) = tempfile::tempdir() else { 732 + panic!("temp dir"); 733 + }; 734 + let path = dir.path().join(name); 735 + let Ok(()) = std::fs::write(&path, text.as_bytes()) else { 736 + panic!("write probe step"); 737 + }; 738 + read(&path) 739 + } 740 + 741 + #[test] 742 + fn two_solid_roots_are_rejected_as_assembly() { 743 + let text = wedge_text().replace( 744 + "#1 = MANIFOLD_SOLID_BREP('wedge', #2);", 745 + "#1 = MANIFOLD_SOLID_BREP('wedge', #2);\n#950 = MANIFOLD_SOLID_BREP('clone', #2);", 746 + ); 747 + assert!( 748 + matches!( 749 + read_step_text(&text, "assembly.step"), 750 + Err(StepError::UnsupportedAssembly { solids: 2 }) 751 + ), 752 + "two manifold_solid_brep roots are an assembly, not one body" 753 + ); 754 + } 755 + 756 + #[test] 757 + fn a_stray_closed_shell_is_excluded_not_absorbed() { 758 + let Ok(clean) = read_step_text(&wedge_text(), "clean_wedge.step") else { 759 + panic!("the clean wedge imports"); 760 + }; 761 + let Ok(clean_solid) = body_of(&clean) else { 762 + panic!("clean wedge has one body"); 763 + }; 764 + let text = wedge_text().replace( 765 + "DATA;\n", 766 + "DATA;\n#960 = CLOSED_SHELL('stray', (#10, #20, #30, #40, #50));\n", 767 + ); 768 + let Ok(document) = read_step_text(&text, "stray_closed.step") else { 769 + panic!("a stray shell with one solid root still imports"); 770 + }; 771 + let Ok(solid) = body_of(&document) else { 772 + panic!("one solid root yields one body"); 773 + }; 774 + assert_eq!( 775 + solid.iter_faces().count(), 776 + clean_solid.iter_faces().count(), 777 + "a shell unreachable from the solid root is dropped, not merged in" 778 + ); 779 + } 780 + 781 + #[test] 782 + fn bare_shells_without_a_solid_root_are_rejected() { 783 + let text = wedge_text() 784 + .replace("#1 = MANIFOLD_SOLID_BREP('wedge', #2);\n", "") 785 + .replace( 786 + "DATA;\n", 787 + "DATA;\n#960 = CLOSED_SHELL('stray', (#10, #20, #30, #40, #50));\n", 788 + ); 789 + assert!( 790 + matches!( 791 + read_step_text(&text, "rootless.step"), 792 + Err(StepError::UnsupportedAssembly { solids: 2 }) 793 + ), 794 + "two closed shells with no solid root cannot be one body" 795 + ); 796 + } 797 + 798 + #[test] 799 + fn a_stray_open_shell_is_ignored() { 800 + let text = wedge_text().replace("DATA;\n", "DATA;\n#970 = OPEN_SHELL('stray', (#10));\n"); 801 + let Ok(document) = read_step_text(&text, "open_shell.step") else { 802 + panic!("a construction open shell does not block the import"); 803 + }; 804 + let Ok(solid) = body_of(&document) else { 805 + panic!("one body"); 806 + }; 807 + assert_bounds( 808 + &solid_bounds(&solid), 809 + [0.0, 0.0, 0.0], 810 + [5.0, 5.0, 8.0], 811 + "wedge with a stray open shell", 812 + ); 813 + } 814 + 815 + #[test] 816 + fn spherical_face_reports_unsupported_entity() { 817 + let text = wedge_text().replace( 818 + "CYLINDRICAL_SURFACE('', #360, 5.0)", 819 + "SPHERICAL_SURFACE('', #360, 5.0)", 820 + ); 821 + assert!(matches!( 822 + read_step_text(&text, "sphere.step"), 823 + Err(StepError::UnsupportedEntity { 824 + kind: StepEntityKind::SphericalSurface 825 + }) 826 + )); 827 + } 828 + 829 + #[test] 830 + fn file_schema_catches_an_unsupported_token_past_the_first() { 831 + let document = document("cube", rectangle_sketch(), 4.0); 832 + let Ok(dir) = tempfile::tempdir() else { 833 + panic!("temp dir"); 834 + }; 835 + let path = write_to_temp(&document, dir.path(), "cube.step"); 836 + let Ok(text) = std::fs::read_to_string(&path) else { 837 + panic!("read back"); 838 + }; 839 + let swapped = text.replace( 840 + "('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }')", 841 + "('GARBAGE_SCHEMA','AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 3 1 4 }')", 842 + ); 843 + let foreign = dir.path().join("cube_multi.step"); 844 + let Ok(()) = std::fs::write(&foreign, swapped.as_bytes()) else { 845 + panic!("write swapped"); 846 + }; 847 + assert!(matches!( 848 + read(&foreign), 849 + Err(StepError::SchemaMismatch { 850 + found: StepSchema::Ap242E2, 851 + .. 852 + }) 853 + )); 854 + } 855 + 856 + #[test] 857 + fn corrupt_sidecar_imports_dumb_body() { 858 + let document = document("cube", rectangle_sketch(), 4.0); 859 + let Ok(dir) = tempfile::tempdir() else { 860 + panic!("temp dir"); 861 + }; 862 + let path = write_to_temp(&document, dir.path(), "part.step"); 863 + let mut labels = path.clone().into_os_string(); 864 + labels.push(".labels"); 865 + let Ok(()) = std::fs::write(PathBuf::from(labels), b"not valid ron @@@ {") else { 866 + panic!("overwrite sidecar with garbage"); 867 + }; 868 + let Ok(imported) = read(&path) else { 869 + panic!("a corrupt sidecar still imports best-effort"); 870 + }; 871 + let Ok(solid) = body_of(&imported) else { 872 + panic!("one body"); 873 + }; 874 + assert!(is_dumb(&solid), "a corrupt sidecar yields a dumb body"); 875 + } 876 + 877 + #[test] 878 + fn imported_body_survives_folder_round_trip() { 879 + let wedge = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/goldens/inbound/wedge.step"); 880 + let Ok(imported) = read(&wedge) else { 881 + panic!("import wedge"); 882 + }; 883 + let Ok(expected) = body_of(&imported) else { 884 + panic!("imported wedge carries one body"); 885 + }; 886 + let Ok(dir) = tempfile::tempdir() else { 887 + panic!("temp dir"); 888 + }; 889 + let folder = bone_document::DocumentFolder::new(dir.path().join("wedge.bone")); 890 + let Ok(()) = bone_document::save(&imported, &folder) else { 891 + panic!("save imported document"); 892 + }; 893 + let Ok(loaded) = bone_document::load(&folder) else { 894 + panic!("load imported document"); 895 + }; 896 + assert_eq!( 897 + loaded.imported_bodies().count(), 898 + 1, 899 + "the body survives on disk" 900 + ); 901 + let Ok(restored) = body_of(&loaded) else { 902 + panic!("the reloaded document still resolves its imported body"); 903 + }; 904 + assert_eq!( 905 + face_labels(&expected), 906 + face_labels(&restored), 907 + "a persisted body keeps its labels across a folder round trip" 908 + ); 909 + assert!(restored.validate(TOL).is_ok()); 910 + } 911 + 341 912 #[test] 342 913 fn cylinder_export_writes_step_and_sidecar() { 343 914 let document = document("cyl", circle_sketch(5.0), 8.0); 344 915 let Ok(dir) = tempfile::tempdir() else { 345 916 panic!("temp dir"); 346 917 }; 347 - let path = dir.path().join("cyl.step"); 348 - let Ok(()) = write(&document, &path, StepSchema::Ap214) else { 349 - panic!("write step"); 350 - }; 918 + let path = write_to_temp(&document, dir.path(), "cyl.step"); 351 919 assert!(path.exists()); 352 920 let mut labels = path.clone().into_os_string(); 353 921 labels.push(".labels"); 354 - assert!(std::path::PathBuf::from(labels).exists()); 922 + assert!(PathBuf::from(labels).exists()); 923 + } 924 + 925 + #[test] 926 + fn step_keywords_in_document_name_round_trip() { 927 + ["endsec", "file_schema", "header"] 928 + .iter() 929 + .for_each(|keyword| { 930 + let name = format!("{keyword} bracket"); 931 + let document = document(&name, rectangle_sketch(), 4.0); 932 + let Ok(dir) = tempfile::tempdir() else { 933 + panic!("temp dir"); 934 + }; 935 + let path = write_to_temp(&document, dir.path(), "part.step"); 936 + let Ok(imported) = read(&path) else { 937 + panic!("a name carrying '{keyword}' must not derail header parsing"); 938 + }; 939 + let Ok(solid) = body_of(&imported) else { 940 + panic!("{keyword}: imports one body"); 941 + }; 942 + assert!( 943 + !is_dumb(&solid), 944 + "{keyword}: a header keyword inside the name does not block sidecar reattach" 945 + ); 946 + }); 947 + } 948 + 949 + fn role_z_extents(solid: &BrepSolid) -> std::collections::BTreeMap<String, (String, String)> { 950 + use std::collections::BTreeMap; 951 + let loop_edges: BTreeMap<_, Vec<_>> = solid 952 + .iter_loops() 953 + .map(|l| (l.id(), l.edges().to_vec())) 954 + .collect(); 955 + let edge_verts: BTreeMap<_, _> = solid.iter_edges().map(|e| (e.id(), e.vertices())).collect(); 956 + let vert_z: BTreeMap<_, f64> = solid 957 + .iter_vertices() 958 + .map(|v| (v.id(), v.position().coords_mm().2)) 959 + .collect(); 960 + let mm = |z: f64| format!("{z:.3}"); 961 + solid 962 + .iter_faces() 963 + .map(|face| { 964 + let zs: Vec<f64> = face 965 + .loops() 966 + .iter() 967 + .flat_map(|lid| loop_edges.get(lid).into_iter().flatten()) 968 + .flat_map(|eid| edge_verts.get(eid).into_iter().flatten()) 969 + .filter_map(|vid| vert_z.get(vid).copied()) 970 + .collect(); 971 + let lo = zs.iter().copied().fold(f64::INFINITY, f64::min); 972 + let hi = zs.iter().copied().fold(f64::NEG_INFINITY, f64::max); 973 + (format!("{:?}", face.label().role), (mm(lo), mm(hi))) 974 + }) 975 + .collect() 976 + } 977 + 978 + #[test] 979 + fn imported_labels_bind_to_original_geometry() { 980 + let seed = document("cube", rectangle_sketch(), 4.0); 981 + let Ok(expected) = body_of(&seed) else { 982 + panic!("one body"); 983 + }; 984 + let Ok(dir) = tempfile::tempdir() else { 985 + panic!("temp dir"); 986 + }; 987 + let path = write_to_temp(&seed, dir.path(), "part.step"); 988 + let Ok(imported) = read(&path) else { 989 + panic!("read step"); 990 + }; 991 + let Ok(round) = body_of(&imported) else { 992 + panic!("one body"); 993 + }; 994 + let after = role_z_extents(&round); 995 + assert_eq!( 996 + role_z_extents(&expected), 997 + after, 998 + "each label binds to the same geometry before and after import, not merely the same label set" 999 + ); 1000 + assert_eq!( 1001 + after.get("StartCap").map(|extent| extent.0.as_str()), 1002 + Some("0.000"), 1003 + "the StartCap label lands on the z=0 face" 1004 + ); 1005 + assert_eq!( 1006 + after.get("EndCap").map(|extent| extent.0.as_str()), 1007 + Some("4.000"), 1008 + "the EndCap label lands on the z=depth face" 1009 + ); 355 1010 }