e509def0b2
- Introduced new crates for digital brushes, dry media brushes, ink brushes, and paint brushes, each containing various presets. - Updated the menus to include "Paste Special" and "Paste as New Layer" options. - Enhanced the feature scorecard tests to validate selection texture encoding. - Added a regression gate workflow for automated testing on push and pull requests. - Implemented Git hooks for pre-commit checks to ensure code formatting and run tests before commits. - Created a settings file for local configurations.
825 lines
28 KiB
Rust
825 lines
28 KiB
Rust
#![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.
|
|
static VISION_BACKEND: AtomicU8 = AtomicU8::new(1);
|
|
|
|
pub fn map_brush_style(style: hcie_protocol::BrushStyle) -> hcie_brush_engine::BrushStyle {
|
|
match style {
|
|
hcie_protocol::BrushStyle::Round => hcie_brush_engine::BrushStyle::Round,
|
|
hcie_protocol::BrushStyle::Square => hcie_brush_engine::BrushStyle::Square,
|
|
hcie_protocol::BrushStyle::HardRound => hcie_brush_engine::BrushStyle::HardRound,
|
|
hcie_protocol::BrushStyle::SoftRound => hcie_brush_engine::BrushStyle::SoftRound,
|
|
hcie_protocol::BrushStyle::Star => hcie_brush_engine::BrushStyle::Star,
|
|
hcie_protocol::BrushStyle::Noise => hcie_brush_engine::BrushStyle::Noise,
|
|
hcie_protocol::BrushStyle::Texture => hcie_brush_engine::BrushStyle::Texture,
|
|
hcie_protocol::BrushStyle::Spray => hcie_brush_engine::BrushStyle::Spray,
|
|
hcie_protocol::BrushStyle::Pencil => hcie_brush_engine::BrushStyle::Pencil,
|
|
hcie_protocol::BrushStyle::Pen => hcie_brush_engine::BrushStyle::Pen,
|
|
hcie_protocol::BrushStyle::Calligraphy => hcie_brush_engine::BrushStyle::Calligraphy,
|
|
hcie_protocol::BrushStyle::Oil => hcie_brush_engine::BrushStyle::Oil,
|
|
hcie_protocol::BrushStyle::Charcoal => hcie_brush_engine::BrushStyle::Charcoal,
|
|
hcie_protocol::BrushStyle::Leaf => hcie_brush_engine::BrushStyle::Leaf,
|
|
hcie_protocol::BrushStyle::Rock => hcie_brush_engine::BrushStyle::Rock,
|
|
hcie_protocol::BrushStyle::Meadow => hcie_brush_engine::BrushStyle::Meadow,
|
|
hcie_protocol::BrushStyle::Wood => hcie_brush_engine::BrushStyle::Wood,
|
|
hcie_protocol::BrushStyle::Watercolor => hcie_brush_engine::BrushStyle::Watercolor,
|
|
hcie_protocol::BrushStyle::Marker => hcie_brush_engine::BrushStyle::Marker,
|
|
hcie_protocol::BrushStyle::Sketch => hcie_brush_engine::BrushStyle::Sketch,
|
|
hcie_protocol::BrushStyle::Hatch => hcie_brush_engine::BrushStyle::Hatch,
|
|
hcie_protocol::BrushStyle::Glow => hcie_brush_engine::BrushStyle::Glow,
|
|
hcie_protocol::BrushStyle::Airbrush => hcie_brush_engine::BrushStyle::Airbrush,
|
|
hcie_protocol::BrushStyle::Crayon => hcie_brush_engine::BrushStyle::Crayon,
|
|
hcie_protocol::BrushStyle::WetPaint => hcie_brush_engine::BrushStyle::WetPaint,
|
|
hcie_protocol::BrushStyle::InkPen => hcie_brush_engine::BrushStyle::InkPen,
|
|
hcie_protocol::BrushStyle::Clouds => hcie_brush_engine::BrushStyle::Clouds,
|
|
hcie_protocol::BrushStyle::Dirt => hcie_brush_engine::BrushStyle::Dirt,
|
|
hcie_protocol::BrushStyle::Tree => hcie_brush_engine::BrushStyle::Tree,
|
|
hcie_protocol::BrushStyle::Bristle => hcie_brush_engine::BrushStyle::Bristle,
|
|
hcie_protocol::BrushStyle::Mixer => hcie_brush_engine::BrushStyle::Mixer,
|
|
hcie_protocol::BrushStyle::Blender => hcie_brush_engine::BrushStyle::Blender,
|
|
hcie_protocol::BrushStyle::Default => hcie_brush_engine::BrushStyle::Round,
|
|
hcie_protocol::BrushStyle::Bitmap => hcie_brush_engine::BrushStyle::Bitmap,
|
|
}
|
|
}
|
|
|
|
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 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,
|
|
drawing_angle: tip.drawing_angle,
|
|
rotation_random: tip.rotation_random,
|
|
};
|
|
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,
|
|
jitter_amount: f32,
|
|
scatter_amount: f32,
|
|
angle: f32,
|
|
roundness: f32,
|
|
rotation_random: f32,
|
|
drawing_angle: bool,
|
|
include_first_dab: bool,
|
|
) {
|
|
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,
|
|
jitter_amount,
|
|
scatter_amount,
|
|
angle,
|
|
roundness,
|
|
rotation_random,
|
|
drawing_angle,
|
|
include_first_dab,
|
|
)
|
|
}
|
|
|
|
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 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 mod vector {
|
|
|
|
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)
|
|
};
|
|
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")))
|
|
{
|
|
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()))
|
|
{
|
|
v.push(exe_dir.clone());
|
|
// 3. Also check the deps subdirectory used by Cargo.
|
|
v.push(exe_dir.join("deps"));
|
|
// 4. If the executable is in target/<profile>, also try the sibling
|
|
// target/<other-profile> directory so debug/release builds can find
|
|
// each other during development.
|
|
if let Some(profile) = exe_dir.file_name().and_then(|s| s.to_str()) {
|
|
if let Some(target_dir) = exe_dir.parent() {
|
|
for other in &["debug", "release"] {
|
|
if *other != profile {
|
|
v.push(target_dir.join(other));
|
|
v.push(target_dir.join(other).join("deps"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 5. Walk up from the current working directory looking for plugins/ or
|
|
// target/ directories. This helps when running from the project root.
|
|
if let Ok(cwd) = std::env::current_dir() {
|
|
let mut c = cwd.clone();
|
|
for _ in 0..5 {
|
|
v.push(c.join("plugins"));
|
|
v.push(c.join("target").join("debug"));
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
v
|
|
};
|
|
for base in candidates {
|
|
let p = base.join(&filename);
|
|
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))
|
|
};
|
|
}
|
|
|
|
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,
|
|
*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),
|
|
}
|
|
|
|
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_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,
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
macro_rules! ai { ($fn:ident, $($a:expr),*) => { unsafe { (ai_plugins().$fn)($($a),*) } }; }
|
|
|
|
/// Set the vision backend preference. If `hcie-vision` is already loaded the
|
|
/// change is forwarded immediately via FFI; otherwise it is applied when the
|
|
/// plugin is first initialised.
|
|
pub fn vision_set_backend(backend: u8) {
|
|
VISION_BACKEND.store(backend, Ordering::Relaxed);
|
|
if AI.get().is_some() {
|
|
ai!(vision_set_backend_fn, backend);
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
/// # Purpose
|
|
/// Searches for AI prompt/action templates matching a specific query or tag.
|
|
///
|
|
/// # Logic & Workflow
|
|
/// 1. Converts the query string to a C-compatible null-terminated string.
|
|
/// 2. Invokes the dynamically loaded FFI function `ai_template_search_fn`.
|
|
/// 3. If successful, copies the returned raw byte slice into a Rust Vec and frees the FFI buffer.
|
|
///
|
|
/// # Arguments
|
|
/// * `query` - A search query string to filter templates.
|
|
///
|
|
/// # Returns
|
|
/// * `Some(Vec<u8>)` containing the serialized search results (JSON), or `None` if the call failed.
|
|
///
|
|
/// # Side Effects / Dependencies
|
|
/// * Relies on the dynamically loaded `hcie_ai` library.
|
|
pub fn template_search(query: &str) -> Option<Vec<u8>> {
|
|
let (mut op, mut ol) = (std::ptr::null_mut(), 0);
|
|
let q = CString::new(query).ok()?;
|
|
if ai!(ai_template_search_fn, q.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,
|
|
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
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
/// 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);
|
|
}
|
|
}
|