feat(underlay): render PDF underlays end-to-end

PDF underlays only drew their clip outline / insertion cross; the page
itself never appeared. The chain now runs: the definition's file path
resolves with the raster-image fallbacks (as stored, relative to the
drawing, basename next to the drawing — attach-time relative paths go
stale), the page rasterises through the system pdftoppm (poppler) at
150 dpi — no new crate; missing tool degrades to the outline — cached
per (path, page) including negative results, and the pixels feed the
existing image pipeline as an ImageModel: 1 underlay unit = 1 PDF inch
(px/dpi × entity scale), rotated about the insertion, fade as opacity,
the clip polygon (non-inverted) as the textured region. Draw-order,
layer visibility and per-viewport freezes ride the generic image path;
in-place edits rebuild through the shared image seed.

When the page renders, the wire path draws its frame with an interior
pick surface instead of the lone cross, so selection covers the visible
page. wasm keeps the outline placeholder (no external process).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-23 03:41:41 +03:00
1 parent ac0a6ac150
commit 1e913e9e78
7 files changed
+322 -16

No files matched your search

+18 -15
View File
@@ -129,11 +129,7 @@ impl Scene {
} else {
None
};
let image_seed = if let EntityType::RasterImage(img) = &entity {
ImageModel::from_raster_image(img)
} else {
None
};
let image_seed = self.image_seed_for(&entity);
let facet_res = self.document.header.facet_resolution;
let isolines = self.document.header.isolines.max(0) as usize;
let mesh_seed = if matches!(
@@ -321,11 +317,7 @@ impl Scene {
} else {
None
};
let image_seed = if let EntityType::RasterImage(img) = &entity {
ImageModel::from_raster_image(img)
} else {
None
};
let image_seed = self.image_seed_for(&entity);
let facet_res = self.document.header.facet_resolution;
let isolines = self.document.header.isolines.max(0) as usize;
let mesh_seed = if matches!(
@@ -402,11 +394,7 @@ impl Scene {
} else {
None
};
let image_seed = if let EntityType::RasterImage(img) = entity {
ImageModel::from_raster_image(img)
} else {
None
};
let image_seed = self.image_seed_for(entity);
let facet_res = self.document.header.facet_resolution;
let isolines = self.document.header.isolines.max(0) as usize;
let mesh_seed = if matches!(
@@ -1716,6 +1704,21 @@ impl Scene {
///
/// Called after loading a document or after undo/redo so that every
/// `Solid3D` entity is represented in the mesh cache.
/// ImageModel for an edited image-bearing entity (RasterImage, or a PDF
/// UNDERLAY resolved through its definition object). `None` for others.
fn image_seed_for(&self, entity: &acadrust::entities::EntityType) -> Option<ImageModel> {
match entity {
EntityType::RasterImage(img) => ImageModel::from_raster_image(img),
EntityType::Underlay(u) => match self.document.objects.get(&u.definition_handle) {
Some(acadrust::objects::ObjectType::UnderlayDefinition(def)) => {
ImageModel::from_underlay(u, def)
}
_ => None,
},
_ => None,
}
}
pub fn populate_meshes_from_document(&mut self) {
self.populate_meshes_impl(false);
}