diff --git a/src/entities/ole2frame.rs b/src/entities/ole2frame.rs index 3761de581..c4324860d 100644 --- a/src/entities/ole2frame.rs +++ b/src/entities/ole2frame.rs @@ -28,8 +28,10 @@ fn to_truck(ole: &Ole2Frame) -> TruckEntity { let cx = (x0 + x1) * 0.5; let cy = (y0 + y1) * 0.5; + // Frame border only — the embedded presentation bitmap is drawn inside the + // rectangle by the image pass (see `ImageModel::from_ole2frame`). The old + // diagonal-X placeholder would have crossed over that image. let pts: Vec<[f64; 3]> = vec![ - // Rectangle [x0, y0, z], [x1, y0, z], [x1, y0, z], @@ -38,12 +40,6 @@ fn to_truck(ole: &Ole2Frame) -> TruckEntity { [x0, y1, z], [x0, y1, z], [x0, y0, z], - // Diagonal X - [x0, y0, z], - [x1, y1, z], - [f64::NAN; 3], - [x1, y0, z], - [x0, y1, z], ]; let center = glam::DVec3::new(cx, cy, z); TruckEntity { @@ -171,9 +167,7 @@ impl TruckConvertible for Ole2Frame { crate::impl_entity_basics!(Ole2Frame); impl crate::entities::traits::FallbackTess for Ole2Frame { - fn fallback_geometry( - &self, - ) -> crate::scene::convert::tess_util::FallbackGeometry { + fn fallback_geometry(&self) -> crate::scene::convert::tess_util::FallbackGeometry { // OLE objects carry a bounding rectangle in model space. // Render a simple X-through-rectangle placeholder. let x0 = self.upper_left_corner.x; @@ -187,7 +181,7 @@ impl crate::entities::traits::FallbackTess for Ole2Frame { return (vec![[-s, 0.0, 0.0], [s, 0.0, 0.0]], vec![], vec![], vec![]); } let pts = vec![ - // Outer rectangle + // Frame border only; the presentation bitmap fills the rectangle. [x0, y0, z], [x1, y0, z], [x1, y0, z], @@ -196,12 +190,6 @@ impl crate::entities::traits::FallbackTess for Ole2Frame { [x0, y1, z], [x0, y1, z], [x0, y0, z], - // Diagonal X - [x0, y0, z], - [x1, y1, z], - [f64::NAN, f64::NAN, f64::NAN], - [x1, y0, z], - [x0, y1, z], ]; (pts, vec![], vec![], vec![[x0, y0, z], [x1, y1, z]]) } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 83580e7cf..91b1153b5 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -289,7 +289,7 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches { let h = e.common().handle; match e { EntityType::Hatch(_) | EntityType::Solid(_) => hatch_handles.push(h), - EntityType::RasterImage(_) => image_handles.push(h), + EntityType::RasterImage(_) | EntityType::Ole2Frame(_) => image_handles.push(h), EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) => { mesh_handles.push(h) } @@ -326,12 +326,14 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches { // images let images: HashMap = image_handles .par_iter() - .filter_map(|&handle| { - if let EntityType::RasterImage(img) = doc.get_entity(handle)? { + .filter_map(|&handle| match doc.get_entity(handle)? { + EntityType::RasterImage(img) => { ImageModel::from_raster_image(img).map(|m| (handle, m)) - } else { - None } + EntityType::Ole2Frame(ole) => { + ImageModel::from_ole2frame(ole).map(|m| (handle, m)) + } + _ => None, }) .collect(); diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index a23780bb8..f0678be39 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -90,6 +90,93 @@ impl ImageModel { } } +impl ImageModel { + /// Build an ImageModel from an OLE2FRAME's embedded presentation bitmap. + /// Returns `None` when the frame is degenerate or carries no decodable + /// bitmap (e.g. a metafile-only OLE), so the caller falls back to the frame + /// placeholder. + pub fn from_ole2frame(ole: &acadrust::entities::Ole2Frame) -> Option { + let (pixels, width, height) = decode_ole_bitmap(&ole.binary_data)?; + + // Frame rectangle in WCS. `upper_left`/`lower_right` name the diagonal; + // normalise to left/right/top/bottom so the bitmap sits upright. + let left = ole.upper_left_corner.x.min(ole.lower_right_corner.x); + let right = ole.upper_left_corner.x.max(ole.lower_right_corner.x); + let bottom = ole.upper_left_corner.y.min(ole.lower_right_corner.y); + let top = ole.upper_left_corner.y.max(ole.lower_right_corner.y); + let z = ole.upper_left_corner.z; + if (right - left).abs() < 1e-9 || (top - bottom).abs() < 1e-9 { + return None; + } + + // Double-single split per corner so the quad stays precise at UTM scale. + let split = |x: f64, y: f64| -> ([f32; 3], [f32; 3]) { + let (hx, hy, hz) = (x as f32, y as f32, z as f32); + ( + [hx, hy, hz], + [ + (x - hx as f64) as f32, + (y - hy as f64) as f32, + (z - hz as f64) as f32, + ], + ) + }; + // corners: [BL, BR, TR, TL] — the image pipeline maps texel (0,0) to TL. + let (c0, l0) = split(left, bottom); + let (c1, l1) = split(right, bottom); + let (c2, l2) = split(right, top); + let (c3, l3) = split(left, top); + + Some(Self { + file_path: "OLE2FRAME".to_string(), + pixels: Arc::new(pixels), + width, + height, + opacity: 1.0, + corners: [c0, c1, c2, c3], + corners_low: [l0, l1, l2, l3], + vp_scissor: None, + draw_depth: 0.0, + }) + } +} + +/// Extract and decode the presentation BMP embedded in an OLE2FRAME data blob. +/// The blob carries the OLE object's cached bitmap (a `BITMAPFILEHEADER` "BM" +/// followed by a DIB); scan for a self-consistent one and decode it. Returns +/// `None` when no valid BMP is present (a metafile/other-format OLE). +fn decode_ole_bitmap(data: &[u8]) -> Option<(Vec, u32, u32)> { + let mut i = 0usize; + while i + 54 <= data.len() { + if &data[i..i + 2] != b"BM" { + i += 1; + continue; + } + let file_size = u32::from_le_bytes(data[i + 2..i + 6].try_into().unwrap()) as usize; + let dib_size = u32::from_le_bytes(data[i + 14..i + 18].try_into().unwrap()); + // A plausible BITMAPFILEHEADER points at a known DIB-header size. + if !matches!(dib_size, 12 | 40 | 52 | 56 | 64 | 108 | 124) { + i += 1; + continue; + } + let end = if file_size >= 54 && i + file_size <= data.len() { + i + file_size + } else { + data.len() + }; + if let Ok(img) = image::load_from_memory_with_format(&data[i..end], image::ImageFormat::Bmp) + { + let rgba = img.to_rgba8(); + let (w, h) = rgba.dimensions(); + if w > 0 && h > 0 { + return Some((rgba.into_raw(), w, h)); + } + } + i += 1; + } + None +} + /// Decode a raster image file into RGBA8 pixels. /// Returns `None` if the file does not exist or cannot be decoded. pub fn load_pixels(path_str: &str) -> Option<(Vec, u32, u32)> {