init
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "hcie-document"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
hcie-protocol = { path = "../hcie-protocol" }
|
||||
hcie-blend = { path = "../hcie-blend" }
|
||||
hcie-history = { path = "../hcie-history" }
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.23"
|
||||
proptest = "1.5"
|
||||
approx = "0.5"
|
||||
@@ -0,0 +1,666 @@
|
||||
#![allow(dead_code)]
|
||||
use hcie_protocol::{BlendMode, Layer, LayerData, LayerInfo, LayerStyle, LayerType, MAX_UNDO_STEPS_DEFAULT, VectorShape};
|
||||
use hcie_history::{HistoryManager, UndoableAction};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
static NEXT_LAYER_ID: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
fn next_layer_id() -> u64 {
|
||||
NEXT_LAYER_ID.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub struct Document {
|
||||
pub name: String,
|
||||
pub file_path: Option<std::path::PathBuf>,
|
||||
pub modified: bool,
|
||||
pub canvas_width: u32,
|
||||
pub canvas_height: u32,
|
||||
pub layers: Vec<Layer>,
|
||||
pub active_layer: usize,
|
||||
history: HistoryManager<Vec<Layer>>,
|
||||
pub composite_dirty: bool,
|
||||
pub dirty_bounds: Option<[u32; 4]>,
|
||||
pub zoom: f32,
|
||||
pub pan_x: f32,
|
||||
pub pan_y: f32,
|
||||
pub selection_mask: Option<Vec<u8>>,
|
||||
pub selection_active: bool,
|
||||
pub selection_rect_cached: Option<[u32; 4]>,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
pub fn new_blank(name: impl Into<String>, width: u32, height: u32, transparent: bool) -> Self {
|
||||
let layer = if transparent {
|
||||
Layer::new_transparent("Background", width, height)
|
||||
} else {
|
||||
Layer::new_blank("Background", width, height)
|
||||
};
|
||||
let mut doc = Self {
|
||||
name: name.into(),
|
||||
file_path: None,
|
||||
modified: false,
|
||||
canvas_width: width,
|
||||
canvas_height: height,
|
||||
layers: vec![layer],
|
||||
active_layer: 0,
|
||||
history: HistoryManager::new(MAX_UNDO_STEPS_DEFAULT),
|
||||
composite_dirty: true,
|
||||
dirty_bounds: None,
|
||||
zoom: 1.0,
|
||||
pan_x: 0.0,
|
||||
pan_y: 0.0,
|
||||
selection_mask: None,
|
||||
selection_active: false,
|
||||
selection_rect_cached: None,
|
||||
};
|
||||
if let Some(l) = doc.layers.first_mut() {
|
||||
l.id = next_layer_id();
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
pub fn tab_label(&self) -> String {
|
||||
let marker = if self.modified { " ●" } else { "" };
|
||||
format!("{}{}", self.name, marker)
|
||||
}
|
||||
|
||||
pub fn add_layer(&mut self, name: impl Into<String>) -> u64 {
|
||||
let name_str = name.into();
|
||||
let new_layer = Layer::new_transparent(name_str.clone(), self.canvas_width, self.canvas_height);
|
||||
let new_id = next_layer_id();
|
||||
let idx = self.layers.len();
|
||||
self.layers.push(new_layer);
|
||||
if let Some(l) = self.layers.last_mut() {
|
||||
l.id = new_id;
|
||||
}
|
||||
self.active_layer = idx;
|
||||
self.composite_dirty = true;
|
||||
self.modified = true;
|
||||
|
||||
let captured_layer = self.layers[idx].clone();
|
||||
self.history.push(Box::new(LayerAddAction {
|
||||
layer_index: idx,
|
||||
layer: captured_layer,
|
||||
description: format!("Add Layer: {}", name_str),
|
||||
}));
|
||||
|
||||
new_id
|
||||
}
|
||||
|
||||
pub fn delete_layer(&mut self, idx: usize) {
|
||||
if self.layers.len() <= 1 || idx >= self.layers.len() { return; }
|
||||
|
||||
let layer = &self.layers[idx];
|
||||
let action = LayerDeleteAction {
|
||||
layer_index: idx,
|
||||
pixels: layer.pixels.clone(),
|
||||
shapes: if let LayerData::Vector { shapes } = &layer.data {
|
||||
Some(shapes.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
name: layer.name.clone(),
|
||||
layer_type: layer.layer_type,
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
opacity: layer.opacity,
|
||||
blend_mode: layer.blend_mode,
|
||||
visible: layer.visible,
|
||||
locked: layer.locked,
|
||||
id: layer.id,
|
||||
parent_id: layer.parent_id,
|
||||
styles: layer.styles.clone(),
|
||||
clipping_mask: layer.clipping_mask,
|
||||
data: layer.data.clone(),
|
||||
description: format!("Delete Layer: {}", layer.name),
|
||||
};
|
||||
self.layers.remove(idx);
|
||||
self.active_layer = self.active_layer.min(self.layers.len().saturating_sub(1));
|
||||
self.composite_dirty = true;
|
||||
self.modified = true;
|
||||
self.history.push(Box::new(action));
|
||||
}
|
||||
|
||||
pub fn set_active_layer(&mut self, idx: usize) {
|
||||
if idx < self.layers.len() {
|
||||
self.active_layer = idx;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_layer_visible(&mut self, idx: usize, visible: bool) {
|
||||
if idx >= self.layers.len() { return; }
|
||||
let old_visible = self.layers[idx].visible;
|
||||
if old_visible == visible { return; }
|
||||
|
||||
self.layers[idx].visible = visible;
|
||||
self.layers[idx].dirty = true;
|
||||
self.composite_dirty = true;
|
||||
self.modified = true;
|
||||
|
||||
self.history.push(Box::new(LayerVisibilityAction {
|
||||
layer_index: idx,
|
||||
old_visible,
|
||||
new_visible: visible,
|
||||
description: format!("Toggle Visibility: {}", self.layers[idx].name),
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn active_layer(&self) -> Option<&Layer> {
|
||||
self.layers.get(self.active_layer)
|
||||
}
|
||||
|
||||
pub fn active_layer_mut(&mut self) -> Option<&mut Layer> {
|
||||
self.layers.get_mut(self.active_layer)
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) {
|
||||
self.history.undo(&mut self.layers);
|
||||
self.composite_dirty = true;
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) {
|
||||
self.history.redo(&mut self.layers);
|
||||
self.composite_dirty = true;
|
||||
}
|
||||
|
||||
pub fn can_undo(&self) -> bool {
|
||||
self.history.can_undo()
|
||||
}
|
||||
|
||||
pub fn can_redo(&self) -> bool {
|
||||
self.history.can_redo()
|
||||
}
|
||||
|
||||
pub fn history_len(&self) -> usize {
|
||||
self.history.len()
|
||||
}
|
||||
|
||||
pub fn history_current(&self) -> i32 {
|
||||
self.history.current_index()
|
||||
}
|
||||
|
||||
pub fn history_description(&self, idx: usize) -> Option<String> {
|
||||
self.history.entry_description(idx)
|
||||
}
|
||||
|
||||
pub fn jump_to_history(&mut self, idx: i32) {
|
||||
self.history.jump_to(idx, &mut self.layers);
|
||||
self.composite_dirty = true;
|
||||
}
|
||||
|
||||
pub fn push_vector_snapshot(
|
||||
&mut self,
|
||||
layer_idx: usize,
|
||||
before_shapes: Option<Vec<VectorShape>>,
|
||||
description: String,
|
||||
) {
|
||||
if layer_idx >= self.layers.len() { return; }
|
||||
let layer = &self.layers[layer_idx];
|
||||
let after_shapes = if let LayerData::Vector { shapes } = &layer.data {
|
||||
Some(shapes.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.history.push(Box::new(DrawAction {
|
||||
layer_index: layer_idx,
|
||||
before_pixels: layer.pixels.clone(),
|
||||
after_pixels: layer.pixels.clone(),
|
||||
bounds: None,
|
||||
before_shapes,
|
||||
after_shapes,
|
||||
description,
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn push_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;
|
||||
}
|
||||
let before_shapes: Option<Vec<VectorShape>> = None;
|
||||
let after_shapes: Option<Vec<VectorShape>> = None;
|
||||
self.history.push(Box::new(DrawAction {
|
||||
layer_index: layer_idx,
|
||||
before_pixels,
|
||||
after_pixels,
|
||||
bounds: Some([bounds.0, bounds.1, bounds.2, bounds.3]),
|
||||
before_shapes,
|
||||
after_shapes,
|
||||
description,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Legacy full-layer snapshot (used by non-brush operations: filter, fill, etc.)
|
||||
pub fn push_draw_snapshot(
|
||||
&mut self,
|
||||
layer_idx: usize,
|
||||
before_pixels: Vec<u8>,
|
||||
before_shapes: Option<Vec<VectorShape>>,
|
||||
description: String,
|
||||
) {
|
||||
if layer_idx >= self.layers.len() {
|
||||
return;
|
||||
}
|
||||
let layer = &self.layers[layer_idx];
|
||||
let after_pixels = layer.pixels.clone();
|
||||
let after_shapes = if let LayerData::Vector { shapes } = &layer.data {
|
||||
Some(shapes.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if before_pixels == after_pixels {
|
||||
return;
|
||||
}
|
||||
self.history.push(Box::new(DrawAction {
|
||||
layer_index: layer_idx,
|
||||
before_pixels,
|
||||
after_pixels,
|
||||
bounds: None,
|
||||
before_shapes,
|
||||
after_shapes,
|
||||
description,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Clears the undo/redo history stack.
|
||||
///
|
||||
/// **Purpose:** Removes all recorded actions so a freshly loaded document
|
||||
/// starts with a clean history instead of carrying over stale snapshots.
|
||||
///
|
||||
/// **Side Effects:** Resets the internal history entries and current index.
|
||||
/// Does not touch layer pixel data or the `modified` flag.
|
||||
pub fn clear_history(&mut self) {
|
||||
self.history.clear();
|
||||
}
|
||||
|
||||
pub fn initialize_history(&mut self, description: &str) {
|
||||
for (idx, layer) in self.layers.iter().enumerate() {
|
||||
let after_shapes = if let LayerData::Vector { shapes } = &layer.data {
|
||||
Some(shapes.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.history.push(Box::new(DrawAction {
|
||||
layer_index: idx,
|
||||
before_pixels: Vec::new(),
|
||||
after_pixels: layer.pixels.clone(),
|
||||
bounds: None,
|
||||
before_shapes: None,
|
||||
after_shapes,
|
||||
description: description.to_string(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_dirty(&mut self, x: u32, y: u32, radius: f32) {
|
||||
let r = (radius as i32).max(1);
|
||||
let x = x.min(self.canvas_width.saturating_sub(1));
|
||||
let y = y.min(self.canvas_height.saturating_sub(1));
|
||||
let x0 = (x as i32 - r).max(0) as u32;
|
||||
let y0 = (y as i32 - r).max(0) as u32;
|
||||
let x1 = ((x as i32 + r + 1).min(self.canvas_width as i32 - 1)).max(0) as u32;
|
||||
let y1 = ((y as i32 + r + 1).min(self.canvas_height as i32 - 1)).max(0) as u32;
|
||||
match &mut self.dirty_bounds {
|
||||
Some(b) => {
|
||||
b[0] = b[0].min(x0);
|
||||
b[1] = b[1].min(y0);
|
||||
b[2] = b[2].max(x1);
|
||||
b[3] = b[3].max(y1);
|
||||
}
|
||||
None => {
|
||||
self.dirty_bounds = Some([x0, y0, x1, y1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_dirty(&mut self) {
|
||||
self.composite_dirty = false;
|
||||
self.dirty_bounds = None;
|
||||
for layer in &mut self.layers {
|
||||
layer.dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_layer_by_id(&self, id: u64) -> Option<&Layer> {
|
||||
self.layers.iter().find(|l| l.id == id)
|
||||
}
|
||||
|
||||
pub fn get_layer_by_id_mut(&mut self, id: u64) -> Option<&mut Layer> {
|
||||
self.layers.iter_mut().find(|l| l.id == id)
|
||||
}
|
||||
|
||||
pub fn layer_index_by_id(&self, id: u64) -> Option<usize> {
|
||||
self.layers.iter().position(|l| l.id == id)
|
||||
}
|
||||
|
||||
pub fn move_layer(&mut self, from: usize, to: usize) {
|
||||
if from >= self.layers.len() || to >= self.layers.len() || from == to { return; }
|
||||
let layer = self.layers.remove(from);
|
||||
let insert_idx = if to > from { to } else { to };
|
||||
self.layers.insert(insert_idx, layer);
|
||||
self.composite_dirty = true;
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
pub fn crop(&mut self, x: u32, y: u32, w: u32, h: u32) {
|
||||
let x1 = x.min(self.canvas_width);
|
||||
let y1 = y.min(self.canvas_height);
|
||||
let w1 = w.min(self.canvas_width - x1);
|
||||
let h1 = h.min(self.canvas_height - y1);
|
||||
if w1 == 0 || h1 == 0 { return; }
|
||||
|
||||
for layer in &mut self.layers {
|
||||
let mut new_pixels = vec![0u8; (w1 * h1 * 4) as usize];
|
||||
for ly in 0..h1 {
|
||||
for lx in 0..w1 {
|
||||
let src_x = x1 + lx;
|
||||
let src_y = y1 + ly;
|
||||
if src_x < layer.width && src_y < layer.height {
|
||||
let src_i = ((src_y * layer.width + src_x) * 4) as usize;
|
||||
let dst_i = ((ly * w1 + lx) * 4) as usize;
|
||||
new_pixels[dst_i..dst_i + 4].copy_from_slice(&layer.pixels[src_i..src_i + 4]);
|
||||
}
|
||||
}
|
||||
}
|
||||
layer.pixels = new_pixels;
|
||||
layer.width = w1;
|
||||
layer.height = h1;
|
||||
layer.dirty = true;
|
||||
}
|
||||
self.canvas_width = w1;
|
||||
self.canvas_height = h1;
|
||||
self.composite_dirty = true;
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
pub fn resize_canvas(&mut self, w: u32, h: u32) {
|
||||
if w == 0 || h == 0 { return; }
|
||||
for layer in &mut self.layers {
|
||||
let mut new_pixels = vec![0u8; (w * h * 4) as usize];
|
||||
let copy_w = w.min(layer.width);
|
||||
let copy_h = h.min(layer.height);
|
||||
for y in 0..copy_h {
|
||||
let src_start = (y * layer.width) as usize * 4;
|
||||
let dst_start = (y * w) as usize * 4;
|
||||
let row_bytes = (copy_w * 4) as usize;
|
||||
new_pixels[dst_start..dst_start + row_bytes].copy_from_slice(
|
||||
&layer.pixels[src_start..src_start + row_bytes]
|
||||
);
|
||||
}
|
||||
layer.pixels = new_pixels;
|
||||
layer.width = w;
|
||||
layer.height = h;
|
||||
layer.dirty = true;
|
||||
}
|
||||
self.canvas_width = w;
|
||||
self.canvas_height = h;
|
||||
self.composite_dirty = true;
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
pub fn set_selection_rect(&mut self, start: (u32, u32), end: (u32, u32)) {
|
||||
let x_min = start.0.min(end.0);
|
||||
let x_max = start.0.max(end.0);
|
||||
let y_min = start.1.min(end.1);
|
||||
let y_max = start.1.max(end.1);
|
||||
let mut mask = vec![0u8; (self.canvas_width * self.canvas_height) as usize];
|
||||
for y in y_min..=y_max {
|
||||
for x in x_min..=x_max {
|
||||
if x < self.canvas_width && y < self.canvas_height {
|
||||
let idx = (y * self.canvas_width + x) as usize;
|
||||
mask[idx] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.selection_mask = Some(mask);
|
||||
self.selection_active = true;
|
||||
self.selection_rect_cached = Some([x_min, y_min, x_max, y_max]);
|
||||
}
|
||||
|
||||
pub fn clear_selection(&mut self) {
|
||||
self.selection_mask = None;
|
||||
self.selection_active = false;
|
||||
self.selection_rect_cached = None;
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self) {
|
||||
let mask = vec![255u8; (self.canvas_width * self.canvas_height) as usize];
|
||||
self.selection_mask = Some(mask);
|
||||
self.selection_active = true;
|
||||
self.selection_rect_cached = Some([0, 0, self.canvas_width - 1, self.canvas_height - 1]);
|
||||
}
|
||||
|
||||
pub fn invert_selection(&mut self) {
|
||||
if let Some(mask) = &mut self.selection_mask {
|
||||
for val in mask.iter_mut() { *val = 255 - *val; }
|
||||
} else {
|
||||
self.select_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mask_at(&self, x: u32, y: u32) -> u8 {
|
||||
match &self.selection_mask {
|
||||
Some(mask) => mask.get((y * self.canvas_width + x) as usize).copied().unwrap_or(255),
|
||||
None => 255,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layer_info(&self, id: u64) -> Option<LayerInfo> {
|
||||
self.get_layer_by_id(id).map(|l| LayerInfo {
|
||||
id: l.id,
|
||||
name: l.name.clone(),
|
||||
layer_type: l.layer_type,
|
||||
width: l.width,
|
||||
height: l.height,
|
||||
visible: l.visible,
|
||||
opacity: l.opacity,
|
||||
blend_mode: l.blend_mode,
|
||||
locked: l.locked,
|
||||
parent_id: l.parent_id,
|
||||
clipping_mask: l.clipping_mask,
|
||||
collapsed: l.collapsed,
|
||||
has_vector_shapes: matches!(&l.data, LayerData::Vector { .. }),
|
||||
shape_count: match &l.data {
|
||||
LayerData::Vector { shapes } => shapes.len(),
|
||||
_ => 0,
|
||||
},
|
||||
has_effects: !l.effects.is_empty() || !l.styles.is_empty(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
pub fn all_layer_ids(&self) -> Vec<u64> {
|
||||
self.layers.iter().map(|l| l.id).collect()
|
||||
}
|
||||
|
||||
pub fn layer_infos(&self) -> Vec<LayerInfo> {
|
||||
self.layers.iter().map(|l| LayerInfo {
|
||||
id: l.id,
|
||||
name: l.name.clone(),
|
||||
layer_type: l.layer_type,
|
||||
width: l.width,
|
||||
height: l.height,
|
||||
visible: l.visible,
|
||||
opacity: l.opacity,
|
||||
blend_mode: l.blend_mode,
|
||||
locked: l.locked,
|
||||
parent_id: l.parent_id,
|
||||
clipping_mask: l.clipping_mask,
|
||||
collapsed: l.collapsed,
|
||||
has_vector_shapes: matches!(&l.data, LayerData::Vector { .. }),
|
||||
shape_count: match &l.data {
|
||||
LayerData::Vector { shapes } => shapes.len(),
|
||||
_ => 0,
|
||||
},
|
||||
has_effects: !l.effects.is_empty() || !l.styles.is_empty(),
|
||||
}).collect()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Undoable Actions ─────────────────────────────────────────────────────────
|
||||
|
||||
struct LayerAddAction {
|
||||
layer_index: usize,
|
||||
layer: Layer,
|
||||
description: String,
|
||||
}
|
||||
|
||||
impl UndoableAction<Vec<Layer>> for LayerAddAction {
|
||||
fn undo(&mut self, layers: &mut Vec<Layer>) {
|
||||
if self.layer_index < layers.len() {
|
||||
layers.remove(self.layer_index);
|
||||
}
|
||||
}
|
||||
fn redo(&mut self, layers: &mut Vec<Layer>) {
|
||||
let idx = self.layer_index.min(layers.len());
|
||||
layers.insert(idx, self.layer.clone());
|
||||
}
|
||||
fn description(&self) -> String {
|
||||
self.description.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct LayerDeleteAction {
|
||||
layer_index: usize,
|
||||
pixels: Vec<u8>,
|
||||
shapes: Option<Vec<VectorShape>>,
|
||||
name: String,
|
||||
layer_type: LayerType,
|
||||
width: u32,
|
||||
height: u32,
|
||||
opacity: f32,
|
||||
blend_mode: BlendMode,
|
||||
visible: bool,
|
||||
locked: bool,
|
||||
id: u64,
|
||||
parent_id: Option<u64>,
|
||||
styles: Vec<LayerStyle>,
|
||||
clipping_mask: bool,
|
||||
data: LayerData,
|
||||
description: String,
|
||||
}
|
||||
|
||||
impl UndoableAction<Vec<Layer>> for LayerDeleteAction {
|
||||
fn undo(&mut self, layers: &mut Vec<Layer>) {
|
||||
let mut restored = Layer::from_rgba(self.name.clone(), self.width, self.height, self.pixels.clone());
|
||||
restored.layer_type = self.layer_type;
|
||||
restored.opacity = self.opacity;
|
||||
restored.blend_mode = self.blend_mode;
|
||||
restored.visible = self.visible;
|
||||
restored.locked = self.locked;
|
||||
restored.id = self.id;
|
||||
restored.parent_id = self.parent_id;
|
||||
restored.styles = self.styles.clone();
|
||||
restored.clipping_mask = self.clipping_mask;
|
||||
restored.data = self.data.clone();
|
||||
let idx = self.layer_index.min(layers.len());
|
||||
layers.insert(idx, restored);
|
||||
}
|
||||
fn redo(&mut self, layers: &mut Vec<Layer>) {
|
||||
if self.layer_index < layers.len() {
|
||||
layers.remove(self.layer_index);
|
||||
}
|
||||
}
|
||||
fn description(&self) -> String {
|
||||
self.description.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct DrawAction {
|
||||
layer_index: usize,
|
||||
before_pixels: Vec<u8>,
|
||||
after_pixels: Vec<u8>,
|
||||
/// When `Some`, the stored pixels are a sub-rect [x0, y0, x1, y1] of the
|
||||
/// full layer. Undo/redo only copies this region. `None` means full-layer
|
||||
/// pixels (backward-compatible legacy path).
|
||||
bounds: Option<[u32; 4]>,
|
||||
before_shapes: Option<Vec<VectorShape>>,
|
||||
after_shapes: Option<Vec<VectorShape>>,
|
||||
description: String,
|
||||
}
|
||||
|
||||
impl UndoableAction<Vec<Layer>> for DrawAction {
|
||||
fn undo(&mut self, layers: &mut Vec<Layer>) {
|
||||
if let Some(layer) = layers.get_mut(self.layer_index) {
|
||||
if let Some([x0, y0, x1, y1]) = self.bounds {
|
||||
// Sub-rect copy: only restore changed region
|
||||
let dst_w = layer.width;
|
||||
let rw = x1 - x0;
|
||||
for row in 0..(y1 - y0) {
|
||||
let src_start = (row * rw * 4) as usize;
|
||||
let dst_start = (((y0 + row) * dst_w + x0) * 4) as usize;
|
||||
let len = (rw * 4) as usize;
|
||||
if src_start + len <= self.before_pixels.len() && dst_start + len <= layer.pixels.len() {
|
||||
layer.pixels[dst_start..dst_start + len].copy_from_slice(&self.before_pixels[src_start..src_start + len]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
layer.pixels.clone_from(&self.before_pixels);
|
||||
}
|
||||
layer.dirty = true;
|
||||
if let Some(ref shapes) = self.before_shapes {
|
||||
layer.data = LayerData::Vector { shapes: shapes.clone() };
|
||||
}
|
||||
}
|
||||
}
|
||||
fn redo(&mut self, layers: &mut Vec<Layer>) {
|
||||
if let Some(layer) = layers.get_mut(self.layer_index) {
|
||||
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 * 4) as usize;
|
||||
let dst_start = (((y0 + row) * dst_w + x0) * 4) as usize;
|
||||
let len = (rw * 4) as usize;
|
||||
if src_start + len <= self.after_pixels.len() && dst_start + len <= layer.pixels.len() {
|
||||
layer.pixels[dst_start..dst_start + len].copy_from_slice(&self.after_pixels[src_start..src_start + len]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
layer.pixels.clone_from(&self.after_pixels);
|
||||
}
|
||||
layer.dirty = true;
|
||||
if let Some(ref shapes) = self.after_shapes {
|
||||
layer.data = LayerData::Vector { shapes: shapes.clone() };
|
||||
}
|
||||
}
|
||||
}
|
||||
fn description(&self) -> String {
|
||||
self.description.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct LayerVisibilityAction {
|
||||
layer_index: usize,
|
||||
old_visible: bool,
|
||||
new_visible: bool,
|
||||
description: String,
|
||||
}
|
||||
|
||||
impl UndoableAction<Vec<Layer>> for LayerVisibilityAction {
|
||||
fn undo(&mut self, layers: &mut Vec<Layer>) {
|
||||
if let Some(layer) = layers.get_mut(self.layer_index) {
|
||||
layer.visible = self.old_visible;
|
||||
layer.dirty = true;
|
||||
}
|
||||
}
|
||||
fn redo(&mut self, layers: &mut Vec<Layer>) {
|
||||
if let Some(layer) = layers.get_mut(self.layer_index) {
|
||||
layer.visible = self.new_visible;
|
||||
layer.dirty = true;
|
||||
}
|
||||
}
|
||||
fn description(&self) -> String {
|
||||
self.description.clone()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user