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:
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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()]))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user