diff --git a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs index 70f4549..a77ec76 100644 --- a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs +++ b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs @@ -40,7 +40,7 @@ use winit::{ #[cfg(not(target_arch = "wasm32"))] enum PreviewEvent { - Prepared(Result), + Prepared(Result), } #[cfg(not(target_arch = "wasm32"))] @@ -257,11 +257,17 @@ fn camera_pose_for_viewport( } #[cfg(not(target_arch = "wasm32"))] -fn terminal_result(snapshot: polymodel_renderer_worker::NativeTraceSnapshot) -> Result<(), String> { +fn terminal_result( + snapshot: polymodel_renderer_worker::NativeTraceSnapshot, +) -> Result<(), RendererError> { match snapshot.terminal { Some(NativeTerminal::Success) => Ok(()), - Some(NativeTerminal::Failure(error)) | Some(NativeTerminal::Timeout(error)) => Err(error), - None => Err("native preview exited without terminal state".into()), + Some(NativeTerminal::Failure(error)) | Some(NativeTerminal::Timeout(error)) => { + Err(RendererError::External(error)) + } + None => Err(RendererError::External( + "native preview exited without terminal state".to_owned(), + )), } } @@ -271,7 +277,7 @@ fn run_visible_preview( library_root: PathBuf, options: VisiblePreviewOptions, trace: NativeTrace, -) -> Result<(), String> { +) -> Result<(), RendererError> { let VisiblePreviewOptions { width, height, @@ -302,8 +308,12 @@ fn run_visible_preview( Some((width.parse::().ok()?, height.parse::().ok()?)) }) .filter(|(width, height)| *width > 0 && *height > 0) - .ok_or_else(|| format!("invalid POLYMODEL_LDRAW_PREVIEW_TEST_WINDOW_SIZE={size}")) - .inspect_err(|error| trace.finish(NativeTerminal::Failure(error.clone())))?; + .ok_or_else(|| { + RendererError::External(format!( + "invalid POLYMODEL_LDRAW_PREVIEW_TEST_WINDOW_SIZE={size}" + )) + }) + .inspect_err(|error| trace.finish(NativeTerminal::Failure(error.to_string())))?; window.set_inner_size(winit::dpi::LogicalSize::new(test_width, test_height)); } trace.record(NativeStage::WindowCreated); @@ -322,10 +332,10 @@ fn run_visible_preview( let context = match WindowedContext::from_winit_window(&window, SurfaceSettings::default()) { Ok(context) => context, Err(error) => { - let message = format!("native window context: {error}"); - trace.finish(NativeTerminal::Failure(message.clone())); - eprintln!("[ldraw-preview] preparation-failed error={message}"); - return Err(message); + let error = RendererError::External(format!("native window context: {error}")); + trace.finish(NativeTerminal::Failure(error.to_string())); + eprintln!("[ldraw-preview] preparation-failed error={error}"); + return Err(error); } }; let mut frame_generator = three_d::FrameInputGenerator::from_winit_window(&window); @@ -333,8 +343,7 @@ fn run_visible_preview( let worker_trace = trace.clone(); thread::spawn(move || { worker_trace.record(NativeStage::PreparationStarted); - let prepared = - prepare_preview_scene(&input, &library_root).map_err(|error| error.to_string()); + let prepared = prepare_preview_scene(&input, &library_root); worker_trace.record(if prepared.is_ok() { NativeStage::PreparationComplete } else { @@ -370,7 +379,7 @@ fn run_visible_preview( let mut last_viewport = None; if let Some(path) = readback_prefix.as_ref() { fs::create_dir_all(path.parent().unwrap_or_else(|| Path::new("."))) - .map_err(|error| format!("create readback directory: {error}"))?; + .map_err(|error| RendererError::Io(format!("create readback directory: {error}")))?; } let proof_deadline = proof_ms.map(|milliseconds| { std::time::Instant::now() + std::time::Duration::from_millis(milliseconds as u64) @@ -463,14 +472,14 @@ fn run_visible_preview( loop_trace.record(NativeStage::FirstRedrawRequested); } Err(error) => { - loop_trace.finish(NativeTerminal::Failure(error)); + loop_trace.finish(NativeTerminal::Failure(error.to_string())); *control_flow = ControlFlow::Exit; std::process::exit(1); } } } Event::UserEvent(PreviewEvent::Prepared(Err(error))) => { - loop_trace.finish(NativeTerminal::Failure(error.clone())); + loop_trace.finish(NativeTerminal::Failure(error.to_string())); eprintln!("[ldraw-preview] preparation-failed error={error}"); *control_flow = ControlFlow::Exit; std::process::exit(1); @@ -806,21 +815,25 @@ fn run_visible_preview( if let Some(label) = label { let pixels = screen.read_color::<[u8; 4]>(); let path = persistent_readback_path(prefix, label, frame.viewport); - let readback = (|| -> Result<(), String> { + let readback = (|| -> Result<(), RendererError> { if env::var_os("POLYMODEL_LDRAW_PREVIEW_TEST_FAIL_READBACK").is_some() { - return Err("injected persistent readback failure".into()); + return Err(RendererError::External( + "injected persistent readback failure".to_owned(), + )); } let png = try_native_png_bytes( frame.viewport.width, frame.viewport.height, &pixels.into_iter().flatten().collect::>(), - )?; - fs::write(&path, png) - .map_err(|error| format!("write persistent readback: {error}"))?; + ) + .map_err(RendererError::External)?; + fs::write(&path, png).map_err(|error| { + RendererError::Io(format!("write persistent readback: {error}")) + })?; Ok(()) })(); if let Err(error) = readback { - loop_trace.finish(NativeTerminal::Failure(error)); + loop_trace.finish(NativeTerminal::Failure(error.to_string())); *control_flow = ControlFlow::Exit; return; } @@ -1120,23 +1133,27 @@ struct ParsedOptions { } #[cfg(not(target_arch = "wasm32"))] -fn parse_options(args: &[String]) -> Result { +fn parse_options(args: &[String]) -> Result { let input = positional_input(args) .unwrap_or_else(|| PathBuf::from("crates/polymodel-ldraw-testkit/corpus/omr-10030-1.mpd")); - let optional_u32 = |name: &str| -> Result, String> { + let optional_u32 = |name: &str| -> Result, RendererError> { option_path(args, name) .map(|path| { path.to_str() - .ok_or_else(|| format!("{name} must be valid UTF-8"))? + .ok_or_else(|| RendererError::External(format!("{name} must be valid UTF-8")))? .parse::() - .map_err(|_| format!("{name} must be a non-negative integer")) + .map_err(|_| { + RendererError::External(format!("{name} must be a non-negative integer")) + }) }) .transpose() }; let width = optional_u32("--width")?.unwrap_or(1280); let height = optional_u32("--height")?.unwrap_or(720); if width == 0 || height == 0 { - return Err("--width and --height must be greater than zero".into()); + return Err(RendererError::External( + "--width and --height must be greater than zero".to_owned(), + )); } let proof_ms = optional_u32("--proof-ms")?; let yaw_path = option_path(args, "--yaw-degrees"); @@ -1144,46 +1161,57 @@ fn parse_options(args: &[String]) -> Result { .as_ref() .map(|path| { path.to_str() - .ok_or_else(|| "--yaw-degrees must be valid UTF-8".to_owned())? + .ok_or_else(|| { + RendererError::External("--yaw-degrees must be valid UTF-8".to_owned()) + })? .parse::() - .map_err(|_| "--yaw-degrees must be a finite number".to_owned()) + .map_err(|_| { + RendererError::External("--yaw-degrees must be a finite number".to_owned()) + }) }) .transpose()? .unwrap_or(0.0); if !yaw_degrees.is_finite() { - return Err("--yaw-degrees must be finite".into()); + return Err(RendererError::External( + "--yaw-degrees must be finite".to_owned(), + )); } let case_path = option_path(args, "--camera-case"); if case_path.is_some() && yaw_path.is_some() { - return Err("--camera-case and --yaw-degrees are mutually exclusive".into()); + return Err(RendererError::External( + "--camera-case and --yaw-degrees are mutually exclusive".to_owned(), + )); } let camera_case = case_path .as_ref() .map(|path| { - let case_id = path - .to_str() - .ok_or_else(|| "--camera-case must be valid UTF-8".to_owned())?; - named_camera_case(case_id).map_err(|error| format!("invalid --camera-case: {error}")) + let case_id = path.to_str().ok_or_else(|| { + RendererError::External("--camera-case must be valid UTF-8".to_owned()) + })?; + named_camera_case(case_id) + .map_err(|error| RendererError::External(format!("invalid --camera-case: {error}"))) }) .transpose()?; let variant_path = option_path(args, "--scene-variant"); if variant_path.is_some() && camera_case.is_none() { - return Err("--scene-variant requires --camera-case".into()); + return Err(RendererError::External( + "--scene-variant requires --camera-case".to_owned(), + )); } let variant = variant_path .as_ref() .map(|path| { - let variant_id = path - .to_str() - .ok_or_else(|| "--scene-variant must be valid UTF-8".to_owned())?; + let variant_id = path.to_str().ok_or_else(|| { + RendererError::External("--scene-variant must be valid UTF-8".to_owned()) + })?; let mut binding = SceneVariantBinding::for_case( camera_case .as_ref() .map_or("identity_front", |case| &case.case_id), ) - .map_err(|error| format!("invalid scene variant: {error}"))?; + .map_err(|error| RendererError::External(format!("invalid scene variant: {error}")))?; binding.variant_id = variant_id.to_owned(); - Ok::<_, String>(binding) + Ok(binding) }) .transpose()? .or_else(|| { @@ -1204,7 +1232,7 @@ fn parse_options(args: &[String]) -> Result { ]; for arg in args.iter().filter(|arg| arg.starts_with('-')) { if !known.contains(&arg.as_str()) { - return Err(format!("unknown option {arg}")); + return Err(RendererError::External(format!("unknown option {arg}"))); } } for name in [ @@ -1218,12 +1246,16 @@ fn parse_options(args: &[String]) -> Result { "--scene-variant", ] { if args.iter().any(|arg| arg == name) && option_path(args, name).is_none() { - return Err(format!("{name} requires a value")); + return Err(RendererError::External(format!("{name} requires a value"))); } } let library_root = option_path(args, "--library-root") .or_else(|| env::var_os("LDRAWDIR").map(PathBuf::from)) - .ok_or_else(|| "--library-root or LDRAWDIR is required for official parts".to_owned())?; + .ok_or_else(|| { + RendererError::External( + "--library-root or LDRAWDIR is required for official parts".to_owned(), + ) + })?; Ok(ParsedOptions { input, width, diff --git a/crates/polymodel-renderer-worker/src/lib.rs b/crates/polymodel-renderer-worker/src/lib.rs index 9d90d67..e620c96 100644 --- a/crates/polymodel-renderer-worker/src/lib.rs +++ b/crates/polymodel-renderer-worker/src/lib.rs @@ -1846,9 +1846,9 @@ fn official_texture_resources( target_cids: Vec::new(), bytes: materialization.bytes.clone(), }; - resource - .validate() - .map_err(|error| format!("official TEXMAP resource rejected: {error:?}"))?; + resource.validate().map_err(|error| { + RendererError::External(format!("official TEXMAP resource rejected: {error:?}")) + })?; if !resources.iter().any(|existing: &ModelResource| { existing.root == resource.root && existing.path == resource.path }) { @@ -1867,14 +1867,19 @@ pub fn prepare_preview_scene( AdapterRequest, CancellationPolicy, InProcessRustAdapter, Materialization, NormalizedPath, ParserProfile, RootId, }; - let bytes = - std::fs::read(input).map_err(|error| format!("read {}: {error}", input.display()))?; + let bytes = std::fs::read(input) + .map_err(|error| RendererError::Io(format!("read {}: {error}", input.display())))?; let logical_name = input .file_name() .and_then(std::ffi::OsStr::to_str) - .ok_or_else(|| format!("preview input has no UTF-8 file name: {}", input.display()))?; + .ok_or_else(|| { + RendererError::External(format!( + "preview input has no UTF-8 file name: {}", + input.display() + )) + })?; let path = NormalizedPath::new(logical_name) - .map_err(|error| format!("preview input path: {error}"))?; + .map_err(|error| RendererError::External(format!("preview input path: {error}")))?; let started = Instant::now(); let closure = official_materializations(library_root, &path, &bytes)?; tracing::debug!( @@ -1918,16 +1923,15 @@ pub fn prepare_preview_scene( }, preview_limits(), ) - .map_err(|error| format!("pinned OMR parse: {error}"))?; - let scene = adapt_ldraw_parse_with_resources(&result, Some(&resources)) - .map_err(|error| error.to_string())?; + .map_err(|error| RendererError::ParseMessage(format!("pinned OMR parse: {error}")))?; + let scene = adapt_ldraw_parse_with_resources(&result, Some(&resources))?; if scene.stats.triangles == 0 || scene.stats.occurrences == 0 || scene.bounds.is_none() { - return Err(format!( + return Err(RendererError::ParseMessage(format!( "preview root scene incomplete: occurrences={} triangles={} bounds={}", scene.stats.occurrences, scene.stats.triangles, scene.bounds.is_some() - )); + ))); } Ok(scene) } @@ -1971,14 +1975,14 @@ pub fn official_materializations( const MAX_BYTES: usize = 256 * 1024 * 1024; const MAX_DEPTH: usize = 128; - let canonical_root = library_root - .canonicalize() - .map_err(|error| format!("canonicalize official LDraw library: {error}"))?; + let canonical_root = library_root.canonicalize().map_err(|error| { + RendererError::Io(format!("canonicalize official LDraw library: {error}")) + })?; if !canonical_root.is_dir() { - return Err(format!( + return Err(RendererError::External(format!( "official LDraw library is not a directory: {}", library_root.display() - )); + ))); } #[derive(Clone)] struct Pending { @@ -2011,14 +2015,18 @@ pub fn official_materializations( while let Some(current) = queue.pop_front() { if current.depth > MAX_DEPTH { - return Err(format!("official closure depth exceeded {MAX_DEPTH}")); + return Err(RendererError::External(format!( + "official closure depth exceeded {MAX_DEPTH}" + ))); } let inspection = polymodel_ldraw_core::inspect_compound_source( ¤t.bytes, ¤t.path, current.key.resolved_root, ) - .map_err(|error| format!("inspect {}: {error}", current.path.as_str()))?; + .map_err(|error| { + RendererError::ParseMessage(format!("inspect {}: {error}", current.path.as_str())) + })?; let source_keys = inspection.source_keys.clone(); let has_virtual_files = inspection.has_virtual_files; let inspection_closure = polymodel_ldraw_core::ResourceClosure::from_inspection( @@ -2039,7 +2047,9 @@ pub fn official_materializations( continue; } let resolved = polymodel_ldraw_core::resolve_reference_path(¤t.path, &reference) - .map_err(|error| format!("resolve {}: {error}", reference.target))?; + .map_err(|error| { + RendererError::ParseMessage(format!("resolve {}: {error}", reference.target)) + })?; let unqualified_official = if !reference.target.contains('/') && matches!(resolved.root, polymodel_ldraw_core::ReferenceRoot::Models) { @@ -2083,7 +2093,12 @@ pub fn official_materializations( .iter() .find(|(name, _)| name == &reference.file_name.to_ascii_lowercase()) .map(|(_, key)| key.clone()) - .ok_or_else(|| format!("source section missing: {}", reference.file_name))? + .ok_or_else(|| { + RendererError::External(format!( + "source section missing: {}", + reference.file_name + )) + })? } else { current.key.clone() }; @@ -2091,7 +2106,12 @@ pub fn official_materializations( .as_str() .strip_prefix("parts/") .or_else(|| path.as_str().strip_prefix("p/")) - .ok_or_else(|| format!("official path outside library roots: {}", path.as_str()))?; + .ok_or_else(|| { + RendererError::External(format!( + "official path outside library roots: {}", + path.as_str() + )) + })?; let fallback_root = if selected_root == "parts" { "p" } else { @@ -2150,10 +2170,10 @@ pub fn official_materializations( let candidate = library_root.join(prefix).join(&relative_candidate); if let Ok(path) = candidate.canonicalize() { if !path.starts_with(&canonical_root) { - return Err(format!( + return Err(RendererError::External(format!( "official path escapes library: {}", candidate.display() - )); + ))); } canonical_path = Some((prefix, path, relative_candidate)); break; @@ -2161,15 +2181,18 @@ pub fn official_materializations( } let (actual_root, canonical_path, relative_candidate) = canonical_path.ok_or_else(|| { - format!( + RendererError::External(format!( "official part is missing from parts/ and p/: {}", path.as_str() - ) + )) })?; let logical_path = NormalizedPath::new(&format!("{actual_root}/{relative_candidate}")) - .map_err(|error| format!("normalize official part path: {error}"))?; - let child_bytes = std::fs::read(&canonical_path) - .map_err(|error| format!("read {}: {error}", canonical_path.display()))?; + .map_err(|error| { + RendererError::External(format!("normalize official part path: {error}")) + })?; + let child_bytes = std::fs::read(&canonical_path).map_err(|error| { + RendererError::Io(format!("read {}: {error}", canonical_path.display())) + })?; let target = CacheKey::new(logical_path.clone(), RootId::OfficialLibrary, &child_bytes); target_selections.push(polymodel_ldraw_core::TargetSelection { source, @@ -2178,13 +2201,20 @@ pub fn official_materializations( }); if seen.insert(target.clone()) { if materializations.len() >= MAX_FILES { - return Err(format!("official closure file limit exceeded {MAX_FILES}")); + return Err(RendererError::External(format!( + "official closure file limit exceeded {MAX_FILES}" + ))); } - total_bytes = total_bytes - .checked_add(child_bytes.len()) - .ok_or_else(|| "official closure byte count overflow".to_owned())?; + total_bytes = + total_bytes + .checked_add(child_bytes.len()) + .ok_or(RendererError::External( + "official closure byte count overflow".to_owned(), + ))?; if total_bytes > MAX_BYTES { - return Err(format!("official closure byte limit exceeded {MAX_BYTES}")); + return Err(RendererError::External(format!( + "official closure byte limit exceeded {MAX_BYTES}" + ))); } materializations.push(Materialization::new( logical_path.clone(), @@ -2201,7 +2231,9 @@ pub fn official_materializations( }); } if target_selections.len() > MAX_EDGES { - return Err(format!("official closure edge limit exceeded {MAX_EDGES}")); + return Err(RendererError::External(format!( + "official closure edge limit exceeded {MAX_EDGES}" + ))); } } } @@ -2213,10 +2245,10 @@ pub fn official_materializations( let candidate = library_root.join(dependency.path.as_str()); if !candidate.is_file() { if dependency.required { - return Err(format!( + return Err(RendererError::External(format!( "required TEXMAP resource is missing: {} (source {})", dependency.path, dependency.source - )); + ))); } continue; } @@ -2225,8 +2257,12 @@ pub fn official_materializations( }) { continue; } - let bytes = std::fs::read(&candidate) - .map_err(|error| format!("read TEXMAP resource {}: {error}", candidate.display()))?; + let bytes = std::fs::read(&candidate).map_err(|error| { + RendererError::Io(format!( + "read TEXMAP resource {}: {error}", + candidate.display() + )) + })?; materializations.push(Materialization::new( dependency.path.clone(), dependency.root, @@ -2276,17 +2312,21 @@ pub fn native_png_bytes(width: u32, height: u32, pixels: &[u8]) -> Vec { } #[cfg(not(target_arch = "wasm32"))] -pub fn try_native_png_bytes(width: u32, height: u32, pixels: &[u8]) -> Result, String> { +pub fn try_native_png_bytes( + width: u32, + height: u32, + pixels: &[u8], +) -> Result, RendererError> { let mut output = Vec::new(); let mut encoder = png::Encoder::new(std::io::Cursor::new(&mut output), width, height); encoder.set_color(png::ColorType::Rgba); encoder.set_depth(png::BitDepth::Eight); let mut writer = encoder .write_header() - .map_err(|error| format!("encode PNG header: {error}"))?; + .map_err(|error| RendererError::External(format!("encode PNG header: {error}")))?; writer .write_image_data(pixels) - .map_err(|error| format!("encode PNG pixels: {error}"))?; + .map_err(|error| RendererError::External(format!("encode PNG pixels: {error}")))?; drop(writer); Ok(output) } @@ -2324,7 +2364,10 @@ impl Default for NativeRenderOptions { } #[cfg(not(target_arch = "wasm32"))] -pub fn render_scene_window(scene: &Scene, output: Option<&std::path::Path>) -> Result<(), String> { +pub fn render_scene_window( + scene: &Scene, + output: Option<&std::path::Path>, +) -> Result<(), RendererError> { render_scene_window_configured(scene, output, NativeRenderOptions::default()) } @@ -3354,7 +3397,7 @@ pub fn build_scene_occurrence_models( }) }) .collect::>()?; - build_scene_model(context, &selected).map_err(|error| RendererError::GpuResource(error)) + build_scene_model(context, &selected) } #[cfg(not(target_arch = "wasm32"))] @@ -3535,7 +3578,7 @@ pub fn scene_render_plan(scene: &Scene) -> Result Result, String> { +pub fn scene_instance_batches(scene: &Scene) -> Result, RendererError> { Ok(scene_render_plan(scene)?.batches) } @@ -3845,7 +3888,6 @@ pub fn build_scene_model_for_view_with_resources( &plan.topologies, resources, ) - .map_err(|error| error.to_string()) } pub type EdgeModel = Gm; @@ -3855,12 +3897,9 @@ pub type EdgeModelParts = (Option>, Option>); pub fn update_edge_model_culling_bounds( models: &mut [EdgeModel], viewer: &dyn Viewer, -) -> Result<(), String> { +) -> Result<(), RendererError> { for model in models { - model - .geometry - .update_culling_aabb(viewer) - .map_err(|error| error.to_string())?; + model.geometry.update_culling_aabb(viewer)?; } Ok(()) } @@ -4596,8 +4635,7 @@ pub fn build_scene_model_parts_for_style_with_resources( &plan.upload_batches, &plan.topologies, resources, - ) - .map_err(|error| error.to_string())?; + )?; let static_elapsed = static_started.elapsed(); tracing::debug!( "[ldraw-preview] native-build-stage=static elapsed_ms={} cumulative_ms={}", @@ -4606,8 +4644,7 @@ pub fn build_scene_model_parts_for_style_with_resources( ); let (explicit_model, conditional_model) = if style == RenderStyle::Edges { let edge_started = Instant::now(); - let models = build_edge_models_for_view(context, scene, &plan, view) - .map_err(|error| error.to_string())?; + let models = build_edge_models_for_view(context, scene, &plan, view)?; tracing::debug!( "[ldraw-preview] native-build-stage=edge-output elapsed_ms={} cumulative_ms={}", edge_started.elapsed().as_millis(), @@ -4617,7 +4654,7 @@ pub fn build_scene_model_parts_for_style_with_resources( } else { (None, None) }; - let picker = ScenePicker::new(scene, plan.clone()).map_err(|error| error.to_string())?; + let picker = ScenePicker::new(scene, plan.clone())?; tracing::debug!( "[ldraw-preview] native-build-stage=picker elapsed_ms={}", started.elapsed().as_millis() @@ -4633,9 +4670,9 @@ pub fn build_scene_model_parts_for_style_with_resources( pub fn build_scene_faces_and_picker( context: &three_d::Context, scene: &Scene, -) -> Result<(NativeSceneModel, ScenePicker), String> { +) -> Result<(NativeSceneModel, ScenePicker), RendererError> { let started = Instant::now(); - let plan = scene_render_plan(scene).map_err(|error| error.to_string())?; + let plan = scene_render_plan(scene)?; tracing::debug!( "[ldraw-preview] native-build-stage=plan elapsed_ms={}", started.elapsed().as_millis() @@ -4647,13 +4684,12 @@ pub fn build_scene_faces_and_picker( &plan.upload_batches, &plan.topologies, None, - ) - .map_err(|error| error.to_string())?; + )?; tracing::debug!( "[ldraw-preview] native-build-stage=static elapsed_ms={}", started.elapsed().as_millis() ); - let picker = ScenePicker::new(scene, plan).map_err(|error| error.to_string())?; + let picker = ScenePicker::new(scene, plan)?; tracing::debug!( "[ldraw-preview] native-build-stage=picker elapsed_ms={}", started.elapsed().as_millis() @@ -5265,19 +5301,18 @@ pub fn render_scene_window_options_traced( output: Option<&std::path::Path>, trace: NativeTrace, options: NativeRenderOptions, -) -> Result<(), String> { +) -> Result<(), RendererError> { use std::panic::{AssertUnwindSafe, catch_unwind}; use three_d::{ClearState, FrameOutput, Window, WindowSettings}; trace.record(NativeStage::Started); let bounds = scene.bounds.ok_or_else(|| { - let error = "scene has no bounds".to_owned(); - trace.finish(NativeTerminal::Failure(error.clone())); - error + trace.finish(NativeTerminal::Failure("scene has no bounds".to_owned())); + RendererError::ParseAdaptation("scene has no bounds") })?; if scene.stats.triangles == 0 { - let error = "scene has no triangles".to_owned(); - trace.finish(NativeTerminal::Failure(error.clone())); + let error = RendererError::ParseAdaptation("scene has no triangles"); + trace.finish(NativeTerminal::Failure(error.to_string())); return Err(error); } trace.record(NativeStage::LibraryResolved); @@ -5295,8 +5330,8 @@ pub fn render_scene_window_options_traced( ..Default::default() }) .map_err(|error| { - let error = format!("native window: {error}"); - trace.finish(NativeTerminal::Failure(error.clone())); + let error = RendererError::External(format!("native window: {error}")); + trace.finish(NativeTerminal::Failure(error.to_string())); error })?; trace.record(NativeStage::WindowCreated); @@ -5309,12 +5344,12 @@ pub fn render_scene_window_options_traced( })) { Ok(Ok(parts)) => parts, Ok(Err(error)) => { - trace.finish(NativeTerminal::Failure(error.clone())); + trace.finish(NativeTerminal::Failure(error.to_string())); return Err(error); } Err(_) => { - let error = "GPU resource creation panicked".to_owned(); - trace.finish(NativeTerminal::Failure(error.clone())); + let error = RendererError::GpuResource("GPU resource creation panicked".to_owned()); + trace.finish(NativeTerminal::Failure(error.to_string())); return Err(error); } }; @@ -5322,17 +5357,19 @@ pub fn render_scene_window_options_traced( let lights = NativeLights::new(&context); let viewport = window.viewport(); if viewport.width != options.width || viewport.height != options.height { - let error = format!( + let error = RendererError::External(format!( "native viewport mismatch: requested {}x{}, actual {}x{}", options.width, options.height, viewport.width, viewport.height - ); - trace.finish(NativeTerminal::Failure(error.clone())); + )); + trace.finish(NativeTerminal::Failure(error.to_string())); return Err(error); } let fit = scene_camera_fit(bounds, options.width, options.height) - .map_err(|error| error.to_string())?; + .map_err(|error| RendererError::External(error.to_string()))?; let (position, target, up, fov_degrees) = if let Some(case) = options.camera_case.as_ref() { - let pose = case.native_pose(fit).map_err(|error| error.to_string())?; + let pose = case + .native_pose(fit) + .map_err(|error| RendererError::External(error.to_string()))?; (pose.position, pose.target, pose.up, pose.fov_degrees) } else { ( @@ -5381,8 +5418,8 @@ pub fn render_scene_window_options_traced( viewport.width, viewport.height, ); - let projection = - camera_space_clip_planes(bounds, position, target).map_err(|error| error.to_string())?; + let projection = camera_space_clip_planes(bounds, position, target) + .map_err(|error| RendererError::External(error.to_string()))?; let mut camera = Camera::new_perspective( viewport, position, @@ -5407,14 +5444,18 @@ pub fn render_scene_window_options_traced( camera.set_viewport(viewport); control.handle_events(&mut camera, &mut frame_input.events); let projection = camera_space_clip_planes(bounds, camera.position(), camera.target()) - .map_err(|error| error.to_string())?; + .map_err(|error| RendererError::External(error.to_string()))?; let fov = { let projection_type = camera.projection_type(); match projection_type { three_d_asset::ProjectionType::Perspective { field_of_view_y } => { *field_of_view_y } - _ => return Err("camera projection is not perspective".into()), + _ => { + return Err(RendererError::External( + "camera projection is not perspective".to_owned(), + )); + } } }; camera.set_perspective_projection(fov, projection.near, projection.far); @@ -5440,7 +5481,9 @@ pub fn render_scene_window_options_traced( std::fs::create_dir_all( path.parent().unwrap_or_else(|| std::path::Path::new(".")), ) - .map_err(|error| format!("create preview output directory: {error}"))?; + .map_err(|error| { + RendererError::Io(format!("create preview output directory: {error}")) + })?; std::fs::write( path, native_png_bytes( @@ -5449,7 +5492,7 @@ pub fn render_scene_window_options_traced( &pixels.into_iter().flatten().collect::>(), ), ) - .map_err(|error| format!("write preview output: {error}"))?; + .map_err(|error| RendererError::Io(format!("write preview output: {error}")))?; callback_trace.record(NativeStage::OutputWritten); callback_trace.finish(NativeTerminal::Success); return Ok(FrameOutput { @@ -5463,7 +5506,7 @@ pub fn render_scene_window_options_traced( match result { Ok(Ok(output)) => output, Ok(Err(error)) => { - callback_trace.finish(NativeTerminal::Failure(error)); + callback_trace.finish(NativeTerminal::Failure(error.to_string())); FrameOutput { exit: true, ..Default::default() @@ -5482,8 +5525,12 @@ pub fn render_scene_window_options_traced( match snapshot.terminal { Some(NativeTerminal::Success) | None if output.is_none() => Ok(()), Some(NativeTerminal::Success) => Ok(()), - Some(NativeTerminal::Failure(error)) | Some(NativeTerminal::Timeout(error)) => Err(error), - None => Err("native render exited without terminal state".into()), + Some(NativeTerminal::Failure(error)) | Some(NativeTerminal::Timeout(error)) => { + Err(RendererError::External(error)) + } + None => Err(RendererError::External( + "native render exited without terminal state".to_owned(), + )), } } diff --git a/crates/polymodel-renderer-worker/src/worker.rs b/crates/polymodel-renderer-worker/src/worker.rs index d6e3e2f..bc34cfa 100644 --- a/crates/polymodel-renderer-worker/src/worker.rs +++ b/crates/polymodel-renderer-worker/src/worker.rs @@ -28,7 +28,7 @@ use polymodel_renderer_protocol::{ NegotiatedProtocol, ProtocolHello, RendererCommand, RendererEvent, RendererNamespace, ResourceToken, deserialize_command_envelope, serialize_event_envelope, }; -use polymodel_renderer_worker::adapt_ldraw_parse_with_resources; +use polymodel_renderer_worker::{RendererError, adapt_ldraw_parse_with_resources}; use three_d_asset::InnerSpace as _; use polymodel_renderer_worker::admission::model_with_default_materials; @@ -123,33 +123,35 @@ struct VerifiedCompoundSource { target_selections: Vec, } -fn descriptor_root(root: &str) -> Result { +fn descriptor_root(root: &str) -> Result { match root { "mpd" => Ok(RootId::CurrentMpd), "manifest" => Ok(RootId::UploadedManifest), "models" | "parts" | "p" | "external" => Ok(RootId::UploadedLdraw), "official-parts" | "official-p" => Ok(RootId::OfficialLibrary), - _ => Err(format!("unsupported compound descriptor root: {root}")), + _ => Err(RendererError::ParseMessage(format!( + "unsupported compound descriptor root: {root}" + ))), } } fn validate_bundle_resources( primary: &str, resources: &[polymodel_renderer_protocol::ModelResource], -) -> Result<(), String> { +) -> Result<(), CompoundLoadError> { let mut normalized = HashSet::with_capacity(resources.len()); let mut primary_found = false; for resource in resources { resource .validate() - .map_err(|error| format!("model resource rejected: {error:?}"))?; - let path = NormalizedPath::new(&resource.path).map_err(|error| error.to_string())?; - let root = descriptor_root(&resource.root)?; + .map_err(|_| CompoundLoadError::Bundle("model resource rejected"))?; + let path = NormalizedPath::new(&resource.path) + .map_err(|_| CompoundLoadError::Bundle("model resource path rejected"))?; + let root = descriptor_root(&resource.root).map_err(CompoundLoadError::Adapt)?; let identity = format!("{root:?}:{}", path.as_str()); if !normalized.insert(identity) { - return Err(format!( - "duplicate normalized resource identity: {}", - resource.path + return Err(CompoundLoadError::Bundle( + "duplicate normalized resource identity", )); } if resource.path == primary { @@ -157,7 +159,9 @@ fn validate_bundle_resources( } } if !primary_found { - return Err("model bundle primary resource missing".to_owned()); + return Err(CompoundLoadError::Bundle( + "model bundle primary resource missing", + )); } Ok(()) } @@ -165,13 +169,15 @@ fn validate_bundle_resources( fn compound_materializations( resources: &[polymodel_renderer_protocol::ModelResource], primary: &str, -) -> Result, String> { +) -> Result, RendererError> { let mut materializations = Vec::with_capacity(resources.len().saturating_sub(1)); for resource in resources { if resource.path == primary { continue; } - let path = NormalizedPath::new(&resource.path).map_err(|error| error.to_string())?; + let path = NormalizedPath::new(&resource.path).map_err(|error| { + RendererError::ParseMessage(format!("invalid compound resource path: {error}")) + })?; materializations.push(Materialization::new( path, descriptor_root(&resource.root)?, @@ -198,7 +204,9 @@ fn parse_compound_source( .resources .iter() .find(|resource| resource.path == source.primary) - .ok_or_else(|| "model bundle primary resource missing".to_string())?; + .ok_or(CompoundLoadError::Bundle( + "model bundle primary resource missing", + ))?; let primary_root = source .source_identities .first() @@ -223,12 +231,10 @@ fn parse_compound_source( }) .map_err(|error| match error { polymodel_ldraw_core::ParseError::Cancelled => CompoundLoadError::Cancelled, - // This function is the boundary from the parser's typed errors to the - // worker's load error; preserve cancellation, stringify other details. - error => CompoundLoadError::Failed(error.to_string()), + error => CompoundLoadError::Parse(error), })?; let scene = adapt_ldraw_parse_with_resources(&result, Some(&source.resources)) - .map_err(|error| CompoundLoadError::Failed(error.to_string()))?; + .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, @@ -524,7 +530,7 @@ fn build_scene( three_d::Camera, three_d::OrbitControl, ), - String, + RendererError, > { let fit = model.camera_fit(); let cpu_model = model_with_default_materials(model); @@ -539,9 +545,11 @@ fn build_scene( "renderer worker: building scene" ); let render_model = three_d::Model::::new(context, &cpu_model) - .map_err(|e| format!("renderer model: {e}"))?; + .map_err(|error| RendererError::GpuResource(format!("renderer model: {error}")))?; if render_model.iter().next().is_none() { - return Err("model contains no renderable triangle geometry".into()); + return Err(RendererError::ParseAdaptation( + "model contains no renderable triangle geometry", + )); } let radius = fit.radius.max(0.001); let target = three_d::vec3(fit.center[0], fit.center[1], fit.center[2]); @@ -573,7 +581,7 @@ mod tests { #[wasm_bindgen_test] fn compound_errors_use_variants_not_display_text() { - let ordinary = CompoundLoadError::Failed("ordinary cancelled-looking failure".into()); + let ordinary = CompoundLoadError::Bundle("ordinary cancelled-looking failure"); assert!(!matches!( ordinary, CompoundLoadError::Cancelled @@ -989,25 +997,27 @@ mod tests { } } -fn create_preview_target(width: u32, height: u32) -> Result { +fn create_preview_target(width: u32, height: u32) -> Result { let canvas = web_sys::OffscreenCanvas::new(width, height) - .map_err(|error| format!("preview canvas: {error:?}"))?; + .map_err(|error| RendererError::External(format!("preview canvas: {error:?}")))?; let options = js_sys::Object::new(); js_sys::Reflect::set( &options, &"preserveDrawingBuffer".into(), &JsValue::from_bool(true), ) - .map_err(|error| format!("preview context options: {error:?}"))?; + .map_err(|error| RendererError::External(format!("preview context options: {error:?}")))?; let gl = canvas .get_context_with_context_options("webgl2", &options) - .map_err(|error| format!("preview context: {error:?}"))? - .ok_or_else(|| "preview WebGL2 context unavailable".to_owned())? + .map_err(|error| RendererError::External(format!("preview context: {error:?}")))? + .ok_or(RendererError::External( + "preview WebGL2 context unavailable".to_owned(), + ))? .dyn_into::() - .map_err(|_| "preview context is not WebGL2".to_owned())?; + .map_err(|_| RendererError::External("preview context is not WebGL2".to_owned()))?; let glow_context = three_d::context::Context::from_webgl2_context(gl); let context = three_d::Context::from_gl_context(Arc::new(glow_context)) - .map_err(|error| format!("preview three-d context: {error}"))?; + .map_err(|error| RendererError::GpuResource(format!("preview three-d context: {error}")))?; Ok(PreviewTarget { canvas, context }) } @@ -1080,11 +1090,11 @@ fn start_frame_loop(state: &Rc>) { if st.needs_render && st.renderer.is_some() { match consume_input_and_render(&mut st) { Ok(()) => st.needs_render = false, - Err(message) => { + Err(error) => { st.needs_render = false; st.post_event(&RendererEvent::Error { fatal: true, - message, + message: error.to_string(), }); st.dispose_resources(false); return; @@ -1143,7 +1153,7 @@ fn schedule_next_frame(st: &mut WorkerState, state: Rc>) { } } -fn consume_input_and_render(st: &mut WorkerState) -> Result<(), String> { +fn consume_input_and_render(st: &mut WorkerState) -> Result<(), RendererError> { if st.renderer.is_none() { return Ok(()); } @@ -1267,7 +1277,9 @@ fn consume_input_and_render(st: &mut WorkerState) -> Result<(), String> { } } Err(error) => { - return Err(format!("renderer camera clipping rejected: {error}")); + return Err(RendererError::External(format!( + "renderer camera clipping rejected: {error}" + ))); } } } @@ -1456,6 +1468,7 @@ fn handle_load_mesh( let candidate = match build_scene(&context, &model, pw, ph) { Ok(candidate) => candidate, Err(detail) => { + let detail = detail.to_string(); let error = LoadError { code: LoadErrorCode::Install, detail: detail.clone(), @@ -1511,6 +1524,7 @@ fn handle_load_mesh( }); } Err(detail) => { + let detail = detail.to_string(); let error = LoadError { code: LoadErrorCode::Fetch, detail: detail.clone(), @@ -1543,7 +1557,7 @@ async fn load_mesh_async_with_verification( expected_size: Option, expected_digest: Option>, abort: Option<&AbortController>, -) -> Result { +) -> Result { tracing::info!( ?format, primary_url, @@ -1562,8 +1576,8 @@ async fn load_mesh_async_with_verification( }; let bytes = fetch_bytes(&url, abort.as_ref(), expected_bytes) .await - .map_err(|e| { - polymodel_mesh::contract::MeshLoadError::Decode(format!("fetch {url}: {e}")) + .map_err(|error| { + polymodel_mesh::contract::MeshLoadError::Decode(format!("fetch {url}: {error}")) })?; tracing::debug!( %url, @@ -1596,7 +1610,7 @@ async fn load_mesh_async_with_verification( } }) .await - .map_err(|e| e.to_string())?; + .map_err(|error| RendererError::External(format!("mesh load failed: {error}")))?; tracing::info!( ?format, parts = model.part_count(), @@ -1757,7 +1771,15 @@ enum CompoundLoadError { Cancelled, Superseded, Disposed, - Failed(String), + Parse(polymodel_ldraw_core::ParseError), + Adapt(RendererError), + Bundle(&'static str), +} + +impl From for CompoundLoadError { + fn from(error: RendererError) -> Self { + Self::Adapt(error) + } } impl std::fmt::Display for CompoundLoadError { @@ -1766,23 +1788,13 @@ impl std::fmt::Display for CompoundLoadError { Self::Cancelled => formatter.write_str("compound load cancelled"), Self::Superseded => formatter.write_str("compound load superseded"), Self::Disposed => formatter.write_str("renderer worker is disposed"), - Self::Failed(error) => formatter.write_str(error), + Self::Parse(error) => write!(formatter, "compound parse failed: {error}"), + Self::Adapt(error) => write!(formatter, "compound adaptation failed: {error}"), + Self::Bundle(detail) => write!(formatter, "invalid model bundle: {detail}"), } } } -impl From for CompoundLoadError { - fn from(error: String) -> Self { - Self::Failed(error) - } -} - -impl From for String { - fn from(error: CompoundLoadError) -> Self { - error.to_string() - } -} - fn finish_compound_preview( state: &Rc>, request_id: u32, @@ -1866,7 +1878,7 @@ async fn render_compound_preview_request( compound_is_current(state, RendererNamespace::Preview, identity, abort.as_ref())?; let bounds = scene .bounds - .ok_or_else(|| "LDraw scene has no bounds".to_owned())?; + .ok_or(CompoundLoadError::Bundle("LDraw scene has no bounds"))?; let target = create_preview_target(width, height)?; let scene = Rc::new(scene); let style = polymodel_renderer_worker::RenderStyle::Faces; @@ -1880,10 +1892,12 @@ async fn render_compound_preview_request( )?; 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())?; + .map_err(|error| CompoundLoadError::Adapt(RendererError::External(error.to_string())))?; let projection = polymodel_renderer_worker::camera_space_clip_planes(bounds, fit.position, fit.center) - .map_err(|error| error.to_string())?; + .map_err(|error| { + CompoundLoadError::Adapt(RendererError::External(error.to_string())) + })?; let camera = three_d::Camera::new_perspective( three_d::Viewport::new_at_origo(width, height), fit.position, @@ -1950,7 +1964,7 @@ async fn load_compound_source( identity: LoadIdentity, bundle: polymodel_renderer_protocol::ModelLoadBundle, abort: Option, -) -> Result { +) -> Result { tracing::info!( ?namespace, ?identity, @@ -1960,14 +1974,18 @@ async fn load_compound_source( "renderer worker: compound bundle received" ); if bundle.version != polymodel_renderer_protocol::PROTOCOL_VERSION { - return Err("model bundle protocol version mismatch".into()); + return Err(CompoundLoadError::Bundle( + "model bundle protocol version mismatch", + )); } if bundle.format != MeshFormat::CompoundLdraw { - return Err("model bundle format is not LDraw".into()); + return Err(CompoundLoadError::Bundle( + "model bundle format is not LDraw", + )); } bundle .validate() - .map_err(|error| format!("model bundle rejected: {error:?}"))?; + .map_err(|_| CompoundLoadError::Bundle("model bundle rejected"))?; validate_bundle_resources(&bundle.primary, &bundle.resources)?; tracing::debug!( ?namespace, @@ -1980,10 +1998,14 @@ async fn load_compound_source( let total_bytes = bundle.resources.iter().try_fold(0_u64, |total, resource| { total .checked_add(resource.byte_length) - .ok_or("model bundle byte bound overflow") + .ok_or(CompoundLoadError::Bundle( + "model bundle byte bound overflow", + )) })?; if total_bytes > MAX_UNKNOWN_FETCH_BYTES { - return Err("model bundle exceeds decoded source bound".into()); + return Err(CompoundLoadError::Bundle( + "model bundle exceeds decoded source bound", + )); } compound_is_current(state, namespace, identity, abort.as_ref())?; let source = VerifiedCompoundSource { @@ -2039,7 +2061,7 @@ fn finish_compound_source( state: &Rc>, namespace: RendererNamespace, identity: LoadIdentity, - result: Result, + result: Result, cancellation: CancellationPolicy, ) { { @@ -2105,7 +2127,12 @@ fn finish_compound_source( } return; } - Err(CompoundLoadError::Failed(detail)) => { + Err( + detail @ (CompoundLoadError::Parse(_) + | CompoundLoadError::Adapt(_) + | CompoundLoadError::Bundle(_)), + ) => { + let detail = detail.to_string(); tracing::error!( ?namespace, ?identity, @@ -2199,7 +2226,12 @@ fn finish_compound_source( (static_model, edge_model, plan, style) } Err(detail) => { - fail_compound_install(&mut st, namespace, identity, detail); + fail_compound_install( + &mut st, + namespace, + identity, + detail.to_string(), + ); return; } }; @@ -2287,6 +2319,7 @@ fn finish_compound_source( } } Err(detail) => { + let detail = detail.to_string(); let reducer = match namespace { RendererNamespace::Interactive => &mut st.load_reducer, RendererNamespace::Preview => &mut st.preview_reducer, @@ -2384,7 +2417,7 @@ fn handle_render_preview_image( Ok(png) => RendererEvent::PreviewImageRendered { request_id, png }, Err(message) => RendererEvent::PreviewImageError { request_id, - message, + message: message.to_string(), }, }; st.post_event(&event); @@ -2397,7 +2430,7 @@ async fn render_preview_request( width: u32, height: u32, abort: Option, -) -> Result, String> { +) -> Result, RendererError> { let fetch_bytes = MAX_UNKNOWN_FETCH_BYTES.min( u64::from(width) .saturating_mul(u64::from(height)) @@ -2408,7 +2441,7 @@ async fn render_preview_request( .as_ref() .is_some_and(|controller| controller.signal().aborted()) { - return Err("preview load cancelled".into()); + return Err(RendererError::External("preview load cancelled".to_owned())); } let model = load_mesh_async_with_abort(primary_url, format, fetch_bytes, abort.as_ref()).await?; @@ -2416,7 +2449,7 @@ async fn render_preview_request( .as_ref() .is_some_and(|controller| controller.signal().aborted()) { - return Err("preview load cancelled".into()); + return Err(RendererError::External("preview load cancelled".to_owned())); } render_preview_png(&model, width, height).await } @@ -2426,7 +2459,7 @@ async fn load_mesh_async_with_abort( format: MeshFormat, fetch_limit: u64, abort: Option<&AbortController>, -) -> Result { +) -> Result { let remaining = Rc::new(Cell::new(fetch_limit)); let model = load_mesh_with_companions(primary_url, format, |url| { let route = url.clone(); @@ -2454,11 +2487,15 @@ async fn load_mesh_async_with_abort( } }) .await - .map_err(|error| error.to_string())?; + .map_err(|error| RendererError::External(format!("mesh load failed: {error}")))?; Ok(model) } -async fn render_preview_png(model: &ModelMesh, width: u32, height: u32) -> Result, String> { +async fn render_preview_png( + model: &ModelMesh, + width: u32, + height: u32, +) -> Result, RendererError> { let target = create_preview_target(width, height)?; let (model, camera, _control) = build_scene(&target.context, model, width, height)?; let (ambient_light, key_light, fill_light) = standard_lights(&target.context); @@ -2476,27 +2513,33 @@ async fn render_preview_png(model: &ModelMesh, width: u32, height: u32) -> Resul offscreen_canvas_png_bytes(&target.canvas).await } -async fn offscreen_canvas_png_bytes(canvas: &web_sys::OffscreenCanvas) -> Result, String> { +async fn offscreen_canvas_png_bytes( + canvas: &web_sys::OffscreenCanvas, +) -> Result, RendererError> { let options = js_sys::Object::new(); js_sys::Reflect::set(&options, &"type".into(), &JsValue::from_str("image/png")) - .map_err(|e| format!("preview blob options: {e:?}"))?; + .map_err(|error| RendererError::External(format!("preview blob options: {error:?}")))?; let convert = js_sys::Reflect::get(canvas, &"convertToBlob".into()) - .map_err(|e| format!("preview convertToBlob lookup: {e:?}"))? + .map_err(|error| { + RendererError::External(format!("preview convertToBlob lookup: {error:?}")) + })? .dyn_into::() - .map_err(|_| "preview convertToBlob is unavailable".to_string())?; + .map_err(|_| RendererError::External("preview convertToBlob is unavailable".to_owned()))?; let promise = convert .call1(canvas, &options) - .map_err(|e| format!("preview convertToBlob: {e:?}"))? + .map_err(|error| RendererError::External(format!("preview convertToBlob: {error:?}")))? .dyn_into::() - .map_err(|_| "preview convertToBlob did not return a Promise".to_string())?; + .map_err(|_| { + RendererError::External("preview convertToBlob did not return a Promise".to_owned()) + })?; let blob = JsFuture::from(promise) .await - .map_err(|e| format!("preview blob await: {e:?}"))? + .map_err(|error| RendererError::External(format!("preview blob await: {error:?}")))? .dyn_into::() - .map_err(|_| "preview result is not a Blob".to_string())?; + .map_err(|_| RendererError::External("preview result is not a Blob".to_owned()))?; let buffer = JsFuture::from(blob.array_buffer()) .await - .map_err(|e| format!("preview blob arrayBuffer: {e:?}"))?; + .map_err(|error| RendererError::External(format!("preview blob arrayBuffer: {error:?}")))?; Ok(js_sys::Uint8Array::new(&buffer).to_vec()) } @@ -2504,7 +2547,7 @@ async fn fetch_bytes( url: &str, abort: Option<&AbortController>, expected_bytes: Option, -) -> Result, String> { +) -> Result, RendererError> { let limit = expected_bytes.unwrap_or(MAX_UNKNOWN_FETCH_BYTES); let opts = web_sys::RequestInit::new(); opts.set_method("GET"); @@ -2514,20 +2557,20 @@ async fn fetch_bytes( } let request = web_sys::Request::new_with_str_and_init(url, &opts) - .map_err(|e| format!("request: {:?}", e))?; + .map_err(|error| RendererError::External(format!("request: {error:?}")))?; let global = js_sys::global(); - let fetch_fn = - js_sys::Reflect::get(&global, &"fetch".into()).map_err(|e| format!("no fetch: {:?}", e))?; + let fetch_fn = js_sys::Reflect::get(&global, &"fetch".into()) + .map_err(|error| RendererError::External(format!("no fetch: {error:?}")))?; let fetch_promise = js_sys::Function::from(fetch_fn) .call1(&global, &request) - .map_err(|e| format!("fetch call: {:?}", e))? + .map_err(|error| RendererError::External(format!("fetch call: {error:?}")))? .dyn_into::() - .map_err(|_| "fetch: not a Promise".to_string())?; + .map_err(|_| RendererError::External("fetch: not a Promise".to_owned()))?; let response = JsFuture::from(fetch_promise) .await - .map_err(|e| format!("fetch: {:?}", e))? + .map_err(|error| RendererError::External(format!("fetch: {error:?}")))? .dyn_into::() - .map_err(|_| "fetch: not a Response".to_string())?; + .map_err(|_| RendererError::External("fetch: not a Response".to_owned()))?; if !response.ok() { let status = response.status(); @@ -2538,7 +2581,7 @@ async fn fetch_bytes( } else { format!("fetch returned HTTP {status} {status_text}: {bounded_body}") }; - return Err(detail); + return Err(RendererError::External(detail)); } let content_length = response @@ -2550,38 +2593,42 @@ async fn fetch_bytes( if let Some(content_length) = content_length && content_length > limit { - return Err(format!( + return Err(RendererError::External(format!( "response exceeds fetch bound: {content_length} bytes > {limit}" - )); + ))); } let body = js_sys::Reflect::get(&response, &"body".into()) - .map_err(|e| format!("response body: {e:?}"))?; + .map_err(|error| RendererError::External(format!("response body: {error:?}")))?; if body.is_null() || body.is_undefined() { - return Err("response has no readable body".into()); + return Err(RendererError::External( + "response has no readable body".to_owned(), + )); } let reader = js_sys::Reflect::get(&body, &"getReader".into()) - .map_err(|e| format!("response reader: {e:?}"))? + .map_err(|error| RendererError::External(format!("response reader: {error:?}")))? .dyn_into::() - .map_err(|_| "response body has no reader".to_string())? + .map_err(|_| RendererError::External("response body has no reader".to_owned()))? .call0(&body) - .map_err(|e| format!("response reader: {e:?}"))?; + .map_err(|error| RendererError::External(format!("response reader: {error:?}")))?; let read = js_sys::Reflect::get(&reader, &"read".into()) - .map_err(|e| format!("response reader read: {e:?}"))? + .map_err(|error| RendererError::External(format!("response reader read: {error:?}")))? .dyn_into::() - .map_err(|_| "response reader has no read method".to_string())?; + .map_err(|_| RendererError::External("response reader has no read method".to_owned()))?; let mut bytes = Vec::with_capacity( usize::try_from(content_length.unwrap_or(0).min(limit)).unwrap_or_default(), ); loop { let result = read .call0(&reader) - .map_err(|e| format!("response reader read: {e:?}"))? + .map_err(|error| RendererError::External(format!("response reader read: {error:?}")))? .dyn_into::() - .map_err(|_| "response reader read did not return a Promise".to_string())?; + .map_err(|_| { + RendererError::External("response reader read did not return a Promise".to_owned()) + })?; let result = JsFuture::from(result) .await - .map_err(|e| format!("response body: {e:?}"))?; + .map_err(|error| RendererError::External(format!("response body: {error:?}")))?; if js_sys::Reflect::get(&result, &"done".into()) .ok() .and_then(|done| done.as_bool()) @@ -2590,16 +2637,19 @@ async fn fetch_bytes( break; } let value = js_sys::Reflect::get(&result, &"value".into()) - .map_err(|e| format!("response body chunk: {e:?}"))?; + .map_err(|error| RendererError::External(format!("response body chunk: {error:?}")))?; let chunk = js_sys::Uint8Array::new(&value); - let next_len = bytes - .len() - .checked_add(chunk.length() as usize) - .ok_or_else(|| "response exceeds fetch bound".to_string())?; + let next_len = + bytes + .len() + .checked_add(chunk.length() as usize) + .ok_or(RendererError::External( + "response exceeds fetch bound".to_owned(), + ))?; if next_len > usize::try_from(limit).unwrap_or(usize::MAX) { - return Err(format!( + return Err(RendererError::External(format!( "response exceeds fetch bound: more than {limit} bytes" - )); + ))); } bytes.extend_from_slice(&chunk.to_vec()); }