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

- Add `persistence.rs` for stable serialization of dock layouts without runtime identifiers.
- Introduce `preview.rs` for geometry handling of dock drop previews.
- Create `sizing.rs` to manage content-aware sizing policies and constraint solving for dock panels.
- Implement `welcome.rs` to provide a welcome surface when no documents are open, featuring New and Open actions.
This commit is contained in:
2026-07-16 22:10:22 +03:00
parent 1240d25011
commit b09795ccff
72 changed files with 11898 additions and 3115 deletions
+70 -20
View File
@@ -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
}
}
+552 -102
View File
@@ -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
View File
@@ -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(&params_str).unwrap_or(serde_json::json!({}));
let params: serde_json::Value =
serde_json::from_str(&params_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);
});
}
+36 -12
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+121 -57
View File
@@ -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,
);
+87 -27
View File
@@ -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
)
}
}
+66 -24
View File
@@ -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);
+33 -10
View File
@@ -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
);
}
+8 -3
View File
@@ -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");
}
+8 -3
View File
@@ -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");
}
+16 -5
View File
@@ -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.
+8 -3
View File
@@ -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");
}
+59 -16
View File
@@ -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");
}
}