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:
parent
3d1f355a5c
commit
4312b6c3a5
39 changed files with 702 additions and 15 deletions
|
|
@ -159,6 +159,7 @@ pub fn apply_along(
|
|||
line_weight_px,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
})
|
||||
.collect()
|
||||
|
|
|
|||
61
src/scene/image_model.rs
Normal file
61
src/scene/image_model.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// 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;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImageModel {
|
||||
/// Original file path (used for reload / display in properties).
|
||||
pub file_path: String,
|
||||
/// RGBA8 pixel data in row-major order.
|
||||
pub pixels: Vec<u8>,
|
||||
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],
|
||||
}
|
||||
|
||||
impl ImageModel {
|
||||
/// Build an ImageModel from a DXF RasterImage entity.
|
||||
/// Returns `None` if the image file cannot be opened or decoded.
|
||||
pub fn from_raster_image(img: &acadrust::entities::RasterImage) -> Option<Self> {
|
||||
let w = img.size.x;
|
||||
let h = img.size.y;
|
||||
let ox = img.insertion_point.x as f32;
|
||||
let oy = img.insertion_point.y as f32;
|
||||
let oz = img.insertion_point.z as f32;
|
||||
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;
|
||||
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],
|
||||
];
|
||||
let opacity = 1.0 - img.fade as f32 / 100.0;
|
||||
|
||||
let (pixels, width, height) = load_pixels(&img.file_path)?;
|
||||
Some(Self { file_path: img.file_path.clone(), pixels, width, height, opacity, corners })
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()?;
|
||||
let rgba = img.to_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
Some((rgba.into_raw(), w, h))
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ pub mod grip;
|
|||
pub mod hatch_model;
|
||||
pub mod hatch_patterns;
|
||||
pub mod hit_test;
|
||||
pub mod image_model;
|
||||
pub mod mesh_model;
|
||||
pub mod object;
|
||||
pub mod pipeline;
|
||||
|
|
@ -21,6 +22,7 @@ pub mod wire_model;
|
|||
use camera::Camera;
|
||||
pub use camera::Projection;
|
||||
pub use hatch_model::HatchModel;
|
||||
pub use image_model::ImageModel;
|
||||
pub use mesh_model::MeshModel;
|
||||
pub use object::{GripApply, GripDef};
|
||||
pub use pipeline::uniforms::Uniforms;
|
||||
|
|
@ -61,6 +63,8 @@ pub struct Scene {
|
|||
pub hatches: HashMap<Handle, HatchModel>,
|
||||
/// GPU render data for solid meshes (truck Shell/Solid tessellation).
|
||||
pub meshes: HashMap<Handle, MeshModel>,
|
||||
/// GPU render data for raster images (RasterImage entities), keyed by handle.
|
||||
pub images: HashMap<Handle, ImageModel>,
|
||||
/// The viewport that is currently "entered" (MSPACE mode).
|
||||
/// `None` = paper space editing (PSPACE). Only meaningful when
|
||||
/// `current_layout != "Model"`.
|
||||
|
|
@ -80,6 +84,7 @@ impl Scene {
|
|||
current_layout: "Model".to_string(),
|
||||
hatches: HashMap::new(),
|
||||
meshes: HashMap::new(),
|
||||
images: HashMap::new(),
|
||||
active_viewport: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -1025,6 +1030,11 @@ impl Scene {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let image_seed = if let EntityType::RasterImage(img) = &entity {
|
||||
ImageModel::from_raster_image(img)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Route to the correct block based on current editing mode:
|
||||
// - PSPACE (paper layout, no active viewport): paper-space layout block.
|
||||
|
|
@ -1042,6 +1052,9 @@ impl Scene {
|
|||
if let Some(model) = hatch_seed {
|
||||
self.hatches.insert(handle, model);
|
||||
}
|
||||
if let Some(model) = image_seed {
|
||||
self.images.insert(handle, model);
|
||||
}
|
||||
}
|
||||
handle
|
||||
}
|
||||
|
|
@ -1434,6 +1447,28 @@ impl Scene {
|
|||
})
|
||||
}
|
||||
|
||||
/// Decode and cache all RasterImage entities from the current document.
|
||||
/// Silently skips images whose files cannot be read.
|
||||
pub fn populate_images_from_document(&mut self) {
|
||||
self.images.clear();
|
||||
let entries: Vec<(Handle, acadrust::entities::RasterImage)> = self
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|e| {
|
||||
if let EntityType::RasterImage(img) = e {
|
||||
Some((img.common.handle, img.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for (handle, img) in entries {
|
||||
if let Some(model) = ImageModel::from_raster_image(&img) {
|
||||
self.images.insert(handle, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn populate_hatches_from_document(&mut self) {
|
||||
self.hatches.clear();
|
||||
|
||||
|
|
|
|||
169
src/scene/pipeline/image_gpu.rs
Normal file
169
src/scene/pipeline/image_gpu.rs
Normal file
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod hatch_gpu;
|
||||
pub mod image_gpu;
|
||||
pub mod mesh_gpu;
|
||||
pub mod uniforms;
|
||||
pub mod viewcube;
|
||||
|
|
@ -8,26 +9,31 @@ use iced::wgpu;
|
|||
use iced::{Rectangle, Size};
|
||||
|
||||
pub use hatch_gpu::HatchGpu;
|
||||
pub use image_gpu::ImageGpu;
|
||||
pub use mesh_gpu::MeshGpu;
|
||||
pub use uniforms::Uniforms;
|
||||
pub use viewcube::ViewCubePipeline;
|
||||
pub use wire_gpu::WireGpu;
|
||||
|
||||
use crate::scene::hatch_model::HatchModel;
|
||||
use crate::scene::image_model::ImageModel;
|
||||
use crate::scene::mesh_model::MeshModel;
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
pub struct Pipeline {
|
||||
wire_pipeline: wgpu::RenderPipeline,
|
||||
hatch_pipeline: wgpu::RenderPipeline,
|
||||
image_pipeline: wgpu::RenderPipeline,
|
||||
mesh_pipeline: wgpu::RenderPipeline,
|
||||
uniform_buffer: wgpu::Buffer,
|
||||
uniform_bind_group: wgpu::BindGroup,
|
||||
hatch_bgl1: wgpu::BindGroupLayout,
|
||||
image_bgl1: wgpu::BindGroupLayout,
|
||||
depth_texture_size: Size<u32>,
|
||||
depth_view: wgpu::TextureView,
|
||||
gpu_wires: Vec<WireGpu>,
|
||||
gpu_hatches: Vec<HatchGpu>,
|
||||
gpu_images: Vec<ImageGpu>,
|
||||
gpu_meshes: Vec<MeshGpu>,
|
||||
pub viewcube: ViewCubePipeline,
|
||||
}
|
||||
|
|
@ -246,19 +252,111 @@ impl Pipeline {
|
|||
cache: None,
|
||||
});
|
||||
|
||||
// ── Image pipeline ─────────────────────────────────────────────────
|
||||
let image_bgl1 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("image.bgl1"),
|
||||
entries: &[
|
||||
// binding 0: texture
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
// binding 1: sampler
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
// binding 2: ImageParams uniform
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let image_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("image.pipeline_layout"),
|
||||
bind_group_layouts: &[&frame_bgl, &image_bgl1],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("image.shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
|
||||
"../../shaders/image.wgsl"
|
||||
))),
|
||||
});
|
||||
|
||||
let image_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("image.pipeline"),
|
||||
layout: Some(&image_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &image_shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[image_gpu::ImageVertex::layout()],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
cull_mode: None,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &image_shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
}),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let viewcube = ViewCubePipeline::new(device, queue, format);
|
||||
|
||||
Self {
|
||||
wire_pipeline,
|
||||
hatch_pipeline,
|
||||
image_pipeline,
|
||||
mesh_pipeline,
|
||||
uniform_buffer,
|
||||
uniform_bind_group,
|
||||
hatch_bgl1,
|
||||
image_bgl1,
|
||||
depth_texture_size: Size::new(1, 1),
|
||||
depth_view,
|
||||
gpu_wires: vec![],
|
||||
gpu_hatches: vec![],
|
||||
gpu_images: vec![],
|
||||
gpu_meshes: vec![],
|
||||
viewcube,
|
||||
}
|
||||
|
|
@ -284,6 +382,18 @@ impl Pipeline {
|
|||
.collect();
|
||||
}
|
||||
|
||||
pub fn upload_images(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
images: &[ImageModel],
|
||||
) {
|
||||
self.gpu_images = images
|
||||
.iter()
|
||||
.filter_map(|m| ImageGpu::new(device, queue, m, &self.image_bgl1))
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub fn upload_uniforms(&self, queue: &wgpu::Queue, uniforms: &Uniforms) {
|
||||
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(uniforms));
|
||||
}
|
||||
|
|
@ -339,7 +449,48 @@ impl Pipeline {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Pass 2: solid meshes ───────────────────────────────────────────
|
||||
// ── Pass 2: raster images ─────────────────────────────────────────
|
||||
if !self.gpu_images.is_empty() {
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("image.render_pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: target,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.depth_view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_viewport(
|
||||
vp.x as f32,
|
||||
vp.y as f32,
|
||||
vp.width as f32,
|
||||
vp.height as f32,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
pass.set_pipeline(&self.image_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
for img in &self.gpu_images {
|
||||
pass.set_bind_group(1, &img.bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, img.vertex_buffer.slice(..));
|
||||
pass.draw(0..6, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 4: solid meshes ──────────────────────────────────────────
|
||||
if !self.gpu_meshes.is_empty() {
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("mesh.render_pass"),
|
||||
|
|
@ -382,7 +533,7 @@ impl Pipeline {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Pass 3: wires ──────────────────────────────────────────────────
|
||||
// ── Pass 5: wires ─────────────────────────────────────────────────
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("wire.render_pass"),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use iced::{Event, Rectangle, Size};
|
|||
use super::pipeline::viewcube::{hover_id, VIEWCUBE_PX};
|
||||
use super::pipeline::Pipeline;
|
||||
use super::tessellate;
|
||||
use super::{HatchModel, MeshModel, Scene, Uniforms, WireModel};
|
||||
use super::{HatchModel, ImageModel, MeshModel, Scene, Uniforms, WireModel};
|
||||
|
||||
// ── Camera hover state (shader::Program::State) ───────────────────────────
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ pub struct CameraState {
|
|||
pub struct Primitive {
|
||||
pub(super) wires: Vec<WireModel>,
|
||||
pub(super) hatches: Vec<HatchModel>,
|
||||
pub(super) images: Vec<ImageModel>,
|
||||
pub(super) meshes: Vec<MeshModel>,
|
||||
pub(super) uniforms: Uniforms,
|
||||
/// Camera rotation matrix derived from the quaternion.
|
||||
|
|
@ -59,6 +60,7 @@ impl<Msg: std::fmt::Debug + Clone> shader::Program<Msg> for Scene {
|
|||
Primitive {
|
||||
wires: all_wires,
|
||||
hatches: self.synced_hatch_models(),
|
||||
images: self.images.values().cloned().collect(),
|
||||
meshes: self.meshes.values().cloned().collect(),
|
||||
uniforms: Uniforms::new(&cam, bounds),
|
||||
cam_rotation: cam.view_rotation_mat(),
|
||||
|
|
@ -123,6 +125,7 @@ impl shader::Primitive for Primitive {
|
|||
pipeline.viewcube.ensure_depth_texture(device, size);
|
||||
pipeline.upload_uniforms(queue, &self.uniforms);
|
||||
pipeline.upload_hatches(device, &self.hatches);
|
||||
pipeline.upload_images(device, queue, &self.images);
|
||||
pipeline.upload_meshes(device, &self.meshes);
|
||||
pipeline.upload_wires(device, &self.wires);
|
||||
let logical = viewport.logical_size();
|
||||
|
|
|
|||
Loading…
Reference in a new issue