diff --git a/crates/polymodel-renderer-worker/src/lib.rs b/crates/polymodel-renderer-worker/src/lib.rs index fadf132..f70c956 100644 --- a/crates/polymodel-renderer-worker/src/lib.rs +++ b/crates/polymodel-renderer-worker/src/lib.rs @@ -8,11 +8,12 @@ use polymodel_renderer_protocol::scene::{ SceneId, SceneMaterial, SceneOccurrence, ScenePrimitiveRange, SceneStats, SceneTexmapAssociation, SceneTexmapProjectionMode, SceneTexture, TextureId, }; -use polymodel_renderer_protocol::{LengthUnit, Units}; +use polymodel_renderer_protocol::{LengthUnit, ModelResource, Units}; use smol_str::SmolStr; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::rc::Rc; #[cfg(not(target_arch = "wasm32"))] use std::sync::{Arc, Mutex}; @@ -26,6 +27,7 @@ use three_d::{ }; #[cfg(not(target_arch = "wasm32"))] use three_d::{InnerSpace, Object}; +use three_d_asset::{Texture2D, TextureData}; use web_time::Instant; #[cfg(not(target_arch = "wasm32"))] @@ -368,7 +370,7 @@ fn adapt_ldraw_assembly( .iter() .map(|value| value.parse::().map_err(|_| "invalid TEXMAP parameter")) .collect::, _>>()?; - let coordinate_len = match texmap.mode { + let control_point_len = match texmap.mode { SceneTexmapProjectionMode::Planar => 9, SceneTexmapProjectionMode::Cylindrical => 9, SceneTexmapProjectionMode::Spherical => 9, @@ -381,7 +383,7 @@ fn adapt_ldraw_assembly( if parameters.len() != parameter_count { return Err("invalid TEXMAP parameter count"); } - for offset in (0..coordinate_len).step_by(3) { + for offset in (0..control_point_len).step_by(3) { let world = ldraw_to_world([ parameters[offset], parameters[offset + 1], @@ -3416,6 +3418,14 @@ pub fn build_scene_model_for_view( context: &three_d::Context, scene: &Scene, _view: [f64; 3], +) -> Result { + build_scene_model_for_view_with_resources(context, scene, None) +} + +pub fn build_scene_model_for_view_with_resources( + context: &three_d::Context, + scene: &Scene, + resources: Option<&[ModelResource]>, ) -> Result { let plan = scene_render_plan(scene)?; build_scene_model_from_batches( @@ -3424,6 +3434,7 @@ pub fn build_scene_model_for_view( &plan.index, &plan.upload_batches, &plan.topologies, + resources, ) } @@ -3912,7 +3923,7 @@ impl Geometry for CompactSegmentGeometry { if let Err(error) = three_d::render_with_material(&self.context, viewer, self, material, lights) { - panic!("{error}"); + tracing::error!(%error, "rendering compact geometry with material failed"); } } fn render_with_effect( @@ -3932,7 +3943,7 @@ impl Geometry for CompactSegmentGeometry { color_texture, depth_texture, ) { - panic!("{error}"); + tracing::error!(%error, "rendering compact geometry with effect failed"); } } fn aabb(&self) -> AxisAlignedBoundingBox { @@ -4113,6 +4124,16 @@ pub fn build_scene_model_parts_for_style( scene: &Scene, view: [f64; 3], style: RenderStyle, +) -> Result { + build_scene_model_parts_for_style_with_resources(context, scene, view, style, None) +} + +pub fn build_scene_model_parts_for_style_with_resources( + context: &three_d::Context, + scene: &Scene, + view: [f64; 3], + style: RenderStyle, + resources: Option<&[ModelResource]>, ) -> Result { let started = Instant::now(); let plan = scene_render_plan(scene)?; @@ -4129,6 +4150,7 @@ pub fn build_scene_model_parts_for_style( &plan.index, &plan.upload_batches, &plan.topologies, + resources, )?; let static_elapsed = static_started.elapsed(); eprintln!( @@ -4177,6 +4199,7 @@ pub fn build_scene_faces_and_picker( &plan.index, &plan.upload_batches, &plan.topologies, + None, )?; eprintln!( "[ldraw-preview] native-build-stage=static elapsed_ms={}", @@ -4551,7 +4574,25 @@ fn build_scene_model_from_batches( index: &SceneIndex, batches: &[SceneInstanceBatch], topologies: &[SceneMeshTopology], + resources: Option<&[ModelResource]>, ) -> Result { + let resource_bytes = |texture: &polymodel_renderer_protocol::scene::SceneTexture| { + let key = texture + .resource_key + .as_deref() + .unwrap_or(texture.uri.as_str()); + resources + .and_then(|resources| { + resources + .iter() + .find(|resource| resource.key == key || resource.path == key) + }) + .map(|resource| (key.to_owned(), resource.bytes.as_slice())) + .ok_or_else(|| format!("texture resource is not supplied: {key}")) + }; + let mut albedo_cache = HashMap::::new(); + let mut gloss_cache = HashMap::::new(); + let mut transparency_cache = HashMap::::new(); let model = batches .iter() .map(|batch| { @@ -4561,27 +4602,91 @@ fn build_scene_model_from_batches( .iter() .map(|instance| scene_transform_to_mat4(instance.transform).map_err(str::to_owned)) .collect::, _>>()?; - let colour = index.material(scene, batch.material)?.colour; + let scene_material = index.material(scene, batch.material)?; + let colour = scene_material.colour; let policy = LdrawLightingMaterialPolicy::ldraw(); - let mut material = PhysicalMaterial::new_opaque( - context, - &CpuMaterial { - albedo: ldraw_colour_with_policy(colour, policy), - roughness: if colour == 0 { - policy.black_roughness - } else { - policy.roughness - }, - metallic: policy.metallic, - ..Default::default() + let mut cpu_material = CpuMaterial { + albedo: ldraw_colour_with_policy(colour, policy), + roughness: if colour == 0 { + policy.black_roughness + } else { + policy.roughness }, - ); + metallic: policy.metallic, + ..Default::default() + }; + let mut translucent = false; + if let Some(texture_id) = scene_material.albedo_texture { + let texture = scene + .textures + .iter() + .find(|texture| texture.id == texture_id) + .ok_or_else(|| format!("albedo texture {texture_id:?} is missing"))?; + let (key, bytes) = resource_bytes(texture)?; + if !albedo_cache.contains_key(&key) { + let (width, height, pixels, transparency) = decode_albedo_png(bytes)?; + transparency_cache.insert(key.clone(), transparency); + albedo_cache.insert( + key.clone(), + Texture2D { + name: texture.uri.to_string(), + data: TextureData::RgbaU8(pixels), + width, + height, + ..Default::default() + }, + ); + } + translucent = *transparency_cache + .get(&key) + .ok_or_else(|| format!("albedo texture cache lost resource {key}"))?; + cpu_material.albedo_texture = albedo_cache.get(&key).cloned(); + } + if let Some(texture_id) = scene_material.metallic_roughness_texture { + let texture = scene + .textures + .iter() + .find(|texture| texture.id == texture_id) + .ok_or_else(|| format!("GLOSSMAP texture {texture_id:?} is missing"))?; + let (key, bytes) = resource_bytes(texture)?; + if !gloss_cache.contains_key(&key) { + let decoded = decode_glossmap_png(bytes)?; + gloss_cache.insert( + key.clone(), + Texture2D { + name: texture.uri.to_string(), + data: TextureData::RgbU8(decoded.packed_rgb), + width: decoded.width, + height: decoded.height, + ..Default::default() + }, + ); + } + cpu_material.metallic_roughness_texture = gloss_cache.get(&key).cloned(); + } + let mut material = if translucent { + PhysicalMaterial::new_transparent(context, &cpu_material) + } else { + PhysicalMaterial::new_opaque(context, &cpu_material) + }; material.render_states = RenderStates { cull: if batch.bfc_cull && policy.cull_back_faces { Cull::Back } else { Cull::None }, + blend: if translucent { + Blend::Enabled { + source_rgb_multiplier: BlendMultiplierType::SrcAlpha, + source_alpha_multiplier: BlendMultiplierType::One, + destination_rgb_multiplier: BlendMultiplierType::OneMinusSrcAlpha, + destination_alpha_multiplier: BlendMultiplierType::OneMinusSrcAlpha, + rgb_equation: BlendEquationType::Add, + alpha_equation: BlendEquationType::Add, + } + } else { + Blend::Disabled + }, ..Default::default() }; if transformations.is_empty() { @@ -4914,7 +5019,7 @@ pub fn conditional_line_visible( pub mod admission; mod texture; pub use texture::{ - GlossMapPixels, decode_glossmap_png, packed_roughness_from_alpha, + GlossMapPixels, decode_albedo_png, decode_glossmap_png, packed_roughness_from_alpha, packed_roughness_from_alpha_samples, texmap_uvs, }; diff --git a/crates/polymodel-renderer-worker/src/tests.rs b/crates/polymodel-renderer-worker/src/tests.rs index cf0c5fe..691682b 100644 --- a/crates/polymodel-renderer-worker/src/tests.rs +++ b/crates/polymodel-renderer-worker/src/tests.rs @@ -20,6 +20,60 @@ fn texmap_planar_uvs_follow_basis_and_clip_to_texture() { assert_eq!(uvs, [[0.0, 0.0], [0.5, 0.5], [1.0, 0.0]]); } +#[test] +fn texmap_planar_solves_non_orthogonal_basis() { + let uvs = texmap_uvs( + SceneTexmapProjectionMode::Planar, + &[0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 1.0, 2.0, 0.0], + &[[0.0, 0.0, 0.0], [1.5, 1.0, 0.0]], + ) + .unwrap(); + assert_eq!(uvs, [[0.0, 0.0], [0.5, 0.5]]); +} + +#[test] +fn texmap_planar_rejects_collinear_basis() { + assert!( + texmap_uvs( + SceneTexmapProjectionMode::Planar, + &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.0, 0.0], + &[[0.5, 0.0, 0.0]], + ) + .is_err() + ); +} + +#[test] +fn texmap_cylindrical_and_spherical_use_degree_extents() { + let cylindrical = texmap_uvs( + SceneTexmapProjectionMode::Cylindrical, + &[0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 1.0, 0.0, 0.0, 360.0], + &[[1.0, 0.0, 0.0], [0.0, 1.0, 1.0]], + ) + .unwrap(); + assert!((cylindrical[0][0] - 0.5).abs() < 1e-6); + assert!((cylindrical[1][0] - 0.75).abs() < 1e-6); + + let spherical = texmap_uvs( + SceneTexmapProjectionMode::Spherical, + &[0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 360.0, 180.0], + &[[1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], + ) + .unwrap(); + assert!((spherical[0][0] - 0.75).abs() < 1e-6); + assert!((spherical[1][1] - 0.5).abs() < 1e-6); +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn albedo_decode_expands_rgb_and_preserves_alpha() { + let bytes = try_native_png_bytes(2, 1, &[255, 0, 0, 0, 255, 0, 128, 0]).unwrap(); + let (width, height, pixels, translucent) = decode_albedo_png(&bytes).unwrap(); + assert_eq!((width, height), (2, 1)); + assert_eq!(pixels, vec![[255, 0, 0, 0], [255, 0, 128, 0]]); + assert!(translucent); +} + #[test] fn gloss_roughness_packing_uses_alpha_only_and_checked_half_up() { assert_eq!(packed_roughness_from_alpha(0, 255).unwrap(), [0, 255, 0]); @@ -1582,8 +1636,10 @@ fn fixture_hash_and_required_metadata_are_stable() { .windows(b"BFC INVERTNEXT".len()) .any(|bytes| bytes == b"BFC INVERTNEXT") ); - assert_eq!(pm90_fixture_hash(), pm90_fixture_hash()); - assert_eq!(pm90_fixture_hash().len(), 64); + assert_eq!( + pm90_fixture_hash(), + "ab11c535b8676694e166ace527a1602d6c25897e7b06aac28717aeb3321d1a55" + ); } #[test] diff --git a/crates/polymodel-renderer-worker/src/texture.rs b/crates/polymodel-renderer-worker/src/texture.rs index 96186b8..fee5570 100644 --- a/crates/polymodel-renderer-worker/src/texture.rs +++ b/crates/polymodel-renderer-worker/src/texture.rs @@ -10,18 +10,7 @@ pub struct GlossMapPixels { } pub fn decode_glossmap_png(bytes: &[u8]) -> Result { - let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes)); - decoder.set_transformations(png::Transformations::EXPAND); - let mut reader = decoder - .read_info() - .map_err(|error| format!("decode GLOSSMAP header: {error}"))?; - let output_size = reader - .output_buffer_size() - .ok_or_else(|| "GLOSSMAP decoded size overflow".to_owned())?; - let mut output = vec![0; output_size]; - let info = reader - .next_frame(&mut output) - .map_err(|error| format!("decode GLOSSMAP pixels: {error}"))?; + let (info, output) = decode_png(bytes, "GLOSSMAP")?; let bytes = &output[..info.buffer_size()]; let (channels, has_alpha) = match info.color_type { png::ColorType::Rgba => (4usize, true), @@ -65,6 +54,62 @@ pub fn decode_glossmap_png(bytes: &[u8]) -> Result { }) } +fn decode_png(bytes: &[u8], label: &str) -> Result<(png::OutputInfo, Vec), String> { + let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes)); + decoder.set_transformations(png::Transformations::EXPAND); + let mut reader = decoder + .read_info() + .map_err(|error| format!("decode {label} header: {error}"))?; + let output_size = reader + .output_buffer_size() + .ok_or_else(|| format!("{label} decoded size overflow"))?; + let mut output = vec![0; output_size]; + let info = reader + .next_frame(&mut output) + .map_err(|error| format!("decode {label} pixels: {error}"))?; + Ok((info, output)) +} + +pub fn decode_albedo_png(bytes: &[u8]) -> Result<(u32, u32, Vec<[u8; 4]>, bool), String> { + let (info, output) = decode_png(bytes, "albedo")?; + if info.bit_depth != png::BitDepth::Eight { + return Err(format!( + "unsupported albedo bit depth: {:?}", + info.bit_depth + )); + } + let source = &output[..info.buffer_size()]; + let (channels, has_alpha) = match info.color_type { + png::ColorType::Rgba => (4, true), + png::ColorType::Rgb => (3, false), + png::ColorType::GrayscaleAlpha => (2, true), + png::ColorType::Grayscale => (1, false), + png::ColorType::Indexed => return Err("albedo indexed output was not expanded".into()), + }; + let mut rgba = Vec::with_capacity( + (info.width as usize) + .saturating_mul(info.height as usize) + .saturating_mul(4), + ); + let mut translucent = false; + for pixel in source.chunks_exact(channels) { + let (r, g, b, a) = match channels { + 4 => (pixel[0], pixel[1], pixel[2], pixel[3]), + 3 => (pixel[0], pixel[1], pixel[2], 255), + 2 => (pixel[0], pixel[0], pixel[0], pixel[1]), + 1 => (pixel[0], pixel[0], pixel[0], 255), + _ => unreachable!(), + }; + translucent |= a < 255; + rgba.push([r, g, b, a]); + } + if source.len() != rgba.len() * channels { + return Err("albedo decoded pixel length is inconsistent".into()); + } + let translucent = translucent || has_alpha && rgba.iter().any(|p| p[3] < 255); + Ok((info.width, info.height, rgba, translucent)) +} + pub fn packed_roughness_from_alpha(alpha: u32, max: u32) -> Result<[u8; 3], String> { if max == 0 || (max != u8::MAX as u32 && max != u16::MAX as u32) || alpha > max { return Err("invalid gloss alpha range".to_owned()); @@ -135,15 +180,26 @@ pub fn texmap_uvs( let u = sub(p2, p1); let v = sub(p3, p1); let uu = dot(u, u); + let uv = dot(u, v); let vv = dot(v, v); - if !uu.is_finite() || !vv.is_finite() || uu <= f64::EPSILON || vv <= f64::EPSILON { + let determinant = uu * vv - uv * uv; + if !uu.is_finite() + || !uv.is_finite() + || !vv.is_finite() + || !determinant.is_finite() + || determinant <= f64::EPSILON + { return Err("degenerate PLANAR TEXMAP basis".to_owned()); } points .iter() .map(|point| { let delta = sub(*point, p1); - [clamp01(dot(delta, u) / uu), clamp01(dot(delta, v) / vv)] + let du = dot(delta, u); + let dv = dot(delta, v); + let u_coordinate = (du * vv - dv * uv) / determinant; + let v_coordinate = (dv * uu - du * uv) / determinant; + [clamp01(u_coordinate), clamp01(v_coordinate)] }) .collect::>() } @@ -161,7 +217,7 @@ pub fn texmap_uvs( ], ); let radial_n = normalize(radial).ok_or("degenerate CYLINDRICAL TEXMAP radius")?; - let angle_extent = parameters[9]; + let angle_extent = parameters[9].to_radians(); if !angle_extent.is_finite() || angle_extent.abs() <= f64::EPSILON { return Err("degenerate CYLINDRICAL TEXMAP angle".to_owned()); } @@ -197,9 +253,13 @@ pub fn texmap_uvs( let horizontal = normalize(horizontal).ok_or("degenerate SPHERICAL TEXMAP basis")?; let vertical = normalize(cross(normal, horizontal)) .ok_or("degenerate SPHERICAL TEXMAP vertical")?; - let a = parameters[9]; - let b = parameters[10]; - if a.abs() <= f64::EPSILON || b.abs() <= f64::EPSILON { + let a = parameters[9].to_radians(); + let b = parameters[10].to_radians(); + if !a.is_finite() + || !b.is_finite() + || a.abs() <= f64::EPSILON + || b.abs() <= f64::EPSILON + { return Err("degenerate SPHERICAL TEXMAP angle".to_owned()); } points diff --git a/crates/polymodel-renderer-worker/src/worker.rs b/crates/polymodel-renderer-worker/src/worker.rs index d5ce5f0..a1a344c 100644 --- a/crates/polymodel-renderer-worker/src/worker.rs +++ b/crates/polymodel-renderer-worker/src/worker.rs @@ -100,6 +100,7 @@ struct VerifiedCompoundSource { resources: Vec, source_identities: Vec, target_selections: Vec, + reservation: CompositeReservation, } fn descriptor_root(root: &str) -> Result { @@ -170,6 +171,8 @@ fn parse_compound_source( ( polymodel_renderer_protocol::MeshStats, polymodel_renderer_protocol::scene::Scene, + Vec, + CompositeReservation, ), String, > { @@ -211,7 +214,7 @@ fn parse_compound_source( vertices: scene.stats.points.min(u64::from(u32::MAX)) as u32, triangles: scene.stats.triangles.min(u64::from(u32::MAX)) as u32, }; - Ok((stats, scene)) + Ok((stats, scene, source.resources, source.reservation)) } const WORKER_ID: u64 = 1; @@ -252,6 +255,7 @@ struct WorkerState { scope: DedicatedWorkerGlobalScope, canvas: Option, context_loss_listener: Option>, + context_loss_pending: Rc>, renderer: Option, pending_scene: Option<( three_d::Model, @@ -285,6 +289,7 @@ impl WorkerState { scope, canvas: None, context_loss_listener: None, + context_loss_pending: Rc::new(Cell::new(false)), renderer: None, pending_scene: None, pixel_width: 1, @@ -351,6 +356,18 @@ impl WorkerState { }); } + fn process_context_loss(&mut self) { + if !self.context_loss_pending.replace(false) || self.disposed { + return; + } + tracing::warn!("renderer worker: processing deferred WebGL context loss"); + self.dispose_resources(false); + self.post_event(&RendererEvent::Error { + fatal: true, + message: "OffscreenCanvas WebGL context lost; restarting renderer".into(), + }); + } + fn dispose_resources(&mut self, close_worker: bool) { if self.disposed { if close_worker { @@ -477,8 +494,10 @@ impl WorkerState { let control = three_d::OrbitControl::new(three_d::vec3(0.0, 0.0, 0.0), 0.1, 10.0); let state_for_context_loss = Rc::downgrade(&state); + let context_loss_pending = self.context_loss_pending.clone(); let listener = Closure::wrap(Box::new(move |event: web_sys::WebGlContextEvent| { event.prevent_default(); + context_loss_pending.set(true); let Some(state_for_context_loss) = state_for_context_loss.upgrade() else { return; }; @@ -489,6 +508,7 @@ impl WorkerState { return; } tracing::warn!("renderer worker: OffscreenCanvas WebGL context lost"); + state.context_loss_pending.set(false); state.dispose_resources(false); state.post_event(&RendererEvent::Error { fatal: true, @@ -726,9 +746,12 @@ mod tests { }, target: child_key, }], + reservation: ReservationLedger::new() + .composite(ReservationOwner::preview(79, 1, 1), &[]) + .expect("empty test reservation should be admitted"), }; let ledger = ReservationLedger::new(); - let (_stats, scene) = + let (_stats, scene, _resources, _reservation) = parse_compound_source(source, ReservationOwner::preview(79, 1, 1), &ledger) .expect("uploaded-root MPD selections should materialize"); assert_eq!(scene.stats.triangles, 1); @@ -1065,6 +1088,7 @@ fn start_frame_loop(state: &Rc>) { Err(_) => return, }; st.raf_id = None; + st.process_context_loss(); if st.disposed { return; @@ -1079,7 +1103,7 @@ fn start_frame_loop(state: &Rc>) { fatal: true, message, }); - st.disposed = true; + st.dispose_resources(false); return; } } @@ -1112,7 +1136,7 @@ fn schedule_next_frame(st: &mut WorkerState, state: Rc>) { fatal: true, message: "requestAnimationFrame scheduling failed repeatedly".into(), }); - st.disposed = true; + st.dispose_resources(false); return; } st.raf_retry_count = st.raf_retry_count.saturating_add(1); @@ -1890,7 +1914,8 @@ async fn render_compound_preview_request( compound_is_current(state, RendererNamespace::Preview, identity, abort.as_ref())?; let owner = preview_owner(identity); let ledger = state.borrow().ledger.clone(); - let (_stats, scene) = parse_compound_source(source, owner, &ledger)?; + let (_stats, scene, source_resources, source_reservation) = + parse_compound_source(source, owner, &ledger)?; compound_is_current(state, RendererNamespace::Preview, identity, abort.as_ref())?; let bounds = scene .bounds @@ -1899,12 +1924,14 @@ async fn render_compound_preview_request( let scene = Rc::new(scene); let style = polymodel_renderer_worker::RenderStyle::Faces; let (static_model, (_explicit, _conditional), _picker, _plan) = - polymodel_renderer_worker::build_scene_model_parts_for_style( + polymodel_renderer_worker::build_scene_model_parts_for_style_with_resources( &target.context, &scene, [0.0, 0.0, 1.0], style, + Some(&source_resources), )?; + drop(source_reservation); compound_is_current(state, RendererNamespace::Preview, identity, abort.as_ref())?; let fit = polymodel_renderer_worker::scene_camera_fit(bounds, width, height) .map_err(|error| error.to_string())?; @@ -2029,6 +2056,7 @@ async fn load_compound_source( resources: bundle.resources, source_identities: bundle.source_identities, target_selections: bundle.target_selections, + reservation, }; tracing::debug!( ?namespace, @@ -2037,7 +2065,6 @@ async fn load_compound_source( resources = source.resources.len(), "renderer worker: compound bundle admitted" ); - drop(reservation); Ok(source) } @@ -2104,53 +2131,54 @@ fn finish_compound_source( ?identity, "renderer worker: parsing compound bundle" ); - let (stats, scene) = match parse_compound_source(source, parse_owner, &st.ledger) { - Ok((stats, scene)) => { - tracing::info!( - ?namespace, - ?identity, - vertices = stats.vertices, - triangles = stats.triangles, - has_bounds = scene.bounds.is_some(), - "renderer worker: compound parse succeeded" - ); - (stats, scene) - } - Err(detail) => { - tracing::error!( - ?namespace, - ?identity, - error = %detail, - "renderer worker: compound parse failed" - ); - let error = LoadError { - code: LoadErrorCode::Parse, - detail: detail.clone(), - }; - let reducer = match namespace { - RendererNamespace::Interactive => &mut st.load_reducer, - RendererNamespace::Preview => &mut st.preview_reducer, - }; - let transition = reducer.reduce(LoadEvent::LoadFailed { - namespace, - identity, - error, - candidate: None, - }); - if matches!( - transition.outcome, - polymodel_renderer_protocol::TransitionOutcome::Committed - ) { - st.post_event(&RendererEvent::LoadFailed { + let (stats, scene, source_resources, _source_reservation) = + match parse_compound_source(source, parse_owner, &st.ledger) { + Ok((stats, scene, source_resources, source_reservation)) => { + tracing::info!( + ?namespace, + ?identity, + vertices = stats.vertices, + triangles = stats.triangles, + has_bounds = scene.bounds.is_some(), + "renderer worker: compound parse succeeded" + ); + (stats, scene, source_resources, source_reservation) + } + Err(detail) => { + tracing::error!( + ?namespace, + ?identity, + error = %detail, + "renderer worker: compound parse failed" + ); + let error = LoadError { + code: LoadErrorCode::Parse, + detail: detail.clone(), + }; + let reducer = match namespace { + RendererNamespace::Interactive => &mut st.load_reducer, + RendererNamespace::Preview => &mut st.preview_reducer, + }; + let transition = reducer.reduce(LoadEvent::LoadFailed { namespace, identity, - code: LoadErrorCode::Parse, - detail, + error, + candidate: None, }); + if matches!( + transition.outcome, + polymodel_renderer_protocol::TransitionOutcome::Committed + ) { + st.post_event(&RendererEvent::LoadFailed { + namespace, + identity, + code: LoadErrorCode::Parse, + detail, + }); + } + return; } - return; - } - }; + }; let reducer = match namespace { RendererNamespace::Interactive => &mut st.load_reducer, RendererNamespace::Preview => &mut st.preview_reducer, @@ -2199,11 +2227,12 @@ fn finish_compound_source( }; let scene_ref = std::rc::Rc::new(scene.clone()); let style = polymodel_renderer_worker::RenderStyle::Faces; - let models = match polymodel_renderer_worker::build_scene_model_parts_for_style( + let models = match polymodel_renderer_worker::build_scene_model_parts_for_style_with_resources( &context, &scene_ref, [0.0, 0.0, 1.0], style, + Some(&source_resources), ) { Ok((static_model, edge_model, _picker, plan)) => { (static_model, edge_model, plan, style)