first commit

This commit is contained in:
Hakan Seven 2026-03-22 14:56:27 +03:00
commit d2fdac7f1e
274 changed files with 89330 additions and 0 deletions

View file

@ -0,0 +1,351 @@
// Hatch GPU buffers — renders hatch fills as shader-driven bounding-box quads.
//
// Group 1 bindings per hatch:
// binding 0 — HatchUniformData (64 bytes) mode, color, angle_offset, scale, gradient params
// binding 1 — BoundaryData (1024 bytes) up to 64 boundary polygon vertices
// binding 2 — FamilyBatchData (1296 bytes) up to 16 line families + 128 dash values
use crate::scene::hatch_model::{HatchModel, HatchPattern, PatFamily};
use iced::wgpu;
use iced::wgpu::util::DeviceExt;
// ── Constants ──────────────────────────────────────────────────────────────
pub const MAX_FAMILIES: usize = 16;
pub const MAX_DASHES: usize = 128;
// ── Vertex ────────────────────────────────────────────────────────────────
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct HatchVertex {
pub pos: [f32; 3],
pub _pad: f32,
}
impl HatchVertex {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<HatchVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}],
}
}
}
// ── Uniform structs ───────────────────────────────────────────────────────
/// Per-hatch parameters (binding 0). Must be 64 bytes.
///
/// `mode` encoding:
/// 0 → Pattern (families in FamilyBatchData)
/// 1 → Solid
/// 2 → Gradient (grad_cos/sin/min/range used)
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct HatchUniformData {
pub color: [f32; 4], // 0: primary RGBA / gradient start
pub color2: [f32; 4], // 16: gradient end color
pub mode: u32, // 32: 0=pattern, 1=solid, 2=gradient
pub vertex_count: u32, // 36: boundary vertex count
pub angle_offset: f32, // 40: pattern rotation (radians)
pub scale: f32, // 44: pattern scale multiplier
pub grad_cos: f32, // 48: gradient direction cos
pub grad_sin: f32, // 52: gradient direction sin
pub grad_min: f32, // 56: gradient proj_min
pub grad_range: f32, // 60: gradient proj_range
} // total 64 bytes
/// Boundary polygon (binding 1). Matches WGSL `Boundary`.
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct BoundaryData {
pub verts: [[f32; 4]; 64], // world XY in .xy, .zw unused
}
/// One line family packed for the shader (48 bytes).
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LineFamilyGpu {
pub cos_a: f32, // cos(angle_rad)
pub sin_a: f32, // sin(angle_rad)
pub x0: f32, // family origin x
pub y0: f32, // family origin y
pub dx: f32, // step vector x
pub dy: f32, // step vector y
pub perp_step: f32, // -dx*sin_a + dy*cos_a (perpendicular spacing)
pub along_step: f32, // dx*cos_a + dy*sin_a (along-line phase shift per step)
pub line_width: f32, // |perp_step| * 0.08
pub period: f32, // sum of |dash values| (0 = solid)
pub n_dashes: u32, // number of dash entries (0 = solid)
pub dash_off: u32, // index into FamilyBatchData::dash_values
}
/// All line families + dash values for one hatch (binding 2).
/// Total size: 16×48 + 32×16 + 4×4 = 768 + 512 + 16 = 1296 bytes.
/// dash_values is [f32; 4]×32 so each element is vec4<f32> (16-byte stride, valid in uniform).
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct FamilyBatchData {
pub families: [LineFamilyGpu; MAX_FAMILIES], // 768 bytes
pub dash_values: [[f32; 4]; 32], // 512 bytes (128 f32s as 32×vec4)
pub n_families: u32, // 4 bytes
pub _pad: [u32; 3], // 12 bytes
} // 1296 bytes total
// ── Per-hatch GPU handle ───────────────────────────────────────────────────
pub struct HatchGpu {
pub vertex_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
_uniform_buf: wgpu::Buffer,
_boundary_buf: wgpu::Buffer,
_family_buf: wgpu::Buffer,
}
impl HatchGpu {
pub fn new(device: &wgpu::Device, model: &HatchModel, bgl1: &wgpu::BindGroupLayout) -> Self {
// ── Decode pattern mode ──────────────────────────────────────────
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
HatchPattern::Pattern(_) => (0u32, [0.0f32; 4], 0.0f32, 0.0f32),
HatchPattern::Gradient { angle_deg, color2 } => {
let r = angle_deg.to_radians();
(2u32, *color2, r.cos(), r.sin())
}
};
// ── Bounding box ─────────────────────────────────────────────────
let min_x = model
.boundary
.iter()
.map(|v| v[0])
.fold(f32::INFINITY, f32::min);
let max_x = model
.boundary
.iter()
.map(|v| v[0])
.fold(f32::NEG_INFINITY, f32::max);
let min_y = model
.boundary
.iter()
.map(|v| v[1])
.fold(f32::INFINITY, f32::min);
let max_y = model
.boundary
.iter()
.map(|v| v[1])
.fold(f32::NEG_INFINITY, f32::max);
let max_spacing = match &model.pattern {
HatchPattern::Pattern(families) => families
.iter()
.map(|f| perp_spacing(f).abs())
.fold(0.0f32, f32::max),
_ => 5.0,
};
let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt();
let pad = (diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0);
let (x0, x1, y0, y1) = (min_x - pad, max_x + pad, min_y - pad, max_y + pad);
let quad = [
HatchVertex {
pos: [x0, y0, 0.0],
_pad: 0.0,
},
HatchVertex {
pos: [x1, y0, 0.0],
_pad: 0.0,
},
HatchVertex {
pos: [x1, y1, 0.0],
_pad: 0.0,
},
HatchVertex {
pos: [x0, y0, 0.0],
_pad: 0.0,
},
HatchVertex {
pos: [x1, y1, 0.0],
_pad: 0.0,
},
HatchVertex {
pos: [x0, y1, 0.0],
_pad: 0.0,
},
];
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.vbuf"),
contents: bytemuck::cast_slice(&quad),
usage: wgpu::BufferUsages::VERTEX,
});
// ── Gradient: projection range ───────────────────────────────────
let (grad_min, grad_range) = if mode == 2 {
let projs: Vec<f32> = model
.boundary
.iter()
.map(|&[x, y]| x * grad_cos + y * grad_sin)
.collect();
let proj_min = projs.iter().cloned().fold(f32::INFINITY, f32::min);
let proj_max = projs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
(proj_min, (proj_max - proj_min).max(1e-4))
} else {
(0.0, 1.0)
};
// ── HatchUniformData ─────────────────────────────────────────────
let n = model.boundary.len().min(64);
let uniform_data = HatchUniformData {
color: model.color,
color2,
mode,
vertex_count: n as u32,
angle_offset: model.angle_offset,
scale: model.scale,
grad_cos,
grad_sin,
grad_min,
grad_range,
};
// ── BoundaryData ─────────────────────────────────────────────────
let mut boundary_data = BoundaryData {
verts: [[0.0; 4]; 64],
};
for (i, &[x, y]) in model.boundary.iter().take(64).enumerate() {
boundary_data.verts[i] = [x, y, 0.0, 0.0];
}
// ── FamilyBatchData ───────────────────────────────────────────────
let family_batch = build_family_batch(&model.pattern);
// ── GPU buffers ───────────────────────────────────────────────────
let _uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.uniforms"),
contents: bytemuck::bytes_of(&uniform_data),
usage: wgpu::BufferUsages::UNIFORM,
});
let _boundary_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.boundary"),
contents: bytemuck::bytes_of(&boundary_data),
usage: wgpu::BufferUsages::UNIFORM,
});
let _family_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.families"),
contents: bytemuck::bytes_of(&family_batch),
usage: wgpu::BufferUsages::UNIFORM,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("hatch.bind_group1"),
layout: bgl1,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: _uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: _boundary_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: _family_buf.as_entire_binding(),
},
],
});
Self {
vertex_buffer,
bind_group,
_uniform_buf,
_boundary_buf,
_family_buf,
}
}
}
// ── Helpers ───────────────────────────────────────────────────────────────
/// Perpendicular spacing between adjacent parallel lines.
/// QCAD PAT format stores (dx, dy) in line-local coordinates: dy = perpendicular, dx = along-line.
fn perp_spacing(f: &PatFamily) -> f32 {
f.dy
}
/// Pack pattern families into the GPU batch struct.
fn build_family_batch(pattern: &HatchPattern) -> FamilyBatchData {
let mut batch = FamilyBatchData {
families: [LineFamilyGpu {
cos_a: 0.0,
sin_a: 0.0,
x0: 0.0,
y0: 0.0,
dx: 0.0,
dy: 0.0,
perp_step: 0.0,
along_step: 0.0,
line_width: 0.0,
period: 0.0,
n_dashes: 0,
dash_off: 0,
}; MAX_FAMILIES],
dash_values: [[0.0; 4]; 32],
n_families: 0,
_pad: [0; 3],
};
let HatchPattern::Pattern(families) = pattern else {
return batch;
};
let mut dash_cursor: usize = 0;
for (fi, family) in families.iter().take(MAX_FAMILIES).enumerate() {
let angle_r = family.angle_deg.to_radians();
let cos_a = angle_r.cos();
let sin_a = angle_r.sin();
// QCAD PAT local-frame convention: dy = perpendicular spacing, dx = along-line phase shift.
let perp_step = family.dy;
let along_step = family.dx;
let line_width = 0.0_f32; // unused: shader uses screen-space derivative for 1px lines
// Dash pattern: collect up to available space.
let n_avail = MAX_DASHES.saturating_sub(dash_cursor);
let n_dashes = family.dashes.len().min(n_avail);
let period: f32 = family.dashes[..n_dashes].iter().map(|d| d.abs()).sum();
let dash_off = dash_cursor as u32;
for &d in &family.dashes[..n_dashes] {
batch.dash_values[dash_cursor / 4][dash_cursor % 4] = d;
dash_cursor += 1;
}
batch.families[fi] = LineFamilyGpu {
cos_a,
sin_a,
x0: family.x0,
y0: family.y0,
dx: family.dx,
dy: family.dy,
perp_step,
along_step,
line_width,
period: if n_dashes > 0 { period } else { 0.0 },
n_dashes: n_dashes as u32,
dash_off,
};
batch.n_families += 1;
if dash_cursor >= MAX_DASHES {
break;
}
}
batch
}

