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:
parent
244feb6e89
commit
1e1f68bf40
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 ----------------
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue