This commit is contained in:
2026-07-09 02:59:53 +03:00
commit 7ace048d94
817 changed files with 234289 additions and 0 deletions
+17
View File
@@ -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"
+666
View File
@@ -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()
}
}