From 6a04c5c9efe93039d1fee121cc6d5b1d380ce60c Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 10 Aug 2026 19:23:37 -0400 Subject: [PATCH] PM-84: add typed cancellation variant, warn on dropped edges, allow dead Edges variant Epic: PM-86 Task: PM-84 --- crates/polymodel-renderer-worker/src/lib.rs | 142 ++++++++++++++++-- .../polymodel-renderer-worker/src/worker.rs | 42 ++++-- tools/ldraw-compat-harness/Cargo.lock | 12 +- tools/ldraw-compat-harness/src/main.rs | 46 +++++- 4 files changed, 217 insertions(+), 25 deletions(-) diff --git a/crates/polymodel-renderer-worker/src/lib.rs b/crates/polymodel-renderer-worker/src/lib.rs index bb9b7a1..8c39696 100644 --- a/crates/polymodel-renderer-worker/src/lib.rs +++ b/crates/polymodel-renderer-worker/src/lib.rs @@ -184,6 +184,8 @@ pub enum RendererError { Io(String), #[error("renderer generation exhausted; restart the worker")] GenerationExhausted, + #[error("renderer request cancelled")] + Cancelled, #[error("external runtime failure: {0}")] External(String), } @@ -2790,6 +2792,7 @@ pub fn scene_mesh_topology_for_view( #[derive(Clone, Copy)] enum SceneTopologyKind { Faces, + #[allow(dead_code)] Edges, Complete, } @@ -3813,31 +3816,142 @@ fn planned_edge_source(geometry: &SceneGeometry) -> Result = geometry .lines .iter() - .filter_map(|line| { - let start = geometry.points.get(line[0] as usize).copied()?; - let end = geometry.points.get(line[1] as usize).copied()?; - Some((finite_f32_vec3(start).ok()?, finite_f32_vec3(end).ok()?)) + .enumerate() + .filter_map(|(line_index, line)| { + let Some(start) = geometry.points.get(line[0] as usize).copied() else { + tracing::warn!( + edge_kind = "line", + record_index = line_index, + indices = ?line, + "dropping malformed edge geometry" + ); + return None; + }; + let Some(end) = geometry.points.get(line[1] as usize).copied() else { + tracing::warn!( + edge_kind = "line", + record_index = line_index, + indices = ?line, + "dropping malformed edge geometry" + ); + return None; + }; + let start = match finite_f32_vec3(start) { + Ok(start) => start, + Err(_) => { + tracing::warn!( + edge_kind = "line", + record_index = line_index, + indices = ?line, + "dropping malformed edge geometry" + ); + return None; + } + }; + let end = match finite_f32_vec3(end) { + Ok(end) => end, + Err(_) => { + tracing::warn!( + edge_kind = "line", + record_index = line_index, + indices = ?line, + "dropping malformed edge geometry" + ); + return None; + } + }; + if start == end { + tracing::warn!( + edge_kind = "line", + record_index = line_index, + indices = ?line, + "dropping malformed edge geometry" + ); + return None; + } + Some([start, end]) }) - .filter(|(start, end)| start != end) - .map(|(start, end)| [start, end]) .collect(); let conditional: Vec = geometry .conditional_lines .iter() - .filter_map(|line| { - let points = line + .enumerate() + .filter_map(|(line_index, line)| { + let Some(points) = line .iter() .map(|index| geometry.points.get(*index as usize).copied()) - .collect::>>()?; + .collect::>>() + else { + tracing::warn!( + edge_kind = "conditional-line", + record_index = line_index, + indices = ?line, + "dropping malformed edge geometry" + ); + return None; + }; let endpoints = [ - finite_f32_vec3(points[0]).ok()?, - finite_f32_vec3(points[1]).ok()?, + match finite_f32_vec3(points[0]) { + Ok(point) => point, + Err(_) => { + tracing::warn!( + edge_kind = "conditional-line", + record_index = line_index, + indices = ?line, + "dropping malformed edge geometry" + ); + return None; + } + }, + match finite_f32_vec3(points[1]) { + Ok(point) => point, + Err(_) => { + tracing::warn!( + edge_kind = "conditional-line", + record_index = line_index, + indices = ?line, + "dropping malformed edge geometry" + ); + return None; + } + }, ]; let controls = [ - finite_f32_vec3(points[2]).ok()?, - finite_f32_vec3(points[3]).ok()?, + match finite_f32_vec3(points[2]) { + Ok(point) => point, + Err(_) => { + tracing::warn!( + edge_kind = "conditional-line", + record_index = line_index, + indices = ?line, + "dropping malformed edge geometry" + ); + return None; + } + }, + match finite_f32_vec3(points[3]) { + Ok(point) => point, + Err(_) => { + tracing::warn!( + edge_kind = "conditional-line", + record_index = line_index, + indices = ?line, + "dropping malformed edge geometry" + ); + return None; + } + }, ]; - (endpoints[0] != endpoints[1]).then_some((endpoints, controls)) + if endpoints[0] == endpoints[1] { + tracing::warn!( + edge_kind = "conditional-line", + record_index = line_index, + indices = ?line, + "dropping malformed edge geometry" + ); + return None; + } + Some((endpoints, controls)) }) .collect(); Ok(PlannedEdgeSource { diff --git a/crates/polymodel-renderer-worker/src/worker.rs b/crates/polymodel-renderer-worker/src/worker.rs index 4c4498d..7eae5a7 100644 --- a/crates/polymodel-renderer-worker/src/worker.rs +++ b/crates/polymodel-renderer-worker/src/worker.rs @@ -606,6 +606,15 @@ mod tests { )); } + #[wasm_bindgen_test] + fn preview_errors_use_cancelled_variant_not_display_text() { + let ordinary = RendererError::External("preview load cancelled".to_owned()); + assert!(!matches!(ordinary, RendererError::Cancelled)); + + let cancelled = RendererError::Cancelled; + assert!(matches!(cancelled, RendererError::Cancelled)); + } + #[wasm_bindgen_test] fn cancelled_compound_parse_maps_to_cancelled_load_error() { let bytes = b"0 FILE root.ldr\n3 16 0 0 0 1 0 0 0 1 0 0\n"; @@ -2425,14 +2434,29 @@ fn handle_render_preview_image( return; } st.preview_abort.take(); - let event = match result { - Ok(png) => RendererEvent::PreviewImageRendered { request_id, png }, - Err(message) => RendererEvent::PreviewImageError { + match result { + Ok(png) => st.post_event(&RendererEvent::PreviewImageRendered { request_id, png }), + Err(RendererError::Cancelled) => { + let transition = st.preview_reducer.reduce(LoadEvent::Cancel { + namespace: RendererNamespace::Preview, + identity, + candidate: None, + }); + if matches!( + transition.outcome, + polymodel_renderer_protocol::TransitionOutcome::Committed + ) { + st.post_event(&RendererEvent::LoadCancelled { + namespace: RendererNamespace::Preview, + identity, + }); + } + } + Err(error) => st.post_event(&RendererEvent::PreviewImageError { request_id, - message: message.to_string(), - }, - }; - st.post_event(&event); + message: error.to_string(), + }), + } }); } @@ -2453,7 +2477,7 @@ async fn render_preview_request( .as_ref() .is_some_and(|controller| controller.signal().aborted()) { - return Err(RendererError::External("preview load cancelled".to_owned())); + return Err(RendererError::Cancelled); } let model = load_mesh_async_with_abort(primary_url, format, fetch_bytes, abort.as_ref()).await?; @@ -2461,7 +2485,7 @@ async fn render_preview_request( .as_ref() .is_some_and(|controller| controller.signal().aborted()) { - return Err(RendererError::External("preview load cancelled".to_owned())); + return Err(RendererError::Cancelled); } render_preview_png(&model, width, height).await } diff --git a/tools/ldraw-compat-harness/Cargo.lock b/tools/ldraw-compat-harness/Cargo.lock index 3eb8077..b58e505 100644 --- a/tools/ldraw-compat-harness/Cargo.lock +++ b/tools/ldraw-compat-harness/Cargo.lock @@ -195,13 +195,23 @@ version = "0.1.0" dependencies = [ "base64", "miette", + "polymodel-ldraw-identity", "serde", - "sha2", "smol_str", "thiserror", "web-time", ] +[[package]] +name = "polymodel-ldraw-identity" +version = "0.1.0" +dependencies = [ + "serde", + "sha2", + "smol_str", + "thiserror", +] + [[package]] name = "polymodel-ldraw-testkit" version = "0.1.0" diff --git a/tools/ldraw-compat-harness/src/main.rs b/tools/ldraw-compat-harness/src/main.rs index 7a555dc..e947e54 100644 --- a/tools/ldraw-compat-harness/src/main.rs +++ b/tools/ldraw-compat-harness/src/main.rs @@ -823,6 +823,39 @@ fn candidate_observation( })) } +fn build_semantic_from_geometry( + geometry: &[polymodel_ldraw_core::GeometryRecord], +) -> polymodel_ldraw_core::LdMeshSemantic { + use polymodel_ldraw_core::LineType; + let mut positions = Vec::new(); + let mut indices = Vec::new(); + let mut bf_indices = Vec::new(); + for record in geometry { + match record.line_type { + LineType::Three if record.vertices.len() == 3 => { + let base = u32::try_from(positions.len()).unwrap_or(u32::MAX); + positions.extend(record.vertices.iter().map(|p| [p.x, p.y, p.z])); + indices.extend([base, base + 1, base + 2]); + bf_indices.extend([base + 2, base + 1, base]); + } + LineType::Four if record.vertices.len() == 4 => { + let base = u32::try_from(positions.len()).unwrap_or(u32::MAX); + positions.extend(record.vertices.iter().map(|p| [p.x, p.y, p.z])); + indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]); + bf_indices.extend([base + 2, base + 1, base, base + 3, base + 2, base]); + } + _ => {} + } + } + polymodel_ldraw_core::LdMeshSemantic { + positions, + normals: None, + indices, + bf_indices, + step_offsets: Vec::new(), + } +} + fn compare_candidate_to_oracle( fixture: &polymodel_ldraw_testkit::FixtureCase, oracle: &OracleProjection, @@ -981,11 +1014,22 @@ fn compare_candidate_to_oracle( )); } if oracle_model.mesh.vertex_count > 0 { + let semantic = parsed + .geometry + .get(&candidate_model.cache_key) + .map(|geometry| build_semantic_from_geometry(geometry)) + .unwrap_or_else(|| polymodel_ldraw_core::LdMeshSemantic { + positions: Vec::new(), + normals: None, + indices: Vec::new(), + bf_indices: Vec::new(), + step_offsets: Vec::new(), + }); polymodel_ldraw_testkit::compare_mesh_semantic( &oracle_model.name, &oracle_model.mesh, &oracle_model.step_ends, - &candidate_model.semantic, + &semantic, ) .map_err(|error| { format!( -- knotmirror 0.0.1