mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
1 parent
40a93b7dc4
commit
1bc7d7baad
2 files changed
+127
-2
No files matched your search
@@ -172,11 +172,16 @@ pub fn keys(app: &mut PhotocraftApp, ctx: &egui::Context) -> bool {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if app.distort.puppet.is_some() {
|
||||
if let Some(p) = app.distort.puppet.as_mut() {
|
||||
let cmd_shift = egui::Modifiers::COMMAND | egui::Modifiers::SHIFT;
|
||||
if enter(ctx) {
|
||||
crate::puppet_ui::commit(app);
|
||||
} else if esc(ctx) {
|
||||
app.distort.puppet = None;
|
||||
} else if ctx.input_mut(|i| i.consume_key(cmd_shift, egui::Key::Z)) {
|
||||
p.redo();
|
||||
} else if ctx.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Z)) {
|
||||
p.undo();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Edit › Puppet Warp on the canvas: click on the mesh to add pins, drag pins to move them,
|
||||
//! ⌥-click a pin to remove it; the options bar sets mode, density, expansion, mesh visibility,
|
||||
//! the selected pin's depth and rotation. ↩ / Commit runs `edit.puppetWarp` with the pins.
|
||||
//! the selected pin's depth and rotation. ⌘Z / ⌘⇧Z undo and redo pin edits inside the session
|
||||
//! (#4440). ↩ / Commit runs `edit.puppetWarp` with the pins.
|
||||
//!
|
||||
//! The preview is the layer texture on the deformed triangle mesh (an egui textured mesh, drawn
|
||||
//! by the GPU), re-solved with a few warm-started ARAP rounds per drag event (well under a
|
||||
@@ -37,9 +38,47 @@ pub struct PuppetSession {
|
||||
texture: TextureHandle,
|
||||
opacity: f32,
|
||||
pub preview_doc: Arc<Document>,
|
||||
/// Pins before each edit of this session, newest last (⌘Z), and the edits undone (⌘⇧Z).
|
||||
undo: Vec<Vec<PuppetPin>>,
|
||||
redo: Vec<Vec<PuppetPin>>,
|
||||
/// The pins when the current drag started: one undo step per drag that moved something.
|
||||
drag_from: Option<Vec<PuppetPin>>,
|
||||
}
|
||||
|
||||
impl PuppetSession {
|
||||
/// Records `before` as one undoable pin edit.
|
||||
fn record(&mut self, before: Vec<PuppetPin>) {
|
||||
if before != self.warp.pins {
|
||||
self.undo.push(before);
|
||||
self.redo.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts `pins` back (undo or redo), keeping the selection when that pin still exists.
|
||||
fn restore(&mut self, pins: Vec<PuppetPin>) {
|
||||
self.warp.pins = pins;
|
||||
self.selected = self.selected.filter(|i| *i < self.warp.pins.len());
|
||||
self.drag = None;
|
||||
self.drag_from = None;
|
||||
self.rebind();
|
||||
}
|
||||
|
||||
/// ⌘Z: the last pin edit undone. False when there is none.
|
||||
pub fn undo(&mut self) -> bool {
|
||||
let Some(pins) = self.undo.pop() else { return false };
|
||||
self.redo.push(std::mem::replace(&mut self.warp.pins, pins.clone()));
|
||||
self.restore(pins);
|
||||
true
|
||||
}
|
||||
|
||||
/// ⌘⇧Z: the last undone pin edit redone. False when there is none.
|
||||
pub fn redo(&mut self) -> bool {
|
||||
let Some(pins) = self.redo.pop() else { return false };
|
||||
self.undo.push(std::mem::replace(&mut self.warp.pins, pins.clone()));
|
||||
self.restore(pins);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn describe(&self) -> Value {
|
||||
json!({
|
||||
"layer": self.layer.0,
|
||||
@@ -162,6 +201,9 @@ pub fn begin(app: &mut PhotocraftApp, ctx: &egui::Context, command: &str) -> Res
|
||||
texture,
|
||||
opacity,
|
||||
preview_doc,
|
||||
undo: Vec::new(),
|
||||
redo: Vec::new(),
|
||||
drag_from: None,
|
||||
};
|
||||
s.rebuild_mesh();
|
||||
if s.mesh.tris.is_empty() {
|
||||
@@ -192,6 +234,14 @@ pub fn control(app: &mut PhotocraftApp, ui: &Value) -> Result<Value, String> {
|
||||
return Ok(json!({"cancelled": true}));
|
||||
}
|
||||
let s = app.distort.puppet.as_mut().ok_or(tl!("Puppet Warp is not active"))?;
|
||||
if ui.get("undo").and_then(Value::as_bool) == Some(true) {
|
||||
s.undo();
|
||||
return Ok(s.describe());
|
||||
}
|
||||
if ui.get("redo").and_then(Value::as_bool) == Some(true) {
|
||||
s.redo();
|
||||
return Ok(s.describe());
|
||||
}
|
||||
let mut remesh = false;
|
||||
let (density, expansion) = (s.warp.density, s.warp.expansion);
|
||||
if let Some(m) = ui.get("mode").and_then(Value::as_str) {
|
||||
@@ -222,14 +272,17 @@ pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers) {
|
||||
match ev {
|
||||
ToolEvent::Down { x, y, .. } => {
|
||||
let p = [x, y];
|
||||
let before = s.warp.pins.clone();
|
||||
if let Some(i) = s.pin_at(p, tol) {
|
||||
if mods.alt {
|
||||
s.warp.pins.remove(i);
|
||||
s.selected = None;
|
||||
s.rebind();
|
||||
s.record(before);
|
||||
} else {
|
||||
s.selected = Some(i);
|
||||
s.drag = Some(i);
|
||||
s.drag_from = Some(before);
|
||||
}
|
||||
} else if !mods.alt
|
||||
&& let Some(src) = s.rest_of(p)
|
||||
@@ -238,6 +291,8 @@ pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers) {
|
||||
s.selected = Some(s.warp.pins.len() - 1);
|
||||
s.drag = s.selected;
|
||||
s.rebind();
|
||||
// Placing a pin is one step; dragging it on from there is part of that step.
|
||||
s.record(before);
|
||||
}
|
||||
}
|
||||
ToolEvent::Move { x, y, .. } => {
|
||||
@@ -255,6 +310,9 @@ pub fn pointer(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers) {
|
||||
pin.dst = [x, y];
|
||||
}
|
||||
s.solve(true);
|
||||
if let Some(before) = s.drag_from.take() {
|
||||
s.record(before);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,6 +363,8 @@ pub fn draw(s: &PuppetSession, painter: &egui::Painter, xf: &ViewXform) {
|
||||
|
||||
pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
let Some(s) = app.distort.puppet.as_mut() else { return };
|
||||
// Pin depth, rotation and Remove All Pins are undoable pin edits too.
|
||||
let pins_before = s.warp.pins.clone();
|
||||
let mut remesh = false;
|
||||
let mut resolve = false;
|
||||
let mut rebind = false;
|
||||
@@ -369,6 +429,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
s.selected = None;
|
||||
rebind = true;
|
||||
}
|
||||
s.record(pins_before);
|
||||
if remesh {
|
||||
// A refused value snaps the fields back to the previous settings.
|
||||
let _ = s.remesh_or_restore(density, expansion);
|
||||
@@ -393,6 +454,65 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// #4440: ⌘Z / ⌘⇧Z undo and redo pin edits while Puppet Warp is open: placing a pin,
|
||||
/// dragging one and removing one are a step each; a click that moves nothing isn't one.
|
||||
#[test]
|
||||
fn pin_edits_undo_and_redo_inside_the_session() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
app.session.execute("file.new", json!({"width": 100, "height": 80, "depth": 8})).unwrap();
|
||||
app.session.execute("layer.new.layer", json!({})).unwrap();
|
||||
app.session
|
||||
.edit("paint", |doc, active| {
|
||||
doc.layer_mut(active.unwrap()).unwrap().surface_mut().unwrap().fill_rect(Rect::new(20, 30, 80, 50), &[1.0, 0.0, 0.0, 1.0]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
app.sync_views();
|
||||
crate::distort_ui::menu(&mut app, &ctx, "edit.puppetWarp", &json!({})).unwrap().unwrap();
|
||||
let ev = |app: &mut PhotocraftApp, e| crate::distort_ui::pointer(app, e, egui::Modifiers::NONE);
|
||||
let pins = |app: &PhotocraftApp| app.distort.puppet.as_ref().unwrap().warp.pins.iter().map(|p| p.dst).collect::<Vec<_>>();
|
||||
ev(&mut app, ToolEvent::Down { x: 25.0, y: 40.0, pressure: 1.0 });
|
||||
ev(&mut app, ToolEvent::Up { x: 25.0, y: 40.0 });
|
||||
ev(&mut app, ToolEvent::Down { x: 75.0, y: 40.0, pressure: 1.0 });
|
||||
ev(&mut app, ToolEvent::Up { x: 75.0, y: 40.0 });
|
||||
// Drag the second pin up; then a click on it that moves nothing.
|
||||
ev(&mut app, ToolEvent::Down { x: 75.0, y: 40.0, pressure: 1.0 });
|
||||
ev(&mut app, ToolEvent::Move { x: 75.0, y: 30.0, pressure: 1.0 });
|
||||
ev(&mut app, ToolEvent::Up { x: 75.0, y: 20.0 });
|
||||
ev(&mut app, ToolEvent::Down { x: 75.0, y: 20.0, pressure: 1.0 });
|
||||
ev(&mut app, ToolEvent::Up { x: 75.0, y: 20.0 });
|
||||
crate::distort_ui::pointer(&mut app, ToolEvent::Down { x: 25.0, y: 40.0, pressure: 1.0 }, egui::Modifiers { alt: true, ..Default::default() });
|
||||
assert_eq!(pins(&app), [[75.0, 20.0]]);
|
||||
// ⌘Z, through the keyboard: the removed pin comes back, then the drag is undone.
|
||||
let press = |app: &mut PhotocraftApp, modifiers: egui::Modifiers| {
|
||||
let ev = egui::Event::Key { key: egui::Key::Z, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
let input = egui::RawInput { events: vec![ev], ..Default::default() };
|
||||
ctx.run_ui(input, |ui| assert!(crate::distort_ui::keys(app, ui.ctx()))).textures_delta.clear();
|
||||
};
|
||||
press(&mut app, egui::Modifiers::COMMAND);
|
||||
assert_eq!(pins(&app), [[25.0, 40.0], [75.0, 20.0]]);
|
||||
press(&mut app, egui::Modifiers::COMMAND);
|
||||
assert_eq!(pins(&app), [[25.0, 40.0], [75.0, 40.0]], "the drag, not the click after it");
|
||||
// ⌘⇧Z redoes it; a new edit after an undo drops what was undone.
|
||||
press(&mut app, egui::Modifiers::COMMAND | egui::Modifiers::SHIFT);
|
||||
assert_eq!(pins(&app), [[25.0, 40.0], [75.0, 20.0]]);
|
||||
control(&mut app, &json!({"undo": true})).unwrap();
|
||||
control(&mut app, &json!({"undo": true})).unwrap();
|
||||
assert_eq!(pins(&app), [[25.0, 40.0]]);
|
||||
ev(&mut app, ToolEvent::Down { x: 50.0, y: 40.0, pressure: 1.0 });
|
||||
ev(&mut app, ToolEvent::Up { x: 50.0, y: 40.0 });
|
||||
control(&mut app, &json!({"redo": true})).unwrap();
|
||||
assert_eq!(pins(&app), [[25.0, 40.0], [50.0, 40.0]], "nothing left to redo");
|
||||
// Undo past the first pin stops there, and the document's History is untouched.
|
||||
let past = app.session.active().unwrap().history.past_len();
|
||||
for _ in 0..5 {
|
||||
control(&mut app, &json!({"undo": true})).unwrap();
|
||||
}
|
||||
assert!(pins(&app).is_empty());
|
||||
assert_eq!(app.session.active().unwrap().history.past_len(), past);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pins_drag_and_commit_through_the_engine() {
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
Reference in new issue
Block a user