Implement stable serialization and restoration for dock layouts, including auto-hidden and floating panels

- Add `persistence.rs` for stable serialization of dock layouts without runtime identifiers.
- Introduce `preview.rs` for geometry handling of dock drop previews.
- Create `sizing.rs` to manage content-aware sizing policies and constraint solving for dock panels.
- Implement `welcome.rs` to provide a welcome surface when no documents are open, featuring New and Open actions.
This commit is contained in:
2026-07-16 22:10:22 +03:00
parent 1240d25011
commit b09795ccff
72 changed files with 11898 additions and 3115 deletions
+15 -3
View File
@@ -15,8 +15,20 @@ pub fn composite_layers_region(
layers: &[Layer],
canvas_width: u32,
canvas_height: u32,
x0: u32, y0: u32, x1: u32, y1: u32,
x0: u32,
y0: u32,
x1: u32,
y1: u32,
output: &mut [u8],
) {
parallel::composite_layers_region_parallel(layers, canvas_width, canvas_height, x0, y0, x1, y1, output)
}
parallel::composite_layers_region_parallel(
layers,
canvas_width,
canvas_height,
x0,
y0,
x1,
y1,
output,
)
}
+291 -99
View File
@@ -1,39 +1,51 @@
use hcie_protocol::Layer;
use hcie_blend::blend_pixels;
use hcie_fx::protocol_to_hcie_fx_effect;
use hcie_protocol::Layer;
use rayon::prelude::*;
pub(crate) fn apply_curves(r: u8, g: u8, b: u8, lut_r: &[u8], lut_g: &[u8], lut_b: &[u8]) -> [u8; 3] {
[
lut_r[r as usize],
lut_g[g as usize],
lut_b[b as usize],
]
pub(crate) fn apply_curves(
r: u8,
g: u8,
b: u8,
lut_r: &[u8],
lut_g: &[u8],
lut_b: &[u8],
) -> [u8; 3] {
[lut_r[r as usize], lut_g[g as usize], lut_b[b as usize]]
}
pub(crate) fn apply_gradient_map(r: u8, g: u8, b: u8, lut_r: &[u8], lut_g: &[u8], lut_b: &[u8]) -> [u8; 3] {
pub(crate) fn apply_gradient_map(
r: u8,
g: u8,
b: u8,
lut_r: &[u8],
lut_g: &[u8],
lut_b: &[u8],
) -> [u8; 3] {
let dr = r as f32;
let dg = g as f32;
let db = b as f32;
let l = (0.30 * dr + 0.59 * dg + 0.11 * db).clamp(0.0, 255.0).round() as usize;
[
lut_r[l],
lut_g[l],
lut_b[l],
]
let l = (0.30 * dr + 0.59 * dg + 0.11 * db)
.clamp(0.0, 255.0)
.round() as usize;
[lut_r[l], lut_g[l], lut_b[l]]
}
pub(crate) fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let c_max = r.max(g).max(b);
let c_min = r.min(g).min(b);
let delta = c_max - c_min;
let mut h = 0.0;
let mut s = 0.0;
let l = (c_max + c_min) / 2.0;
if delta > 1e-5 {
s = if l < 0.5 { delta / (c_max + c_min) } else { delta / (2.0 - c_max - c_min) };
s = if l < 0.5 {
delta / (c_max + c_min)
} else {
delta / (2.0 - c_max - c_min)
};
if c_max == r {
h = (g - b) / delta + (if g < b { 6.0 } else { 0.0 });
} else if c_max == g {
@@ -73,14 +85,18 @@ pub(crate) fn apply_hue_saturation(r: u8, g: u8, b: u8, dh: i32, ds: i32, dl: i3
let fr = r as f32 / 255.0;
let fg = g as f32 / 255.0;
let fb = b as f32 / 255.0;
let (h, s, l) = rgb_to_hsl(fr, fg, fb);
// 1. Hue shift
let mut new_h = h + dh as f32;
while new_h < 0.0 { new_h += 360.0; }
while new_h >= 360.0 { new_h -= 360.0; }
while new_h < 0.0 {
new_h += 360.0;
}
while new_h >= 360.0 {
new_h -= 360.0;
}
// 2. Saturation relative/clamped shift
let s_factor = ds as f32 / 100.0;
let new_s = if s_factor >= 0.0 {
@@ -89,7 +105,7 @@ pub(crate) fn apply_hue_saturation(r: u8, g: u8, b: u8, dh: i32, ds: i32, dl: i3
s + s_factor * s
};
let new_s = new_s.clamp(0.0, 1.0);
// 3. Lightness relative/clamped shift
let l_factor = dl as f32 / 100.0;
let new_l = if l_factor >= 0.0 {
@@ -98,7 +114,7 @@ pub(crate) fn apply_hue_saturation(r: u8, g: u8, b: u8, dh: i32, ds: i32, dl: i3
l + l_factor * l
};
let new_l = new_l.clamp(0.0, 1.0);
let (out_r, out_g, out_b) = hsl_to_rgb(new_h, new_s, new_l);
[
(out_r * 255.0).round().clamp(0.0, 255.0) as u8,
@@ -107,12 +123,21 @@ pub(crate) fn apply_hue_saturation(r: u8, g: u8, b: u8, dh: i32, ds: i32, dl: i3
]
}
pub fn composite_layers_parallel(layers: &[Layer], canvas_width: u32, canvas_height: u32) -> Vec<u8> {
pub fn composite_layers_parallel(
layers: &[Layer],
canvas_width: u32,
canvas_height: u32,
) -> Vec<u8> {
let px_count = (canvas_width * canvas_height) as usize;
let mut output = vec![0u8; px_count * 4];
log::debug!("[composite_layers_parallel] START: {} layers, canvas={}x{}, px_count={}",
layers.len(), canvas_width, canvas_height, px_count);
log::debug!(
"[composite_layers_parallel] START: {} layers, canvas={}x{}, px_count={}",
layers.len(),
canvas_width,
canvas_height,
px_count
);
for (i, l) in layers.iter().enumerate() {
log::debug!("[composite_layers_parallel] input layer[{}]: name='{}' visible={} w={} h={} opacity={} blend={:?} clip={} pixels_len={}",
i, l.name, l.visible, l.width, l.height, l.opacity, l.blend_mode, l.clipping_mask, l.pixels.len());
@@ -136,13 +161,17 @@ pub fn composite_layers_parallel(layers: &[Layer], canvas_width: u32, canvas_hei
}
for (rev_i, layer) in layers.iter().enumerate() {
if !layer.visible { continue; }
if !layer.visible {
continue;
}
if layer.layer_type == hcie_protocol::LayerType::Group {
continue;
}
let is_adj = layer.adjustment.is_some();
if !is_adj && (layer.width == 0 || layer.height == 0) { continue; }
if !is_adj && (layer.width == 0 || layer.height == 0) {
continue;
}
let i = rev_i;
let blend: hcie_blend::BlendMode = layer.blend_mode.into();
@@ -151,7 +180,8 @@ pub fn composite_layers_parallel(layers: &[Layer], canvas_width: u32, canvas_hei
let base_idx = base_indices[i];
// Apply layer effects if present
let has_effects = !layer.effects.is_empty() || !layer.styles.is_empty() || (layer.fill_opacity < 1.0);
let has_effects =
!layer.effects.is_empty() || !layer.styles.is_empty() || (layer.fill_opacity < 1.0);
let effect_pixels = if has_effects && !is_adj {
if let Ok(Some(cached)) = layer.effects_cache.lock().as_deref() {
if cached.width == layer.width && cached.height == layer.height {
@@ -160,9 +190,24 @@ pub fn composite_layers_parallel(layers: &[Layer], canvas_width: u32, canvas_hei
None
}
} else {
let mut fx_effects: Vec<hcie_fx::LayerEffect> = layer.effects.iter().map(hcie_fx::protocol_to_hcie_fx_effect).collect();
fx_effects.extend(layer.styles.iter().filter_map(|s| hcie_fx::layer_style_to_effect(s)));
Some(hcie_fx::apply_layer_effects(&layer.pixels, layer.width, layer.height, &fx_effects, layer.fill_opacity))
let mut fx_effects: Vec<hcie_fx::LayerEffect> = layer
.effects
.iter()
.map(hcie_fx::protocol_to_hcie_fx_effect)
.collect();
fx_effects.extend(
layer
.styles
.iter()
.filter_map(|s| hcie_fx::layer_style_to_effect(s)),
);
Some(hcie_fx::apply_layer_effects(
&layer.pixels,
layer.width,
layer.height,
&fx_effects,
layer.fill_opacity,
))
}
} else {
None
@@ -170,35 +215,69 @@ pub fn composite_layers_parallel(layers: &[Layer], canvas_width: u32, canvas_hei
let pixels = effect_pixels.as_deref().unwrap_or(&layer.pixels);
if is_adj {
output.par_chunks_exact_mut(canvas_width as usize * 4)
output
.par_chunks_exact_mut(canvas_width as usize * 4)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
for x in 0..canvas_width {
let out_idx = x as usize * 4;
let dst = [row[out_idx], row[out_idx + 1], row[out_idx + 2], row[out_idx + 3]];
if dst[3] == 0 { continue; }
let dst = [
row[out_idx],
row[out_idx + 1],
row[out_idx + 2],
row[out_idx + 3],
];
if dst[3] == 0 {
continue;
}
let mask_val = layer.get_mask_value(x, gy);
if mask_val == 0 { continue; }
if mask_val == 0 {
continue;
}
let adj_rgb = match layer.adjustment.as_ref().unwrap() {
hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b } => {
apply_curves(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
}
hcie_blend::Adjustment::GradientMap { lut_r, lut_g, lut_b } => {
apply_gradient_map(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
}
hcie_blend::Adjustment::HueSaturation { hue, saturation, lightness } => {
apply_hue_saturation(dst[0], dst[1], dst[2], *hue, *saturation, *lightness)
}
hcie_blend::Adjustment::Curves {
lut_r,
lut_g,
lut_b,
} => apply_curves(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b),
hcie_blend::Adjustment::GradientMap {
lut_r,
lut_g,
lut_b,
} => apply_gradient_map(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b),
hcie_blend::Adjustment::HueSaturation {
hue,
saturation,
lightness,
} => apply_hue_saturation(
dst[0],
dst[1],
dst[2],
*hue,
*saturation,
*lightness,
),
};
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended_opaque = blend_pixels([dst[0], dst[1], dst[2], 255], [adj_rgb[0], adj_rgb[1], adj_rgb[2], 255], blend, 1.0);
let out_r = ((1.0 - eff_opacity) * dst[0] as f32 + eff_opacity * blended_opaque[0] as f32).round() as u8;
let out_g = ((1.0 - eff_opacity) * dst[1] as f32 + eff_opacity * blended_opaque[1] as f32).round() as u8;
let out_b = ((1.0 - eff_opacity) * dst[2] as f32 + eff_opacity * blended_opaque[2] as f32).round() as u8;
let blended_opaque = blend_pixels(
[dst[0], dst[1], dst[2], 255],
[adj_rgb[0], adj_rgb[1], adj_rgb[2], 255],
blend,
1.0,
);
let out_r = ((1.0 - eff_opacity) * dst[0] as f32
+ eff_opacity * blended_opaque[0] as f32)
.round() as u8;
let out_g = ((1.0 - eff_opacity) * dst[1] as f32
+ eff_opacity * blended_opaque[1] as f32)
.round() as u8;
let out_b = ((1.0 - eff_opacity) * dst[2] as f32
+ eff_opacity * blended_opaque[2] as f32)
.round() as u8;
row[out_idx] = out_r;
row[out_idx + 1] = out_g;
row[out_idx + 2] = out_b;
@@ -206,21 +285,34 @@ pub fn composite_layers_parallel(layers: &[Layer], canvas_width: u32, canvas_hei
});
} else {
let (offset_x, offset_y) = match &layer.data {
hcie_protocol::LayerData::Text { offset_x, offset_y, .. } => (*offset_x as i32, *offset_y as i32),
hcie_protocol::LayerData::Text {
offset_x, offset_y, ..
} => (*offset_x as i32, *offset_y as i32),
_ => (0, 0),
};
let cx0 = (0).max(offset_x).max(0) as u32;
let cx1 = (canvas_width as i32).min(offset_x + layer.width as i32).min(canvas_width as i32).max(0) as u32;
let cx1 = (canvas_width as i32)
.min(offset_x + layer.width as i32)
.min(canvas_width as i32)
.max(0) as u32;
let cy0 = (0).max(offset_y).max(0) as u32;
let cy1 = (canvas_height as i32).min(offset_y + layer.height as i32).min(canvas_height as i32).max(0) as u32;
if cx0 >= cx1 || cy0 >= cy1 { continue; }
let cy1 = (canvas_height as i32)
.min(offset_y + layer.height as i32)
.min(canvas_height as i32)
.max(0) as u32;
if cx0 >= cx1 || cy0 >= cy1 {
continue;
}
output.par_chunks_exact_mut(canvas_width as usize * 4)
output
.par_chunks_exact_mut(canvas_width as usize * 4)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < cy0 || gy >= cy1 { return; }
if gy < cy0 || gy >= cy1 {
return;
}
let ly = (y as i32 - offset_y) as u32;
for x in cx0 as usize..cx1 as usize {
let lx = (x as i32 - offset_x) as u32;
@@ -233,7 +325,9 @@ pub fn composite_layers_parallel(layers: &[Layer], canvas_width: u32, canvas_hei
};
let mask_val = layer.get_mask_value(x as u32, gy);
if mask_val == 0 { continue; }
if mask_val == 0 {
continue;
}
if is_clipping {
if let Some(bi) = base_idx {
@@ -244,8 +338,15 @@ pub fn composite_layers_parallel(layers: &[Layer], canvas_width: u32, canvas_hei
}
}
if src[3] == 0 { continue; }
let dst = [row[out_idx], row[out_idx + 1], row[out_idx + 2], row[out_idx + 3]];
if src[3] == 0 {
continue;
}
let dst = [
row[out_idx],
row[out_idx + 1],
row[out_idx + 2],
row[out_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[out_idx..out_idx + 4].copy_from_slice(&blended);
@@ -255,8 +356,16 @@ pub fn composite_layers_parallel(layers: &[Layer], canvas_width: u32, canvas_hei
}
let non_zero_alpha = output.iter().skip(3).step_by(4).filter(|&&a| a > 0).count();
log::debug!("[composite_layers_parallel] DONE: output non_zero_alpha={}/{} ({}%)",
non_zero_alpha, px_count, if px_count > 0 { non_zero_alpha * 100 / px_count } else { 0 });
log::debug!(
"[composite_layers_parallel] DONE: output non_zero_alpha={}/{} ({}%)",
non_zero_alpha,
px_count,
if px_count > 0 {
non_zero_alpha * 100 / px_count
} else {
0
}
);
output
}
@@ -264,12 +373,17 @@ pub fn composite_layers_region_parallel(
layers: &[Layer],
canvas_width: u32,
canvas_height: u32,
x0: u32, y0: u32, x1: u32, y1: u32,
x0: u32,
y0: u32,
x1: u32,
y1: u32,
output: &mut [u8],
) {
let w = x1.saturating_sub(x0);
let h = y1.saturating_sub(y0);
if w == 0 || h == 0 { return; }
if w == 0 || h == 0 {
return;
}
let mut base_indices = Vec::with_capacity(layers.len());
for i in 0..layers.len() {
@@ -288,11 +402,13 @@ pub fn composite_layers_region_parallel(
}
for (rev_i, layer) in layers.iter().enumerate() {
if !layer.visible { continue; }
if !layer.visible {
continue;
}
if layer.layer_type == hcie_protocol::LayerType::Group {
continue;
}
let blend: hcie_blend::BlendMode = layer.blend_mode.into();
let opacity = layer.opacity.clamp(0.0, 1.0);
let is_clipping = layer.clipping_mask;
@@ -302,7 +418,8 @@ pub fn composite_layers_region_parallel(
let is_adj = layer.adjustment.is_some();
// Apply layer effects if present
let has_effects = !layer.effects.is_empty() || !layer.styles.is_empty() || (layer.fill_opacity < 1.0);
let has_effects =
!layer.effects.is_empty() || !layer.styles.is_empty() || (layer.fill_opacity < 1.0);
let effect_pixels = if has_effects && !is_adj {
if let Ok(Some(cached)) = layer.effects_cache.lock().as_deref() {
if cached.width == layer.width && cached.height == layer.height {
@@ -311,9 +428,24 @@ pub fn composite_layers_region_parallel(
None
}
} else {
let mut fx_effects: Vec<hcie_fx::LayerEffect> = layer.effects.iter().map(hcie_fx::protocol_to_hcie_fx_effect).collect();
fx_effects.extend(layer.styles.iter().filter_map(|s| hcie_fx::layer_style_to_effect(s)));
Some(hcie_fx::apply_layer_effects(&layer.pixels, layer.width, layer.height, &fx_effects, layer.fill_opacity))
let mut fx_effects: Vec<hcie_fx::LayerEffect> = layer
.effects
.iter()
.map(hcie_fx::protocol_to_hcie_fx_effect)
.collect();
fx_effects.extend(
layer
.styles
.iter()
.filter_map(|s| hcie_fx::layer_style_to_effect(s)),
);
Some(hcie_fx::apply_layer_effects(
&layer.pixels,
layer.width,
layer.height,
&fx_effects,
layer.fill_opacity,
))
}
} else {
None
@@ -328,62 +460,113 @@ pub fn composite_layers_region_parallel(
let row_stride = canvas_width as usize * 4;
output.par_chunks_exact_mut(row_stride)
output
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < region_y0 || gy >= region_y1 { return; }
if gy < region_y0 || gy >= region_y1 {
return;
}
for x in region_x0..region_x1 {
let out_idx = (x as usize) * 4;
let dst = [row[out_idx], row[out_idx + 1], row[out_idx + 2], row[out_idx + 3]];
if dst[3] == 0 { continue; }
let dst = [
row[out_idx],
row[out_idx + 1],
row[out_idx + 2],
row[out_idx + 3],
];
if dst[3] == 0 {
continue;
}
let mask_val = layer.get_mask_value(x, gy);
if mask_val == 0 { continue; }
if mask_val == 0 {
continue;
}
let adj_rgb = match layer.adjustment.as_ref().unwrap() {
hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b } => {
apply_curves(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
}
hcie_blend::Adjustment::GradientMap { lut_r, lut_g, lut_b } => {
apply_gradient_map(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
}
hcie_blend::Adjustment::HueSaturation { hue, saturation, lightness } => {
apply_hue_saturation(dst[0], dst[1], dst[2], *hue, *saturation, *lightness)
}
hcie_blend::Adjustment::Curves {
lut_r,
lut_g,
lut_b,
} => apply_curves(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b),
hcie_blend::Adjustment::GradientMap {
lut_r,
lut_g,
lut_b,
} => apply_gradient_map(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b),
hcie_blend::Adjustment::HueSaturation {
hue,
saturation,
lightness,
} => apply_hue_saturation(
dst[0],
dst[1],
dst[2],
*hue,
*saturation,
*lightness,
),
};
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended_opaque = blend_pixels([dst[0], dst[1], dst[2], 255], [adj_rgb[0], adj_rgb[1], adj_rgb[2], 255], blend, 1.0);
let out_r = ((1.0 - eff_opacity) * dst[0] as f32 + eff_opacity * blended_opaque[0] as f32).round() as u8;
let out_g = ((1.0 - eff_opacity) * dst[1] as f32 + eff_opacity * blended_opaque[1] as f32).round() as u8;
let out_b = ((1.0 - eff_opacity) * dst[2] as f32 + eff_opacity * blended_opaque[2] as f32).round() as u8;
let blended_opaque = blend_pixels(
[dst[0], dst[1], dst[2], 255],
[adj_rgb[0], adj_rgb[1], adj_rgb[2], 255],
blend,
1.0,
);
let out_r = ((1.0 - eff_opacity) * dst[0] as f32
+ eff_opacity * blended_opaque[0] as f32)
.round() as u8;
let out_g = ((1.0 - eff_opacity) * dst[1] as f32
+ eff_opacity * blended_opaque[1] as f32)
.round() as u8;
let out_b = ((1.0 - eff_opacity) * dst[2] as f32
+ eff_opacity * blended_opaque[2] as f32)
.round() as u8;
row[out_idx] = out_r;
row[out_idx + 1] = out_g;
row[out_idx + 2] = out_b;
}
});
} else {
if layer.width == 0 || layer.height == 0 { continue; }
if layer.width == 0 || layer.height == 0 {
continue;
}
let (offset_x, offset_y) = match &layer.data {
hcie_protocol::LayerData::Text { offset_x, offset_y, .. } => (*offset_x as i32, *offset_y as i32),
hcie_protocol::LayerData::Text {
offset_x, offset_y, ..
} => (*offset_x as i32, *offset_y as i32),
_ => (0, 0),
};
let cx0 = (x0 as i32).max(offset_x).max(0) as u32;
let cx1 = (x1 as i32).min(offset_x + layer.width as i32).min(canvas_width as i32).max(0) as u32;
let cx1 = (x1 as i32)
.min(offset_x + layer.width as i32)
.min(canvas_width as i32)
.max(0) as u32;
let cy0 = (y0 as i32).max(offset_y).max(0) as u32;
let cy1 = (y1 as i32).min(offset_y + layer.height as i32).min(canvas_height as i32).max(0) as u32;
if cx0 >= cx1 || cy0 >= cy1 { continue; }
let cy1 = (y1 as i32)
.min(offset_y + layer.height as i32)
.min(canvas_height as i32)
.max(0) as u32;
if cx0 >= cx1 || cy0 >= cy1 {
continue;
}
let row_stride = canvas_width as usize * 4;
output.par_chunks_exact_mut(row_stride)
output
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < cy0 || gy >= cy1 { return; }
if gy < cy0 || gy >= cy1 {
return;
}
let ly = (gy as i32 - offset_y) as u32;
for x in cx0..cx1 {
let lx = (x as i32 - offset_x) as u32;
@@ -396,7 +579,9 @@ pub fn composite_layers_region_parallel(
};
let mask_val = layer.get_mask_value(x, gy);
if mask_val == 0 { continue; }
if mask_val == 0 {
continue;
}
if is_clipping {
if let Some(bi) = base_idx {
@@ -407,8 +592,15 @@ pub fn composite_layers_region_parallel(
}
}
if src[3] == 0 { continue; }
let dst = [row[out_idx], row[out_idx + 1], row[out_idx + 2], row[out_idx + 3]];
if src[3] == 0 {
continue;
}
let dst = [
row[out_idx],
row[out_idx + 1],
row[out_idx + 2],
row[out_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[out_idx..out_idx + 4].copy_from_slice(&blended);
+18 -6
View File
@@ -9,7 +9,9 @@ fn test_composite_basic() {
name: "Red".into(),
layer_type: hcie_protocol::LayerType::Raster,
data: hcie_protocol::LayerData::Raster,
pixels: vec![255,0,0,255, 255,0,0,255, 255,0,0,255, 255,0,0,255],
pixels: vec![
255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255,
],
width: 2,
height: 2,
visible: true,
@@ -37,13 +39,18 @@ fn test_composite_basic() {
};
let layers = vec![layer];
let out = crate::composite_layers(&layers, 2, 2);
assert_eq!(out, vec![255,0,0,255, 255,0,0,255, 255,0,0,255, 255,0,0,255]);
assert_eq!(
out,
vec![255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255]
);
let layer0 = hcie_protocol::Layer {
name: "Red".into(),
layer_type: hcie_protocol::LayerType::Raster,
data: hcie_protocol::LayerData::Raster,
pixels: vec![255,0,0,255, 255,0,0,255, 255,0,0,255, 255,0,0,255],
pixels: vec![
255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255,
],
width: 2,
height: 2,
visible: true,
@@ -73,7 +80,9 @@ fn test_composite_basic() {
name: "Blue".into(),
layer_type: hcie_protocol::LayerType::Raster,
data: hcie_protocol::LayerData::Raster,
pixels: vec![0,0,255,255, 0,0,255,255, 0,0,255,255, 0,0,255,255],
pixels: vec![
0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255,
],
width: 2,
height: 2,
visible: true,
@@ -101,5 +110,8 @@ fn test_composite_basic() {
};
let layers2 = vec![layer0, layer1];
let out2 = crate::composite_layers(&layers2, 2, 2);
assert_eq!(out2, vec![0,0,255,255, 0,0,255,255, 0,0,255,255, 0,0,255,255]);
}
assert_eq!(
out2,
vec![0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255]
);
}
+369 -122
View File
@@ -1,8 +1,8 @@
use hcie_tile::{TiledLayer, TILE_SIZE};
use hcie_protocol::Layer;
use hcie_blend::blend_pixels;
use hcie_fx::apply_layer_effects;
use hcie_fx::protocol_to_hcie_fx_effect;
use hcie_protocol::Layer;
use hcie_tile::{TiledLayer, TILE_SIZE};
use rayon::prelude::*;
/// Pixel-area threshold below which the compositor uses a sequential row
@@ -24,7 +24,17 @@ pub fn composite_tiled(
) -> Vec<u8> {
let px_count = (canvas_width * canvas_height) as usize;
let mut output = vec![0u8; px_count * 4];
composite_tiled_into(layers, tile_layers, canvas_width, canvas_height, 0, 0, canvas_width, canvas_height, &mut output);
composite_tiled_into(
layers,
tile_layers,
canvas_width,
canvas_height,
0,
0,
canvas_width,
canvas_height,
&mut output,
);
output
}
@@ -34,12 +44,17 @@ pub fn composite_tiled_into(
tile_layers: &[Option<TiledLayer>],
canvas_width: u32,
canvas_height: u32,
x0: u32, y0: u32, x1: u32, y1: u32,
x0: u32,
y0: u32,
x1: u32,
y1: u32,
output: &mut [u8],
) {
let region_w = x1.saturating_sub(x0);
let region_h = y1.saturating_sub(y0);
if region_w == 0 || region_h == 0 { return; }
if region_w == 0 || region_h == 0 {
return;
}
let use_sequential = region_w.saturating_mul(region_h) <= SEQUENTIAL_PIXEL_THRESHOLD;
let row_stride = canvas_width as usize * 4;
@@ -63,7 +78,9 @@ pub fn composite_tiled_into(
}
for (i, layer) in layers.iter().enumerate() {
if !layer.visible { continue; }
if !layer.visible {
continue;
}
if layer.layer_type == hcie_protocol::LayerType::Group {
continue;
}
@@ -73,14 +90,22 @@ pub fn composite_tiled_into(
let base_idx = base_indices[i];
let (offset_x, offset_y) = match &layer.data {
hcie_protocol::LayerData::Text { offset_x, offset_y, .. } => (*offset_x as i32, *offset_y as i32),
hcie_protocol::LayerData::Text {
offset_x, offset_y, ..
} => (*offset_x as i32, *offset_y as i32),
_ => (0, 0),
};
let cx0 = (x0 as i32).max(offset_x).max(0) as u32;
let cx1 = (x1 as i32).min(offset_x + layer.width as i32).min(canvas_width as i32).max(0) as u32;
let cx1 = (x1 as i32)
.min(offset_x + layer.width as i32)
.min(canvas_width as i32)
.max(0) as u32;
let cy0 = (y0 as i32).max(offset_y).max(0) as u32;
let cy1 = (y1 as i32).min(offset_y + layer.height as i32).min(canvas_height as i32).max(0) as u32;
let cy1 = (y1 as i32)
.min(offset_y + layer.height as i32)
.min(canvas_height as i32)
.max(0) as u32;
let layer_w = layer.width.min(canvas_width);
let layer_h = layer.height.min(canvas_height);
@@ -101,11 +126,14 @@ pub fn composite_tiled_into(
}
}
} else {
output.par_chunks_exact_mut(row_stride)
output
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < region_y0 || gy >= region_y1 { return; }
if gy < region_y0 || gy >= region_y1 {
return;
}
for x in region_x0..region_x1 {
blend_adjustment_pixel(row, x, gy, layer, blend, opacity);
}
@@ -117,9 +145,19 @@ pub fn composite_tiled_into(
// If layer has effects or non-default fill_opacity, compute effects buffer.
// Prefer the cached `effects_cache` when available to avoid re-running the
// expensive effects pipeline on every partial composite.
let has_effects = !layer.effects.is_empty() || !layer.styles.is_empty() || (layer.fill_opacity < 1.0);
let mut fx_effects: Vec<hcie_fx::LayerEffect> = layer.effects.iter().map(hcie_fx::protocol_to_hcie_fx_effect).collect();
fx_effects.extend(layer.styles.iter().filter_map(|s| hcie_fx::layer_style_to_effect(s)));
let has_effects =
!layer.effects.is_empty() || !layer.styles.is_empty() || (layer.fill_opacity < 1.0);
let mut fx_effects: Vec<hcie_fx::LayerEffect> = layer
.effects
.iter()
.map(hcie_fx::protocol_to_hcie_fx_effect)
.collect();
fx_effects.extend(
layer
.styles
.iter()
.filter_map(|s| hcie_fx::layer_style_to_effect(s)),
);
let effect_buf = if has_effects {
if let Ok(Some(cached)) = layer.effects_cache.lock().as_deref() {
if cached.width == layer.width && cached.height == layer.height {
@@ -129,94 +167,167 @@ pub fn composite_tiled_into(
}
} else {
None
}.unwrap_or_else(|| apply_layer_effects(
&layer.pixels, layer.width, layer.height,
&fx_effects, layer.fill_opacity,
))
}
.unwrap_or_else(|| {
apply_layer_effects(
&layer.pixels,
layer.width,
layer.height,
&fx_effects,
layer.fill_opacity,
)
})
} else {
Vec::new()
};
// Try tile path first (only if no effects — effects need full dense buffer)
if !has_effects {
if let Some(Some(tl)) = tile_layers.get(i).filter(|_| layer.layer_type != hcie_protocol::LayerType::Text) {
if tl.tile_count() == 0 && i > 0 {
continue; // fully transparent layer with no tiles
}
let t_start_x = x0 / TILE_SIZE;
let _t_start_y = y0 / TILE_SIZE;
let t_end_x = div_ceil(x1, TILE_SIZE);
let _t_end_y = div_ceil(y1, TILE_SIZE);
if use_sequential {
for y in y0..y1.min(layer_h) {
let row = &mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
let gy = y;
let ty = gy / TILE_SIZE;
let ly = (gy % TILE_SIZE) as usize;
for tx in t_start_x..t_end_x {
let gx_start = tx * TILE_SIZE;
let gx_end = (gx_start + TILE_SIZE).min(x1).min(layer_w);
if gx_end <= gx_start { continue; }
if let Some(tile) = tl.get_tile(tx, ty) {
let start_x = gx_start.max(x0);
for gx in start_x..gx_end {
blend_tile_pixel(row, gx, gy, ly, tile, layer, is_clipping, base_idx, layers, blend, opacity);
}
}
}
if let Some(Some(tl)) = tile_layers
.get(i)
.filter(|_| layer.layer_type != hcie_protocol::LayerType::Text)
{
if tl.tile_count() == 0 && i > 0 {
continue; // fully transparent layer with no tiles
}
} else {
output.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < y0 || gy >= y1 || gy >= layer_h { return; }
let t_start_x = x0 / TILE_SIZE;
let _t_start_y = y0 / TILE_SIZE;
let t_end_x = div_ceil(x1, TILE_SIZE);
let _t_end_y = div_ceil(y1, TILE_SIZE);
if use_sequential {
for y in y0..y1.min(layer_h) {
let row =
&mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
let gy = y;
let ty = gy / TILE_SIZE;
let ly = (gy % TILE_SIZE) as usize;
for tx in t_start_x..t_end_x {
let gx_start = tx * TILE_SIZE;
let gx_end = (gx_start + TILE_SIZE).min(x1).min(layer_w);
if gx_end <= gx_start { continue; }
if gx_end <= gx_start {
continue;
}
if let Some(tile) = tl.get_tile(tx, ty) {
let start_x = gx_start.max(x0);
for gx in start_x..gx_end {
blend_tile_pixel(row, gx, gy, ly, tile, layer, is_clipping, base_idx, layers, blend, opacity);
blend_tile_pixel(
row,
gx,
gy,
ly,
tile,
layer,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
}
}
});
}
} else {
// Fallback to dense pixel path (no effects)
if cx0 >= cx1 || cy0 >= cy1 { continue; }
if use_sequential {
for y in cy0..cy1 {
let row = &mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
let gy = y;
let ly = (gy as i32 - offset_y) as u32;
for gx in cx0..cx1 {
let lx = (gx as i32 - offset_x) as u32;
blend_dense_pixel_offset(row, gx, gy, lx, ly, layer, is_clipping, base_idx, layers, blend, opacity);
}
} else {
output
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < y0 || gy >= y1 || gy >= layer_h {
return;
}
let ty = gy / TILE_SIZE;
let ly = (gy % TILE_SIZE) as usize;
for tx in t_start_x..t_end_x {
let gx_start = tx * TILE_SIZE;
let gx_end = (gx_start + TILE_SIZE).min(x1).min(layer_w);
if gx_end <= gx_start {
continue;
}
if let Some(tile) = tl.get_tile(tx, ty) {
let start_x = gx_start.max(x0);
for gx in start_x..gx_end {
blend_tile_pixel(
row,
gx,
gy,
ly,
tile,
layer,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
}
}
});
}
} else {
output.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < cy0 || gy >= cy1 { return; }
// Fallback to dense pixel path (no effects)
if cx0 >= cx1 || cy0 >= cy1 {
continue;
}
if use_sequential {
for y in cy0..cy1 {
let row =
&mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
let gy = y;
let ly = (gy as i32 - offset_y) as u32;
for gx in cx0..cx1 {
let lx = (gx as i32 - offset_x) as u32;
blend_dense_pixel_offset(row, gx, gy, lx, ly, layer, is_clipping, base_idx, layers, blend, opacity);
blend_dense_pixel_offset(
row,
gx,
gy,
lx,
ly,
layer,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
});
}
} else {
output
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < cy0 || gy >= cy1 {
return;
}
let ly = (gy as i32 - offset_y) as u32;
for gx in cx0..cx1 {
let lx = (gx as i32 - offset_x) as u32;
blend_dense_pixel_offset(
row,
gx,
gy,
lx,
ly,
layer,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
});
}
}
}
} else {
// Effects path — use pre-computed effects buffer
if cx0 >= cx1 || cy0 >= cy1 { continue; }
if cx0 >= cx1 || cy0 >= cy1 {
continue;
}
let eb = effect_buf.as_slice();
if use_sequential {
for y in cy0..cy1 {
@@ -225,19 +336,48 @@ pub fn composite_tiled_into(
let ly = (gy as i32 - offset_y) as u32;
for gx in cx0..cx1 {
let lx = (gx as i32 - offset_x) as u32;
blend_effects_pixel_offset(row, gx, gy, lx, ly, layer, eb, is_clipping, base_idx, layers, blend, opacity);
blend_effects_pixel_offset(
row,
gx,
gy,
lx,
ly,
layer,
eb,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
}
} else {
output.par_chunks_exact_mut(row_stride)
output
.par_chunks_exact_mut(row_stride)
.enumerate()
.for_each(|(y, row)| {
let gy = y as u32;
if gy < cy0 || gy >= cy1 { return; }
if gy < cy0 || gy >= cy1 {
return;
}
let ly = (gy as i32 - offset_y) as u32;
for gx in cx0..cx1 {
let lx = (gx as i32 - offset_x) as u32;
blend_effects_pixel_offset(row, gx, gy, lx, ly, layer, eb, is_clipping, base_idx, layers, blend, opacity);
blend_effects_pixel_offset(
row,
gx,
gy,
lx,
ly,
layer,
eb,
is_clipping,
base_idx,
layers,
blend,
opacity,
);
}
});
}
@@ -255,29 +395,61 @@ fn blend_adjustment_pixel(
opacity: f32,
) {
let out_idx = (x as usize) * 4;
let dst = [row[out_idx], row[out_idx + 1], row[out_idx + 2], row[out_idx + 3]];
if dst[3] == 0 { return; }
let dst = [
row[out_idx],
row[out_idx + 1],
row[out_idx + 2],
row[out_idx + 3],
];
if dst[3] == 0 {
return;
}
let mask_val = layer.get_mask_value(x, gy);
if mask_val == 0 { return; }
if mask_val == 0 {
return;
}
let adj_rgb = match layer.adjustment.as_ref().unwrap() {
hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b } => {
crate::parallel::apply_curves(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
}
hcie_blend::Adjustment::GradientMap { lut_r, lut_g, lut_b } => {
crate::parallel::apply_gradient_map(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
}
hcie_blend::Adjustment::HueSaturation { hue, saturation, lightness } => {
crate::parallel::apply_hue_saturation(dst[0], dst[1], dst[2], *hue, *saturation, *lightness)
}
hcie_blend::Adjustment::Curves {
lut_r,
lut_g,
lut_b,
} => crate::parallel::apply_curves(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b),
hcie_blend::Adjustment::GradientMap {
lut_r,
lut_g,
lut_b,
} => crate::parallel::apply_gradient_map(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b),
hcie_blend::Adjustment::HueSaturation {
hue,
saturation,
lightness,
} => crate::parallel::apply_hue_saturation(
dst[0],
dst[1],
dst[2],
*hue,
*saturation,
*lightness,
),
};
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended_opaque = blend_pixels([dst[0], dst[1], dst[2], 255], [adj_rgb[0], adj_rgb[1], adj_rgb[2], 255], blend, 1.0);
row[out_idx] = ((1.0 - eff_opacity) * dst[0] as f32 + eff_opacity * blended_opaque[0] as f32).round() as u8;
row[out_idx + 1] = ((1.0 - eff_opacity) * dst[1] as f32 + eff_opacity * blended_opaque[1] as f32).round() as u8;
row[out_idx + 2] = ((1.0 - eff_opacity) * dst[2] as f32 + eff_opacity * blended_opaque[2] as f32).round() as u8;
let blended_opaque = blend_pixels(
[dst[0], dst[1], dst[2], 255],
[adj_rgb[0], adj_rgb[1], adj_rgb[2], 255],
blend,
1.0,
);
row[out_idx] = ((1.0 - eff_opacity) * dst[0] as f32 + eff_opacity * blended_opaque[0] as f32)
.round() as u8;
row[out_idx + 1] = ((1.0 - eff_opacity) * dst[1] as f32
+ eff_opacity * blended_opaque[1] as f32)
.round() as u8;
row[out_idx + 2] = ((1.0 - eff_opacity) * dst[2] as f32
+ eff_opacity * blended_opaque[2] as f32)
.round() as u8;
}
#[inline]
@@ -305,33 +477,50 @@ fn blend_tile_pixel(
// Inline get_mask_value — hot path
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
let (m_top, m_left, m_bottom, m_right) = (mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
let (m_top, m_left, m_bottom, m_right) =
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
let mask_w = m_right - m_left;
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
mask.get(mask_idx).copied().unwrap_or(layer.mask_default_color)
mask.get(mask_idx)
.copied()
.unwrap_or(layer.mask_default_color)
} else {
layer.mask_default_color
}
} else {
255u8
};
if mask_val == 0 { return; }
if mask_val == 0 {
return;
}
if is_clipping {
if let Some(bi) = base_idx {
// Inline get_pixel for clipping base layer
let base_i = (gy * layers[bi].width + gx) as usize * 4;
let base = [layers[bi].pixels[base_i], layers[bi].pixels[base_i + 1], layers[bi].pixels[base_i + 2], layers[bi].pixels[base_i + 3]];
let base = [
layers[bi].pixels[base_i],
layers[bi].pixels[base_i + 1],
layers[bi].pixels[base_i + 2],
layers[bi].pixels[base_i + 3],
];
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
} else {
src[3] = 0;
}
}
if src[3] == 0 { return; }
if src[3] == 0 {
return;
}
let dst_idx = (gx as usize) * 4;
let dst = [row[dst_idx], row[dst_idx + 1], row[dst_idx + 2], row[dst_idx + 3]];
let dst = [
row[dst_idx],
row[dst_idx + 1],
row[dst_idx + 2],
row[dst_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
@@ -363,33 +552,50 @@ fn blend_dense_pixel(
// Inline get_mask_value — hot path
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
let (m_top, m_left, m_bottom, m_right) = (mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
let (m_top, m_left, m_bottom, m_right) =
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
let mask_w = m_right - m_left;
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
mask.get(mask_idx).copied().unwrap_or(layer.mask_default_color)
mask.get(mask_idx)
.copied()
.unwrap_or(layer.mask_default_color)
} else {
layer.mask_default_color
}
} else {
255u8
};
if mask_val == 0 { return; }
if mask_val == 0 {
return;
}
if is_clipping {
if let Some(bi) = base_idx {
// Inline get_pixel for clipping base layer
let base_i = (gy * layers[bi].width + gx) as usize * 4;
let base = [layers[bi].pixels[base_i], layers[bi].pixels[base_i + 1], layers[bi].pixels[base_i + 2], layers[bi].pixels[base_i + 3]];
let base = [
layers[bi].pixels[base_i],
layers[bi].pixels[base_i + 1],
layers[bi].pixels[base_i + 2],
layers[bi].pixels[base_i + 3],
];
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
} else {
src[3] = 0;
}
}
if src[3] == 0 { return; }
if src[3] == 0 {
return;
}
let dst_idx = (gx as usize) * 4;
let dst = [row[dst_idx], row[dst_idx + 1], row[dst_idx + 2], row[dst_idx + 3]];
let dst = [
row[dst_idx],
row[dst_idx + 1],
row[dst_idx + 2],
row[dst_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
@@ -418,33 +624,50 @@ fn blend_effects_pixel(
// Inline get_mask_value — hot path
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
let (m_top, m_left, m_bottom, m_right) = (mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
let (m_top, m_left, m_bottom, m_right) =
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
let mask_w = m_right - m_left;
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
mask.get(mask_idx).copied().unwrap_or(layer.mask_default_color)
mask.get(mask_idx)
.copied()
.unwrap_or(layer.mask_default_color)
} else {
layer.mask_default_color
}
} else {
255u8
};
if mask_val == 0 { return; }
if mask_val == 0 {
return;
}
if is_clipping {
if let Some(bi) = base_idx {
// Inline get_pixel for clipping base layer
let base_i = (gy * layers[bi].width + gx) as usize * 4;
let base = [layers[bi].pixels[base_i], layers[bi].pixels[base_i + 1], layers[bi].pixels[base_i + 2], layers[bi].pixels[base_i + 3]];
let base = [
layers[bi].pixels[base_i],
layers[bi].pixels[base_i + 1],
layers[bi].pixels[base_i + 2],
layers[bi].pixels[base_i + 3],
];
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
} else {
src[3] = 0;
}
}
if src[3] == 0 { return; }
if src[3] == 0 {
return;
}
let dst_idx = (gx as usize) * 4;
let dst = [row[dst_idx], row[dst_idx + 1], row[dst_idx + 2], row[dst_idx + 3]];
let dst = [
row[dst_idx],
row[dst_idx + 1],
row[dst_idx + 2],
row[dst_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
@@ -476,18 +699,23 @@ fn blend_dense_pixel_offset(
];
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
let (m_top, m_left, m_bottom, m_right) = (mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
let (m_top, m_left, m_bottom, m_right) =
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
let mask_w = m_right - m_left;
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
mask.get(mask_idx).copied().unwrap_or(layer.mask_default_color)
mask.get(mask_idx)
.copied()
.unwrap_or(layer.mask_default_color)
} else {
layer.mask_default_color
}
} else {
255u8
};
if mask_val == 0 { return; }
if mask_val == 0 {
return;
}
if is_clipping {
if let Some(bi) = base_idx {
@@ -498,9 +726,16 @@ fn blend_dense_pixel_offset(
}
}
if src[3] == 0 { return; }
if src[3] == 0 {
return;
}
let dst_idx = (gx as usize) * 4;
let dst = [row[dst_idx], row[dst_idx + 1], row[dst_idx + 2], row[dst_idx + 3]];
let dst = [
row[dst_idx],
row[dst_idx + 1],
row[dst_idx + 2],
row[dst_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
@@ -530,18 +765,23 @@ fn blend_effects_pixel_offset(
];
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
let (m_top, m_left, m_bottom, m_right) = (mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
let (m_top, m_left, m_bottom, m_right) =
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
let mask_w = m_right - m_left;
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
mask.get(mask_idx).copied().unwrap_or(layer.mask_default_color)
mask.get(mask_idx)
.copied()
.unwrap_or(layer.mask_default_color)
} else {
layer.mask_default_color
}
} else {
255u8
};
if mask_val == 0 { return; }
if mask_val == 0 {
return;
}
if is_clipping {
if let Some(bi) = base_idx {
@@ -552,9 +792,16 @@ fn blend_effects_pixel_offset(
}
}
if src[3] == 0 { return; }
if src[3] == 0 {
return;
}
let dst_idx = (gx as usize) * 4;
let dst = [row[dst_idx], row[dst_idx + 1], row[dst_idx + 2], row[dst_idx + 3]];
let dst = [
row[dst_idx],
row[dst_idx + 1],
row[dst_idx + 2],
row[dst_idx + 3],
];
let eff_opacity = opacity * (mask_val as f32 / 255.0);
let blended = blend_pixels(dst, src, blend, eff_opacity);
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
+41 -17
View File
@@ -1,4 +1,10 @@
#![allow(dead_code, unused_imports, unused_variables, unused_macros, unreachable_patterns)]
#![allow(
dead_code,
unused_imports,
unused_variables,
unused_macros,
unreachable_patterns
)]
use hcie_composite::composite_layers;
use hcie_protocol::Layer as ProtoLayer;
@@ -13,30 +19,48 @@ fn proto_to_comp(l: &ProtoLayer) -> ProtoLayer {
#[test]
fn test_composite_visibility_from_protocol_layer() {
let _ = env_logger::try_init();
let w = 100u32; let h = 100u32;
let w = 100u32;
let h = 100u32;
// Build layers using hcie_protocol::Layer (like Engine does)
let mut bg = ProtoLayer::new_transparent("bg", w, h);
bg.id = 1;
bg.pixels = vec![255u8; (w*h*4) as usize];
bg.pixels = vec![255u8; (w * h * 4) as usize];
let mut top = ProtoLayer::new_transparent("top", w, h);
top.id = 2;
top.pixels = vec![0u8; (w*h*4) as usize];
for y in 20..80 { for x in 20..80 {
let i = (y*w+x) as usize * 4;
top.pixels[i]=0; top.pixels[i+1]=0; top.pixels[i+2]=0; top.pixels[i+3]=255;
}}
top.pixels = vec![0u8; (w * h * 4) as usize];
for y in 20..80 {
for x in 20..80 {
let i = (y * w + x) as usize * 4;
top.pixels[i] = 0;
top.pixels[i + 1] = 0;
top.pixels[i + 2] = 0;
top.pixels[i + 3] = 255;
}
}
let run_composite = |layers: &[ProtoLayer]| -> Vec<u8> {
composite_layers(layers, w, h)
};
let run_composite = |layers: &[ProtoLayer]| -> Vec<u8> { composite_layers(layers, w, h) };
eprintln!("BOTH VISIBLE: opaque={}/10000", count_opaque(&run_composite(&[bg.clone(), top.clone()])));
eprintln!(
"BOTH VISIBLE: opaque={}/10000",
count_opaque(&run_composite(&[bg.clone(), top.clone()]))
);
bg.visible = false;
eprintln!("BG HIDDEN: opaque={}/10000 (expect 3600)", count_opaque(&run_composite(&[bg.clone(), top.clone()])));
bg.visible = true; top.visible = false;
eprintln!("TOP HIDDEN: opaque={}/10000 (expect 10000)", count_opaque(&run_composite(&[bg.clone(), top.clone()])));
bg.visible = false; top.visible = false;
eprintln!("BOTH HIDDEN: opaque={}/10000 (expect 0)", count_opaque(&run_composite(&[bg.clone(), top.clone()])));
eprintln!(
"BG HIDDEN: opaque={}/10000 (expect 3600)",
count_opaque(&run_composite(&[bg.clone(), top.clone()]))
);
bg.visible = true;
top.visible = false;
eprintln!(
"TOP HIDDEN: opaque={}/10000 (expect 10000)",
count_opaque(&run_composite(&[bg.clone(), top.clone()]))
);
bg.visible = false;
top.visible = false;
eprintln!(
"BOTH HIDDEN: opaque={}/10000 (expect 0)",
count_opaque(&run_composite(&[bg.clone(), top.clone()]))
);
}
+19 -9
View File
@@ -1,9 +1,13 @@
use hcie_composite::composite_layers;
use hcie_protocol::{BlendMode, Layer, LayerType};
fn make_layer(name: &str, w: u32, h: u32, rgba: [u8; 4]) -> Layer {
let mut l = Layer::from_rgba(name, w, h, vec![rgba; (w * h) as usize].into_iter().flatten().collect());
let mut l = Layer::from_rgba(
name,
w,
h,
vec![rgba; (w * h) as usize].into_iter().flatten().collect(),
);
l.blend_mode = BlendMode::Normal;
l
}
@@ -39,9 +43,13 @@ fn test_pass_through_group_skipped_in_composite() {
// PassThrough: child blends directly onto bg.
// Multiply: 255 * 128 / 255 = 128 for each channel.
// Expected: every pixel [128, 128, 128, 255]
let expected = vec![128u8, 128, 128, 255, 128, 128, 128, 255,
128, 128, 128, 255, 128, 128, 128, 255];
assert_eq!(out, expected, "PassThrough group should let child blend directly onto backdrop");
let expected = vec![
128u8, 128, 128, 255, 128, 128, 128, 255, 128, 128, 128, 255, 128, 128, 128, 255,
];
assert_eq!(
out, expected,
"PassThrough group should let child blend directly onto backdrop"
);
}
#[test]
@@ -60,8 +68,9 @@ fn test_pass_through_group_invisible_children() {
let out = composite_layers(&layers, w, h);
// Child hidden → only bg visible
let expected = vec![255u8, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255];
let expected = vec![
255u8, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
];
assert_eq!(out, expected);
}
@@ -90,7 +99,8 @@ fn test_pass_through_group_with_opacity() {
let out = composite_layers(&layers, w, h);
// Multiply full-strength
let expected = vec![128u8, 128, 128, 255, 128, 128, 128, 255,
128, 128, 128, 255, 128, 128, 128, 255];
let expected = vec![
128u8, 128, 128, 255, 128, 128, 128, 255, 128, 128, 128, 255, 128, 128, 128, 255,
];
assert_eq!(out, expected);
}
+12 -4
View File
@@ -49,7 +49,10 @@ fn test_composite_visibility_toggle() {
let layers = vec![bg_hidden, top.clone()];
let p = composite_layers(&layers, w, h);
let opaque = count_opaque_pixels(&p);
eprintln!("Test 2 (bg hidden): opaque={}/10000 (expected 3600)", opaque);
eprintln!(
"Test 2 (bg hidden): opaque={}/10000 (expected 3600)",
opaque
);
assert_eq!(opaque, 3600, "only black square should be visible");
// Test 3: hide top → only white background visible
@@ -58,12 +61,17 @@ fn test_composite_visibility_toggle() {
let layers = vec![bg.clone(), top_hidden];
let p = composite_layers(&layers, w, h);
let opaque = count_opaque_pixels(&p);
eprintln!("Test 3 (top hidden): opaque={}/10000 (expected 10000)", opaque);
eprintln!(
"Test 3 (top hidden): opaque={}/10000 (expected 10000)",
opaque
);
assert_eq!(opaque, 10000, "white background should be visible");
// Test 4: hide both → fully transparent
let mut bg_h = bg.clone(); bg_h.visible = false;
let mut top_h = top.clone(); top_h.visible = false;
let mut bg_h = bg.clone();
bg_h.visible = false;
let mut top_h = top.clone();
top_h.visible = false;
let layers = vec![bg_h, top_h];
let p = composite_layers(&layers, w, h);
let opaque = count_opaque_pixels(&p);