Another project
0

Configure Feed

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

feat(interop): step rw facade crate

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

author
Lewis
date (May 31, 2026, 4:15 PM +0300) commit ef566bfe parent d2485139 change-id kqzpozlw
+670 -13
+208 -13
Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 + name = "Inflector" 7 + version = "0.11.4" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 10 + dependencies = [ 11 + "lazy_static", 12 + "regex", 13 + ] 14 + 15 + [[package]] 6 16 name = "ab_glyph" 7 17 version = "0.2.32" 8 18 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 414 424 version = "0.0.0" 415 425 dependencies = [ 416 426 "blake3", 427 + "bone-kernel", 417 428 "bone-solver", 418 429 "bone-types", 419 430 "insta", ··· 428 439 ] 429 440 430 441 [[package]] 442 + name = "bone-interop" 443 + version = "0.0.0" 444 + dependencies = [ 445 + "bone-document", 446 + "bone-kernel", 447 + "bone-types", 448 + "slotmap", 449 + "tempfile", 450 + "thiserror 2.0.18", 451 + ] 452 + 453 + [[package]] 431 454 name = "bone-kernel" 432 455 version = "0.0.0" 433 456 dependencies = [ 457 + "blake3", 434 458 "bone-types", 435 459 "insta", 436 460 "proptest", ··· 440 464 "thiserror 2.0.18", 441 465 "truck-meshalgo", 442 466 "truck-modeling", 467 + "truck-stepio", 468 + "truck-topology", 443 469 "uom", 444 470 ] 445 471 ··· 635 661 ] 636 662 637 663 [[package]] 664 + name = "chrono" 665 + version = "0.4.44" 666 + source = "registry+https://github.com/rust-lang/crates.io-index" 667 + checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" 668 + dependencies = [ 669 + "iana-time-zone", 670 + "js-sys", 671 + "num-traits", 672 + "wasm-bindgen", 673 + "windows-link", 674 + ] 675 + 676 + [[package]] 638 677 name = "codespan-reporting" 639 678 version = "0.13.1" 640 679 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 796 835 version = "0.2.1" 797 836 source = "registry+https://github.com/rust-lang/crates.io-index" 798 837 checksum = "930c7171c8df9fb1782bdf9b918ed9ed2d33d1d22300abb754f9085bc48bf8e8" 838 + 839 + [[package]] 840 + name = "derive-new" 841 + version = "0.5.9" 842 + source = "registry+https://github.com/rust-lang/crates.io-index" 843 + checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" 844 + dependencies = [ 845 + "proc-macro2", 846 + "quote", 847 + "syn 1.0.109", 848 + ] 799 849 800 850 [[package]] 801 851 name = "derive_more" ··· 1538 1588 checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" 1539 1589 1540 1590 [[package]] 1591 + name = "iana-time-zone" 1592 + version = "0.1.65" 1593 + source = "registry+https://github.com/rust-lang/crates.io-index" 1594 + checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" 1595 + dependencies = [ 1596 + "android_system_properties", 1597 + "core-foundation-sys", 1598 + "iana-time-zone-haiku", 1599 + "js-sys", 1600 + "log", 1601 + "wasm-bindgen", 1602 + "windows-core", 1603 + ] 1604 + 1605 + [[package]] 1606 + name = "iana-time-zone-haiku" 1607 + version = "0.1.2" 1608 + source = "registry+https://github.com/rust-lang/crates.io-index" 1609 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 1610 + dependencies = [ 1611 + "cc", 1612 + ] 1613 + 1614 + [[package]] 1541 1615 name = "icu_collections" 1542 1616 version = "2.2.0" 1543 1617 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1705 1779 "proc-macro2", 1706 1780 "quote", 1707 1781 "syn 1.0.109", 1782 + ] 1783 + 1784 + [[package]] 1785 + name = "itertools" 1786 + version = "0.10.5" 1787 + source = "registry+https://github.com/rust-lang/crates.io-index" 1788 + checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 1789 + dependencies = [ 1790 + "either", 1708 1791 ] 1709 1792 1710 1793 [[package]] ··· 2008 2091 ] 2009 2092 2010 2093 [[package]] 2094 + name = "minimal-lexical" 2095 + version = "0.2.1" 2096 + source = "registry+https://github.com/rust-lang/crates.io-index" 2097 + checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 2098 + 2099 + [[package]] 2011 2100 name = "miniz_oxide" 2012 2101 version = "0.8.9" 2013 2102 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2159 2248 ] 2160 2249 2161 2250 [[package]] 2251 + name = "nom" 2252 + version = "7.1.3" 2253 + source = "registry+https://github.com/rust-lang/crates.io-index" 2254 + checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 2255 + dependencies = [ 2256 + "memchr", 2257 + "minimal-lexical", 2258 + ] 2259 + 2260 + [[package]] 2162 2261 name = "nu-ansi-term" 2163 2262 version = "0.50.3" 2164 2263 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2243 2342 source = "registry+https://github.com/rust-lang/crates.io-index" 2244 2343 checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" 2245 2344 dependencies = [ 2246 - "proc-macro-crate", 2345 + "proc-macro-crate 3.5.0", 2247 2346 "proc-macro2", 2248 2347 "quote", 2249 2348 "syn 2.0.117", ··· 2792 2891 "interpol", 2793 2892 "num_cpus", 2794 2893 "raw-cpuid", 2894 + ] 2895 + 2896 + [[package]] 2897 + name = "proc-macro-crate" 2898 + version = "1.3.1" 2899 + source = "registry+https://github.com/rust-lang/crates.io-index" 2900 + checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" 2901 + dependencies = [ 2902 + "once_cell", 2903 + "toml_edit 0.19.15", 2795 2904 ] 2796 2905 2797 2906 [[package]] ··· 2800 2909 source = "registry+https://github.com/rust-lang/crates.io-index" 2801 2910 checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" 2802 2911 dependencies = [ 2803 - "toml_edit", 2912 + "toml_edit 0.25.11+spec-1.1.0", 2804 2913 ] 2805 2914 2806 2915 [[package]] ··· 3113 3222 ] 3114 3223 3115 3224 [[package]] 3225 + name = "regex" 3226 + version = "1.12.3" 3227 + source = "registry+https://github.com/rust-lang/crates.io-index" 3228 + checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" 3229 + dependencies = [ 3230 + "aho-corasick", 3231 + "memchr", 3232 + "regex-automata", 3233 + "regex-syntax", 3234 + ] 3235 + 3236 + [[package]] 3116 3237 name = "regex-automata" 3117 3238 version = "0.4.14" 3118 3239 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3200 3321 "libc", 3201 3322 "linux-raw-sys 0.12.1", 3202 3323 "windows-sys 0.61.2", 3324 + ] 3325 + 3326 + [[package]] 3327 + name = "ruststep" 3328 + version = "0.4.0" 3329 + source = "registry+https://github.com/rust-lang/crates.io-index" 3330 + checksum = "5df866eb24b48dd4bb8a3cb0dfeb3a25ff2c251ed93aae1cbfc03eb8ee3c7dcc" 3331 + dependencies = [ 3332 + "Inflector", 3333 + "derive-new", 3334 + "derive_more", 3335 + "itertools 0.10.5", 3336 + "nom", 3337 + "ruststep-derive", 3338 + "serde", 3339 + "thiserror 1.0.69", 3340 + ] 3341 + 3342 + [[package]] 3343 + name = "ruststep-derive" 3344 + version = "0.4.0" 3345 + source = "registry+https://github.com/rust-lang/crates.io-index" 3346 + checksum = "81bfdd035ae42e977d18e3197065392784566ef230eda4241d15799ea2154e84" 3347 + dependencies = [ 3348 + "Inflector", 3349 + "proc-macro-crate 1.3.1", 3350 + "proc-macro-error", 3351 + "proc-macro2", 3352 + "quote", 3353 + "syn 2.0.117", 3203 3354 ] 3204 3355 3205 3356 [[package]] ··· 3677 3828 3678 3829 [[package]] 3679 3830 name = "toml_datetime" 3831 + version = "0.6.11" 3832 + source = "registry+https://github.com/rust-lang/crates.io-index" 3833 + checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 3834 + 3835 + [[package]] 3836 + name = "toml_datetime" 3680 3837 version = "1.1.1+spec-1.1.0" 3681 3838 source = "registry+https://github.com/rust-lang/crates.io-index" 3682 3839 checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" ··· 3686 3843 3687 3844 [[package]] 3688 3845 name = "toml_edit" 3846 + version = "0.19.15" 3847 + source = "registry+https://github.com/rust-lang/crates.io-index" 3848 + checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 3849 + dependencies = [ 3850 + "indexmap", 3851 + "toml_datetime 0.6.11", 3852 + "winnow 0.5.40", 3853 + ] 3854 + 3855 + [[package]] 3856 + name = "toml_edit" 3689 3857 version = "0.25.11+spec-1.1.0" 3690 3858 source = "registry+https://github.com/rust-lang/crates.io-index" 3691 3859 checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" 3692 3860 dependencies = [ 3693 3861 "indexmap", 3694 - "toml_datetime", 3862 + "toml_datetime 1.1.1+spec-1.1.0", 3695 3863 "toml_parser", 3696 - "winnow", 3864 + "winnow 1.0.1", 3697 3865 ] 3698 3866 3699 3867 [[package]] ··· 3702 3870 source = "registry+https://github.com/rust-lang/crates.io-index" 3703 3871 checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" 3704 3872 dependencies = [ 3705 - "winnow", 3873 + "winnow 1.0.1", 3706 3874 ] 3707 3875 3708 3876 [[package]] ··· 3823 3991 dependencies = [ 3824 3992 "array-macro", 3825 3993 "derive_more", 3826 - "itertools", 3994 + "itertools 0.13.0", 3827 3995 "rayon", 3828 3996 "rustc-hash 2.1.2", 3829 3997 "spade", ··· 3858 4026 dependencies = [ 3859 4027 "array-macro", 3860 4028 "bytemuck", 3861 - "itertools", 4029 + "itertools 0.13.0", 3862 4030 "rustc-hash 2.1.2", 3863 4031 "serde", 3864 4032 "thiserror 1.0.69", 3865 4033 "truck-base", 3866 4034 "truck-geotrait", 4035 + ] 4036 + 4037 + [[package]] 4038 + name = "truck-stepio" 4039 + version = "0.3.0" 4040 + source = "registry+https://github.com/rust-lang/crates.io-index" 4041 + checksum = "2d29cf5b0dec4b927e80b5dca26ce3ca69601c77ea5b9c953e30bfc3e123d189" 4042 + dependencies = [ 4043 + "chrono", 4044 + "derive_more", 4045 + "ruststep", 4046 + "serde", 4047 + "truck-derivers", 4048 + "truck-geometry", 4049 + "truck-geotrait", 4050 + "truck-modeling", 4051 + "truck-polymesh", 4052 + "truck-topology", 3867 4053 ] 3868 4054 3869 4055 [[package]] ··· 4673 4859 4674 4860 [[package]] 4675 4861 name = "winnow" 4862 + version = "0.5.40" 4863 + source = "registry+https://github.com/rust-lang/crates.io-index" 4864 + checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" 4865 + dependencies = [ 4866 + "memchr", 4867 + ] 4868 + 4869 + [[package]] 4870 + name = "winnow" 4676 4871 version = "1.0.1" 4677 4872 source = "registry+https://github.com/rust-lang/crates.io-index" 4678 4873 checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" ··· 4901 5096 "uds_windows", 4902 5097 "uuid", 4903 5098 "windows-sys 0.61.2", 4904 - "winnow", 5099 + "winnow 1.0.1", 4905 5100 "zbus_macros", 4906 5101 "zbus_names", 4907 5102 "zvariant", ··· 4913 5108 source = "registry+https://github.com/rust-lang/crates.io-index" 4914 5109 checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" 4915 5110 dependencies = [ 4916 - "proc-macro-crate", 5111 + "proc-macro-crate 3.5.0", 4917 5112 "proc-macro2", 4918 5113 "quote", 4919 5114 "syn 2.0.117", ··· 4929 5124 checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" 4930 5125 dependencies = [ 4931 5126 "serde", 4932 - "winnow", 5127 + "winnow 1.0.1", 4933 5128 "zvariant", 4934 5129 ] 4935 5130 ··· 5030 5225 "endi", 5031 5226 "enumflags2", 5032 5227 "serde", 5033 - "winnow", 5228 + "winnow 1.0.1", 5034 5229 "zvariant_derive", 5035 5230 "zvariant_utils", 5036 5231 ] ··· 5041 5236 source = "registry+https://github.com/rust-lang/crates.io-index" 5042 5237 checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" 5043 5238 dependencies = [ 5044 - "proc-macro-crate", 5239 + "proc-macro-crate 3.5.0", 5045 5240 "proc-macro2", 5046 5241 "quote", 5047 5242 "syn 2.0.117", ··· 5058 5253 "quote", 5059 5254 "serde", 5060 5255 "syn 2.0.117", 5061 - "winnow", 5256 + "winnow 1.0.1", 5062 5257 ]
+2
Cargo.toml
··· 5 5 "crates/bone-kernel", 6 6 "crates/bone-solver", 7 7 "crates/bone-document", 8 + "crates/bone-interop", 8 9 "crates/bone-text", 9 10 "crates/bone-render", 10 11 "crates/bone-ui", ··· 35 36 bone-kernel = { path = "crates/bone-kernel" } 36 37 bone-solver = { path = "crates/bone-solver" } 37 38 bone-document = { path = "crates/bone-document" } 39 + bone-interop = { path = "crates/bone-interop" } 38 40 bone-render = { path = "crates/bone-render" } 39 41 bone-text = { path = "crates/bone-text" } 40 42 bone-ui = { path = "crates/bone-ui" }
+19
crates/bone-interop/Cargo.toml
··· 1 + [package] 2 + name = "bone-interop" 3 + version.workspace = true 4 + edition.workspace = true 5 + license.workspace = true 6 + rust-version.workspace = true 7 + 8 + [dependencies] 9 + bone-types = { workspace = true } 10 + bone-kernel = { workspace = true } 11 + bone-document = { workspace = true } 12 + thiserror = { workspace = true } 13 + 14 + [dev-dependencies] 15 + slotmap = { workspace = true } 16 + tempfile = { workspace = true } 17 + 18 + [lints] 19 + workspace = true
+3
crates/bone-interop/src/lib.rs
··· 1 + pub mod step; 2 + 3 + pub use step::{ImportOutcome, StepError, read, write};
+191
crates/bone-interop/src/step.rs
··· 1 + use core::fmt::Write as _; 2 + use std::io; 3 + use std::path::{Path, PathBuf}; 4 + 5 + use bone_document::LabelSidecar; 6 + use bone_kernel::{BrepError, BrepSolid}; 7 + use bone_types::{ 8 + FaceRole, FeatureId, StepFileHeader, StepFileName, StepOrganization, StepOriginatingSystem, 9 + StepSchema, 10 + }; 11 + 12 + const PINNED_TIMESTAMP: &str = "1970-01-01T00:00:00"; 13 + const ORIGINATING_SYSTEM: &str = concat!("Bone ", env!("CARGO_PKG_VERSION")); 14 + 15 + const fn schema_token(schema: StepSchema) -> &'static str { 16 + match schema { 17 + StepSchema::Ap214 => "AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }", 18 + StepSchema::Ap242E2 => "AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 3 1 4 }", 19 + } 20 + } 21 + 22 + #[derive(Debug, thiserror::Error)] 23 + pub enum StepError { 24 + #[error("schema {0} is unsupported for export; only AP214 is emitted")] 25 + SchemaUnsupported(StepSchema), 26 + #[error("io at {path}: {source}")] 27 + Io { 28 + path: PathBuf, 29 + #[source] 30 + source: io::Error, 31 + }, 32 + #[error("step geometry: {0}")] 33 + Geometry(#[from] BrepError), 34 + #[error("label sidecar at {path}: {source}")] 35 + Sidecar { 36 + path: PathBuf, 37 + #[source] 38 + source: Box<bone_document::RonError>, 39 + }, 40 + } 41 + 42 + pub enum ImportOutcome { 43 + Labeled(BrepSolid), 44 + Imported(BrepSolid), 45 + } 46 + 47 + impl ImportOutcome { 48 + #[must_use] 49 + pub fn solid(&self) -> &BrepSolid { 50 + match self { 51 + Self::Labeled(solid) | Self::Imported(solid) => solid, 52 + } 53 + } 54 + 55 + #[must_use] 56 + pub fn into_solid(self) -> BrepSolid { 57 + match self { 58 + Self::Labeled(solid) | Self::Imported(solid) => solid, 59 + } 60 + } 61 + 62 + #[must_use] 63 + pub fn is_labeled(&self) -> bool { 64 + matches!(self, Self::Labeled(_)) 65 + } 66 + } 67 + 68 + pub fn write(solid: &BrepSolid, path: &Path, schema: StepSchema) -> Result<(), StepError> { 69 + if schema != StepSchema::Ap214 { 70 + return Err(StepError::SchemaUnsupported(schema)); 71 + } 72 + let body = solid.to_step_body()?; 73 + let file_name = path 74 + .file_name() 75 + .and_then(|name| name.to_str()) 76 + .unwrap_or(""); 77 + let document = envelope(&render_header(&export_header(file_name, schema)), &body); 78 + write_file(path, document.as_bytes())?; 79 + 80 + let labels_path = sidecar_path(path); 81 + let sidecar = LabelSidecar::capture(solid) 82 + .to_ron() 83 + .map_err(|source| StepError::Sidecar { 84 + path: labels_path.clone(), 85 + source: Box::new(source), 86 + })?; 87 + write_file(&labels_path, sidecar.as_bytes()) 88 + } 89 + 90 + pub fn read(path: &Path, feature: FeatureId) -> Result<ImportOutcome, StepError> { 91 + let text = read_file(path)?; 92 + let sidecar = read_sidecar(path)?; 93 + let solid = BrepSolid::from_step( 94 + &text, 95 + feature, 96 + sidecar.as_ref().map(|side| (side.solid(), side.reattach())), 97 + )?; 98 + let labeled = solid 99 + .iter_faces() 100 + .any(|face| !matches!(face.label().role, FaceRole::Imported { .. })); 101 + Ok(if labeled { 102 + ImportOutcome::Labeled(solid) 103 + } else { 104 + ImportOutcome::Imported(solid) 105 + }) 106 + } 107 + 108 + fn envelope(header: &str, body: &str) -> String { 109 + format!("ISO-10303-21;\n{header}DATA;\n{body}ENDSEC;\nEND-ISO-10303-21;\n") 110 + } 111 + 112 + fn export_header(file_name: &str, schema: StepSchema) -> StepFileHeader { 113 + StepFileHeader { 114 + schema, 115 + originating_system: StepOriginatingSystem::new(ORIGINATING_SYSTEM), 116 + organization: StepOrganization::new(""), 117 + file_name: StepFileName::new(file_name), 118 + } 119 + } 120 + 121 + fn render_header(header: &StepFileHeader) -> String { 122 + format!( 123 + "HEADER;\nFILE_DESCRIPTION(('Bone geometry'),'2;1');\nFILE_NAME('{name}','{PINNED_TIMESTAMP}',(''),('{organization}'),'','{system}','');\nFILE_SCHEMA(('{schema}'));\nENDSEC;\n", 124 + name = escape_step_string(header.file_name.as_str()), 125 + organization = escape_step_string(header.organization.as_str()), 126 + system = escape_step_string(header.originating_system.as_str()), 127 + schema = schema_token(header.schema), 128 + ) 129 + } 130 + 131 + fn escape_step_string(value: &str) -> String { 132 + value.chars().map(escape_step_char).collect() 133 + } 134 + 135 + fn escape_step_char(ch: char) -> String { 136 + match ch { 137 + '\'' => "''".to_string(), 138 + '\\' => "\\\\".to_string(), 139 + c if c.is_ascii_graphic() || c == ' ' => c.to_string(), 140 + c => { 141 + let mut units = [0u16; 2]; 142 + c.encode_utf16(&mut units) 143 + .iter() 144 + .fold(String::from("\\X2\\"), |mut acc, unit| { 145 + let _ = write!(acc, "{unit:04X}"); 146 + acc 147 + }) 148 + + "\\X0\\" 149 + } 150 + } 151 + } 152 + 153 + fn sidecar_path(step: &Path) -> PathBuf { 154 + let mut name = step.as_os_str().to_os_string(); 155 + name.push(".labels"); 156 + PathBuf::from(name) 157 + } 158 + 159 + fn write_file(path: &Path, bytes: &[u8]) -> Result<(), StepError> { 160 + if let Some(parent) = path.parent() { 161 + std::fs::create_dir_all(parent).map_err(|source| StepError::Io { 162 + path: parent.to_path_buf(), 163 + source, 164 + })?; 165 + } 166 + std::fs::write(path, bytes).map_err(|source| StepError::Io { 167 + path: path.to_path_buf(), 168 + source, 169 + }) 170 + } 171 + 172 + fn read_file(path: &Path) -> Result<String, StepError> { 173 + std::fs::read_to_string(path).map_err(|source| StepError::Io { 174 + path: path.to_path_buf(), 175 + source, 176 + }) 177 + } 178 + 179 + fn read_sidecar(step: &Path) -> Result<Option<LabelSidecar>, StepError> { 180 + let path = sidecar_path(step); 181 + match std::fs::read_to_string(&path) { 182 + Ok(text) => LabelSidecar::from_ron(&text) 183 + .map(Some) 184 + .map_err(|source| StepError::Sidecar { 185 + path, 186 + source: Box::new(source), 187 + }), 188 + Err(ref source) if source.kind() == io::ErrorKind::NotFound => Ok(None), 189 + Err(source) => Err(StepError::Io { path, source }), 190 + } 191 + }
+247
crates/bone-interop/tests/step.rs
··· 1 + use bone_interop::{StepError, read, write}; 2 + use bone_kernel::{ 3 + BrepFace, BrepSolid, Circle2, Curve2Kind, ExtrudeDirection, ExtrudeEndCondition, 4 + ExtrudeFeature, ExtrudeProfile, ExtrudeSense, Line2, MergeResult, ProfileEdge, ProfileLoop, 5 + evaluate_extrude, 6 + }; 7 + use bone_types::{ 8 + FaceLabel, FeatureId, Length, Plane3, Point2, PositiveLength, SketchEntityId, SketchId, 9 + StepSchema, Tolerance, UnitVec3, millimeter, 10 + }; 11 + use slotmap::{Key, SlotMap}; 12 + 13 + const TOL: Tolerance = Tolerance::new(1.0e-9); 14 + 15 + fn xy_plane() -> Plane3 { 16 + let Ok(plane) = Plane3::new( 17 + bone_types::Point3::origin(), 18 + UnitVec3::x_axis(), 19 + UnitVec3::y_axis(), 20 + TOL, 21 + ) else { 22 + panic!("orthonormal axes"); 23 + }; 24 + plane 25 + } 26 + 27 + fn blind(depth_mm: f64) -> ExtrudeFeature { 28 + let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(depth_mm)) else { 29 + panic!("positive depth"); 30 + }; 31 + ExtrudeFeature { 32 + sketch: SketchId::null(), 33 + direction: ExtrudeDirection::Normal { 34 + sense: ExtrudeSense::Forward, 35 + }, 36 + end_condition: ExtrudeEndCondition::Blind { depth }, 37 + draft: None, 38 + thin_wall: None, 39 + merge_result: MergeResult::Merge, 40 + } 41 + } 42 + 43 + fn rectangle(entities: &mut SlotMap<SketchEntityId, ()>) -> ProfileLoop { 44 + let point = |x: f64, y: f64| Point2::from_mm(x, y); 45 + let line = |a: Point2, b: Point2| { 46 + let Ok(segment) = Line2::new(a, b, TOL) else { 47 + panic!("distinct endpoints"); 48 + }; 49 + Curve2Kind::Line(segment) 50 + }; 51 + let corners = [ 52 + point(0.0, 0.0), 53 + point(4.0, 0.0), 54 + point(4.0, 2.0), 55 + point(0.0, 2.0), 56 + ]; 57 + let edges = (0..4) 58 + .map(|index| { 59 + ProfileEdge::new( 60 + line(corners[index], corners[(index + 1) % 4]), 61 + entities.insert(()), 62 + entities.insert(()), 63 + ) 64 + }) 65 + .collect(); 66 + ProfileLoop::Open(edges) 67 + } 68 + 69 + fn cylinder(entities: &mut SlotMap<SketchEntityId, ()>) -> ProfileLoop { 70 + let Ok(disk) = Circle2::new( 71 + Point2::from_mm(0.0, 0.0), 72 + Length::new::<millimeter>(5.0), 73 + TOL, 74 + ) else { 75 + panic!("positive radius"); 76 + }; 77 + ProfileLoop::Closed { 78 + curve: Curve2Kind::Circle(disk), 79 + curve_entity: entities.insert(()), 80 + } 81 + } 82 + 83 + fn build(loops: Vec<ProfileLoop>, depth_mm: f64) -> BrepSolid { 84 + let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key(); 85 + let profile = ExtrudeProfile::new(xy_plane(), loops); 86 + let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &blind(depth_mm)) else { 87 + panic!("profile extrudes"); 88 + }; 89 + solid 90 + } 91 + 92 + fn cube() -> BrepSolid { 93 + let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key(); 94 + build(vec![rectangle(&mut entities)], 5.0) 95 + } 96 + 97 + fn face_labels(solid: &BrepSolid) -> Vec<FaceLabel> { 98 + solid.iter_faces().map(BrepFace::label).collect() 99 + } 100 + 101 + fn import_feature() -> FeatureId { 102 + let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key(); 103 + features.insert(()) 104 + } 105 + 106 + fn donut(entities: &mut SlotMap<SketchEntityId, ()>) -> Vec<ProfileLoop> { 107 + let ring = |radius_mm: f64, entities: &mut SlotMap<SketchEntityId, ()>| { 108 + let Ok(disk) = Circle2::new(Point2::from_mm(0.0, 0.0), Length::new::<millimeter>(radius_mm), TOL) 109 + else { 110 + panic!("positive radius"); 111 + }; 112 + ProfileLoop::Closed { 113 + curve: Curve2Kind::Circle(disk), 114 + curve_entity: entities.insert(()), 115 + } 116 + }; 117 + vec![ring(10.0, entities), ring(4.0, entities)] 118 + } 119 + 120 + fn step_round_trip_keeps_labels(solid: &BrepSolid) { 121 + let Ok(dir) = tempfile::tempdir() else { 122 + panic!("temp dir"); 123 + }; 124 + let path = dir.path().join("part.step"); 125 + let Ok(()) = write(solid, &path, StepSchema::Ap214) else { 126 + panic!("write step"); 127 + }; 128 + let Ok(outcome) = read(&path, import_feature()) else { 129 + panic!("read step"); 130 + }; 131 + assert!(outcome.is_labeled(), "matching sidecar restores labels"); 132 + assert_eq!(face_labels(solid), face_labels(outcome.solid())); 133 + assert!(outcome.solid().validate(TOL).is_ok()); 134 + } 135 + 136 + #[test] 137 + fn cube_step_round_trip_keeps_labels() { 138 + step_round_trip_keeps_labels(&cube()); 139 + } 140 + 141 + #[test] 142 + fn cylinder_step_round_trip_keeps_labels() { 143 + let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key(); 144 + let solid = build(vec![cylinder(&mut entities)], 8.0); 145 + step_round_trip_keeps_labels(&solid); 146 + } 147 + 148 + #[test] 149 + fn donut_step_round_trip_keeps_labels() { 150 + let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key(); 151 + let solid = build(donut(&mut entities), 6.0); 152 + step_round_trip_keeps_labels(&solid); 153 + } 154 + 155 + #[test] 156 + fn apostrophe_in_file_name_round_trips() { 157 + let solid = cube(); 158 + let Ok(dir) = tempfile::tempdir() else { 159 + panic!("temp dir"); 160 + }; 161 + let path = dir.path().join("nel's bracket.step"); 162 + let Ok(()) = write(&solid, &path, StepSchema::Ap214) else { 163 + panic!("write step"); 164 + }; 165 + let Ok(outcome) = read(&path, import_feature()) else { 166 + panic!("an apostrophe in the file name must not break the header"); 167 + }; 168 + assert!(outcome.is_labeled(), "matching sidecar restores labels"); 169 + assert_eq!(face_labels(&solid), face_labels(outcome.solid())); 170 + } 171 + 172 + #[test] 173 + fn step_export_is_byte_deterministic() { 174 + let solid = cube(); 175 + let Ok(dir) = tempfile::tempdir() else { 176 + panic!("temp dir"); 177 + }; 178 + let path = dir.path().join("part.step"); 179 + let Ok(()) = write(&solid, &path, StepSchema::Ap214) else { 180 + panic!("write first"); 181 + }; 182 + let Ok(a) = std::fs::read(&path) else { 183 + panic!("read first"); 184 + }; 185 + let Ok(()) = write(&solid, &path, StepSchema::Ap214) else { 186 + panic!("write second"); 187 + }; 188 + let Ok(b) = std::fs::read(&path) else { 189 + panic!("read second"); 190 + }; 191 + assert_eq!(a, b, "same solid and path write byte-identical step"); 192 + } 193 + 194 + #[test] 195 + fn missing_sidecar_imports_dumb_body() { 196 + let solid = cube(); 197 + let Ok(dir) = tempfile::tempdir() else { 198 + panic!("temp dir"); 199 + }; 200 + let path = dir.path().join("part.step"); 201 + let Ok(()) = write(&solid, &path, StepSchema::Ap214) else { 202 + panic!("write step"); 203 + }; 204 + let mut labels = dir.path().join("part.step").into_os_string(); 205 + labels.push(".labels"); 206 + let Ok(()) = std::fs::remove_file(std::path::PathBuf::from(labels)) else { 207 + panic!("remove sidecar"); 208 + }; 209 + let Ok(outcome) = read(&path, import_feature()) else { 210 + panic!("read step"); 211 + }; 212 + assert!(!outcome.is_labeled(), "no sidecar yields a dumb body"); 213 + assert_eq!( 214 + outcome.solid().iter_faces().count(), 215 + solid.iter_faces().count() 216 + ); 217 + } 218 + 219 + #[test] 220 + fn ap242_export_is_unsupported() { 221 + let solid = cube(); 222 + let Ok(dir) = tempfile::tempdir() else { 223 + panic!("temp dir"); 224 + }; 225 + let path = dir.path().join("part.step"); 226 + assert!(matches!( 227 + write(&solid, &path, StepSchema::Ap242E2), 228 + Err(StepError::SchemaUnsupported(StepSchema::Ap242E2)) 229 + )); 230 + } 231 + 232 + #[test] 233 + fn cylinder_export_writes_step_and_sidecar() { 234 + let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key(); 235 + let solid = build(vec![cylinder(&mut entities)], 8.0); 236 + let Ok(dir) = tempfile::tempdir() else { 237 + panic!("temp dir"); 238 + }; 239 + let path = dir.path().join("cyl.step"); 240 + let Ok(()) = write(&solid, &path, StepSchema::Ap214) else { 241 + panic!("write step"); 242 + }; 243 + assert!(path.exists()); 244 + let mut labels = path.clone().into_os_string(); 245 + labels.push(".labels"); 246 + assert!(std::path::PathBuf::from(labels).exists()); 247 + }