mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
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:
1 parent
cf797cbafe
commit
d365b60704
5 files changed
+99
-16
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user