View file

@ -0,0 +1,97 @@
// Mesh GPU buffers — TriangleList rendering for solid objects.
//
// Vertex layout (40 bytes):
// position [f32; 3] offset 0 12 B
// normal [f32; 3] offset 12 12 B
// color [f32; 4] offset 24 16 B
// ------
// 40 B / vertex
use crate::scene::mesh_model::MeshModel;
use iced::wgpu;
use iced::wgpu::util::DeviceExt;
// ── Vertex layout ─────────────────────────────────────────────────────────
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MeshVertex {
pub position: [f32; 3],
pub normal: [f32; 3],
pub color: [f32; 4],
}
impl MeshVertex {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<MeshVertex>() 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::Float32x3,
},
wgpu::VertexAttribute {
offset: 24,
shader_location: 2,
format: wgpu::VertexFormat::Float32x4,
},
],
}
}
}
// ── GPU handle ────────────────────────────────────────────────────────────
pub struct MeshGpu {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
}
impl MeshGpu {
pub fn new(device: &wgpu::Device, mesh: &MeshModel) -> Self {
let has_normals = mesh.normals.len() == mesh.verts.len();
let vertices: Vec<MeshVertex> = mesh
.verts
.iter()
.enumerate()
.map(|(i, &pos)| {
let normal = if has_normals {
mesh.normals[i]
} else {
[0.0, 1.0, 0.0]
};
MeshVertex {
position: pos,
normal,
color: mesh.color,
}
})
.collect();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("mesh.vbuf.{}", mesh.name)),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("mesh.ibuf.{}", mesh.name)),
contents: bytemuck::cast_slice(&mesh.indices),
usage: wgpu::BufferUsages::INDEX,
});
Self {
vertex_buffer,
index_buffer,
index_count: mesh.indices.len() as u32,
}
}
}

