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 Seven 2026-04-06 23:19:08 +03:00
commit 4312b6c3a5
39 changed files with 702 additions and 15 deletions

View file

@ -410,6 +410,10 @@ impl H7CAD {
self.tabs[i].active_cmd = Some(Box::new(wo_cmd));
}
cmd if cmd == "IMAGE" || cmd == "IMAGEATTACH" || cmd == "IM" => {
return Task::done(Message::ImagePick);
}
"REVCLOUD" => {
use crate::modules::home::draw::revcloud::RevCloudCommand;
let cmd = RevCloudCommand::new();

View file

@ -35,8 +35,10 @@ impl H7CAD {
.filter(|h| self.tabs[i].scene.document.get_entity(*h).is_some())
.collect::<HashSet<_>>();
self.tabs[i].scene.populate_hatches_from_document();
self.tabs[i].scene.populate_images_from_document();
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].scene.meshes.clear();
self.tabs[i].scene.images.clear();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].active_grip = None;

View file

@ -397,6 +397,11 @@ pub enum Message {
// Field edit messages:
DsEdit(DsField, String),
DsToggle(DsField),
// ── Raster Image ──────────────────────────────────────────────────────
/// Open file-picker dialog for IMAGE command (async).
ImagePick,
/// Result of the image file picker + pixel dimension decode.
ImagePickResult(Result<(std::path::PathBuf, u32, u32), String>),
}
impl H7CAD {

View file

@ -49,6 +49,7 @@ impl H7CAD {
self.tabs[i].current_path = Some(path);
self.tabs[i].scene.document = doc;
self.tabs[i].scene.populate_hatches_from_document();
self.tabs[i].scene.populate_images_from_document();
self.tabs[i].scene.selected = std::collections::HashSet::new();
self.tabs[i].scene.preview_wires = vec![];
self.tabs[i].scene.current_layout = "Model".to_string();
@ -71,6 +72,35 @@ impl H7CAD {
Task::none()
}
Message::ImagePick => {
Task::perform(crate::io::pick_image_file(), Message::ImagePickResult)
}
Message::ImagePickResult(Ok((path, pw, ph))) => {
use crate::command::CadCommand;
use crate::modules::home::draw::raster_image::ImageCommand;
let path_str = path.to_string_lossy().into_owned();
let short = std::path::Path::new(&path_str)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&path_str)
.to_string();
self.command_line
.push_output(&format!("IMAGE \"{short}\": {pw}×{ph} px"));
let cmd = ImageCommand::new(path_str, pw, ph);
let i = self.active_tab;
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
Task::none()
}
Message::ImagePickResult(Err(e)) => {
if e != "Cancelled" {
self.command_line.push_error(&format!("IMAGE: {e}"));
}
Task::none()
}
Message::SaveFile => {
let i = self.active_tab;
if let Some(path) = &self.tabs[i].current_path {