feat: raster image render pipeline (IMAGE command + GPU textured quad)

- Add IMAGE / IMAGEATTACH / IM command with async file-picker (rfd)
- src/scene/image_model.rs: ImageModel with RGBA pixel data + quad corners
- src/scene/pipeline/image_gpu.rs: ImageGpu with wgpu texture, sampler, opacity uniform
- src/scene/pipeline/mod.rs: image render pipeline (pass 2, between hatches and meshes)
- src/scene/render.rs: include images in Primitive, call upload_images in prepare
- src/scene/mod.rs: images HashMap, populate_images_from_document, cache on add_entity
- src/modules/home/draw/raster_image.rs: ImageCommand (two-click: origin + width, aspect-correct preview)
- src/io/mod.rs: pick_image_file() async helper (file dialog + dimension decode)
- app: ImagePick / ImagePickResult messages, populate on file open and undo/redo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-04-06 23:19:08 +03:00
1 parent 3d1f355a5c
commit 4312b6c3a5
39 files changed
+702 -15

No files matched your search

+3 -1
View File
@@ -87,6 +87,7 @@ impl CadCommand for AlignedDimensionCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
@@ -119,6 +120,7 @@ fn preview_aligned(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> WireModel {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
}
}
+2 -1
View File
@@ -122,7 +122,8 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
}
}
+2 -1
View File
@@ -86,6 +86,7 @@ fn preview_line(a: Vec3, b: Vec3) -> WireModel {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
}
}
+1
View File
@@ -128,6 +128,7 @@ impl CadCommand for DimBaselineCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
+1
View File
@@ -125,6 +125,7 @@ impl CadCommand for DimContinueCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
+2 -1
View File
@@ -199,7 +199,8 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
}
}
+2 -1
View File
@@ -110,7 +110,8 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
}
}
+2 -1
View File
@@ -152,7 +152,8 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
}
}
+2 -1
View File
@@ -109,6 +109,7 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
}
}
+2 -1
View File
@@ -130,7 +130,8 @@ impl CadCommand for TableCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
})
} else {
None
+1
View File
@@ -83,6 +83,7 @@ impl CadCommand for ToleranceCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
+1
View File
@@ -131,6 +131,7 @@ impl CadCommand for AttdefCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
+2 -1
View File
@@ -524,6 +524,7 @@ fn line_wire(from: Vec3, to: Vec3) -> WireModel {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
}
}
+1
View File
@@ -85,6 +85,7 @@ impl CadCommand for LineCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
+1
View File
@@ -8,6 +8,7 @@ pub mod line;
pub mod mline;
pub mod point;
pub mod polyline;
pub mod raster_image;
pub mod ray;
pub mod revcloud;
pub mod shapes;
+123
View File
@@ -0,0 +1,123 @@
// IMAGE / IMAGEATTACH command — place a raster image in the drawing.
//
// Workflow:
// 1. File dialog opens (async, handled in update.rs).
// 2. User picks insertion point (first click).
// 3. User drags to pick width; height is computed from the image's aspect ratio.
// 4. Entity is committed.
use acadrust::entities::RasterImage;
use acadrust::types::Vector3;
use acadrust::EntityType;
use glam::Vec3;
use crate::command::{CadCommand, CmdResult};
use crate::scene::wire_model::WireModel;
pub struct ImageCommand {
file_path: String,
pixel_width: u32,
pixel_height: u32,
origin: Option<Vec3>,
}
impl ImageCommand {
pub fn new(file_path: String, pixel_width: u32, pixel_height: u32) -> Self {
Self { file_path, pixel_width, pixel_height, origin: None }
}
fn aspect(&self) -> f64 {
if self.pixel_height == 0 {
1.0
} else {
self.pixel_width as f64 / self.pixel_height as f64
}
}
fn make_entity(&self, origin: Vec3, width_pt: Vec3) -> EntityType {
let world_width = ((width_pt.x - origin.x) as f64).abs().max(0.001);
let world_height = world_width / self.aspect();
let ins = Vector3::new(origin.x as f64, origin.z as f64, origin.y as f64);
let mut img = RasterImage::with_size(
&self.file_path,
ins,
self.pixel_width as f64,
self.pixel_height as f64,
world_width,
world_height,
);
img.flags = acadrust::entities::ImageDisplayFlags::SHOW_IMAGE
| acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
EntityType::RasterImage(img)
}
}
impl CadCommand for ImageCommand {
fn name(&self) -> &'static str {
"IMAGE"
}
fn prompt(&self) -> String {
if self.origin.is_none() {
format!("IMAGE Specify insertion point ({}): ", short_name(&self.file_path))
} else {
"IMAGE Specify width (drag right):".into()
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
if let Some(origin) = self.origin {
let entity = self.make_entity(origin, pt);
CmdResult::CommitAndExit(entity)
} else {
self.origin = Some(pt);
CmdResult::NeedPoint
}
}
fn on_enter(&mut self) -> CmdResult {
// If origin is set, place with a default width of 1 unit * pixel count / 100
if let Some(origin) = self.origin {
let default_w = (self.pixel_width as f64 / 100.0).max(1.0);
let width_pt = Vec3::new(origin.x + default_w as f32, origin.y, origin.z);
let entity = self.make_entity(origin, width_pt);
CmdResult::CommitAndExit(entity)
} else {
CmdResult::Cancel
}
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
let origin = self.origin?;
let world_width = (pt.x - origin.x).abs().max(0.001);
let world_height = world_width / self.aspect() as f32;
let p0 = [origin.x, origin.y, origin.z];
let p1 = [origin.x + world_width, origin.y, origin.z];
let p2 = [origin.x + world_width, origin.y + world_height, origin.z];
let p3 = [origin.x, origin.y + world_height, origin.z];
Some(WireModel {
name: "image_preview".into(),
points: vec![p0, p1, p2, p3, p0],
color: WireModel::CYAN,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
}
fn short_name(path: &str) -> &str {
std::path::Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path)
}
+2
View File
@@ -83,6 +83,7 @@ impl CadCommand for RayCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
@@ -159,6 +160,7 @@ impl CadCommand for XLineCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
+1
View File
@@ -68,6 +68,7 @@ impl CadCommand for RevCloudCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
+4 -2
View File
@@ -104,7 +104,8 @@ impl CadCommand for WipeoutCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
})
}
WipeoutMode::Polygonal => {
@@ -123,7 +124,8 @@ impl CadCommand for WipeoutCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
})
}
}
+1
View File
@@ -73,6 +73,7 @@ impl CadCommand for AreaCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
+1
View File
@@ -68,6 +68,7 @@ impl CadCommand for DistCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
+2 -1
View File
@@ -100,7 +100,8 @@ impl CadCommand for StretchCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
aci: 0,
key_vertices: vec![],
})
} else {
None
+1
View File
@@ -95,6 +95,7 @@ impl CadCommand for MviewCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
+1
View File
@@ -60,6 +60,7 @@ impl CadCommand for PlotWindowCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}
+1
View File
@@ -66,6 +66,7 @@ impl CadCommand for ZoomWindowCommand {
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
})
}