forked from 1b-agent/opencadstudio-upstream
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:
1 parent
3d1f355a5c
commit
4312b6c3a5
39 files changed
+702
-15
No files matched your search
@@ -0,0 +1,169 @@
|
||||
// Image GPU buffers — renders raster images as textured quads.
|
||||
//
|
||||
// Group 1 bindings per image:
|
||||
// binding 0 — texture_2d<f32> (RGBA image texture)
|
||||
// binding 1 — sampler (bilinear filtering)
|
||||
// binding 2 — ImageParams (opacity uniform, 16 bytes)
|
||||
|
||||
use crate::scene::image_model::ImageModel;
|
||||
use iced::wgpu;
|
||||
use iced::wgpu::util::DeviceExt;
|
||||
|
||||
// ── Vertex ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct ImageVertex {
|
||||
pub pos: [f32; 3],
|
||||
pub uv: [f32; 2],
|
||||
}
|
||||
|
||||
impl ImageVertex {
|
||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<ImageVertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: 12,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x2,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Uniform ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct ImageParams {
|
||||
opacity: f32,
|
||||
_pad: [f32; 3],
|
||||
} // 16 bytes
|
||||
|
||||
// ── Per-image GPU handle ──────────────────────────────────────────────────
|
||||
|
||||
pub struct ImageGpu {
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
pub bind_group: wgpu::BindGroup,
|
||||
_texture: wgpu::Texture,
|
||||
_sampler: wgpu::Sampler,
|
||||
_params_buf: wgpu::Buffer,
|
||||
}
|
||||
|
||||
impl ImageGpu {
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
model: &ImageModel,
|
||||
bgl1: &wgpu::BindGroupLayout,
|
||||
) -> Option<Self> {
|
||||
if model.pixels.is_empty() || model.width == 0 || model.height == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// ── Upload texture ────────────────────────────────────────────────
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("image.texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: model.width,
|
||||
height: model.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
queue.write_texture(
|
||||
texture.as_image_copy(),
|
||||
&model.pixels,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * model.width),
|
||||
rows_per_image: Some(model.height),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: model.width,
|
||||
height: model.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
let tex_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
// ── Sampler ───────────────────────────────────────────────────────
|
||||
let _sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("image.sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// ── Opacity uniform ───────────────────────────────────────────────
|
||||
let params = ImageParams { opacity: model.opacity.clamp(0.0, 1.0), _pad: [0.0; 3] };
|
||||
let _params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("image.params"),
|
||||
contents: bytemuck::bytes_of(¶ms),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
|
||||
// ── Bind group ────────────────────────────────────────────────────
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("image.bind_group1"),
|
||||
layout: bgl1,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&tex_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&_sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: _params_buf.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// ── Vertex buffer — two triangles ─────────────────────────────────
|
||||
// corners: [p0=BL, p1=BR, p2=TR, p3=TL]
|
||||
// UV: BL=(0,1), BR=(1,1), TR=(1,0), TL=(0,0)
|
||||
let [p0, p1, p2, p3] = model.corners;
|
||||
let verts = [
|
||||
ImageVertex { pos: p0, uv: [0.0, 1.0] },
|
||||
ImageVertex { pos: p1, uv: [1.0, 1.0] },
|
||||
ImageVertex { pos: p2, uv: [1.0, 0.0] },
|
||||
ImageVertex { pos: p0, uv: [0.0, 1.0] },
|
||||
ImageVertex { pos: p2, uv: [1.0, 0.0] },
|
||||
ImageVertex { pos: p3, uv: [0.0, 0.0] },
|
||||
];
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("image.vbuf"),
|
||||
contents: bytemuck::cast_slice(&verts),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
Some(Self {
|
||||
vertex_buffer,
|
||||
bind_group,
|
||||
_texture: texture,
|
||||
_sampler,
|
||||
_params_buf,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user