From f720e40ba1efc1b6bfeb1b76771455558f6cdabd Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 17 Jul 2026 12:53:42 +0300 Subject: [PATCH] feat(render): draw a proxy entity's cached preview instead of nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An entity from an application we have no reader for — e.g. an Autodesk Raster Design embedded raster image — now arrives as `Unknown` (acadrust 769741d) and rendered as nothing at all, so the drawing silently lost it with no hint that anything was there. Such entities almost always ship proxy graphics: the vector preview their author cached for precisely this case, and what AutoCAD itself falls back to when the object enabler is absent. Decode it and draw it, so the entity holds its real place and size. The record grammar has no reference implementation to copy — LibreDWG keeps the blob raw ("see par 29") and ACadSharp stores the bytes without parsing them — so rather than infer a general decoder from one sample, `proxy_graphics` accepts a single polyline record and only when the blob matches it byte-for-byte: sizes must agree, the points must consume every remaining byte, and coordinates must be finite. Anything else returns None and draws nothing, exactly as before, so a file this does not model cannot regress or sprout invented geometry. Verified on a Raster Design drawing: its 140-byte preview decodes with 0 bytes left over to the image frame (1192.149 x 877.824, closed), matching what ODA reports for the same entity. Unit-tested against that real blob. Co-Authored-By: Claude Opus 4.8 --- src/scene/convert/mod.rs | 1 + src/scene/convert/proxy_graphics.rs | 111 ++++++++++++++++++++++++++++ src/scene/convert/tess.rs | 21 ++++++ src/scene/convert/tessellate.rs | 2 +- 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/scene/convert/proxy_graphics.rs diff --git a/src/scene/convert/mod.rs b/src/scene/convert/mod.rs index 2b27969b8..cfd4e59ad 100644 --- a/src/scene/convert/mod.rs +++ b/src/scene/convert/mod.rs @@ -22,6 +22,7 @@ pub mod dgn_linestyle; pub mod truck_tess; pub mod tessellate; pub(crate) mod tess; +pub mod proxy_graphics; pub mod tess_util; pub mod solid3d_tess; pub mod spline_tess; diff --git a/src/scene/convert/proxy_graphics.rs b/src/scene/convert/proxy_graphics.rs new file mode 100644 index 000000000..86644f9a4 --- /dev/null +++ b/src/scene/convert/proxy_graphics.rs @@ -0,0 +1,111 @@ +//! Proxy entity graphics — the cached vector preview an application stores +//! alongside a custom entity so viewers without its object enabler can still +//! draw something. AutoCAD falls back to exactly this; so do we. +//! +//! No reference decoder exists to copy: LibreDWG keeps the blob raw and points +//! at the spec ("see par 29"), ACadSharp likewise stores `ProxyGraphics` bytes +//! and never parses them. The full record grammar is therefore unverified here, +//! and guessing at it from a sample is how you end up misreading every file. +//! +//! So this decodes exactly one shape — a single polyline record — and only when +//! the blob matches it byte-for-byte. Anything else returns `None` and draws +//! nothing, which is what happened before this existed: no regression, no +//! invented geometry. + +/// A closed/open polyline lifted from a proxy-graphics blob (world coords). +pub struct ProxyPolyline { + pub points: Vec<[f64; 3]>, +} + +/// Record type for a polyline entry. The only one accepted. +const ENTRY_POLYLINE: u32 = 6; + +/// Layout accepted (little-endian), validated in full before anything is +/// emitted: +/// +/// ```text +/// u32 total_size == blob.len() +/// u32 entry_count == 1 +/// u32 payload_size == blob.len() - 8 +/// u32 entry_type == 6 (polyline) +/// u32 point_count +/// [f64; 3] * point_count +/// ``` +/// +/// Verified against an Autodesk Raster Design embedded raster image, whose +/// 140-byte preview decodes to its image frame with 0 bytes left over. +pub fn decode_polyline(blob: &[u8]) -> Option { + let u32_at = |o: usize| -> Option { + blob.get(o..o + 4) + .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]])) + }; + + if blob.len() < 20 || u32_at(0)? as usize != blob.len() || u32_at(4)? != 1 { + return None; + } + if u32_at(8)? as usize != blob.len() - 8 || u32_at(12)? != ENTRY_POLYLINE { + return None; + } + let n = u32_at(16)? as usize; + // The points must account for every remaining byte — a short read would + // mean the record carries something this decoder does not model. + if n < 2 || 20usize.checked_add(n.checked_mul(24)?)? != blob.len() { + return None; + } + + let mut points = Vec::with_capacity(n); + for i in 0..n { + let o = 20 + i * 24; + let f = |k: usize| -> f64 { + let b = &blob[o + k * 8..o + k * 8 + 8]; + f64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]) + }; + let (x, y, z) = (f(0), f(1), f(2)); + if !x.is_finite() || !y.is_finite() || !z.is_finite() { + return None; + } + points.push([x, y, z]); + } + Some(ProxyPolyline { points }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The real 140-byte preview of an Autodesk Raster Design embedded raster + /// image: its frame, 1192.149 x 877.824, closed back to the origin. + #[test] + fn decodes_the_raster_design_image_frame() { + let hex = "8C00000001000000840000000600000005000000\ + 000000000000000000000000000000000000000000000000\ + DEF97E6A1C422E3D3DDF4F8D976E8B400000000000000000\ + 6BBC749398A092403DDF4F8D976E8B400000000000000000\ + 6BBC7493 98A09240 0000000000000000 0000000000000000\ + 000000000000000000000000000000000000000000000000"; + let blob: Vec = hex + .split_whitespace() + .collect::() + .as_bytes() + .chunks(2) + .map(|c| u8::from_str_radix(std::str::from_utf8(c).unwrap(), 16).unwrap()) + .collect(); + assert_eq!(blob.len(), 140); + + let p = decode_polyline(&blob).expect("frame decodes"); + assert_eq!(p.points.len(), 5); + assert!((p.points[2][0] - 1192.1490).abs() < 1e-3); + assert!((p.points[2][1] - 877.8240).abs() < 1e-3); + // Closed: last point returns to the first. + assert_eq!(p.points[0], p.points[4]); + } + + #[test] + fn rejects_anything_it_does_not_model() { + assert!(decode_polyline(&[]).is_none()); + assert!(decode_polyline(&[0u8; 140]).is_none()); // size field wrong + let mut b = vec![0u8; 140]; + b[0] = 140; // right size, wrong everything else + assert!(decode_polyline(&b).is_none()); + } +} diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 3f3c4e82b..e987eff73 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -228,6 +228,27 @@ pub(crate) fn tessellate_entity( let pattern_length = pattern_length * pslt_factor; let pattern = pattern.map(|v| v * pslt_factor); + // ── Proxy entity: draw its cached preview ─────────────────────────────── + // + // An entity from an application we have no reader for (e.g. an Autodesk + // Raster Design embedded raster image) arrives as `Unknown`. Its own data is + // a private format we cannot decode — but it usually ships a proxy-graphics + // blob, the vector preview its author cached for exactly this case. AutoCAD + // draws that when the object enabler is missing; draw it too, so the entity + // occupies its real place instead of silently disappearing. + if let EntityType::Unknown(_) = e { + if let Some(blob) = e.common().graphic_data.as_ref() { + if let Some(p) = convert::proxy_graphics::decode_polyline(blob) { + let (pts, pts_low) = convert::tessellate::points_to_ds(p.points); + let mut w = WireModel::solid(h.value().to_string(), pts, entity_color, sel); + w.points_low = pts_low; + w.line_weight_px = line_weight_px; + w.aci = aci; + return vec![w]; + } + } + } + // ── Dimension baked-block fast path ───────────────────────────────────── // // AutoCAD bakes each dimension's final geometry (extension lines, dim diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 78979783e..06fd12bdc 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -46,7 +46,7 @@ fn split_ds_xyz(x: f64, y: f64, z: f64) -> ([f32; 3], [f32; 3]) { /// Split each absolute f64 source point into double-single (high, low) f32 /// buffers in one pass — the relative-to-eye residual the GPU/CPU reconstruct /// to f64 precision at UTM-scale coordinates. -fn points_to_ds( +pub(crate) fn points_to_ds( src: impl IntoIterator, ) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) { let it = src.into_iter();