feat: implement layer mask editing support with scratch buffer and coordinate normalization
This commit is contained in:
@@ -1,3 +1,12 @@
|
||||
[env]
|
||||
PKG_CONFIG_PATH = "/tmp/dav1d-dev/usr/lib/x86_64-linux-gnu/pkgconfig"
|
||||
LIBRARY_PATH = "/tmp/dav1d-dev/usr/lib/x86_64-linux-gnu"
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
linker = "rust-lld.exe"
|
||||
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
linker = "rust-lld.exe"
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ approx = "0.5"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
codegen-units = 1
|
||||
codegen-units = 256
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -459,4 +459,102 @@ impl Engine {
|
||||
self.below_cache_active_idx = None;
|
||||
self.below_cache_dirty = true;
|
||||
}
|
||||
|
||||
/// Check if a layer has an active layer mask.
|
||||
pub fn has_layer_mask(&self, layer_id: u64) -> bool {
|
||||
if let Some(idx) = self.document.layer_index_by_id(layer_id) {
|
||||
self.document.layers.get(idx).map(|l| l.mask_pixels.is_some()).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a white (opaque/reveal) layer mask to the specified layer if not present.
|
||||
pub fn add_layer_mask(&mut self, layer_id: u64) {
|
||||
if let Some(idx) = self.document.layer_index_by_id(layer_id) {
|
||||
if let Some(layer) = self.document.layers.get_mut(idx) {
|
||||
let mask_size = (layer.width * layer.height) as usize;
|
||||
layer.mask_pixels = Some(vec![255u8; mask_size]);
|
||||
layer.mask_bounds = Some([0, 0, layer.height as i32, layer.width as i32]);
|
||||
layer.mask_default_color = 255;
|
||||
layer.dirty = true;
|
||||
self.document.composite_dirty = true;
|
||||
self.document.modified = true;
|
||||
self.below_cache_dirty = true;
|
||||
self.document.composite_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the layer mask from the specified layer.
|
||||
pub fn remove_layer_mask(&mut self, layer_id: u64) {
|
||||
if let Some(idx) = self.document.layer_index_by_id(layer_id) {
|
||||
if let Some(layer) = self.document.layers.get_mut(idx) {
|
||||
layer.mask_pixels = None;
|
||||
layer.mask_bounds = None;
|
||||
layer.dirty = true;
|
||||
self.document.composite_dirty = true;
|
||||
self.document.modified = true;
|
||||
self.below_cache_dirty = true;
|
||||
self.document.composite_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle the existence of a layer mask on the specified layer.
|
||||
pub fn toggle_layer_mask(&mut self, layer_id: u64) {
|
||||
if self.has_layer_mask(layer_id) {
|
||||
self.remove_layer_mask(layer_id);
|
||||
} else {
|
||||
self.add_layer_mask(layer_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if current drawing operations target the layer mask.
|
||||
pub fn is_editing_mask(&self) -> bool {
|
||||
self.editing_mask
|
||||
}
|
||||
|
||||
/// Set whether current drawing operations target the active layer's mask.
|
||||
pub fn set_editing_mask(&mut self, editing: bool) {
|
||||
self.editing_mask = editing;
|
||||
}
|
||||
|
||||
/// Toggle mask editing mode.
|
||||
pub fn toggle_editing_mask(&mut self) {
|
||||
self.editing_mask = !self.editing_mask;
|
||||
}
|
||||
|
||||
/// Returns a reference to the mask pixel bytes for the given layer.
|
||||
pub fn get_layer_mask_pixels(&self, layer_id: u64) -> Option<&[u8]> {
|
||||
if let Some(idx) = self.document.layer_index_by_id(layer_id) {
|
||||
self.document.layers.get(idx).and_then(|l| l.mask_pixels.as_deref())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns mask bounds [top, left, bottom, right] for the given layer if set.
|
||||
pub fn get_layer_mask_bounds(&self, layer_id: u64) -> Option<[i32; 4]> {
|
||||
if let Some(idx) = self.document.layer_index_by_id(layer_id) {
|
||||
self.document.layers.get(idx).and_then(|l| l.mask_bounds)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the mask pixel bytes and bounds for the given layer.
|
||||
pub fn set_layer_mask_pixels(&mut self, layer_id: u64, mask: Vec<u8>, bounds: Option<[i32; 4]>) {
|
||||
if let Some(idx) = self.document.layer_index_by_id(layer_id) {
|
||||
if let Some(layer) = self.document.layers.get_mut(idx) {
|
||||
layer.mask_pixels = Some(mask);
|
||||
layer.mask_bounds = bounds;
|
||||
layer.dirty = true;
|
||||
self.document.composite_dirty = true;
|
||||
self.document.modified = true;
|
||||
self.below_cache_dirty = true;
|
||||
self.document.composite_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,12 +348,14 @@ pub struct Engine {
|
||||
/// At `end_stroke()`, the buffer is moved into the background thread for
|
||||
/// sub-rect extraction, then returned via the pending_history channel.
|
||||
stroke_before_buf: Option<Vec<u8>>,
|
||||
mask_before_buf: Option<Vec<u8>>,
|
||||
tile_layers: Vec<Option<TiledLayer>>,
|
||||
svg_sources: std::collections::HashMap<u64, Vec<u8>>,
|
||||
filter_preview_original: Option<Vec<u8>>,
|
||||
stroke_effects_backup: Option<Vec<hcie_protocol::effects::LayerEffect>>,
|
||||
stroke_styles_backup: Option<Vec<hcie_protocol::LayerStyle>>,
|
||||
pub is_eraser: bool,
|
||||
pub editing_mask: bool,
|
||||
/// Pooled stroke mask buffer. Zeroed on `begin_stroke()` and `end_stroke()`
|
||||
/// but **never dropped** between strokes. Reused if size matches, reallocated
|
||||
/// only when canvas dimensions change. Avoids ~8MB alloc/stroke on 4K.
|
||||
@@ -363,6 +365,8 @@ pub struct Engine {
|
||||
/// on 4K canvases. The pixel-bridge extracts dirty rect from this buffer
|
||||
/// without allocating its own buffer.
|
||||
composite_scratch: Option<Vec<u8>>,
|
||||
/// Pooled RGBA scratch buffer for layer mask editing passes.
|
||||
pub mask_rgba_scratch: Option<Vec<u8>>,
|
||||
/// Accumulated stroke extent `[x0, y0, x1, y1]` for sub-rect snapshot.
|
||||
/// Tracks the union of all brush areas painted during the current stroke.
|
||||
/// Independent from `document.dirty_bounds` — survives composite emits.
|
||||
@@ -540,14 +544,17 @@ impl Engine {
|
||||
cyclic_speed: 0.5,
|
||||
stroke_before: None,
|
||||
stroke_before_buf: None,
|
||||
mask_before_buf: None,
|
||||
tile_layers: Vec::new(),
|
||||
svg_sources: std::collections::HashMap::new(),
|
||||
filter_preview_original: None,
|
||||
stroke_effects_backup: None,
|
||||
stroke_styles_backup: None,
|
||||
is_eraser: false,
|
||||
editing_mask: false,
|
||||
active_stroke_mask: None,
|
||||
composite_scratch: None,
|
||||
mask_rgba_scratch: None,
|
||||
last_stroke_bounds: None,
|
||||
below_cache: None,
|
||||
below_cache_active_idx: None,
|
||||
|
||||
@@ -105,6 +105,167 @@ impl Engine {
|
||||
tip
|
||||
}
|
||||
|
||||
/// Helper method to execute a drawing action either on active layer pixels (when editing_mask is false)
|
||||
/// or on the active layer mask (when editing_mask is true), converting between RGBA and greyscale mask.
|
||||
/// Ensures active layer's mask is expanded to full canvas dimensions if present.
|
||||
pub fn ensure_full_canvas_mask(&mut self, layer_id: u64) {
|
||||
let cw = self.document.canvas_width;
|
||||
let ch = self.document.canvas_height;
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
let full_size = (cw * ch) as usize;
|
||||
if let Some(ref mask) = layer.mask_pixels {
|
||||
if mask.len() != full_size {
|
||||
let mut full_mask = vec![layer.mask_default_color; full_size];
|
||||
if let Some(mb) = layer.mask_bounds {
|
||||
let (m_top, m_left, m_bottom, m_right) =
|
||||
(mb[0].max(0) as u32, mb[1].max(0) as u32, mb[2].max(0) as u32, mb[3].max(0) as u32);
|
||||
let mw = (m_right - m_left) as usize;
|
||||
let mh = (m_bottom - m_top) as usize;
|
||||
if mw > 0 && mh > 0 && mask.len() == mw * mh {
|
||||
for y in 0..mh {
|
||||
let gy = m_top as usize + y;
|
||||
if gy >= ch as usize { break; }
|
||||
for x in 0..mw {
|
||||
let gx = m_left as usize + x;
|
||||
if gx >= cw as usize { break; }
|
||||
full_mask[gy * cw as usize + gx] = mask[y * mw + x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
layer.mask_pixels = Some(full_mask);
|
||||
layer.mask_bounds = Some([0, 0, ch as i32, cw as i32]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_target_pixels_or_mask<F>(&mut self, layer_id: u64, f: F)
|
||||
where
|
||||
F: FnOnce(
|
||||
&mut [u8],
|
||||
u32,
|
||||
u32,
|
||||
Option<&[u8]>,
|
||||
Option<&mut [u8]>,
|
||||
Option<&[u8]>,
|
||||
Option<&mut Vec<(f32, f32)>>,
|
||||
),
|
||||
{
|
||||
let is_editing = self.editing_mask;
|
||||
if is_editing {
|
||||
self.ensure_full_canvas_mask(layer_id);
|
||||
}
|
||||
|
||||
if !is_editing {
|
||||
let mask_ref = self.cached_selection_mask.as_deref();
|
||||
let active_stroke_mask = self.active_stroke_mask.as_deref_mut();
|
||||
let stroke_before_buf = self.stroke_before_buf.as_deref();
|
||||
let sketch_hist = &mut self.sketch_history;
|
||||
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
let w = layer.width;
|
||||
let h = layer.height;
|
||||
f(
|
||||
&mut layer.pixels,
|
||||
w,
|
||||
h,
|
||||
mask_ref,
|
||||
active_stroke_mask,
|
||||
stroke_before_buf,
|
||||
Some(sketch_hist),
|
||||
);
|
||||
layer.dirty = true;
|
||||
}
|
||||
} else {
|
||||
let cw = self.document.canvas_width as usize;
|
||||
let ch = self.document.canvas_height as usize;
|
||||
let mask_len = cw * ch;
|
||||
if mask_len == 0 { return; }
|
||||
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
if layer.mask_pixels.is_none() {
|
||||
layer.mask_pixels = Some(vec![255u8; mask_len]);
|
||||
layer.mask_bounds = Some([0, 0, ch as i32, cw as i32]);
|
||||
layer.mask_default_color = 255;
|
||||
}
|
||||
}
|
||||
|
||||
let (rx0, ry0, rx1, ry1) = match self.last_stroke_bounds {
|
||||
Some([x0, y0, x1, y1]) => (
|
||||
(x0.saturating_sub(15) as usize).min(cw),
|
||||
(y0.saturating_sub(15) as usize).min(ch),
|
||||
((x1 + 15) as usize).min(cw),
|
||||
((y1 + 15) as usize).min(ch),
|
||||
),
|
||||
None => (0, 0, cw, ch),
|
||||
};
|
||||
|
||||
let needed_len = mask_len * 4;
|
||||
let mut scratch = self.mask_rgba_scratch.take().unwrap_or_default();
|
||||
if scratch.len() != needed_len {
|
||||
scratch.resize(needed_len, 0);
|
||||
}
|
||||
|
||||
if let Some(layer) = self.document.get_layer_by_id(layer_id) {
|
||||
if let Some(ref mask) = layer.mask_pixels {
|
||||
for y in ry0..ry1 {
|
||||
let row_off = y * cw;
|
||||
for x in rx0..rx1 {
|
||||
let idx = row_off + x;
|
||||
let v = mask[idx];
|
||||
let off = idx * 4;
|
||||
scratch[off] = v;
|
||||
scratch[off + 1] = v;
|
||||
scratch[off + 2] = v;
|
||||
scratch[off + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mask_ref = self.cached_selection_mask.as_deref();
|
||||
let active_stroke_mask = self.active_stroke_mask.as_deref_mut();
|
||||
let stroke_before_buf = self.stroke_before_buf.as_deref();
|
||||
let sketch_hist = &mut self.sketch_history;
|
||||
f(
|
||||
&mut scratch,
|
||||
cw as u32,
|
||||
ch as u32,
|
||||
mask_ref,
|
||||
active_stroke_mask,
|
||||
stroke_before_buf,
|
||||
Some(sketch_hist),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
if let Some(ref mut mask) = layer.mask_pixels {
|
||||
for y in ry0..ry1 {
|
||||
let row_off = y * cw;
|
||||
for x in rx0..rx1 {
|
||||
let idx = row_off + x;
|
||||
let off = idx * 4;
|
||||
let r = scratch[off] as u32;
|
||||
let g = scratch[off + 1] as u32;
|
||||
let b = scratch[off + 2] as u32;
|
||||
let a = scratch[off + 3] as u32;
|
||||
let gray = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
mask[idx] = ((gray * a) / 255) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
layer.dirty = true;
|
||||
}
|
||||
|
||||
self.mask_rgba_scratch = Some(scratch);
|
||||
}
|
||||
self.below_cache_dirty = true;
|
||||
self.document.composite_dirty = true;
|
||||
self.document.modified = true;
|
||||
}
|
||||
|
||||
pub fn stroke_to(&mut self, layer_id: u64, x: f32, y: f32, pressure: f32) {
|
||||
let w = self.document.canvas_width as f32;
|
||||
let h = self.document.canvas_height as f32;
|
||||
@@ -115,59 +276,66 @@ impl Engine {
|
||||
}
|
||||
let tip = self.brush_tip_with_time_dynamics(x, y);
|
||||
let color = self.apply_cyclic_color(self.current_color);
|
||||
let mask_ref = self.cached_selection_mask.as_deref();
|
||||
|
||||
if Self::is_specialized_style(tip.style) {
|
||||
if let Some((lx, ly, lp)) = self.last_stroke_pos {
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
// If click without movement, draw a single dab at current position
|
||||
let points_vec = if (lx - x).abs() < 0.001 && (ly - y).abs() < 0.001 {
|
||||
vec![(x, y, pressure)]
|
||||
} else {
|
||||
vec![(lx, ly, lp), (x, y, pressure)]
|
||||
};
|
||||
draw_specialized_stroke(
|
||||
&mut layer.pixels,
|
||||
layer.width,
|
||||
layer.height,
|
||||
&points_vec,
|
||||
tip.style,
|
||||
tip.size,
|
||||
tip.hardness,
|
||||
color,
|
||||
tip.opacity,
|
||||
tip.spacing,
|
||||
self.is_eraser,
|
||||
if tip.style == BrushStyle::Sketch {
|
||||
Some(&mut self.sketch_history)
|
||||
let points_vec = if (lx - x).abs() < 0.001 && (ly - y).abs() < 0.001 {
|
||||
vec![(x, y, pressure)]
|
||||
} else {
|
||||
vec![(lx, ly, lp), (x, y, pressure)]
|
||||
};
|
||||
let is_eraser = self.is_eraser;
|
||||
|
||||
let dirty_r = Self::brush_dirty_radius(&tip);
|
||||
self.document.expand_dirty(lx as u32, ly as u32, dirty_r);
|
||||
self.document.expand_dirty(x as u32, y as u32, dirty_r);
|
||||
self.expand_stroke_bounds(lx as u32, ly as u32, dirty_r);
|
||||
self.expand_stroke_bounds(x as u32, y as u32, dirty_r);
|
||||
|
||||
self.draw_target_pixels_or_mask(
|
||||
layer_id,
|
||||
|pixels, lw, lh, mask_ref, active_stroke_mask, stroke_before_buf, sketch_history| {
|
||||
let sketch_hist = if tip.style == BrushStyle::Sketch {
|
||||
sketch_history
|
||||
} else {
|
||||
None
|
||||
},
|
||||
tip.spray_particle_size,
|
||||
tip.spray_density,
|
||||
mask_ref,
|
||||
self.active_stroke_mask.as_deref_mut(),
|
||||
self.stroke_before_buf.as_deref(),
|
||||
tip.color_variant,
|
||||
tip.variant_amount,
|
||||
tip.density,
|
||||
tip.jitter_amount,
|
||||
tip.scatter_amount,
|
||||
tip.angle,
|
||||
tip.roundness,
|
||||
tip.rotation_random,
|
||||
tip.drawing_angle,
|
||||
false,
|
||||
);
|
||||
layer.dirty = true;
|
||||
};
|
||||
draw_specialized_stroke(
|
||||
pixels,
|
||||
lw,
|
||||
lh,
|
||||
&points_vec,
|
||||
tip.style,
|
||||
tip.size,
|
||||
tip.hardness,
|
||||
color,
|
||||
tip.opacity,
|
||||
tip.spacing,
|
||||
is_eraser,
|
||||
sketch_hist,
|
||||
tip.spray_particle_size,
|
||||
tip.spray_density,
|
||||
mask_ref,
|
||||
active_stroke_mask,
|
||||
stroke_before_buf,
|
||||
tip.color_variant,
|
||||
tip.variant_amount,
|
||||
tip.density,
|
||||
tip.jitter_amount,
|
||||
tip.scatter_amount,
|
||||
tip.angle,
|
||||
tip.roundness,
|
||||
tip.rotation_random,
|
||||
tip.drawing_angle,
|
||||
false,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
if !layer.effects.is_empty() || !layer.styles.is_empty() {
|
||||
*layer.effects_cache.lock().unwrap() = None;
|
||||
}
|
||||
let dirty_r = Self::brush_dirty_radius(&tip);
|
||||
self.document.expand_dirty(lx as u32, ly as u32, dirty_r);
|
||||
self.document.expand_dirty(x as u32, y as u32, dirty_r);
|
||||
self.expand_stroke_bounds(lx as u32, ly as u32, dirty_r);
|
||||
self.expand_stroke_bounds(x as u32, y as u32, dirty_r);
|
||||
}
|
||||
self.document.composite_dirty = true;
|
||||
self.document.modified = true;
|
||||
@@ -176,32 +344,60 @@ impl Engine {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
draw_brush_stroke(
|
||||
layer,
|
||||
&[(x, y, pressure)],
|
||||
color,
|
||||
&tip,
|
||||
self.is_eraser,
|
||||
mask_ref,
|
||||
self.active_stroke_mask.as_deref_mut(),
|
||||
self.stroke_before_buf.as_deref(),
|
||||
let is_eraser = self.is_eraser;
|
||||
|
||||
let dirty_r = tip.size.max(2.0);
|
||||
if let Some((lx, ly, _)) = self.last_stroke_pos {
|
||||
self.document.expand_dirty(lx as u32, ly as u32, dirty_r);
|
||||
}
|
||||
self.document.expand_dirty(x as u32, y as u32, dirty_r);
|
||||
if let Some((lx, ly, _)) = self.last_stroke_pos {
|
||||
self.expand_stroke_bounds(lx as u32, ly as u32, dirty_r);
|
||||
}
|
||||
self.expand_stroke_bounds(x as u32, y as u32, dirty_r);
|
||||
|
||||
if self.editing_mask {
|
||||
self.draw_target_pixels_or_mask(
|
||||
layer_id,
|
||||
|pixels, lw, lh, mask_ref, active_stroke_mask, stroke_before_buf, _sketch_history| {
|
||||
let mut temp_layer = hcie_protocol::Layer::from_rgba("temp", lw, lh, pixels.to_vec());
|
||||
draw_brush_stroke(
|
||||
&mut temp_layer,
|
||||
&[(x, y, pressure)],
|
||||
color,
|
||||
&tip,
|
||||
is_eraser,
|
||||
mask_ref,
|
||||
active_stroke_mask,
|
||||
stroke_before_buf,
|
||||
);
|
||||
pixels.copy_from_slice(&temp_layer.pixels);
|
||||
},
|
||||
);
|
||||
layer.dirty = true;
|
||||
} else {
|
||||
let mask_ref = self.cached_selection_mask.as_deref();
|
||||
let active_stroke_mask = self.active_stroke_mask.as_deref_mut();
|
||||
let stroke_before_buf = self.stroke_before_buf.as_deref();
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
draw_brush_stroke(
|
||||
layer,
|
||||
&[(x, y, pressure)],
|
||||
color,
|
||||
&tip,
|
||||
is_eraser,
|
||||
mask_ref,
|
||||
active_stroke_mask,
|
||||
stroke_before_buf,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
if !layer.effects.is_empty() || !layer.styles.is_empty() {
|
||||
layer
|
||||
.effects_dirty
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
let dirty_r = tip.size.max(2.0);
|
||||
if let Some((lx, ly, _)) = self.last_stroke_pos {
|
||||
self.document.expand_dirty(lx as u32, ly as u32, dirty_r);
|
||||
}
|
||||
self.document.expand_dirty(x as u32, y as u32, dirty_r);
|
||||
if let Some((lx, ly, _)) = self.last_stroke_pos {
|
||||
self.expand_stroke_bounds(lx as u32, ly as u32, dirty_r);
|
||||
}
|
||||
self.expand_stroke_bounds(x as u32, y as u32, dirty_r);
|
||||
}
|
||||
self.document.composite_dirty = true;
|
||||
self.document.modified = true;
|
||||
|
||||
@@ -59,6 +59,10 @@ pub struct PendingHistoryItem {
|
||||
pub return_before: Option<Vec<u8>>,
|
||||
/// Full-layer after buffer returned from background thread for pool reuse.
|
||||
pub return_after: Option<Vec<u8>>,
|
||||
/// Grayscale before mask buffer returned from background thread for pool reuse.
|
||||
pub return_mask_before: Option<Vec<u8>>,
|
||||
/// Indicates if this snapshot represents a mask edit rather than a pixel edit.
|
||||
pub is_mask: bool,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
@@ -98,8 +102,33 @@ impl Engine {
|
||||
self.stroke_before_buf = Some(buf);
|
||||
}
|
||||
}
|
||||
if let Some(buf) = item.return_mask_before {
|
||||
if self
|
||||
.mask_before_buf
|
||||
.as_ref()
|
||||
.map_or(true, |b| b.len() != buf.len())
|
||||
{
|
||||
self.mask_before_buf = Some(buf);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(bounds) = item.bounds {
|
||||
if item.is_mask {
|
||||
if let Some(bounds) = item.bounds {
|
||||
self.document.push_mask_draw_snapshot_subrect(
|
||||
item.layer_idx,
|
||||
item.before_pixels,
|
||||
item.after_pixels,
|
||||
(bounds[0], bounds[1], bounds[2], bounds[3]),
|
||||
item.description,
|
||||
);
|
||||
} else {
|
||||
self.document.push_mask_draw_snapshot(
|
||||
item.layer_idx,
|
||||
item.before_pixels,
|
||||
item.description,
|
||||
);
|
||||
}
|
||||
} else if let Some(bounds) = item.bounds {
|
||||
self.document.push_draw_snapshot_subrect(
|
||||
item.layer_idx,
|
||||
item.before_pixels,
|
||||
@@ -191,12 +220,71 @@ impl Engine {
|
||||
|
||||
// Pool the "before" pixel buffer: reuse if size matches, otherwise allocate.
|
||||
// This avoids a ~33MB allocation per stroke on 4K canvases.
|
||||
match &mut self.stroke_before_buf {
|
||||
Some(buf) if buf.len() == layer_pixels => {
|
||||
buf.copy_from_slice(&layer.pixels);
|
||||
if self.editing_mask {
|
||||
let mut rgba_buf = match self.stroke_before_buf.take() {
|
||||
Some(mut b) if b.len() == layer_pixels => {
|
||||
b.fill(255);
|
||||
b
|
||||
}
|
||||
_ => vec![255u8; layer_pixels],
|
||||
};
|
||||
let mut gray_buf = match self.mask_before_buf.take() {
|
||||
Some(mut b) if b.len() == layer_size => {
|
||||
b.fill(255);
|
||||
b
|
||||
}
|
||||
_ => vec![255u8; layer_size],
|
||||
};
|
||||
|
||||
let ch = layer.height as usize;
|
||||
let cw = layer.width as usize;
|
||||
let full_size = cw * ch;
|
||||
|
||||
if let Some(ref mut mask) = layer.mask_pixels {
|
||||
if mask.len() != full_size {
|
||||
let mut full_mask = vec![layer.mask_default_color; full_size];
|
||||
if let Some(mb) = layer.mask_bounds {
|
||||
let (m_top, m_left, m_bottom, m_right) =
|
||||
(mb[0].max(0) as u32, mb[1].max(0) as u32, mb[2].max(0) as u32, mb[3].max(0) as u32);
|
||||
let mw = (m_right - m_left) as usize;
|
||||
let mh = (m_bottom - m_top) as usize;
|
||||
if mw > 0 && mh > 0 && mask.len() == mw * mh {
|
||||
for y in 0..mh {
|
||||
let gy = m_top as usize + y;
|
||||
if gy >= ch { break; }
|
||||
for x in 0..mw {
|
||||
let gx = m_left as usize + x;
|
||||
if gx >= cw { break; }
|
||||
full_mask[gy * cw + gx] = mask[y * mw + x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*mask = full_mask;
|
||||
layer.mask_bounds = Some([0, 0, ch as i32, cw as i32]);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.stroke_before_buf = Some(layer.pixels.clone());
|
||||
|
||||
if let Some(ref mask) = layer.mask_pixels {
|
||||
gray_buf.copy_from_slice(mask);
|
||||
for (i, &v) in mask.iter().enumerate() {
|
||||
let off = i * 4;
|
||||
rgba_buf[off] = v;
|
||||
rgba_buf[off + 1] = v;
|
||||
rgba_buf[off + 2] = v;
|
||||
rgba_buf[off + 3] = 255;
|
||||
}
|
||||
}
|
||||
self.stroke_before_buf = Some(rgba_buf);
|
||||
self.mask_before_buf = Some(gray_buf);
|
||||
} else {
|
||||
match &mut self.stroke_before_buf {
|
||||
Some(buf) if buf.len() == layer_pixels => {
|
||||
buf.copy_from_slice(&layer.pixels);
|
||||
}
|
||||
_ => {
|
||||
self.stroke_before_buf = Some(layer.pixels.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let before_shapes = if let LayerData::Vector { shapes } = &layer.data {
|
||||
@@ -254,17 +342,35 @@ impl Engine {
|
||||
layer_pixels,
|
||||
self.stroke_before_buf.as_ref().map(|b| b.len())
|
||||
);
|
||||
let before = match self.stroke_before_buf.take() {
|
||||
Some(buf) if buf.len() == layer_pixels => buf,
|
||||
_ => vec![0u8; layer_pixels],
|
||||
let mut mask_before = None;
|
||||
let before = if self.editing_mask {
|
||||
let gray_len = lw as usize * layer.height as usize;
|
||||
let mb = match self.mask_before_buf.take() {
|
||||
Some(buf) if buf.len() == gray_len => buf,
|
||||
_ => vec![255u8; gray_len],
|
||||
};
|
||||
mask_before = Some(mb.clone());
|
||||
mb
|
||||
} else {
|
||||
match self.stroke_before_buf.take() {
|
||||
Some(buf) if buf.len() == layer_pixels => buf,
|
||||
_ => vec![0u8; layer_pixels],
|
||||
}
|
||||
};
|
||||
log::debug!("[end_stroke] before.len()={}, lw={}", before.len(), lw);
|
||||
|
||||
// Layer 3: Zero-copy after-snapshot via raw pointer.
|
||||
// SAFETY: After end_stroke(), no mutations happen to layer.pixels
|
||||
// until the next begin_stroke(). The background thread only reads.
|
||||
let after_ptr = SendPtr::new(layer.pixels.as_ptr());
|
||||
let after_len = layer.pixels.len();
|
||||
let (after_ptr, after_len) = if self.editing_mask {
|
||||
if layer.mask_pixels.is_none() {
|
||||
layer.mask_pixels = Some(vec![255; (layer.width * layer.height) as usize]);
|
||||
}
|
||||
let mask = layer.mask_pixels.as_ref().unwrap();
|
||||
(SendPtr::new(mask.as_ptr()), mask.len())
|
||||
} else {
|
||||
(SendPtr::new(layer.pixels.as_ptr()), layer.pixels.len())
|
||||
};
|
||||
|
||||
let after_shapes = if let LayerData::Vector { shapes } = &layer.data {
|
||||
Some(shapes.clone())
|
||||
@@ -275,6 +381,7 @@ impl Engine {
|
||||
let style = self.current_tip.style;
|
||||
let bounds = self.last_stroke_bounds;
|
||||
let pending_history = self.pending_history.clone();
|
||||
let is_mask = self.editing_mask;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let t_start = std::time::Instant::now();
|
||||
@@ -290,25 +397,33 @@ impl Engine {
|
||||
before_shapes,
|
||||
after_shapes,
|
||||
description: format!(
|
||||
"Brush Stroke ({})",
|
||||
"{} ({})",
|
||||
if is_mask { "Mask Edit" } else { "Brush Stroke" },
|
||||
crate::stroke_brush::brush_style_label(style)
|
||||
),
|
||||
return_before: None,
|
||||
return_after: None,
|
||||
return_mask_before: mask_before,
|
||||
is_mask,
|
||||
};
|
||||
|
||||
if let Some([sx0, sy0, sx1, sy1]) = bounds {
|
||||
if sx0 < sx1 && sy0 < sy1 {
|
||||
let rw = sx1 - sx0;
|
||||
let rh = sy1 - sy0;
|
||||
let rect_size = (rw * rh * 4) as usize;
|
||||
let rect_size = if is_mask {
|
||||
(rw * rh) as usize
|
||||
} else {
|
||||
(rw * rh * 4) as usize
|
||||
};
|
||||
log::debug!("[end_stroke_bg] bounds=[{},{},{},{}], rw={}, rh={}, rect_size={}, before.len()={}, after.len()={}, lw={}", sx0, sy0, sx1, sy1, rw, rh, rect_size, before.len(), after_slice.len(), lw);
|
||||
let mut before_rect = vec![0u8; rect_size];
|
||||
let mut after_rect = vec![0u8; rect_size];
|
||||
let bpp = if is_mask { 1 } else { 4 };
|
||||
for row in 0..rh {
|
||||
let src_start = (((sy0 + row) * lw + sx0) * 4) as usize;
|
||||
let dst_start = (row * rw * 4) as usize;
|
||||
let len = (rw * 4) as usize;
|
||||
let src_start = (((sy0 + row) * lw + sx0) * bpp) as usize;
|
||||
let dst_start = (row * rw * bpp) as usize;
|
||||
let len = (rw * bpp) as usize;
|
||||
before_rect[dst_start..dst_start + len]
|
||||
.copy_from_slice(&before[src_start..src_start + len]);
|
||||
after_rect[dst_start..dst_start + len]
|
||||
@@ -320,7 +435,9 @@ impl Engine {
|
||||
item.after_pixels = after_rect;
|
||||
item.bounds = Some([sx0, sy0, sx1, sy1]);
|
||||
// Return before buffer to pool (after is a borrowed pointer)
|
||||
item.return_before = Some(before);
|
||||
if !is_mask {
|
||||
item.return_before = Some(before);
|
||||
}
|
||||
pending_history.lock().unwrap().push(item);
|
||||
}
|
||||
log::trace!(
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use hcie_engine_api::Engine;
|
||||
|
||||
#[test]
|
||||
fn test_layer_mask_api_and_editing() {
|
||||
let mut engine = Engine::new(100, 100);
|
||||
let layer_id = engine.active_layer_id();
|
||||
|
||||
// Initially layer has no mask
|
||||
assert!(!engine.has_layer_mask(layer_id));
|
||||
assert!(engine.get_layer_mask_pixels(layer_id).is_none());
|
||||
|
||||
// Add layer mask
|
||||
engine.add_layer_mask(layer_id);
|
||||
assert!(engine.has_layer_mask(layer_id));
|
||||
let mask = engine.get_layer_mask_pixels(layer_id).expect("Mask pixels must exist");
|
||||
assert_eq!(mask.len(), 100 * 100);
|
||||
assert_eq!(mask[0], 255);
|
||||
|
||||
// Toggle mask editing mode
|
||||
assert!(!engine.is_editing_mask());
|
||||
engine.set_editing_mask(true);
|
||||
assert!(engine.is_editing_mask());
|
||||
|
||||
// Set custom mask pixels
|
||||
let custom_mask = vec![128u8; 100 * 100];
|
||||
engine.set_layer_mask_pixels(layer_id, custom_mask.clone(), Some([0, 0, 100, 100]));
|
||||
assert_eq!(engine.get_layer_mask_pixels(layer_id), Some(custom_mask.as_slice()));
|
||||
|
||||
// Remove layer mask
|
||||
engine.remove_layer_mask(layer_id);
|
||||
assert!(!engine.has_layer_mask(layer_id));
|
||||
assert!(engine.get_layer_mask_pixels(layer_id).is_none());
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 268 B |
@@ -724,6 +724,11 @@ pub enum Message {
|
||||
LayerSetFillOpacity(u64, f32),
|
||||
LayerAddGroup,
|
||||
LayerToggleCollapse(u64),
|
||||
LayerAddMask(u64),
|
||||
LayerRemoveMask(u64),
|
||||
LayerToggleMask(u64),
|
||||
LayerSetEditMask(bool),
|
||||
LayerToggleEditMask,
|
||||
|
||||
// ── History ─────────────────────────────────────────
|
||||
HistoryJumpTo(i32),
|
||||
@@ -3686,6 +3691,45 @@ impl HcieIcedApp {
|
||||
return Task::perform(async {}, |_| Message::CompositeRefresh);
|
||||
}
|
||||
|
||||
Message::LayerAddMask(id) => {
|
||||
self.documents[self.active_doc].engine.add_layer_mask(id);
|
||||
self.documents[self.active_doc].modified = true;
|
||||
self.documents[self.active_doc].cached_layers =
|
||||
self.documents[self.active_doc].engine.layer_infos();
|
||||
self.refresh_composite_if_needed();
|
||||
return Task::perform(async {}, |_| Message::CompositeRefresh);
|
||||
}
|
||||
|
||||
Message::LayerRemoveMask(id) => {
|
||||
self.documents[self.active_doc].engine.remove_layer_mask(id);
|
||||
self.documents[self.active_doc].modified = true;
|
||||
self.documents[self.active_doc].cached_layers =
|
||||
self.documents[self.active_doc].engine.layer_infos();
|
||||
self.refresh_composite_if_needed();
|
||||
return Task::perform(async {}, |_| Message::CompositeRefresh);
|
||||
}
|
||||
|
||||
Message::LayerToggleMask(id) => {
|
||||
self.documents[self.active_doc].engine.toggle_layer_mask(id);
|
||||
self.documents[self.active_doc].modified = true;
|
||||
self.documents[self.active_doc].cached_layers =
|
||||
self.documents[self.active_doc].engine.layer_infos();
|
||||
self.refresh_composite_if_needed();
|
||||
return Task::perform(async {}, |_| Message::CompositeRefresh);
|
||||
}
|
||||
|
||||
Message::LayerSetEditMask(editing) => {
|
||||
self.documents[self.active_doc].engine.set_editing_mask(editing);
|
||||
self.documents[self.active_doc].cached_layers =
|
||||
self.documents[self.active_doc].engine.layer_infos();
|
||||
}
|
||||
|
||||
Message::LayerToggleEditMask => {
|
||||
self.documents[self.active_doc].engine.toggle_editing_mask();
|
||||
self.documents[self.active_doc].cached_layers =
|
||||
self.documents[self.active_doc].engine.layer_infos();
|
||||
}
|
||||
|
||||
Message::LayerDelete(id) => {
|
||||
self.documents[self.active_doc].engine.delete_layer(id);
|
||||
self.documents[self.active_doc].modified = true;
|
||||
|
||||
@@ -173,15 +173,23 @@ fn layer_thumbnail<'a>(
|
||||
thumb_cache: &std::collections::HashMap<u64, (u32, u32, Vec<u8>)>,
|
||||
_thumb_gen: u64,
|
||||
colors: ThemeColors,
|
||||
is_active_target: bool,
|
||||
) -> Element<'a, Message> {
|
||||
if entry.is_group {
|
||||
return container(text("\u{1F4C1}").size(16))
|
||||
.width(50)
|
||||
.height(38)
|
||||
.center_y(Length::Fixed(38.0))
|
||||
.width(42)
|
||||
.height(32)
|
||||
.center_y(Length::Fixed(32.0))
|
||||
.into();
|
||||
}
|
||||
|
||||
let border_color = if is_active_target {
|
||||
iced::Color::WHITE
|
||||
} else {
|
||||
colors.border_high
|
||||
};
|
||||
let border_width = if is_active_target { 2 } else { 1 };
|
||||
|
||||
if let Some((tw, th, pixels)) = thumb_cache.get(&entry.info.id) {
|
||||
let (tw32, th32) = (*tw as u32, *th as u32);
|
||||
let handle = iced::widget::image::Handle::from_rgba(tw32, th32, pixels.clone());
|
||||
@@ -189,12 +197,14 @@ fn layer_thumbnail<'a>(
|
||||
.width(Length::Fixed(*tw as f32))
|
||||
.height(Length::Fixed(*th as f32));
|
||||
return container(img)
|
||||
.width(50)
|
||||
.height(38)
|
||||
.center_y(Length::Fixed(38.0))
|
||||
.width(42)
|
||||
.height(32)
|
||||
.center_x(Length::Fixed(42.0))
|
||||
.center_y(Length::Fixed(32.0))
|
||||
.padding(1)
|
||||
.style(move |_theme| iced::widget::container::Style {
|
||||
border: iced::Border::default().color(colors.border_high).width(1),
|
||||
background: Some(iced::Background::Color(iced::Color::BLACK)),
|
||||
border: iced::Border::default().color(border_color).width(border_width),
|
||||
..Default::default()
|
||||
})
|
||||
.into();
|
||||
@@ -213,15 +223,99 @@ fn layer_thumbnail<'a>(
|
||||
color: Some(icon_color),
|
||||
}
|
||||
}))
|
||||
.width(50)
|
||||
.height(38)
|
||||
.center_x(Length::Fixed(50.0))
|
||||
.center_y(Length::Fixed(38.0))
|
||||
.width(42)
|
||||
.height(32)
|
||||
.center_x(Length::Fixed(42.0))
|
||||
.center_y(Length::Fixed(32.0))
|
||||
.style(move |_theme| iced::widget::container::Style {
|
||||
background: Some(iced::Background::Color(iced::Color::from_rgba(
|
||||
1.0, 1.0, 1.0, 0.05,
|
||||
))),
|
||||
border: iced::Border::default().color(colors.border_high).width(1),
|
||||
border: iced::Border::default().color(border_color).width(border_width),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Build the black-and-white thumbnail element for a layer mask.
|
||||
fn mask_thumbnail<'a>(
|
||||
engine: &hcie_engine_api::Engine,
|
||||
layer_id: u64,
|
||||
colors: ThemeColors,
|
||||
is_active_target: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let border_color = if is_active_target {
|
||||
iced::Color::WHITE
|
||||
} else {
|
||||
colors.border_high
|
||||
};
|
||||
let border_width = if is_active_target { 2 } else { 1 };
|
||||
|
||||
if let Some(mask_pixels) = engine.get_layer_mask_pixels(layer_id) {
|
||||
if let Some(info) = engine.get_layer_info(layer_id) {
|
||||
let (cw, ch) = (info.width as usize, info.height as usize);
|
||||
if cw > 0 && ch > 0 {
|
||||
let (mw, mh) = match engine.get_layer_mask_bounds(layer_id) {
|
||||
Some(mb) if (mb[3] > mb[1]) && (mb[2] > mb[0]) => {
|
||||
let w = (mb[3] - mb[1]) as usize;
|
||||
let h = (mb[2] - mb[0]) as usize;
|
||||
if mask_pixels.len() == w * h {
|
||||
(w, h)
|
||||
} else {
|
||||
(cw, ch)
|
||||
}
|
||||
}
|
||||
_ => (cw, ch),
|
||||
};
|
||||
|
||||
if mask_pixels.len() == mw * mh {
|
||||
let tw = 40usize;
|
||||
let th = (40 * mh / mw).max(16).min(32);
|
||||
let mut rgba = Vec::with_capacity(tw * th * 4);
|
||||
for ty in 0..th {
|
||||
let sy = (ty * mh / th).min(mh - 1);
|
||||
for tx in 0..tw {
|
||||
let sx = (tx * mw / tw).min(mw - 1);
|
||||
let val = mask_pixels[sy * mw + sx];
|
||||
rgba.push(val);
|
||||
rgba.push(val);
|
||||
rgba.push(val);
|
||||
rgba.push(255);
|
||||
}
|
||||
}
|
||||
let handle = iced::widget::image::Handle::from_rgba(tw as u32, th as u32, rgba);
|
||||
let img = iced::widget::Image::new(handle)
|
||||
.width(Length::Fixed(tw as f32))
|
||||
.height(Length::Fixed(th as f32));
|
||||
return container(img)
|
||||
.width(42)
|
||||
.height(32)
|
||||
.center_x(Length::Fixed(42.0))
|
||||
.center_y(Length::Fixed(32.0))
|
||||
.padding(1)
|
||||
.style(move |_theme| iced::widget::container::Style {
|
||||
background: Some(iced::Background::Color(iced::Color::BLACK)),
|
||||
border: iced::Border::default().color(border_color).width(border_width),
|
||||
..Default::default()
|
||||
})
|
||||
.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
container(text("Mask").size(9).style(move |_theme: &iced::Theme| {
|
||||
iced::widget::text::Style {
|
||||
color: Some(colors.text_primary),
|
||||
}
|
||||
}))
|
||||
.width(42)
|
||||
.height(32)
|
||||
.center_x(Length::Fixed(42.0))
|
||||
.center_y(Length::Fixed(32.0))
|
||||
.style(move |_theme| iced::widget::container::Style {
|
||||
background: Some(iced::Background::Color(iced::Color::BLACK)),
|
||||
border: iced::Border::default().color(border_color).width(border_width),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
@@ -282,14 +376,44 @@ pub fn view<'a>(
|
||||
.padding([1, 4])
|
||||
.style(move |_theme, _status| flat_btn_style(colors));
|
||||
|
||||
let mask_banner: Element<'a, Message> = if engine.is_editing_mask() {
|
||||
let white_btn = button(text("⚪ White (Reveal)").size(10).style(move |_theme: &iced::Theme| {
|
||||
iced::widget::text::Style { color: Some(iced::Color::BLACK) }
|
||||
}))
|
||||
.on_press(Message::FgColorChanged([255, 255, 255, 255]))
|
||||
.padding([2, 4])
|
||||
.style(move |_theme, _status| flat_btn_style(colors));
|
||||
|
||||
let black_btn = button(text("⬛ Black (Hide)").size(10).style(move |_theme: &iced::Theme| {
|
||||
iced::widget::text::Style { color: Some(iced::Color::WHITE) }
|
||||
}))
|
||||
.on_press(Message::FgColorChanged([0, 0, 0, 255]))
|
||||
.padding([2, 4])
|
||||
.style(move |_theme, _status| flat_btn_style(colors));
|
||||
|
||||
container(
|
||||
column![
|
||||
text("Mask Mode Active:").size(10).style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
||||
color: Some(colors.accent),
|
||||
}),
|
||||
row![white_btn, black_btn].spacing(4).align_y(iced::Alignment::Center),
|
||||
].spacing(2)
|
||||
)
|
||||
.padding(4)
|
||||
.style(move |_theme| iced::widget::container::Style {
|
||||
background: Some(iced::Background::Color(iced::Color::from_rgba(0.2, 0.2, 0.2, 0.5))),
|
||||
border: iced::Border::default().color(colors.accent).width(1),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
} else {
|
||||
Space::with_width(Length::Fixed(0.0)).into()
|
||||
};
|
||||
|
||||
let controls = column![
|
||||
mask_banner,
|
||||
row![blend_list].spacing(2),
|
||||
opacity_row,
|
||||
text("Fill opacity: unavailable in hcie-engine-api")
|
||||
.size(BODY)
|
||||
.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
||||
color: Some(colors.text_secondary),
|
||||
}),
|
||||
row![lock_btn, fx_btn]
|
||||
.spacing(8)
|
||||
.align_y(iced::Alignment::Center),
|
||||
@@ -352,8 +476,42 @@ pub fn view<'a>(
|
||||
12.0,
|
||||
);
|
||||
|
||||
// Thumbnail
|
||||
let thumb_elem = layer_thumbnail(entry, thumb_cache, thumb_gen, colors);
|
||||
// Thumbnail targets
|
||||
let is_layer_target_active = is_active && !engine.is_editing_mask();
|
||||
let is_mask_target_active = is_active && engine.is_editing_mask();
|
||||
|
||||
let thumb_elem = iced::widget::mouse_area(layer_thumbnail(
|
||||
entry,
|
||||
thumb_cache,
|
||||
thumb_gen,
|
||||
colors,
|
||||
is_layer_target_active,
|
||||
))
|
||||
.on_press(Message::LayerSetEditMask(false));
|
||||
|
||||
// Layer mask thumbnail preview
|
||||
let has_mask = engine.has_layer_mask(info.id);
|
||||
let link_icon: Element<'a, Message> = if has_mask {
|
||||
svg_icon_btn(
|
||||
"icons/link.svg",
|
||||
"Link Mask",
|
||||
Message::LayerToggleEditMask,
|
||||
colors,
|
||||
12.0,
|
||||
)
|
||||
} else {
|
||||
Space::with_width(Length::Fixed(0.0)).into()
|
||||
};
|
||||
|
||||
let mask_elem: Element<'a, Message> = if has_mask {
|
||||
let mask_btn = button(mask_thumbnail(engine, info.id, colors, is_mask_target_active))
|
||||
.on_press(Message::LayerSetEditMask(!is_mask_target_active))
|
||||
.padding(0)
|
||||
.style(move |_theme, _status| flat_btn_style(colors));
|
||||
mask_btn.into()
|
||||
} else {
|
||||
Space::with_width(Length::Fixed(0.0)).into()
|
||||
};
|
||||
|
||||
// Layer name
|
||||
let c = if is_active {
|
||||
@@ -412,7 +570,7 @@ pub fn view<'a>(
|
||||
|
||||
// Main layer row
|
||||
let name_row =
|
||||
row![collapse, vis_btn, row_lock, thumb_elem, name_text, fx_icon, type_badge,]
|
||||
row![collapse, vis_btn, row_lock, thumb_elem, link_icon, mask_elem, name_text, fx_icon, type_badge,]
|
||||
.spacing(2)
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
@@ -526,10 +684,18 @@ pub fn view<'a>(
|
||||
.padding([3, 6])
|
||||
.style(move |_theme, _status| flat_btn_style(colors));
|
||||
let duplicate_btn = button(text("Duplicate").size(BODY)).padding([5, 8]);
|
||||
let mask_btn = button(text("Mask").size(BODY)).padding([5, 8]);
|
||||
let has_active_mask = engine.has_layer_mask(active_layer_id);
|
||||
let mask_btn_label = if has_active_mask { "Remove Mask" } else { "Add Mask" };
|
||||
let mask_btn = button(text(mask_btn_label).size(BODY))
|
||||
.on_press(if has_active_mask {
|
||||
Message::LayerRemoveMask(active_layer_id)
|
||||
} else {
|
||||
Message::LayerAddMask(active_layer_id)
|
||||
})
|
||||
.padding([5, 8])
|
||||
.style(move |_theme, _status| flat_btn_style(colors));
|
||||
let rasterize_btn = button(text("Rasterize").size(BODY)).padding([5, 8]);
|
||||
let toolbar = column![
|
||||
text("Unavailable: duplicate, mask, rasterize").size(BODY),
|
||||
row![duplicate_btn, mask_btn]
|
||||
.spacing(2)
|
||||
.align_y(iced::Alignment::Center),
|
||||
|
||||
+157
-33
@@ -106,13 +106,27 @@ pub fn save_kra(
|
||||
let base_filename = format!("layer{}", i + 1);
|
||||
|
||||
match layer.layer_type {
|
||||
LayerType::Raster | LayerType::Mask => {
|
||||
LayerType::Raster | LayerType::Mask | LayerType::Group => {
|
||||
let encoded = encode_layer_vers(layer);
|
||||
let layer_path = format!("{}/layers/{}", internal_name, base_filename);
|
||||
zip.start_file(&layer_path, zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(&encoded)
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(&encoded).map_err(|e| e.to_string())?;
|
||||
|
||||
if let Some(ref mask) = layer.mask_pixels {
|
||||
let mask_filename = format!("{}_mask", base_filename);
|
||||
let mask_encoded = encode_mask_vers(layer, mask);
|
||||
let mask_path = format!("{}/layers/{}", internal_name, mask_filename);
|
||||
zip.start_file(&mask_path, zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(&mask_encoded).map_err(|e| e.to_string())?;
|
||||
|
||||
let mask_dp_path = format!("{}/layers/{}.defaultpixel", internal_name, mask_filename);
|
||||
zip.start_file(&mask_dp_path, zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(&[0u8; 1]) // 1 byte for Alpha
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let icc_path = format!("{}/layers/{}.icc", internal_name, base_filename);
|
||||
zip.start_file(&icc_path, zip::write::FileOptions::<'_, ()>::default())
|
||||
@@ -125,6 +139,18 @@ pub fn save_kra(
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(&[0u8; 4])
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if layer.layer_type == LayerType::Group {
|
||||
let group_json = serde_json::json!({
|
||||
"collapsed": layer.collapsed,
|
||||
"parent_id": layer.parent_id,
|
||||
});
|
||||
let json_path = format!("{}/layers/{}.grouplayer", internal_name, base_filename);
|
||||
zip.start_file(&json_path, zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(group_json.to_string().as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
LayerType::Vector | LayerType::Text => {
|
||||
let layer_path = format!("{}/layers/{}", internal_name, base_filename);
|
||||
@@ -175,22 +201,7 @@ pub fn save_kra(
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
LayerType::Group => {
|
||||
let group_json = serde_json::json!({
|
||||
"collapsed": layer.collapsed,
|
||||
"parent_id": layer.parent_id,
|
||||
});
|
||||
let group_path =
|
||||
format!("{}/layers/group/{}.json", internal_name, base_filename);
|
||||
zip.start_file(&group_path, zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(
|
||||
serde_json::to_string_pretty(&group_json)
|
||||
.unwrap_or_default()
|
||||
.as_bytes(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,9 +214,16 @@ fn generate_maindoc(layers: &[Layer], w: u32, h: u32, name: &str) -> String {
|
||||
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||
xml.push_str("<!DOCTYPE DOC PUBLIC '-//KDE//DTD krita 2.0//EN' 'http://www.calligra.org/DTD/krita-2.0.dtd'>\n");
|
||||
xml.push_str("<DOC xmlns=\"http://www.calligra.org/DTD/krita\" editor=\"Krita\" kritaVersion=\"6.0.1\" syntaxVersion=\"2.0\">\n");
|
||||
let escaped_doc_name = name
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'");
|
||||
|
||||
xml.push_str(&format!(
|
||||
" <IMAGE width=\"{}\" height=\"{}\" name=\"{}\" mime=\"application/x-kra\" colorspacename=\"RGBA\" channeldepth=\"U8\" profile=\"sRGB-elle-V2-g10.icc\" x-res=\"100\" y-res=\"100\">\n",
|
||||
w, h, name
|
||||
w, h, escaped_doc_name
|
||||
));
|
||||
xml.push_str(" <layers>\n");
|
||||
|
||||
@@ -213,7 +231,7 @@ fn generate_maindoc(layers: &[Layer], w: u32, h: u32, name: &str) -> String {
|
||||
let node_type = match layer.layer_type {
|
||||
LayerType::Raster | LayerType::Mask => "paintlayer",
|
||||
LayerType::Vector | LayerType::Text => "shapelayer",
|
||||
LayerType::Group => "grouplayer",
|
||||
LayerType::Group => "paintlayer",
|
||||
};
|
||||
|
||||
let visible = if layer.visible { "1" } else { "0" };
|
||||
@@ -222,10 +240,28 @@ fn generate_maindoc(layers: &[Layer], w: u32, h: u32, name: &str) -> String {
|
||||
let uuid = format!("{{{:032x}}}", layer.id);
|
||||
let compositeop = blend_mode_to_krita_compositeop(layer.blend_mode);
|
||||
|
||||
let escaped_name = layer.name
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'");
|
||||
|
||||
xml.push_str(&format!(
|
||||
" <layer name=\"{}\" filename=\"{}\" nodetype=\"{}\" visible=\"{}\" opacity=\"{}\" uuid=\"{}\" compositeop=\"{}\" colorspacename=\"RGBA\" channeldepth=\"U8\" profile=\"sRGB-elle-V2-g10.icc\" x=\"0\" y=\"0\" />\n",
|
||||
layer.name, filename, node_type, visible, opacity, uuid, compositeop
|
||||
" <layer name=\"{}\" filename=\"{}\" nodetype=\"{}\" visible=\"{}\" opacity=\"{}\" uuid=\"{}\" compositeop=\"{}\" colorspacename=\"RGBA\" channeldepth=\"U8\" profile=\"sRGB-elle-V2-g10.icc\" x=\"0\" y=\"0\">\n",
|
||||
escaped_name, filename, node_type, visible, opacity, uuid, compositeop
|
||||
));
|
||||
if layer.mask_pixels.is_some() {
|
||||
xml.push_str(" <layers>\n");
|
||||
let mask_filename = format!("{}_mask", filename);
|
||||
let mask_uuid = format!("{{{:032x}}}", layer.id.wrapping_add(1)); // pseudo uuid
|
||||
xml.push_str(&format!(
|
||||
" <layer name=\"Transparency Mask\" filename=\"{}\" nodetype=\"transparencymask\" visible=\"1\" opacity=\"255\" uuid=\"{}\" compositeop=\"normal\" colorspacename=\"Alpha\" channeldepth=\"U8\" profile=\"\" x=\"0\" y=\"0\" />\n",
|
||||
mask_filename, mask_uuid
|
||||
));
|
||||
xml.push_str(" </layers>\n");
|
||||
}
|
||||
xml.push_str(" </layer>\n");
|
||||
}
|
||||
|
||||
xml.push_str(" </layers>\n");
|
||||
@@ -285,19 +321,107 @@ fn encode_layer_vers(layer: &Layer) -> Vec<u8> {
|
||||
ty += th;
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
writeln!(buf, "VERSION 2").unwrap();
|
||||
writeln!(buf, "TILEWIDTH 64").unwrap();
|
||||
writeln!(buf, "TILEHEIGHT 64").unwrap();
|
||||
writeln!(buf, "PIXELSIZE 4").unwrap();
|
||||
writeln!(buf, "DATA {}", tiles.len()).unwrap();
|
||||
let mut output = Vec::new();
|
||||
writeln!(output, "VERSION 2").unwrap();
|
||||
writeln!(output, "TILEWIDTH 64").unwrap();
|
||||
writeln!(output, "TILEHEIGHT 64").unwrap();
|
||||
writeln!(output, "PIXELSIZE 4").unwrap();
|
||||
writeln!(output, "DATA {}", tiles.len()).unwrap();
|
||||
|
||||
for (tx, ty, data) in tiles {
|
||||
writeln!(buf, "{},{},LZF,{}", tx, ty, data.len()).unwrap();
|
||||
buf.extend_from_slice(&data);
|
||||
tiles.sort_by_key(|(x, y, _)| (*y, *x));
|
||||
for (tx, ty, tile_data) in tiles {
|
||||
let header = format!("{},{},LZF,{}\n", tx, ty, tile_data.len());
|
||||
output.extend_from_slice(header.as_bytes());
|
||||
output.extend_from_slice(&tile_data);
|
||||
}
|
||||
|
||||
buf
|
||||
output
|
||||
}
|
||||
|
||||
fn encode_mask_vers(layer: &Layer, mask: &[u8]) -> Vec<u8> {
|
||||
let tw: i32 = 64;
|
||||
let th: i32 = 64;
|
||||
let plane_size = (tw * th) as usize;
|
||||
let w = layer.width as i32;
|
||||
let h = layer.height as i32;
|
||||
|
||||
let mut tiles: Vec<(i32, i32, Vec<u8>)> = Vec::new();
|
||||
let mut ty = 0;
|
||||
while ty < h {
|
||||
let mut tx = 0;
|
||||
while tx < w {
|
||||
let mut tile_raw = vec![0u8; plane_size];
|
||||
let mut has_content = false;
|
||||
|
||||
for y in 0..th {
|
||||
let ly = ty + y;
|
||||
if ly >= h {
|
||||
continue;
|
||||
}
|
||||
for x in 0..tw {
|
||||
let lx = tx + x;
|
||||
if lx >= w {
|
||||
continue;
|
||||
}
|
||||
let tidx = (y as usize) * (tw as usize) + (x as usize);
|
||||
let mut v = layer.mask_default_color;
|
||||
if let Some(mb) = layer.mask_bounds {
|
||||
let m_top = mb[0] as i32;
|
||||
let m_left = mb[1] as i32;
|
||||
let m_bottom = mb[2] as i32;
|
||||
let m_right = mb[3] as i32;
|
||||
if ly >= m_top && ly < m_bottom && lx >= m_left && lx < m_right {
|
||||
let mask_w = m_right - m_left;
|
||||
let mask_idx = ((ly - m_top) * mask_w + (lx - m_left)) as usize;
|
||||
if mask_idx < mask.len() {
|
||||
v = mask[mask_idx];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let idx = ly as usize * layer.width as usize + lx as usize;
|
||||
if idx < mask.len() {
|
||||
v = mask[idx];
|
||||
}
|
||||
}
|
||||
|
||||
tile_raw[tidx] = v;
|
||||
if v < 255 {
|
||||
has_content = true; // Any masking means content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if has_content {
|
||||
let mut compressed = vec![1u8];
|
||||
let mut i = 0;
|
||||
while i < tile_raw.len() {
|
||||
let chunk = (tile_raw.len() - i).min(32);
|
||||
compressed.push((chunk - 1) as u8);
|
||||
compressed.extend_from_slice(&tile_raw[i..i + chunk]);
|
||||
i += chunk;
|
||||
}
|
||||
tiles.push((tx, ty, compressed));
|
||||
}
|
||||
tx += tw;
|
||||
}
|
||||
ty += th;
|
||||
}
|
||||
|
||||
let mut output = Vec::new();
|
||||
output.extend_from_slice(b"VERSION 2\n");
|
||||
output.extend_from_slice(b"TILEWIDTH 64\n");
|
||||
output.extend_from_slice(b"TILEHEIGHT 64\n");
|
||||
output.extend_from_slice(b"PIXELSIZE 1\n");
|
||||
output.extend_from_slice(b"DATA\n");
|
||||
|
||||
tiles.sort_by_key(|(x, y, _)| (*y, *x));
|
||||
for (tx, ty, tile_data) in tiles {
|
||||
let header = format!("{},{},LZF,{}\n", tx, ty, tile_data.len());
|
||||
output.extend_from_slice(header.as_bytes());
|
||||
output.extend_from_slice(&tile_data);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn shapes_to_svg(shapes: &[VectorShape], w: u32, h: u32) -> String {
|
||||
|
||||
+85
-14
@@ -40,6 +40,7 @@ pub fn import_kra(path: &Path) -> Result<Vec<Layer>, String> {
|
||||
}
|
||||
|
||||
/// Purpose: Intermediate metadata structure representing Krita layer elements.
|
||||
#[derive(Debug, Clone)]
|
||||
struct KritaLayerInfo {
|
||||
name: String,
|
||||
filename: String,
|
||||
@@ -50,6 +51,7 @@ struct KritaLayerInfo {
|
||||
nodetype: String,
|
||||
compositeop: String,
|
||||
layerstyle_uuid: String,
|
||||
parent_filename: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +113,8 @@ fn parse_vers_tiles(
|
||||
return Err("DATA marker not found".into());
|
||||
}
|
||||
|
||||
let mut pixels = vec![0u8; (doc_w * doc_h * 4) as usize];
|
||||
let bpp = if pixel_size == 1 { 1 } else { 4 };
|
||||
let mut pixels = vec![0u8; (doc_w * doc_h * bpp) as usize];
|
||||
let plane_size = tile_w * tile_h;
|
||||
let mut offset = data_offset;
|
||||
let mut tiles_decoded = 0usize;
|
||||
@@ -173,7 +176,7 @@ fn parse_vers_tiles(
|
||||
if target_x >= 0 && target_x < doc_w as i32
|
||||
&& target_y >= 0 && target_y < doc_h as i32
|
||||
{
|
||||
let target_idx = (target_y as usize * doc_w as usize + target_x as usize) * 4;
|
||||
let target_idx = (target_y as usize * doc_w as usize + target_x as usize) * bpp as usize;
|
||||
let src_idx = y * tile_w + x;
|
||||
|
||||
if pixel_size == 4 {
|
||||
@@ -186,6 +189,8 @@ fn parse_vers_tiles(
|
||||
pixels[target_idx + 1] = tile_data[src_idx + 3 * plane_size];
|
||||
pixels[target_idx + 2] = tile_data[src_idx + 1 * plane_size];
|
||||
pixels[target_idx + 3] = tile_data[src_idx + 7 * plane_size];
|
||||
} else if pixel_size == 1 {
|
||||
pixels[target_idx] = tile_data[src_idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,8 +201,18 @@ fn parse_vers_tiles(
|
||||
|
||||
if tiles_decoded == 0 {
|
||||
if let Some(dp) = default_pixel {
|
||||
for chunk in pixels.chunks_exact_mut(4) {
|
||||
chunk.copy_from_slice(&dp);
|
||||
if pixel_size == 1 {
|
||||
let v = dp.get(0).copied().unwrap_or(0);
|
||||
for p in pixels.iter_mut() {
|
||||
*p = v;
|
||||
}
|
||||
} else {
|
||||
for chunk in pixels.chunks_exact_mut(4) {
|
||||
chunk[0] = dp[2];
|
||||
chunk[1] = dp[1];
|
||||
chunk[2] = dp[0];
|
||||
chunk[3] = dp[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -624,10 +639,13 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
let mut height = 600u32;
|
||||
let mut layers_info = Vec::new();
|
||||
let mut buf = Vec::new();
|
||||
let mut layer_stack: Vec<String> = Vec::new();
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
|
||||
let event = reader.read_event_into(&mut buf);
|
||||
match event {
|
||||
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||
let is_empty = matches!(event, Ok(Event::Empty(_)));
|
||||
let name = e.name();
|
||||
let tag_name = std::str::from_utf8(name.as_ref()).unwrap_or("").to_lowercase();
|
||||
if tag_name == "image" {
|
||||
@@ -681,7 +699,7 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
if has_filename && !filename.is_empty() {
|
||||
layers_info.push(KritaLayerInfo {
|
||||
name,
|
||||
filename,
|
||||
filename: filename.clone(),
|
||||
visible,
|
||||
opacity,
|
||||
x: lx,
|
||||
@@ -689,10 +707,29 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
nodetype,
|
||||
compositeop,
|
||||
layerstyle_uuid,
|
||||
parent_filename: layer_stack.last().cloned(),
|
||||
});
|
||||
if !is_empty {
|
||||
layer_stack.push(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Event::End(ref e)) => {
|
||||
let name = e.name();
|
||||
let tag_name = std::str::from_utf8(name.as_ref()).unwrap_or("").to_lowercase();
|
||||
if tag_name == "layer"
|
||||
|| tag_name == "paintlayer"
|
||||
|| tag_name == "vectorlayer"
|
||||
|| tag_name == "filllayer"
|
||||
|| tag_name == "shapelayer"
|
||||
|| tag_name == "adjustmentlayer"
|
||||
|| tag_name == "filterlayer"
|
||||
|| tag_name == "grouplayer"
|
||||
{
|
||||
layer_stack.pop();
|
||||
}
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(format!("XML parse error: {}", e)),
|
||||
_ => {}
|
||||
@@ -701,7 +738,7 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
}
|
||||
|
||||
let all_files: Vec<String> = archive.file_names().map(|n| n.to_string()).collect();
|
||||
let mut layers = Vec::new();
|
||||
let mut intermediate_layers: Vec<(String, Layer)> = Vec::new();
|
||||
|
||||
let asl_styles: std::collections::HashMap<String, Vec<hcie_protocol::effects::LayerEffect>> = {
|
||||
let mut styles_map = std::collections::HashMap::new();
|
||||
@@ -724,7 +761,7 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
styles_map
|
||||
};
|
||||
|
||||
for info in layers_info.into_iter().rev() {
|
||||
for info in layers_info.clone().into_iter().rev() {
|
||||
if info.nodetype == "adjustmentlayer" || info.nodetype == "filterlayer" {
|
||||
let pixels = vec![0u8; (width * height * 4) as usize];
|
||||
let mut layer = Layer::from_rgba(&info.name, width, height, pixels);
|
||||
@@ -739,7 +776,11 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
layer.styles = effects.iter().map(kra_effect_to_layer_style).collect();
|
||||
}
|
||||
}
|
||||
layers.push(layer);
|
||||
intermediate_layers.push((info.filename.clone(), layer));
|
||||
continue;
|
||||
}
|
||||
|
||||
if info.nodetype == "transparencymask" {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -827,19 +868,49 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
layer.adjustment_raw = Some((*b"svg ", buf.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if !info.layerstyle_uuid.is_empty() {
|
||||
if let Some(effects) = asl_styles.get(&info.layerstyle_uuid) {
|
||||
layer.effects = effects.clone();
|
||||
layer.styles = effects.iter().map(kra_effect_to_layer_style).collect();
|
||||
}
|
||||
}
|
||||
|
||||
layers.push(layer);
|
||||
intermediate_layers.push((info.filename.clone(), layer));
|
||||
}
|
||||
|
||||
// Pass 2: Transparency Masks
|
||||
let layers_info_clone = layers_info.clone();
|
||||
for info in layers_info_clone.into_iter().rev() {
|
||||
if info.nodetype == "transparencymask" {
|
||||
if let Some(parent_filename) = info.parent_filename {
|
||||
let layer_path = find_krita_layer_file(&all_files, &info.filename, &info.nodetype);
|
||||
if let Some(zip_path) = layer_path {
|
||||
let mut buf = Vec::new();
|
||||
{
|
||||
let mut file = match archive.by_name(&zip_path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let _ = file.read_to_end(&mut buf);
|
||||
}
|
||||
if !buf.is_empty() {
|
||||
let doc_w = width;
|
||||
let doc_h = height;
|
||||
let default_pixel = read_defaultpixel(archive, &info.filename);
|
||||
if let Ok(mask_pixels) = parse_vers_tiles(&buf, doc_w, doc_h, info.x, info.y, default_pixel) {
|
||||
if let Some((_, parent_layer)) = intermediate_layers.iter_mut().find(|(fname, _)| *fname == parent_filename) {
|
||||
parent_layer.mask_pixels = Some(mask_pixels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let layers = intermediate_layers.into_iter().map(|(_, l)| l).collect::<Vec<_>>();
|
||||
|
||||
if layers.is_empty() {
|
||||
Err("No valid layers found in maindoc.xml".to_string())
|
||||
return Err("No valid layers found in maindoc.xml".to_string());
|
||||
} else {
|
||||
Ok(layers)
|
||||
}
|
||||
|
||||
+67
-2
@@ -27,6 +27,14 @@ struct HcieLayerMeta {
|
||||
effects: Vec<hcie_protocol::effects::LayerEffect>,
|
||||
#[serde(default = "default_fill_opacity")]
|
||||
fill_opacity: f32,
|
||||
#[serde(default)]
|
||||
mask_bounds: Option<[i32; 4]>,
|
||||
#[serde(default)]
|
||||
mask_default_color: u8,
|
||||
#[serde(default)]
|
||||
mask_offset: Option<u64>,
|
||||
#[serde(default)]
|
||||
mask_len: Option<u64>,
|
||||
}
|
||||
|
||||
fn default_fill_opacity() -> f32 { 1.0 }
|
||||
@@ -44,10 +52,22 @@ pub fn save_native(layers: &[Layer], path: &Path) -> Result<(), String> {
|
||||
|
||||
let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut pixel_offset = 0u64;
|
||||
let mut binary_offset = 0u64;
|
||||
let mut layer_metas = Vec::with_capacity(layers.len());
|
||||
for layer in layers {
|
||||
let pixel_len = layer.pixels.len() as u64;
|
||||
let pixel_offset = binary_offset;
|
||||
binary_offset += pixel_len;
|
||||
|
||||
let (mask_offset, mask_len) = if let Some(ref mask) = layer.mask_pixels {
|
||||
let m_off = binary_offset;
|
||||
let m_len = mask.len() as u64;
|
||||
binary_offset += m_len;
|
||||
(Some(m_off), Some(m_len))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
layer_metas.push(HcieLayerMeta {
|
||||
id: layer.id,
|
||||
name: layer.name.clone(),
|
||||
@@ -67,8 +87,11 @@ pub fn save_native(layers: &[Layer], path: &Path) -> Result<(), String> {
|
||||
pixel_len,
|
||||
effects: layer.effects.clone(),
|
||||
fill_opacity: layer.fill_opacity,
|
||||
mask_bounds: layer.mask_bounds,
|
||||
mask_default_color: layer.mask_default_color,
|
||||
mask_offset,
|
||||
mask_len,
|
||||
});
|
||||
pixel_offset += pixel_len;
|
||||
}
|
||||
|
||||
let doc_meta = HcieDocumentMeta {
|
||||
@@ -88,6 +111,9 @@ pub fn save_native(layers: &[Layer], path: &Path) -> Result<(), String> {
|
||||
|
||||
for layer in layers {
|
||||
file.write_all(&layer.pixels).map_err(|e| e.to_string())?;
|
||||
if let Some(ref mask) = layer.mask_pixels {
|
||||
file.write_all(mask).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -144,10 +170,49 @@ pub fn load_native(path: &Path) -> Result<Vec<Layer>, String> {
|
||||
layer.id = meta.id;
|
||||
layer.effects = meta.effects;
|
||||
layer.fill_opacity = meta.fill_opacity;
|
||||
|
||||
if let (Some(m_off), Some(m_len)) = (meta.mask_offset, meta.mask_len) {
|
||||
let m_off_sz = m_off as usize;
|
||||
let m_len_sz = m_len as usize;
|
||||
if m_off_sz + m_len_sz <= pixel_data.len() {
|
||||
layer.mask_pixels = Some(pixel_data[m_off_sz..m_off_sz + m_len_sz].to_vec());
|
||||
layer.mask_bounds = meta.mask_bounds;
|
||||
layer.mask_default_color = meta.mask_default_color;
|
||||
}
|
||||
}
|
||||
|
||||
layers.push(layer);
|
||||
}
|
||||
|
||||
Ok(layers)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_save_load_native_layer_mask() {
|
||||
let mut layer = Layer::new_transparent("Test Layer", 10, 10);
|
||||
let mask = vec![128u8; 100];
|
||||
layer.mask_pixels = Some(mask.clone());
|
||||
layer.mask_bounds = Some([0, 0, 10, 10]);
|
||||
layer.mask_default_color = 255;
|
||||
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let path = temp_dir.join("test_mask_native.hcie");
|
||||
|
||||
save_native(&[layer], &path).expect("Failed to save native document");
|
||||
|
||||
let loaded = load_native(&path).expect("Failed to load native document");
|
||||
assert_eq!(loaded.len(), 1);
|
||||
let loaded_layer = &loaded[0];
|
||||
assert_eq!(loaded_layer.mask_pixels, Some(mask));
|
||||
assert_eq!(loaded_layer.mask_bounds, Some([0, 0, 10, 10]));
|
||||
assert_eq!(loaded_layer.mask_default_color, 255);
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -615,7 +615,7 @@ pub fn parse_psd_sequential_full(bytes: &[u8]) -> Result<ParsedPsdData, String>
|
||||
let mut mask_default_color = 0;
|
||||
|
||||
for channel in &layer.channels {
|
||||
if channel.id == -2 {
|
||||
if (channel.id == -2 || channel.id == -3) && decompressed_mask.is_none() {
|
||||
if let Some(mask) = &layer.mask_info {
|
||||
let w = (mask.right - mask.left) as u32;
|
||||
let h = (mask.bottom - mask.top) as u32;
|
||||
@@ -892,3 +892,19 @@ pub fn import_psd(path: &Path) -> Result<Vec<hcie_protocol::Layer>, String> {
|
||||
|
||||
Ok(flat_layers)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_example3_mask_import() {
|
||||
let path = std::path::Path::new("/mnt/extra/00_PROJECTS/hcie-rust-v3.05/_images/_test_images/example3/Example3-mini.psd");
|
||||
if path.exists() {
|
||||
let layers = import_psd(path).expect("import_psd failed");
|
||||
for l in &layers {
|
||||
println!("TEST_LAYER: '{}' has_mask={} bounds={:?}", l.name, l.mask_pixels.is_some(), l.mask_bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ cargo run -p hcie-wx-app --bin hcie-wx-app
|
||||
cargo run --bin hcie-gui
|
||||
cargo run --example gui
|
||||
cargo run -p hcie-iced-gui
|
||||
cargo build -p hcie-iced-gui --bin hcie-iced
|
||||
scripts/cargo-with-build-id.sh run -p hcie-iced-gui
|
||||
|
||||
##test
|
||||
|
||||
Reference in New Issue
Block a user