Fix fatal wgpu buffer overflow; draw MOVE/COPY/ROTATE/SCALE previews and the selection highlight on the GPU (#642)

Squash of PR #642 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aldozar 2026-10-11 18:10:51 -06:00 committed by GitHub
parent 244feb6e89
commit 1e1f68bf40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 363 additions and 43 deletions

View File

@ -252,6 +252,17 @@ fn main() -> eframe::Result {
// Before eframe creates the wgpu instance: default Windows to DirectX 12 with FXC (see graphics.rs).
#[cfg(target_os = "windows")]
graphics::configure(&mut options, eframe::wgpu::Backends::from_env(), eframe::wgpu::Dx12Compiler::from_env());
// Command previews (COPY, MOVE...) of large selections are tessellated by egui, whose index
// buffer outgrew wgpu's default 256 MiB cap; the validation error is fatal. Ask for what the
// adapter actually supports.
if let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup {
let base = std::sync::Arc::clone(&create.device_descriptor);
create.device_descriptor = std::sync::Arc::new(move |adapter| {
let mut d = base(adapter);
d.required_limits.max_buffer_size = d.required_limits.max_buffer_size.max(adapter.limits().max_buffer_size);
d
});
}
// winit has no file drag-and-drop on Wayland (only on X11), so dropping files from the file
// manager showed a "no" cursor. Run through XWayland when it's there; CADCRAFT_WAYLAND=1
// keeps the native Wayland backend.

View File

@ -447,6 +447,9 @@ impl Interactive for ArrayM {
fn preview(&self, s: &Session, cursor: Vec2) -> Vec<EntityKind> {
self.inner.as_ref().map(|m| m.preview(s, cursor)).unwrap_or_default()
}
fn drag(&self, s: &Session, cursor: Vec2) -> Option<crate::prompt::Drag> {
self.inner.as_ref().and_then(|m| m.drag(s, cursor))
}
}
// ---------------- ARRAYRECT ----------------

View File

@ -1927,6 +1927,21 @@ impl Interactive for SelectThen {
out.push(line(base, c));
out
}
fn drag(&self, _s: &Session, c: Vec2) -> Option<crate::prompt::Drag> {
// Mirror stays on `preview`: MIRRTEXT keeps text readable, which a plain matrix can't.
if (self.op == Op::Rotate && self.reference) || !self.sel.done {
return None;
}
let base = self.pts.first().copied()?;
let matrix = match self.op {
Op::Move | Op::Copy => Mat3::translate(c - base),
Op::Rotate => Mat3::rotate_about(base, base.angle_to(c)),
Op::Scale if !self.reference => Mat3::scale_about(base, base.dist(c).max(1e-9)),
Op::Scale => Mat3::scale_about(base, self.ref_len.and_then(|r| positive_length(Some(base.dist(c) / r)))?),
_ => return None,
};
Some(crate::prompt::Drag { handles: self.objs.clone(), matrix, extra: vec![line(base, c)] })
}
}
/// An OFFSET option prompt opened from "Specify offset distance".
@ -2730,6 +2745,12 @@ impl Interactive for DisplaceM {
}
self.inner.preview(s, c)
}
fn drag(&self, s: &Session, c: Vec2) -> Option<crate::prompt::Drag> {
if self.displacement {
return None;
}
self.inner.drag(s, c)
}
}
pub(crate) fn _unused(_: Circle) {}

View File

@ -75,6 +75,15 @@ impl Interactive for Guarded {
Vec::new()
})
}
fn drag(&self, s: &Session, cursor: Vec2) -> Option<crate::prompt::Drag> {
if self.broken.get().is_some() {
return None;
}
catch_unwind(AssertUnwindSafe(|| self.inner.drag(s, cursor))).unwrap_or_else(|_| {
self.broken.set(Some("panic in preview"));
None
})
}
}
#[cfg(test)]

View File

