fix double paste and make selections always floating
This commit is contained in:
parent
6f1a706dd2
commit
73ef9e3b9c
|
|
@ -88,6 +88,13 @@ pub enum ToolState {
|
||||||
current: (i32, i32),
|
current: (i32, i32),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Moving an existing raster selection (and its floating pixels, if any).
|
||||||
|
MovingRasterSelection {
|
||||||
|
/// Canvas position of the pointer at the last processed event, used to
|
||||||
|
/// compute per-frame deltas.
|
||||||
|
last: (i32, i32),
|
||||||
|
},
|
||||||
|
|
||||||
/// Dragging selected objects
|
/// Dragging selected objects
|
||||||
DraggingSelection {
|
DraggingSelection {
|
||||||
start_pos: Point,
|
start_pos: Point,
|
||||||
|
|
|
||||||
|
|
@ -1963,27 +1963,62 @@ impl EditorApp {
|
||||||
kf.raw_pixels = float.canvas_before;
|
kf.raw_pixels = float.canvas_before;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drop (discard) the floating selection keeping the hole punched in the
|
||||||
|
/// canvas. Records a `RasterStrokeAction` for undo. Used by cut (Ctrl+X).
|
||||||
|
fn drop_raster_float(&mut self) {
|
||||||
|
use lightningbeam_core::layer::AnyLayer;
|
||||||
|
use lightningbeam_core::actions::RasterStrokeAction;
|
||||||
|
|
||||||
|
let Some(float) = self.selection.raster_floating.take() else { return };
|
||||||
|
self.selection.raster_selection = None;
|
||||||
|
|
||||||
|
let doc = self.action_executor.document_mut();
|
||||||
|
let Some(AnyLayer::Raster(rl)) = doc.get_layer_mut(&float.layer_id) else { return };
|
||||||
|
let Some(kf) = rl.keyframe_at_mut(float.time) else { return };
|
||||||
|
// raw_pixels already has the hole; record the undo action.
|
||||||
|
let canvas_after = kf.raw_pixels.clone();
|
||||||
|
let (w, h) = (kf.width, kf.height);
|
||||||
|
let action = RasterStrokeAction::new(
|
||||||
|
float.layer_id, float.time,
|
||||||
|
float.canvas_before, canvas_after,
|
||||||
|
w, h,
|
||||||
|
);
|
||||||
|
if let Err(e) = self.action_executor.execute(Box::new(action)) {
|
||||||
|
eprintln!("drop_raster_float: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Copy the current selection to the clipboard
|
/// Copy the current selection to the clipboard
|
||||||
fn clipboard_copy_selection(&mut self) {
|
fn clipboard_copy_selection(&mut self) {
|
||||||
use lightningbeam_core::clipboard::{ClipboardContent, ClipboardLayerType};
|
use lightningbeam_core::clipboard::{ClipboardContent, ClipboardLayerType};
|
||||||
use lightningbeam_core::layer::AnyLayer;
|
use lightningbeam_core::layer::AnyLayer;
|
||||||
|
|
||||||
// Raster selection takes priority when on a raster layer
|
// Raster selection takes priority when on a raster layer.
|
||||||
if let (Some(layer_id), Some(raster_sel)) = (
|
// If a floating selection exists (auto-lifted pixels), read directly from
|
||||||
self.active_layer_id,
|
// the float so we get exactly the lifted pixels.
|
||||||
self.selection.raster_selection.as_ref(),
|
if let Some(layer_id) = self.active_layer_id {
|
||||||
) {
|
|
||||||
let document = self.action_executor.document();
|
let document = self.action_executor.document();
|
||||||
if let Some(AnyLayer::Raster(rl)) = document.get_layer(&layer_id) {
|
if matches!(document.get_layer(&layer_id), Some(AnyLayer::Raster(_))) {
|
||||||
if let Some(kf) = rl.keyframe_at(self.playback_time) {
|
if let Some(float) = &self.selection.raster_floating {
|
||||||
let (pixels, w, h) = Self::extract_raster_selection(
|
|
||||||
&kf.raw_pixels, kf.width, kf.height, raster_sel,
|
|
||||||
);
|
|
||||||
self.clipboard_manager.copy(ClipboardContent::RasterPixels {
|
self.clipboard_manager.copy(ClipboardContent::RasterPixels {
|
||||||
pixels, width: w, height: h,
|
pixels: float.pixels.clone(),
|
||||||
|
width: float.width,
|
||||||
|
height: float.height,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
} else if let Some(raster_sel) = self.selection.raster_selection.as_ref() {
|
||||||
|
if let Some(AnyLayer::Raster(rl)) = document.get_layer(&layer_id) {
|
||||||
|
if let Some(kf) = rl.keyframe_at(self.playback_time) {
|
||||||
|
let (pixels, w, h) = Self::extract_raster_selection(
|
||||||
|
&kf.raw_pixels, kf.width, kf.height, raster_sel,
|
||||||
|
);
|
||||||
|
self.clipboard_manager.copy(ClipboardContent::RasterPixels {
|
||||||
|
pixels, width: w, height: h,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2062,15 +2097,24 @@ impl EditorApp {
|
||||||
use lightningbeam_core::layer::AnyLayer;
|
use lightningbeam_core::layer::AnyLayer;
|
||||||
use lightningbeam_core::actions::RasterStrokeAction;
|
use lightningbeam_core::actions::RasterStrokeAction;
|
||||||
|
|
||||||
// Raster: commit any floating selection first, then erase the marquee region
|
// Raster: if a floating selection exists (auto-lifted), just drop it
|
||||||
|
// (keeps the hole). Otherwise commit any float then erase the marquee region.
|
||||||
|
if let Some(layer_id) = self.active_layer_id {
|
||||||
|
let document = self.action_executor.document();
|
||||||
|
if matches!(document.get_layer(&layer_id), Some(AnyLayer::Raster(_))) {
|
||||||
|
if self.selection.raster_floating.is_some() {
|
||||||
|
self.drop_raster_float();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let (Some(layer_id), Some(raster_sel)) = (
|
if let (Some(layer_id), Some(raster_sel)) = (
|
||||||
self.active_layer_id,
|
self.active_layer_id,
|
||||||
self.selection.raster_selection.clone(),
|
self.selection.raster_selection.clone(),
|
||||||
) {
|
) {
|
||||||
let document = self.action_executor.document();
|
let document = self.action_executor.document();
|
||||||
if matches!(document.get_layer(&layer_id), Some(AnyLayer::Raster(_))) {
|
if matches!(document.get_layer(&layer_id), Some(AnyLayer::Raster(_))) {
|
||||||
// Committing a floating selection before erasing ensures any
|
|
||||||
// prior paste is baked in before we punch the new hole.
|
|
||||||
self.commit_raster_floating();
|
self.commit_raster_floating();
|
||||||
|
|
||||||
let document = self.action_executor.document_mut();
|
let document = self.action_executor.document_mut();
|
||||||
|
|
@ -2314,6 +2358,13 @@ impl EditorApp {
|
||||||
}
|
}
|
||||||
ClipboardContent::RasterPixels { pixels, width, height } => {
|
ClipboardContent::RasterPixels { pixels, width, height } => {
|
||||||
let Some(layer_id) = self.active_layer_id else { return };
|
let Some(layer_id) = self.active_layer_id else { return };
|
||||||
|
|
||||||
|
// Commit any pre-existing floating selection FIRST so that
|
||||||
|
// canvas_before captures the fully-composited state (not the
|
||||||
|
// pre-commit state, which would corrupt the undo snapshot).
|
||||||
|
self.commit_raster_floating();
|
||||||
|
|
||||||
|
// Re-borrow the document after commit to get post-commit state.
|
||||||
let document = self.action_executor.document();
|
let document = self.action_executor.document();
|
||||||
let layer = document.get_layer(&layer_id);
|
let layer = document.get_layer(&layer_id);
|
||||||
let Some(AnyLayer::Raster(rl)) = layer else { return };
|
let Some(AnyLayer::Raster(rl)) = layer else { return };
|
||||||
|
|
@ -2326,15 +2377,12 @@ impl EditorApp {
|
||||||
.map(|s| { let (x0, y0, _, _) = s.bounding_rect(); (x0, y0) })
|
.map(|s| { let (x0, y0, _, _) = s.bounding_rect(); (x0, y0) })
|
||||||
.unwrap_or((0, 0));
|
.unwrap_or((0, 0));
|
||||||
|
|
||||||
// Snapshot canvas before for undo on commit / restore on cancel.
|
// Snapshot canvas AFTER commit for correct undo on commit / restore on cancel.
|
||||||
let canvas_before = kf.raw_pixels.clone();
|
let canvas_before = kf.raw_pixels.clone();
|
||||||
let canvas_w = kf.width;
|
let canvas_w = kf.width;
|
||||||
let canvas_h = kf.height;
|
let canvas_h = kf.height;
|
||||||
drop(kf); // release immutable borrow before taking mutable
|
drop(kf); // release immutable borrow before taking mutable
|
||||||
|
|
||||||
// Commit any pre-existing floating selection before creating a new one.
|
|
||||||
self.commit_raster_floating();
|
|
||||||
|
|
||||||
use lightningbeam_core::selection::{RasterFloatingSelection, RasterSelection};
|
use lightningbeam_core::selection::{RasterFloatingSelection, RasterSelection};
|
||||||
self.selection.raster_floating = Some(RasterFloatingSelection {
|
self.selection.raster_floating = Some(RasterFloatingSelection {
|
||||||
pixels,
|
pixels,
|
||||||
|
|
@ -5821,17 +5869,21 @@ impl eframe::App for EditorApp {
|
||||||
// Event::Copy/Cut/Paste instead of regular key events, so
|
// Event::Copy/Cut/Paste instead of regular key events, so
|
||||||
// check_shortcuts won't see them via key_pressed().
|
// check_shortcuts won't see them via key_pressed().
|
||||||
// Skip if a pane (e.g. piano roll) already handled the clipboard event.
|
// Skip if a pane (e.g. piano roll) already handled the clipboard event.
|
||||||
|
let mut clipboard_handled = clipboard_consumed;
|
||||||
if !clipboard_consumed {
|
if !clipboard_consumed {
|
||||||
for event in &i.events {
|
for event in &i.events {
|
||||||
match event {
|
match event {
|
||||||
egui::Event::Copy => {
|
egui::Event::Copy => {
|
||||||
self.handle_menu_action(MenuAction::Copy);
|
self.handle_menu_action(MenuAction::Copy);
|
||||||
|
clipboard_handled = true;
|
||||||
}
|
}
|
||||||
egui::Event::Cut => {
|
egui::Event::Cut => {
|
||||||
self.handle_menu_action(MenuAction::Cut);
|
self.handle_menu_action(MenuAction::Cut);
|
||||||
|
clipboard_handled = true;
|
||||||
}
|
}
|
||||||
egui::Event::Paste(_) => {
|
egui::Event::Paste(_) => {
|
||||||
self.handle_menu_action(MenuAction::Paste);
|
self.handle_menu_action(MenuAction::Paste);
|
||||||
|
clipboard_handled = true;
|
||||||
}
|
}
|
||||||
// When text/plain is absent from the system clipboard egui-winit
|
// When text/plain is absent from the system clipboard egui-winit
|
||||||
// falls through to a Key event instead of Event::Paste.
|
// falls through to a Key event instead of Event::Paste.
|
||||||
|
|
@ -5842,6 +5894,7 @@ impl eframe::App for EditorApp {
|
||||||
..
|
..
|
||||||
} if modifiers.ctrl || modifiers.command => {
|
} if modifiers.ctrl || modifiers.command => {
|
||||||
self.handle_menu_action(MenuAction::Paste);
|
self.handle_menu_action(MenuAction::Paste);
|
||||||
|
clipboard_handled = true;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -5850,10 +5903,15 @@ impl eframe::App for EditorApp {
|
||||||
|
|
||||||
// Check menu shortcuts that use modifiers (Cmd+S, etc.) - allow even when typing
|
// Check menu shortcuts that use modifiers (Cmd+S, etc.) - allow even when typing
|
||||||
// But skip shortcuts without modifiers when keyboard input is claimed (e.g., virtual piano)
|
// But skip shortcuts without modifiers when keyboard input is claimed (e.g., virtual piano)
|
||||||
|
// Also skip clipboard actions (Copy/Cut/Paste) if already handled above to prevent
|
||||||
|
// double-firing when egui emits both Event::Key{V} and key_pressed(V) is true.
|
||||||
if let Some(action) = MenuSystem::check_shortcuts(i, Some(&self.keymap)) {
|
if let Some(action) = MenuSystem::check_shortcuts(i, Some(&self.keymap)) {
|
||||||
|
let is_clipboard = matches!(action, MenuAction::Copy | MenuAction::Cut | MenuAction::Paste);
|
||||||
// Only trigger if keyboard isn't claimed OR the shortcut uses modifiers
|
// Only trigger if keyboard isn't claimed OR the shortcut uses modifiers
|
||||||
if !wants_keyboard || i.modifiers.ctrl || i.modifiers.command || i.modifiers.alt || i.modifiers.shift {
|
if !wants_keyboard || i.modifiers.ctrl || i.modifiers.command || i.modifiers.alt || i.modifiers.shift {
|
||||||
self.handle_menu_action(action);
|
if !(is_clipboard && clipboard_handled) {
|
||||||
|
self.handle_menu_action(action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2284,6 +2284,10 @@ pub struct StagePane {
|
||||||
/// and updating raw_pixels, so the canvas lives one extra composite frame to
|
/// and updating raw_pixels, so the canvas lives one extra composite frame to
|
||||||
/// avoid a flash of the stale Vello scene.
|
/// avoid a flash of the stale Vello scene.
|
||||||
pending_canvas_removal: Option<uuid::Uuid>,
|
pending_canvas_removal: Option<uuid::Uuid>,
|
||||||
|
/// Selection outline saved at stroke mouse-down for post-readback pixel masking.
|
||||||
|
/// Pixels outside the selection are restored from `buffer_before` so strokes
|
||||||
|
/// only affect the area inside the selection outline.
|
||||||
|
stroke_clip_selection: Option<lightningbeam_core::selection::RasterSelection>,
|
||||||
/// Synthetic drag/click override for test mode replay (debug builds only)
|
/// Synthetic drag/click override for test mode replay (debug builds only)
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
replay_override: Option<ReplayDragState>,
|
replay_override: Option<ReplayDragState>,
|
||||||
|
|
@ -2405,6 +2409,7 @@ impl StagePane {
|
||||||
pending_undo_before: None,
|
pending_undo_before: None,
|
||||||
painting_canvas: None,
|
painting_canvas: None,
|
||||||
pending_canvas_removal: None,
|
pending_canvas_removal: None,
|
||||||
|
stroke_clip_selection: None,
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
replay_override: None,
|
replay_override: None,
|
||||||
}
|
}
|
||||||
|
|
@ -4434,6 +4439,63 @@ impl StagePane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lift the pixels enclosed by the current `raster_selection` into a
|
||||||
|
/// `RasterFloatingSelection`, punching a transparent hole in `raw_pixels`.
|
||||||
|
///
|
||||||
|
/// Call this immediately after a marquee / lasso selection is finalized so
|
||||||
|
/// that all downstream operations (drag-move, copy, cut, stroke-masking)
|
||||||
|
/// see a consistent `raster_floating` whenever a selection is active.
|
||||||
|
fn lift_selection_to_float(shared: &mut SharedPaneState) {
|
||||||
|
use lightningbeam_core::layer::AnyLayer;
|
||||||
|
use lightningbeam_core::selection::RasterFloatingSelection;
|
||||||
|
|
||||||
|
// Clone the selection before any mutable borrows.
|
||||||
|
let Some(sel) = shared.selection.raster_selection.clone() else { return };
|
||||||
|
let Some(layer_id) = *shared.active_layer_id else { return };
|
||||||
|
let time = *shared.playback_time;
|
||||||
|
|
||||||
|
// Commit any existing float first (clears raster_selection — re-set below).
|
||||||
|
Self::commit_raster_floating_now(shared);
|
||||||
|
|
||||||
|
let doc = shared.action_executor.document_mut();
|
||||||
|
let Some(AnyLayer::Raster(rl)) = doc.get_layer_mut(&layer_id) else { return };
|
||||||
|
let Some(kf) = rl.keyframe_at_mut(time) else { return };
|
||||||
|
|
||||||
|
let canvas_before = kf.raw_pixels.clone();
|
||||||
|
let (x0, y0, x1, y1) = sel.bounding_rect();
|
||||||
|
let w = (x1 - x0).max(0) as u32;
|
||||||
|
let h = (y1 - y0).max(0) as u32;
|
||||||
|
if w == 0 || h == 0 { return; }
|
||||||
|
|
||||||
|
let mut float_pixels = vec![0u8; (w * h * 4) as usize];
|
||||||
|
for row in 0..h {
|
||||||
|
let sy = y0 + row as i32;
|
||||||
|
if sy < 0 || sy >= kf.height as i32 { continue; }
|
||||||
|
for col in 0..w {
|
||||||
|
let sx = x0 + col as i32;
|
||||||
|
if sx < 0 || sx >= kf.width as i32 { continue; }
|
||||||
|
if !sel.contains_pixel(sx, sy) { continue; }
|
||||||
|
let si = ((sy as u32 * kf.width + sx as u32) * 4) as usize;
|
||||||
|
let di = ((row * w + col) * 4) as usize;
|
||||||
|
float_pixels[di..di + 4].copy_from_slice(&kf.raw_pixels[si..si + 4]);
|
||||||
|
kf.raw_pixels[si..si + 4].fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-set selection (commit_raster_floating_now cleared it) and create float.
|
||||||
|
shared.selection.raster_selection = Some(sel);
|
||||||
|
shared.selection.raster_floating = Some(RasterFloatingSelection {
|
||||||
|
pixels: float_pixels,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
x: x0,
|
||||||
|
y: y0,
|
||||||
|
layer_id,
|
||||||
|
time,
|
||||||
|
canvas_before,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// `self.pending_raster_dabs` for dispatch by `VelloCallback::prepare()`.
|
/// `self.pending_raster_dabs` for dispatch by `VelloCallback::prepare()`.
|
||||||
///
|
///
|
||||||
/// The actual pixel rendering happens on the GPU (compute shader). The CPU
|
/// The actual pixel rendering happens on the GPU (compute shader). The CPU
|
||||||
|
|
@ -4490,6 +4552,10 @@ impl StagePane {
|
||||||
// Mouse down: capture buffer_before, start stroke, compute first dab
|
// Mouse down: capture buffer_before, start stroke, compute first dab
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
if self.rsp_drag_started(response) || self.rsp_clicked(response) {
|
if self.rsp_drag_started(response) || self.rsp_clicked(response) {
|
||||||
|
// Save selection BEFORE commit clears it — used after readback to
|
||||||
|
// mask the stroke result so only pixels inside the outline change.
|
||||||
|
self.stroke_clip_selection = shared.selection.raster_selection.clone();
|
||||||
|
|
||||||
// Commit any floating selection synchronously so buffer_before and
|
// Commit any floating selection synchronously so buffer_before and
|
||||||
// the GPU canvas initial upload see the fully-composited canvas.
|
// the GPU canvas initial upload see the fully-composited canvas.
|
||||||
Self::commit_raster_floating_now(shared);
|
Self::commit_raster_floating_now(shared);
|
||||||
|
|
@ -4695,42 +4761,88 @@ impl StagePane {
|
||||||
let (canvas_w, canvas_h) = (kf.width as i32, kf.height as i32);
|
let (canvas_w, canvas_h) = (kf.width as i32, kf.height as i32);
|
||||||
|
|
||||||
if self.rsp_drag_started(response) {
|
if self.rsp_drag_started(response) {
|
||||||
Self::commit_raster_floating_now(shared);
|
|
||||||
let (px, py) = (world_pos.x as i32, world_pos.y as i32);
|
let (px, py) = (world_pos.x as i32, world_pos.y as i32);
|
||||||
*shared.tool_state = ToolState::DrawingRasterMarquee {
|
let inside = shared.selection.raster_selection
|
||||||
start: (px, py),
|
.as_ref()
|
||||||
current: (px, py),
|
.map_or(false, |sel| sel.contains_pixel(px, py));
|
||||||
};
|
|
||||||
|
if inside {
|
||||||
|
// Drag inside the selection — move it (and any floating pixels).
|
||||||
|
// As a safety net, lift the selection if no float exists yet.
|
||||||
|
if shared.selection.raster_floating.is_none() {
|
||||||
|
Self::lift_selection_to_float(shared);
|
||||||
|
}
|
||||||
|
*shared.tool_state = ToolState::MovingRasterSelection { last: (px, py) };
|
||||||
|
} else {
|
||||||
|
// Drag outside — start a new marquee (commit any floating first).
|
||||||
|
Self::commit_raster_floating_now(shared);
|
||||||
|
*shared.tool_state = ToolState::DrawingRasterMarquee {
|
||||||
|
start: (px, py),
|
||||||
|
current: (px, py),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.rsp_dragged(response) {
|
if self.rsp_dragged(response) {
|
||||||
if let ToolState::DrawingRasterMarquee { start, ref mut current } = *shared.tool_state {
|
let (px, py) = (world_pos.x as i32, world_pos.y as i32);
|
||||||
let (px, py) = (world_pos.x as i32, world_pos.y as i32);
|
match *shared.tool_state {
|
||||||
*current = (px, py);
|
ToolState::DrawingRasterMarquee { start, ref mut current } => {
|
||||||
let (x0, x1) = (start.0.min(px).max(0), start.0.max(px).min(canvas_w));
|
*current = (px, py);
|
||||||
let (y0, y1) = (start.1.min(py).max(0), start.1.max(py).min(canvas_h));
|
let (x0, x1) = (start.0.min(px).max(0), start.0.max(px).min(canvas_w));
|
||||||
shared.selection.raster_selection = Some(RasterSelection::Rect(x0, y0, x1, y1));
|
let (y0, y1) = (start.1.min(py).max(0), start.1.max(py).min(canvas_h));
|
||||||
|
shared.selection.raster_selection = Some(RasterSelection::Rect(x0, y0, x1, y1));
|
||||||
|
}
|
||||||
|
ToolState::MovingRasterSelection { ref mut last } => {
|
||||||
|
let (dx, dy) = (px - last.0, py - last.1);
|
||||||
|
*last = (px, py);
|
||||||
|
// Shift the marquee.
|
||||||
|
if let Some(ref mut sel) = shared.selection.raster_selection {
|
||||||
|
*sel = match sel {
|
||||||
|
RasterSelection::Rect(x0, y0, x1, y1) =>
|
||||||
|
RasterSelection::Rect(*x0 + dx, *y0 + dy, *x1 + dx, *y1 + dy),
|
||||||
|
RasterSelection::Lasso(pts) =>
|
||||||
|
RasterSelection::Lasso(pts.iter().map(|(x, y)| (x + dx, y + dy)).collect()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Shift floating pixels if any.
|
||||||
|
if let Some(ref mut float) = shared.selection.raster_floating {
|
||||||
|
float.x += dx;
|
||||||
|
float.y += dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.rsp_drag_stopped(response) {
|
if self.rsp_drag_stopped(response) {
|
||||||
if let ToolState::DrawingRasterMarquee { start, current } = *shared.tool_state {
|
match *shared.tool_state {
|
||||||
let (x0, x1) = (start.0.min(current.0).max(0), start.0.max(current.0).min(canvas_w));
|
ToolState::DrawingRasterMarquee { start, current } => {
|
||||||
let (y0, y1) = (start.1.min(current.1).max(0), start.1.max(current.1).min(canvas_h));
|
let (x0, x1) = (start.0.min(current.0).max(0), start.0.max(current.0).min(canvas_w));
|
||||||
shared.selection.raster_selection = if x1 > x0 && y1 > y0 {
|
let (y0, y1) = (start.1.min(current.1).max(0), start.1.max(current.1).min(canvas_h));
|
||||||
Some(RasterSelection::Rect(x0, y0, x1, y1))
|
if x1 > x0 && y1 > y0 {
|
||||||
} else {
|
shared.selection.raster_selection = Some(RasterSelection::Rect(x0, y0, x1, y1));
|
||||||
None
|
Self::lift_selection_to_float(shared);
|
||||||
};
|
} else {
|
||||||
*shared.tool_state = ToolState::Idle;
|
shared.selection.raster_selection = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolState::MovingRasterSelection { .. } => {}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
*shared.tool_state = ToolState::Idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.rsp_clicked(response) {
|
if self.rsp_clicked(response) {
|
||||||
// A click with no drag: commit float (clicked() fires on release, so
|
// A click with no drag: if outside the selection, commit any float and
|
||||||
// drag_started() may not have fired) then clear the selection.
|
// clear; if inside, do nothing (preserves the selection).
|
||||||
Self::commit_raster_floating_now(shared);
|
let (px, py) = (world_pos.x as i32, world_pos.y as i32);
|
||||||
shared.selection.raster_selection = None;
|
let inside = shared.selection.raster_selection
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |sel| sel.contains_pixel(px, py));
|
||||||
|
if !inside {
|
||||||
|
Self::commit_raster_floating_now(shared);
|
||||||
|
shared.selection.raster_selection = None;
|
||||||
|
}
|
||||||
*shared.tool_state = ToolState::Idle;
|
*shared.tool_state = ToolState::Idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4778,11 +4890,12 @@ impl StagePane {
|
||||||
|
|
||||||
if self.rsp_drag_stopped(response) {
|
if self.rsp_drag_stopped(response) {
|
||||||
if let ToolState::DrawingRasterLasso { ref points } = *shared.tool_state {
|
if let ToolState::DrawingRasterLasso { ref points } = *shared.tool_state {
|
||||||
shared.selection.raster_selection = if points.len() >= 3 {
|
if points.len() >= 3 {
|
||||||
Some(RasterSelection::Lasso(points.clone()))
|
shared.selection.raster_selection = Some(RasterSelection::Lasso(points.clone()));
|
||||||
|
Self::lift_selection_to_float(shared);
|
||||||
} else {
|
} else {
|
||||||
None
|
shared.selection.raster_selection = None;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
*shared.tool_state = ToolState::Idle;
|
*shared.tool_state = ToolState::Idle;
|
||||||
}
|
}
|
||||||
|
|
@ -7427,11 +7540,28 @@ impl PaneRenderer for StagePane {
|
||||||
if let Some(readback) = results.remove(&self.instance_id) {
|
if let Some(readback) = results.remove(&self.instance_id) {
|
||||||
if let Some((layer_id, time, w, h, buffer_before)) = self.pending_undo_before.take() {
|
if let Some((layer_id, time, w, h, buffer_before)) = self.pending_undo_before.take() {
|
||||||
use lightningbeam_core::actions::RasterStrokeAction;
|
use lightningbeam_core::actions::RasterStrokeAction;
|
||||||
|
// If a selection was active at stroke-start, restore any pixels
|
||||||
|
// outside the selection outline to their pre-stroke values.
|
||||||
|
let canvas_after = match self.stroke_clip_selection.take() {
|
||||||
|
None => readback.pixels,
|
||||||
|
Some(sel) => {
|
||||||
|
let mut masked = readback.pixels;
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
if !sel.contains_pixel(x as i32, y as i32) {
|
||||||
|
let i = ((y * w + x) * 4) as usize;
|
||||||
|
masked[i..i + 4].copy_from_slice(&buffer_before[i..i + 4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
masked
|
||||||
|
}
|
||||||
|
};
|
||||||
let action = RasterStrokeAction::new(
|
let action = RasterStrokeAction::new(
|
||||||
layer_id,
|
layer_id,
|
||||||
time,
|
time,
|
||||||
buffer_before,
|
buffer_before,
|
||||||
readback.pixels.clone(),
|
canvas_after,
|
||||||
w,
|
w,
|
||||||
h,
|
h,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue