init
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "hcie-engine-api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
hcie-protocol = { path = "../hcie-protocol" }
|
||||
hcie-document = { path = "../hcie-document" }
|
||||
hcie-tile = { path = "../hcie-tile" }
|
||||
hcie-selection = { path = "../hcie-selection" }
|
||||
hcie-text = { path = "../hcie-text" }
|
||||
hcie-history = { path = "../hcie-history" }
|
||||
hcie-fx = { path = "../hcie-fx" }
|
||||
hcie-blend = { path = "../hcie-blend" }
|
||||
hcie-brush-engine = { path = "../hcie-brush-engine" }
|
||||
hcie-draw = { path = "../hcie-draw" }
|
||||
hcie-filter = { path = "../hcie-filter" }
|
||||
hcie-composite = { path = "../hcie-composite" }
|
||||
hcie-io = { path = "../hcie-io" }
|
||||
hcie-psd = { path = "../hcie-psd", package = "psd" }
|
||||
hcie-kra = { path = "../hcie-kra" }
|
||||
hcie-native = { path = "../hcie-native" }
|
||||
hcie-vector = { path = "../hcie-vector" }
|
||||
|
||||
libloading = "0.8"
|
||||
rand = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "bmp", "gif", "tiff"] }
|
||||
rayon = "1.10"
|
||||
log = "0.4"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib", "staticlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.23"
|
||||
proptest = "1.5"
|
||||
approx = "0.5"
|
||||
@@ -0,0 +1,223 @@
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ParamDef {
|
||||
pub default: String,
|
||||
#[allow(dead_code)]
|
||||
pub desc: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ComponentDef {
|
||||
#[serde(rename = "type")]
|
||||
pub ctype: String,
|
||||
#[serde(default)]
|
||||
pub pts: Vec<[String; 2]>,
|
||||
#[serde(default)]
|
||||
pub cx: String,
|
||||
#[serde(default)]
|
||||
pub cy: String,
|
||||
#[serde(default)]
|
||||
pub rx: String,
|
||||
#[serde(default)]
|
||||
pub ry: String,
|
||||
#[serde(default)]
|
||||
pub r: String,
|
||||
#[serde(default)]
|
||||
pub n: usize,
|
||||
#[serde(default)]
|
||||
pub fill: String,
|
||||
#[serde(default)]
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ShapeTemplateDef {
|
||||
pub name: String,
|
||||
#[allow(dead_code)]
|
||||
pub size_mm: u32,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub parameters: HashMap<String, ParamDef>,
|
||||
pub components: Vec<ComponentDef>,
|
||||
}
|
||||
|
||||
pub struct TemplateComponent {
|
||||
pub pts: Vec<[f32; 2]>,
|
||||
pub closed: bool,
|
||||
pub fill: bool,
|
||||
pub fill_color: String,
|
||||
}
|
||||
|
||||
static TEMPLATES: OnceLock<Vec<ShapeTemplateDef>> = OnceLock::new();
|
||||
|
||||
fn init_templates() -> Vec<ShapeTemplateDef> {
|
||||
let json = include_str!("../../hcie-ai/templates_db.json");
|
||||
serde_json::from_str(json).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_template_def(name: &str) -> Option<ShapeTemplateDef> {
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn search_templates(query: &str) -> Vec<ShapeTemplateDef> {
|
||||
let q_lower = query.to_lowercase();
|
||||
TEMPLATES.get_or_init(init_templates)
|
||||
.iter()
|
||||
.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> {
|
||||
let mut num_vars = HashMap::new();
|
||||
let mut str_vars = HashMap::new();
|
||||
|
||||
for (k, v) in &def.parameters {
|
||||
let val_str = provided_params.get(k).unwrap_or(&v.default);
|
||||
if let Ok(num) = val_str.parse::<f32>() {
|
||||
num_vars.insert(k.clone(), num);
|
||||
} else {
|
||||
str_vars.insert(k.clone(), val_str.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let eval = |expr: &str| -> f32 {
|
||||
if let Ok(num) = expr.parse::<f32>() { return num; }
|
||||
math_eval(expr, &num_vars).unwrap_or(0.0)
|
||||
};
|
||||
|
||||
let subst = |expr: &str| -> String {
|
||||
let mut res = expr.to_string();
|
||||
for (k, v) in &str_vars {
|
||||
res = res.replace(&format!("{{{}}}", k), v);
|
||||
}
|
||||
res
|
||||
};
|
||||
|
||||
let mut out = Vec::new();
|
||||
for c in &def.components {
|
||||
let fill_str = subst(&c.fill);
|
||||
let fill = fill_str != "none" && !fill_str.is_empty();
|
||||
|
||||
let mut pts = Vec::new();
|
||||
match c.ctype.as_str() {
|
||||
"path" => {
|
||||
for pt in &c.pts {
|
||||
pts.push([eval(&pt[0]), eval(&pt[1])]);
|
||||
}
|
||||
}
|
||||
"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) };
|
||||
pts = circle_points(cx, cy, rx_val, ry_val, c.n.max(8));
|
||||
}
|
||||
"star" => {
|
||||
let outer = eval(&c.rx);
|
||||
let inner = eval(&c.ry);
|
||||
pts = star_points(c.n.max(5), outer, inner);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if !pts.is_empty() {
|
||||
out.push(TemplateComponent { pts, closed: c.closed, fill, fill_color: fill_str });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn math_eval(expr: &str, vars: &HashMap<String, f32>) -> Option<f32> {
|
||||
let tokens = tokenize(expr);
|
||||
let mut pos = 0;
|
||||
parse_expr(&tokens, &mut pos, vars)
|
||||
}
|
||||
|
||||
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 "+-*/()".contains(c) {
|
||||
if !current.is_empty() {
|
||||
tokens.push(current.clone());
|
||||
current.clear();
|
||||
}
|
||||
tokens.push(c.to_string());
|
||||
} else {
|
||||
current.push(c);
|
||||
}
|
||||
}
|
||||
if !current.is_empty() { tokens.push(current); }
|
||||
tokens
|
||||
}
|
||||
|
||||
fn parse_expr(tokens: &[String], pos: &mut usize, vars: &HashMap<String, f32>) -> Option<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; }
|
||||
}
|
||||
Some(left)
|
||||
}
|
||||
|
||||
fn parse_term(tokens: &[String], pos: &mut usize, vars: &HashMap<String, f32>) -> Option<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; }
|
||||
}
|
||||
Some(left)
|
||||
}
|
||||
|
||||
fn parse_factor(tokens: &[String], pos: &mut usize, vars: &HashMap<String, f32>) -> Option<f32> {
|
||||
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; }
|
||||
return Some(val);
|
||||
}
|
||||
if let Ok(n) = t.parse::<f32>() {
|
||||
*pos += 1;
|
||||
return Some(n);
|
||||
}
|
||||
if let Some(v) = vars.get(t) {
|
||||
*pos += 1;
|
||||
return Some(*v);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn circle_points(cx: f32, cy: f32, rx: f32, ry: f32, n: usize) -> Vec<[f32; 2]> {
|
||||
let mut pts = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let a = i as f32 / n as f32 * std::f32::consts::TAU;
|
||||
pts.push([cx + a.cos() * rx, cy + a.sin() * ry]);
|
||||
}
|
||||
pts
|
||||
}
|
||||
|
||||
fn star_points(n: usize, outer: f32, inner: f32) -> Vec<[f32; 2]> {
|
||||
let mut pts = Vec::with_capacity(n * 2);
|
||||
for i in 0..n {
|
||||
let a1 = i as f32 / n as f32 * std::f32::consts::TAU - std::f32::consts::FRAC_PI_2;
|
||||
let a2 = (i as f32 + 0.5) / n as f32 * std::f32::consts::TAU - std::f32::consts::FRAC_PI_2;
|
||||
pts.push([a1.cos() * outer, a1.sin() * outer]);
|
||||
pts.push([a2.cos() * inner, a2.sin() * inner]);
|
||||
}
|
||||
pts
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
use std::sync::OnceLock;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::os::raw::c_char;
|
||||
use std::ffi::CString;
|
||||
use libloading::Library;
|
||||
|
||||
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> {
|
||||
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);
|
||||
let len = pixels.len().min(fl.pixels.len());
|
||||
pixels[..len].copy_from_slice(&fl.pixels[..len]);
|
||||
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_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 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]>) {
|
||||
let ct = hcie_brush_engine::BrushTip {
|
||||
style: unsafe { std::mem::transmute::<i32, hcie_brush_engine::BrushStyle>(tip.style as i32) },
|
||||
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,
|
||||
};
|
||||
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]>) {
|
||||
let bs: hcie_brush_engine::BrushStyle = unsafe { std::mem::transmute_copy(&brush_style) };
|
||||
hcie_brush_engine::draw_specialized_stroke(pixels, width, height, points, bs, size, hardness, color, opacity, spacing_ratio, is_eraser, sketch_history, spray_particle_size, spray_density, mask, stroke_mask, bg_pixels)
|
||||
}
|
||||
|
||||
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 {
|
||||
use super::*;
|
||||
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 mod svg_import {
|
||||
use super::*;
|
||||
pub fn import_svg(data: &[u8]) -> Result<Vec<hcie_protocol::Layer>, String> { hcie_io::svg_import::import_svg(data) }
|
||||
}
|
||||
|
||||
pub mod vector {
|
||||
use super::*;
|
||||
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 };
|
||||
hcie_vector::path_boolean::boolean_shapes(vop, a, b)
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// FFI — only hcie-ai and hcie-vision
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
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) };
|
||||
for base in &[PathBuf::from("plugins"), std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.join("plugins"))).unwrap_or_default()] {
|
||||
let p = base.join(&filename); if p.exists() { return Ok(p); }
|
||||
}
|
||||
if let Ok(cwd) = std::env::current_dir() { let mut c = cwd; for _ in 0..5 { let p = c.join("plugins").join(&filename); if p.exists() { return Ok(p); } if !c.pop() { break; } } }
|
||||
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)) }; }
|
||||
|
||||
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,
|
||||
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,
|
||||
ai_session_clear_fn: unsafe extern "C" fn(i64) -> bool,
|
||||
ai_session_destroy_fn: unsafe extern "C" fn(i64),
|
||||
ai_session_serialize_fn: unsafe extern "C" fn(i64, *mut *mut u8, *mut usize) -> bool,
|
||||
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_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, *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_free_buffer_fn: unsafe extern "C" fn(*mut u8, usize),
|
||||
}
|
||||
|
||||
static AI: OnceLock<AiPlugins> = OnceLock::new();
|
||||
|
||||
fn ai_plugins() -> &'static AiPlugins {
|
||||
AI.get_or_init(|| {
|
||||
let ap = find_plugin_path("hcie_ai").expect("Critical: hcie_ai plugin not found");
|
||||
let vp = find_plugin_path("hcie_vision").expect("Critical: hcie_vision plugin not found");
|
||||
unsafe {
|
||||
let la = Library::new(ap).expect("Failed hcie_ai");
|
||||
let lv = Library::new(vp).expect("Failed hcie_vision");
|
||||
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_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();
|
||||
let ai_template_names_fn = *la.get(b"hcie_ai_template_names_c\0").unwrap();
|
||||
let ai_template_get_fn = *la.get(b"hcie_ai_template_get_c\0").unwrap();
|
||||
let ai_template_search_fn = *la.get(b"hcie_ai_template_search_c\0").unwrap();
|
||||
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_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_free_buffer_fn = *lv.get(b"free_buffer_c\0").unwrap();
|
||||
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_free_buffer_fn,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
macro_rules! ai { ($fn:ident, $($a:expr),*) => { unsafe { (ai_plugins().$fn)($($a),*) } }; }
|
||||
|
||||
pub mod ai {
|
||||
use super::*;
|
||||
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()) }
|
||||
}
|
||||
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 }
|
||||
}
|
||||
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_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 } }
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
pub fn object_segment(img: &[u8], w: u32, h: u32, px: u32, py: u32, bx0: u32, by0: u32, bx1: u32, by1: u32) -> Option<Vec<u8>> {
|
||||
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
||||
if ai!(vision_object_segment_fn, img.as_ptr(), img.len(), w, h, px, py, bx0, by0, bx1, by1, &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 }
|
||||
}
|
||||
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 }
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,135 @@
|
||||
use hcie_engine_api::Engine;
|
||||
|
||||
#[test]
|
||||
fn test_engine_new() {
|
||||
let engine = Engine::new(100, 200);
|
||||
assert_eq!(engine.canvas_width(), 100);
|
||||
assert_eq!(engine.canvas_height(), 200);
|
||||
assert_eq!(engine.get_layer_count(), 1);
|
||||
assert!(!engine.is_modified(), "new engine should not be modified");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_add_layer() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
let id = engine.add_layer("Layer 1");
|
||||
assert_ne!(id, 0);
|
||||
assert_eq!(engine.get_layer_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_delete_layer() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
let id = engine.active_layer_id();
|
||||
engine.delete_layer(id);
|
||||
assert!(!engine.layer_infos().is_empty(), "should keep at least background layer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_composite_pixels() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
engine.draw_filled_rect_rgba(0, 0, 10, 10, [255, 0, 0, 255]);
|
||||
let pixels = engine.get_composite_pixels();
|
||||
assert_eq!(pixels.len(), 10 * 10 * 4);
|
||||
assert_eq!(pixels[0], 255, "R channel");
|
||||
assert_eq!(pixels[1], 0, "G channel");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_undo() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
engine.draw_filled_rect_rgba(0, 0, 10, 10, [255, 0, 0, 255]);
|
||||
assert!(engine.is_modified());
|
||||
assert!(engine.undo(), "undo should succeed after modification");
|
||||
assert!(!engine.undo(), "double undo should return false at boundary");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_undo_returns_false_at_boundary() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
assert!(!engine.undo(), "no history should return false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_tab_label() {
|
||||
let engine = Engine::new(10, 10);
|
||||
let label = engine.tab_label();
|
||||
assert!(label.contains("Untitled"), "default name should show");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_layer_info() {
|
||||
let engine = Engine::new(10, 10);
|
||||
let id = engine.active_layer_id();
|
||||
let info = engine.get_layer_info(id);
|
||||
assert!(info.is_some());
|
||||
let info = info.unwrap();
|
||||
assert_eq!(info.width, 10);
|
||||
assert_eq!(info.height, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_get_pixel() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
engine.draw_filled_rect_rgba(0, 0, 10, 10, [255, 128, 64, 255]);
|
||||
let px = engine.get_pixel(engine.active_layer_id(), 5, 5);
|
||||
assert!(px.is_some());
|
||||
let px = px.unwrap();
|
||||
assert_eq!(px[0], 255);
|
||||
assert_eq!(px[1], 128);
|
||||
assert_eq!(px[2], 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_layer_count() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
assert_eq!(engine.get_layer_count(), 1);
|
||||
engine.add_layer("new");
|
||||
assert_eq!(engine.get_layer_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_get_all_layer_ids() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
let id1 = engine.add_layer("A");
|
||||
let id2 = engine.add_layer("B");
|
||||
let ids = engine.get_all_layer_ids();
|
||||
assert!(ids.contains(&id1));
|
||||
assert!(ids.contains(&id2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_set_layer_visible() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
let id = engine.active_layer_id();
|
||||
engine.set_layer_visible(id, false);
|
||||
let info = engine.get_layer_info(id).unwrap();
|
||||
assert!(!info.visible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_set_layer_opacity() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
let id = engine.active_layer_id();
|
||||
engine.set_layer_opacity(id, 0.5);
|
||||
let info = engine.get_layer_info(id).unwrap();
|
||||
assert!((info.opacity - 0.5).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_rename_layer() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
let id = engine.active_layer_id();
|
||||
engine.rename_layer(id, "NewName");
|
||||
let info = engine.get_layer_info(id).unwrap();
|
||||
assert_eq!(info.name, "NewName");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_apply_filter() {
|
||||
let mut engine = Engine::new(10, 10);
|
||||
engine.draw_filled_rect_rgba(0, 0, 10, 10, [100, 100, 100, 255]);
|
||||
engine.apply_filter("invert", serde_json::json!({}));
|
||||
let ctx = engine.get_composite_pixels();
|
||||
assert_ne!(ctx[0], 100, "invert should change pixel values");
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
use hcie_engine_api::Engine;
|
||||
|
||||
fn count_opaque_pixels(pixels: &[u8]) -> usize {
|
||||
pixels.iter().skip(3).step_by(4).filter(|&&a| a > 0).count()
|
||||
}
|
||||
|
||||
fn has_non_black_pixel(pixels: &[u8]) -> bool {
|
||||
pixels.chunks_exact(4).any(|px| px[0] > 0 || px[1] > 0 || px[2] > 0 || px[3] > 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_visibility_toggle_bug_repro() {
|
||||
let mut engine = Engine::new(100, 100);
|
||||
|
||||
// Fill background layer (layer 0) with opaque white
|
||||
let bg_id = engine.active_layer_id();
|
||||
engine.draw_filled_rect_rgba(0, 0, 100, 100, [255, 255, 255, 255]);
|
||||
log::info!("=== AFTER FILL BG: pixels should be white ===");
|
||||
let p = engine.get_composite_pixels();
|
||||
log::info!(" composite opaque pixels: {}/{}", count_opaque_pixels(&p), 100*100);
|
||||
assert_eq!(count_opaque_pixels(&p), 100*100, "background should be fully opaque");
|
||||
|
||||
// Add a new layer on top
|
||||
let top_id = engine.add_layer("Top Layer");
|
||||
log::info!("=== AFTER ADD TOP LAYER: id={} ===", top_id);
|
||||
let p = engine.get_composite_pixels();
|
||||
log::info!(" composite opaque pixels: {}/{} (expected 10000)", count_opaque_pixels(&p), 100*100);
|
||||
// After adding layer, composite should still show the background (10000 opaque)
|
||||
|
||||
// Draw something on the top layer
|
||||
engine.draw_filled_rect_rgba(20, 20, 80, 80, [0, 0, 0, 255]);
|
||||
log::info!("=== AFTER DRAW ON TOP: black square 60x60 at (20,20) ===");
|
||||
let p = engine.get_composite_pixels();
|
||||
log::info!(" composite opaque pixels: {}/{} (expected 10000)", count_opaque_pixels(&p), 100*100);
|
||||
assert_eq!(count_opaque_pixels(&p), 100*100, "all pixels should still be opaque");
|
||||
|
||||
// Now hide the BACKGROUND layer
|
||||
log::info!("=== HIDE BACKGROUND (id={}) ===", bg_id);
|
||||
engine.set_layer_visible(bg_id, false);
|
||||
let p = engine.get_composite_pixels();
|
||||
log::info!(" composite opaque pixels: {}/{} (expected 3600 = 60*60 black square)", count_opaque_pixels(&p), 100*100);
|
||||
log::info!(" has non-black: {}", has_non_black_pixel(&p));
|
||||
// After hiding background, only the black square (3600 px) should be visible
|
||||
assert!(count_opaque_pixels(&p) > 0, "at least the drawn square should be visible");
|
||||
assert!(count_opaque_pixels(&p) <= 3600 + 100, "only drawn square should be visible, got {}", count_opaque_pixels(&p));
|
||||
|
||||
// Show background again
|
||||
log::info!("=== SHOW BACKGROUND (id={}) ===", bg_id);
|
||||
engine.set_layer_visible(bg_id, true);
|
||||
let p = engine.get_composite_pixels();
|
||||
log::info!(" composite opaque pixels: {}/{} (expected 10000)", count_opaque_pixels(&p), 100*100);
|
||||
assert_eq!(count_opaque_pixels(&p), 100*100, "all pixels should be opaque again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_visibility_toggle_with_stroke() {
|
||||
let mut engine = Engine::new(100, 100);
|
||||
|
||||
let bg_id = engine.active_layer_id();
|
||||
engine.draw_filled_rect_rgba(0, 0, 100, 100, [255, 255, 255, 255]);
|
||||
let top_id = engine.add_layer("Top Layer");
|
||||
|
||||
// Hide background
|
||||
log::info!("=== HIDE BG, THEN STROKE ON TOP ===");
|
||||
engine.set_layer_visible(bg_id, false);
|
||||
|
||||
// Begin a stroke on the top layer
|
||||
engine.begin_stroke(top_id, 50.0, 50.0);
|
||||
engine.stroke_to(top_id, 55.0, 55.0, 1.0);
|
||||
engine.stroke_to(top_id, 60.0, 60.0, 1.0);
|
||||
engine.end_stroke(top_id);
|
||||
|
||||
let p = engine.get_composite_pixels();
|
||||
let opaque = count_opaque_pixels(&p);
|
||||
log::info!(" After stroke with bg hidden: opaque pixels: {}/{}", opaque, 100*100);
|
||||
log::info!(" has non-black: {}", has_non_black_pixel(&p));
|
||||
// The stroke should be visible
|
||||
assert!(opaque > 0, "stroke should be visible even with bg hidden");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_visibility_toggle_during_stroke() {
|
||||
let mut engine = Engine::new(100, 100);
|
||||
|
||||
let bg_id = engine.active_layer_id();
|
||||
engine.draw_filled_rect_rgba(0, 0, 100, 100, [255, 255, 255, 255]);
|
||||
let top_id = engine.add_layer("Top Layer");
|
||||
|
||||
// Begin a stroke on the top layer
|
||||
log::info!("=== STROKE THEN HIDE BG DURING STROKE ===");
|
||||
engine.begin_stroke(top_id, 50.0, 50.0);
|
||||
engine.stroke_to(top_id, 55.0, 55.0, 1.0);
|
||||
|
||||
// Hide background mid-stroke
|
||||
engine.set_layer_visible(bg_id, false);
|
||||
engine.stroke_to(top_id, 60.0, 60.0, 1.0);
|
||||
engine.stroke_to(top_id, 65.0, 65.0, 1.0);
|
||||
engine.end_stroke(top_id);
|
||||
|
||||
let p = engine.get_composite_pixels();
|
||||
let opaque = count_opaque_pixels(&p);
|
||||
log::info!(" After stroke with bg hidden mid-stroke: opaque pixels: {}/{}", opaque, 100*100);
|
||||
assert!(opaque > 0, "stroke should be visible even after hiding bg mid-stroke");
|
||||
}
|
||||
Reference in New Issue
Block a user