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:
@@ -59,23 +59,38 @@ fn init_templates() -> Vec<ShapeTemplateDef> {
|
||||
}
|
||||
|
||||
pub fn get_template_def(name: &str) -> Option<ShapeTemplateDef> {
|
||||
TEMPLATES.get_or_init(init_templates).iter().find(|t| t.name == name).cloned()
|
||||
TEMPLATES
|
||||
.get_or_init(init_templates)
|
||||
.iter()
|
||||
.find(|t| t.name == name)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn all_names() -> Vec<String> {
|
||||
TEMPLATES.get_or_init(init_templates).iter().map(|t| t.name.clone()).collect()
|
||||
TEMPLATES
|
||||
.get_or_init(init_templates)
|
||||
.iter()
|
||||
.map(|t| t.name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn search_templates(query: &str) -> Vec<ShapeTemplateDef> {
|
||||
let q_lower = query.to_lowercase();
|
||||
TEMPLATES.get_or_init(init_templates)
|
||||
TEMPLATES
|
||||
.get_or_init(init_templates)
|
||||
.iter()
|
||||
.filter(|t| t.name.to_lowercase().contains(&q_lower) || t.description.to_lowercase().contains(&q_lower))
|
||||
.filter(|t| {
|
||||
t.name.to_lowercase().contains(&q_lower)
|
||||
|| t.description.to_lowercase().contains(&q_lower)
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn eval_template(def: &ShapeTemplateDef, provided_params: &HashMap<String, String>) -> Vec<TemplateComponent> {
|
||||
pub fn eval_template(
|
||||
def: &ShapeTemplateDef,
|
||||
provided_params: &HashMap<String, String>,
|
||||
) -> Vec<TemplateComponent> {
|
||||
let mut num_vars = HashMap::new();
|
||||
let mut str_vars = HashMap::new();
|
||||
|
||||
@@ -89,7 +104,9 @@ pub fn eval_template(def: &ShapeTemplateDef, provided_params: &HashMap<String, S
|
||||
}
|
||||
|
||||
let eval = |expr: &str| -> f32 {
|
||||
if let Ok(num) = expr.parse::<f32>() { return num; }
|
||||
if let Ok(num) = expr.parse::<f32>() {
|
||||
return num;
|
||||
}
|
||||
math_eval(expr, &num_vars).unwrap_or(0.0)
|
||||
};
|
||||
|
||||
@@ -116,8 +133,16 @@ pub fn eval_template(def: &ShapeTemplateDef, provided_params: &HashMap<String, S
|
||||
"circle" => {
|
||||
let cx = eval(&c.cx);
|
||||
let cy = eval(&c.cy);
|
||||
let rx_val = if !c.r.is_empty() { eval(&c.r) } else { eval(&c.rx) };
|
||||
let ry_val = if !c.r.is_empty() { eval(&c.r) } else { eval(&c.ry) };
|
||||
let rx_val = if !c.r.is_empty() {
|
||||
eval(&c.r)
|
||||
} else {
|
||||
eval(&c.rx)
|
||||
};
|
||||
let ry_val = if !c.r.is_empty() {
|
||||
eval(&c.r)
|
||||
} else {
|
||||
eval(&c.ry)
|
||||
};
|
||||
pts = circle_points(cx, cy, rx_val, ry_val, c.n.max(8));
|
||||
}
|
||||
"star" => {
|
||||
@@ -129,7 +154,12 @@ pub fn eval_template(def: &ShapeTemplateDef, provided_params: &HashMap<String, S
|
||||
}
|
||||
|
||||
if !pts.is_empty() {
|
||||
out.push(TemplateComponent { pts, closed: c.closed, fill, fill_color: fill_str });
|
||||
out.push(TemplateComponent {
|
||||
pts,
|
||||
closed: c.closed,
|
||||
fill,
|
||||
fill_color: fill_str,
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -145,7 +175,9 @@ fn tokenize(expr: &str) -> Vec<String> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut current = String::new();
|
||||
for c in expr.chars() {
|
||||
if c.is_whitespace() { continue; }
|
||||
if c.is_whitespace() {
|
||||
continue;
|
||||
}
|
||||
if "+-*/()".contains(c) {
|
||||
if !current.is_empty() {
|
||||
tokens.push(current.clone());
|
||||
@@ -156,7 +188,9 @@ fn tokenize(expr: &str) -> Vec<String> {
|
||||
current.push(c);
|
||||
}
|
||||
}
|
||||
if !current.is_empty() { tokens.push(current); }
|
||||
if !current.is_empty() {
|
||||
tokens.push(current);
|
||||
}
|
||||
tokens
|
||||
}
|
||||
|
||||
@@ -164,9 +198,15 @@ fn parse_expr(tokens: &[String], pos: &mut usize, vars: &HashMap<String, f32>) -
|
||||
let mut left = parse_term(tokens, pos, vars)?;
|
||||
while *pos < tokens.len() {
|
||||
let op = &tokens[*pos];
|
||||
if op == "+" { *pos += 1; left += parse_term(tokens, pos, vars)?; }
|
||||
else if op == "-" { *pos += 1; left -= parse_term(tokens, pos, vars)?; }
|
||||
else { break; }
|
||||
if op == "+" {
|
||||
*pos += 1;
|
||||
left += parse_term(tokens, pos, vars)?;
|
||||
} else if op == "-" {
|
||||
*pos += 1;
|
||||
left -= parse_term(tokens, pos, vars)?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(left)
|
||||
}
|
||||
@@ -175,20 +215,30 @@ fn parse_term(tokens: &[String], pos: &mut usize, vars: &HashMap<String, f32>) -
|
||||
let mut left = parse_factor(tokens, pos, vars)?;
|
||||
while *pos < tokens.len() {
|
||||
let op = &tokens[*pos];
|
||||
if op == "*" { *pos += 1; left *= parse_factor(tokens, pos, vars)?; }
|
||||
else if op == "/" { *pos += 1; left /= parse_factor(tokens, pos, vars)?; }
|
||||
else { break; }
|
||||
if op == "*" {
|
||||
*pos += 1;
|
||||
left *= parse_factor(tokens, pos, vars)?;
|
||||
} else if op == "/" {
|
||||
*pos += 1;
|
||||
left /= parse_factor(tokens, pos, vars)?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(left)
|
||||
}
|
||||
|
||||
fn parse_factor(tokens: &[String], pos: &mut usize, vars: &HashMap<String, f32>) -> Option<f32> {
|
||||
if *pos >= tokens.len() { return None; }
|
||||
if *pos >= tokens.len() {
|
||||
return None;
|
||||
}
|
||||
let t = &tokens[*pos];
|
||||
if t == "(" {
|
||||
*pos += 1;
|
||||
let val = parse_expr(tokens, pos, vars)?;
|
||||
if *pos < tokens.len() && tokens[*pos] == ")" { *pos += 1; }
|
||||
if *pos < tokens.len() && tokens[*pos] == ")" {
|
||||
*pos += 1;
|
||||
}
|
||||
return Some(val);
|
||||
}
|
||||
if let Ok(n) = t.parse::<f32>() {
|
||||
@@ -220,4 +270,4 @@ fn star_points(n: usize, outer: f32, inner: f32) -> Vec<[f32; 2]> {
|
||||
pts.push([a2.cos() * inner, a2.sin() * inner]);
|
||||
}
|
||||
pts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
#![allow(dead_code, unused_imports, unused_variables, unused_macros, unreachable_patterns)]
|
||||
use std::sync::{OnceLock, atomic::{AtomicU8, Ordering}};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::os::raw::c_char;
|
||||
use std::ffi::CString;
|
||||
#![allow(
|
||||
dead_code,
|
||||
unused_imports,
|
||||
unused_variables,
|
||||
unused_macros,
|
||||
unreachable_patterns
|
||||
)]
|
||||
use libloading::Library;
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_char;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{
|
||||
atomic::{AtomicU8, Ordering},
|
||||
OnceLock,
|
||||
};
|
||||
|
||||
/// Global vision backend preference mirrored into the dynamic `hcie-vision` plugin.
|
||||
/// 0 = CPU, 1 = GPU. Stored here so it can be set before the plugin is loaded.
|
||||
@@ -48,15 +57,25 @@ pub fn map_brush_style(style: hcie_protocol::BrushStyle) -> hcie_brush_engine::B
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blend_pixels(dst: [u8; 4], src: [u8; 4], mode: hcie_protocol::BlendMode, opacity: f32) -> [u8; 4] {
|
||||
|
||||
pub fn blend_pixels(
|
||||
dst: [u8; 4],
|
||||
src: [u8; 4],
|
||||
mode: hcie_protocol::BlendMode,
|
||||
opacity: f32,
|
||||
) -> [u8; 4] {
|
||||
hcie_blend::blend_pixels(dst, src, mode.into(), opacity)
|
||||
}
|
||||
pub fn blend_buffers(dst: &mut [u8], src: &[u8], mode: hcie_protocol::BlendMode, opacity: f32) {
|
||||
hcie_blend::blend_buffers(dst, src, mode.into(), opacity)
|
||||
}
|
||||
|
||||
pub fn apply_filter(filter_id: &str, params: &serde_json::Value, pixels: &mut [u8], width: u32, height: u32) -> Result<(), String> {
|
||||
pub fn apply_filter(
|
||||
filter_id: &str,
|
||||
params: &serde_json::Value,
|
||||
pixels: &mut [u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(), String> {
|
||||
let mut fl = hcie_protocol::Layer::new_transparent("", width, height);
|
||||
fl.pixels.copy_from_slice(pixels);
|
||||
hcie_filter::apply_filter(&mut fl, filter_id, params);
|
||||
@@ -65,69 +84,255 @@ pub fn apply_filter(filter_id: &str, params: &serde_json::Value, pixels: &mut [u
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn draw_line(layer: &mut hcie_protocol::Layer, x0: f32, y0: f32, x1: f32, y1: f32, color: [u8; 4], stroke_size: f32, mask: Option<&[u8]>) {
|
||||
hcie_draw::draw_line(layer, x0, y0, x1, y1, color, stroke_size, mask); layer.dirty = true;
|
||||
pub fn draw_line(
|
||||
layer: &mut hcie_protocol::Layer,
|
||||
x0: f32,
|
||||
y0: f32,
|
||||
x1: f32,
|
||||
y1: f32,
|
||||
color: [u8; 4],
|
||||
stroke_size: f32,
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
hcie_draw::draw_line(layer, x0, y0, x1, y1, color, stroke_size, mask);
|
||||
layer.dirty = true;
|
||||
}
|
||||
pub fn draw_filled_rect(layer: &mut hcie_protocol::Layer, x1: f32, y1: f32, x2: f32, y2: f32, color: [u8; 4], mask: Option<&[u8]>) {
|
||||
hcie_draw::draw_filled_rect(layer, x1, y1, x2, y2, color, mask); layer.dirty = true;
|
||||
pub fn draw_filled_rect(
|
||||
layer: &mut hcie_protocol::Layer,
|
||||
x1: f32,
|
||||
y1: f32,
|
||||
x2: f32,
|
||||
y2: f32,
|
||||
color: [u8; 4],
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
hcie_draw::draw_filled_rect(layer, x1, y1, x2, y2, color, mask);
|
||||
layer.dirty = true;
|
||||
}
|
||||
pub fn draw_filled_ellipse(layer: &mut hcie_protocol::Layer, cx: f32, cy: f32, rx: f32, ry: f32, color: [u8; 4], mask: Option<&[u8]>) {
|
||||
hcie_draw::draw_filled_ellipse(layer, cx, cy, rx, ry, color, mask); layer.dirty = true;
|
||||
pub fn draw_filled_ellipse(
|
||||
layer: &mut hcie_protocol::Layer,
|
||||
cx: f32,
|
||||
cy: f32,
|
||||
rx: f32,
|
||||
ry: f32,
|
||||
color: [u8; 4],
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
hcie_draw::draw_filled_ellipse(layer, cx, cy, rx, ry, color, mask);
|
||||
layer.dirty = true;
|
||||
}
|
||||
pub fn flood_fill(layer: &mut hcie_protocol::Layer, x: u32, y: u32, color: [u8; 4], tolerance: u8, mask: Option<&[u8]>) {
|
||||
hcie_draw::flood_fill(layer, x, y, color, tolerance, mask); layer.dirty = true;
|
||||
pub fn flood_fill(
|
||||
layer: &mut hcie_protocol::Layer,
|
||||
x: u32,
|
||||
y: u32,
|
||||
color: [u8; 4],
|
||||
tolerance: u8,
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
hcie_draw::flood_fill(layer, x, y, color, tolerance, mask);
|
||||
layer.dirty = true;
|
||||
}
|
||||
pub fn draw_brush_stroke(layer: &mut hcie_protocol::Layer, points: &[(f32, f32, f32)], color: [u8; 4], tip: &hcie_protocol::BrushTip, is_eraser: bool, mask: Option<&[u8]>, stroke_mask: Option<&mut [u8]>, bg_pixels: Option<&[u8]>) {
|
||||
pub fn draw_brush_stroke(
|
||||
layer: &mut hcie_protocol::Layer,
|
||||
points: &[(f32, f32, f32)],
|
||||
color: [u8; 4],
|
||||
tip: &hcie_protocol::BrushTip,
|
||||
is_eraser: bool,
|
||||
mask: Option<&[u8]>,
|
||||
stroke_mask: Option<&mut [u8]>,
|
||||
bg_pixels: Option<&[u8]>,
|
||||
) {
|
||||
let style = map_brush_style(tip.style);
|
||||
let ct = hcie_brush_engine::BrushTip {
|
||||
style,
|
||||
size: tip.size, opacity: tip.opacity, hardness: tip.hardness,
|
||||
spacing: tip.spacing, flow: tip.flow,
|
||||
jitter_amount: tip.jitter_amount, scatter_amount: tip.scatter_amount,
|
||||
angle: tip.angle, roundness: tip.roundness,
|
||||
spray_particle_size: tip.spray_particle_size, spray_density: tip.spray_density,
|
||||
bitmap_pixels: tip.bitmap_pixels.clone(), bitmap_w: tip.bitmap_w, bitmap_h: tip.bitmap_h,
|
||||
color_variant: tip.color_variant, variant_amount: tip.variant_amount, density: tip.density,
|
||||
size: tip.size,
|
||||
opacity: tip.opacity,
|
||||
hardness: tip.hardness,
|
||||
spacing: tip.spacing,
|
||||
flow: tip.flow,
|
||||
jitter_amount: tip.jitter_amount,
|
||||
scatter_amount: tip.scatter_amount,
|
||||
angle: tip.angle,
|
||||
roundness: tip.roundness,
|
||||
spray_particle_size: tip.spray_particle_size,
|
||||
spray_density: tip.spray_density,
|
||||
bitmap_pixels: tip.bitmap_pixels.clone(),
|
||||
bitmap_w: tip.bitmap_w,
|
||||
bitmap_h: tip.bitmap_h,
|
||||
color_variant: tip.color_variant,
|
||||
variant_amount: tip.variant_amount,
|
||||
density: tip.density,
|
||||
drawing_angle: false,
|
||||
rotation_random: 0.0,
|
||||
};
|
||||
hcie_draw::draw_brush_stroke(layer, points, color, &ct, is_eraser, mask, stroke_mask, bg_pixels); layer.dirty = true;
|
||||
hcie_draw::draw_brush_stroke(
|
||||
layer,
|
||||
points,
|
||||
color,
|
||||
&ct,
|
||||
is_eraser,
|
||||
mask,
|
||||
stroke_mask,
|
||||
bg_pixels,
|
||||
);
|
||||
layer.dirty = true;
|
||||
}
|
||||
pub fn draw_specialized_stroke(pixels: &mut [u8], width: u32, height: u32, points: &[(f32, f32, f32)], brush_style: hcie_protocol::BrushStyle, size: f32, hardness: f32, color: [u8; 4], opacity: f32, spacing_ratio: f32, is_eraser: bool, sketch_history: Option<&mut Vec<(f32, f32)>>, spray_particle_size: f32, spray_density: u32, mask: Option<&[u8]>, stroke_mask: Option<&mut [u8]>, bg_pixels: Option<&[u8]>, color_variant: bool, variant_amount: f32, density: f32) {
|
||||
pub fn draw_specialized_stroke(
|
||||
pixels: &mut [u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
points: &[(f32, f32, f32)],
|
||||
brush_style: hcie_protocol::BrushStyle,
|
||||
size: f32,
|
||||
hardness: f32,
|
||||
color: [u8; 4],
|
||||
opacity: f32,
|
||||
spacing_ratio: f32,
|
||||
is_eraser: bool,
|
||||
sketch_history: Option<&mut Vec<(f32, f32)>>,
|
||||
spray_particle_size: f32,
|
||||
spray_density: u32,
|
||||
mask: Option<&[u8]>,
|
||||
stroke_mask: Option<&mut [u8]>,
|
||||
bg_pixels: Option<&[u8]>,
|
||||
color_variant: bool,
|
||||
variant_amount: f32,
|
||||
density: f32,
|
||||
) {
|
||||
let style = map_brush_style(brush_style);
|
||||
hcie_brush_engine::draw_specialized_stroke(pixels, width, height, points, style, size, hardness, color, opacity, spacing_ratio, is_eraser, sketch_history, spray_particle_size, spray_density, mask, stroke_mask, bg_pixels, color_variant, variant_amount, density)
|
||||
hcie_brush_engine::draw_specialized_stroke(
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
points,
|
||||
style,
|
||||
size,
|
||||
hardness,
|
||||
color,
|
||||
opacity,
|
||||
spacing_ratio,
|
||||
is_eraser,
|
||||
sketch_history,
|
||||
spray_particle_size,
|
||||
spray_density,
|
||||
mask,
|
||||
stroke_mask,
|
||||
bg_pixels,
|
||||
color_variant,
|
||||
variant_amount,
|
||||
density,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn composite_layers(layers: &[hcie_protocol::Layer], cw: u32, ch: u32) -> Vec<u8> { hcie_composite::composite_layers(layers, cw, ch) }
|
||||
pub fn composite_layers_region(layers: &[hcie_protocol::Layer], cw: u32, ch: u32, x0: u32, y0: u32, x1: u32, y1: u32, out: &mut [u8]) { hcie_composite::composite_layers_region(layers, cw, ch, x0, y0, x1, y1, out) }
|
||||
pub fn composite_layers(layers: &[hcie_protocol::Layer], cw: u32, ch: u32) -> Vec<u8> {
|
||||
hcie_composite::composite_layers(layers, cw, ch)
|
||||
}
|
||||
pub fn composite_layers_region(
|
||||
layers: &[hcie_protocol::Layer],
|
||||
cw: u32,
|
||||
ch: u32,
|
||||
x0: u32,
|
||||
y0: u32,
|
||||
x1: u32,
|
||||
y1: u32,
|
||||
out: &mut [u8],
|
||||
) {
|
||||
hcie_composite::composite_layers_region(layers, cw, ch, x0, y0, x1, y1, out)
|
||||
}
|
||||
pub mod tiled {
|
||||
|
||||
pub fn composite_tiled_into(layers: &[hcie_protocol::Layer], tile_layers: &[Option<hcie_tile::TiledLayer>], cw: u32, ch: u32, x0: u32, y0: u32, x1: u32, y1: u32, out: &mut [u8]) {
|
||||
hcie_composite::tiled::composite_tiled_into(layers, tile_layers, cw, ch, x0, y0, x1, y1, out)
|
||||
|
||||
pub fn composite_tiled_into(
|
||||
layers: &[hcie_protocol::Layer],
|
||||
tile_layers: &[Option<hcie_tile::TiledLayer>],
|
||||
cw: u32,
|
||||
ch: u32,
|
||||
x0: u32,
|
||||
y0: u32,
|
||||
x1: u32,
|
||||
y1: u32,
|
||||
out: &mut [u8],
|
||||
) {
|
||||
hcie_composite::tiled::composite_tiled_into(
|
||||
layers,
|
||||
tile_layers,
|
||||
cw,
|
||||
ch,
|
||||
x0,
|
||||
y0,
|
||||
x1,
|
||||
y1,
|
||||
out,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_image(path: &Path) -> Result<hcie_protocol::Layer, String> { hcie_io::load_image(path) }
|
||||
pub fn save_image(layer: &hcie_protocol::Layer, path: &Path, format: &str) -> Result<(), String> { hcie_io::save_image(layer, path, format) }
|
||||
pub fn import_psd(path: &Path) -> Result<Vec<hcie_protocol::Layer>, String> { hcie_psd::import_psd(path) }
|
||||
pub fn export_psd(layers: &[hcie_protocol::Layer], cw: u32, ch: u32, composited: &[u8], path: &Path) -> Result<(), String> { hcie_psd::save_psd(layers, cw, ch, composited, path) }
|
||||
pub fn import_kra(path: &Path) -> Result<Vec<hcie_protocol::Layer>, String> { hcie_kra::import_kra(path) }
|
||||
pub fn export_kra(layers: &[hcie_protocol::Layer], cw: u32, ch: u32, composited: &[u8], path: &Path) -> Result<(), String> { hcie_kra::export_kra(layers, cw, ch, composited, path) }
|
||||
pub fn load_native(path: &Path) -> Result<Vec<hcie_protocol::Layer>, String> { hcie_native::load_native(path) }
|
||||
pub fn save_native(layers: &[hcie_protocol::Layer], path: &Path) -> Result<(), String> { hcie_native::save_native(layers, path) }
|
||||
pub fn load_image(path: &Path) -> Result<hcie_protocol::Layer, String> {
|
||||
hcie_io::load_image(path)
|
||||
}
|
||||
pub fn save_image(layer: &hcie_protocol::Layer, path: &Path, format: &str) -> Result<(), String> {
|
||||
hcie_io::save_image(layer, path, format)
|
||||
}
|
||||
pub fn import_psd(path: &Path) -> Result<Vec<hcie_protocol::Layer>, String> {
|
||||
hcie_psd::import_psd(path)
|
||||
}
|
||||
pub fn export_psd(
|
||||
layers: &[hcie_protocol::Layer],
|
||||
cw: u32,
|
||||
ch: u32,
|
||||
composited: &[u8],
|
||||
path: &Path,
|
||||
) -> Result<(), String> {
|
||||
hcie_psd::save_psd(layers, cw, ch, composited, path)
|
||||
}
|
||||
pub fn import_kra(path: &Path) -> Result<Vec<hcie_protocol::Layer>, String> {
|
||||
hcie_kra::import_kra(path)
|
||||
}
|
||||
pub fn export_kra(
|
||||
layers: &[hcie_protocol::Layer],
|
||||
cw: u32,
|
||||
ch: u32,
|
||||
composited: &[u8],
|
||||
path: &Path,
|
||||
) -> Result<(), String> {
|
||||
hcie_kra::export_kra(layers, cw, ch, composited, path)
|
||||
}
|
||||
pub fn load_native(path: &Path) -> Result<Vec<hcie_protocol::Layer>, String> {
|
||||
hcie_native::load_native(path)
|
||||
}
|
||||
pub fn save_native(layers: &[hcie_protocol::Layer], path: &Path) -> Result<(), String> {
|
||||
hcie_native::save_native(layers, path)
|
||||
}
|
||||
|
||||
pub mod svg_import {
|
||||
|
||||
pub fn import_svg(data: &[u8]) -> Result<Vec<hcie_protocol::Layer>, String> { hcie_io::svg_import::import_svg(data) }
|
||||
|
||||
pub fn import_svg(data: &[u8]) -> Result<Vec<hcie_protocol::Layer>, String> {
|
||||
hcie_io::svg_import::import_svg(data)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod vector {
|
||||
|
||||
pub fn render_vector_shapes(layer: &mut hcie_protocol::Layer) { hcie_vector::render_vector_shapes(layer) }
|
||||
|
||||
pub fn render_vector_shapes(layer: &mut hcie_protocol::Layer) {
|
||||
hcie_vector::render_vector_shapes(layer)
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum BooleanOp { Union, Intersect, Subtract, Xor }
|
||||
pub fn boolean_shapes(op: BooleanOp, a: &hcie_protocol::VectorShape, b: &hcie_protocol::VectorShape) -> Option<hcie_protocol::VectorShape> {
|
||||
let vop = match op { BooleanOp::Union => hcie_vector::path_boolean::BooleanOp::Union, BooleanOp::Intersect => hcie_vector::path_boolean::BooleanOp::Intersect, BooleanOp::Subtract => hcie_vector::path_boolean::BooleanOp::Subtract, BooleanOp::Xor => hcie_vector::path_boolean::BooleanOp::Xor };
|
||||
pub enum BooleanOp {
|
||||
Union,
|
||||
Intersect,
|
||||
Subtract,
|
||||
Xor,
|
||||
}
|
||||
pub fn boolean_shapes(
|
||||
op: BooleanOp,
|
||||
a: &hcie_protocol::VectorShape,
|
||||
b: &hcie_protocol::VectorShape,
|
||||
) -> Option<hcie_protocol::VectorShape> {
|
||||
let vop = match op {
|
||||
BooleanOp::Union => hcie_vector::path_boolean::BooleanOp::Union,
|
||||
BooleanOp::Intersect => hcie_vector::path_boolean::BooleanOp::Intersect,
|
||||
BooleanOp::Subtract => hcie_vector::path_boolean::BooleanOp::Subtract,
|
||||
BooleanOp::Xor => hcie_vector::path_boolean::BooleanOp::Xor,
|
||||
};
|
||||
hcie_vector::path_boolean::boolean_shapes(vop, a, b)
|
||||
}
|
||||
}
|
||||
@@ -137,17 +342,29 @@ pub mod vector {
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn find_plugin_path(name: &str) -> Result<PathBuf, String> {
|
||||
let filename = if cfg!(target_os = "windows") { format!("{}.dll", name) } else if cfg!(target_os = "macos") { format!("lib{}.dylib", name) } else { format!("lib{}.so", name) };
|
||||
let filename = if cfg!(target_os = "windows") {
|
||||
format!("{}.dll", name)
|
||||
} else if cfg!(target_os = "macos") {
|
||||
format!("lib{}.dylib", name)
|
||||
} else {
|
||||
format!("lib{}.so", name)
|
||||
};
|
||||
let candidates: Vec<PathBuf> = {
|
||||
let mut v = Vec::new();
|
||||
// 1. Traditional plugins directory next to the executable or cwd.
|
||||
v.push(PathBuf::from("plugins"));
|
||||
if let Some(exe_plugins) = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.join("plugins"))) {
|
||||
if let Some(exe_plugins) = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|d| d.join("plugins")))
|
||||
{
|
||||
v.push(exe_plugins.clone());
|
||||
}
|
||||
// 2. Cargo workspace layout: the dynamic library is placed directly in
|
||||
// the target profile directory (e.g. target/debug/libhcie_ai.so).
|
||||
if let Some(exe_dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())) {
|
||||
if let Some(exe_dir) = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|d| d.to_path_buf()))
|
||||
{
|
||||
v.push(exe_dir.clone());
|
||||
// 3. Also check the deps subdirectory used by Cargo.
|
||||
v.push(exe_dir.join("deps"));
|
||||
@@ -175,23 +392,35 @@ fn find_plugin_path(name: &str) -> Result<PathBuf, String> {
|
||||
v.push(c.join("target").join("debug").join("deps"));
|
||||
v.push(c.join("target").join("release"));
|
||||
v.push(c.join("target").join("release").join("deps"));
|
||||
if !c.pop() { break; }
|
||||
if !c.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
};
|
||||
for base in candidates {
|
||||
let p = base.join(&filename);
|
||||
if p.exists() { return Ok(p); }
|
||||
if p.exists() {
|
||||
return Ok(p);
|
||||
}
|
||||
}
|
||||
Err(format!("Plugin '{}' not found", name))
|
||||
}
|
||||
|
||||
macro_rules! sym { ($lib:ident, $name:literal) => { *$lib.get(concat!($name, "\0").as_bytes()).expect(concat!("Failed to resolve ", $name)) }; }
|
||||
macro_rules! sym {
|
||||
($lib:ident, $name:literal) => {
|
||||
*$lib
|
||||
.get(concat!($name, "\0").as_bytes())
|
||||
.expect(concat!("Failed to resolve ", $name))
|
||||
};
|
||||
}
|
||||
|
||||
struct AiPlugins {
|
||||
_lib_ai: Library, _lib_vision: Library,
|
||||
ai_chat_complete_fn: unsafe extern "C" fn(*const c_char, *const c_char, *mut *mut u8, *mut usize) -> bool,
|
||||
_lib_ai: Library,
|
||||
_lib_vision: Library,
|
||||
ai_chat_complete_fn:
|
||||
unsafe extern "C" fn(*const c_char, *const c_char, *mut *mut u8, *mut usize) -> bool,
|
||||
ai_session_create_fn: unsafe extern "C" fn(*const c_char) -> i64,
|
||||
ai_session_push_user_fn: unsafe extern "C" fn(i64, *const c_char) -> bool,
|
||||
ai_session_push_assistant_fn: unsafe extern "C" fn(i64, *const c_char) -> bool,
|
||||
@@ -201,15 +430,43 @@ struct AiPlugins {
|
||||
ai_template_names_fn: unsafe extern "C" fn(*mut *mut u8, *mut usize) -> bool,
|
||||
ai_template_get_fn: unsafe extern "C" fn(*const c_char, *mut *mut u8, *mut usize) -> bool,
|
||||
ai_template_search_fn: unsafe extern "C" fn(*const c_char, *mut *mut u8, *mut usize) -> bool,
|
||||
ai_template_execute_fn: unsafe extern "C" fn(*const c_char, *const c_char, *mut *mut u8, *mut usize) -> bool,
|
||||
ai_template_execute_fn:
|
||||
unsafe extern "C" fn(*const c_char, *const c_char, *mut *mut u8, *mut usize) -> bool,
|
||||
ai_execute_action_fn: unsafe extern "C" fn(*const c_char, *mut *mut u8, *mut usize) -> bool,
|
||||
ai_free_buffer_fn: unsafe extern "C" fn(*mut u8, usize),
|
||||
vision_background_removal_fn: unsafe extern "C" fn(*const u8, usize, u32, u32, *mut *mut u8, *mut usize) -> bool,
|
||||
vision_object_segment_fn: unsafe extern "C" fn(*const u8, usize, u32, u32, u32, u32, u32, u32, u32, u32, *const c_char, *mut *mut u8, *mut usize) -> bool,
|
||||
vision_super_resolution_fn: unsafe extern "C" fn(*const u8, usize, u32, u32, *mut *mut u8, *mut usize) -> bool,
|
||||
vision_inpaint_fn: unsafe extern "C" fn(*mut u8, usize, u32, u32, *const u8, usize, i32) -> bool,
|
||||
vision_build_polygon_mask_fn: unsafe extern "C" fn(*const c_char, u32, u32, *mut *mut u8, *mut usize) -> bool,
|
||||
vision_build_stroke_mask_fn: unsafe extern "C" fn(*const c_char, *const u8, usize, f32, u32, u32, *mut *mut u8, *mut usize) -> bool,
|
||||
vision_background_removal_fn:
|
||||
unsafe extern "C" fn(*const u8, usize, u32, u32, *mut *mut u8, *mut usize) -> bool,
|
||||
vision_object_segment_fn: unsafe extern "C" fn(
|
||||
*const u8,
|
||||
usize,
|
||||
u32,
|
||||
u32,
|
||||
u32,
|
||||
u32,
|
||||
u32,
|
||||
u32,
|
||||
u32,
|
||||
u32,
|
||||
*const c_char,
|
||||
*mut *mut u8,
|
||||
*mut usize,
|
||||
) -> bool,
|
||||
vision_super_resolution_fn:
|
||||
unsafe extern "C" fn(*const u8, usize, u32, u32, *mut *mut u8, *mut usize) -> bool,
|
||||
vision_inpaint_fn:
|
||||
unsafe extern "C" fn(*mut u8, usize, u32, u32, *const u8, usize, i32) -> bool,
|
||||
vision_build_polygon_mask_fn:
|
||||
unsafe extern "C" fn(*const c_char, u32, u32, *mut *mut u8, *mut usize) -> bool,
|
||||
vision_build_stroke_mask_fn: unsafe extern "C" fn(
|
||||
*const c_char,
|
||||
*const u8,
|
||||
usize,
|
||||
f32,
|
||||
u32,
|
||||
u32,
|
||||
*mut *mut u8,
|
||||
*mut usize,
|
||||
) -> bool,
|
||||
vision_set_backend_fn: unsafe extern "C" fn(u8),
|
||||
vision_free_buffer_fn: unsafe extern "C" fn(*mut u8, usize),
|
||||
}
|
||||
@@ -226,7 +483,8 @@ fn ai_plugins() -> &'static AiPlugins {
|
||||
let ai_chat_complete_fn = *la.get(b"hcie_ai_chat_complete_c\0").unwrap();
|
||||
let ai_session_create_fn = *la.get(b"hcie_ai_session_create_c\0").unwrap();
|
||||
let ai_session_push_user_fn = *la.get(b"hcie_ai_session_push_user_c\0").unwrap();
|
||||
let ai_session_push_assistant_fn = *la.get(b"hcie_ai_session_push_assistant_c\0").unwrap();
|
||||
let ai_session_push_assistant_fn =
|
||||
*la.get(b"hcie_ai_session_push_assistant_c\0").unwrap();
|
||||
let ai_session_clear_fn = *la.get(b"hcie_ai_session_clear_c\0").unwrap();
|
||||
let ai_session_destroy_fn = *la.get(b"hcie_ai_session_destroy_c\0").unwrap();
|
||||
let ai_session_serialize_fn = *la.get(b"hcie_ai_session_serialize_c\0").unwrap();
|
||||
@@ -236,26 +494,46 @@ fn ai_plugins() -> &'static AiPlugins {
|
||||
let ai_template_execute_fn = *la.get(b"hcie_ai_template_execute_c\0").unwrap();
|
||||
let ai_execute_action_fn = *la.get(b"hcie_ai_execute_action_c\0").unwrap();
|
||||
let ai_free_buffer_fn = *la.get(b"free_buffer_c\0").unwrap();
|
||||
let vision_background_removal_fn = *lv.get(b"hcie_vision_background_removal_c\0").unwrap();
|
||||
let vision_background_removal_fn =
|
||||
*lv.get(b"hcie_vision_background_removal_c\0").unwrap();
|
||||
let vision_object_segment_fn = *lv.get(b"hcie_vision_object_segment_c\0").unwrap();
|
||||
let vision_super_resolution_fn = *lv.get(b"hcie_vision_super_resolution_c\0").unwrap();
|
||||
let vision_inpaint_fn = *lv.get(b"hcie_vision_inpaint_c\0").unwrap();
|
||||
let vision_build_polygon_mask_fn = *lv.get(b"hcie_vision_build_polygon_mask_c\0").unwrap();
|
||||
let vision_build_stroke_mask_fn = *lv.get(b"hcie_vision_build_stroke_mask_c\0").unwrap();
|
||||
let vision_set_backend_fn: unsafe extern "C" fn(u8) = *lv.get(b"hcie_vision_set_backend_c\0").unwrap();
|
||||
let vision_build_polygon_mask_fn =
|
||||
*lv.get(b"hcie_vision_build_polygon_mask_c\0").unwrap();
|
||||
let vision_build_stroke_mask_fn =
|
||||
*lv.get(b"hcie_vision_build_stroke_mask_c\0").unwrap();
|
||||
let vision_set_backend_fn: unsafe extern "C" fn(u8) =
|
||||
*lv.get(b"hcie_vision_set_backend_c\0").unwrap();
|
||||
let vision_free_buffer_fn = *lv.get(b"free_buffer_c\0").unwrap();
|
||||
// Mirror the current backend preference into the freshly loaded plugin.
|
||||
let backend = VISION_BACKEND.load(Ordering::Relaxed);
|
||||
(vision_set_backend_fn)(backend);
|
||||
|
||||
AiPlugins {
|
||||
_lib_ai: la, _lib_vision: lv,
|
||||
ai_chat_complete_fn, ai_session_create_fn, ai_session_push_user_fn, ai_session_push_assistant_fn,
|
||||
ai_session_clear_fn, ai_session_destroy_fn, ai_session_serialize_fn, ai_template_names_fn,
|
||||
ai_template_get_fn, ai_template_search_fn, ai_template_execute_fn, ai_execute_action_fn, ai_free_buffer_fn,
|
||||
vision_background_removal_fn, vision_object_segment_fn, vision_super_resolution_fn,
|
||||
vision_inpaint_fn, vision_build_polygon_mask_fn, vision_build_stroke_mask_fn,
|
||||
vision_set_backend_fn, vision_free_buffer_fn,
|
||||
_lib_ai: la,
|
||||
_lib_vision: lv,
|
||||
ai_chat_complete_fn,
|
||||
ai_session_create_fn,
|
||||
ai_session_push_user_fn,
|
||||
ai_session_push_assistant_fn,
|
||||
ai_session_clear_fn,
|
||||
ai_session_destroy_fn,
|
||||
ai_session_serialize_fn,
|
||||
ai_template_names_fn,
|
||||
ai_template_get_fn,
|
||||
ai_template_search_fn,
|
||||
ai_template_execute_fn,
|
||||
ai_execute_action_fn,
|
||||
ai_free_buffer_fn,
|
||||
vision_background_removal_fn,
|
||||
vision_object_segment_fn,
|
||||
vision_super_resolution_fn,
|
||||
vision_inpaint_fn,
|
||||
vision_build_polygon_mask_fn,
|
||||
vision_build_stroke_mask_fn,
|
||||
vision_set_backend_fn,
|
||||
vision_free_buffer_fn,
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -275,26 +553,73 @@ pub fn vision_set_backend(backend: u8) {
|
||||
|
||||
pub mod ai {
|
||||
use super::*;
|
||||
macro_rules! c { ($e:expr) => { CString::new($e).unwrap() }; }
|
||||
macro_rules! c {
|
||||
($e:expr) => {
|
||||
CString::new($e).unwrap()
|
||||
};
|
||||
}
|
||||
pub fn chat_complete(config: &str, messages: &str) -> Result<Vec<u8>, String> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
if ai!(ai_chat_complete_fn, c!(config).as_ptr(), c!(messages).as_ptr(), &mut op, &mut ol) {
|
||||
let data = unsafe { std::slice::from_raw_parts(op, ol).to_vec() }; ai!(ai_free_buffer_fn, op, ol); Ok(data)
|
||||
} else { Err("AI chat failed".into()) }
|
||||
if ai!(
|
||||
ai_chat_complete_fn,
|
||||
c!(config).as_ptr(),
|
||||
c!(messages).as_ptr(),
|
||||
&mut op,
|
||||
&mut ol
|
||||
) {
|
||||
let data = unsafe { std::slice::from_raw_parts(op, ol).to_vec() };
|
||||
ai!(ai_free_buffer_fn, op, ol);
|
||||
Ok(data)
|
||||
} else {
|
||||
Err("AI chat failed".into())
|
||||
}
|
||||
}
|
||||
pub fn session_create(s: &str) -> i64 {
|
||||
ai!(ai_session_create_fn, c!(s).as_ptr())
|
||||
}
|
||||
pub fn session_push_user(sid: i64, t: &str) -> bool {
|
||||
ai!(ai_session_push_user_fn, sid, c!(t).as_ptr())
|
||||
}
|
||||
pub fn session_push_assistant(sid: i64, t: &str) -> bool {
|
||||
ai!(ai_session_push_assistant_fn, sid, c!(t).as_ptr())
|
||||
}
|
||||
pub fn session_clear(sid: i64) -> bool {
|
||||
ai!(ai_session_clear_fn, sid)
|
||||
}
|
||||
pub fn session_destroy(sid: i64) {
|
||||
ai!(ai_session_destroy_fn, sid)
|
||||
}
|
||||
pub fn session_create(s: &str) -> i64 { ai!(ai_session_create_fn, c!(s).as_ptr()) }
|
||||
pub fn session_push_user(sid: i64, t: &str) -> bool { ai!(ai_session_push_user_fn, sid, c!(t).as_ptr()) }
|
||||
pub fn session_push_assistant(sid: i64, t: &str) -> bool { ai!(ai_session_push_assistant_fn, sid, c!(t).as_ptr()) }
|
||||
pub fn session_clear(sid: i64) -> bool { ai!(ai_session_clear_fn, sid) }
|
||||
pub fn session_destroy(sid: i64) { ai!(ai_session_destroy_fn, sid) }
|
||||
pub fn session_serialize(sid: i64) -> Option<Vec<u8>> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
if ai!(ai_session_serialize_fn, sid, &mut op, &mut ol) { let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() }; ai!(ai_free_buffer_fn, op, ol); Some(d) } else { None }
|
||||
if ai!(ai_session_serialize_fn, sid, &mut op, &mut ol) {
|
||||
let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() };
|
||||
ai!(ai_free_buffer_fn, op, ol);
|
||||
Some(d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn template_names() -> Option<Vec<u8>> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
if ai!(ai_template_names_fn, &mut op, &mut ol) {
|
||||
let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() };
|
||||
ai!(ai_free_buffer_fn, op, ol);
|
||||
Some(d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn template_get(name: &str) -> Option<Vec<u8>> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
let n = CString::new(name).ok()?;
|
||||
if ai!(ai_template_get_fn, n.as_ptr(), &mut op, &mut ol) {
|
||||
let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() };
|
||||
ai!(ai_free_buffer_fn, op, ol);
|
||||
Some(d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn template_names() -> Option<Vec<u8>> { let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
if ai!(ai_template_names_fn, &mut op, &mut ol) { let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() }; ai!(ai_free_buffer_fn, op, ol); Some(d) } else { None } }
|
||||
pub fn template_get(name: &str) -> Option<Vec<u8>> { let (mut op, mut ol) = (std::ptr::null_mut(), 0); let n = CString::new(name).ok()?;
|
||||
if ai!(ai_template_get_fn, n.as_ptr(), &mut op, &mut ol) { let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() }; ai!(ai_free_buffer_fn, op, ol); Some(d) } else { None } }
|
||||
/// # Purpose
|
||||
/// Searches for AI prompt/action templates matching a specific query or tag.
|
||||
///
|
||||
@@ -322,39 +647,164 @@ pub mod ai {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn template_execute(name: &str, params: &str) -> Option<Vec<u8>> { let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
pub fn template_execute(name: &str, params: &str) -> Option<Vec<u8>> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
let (n, j) = (CString::new(name).ok()?, CString::new(params).ok()?);
|
||||
if ai!(ai_template_execute_fn, n.as_ptr(), j.as_ptr(), &mut op, &mut ol) { let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() }; ai!(ai_free_buffer_fn, op, ol); Some(d) } else { None } }
|
||||
pub fn execute_action(json: &str) -> Option<Vec<u8>> { let (mut op, mut ol) = (std::ptr::null_mut(), 0); let j = CString::new(json).ok()?;
|
||||
if ai!(ai_execute_action_fn, j.as_ptr(), &mut op, &mut ol) { let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() }; ai!(ai_free_buffer_fn, op, ol); Some(d) } else { None } }
|
||||
if ai!(
|
||||
ai_template_execute_fn,
|
||||
n.as_ptr(),
|
||||
j.as_ptr(),
|
||||
&mut op,
|
||||
&mut ol
|
||||
) {
|
||||
let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() };
|
||||
ai!(ai_free_buffer_fn, op, ol);
|
||||
Some(d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn execute_action(json: &str) -> Option<Vec<u8>> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
let j = CString::new(json).ok()?;
|
||||
if ai!(ai_execute_action_fn, j.as_ptr(), &mut op, &mut ol) {
|
||||
let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() };
|
||||
ai!(ai_free_buffer_fn, op, ol);
|
||||
Some(d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod vision {
|
||||
use super::*;
|
||||
pub fn background_removal(img: &[u8], w: u32, h: u32) -> Option<Vec<u8>> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
if ai!(vision_background_removal_fn, img.as_ptr(), img.len(), w, h, &mut op, &mut ol) { let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() }; ai!(vision_free_buffer_fn, op, ol); Some(d) } else { None }
|
||||
if ai!(
|
||||
vision_background_removal_fn,
|
||||
img.as_ptr(),
|
||||
img.len(),
|
||||
w,
|
||||
h,
|
||||
&mut op,
|
||||
&mut ol
|
||||
) {
|
||||
let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() };
|
||||
ai!(vision_free_buffer_fn, op, ol);
|
||||
Some(d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn object_segment(img: &[u8], w: u32, h: u32, px: u32, py: u32, bx0: u32, by0: u32, bx1: u32, by1: u32, model_path: &str) -> Option<Vec<u8>> {
|
||||
pub fn object_segment(
|
||||
img: &[u8],
|
||||
w: u32,
|
||||
h: u32,
|
||||
px: u32,
|
||||
py: u32,
|
||||
bx0: u32,
|
||||
by0: u32,
|
||||
bx1: u32,
|
||||
by1: u32,
|
||||
model_path: &str,
|
||||
) -> Option<Vec<u8>> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
let mp = CString::new(model_path).unwrap_or_default();
|
||||
if ai!(vision_object_segment_fn, img.as_ptr(), img.len(), w, h, px, py, bx0, by0, bx1, by1, mp.as_ptr(), &mut op, &mut ol) { let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() }; ai!(vision_free_buffer_fn, op, ol); Some(d) } else { None }
|
||||
if ai!(
|
||||
vision_object_segment_fn,
|
||||
img.as_ptr(),
|
||||
img.len(),
|
||||
w,
|
||||
h,
|
||||
px,
|
||||
py,
|
||||
bx0,
|
||||
by0,
|
||||
bx1,
|
||||
by1,
|
||||
mp.as_ptr(),
|
||||
&mut op,
|
||||
&mut ol
|
||||
) {
|
||||
let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() };
|
||||
ai!(vision_free_buffer_fn, op, ol);
|
||||
Some(d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn super_resolution(img: &[u8], w: u32, h: u32) -> Option<Vec<u8>> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
if ai!(vision_super_resolution_fn, img.as_ptr(), img.len(), w, h, &mut op, &mut ol) { let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() }; ai!(vision_free_buffer_fn, op, ol); Some(d) } else { None }
|
||||
if ai!(
|
||||
vision_super_resolution_fn,
|
||||
img.as_ptr(),
|
||||
img.len(),
|
||||
w,
|
||||
h,
|
||||
&mut op,
|
||||
&mut ol
|
||||
) {
|
||||
let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() };
|
||||
ai!(vision_free_buffer_fn, op, ol);
|
||||
Some(d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn inpaint(pixels: &mut [u8], w: u32, h: u32, mask: &[u8], r: i32) -> bool {
|
||||
ai!(
|
||||
vision_inpaint_fn,
|
||||
pixels.as_mut_ptr(),
|
||||
pixels.len(),
|
||||
w,
|
||||
h,
|
||||
mask.as_ptr(),
|
||||
mask.len(),
|
||||
r
|
||||
)
|
||||
}
|
||||
pub fn inpaint(pixels: &mut [u8], w: u32, h: u32, mask: &[u8], r: i32) -> bool { ai!(vision_inpaint_fn, pixels.as_mut_ptr(), pixels.len(), w, h, mask.as_ptr(), mask.len(), r) }
|
||||
pub fn build_polygon_mask(json: &str, w: u32, h: u32) -> Option<Vec<u8>> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0); let j = CString::new(json).ok()?;
|
||||
if ai!(vision_build_polygon_mask_fn, j.as_ptr(), w, h, &mut op, &mut ol) { let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() }; ai!(vision_free_buffer_fn, op, ol); Some(d) } else { None }
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
let j = CString::new(json).ok()?;
|
||||
if ai!(
|
||||
vision_build_polygon_mask_fn,
|
||||
j.as_ptr(),
|
||||
w,
|
||||
h,
|
||||
&mut op,
|
||||
&mut ol
|
||||
) {
|
||||
let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() };
|
||||
ai!(vision_free_buffer_fn, op, ol);
|
||||
Some(d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn build_stroke_mask(json: &str, sel: &[u8], r: f32, w: u32, h: u32) -> Option<Vec<u8>> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0); let j = CString::new(json).ok()?;
|
||||
if ai!(vision_build_stroke_mask_fn, j.as_ptr(), sel.as_ptr(), sel.len(), r, w, h, &mut op, &mut ol) { let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() }; ai!(vision_free_buffer_fn, op, ol); Some(d) } else { None }
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
let j = CString::new(json).ok()?;
|
||||
if ai!(
|
||||
vision_build_stroke_mask_fn,
|
||||
j.as_ptr(),
|
||||
sel.as_ptr(),
|
||||
sel.len(),
|
||||
r,
|
||||
w,
|
||||
h,
|
||||
&mut op,
|
||||
&mut ol
|
||||
) {
|
||||
let d = unsafe { std::slice::from_raw_parts(op, ol).to_vec() };
|
||||
ai!(vision_free_buffer_fn, op, ol);
|
||||
Some(d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Forward the CPU/GPU backend preference to the dynamic `hcie-vision` plugin.
|
||||
pub fn set_backend(backend: u8) {
|
||||
crate::dynamic_loader::vision_set_backend(backend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+93
-58
@@ -55,7 +55,9 @@ where
|
||||
#[no_mangle]
|
||||
pub extern "C" fn hcie_engine_create(w: u32, h: u32) -> *mut EngineHandle {
|
||||
let engine = Engine::new(w, h);
|
||||
let handle = Box::new(EngineHandle { engine: Box::new(engine) });
|
||||
let handle = Box::new(EngineHandle {
|
||||
engine: Box::new(engine),
|
||||
});
|
||||
Box::into_raw(handle)
|
||||
}
|
||||
|
||||
@@ -206,19 +208,25 @@ pub unsafe extern "C" fn hcie_engine_move_layer(
|
||||
/// Merge the active layer down into the layer below.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_merge_down(handle: *mut EngineHandle) {
|
||||
with_engine(handle, |e| { e.merge_down(); });
|
||||
with_engine(handle, |e| {
|
||||
e.merge_down();
|
||||
});
|
||||
}
|
||||
|
||||
/// Flatten all layers into a single background.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_flatten_image(handle: *mut EngineHandle) {
|
||||
with_engine(handle, |e| { e.flatten_image(); });
|
||||
with_engine(handle, |e| {
|
||||
e.flatten_image();
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear the active layer's pixels (make transparent).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_clear_active_layer(handle: *mut EngineHandle) {
|
||||
with_engine(handle, |e| { e.clear_active_layer_pixels(); });
|
||||
with_engine(handle, |e| {
|
||||
e.clear_active_layer_pixels();
|
||||
});
|
||||
}
|
||||
|
||||
/// Rename a layer.
|
||||
@@ -228,9 +236,13 @@ pub unsafe extern "C" fn hcie_engine_rename_layer(
|
||||
id: u64,
|
||||
name: *const c_char,
|
||||
) {
|
||||
if name.is_null() { return; }
|
||||
if name.is_null() {
|
||||
return;
|
||||
}
|
||||
let name_str = CStr::from_ptr(name).to_string_lossy().into_owned();
|
||||
with_engine(handle, |e| { e.rename_layer(id, &name_str); });
|
||||
with_engine(handle, |e| {
|
||||
e.rename_layer(id, &name_str);
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -282,32 +294,44 @@ pub unsafe extern "C" fn hcie_engine_end_stroke(handle: *mut EngineHandle) {
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_select_all(handle: *mut EngineHandle) {
|
||||
with_engine(handle, |e| { e.selection_all(); });
|
||||
with_engine(handle, |e| {
|
||||
e.selection_all();
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_deselect(handle: *mut EngineHandle) {
|
||||
with_engine(handle, |e| { e.selection_clear(); });
|
||||
with_engine(handle, |e| {
|
||||
e.selection_clear();
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_invert_selection(handle: *mut EngineHandle) {
|
||||
with_engine(handle, |e| { e.selection_invert(); });
|
||||
with_engine(handle, |e| {
|
||||
e.selection_invert();
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_grow_selection(handle: *mut EngineHandle, amount: u32) {
|
||||
with_engine(handle, |e| { e.selection_grow(amount); });
|
||||
with_engine(handle, |e| {
|
||||
e.selection_grow(amount);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_shrink_selection(handle: *mut EngineHandle, amount: u32) {
|
||||
with_engine(handle, |e| { e.selection_shrink(amount); });
|
||||
with_engine(handle, |e| {
|
||||
e.selection_shrink(amount);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_feather_selection(handle: *mut EngineHandle, amount: u32) {
|
||||
with_engine(handle, |e| { e.selection_feather(amount as f32); });
|
||||
with_engine(handle, |e| {
|
||||
e.selection_feather(amount as f32);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -320,7 +344,9 @@ pub unsafe extern "C" fn hcie_engine_erode_selection(handle: *mut EngineHandle,
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_fade_selection(handle: *mut EngineHandle, amount: u32) {
|
||||
with_engine(handle, |e| { e.selection_feather(amount as f32); });
|
||||
with_engine(handle, |e| {
|
||||
e.selection_feather(amount as f32);
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -336,48 +362,49 @@ pub unsafe extern "C" fn hcie_engine_apply_filter(
|
||||
filter_name: *const c_char,
|
||||
params_json: *const c_char,
|
||||
) {
|
||||
if filter_name.is_null() { return; }
|
||||
if filter_name.is_null() {
|
||||
return;
|
||||
}
|
||||
let name = CStr::from_ptr(filter_name).to_string_lossy().into_owned();
|
||||
let params_str = if params_json.is_null() {
|
||||
"{}".to_string()
|
||||
} else {
|
||||
CStr::from_ptr(params_json).to_string_lossy().into_owned()
|
||||
};
|
||||
let params: serde_json::Value = serde_json::from_str(¶ms_str).unwrap_or(serde_json::json!({}));
|
||||
let params: serde_json::Value =
|
||||
serde_json::from_str(¶ms_str).unwrap_or(serde_json::json!({}));
|
||||
|
||||
// Internal bridge commands used by the Qt6 frontend to set brush / color
|
||||
// through the single FFI entry point until dedicated functions are added.
|
||||
with_engine(handle, |e| {
|
||||
match name.as_str() {
|
||||
"__internal_set_brush" => {
|
||||
let mut tip = e.current_tip.clone();
|
||||
if let Some(sz) = params["size"].as_f64() {
|
||||
tip.size = sz as f32;
|
||||
}
|
||||
if let Some(o) = params["opacity"].as_f64() {
|
||||
tip.opacity = o as f32;
|
||||
}
|
||||
e.set_brush_tip(tip);
|
||||
with_engine(handle, |e| match name.as_str() {
|
||||
"__internal_set_brush" => {
|
||||
let mut tip = e.current_tip.clone();
|
||||
if let Some(sz) = params["size"].as_f64() {
|
||||
tip.size = sz as f32;
|
||||
}
|
||||
"__internal_set_color" => {
|
||||
if let Some(hex) = params["hex"].as_str() {
|
||||
let hex = hex.trim_start_matches('#');
|
||||
if hex.len() >= 6 {
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
|
||||
let a = if hex.len() >= 8 {
|
||||
u8::from_str_radix(&hex[6..8], 16).unwrap_or(255)
|
||||
} else {
|
||||
255
|
||||
};
|
||||
e.set_color([r, g, b, a]);
|
||||
}
|
||||
if let Some(o) = params["opacity"].as_f64() {
|
||||
tip.opacity = o as f32;
|
||||
}
|
||||
e.set_brush_tip(tip);
|
||||
}
|
||||
"__internal_set_color" => {
|
||||
if let Some(hex) = params["hex"].as_str() {
|
||||
let hex = hex.trim_start_matches('#');
|
||||
if hex.len() >= 6 {
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
|
||||
let a = if hex.len() >= 8 {
|
||||
u8::from_str_radix(&hex[6..8], 16).unwrap_or(255)
|
||||
} else {
|
||||
255
|
||||
};
|
||||
e.set_color([r, g, b, a]);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
e.apply_filter(&name, params);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
e.apply_filter(&name, params);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -388,12 +415,16 @@ pub unsafe extern "C" fn hcie_engine_apply_filter(
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_undo(handle: *mut EngineHandle) {
|
||||
with_engine(handle, |e| { e.undo(); });
|
||||
with_engine(handle, |e| {
|
||||
e.undo();
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_redo(handle: *mut EngineHandle) {
|
||||
with_engine(handle, |e| { e.redo(); });
|
||||
with_engine(handle, |e| {
|
||||
e.redo();
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -419,7 +450,9 @@ pub unsafe extern "C" fn hcie_engine_history_len(handle: *mut EngineHandle) -> u
|
||||
/// Jump to a specific history step by index. Pass -1 for original image.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_jump_to_history(handle: *mut EngineHandle, idx: i32) {
|
||||
with_engine(handle, |e| { e.jump_to_history(idx); });
|
||||
with_engine(handle, |e| {
|
||||
e.jump_to_history(idx);
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -458,16 +491,16 @@ pub unsafe extern "C" fn hcie_engine_set_active_tool(handle: *mut EngineHandle,
|
||||
with_engine(handle, |e| {
|
||||
use crate::Tool;
|
||||
let tool = match tool_id {
|
||||
0 => Tool::Eyedropper,
|
||||
1 => Tool::Pen,
|
||||
2 => Tool::Brush,
|
||||
3 => Tool::Eraser,
|
||||
4 => Tool::Spray,
|
||||
5 => Tool::FloodFill,
|
||||
6 => Tool::MagicWand,
|
||||
7 => Tool::Select,
|
||||
8 => Tool::Lasso,
|
||||
9 => Tool::PolygonSelect,
|
||||
0 => Tool::Eyedropper,
|
||||
1 => Tool::Pen,
|
||||
2 => Tool::Brush,
|
||||
3 => Tool::Eraser,
|
||||
4 => Tool::Spray,
|
||||
5 => Tool::FloodFill,
|
||||
6 => Tool::MagicWand,
|
||||
7 => Tool::Select,
|
||||
8 => Tool::Lasso,
|
||||
9 => Tool::PolygonSelect,
|
||||
10 => Tool::Move,
|
||||
11 => Tool::Crop,
|
||||
12 => Tool::Text,
|
||||
@@ -482,7 +515,7 @@ pub unsafe extern "C" fn hcie_engine_set_active_tool(handle: *mut EngineHandle,
|
||||
21 => Tool::AiObjectRemoval,
|
||||
22 => Tool::SmartSelect,
|
||||
23 => Tool::VisionSelect,
|
||||
_ => Tool::Brush,
|
||||
_ => Tool::Brush,
|
||||
};
|
||||
e.set_tool(tool);
|
||||
});
|
||||
@@ -499,5 +532,7 @@ pub unsafe extern "C" fn hcie_engine_is_modified(handle: *mut EngineHandle) -> b
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_engine_set_modified(handle: *mut EngineHandle, modified: bool) {
|
||||
with_engine(handle, |e| { e.set_modified(modified); });
|
||||
with_engine(handle, |e| {
|
||||
e.set_modified(modified);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
//! Mutates `Engine.document`, `Engine.below_cache_dirty`, and (for structural
|
||||
//! changes) `Engine.tile_layers` / `Engine.below_cache`.
|
||||
|
||||
use hcie_protocol::{BlendMode, LayerType};
|
||||
use crate::Engine;
|
||||
use hcie_protocol::{BlendMode, LayerType};
|
||||
|
||||
impl Engine {
|
||||
/// Removes all layers and resets layer-dependent caches without marking
|
||||
@@ -102,7 +102,9 @@ impl Engine {
|
||||
if let Some(idx) = self.document.layer_index_by_id(id) {
|
||||
log::trace!(
|
||||
"[set_active_layer] switching from layer idx={} to idx={} (id={})",
|
||||
self.document.active_layer, idx, id
|
||||
self.document.active_layer,
|
||||
idx,
|
||||
id
|
||||
);
|
||||
self.document.set_active_layer(idx);
|
||||
// Active layer change invalidates the below-layer composite cache
|
||||
@@ -130,7 +132,11 @@ impl Engine {
|
||||
}
|
||||
|
||||
pub fn set_layer_visible(&mut self, id: u64, visible: bool) {
|
||||
log::trace!("[set_layer_visible] ===== START id={} visible={} =====", id, visible);
|
||||
log::trace!(
|
||||
"[set_layer_visible] ===== START id={} visible={} =====",
|
||||
id,
|
||||
visible
|
||||
);
|
||||
log::trace!(
|
||||
"[set_layer_visible] BEFORE: active_layer={}, composite_dirty={}, dirty_bounds={:?}, below_cache={}, below_cache_active_idx={:?}, tile_layers_len={}",
|
||||
self.document.active_layer,
|
||||
@@ -163,7 +169,11 @@ impl Engine {
|
||||
);
|
||||
for (i, tl) in self.tile_layers.iter().enumerate() {
|
||||
let tile_count = tl.as_ref().map(|t| t.tile_count()).unwrap_or(0);
|
||||
log::trace!("[set_layer_visible] tile_layers[{}] tile_count={}", i, tile_count);
|
||||
log::trace!(
|
||||
"[set_layer_visible] tile_layers[{}] tile_count={}",
|
||||
i,
|
||||
tile_count
|
||||
);
|
||||
}
|
||||
log::trace!("[set_layer_visible] ===== END id={} =====", id);
|
||||
} else {
|
||||
@@ -191,14 +201,19 @@ impl Engine {
|
||||
/// Group and Mask layers are not editable because they do not own pixel
|
||||
/// buffers. Locked layers are also not editable.
|
||||
pub fn is_layer_editable(&self, id: u64) -> bool {
|
||||
let Some(layer) = self.document.get_layer_by_id(id) else { return false };
|
||||
if layer.locked { return false; }
|
||||
let Some(layer) = self.document.get_layer_by_id(id) else {
|
||||
return false;
|
||||
};
|
||||
if layer.locked {
|
||||
return false;
|
||||
}
|
||||
!matches!(layer.layer_type, LayerType::Group | LayerType::Mask)
|
||||
}
|
||||
|
||||
/// Returns whether the currently active layer can receive pixel edits.
|
||||
pub fn active_layer_is_editable(&self) -> bool {
|
||||
self.document.active_layer()
|
||||
self.document
|
||||
.active_layer()
|
||||
.map(|l| self.is_layer_editable(l.id))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -292,10 +307,18 @@ impl Engine {
|
||||
self.below_cache_dirty = true;
|
||||
}
|
||||
|
||||
pub fn can_undo(&self) -> bool { self.document.can_undo() }
|
||||
pub fn can_redo(&self) -> bool { self.document.can_redo() }
|
||||
pub fn history_len(&self) -> usize { self.document.history_len() }
|
||||
pub fn history_current(&self) -> i32 { self.document.history_current() }
|
||||
pub fn can_undo(&self) -> bool {
|
||||
self.document.can_undo()
|
||||
}
|
||||
pub fn can_redo(&self) -> bool {
|
||||
self.document.can_redo()
|
||||
}
|
||||
pub fn history_len(&self) -> usize {
|
||||
self.document.history_len()
|
||||
}
|
||||
pub fn history_current(&self) -> i32 {
|
||||
self.document.history_current()
|
||||
}
|
||||
pub fn history_description(&self, idx: usize) -> Option<String> {
|
||||
self.document.history_description(idx)
|
||||
}
|
||||
@@ -354,7 +377,8 @@ impl Engine {
|
||||
|
||||
// Push undo snapshot for the below layer
|
||||
if let Some(idx) = self.document.layer_index_by_id(below_id) {
|
||||
self.document.push_draw_snapshot(idx, before_pixels, None, "Merge Down".to_string());
|
||||
self.document
|
||||
.push_draw_snapshot(idx, before_pixels, None, "Merge Down".to_string());
|
||||
}
|
||||
|
||||
self.document.composite_dirty = true;
|
||||
|
||||
+1471
-416
File diff suppressed because it is too large
Load Diff
@@ -21,10 +21,10 @@
|
||||
//! and dirty flags. Depends on `hcie_tile::TiledLayer`, `hcie_fx`, and the
|
||||
//! dynamic `tiled::composite_tiled_into` / `composite_layers` compositors.
|
||||
|
||||
use hcie_tile::TiledLayer;
|
||||
use crate::Engine;
|
||||
use crate::dynamic_loader::{composite_layers, tiled};
|
||||
use crate::dynamic_loader::vector::render_vector_shapes;
|
||||
use crate::dynamic_loader::{composite_layers, tiled};
|
||||
use crate::Engine;
|
||||
use hcie_tile::TiledLayer;
|
||||
|
||||
impl Engine {
|
||||
/// **Purpose:**
|
||||
@@ -62,7 +62,13 @@ impl Engine {
|
||||
if output.len() >= 4 { [output[0], output[1], output[2], output[3]] } else { [0; 4] }
|
||||
);
|
||||
for (i, l) in self.document.layers.iter().enumerate() {
|
||||
let opaque_count = l.pixels.iter().skip(3).step_by(4).filter(|&&a| a > 0).count();
|
||||
let opaque_count = l
|
||||
.pixels
|
||||
.iter()
|
||||
.skip(3)
|
||||
.step_by(4)
|
||||
.filter(|&&a| a > 0)
|
||||
.count();
|
||||
let total = l.pixels.len() / 4;
|
||||
log::trace!(
|
||||
"[get_composite_pixels] LAYER CONTENT: layer[{}] id={} name='{}' visible={} pixels_total={} opaque_pixels={}/{} ({}%)",
|
||||
@@ -100,7 +106,8 @@ impl Engine {
|
||||
}
|
||||
log::trace!(
|
||||
"[render_composite_region] ===== START composite_dirty={}, dirty_bounds={:?} =====",
|
||||
has_dirty, dirty
|
||||
has_dirty,
|
||||
dirty
|
||||
);
|
||||
let w = self.document.canvas_width;
|
||||
let h = self.document.canvas_height;
|
||||
@@ -109,15 +116,17 @@ impl Engine {
|
||||
self.apply_effects_and_sync_tiles();
|
||||
|
||||
let (x0, y0, x1, y1) = match dirty {
|
||||
Some([x0, y0, x1, y1]) => {
|
||||
(x0.min(w), y0.min(h), x1.min(w), y1.min(h))
|
||||
}
|
||||
Some([x0, y0, x1, y1]) => (x0.min(w), y0.min(h), x1.min(w), y1.min(h)),
|
||||
None => (0, 0, w, h),
|
||||
};
|
||||
|
||||
// Now safe to borrow composite_scratch — no more mutable calls to self
|
||||
// that could touch this buffer until we return.
|
||||
if self.composite_scratch.as_ref().map_or(true, |b| b.len() != buf_size) {
|
||||
if self
|
||||
.composite_scratch
|
||||
.as_ref()
|
||||
.map_or(true, |b| b.len() != buf_size)
|
||||
{
|
||||
self.composite_scratch = Some(vec![0u8; buf_size]);
|
||||
}
|
||||
let buf = self.composite_scratch.as_mut().unwrap();
|
||||
@@ -137,7 +146,11 @@ impl Engine {
|
||||
active_idx, cache_valid, self.below_cache.is_some(), self.below_cache_active_idx, self.tile_layers.len(), x0, y0, x1, y1
|
||||
);
|
||||
for (i, l) in self.document.layers.iter().enumerate() {
|
||||
let tc = self.tile_layers.get(i).and_then(|t| t.as_ref().map(|tl| tl.tile_count())).unwrap_or(0);
|
||||
let tc = self
|
||||
.tile_layers
|
||||
.get(i)
|
||||
.and_then(|t| t.as_ref().map(|tl| tl.tile_count()))
|
||||
.unwrap_or(0);
|
||||
log::trace!(
|
||||
"[render_composite_region] layer[{}] id={} visible={} dirty={} opacity={} blend={:?} tile_count={}",
|
||||
i, l.id, l.visible, l.dirty, l.opacity, l.blend_mode, tc
|
||||
@@ -162,8 +175,12 @@ impl Engine {
|
||||
tiled::composite_tiled_into(
|
||||
&self.document.layers[active_idx..],
|
||||
&self.tile_layers[tile_start..],
|
||||
w, h,
|
||||
x0, y0, x1, y1,
|
||||
w,
|
||||
h,
|
||||
x0,
|
||||
y0,
|
||||
x1,
|
||||
y1,
|
||||
buf,
|
||||
);
|
||||
log::trace!(
|
||||
@@ -183,13 +200,20 @@ impl Engine {
|
||||
tiled::composite_tiled_into(
|
||||
&self.document.layers,
|
||||
&self.tile_layers,
|
||||
w, h,
|
||||
x0, y0, x1, y1,
|
||||
w,
|
||||
h,
|
||||
x0,
|
||||
y0,
|
||||
x1,
|
||||
y1,
|
||||
buf,
|
||||
);
|
||||
log::trace!(
|
||||
"[render_composite_region] full composite DONE, dirty_rect=[{},{},{},{}]",
|
||||
x0, y0, x1, y1
|
||||
x0,
|
||||
y0,
|
||||
x1,
|
||||
y1
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -218,7 +242,10 @@ impl Engine {
|
||||
// Pass 1: Snapshot raw pixels for any effect-bearing layer that does NOT yet
|
||||
// have a backup.
|
||||
{
|
||||
let ids_needing_backup: Vec<(u64, Vec<u8>)> = self.document.layers.iter()
|
||||
let ids_needing_backup: Vec<(u64, Vec<u8>)> = self
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|l| {
|
||||
(!l.effects.is_empty() || !l.styles.is_empty())
|
||||
&& !l.pixels.is_empty()
|
||||
@@ -238,48 +265,54 @@ impl Engine {
|
||||
// on top of clean pixels (not on effects-applied pixels). The composite
|
||||
// uses effects_cache for layers with effects, not layer.pixels.
|
||||
for layer in &mut self.document.layers {
|
||||
if layer.effects.is_empty() && layer.styles.is_empty() { continue; }
|
||||
if layer.effects.is_empty() && layer.styles.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if there are any active/enabled effects or styles
|
||||
let has_enabled = layer.effects.iter().any(|e| {
|
||||
match e {
|
||||
hcie_protocol::effects::LayerEffect::DropShadow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::InnerShadow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::OuterGlow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::InnerGlow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::BevelEmboss { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::Satin { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::ColorOverlay { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::GradientOverlay { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::PatternOverlay { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::Stroke { enabled, .. } => *enabled,
|
||||
}
|
||||
}) || layer.styles.iter().any(|s| {
|
||||
match s {
|
||||
hcie_protocol::LayerStyle::DropShadow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::InnerShadow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::OuterGlow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::InnerGlow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::BevelEmboss { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::Satin { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::ColorOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::GradientOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::PatternOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::Stroke { enabled, .. } => *enabled,
|
||||
}
|
||||
let has_enabled = layer.effects.iter().any(|e| match e {
|
||||
hcie_protocol::effects::LayerEffect::DropShadow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::InnerShadow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::OuterGlow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::InnerGlow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::BevelEmboss { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::Satin { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::ColorOverlay { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::GradientOverlay { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::PatternOverlay { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::Stroke { enabled, .. } => *enabled,
|
||||
}) || layer.styles.iter().any(|s| match s {
|
||||
hcie_protocol::LayerStyle::DropShadow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::InnerShadow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::OuterGlow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::InnerGlow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::BevelEmboss { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::Satin { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::ColorOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::GradientOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::PatternOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::Stroke { enabled, .. } => *enabled,
|
||||
});
|
||||
|
||||
if !has_enabled {
|
||||
// No active effects: clean up cached rendering and backup to restore raw performance.
|
||||
*layer.effects_cache.lock().unwrap() = None;
|
||||
self.raw_pixel_backup.remove(&layer.id);
|
||||
layer.effects_dirty.store(false, std::sync::atomic::Ordering::Release);
|
||||
layer
|
||||
.effects_dirty
|
||||
.store(false, std::sync::atomic::Ordering::Release);
|
||||
layer.dirty = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if layer.effects_dirty.load(std::sync::atomic::Ordering::Acquire) {
|
||||
log::trace!("Applying active effects/styles for layer ID {} because effects are dirty", layer.id);
|
||||
if layer
|
||||
.effects_dirty
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
{
|
||||
log::trace!(
|
||||
"Applying active effects/styles for layer ID {} because effects are dirty",
|
||||
layer.id
|
||||
);
|
||||
// DON'T restore raw pixels from backup! Restoring raw pixels from backup overwrites
|
||||
// active stroke drawing and wipes out new strokes. Since we never overwrite layer.pixels
|
||||
// with the effects output, layer.pixels already contains the clean, raw pixels.
|
||||
@@ -287,8 +320,15 @@ impl Engine {
|
||||
.filter(|e| !matches!(e, hcie_protocol::effects::LayerEffect::DropShadow { noise, .. } if *noise == -999.0))
|
||||
.map(|e| hcie_fx::protocol_to_hcie_fx_effect(e))
|
||||
.collect();
|
||||
effects.extend(layer.styles.iter().filter_map(|s| hcie_fx::layer_style_to_effect(s)));
|
||||
if effects.is_empty() { continue; }
|
||||
effects.extend(
|
||||
layer
|
||||
.styles
|
||||
.iter()
|
||||
.filter_map(|s| hcie_fx::layer_style_to_effect(s)),
|
||||
);
|
||||
if effects.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let processed = hcie_fx::apply_layer_effects(
|
||||
&layer.pixels,
|
||||
layer.width,
|
||||
@@ -301,7 +341,9 @@ impl Engine {
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
});
|
||||
layer.effects_dirty.store(false, std::sync::atomic::Ordering::Release);
|
||||
layer
|
||||
.effects_dirty
|
||||
.store(false, std::sync::atomic::Ordering::Release);
|
||||
layer.dirty = true;
|
||||
}
|
||||
}
|
||||
@@ -321,7 +363,9 @@ impl Engine {
|
||||
let db = self.document.dirty_bounds;
|
||||
let mut synced_count = 0usize;
|
||||
for (i, layer) in self.document.layers.iter().enumerate() {
|
||||
if !layer.dirty || layer.pixels.is_empty() { continue; }
|
||||
if !layer.dirty || layer.pixels.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some([dx0, dy0, dx1, dy1]) = db {
|
||||
if let Some(ref mut tl) = self.tile_layers[i] {
|
||||
if tl.width() == layer.width && tl.height() == layer.height {
|
||||
@@ -347,7 +391,11 @@ impl Engine {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.tile_layers[i] = Some(TiledLayer::from_dense(&layer.pixels, layer.width, layer.height));
|
||||
self.tile_layers[i] = Some(TiledLayer::from_dense(
|
||||
&layer.pixels,
|
||||
layer.width,
|
||||
layer.height,
|
||||
));
|
||||
log::trace!(
|
||||
"[sync_dirty_tiles] full rebuild TiledLayer for layer[{}] id={} visible={} (no dirty_bounds)",
|
||||
i, layer.id, layer.visible
|
||||
@@ -357,7 +405,12 @@ impl Engine {
|
||||
}
|
||||
self.tile_layers.truncate(count);
|
||||
if synced_count > 0 {
|
||||
log::trace!("[sync_dirty_tiles] synced {} layers, db={:?}, tile_layers_len={}", synced_count, db, self.tile_layers.len());
|
||||
log::trace!(
|
||||
"[sync_dirty_tiles] synced {} layers, db={:?}, tile_layers_len={}",
|
||||
synced_count,
|
||||
db,
|
||||
self.tile_layers.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,7 +487,10 @@ pub fn composite_from_snapshot(snapshot: CompositeSnapshot) -> Vec<u8> {
|
||||
// Build tile slices for the compositor
|
||||
let tile_slice_len = snapshot.active_layer.min(snapshot.tile_layers.len());
|
||||
let _visible_below: Vec<(usize, bool)> = snapshot.layers[..snapshot.active_layer]
|
||||
.iter().enumerate().map(|(i, l)| (i, l.visible)).collect();
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, l)| (i, l.visible))
|
||||
.collect();
|
||||
|
||||
let mut buf = vec![0u8; buf_size];
|
||||
|
||||
@@ -443,8 +499,12 @@ pub fn composite_from_snapshot(snapshot: CompositeSnapshot) -> Vec<u8> {
|
||||
tiled::composite_tiled_into(
|
||||
&snapshot.layers[..snapshot.active_layer],
|
||||
&snapshot.tile_layers[..tile_slice_len],
|
||||
w, h,
|
||||
0, 0, w, h,
|
||||
w,
|
||||
h,
|
||||
0,
|
||||
0,
|
||||
w,
|
||||
h,
|
||||
&mut buf,
|
||||
);
|
||||
}
|
||||
@@ -454,8 +514,12 @@ pub fn composite_from_snapshot(snapshot: CompositeSnapshot) -> Vec<u8> {
|
||||
tiled::composite_tiled_into(
|
||||
&snapshot.layers[snapshot.active_layer..],
|
||||
&snapshot.tile_layers[tile_start..],
|
||||
w, h,
|
||||
0, 0, w, h,
|
||||
w,
|
||||
h,
|
||||
0,
|
||||
0,
|
||||
w,
|
||||
h,
|
||||
&mut buf,
|
||||
);
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
//! engine's stroke state. Calls into `hcie-draw` and `hcie-brush-engine` via
|
||||
//! the dynamic loader.
|
||||
|
||||
use hcie_protocol::{BrushStyle, BrushTip};
|
||||
use crate::Engine;
|
||||
use crate::dynamic_loader::{
|
||||
draw_brush_stroke, draw_filled_ellipse, draw_filled_rect, draw_line, draw_specialized_stroke,
|
||||
};
|
||||
use crate::Engine;
|
||||
use hcie_protocol::{BrushStyle, BrushTip};
|
||||
|
||||
impl Engine {
|
||||
/// Interpolate a brush property along the stroke using accumulated distance.
|
||||
@@ -30,8 +30,16 @@ impl Engine {
|
||||
/// `time_enabled` flag is off or the start/end values are equal, the base
|
||||
/// value is returned unchanged. Otherwise the value is lerped from start
|
||||
/// to end and multiplied with the base.
|
||||
fn apply_time_interpolation(base: f32, start: f32, end: f32, time_enabled: bool, t: f32) -> f32 {
|
||||
if !time_enabled { return base; }
|
||||
fn apply_time_interpolation(
|
||||
base: f32,
|
||||
start: f32,
|
||||
end: f32,
|
||||
time_enabled: bool,
|
||||
t: f32,
|
||||
) -> f32 {
|
||||
if !time_enabled {
|
||||
return base;
|
||||
}
|
||||
let lerp = start + (end - start) * t.clamp(0.0, 1.0);
|
||||
base * lerp
|
||||
}
|
||||
@@ -116,7 +124,11 @@ impl Engine {
|
||||
tip.opacity,
|
||||
tip.spacing,
|
||||
self.is_eraser,
|
||||
if tip.style == BrushStyle::Sketch { Some(&mut self.sketch_history) } else { None },
|
||||
if tip.style == BrushStyle::Sketch {
|
||||
Some(&mut self.sketch_history)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
tip.spray_particle_size,
|
||||
tip.spray_density,
|
||||
mask_ref,
|
||||
@@ -164,7 +176,9 @@ impl Engine {
|
||||
);
|
||||
layer.dirty = true;
|
||||
if !layer.effects.is_empty() || !layer.styles.is_empty() {
|
||||
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
|
||||
layer
|
||||
.effects_dirty
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
let dirty_r = tip.size.max(2.0);
|
||||
if let Some((lx, ly, _)) = self.last_stroke_pos {
|
||||
@@ -184,7 +198,14 @@ impl Engine {
|
||||
|
||||
/// Pen tool stroke — draws a true anti-aliased line segment.
|
||||
/// Uses begin_stroke/end_stroke for undo.
|
||||
pub fn draw_pen_segment(&mut self, layer_id: u64, x0: f32, y0: f32, x1: f32, y1: f32, pressure: f32,
|
||||
pub fn draw_pen_segment(
|
||||
&mut self,
|
||||
layer_id: u64,
|
||||
x0: f32,
|
||||
y0: f32,
|
||||
x1: f32,
|
||||
y1: f32,
|
||||
pressure: f32,
|
||||
) {
|
||||
let tip = self.current_tip.clone();
|
||||
let color = self.apply_cyclic_color(self.current_color);
|
||||
@@ -192,11 +213,18 @@ impl Engine {
|
||||
let w = self.document.canvas_width as f32;
|
||||
let h = self.document.canvas_height as f32;
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
let px_color = [color[0], color[1], color[2], (color[3] as f32 * tip.opacity * pressure).round() as u8];
|
||||
let px_color = [
|
||||
color[0],
|
||||
color[1],
|
||||
color[2],
|
||||
(color[3] as f32 * tip.opacity * pressure).round() as u8,
|
||||
];
|
||||
draw_line(layer, x0, y0, x1, y1, px_color, tip.size, mask_ref);
|
||||
layer.dirty = true;
|
||||
if !layer.effects.is_empty() || !layer.styles.is_empty() {
|
||||
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
|
||||
layer
|
||||
.effects_dirty
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
let dirty_r = tip.size.max(2.0);
|
||||
if x0 >= 0.0 && y0 >= 0.0 && x0 < w && y0 < h {
|
||||
@@ -235,7 +263,11 @@ impl Engine {
|
||||
tip.opacity,
|
||||
tip.spacing,
|
||||
self.is_eraser,
|
||||
if tip.style == BrushStyle::Sketch { Some(&mut self.sketch_history) } else { None },
|
||||
if tip.style == BrushStyle::Sketch {
|
||||
Some(&mut self.sketch_history)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
tip.spray_particle_size,
|
||||
tip.spray_density,
|
||||
mask_ref,
|
||||
@@ -259,7 +291,9 @@ impl Engine {
|
||||
);
|
||||
}
|
||||
if !layer.effects.is_empty() || !layer.styles.is_empty() {
|
||||
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
|
||||
layer
|
||||
.effects_dirty
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
self.document.composite_dirty = true;
|
||||
self.document.modified = true;
|
||||
@@ -273,17 +307,19 @@ impl Engine {
|
||||
_ => tip.size.max(2.0),
|
||||
};
|
||||
for &(px, py, _) in points {
|
||||
if px >= 0.0 && py >= 0.0 && px < self.document.canvas_width as f32 && py < self.document.canvas_height as f32 {
|
||||
if px >= 0.0
|
||||
&& py >= 0.0
|
||||
&& px < self.document.canvas_width as f32
|
||||
&& py < self.document.canvas_height as f32
|
||||
{
|
||||
self.document.expand_dirty(px as u32, py as u32, dirty_r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(before) = before_pixels {
|
||||
self.document.push_draw_snapshot(
|
||||
layer_idx, before, None,
|
||||
"Brush Stroke".to_string(),
|
||||
);
|
||||
self.document
|
||||
.push_draw_snapshot(layer_idx, before, None, "Brush Stroke".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +338,8 @@ impl Engine {
|
||||
self.document.modified = true;
|
||||
}
|
||||
if let Some(before) = before_pixels {
|
||||
self.document.push_draw_snapshot(layer_idx, before, None, "Line".to_string());
|
||||
self.document
|
||||
.push_draw_snapshot(layer_idx, before, None, "Line".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +358,8 @@ impl Engine {
|
||||
self.document.modified = true;
|
||||
}
|
||||
if let Some(before) = before_pixels {
|
||||
self.document.push_draw_snapshot(layer_idx, before, None, "Rectangle".to_string());
|
||||
self.document
|
||||
.push_draw_snapshot(layer_idx, before, None, "Rectangle".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +381,8 @@ impl Engine {
|
||||
self.document.modified = true;
|
||||
}
|
||||
if let Some(before) = before_pixels {
|
||||
self.document.push_draw_snapshot(layer_idx, before, None, "Fill Rect".to_string());
|
||||
self.document
|
||||
.push_draw_snapshot(layer_idx, before, None, "Fill Rect".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +401,8 @@ impl Engine {
|
||||
self.document.modified = true;
|
||||
}
|
||||
if let Some(before) = before_pixels {
|
||||
self.document.push_draw_snapshot(layer_idx, before, None, "Ellipse".to_string());
|
||||
self.document
|
||||
.push_draw_snapshot(layer_idx, before, None, "Ellipse".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,12 +445,32 @@ impl Engine {
|
||||
/// Check if the given brush style needs specialized procedural rendering.
|
||||
/// Basic stamp-based styles (Round, Square, HardRound, SoftRound) return false.
|
||||
pub(crate) fn is_specialized_style(s: BrushStyle) -> bool {
|
||||
matches!(s, BrushStyle::Oil | BrushStyle::Charcoal | BrushStyle::Watercolor |
|
||||
BrushStyle::Calligraphy | BrushStyle::Marker | BrushStyle::Glow | BrushStyle::Airbrush |
|
||||
BrushStyle::Spray |
|
||||
BrushStyle::Pencil | BrushStyle::Crayon | BrushStyle::Tree | BrushStyle::Meadow |
|
||||
BrushStyle::Rock | BrushStyle::Clouds | BrushStyle::Dirt | BrushStyle::Star |
|
||||
BrushStyle::Bristle | BrushStyle::Wood | BrushStyle::Sketch | BrushStyle::Hatch |
|
||||
BrushStyle::Square | BrushStyle::WetPaint | BrushStyle::Leaf | BrushStyle::Mixer)
|
||||
matches!(
|
||||
s,
|
||||
BrushStyle::Oil
|
||||
| BrushStyle::Charcoal
|
||||
| BrushStyle::Watercolor
|
||||
| BrushStyle::Calligraphy
|
||||
| BrushStyle::Marker
|
||||
| BrushStyle::Glow
|
||||
| BrushStyle::Airbrush
|
||||
| BrushStyle::Spray
|
||||
| BrushStyle::Pencil
|
||||
| BrushStyle::Crayon
|
||||
| BrushStyle::Tree
|
||||
| BrushStyle::Meadow
|
||||
| BrushStyle::Rock
|
||||
| BrushStyle::Clouds
|
||||
| BrushStyle::Dirt
|
||||
| BrushStyle::Star
|
||||
| BrushStyle::Bristle
|
||||
| BrushStyle::Wood
|
||||
| BrushStyle::Sketch
|
||||
| BrushStyle::Hatch
|
||||
| BrushStyle::Square
|
||||
| BrushStyle::WetPaint
|
||||
| BrushStyle::Leaf
|
||||
| BrushStyle::Mixer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
//! buffers, dirty flags). They depend on `hcie_tile::TiledLayer` and the
|
||||
//! dynamic `tiled::composite_tiled_into` compositor.
|
||||
|
||||
use crate::dynamic_loader::tiled;
|
||||
use crate::Engine;
|
||||
use hcie_protocol::LayerData;
|
||||
use hcie_tile::TiledLayer;
|
||||
use crate::Engine;
|
||||
use crate::dynamic_loader::tiled;
|
||||
|
||||
/// Wrapper to send a raw pixel pointer to a background thread as a `usize`.
|
||||
///
|
||||
@@ -35,9 +35,13 @@ use crate::dynamic_loader::tiled;
|
||||
struct SendPtr(usize);
|
||||
impl SendPtr {
|
||||
#[inline]
|
||||
fn new(p: *const u8) -> Self { Self(p as usize) }
|
||||
fn new(p: *const u8) -> Self {
|
||||
Self(p as usize)
|
||||
}
|
||||
#[inline]
|
||||
fn as_ptr(&self) -> *const u8 { self.0 as *const u8 }
|
||||
fn as_ptr(&self) -> *const u8 {
|
||||
self.0 as *const u8
|
||||
}
|
||||
}
|
||||
// SAFETY: The usize encodes a pointer to layer.pixels which is stable for the engine lifetime.
|
||||
// The background thread only reads, never writes.
|
||||
@@ -74,7 +78,11 @@ impl Engine {
|
||||
for item in items {
|
||||
// Recycle returned before buffer back into the pool
|
||||
if let Some(buf) = item.return_before {
|
||||
if self.stroke_before_buf.as_ref().map_or(true, |b| b.len() != buf.len()) {
|
||||
if self
|
||||
.stroke_before_buf
|
||||
.as_ref()
|
||||
.map_or(true, |b| b.len() != buf.len())
|
||||
{
|
||||
self.stroke_before_buf = Some(buf);
|
||||
}
|
||||
}
|
||||
@@ -127,8 +135,8 @@ impl Engine {
|
||||
self.last_stroke_pos = Some((x, y, 1.0));
|
||||
self.sketch_history.clear();
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
let layer_pixels = (layer.width * layer.height * 4) as usize; // RGBA byte size
|
||||
let layer_size = (layer.width * layer.height) as usize; // pixel count (for mask)
|
||||
let layer_pixels = (layer.width * layer.height * 4) as usize; // RGBA byte size
|
||||
let layer_size = (layer.width * layer.height) as usize; // pixel count (for mask)
|
||||
|
||||
// Pool active_stroke_mask: reuse buffer if size matches, otherwise allocate.
|
||||
// This avoids a ~16MB allocation per stroke on a 4K canvas.
|
||||
@@ -211,8 +219,8 @@ impl Engine {
|
||||
if let Some(idx) = self.document.layer_index_by_id(layer_id) {
|
||||
let layer = &mut self.document.layers[idx];
|
||||
let lw = layer.width;
|
||||
let layer_pixels = (layer.width * layer.height * 4) as usize; // RGBA byte size
|
||||
// Restore layer effects from backup so they are re-composited correctly
|
||||
let layer_pixels = (layer.width * layer.height * 4) as usize; // RGBA byte size
|
||||
// Restore layer effects from backup so they are re-composited correctly
|
||||
if let Some(backup) = self.stroke_effects_backup.take() {
|
||||
layer.effects = backup;
|
||||
}
|
||||
@@ -221,12 +229,18 @@ impl Engine {
|
||||
}
|
||||
|
||||
if !layer.effects.is_empty() || !layer.styles.is_empty() {
|
||||
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
|
||||
layer
|
||||
.effects_dirty
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
self.document.composite_dirty = true;
|
||||
}
|
||||
|
||||
// Take the pooled before buffer (no allocation)
|
||||
log::debug!("[end_stroke] layer_pixels={}, stroke_before_buf={:?}", layer_pixels, self.stroke_before_buf.as_ref().map(|b| b.len()));
|
||||
log::debug!(
|
||||
"[end_stroke] layer_pixels={}, stroke_before_buf={:?}",
|
||||
layer_pixels,
|
||||
self.stroke_before_buf.as_ref().map(|b| b.len())
|
||||
);
|
||||
let before = match self.stroke_before_buf.take() {
|
||||
Some(buf) if buf.len() == layer_pixels => buf,
|
||||
_ => vec![0u8; layer_pixels],
|
||||
@@ -252,7 +266,8 @@ impl Engine {
|
||||
let t_start = std::time::Instant::now();
|
||||
// SAFETY: after_ptr points to layer.pixels which is valid for the
|
||||
// engine lifetime. No mutations happen between end_stroke() and next begin_stroke().
|
||||
let after_slice = unsafe { std::slice::from_raw_parts(after_ptr.as_ptr(), after_len) };
|
||||
let after_slice =
|
||||
unsafe { std::slice::from_raw_parts(after_ptr.as_ptr(), after_len) };
|
||||
let mut item = PendingHistoryItem {
|
||||
layer_idx: idx,
|
||||
before_pixels: Vec::new(),
|
||||
@@ -291,7 +306,10 @@ impl Engine {
|
||||
item.return_before = Some(before);
|
||||
pending_history.lock().unwrap().push(item);
|
||||
}
|
||||
log::trace!("[end_stroke_async] Background snapshot comparison took {}ms", t_start.elapsed().as_millis());
|
||||
log::trace!(
|
||||
"[end_stroke_async] Background snapshot comparison took {}ms",
|
||||
t_start.elapsed().as_millis()
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -302,7 +320,10 @@ impl Engine {
|
||||
// Copy into owned memory for PendingHistoryItem.
|
||||
item.after_pixels = after_slice.to_vec();
|
||||
pending_history.lock().unwrap().push(item);
|
||||
log::trace!("[end_stroke_async] Background fallback snapshot comparison took {}ms", t_start.elapsed().as_millis());
|
||||
log::trace!(
|
||||
"[end_stroke_async] Background fallback snapshot comparison took {}ms",
|
||||
t_start.elapsed().as_millis()
|
||||
);
|
||||
});
|
||||
|
||||
// Stroke changed layer.pixels — invalidate effects backup so
|
||||
@@ -364,7 +385,10 @@ impl Engine {
|
||||
let can_reuse = !self.below_cache_dirty
|
||||
&& self.below_cache_active_idx == Some(active_idx)
|
||||
&& active_idx > 0
|
||||
&& self.below_cache.as_ref().map_or(false, |b| b.len() == buf_size);
|
||||
&& self
|
||||
.below_cache
|
||||
.as_ref()
|
||||
.map_or(false, |b| b.len() == buf_size);
|
||||
|
||||
log::trace!(
|
||||
"[begin_stroke] below_cache state: active_idx={}, reuse={}, dirty={}, existing_cache={}, existing_active_idx={:?}",
|
||||
@@ -372,7 +396,10 @@ impl Engine {
|
||||
);
|
||||
|
||||
if can_reuse {
|
||||
log::trace!("[begin_stroke] REUSING below_cache for active_idx={}", active_idx);
|
||||
log::trace!(
|
||||
"[begin_stroke] REUSING below_cache for active_idx={}",
|
||||
active_idx
|
||||
);
|
||||
self.below_cache_dirty = false;
|
||||
return;
|
||||
}
|
||||
@@ -384,13 +411,18 @@ impl Engine {
|
||||
}
|
||||
let mut tiles_built = 0usize;
|
||||
for (ti, tl) in self.document.layers.iter().enumerate() {
|
||||
if ti >= active_idx { break; }
|
||||
if ti >= active_idx {
|
||||
break;
|
||||
}
|
||||
if !tl.pixels.is_empty() && self.tile_layers[ti].is_none() {
|
||||
self.tile_layers[ti] = Some(TiledLayer::from_dense(
|
||||
&tl.pixels, tl.width, tl.height,
|
||||
));
|
||||
self.tile_layers[ti] =
|
||||
Some(TiledLayer::from_dense(&tl.pixels, tl.width, tl.height));
|
||||
tiles_built += 1;
|
||||
log::trace!("[begin_stroke] built tile for below layer[{}] id={}", ti, tl.id);
|
||||
log::trace!(
|
||||
"[begin_stroke] built tile for below layer[{}] id={}",
|
||||
ti,
|
||||
tl.id
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut cache = match self.below_cache.take() {
|
||||
@@ -402,7 +434,10 @@ impl Engine {
|
||||
};
|
||||
let tile_slice_len = active_idx.min(self.tile_layers.len());
|
||||
let visible_below: Vec<(usize, bool)> = self.document.layers[..active_idx]
|
||||
.iter().enumerate().map(|(i, l)| (i, l.visible)).collect();
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, l)| (i, l.visible))
|
||||
.collect();
|
||||
log::trace!(
|
||||
"[begin_stroke] compositing below_cache: {} below_layers, tile_slice_len={}, visible_below={:?}, tiles_built={}",
|
||||
active_idx, tile_slice_len, visible_below, tiles_built
|
||||
@@ -410,13 +445,20 @@ impl Engine {
|
||||
tiled::composite_tiled_into(
|
||||
&self.document.layers[..active_idx],
|
||||
&self.tile_layers[..tile_slice_len],
|
||||
cw, ch, 0, 0, cw, ch,
|
||||
cw,
|
||||
ch,
|
||||
0,
|
||||
0,
|
||||
cw,
|
||||
ch,
|
||||
&mut cache,
|
||||
);
|
||||
let non_zero = cache.iter().filter(|&&b| b != 0).count();
|
||||
log::trace!(
|
||||
"[begin_stroke] below_cache built: {} bytes, non-zero bytes={}, active_idx={}",
|
||||
cache.len(), non_zero, active_idx
|
||||
cache.len(),
|
||||
non_zero,
|
||||
active_idx
|
||||
);
|
||||
self.below_cache = Some(cache);
|
||||
self.below_cache_active_idx = Some(active_idx);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use hcie_engine_api::{Engine, BrushTip, BrushStyle};
|
||||
use hcie_engine_api::{BrushStyle, BrushTip, Engine};
|
||||
|
||||
#[test]
|
||||
fn bitmap_brush_stamp_shape() {
|
||||
let mut engine = Engine::new(64, 64);
|
||||
// 4x4 solid white square bitmap stamp
|
||||
let mut pixels = vec![0u8; 16];
|
||||
for i in 0..16 { pixels[i] = 255; }
|
||||
for i in 0..16 {
|
||||
pixels[i] = 255;
|
||||
}
|
||||
let brush = BrushTip {
|
||||
style: BrushStyle::Bitmap,
|
||||
size: 8.0,
|
||||
@@ -25,25 +27,46 @@ fn bitmap_brush_stamp_shape() {
|
||||
|
||||
// Check that the painted area is roughly 4x4, not a circular blob.
|
||||
// Count alpha > 0 pixels and bounding box.
|
||||
let mut min_x = 63; let mut max_x = 0; let mut min_y = 63; let mut max_y = 0;
|
||||
let mut min_x = 63;
|
||||
let mut max_x = 0;
|
||||
let mut min_y = 63;
|
||||
let mut max_y = 0;
|
||||
let mut count = 0;
|
||||
for y in 0..64 {
|
||||
for x in 0..64 {
|
||||
let a = output[(y*64+x)*4+3];
|
||||
let a = output[(y * 64 + x) * 4 + 3];
|
||||
if a > 0 {
|
||||
count += 1;
|
||||
if x < min_x { min_x = x; }
|
||||
if x > max_x { max_x = x; }
|
||||
if y < min_y { min_y = y; }
|
||||
if y > max_y { max_y = y; }
|
||||
if x < min_x {
|
||||
min_x = x;
|
||||
}
|
||||
if x > max_x {
|
||||
max_x = x;
|
||||
}
|
||||
if y < min_y {
|
||||
min_y = y;
|
||||
}
|
||||
if y > max_y {
|
||||
max_y = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let w = (max_x - min_x + 1) as i32;
|
||||
let h = (max_y - min_y + 1) as i32;
|
||||
// Should fit inside ~8x8 because size=8 and stamp 4x4 scaled up
|
||||
assert!(w <= 8 && h <= 8, "bitmap dab bounding box {}x{} too large for 4x4 stamp", w, h);
|
||||
assert!(
|
||||
w <= 8 && h <= 8,
|
||||
"bitmap dab bounding box {}x{} too large for 4x4 stamp",
|
||||
w,
|
||||
h
|
||||
);
|
||||
assert!(count >= 8, "bitmap dab should paint some pixels");
|
||||
// Square-ish: aspect ratio not too far from 1 (was very low for circle vs square?)
|
||||
assert!(w.abs_diff(h) <= 2, "bitmap dab bounding box should be roughly square, got {}x{}", w, h);
|
||||
assert!(
|
||||
w.abs_diff(h) <= 2,
|
||||
"bitmap dab bounding box should be roughly square, got {}x{}",
|
||||
w,
|
||||
h
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! public engine API and saves the result as a PNG. Used for manual inspection
|
||||
//! and regression detection.
|
||||
|
||||
use hcie_engine_api::{Engine, BrushTip, BrushStyle};
|
||||
use hcie_engine_api::{BrushStyle, BrushTip, Engine};
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual visual check: run with --ignored --test leaves_check and inspect target/leaves_check.png"]
|
||||
@@ -44,11 +44,16 @@ fn leaves_brush_visual_check() {
|
||||
let pixels = engine.get_composite_pixels();
|
||||
let mut output_layer = hcie_protocol::Layer::new_transparent("", 512, 512);
|
||||
output_layer.pixels = pixels.clone();
|
||||
let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().join("target");
|
||||
let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("target");
|
||||
std::fs::create_dir_all(&out_dir).unwrap();
|
||||
let path = out_dir.join("leaves_check.png");
|
||||
hcie_engine_api::dynamic_loader::save_image(&output_layer, &path, "png").expect("save png");
|
||||
|
||||
let has_color = pixels.chunks_exact(4).any(|p| p[3] > 0 && (p[0] != 255 || p[1] != 255 || p[2] != 255));
|
||||
let has_color = pixels
|
||||
.chunks_exact(4)
|
||||
.any(|p| p[3] > 0 && (p[0] != 255 || p[1] != 255 || p[2] != 255));
|
||||
assert!(has_color, "Leaves check produced no colored pixels");
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! 3. Draw multiple short strokes at different positions.
|
||||
//! 4. Save the composited pixels to `target/meadow_check.png`.
|
||||
|
||||
use hcie_engine_api::{Engine, BrushTip, BrushStyle};
|
||||
use hcie_engine_api::{BrushStyle, BrushTip, Engine};
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual visual check: run with --ignored --test meadow_check and inspect target/meadow_check.png"]
|
||||
@@ -60,12 +60,17 @@ fn meadow_brush_visual_check() {
|
||||
let pixels = engine.get_composite_pixels();
|
||||
let mut output_layer = hcie_protocol::Layer::new_transparent("", 512, 512);
|
||||
output_layer.pixels = pixels.clone();
|
||||
let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().join("target");
|
||||
let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("target");
|
||||
std::fs::create_dir_all(&out_dir).unwrap();
|
||||
let path = out_dir.join("meadow_check.png");
|
||||
hcie_engine_api::dynamic_loader::save_image(&output_layer, &path, "png").expect("save png");
|
||||
|
||||
// Basic sanity: output should contain non-white, non-transparent pixels.
|
||||
let has_color = pixels.chunks_exact(4).any(|p| p[3] > 0 && (p[0] != 255 || p[1] != 255 || p[2] != 255));
|
||||
let has_color = pixels
|
||||
.chunks_exact(4)
|
||||
.any(|p| p[3] > 0 && (p[0] != 255 || p[1] != 255 || p[2] != 255));
|
||||
assert!(has_color, "Meadow check produced no colored pixels");
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
//! - Allocates ~400MB of pixel/tile/mask buffers.
|
||||
//! - Uses `std::time::Instant` for measurement; results depend on the host CPU.
|
||||
|
||||
use hcie_engine_api::{Engine, BrushTip, BrushStyle};
|
||||
use hcie_engine_api::{BrushStyle, BrushTip, Engine};
|
||||
|
||||
const W: u32 = 3840;
|
||||
const H: u32 = 2160;
|
||||
@@ -112,18 +112,29 @@ fn benchmark_4k_stroke_on_multilayer_document() {
|
||||
let total_us = total_start.elapsed().as_micros() as f64;
|
||||
|
||||
let avg_segment_us = segment_times.iter().sum::<f64>() / segment_times.len().max(1) as f64;
|
||||
let avg_composite_us = composite_times.iter().sum::<f64>() / composite_times.len().max(1) as f64;
|
||||
let avg_composite_us =
|
||||
composite_times.iter().sum::<f64>() / composite_times.len().max(1) as f64;
|
||||
let max_segment_us = segment_times.iter().copied().fold(0.0, f64::max);
|
||||
let max_composite_us = composite_times.iter().copied().fold(0.0, f64::max);
|
||||
|
||||
println!("\n=== 4K Multi-Layer Stroke Benchmark ===");
|
||||
println!("Canvas: {}x{}, Layers: {}, Segments: {}", W, H, LAYERS, SEGMENTS);
|
||||
println!(
|
||||
"Canvas: {}x{}, Layers: {}, Segments: {}",
|
||||
W, H, LAYERS, SEGMENTS
|
||||
);
|
||||
println!("Average stroke_to: {:>8.1} us", avg_segment_us);
|
||||
println!("Max stroke_to: {:>8.1} us", max_segment_us);
|
||||
println!("Average composite: {:>8.1} us", avg_composite_us);
|
||||
println!("Max composite: {:>8.1} us", max_composite_us);
|
||||
println!("Total wall time: {:>8.1} us ({:.2} s)", total_us, total_us / 1_000_000.0);
|
||||
println!("Segments per second: {:>8.1}", SEGMENTS as f64 / (total_us / 1_000_000.0));
|
||||
println!(
|
||||
"Total wall time: {:>8.1} us ({:.2} s)",
|
||||
total_us,
|
||||
total_us / 1_000_000.0
|
||||
);
|
||||
println!(
|
||||
"Segments per second: {:>8.1}",
|
||||
SEGMENTS as f64 / (total_us / 1_000_000.0)
|
||||
);
|
||||
|
||||
// Soft sanity check: the engine must still report a valid top layer and the
|
||||
// document must be marked modified after painting.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! using the public engine API and saves the result as a PNG. Used for manual
|
||||
//! inspection and regression detection.
|
||||
|
||||
use hcie_engine_api::{Engine, BrushTip, BrushStyle};
|
||||
use hcie_engine_api::{BrushStyle, BrushTip, Engine};
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual visual check: run with --ignored --test stamp_floor_check and inspect target/stamp_floor_check.png"]
|
||||
@@ -46,11 +46,16 @@ fn stamp_floor_brush_visual_check() {
|
||||
let pixels = engine.get_composite_pixels();
|
||||
let mut output_layer = hcie_protocol::Layer::new_transparent("", 512, 512);
|
||||
output_layer.pixels = pixels.clone();
|
||||
let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().join("target");
|
||||
let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("target");
|
||||
std::fs::create_dir_all(&out_dir).unwrap();
|
||||
let path = out_dir.join("stamp_floor_check.png");
|
||||
hcie_engine_api::dynamic_loader::save_image(&output_layer, &path, "png").expect("save png");
|
||||
|
||||
let has_color = pixels.chunks_exact(4).any(|p| p[3] > 0 && (p[0] != 255 || p[1] != 255 || p[2] != 255));
|
||||
let has_color = pixels
|
||||
.chunks_exact(4)
|
||||
.any(|p| p[3] > 0 && (p[0] != 255 || p[1] != 255 || p[2] != 255));
|
||||
assert!(has_color, "Stamp Floor check produced no colored pixels");
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
//! - undo_after_rect_golden: validates history snapshot and restoration
|
||||
|
||||
use hcie_engine_api::Engine;
|
||||
use hcie_engine_api::{BrushTip, BrushStyle, VectorShape};
|
||||
use hcie_engine_api::{BrushStyle, BrushTip, VectorShape};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Compute SHA-256 hash of pixel buffer for golden comparison.
|
||||
@@ -46,7 +46,10 @@ fn white_canvas_empty_document() {
|
||||
for i in (0..pixels.len()).step_by(4) {
|
||||
assert_eq!(pixels[i + 3], 0, "Alpha channel should be 0 (transparent)");
|
||||
}
|
||||
assert_eq!(hash_pixels(&pixels), "5341e6b2646979a70e57653007a1f310169421ec9bdd9f1a5648f75ade005af1");
|
||||
assert_eq!(
|
||||
hash_pixels(&pixels),
|
||||
"5341e6b2646979a70e57653007a1f310169421ec9bdd9f1a5648f75ade005af1"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Green rectangle draw and composite.
|
||||
@@ -64,7 +67,10 @@ fn green_rect_draw_and_composite() {
|
||||
assert_eq!(pixels[center_idx + 1], 255);
|
||||
assert_eq!(pixels[center_idx + 2], 0);
|
||||
assert_eq!(pixels[center_idx + 3], 255);
|
||||
assert_eq!(hash_pixels(&pixels), "01b9681b08e6d9bafd568f110f0656613c7447c667fda4af9350d705dadc758a");
|
||||
assert_eq!(
|
||||
hash_pixels(&pixels),
|
||||
"01b9681b08e6d9bafd568f110f0656613c7447c667fda4af9350d705dadc758a"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Red rectangle over green rectangle.
|
||||
@@ -91,7 +97,10 @@ fn red_rect_over_green_rect() {
|
||||
assert_eq!(pixels[center_idx], 255);
|
||||
assert_eq!(pixels[center_idx + 1], 0);
|
||||
assert_eq!(pixels[center_idx + 2], 0);
|
||||
assert_eq!(hash_pixels(&pixels), "c893ae44fb82ff080e286a6625389fef843ac60e82cdb4d7dde8c7975bbae750");
|
||||
assert_eq!(
|
||||
hash_pixels(&pixels),
|
||||
"c893ae44fb82ff080e286a6625389fef843ac60e82cdb4d7dde8c7975bbae750"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Vector shape rendering.
|
||||
@@ -100,7 +109,10 @@ fn red_rect_over_green_rect() {
|
||||
fn vector_rect_golden() {
|
||||
let mut engine = Engine::new(16, 16);
|
||||
let shape = VectorShape::Rect {
|
||||
x1: 0.0, y1: 0.0, x2: 16.0, y2: 16.0,
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: 16.0,
|
||||
y2: 16.0,
|
||||
stroke: 1.0,
|
||||
color: [255, 0, 0, 255],
|
||||
fill_color: [255, 0, 0, 255],
|
||||
@@ -116,9 +128,14 @@ fn vector_rect_golden() {
|
||||
assert_eq!(pixels.len(), 16 * 16 * 4);
|
||||
|
||||
// Verify at least one red pixel (vector rect was drawn)
|
||||
let has_red = pixels.chunks(4).any(|p| p[0] == 255 && p[1] == 0 && p[2] == 0);
|
||||
let has_red = pixels
|
||||
.chunks(4)
|
||||
.any(|p| p[0] == 255 && p[1] == 0 && p[2] == 0);
|
||||
assert!(has_red, "Vector rect should contain red pixels");
|
||||
assert_eq!(hash_pixels(&pixels), "71205eb7a329a3ead670c77eee185c0fbeb612f7a2b3d6aadbe2af4f9276b60d");
|
||||
assert_eq!(
|
||||
hash_pixels(&pixels),
|
||||
"71205eb7a329a3ead670c77eee185c0fbeb612f7a2b3d6aadbe2af4f9276b60d"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Brush stroke rendering.
|
||||
@@ -144,8 +161,14 @@ fn brush_stroke_golden() {
|
||||
|
||||
// Brush stroke should produce non-white pixels
|
||||
let has_non_white = pixels.iter().any(|&p| p < 255);
|
||||
assert!(has_non_white, "Brush stroke should produce non-white pixels");
|
||||
assert_eq!(hash_pixels(&pixels), "e97f0dd89644a40cd4d16b7440576265761849c45180fd4dece66b4f709fa087");
|
||||
assert!(
|
||||
has_non_white,
|
||||
"Brush stroke should produce non-white pixels"
|
||||
);
|
||||
assert_eq!(
|
||||
hash_pixels(&pixels),
|
||||
"e97f0dd89644a40cd4d16b7440576265761849c45180fd4dece66b4f709fa087"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Invert filter.
|
||||
@@ -164,7 +187,10 @@ fn invert_filter_golden() {
|
||||
assert_eq!(pixels[idx], 155); // 255 - 100
|
||||
assert_eq!(pixels[idx + 1], 105); // 255 - 150
|
||||
assert_eq!(pixels[idx + 2], 55); // 255 - 200
|
||||
assert_eq!(hash_pixels(&pixels), "35490247d911e119aeae86afa584459b47b853262afde75f832521a738bb069f");
|
||||
assert_eq!(
|
||||
hash_pixels(&pixels),
|
||||
"35490247d911e119aeae86afa584459b47b853262afde75f832521a738bb069f"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Undo after rectangle.
|
||||
@@ -196,7 +222,10 @@ fn vector_fill_toggle_and_delete() {
|
||||
|
||||
// Create a rect with fill=false
|
||||
let shape = VectorShape::Rect {
|
||||
x1: 2.0, y1: 2.0, x2: 14.0, y2: 14.0,
|
||||
x1: 2.0,
|
||||
y1: 2.0,
|
||||
x2: 14.0,
|
||||
y2: 14.0,
|
||||
stroke: 1.0,
|
||||
color: [255, 0, 0, 255],
|
||||
fill_color: [0, 0, 255, 255],
|
||||
@@ -211,25 +240,39 @@ fn vector_fill_toggle_and_delete() {
|
||||
|
||||
// Verify fill is initially false
|
||||
let shapes = engine.active_vector_shapes().unwrap();
|
||||
assert_eq!(shapes[0].fill(), Some(false), "fill should be false initially");
|
||||
assert_eq!(
|
||||
shapes[0].fill(),
|
||||
Some(false),
|
||||
"fill should be false initially"
|
||||
);
|
||||
|
||||
// Toggle fill ON
|
||||
engine.set_vector_shape_fill(layer_id, 0, true);
|
||||
let shapes = engine.active_vector_shapes().unwrap();
|
||||
assert_eq!(shapes[0].fill(), Some(true), "fill should be true after toggle");
|
||||
assert_eq!(
|
||||
shapes[0].fill(),
|
||||
Some(true),
|
||||
"fill should be true after toggle"
|
||||
);
|
||||
|
||||
// Verify composite pixels contain blue fill
|
||||
let pixels = engine.get_composite_pixels();
|
||||
let has_blue = pixels.chunks(4).any(|p| p[2] == 255 && p[0] == 0 && p[1] == 0 && p[3] > 0);
|
||||
let has_blue = pixels
|
||||
.chunks(4)
|
||||
.any(|p| p[2] == 255 && p[0] == 0 && p[1] == 0 && p[3] > 0);
|
||||
assert!(has_blue, "should have blue fill pixels after enabling fill");
|
||||
|
||||
// Toggle fill OFF
|
||||
engine.set_vector_shape_fill(layer_id, 0, false);
|
||||
let shapes = engine.active_vector_shapes().unwrap();
|
||||
assert_eq!(shapes[0].fill(), Some(false), "fill should be false after toggling off");
|
||||
assert_eq!(
|
||||
shapes[0].fill(),
|
||||
Some(false),
|
||||
"fill should be false after toggling off"
|
||||
);
|
||||
|
||||
// Delete the shape
|
||||
engine.delete_vector_shape(layer_id, 0);
|
||||
let shapes = engine.active_vector_shapes().unwrap();
|
||||
assert!(shapes.is_empty(), "shapes should be empty after delete");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user