diff --git a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs index 3674690..785a585 100644 --- a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs +++ b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs @@ -339,7 +339,7 @@ fn run_visible_preview( }); let lights = NativeLights::new(&context); - let mut static_model: Option>> = None; + let mut static_model: Option = None; let mut explicit_model: Option> = None; let mut conditional_model: Option> = None; let mut render_plan: Option = None; diff --git a/crates/polymodel-renderer-worker/src/lib.rs b/crates/polymodel-renderer-worker/src/lib.rs index 2067491..bdbf947 100644 --- a/crates/polymodel-renderer-worker/src/lib.rs +++ b/crates/polymodel-renderer-worker/src/lib.rs @@ -195,6 +195,7 @@ fn adapt_ldraw_assembly( ) -> Result { use polymodel_ldraw_core::LineType; use std::collections::HashMap; + use std::hash::{Hash, Hasher}; #[derive(Clone)] struct DefinitionMetadata { @@ -323,6 +324,7 @@ fn adapt_ldraw_assembly( span: None, }); let mut batches = Vec::<(PrimitiveBatchKey, SceneGeometry)>::new(); + let mut batch_indices = HashMap::>::new(); for primitive in &model.geometry { let texmap = primitive.texmap.as_ref().and_then(|texmap| { let mode = match texmap.mode { @@ -361,14 +363,41 @@ fn adapt_ldraw_assembly( ^ !matches!(primitive.bfc.winding, polymodel_ldraw_core::Winding::Ccw), texmap: texmap.clone(), }; - let batch = if let Some((_, geometry)) = - batches.iter_mut().find(|(batch_key, _)| *batch_key == key) - { - geometry + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + key.colour.hash(&mut hasher); + match key.bfc_state { + polymodel_ldraw_core::BfcState::Unknown => 0_u8.hash(&mut hasher), + polymodel_ldraw_core::BfcState::Certified => 1_u8.hash(&mut hasher), + polymodel_ldraw_core::BfcState::Uncertified => 2_u8.hash(&mut hasher), + } + key.bfc_cull.hash(&mut hasher); + key.bfc_ccw.hash(&mut hasher); + key.reverse_winding.hash(&mut hasher); + key.texmap.as_ref().map(|texmap| { + match texmap.mode { + SceneTexmapProjectionMode::Planar => 0_u8.hash(&mut hasher), + SceneTexmapProjectionMode::Cylindrical => 1_u8.hash(&mut hasher), + SceneTexmapProjectionMode::Spherical => 2_u8.hash(&mut hasher), + } + texmap.parameters.hash(&mut hasher); + 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 } else { let id = GeometryId((geometries.len() + batches.len() + 1) as u64); + let batch_index = batches.len(); batches.push(( - key, + key.clone(), SceneGeometry { id, provenance: provenance_id, @@ -381,8 +410,10 @@ fn adapt_ldraw_assembly( primitives: Vec::new(), }, )); - &mut batches.last_mut().expect("inserted geometry batch").1 + batch_indices.entry(key_hash).or_default().push(batch_index); + batch_index }; + let batch = &mut batches[batch_index].1; let base = u32::try_from(batch.points.len()).map_err(|_| "geometry index overflow")?; let point_count = u32::try_from(primitive.vertices.len()) .map_err(|_| "primitive point count overflow")?; @@ -415,11 +446,8 @@ fn adapt_ldraw_assembly( .iter() .map(|value| value.parse::().map_err(|_| "invalid TEXMAP parameter")) .collect::, _>>()?; - let control_point_len = match texmap.mode { - SceneTexmapProjectionMode::Planar => 9, - SceneTexmapProjectionMode::Cylindrical => 9, - SceneTexmapProjectionMode::Spherical => 9, - }; + const TEXMAP_CONTROL_POINT_LEN: usize = 9; + let control_point_len = TEXMAP_CONTROL_POINT_LEN; let parameter_count = match texmap.mode { SceneTexmapProjectionMode::Planar => 9, SceneTexmapProjectionMode::Cylindrical => 10, @@ -579,6 +607,11 @@ fn adapt_ldraw_assembly( [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ]; + let mut geometry_indices = geometries + .iter() + .enumerate() + .map(|(index, geometry)| (geometry.id, index)) + .collect::>(); let root_batches = nodes_by_model .get(&models[0].model) .ok_or("root model has no geometry batches")?; @@ -657,16 +690,15 @@ fn adapt_ldraw_assembly( let transform = world_transform(occurrence.transform)?; for (node, primitive) in batches { let render_node = if let Some(texmap) = primitive.texmap.as_ref() { - let source_index = geometries - .iter() - .position(|geometry| geometry.id == GeometryId(node.0)) + let source_index = geometry_indices + .get(&GeometryId(node.0)) + .copied() .ok_or("occurrence geometry is missing")?; let mut occurrence_geometry = geometries[source_index].clone(); occurrence_geometry.id = GeometryId( geometries - .iter() + .last() .map(|geometry| geometry.id.0) - .max() .unwrap_or(0) .checked_add(1) .ok_or("geometry id overflow")?, @@ -674,6 +706,8 @@ fn adapt_ldraw_assembly( occurrence_geometry.uvs = occurrence_texmap_uvs(texmap, transform, &occurrence_geometry.points)?; let render_node = NodeId(occurrence_geometry.id.0); + let occurrence_index = geometries.len(); + geometry_indices.insert(occurrence_geometry.id, occurrence_index); geometries.push(occurrence_geometry); render_node } else { @@ -715,11 +749,6 @@ fn adapt_ldraw_assembly( }); } } - let geometry_indices = geometries - .iter() - .enumerate() - .map(|(index, geometry)| (geometry.id, index)) - .collect::>(); let bounds = parsed_scene .and_then(|scene| scene.bounds) .map(|bounds| SceneBounds { @@ -2151,7 +2180,7 @@ pub fn scene_oriented_mesh_topology( ) } -pub fn scene_oriented_mesh_topology_for_view( +fn scene_oriented_mesh_topology_for_view( geometry: &SceneGeometry, bfc_cull: bool, bfc_ccw: bool, @@ -3072,12 +3101,14 @@ impl ScenePicker { } } -pub fn pick_scene(scene: &Scene, ray: PickRay) -> Result, String> { +#[cfg(not(target_arch = "wasm32"))] +fn pick_scene(scene: &Scene, ray: PickRay) -> Result, String> { let plan = scene_render_plan(scene)?; ScenePicker::new(scene, plan)?.pick(ray) } -pub fn logical_part_selection( +#[cfg(not(target_arch = "wasm32"))] +fn logical_part_selection( picker: &ScenePicker, raw_index: usize, ) -> Result { @@ -3089,7 +3120,7 @@ pub fn build_scene_occurrence_models( context: &three_d::Context, scene: &Scene, occurrence_indices: &[usize], -) -> Result>, String> { +) -> Result { let mut selected = scene.clone(); selected.occurrences = occurrence_indices .iter() @@ -3109,7 +3140,7 @@ pub fn build_scene_occurrence_model( context: &three_d::Context, scene: &Scene, occurrence_index: usize, -) -> Result>, String> { +) -> Result { build_scene_occurrence_models(context, scene, &[occurrence_index]) } @@ -3556,19 +3587,11 @@ fn ldraw_colour_with_policy(rgb: [u8; 3], policy: LdrawLightingMaterialPolicy, a Srgba::new(r, g, b, alpha) } -pub type NativeSceneModel = Vec>; +pub type NativeSceneModel = Vec>>; pub fn build_scene_model( context: &three_d::Context, scene: &Scene, -) -> Result { - build_scene_model_for_view(context, scene, [0.0, 0.0, 1.0]) -} - -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) } @@ -4784,6 +4807,7 @@ fn build_scene_model_from_batches( let mut albedo_cache = HashMap::::new(); let mut gloss_cache = HashMap::::new(); let mut transparency_cache = HashMap::::new(); + let mut material_cache = HashMap::<(MaterialId, bool), Rc>::new(); let model = batches .iter() .map(|batch| { @@ -4859,14 +4883,21 @@ fn build_scene_model_from_batches( } cpu_material.metallic_roughness_texture = gloss_cache.get(&key).cloned(); } - let mut material = - if matches!(scene_material_type(translucent), MaterialType::Transparent) { - PhysicalMaterial::new_transparent(context, &cpu_material) - } else { - PhysicalMaterial::new_opaque(context, &cpu_material) - }; - material.render_states = - scene_material_render_states(translucent, batch.bfc_cull && policy.cull_back_faces); + let material_key = (batch.material, batch.bfc_cull && policy.cull_back_faces); + let material = if let Some(material) = material_cache.get(&material_key) { + material.clone() + } else { + let mut material = + if matches!(scene_material_type(translucent), MaterialType::Transparent) { + PhysicalMaterial::new_transparent(context, &cpu_material) + } else { + PhysicalMaterial::new_opaque(context, &cpu_material) + }; + material.render_states = scene_material_render_states(translucent, material_key.1); + let material = Rc::new(material); + material_cache.insert(material_key, material.clone()); + material + }; if transformations.is_empty() { return Ok(None); } @@ -4924,7 +4955,7 @@ fn build_scene_model_from_batches( }, &cpu_mesh, ), - material.clone(), + material, ))) }) .collect::, String>>()? diff --git a/crates/polymodel-renderer-worker/src/worker.rs b/crates/polymodel-renderer-worker/src/worker.rs index 1082080..2cd46a3 100644 --- a/crates/polymodel-renderer-worker/src/worker.rs +++ b/crates/polymodel-renderer-worker/src/worker.rs @@ -61,7 +61,7 @@ struct InputState { struct RendererState { context: three_d::Context, model: Option>, - ldraw_static_model: Option>>, + ldraw_static_model: Option, ldraw_explicit_model: Option>, ldraw_conditional_model: Option>, ldraw_scene: Option>, @@ -221,6 +221,8 @@ struct WorkerState { )>, pixel_width: u32, pixel_height: u32, + cached_viewport: Option, + frame_events: Vec, input: InputState, needs_render: bool, disposed: bool, @@ -249,6 +251,8 @@ impl WorkerState { pending_scene: None, pixel_width: 1, pixel_height: 1, + cached_viewport: None, + frame_events: Vec::with_capacity(2), input: InputState::default(), needs_render: false, disposed: false, @@ -1076,12 +1080,11 @@ fn schedule_next_frame(st: &mut WorkerState, state: Rc>) { } fn consume_input_and_render(st: &mut WorkerState) -> Result<(), String> { - let renderer = match st.renderer.as_mut() { - Some(renderer) => renderer, - None => return Ok(()), - }; + if st.renderer.is_none() { + return Ok(()); + } - let mut events: Vec = Vec::new(); + st.frame_events.clear(); // Read input into locals to avoid borrowing st.input across the assignment below. let (x, y, button_down, last_consumed, wheel_delta) = { @@ -1100,7 +1103,7 @@ fn consume_input_and_render(st: &mut WorkerState) -> Result<(), String> { let dx = x - last_x; let dy = y - last_y; if dx != 0.0 || dy != 0.0 { - events.push(three_d::Event::MouseMotion { + st.frame_events.push(three_d::Event::MouseMotion { button: Some(three_d::MouseButton::Left), delta: (dx, dy), position: (x, y).into(), @@ -1113,7 +1116,7 @@ fn consume_input_and_render(st: &mut WorkerState) -> Result<(), String> { } if wheel_delta != 0.0 { - events.push(three_d::Event::MouseWheel { + st.frame_events.push(three_d::Event::MouseWheel { delta: (0.0, -wheel_delta), position: (x, y).into(), modifiers: Default::default(), @@ -1122,12 +1125,15 @@ fn consume_input_and_render(st: &mut WorkerState) -> Result<(), String> { st.input.wheel_delta = 0.0; } - if !events.is_empty() { + if let Some(renderer) = st.renderer.as_mut() + && !st.frame_events.is_empty() + { renderer .control - .handle_events(&mut renderer.camera, &mut events); + .handle_events(&mut renderer.camera, &mut st.frame_events); } + let renderer = st.renderer.as_mut().expect("renderer checked above"); if !button_down && renderer.ldraw_style == polymodel_renderer_worker::RenderStyle::Edges && let Some(plan) = renderer.ldraw_plan.as_ref() @@ -1166,8 +1172,13 @@ fn consume_input_and_render(st: &mut WorkerState) -> Result<(), String> { } } - let viewport = three_d::Viewport::new_at_origo(st.pixel_width, st.pixel_height); - renderer.camera.set_viewport(viewport); + let viewport = st + .cached_viewport + .get_or_insert_with(|| three_d::Viewport::new_at_origo(st.pixel_width, st.pixel_height)); + if viewport.width != st.pixel_width || viewport.height != st.pixel_height { + *viewport = three_d::Viewport::new_at_origo(st.pixel_width, st.pixel_height); + } + renderer.camera.set_viewport(*viewport); if let Some(bounds) = renderer.scene_bounds { match polymodel_renderer_worker::camera_space_clip_planes( bounds, diff --git a/src/viewer.rs b/src/viewer.rs index 931b523..d7e6b09 100644 --- a/src/viewer.rs +++ b/src/viewer.rs @@ -10,8 +10,6 @@ //! worker emits `ContextAcquired` once; the `context_acquisitions` signal stays at 1 //! across model switches (retention = no worker respawn). -#![allow(dead_code)] - #[cfg(target_arch = "wasm32")] use crate::client::PolymodelClient; use dioxus::prelude::*; @@ -2212,37 +2210,6 @@ impl WorkerBridgeState { Err(e) => tracing::error!("failed to serialize command envelope: {e}"), } } - - fn post_pointer_state(&mut self) { - let Some(state) = self.pending_pointer.take() else { - return; - }; - self.pointer_post_scheduled = false; - self.post_command(&RendererCommand::SetPointerState { - x: state.x, - y: state.y, - buttons: state.buttons, - modifiers: state.modifiers, - wheel_delta: state.wheel_delta, - }); - } - - fn schedule_pointer_post(&mut self) { - if self.pointer_post_scheduled { - return; - } - self.pointer_post_scheduled = true; - - // Use a box to capture self mutably — we need a self-referential closure. - // Instead, use a signal-like approach: store the bridge in a Rc and - // have the rAF closure borrow it. But we don't have the Rc here. - // Simpler: use window.requestAnimationFrame with a static closure that posts - // the pending state. The closure is stored in pointer_raf_closure. - // - // We can't capture `self` in a Closure (we're inside &mut self). The caller - // (attach_throttled_pointer_listeners) owns the Rc>, - // so the rAF closure is set up there, not here. - } } #[cfg(target_arch = "wasm32")]