forked from 1b-agent/opencadstudio-upstream
feat(ole): render the OLE2FRAME embedded presentation bitmap
An OLE2FRAME drew only a rectangle-with-X placeholder. AutoCAD shows the OLE object's cached presentation image, which the DWG stores as a BMP embedded in the OLE data blob (a BITMAPFILEHEADER "BM" + DIB). `ImageModel::from_ole2frame` scans the blob for a self-consistent embedded BMP (validating the DIB-header size, so a metafile/other-format OLE cleanly falls back), decodes it with the `image` crate, and builds the textured quad at the frame rectangle with a per-corner double-single split so it stays precise at UTM scale. The scene's image collection now feeds OLE2FRAME entities through it alongside RasterImage. The placeholder's diagonal X is dropped — only the frame border is drawn now, so it no longer crosses over the bitmap. Verified on budweiser handle 3088: the 32×32 embedded bitmap decodes to RGBA with the expected colours (yellow upper-left, blue centre, red lower-right), upright and positioned at the frame corners. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e127ffa75
commit
b655bd232b
3 files changed
+99
-22
No files matched your search
@@ -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]])
|
||||
}
|
||||
|
||||
+7
-5
@@ -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<Handle, ImageModel> = 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();
|
||||
|
||||
|
||||
@@ -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<Self> {
|
||||
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<u8>, 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<u8>, u32, u32)> {
|
||||
|
||||
Reference in new issue
Block a user