diff --git a/.sqlx/query-272bebe0a71065b578e43eb6ea762c786dc120d94affe5a89c9348b6c8088d7e.json b/.sqlx/query-272bebe0a71065b578e43eb6ea762c786dc120d94affe5a89c9348b6c8088d7e.json new file mode 100644 index 0000000..e1d0050 --- /dev/null +++ b/.sqlx/query-272bebe0a71065b578e43eb6ea762c786dc120d94affe5a89c9348b6c8088d7e.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT source_identity FROM ldraw_verification WHERE resource_uri = ?", + "describe": { + "columns": [ + { + "name": "source_identity", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "ldraw_verification", + "name": "source_identity" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "272bebe0a71065b578e43eb6ea762c786dc120d94affe5a89c9348b6c8088d7e" +} diff --git a/crates/polymodel-ldraw-core/src/adapter.rs b/crates/polymodel-ldraw-core/src/adapter.rs index 68a0613..b4c37cc 100644 --- a/crates/polymodel-ldraw-core/src/adapter.rs +++ b/crates/polymodel-ldraw-core/src/adapter.rs @@ -9,11 +9,9 @@ use std::borrow::Cow; pub struct InProcessRustAdapter; #[derive(Clone)] pub struct AdapterRequest<'src> { - pub fixture_id: &'src str, pub bytes: &'src [u8], pub profile: ParserProfile, pub provenance_id: &'src str, - pub semantic_budget: Option, pub root_name: &'src str, pub root: RootId, pub materializations: &'src [Materialization], @@ -32,7 +30,6 @@ impl InProcessRustAdapter { let mut options = ParseOptions::new(); options.profile = request.profile; options.limits = limits; - options.semantic_budget = request.semantic_budget; options.root_name = request.root_name.into(); options.resolved_root = request.root; options.target_selections = request.target_selections; @@ -40,7 +37,6 @@ impl InProcessRustAdapter { let mut result = LdrawParser.parse_bytes(request.bytes, options, request.materializations)?; result.result.provenance_id = Cow::Owned(request.provenance_id.to_owned()); - let _ = request.fixture_id; Ok(result) } pub fn parse_fixture<'a>( @@ -49,11 +45,9 @@ impl InProcessRustAdapter { profile: ParserProfile, ) -> Result { Self::parse(AdapterRequest { - fixture_id, bytes, profile, provenance_id: fixture_id, - semantic_budget: Some(16 * 1024 * 1024), root_name: fixture_id, root: RootId::UploadedManifest, materializations: &[], diff --git a/crates/polymodel-ldraw-core/src/lib.rs b/crates/polymodel-ldraw-core/src/lib.rs index ac50db4..ffc2de5 100644 --- a/crates/polymodel-ldraw-core/src/lib.rs +++ b/crates/polymodel-ldraw-core/src/lib.rs @@ -142,11 +142,9 @@ mod tests { Some("caller-materialization".into()), ); let result = InProcessRustAdapter::parse(AdapterRequest { - fixture_id: "materialized", bytes, profile: ParserProfile::Compatibility, provenance_id: "caller-provenance", - semantic_budget: Some(16 * 1024 * 1024), root_name: "root.ldr", root: RootId::UploadedManifest, materializations: std::slice::from_ref(&owned_materialization), @@ -164,8 +162,7 @@ mod tests { let bytes = (0..N) .map(|index| format!("1 16 0 0 0 1 0 0 0 1 0 0 0 0 missing-{index:03}.dat\n")) .collect::(); - let mut parse_options = options(); - parse_options.semantic_budget = Some(16 * 1024 * 1024); + let parse_options = options(); let result = LdrawParser .parse_bytes(bytes.as_bytes(), parse_options, &[]) .expect("type-1 instances should parse within the semantic budget"); @@ -571,11 +568,9 @@ mod tests { None, ); let result = InProcessRustAdapter::parse(AdapterRequest { - fixture_id: "occurrence-state", bytes, profile: ParserProfile::Compatibility, provenance_id: "occurrence-state", - semantic_budget: Some(16 * 1024 * 1024), root_name: "root.ldr", root: RootId::UploadedManifest, materializations: std::slice::from_ref(&child), diff --git a/crates/polymodel-ldraw-core/src/mpd.rs b/crates/polymodel-ldraw-core/src/mpd.rs index 2b78105..69385b7 100644 --- a/crates/polymodel-ldraw-core/src/mpd.rs +++ b/crates/polymodel-ldraw-core/src/mpd.rs @@ -235,13 +235,70 @@ pub fn official_library_candidates( Ok(candidates) } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TexmapReference { + pub pngfile: String, + pub glossmap: Option, +} + #[derive(Clone, Debug)] pub struct CompoundSourceInspection { pub references: Vec, + pub texmap_references: Vec, pub source_keys: Vec<(String, CacheKey)>, pub has_virtual_files: bool, } +fn extract_texmap_references_from_files(files: &[VirtualFile<'_>]) -> Vec { + let mut references = Vec::new(); + for file in files { + for line in &file.lines { + if line.line_type != Some(LineType::Zero) + || line.tokens.get(1).map(|token| token.kind) != Some(TokenKind::Texmap) + || !matches!( + line.tokens.get(2).map(|token| token.kind), + Some(TokenKind::Start | TokenKind::Next) + ) + { + continue; + } + let Some(mode) = line.tokens.get(3).map(|token| token.text) else { + continue; + }; + let parameter_count = if mode.eq_ignore_ascii_case("planar") { + 9 + } else if mode.eq_ignore_ascii_case("cylindrical") { + 10 + } else if mode.eq_ignore_ascii_case("spherical") { + 11 + } else { + continue; + }; + let reference_index = 4 + parameter_count; + let Some(pngfile) = line.tokens.get(reference_index).map(|token| token.text) else { + continue; + }; + if pngfile.is_empty() { + continue; + } + let glossmap = (line.tokens.get(reference_index + 1).map(|token| token.text) + == Some("GLOSSMAP")) + .then(|| line.tokens.get(reference_index + 2).map(|token| token.text)) + .flatten() + .filter(|value| !value.is_empty()) + .map(str::to_owned); + let reference = TexmapReference { + pngfile: pngfile.to_owned(), + glossmap, + }; + if !references.contains(&reference) { + references.push(reference); + } + } + } + references +} + /// Scan one compound source and retain the exact source identities used for /// resolver selections. The caller can resolve edges while this inspection is /// still live; no second source scan is needed. @@ -279,6 +336,7 @@ pub fn inspect_compound_source( .collect::, _>>()?; Ok(CompoundSourceInspection { references: extract_references_from_files(&files, has_virtual_files)?, + texmap_references: extract_texmap_references_from_files(&files), source_keys, has_virtual_files, }) diff --git a/crates/polymodel-ldraw-core/src/parser.rs b/crates/polymodel-ldraw-core/src/parser.rs index a781b13..6382096 100644 --- a/crates/polymodel-ldraw-core/src/parser.rs +++ b/crates/polymodel-ldraw-core/src/parser.rs @@ -409,7 +409,6 @@ fn parse_model<'src>( diagnostic.code, DiagnosticCode::LineBytesLimit | DiagnosticCode::FilesLimit - | DiagnosticCode::SemanticArenaReservationLimit | DiagnosticCode::IncludeDepthLimit | DiagnosticCode::CommandsLimit | DiagnosticCode::InstancesLimit diff --git a/crates/polymodel-ldraw-core/src/traversal.rs b/crates/polymodel-ldraw-core/src/traversal.rs index 36c56ce..0ee1797 100644 --- a/crates/polymodel-ldraw-core/src/traversal.rs +++ b/crates/polymodel-ldraw-core/src/traversal.rs @@ -378,7 +378,7 @@ pub(crate) fn traverse( .map_err(|name| limit_error(name, Some(include.span)))?; charged_instances = charged_instances .checked_add(1) - .ok_or(ParseError::Overflow("instance semantic reservation"))?; + .ok_or(ParseError::Overflow("instance count overflow"))?; let Some(target) = resolved_includes[index][include_index] else { add_diag( options.profile, diff --git a/crates/polymodel-ldraw-core/src/types.rs b/crates/polymodel-ldraw-core/src/types.rs index c10710b..084304b 100644 --- a/crates/polymodel-ldraw-core/src/types.rs +++ b/crates/polymodel-ldraw-core/src/types.rs @@ -56,7 +56,6 @@ pub enum DiagnosticCode { InvalidUtf8, FilesLimit, DuplicateFileName, - SemanticArenaReservationLimit, InvalidType1, InvalidType2, InvalidType3, @@ -110,7 +109,6 @@ impl fmt::Display for DiagnosticCode { Self::InvalidUtf8 => "INVALID_UTF8", Self::FilesLimit => "FILES_LIMIT", Self::DuplicateFileName => "DUPLICATE_FILE_NAME", - Self::SemanticArenaReservationLimit => "SEMANTIC_ARENA_RESERVATION_LIMIT", Self::InvalidType1 => "INVALID_TYPE1", Self::InvalidType2 => "INVALID_TYPE2", Self::InvalidType3 => "INVALID_TYPE3", @@ -198,8 +196,6 @@ pub enum LdrawError { Diagnostic(Diagnostic), #[error(transparent)] LdrawDiagnostic(#[from] LdrawDiagnostic), - #[error("semantic arena reservation failed: {0}")] - Reservation(String), #[error("input arithmetic overflow while accounting {0}")] Overflow(&'static str), #[error("parse cancelled")] @@ -448,7 +444,6 @@ pub use polymodel_renderer_protocol::TargetSelection; pub struct ParseOptions<'a> { pub profile: ParserProfile, pub limits: LdrawLimits, - pub semantic_budget: Option, pub root_name: String, pub resolved_root: RootId, pub fetch_count: u64, @@ -493,7 +488,6 @@ impl<'a> ParseOptions<'a> { Self { profile: ParserProfile::Strict, limits: LdrawLimits::default(), - semantic_budget: None, root_name: DEFAULT_ROOT_NAME.into(), resolved_root: RootId::UploadedManifest, fetch_count: 0, diff --git a/crates/polymodel-renderer-worker/src/lib.rs b/crates/polymodel-renderer-worker/src/lib.rs index 6811857..a424178 100644 --- a/crates/polymodel-renderer-worker/src/lib.rs +++ b/crates/polymodel-renderer-worker/src/lib.rs @@ -109,7 +109,7 @@ impl NativeTrace { let mut state = self.state.lock().expect("native trace poisoned"); state.stages.push(stage); state.records.push(NativeStageRecord { stage, elapsed_ms }); - eprintln!("[ldraw-preview] stage={stage:?} elapsed_ms={elapsed_ms}"); + tracing::debug!("[ldraw-preview] stage={stage:?} elapsed_ms={elapsed_ms}"); } pub fn finish(&self, terminal: NativeTerminal) { @@ -156,10 +156,6 @@ fn finite_f32(value: f64) -> Result { Ok(value as f32) } -pub fn adapt_ldraw_model(model: &ModelSummary) -> Result { - adapt_ldraw_assembly(std::slice::from_ref(model), None, None, None) -} - pub fn adapt_ldraw_parse(result: &OwnedParseResult) -> Result { adapt_ldraw_parse_with_resources(result, None) } @@ -236,7 +232,7 @@ fn adapt_ldraw_assembly( .any(|geometry| matches!(geometry.line_type, LineType::Three | LineType::Four)) }) .count(); - eprintln!( + tracing::debug!( "[ldraw-adapter] definitions={} semantic_triangle_defs={} geometry_triangle_defs={} root_key={:?}", models.len(), definitions_with_semantic_triangles, @@ -244,7 +240,7 @@ fn adapt_ldraw_assembly( models.first().map(|model| &model.cache_key), ); for (index, model) in models.iter().take(8).enumerate() { - eprintln!( + tracing::debug!( "[ldraw-adapter] def[{index}] path={} key={:?} semantic_triangles={} geometry_records={} geometry_triangles={}", model.path, model.cache_key, @@ -292,6 +288,22 @@ fn adapt_ldraw_assembly( texture_ids.insert(key, id); Ok(id) }; + let texmap_available = |root: RootId, texmap: &SceneTexmapAssociation| { + let Some(resources) = resources else { + return false; + }; + let valid = |reference: &str| { + let path = NormalizedPath::new(reference).ok()?; + let resource = texture_resource_for(resources, root, &path)?; + resource.validate().ok().map(|_| ()) + }; + valid(texmap.pngfile.as_str()).is_some() + && texmap + .glossmap + .as_deref() + .map(|reference| valid(reference).is_some()) + .unwrap_or(true) + }; let mut material_ids = HashMap::< ( u32, @@ -538,13 +550,15 @@ fn adapt_ldraw_assembly( exact_misses += 1; } if index < 12 { - eprintln!( + tracing::debug!( "[ldraw-adapter] edge[{index}] source={} target={} target_key={:?} exact_hit={hit}", - occurrence.source_model, occurrence.target_model, occurrence.target_key, + occurrence.source_model, + occurrence.target_model, + occurrence.target_key, ); } } - eprintln!( + tracing::debug!( "[ldraw-adapter] edges_sampled={} exact_target_hits={} exact_target_misses={}", instances.len().min(1024), exact_hits, @@ -739,7 +753,7 @@ fn adapt_ldraw_assembly( ] }); if let Some(value) = bounds { - eprintln!( + tracing::debug!( "[ldraw-scene] aggregate_bounds min={:?} max={:?} center={:?} extents={:?}", value.min, value.max, @@ -819,12 +833,12 @@ fn adapt_ldraw_assembly( .partial_cmp(&left.center_distance) .unwrap_or(std::cmp::Ordering::Equal) }); - eprintln!( + tracing::debug!( "[ldraw-scene] occurrence_bounds count={} ranked_by=center_distance", distribution.len() ); for diagnostic in distribution.iter().take(8) { - eprintln!( + tracing::debug!( "[ldraw-scene] outlier index={} center_distance={:.3} max_extent={:.3} min={:?} max={:?} source={} target={} target_key={:?}", diagnostic.index, diagnostic.center_distance, @@ -843,7 +857,7 @@ fn adapt_ldraw_assembly( .unwrap_or(std::cmp::Ordering::Equal) }); for diagnostic in distribution.iter().take(8) { - eprintln!( + tracing::debug!( "[ldraw-scene] largest index={} center_distance={:.3} max_extent={:.3} min={:?} max={:?} source={} target={} target_key={:?}", diagnostic.index, diagnostic.center_distance, @@ -1364,7 +1378,7 @@ pub fn lighting_diagnostics_enabled() -> bool { pub fn emit_lighting_diagnostic(metadata: &NativeCaptureMetadata, scene: &Scene) { if lighting_diagnostics_enabled() { - eprintln!( + tracing::debug!( "[ldraw-diagnostic] case={} schema={} fixture={} path={} variant={} occurrence={} source_span={:?} determinant={} material_id={} scene_triangles={}", metadata.case_id, metadata.schema_version, @@ -1695,7 +1709,7 @@ pub fn prepare_preview_scene( .map_err(|error| format!("preview input path: {error}"))?; let started = Instant::now(); let closure = official_materializations(library_root, &path, &bytes)?; - eprintln!( + tracing::debug!( "[ldraw-preview] preparation-stage=closure elapsed_ms={} materializations={} selections={}", started.elapsed().as_millis(), closure.materializations.len(), @@ -1715,11 +1729,9 @@ pub fn prepare_preview_scene( ); let result = InProcessRustAdapter::parse_with_limits( AdapterRequest { - fixture_id: logical_name, bytes: &bytes, profile: ParserProfile::Compatibility, provenance_id: logical_name, - semantic_budget: Some(64 * 1024 * 1024), root_name: logical_name, root: RootId::CurrentMpd, materializations: &materializations, @@ -3057,26 +3069,6 @@ pub fn pick_scene(scene: &Scene, ray: PickRay) -> Result, S ScenePicker::new(scene, plan)?.pick(ray) } -pub fn occurrence_selection_bounds( - scene: &Scene, - occurrence_indices: &[usize], -) -> Result { - let index = SceneIndex::new(scene)?; - let mut points = Vec::new(); - for occurrence_index in occurrence_indices { - let occurrence = scene - .occurrences - .get(*occurrence_index) - .ok_or("selected occurrence is outside the scene table")?; - let geometry = index.geometry(scene, occurrence.node)?; - for point in &geometry.points { - points.push(apply_scene_transform(occurrence.transform, *point)?); - } - } - SceneBounds::from_points(points) - .ok_or_else(|| "selected subtree has no finite bounds".to_owned()) -} - pub fn logical_part_selection( picker: &ScenePicker, raw_index: usize, @@ -3248,7 +3240,7 @@ pub fn scene_render_plan(scene: &Scene) -> Result { occurrence_gpu_ranges_from_batches(scene, &index, &upload_batches, &topologies)?; let ranges_complete = started.elapsed(); if diagnostics { - eprintln!( + tracing::debug!( "[ldraw-preview] plan-detail index_ms={} topology_ms={} local_bounds_ms={} batches_ms={} ranges_ms={}", indexed.as_millis(), (topology_complete - indexed).as_millis(), @@ -4133,7 +4125,7 @@ pub fn diagnostic_edge_models(label: &str, models: &[EdgeModel], camera: &Camera return; } let frustum = three_d::Frustum::new(camera.projection() * camera.view()); - eprintln!( + tracing::debug!( "[ldraw-runtime] slot={label} generation={generation} objects={} camera_position={:?} camera_target={:?} camera_distance={:.6} viewport={}x{}", models.len(), camera.position(), @@ -4145,7 +4137,7 @@ pub fn diagnostic_edge_models(label: &str, models: &[EdgeModel], camera: &Camera for (index, model) in models.iter().enumerate() { let aabb = model.aabb(); let center_distance = (aabb.center() - camera.position()).magnitude(); - eprintln!( + tracing::debug!( "[ldraw-runtime] slot={label} object={index} material_type={:?} alpha={} render_states={:?} aabb_min={:?} aabb_max={:?} center_distance={center_distance:.6} frustum_visible={} segments={} instances={} vertices={}", model.material_type(), model.material.color.a, @@ -4163,7 +4155,7 @@ pub fn diagnostic_edge_models(label: &str, models: &[EdgeModel], camera: &Camera #[cfg(not(target_arch = "wasm32"))] pub fn diagnostic_edge_model_slot(label: &str, models: Option<&[EdgeModel]>, generation: u64) { if std::env::var_os("POLYMODEL_LDRAW_RUNTIME_DIAGNOSTICS").is_some() { - eprintln!( + tracing::debug!( "[ldraw-runtime] slot-summary slot={label} generation={generation} present={} objects={}", models.is_some(), models.map_or(0, <[EdgeModel]>::len), @@ -4185,7 +4177,7 @@ pub fn build_edge_models_for_view( let explicit = build_compact_edge_models(context, explicit_inputs.batches, false)?; let conditional = build_compact_edge_models(context, conditional_inputs.batches, false)?; let model_upload_ms = upload_started.elapsed().as_millis(); - eprintln!( + tracing::debug!( "[ldraw-preview] edge-output source_geometries={} planned_occurrences={} explicit_segments={} conditional_visible_segments={} vertices={} indices={} material_groups={} topology_cpu_ms={} model_upload_ms={}", stats.source_geometry_count, stats.planned_occurrences, @@ -4289,7 +4281,7 @@ pub fn build_scene_model_parts_for_style_with_resources( let started = Instant::now(); let plan = scene_render_plan(scene)?; let plan_elapsed = started.elapsed(); - eprintln!( + tracing::debug!( "[ldraw-preview] native-build-stage=plan elapsed_ms={} cumulative_ms={}", plan_elapsed.as_millis(), plan_elapsed.as_millis() @@ -4304,7 +4296,7 @@ pub fn build_scene_model_parts_for_style_with_resources( resources, )?; let static_elapsed = static_started.elapsed(); - eprintln!( + tracing::debug!( "[ldraw-preview] native-build-stage=static elapsed_ms={} cumulative_ms={}", static_elapsed.as_millis(), started.elapsed().as_millis() @@ -4312,7 +4304,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)?; - eprintln!( + tracing::debug!( "[ldraw-preview] native-build-stage=edge-output elapsed_ms={} cumulative_ms={}", edge_started.elapsed().as_millis(), started.elapsed().as_millis() @@ -4322,7 +4314,7 @@ pub fn build_scene_model_parts_for_style_with_resources( (None, None) }; let picker = ScenePicker::new(scene, plan.clone())?; - eprintln!( + tracing::debug!( "[ldraw-preview] native-build-stage=picker elapsed_ms={}", started.elapsed().as_millis() ); @@ -4340,7 +4332,7 @@ pub fn build_scene_faces_and_picker( ) -> Result<(NativeSceneModel, ScenePicker), String> { let started = Instant::now(); let plan = scene_render_plan(scene)?; - eprintln!( + tracing::debug!( "[ldraw-preview] native-build-stage=plan elapsed_ms={}", started.elapsed().as_millis() ); @@ -4352,12 +4344,12 @@ pub fn build_scene_faces_and_picker( &plan.topologies, None, )?; - eprintln!( + tracing::debug!( "[ldraw-preview] native-build-stage=static elapsed_ms={}", started.elapsed().as_millis() ); let picker = ScenePicker::new(scene, plan)?; - eprintln!( + tracing::debug!( "[ldraw-preview] native-build-stage=picker elapsed_ms={}", started.elapsed().as_millis() ); @@ -4962,7 +4954,7 @@ pub fn render_scene_window_options_traced( trace.record(NativeStage::LibraryResolved); trace.record(NativeStage::OmrParsed); trace.record(NativeStage::SceneAdapted); - eprintln!( + tracing::debug!( "[ldraw-preview] backend=wayland WAYLAND_DISPLAY={} XDG_RUNTIME_DIR={}", std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "".to_owned()), std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "".to_owned()), @@ -4979,7 +4971,7 @@ pub fn render_scene_window_options_traced( error })?; trace.record(NativeStage::WindowCreated); - eprintln!("[ldraw-preview] window/context created"); + tracing::debug!("[ldraw-preview] window/context created"); let context = window.gl(); trace.record(NativeStage::ContextCreated); let (static_model, (explicit_model, conditional_model), _, _) = @@ -5034,7 +5026,7 @@ pub fn render_scene_window_options_traced( native_path: "static".into(), }; emit_lighting_diagnostic(&metadata, scene); - eprintln!( + tracing::debug!( "[ldraw-preview] capture case={} schema={} variant={} occurrence={} fixture={} requested={}x{} actual={}x{}", metadata.case_id, metadata.schema_version, @@ -5047,7 +5039,7 @@ pub fn render_scene_window_options_traced( metadata.actual_height, ); } - eprintln!( + tracing::debug!( "[ldraw-preview] camera center={:?} position={:?} target={:?} up={:?} fov={:.1} near={:.3} far={:.3} yaw_degrees={:.1} viewport={}x{}", fit.center, position, diff --git a/crates/polymodel-renderer-worker/src/worker.rs b/crates/polymodel-renderer-worker/src/worker.rs index 0d3c8a5..eb07cee 100644 --- a/crates/polymodel-renderer-worker/src/worker.rs +++ b/crates/polymodel-renderer-worker/src/worker.rs @@ -181,11 +181,9 @@ fn parse_compound_source( "renderer worker: invoking compound parser" ); let result = InProcessRustAdapter::parse(AdapterRequest { - fixture_id: &source.primary, bytes: &primary.bytes, profile: ParserProfile::Compatibility, provenance_id: &source.primary, - semantic_budget: Some(64 * 1024 * 1024), root_name: &source.primary, root: primary_root, materializations: &materializations, @@ -2019,7 +2017,7 @@ fn finish_compound_source( return; } }; - let scene_ref = std::rc::Rc::new(scene.clone()); + let scene_ref = std::rc::Rc::new(scene); let style = polymodel_renderer_worker::RenderStyle::Faces; let models = match polymodel_renderer_worker::build_scene_model_parts_for_style_with_resources( &context, diff --git a/crates/polymodel-renderer-worker/tests/ldraw_preview.rs b/crates/polymodel-renderer-worker/tests/ldraw_preview.rs index 40838f5..2a730f0 100644 --- a/crates/polymodel-renderer-worker/tests/ldraw_preview.rs +++ b/crates/polymodel-renderer-worker/tests/ldraw_preview.rs @@ -42,11 +42,9 @@ fn isolated_plate() -> IsolatedPlate { materializations.insert(0, plate); let result = InProcessRustAdapter::parse_with_limits( AdapterRequest { - fixture_id: "3021.dat", bytes: &bytes, profile: ParserProfile::Compatibility, provenance_id: "official-3021.dat", - semantic_budget: Some(8 * 1024 * 1024), root_name: PLATE_2_X_3, root: RootId::OfficialLibrary, materializations: &materializations, @@ -202,11 +200,9 @@ fn assert_official_subpart_identity(part: &str, authored_target: &str, physical_ ); let result = InProcessRustAdapter::parse_with_limits( AdapterRequest { - fixture_id: part, bytes: &bytes, profile: ParserProfile::Compatibility, provenance_id: part, - semantic_budget: Some(64 * 1024 * 1024), root_name: part, root: RootId::OfficialLibrary, materializations: &materializations, @@ -370,11 +366,9 @@ fn pinned_scene() -> polymodel_renderer_protocol::scene::Scene { eprintln!("[ldraw-preview-test] parsing pinned OMR"); let result = InProcessRustAdapter::parse_with_limits( AdapterRequest { - fixture_id: "omr-10030-1.mpd", bytes: &bytes, profile: ParserProfile::Compatibility, provenance_id: "omr-10030-1.mpd", - semantic_budget: Some(64 * 1024 * 1024), root_name: "omr-10030-1.mpd", root: RootId::CurrentMpd, materializations: &materializations, diff --git a/src/appview/ldraw.rs b/src/appview/ldraw.rs index 225a919..276b193 100644 --- a/src/appview/ldraw.rs +++ b/src/appview/ldraw.rs @@ -167,27 +167,92 @@ async fn resolve_official_library(path: &CanonicalPath, libdir: &Path) -> Option let normalized = polymodel_ldraw_core::NormalizedPath::new(path.as_str()).ok()?; let candidates = polymodel_ldraw_core::official_library_candidates(&normalized).ok()?; let (logical_path, file_path) = find_official_candidate(libdir, &candidates).await?; - let root = if logical_path.as_str().starts_with("parts/") { - RootId::OfficialParts - } else { - RootId::OfficialP + read_official_resource( + RootId::OfficialParts, + logical_path.as_str(), + &file_path, + "application/x-ldraw", + ) + .await +} + +/// Resolve a TEXMAP asset or the shared colour table from the official library. +/// Bare TEXMAP filenames are rooted at `textures/`; explicit `textures/` paths +/// remain canonical. No path outside the configured library is accepted. +async fn resolve_official_asset(reference: &str, libdir: &Path) -> Option { + let normalized = polymodel_ldraw_core::NormalizedPath::new(reference).ok()?; + let canonical_root = tokio::fs::canonicalize(libdir).await.ok()?; + if normalized.as_str().eq_ignore_ascii_case("ldconfig.ldr") { + let file_path = case_insensitive_lookup(libdir, "LDConfig.ldr").await?; + if !tokio::fs::canonicalize(&file_path) + .await + .ok()? + .starts_with(&canonical_root) + { + return None; + } + return read_official_resource( + RootId::OfficialParts, + "LDConfig.ldr", + &file_path, + "application/x-ldraw", + ) + .await; + } + let relative = normalized + .as_str() + .strip_prefix("textures/") + .unwrap_or(normalized.as_str()); + if relative.is_empty() || relative.eq_ignore_ascii_case(".") { + return None; + } + let textures_dir = case_insensitive_lookup(libdir, "textures").await?; + let mut current = textures_dir; + for segment in relative.split('/') { + current = case_insensitive_lookup(¤t, segment).await?; + } + let logical_path = format!("textures/{relative}"); + if !tokio::fs::canonicalize(¤t) + .await + .ok()? + .starts_with(&canonical_root) + { + return None; + } + let mime_type = match logical_path + .rsplit_once('.') + .map(|(_, extension)| extension.to_ascii_lowercase()) + .as_deref() + { + Some("png") => "image/png", + Some("jpg" | "jpeg") => "image/jpeg", + Some("webp") => "image/webp", + _ => "application/octet-stream", }; - let metadata = tokio::fs::metadata(&file_path).await.ok()?; + read_official_resource(RootId::OfficialParts, &logical_path, ¤t, mime_type).await +} + +async fn read_official_resource( + root: RootId, + logical_path: &str, + file_path: &Path, + mime_type: &str, +) -> Option { + let metadata = tokio::fs::metadata(file_path).await.ok()?; let file_size = metadata.len(); - if file_size > MAX_MODEL_RESOURCE_BYTES { + if file_size > MAX_MODEL_RESOURCE_BYTES || !metadata.is_file() { return None; } - let bytes = tokio::fs::read(&file_path).await.ok()?; + let bytes = tokio::fs::read(file_path).await.ok()?; if bytes.len() as u64 != file_size { return None; } - let sha256 = Sha256::digest(&bytes).to_vec(); Some(OfficialResource { root, - path: CanonicalPath::parse(logical_path.as_str()).ok()?, - bytes, - sha256, - mime_type: "application/x-ldraw".to_owned(), + path: CanonicalPath::parse(logical_path).ok()?, + bytes: bytes.clone(), + sha256: Sha256::digest(&bytes).to_vec(), + mime_type: mime_type.to_owned(), }) } @@ -557,6 +622,40 @@ async fn build_model_load_bundle(state: AppState, uri: AtUri) -> AppResult) -> a let (blob_cids, digest, byte_length, mime_type, source_identity) = source; let ordered_blob_cids = serde_json::to_string(&blob_cids)?; let now = now_nanos(); - let existing_identity: Option = sqlx::query_scalar( + let existing_identity: Option = sqlx::query_scalar!( "SELECT source_identity FROM ldraw_verification WHERE resource_uri = ?", + &row.uri ) - .bind(&row.uri) .fetch_optional(&mut **tx) .await?; let identity_changed = diff --git a/tools/ldraw-compat-harness/src/main.rs b/tools/ldraw-compat-harness/src/main.rs index b7a255f..8b0af47 100644 --- a/tools/ldraw-compat-harness/src/main.rs +++ b/tools/ldraw-compat-harness/src/main.rs @@ -227,11 +227,9 @@ fn corpus() -> Result { let cancellation = CancellationPolicy::new(); cancellation.cancel_after_checkpoints(1); InProcessRustAdapter::parse(polymodel_ldraw_core::AdapterRequest { - fixture_id: fixture.id, bytes: fixture.bytes, profile: core_profile(fixture.profile), provenance_id: fixture.id, - semantic_budget: Some(16 * 1024 * 1024), root_name: fixture.id, root: RootId::UploadedManifest, materializations: &[], @@ -763,11 +761,9 @@ fn parse_fixture_with_root( fixture: &polymodel_ldraw_testkit::FixtureCase, ) -> Result { InProcessRustAdapter::parse(AdapterRequest { - fixture_id: fixture.id, bytes: fixture.bytes, profile: core_profile(fixture.profile), provenance_id: fixture.id, - semantic_budget: Some(16 * 1024 * 1024), root_name: fixture_root_name(fixture), root: RootId::UploadedManifest, materializations: &[],