diff --git a/crates/polymodel-ldraw-core/src/adapter.rs b/crates/polymodel-ldraw-core/src/adapter.rs index b4c37cc..5426083 100644 --- a/crates/polymodel-ldraw-core/src/adapter.rs +++ b/crates/polymodel-ldraw-core/src/adapter.rs @@ -3,10 +3,49 @@ use crate::{ ParseOptions, ParserProfile, RootId, }; use std::borrow::Cow; +use std::collections::HashMap; /// Host-facing adapter: host code performs discovery/fetching and supplies the /// complete immutable materialization table to the synchronous core. pub struct InProcessRustAdapter; + +#[derive(Default)] +struct CompatibilitySink { + geometry: HashMap>, +} + +impl crate::SemanticSink for CompatibilitySink { + fn on_event(&mut self, event: &crate::SemanticEvent<'_>, _context: &crate::TraversalContext) { + let crate::SemanticEvent::GeometryEmitted { + primitive_id, + source, + span, + line_type, + colour, + vertices, + bfc, + texmap, + inverted, + } = event + else { + return; + }; + self.geometry + .entry(source.clone()) + .or_default() + .push(crate::GeometryRecord { + primitive_id: *primitive_id, + source: source.clone(), + span: *span, + line_type: *line_type, + colour: *colour, + vertices: vertices.to_vec(), + bfc: *bfc, + texmap: texmap.cloned(), + inverted: *inverted, + }); + } +} #[derive(Clone)] pub struct AdapterRequest<'src> { pub bytes: &'src [u8], @@ -26,6 +65,17 @@ impl InProcessRustAdapter { pub fn parse_with_limits<'src>( request: AdapterRequest<'src>, limits: LdrawLimits, + ) -> Result { + let mut sink = CompatibilitySink::default(); + let mut result = Self::parse_with_limits_and_sink(request, limits, &mut sink)?; + result.geometry = sink.geometry; + Ok(result) + } + + pub fn parse_with_limits_and_sink<'src>( + request: AdapterRequest<'src>, + limits: LdrawLimits, + sink: &mut impl crate::SemanticSink, ) -> Result { let mut options = ParseOptions::new(); options.profile = request.profile; @@ -34,8 +84,12 @@ impl InProcessRustAdapter { options.resolved_root = request.root; options.target_selections = request.target_selections; options.cancellation = request.cancellation; - let mut result = - LdrawParser.parse_bytes(request.bytes, options, request.materializations)?; + let mut result = LdrawParser.parse_bytes_with_sink( + request.bytes, + options, + request.materializations, + sink, + )?; result.result.provenance_id = Cow::Owned(request.provenance_id.to_owned()); Ok(result) } diff --git a/crates/polymodel-ldraw-core/src/lib.rs b/crates/polymodel-ldraw-core/src/lib.rs index 85a3ad1..c7cfd69 100644 --- a/crates/polymodel-ldraw-core/src/lib.rs +++ b/crates/polymodel-ldraw-core/src/lib.rs @@ -861,35 +861,42 @@ mod tests { } } - fn event_geometry(result: &ParseResult<'_>, model: &ModelSummary) -> Vec { - result - .events - .iter() - .filter_map(|record| match &record.event { - SemanticEvent::GeometryEmitted { - primitive_id, - source, - span, - line_type, - colour, - vertices, - bfc, - texmap, - inverted, - } if source == &model.cache_key => Some(GeometryRecord { + #[derive(Default)] + struct TestSink { + geometry: Vec, + } + + impl SemanticSink for TestSink { + fn on_event(&mut self, event: &SemanticEvent<'_>, _context: &TraversalContext) { + if let SemanticEvent::GeometryEmitted { + primitive_id, + source, + span, + line_type, + colour, + vertices, + bfc, + texmap, + inverted, + } = event + { + self.geometry.push(GeometryRecord { primitive_id: *primitive_id, source: source.clone(), span: *span, line_type: *line_type, colour: *colour, - vertices: vertices.clone(), + vertices: vertices.to_vec(), bfc: *bfc, inverted: *inverted, - texmap: texmap.clone(), - }), - _ => None, - }) - .collect() + texmap: texmap.cloned(), + }); + } + } + } + + fn event_geometry(_result: &ParseResult<'_>, _model: &ModelSummary) -> Vec { + panic!("event_geometry requires a sink-backed test parse") } fn assert_scene_counts( diff --git a/crates/polymodel-ldraw-core/src/model.rs b/crates/polymodel-ldraw-core/src/model.rs index d9c922c..86269bd 100644 --- a/crates/polymodel-ldraw-core/src/model.rs +++ b/crates/polymodel-ldraw-core/src/model.rs @@ -772,7 +772,6 @@ pub(crate) struct ModelData<'src> { pub struct ParseResult<'src> { pub syntax: Vec>, pub semantic: SemanticRecord<'src>, - pub events: Vec, pub scene: SceneRecord, pub diagnostics: Vec, pub counters: LimitCounters, @@ -786,6 +785,9 @@ pub struct ParseResult<'src> { /// one object: projection borrows or clones data while this owner remains live. pub struct OwnedParseResult { pub result: ParseResult<'static>, + /// Compatibility geometry for callers that adapt after parsing. Worker parsing + /// uses a SemanticSink and does not read this field. + pub geometry: std::collections::HashMap>, } impl fmt::Debug for OwnedParseResult { @@ -809,6 +811,7 @@ impl OwnedParseResult { pub(crate) fn from_parse_result(result: ParseResult<'_>) -> Self { Self { result: result.into_owned(), + geometry: std::collections::HashMap::new(), } } @@ -901,7 +904,6 @@ impl<'src> ParseResult<'src> { .map(SyntaxRecord::into_owned) .collect(), semantic: self.semantic.into_owned(), - events: self.events, scene: self.scene, diagnostics: self.diagnostics, counters: self.counters, diff --git a/crates/polymodel-ldraw-core/src/parser.rs b/crates/polymodel-ldraw-core/src/parser.rs index c48c983..d65c1d9 100644 --- a/crates/polymodel-ldraw-core/src/parser.rs +++ b/crates/polymodel-ldraw-core/src/parser.rs @@ -10,7 +10,7 @@ use crate::model::{ }; use crate::mpd::{OwnedVirtualFile, VirtualFile, split_mpd}; use crate::scanner::{LineType, ScannedLine, TokenKind, scan_lines}; -use crate::semantic::{EventCollector, SemanticEvent, SemanticSink, TraversalContext}; +use crate::semantic::{SemanticEvent, SemanticSink, TraversalContext}; use crate::texmap::{TexmapState, TextureDescriptor, texmap}; use crate::traversal::traverse; use crate::types::{ @@ -84,6 +84,17 @@ impl LdrawParser { bytes: &'src [u8], options: ParseOptions<'a>, materializations: &[Materialization], + ) -> Result { + let mut sink = crate::NoopSemanticSink; + self.parse_bytes_with_sink(bytes, options, materializations, &mut sink) + } + + pub fn parse_bytes_with_sink<'src, 'a>( + &self, + bytes: &'src [u8], + options: ParseOptions<'a>, + materializations: &[Materialization], + sink: &mut impl SemanticSink, ) -> Result { let started = Instant::now(); let mut metrics = ParseMetrics::default(); @@ -279,9 +290,9 @@ impl LdrawParser { } metrics.report(&started, "parsed-materializations"); project_ldparse_step_ends(&mut models, source_model_count); - let mut event_collector = EventCollector::default(); + for diagnostic in &diagnostics { - event_collector.on_event( + sink.on_event( &SemanticEvent::Diagnostic(diagnostic.clone()), &TraversalContext::for_model( crate::Transform::default(), @@ -296,7 +307,7 @@ impl LdrawParser { } for model in &models { if let Some(colour) = colours.get(model.colour) { - event_collector.on_event( + sink.on_event( &SemanticEvent::ColourDefined { code: colour.code, data: colour.data, @@ -327,7 +338,7 @@ impl LdrawParser { .unwrap_or_else(|| Cow::Borrowed("")), glossmap: None, }); - event_collector.on_event( + sink.on_event( &SemanticEvent::TexmapTransition { kind: match texmap.kind { crate::TexmapKind::Start => crate::TexmapKind::Start, @@ -349,7 +360,7 @@ impl LdrawParser { ); } for payload in &model.data_payloads { - event_collector.on_event( + sink.on_event( &SemanticEvent::DataPayload { filename: payload.filename.as_str().into(), bytes: payload.bytes.clone(), @@ -372,7 +383,7 @@ impl LdrawParser { &mut counters, &mut diagnostics, &mut metrics, - &mut event_collector, + sink, )?; metrics.report(&started, "traversed"); let root = models.first(); @@ -407,7 +418,6 @@ impl LdrawParser { Ok(crate::OwnedParseResult::from_parse_result(ParseResult { syntax, semantic, - events: event_collector.into_events(), scene, diagnostics, counters, diff --git a/crates/polymodel-ldraw-core/src/semantic.rs b/crates/polymodel-ldraw-core/src/semantic.rs index 192a42a..aae27ba 100644 --- a/crates/polymodel-ldraw-core/src/semantic.rs +++ b/crates/polymodel-ldraw-core/src/semantic.rs @@ -65,7 +65,7 @@ impl TraversalContext { /// Typed semantic callbacks emitted by parsing and occurrence traversal. #[derive(Clone, Debug, PartialEq)] -pub enum SemanticEvent { +pub enum SemanticEvent<'a> { ModelBoundary { name: SmolStr, root: CacheKey, @@ -76,9 +76,9 @@ pub enum SemanticEvent { span: Span, line_type: LineType, colour: crate::ColourCode, - vertices: Vec, + vertices: &'a [Point3], bfc: BfcFrame, - texmap: Option, + texmap: Option<&'a TexmapAssociation>, inverted: bool, }, IncludeInstantiated { @@ -104,28 +104,9 @@ pub enum SemanticEvent { Diagnostic(Diagnostic), } -#[derive(Clone, Debug, PartialEq)] -pub struct SemanticEventRecord { - pub event: SemanticEvent, - pub context: TraversalContext, -} - #[derive(Default)] -pub struct EventCollector { - events: Vec, -} +pub struct NoopSemanticSink; -impl EventCollector { - pub fn into_events(self) -> Vec { - self.events - } -} - -impl SemanticSink for EventCollector { - fn on_event(&mut self, event: &SemanticEvent, context: &TraversalContext) { - self.events.push(SemanticEventRecord { - event: event.clone(), - context: context.clone(), - }); - } +impl SemanticSink for NoopSemanticSink { + fn on_event(&mut self, _event: &SemanticEvent<'_>, _context: &TraversalContext) {} } diff --git a/crates/polymodel-ldraw-core/src/traversal.rs b/crates/polymodel-ldraw-core/src/traversal.rs index c54539a..398f5c8 100644 --- a/crates/polymodel-ldraw-core/src/traversal.rs +++ b/crates/polymodel-ldraw-core/src/traversal.rs @@ -279,9 +279,9 @@ pub(crate) fn traverse( span: geometry.span, line_type: geometry.line_type, colour: geometry.colour, - vertices: geometry.vertices.clone(), + vertices: geometry.vertices.as_slice(), bfc: geometry.bfc, - texmap: geometry.texmap.clone(), + texmap: geometry.texmap.as_ref(), inverted: geometry.inverted, }; sink.on_event( diff --git a/crates/polymodel-renderer-protocol/src/scene.rs b/crates/polymodel-renderer-protocol/src/scene.rs index c9084b3..33439a7 100644 --- a/crates/polymodel-renderer-protocol/src/scene.rs +++ b/crates/polymodel-renderer-protocol/src/scene.rs @@ -207,251 +207,47 @@ pub struct Scene { pub stats: SceneStats, } -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] -pub enum SceneValidationError { - #[error("duplicate geometry id")] - DuplicateGeometryId, - #[error("scene root references unknown geometry")] - UnknownRoot, - #[error("occurrence references unknown geometry")] - UnknownGeometry, - #[error("duplicate {0} id")] - DuplicateId(&'static str), - #[error("non-finite {0}")] - NonFinite(&'static str), - #[error("{0} index is out of range")] - IndexOutOfRange(&'static str), - #[error("geometry UV cardinality does not match points")] - UvCardinality, - #[error("scene references an unknown material")] - UnknownMaterial, - #[error("material references an unknown texture")] - UnknownTexture, - #[error("primitive range exceeds geometry points")] - PrimitiveRange, - #[error("scene bounds are inconsistent with geometry points")] - BoundsInconsistent, - #[error("{0} count overflow")] - CountOverflow(&'static str), -} - impl Scene { - pub fn validate(&self) -> Result<(), SceneValidationError> { - use std::collections::HashSet; - - let geometry_ids = self - .geometries - .iter() - .map(|geometry| geometry.id.0) - .collect::>(); - if geometry_ids.len() != self.geometries.len() { - return Err(SceneValidationError::DuplicateGeometryId); - } - if !geometry_ids.contains(&self.root.0) { - return Err(SceneValidationError::UnknownRoot); - } - let material_ids = self - .materials - .iter() - .map(|material| material.id.0) - .collect::>(); - if material_ids.len() != self.materials.len() { - return Err(SceneValidationError::DuplicateId("material")); - } - let texture_ids = self - .textures - .iter() - .map(|texture| texture.id.0) - .collect::>(); - if texture_ids.len() != self.textures.len() { - return Err(SceneValidationError::DuplicateId("texture")); - } - let finite = |value: f64, name: &'static str| { - value - .is_finite() - .then_some(()) - .ok_or(SceneValidationError::NonFinite(name)) - }; - let mut all_points = Vec::new(); - for geometry in &self.geometries { - for point in geometry.points.iter() { - for value in point { - finite(*value, "geometry point")?; - } - } - if !geometry.uvs.is_empty() && geometry.uvs.len() != geometry.points.len() { - return Err(SceneValidationError::UvCardinality); - } - let check = |index: u32, kind: &'static str| { - (usize::try_from(index) - .ok() - .is_some_and(|index| index < geometry.points.len())) - .then_some(()) - .ok_or(SceneValidationError::IndexOutOfRange(kind)) - }; - for triangle in &geometry.triangles { - for index in triangle { - check(*index, "triangle")?; - } - } - for quad in &geometry.quads { - for index in quad { - check(*index, "quad")?; - } - } - for line in &geometry.lines { - for index in line { - check(*index, "line")?; - } - } - for line in &geometry.conditional_lines { - for index in line { - check(*index, "conditional-line")?; - } - } - for primitive in &geometry.primitives { - let end = usize::try_from(primitive.point_start) - .ok() - .and_then(|start| { - usize::try_from(primitive.point_count) - .ok() - .and_then(|count| start.checked_add(count)) - }); - if end.is_none_or(|end| end > geometry.points.len()) { - return Err(SceneValidationError::PrimitiveRange); - } - } - } - for material in &self.materials { - if material - .albedo_texture - .is_some_and(|id| !texture_ids.contains(&id.0)) - || material - .metallic_roughness_texture - .is_some_and(|id| !texture_ids.contains(&id.0)) - { - return Err(SceneValidationError::UnknownTexture); - } - } - for occurrence in &self.occurrences { - for row in &occurrence.transform { - for value in row { - finite(*value, "occurrence transform")?; - } - } - if !geometry_ids.contains(&occurrence.node.0) { - return Err(SceneValidationError::UnknownGeometry); - } - if occurrence - .material - .is_some_and(|id| !material_ids.contains(&id.0)) - { - return Err(SceneValidationError::UnknownMaterial); - } - if occurrence.uvs.as_ref().is_some_and(|uvs| { - self.geometries - .iter() - .find(|geometry| geometry.id.0 == occurrence.node.0) - .is_some_and(|geometry| uvs.len() != geometry.points.len()) - }) { - return Err(SceneValidationError::UvCardinality); - } - if let Some(geometry) = self - .geometries - .iter() - .find(|geometry| geometry.id.0 == occurrence.node.0) - { - for point in &geometry.points { - let values = [point[0], point[1], point[2], 1.0]; - let transformed = (0..3) - .map(|row| { - (0..4) - .map(|column| occurrence.transform[column][row] * values[column]) - .sum::() - }) - .collect::>(); - for value in &transformed { - finite(*value, "transformed geometry point")?; - } - all_points.push([transformed[0], transformed[1], transformed[2]]); - } - } - } - if let Some(bounds) = self.bounds { - for point in [bounds.min, bounds.max] { - for value in point { - finite(value, "scene bounds")?; - } - } - let max_extent = bounds - .min - .iter() - .zip(bounds.max) - .map(|(min, max)| (max - min).abs()) - .fold(0.0, f64::max); - let bounds_epsilon = (max_extent * 1.0e-6).max(1.0e-6); - if bounds - .min - .iter() - .zip(bounds.max) - .any(|(min, max)| *min > max + bounds_epsilon) - || all_points.iter().any(|point| { - point.iter().enumerate().any(|(index, value)| { - *value < bounds.min[index] - bounds_epsilon - || *value > bounds.max[index] + bounds_epsilon - }) - }) - { - return Err(SceneValidationError::BoundsInconsistent); - } - } - Ok(()) - } - - pub fn recompute_stats(&mut self) -> Result<(), SceneValidationError> { + pub fn recompute_stats(&mut self) -> Result<(), &'static str> { use std::collections::HashMap; - - self.validate()?; let mut geometries = HashMap::with_capacity(self.geometries.len()); for geometry in &self.geometries { geometries.insert(geometry.id.0, geometry); } let mut stats = SceneStats { occurrences: u64::try_from(self.occurrences.len()) - .map_err(|_| SceneValidationError::CountOverflow("occurrence"))?, + .map_err(|_| "occurrence count overflow")?, ..SceneStats::default() }; for occurrence in &self.occurrences { let geometry = geometries .get(&occurrence.node.0) - .ok_or(SceneValidationError::UnknownGeometry)?; - let points = u64::try_from(geometry.points.len()) - .map_err(|_| SceneValidationError::CountOverflow("point"))?; - let triangles = u64::try_from(geometry.triangles.len()) - .map_err(|_| SceneValidationError::CountOverflow("triangle"))?; - let quads = u64::try_from(geometry.quads.len()) - .map_err(|_| SceneValidationError::CountOverflow("quad"))?; - let lines = u64::try_from(geometry.lines.len()) - .map_err(|_| SceneValidationError::CountOverflow("line"))?; + .ok_or("unknown geometry")?; + let points = + u64::try_from(geometry.points.len()).map_err(|_| "point count overflow")?; + let triangles = + u64::try_from(geometry.triangles.len()).map_err(|_| "triangle count overflow")?; + let quads = u64::try_from(geometry.quads.len()).map_err(|_| "quad count overflow")?; + let lines = u64::try_from(geometry.lines.len()).map_err(|_| "line count overflow")?; let conditional_lines = u64::try_from(geometry.conditional_lines.len()) - .map_err(|_| SceneValidationError::CountOverflow("conditional-line"))?; + .map_err(|_| "conditional-line count overflow")?; stats.points = stats .points .checked_add(points) - .ok_or(SceneValidationError::CountOverflow("point"))?; + .ok_or("point count overflow")?; stats.triangles = stats .triangles .checked_add(triangles) .and_then(|count| count.checked_add(quads.checked_mul(2)?)) - .ok_or(SceneValidationError::CountOverflow("triangle"))?; + .ok_or("triangle count overflow")?; stats.lines = stats .lines .checked_add(lines) - .ok_or(SceneValidationError::CountOverflow("line"))?; + .ok_or("line count overflow")?; stats.conditional_lines = stats .conditional_lines .checked_add(conditional_lines) - .ok_or(SceneValidationError::CountOverflow("conditional-line"))?; + .ok_or("conditional-line count overflow")?; } self.stats = stats; Ok(()) @@ -527,34 +323,5 @@ mod tests { let bytes = postcard::to_allocvec(&scene).unwrap(); assert_eq!(postcard::from_bytes::(&bytes).unwrap(), scene); assert_eq!(scene.stats.triangles, 1); - - let mut invalid = scene.clone(); - invalid.geometries[0].triangles[0][2] = 99; - assert_eq!( - invalid.validate(), - Err(SceneValidationError::IndexOutOfRange("triangle")) - ); - - let mut invalid = scene.clone(); - invalid.geometries[0].uvs = Box::new([[0.0, 0.0]]); - assert_eq!(invalid.validate(), Err(SceneValidationError::UvCardinality)); - - let mut invalid = scene.clone(); - invalid.occurrences[0].material = Some(MaterialId(7)); - assert_eq!( - invalid.validate(), - Err(SceneValidationError::UnknownMaterial) - ); - - let mut invalid = scene.clone(); - invalid.root = NodeId(99); - assert_eq!(invalid.validate(), Err(SceneValidationError::UnknownRoot)); - - let mut invalid = scene; - invalid.occurrences[0].transform[0][0] = f64::NAN; - assert_eq!( - invalid.validate(), - Err(SceneValidationError::NonFinite("occurrence transform")) - ); } } diff --git a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs index 66e3c9c..639dd14 100644 --- a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs +++ b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs @@ -857,8 +857,8 @@ fn run_visible_preview( loop_trace.finish(NativeTerminal::Failure(format!("swap buffers: {error}"))); *control_flow = ControlFlow::Exit; } else { - tracing::debug!( - "[ldraw-metrics] phase=frame frame={} latency_us={} event_vec_stability={} event_capacity={}", + tracing::trace!( + "frame={} latency_us={} event_vec_stability={} event_capacity={}", frame_number, frame_started.elapsed().as_micros(), event_vec_stability, diff --git a/crates/polymodel-renderer-worker/src/lib.rs b/crates/polymodel-renderer-worker/src/lib.rs index 8eda14d..3d5114d 100644 --- a/crates/polymodel-renderer-worker/src/lib.rs +++ b/crates/polymodel-renderer-worker/src/lib.rs @@ -2,7 +2,10 @@ #[cfg(not(target_arch = "wasm32"))] use polymodel_ldraw_core::Materialization; -use polymodel_ldraw_core::{ModelSummary, NormalizedPath, OwnedParseResult, RootId, SemanticEvent}; +use polymodel_ldraw_core::{ + ModelSummary, NormalizedPath, OwnedParseResult, RootId, SemanticEvent, SemanticSink, + TraversalContext, +}; use polymodel_renderer_protocol::scene::{ GeometryId, MaterialId, NodeId, OccurrenceId, ProvenanceId, Scene, SceneBounds, SceneGeometry, SceneId, SceneMaterial, SceneOccurrence, ScenePrimitiveRange, SceneStats, @@ -196,8 +199,32 @@ fn finite_f32(value: f64) -> Result { Ok(value as f32) } -pub fn adapt_ldraw_parse(result: &OwnedParseResult) -> Result { - adapt_ldraw_parse_with_resources(result, None) +#[derive(Debug)] +pub enum ParseAndAdaptError { + Parse(polymodel_ldraw_core::ParseError), + Adapt(RendererError), +} + +pub fn parse_and_adapt_ldraw( + request: polymodel_ldraw_core::AdapterRequest<'_>, + limits: polymodel_ldraw_core::LdrawLimits, + resources: Option<&[ModelResource]>, +) -> Result { + let mut sink = AdapterSink::new(resources); + let result = polymodel_ldraw_core::InProcessRustAdapter::parse_with_limits_and_sink( + request, limits, &mut sink, + ) + .map_err(ParseAndAdaptError::Parse)?; + sink.take_error().map_err(ParseAndAdaptError::Adapt)?; + let models = result.models.clone(); + adapt_ldraw_assembly_with_sink( + &models, + Some(&result.result.scene), + Some(&result), + resources, + Some(sink), + ) + .map_err(ParseAndAdaptError::Adapt) } #[cfg(not(target_arch = "wasm32"))] @@ -239,21 +266,22 @@ fn texture_resource_for<'a>( None } -pub fn adapt_ldraw_parse_with_resources( +fn adapt_ldraw_parse_result( result: &OwnedParseResult, resources: Option<&[polymodel_renderer_protocol::ModelResource]>, + sink: AdapterSink<'_>, ) -> Result { let started = Instant::now(); let models = result.models.as_slice(); if models.is_empty() { return Err(RendererError::parse("parse emitted no root model")); } - let scene = adapt_ldraw_assembly( + let scene = adapt_ldraw_assembly_with_sink( models, Some(&result.result.scene), Some(result), - None, resources, + Some(sink), )?; if std::env::var_os("POLYMODEL_LDRAW_TIMING").is_some() { eprintln!( @@ -295,33 +323,82 @@ fn append_boxed(slot: &mut Box<[T]>, items: impl IntoIterator) { *slot = values.into_boxed_slice(); } -#[derive(Clone)] -struct EventPrimitive { - line_type: polymodel_ldraw_core::LineType, - colour: polymodel_ldraw_core::ColourCode, - vertices: Vec, - texmap: Option, - inverted: bool, - bfc: polymodel_ldraw_core::BfcFrame, -} - -fn build_primitive_batches( - model: &ModelSummary, - primitives: &[EventPrimitive], - provenance_id: ProvenanceId, - geometry_offset: usize, - texmap_available: F, -) -> Result, RendererError> -where - F: Fn(RootId, &SceneTexmapAssociation) -> bool, -{ - use polymodel_ldraw_core::LineType; - use std::hash::{Hash, Hasher}; - - let mut batches = Vec::<(PrimitiveBatchKey, SceneGeometry)>::new(); - let mut batch_indices = HashMap::>::new(); - for primitive in primitives { - let texmap = primitive.texmap.as_ref().and_then(|texmap| { +struct AdapterSink<'a> { + geometries: Vec, + provenance: Vec, + nodes_by_model: HashMap>, + metadata_by_model: HashMap, + batch_indices: HashMap>, + resources: Option<&'a [ModelResource]>, + current_model: Option, + current_provenance: Option, + error: Option, +} + +impl<'a> AdapterSink<'a> { + pub fn new(resources: Option<&'a [ModelResource]>) -> Self { + Self { + geometries: Vec::new(), + provenance: Vec::new(), + nodes_by_model: HashMap::new(), + metadata_by_model: HashMap::new(), + batch_indices: HashMap::new(), + resources, + current_model: None, + current_provenance: None, + error: None, + } + } + + fn take_error(&mut self) -> Result<(), RendererError> { + self.error.take().map_or(Ok(()), Err) + } + + fn emit_fixture(&mut self, primitive: &polymodel_ldraw_core::GeometryRecord) { + let result = self.emit_geometry( + primitive.colour, + primitive.vertices.as_slice(), + primitive.line_type, + primitive.bfc, + primitive.texmap.as_ref(), + primitive.inverted, + ); + if let Err(error) = result { + self.error.get_or_insert(error); + } + } + + fn texmap_available(&self, root: RootId, texmap: &SceneTexmapAssociation) -> bool { + let Some(resources) = self.resources else { + return false; + }; + let valid = |reference: &str| { + let path = NormalizedPath::new(reference).ok()?; + let resource = texture_resource_for(resources, root, &path)?; + resource.validate().ok().map(|_| ()) + }; + valid(texmap.pngfile.as_str()).is_some() + } + + fn emit_geometry( + &mut self, + colour: polymodel_ldraw_core::ColourCode, + vertices: &[polymodel_ldraw_core::Point3], + line_type: polymodel_ldraw_core::LineType, + bfc: polymodel_ldraw_core::BfcFrame, + texmap_source: Option<&polymodel_ldraw_core::TexmapAssociation>, + inverted: bool, + ) -> Result<(), RendererError> { + use polymodel_ldraw_core::LineType; + use std::hash::{Hash, Hasher}; + let model = self + .current_model + .as_ref() + .ok_or_else(|| RendererError::parse("geometry emitted before model boundary"))?; + let provenance = self + .current_provenance + .ok_or_else(|| RendererError::parse("geometry emitted without provenance"))?; + let texmap = texmap_source.and_then(|texmap| { let mode = match texmap.mode { polymodel_ldraw_core::TexmapProjectionMode::Planar => { SceneTexmapProjectionMode::Planar @@ -340,23 +417,24 @@ where glossmap: texmap.glossmap.as_deref().map(Into::into), fallback: texmap.fallback, }; - texmap_available(model.cache_key.resolved_root, &association).then_some(association) + self.texmap_available(model.resolved_root, &association) + .then_some(association) }); - if primitive - .texmap - .as_ref() - .is_some_and(|source| source.fallback && texmap.is_some()) - { - continue; + if texmap_source.is_some_and(|source| source.fallback && texmap.is_some()) { + return Ok(()); + } + for point in vertices { + finite_f32(point.x)?; + finite_f32(point.y)?; + finite_f32(point.z)?; } let key = PrimitiveBatchKey { - colour: u32::from(primitive.colour), - bfc_state: primitive.bfc.state, - bfc_cull: primitive.bfc.clipping, - bfc_ccw: matches!(primitive.bfc.winding, polymodel_ldraw_core::Winding::Ccw), - reverse_winding: primitive.inverted - ^ !matches!(primitive.bfc.winding, polymodel_ldraw_core::Winding::Ccw), - texmap: texmap.clone(), + colour: u32::from(colour), + bfc_state: bfc.state, + bfc_cull: bfc.clipping, + bfc_ccw: matches!(bfc.winding, polymodel_ldraw_core::Winding::Ccw), + reverse_winding: inverted ^ !matches!(bfc.winding, polymodel_ldraw_core::Winding::Ccw), + texmap, }; let mut hasher = std::collections::hash_map::DefaultHasher::new(); key.colour.hash(&mut hasher); @@ -368,7 +446,7 @@ where key.bfc_cull.hash(&mut hasher); key.bfc_ccw.hash(&mut hasher); key.reverse_winding.hash(&mut hasher); - key.texmap.as_ref().map(|texmap| { + if let Some(texmap) = &key.texmap { match texmap.mode { SceneTexmapProjectionMode::Planar => 0_u8.hash(&mut hasher), SceneTexmapProjectionMode::Cylindrical => 1_u8.hash(&mut hasher), @@ -378,42 +456,47 @@ where texmap.pngfile.hash(&mut hasher); texmap.glossmap.hash(&mut hasher); texmap.fallback.hash(&mut hasher); - }); + } let key_hash = hasher.finish(); - let batch_index = if let Some(batch_index) = - batch_indices.get(&key_hash).and_then(|indices| { - indices - .iter() - .copied() - .find(|index| batches[*index].0 == key) - }) { - batch_index + let batch_index = self.batch_indices.get(&key_hash).and_then(|indices| { + indices.iter().copied().find(|index| { + self.geometries[*index].provenance == provenance + && self.nodes_by_model.get(model).is_some_and(|nodes| { + nodes.iter().any(|(node, candidate)| { + *node == NodeId(self.geometries[*index].id.0) && candidate == &key + }) + }) + }) + }); + let batch_index = if let Some(index) = batch_index { + index } else { - let id = GeometryId((geometry_offset + batches.len() + 1) as u64); - let batch_index = batches.len(); - batches.push(( - key.clone(), - SceneGeometry { - id, - provenance: provenance_id, - points: Box::default(), - uvs: Box::default(), - triangles: Box::default(), - quads: Box::default(), - lines: Box::default(), - conditional_lines: Box::default(), - primitives: Box::default(), - }, - )); - batch_indices.entry(key_hash).or_default().push(batch_index); - batch_index + let id = GeometryId((self.geometries.len() + 1) as u64); + let index = self.geometries.len(); + self.geometries.push(SceneGeometry { + id, + provenance, + points: Box::default(), + uvs: Box::default(), + triangles: Box::default(), + quads: Box::default(), + lines: Box::default(), + conditional_lines: Box::default(), + primitives: Box::default(), + }); + self.nodes_by_model + .entry(model.clone()) + .or_default() + .push((NodeId(id.0), key.clone())); + self.batch_indices.entry(key_hash).or_default().push(index); + index }; - let batch = &mut batches[batch_index].1; + let batch = &mut self.geometries[batch_index]; let base = u32::try_from(batch.points.len()).map_err(|_| RendererError::IndexOverflow { context: "geometry index", })?; let point_count = - u32::try_from(primitive.vertices.len()).map_err(|_| RendererError::IndexOverflow { + u32::try_from(vertices.len()).map_err(|_| RendererError::IndexOverflow { context: "primitive point count", })?; append_boxed( @@ -421,28 +504,29 @@ where [ScenePrimitiveRange { point_start: base, point_count, - topology: smol_str::format_smolstr!("{:?}", primitive.line_type), - bfc_state: smol_str::format_smolstr!("{:?}", primitive.bfc.state), - bfc_cull: primitive.bfc.clipping, - bfc_ccw: matches!(primitive.bfc.winding, polymodel_ldraw_core::Winding::Ccw), - bfc_inverted: primitive.inverted, - texmap: texmap.clone(), + topology: smol_str::format_smolstr!("{:?}", line_type), + bfc_state: smol_str::format_smolstr!("{:?}", bfc.state), + bfc_cull: bfc.clipping, + bfc_ccw: matches!(bfc.winding, polymodel_ldraw_core::Winding::Ccw), + bfc_inverted: inverted, + texmap: self.nodes_by_model[model] + .iter() + .find(|(node, _)| *node == NodeId(batch.id.0)) + .map(|(_, key)| key.texmap.clone()) + .flatten(), }], ); - let point_start = batch.points.len(); - let world_points = primitive - .vertices + append_boxed( + &mut batch.points, + vertices + .iter() + .map(|point| ldraw_to_world([point.x, point.y, point.z])), + ); + if let Some(texmap) = self.nodes_by_model[model] .iter() - .copied() - .map(|point| { - finite_f32(point.x)?; - finite_f32(point.y)?; - finite_f32(point.z)?; - Ok(ldraw_to_world([point.x, point.y, point.z])) - }) - .collect::, RendererError>>()?; - append_boxed(&mut batch.points, world_points.iter().copied()); - if let Some(texmap) = texmap.as_ref() { + .find(|(node, _)| *node == NodeId(batch.id.0)) + .and_then(|(_, key)| key.texmap.as_ref()) + { let mut parameters = texmap .parameters .iter() @@ -452,119 +536,140 @@ where .map_err(|_| RendererError::parse("invalid TEXMAP parameter")) }) .collect::, _>>()?; - const TEXMAP_CONTROL_POINT_LEN: usize = 9; - let parameter_count = match texmap.mode { + let expected = match texmap.mode { SceneTexmapProjectionMode::Planar => 9, SceneTexmapProjectionMode::Cylindrical => 10, SceneTexmapProjectionMode::Spherical => 11, }; - if parameters.len() != parameter_count { + if parameters.len() != expected { return Err(RendererError::parse("invalid TEXMAP parameter count")); } - for offset in (0..TEXMAP_CONTROL_POINT_LEN).step_by(3) { - let world = ldraw_to_world([ + for offset in (0..9).step_by(3) { + let point = [ parameters[offset], parameters[offset + 1], parameters[offset + 2], - ]); - parameters[offset..offset + 3].copy_from_slice(&world); + ]; + parameters[offset..offset + 3].copy_from_slice(&ldraw_to_world(point)); } - let generated = texmap_uvs(texmap.mode, ¶meters, &world_points) - .map_err(|_| RendererError::parse("invalid TEXMAP UV projection"))?; + let generated = texmap_uvs( + texmap.mode, + ¶meters, + &batch.points[batch.points.len() - vertices.len()..], + ) + .map_err(|_| RendererError::parse("invalid TEXMAP UV projection"))?; append_boxed(&mut batch.uvs, generated); } - debug_assert_eq!(batch.points.len(), point_start + primitive.vertices.len()); - match primitive.line_type { - LineType::Two if primitive.vertices.len() == 2 => { + match line_type { + LineType::Two if vertices.len() == 2 => { append_boxed(&mut batch.lines, [[base, base + 1]]) } - LineType::Three if primitive.vertices.len() == 3 => { + LineType::Three if vertices.len() == 3 => { append_boxed(&mut batch.triangles, [[base, base + 1, base + 2]]) } - LineType::Four if primitive.vertices.len() == 4 => { + LineType::Four if vertices.len() == 4 => { append_boxed(&mut batch.quads, [[base, base + 1, base + 2, base + 3]]) } - LineType::Five if primitive.vertices.len() == 4 => append_boxed( + LineType::Five if vertices.len() == 4 => append_boxed( &mut batch.conditional_lines, [[base, base + 1, base + 2, base + 3]], ), _ => {} } + Ok(()) } - Ok(batches) } -fn adapt_ldraw_assembly( - models: &[ModelSummary], - parsed_scene: Option<&polymodel_ldraw_core::SceneRecord>, - parsed: Option<&OwnedParseResult>, - fixture_event_primitives: Option< - &std::collections::HashMap>, - >, - resources: Option<&[polymodel_renderer_protocol::ModelResource]>, -) -> Result { - use polymodel_ldraw_core::LineType; - use std::collections::HashMap; - - // Semantic events are the adapter's geometry authority. Build a borrowed - // per-definition view for batching. - let parsed_event_primitives = parsed.map(|parsed| { - let mut by_key = HashMap::>::new(); - for record in &parsed.result.events { - if let SemanticEvent::GeometryEmitted { - source, - line_type, +impl SemanticSink for AdapterSink<'_> { + fn on_event(&mut self, event: &SemanticEvent<'_>, _context: &TraversalContext) { + let result = match event { + SemanticEvent::ModelBoundary { name, root } => { + let provenance = ProvenanceId((self.provenance.len() + 1) as u64); + self.provenance + .push(polymodel_renderer_protocol::scene::SceneProvenance { + id: provenance, + source: name.clone(), + span: None, + }); + self.metadata_by_model.insert( + root.clone(), + DefinitionMetadata { + definition_id: smol_str::format_smolstr!("{}", root.content_hash), + canonical_path: root.canonical_path.as_str().into(), + source_class: smol_str::format_smolstr!("{:?}", root.resolved_root), + }, + ); + self.current_model = Some(root.clone()); + self.current_provenance = Some(provenance); + Ok(()) + } + SemanticEvent::GeometryEmitted { colour, vertices, + line_type, bfc, texmap, inverted, .. - } = &record.event - { - by_key - .entry(source.clone()) - .or_default() - .push(EventPrimitive { - line_type: *line_type, - colour: *colour, - vertices: vertices.clone(), - texmap: texmap.clone(), - inverted: *inverted, - bfc: *bfc, - }); - } + } => self.emit_geometry(*colour, vertices, *line_type, *bfc, *texmap, *inverted), + _ => Ok(()), + }; + if let Err(error) = result { + self.error.get_or_insert(error); } - by_key - }); - let event_primitives = parsed_event_primitives - .as_ref() - .or(fixture_event_primitives); - let event_models = models; + } +} +fn adapt_ldraw_assembly_with_sink( + models: &[ModelSummary], + parsed_scene: Option<&polymodel_ldraw_core::SceneRecord>, + parsed: Option<&OwnedParseResult>, + resources: Option<&[polymodel_renderer_protocol::ModelResource]>, + sink: Option>, +) -> Result { + use std::collections::HashMap; + + let event_models = models; + let mut adapter_sink = sink.unwrap_or_else(|| AdapterSink::new(resources)); + adapter_sink.take_error()?; let diagnostics_enabled = std::env::var_os("POLYMODEL_LDRAW_ADAPTER_DIAGNOSTICS").is_some() || std::env::var_os("POLYMODEL_LDRAW_SCENE_DIAGNOSTICS").is_some(); let mut node_by_key = diagnostics_enabled.then(HashMap::new); - let mut nodes_by_model = HashMap::with_capacity(event_models.len()); - let mut metadata_by_model = HashMap::with_capacity(event_models.len()); + let mut metadata_by_model = event_models + .iter() + .map(|model| { + ( + model.model, + DefinitionMetadata { + definition_id: smol_str::format_smolstr!("{}", model.cache_key.content_hash), + canonical_path: model.cache_key.canonical_path.as_str().into(), + source_class: smol_str::format_smolstr!("{:?}", model.cache_key.resolved_root), + }, + ) + }) + .collect::>(); + let mut nodes_by_model = event_models + .iter() + .map(|model| { + ( + model.model, + adapter_sink + .nodes_by_model + .get(&model.cache_key) + .cloned() + .unwrap_or_default(), + ) + }) + .collect::>(); let source_by_model = event_models .iter() .map(|model| (model.model, Arc::::from(model.path.as_str()))) .collect::>(); if std::env::var_os("POLYMODEL_LDRAW_ADAPTER_DIAGNOSTICS").is_some() { - let definitions_with_semantic_triangles = event_primitives - .as_ref() - .map(|by_key| { - by_key - .values() - .filter(|primitives| { - primitives.iter().any(|primitive| { - matches!(primitive.line_type, LineType::Three | LineType::Four) - }) - }) - .count() - }) - .unwrap_or(0); + let definitions_with_semantic_triangles = nodes_by_model + .values() + .filter(|nodes| !nodes.is_empty()) + .count(); let definitions_with_geometry_triangles = definitions_with_semantic_triangles; tracing::debug!( "[ldraw-adapter] definitions={} semantic_triangle_defs={} geometry_triangle_defs={} root_key={:?}", @@ -578,33 +683,23 @@ fn adapt_ldraw_assembly( "[ldraw-adapter] def[{index}] path={} key={:?} semantic_triangles={} geometry_records={} geometry_triangles={}", model.path, model.cache_key, - event_primitives - .as_ref() - .and_then(|by_key| by_key.get(&model.cache_key)) - .map(|primitives| primitives.len()) + nodes_by_model + .get(&model.model) + .map(|nodes| nodes.len()) .unwrap_or(0), - event_primitives - .as_ref() - .and_then(|by_key| by_key.get(&model.cache_key)) - .map(|primitives| primitives.len()) + nodes_by_model + .get(&model.model) + .map(|nodes| nodes.len()) .unwrap_or(0), - event_primitives - .as_ref() - .and_then(|by_key| by_key.get(&model.cache_key)) - .map(|primitives| { - primitives - .iter() - .filter(|primitive| { - matches!(primitive.line_type, LineType::Three | LineType::Four) - }) - .count() - }) + nodes_by_model + .get(&model.model) + .map(|nodes| nodes.len()) .unwrap_or(0), ); } } - let mut geometries = Vec::with_capacity(models.len()); - let mut provenance = Vec::with_capacity(models.len()); + let mut geometries = adapter_sink.geometries; + let mut provenance = adapter_sink.provenance; let mut uv_cache = HashMap::<(GeometryId, u64, u64), Arc<[[f32; 2]]>>::new(); let mut uv_cache_hits = 0_u64; let mut uv_cache_misses = 0_u64; @@ -663,44 +758,15 @@ fn adapt_ldraw_assembly( >::new(); let mut materials = Vec::new(); for model in event_models { - let provenance_id = ProvenanceId((provenance.len() + 1) as u64); - provenance.push(polymodel_renderer_protocol::scene::SceneProvenance { - id: provenance_id, - source: model.path.clone(), - span: None, - }); - let primitives = event_primitives - .as_ref() - .and_then(|by_key| by_key.get(&model.cache_key)) - .map(Vec::as_slice) - .unwrap_or(&[]); - let batches = build_primitive_batches( - model, - primitives, - provenance_id, - geometries.len(), - &texmap_available, - )?; - let nodes = batches - .into_iter() - .map(|(key, geometry)| { - let node = NodeId(geometry.id.0); - geometries.push(geometry); - (node, key) - }) - .collect::>(); - metadata_by_model.insert( - model.model, - DefinitionMetadata { - definition_id: smol_str::format_smolstr!("{}", model.cache_key.content_hash), - canonical_path: model.cache_key.canonical_path.as_str().into(), - source_class: smol_str::format_smolstr!("{:?}", model.cache_key.resolved_root), - }, - ); if let Some(node_by_key) = node_by_key.as_mut() { - node_by_key.insert(model.cache_key.clone(), nodes.clone()); + node_by_key.insert( + model.cache_key.clone(), + nodes_by_model + .get(&model.model) + .cloned() + .unwrap_or_default(), + ); } - nodes_by_model.insert(model.model, nodes); } let edge_colour_for = |colour: u32| { parsed @@ -763,25 +829,6 @@ fn adapt_ldraw_assembly( let instances = parsed_scene .map(|scene| scene.instances.as_slice()) .unwrap_or(&[]); - let event_includes = parsed - .map(|parsed| { - parsed - .result - .events - .iter() - .filter_map(|record| match &record.event { - SemanticEvent::IncludeInstantiated { - target, - transform, - colour, - bfc, - texmap, - } => Some((target.clone(), *transform, *colour, *bfc, texmap.clone())), - _ => None, - }) - .collect::>() - }) - .unwrap_or_default(); if let Some(node_by_key) = node_by_key.as_ref() && std::env::var_os("POLYMODEL_LDRAW_ADAPTER_DIAGNOSTICS").is_some() { @@ -872,7 +919,6 @@ fn adapt_ldraw_assembly( let mut logical_parts = Vec::>::with_capacity(instances.len()); for (instance_index, occurrence) in instances.iter().enumerate() { - let event_include = event_includes.get(instance_index); let inherited_logical_part = occurrence .parent .and_then(|parent| logical_parts.get(parent as usize)) @@ -900,13 +946,8 @@ fn adapt_ldraw_assembly( .parent .and_then(|parent| instances.get(parent as usize)) .map(|parent| parent.instance_id.clone()); - let inherited_colour = event_include - .map(|(_, _, colour, _, _)| u32::from(*colour)) - .or_else(|| occurrence.colour.map(u32::from)) - .unwrap_or(16); - let occurrence_transform = event_include - .map(|(_, transform, _, _, _)| *transform) - .unwrap_or(occurrence.transform); + let inherited_colour = occurrence.colour.map(u32::from).unwrap_or(16); + let occurrence_transform = occurrence.transform; let transform = world_transform(occurrence_transform)?; for (node, primitive) in batches { let (render_node, occurrence_uvs) = if let Some(texmap) = primitive.texmap.as_ref() { @@ -2089,7 +2130,8 @@ pub fn prepare_preview_scene( Some("pinned-omr".into()), ), ); - let result = InProcessRustAdapter::parse_with_limits( + let mut sink = AdapterSink::new(Some(&resources)); + let result = InProcessRustAdapter::parse_with_limits_and_sink( AdapterRequest { bytes: &bytes, profile: ParserProfile::Compatibility, @@ -2101,9 +2143,11 @@ pub fn prepare_preview_scene( cancellation: CancellationPolicy::default(), }, preview_limits(), + &mut sink, ) .map_err(|error| RendererError::ParseMessage(format!("pinned OMR parse: {error}")))?; - let scene = adapt_ldraw_parse_with_resources(&result, Some(&resources))?; + sink.take_error()?; + let scene = adapt_ldraw_parse_result(&result, Some(&resources), sink)?; if scene.stats.triangles == 0 || scene.stats.occurrences == 0 || scene.bounds.is_none() { return Err(RendererError::ParseMessage(format!( "preview root scene incomplete: occurrences={} triangles={} bounds={}", diff --git a/crates/polymodel-renderer-worker/src/tests.rs b/crates/polymodel-renderer-worker/src/tests.rs index 25e0db9..1009815 100644 --- a/crates/polymodel-renderer-worker/src/tests.rs +++ b/crates/polymodel-renderer-worker/src/tests.rs @@ -2043,13 +2043,24 @@ fn ldraw_policy_matches_three_d_direction_and_material_contract() { #[test] fn seeded_ldconfig_red_is_not_the_grey_material_fallback() { let bytes = b"0 !COLOUR Red CODE 4 VALUE #C91A09 EDGE #212121\n3 4 0 0 0 10 0 0 0 10 0\n"; - let parsed = polymodel_ldraw_core::InProcessRustAdapter::parse_fixture( - "seeded-ldconfig-red", - bytes, - polymodel_ldraw_core::ParserProfile::Strict, + let mut sink = AdapterSink::new(None); + let parsed = polymodel_ldraw_core::InProcessRustAdapter::parse_with_limits_and_sink( + polymodel_ldraw_core::AdapterRequest { + bytes, + profile: polymodel_ldraw_core::ParserProfile::Strict, + provenance_id: "seeded-ldconfig-red", + root_name: "seeded-ldconfig-red", + root: polymodel_ldraw_core::RootId::CurrentMpd, + materializations: &[], + target_selections: &[], + cancellation: polymodel_ldraw_core::CancellationPolicy::default(), + }, + polymodel_ldraw_core::LdrawLimits::default(), + &mut sink, ) .expect("seeded LDConfig colour should parse"); - let scene = adapt_ldraw_parse(&parsed).expect("seeded colour should adapt"); + sink.take_error().expect("seeded geometry should adapt"); + let scene = adapt_ldraw_parse_result(&parsed, None, sink).expect("seeded colour should adapt"); let material = scene.materials.first().expect("seeded code-4 material"); assert_eq!(material.colour, 4); assert_eq!(material.colour_rgb, Some([201, 26, 9])); @@ -2262,7 +2273,15 @@ fn ldraw_occurrence_transform_is_conjugated_and_bounds_use_instances() { polymodel_ldraw_core::RootId::OfficialLibrary, b"child-a", ); - let child_geometry = EventPrimitive { + let child_geometry = polymodel_ldraw_core::GeometryRecord { + primitive_id: 1, + source: child_key.clone(), + span: polymodel_ldraw_core::Span { + start: 0, + end: 0, + line: 1, + column: 1, + }, line_type: LineType::Three, colour: polymodel_ldraw_core::ColourCode(4), vertices: vec![ @@ -2356,14 +2375,22 @@ fn ldraw_occurrence_transform_is_conjugated_and_bounds_use_instances() { bounds: None, reflection: false, }; - let scene = adapt_ldraw_assembly( - &[root, child], - Some(&parsed_scene), - None, - Some(&fixture_events), - None, - ) - .unwrap(); + let mut sink = AdapterSink::new(None); + sink.current_model = Some(child.cache_key.clone()); + sink.current_provenance = Some(ProvenanceId(1)); + sink.provenance + .push(polymodel_renderer_protocol::scene::SceneProvenance { + id: ProvenanceId(1), + source: child.path.clone(), + span: None, + }); + for primitive in fixture_events.get(&child.cache_key).unwrap() { + sink.emit_fixture(primitive); + } + sink.take_error().unwrap(); + let scene = + adapt_ldraw_assembly_with_sink(&[root, child], Some(&parsed_scene), None, None, Some(sink)) + .unwrap(); let child_geometry = scene .geometries .iter() @@ -2486,7 +2513,15 @@ fn ldraw_repeated_same_path_instances_keep_exact_definition_keys() { texmap: None, } }; - let fixture_primitive = || EventPrimitive { + let fixture_primitive = || polymodel_ldraw_core::GeometryRecord { + primitive_id: 1, + source: child_a_key.clone(), + span: polymodel_ldraw_core::Span { + start: 0, + end: 0, + line: 1, + column: 1, + }, line_type: LineType::Three, colour: polymodel_ldraw_core::ColourCode(4), vertices: vec![ @@ -2519,15 +2554,26 @@ fn ldraw_repeated_same_path_instances_keep_exact_definition_keys() { }; let child_a = child(polymodel_ldraw_core::ModelId(2), "a", child_a_key.clone()); let child_b = child(polymodel_ldraw_core::ModelId(3), "b", child_b_key.clone()); - let mut fixture_events = std::collections::HashMap::new(); - fixture_events.insert(child_a.cache_key.clone(), vec![fixture_primitive()]); - fixture_events.insert(child_b.cache_key.clone(), vec![fixture_primitive()]); - let scene = adapt_ldraw_assembly( + let mut sink = AdapterSink::new(None); + for child in [&child_a, &child_b] { + sink.current_model = Some(child.cache_key.clone()); + sink.current_provenance = Some(ProvenanceId((sink.provenance.len() + 1) as u64)); + sink.provenance + .push(polymodel_renderer_protocol::scene::SceneProvenance { + id: sink.current_provenance.unwrap(), + source: child.path.clone(), + span: None, + }); + let primitive = fixture_primitive(); + sink.emit_fixture(&primitive); + } + sink.take_error().unwrap(); + let scene = adapt_ldraw_assembly_with_sink( &[root, child_a, child_b], Some(&parsed_scene), None, - Some(&fixture_events), None, + Some(sink), ) .unwrap(); assert_eq!(scene.occurrences.len(), 2); diff --git a/crates/polymodel-renderer-worker/src/worker.rs b/crates/polymodel-renderer-worker/src/worker.rs index 080866e..9bbfa76 100644 --- a/crates/polymodel-renderer-worker/src/worker.rs +++ b/crates/polymodel-renderer-worker/src/worker.rs @@ -17,8 +17,7 @@ use wasm_bindgen_futures::spawn_local; use web_sys::{AbortController, DedicatedWorkerGlobalScope}; use polymodel_ldraw_core::{ - AdapterRequest, CancellationPolicy, InProcessRustAdapter, Materialization, NormalizedPath, - ParserProfile, RootId, + AdapterRequest, CancellationPolicy, Materialization, NormalizedPath, ParserProfile, RootId, }; use polymodel_mesh::contract::ModelMesh; use polymodel_mesh::parser::load_mesh_with_companions; @@ -28,7 +27,7 @@ use polymodel_renderer_protocol::{ NegotiatedProtocol, ProtocolHello, RendererCommand, RendererEvent, RendererNamespace, ResourceToken, deserialize_command_envelope, serialize_event_envelope, }; -use polymodel_renderer_worker::{RendererError, adapt_ldraw_parse_with_resources}; +use polymodel_renderer_worker::{ParseAndAdaptError, RendererError, parse_and_adapt_ldraw}; use three_d_asset::InnerSpace as _; use polymodel_renderer_worker::admission::model_with_default_materials; @@ -219,22 +218,27 @@ fn parse_compound_source( resources = source.resources.len(), "renderer worker: invoking compound parser" ); - let result = InProcessRustAdapter::parse(AdapterRequest { - bytes: &primary.bytes, - profile: ParserProfile::Compatibility, - provenance_id: &source.primary, - root_name: &source.primary, - root: primary_root, - materializations: &materializations, - target_selections: &target_selections, - cancellation, - }) + let scene = parse_and_adapt_ldraw( + AdapterRequest { + bytes: &primary.bytes, + profile: ParserProfile::Compatibility, + provenance_id: &source.primary, + root_name: &source.primary, + root: primary_root, + materializations: &materializations, + target_selections: &target_selections, + cancellation, + }, + polymodel_ldraw_core::LdrawLimits::default(), + Some(&source.resources), + ) .map_err(|error| match error { - polymodel_ldraw_core::ParseError::Cancelled => CompoundLoadError::Cancelled, - error => CompoundLoadError::Parse(error), + ParseAndAdaptError::Parse(polymodel_ldraw_core::ParseError::Cancelled) => { + CompoundLoadError::Cancelled + } + ParseAndAdaptError::Parse(error) => CompoundLoadError::Parse(error), + ParseAndAdaptError::Adapt(error) => CompoundLoadError::Adapt(error), })?; - let scene = adapt_ldraw_parse_with_resources(&result, Some(&source.resources)) - .map_err(CompoundLoadError::Adapt)?; let stats = polymodel_renderer_protocol::MeshStats { vertices: scene.stats.points.min(u64::from(u32::MAX)) as u32, triangles: scene.stats.triangles.min(u64::from(u32::MAX)) as u32, diff --git a/crates/polymodel-renderer-worker/tests/ldraw_preview.rs b/crates/polymodel-renderer-worker/tests/ldraw_preview.rs index 283a4eb..91d4493 100644 --- a/crates/polymodel-renderer-worker/tests/ldraw_preview.rs +++ b/crates/polymodel-renderer-worker/tests/ldraw_preview.rs @@ -10,7 +10,7 @@ use polymodel_ldraw_core::{ ParserProfile, RootId, }; use polymodel_renderer_worker::{ - LdrawLightingMaterialPolicy, adapt_ldraw_parse, official_materializations, preview_limits, + LdrawLightingMaterialPolicy, official_materializations, parse_and_adapt_ldraw, preview_limits, scene_instance_batches, scene_mesh_topology, }; @@ -92,7 +92,22 @@ fn isolated_plate() -> IsolatedPlate { ); } - let scene = adapt_ldraw_parse(&result).expect("official 3021.dat must adapt"); + let scene = parse_and_adapt_ldraw( + AdapterRequest { + bytes: &bytes, + profile: ParserProfile::Compatibility, + provenance_id: "official-3021.dat", + root_name: PLATE_2_X_3, + root: RootId::OfficialLibrary, + materializations: &materializations, + target_selections: &closure.target_selections, + cancellation: CancellationPolicy::default(), + }, + preview_limits(), + None, + ) + .map_err(|error| format!("{error:?}")) + .expect("official 3021.dat must adapt"); let plate_geometry = scene .geometries .iter() @@ -229,7 +244,22 @@ fn assert_official_subpart_identity(part: &str, authored_target: &str, physical_ assert_eq!(edge.target_key.as_ref(), Some(&selected_key)); assert_eq!(edge.target_model, physical_target); - let scene = adapt_ldraw_parse(&result).expect("resolved parent must adapt"); + let scene = parse_and_adapt_ldraw( + AdapterRequest { + bytes: &bytes, + profile: ParserProfile::Compatibility, + provenance_id: part, + root_name: part, + root: RootId::OfficialLibrary, + materializations: &materializations, + target_selections: &closure.target_selections, + cancellation: CancellationPolicy::default(), + }, + preview_limits(), + None, + ) + .map_err(|error| format!("{error:?}")) + .expect("resolved parent must adapt"); let occurrence = scene .occurrences .iter() @@ -361,7 +391,22 @@ fn official_materializations_seed_ldconfig_colour_table() { .iter() .find(|model| model.cache_key == root_key) .expect("root materialization must own parsed model"); - let scene = adapt_ldraw_parse(&parsed).expect("seeded colour should adapt"); + let scene = parse_and_adapt_ldraw( + AdapterRequest { + bytes: root_bytes, + profile: ParserProfile::Compatibility, + provenance_id: "official-ldconfig-root", + root_name: path.as_str(), + root: RootId::OfficialLibrary, + materializations: &materializations, + target_selections: &closure.target_selections, + cancellation: CancellationPolicy::default(), + }, + preview_limits(), + None, + ) + .map_err(|error| format!("{error:?}")) + .expect("seeded colour should adapt"); let material = scene .materials .iter() @@ -478,7 +523,22 @@ fn pinned_scene() -> polymodel_renderer_protocol::scene::Scene { ) .expect("pinned OMR must parse"); eprintln!("[ldraw-preview-test] parse complete"); - let scene = adapt_ldraw_parse(&result).expect("pinned OMR must adapt to a scene"); + let scene = parse_and_adapt_ldraw( + AdapterRequest { + bytes: &bytes, + profile: ParserProfile::Compatibility, + provenance_id: "omr-10030-1.mpd", + root_name: "omr-10030-1.mpd", + root: RootId::CurrentMpd, + materializations: &materializations, + target_selections: &target_selections, + cancellation: CancellationPolicy::default(), + }, + preview_limits(), + None, + ) + .map_err(|error| format!("{error:?}")) + .expect("pinned OMR must adapt to a scene"); assert!( scene.stats.occurrences > 0, "OMR root must traverse occurrences"