2026-04-06 23:19:08 +03:00
|
|
|
// ImageModel — CPU-side data for a raster image quad.
|
|
|
|
|
//
|
|
|
|
|
// Holds decoded RGBA pixel data and the world-space quad geometry derived
|
|
|
|
|
// from the RasterImage entity's insertion point, u/v vectors, and pixel size.
|
|
|
|
|
|
|
|
|
|
use std::path::Path;
|
2026-04-26 11:34:52 +03:00
|
|
|
use std::sync::Arc;
|
2026-04-06 23:19:08 +03:00
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct ImageModel {
|
|
|
|
|
/// Original file path (used for reload / display in properties).
|
|
|
|
|
pub file_path: String,
|
2026-04-26 11:34:52 +03:00
|
|
|
/// RGBA8 pixel data in row-major order. Arc-wrapped so cloning ImageModel
|
|
|
|
|
/// is O(1) — the pixel bytes are shared, not copied.
|
|
|
|
|
pub pixels: Arc<Vec<u8>>,
|
2026-04-06 23:19:08 +03:00
|
|
|
pub width: u32,
|
|
|
|
|
pub height: u32,
|
|
|
|
|
/// Opacity: 1.0 = opaque, 0.0 = transparent.
|
|
|
|
|
pub opacity: f32,
|
|
|
|
|
/// World-space quad corners (CCW), same order as image_corners() helper:
|
|
|
|
|
/// [0] origin (bottom-left)
|
|
|
|
|
/// [1] origin + U*W (bottom-right)
|
|
|
|
|
/// [2] origin + U*W + V*H (top-right)
|
|
|
|
|
/// [3] origin + V*H (top-left)
|
|
|
|
|
pub corners: [[f32; 3]; 4],
|
2026-06-23 17:56:40 +03:00
|
|
|
/// Low residual paired with `corners` (double-single) so the GPU keeps
|
|
|
|
|
/// sub-unit precision at UTM-scale insertion points.
|
|
|
|
|
pub corners_low: [[f32; 3]; 4],
|
2026-05-15 23:13:16 +03:00
|
|
|
/// Optional world-space XY rect [x0, y0, x1, y1] for paper-space
|
|
|
|
|
/// viewport clipping. Mirrors `WireModel.vp_scissor` /
|
|
|
|
|
/// `HatchModel.vp_scissor`.
|
|
|
|
|
pub vp_scissor: Option<[f32; 4]>,
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
/// Normalized draw-order depth in (0,1); higher draws on top. Fed to the
|
|
|
|
|
/// image pipeline as a small clip-z bias so the raster orders correctly
|
|
|
|
|
/// against other entity types.
|
|
|
|
|
pub draw_depth: f32,
|
2026-04-06 23:19:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ImageModel {
|
|
|
|
|
/// Build an ImageModel from a DXF RasterImage entity.
|
|
|
|
|
/// Returns `None` if the image file cannot be opened or decoded.
|
2026-06-14 15:06:06 +03:00
|
|
|
pub fn from_raster_image(
|
|
|
|
|
img: &acadrust::entities::RasterImage,
|
|
|
|
|
world_offset: [f64; 3],
|
|
|
|
|
) -> Option<Self> {
|
2026-04-06 23:19:08 +03:00
|
|
|
let w = img.size.x;
|
|
|
|
|
let h = img.size.y;
|
2026-06-14 15:06:06 +03:00
|
|
|
// Model-space geometry is drawn in (WCS - world_offset) so large UTM-
|
|
|
|
|
// scale coordinates stay within f32 precision; offset the image too.
|
2026-06-23 17:56:40 +03:00
|
|
|
// Corners come from a large insertion point plus small u/v spans.
|
|
|
|
|
// Split each into double-single (high, low) f32 so the GPU keeps
|
|
|
|
|
// sub-unit precision at UTM scale and after a cross-drawing paste.
|
|
|
|
|
let oxv = img.insertion_point.x - world_offset[0];
|
|
|
|
|
let oyv = img.insertion_point.y - world_offset[1];
|
|
|
|
|
let ozv = img.insertion_point.z - world_offset[2];
|
2026-04-06 23:19:08 +03:00
|
|
|
let ux = (img.u_vector.x * w) as f32;
|
|
|
|
|
let uy = (img.u_vector.y * w) as f32;
|
|
|
|
|
let uz = (img.u_vector.z * w) as f32;
|
|
|
|
|
let vx = (img.v_vector.x * h) as f32;
|
|
|
|
|
let vy = (img.v_vector.y * h) as f32;
|
|
|
|
|
let vz = (img.v_vector.z * h) as f32;
|
2026-06-23 17:56:40 +03:00
|
|
|
// High/low split of the anchor; the u/v spans are small and added to
|
|
|
|
|
// the high half (their own residual is below f32 noise at this scale).
|
|
|
|
|
let ox = oxv as f32;
|
|
|
|
|
let oy = oyv as f32;
|
|
|
|
|
let oz = ozv as f32;
|
|
|
|
|
let oxl = (oxv - ox as f64) as f32;
|
|
|
|
|
let oyl = (oyv - oy as f64) as f32;
|
|
|
|
|
let ozl = (ozv - oz as f64) as f32;
|
2026-04-06 23:19:08 +03:00
|
|
|
let corners = [
|
|
|
|
|
[ox, oy, oz],
|
|
|
|
|
[ox + ux, oy + uy, oz + uz],
|
|
|
|
|
[ox + ux + vx, oy + uy + vy, oz + uz + vz],
|
|
|
|
|
[ox + vx, oy + vy, oz + vz],
|
|
|
|
|
];
|
2026-06-23 17:56:40 +03:00
|
|
|
let corners_low = [[oxl, oyl, ozl]; 4];
|
2026-04-06 23:19:08 +03:00
|
|
|
let opacity = 1.0 - img.fade as f32 / 100.0;
|
|
|
|
|
|
|
|
|
|
let (pixels, width, height) = load_pixels(&img.file_path)?;
|
2026-05-12 10:36:15 +03:00
|
|
|
Some(Self {
|
|
|
|
|
file_path: img.file_path.clone(),
|
|
|
|
|
pixels: Arc::new(pixels),
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
opacity,
|
|
|
|
|
corners,
|
2026-06-23 17:56:40 +03:00
|
|
|
corners_low,
|
2026-05-15 23:13:16 +03:00
|
|
|
vp_scissor: None,
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
draw_depth: 0.0,
|
2026-05-12 10:36:15 +03:00
|
|
|
})
|
2026-04-06 23:19:08 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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)> {
|
|
|
|
|
let img = image::open(Path::new(path_str)).ok()?;
|
2026-06-14 15:06:06 +03:00
|
|
|
// GPUs cap 2-D texture dimensions (8192 with wgpu's default limits).
|
|
|
|
|
// Downscale oversized images to fit, preserving aspect ratio, so texture
|
|
|
|
|
// creation can't fail — they're displayed scaled-down anyway.
|
|
|
|
|
const MAX_DIM: u32 = 8192;
|
|
|
|
|
let img = if img.width() > MAX_DIM || img.height() > MAX_DIM {
|
|
|
|
|
let longest = img.width().max(img.height()) as f32;
|
|
|
|
|
let scale = MAX_DIM as f32 / longest;
|
|
|
|
|
let nw = ((img.width() as f32 * scale) as u32).clamp(1, MAX_DIM);
|
|
|
|
|
let nh = ((img.height() as f32 * scale) as u32).clamp(1, MAX_DIM);
|
|
|
|
|
img.resize(nw, nh, image::imageops::FilterType::Triangle)
|
|
|
|
|
} else {
|
|
|
|
|
img
|
|
|
|
|
};
|
2026-04-06 23:19:08 +03:00
|
|
|
let rgba = img.to_rgba8();
|
|
|
|
|
let (w, h) = rgba.dimensions();
|
|
|
|
|
Some((rgba.into_raw(), w, h))
|
|
|
|
|
}
|