From bbc2ed48a57512e1d122fed8edc15a3953fbc94b Mon Sep 17 00:00:00 2001 From: Lewis Date: Mon, 8 Jun 2026 12:49:57 +0300 Subject: [PATCH] feat(interop): read step files into documents with schema detection Lewis: May this revision serve well! --- Cargo.lock | 1 + crates/bone-interop/Cargo.toml | 1 + crates/bone-interop/src/lib.rs | 2 +- crates/bone-interop/src/step.rs | 229 +++++++++++++++++++++++++------- 4 files changed, 181 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39063a4..517c4ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,7 @@ dependencies = [ "slotmap", "tempfile", "thiserror 2.0.18", + "tracing", ] [[package]] diff --git a/crates/bone-interop/Cargo.toml b/crates/bone-interop/Cargo.toml index 9f7657a..d0cd0d6 100644 --- a/crates/bone-interop/Cargo.toml +++ b/crates/bone-interop/Cargo.toml @@ -10,6 +10,7 @@ bone-types = { workspace = true } bone-kernel = { workspace = true } bone-document = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } [dev-dependencies] slotmap = { workspace = true } diff --git a/crates/bone-interop/src/lib.rs b/crates/bone-interop/src/lib.rs index 63f958c..321b835 100644 --- a/crates/bone-interop/src/lib.rs +++ b/crates/bone-interop/src/lib.rs @@ -1,3 +1,3 @@ pub mod step; -pub use step::{ImportOutcome, StepError, body_of, read, write}; +pub use step::{HeaderDefect, StepError, body_of, read, write}; diff --git a/crates/bone-interop/src/step.rs b/crates/bone-interop/src/step.rs index 4c43fa1..c7ff15b 100644 --- a/crates/bone-interop/src/step.rs +++ b/crates/bone-interop/src/step.rs @@ -7,13 +7,15 @@ use bone_document::{ }; use bone_kernel::{BrepError, BrepSolid}; use bone_types::{ - FaceRole, FeatureId, StepFileHeader, StepFileName, StepOrganization, StepOriginatingSystem, - StepSchema, + DocumentId, FeatureId, StepEntityKind, StepFileHeader, StepFileName, StepOrganization, + StepOriginatingSystem, StepSchema, }; const PINNED_TIMESTAMP: &str = "1970-01-01T00:00:00"; const ORIGINATING_SYSTEM: &str = concat!("Bone ", env!("CARGO_PKG_VERSION")); +const SUPPORTED_READ: &[StepSchema] = &[StepSchema::Ap214]; + const fn schema_token(schema: StepSchema) -> &'static str { match schema { StepSchema::Ap214 => "AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }", @@ -23,6 +25,24 @@ const fn schema_token(schema: StepSchema) -> &'static str { } } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum HeaderDefect { + NoHeaderSection, + NoFileSchema, + UnreadableFileSchema, +} + +impl HeaderDefect { + #[must_use] + const fn label(self) -> &'static str { + match self { + Self::NoHeaderSection => "no HEADER section", + Self::NoFileSchema => "no FILE_SCHEMA entry", + Self::UnreadableFileSchema => "an unreadable FILE_SCHEMA token", + } + } +} + #[derive(Debug, thiserror::Error)] pub enum StepError { #[error("schema {0} is unsupported for export; only AP214 is emitted")] @@ -37,6 +57,19 @@ pub enum StepError { #[source] source: ExtrudeError, }, + #[error("step geometry uses {kind}, which the reader does not yet bridge")] + UnsupportedEntity { kind: StepEntityKind }, + #[error("step file declares {found}; the reader supports {supported:?}")] + SchemaMismatch { + found: StepSchema, + supported: &'static [StepSchema], + }, + #[error("step header carries {}", reason.label())] + MalformedHeader { reason: HeaderDefect }, + #[error("step file carries no importable solid")] + IncompleteFile, + #[error("step file carries {solids} solids; assemblies are not yet imported")] + UnsupportedAssembly { solids: usize }, #[error("io at {path}: {source}")] Io { path: PathBuf, @@ -53,32 +86,6 @@ pub enum StepError { }, } -pub enum ImportOutcome { - Labeled(BrepSolid), - Imported(BrepSolid), -} - -impl ImportOutcome { - #[must_use] - pub fn solid(&self) -> &BrepSolid { - match self { - Self::Labeled(solid) | Self::Imported(solid) => solid, - } - } - - #[must_use] - pub fn into_solid(self) -> BrepSolid { - match self { - Self::Labeled(solid) | Self::Imported(solid) => solid, - } - } - - #[must_use] - pub fn is_labeled(&self) -> bool { - matches!(self, Self::Labeled(_)) - } -} - pub fn write(document: &Document, path: &Path, schema: StepSchema) -> Result<(), StepError> { if schema != StepSchema::Ap214 { return Err(StepError::SchemaUnsupported(schema)); @@ -101,17 +108,23 @@ pub fn write(document: &Document, path: &Path, schema: StepSchema) -> Result<(), } pub fn body_of(document: &Document) -> Result { - let extrudes: Vec = document + let bodies: Vec = document .feature_tree() .iter() - .filter_map(|(feature, node)| matches!(node, FeatureNode::Extrude(_)).then_some(feature)) + .filter_map(|(feature, node)| { + matches!(node, FeatureNode::Extrude(_) | FeatureNode::ImportedBody(_)) + .then_some(feature) + }) .collect(); - let [feature] = extrudes.as_slice() else { + let [feature] = bodies.as_slice() else { return Err(StepError::BodyCount { - count: extrudes.len(), + count: bodies.len(), }); }; let feature = *feature; + if let Some(solid) = document.imported_body_of_feature(feature) { + return Ok(solid.clone()); + } let extrude = document .extrude_of_feature(feature) .ok_or(StepError::DanglingExtrude { feature })?; @@ -124,22 +137,131 @@ pub fn body_of(document: &Document) -> Result { .map_err(|source| StepError::Evaluation { feature, source }) } -pub fn read(path: &Path, feature: FeatureId) -> Result { +pub fn read(path: &Path) -> Result { let text = read_file(path)?; + classify_schema(&text)?; let sidecar = read_sidecar(path)?; - let solid = BrepSolid::from_step( - &text, - feature, - sidecar.as_ref().map(|side| (side.solid(), side.reattach())), - )?; - let labeled = solid - .iter_faces() - .any(|face| !matches!(face.label().role, FaceRole::Imported { .. })); - Ok(if labeled { - ImportOutcome::Labeled(solid) + let mut document = Document::new(DocumentId::default(), document_name(path)); + document.import_body(|feature| { + BrepSolid::from_step( + &text, + feature, + sidecar.as_ref().map(|side| (side.solid(), side.reattach())), + ) + .map_err(read_geometry_error) + })?; + Ok(document) +} + +fn read_geometry_error(error: BrepError) -> StepError { + match error { + BrepError::StepUnsupported { kind } => StepError::UnsupportedEntity { kind }, + BrepError::StepSyntax | BrepError::StepNoData | BrepError::StepEmpty => { + StepError::IncompleteFile + } + BrepError::StepMultipleSolids { count } => StepError::UnsupportedAssembly { solids: count }, + other => StepError::Geometry(other), + } +} + +fn classify_schema(text: &str) -> Result<(), StepError> { + let found: Vec = file_schema_tokens(text)? + .iter() + .filter_map(|token| recognize_schema(token)) + .collect(); + if found.iter().any(|schema| SUPPORTED_READ.contains(schema)) { + return Ok(()); + } + match found.iter().find(|schema| !SUPPORTED_READ.contains(schema)) { + Some(&found) => Err(StepError::SchemaMismatch { + found, + supported: SUPPORTED_READ, + }), + None => Ok(()), + } +} + +fn file_schema_tokens(text: &str) -> Result, StepError> { + let header = header_section(text)?; + let at = find_in_code_ci(header, "FILE_SCHEMA").ok_or(StepError::MalformedHeader { + reason: HeaderDefect::NoFileSchema, + })?; + let statement = &header[at..]; + let statement = statement + .find(';') + .map_or(statement, |end| &statement[..end]); + let tokens: Vec = quoted_segments(statement) + .map(str::to_ascii_uppercase) + .collect(); + if tokens.is_empty() { + return Err(StepError::MalformedHeader { + reason: HeaderDefect::UnreadableFileSchema, + }); + } + Ok(tokens) +} + +fn header_section(text: &str) -> Result<&str, StepError> { + let start = find_in_code_ci(text, "HEADER").ok_or(StepError::MalformedHeader { + reason: HeaderDefect::NoHeaderSection, + })?; + let rest = &text[start..]; + let end = find_in_code_ci(rest, "ENDSEC").unwrap_or(rest.len()); + Ok(&rest[..end]) +} + +fn quoted_segments(statement: &str) -> impl Iterator { + statement.split('\'').skip(1).step_by(2) +} + +#[derive(Copy, Clone)] +enum HeaderScan { + Code, + Quoted, + Comment, +} + +fn find_in_code_ci(haystack: &str, needle: &str) -> Option { + let (hay, pat) = (haystack.as_bytes(), needle.as_bytes()); + if pat.is_empty() || hay.len() < pat.len() { + return None; + } + hay.iter() + .scan((HeaderScan::Code, 0u8), |(state, prev), &byte| { + let in_code = matches!(state, HeaderScan::Code); + *state = match *state { + HeaderScan::Code if byte == b'\'' => HeaderScan::Quoted, + HeaderScan::Code if *prev == b'/' && byte == b'*' => HeaderScan::Comment, + HeaderScan::Quoted if byte == b'\'' => HeaderScan::Code, + HeaderScan::Comment if *prev == b'*' && byte == b'/' => HeaderScan::Code, + other => other, + }; + *prev = byte; + Some(in_code) + }) + .enumerate() + .take(hay.len() - pat.len() + 1) + .find(|&(start, in_code)| { + in_code && hay[start..start + pat.len()].eq_ignore_ascii_case(pat) + }) + .map(|(start, _)| start) +} + +fn recognize_schema(token: &str) -> Option { + if token.contains("AP242") || token.contains("MANAGED_MODEL_BASED") { + Some(StepSchema::Ap242E2) + } else if token.contains("AUTOMOTIVE_DESIGN") { + Some(StepSchema::Ap214) } else { - ImportOutcome::Imported(solid) - }) + None + } +} + +fn document_name(path: &Path) -> String { + path.file_stem().map_or_else( + || "imported".to_owned(), + |stem| stem.to_string_lossy().into_owned(), + ) } fn envelope(header: &str, body: &str) -> String { @@ -216,12 +338,17 @@ fn read_file(path: &Path) -> Result { fn read_sidecar(step: &Path) -> Result, StepError> { let path = sidecar_path(step); match std::fs::read_to_string(&path) { - Ok(text) => LabelSidecar::from_ron(&text) - .map(Some) - .map_err(|source| StepError::Sidecar { - path, - source: Box::new(source), - }), + Ok(text) => Ok(match LabelSidecar::from_ron(&text) { + Ok(sidecar) => Some(sidecar), + Err(source) => { + tracing::warn!( + path = %path.display(), + error = %source, + "ignoring an unreadable step label sidecar" + ); + None + } + }), Err(ref source) if source.kind() == io::ErrorKind::NotFound => Ok(None), Err(source) => Err(StepError::Io { path, source }), } -- 2.51.2