diff --git a/crates/polymodel-renderer-protocol/src/lib.rs b/crates/polymodel-renderer-protocol/src/lib.rs index 96fb43f..cca59f5 100644 --- a/crates/polymodel-renderer-protocol/src/lib.rs +++ b/crates/polymodel-renderer-protocol/src/lib.rs @@ -443,16 +443,6 @@ pub enum RendererCommand { identity: LoadIdentity, plan: CompoundLoadPlan, }, - /// Render a compound LDraw plan to a temporary PNG preview. Verified bytes and - /// parsed scene data remain inside the worker; only the PNG crosses the bridge. - StartCompoundPreview { - request_id: u32, - namespace: RendererNamespace, - identity: LoadIdentity, - plan: CompoundLoadPlan, - width: u32, - height: u32, - }, /// Cancel the current identity. Late results are rejected by the reducer. Cancel { namespace: RendererNamespace, @@ -489,6 +479,18 @@ pub enum RendererCommand { Dispose, /// Deliberately panic — used by e2e tests to verify crash surfacing. DebugCrash, + /// Render a compound LDraw plan to a temporary PNG preview. Verified bytes and + /// parsed scene data remain inside the worker; only the PNG crosses the bridge. + /// + /// Appended to preserve the discriminants of existing command variants. + StartCompoundPreview { + request_id: u32, + namespace: RendererNamespace, + identity: LoadIdentity, + plan: CompoundLoadPlan, + width: u32, + height: u32, + }, } /// One opaque, same-origin resource route supplied by the public appview. @@ -1164,6 +1166,31 @@ mod tests { assert_eq!(bytes, [1, 7, 2, 8]); } + #[test] + fn independent_golden_compound_preview_envelope_bytes_remain_stable() { + let envelope = CommandEnvelope { + version: PROTOCOL_VERSION, + session_id: 7, + payload: CommandPayload::Command(RendererCommand::StartCompoundPreview { + request_id: 8, + namespace: RendererNamespace::Preview, + identity: LoadIdentity::new(3, 3), + plan: CompoundLoadPlan { + version: PROTOCOL_VERSION, + primary: "p".into(), + descriptors: vec![], + }, + width: 640, + height: 480, + }), + }; + let bytes = serialize_command_envelope(&envelope).unwrap(); + assert_eq!( + bytes, + [1, 7, 2, 10, 8, 1, 3, 3, 1, 1, 112, 0, 128, 5, 224, 3] + ); + } + #[test] fn independent_golden_event_envelope_bytes_remain_stable() { let envelope = EventEnvelope { diff --git a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs index 22b5e6e..f3db100 100644 --- a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs +++ b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs @@ -690,6 +690,7 @@ fn run_visible_preview( } #[cfg(not(target_arch = "wasm32"))] +#[derive(Debug)] struct ParsedOptions { input: PathBuf, output: Option, diff --git a/crates/polymodel-renderer-worker/src/lib.rs b/crates/polymodel-renderer-worker/src/lib.rs index 027a65c..dd56e86 100644 --- a/crates/polymodel-renderer-worker/src/lib.rs +++ b/crates/polymodel-renderer-worker/src/lib.rs @@ -1396,18 +1396,13 @@ pub fn native_png_bytes(width: u32, height: u32, pixels: &[u8]) -> Vec { output } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum RenderStyle { + #[default] Faces, Edges, } -impl Default for RenderStyle { - fn default() -> Self { - Self::Faces - } -} - #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Copy, Debug)] pub struct NativeRenderOptions { @@ -1963,9 +1958,9 @@ impl PickBounds { return None; } any = true; - for axis in 0..3 { - bounds.min[axis] = bounds.min[axis].min(point[axis]); - bounds.max[axis] = bounds.max[axis].max(point[axis]); + for (axis, coordinate) in point.into_iter().enumerate() { + bounds.min[axis] = bounds.min[axis].min(coordinate); + bounds.max[axis] = bounds.max[axis].max(coordinate); } } any.then_some(bounds) @@ -2366,16 +2361,14 @@ pub fn scene_render_plan(scene: &Scene) -> Result { let mut logical_parts = Vec::>::new(); let mut logical_bounds = Vec::::new(); let mut logical_part_bounds = std::collections::HashMap::::new(); - let batches = scene_instance_batches_for_mode( - scene, - &index, - &local_bounds, - &mut pick_occurrences, - &mut logical_part_indices, - &mut logical_parts, - &mut logical_bounds, - &mut logical_part_bounds, - )?; + let mut accumulator = SceneBatchAccumulator { + pick_occurrences: &mut pick_occurrences, + logical_part_indices: &mut logical_part_indices, + logical_parts: &mut logical_parts, + logical_bounds: &mut logical_bounds, + logical_part_bounds: &mut logical_part_bounds, + }; + let batches = scene_instance_batches_for_mode(scene, &index, &local_bounds, &mut accumulator)?; let batches_complete = started.elapsed(); let (upload_batches, planned_to_upload) = finalized_upload_batches(&index, &batches, &topologies)?; @@ -2410,15 +2403,19 @@ pub fn scene_instance_batches(scene: &Scene) -> Result, Ok(scene_render_plan(scene)?.batches) } +struct SceneBatchAccumulator<'a> { + pick_occurrences: &'a mut Vec, + logical_part_indices: &'a mut std::collections::HashMap, + logical_parts: &'a mut Vec>, + logical_bounds: &'a mut Vec, + logical_part_bounds: &'a mut std::collections::HashMap, +} + fn scene_instance_batches_for_mode( scene: &Scene, index: &SceneIndex, local_bounds: &[PickBounds], - pick_occurrences: &mut Vec, - logical_part_indices: &mut std::collections::HashMap, - logical_parts: &mut Vec>, - logical_bounds: &mut Vec, - logical_part_bounds: &mut std::collections::HashMap, + accumulator: &mut SceneBatchAccumulator<'_>, ) -> Result, String> { #[derive(Clone, Copy, PartialEq, Eq, Hash)] struct BatchKey { @@ -2443,7 +2440,7 @@ fn scene_instance_batches_for_mode( let bounds = local .transformed(occurrence.transform) .ok_or("occurrence has no finite render bounds")?; - pick_occurrences.push(PlannedOccurrence { + accumulator.pick_occurrences.push(PlannedOccurrence { occurrence_index, geometry_index, material: occurrence.material.unwrap_or(default_material), @@ -2460,16 +2457,20 @@ fn scene_instance_batches_for_mode( .logical_part_occurrence_id .as_ref() .unwrap_or(&occurrence.canonical_occurrence_id); - if let Some(group_index) = logical_part_indices.get(logical_part).copied() { - logical_parts[group_index].push(occurrence_index); - logical_bounds[group_index] = logical_bounds[group_index].union(bounds); + if let Some(group_index) = accumulator.logical_part_indices.get(logical_part).copied() { + accumulator.logical_parts[group_index].push(occurrence_index); + accumulator.logical_bounds[group_index] = + accumulator.logical_bounds[group_index].union(bounds); } else { - let group_index = logical_parts.len(); - logical_part_indices.insert(logical_part.clone(), group_index); - logical_parts.push(vec![occurrence_index]); - logical_bounds.push(bounds); + let group_index = accumulator.logical_parts.len(); + accumulator + .logical_part_indices + .insert(logical_part.clone(), group_index); + accumulator.logical_parts.push(vec![occurrence_index]); + accumulator.logical_bounds.push(bounds); } - logical_part_bounds + accumulator + .logical_part_bounds .entry(logical_part.clone()) .and_modify(|aggregate| { for axis in 0..3 { @@ -3089,14 +3090,14 @@ fn compact_edge_inputs_for_view( .into_values() .filter(|batch| !batch.segments.is_empty()) .collect(), - stats: stats.clone(), + stats, }; let mut conditional_inputs = CompactEdgeInputs { batches: conditional .into_values() .filter(|batch| !batch.segments.is_empty()) .collect(), - stats: stats.clone(), + stats, }; explicit_inputs.stats.material_groups = explicit_inputs.batches.len() + conditional_inputs.batches.len();