MIRROR and PASTE previews on the GPU; GPU previews in layouts too (#655)

Squash of PR #655 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
aldozarandClaude Opus 5.5 authored and GitHub committed 2026-10-12 03:21:18 -04:00
1 parent cf797cbafe
commit d365b60704
5 files changed
+99 -16

No files matched your search

+6
View File
@@ -609,6 +609,12 @@ impl Interactive for PasteM {
_ => Ok(Step::Continue),
}
}
fn drag(&self, s: &Session, c: Vec2) -> Option<crate::prompt::Drag> {
if self.mode == PasteMode::Orig || s.clipboard.is_empty() {
return None;
}
Some(crate::prompt::Drag { handles: Vec::new(), matrix: Mat3::translate(c - s.clipboard_base), extra: Vec::new(), clipboard: true })
}
fn preview(&self, s: &Session, c: Vec2) -> Vec<cadcraft_doc::EntityKind> {
let m = Mat3::translate(c - s.clipboard_base);
s.clipboard
+43 -3
View File
@@ -1712,6 +1712,26 @@ impl SelectThen {
}
}
/// Objects whose text MIRROR keeps readable instead of mirroring it with the geometry.
fn keeps_reading_when_mirrored(k: &EntityKind) -> bool {
match k {
EntityKind::Insert(i) => !i.attribs.is_empty(),
k => matches!(
k,
EntityKind::Text(_)
| EntityKind::AttDef(_)
| EntityKind::MText(_)
| EntityKind::Dimension(_)
| EntityKind::MLeader(_)
| EntityKind::Table(_)
| EntityKind::Tolerance(_)
),
}
}
/// Text objects transformed for each frame of a MIRROR preview (on the CPU); more are not drawn.
const MAX_MIRROR_TEXT_PREVIEW: usize = 1000;
/// A finite length greater than zero (rejects 0, negatives, NaN and infinity).
fn positive_length(v: Option<f64>) -> Option<f64> {
v.filter(|f| f.is_finite() && *f > 1e-12)
@@ -1927,8 +1947,7 @@ 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.
fn drag(&self, s: &Session, c: Vec2) -> Option<crate::prompt::Drag> {
if (self.op == Op::Rotate && self.reference) || !self.sel.done {
return None;
}
@@ -1938,9 +1957,30 @@ impl Interactive for SelectThen {
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)))?),
Op::Mirror if !self.ask_erase => Mat3::mirror(base, c),
_ => return None,
};
Some(crate::prompt::Drag { handles: self.objs.clone(), matrix, extra: vec![line(base, c)] })
let mut extra = vec![line(base, c)];
if self.op != Op::Mirror {
return Some(crate::prompt::Drag { handles: self.objs.clone(), matrix, extra, clipboard: false });
}
// Mirrored text keeps reading left to right (MIRRTEXT 0), which the matrix alone can't
// draw: those objects are transformed one by one, the rest is mirrored on the GPU.
let d = s.doc().ok()?;
let mut handles = Vec::with_capacity(self.objs.len());
let mut texts = 0;
for h in &self.objs {
let Some(e) = d.entity(*h) else { continue };
if !keeps_reading_when_mirrored(&e.kind) {
handles.push(*h);
} else if texts < MAX_MIRROR_TEXT_PREVIEW {
texts += 1;
let mut k = e.kind.clone();
k.transform(&matrix);
extra.push(k);
}
}
Some(crate::prompt::Drag { handles, matrix, extra, clipboard: false })
}
}
+4 -1
View File
@@ -178,8 +178,11 @@ pub trait Interactive: Send {
pub struct Drag {
pub handles: Vec<Handle>,
pub matrix: Mat3,
/// Rubber-band geometry drawn as well (the base-to-cursor line).
/// Rubber-band geometry drawn as well (the base-to-cursor line), and dragged objects the
/// matrix can't draw (text MIRROR keeps readable), already transformed.
pub extra: Vec<EntityKind>,
/// The dragged objects are the clipboard's ([`Session::clipboard`]), not `handles`.
pub clipboard: bool,
}
/// Parse a typed point: `x,y[,z]`, `@dx,dy`, `@dist<angle`, `dist<angle`, `#x,y` (absolute),
+23 -4
View File
@@ -361,10 +361,29 @@ fn copy_and_rotate_drag_as_a_transform_of_the_selection() {
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());
// MIRROR: geometry is mirrored by the matrix; text, which stays readable, is transformed
// object by object and comes with the extra geometry.
let mut s = Session::new();
s.execute("line", &json!({"points": [[0, 0], [1, 0]]})).unwrap();
s.execute("text", &json!({"at": [2, 2], "height": 1, "text": "A"})).unwrap();
for t in ["mirror", "all", "", "0,0"] {
s.cmdline(t).unwrap();
}
let (d, extra) = s.drag(Vec2::new(0.0, 5.0)).expect("MIRROR drags its selection");
assert_eq!(d.handles.len(), 1, "the line is mirrored by the matrix");
assert!(d.matrix.apply(Vec2::new(1.0, 0.0)).near(Vec2::new(-1.0, 0.0), 1e-9));
assert_eq!(extra.len(), 2, "the base-to-cursor line and the text");
let EntityKind::Text(t) = &extra[1].kind else { panic!("expected the text") };
assert!(t.insert.xy().near(Vec2::new(-2.0, 2.0), 1e-9) && t.rotation.abs() < 1e-9, "mirrored, still reading left to right");
// PASTECLIP drags the clipboard.
let mut s = Session::new();
let l = s.execute("line", &json!({"points": [[0, 0], [1, 0]]})).unwrap();
s.execute("copyclip", &json!({"handles": [l["handles"][0].clone()]})).unwrap();
s.cmdline("pasteclip").unwrap();
let (d, _) = s.drag(Vec2::new(5.0, 5.0)).expect("PASTECLIP drags the clipboard");
assert!(d.clipboard && d.handles.is_empty());
assert!(d.matrix.apply(s.clipboard_base).near(Vec2::new(5.0, 5.0), 1e-9));
}
#[test]
+23 -8
View File
@@ -426,22 +426,35 @@ fn draw_highlight_gpu(
/// 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) {
/// `clipboard` is the session's clipboard and its serial (dragged when `drag.clipboard`, by PASTE).
fn draw_drag_gpu(
c: &mut CanvasState,
p: &egui::Painter,
xf: &Xf,
doc: &cadcraft_doc::Drawing,
drag: &cadcraft_engine::Drag,
clipboard: (&[cadcraft_doc::Entity], u64),
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);
if drag.clipboard {
clipboard.1.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 opts = cadcraft_render::Options { tolerance: 0.5 / band_scale, min_dash: 2.0 / band_scale, ..Default::default() };
let list = if drag.clipboard {
cadcraft_render::build_entities(doc, clipboard.0.iter(), &opts)
} else {
cadcraft_render::build_entities(doc, drag.handles.iter().filter_map(|h| doc.entity(*h)).map(|e| e.as_ref()), &opts)
};
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() {
@@ -1174,13 +1187,15 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
}
}
// Rubber band preview of the active command.
let drag = match (app.canvas.cursor, app.canvas.gpu.is_some() && sheet.is_none()) {
// `xf` is the space being edited: paper space, or model space inside the active viewport.
let drag = match (app.canvas.cursor, app.canvas.gpu.is_some()) {
(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 serial = app.session.clipboard_source.as_ref().map_or(0, |c| c.serial);
draw_drag_gpu(&mut app.canvas, &painter, &xf, &st.doc, &drag, (&app.session.clipboard, serial), bg);
let list = cadcraft_render::build_entities(
&st.doc,
extra.iter(),