@ -38,7 +38,7 @@ pub use cadcraft_doc as doc;
pub use cadcraft_geom as geom;
pub use cadcraft_render as render;
pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
pub use prompt::{Accept, Input, Interactive, Prompt, Step};
pub use prompt::{Accept, Drag, Input, Interactive, Prompt, Step};
#[derive(Debug, thiserror::Error)]
pub enum EngineError {
@ -1183,6 +1183,16 @@ impl Session {
r.machine.preview(self, cursor).into_iter().map(|k| Entity { handle: Handle(0), common: common.clone(), kind: k }).collect()
}
/// The active command's preview as a transform of existing objects (see
/// [`Interactive::drag`]), with its extra rubber-band geometry as entities.
pub fn drag(&self, cursor: Vec2) -> Option<(Drag, Vec<Entity>)> {
let r = self.running.as_ref()?;
let d = r.machine.drag(self, cursor)?;
let common = self.current_common();
let extra = d.extra.iter().map(|k| Entity { handle: Handle(0), common: common.clone(), kind: k.clone() }).collect();
Some((d, extra))
}
/// Process one line typed at the command line (Enter pressed). Spaces separate inputs as in a
/// script line (see [`Session::script`]), so `MOVE L 0,0 5,5` ends the selection with the
/// second space and `ERASE L ` with the trailing one. Input after the command ends is ignored.

View File

@ -1,7 +1,7 @@
//! Interactive commands: prompts, inputs and the command-line coordinate grammar.
use cadcraft_doc::{EntityKind, Handle};
use cadcraft_geom::Vec2;
use cadcraft_geom::{Mat3, Vec2};
use serde::Serialize;
use crate::{Result, Session};
@ -165,6 +165,21 @@ pub trait Interactive: Send {
fn preview(&self, _s: &Session, _cursor: Vec2) -> Vec<EntityKind> {
Vec::new()
}
/// The preview as existing objects under one transform, when it is that (MOVE, COPY,
/// ROTATE, SCALE). The UI then draws them from geometry uploaded once instead of rebuilding
/// the transformed copies every frame; `None` falls back to [`Interactive::preview`].
fn drag(&self, _s: &Session, _cursor: Vec2) -> Option<Drag> {
None
}
}
/// See [`Interactive::drag`].
#[derive(Clone, Debug)]
pub struct Drag {
pub handles: Vec<Handle>,
pub matrix: Mat3,
/// Rubber-band geometry drawn as well (the base-to-cursor line).
pub extra: Vec<EntityKind>,
}
/// Parse a typed point: `x,y[,z]`, `@dx,dy`, `@dist<angle`, `dist<angle`, `#x,y` (absolute),

View File

@ -342,6 +342,31 @@ fn copy_rotate_scale_mirror() {
assert_eq!(s.doc().unwrap().model.len(), 5);
}
#[test]
fn copy_and_rotate_drag_as_a_transform_of_the_selection() {
fn start(cmd: &str) -> Session {
let mut s = Session::new();
s.execute("line", &json!({"points": [[0, 0], [1, 0]]})).unwrap();
for t in [cmd, "all", "", "0,0"] {
s.cmdline(t).unwrap();
}
s
}
let s = start("copy");
let (d, extra) = s.drag(Vec2::new(3.0, 4.0)).expect("COPY drags its selection");
assert_eq!(d.handles.len(), 1);
assert!(d.matrix.apply(Vec2::new(1.0, 0.0)).near(Vec2::new(4.0, 4.0), 1e-9));
assert_eq!(extra.len(), 1, "the base-to-cursor line");
let (d, _) = start("rotate").drag(Vec2::new(0.0, 5.0)).expect("ROTATE drags its selection");
assert!(d.matrix.apply(Vec2::new(1.0, 0.0)).near(Vec2::new(0.0, 1.0), 1e-9));
// MIRROR keeps the per-entity preview (MIRRTEXT).
let s = start("mirror");
assert!(s.drag(Vec2::new(0.0, 5.0)).is_none());
assert!(!s.preview(Vec2::new(0.0, 5.0)).is_empty());
}
#[test]
fn scale_reference_typed_and_picked() {
// Issue #17: SCALE ▸ Reference scales by new length / reference length.

View File

@ -51,6 +51,17 @@ pub struct CanvasState {
mesh_key: Option<u64>,
mesh_slot: crate::gpu::MeshSlot,
mesh_origin: Vec2,
/// The dragged objects of a MOVE/COPY/ROTATE/SCALE preview, uploaded once per drag (see
/// [`draw_drag_gpu`]).
drag_key: Option<u64>,
drag_slot: crate::gpu::MeshSlot,
drag_origin: Vec2,
/// The selection highlight on the GPU (see [`draw_highlight_gpu`]).
hl_key: Option<u64>,
hl_slot: crate::gpu::MeshSlot,
hl_origin: Vec2,
/// Selected infinite lines (clipped and drawn on the CPU each frame).
hl_infinite: Vec<usize>,
/// Indices of infinite-line primitives in `list` (drawn on the CPU in GPU mode).
infinite: Vec<usize>,
/// Milliseconds spent converting the display list to vertex data (last rebuild).
@ -351,6 +362,104 @@ fn draw_list(p: &egui::Painter, xf: &Xf, list: &DisplayList, bg: Rgb, lwdisplay:
p.extend(shapes);
}
/// Draw the selection highlight (dashed, `width` points) through the GPU: its vertex data is
/// rebuilt only when the display list, the selection or the vertex origin changes. Dashing every
/// selected line and tessellating it as egui shapes each frame made large selections sluggish.
#[allow(clippy::too_many_arguments)]
fn draw_highlight_gpu(
p: &egui::Painter,
xf: &Xf,
list: &DisplayList,
list_gen: u64,
prim_index: &mut PrimIndex,
sel: &[Handle],
color: Color32,
width: f32,
hl: (&mut Option<u64>, &crate::gpu::MeshSlot, &mut Vec2, &mut Vec<usize>),
) {
let (hl_key, hl_slot, hl_origin, hl_infinite) = hl;
let origin = crate::gpu::choose_origin(&list.bounds, xf.center, f64::from(xf.rect.height()) / xf.scale.max(1e-300));
let key = {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
(list_gen, sel, origin.x.to_bits(), origin.y.to_bits(), color.to_array(), width.to_bits()).hash(&mut h);
h.finish()
};
if *hl_key != Some(key) {
let prims = prim_index.prims(list_gen, list, sel);
let mesh = crate::gpu::build_highlight_mesh(list, &prims, origin, color.to_array(), width);
*hl_infinite = prims.into_iter().filter(|&i| list.prims.get(i).is_some_and(|p| matches!(p.kind, Kind::Infinite { .. }))).collect();
if let Ok(mut slot) = hl_slot.lock() {
*slot = Some(mesh);
}
*hl_key = Some(key);
*hl_origin = origin;
}
let center = xf.rect.center();
p.add(egui_wgpu::Callback::new_paint_callback(
xf.rect,
crate::gpu::CanvasCallback {
key,
slot: hl_slot.clone(),
offset: [(hl_origin.x - xf.center.x) as f32, (hl_origin.y - xf.center.y) as f32],
scale: xf.scale as f32,
center: [center.x, center.y],
layer: crate::gpu::LAYER_HIGHLIGHT,
xform: crate::gpu::IDENTITY_XFORM,
dash: 10.0,
},
));
}
/// Draw a MOVE/COPY/ROTATE/SCALE preview the way the drawing itself is drawn: the dragged
/// objects are tessellated and uploaded once (again only when the drawing, the selection or the
/// zoom band changes), and each frame only the transform in the view uniform moves. Rebuilding
/// the transformed copies as egui shapes every frame was slow with heavy selections and could
/// overflow the GPU buffer limit.
fn draw_drag_gpu(c: &mut CanvasState, p: &egui::Painter, xf: &Xf, doc: &cadcraft_doc::Drawing, drag: &cadcraft_engine::Drag, bg: Rgb) {
// Tessellate for a power-of-two zoom band so zooming mid-drag rebuilds only every 2x.
let band = xf.scale.max(1e-300).log2().round() as i32;
let key = {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
(c.key, band, bg.0, bg.1, bg.2, &drag.handles).hash(&mut h);
h.finish()
};
if c.drag_key != Some(key) {
let band_scale = 2f64.powi(band);
let list = cadcraft_render::build_entities(
doc,
drag.handles.iter().filter_map(|h| doc.entity(*h)).map(|e| e.as_ref()),
&cadcraft_render::Options { tolerance: 0.5 / band_scale, min_dash: 2.0 / band_scale, ..Default::default() },
);
let origin = if list.bounds.is_empty() { Vec2::ZERO } else { list.bounds.center() };
let mesh = crate::gpu::build_mesh(&list, origin, bg, false);
if let Ok(mut slot) = c.drag_slot.lock() {
*slot = Some(mesh);
}
c.drag_key = Some(key);
c.drag_origin = origin;
}
// Positions are relative to the origin o: M(o + p) = L·p + (M(o) − o).
let m = drag.matrix;
let o = c.drag_origin;
let t = m.apply(o) - o;
let center = xf.rect.center();
p.add(egui_wgpu::Callback::new_paint_callback(
xf.rect,
crate::gpu::CanvasCallback {
key,
slot: c.drag_slot.clone(),
offset: [(o.x - xf.center.x) as f32, (o.y - xf.center.y) as f32],
scale: xf.scale as f32,
center: [center.x, center.y],
layer: crate::gpu::LAYER_DRAG,
xform: [m.a as f32, m.b as f32, m.c as f32, m.d as f32, t.x as f32, t.y as f32],
dash: 0.0,
},
));
}
/// Draw the display list through the GPU paint callback (uploading it when it changed), plus
/// the infinite lines, which are clipped to the view on the CPU every frame.
fn draw_list_gpu(c: &mut CanvasState, p: &egui::Painter, xf: &Xf, bg: Rgb, lwdisplay: bool) {
@ -382,6 +491,9 @@ fn draw_list_gpu(c: &mut CanvasState, p: &egui::Painter, xf: &Xf, bg: Rgb, lwdis
offset: [(c.mesh_origin.x - xf.center.x) as f32, (c.mesh_origin.y - xf.center.y) as f32],
scale: xf.scale as f32,
center: [center.x, center.y],
layer: crate::gpu::LAYER_DRAWING,
xform: crate::gpu::IDENTITY_XFORM,
dash: 0.0,
},
));
let mut shapes = Vec::new();
@ -948,13 +1060,18 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
draw_highlight(&clipped, &xf, &list, &prims_of(&list, &sel), t.selection, 1.5, true);
}
}
} else if let CanvasState { list: Some(list), list_gen, prim_index, hover, .. } = &mut app.canvas {
} else if let CanvasState { list: Some(list), list_gen, prim_index, hover, gpu, hl_key, hl_slot, hl_origin, hl_infinite, .. } = &mut app.canvas {
if let Some(h) = *hover
&& !sel.contains(&h)
{
draw_highlight(&painter, &xf, list, &prim_index.prims(*list_gen, list, &[h]), t.hover, 2.0, false);
}
draw_highlight(&painter, &xf, list, &prim_index.prims(*list_gen, list, &sel), t.selection, 1.5, true);
if gpu.is_some() && !sel.is_empty() {
draw_highlight_gpu(&painter, &xf, list, *list_gen, prim_index, &sel, t.selection, 1.5, (hl_key, hl_slot, hl_origin, &mut *hl_infinite));
draw_highlight(&painter, &xf, list, hl_infinite, t.selection, 1.5, true);
} else {
draw_highlight(&painter, &xf, list, &prim_index.prims(*list_gen, list, &sel), t.selection, 1.5, true);
}
}
app.canvas.draw_ms = crate::now_ms() - t0;
compare::paint(app, ui.ctx(), &painter, &xf_paper, rect);
@ -1007,7 +1124,21 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
}
}
// Rubber band preview of the active command.
if let (Some(c), true) = (app.canvas.cursor, app.session.running.is_some()) {
let drag = match (app.canvas.cursor, app.canvas.gpu.is_some() && sheet.is_none()) {
(Some(c), true) => app.session.drag(c),
_ => None,
};
if let Some((drag, extra)) = drag {
if let Ok(st) = app.session.state() {
draw_drag_gpu(&mut app.canvas, &painter, &xf, &st.doc, &drag, bg);
let list = cadcraft_render::build_entities(
&st.doc,
extra.iter(),
&cadcraft_render::Options { tolerance: 0.5 / scale, min_dash: 2.0 / scale, ..Default::default() },
);
draw_list(&painter, &xf, &list, bg, false);
}
} else if let (Some(c), true) = (app.canvas.cursor, app.session.running.is_some()) {
let ents = app.session.preview(c);
if !ents.is_empty()
&& let Ok(st) = app.session.state()

View File

@ -25,8 +25,9 @@ use egui_wgpu::wgpu::util::DeviceExt;
/// Line/triangle vertex: position (2 × f32, relative to the origin) + sRGB colour (4 × u8).
const VERTEX_SIZE: usize = 12;
/// Wide-line / point instance: a, b (2 × f32 each) + colour (4 × u8) + width in points (f32).
const INSTANCE_SIZE: usize = 24;
/// Wide-line / point instance: a, b (2 × f32 each) + colour (4 × u8) + width in points (f32) +
/// length along its polyline before `a`, in world units (f32; dashing).
const INSTANCE_SIZE: usize = 28;
/// Vertices (or instances) per GPU buffer; divisible by 2 and 3 so no primitive straddles two.
const CHUNK: usize = 4_194_300;
/// Point markers are 1.5 × 1.5 points (as on the CPU path).
@ -127,12 +128,13 @@ fn push_vertex(out: &mut Vec<u8>, p: [f32; 2], c: [u8; 4]) {
out.extend_from_slice(&c);
}
fn push_instance(out: &mut Vec<u8>, a: [f32; 2], b: [f32; 2], c: [u8; 4], width: f32) {
fn push_instance(out: &mut Vec<u8>, a: [f32; 2], b: [f32; 2], c: [u8; 4], width: f32, start: f32) {
for v in [a[0], a[1], b[0], b[1]] {
out.extend_from_slice(&v.to_le_bytes());
}
out.extend_from_slice(&c);
out.extend_from_slice(&width.to_le_bytes());
out.extend_from_slice(&start.to_le_bytes());
}
/// Lineweight in screen points for a primitive (as on the CPU path), or `None` for a hairline.
@ -161,7 +163,7 @@ pub fn build_mesh(list: &DisplayList, origin: Vec2, bg: Rgb, lwdisplay: bool) ->
Some(w) => {
for s in pts.windows(2) {
if let [a, b] = s {
push_instance(&mut m.quads, rel(*a, origin), rel(*b, origin), col, w);
push_instance(&mut m.quads, rel(*a, origin), rel(*b, origin), col, w, 0.0);
}
}
}
@ -192,7 +194,7 @@ pub fn build_mesh(list: &DisplayList, origin: Vec2, bg: Rgb, lwdisplay: bool) ->
Kind::Point => {
if let Some(q) = pts.first() {
let p = rel(*q, origin);
push_instance(&mut m.quads, p, p, col, POINT_SIZE);
push_instance(&mut m.quads, p, p, col, POINT_SIZE, 0.0);
}
}
// Drawn on the CPU each frame (clipped to the view).
@ -202,6 +204,48 @@ pub fn build_mesh(list: &DisplayList, origin: Vec2, bg: Rgb, lwdisplay: bool) ->
m
}
/// Vertex data drawing primitives `prims` of `list` as `width`-point lines in `color` (the
/// selection highlight; dashed by the callback's `dash`). Fills are outlined by their bounding
/// box, as on the CPU path; infinite lines are left to the CPU (they are clipped per frame).
pub fn build_highlight_mesh(list: &DisplayList, prims: &[usize], origin: Vec2, color: [u8; 4], width: f32) -> MeshData {
let mut m = MeshData::default();
for prim in prims.iter().filter_map(|&i| list.prims.get(i)) {
let pts = list.points(prim);
match prim.kind {
Kind::Polyline => {
let mut run = 0.0;
for s in pts.windows(2) {
if let [a, b] = s {
push_instance(&mut m.quads, rel(*a, origin), rel(*b, origin), color, width, run as f32);
run += a.dist(*b);
}
}
}
Kind::Point => {
if let Some(q) = pts.first() {
let p = rel(*q, origin);
push_instance(&mut m.quads, p, p, color, 3.0, 0.0);
}
}
Kind::Tris | Kind::Mask => {
let bb = Bounds2::from_points(pts.iter().copied());
if !bb.is_empty() {
let c = [bb.min, Vec2::new(bb.max.x, bb.min.y), bb.max, Vec2::new(bb.min.x, bb.max.y), bb.min];
let mut run = 0.0;
for s in c.windows(2) {
if let [a, b] = s {
push_instance(&mut m.quads, rel(*a, origin), rel(*b, origin), color, 1.0, run as f32);
run += a.dist(*b);
}
}
}
}
Kind::Infinite { .. } => {}
}
}
m
}
/// The vertex origin for a display list seen at `view_center` / `view_height`: the list's centre
/// normally; at deep zoom (the view much smaller than the drawing) a point near the view, snapped
/// to a coarse grid so panning re-uploads only every few dozen screens.
@ -232,20 +276,32 @@ pub struct CanvasCallback {
pub scale: f32,
/// Canvas rect centre in points.
pub center: [f32; 2],
/// Which buffers to draw: [`LAYER_DRAWING`] or [`LAYER_DRAG`].
pub layer: usize,
/// Affine transform applied to the origin-relative positions before `offset`: the linear
/// part (a, b, c, d as in `Mat3`) and the translation. [`IDENTITY_XFORM`] for the drawing.
pub xform: [f32; 6],
/// Dash period in points for wide lines (0 = solid): the selection highlight.
pub dash: f32,
}
/// The drawing's display list.
pub const LAYER_DRAWING: usize = 0;
/// The objects a MOVE/COPY/ROTATE/SCALE preview drags, uploaded once per drag.
pub const LAYER_DRAG: usize = 1;
/// The selection highlight, rebuilt only when the selection or the drawing changes.
pub const LAYER_HIGHLIGHT: usize = 2;
pub const IDENTITY_XFORM: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
struct Batch {
buffer: wgpu::Buffer,
count: u32,
}
struct Resources {
line_pipeline: wgpu::RenderPipeline,
tri_pipeline: wgpu::RenderPipeline,
quad_pipeline: wgpu::RenderPipeline,
/// One uploaded mesh with its own view uniform (each callback's transform differs).
struct Layer {
uniform: wgpu::Buffer,
bind_group: wgpu::BindGroup,
linear_out: bool,
key: Option<u64>,
lines: Vec<Batch>,
tris: Vec<Batch>,
@ -253,23 +309,36 @@ struct Resources {
breaks: Vec<[usize; 3]>,
}
const UNIFORM_SIZE: u64 = 48;
struct Resources {
line_pipeline: wgpu::RenderPipeline,
tri_pipeline: wgpu::RenderPipeline,
quad_pipeline: wgpu::RenderPipeline,
linear_out: bool,
layers: Vec<Layer>,
}
const UNIFORM_SIZE: u64 = 80;
const SHADER: &str = r#"
struct View {
a: vec4<f32>, // offset x, offset y (world), scale (device px per unit), 1 = linear output
b: vec4<f32>, // canvas centre x, y (device px), screen w, h (device px)
c: vec4<f32>, // pixels per point
c: vec4<f32>, // pixels per point, dash period in points (0 = solid)
d: vec4<f32>, // transform linear part: a, b, c, d (x' = a x + c y, y' = b x + d y)
e: vec4<f32>, // transform translation x, y (world, origin-relative)
};
@group(0) @binding(0) var<uniform> view: View;
struct VOut {
@builtin(position) pos: vec4<f32>,
@location(0) color: vec4<f32>,
// Device pixels along a wide line from its start (dashing).
@location(1) along: f32,
};
fn to_px(p: vec2<f32>) -> vec2<f32> {
let w = (p + view.a.xy) * view.a.z;
let q = vec2<f32>(view.d.x * p.x + view.d.z * p.y, view.d.y * p.x + view.d.w * p.y) + view.e.xy;
let w = (q + view.a.xy) * view.a.z;
return vec2<f32>(view.b.x + w.x, view.b.y - w.y);
}
@ -291,6 +360,7 @@ fn vs_line(@location(0) pos: vec2<f32>, @location(1) color: vec4<f32>) -> VOut {
var o: VOut;
o.pos = px_to_clip(to_px(pos));
o.color = out_color(color);
o.along = 0.0;
return o;
}
@ -301,6 +371,7 @@ fn vs_quad(
@location(1) b: vec2<f32>,
@location(2) color: vec4<f32>,
@location(3) width: f32,
@location(4) start: f32,
) -> VOut {
let pa = to_px(a);
let pb = to_px(b);
@ -319,11 +390,18 @@ fn vs_quad(
var o: VOut;
o.pos = px_to_clip(along + n * hw * s);
o.color = out_color(color);
o.along = start * view.a.z + mix(-hw, len + hw, t);
return o;
}
@fragment
fn fs_main(v: VOut) -> @location(0) vec4<f32> {
if (view.c.y > 0.0) {
// 6 on, 4 off (as the CPU selection highlight's dashes).
if (fract(v.along / (view.c.y * view.c.x)) > 0.6) {
discard;
}
}
return v.color;
}
"#;
@ -334,7 +412,7 @@ fn vertex_layout() -> wgpu::VertexBufferLayout<'static> {
}
fn instance_layout() -> wgpu::VertexBufferLayout<'static> {
const ATTRS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Unorm8x4, 3 => Float32];
const ATTRS: [wgpu::VertexAttribute; 5] = wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Unorm8x4, 3 => Float32, 4 => Float32];
wgpu::VertexBufferLayout { array_stride: INSTANCE_SIZE as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &ATTRS }
}
@ -344,7 +422,7 @@ impl Resources {
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("cad_canvas"), source: wgpu::ShaderSource::Wgsl(SHADER.into()) });
let entry = |binding: u32| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::VERTEX,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
count: None,
};
@ -354,17 +432,20 @@ impl Resources {
bind_group_layouts: &[Some(&bgl)],
immediate_size: 0,
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cad_canvas_view"),
size: UNIFORM_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cad_canvas"),
layout: &bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
});
let layer = || {
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cad_canvas_view"),
size: UNIFORM_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cad_canvas"),
layout: &bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
});
Layer { uniform, bind_group, key: None, lines: Vec::new(), tris: Vec::new(), quads: Vec::new(), breaks: Vec::new() }
};
let pipeline = |label: &str, vs: &str, topology: wgpu::PrimitiveTopology, buffer: wgpu::VertexBufferLayout<'static>| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
@ -396,14 +477,8 @@ impl Resources {
line_pipeline: pipeline("cad_lines", "vs_line", wgpu::PrimitiveTopology::LineList, vertex_layout()),
tri_pipeline: pipeline("cad_fills", "vs_line", wgpu::PrimitiveTopology::TriangleList, vertex_layout()),
quad_pipeline: pipeline("cad_quads", "vs_quad", wgpu::PrimitiveTopology::TriangleList, instance_layout()),
uniform,
bind_group,
linear_out: target.format.is_srgb(),
key: None,
lines: Vec::new(),
tris: Vec::new(),
quads: Vec::new(),
breaks: Vec::new(),
layers: vec![layer(), layer(), layer()],
}
}
}
@ -439,6 +514,8 @@ impl egui_wgpu::CallbackTrait for CanvasCallback {
resources: &mut egui_wgpu::CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
let Some(res) = resources.get_mut::<Resources>() else { return Vec::new() };
let linear_out = res.linear_out;
let Some(res) = res.layers.get_mut(self.layer) else { return Vec::new() };
if res.key != Some(self.key) {
let pending = self.slot.lock().ok().and_then(|mut s| s.take());
if let Some(m) = pending {
@ -452,16 +529,20 @@ impl egui_wgpu::CallbackTrait for CanvasCallback {
}
let ppp = screen.pixels_per_point;
let [w, h] = screen.size_in_pixels;
let a = [self.offset[0], self.offset[1], self.scale * ppp, if res.linear_out { 1.0 } else { 0.0 }];
let a = [self.offset[0], self.offset[1], self.scale * ppp, if linear_out { 1.0 } else { 0.0 }];
let b = [self.center[0] * ppp, self.center[1] * ppp, w.max(1) as f32, h.max(1) as f32];
let c = [ppp, 0.0, 0.0, 0.0];
let bytes: Vec<u8> = a.iter().chain(&b).chain(&c).flat_map(|v| v.to_le_bytes()).collect();
let c = [ppp, self.dash, 0.0, 0.0];
let [ma, mb, mc, md, tx, ty] = self.xform;
let d = [ma, mb, mc, md];
let e = [tx, ty, 0.0, 0.0];
let bytes: Vec<u8> = a.iter().chain(&b).chain(&c).chain(&d).chain(&e).flat_map(|v| v.to_le_bytes()).collect();
queue.write_buffer(&res.uniform, 0, &bytes);
Vec::new()
}
fn paint(&self, info: egui::PaintCallbackInfo, pass: &mut wgpu::RenderPass<'static>, resources: &egui_wgpu::CallbackResources) {
let Some(res) = resources.get::<Resources>() else { return };
let Some(shared) = resources.get::<Resources>() else { return };
let Some(res) = shared.layers.get(self.layer) else { return };
if res.key != Some(self.key) {
return;
}
@ -478,7 +559,7 @@ impl egui_wgpu::CallbackTrait for CanvasCallback {
let mut from = [0usize; 3];
for to in res.breaks.iter().copied().chain(std::iter::once(all)) {
for (i, (pipeline, batches)) in
[(&res.tri_pipeline, &res.tris), (&res.line_pipeline, &res.lines), (&res.quad_pipeline, &res.quads)].into_iter().enumerate()
[(&shared.tri_pipeline, &res.tris), (&shared.line_pipeline, &res.lines), (&shared.quad_pipeline, &res.quads)].into_iter().enumerate()
{
let parts = batch_ranges(batches.iter().map(|b| b.count), from[i]..to[i]);
if parts.is_empty() {
@ -564,6 +645,20 @@ mod tests {
l
}
#[test]
fn highlight_dashes_run_on_along_each_polyline() {
let l = list();
// The 3-point polyline (two unit segments) and the point marker.
let m = build_highlight_mesh(&l, &[0, 2], Vec2::new(1e6, 1e6), [1, 2, 3, 255], 1.5);
assert_eq!(m.quad_instances(), 3);
let start = |i: usize| {
let o = i * INSTANCE_SIZE + 24;
f32::from_le_bytes([m.quads[o], m.quads[o + 1], m.quads[o + 2], m.quads[o + 3]])
};
assert_eq!([start(0), start(1), start(2)], [0.0, 1.0, 0.0]);
assert!(m.lines.is_empty() && m.tris.is_empty());
}
#[test]
fn mesh_counts_and_precision() {
let l = list();