458
src/scene/pipeline/mod.rs Normal file
View file

@ -0,0 +1,458 @@
pub mod hatch_gpu;
pub mod mesh_gpu;
pub mod uniforms;
pub mod viewcube;
pub mod wire_gpu;
use iced::wgpu;
use iced::{Rectangle, Size};
pub use hatch_gpu::HatchGpu;
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::mesh_model::MeshModel;
use crate::scene::wire_model::WireModel;
pub struct Pipeline {
wire_pipeline: wgpu::RenderPipeline,
hatch_pipeline: wgpu::RenderPipeline,
mesh_pipeline: wgpu::RenderPipeline,
uniform_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
hatch_bgl1: wgpu::BindGroupLayout,
depth_texture_size: Size<u32>,
depth_view: wgpu::TextureView,
gpu_wires: Vec<WireGpu>,
gpu_hatches: Vec<HatchGpu>,
gpu_meshes: Vec<MeshGpu>,
pub viewcube: ViewCubePipeline,
}
impl Pipeline {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
// ── Shared frame uniform buffer (view_proj etc.) ───────────────────
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("viewer.uniform_buffer"),
size: std::mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Bind group layout 0 — shared by wire and hatch pipelines.
let frame_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("viewer.frame_bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("viewer.bind_group"),
layout: &frame_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
// ── Wire pipeline ──────────────────────────────────────────────────
let wire_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("wire.pipeline_layout"),
bind_group_layouts: &[&frame_bgl],
push_constant_ranges: &[],
});
let depth_tex = create_depth_texture(device, Size::new(1, 1));
let depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
let wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("wire.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/wire.wgsl"
))),
});
let wire_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("wire.pipeline"),
layout: Some(&wire_layout),
vertex: wgpu::VertexState {
module: &wire_shader,
entry_point: Some("vs_main"),
buffers: &[wire_gpu::WireVertex::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: &wire_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,
});
// ── Hatch pipeline ─────────────────────────────────────────────────
let hatch_entry = |binding: u32| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
};
let hatch_bgl1 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("hatch.bgl1"),
entries: &[hatch_entry(0), hatch_entry(1), hatch_entry(2)],
});
let hatch_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("hatch.pipeline_layout"),
bind_group_layouts: &[&frame_bgl, &hatch_bgl1],
push_constant_ranges: &[],
});
let hatch_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("hatch.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/hatch.wgsl"
))),
});
let hatch_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("hatch.pipeline"),
layout: Some(&hatch_layout),
vertex: wgpu::VertexState {
module: &hatch_shader,
entry_point: Some("vs_main"),
buffers: &[hatch_gpu::HatchVertex::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: &hatch_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,
});
// ── Mesh pipeline ──────────────────────────────────────────────────
let mesh_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("mesh.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/mesh.wgsl"
))),
});
let mesh_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("mesh.pipeline_layout"),
bind_group_layouts: &[&frame_bgl],
push_constant_ranges: &[],
});
let mesh_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("mesh.pipeline"),
layout: Some(&mesh_layout),
vertex: wgpu::VertexState {
module: &mesh_shader,
entry_point: Some("vs_main"),
buffers: &[mesh_gpu::MeshVertex::layout()],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: Some(wgpu::Face::Back),
..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: &mesh_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,
mesh_pipeline,
uniform_buffer,
uniform_bind_group,
hatch_bgl1,
depth_texture_size: Size::new(1, 1),
depth_view,
gpu_wires: vec![],
gpu_hatches: vec![],
gpu_meshes: vec![],
viewcube,
}
}
pub fn upload_wires(&mut self, device: &wgpu::Device, wires: &[WireModel]) {
self.gpu_wires = wires.iter().map(|w| WireGpu::new(device, w)).collect();
}
pub fn upload_meshes(&mut self, device: &wgpu::Device, meshes: &[MeshModel]) {
self.gpu_meshes = meshes
.iter()
.filter(|m| !m.indices.is_empty())
.map(|m| MeshGpu::new(device, m))
.collect();
}
pub fn upload_hatches(&mut self, device: &wgpu::Device, hatches: &[HatchModel]) {
self.gpu_hatches = hatches
.iter()
.filter(|h| h.boundary.len() >= 3)
.map(|h| HatchGpu::new(device, h, &self.hatch_bgl1))
.collect();
}
pub fn upload_uniforms(&self, queue: &wgpu::Queue, uniforms: &Uniforms) {
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(uniforms));
}
pub fn render(
&self,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
clip_bounds: Rectangle<u32>,
) {
let vp = clip_bounds;
// ── Pass 1: hatch fills ────────────────────────────────────────────
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("hatch.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::Clear(1.0),
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,
);
if !self.gpu_hatches.is_empty() {
pass.set_pipeline(&self.hatch_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
for hatch in &self.gpu_hatches {
pass.set_bind_group(1, &hatch.bind_group, &[]);
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
pass.draw(0..6, 0..1);
}
}
}
// ── Pass 2: solid meshes ───────────────────────────────────────────
if !self.gpu_meshes.is_empty() {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("mesh.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.mesh_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
for mesh in &self.gpu_meshes {
if mesh.index_count > 0 {
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..mesh.index_count, 0, 0..1);
}
}
}
// ── Pass 3: wires ──────────────────────────────────────────────────
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("wire.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.wire_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
for wire in &self.gpu_wires {
if wire.vertex_count >= 6 {
pass.set_vertex_buffer(0, wire.vertex_buffer.slice(..));
pass.draw(0..wire.vertex_count, 0..1);
}
}
}
}
pub fn ensure_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
if self.depth_texture_size != size {
let tex = create_depth_texture(device, size);
self.depth_view = tex.create_view(&wgpu::TextureViewDescriptor::default());
self.depth_texture_size = size;
}
}
}
fn create_depth_texture(device: &wgpu::Device, size: Size<u32>) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("viewer.depth_texture"),
size: wgpu::Extent3d {
width: size.width.max(1),
height: size.height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
}
impl iced::widget::shader::Pipeline for Pipeline {
fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
Self::new(device, queue, format)
}
}

