feat: implement layer mask editing support with scratch buffer and coordinate normalization
mandatory-regression-gate / deterministic-tests (push) Has been cancelled
mandatory-regression-gate / protected-performance-path (push) Has been cancelled

This commit is contained in:
Your Name
2026-07-24 06:01:30 +03:00
parent 7496f5e208
commit 505562424b
16 changed files with 1224 additions and 155 deletions
+118
View File
@@ -307,6 +307,29 @@ impl Document {
}));
}
pub fn push_mask_draw_snapshot_subrect(
&mut self,
layer_idx: usize,
before_pixels: Vec<u8>,
after_pixels: Vec<u8>,
bounds: (u32, u32, u32, u32),
description: String,
) {
if layer_idx >= self.layers.len() {
return;
}
if before_pixels == after_pixels {
return;
}
self.history.push(Box::new(MaskDrawAction {
layer_index: layer_idx,
before_pixels,
after_pixels,
bounds: Some([bounds.0, bounds.1, bounds.2, bounds.3]),
description,
}));
}
/// Legacy full-layer snapshot (used by non-brush operations: filter, fill, etc.)
pub fn push_draw_snapshot(
&mut self,
@@ -339,6 +362,31 @@ impl Document {
}));
}
pub fn push_mask_draw_snapshot(
&mut self,
layer_idx: usize,
before_pixels: Vec<u8>,
description: String,
) {
if layer_idx >= self.layers.len() {
return;
}
let layer = &self.layers[layer_idx];
if let Some(ref mask) = layer.mask_pixels {
let after_pixels = mask.clone();
if before_pixels == after_pixels {
return;
}
self.history.push(Box::new(MaskDrawAction {
layer_index: layer_idx,
before_pixels,
after_pixels,
bounds: None,
description,
}));
}
}
/// Clears the undo/redo history stack.
///
/// **Purpose:** Removes all recorded actions so a freshly loaded document
@@ -735,6 +783,76 @@ impl UndoableAction<Vec<Layer>> for DrawAction {
}
}
struct MaskDrawAction {
layer_index: usize,
before_pixels: Vec<u8>,
after_pixels: Vec<u8>,
bounds: Option<[u32; 4]>,
description: String,
}
impl UndoableAction<Vec<Layer>> for MaskDrawAction {
fn undo(&mut self, layers: &mut Vec<Layer>) {
if let Some(layer) = layers.get_mut(self.layer_index) {
if layer.mask_pixels.is_none() {
layer.mask_pixels = Some(vec![255; (layer.width * layer.height) as usize]);
}
if let Some(ref mut mask) = layer.mask_pixels {
if let Some([x0, y0, x1, y1]) = self.bounds {
let dst_w = layer.width;
let rw = x1 - x0;
for row in 0..(y1 - y0) {
let src_start = (row * rw) as usize;
let dst_start = (((y0 + row) * dst_w + x0)) as usize;
let len = (rw) as usize;
if src_start + len <= self.before_pixels.len()
&& dst_start + len <= mask.len()
{
mask[dst_start..dst_start + len]
.copy_from_slice(&self.before_pixels[src_start..src_start + len]);
}
}
} else {
mask.clone_from(&self.before_pixels);
}
}
layer.dirty = true;
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
}
}
fn redo(&mut self, layers: &mut Vec<Layer>) {
if let Some(layer) = layers.get_mut(self.layer_index) {
if layer.mask_pixels.is_none() {
layer.mask_pixels = Some(vec![255; (layer.width * layer.height) as usize]);
}
if let Some(ref mut mask) = layer.mask_pixels {
if let Some([x0, y0, x1, y1]) = self.bounds {
let dst_w = layer.width;
let rw = x1 - x0;
for row in 0..(y1 - y0) {
let src_start = (row * rw) as usize;
let dst_start = (((y0 + row) * dst_w + x0)) as usize;
let len = (rw) as usize;
if src_start + len <= self.after_pixels.len()
&& dst_start + len <= mask.len()
{
mask[dst_start..dst_start + len]
.copy_from_slice(&self.after_pixels[src_start..src_start + len]);
}
}
} else {
mask.clone_from(&self.after_pixels);
}
}
layer.dirty = true;
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
}
}
fn description(&self) -> String {
self.description.clone()
}
}
struct LayerVisibilityAction {
layer_index: usize,
old_visible: bool,