forked from 1b-agent/opencadstudio-upstream
Resolve boundary topology before rendering so corner-aligned pattern rows cannot flip shader parity. Chunk indexed geometry to device buffer limits and retain the texture compatibility transport. Refs #489
681 lines
28 KiB
Rust
681 lines
28 KiB
Rust
// Batched hatch rendering. The kernel triangulates each boundary once; the
|
|
// fragment shader only evaluates the fill pattern inside that mesh.
|
|
|
|
use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily};
|
|
use iced::wgpu;
|
|
use iced::wgpu::util::DeviceExt;
|
|
|
|
// ── GPU structs ────────────────────────────────────────────────────────────
|
|
//
|
|
// Layout matches the WGSL `HatchInstance` exactly. `repr(C)` + manual
|
|
// padding keeps WGSL's 16-byte alignment rules satisfied for arrays of
|
|
// this struct.
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
|
pub struct HatchInstance {
|
|
pub color: [f32; 4], // 0
|
|
pub color2: [f32; 4], // 16 (gradient end)
|
|
pub aabb: [f32; 4], // 32 (local-space xmin,ymin,xmax,ymax)
|
|
pub world_origin: [f32; 2], // 48 (anchor high half, added back in VS)
|
|
pub world_origin_low: [f32; 2], // 56 (anchor low residual — double-single)
|
|
pub angle_offset: f32, // 64
|
|
pub scale: f32, // 68
|
|
pub grad_cos: f32, // 72
|
|
pub grad_sin: f32, // 76
|
|
pub grad_min: f32, // 80
|
|
pub grad_range: f32, // 84
|
|
pub mode: u32, // 88 (0=pattern, 1=solid, 2=gradient)
|
|
pub visible: u32, // 92 (CPU sets to 0 to skip)
|
|
pub family_offset: u32, // 96
|
|
pub family_count: u32, // 100
|
|
/// Signed draw-order depth (-1,1); 0.0 = neutral. Applied as a clip-z
|
|
/// bias in the vertex shader so this fill orders against other types.
|
|
pub draw_depth: f32, // 104
|
|
/// Gradient shape (`GradientKind::shader_kind`), bit 4 = inverted stops.
|
|
pub grad_kind: u32, // 108
|
|
}
|
|
|
|
const _: () = assert!(std::mem::size_of::<HatchInstance>() == 112);
|
|
|
|
/// Split a hatch's f64 world-origin anchor into double-single (high, low) f32
|
|
/// pairs so the GPU keeps sub-unit precision at UTM-scale coordinates.
|
|
#[inline]
|
|
fn split_origin_ds(o: [f64; 2]) -> ([f32; 2], [f32; 2]) {
|
|
let hx = o[0] as f32;
|
|
let hy = o[1] as f32;
|
|
([hx, hy], [(o[0] - hx as f64) as f32, (o[1] - hy as f64) as f32])
|
|
}
|
|
|
|
/// Mirrors the per-family struct used by the existing per-hatch shader,
|
|
/// but the dash slice lives in a separate concatenated DashBuffer (the
|
|
/// old shader had it embedded). `dash_offset` / `dash_count` index into
|
|
/// that flat array.
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
|
pub struct LineFamilyGpu {
|
|
pub cos_a: f32, // 0
|
|
pub sin_a: f32, // 4
|
|
pub x0: f32, // 8
|
|
pub y0: f32, // 12
|
|
pub dx: f32, // 16
|
|
pub dy: f32, // 20
|
|
pub perp_step: f32, // 24
|
|
pub along_step: f32, // 28
|
|
pub line_width: f32, // 32
|
|
pub period: f32, // 36
|
|
pub n_dashes: u32, // 40
|
|
pub dash_offset: u32, // 44
|
|
}
|
|
|
|
const _: () = assert!(std::mem::size_of::<LineFamilyGpu>() == 48);
|
|
|
|
/// Kernel mesh vertex in local space with its source instance.
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
|
pub(super) struct HatchVertex {
|
|
pub local_xy: [f32; 2], // 0 — local-space position
|
|
pub instance_index: u32, // 8 — index into InstanceBuffer
|
|
}
|
|
|
|
impl HatchVertex {
|
|
pub(super) 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::Float32x2,
|
|
},
|
|
wgpu::VertexAttribute {
|
|
offset: 8,
|
|
shader_location: 1,
|
|
format: wgpu::VertexFormat::Uint32,
|
|
},
|
|
],
|
|
}
|
|
}
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
|
pub(super) struct HatchPlacement {
|
|
pub translation: [f32; 2],
|
|
pub translation_low: [f32; 2],
|
|
pub draw_depth: f32,
|
|
pub visible: u32,
|
|
}
|
|
|
|
impl HatchPlacement {
|
|
pub(super) fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
|
wgpu::VertexBufferLayout {
|
|
array_stride: std::mem::size_of::<Self>() as u64,
|
|
step_mode: wgpu::VertexStepMode::Instance,
|
|
attributes: &[
|
|
wgpu::VertexAttribute { offset: 0, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
|
|
wgpu::VertexAttribute { offset: 8, shader_location: 3, format: wgpu::VertexFormat::Float32x2 },
|
|
wgpu::VertexAttribute { offset: 16, shader_location: 4, format: wgpu::VertexFormat::Float32 },
|
|
wgpu::VertexAttribute { offset: 20, shader_location: 5, format: wgpu::VertexFormat::Uint32 },
|
|
],
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Batch builder ──────────────────────────────────────────────────────────
|
|
|
|
/// Pack a list of `HatchModel`s into the four concatenated storage
|
|
/// buffers + the per-vertex buffer needed by `hatch.wgsl`.
|
|
/// Returns `None` when the input slice is empty (caller skips the
|
|
/// hatch render pass entirely).
|
|
pub(super) struct StorageHatchBatch {
|
|
pub vertex_buffer: wgpu::Buffer,
|
|
pub index_buffer: wgpu::Buffer,
|
|
pub placement_buffer: wgpu::Buffer,
|
|
pub draws: Vec<(std::ops::Range<u32>, std::ops::Range<u32>)>,
|
|
// The four storage buffers below are referenced via `bind_group` —
|
|
// dropping them would invalidate it, but the bind group is the
|
|
// only direct consumer. Keep them as fields to keep ownership in
|
|
// one place; `#[allow(dead_code)]` silences the read-never warning.
|
|
#[allow(dead_code)] pub instance_buffer: wgpu::Buffer,
|
|
#[allow(dead_code)] pub family_buffer: wgpu::Buffer,
|
|
#[allow(dead_code)] pub dash_buffer: wgpu::Buffer,
|
|
/// Per-instance visibility flag (1=draw, 0=skip). Stored in its
|
|
/// own small storage buffer so per-frame updates don't have to
|
|
/// touch the large `instance_buffer`. Vertex shader reads
|
|
/// `visibility[instance_index]` — when 0 it emits an out-of-NDC
|
|
/// clip position so the GPU clips the primitive before the
|
|
/// fragment stage runs.
|
|
#[allow(dead_code)] pub visibility_buffer: wgpu::Buffer,
|
|
pub bind_group: wgpu::BindGroup,
|
|
#[allow(dead_code)] pub instance_count: u32,
|
|
/// CPU mirror — `update_visibility` re-uploads this whole slice
|
|
/// when any flag changes. ~4 B per hatch, so 40 KB / 10 k hatches
|
|
/// per pan tick. Far cheaper than touching the instance data.
|
|
pub placements: Vec<HatchPlacement>,
|
|
pub source_visibility: Vec<u32>,
|
|
pub source_aabbs: Vec<[f32; 4]>,
|
|
pub unique_source_count: usize,
|
|
/// CPU-side mirror of each instance's local-space AABB (world-
|
|
/// offset-subtracted, world_origin already added back). Used by
|
|
/// `compute_hatch_lod` to evaluate the sub-pixel + frustum cull
|
|
/// without reading back from the GPU.
|
|
pub instance_aabbs: Vec<[f32; 4]>,
|
|
}
|
|
|
|
fn hatch_buffer_cost(hatch: &HatchModel) -> [u64; 7] {
|
|
let boundary = hatch.boundary.len() as u64;
|
|
let (families, dashes) = match &hatch.pattern {
|
|
HatchPattern::Pattern(families) => (
|
|
families.len() as u64,
|
|
families.iter().map(|family| family.dashes.len() as u64).sum(),
|
|
),
|
|
_ => (0, 0),
|
|
};
|
|
[
|
|
boundary.saturating_mul(std::mem::size_of::<HatchVertex>() as u64),
|
|
boundary.saturating_mul(24),
|
|
std::mem::size_of::<HatchInstance>() as u64,
|
|
families.saturating_mul(std::mem::size_of::<LineFamilyGpu>() as u64),
|
|
dashes.saturating_mul(std::mem::size_of::<f32>() as u64),
|
|
std::mem::size_of::<u32>() as u64,
|
|
std::mem::size_of::<HatchPlacement>() as u64,
|
|
]
|
|
}
|
|
|
|
impl StorageHatchBatch {
|
|
/// Builds whole-hatch chunks within the device's buffer limits.
|
|
pub(super) fn build(
|
|
device: &wgpu::Device,
|
|
bgl: &wgpu::BindGroupLayout,
|
|
hatches: &[HatchModel],
|
|
) -> Vec<Self> {
|
|
if hatches.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let limits = device.limits();
|
|
let chunk_limit = limits
|
|
.max_buffer_size
|
|
.min(limits.max_storage_buffer_binding_size as u64);
|
|
let mut ranges = Vec::new();
|
|
let mut start = 0;
|
|
let mut used = [0u64; 7];
|
|
for (index, hatch) in hatches.iter().enumerate() {
|
|
let cost = hatch_buffer_cost(hatch);
|
|
let overflow = used
|
|
.iter()
|
|
.zip(cost)
|
|
.any(|(used, cost)| used.saturating_add(cost) > chunk_limit);
|
|
if overflow && index > start {
|
|
ranges.push(start..index);
|
|
start = index;
|
|
used = [0; 7];
|
|
}
|
|
for (used, cost) in used.iter_mut().zip(cost) {
|
|
*used = used.saturating_add(cost);
|
|
}
|
|
}
|
|
ranges.push(start..hatches.len());
|
|
ranges
|
|
.into_iter()
|
|
.filter_map(|range| Self::build_chunk(device, bgl, &hatches[range]))
|
|
.collect()
|
|
}
|
|
|
|
fn build_chunk(
|
|
device: &wgpu::Device,
|
|
bgl: &wgpu::BindGroupLayout,
|
|
hatches: &[HatchModel],
|
|
) -> Option<Self> {
|
|
|
|
let mut instances: Vec<HatchInstance> = Vec::with_capacity(hatches.len());
|
|
let mut families: Vec<LineFamilyGpu> = Vec::new();
|
|
let mut dashes: Vec<f32> = Vec::new();
|
|
let mut meshes = Vec::with_capacity(hatches.len());
|
|
let mut slots = rustc_hash::FxHashMap::default();
|
|
let mut groups: Vec<Vec<&HatchModel>> = Vec::new();
|
|
for (index, hatch) in hatches.iter().enumerate() {
|
|
let key = hatch
|
|
.render_instance
|
|
.map(|instance| (true, instance.source_id))
|
|
.unwrap_or((false, index as u64));
|
|
let slot = *slots.entry(key).or_insert_with(|| {
|
|
let slot = groups.len();
|
|
groups.push(Vec::new());
|
|
slot
|
|
});
|
|
groups[slot].push(hatch);
|
|
}
|
|
groups.sort_by_key(|group| std::cmp::Reverse(group.len() == 1));
|
|
let unique_source_count = groups.iter().take_while(|group| group.len() == 1).count();
|
|
|
|
for group in &groups {
|
|
let h = group[0];
|
|
let mesh = h.fill_mesh();
|
|
let has_mesh = !mesh.0.is_empty() && !mesh.1.is_empty();
|
|
meshes.push(mesh);
|
|
let family_offset = families.len() as u32;
|
|
let mut family_count = 0u32;
|
|
|
|
let mut grad_kind = 0u32;
|
|
let (mode, color2, grad_cos, grad_sin, grad_min, grad_range) = match &h.pattern {
|
|
HatchPattern::Solid => (1u32, [0.0; 4], 0.0, 0.0, 0.0, 1.0),
|
|
HatchPattern::Gradient { angle_deg, color2, kind, invert } => {
|
|
grad_kind = kind.shader_kind() | if *invert { 16 } else { 0 };
|
|
if kind.radial() {
|
|
// Radial fill: the boundary is stored relative to its
|
|
// centre (`world_origin`), so the centre is the local
|
|
// origin; radius = the farthest boundary vertex. mode 3.
|
|
let radius = radial_radius(&h.boundary);
|
|
(3u32, *color2, 0.0, 0.0, 0.0, radius)
|
|
} else {
|
|
let r = angle_deg.to_radians();
|
|
// Gradient projection range (proj_min / proj_range) —
|
|
// computed at upload time, identical to per-hatch path.
|
|
let (gmin, gmax) = boundary_projection_range(&h.boundary, r);
|
|
let grange = (gmax - gmin).max(1.0);
|
|
(2u32, *color2, r.cos(), r.sin(), gmin, grange)
|
|
}
|
|
}
|
|
HatchPattern::Pattern(fams) => {
|
|
for fam in fams {
|
|
let dash_offset = dashes.len() as u32;
|
|
for &d in &fam.dashes {
|
|
dashes.push(d);
|
|
}
|
|
let n_dashes = (dashes.len() as u32 - dash_offset).min(u32::MAX);
|
|
// PAT local frame: perpendicular spacing and
|
|
// along-line phase.
|
|
let perp_step = fam.dy;
|
|
let along_step = fam.dx;
|
|
// Screen-space derivative drives 1-px line width
|
|
// in the shader; this stored field is unused.
|
|
let line_width = 0.0_f32;
|
|
let period: f32 = fam.dashes.iter().map(|d| d.abs()).sum();
|
|
families.push(LineFamilyGpu {
|
|
cos_a: fam.angle_deg.to_radians().cos(),
|
|
sin_a: fam.angle_deg.to_radians().sin(),
|
|
x0: fam.x0,
|
|
y0: fam.y0,
|
|
dx: fam.dx,
|
|
dy: fam.dy,
|
|
perp_step,
|
|
along_step,
|
|
line_width,
|
|
period: if n_dashes > 0 { period } else { 0.0 },
|
|
n_dashes,
|
|
dash_offset,
|
|
});
|
|
family_count += 1;
|
|
}
|
|
(0u32, [0.0; 4], 0.0, 0.0, 0.0, 1.0)
|
|
}
|
|
};
|
|
|
|
// Boundary AABB in local space (matches the corner quad
|
|
// emitted by the vertex shader). The verts are already in
|
|
// `world_origin`-relative coords (see scene/mod.rs hatch
|
|
// packing), so this AABB lives in that frame.
|
|
let mut min_x = f32::INFINITY;
|
|
let mut min_y = f32::INFINITY;
|
|
let mut max_x = f32::NEG_INFINITY;
|
|
let mut max_y = f32::NEG_INFINITY;
|
|
for &[x, y] in h.boundary.iter() {
|
|
if x.is_finite() && y.is_finite() {
|
|
if x < min_x { min_x = x; }
|
|
if y < min_y { min_y = y; }
|
|
if x > max_x { max_x = x; }
|
|
if y > max_y { max_y = y; }
|
|
}
|
|
}
|
|
if !min_x.is_finite() {
|
|
// Empty / all-NaN — skip but keep the slot so indices
|
|
// stay in lockstep with the input list (visibility=0).
|
|
let (wo_hi, wo_lo) = split_origin_ds(h.world_origin);
|
|
instances.push(HatchInstance {
|
|
color: h.color,
|
|
color2,
|
|
aabb: [0.0, 0.0, 0.0, 0.0],
|
|
world_origin: wo_hi,
|
|
world_origin_low: wo_lo,
|
|
angle_offset: h.angle_offset,
|
|
scale: h.scale.max(1e-6),
|
|
grad_cos,
|
|
grad_sin,
|
|
grad_min,
|
|
grad_range,
|
|
mode,
|
|
visible: 0,
|
|
family_offset,
|
|
family_count,
|
|
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
|
|
grad_kind,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
let (wo_hi, wo_lo) = split_origin_ds(h.world_origin);
|
|
instances.push(HatchInstance {
|
|
color: h.color,
|
|
color2,
|
|
aabb: [min_x, min_y, max_x, max_y],
|
|
world_origin: wo_hi,
|
|
world_origin_low: wo_lo,
|
|
angle_offset: h.angle_offset,
|
|
scale: h.scale.max(1e-6),
|
|
grad_cos,
|
|
grad_sin,
|
|
grad_min,
|
|
grad_range,
|
|
mode,
|
|
visible: u32::from(has_mesh),
|
|
family_offset,
|
|
family_count,
|
|
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
|
|
grad_kind,
|
|
});
|
|
}
|
|
// Empty fallbacks — storage buffers can't be zero-sized.
|
|
if families.is_empty() {
|
|
families.push(LineFamilyGpu::default_filler());
|
|
}
|
|
if dashes.is_empty() {
|
|
dashes.push(0.0);
|
|
}
|
|
|
|
let mut verts = Vec::new();
|
|
let mut indices = Vec::new();
|
|
let mut mesh_ranges = Vec::with_capacity(meshes.len());
|
|
for (instance_index, (points, mesh_indices)) in meshes.into_iter().enumerate() {
|
|
let Ok(base) = u32::try_from(verts.len()) else {
|
|
return None;
|
|
};
|
|
let Ok(start) = u32::try_from(indices.len()) else {
|
|
return None;
|
|
};
|
|
verts.extend(points.into_iter().map(|local_xy| HatchVertex {
|
|
local_xy,
|
|
instance_index: instance_index as u32,
|
|
}));
|
|
for index in mesh_indices {
|
|
let Some(index) = base.checked_add(index) else {
|
|
return None;
|
|
};
|
|
indices.push(index);
|
|
}
|
|
let Ok(end) = u32::try_from(indices.len()) else {
|
|
return None;
|
|
};
|
|
mesh_ranges.push(start..end);
|
|
}
|
|
if indices.is_empty() {
|
|
return None;
|
|
}
|
|
let mut placements = Vec::new();
|
|
let mut draws = Vec::with_capacity(groups.len());
|
|
let mut instance_aabbs = Vec::new();
|
|
if unique_source_count > 0 {
|
|
placements.push(HatchPlacement {
|
|
translation: [0.0; 2],
|
|
translation_low: [0.0; 2],
|
|
draw_depth: 0.0,
|
|
visible: 1,
|
|
});
|
|
let end = mesh_ranges[unique_source_count - 1].end;
|
|
if end > 0 {
|
|
draws.push((0..end, 0..1));
|
|
}
|
|
let mut union = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
|
|
for inst in instances.iter().take(unique_source_count) {
|
|
union[0] = union[0].min(inst.aabb[0] + inst.world_origin[0]);
|
|
union[1] = union[1].min(inst.aabb[1] + inst.world_origin[1]);
|
|
union[2] = union[2].max(inst.aabb[2] + inst.world_origin[0]);
|
|
union[3] = union[3].max(inst.aabb[3] + inst.world_origin[1]);
|
|
}
|
|
instance_aabbs.push(union);
|
|
}
|
|
for (source_index, group) in groups.iter().enumerate().skip(unique_source_count) {
|
|
let source = group[0];
|
|
let base = source
|
|
.render_instance
|
|
.map_or([0.0; 3], |instance| instance.translation);
|
|
let placement_start = placements.len() as u32;
|
|
for hatch in group {
|
|
let translation = hatch
|
|
.render_instance
|
|
.map_or([0.0; 3], |instance| instance.translation);
|
|
let delta = [translation[0] - base[0], translation[1] - base[1]];
|
|
let high = [delta[0] as f32, delta[1] as f32];
|
|
placements.push(HatchPlacement {
|
|
translation: high,
|
|
translation_low: [
|
|
(delta[0] - high[0] as f64) as f32,
|
|
(delta[1] - high[1] as f64) as f32,
|
|
],
|
|
draw_depth: hatch.draw_depth,
|
|
visible: 1,
|
|
});
|
|
let inst = &instances[source_index];
|
|
instance_aabbs.push([
|
|
inst.aabb[0] + inst.world_origin[0] + high[0],
|
|
inst.aabb[1] + inst.world_origin[1] + high[1],
|
|
inst.aabb[2] + inst.world_origin[0] + high[0],
|
|
inst.aabb[3] + inst.world_origin[1] + high[1],
|
|
]);
|
|
}
|
|
if !mesh_ranges[source_index].is_empty() {
|
|
draws.push((
|
|
mesh_ranges[source_index].clone(),
|
|
placement_start..placements.len() as u32,
|
|
));
|
|
}
|
|
}
|
|
|
|
let visibility: Vec<u32> = instances.iter().map(|instance| instance.visible).collect();
|
|
let limits = device.limits();
|
|
let buffer_fits = |count: usize, stride: usize| {
|
|
(count as u64).saturating_mul(stride as u64) <= limits.max_buffer_size
|
|
};
|
|
let storage_fits = |count: usize, stride: usize| {
|
|
(count as u64).saturating_mul(stride as u64)
|
|
<= limits.max_storage_buffer_binding_size as u64
|
|
};
|
|
if !buffer_fits(verts.len(), std::mem::size_of::<HatchVertex>())
|
|
|| !buffer_fits(indices.len(), std::mem::size_of::<u32>())
|
|
|| !buffer_fits(placements.len(), std::mem::size_of::<HatchPlacement>())
|
|
|| !storage_fits(instances.len(), std::mem::size_of::<HatchInstance>())
|
|
|| !storage_fits(families.len(), std::mem::size_of::<LineFamilyGpu>())
|
|
|| !storage_fits(dashes.len(), std::mem::size_of::<f32>())
|
|
|| !storage_fits(visibility.len(), std::mem::size_of::<u32>())
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("hatch.vertex"),
|
|
contents: bytemuck::cast_slice(&verts),
|
|
usage: wgpu::BufferUsages::VERTEX,
|
|
});
|
|
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("hatch.index"),
|
|
contents: bytemuck::cast_slice(&indices),
|
|
usage: wgpu::BufferUsages::INDEX,
|
|
});
|
|
let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("hatch.placements"),
|
|
contents: bytemuck::cast_slice(&placements),
|
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
|
});
|
|
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("hatch.instances"),
|
|
contents: bytemuck::cast_slice(&instances),
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
|
});
|
|
let family_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("hatch.families"),
|
|
contents: bytemuck::cast_slice(&families),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
let dash_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("hatch.dashes"),
|
|
contents: bytemuck::cast_slice(&dashes),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let visibility_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("hatch.visibility"),
|
|
contents: bytemuck::cast_slice(&visibility),
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("hatch.bg"),
|
|
layout: bgl,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: instance_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: family_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 2,
|
|
resource: dash_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 3,
|
|
resource: visibility_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
let source_aabbs = instances
|
|
.iter()
|
|
.map(|inst| [
|
|
inst.aabb[0] + inst.world_origin[0],
|
|
inst.aabb[1] + inst.world_origin[1],
|
|
inst.aabb[2] + inst.world_origin[0],
|
|
inst.aabb[3] + inst.world_origin[1],
|
|
])
|
|
.collect();
|
|
|
|
Some(Self {
|
|
vertex_buffer,
|
|
index_buffer,
|
|
placement_buffer,
|
|
draws,
|
|
instance_buffer,
|
|
family_buffer,
|
|
dash_buffer,
|
|
visibility_buffer,
|
|
bind_group,
|
|
instance_count: instances.len() as u32,
|
|
placements,
|
|
source_visibility: visibility,
|
|
source_aabbs,
|
|
unique_source_count,
|
|
instance_aabbs,
|
|
})
|
|
}
|
|
|
|
/// Push the CPU `visibility` slice to GPU. Call when any
|
|
/// element changes (typically per-frame from compute_hatch_lod).
|
|
pub(super) fn upload_visibility(&self, queue: &wgpu::Queue) {
|
|
queue.write_buffer(
|
|
&self.placement_buffer,
|
|
0,
|
|
bytemuck::cast_slice(&self.placements),
|
|
);
|
|
queue.write_buffer(
|
|
&self.visibility_buffer,
|
|
0,
|
|
bytemuck::cast_slice(&self.source_visibility),
|
|
);
|
|
}
|
|
|
|
/// Shared layout for instance, family, dash, and visibility storage.
|
|
#[allow(dead_code)]
|
|
pub(super) fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
|
let entry = |binding: u32| wgpu::BindGroupLayoutEntry {
|
|
binding,
|
|
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
|
ty: wgpu::BindingType::Buffer {
|
|
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
|
has_dynamic_offset: false,
|
|
min_binding_size: None,
|
|
},
|
|
count: None,
|
|
};
|
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
label: Some("hatch.bgl"),
|
|
entries: &[entry(0), entry(1), entry(2), entry(3)],
|
|
})
|
|
}
|
|
}
|
|
|
|
impl LineFamilyGpu {
|
|
fn default_filler() -> Self {
|
|
Self {
|
|
cos_a: 1.0,
|
|
sin_a: 0.0,
|
|
x0: 0.0,
|
|
y0: 0.0,
|
|
dx: 1.0,
|
|
dy: 0.0,
|
|
perp_step: 1.0,
|
|
along_step: 1.0,
|
|
line_width: 0.0,
|
|
period: 0.0,
|
|
n_dashes: 0,
|
|
dash_offset: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
/// Project every boundary vertex onto the gradient direction
|
|
/// `(cos θ, sin θ)` and return the (min, max) projection. Used to set
|
|
/// up the gradient's normalized parameter range. Same math as the
|
|
/// per-hatch path; duplicated here to keep this module self-contained.
|
|
fn boundary_projection_range(boundary: &[[f32; 2]], theta: f32) -> (f32, f32) {
|
|
let (cs, sn) = (theta.cos(), theta.sin());
|
|
let mut lo = f32::INFINITY;
|
|
let mut hi = f32::NEG_INFINITY;
|
|
for &[x, y] in boundary {
|
|
if !x.is_finite() || !y.is_finite() {
|
|
continue;
|
|
}
|
|
let p = x * cs + y * sn;
|
|
if p < lo { lo = p; }
|
|
if p > hi { hi = p; }
|
|
}
|
|
if !lo.is_finite() {
|
|
return (0.0, 1.0);
|
|
}
|
|
(lo, hi)
|
|
}
|
|
|
|
/// Radius of a radial gradient — the distance from the boundary centre (the
|
|
/// local origin, since vertices are stored relative to it) to the farthest
|
|
/// vertex, so `t = 1` (the end colour) reaches the corners.
|
|
fn radial_radius(boundary: &[[f32; 2]]) -> f32 {
|
|
boundary
|
|
.iter()
|
|
.filter(|p| p[0].is_finite() && p[1].is_finite())
|
|
.map(|p| (p[0] * p[0] + p[1] * p[1]).sqrt())
|
|
.fold(0.0_f32, f32::max)
|
|
.max(1.0)
|
|
}
|
|
|
|
// PatFamily is re-exported by hatch_model so we don't need to import
|
|
// it explicitly anywhere else — but rust needs the type referenced to
|
|
// confirm the layout assumption above.
|
|
#[allow(dead_code)]
|
|
fn _assert_patfamily_fields(f: &PatFamily) -> (f32, f32, f32) {
|
|
(f.angle_deg, f.x0, f.y0)
|
|
}
|