View file

@ -0,0 +1,22 @@
use crate::scene::camera::Camera;
use iced::Rectangle;
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Uniforms {
pub view_proj: glam::Mat4,
pub camera_pos: glam::Vec4,
pub viewport_size: [f32; 2],
pub _pad: [f32; 2],
}
impl Uniforms {
pub fn new(camera: &Camera, bounds: Rectangle) -> Self {
Self {
view_proj: camera.view_proj(bounds),
camera_pos: camera.position_vec4(),
viewport_size: [bounds.width, bounds.height],
_pad: [0.0; 2],
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,212 @@
// Wire GPU buffers — quad (TriangleList) rendering for thick lines.
//
// Each segment [A→B] emits 6 vertices (2 triangles). Every vertex carries
// both endpoints so the vertex shader can compute the screen-space
// perpendicular direction and expand the quad to the correct pixel width.
//
// NaN sentinel: text glyphs pack multiple disconnected strokes into one
// WireModel, separated by [NaN, NaN, NaN] points. Segments where either
// endpoint contains NaN are silently skipped during upload.
//
// Vertex layout (96 bytes, stride = 96):
// pos_a [f32; 3] offset 0 12 B — segment start (world)
// pos_b [f32; 3] offset 12 12 B — segment end (world)
// which_end f32 offset 24 4 B — 0.0 = A end, 1.0 = B end
// side f32 offset 28 4 B — ±1.0 perpendicular side
// color [f32; 4] offset 32 16 B — RGBA [0,1]
// distance f32 offset 48 4 B — arc-length from wire start
// half_width f32 offset 52 4 B — half line width in pixels
// pattern_length f32 offset 56 4 B — dash pattern total length
// _pad f32 offset 60 4 B
// pat0 [f32; 4] offset 64 16 B — pattern elements 0-3
// pat1 [f32; 4] offset 80 16 B — pattern elements 4-7
// ------
// 96 B / vertex
use crate::scene::wire_model::WireModel;
use iced::wgpu;
use iced::wgpu::util::DeviceExt;
// ── Vertex layout ─────────────────────────────────────────────────────────
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct WireVertex {
pub pos_a: [f32; 3],
pub pos_b: [f32; 3],
pub which_end: f32,
pub side: f32,
pub color: [f32; 4],
pub distance: f32,
pub half_width: f32,
pub pattern_length: f32,
pub _pad: f32,
pub pat0: [f32; 4],
pub pat1: [f32; 4],
}
impl WireVertex {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<WireVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}, // pos_a
wgpu::VertexAttribute {
offset: 12,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}, // pos_b
wgpu::VertexAttribute {
offset: 24,
shader_location: 2,
format: wgpu::VertexFormat::Float32,
}, // which_end
wgpu::VertexAttribute {
offset: 28,
shader_location: 3,
format: wgpu::VertexFormat::Float32,
}, // side
wgpu::VertexAttribute {
offset: 32,
shader_location: 4,
format: wgpu::VertexFormat::Float32x4,
}, // color
wgpu::VertexAttribute {
offset: 48,
shader_location: 5,
format: wgpu::VertexFormat::Float32,
}, // distance
wgpu::VertexAttribute {
offset: 52,
shader_location: 6,
format: wgpu::VertexFormat::Float32,
}, // half_width
wgpu::VertexAttribute {
offset: 56,
shader_location: 7,
format: wgpu::VertexFormat::Float32,
}, // pattern_length
wgpu::VertexAttribute {
offset: 64,
shader_location: 8,
format: wgpu::VertexFormat::Float32x4,
}, // pat0
wgpu::VertexAttribute {
offset: 80,
shader_location: 9,
format: wgpu::VertexFormat::Float32x4,
}, // pat1
],
}
}
}
// ── GPU handle ────────────────────────────────────────────────────────────
pub struct WireGpu {
pub vertex_buffer: wgpu::Buffer,
pub vertex_count: u32,
}
impl WireGpu {
pub fn new(device: &wgpu::Device, wire: &WireModel) -> Self {
let pat0 = [
wire.pattern[0],
wire.pattern[1],
wire.pattern[2],
wire.pattern[3],
];
let pat1 = [
wire.pattern[4],
wire.pattern[5],
wire.pattern[6],
wire.pattern[7],
];
let half_width = wire.line_weight_px * 0.5;
let n = wire.points.len();
let seg_count = if n >= 2 { n - 1 } else { 0 };
let mut vertices: Vec<WireVertex> = Vec::with_capacity(seg_count * 6);
// Precompute cumulative arc-length at each point.
// NaN points do not contribute to arc length — they are separator sentinels.
let mut dists = vec![0.0_f32; n];
for i in 1..n {
let p = wire.points[i - 1];
let q = wire.points[i];
// If either point is a NaN sentinel, keep the same distance
// (the segment will be skipped anyway).
if p[0].is_nan() || q[0].is_nan() {
dists[i] = dists[i - 1];
} else {
let dx = q[0] - p[0];
let dy = q[1] - p[1];
let dz = q[2] - p[2];
dists[i] = dists[i - 1] + (dx * dx + dy * dy + dz * dz).sqrt();
}
}
for i in 0..seg_count {
let a = wire.points[i];
let b = wire.points[i + 1];
// Skip any segment that involves a NaN sentinel point.
// NaN sentinels are inserted between disconnected glyph strokes
// by tessellate.rs so that multiple strokes can share one WireModel.
if a[0].is_nan()
|| a[1].is_nan()
|| a[2].is_nan()
|| b[0].is_nan()
|| b[1].is_nan()
|| b[2].is_nan()
{
continue;
}
let dist_a = dists[i];
let dist_b = dists[i + 1];
let make = |which_end: f32, side: f32| -> WireVertex {
let dist = if which_end < 0.5 { dist_a } else { dist_b };
WireVertex {
pos_a: a,
pos_b: b,
which_end,
side,
color: wire.color,
distance: dist,
half_width,
pattern_length: wire.pattern_length,
_pad: 0.0,
pat0,
pat1,
}
};
// Triangle 1: A(-1), B(-1), B(+1)
vertices.push(make(0.0, -1.0));
vertices.push(make(1.0, -1.0));
vertices.push(make(1.0, 1.0));
// Triangle 2: A(-1), B(+1), A(+1)
vertices.push(make(0.0, -1.0));
vertices.push(make(1.0, 1.0));
vertices.push(make(0.0, 1.0));
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("wire.vbuf.{}", wire.name)),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
Self {
vertex_buffer,
vertex_count: vertices.len() as u32,
}
}
}