Files
hcie-rust-v3.05/hcie-protocol/src/lib.rs
T

2795 lines
92 KiB
Rust
Raw Normal View History

2026-07-09 02:59:53 +03:00
use serde::{Deserialize, Serialize};
use std::sync::atomic::AtomicBool;
use std::sync::Mutex;
pub mod effects;
pub mod tools;
// ─────────────────────────────────────────────────────────────────────────────
// LayerType
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum LayerType {
#[default]
Raster,
Vector,
Text,
Mask,
Group,
}
// ─────────────────────────────────────────────────────────────────────────────
// BlendMode
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum BlendMode {
#[default]
Normal,
Dissolve,
// Darken group
Darken,
Multiply,
ColorBurn,
LinearBurn,
DarkerColor,
// Lighten group
Lighten,
Screen,
ColorDodge,
LinearDodge,
LighterColor,
// Contrast group
Overlay,
SoftLight,
HardLight,
VividLight,
LinearLight,
PinLight,
HardMix,
// Comparative
Difference,
Exclusion,
Subtract,
Divide,
// Color components
Hue,
Saturation,
Color,
Luminosity,
PassThrough,
}
impl From<hcie_blend::BlendMode> for BlendMode {
fn from(m: hcie_blend::BlendMode) -> Self {
match m {
hcie_blend::BlendMode::Normal => BlendMode::Normal,
hcie_blend::BlendMode::Dissolve => BlendMode::Dissolve,
hcie_blend::BlendMode::Darken => BlendMode::Darken,
hcie_blend::BlendMode::Multiply => BlendMode::Multiply,
hcie_blend::BlendMode::ColorBurn => BlendMode::ColorBurn,
hcie_blend::BlendMode::LinearBurn => BlendMode::LinearBurn,
hcie_blend::BlendMode::DarkerColor => BlendMode::DarkerColor,
hcie_blend::BlendMode::Lighten => BlendMode::Lighten,
hcie_blend::BlendMode::Screen => BlendMode::Screen,
hcie_blend::BlendMode::ColorDodge => BlendMode::ColorDodge,
hcie_blend::BlendMode::LinearDodge => BlendMode::LinearDodge,
hcie_blend::BlendMode::LighterColor => BlendMode::LighterColor,
hcie_blend::BlendMode::Overlay => BlendMode::Overlay,
hcie_blend::BlendMode::SoftLight => BlendMode::SoftLight,
hcie_blend::BlendMode::HardLight => BlendMode::HardLight,
hcie_blend::BlendMode::VividLight => BlendMode::VividLight,
hcie_blend::BlendMode::LinearLight => BlendMode::LinearLight,
hcie_blend::BlendMode::PinLight => BlendMode::PinLight,
hcie_blend::BlendMode::HardMix => BlendMode::HardMix,
hcie_blend::BlendMode::Difference => BlendMode::Difference,
hcie_blend::BlendMode::Exclusion => BlendMode::Exclusion,
hcie_blend::BlendMode::Subtract => BlendMode::Subtract,
hcie_blend::BlendMode::Divide => BlendMode::Divide,
hcie_blend::BlendMode::Hue => BlendMode::Hue,
hcie_blend::BlendMode::Saturation => BlendMode::Saturation,
hcie_blend::BlendMode::Color => BlendMode::Color,
hcie_blend::BlendMode::Luminosity => BlendMode::Luminosity,
hcie_blend::BlendMode::PassThrough => BlendMode::PassThrough,
}
}
}
impl From<BlendMode> for hcie_blend::BlendMode {
fn from(m: BlendMode) -> Self {
match m {
BlendMode::Normal => hcie_blend::BlendMode::Normal,
BlendMode::Dissolve => hcie_blend::BlendMode::Dissolve,
BlendMode::Darken => hcie_blend::BlendMode::Darken,
BlendMode::Multiply => hcie_blend::BlendMode::Multiply,
BlendMode::ColorBurn => hcie_blend::BlendMode::ColorBurn,
BlendMode::LinearBurn => hcie_blend::BlendMode::LinearBurn,
BlendMode::DarkerColor => hcie_blend::BlendMode::DarkerColor,
BlendMode::Lighten => hcie_blend::BlendMode::Lighten,
BlendMode::Screen => hcie_blend::BlendMode::Screen,
BlendMode::ColorDodge => hcie_blend::BlendMode::ColorDodge,
BlendMode::LinearDodge => hcie_blend::BlendMode::LinearDodge,
BlendMode::LighterColor => hcie_blend::BlendMode::LighterColor,
BlendMode::Overlay => hcie_blend::BlendMode::Overlay,
BlendMode::SoftLight => hcie_blend::BlendMode::SoftLight,
BlendMode::HardLight => hcie_blend::BlendMode::HardLight,
BlendMode::VividLight => hcie_blend::BlendMode::VividLight,
BlendMode::LinearLight => hcie_blend::BlendMode::LinearLight,
BlendMode::PinLight => hcie_blend::BlendMode::PinLight,
BlendMode::HardMix => hcie_blend::BlendMode::HardMix,
BlendMode::Difference => hcie_blend::BlendMode::Difference,
BlendMode::Exclusion => hcie_blend::BlendMode::Exclusion,
BlendMode::Subtract => hcie_blend::BlendMode::Subtract,
BlendMode::Divide => hcie_blend::BlendMode::Divide,
BlendMode::Hue => hcie_blend::BlendMode::Hue,
BlendMode::Saturation => hcie_blend::BlendMode::Saturation,
BlendMode::Color => hcie_blend::BlendMode::Color,
BlendMode::Luminosity => hcie_blend::BlendMode::Luminosity,
BlendMode::PassThrough => hcie_blend::BlendMode::Normal,
}
}
}
impl BlendMode {
pub const ALL: &[BlendMode] = &[
BlendMode::Normal,
BlendMode::Dissolve,
BlendMode::Darken,
BlendMode::Multiply,
BlendMode::ColorBurn,
BlendMode::LinearBurn,
BlendMode::DarkerColor,
BlendMode::Lighten,
BlendMode::Screen,
BlendMode::ColorDodge,
BlendMode::LinearDodge,
BlendMode::LighterColor,
BlendMode::Overlay,
BlendMode::SoftLight,
BlendMode::HardLight,
BlendMode::VividLight,
BlendMode::LinearLight,
BlendMode::PinLight,
BlendMode::HardMix,
BlendMode::Difference,
BlendMode::Exclusion,
BlendMode::Subtract,
BlendMode::Divide,
BlendMode::Hue,
BlendMode::Saturation,
BlendMode::Color,
BlendMode::Luminosity,
2026-07-09 02:59:53 +03:00
BlendMode::PassThrough,
];
pub fn label(self) -> &'static str {
match self {
BlendMode::Normal => "Normal",
BlendMode::Dissolve => "Dissolve",
BlendMode::Darken => "Darken",
BlendMode::Multiply => "Multiply",
BlendMode::ColorBurn => "Color Burn",
BlendMode::LinearBurn => "Linear Burn",
BlendMode::DarkerColor => "Darker Color",
BlendMode::Lighten => "Lighten",
BlendMode::Screen => "Screen",
BlendMode::ColorDodge => "Color Dodge",
BlendMode::LinearDodge => "Linear Dodge",
BlendMode::LighterColor => "Lighter Color",
BlendMode::Overlay => "Overlay",
BlendMode::SoftLight => "Soft Light",
BlendMode::HardLight => "Hard Light",
BlendMode::VividLight => "Vivid Light",
BlendMode::LinearLight => "Linear Light",
BlendMode::PinLight => "Pin Light",
BlendMode::HardMix => "Hard Mix",
BlendMode::Difference => "Difference",
BlendMode::Exclusion => "Exclusion",
BlendMode::Subtract => "Subtract",
BlendMode::Divide => "Divide",
BlendMode::Hue => "Hue",
BlendMode::Saturation => "Saturation",
BlendMode::Color => "Color",
BlendMode::Luminosity => "Luminosity",
2026-07-09 02:59:53 +03:00
BlendMode::PassThrough => "Pass Through",
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// AlignAxis
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum AlignAxis {
#[default]
Left,
Right,
Top,
Bottom,
HCenter,
VCenter,
}
impl AlignAxis {
pub const ALL: &[AlignAxis] = &[
AlignAxis::Left,
AlignAxis::Right,
AlignAxis::Top,
AlignAxis::Bottom,
AlignAxis::HCenter,
AlignAxis::VCenter,
2026-07-09 02:59:53 +03:00
];
pub fn label(self) -> &'static str {
match self {
AlignAxis::Left => "Left",
AlignAxis::Right => "Right",
AlignAxis::Top => "Top",
AlignAxis::Bottom => "Bottom",
AlignAxis::HCenter => "Center Horizontally",
AlignAxis::VCenter => "Center Vertically",
2026-07-09 02:59:53 +03:00
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// LineCap
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum LineCap {
#[default]
Round,
Square,
Arrow,
Flat,
}
impl LineCap {
pub const ALL: &[LineCap] = &[
LineCap::Round,
LineCap::Square,
LineCap::Arrow,
LineCap::Flat,
];
2026-07-09 02:59:53 +03:00
pub fn label(self) -> &'static str {
match self {
LineCap::Round => "Round",
LineCap::Square => "Square",
LineCap::Arrow => "Arrow",
LineCap::Flat => "Flat",
}
2026-07-09 02:59:53 +03:00
}
}
// ─────────────────────────────────────────────────────────────────────────────
// VectorEditHandle
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum VectorEditHandle {
#[default]
None,
Move,
TopLeft,
Top,
TopRight,
Left,
Right,
BottomLeft,
Bottom,
BottomRight,
2026-07-09 02:59:53 +03:00
Rotate,
}
// ─────────────────────────────────────────────────────────────────────────────
// VectorShape
// ─────────────────────────────────────────────────────────────────────────────
fn default_fill_color() -> [u8; 4] {
[255, 255, 255, 255]
}
fn default_opacity() -> f32 {
1.0
}
fn default_hardness() -> f32 {
1.0
}
2026-07-09 02:59:53 +03:00
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum VectorShape {
Line {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default)]
angle: f32,
#[serde(default)]
cap_start: LineCap,
#[serde(default)]
cap_end: LineCap,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
Rect {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
radius: f32,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
Circle {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
#[serde(skip_serializing)]
Arrow {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
#[serde(default)]
thick: bool,
},
#[serde(skip_serializing)]
Star {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
#[serde(default)]
points: u32,
#[serde(default)]
inner_radius: f32,
},
Polygon {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
#[serde(default)]
sides: u32,
},
#[serde(skip_serializing)]
Rhombus {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
#[serde(skip_serializing)]
Cylinder {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
#[serde(skip_serializing)]
Heart {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
#[serde(skip_serializing)]
Bubble {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
#[serde(skip_serializing)]
Gear {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
#[serde(skip_serializing)]
Cross {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
#[serde(skip_serializing)]
Crescent {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
#[serde(skip_serializing)]
Bolt {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
#[serde(skip_serializing)]
Arrow4 {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
SvgShape {
#[serde(default)]
name: String,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
kind: String,
svg: String,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default)]
angle: f32,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
FreePath {
#[serde(default)]
name: String,
pts: Vec<[f32; 2]>,
closed: bool,
stroke: f32,
color: [u8; 4],
#[serde(default = "default_fill_color")]
fill_color: [u8; 4],
fill: bool,
#[serde(default = "default_opacity")]
opacity: f32,
#[serde(default = "default_hardness")]
hardness: f32,
},
2026-07-09 02:59:53 +03:00
}
// ─────────────────────────────────────────────────────────────────────────────
// LayerData
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Clone, Serialize, Deserialize, PartialEq)]
pub enum LayerData {
Raster,
Vector {
shapes: Vec<VectorShape>,
},
2026-07-09 02:59:53 +03:00
Text {
text: String,
font: String,
size: f32,
color: [u8; 4],
x: f32,
y: f32,
#[serde(default)]
angle: f32,
#[serde(default)]
alignment: tools::TextAlignment,
#[serde(default)]
orientation: tools::TextOrientation,
#[serde(default)]
effects: Vec<tools::TextEffect>,
#[serde(default)]
offset_x: f32,
#[serde(default)]
offset_y: f32,
#[serde(default)]
unrotated_w: f32,
#[serde(default)]
unrotated_h: f32,
},
}
impl Default for LayerData {
fn default() -> Self {
LayerData::Raster
}
2026-07-09 02:59:53 +03:00
}
// ─────────────────────────────────────────────────────────────────────────────
// LayerStyle
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LayerStyle {
DropShadow {
enabled: bool,
opacity: f32,
angle: f32,
distance: f32,
spread: f32,
size: f32,
color: [u8; 4],
blend_mode: String,
},
InnerShadow {
enabled: bool,
opacity: f32,
angle: f32,
distance: f32,
spread: f32,
size: f32,
color: [u8; 4],
blend_mode: String,
},
OuterGlow {
enabled: bool,
opacity: f32,
spread: f32,
size: f32,
color: [u8; 4],
blend_mode: String,
},
InnerGlow {
enabled: bool,
opacity: f32,
spread: f32,
size: f32,
color: [u8; 4],
blend_mode: String,
},
2026-07-09 02:59:53 +03:00
BevelEmboss {
enabled: bool,
depth: f32,
size: f32,
angle: f32,
altitude: f32,
highlight_opacity: f32,
shadow_opacity: f32,
direction: String,
style: String,
technique: String,
soften: f32,
highlight_blend_mode: String,
highlight_color: [u8; 4],
shadow_blend_mode: String,
shadow_color: [u8; 4],
contour: String,
2026-07-09 02:59:53 +03:00
},
Satin {
enabled: bool,
opacity: f32,
angle: f32,
distance: f32,
size: f32,
color: [u8; 4],
invert: bool,
},
ColorOverlay {
enabled: bool,
opacity: f32,
color: [u8; 4],
blend_mode: String,
},
GradientOverlay {
enabled: bool,
opacity: f32,
blend_mode: String,
angle: f32,
scale: f32,
gradient_type: u32,
},
PatternOverlay {
enabled: bool,
opacity: f32,
blend_mode: String,
scale: f32,
pattern_name: String,
},
Stroke {
enabled: bool,
size: f32,
position: String,
opacity: f32,
color: [u8; 4],
blend_mode: String,
},
2026-07-09 02:59:53 +03:00
}
// ─────────────────────────────────────────────────────────────────────────────
// BrushStyle
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[repr(i32)]
pub enum BrushStyle {
#[default]
Round,
Square,
HardRound,
SoftRound,
Star,
Noise,
Texture,
Spray,
Pencil,
Pen,
Calligraphy,
Oil,
Charcoal,
Leaf,
Rock,
Meadow,
Wood,
Watercolor,
Marker,
Sketch,
Hatch,
Glow,
Airbrush,
Crayon,
WetPaint,
InkPen,
Clouds,
Dirt,
Tree,
Bristle,
Mixer,
Blender,
/// Default alias for the round brush — used by older presets and the GUI.
Default,
/// Raster bitmap brush — sampled or imported image data.
Bitmap,
}
// ─────────────────────────────────────────────────────────────────────────────
// BrushTip
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BrushTip {
pub style: BrushStyle,
pub size: f32,
pub opacity: f32,
pub hardness: f32,
pub spacing: f32,
pub jitter_amount: f32,
pub scatter_amount: f32,
pub angle: f32,
pub roundness: f32,
// Spray-specific (used when style == BrushStyle::Spray)
pub spray_particle_size: f32,
pub spray_density: u32,
// Bitmap-specific (used when style == BrushStyle::Bitmap)
/// Grayscale alpha mask, u8 per pixel. Empty when style != Bitmap.
pub bitmap_pixels: Vec<u8>,
pub bitmap_w: u32,
pub bitmap_h: u32,
pub flow: f32,
/// Enable per-blade/per-particle color variation for natural brushes such as Meadow.
pub color_variant: bool,
/// Amount of color variation around the base color. 0.0 = none, 1.0 = max spread.
/// Hue is shifted by ±(amount * 60°) and lightness by ±(amount * 40%).
pub variant_amount: f32,
/// Density multiplier for natural brushes (Meadow, Leaf, Dirt). 0.0 = sparse,
/// 1.0 = default, 2.0+ = very dense. Controls how many blades/leaves/strokes
/// are emitted per dab.
pub density: f32,
/// When true, the brush angle follows the stroke segment direction.
pub drawing_angle: bool,
/// Per-dab random rotation magnitude (radians). 0.0 = deterministic.
pub rotation_random: f32,
/// Enable time/distance-based interpolation of size, opacity and angle.
pub time_enabled: bool,
pub time_size_start: f32,
pub time_size_end: f32,
pub time_opacity_start: f32,
pub time_opacity_end: f32,
pub time_angle_start: f32,
pub time_angle_end: f32,
}
impl Default for BrushTip {
fn default() -> Self {
Self {
style: BrushStyle::Round,
size: 10.0,
opacity: 1.0,
hardness: 0.8,
spacing: 0.25,
jitter_amount: 0.0,
scatter_amount: 0.0,
angle: 0.0,
roundness: 1.0,
spray_particle_size: 2.0,
spray_density: 100,
bitmap_pixels: Vec::new(),
bitmap_w: 0,
bitmap_h: 0,
flow: 1.0,
color_variant: false,
variant_amount: 0.0,
density: 1.0,
drawing_angle: false,
rotation_random: 0.0,
time_enabled: false,
time_size_start: 1.0,
time_size_end: 1.0,
time_opacity_start: 1.0,
time_opacity_end: 1.0,
time_angle_start: 0.0,
time_angle_end: 0.0,
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// EngineCommand (GUI → Engine)
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EngineCommand {
AddLayer {
name: String,
},
DeleteLayer {
id: u64,
},
SelectLayer {
id: u64,
},
RenameLayer {
id: u64,
name: String,
},
SetLayerOpacity {
id: u64,
opacity: f32,
},
SetLayerVisible {
id: u64,
visible: bool,
},
SetLayerBlendMode {
id: u64,
mode: BlendMode,
},
MoveLayer {
id: u64,
new_index: usize,
},
ToggleGroupCollapsed {
id: u64,
},
SetClippingMask {
id: u64,
enabled: bool,
},
2026-07-09 02:59:53 +03:00
Undo,
Redo,
JumpToHistory {
index: i32,
},
2026-07-09 02:59:53 +03:00
SetBrushTip(BrushTip),
SetColor([u8; 4]),
/// One-shot stroke command. Prefer `BeginStroke` + `StrokeTo` + `EndStroke`
/// for interactive pointer dragging so the engine can record a single
/// undo/redo entry for the entire stroke.
DrawStroke {
points: Vec<(f32, f32, f32)>,
}, // (x, y, pressure)
2026-07-09 02:59:53 +03:00
/// Start a new interactive stroke. Snapshots the active layer for undo and
/// prepares the stroke mask / below-layer cache. The initial point is only
/// recorded; send a follow-up `StrokeTo` (or multiple) to actually draw.
BeginStroke {
x: f32,
y: f32,
},
2026-07-09 02:59:53 +03:00
/// Append sampled points to the stroke currently in progress.
/// Points are processed sequentially by `Engine::stroke_to`.
StrokeTo {
points: Vec<(f32, f32, f32)>,
},
2026-07-09 02:59:53 +03:00
/// Finish the interactive stroke and commit a single undo/redo entry.
EndStroke,
DrawLine {
x0: f32,
y0: f32,
x1: f32,
y1: f32,
},
DrawRect {
x1: f32,
y1: f32,
x2: f32,
y2: f32,
},
DrawEllipse {
cx: f32,
cy: f32,
rx: f32,
ry: f32,
},
FloodFill {
x: u32,
y: u32,
color: [u8; 4],
tolerance: u8,
},
2026-07-09 02:59:53 +03:00
CreateSelectionRect {
x1: u32,
y1: u32,
x2: u32,
y2: u32,
},
CreateSelectionEllipse {
cx: u32,
cy: u32,
rx: u32,
ry: u32,
},
CreateSelectionMagicWand {
x: u32,
y: u32,
tolerance: u8,
},
CreateSelectionPolygon {
points: Vec<[f32; 2]>,
},
SelectionLasso {
points: Vec<[f32; 2]>,
},
SelectionGrow {
px: u32,
},
SelectionShrink {
px: u32,
},
SelectionFeather {
radius: f32,
},
2026-07-09 02:59:53 +03:00
SelectionInvert,
SelectionClear,
SelectionAll,
Crop {
x: u32,
y: u32,
w: u32,
h: u32,
},
ResizeCanvas {
w: u32,
h: u32,
},
RotateLayer {
id: u64,
degrees: f32,
},
FlipLayerHorizontal {
id: u64,
},
FlipLayerVertical {
id: u64,
},
2026-07-09 02:59:53 +03:00
ApplyFilter {
filter_id: String,
params: serde_json::Value,
},
2026-07-09 02:59:53 +03:00
ResetTool,
AlignLayer {
id: u64,
axis: AlignAxis,
},
2026-07-09 02:59:53 +03:00
AddText {
text: String,
font: String,
size: f32,
x: f32,
y: f32,
color: [u8; 4],
},
2026-07-09 02:59:53 +03:00
AddVectorShape(VectorShape),
ModifyVectorShape {
layer_id: u64,
shape_idx: usize,
handle: VectorEditHandle,
dx: f32,
dy: f32,
},
SetVectorShapeColor {
layer_id: u64,
shape_idx: usize,
color: [u8; 4],
},
SetVectorShapeOpacity {
layer_id: u64,
shape_idx: usize,
opacity: f32,
},
SetVectorShapeFill {
layer_id: u64,
shape_idx: usize,
fill: bool,
},
SetVectorShapeFillColor {
layer_id: u64,
shape_idx: usize,
color: [u8; 4],
},
SetVectorShapeStroke {
layer_id: u64,
shape_idx: usize,
stroke: f32,
},
SetVectorShapeHardness {
layer_id: u64,
shape_idx: usize,
hardness: f32,
},
SetVectorShapeBounds {
layer_id: u64,
shape_idx: usize,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
},
SetVectorShapeAngle {
layer_id: u64,
shape_idx: usize,
angle: f32,
},
SetVectorShapeLineCaps {
layer_id: u64,
shape_idx: usize,
cap_start: LineCap,
cap_end: LineCap,
},
DeleteVectorShape {
layer_id: u64,
shape_idx: usize,
},
2026-07-09 02:59:53 +03:00
OpenImage(String),
SaveAs {
path: String,
format: String,
},
2026-07-09 02:59:53 +03:00
SaveNative(String),
ExportPsd(String),
ExportKra(String),
ExportLayer {
layer_id: u64,
path: String,
format: String,
},
ExportSelection {
path: String,
format: String,
},
2026-07-09 02:59:53 +03:00
SetZoom(f32),
PanCanvas(f32, f32),
ExecuteAiActions(serde_json::Value),
/// Add, update, or replace a layer style/effect on the target layer.
UpdateLayerStyle {
layer_id: u64,
style: LayerStyle,
},
2026-07-09 02:59:53 +03:00
/// Remove a layer style/effect from the target layer by discriminant index.
/// The index maps to the order of `LayerStyle` variants in this crate.
RemoveLayerStyle {
layer_id: u64,
discriminant_index: u32,
},
2026-07-09 02:59:53 +03:00
/// Merge the active layer down into the layer below it.
/// The active layer is removed and its pixels are composited onto the layer below.
MergeDown,
/// Flatten all visible layers into a single background layer.
/// Invisible layers are discarded.
FlattenImage,
}
// ─────────────────────────────────────────────────────────────────────────────
// EngineEvent (Engine → GUI)
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EngineEvent {
LayerAdded {
id: u64,
name: String,
index: usize,
},
LayerDeleted {
id: u64,
},
LayerSelected {
id: u64,
},
LayerModified {
id: u64,
},
LayerMoved {
id: u64,
new_index: usize,
},
2026-07-09 02:59:53 +03:00
AllLayersReplaced,
UndoPerformed {
current_step: i32,
},
RedoPerformed {
current_step: i32,
},
HistoryJumped {
target_step: i32,
},
2026-07-09 02:59:53 +03:00
CanvasRendered {
width: u32,
height: u32,
},
CanvasResized {
w: u32,
h: u32,
},
2026-07-09 02:59:53 +03:00
SelectionChanged,
BrushStrokeFinished,
ToolFinished,
ImageOpened {
path: String,
},
Saved {
path: String,
success: bool,
},
2026-07-09 02:59:53 +03:00
ZoomChanged(f32),
PanChanged(f32, f32),
ModifiedChanged(bool),
DocumentClosed {
id: u64,
},
DocumentSwitched {
id: u64,
},
2026-07-09 02:59:53 +03:00
AiActionCompleted {
result: String,
},
AiActionFailed {
error: String,
},
2026-07-09 02:59:53 +03:00
/// Emitted after a successful merge-down operation.
LayersMerged,
/// Emitted after a successful flatten-image operation.
ImageFlattened,
}
// ─────────────────────────────────────────────────────────────────────────────
// ModulePanel (For GUI adapter)
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone)]
pub struct ModulePanel {
pub title: String,
pub widgets: Vec<WidgetDescription>,
}
#[derive(Debug, Clone)]
pub enum WidgetDescription {
Label {
id: String,
text: String,
},
Button {
id: String,
label: String,
},
Slider {
id: String,
label: String,
min: f32,
max: f32,
value: f32,
step: f32,
},
ColorPicker {
id: String,
label: String,
rgba: [u8; 4],
},
Checkbox {
id: String,
label: String,
checked: bool,
},
Dropdown {
id: String,
label: String,
options: Vec<String>,
selected: usize,
},
2026-07-09 02:59:53 +03:00
Separator,
Group {
label: String,
children: Vec<WidgetDescription>,
},
2026-07-09 02:59:53 +03:00
}
// ─────────────────────────────────────────────────────────────────────────────
// Layer (Data-only struct, no complex methods)
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Serialize, Deserialize)]
pub struct Layer {
pub name: String,
2026-07-09 02:59:53 +03:00
pub layer_type: LayerType,
pub data: LayerData,
pub pixels: Vec<u8>,
pub width: u32,
pub height: u32,
pub visible: bool,
pub opacity: f32,
2026-07-09 02:59:53 +03:00
pub blend_mode: BlendMode,
pub locked: bool,
pub dirty: bool,
pub id: u64,
2026-07-09 02:59:53 +03:00
pub parent_id: Option<u64>,
pub styles: Vec<LayerStyle>,
2026-07-09 02:59:53 +03:00
pub clipping_mask: bool,
pub collapsed: bool,
2026-07-09 02:59:53 +03:00
pub adjustment: Option<hcie_blend::Adjustment>,
pub mask_pixels: Option<Vec<u8>>,
pub mask_bounds: Option<[i32; 4]>,
pub mask_default_color: u8,
pub curve_points: Vec<(u16, u16)>,
pub fill_opacity: f32,
/// Raw bytes of the original PSD adjustment block (`curv`, `grdm`, `hue2`, etc.),
/// if this layer was imported from a PSD. Stored as `(key, data)` so the writer can
/// pass the block through unchanged when re-saving.
#[serde(default)]
pub adjustment_raw: Option<([u8; 4], Vec<u8>)>,
/// True if this layer is a section-divider (group end marker) imported from a PSD.
#[serde(default)]
pub is_section_divider: bool,
#[serde(default)]
pub effects: Vec<effects::LayerEffect>,
/// Raw bytes of the original PSD image resources section, preserved so the writer
/// can pass non-essential Photoshop metadata (guides, print settings, paths, etc.)
/// through unchanged when re-saving a file that was imported from PSD.
#[serde(default)]
#[serde(skip)]
pub image_resources_raw: Option<Vec<u8>>,
#[serde(skip)]
pub effects_dirty: AtomicBool,
#[serde(skip)]
pub effects_cache: Mutex<Option<LayerEffects>>,
}
impl Clone for Layer {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
layer_type: self.layer_type.clone(),
data: self.data.clone(),
pixels: self.pixels.clone(),
width: self.width,
height: self.height,
visible: self.visible,
opacity: self.opacity,
blend_mode: self.blend_mode.clone(),
locked: self.locked,
dirty: self.dirty,
id: self.id,
parent_id: self.parent_id,
styles: self.styles.clone(),
clipping_mask: self.clipping_mask,
collapsed: self.collapsed,
adjustment: self.adjustment.clone(),
mask_pixels: self.mask_pixels.clone(),
mask_bounds: self.mask_bounds,
mask_default_color: self.mask_default_color,
curve_points: self.curve_points.clone(),
fill_opacity: self.fill_opacity,
adjustment_raw: self.adjustment_raw.clone(),
is_section_divider: self.is_section_divider,
effects: self.effects.clone(),
image_resources_raw: self.image_resources_raw.clone(),
effects_dirty: AtomicBool::new(
self.effects_dirty
.load(std::sync::atomic::Ordering::Relaxed),
),
2026-07-09 02:59:53 +03:00
effects_cache: Mutex::new(None),
}
}
}
impl Default for Layer {
fn default() -> Self {
Self::new_transparent("Layer", 64, 64)
}
}
impl std::fmt::Debug for Layer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Layer")
.field("name", &self.name)
.field("width", &self.width)
.field("height", &self.height)
.field("id", &self.id)
.finish()
}
}
impl Layer {
pub fn new_blank(name: impl Into<String>, width: u32, height: u32) -> Self {
let len = (width * height * 4) as usize;
let mut pixels = vec![0u8; len];
for chunk in pixels.chunks_exact_mut(4) {
chunk[0] = 255;
chunk[1] = 255;
chunk[2] = 255;
chunk[3] = 255;
2026-07-09 02:59:53 +03:00
}
Self::with_pixels(name, width, height, pixels)
}
pub fn new_transparent(name: impl Into<String>, width: u32, height: u32) -> Self {
let pixels = vec![0u8; (width * height * 4) as usize];
Self::with_pixels(name, width, height, pixels)
}
pub fn from_rgba(name: impl Into<String>, width: u32, height: u32, pixels: Vec<u8>) -> Self {
Self::with_pixels(name, width, height, pixels)
}
fn with_pixels(name: impl Into<String>, width: u32, height: u32, pixels: Vec<u8>) -> Self {
Self {
name: name.into(),
layer_type: LayerType::Raster,
data: LayerData::Raster,
pixels,
width,
height,
visible: true,
opacity: 1.0,
blend_mode: BlendMode::default(),
locked: false,
dirty: true,
id: 0,
parent_id: None,
styles: vec![],
clipping_mask: false,
collapsed: false,
adjustment: None,
mask_pixels: None,
mask_bounds: None,
mask_default_color: 0,
curve_points: vec![],
fill_opacity: 1.0,
adjustment_raw: None,
is_section_divider: false,
effects: vec![],
image_resources_raw: None,
effects_dirty: AtomicBool::new(true),
effects_cache: Mutex::new(None),
}
}
pub fn pixel_index(&self, x: u32, y: u32) -> usize {
(y * self.width + x) as usize * 4
}
pub fn get_pixel(&self, x: u32, y: u32) -> [u8; 4] {
let (lx, ly) = match &self.data {
LayerData::Text {
offset_x, offset_y, ..
} => {
2026-07-09 02:59:53 +03:00
let lx = x as i32 - *offset_x as i32;
let ly = y as i32 - *offset_y as i32;
if lx < 0 || ly < 0 {
return [0, 0, 0, 0];
}
2026-07-09 02:59:53 +03:00
(lx as u32, ly as u32)
}
_ => (x, y),
};
if lx >= self.width || ly >= self.height || self.pixels.len() < 4 {
return [0, 0, 0, 0];
}
let i = (ly * self.width + lx) as usize * 4;
if i + 3 >= self.pixels.len() {
return [0, 0, 0, 0];
}
[
self.pixels[i],
self.pixels[i + 1],
self.pixels[i + 2],
self.pixels[i + 3],
]
2026-07-09 02:59:53 +03:00
}
pub fn set_pixel(&mut self, x: u32, y: u32, rgba: [u8; 4]) {
let i = self.pixel_index(x, y);
self.pixels[i..i + 4].copy_from_slice(&rgba);
self.dirty = true;
}
pub fn get_mask_value(&self, x: u32, y: u32) -> u8 {
if let (Some(mask), Some(mb)) = (&self.mask_pixels, &self.mask_bounds) {
let (m_top, m_left, m_bottom, m_right) =
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
2026-07-09 02:59:53 +03:00
if y >= m_top && y < m_bottom && x >= m_left && x < m_right {
let mask_w = m_right - m_left;
let mask_idx = ((y - m_top) * mask_w + (x - m_left)) as usize;
mask.get(mask_idx)
.copied()
.unwrap_or(self.mask_default_color)
2026-07-09 02:59:53 +03:00
} else {
self.mask_default_color
}
} else {
255
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// LayerEffects (internal cache)
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Clone, Debug)]
pub struct LayerEffects {
pub rendered: Vec<u8>,
pub width: u32,
pub height: u32,
}
// ─────────────────────────────────────────────────────────────────────────────
// DocumentInfo / LayerInfo
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentInfo {
pub name: String,
pub canvas_width: u32,
pub canvas_height: u32,
pub layer_count: usize,
pub active_layer_id: u64,
pub modified: bool,
pub zoom: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayerInfo {
pub id: u64,
pub name: String,
pub layer_type: LayerType,
pub width: u32,
pub height: u32,
pub visible: bool,
pub opacity: f32,
pub blend_mode: BlendMode,
pub locked: bool,
pub parent_id: Option<u64>,
pub clipping_mask: bool,
pub collapsed: bool,
pub has_vector_shapes: bool,
pub shape_count: usize,
pub has_effects: bool,
}
// ─────────────────────────────────────────────────────────────────────────────
// Global Constants
// ─────────────────────────────────────────────────────────────────────────────
pub const MAX_UNDO_STEPS_DEFAULT: usize = 128;
pub const ZOOM_DEFAULT: f32 = 1.0;
pub const ZOOM_MIN: f32 = 0.01;
pub const ZOOM_MAX: f32 = 64.0;
pub const TILE_SIZE: u32 = 256;
// ─────────────────────────────────────────────────────────────────────────────
// Pre-defined brush presets (data only)
// ─────────────────────────────────────────────────────────────────────────────
pub mod presets {
use super::BrushStyle;
use super::BrushTip;
2026-07-09 02:59:53 +03:00
pub fn round_soft(size: f32) -> BrushTip {
BrushTip {
style: BrushStyle::Round,
size,
opacity: 1.0,
hardness: 0.0,
spacing: 0.25,
jitter_amount: 0.0,
scatter_amount: 0.0,
angle: 0.0,
roundness: 1.0,
spray_particle_size: 2.0,
spray_density: 100,
bitmap_pixels: Vec::new(),
bitmap_w: 0,
bitmap_h: 0,
flow: 1.0,
color_variant: false,
variant_amount: 0.0,
density: 1.0,
drawing_angle: false,
rotation_random: 0.0,
time_enabled: false,
time_size_start: 1.0,
time_size_end: 1.0,
time_opacity_start: 1.0,
time_opacity_end: 1.0,
time_angle_start: 0.0,
time_angle_end: 0.0,
}
}
pub fn round_hard(size: f32) -> BrushTip {
BrushTip {
style: BrushStyle::Round,
size,
opacity: 1.0,
hardness: 1.0,
spacing: 0.25,
jitter_amount: 0.0,
scatter_amount: 0.0,
angle: 0.0,
roundness: 1.0,
spray_particle_size: 2.0,
spray_density: 100,
bitmap_pixels: Vec::new(),
bitmap_w: 0,
bitmap_h: 0,
flow: 1.0,
color_variant: false,
variant_amount: 0.0,
density: 1.0,
drawing_angle: false,
rotation_random: 0.0,
time_enabled: false,
time_size_start: 1.0,
time_size_end: 1.0,
time_opacity_start: 1.0,
time_opacity_end: 1.0,
time_angle_start: 0.0,
time_angle_end: 0.0,
}
}
pub fn pencil(size: f32) -> BrushTip {
BrushTip {
style: BrushStyle::Pencil,
size: size.max(1.0),
opacity: 1.0,
hardness: 1.0,
spacing: 0.05,
jitter_amount: 0.15,
scatter_amount: 0.0,
angle: 0.0,
roundness: 1.0,
spray_particle_size: 2.0,
spray_density: 100,
bitmap_pixels: Vec::new(),
bitmap_w: 0,
bitmap_h: 0,
flow: 1.0,
color_variant: false,
variant_amount: 0.0,
density: 1.0,
drawing_angle: false,
rotation_random: 0.0,
time_enabled: false,
time_size_start: 1.0,
time_size_end: 1.0,
time_opacity_start: 1.0,
time_opacity_end: 1.0,
time_angle_start: 0.0,
time_angle_end: 0.0,
}
}
pub fn spray(size: f32) -> BrushTip {
BrushTip {
style: BrushStyle::Spray,
size,
opacity: 0.6,
hardness: 0.3,
spacing: 0.35,
jitter_amount: 0.4,
scatter_amount: 0.6,
angle: 0.0,
roundness: 1.0,
spray_particle_size: 2.0,
spray_density: 100,
bitmap_pixels: Vec::new(),
bitmap_w: 0,
bitmap_h: 0,
flow: 1.0,
color_variant: false,
variant_amount: 0.0,
density: 1.0,
drawing_angle: false,
rotation_random: 0.0,
time_enabled: false,
time_size_start: 1.0,
time_size_end: 1.0,
time_opacity_start: 1.0,
time_opacity_end: 1.0,
time_angle_start: 0.0,
time_angle_end: 0.0,
}
}
pub fn calligraphy(size: f32) -> BrushTip {
BrushTip {
style: BrushStyle::Calligraphy,
size,
opacity: 0.9,
hardness: 0.7,
spacing: 0.08,
jitter_amount: 0.0,
scatter_amount: 0.0,
angle: 45.0_f32.to_radians(),
roundness: 0.15,
spray_particle_size: 2.0,
spray_density: 100,
bitmap_pixels: Vec::new(),
bitmap_w: 0,
bitmap_h: 0,
flow: 1.0,
color_variant: false,
variant_amount: 0.0,
density: 1.0,
drawing_angle: false,
rotation_random: 0.0,
time_enabled: false,
time_size_start: 1.0,
time_size_end: 1.0,
time_opacity_start: 1.0,
time_opacity_end: 1.0,
time_angle_start: 0.0,
time_angle_end: 0.0,
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Color helpers (data only)
// ─────────────────────────────────────────────────────────────────────────────
pub mod colors {
pub const TRANSPARENT: [u8; 4] = [0, 0, 0, 0];
pub const BLACK: [u8; 4] = [0, 0, 0, 255];
pub const WHITE: [u8; 4] = [255, 255, 255, 255];
pub const RED: [u8; 4] = [255, 0, 0, 255];
pub const GREEN: [u8; 4] = [0, 255, 0, 255];
pub const BLUE: [u8; 4] = [0, 0, 255, 255];
pub const YELLOW: [u8; 4] = [255, 255, 0, 255];
pub const CYAN: [u8; 4] = [0, 255, 255, 255];
pub const MAGENTA: [u8; 4] = [255, 0, 255, 255];
2026-07-09 02:59:53 +03:00
/// Pack [u8; 4] into 0xAARRGGBB u32 (big-endian alpha first).
pub fn to_u32(rgba: [u8; 4]) -> u32 {
((rgba[3] as u32) << 24)
| ((rgba[0] as u32) << 16)
| ((rgba[1] as u32) << 8)
| (rgba[2] as u32)
2026-07-09 02:59:53 +03:00
}
/// Unpack 0xAARRGGBB u32 to [u8; 4] (R, G, B, A).
pub fn from_u32(argb: u32) -> [u8; 4] {
[
((argb >> 16) & 0xFF) as u8,
((argb >> 8) & 0xFF) as u8,
(argb & 0xFF) as u8,
2026-07-09 02:59:53 +03:00
((argb >> 24) & 0xFF) as u8,
]
}
/// Interpolate between two RGBA colors. `t` in 0.0..=1.0.
pub fn lerp(a: [u8; 4], b: [u8; 4], t: f32) -> [u8; 4] {
let t = t.clamp(0.0, 1.0);
[
(a[0] as f32 + (b[0] as f32 - a[0] as f32) * t).round() as u8,
(a[1] as f32 + (b[1] as f32 - a[1] as f32) * t).round() as u8,
(a[2] as f32 + (b[2] as f32 - a[2] as f32) * t).round() as u8,
(a[3] as f32 + (b[3] as f32 - a[3] as f32) * t).round() as u8,
]
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Coordinate / geometry helpers
// ─────────────────────────────────────────────────────────────────────────────
pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t.clamp(0.0, 1.0)
}
pub fn clamp(val: f32, min: f32, max: f32) -> f32 {
val.clamp(min, max)
}
pub fn distance(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
let dx = x2 - x1;
let dy = y2 - y1;
(dx * dx + dy * dy).sqrt()
}
pub fn point_in_rect(px: f32, py: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> bool {
let (left, right) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
let (top, bottom) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
px >= left && px <= right && py >= top && py <= bottom
}
// ─────────────────────────────────────────────────────────────────────────────
// Canvas preset sizes
// ─────────────────────────────────────────────────────────────────────────────
pub mod canvas_presets {
pub fn size_720p() -> (u32, u32) {
(1280, 720)
}
pub fn size_1080p() -> (u32, u32) {
(1920, 1080)
}
pub fn size_1440p() -> (u32, u32) {
(2560, 1440)
}
pub fn size_4k() -> (u32, u32) {
(3840, 2160)
}
pub fn size_a4() -> (u32, u32) {
(2480, 3508)
} // 300 DPI
pub fn size_a3() -> (u32, u32) {
(3508, 4961)
} // 300 DPI
pub fn size_instagram() -> (u32, u32) {
(1080, 1080)
}
pub fn size_story() -> (u32, u32) {
(1080, 1920)
}
2026-07-09 02:59:53 +03:00
}
// ─────────────────────────────────────────────────────────────────────────────
// Dirty region helpers
// ─────────────────────────────────────────────────────────────────────────────
pub fn expand_dirty_bounds(
bounds: &mut Option<[u32; 4]>,
x: u32,
y: u32,
radius: f32,
canvas_w: u32,
canvas_h: u32,
2026-07-09 02:59:53 +03:00
) {
let r = (radius as i32).max(1);
let x0 = (x as i32 - r).max(0) as u32;
let y0 = (y as i32 - r).max(0) as u32;
let x1 = ((x as i32 + r + 1).min(canvas_w as i32 - 1)).max(0) as u32;
let y1 = ((y as i32 + r + 1).min(canvas_h as i32 - 1)).max(0) as u32;
match bounds {
Some(b) => {
b[0] = b[0].min(x0);
b[1] = b[1].min(y0);
b[2] = b[2].max(x1);
b[3] = b[3].max(y1);
}
None => {
*bounds = Some([x0, y0, x1, y1]);
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Selection helpers
// ─────────────────────────────────────────────────────────────────────────────
pub fn selection_rect_from_mask(mask: &[u8], w: u32, h: u32) -> Option<[u32; 4]> {
let mut x_min = u32::MAX;
let mut y_min = u32::MAX;
let mut x_max = 0u32;
let mut y_max = 0u32;
let mut has = false;
for y in 0..h {
for x in 0..w {
let i = (y * w + x) as usize;
if mask[i] > 0 {
has = true;
x_min = x_min.min(x);
y_min = y_min.min(y);
x_max = x_max.max(x);
y_max = y_max.max(y);
}
}
}
if has {
Some([x_min, y_min, x_max, y_max])
} else {
None
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Thumbnail helpers
// ─────────────────────────────────────────────────────────────────────────────
/// Fast nearest-neighbor downscale to `target_w` x `target_h`.
pub fn thumbnail_nearest(
pixels: &[u8],
src_w: u32,
src_h: u32,
target_w: u32,
target_h: u32,
) -> Vec<u8> {
2026-07-09 02:59:53 +03:00
let mut out = vec![0u8; (target_w * target_h * 4) as usize];
let x_ratio = src_w as f32 / target_w as f32;
let y_ratio = src_h as f32 / target_h as f32;
for y in 0..target_h {
for x in 0..target_w {
let src_x = (x as f32 * x_ratio).min(src_w as f32 - 1.0) as u32;
let src_y = (y as f32 * y_ratio).min(src_h as f32 - 1.0) as u32;
let src_i = ((src_y * src_w + src_x) * 4) as usize;
let dst_i = ((y * target_w + x) * 4) as usize;
out[dst_i..dst_i + 4].copy_from_slice(&pixels[src_i..src_i + 4]);
}
}
out
}
// ─────────────────────────────────────────────────────────────────────────────
// Feature flags
// ─────────────────────────────────────────────────────────────────────────────
/// Global maximum value for Spray radius slider.
pub const SPRAY_MAX_RADIUS: f32 = 600.0;
pub const FEATURE_CROP: &str = "crop";
pub const FEATURE_ROTATE: &str = "rotate";
pub const FEATURE_BRIGHTNESS_CONTRAST: &str = "brightness_contrast";
pub const FEATURE_HUE_SATURATION: &str = "hue_saturation";
pub const FEATURE_BOX_BLUR: &str = "box_blur";
pub const FEATURE_GAUSSIAN_BLUR: &str = "gaussian_blur";
pub const FEATURE_MOTION_BLUR: &str = "motion_blur";
pub const FEATURE_MOSAIC: &str = "mosaic";
pub const FEATURE_OIL_PAINT: &str = "oil_paint";
pub const FEATURE_CRYSTALLIZE: &str = "crystallize";
pub const FEATURE_PINCH: &str = "pinch";
pub const FEATURE_TWIRL: &str = "twirl";
pub const FEATURE_UNSHARP_MASK: &str = "unsharp_mask";
pub const FEATURE_BORDER: &str = "border";
pub const FEATURE_LAYERS: &str = "layers";
pub const FEATURE_LAYER_STYLES: &str = "layer_styles";
pub const FEATURE_SELECTION: &str = "selection";
pub const FEATURE_TEXT: &str = "text";
pub const FEATURE_VECTOR_SHAPES: &str = "vector_shapes";
pub const FEATURE_HISTORY: &str = "history";
pub const FEATURE_UNDO_REDO: &str = "undo_redo";
pub const FEATURE_SMART_SELECT: &str = "smart_select";
pub const FEATURE_AI_INPAINT: &str = "ai_inpaint";
pub const FEATURE_BACKGROUND_REMOVAL: &str = "background_removal";
pub fn all_features() -> Vec<&'static str> {
vec![
FEATURE_CROP,
FEATURE_ROTATE,
FEATURE_BRIGHTNESS_CONTRAST,
FEATURE_HUE_SATURATION,
FEATURE_BOX_BLUR,
FEATURE_GAUSSIAN_BLUR,
FEATURE_MOTION_BLUR,
FEATURE_MOSAIC,
FEATURE_OIL_PAINT,
FEATURE_CRYSTALLIZE,
FEATURE_PINCH,
FEATURE_TWIRL,
FEATURE_UNSHARP_MASK,
FEATURE_BORDER,
FEATURE_LAYERS,
FEATURE_LAYER_STYLES,
FEATURE_SELECTION,
FEATURE_TEXT,
FEATURE_VECTOR_SHAPES,
FEATURE_HISTORY,
FEATURE_UNDO_REDO,
FEATURE_SMART_SELECT,
FEATURE_AI_INPAINT,
FEATURE_BACKGROUND_REMOVAL,
2026-07-09 02:59:53 +03:00
]
}
// ─────────────────────────────────────────────────────────────────────────────
// Version info
// ─────────────────────────────────────────────────────────────────────────────
pub const VERSION_MAJOR: u32 = 3;
pub const VERSION_MINOR: u32 = 0;
pub const VERSION_PATCH: u32 = 0;
pub fn version_string() -> String {
format!("{}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)
}
// Re-export tools module
pub use tools::*;
// ─────────────────────────────────────────────────────────────────────────────
// VectorShape methods (ported from V2 hcie-layers)
// ─────────────────────────────────────────────────────────────────────────────
impl VectorShape {
pub fn bounds(&self) -> (f32, f32, f32, f32) {
match self {
VectorShape::Line { x1, y1, x2, y2, .. }
| VectorShape::Rect { x1, y1, x2, y2, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { x1, y1, x2, y2, .. }
| VectorShape::Arrow { x1, y1, x2, y2, .. }
| VectorShape::Star { x1, y1, x2, y2, .. }
| VectorShape::Polygon { x1, y1, x2, y2, .. }
| VectorShape::Rhombus { x1, y1, x2, y2, .. }
| VectorShape::Cylinder { x1, y1, x2, y2, .. }
| VectorShape::Heart { x1, y1, x2, y2, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { x1, y1, x2, y2, .. }
| VectorShape::Gear { x1, y1, x2, y2, .. }
| VectorShape::Cross { x1, y1, x2, y2, .. }
| VectorShape::Crescent { x1, y1, x2, y2, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { x1, y1, x2, y2, .. }
| VectorShape::SvgShape { x1, y1, x2, y2, .. }
| VectorShape::Bolt { x1, y1, x2, y2, .. } => (*x1, *y1, *x2, *y2),
2026-07-09 02:59:53 +03:00
VectorShape::FreePath { pts, .. } => {
let x1 = pts.iter().map(|p| p[0]).fold(f32::INFINITY, f32::min);
let y1 = pts.iter().map(|p| p[1]).fold(f32::INFINITY, f32::min);
let x2 = pts.iter().map(|p| p[0]).fold(f32::NEG_INFINITY, f32::max);
let y2 = pts.iter().map(|p| p[1]).fold(f32::NEG_INFINITY, f32::max);
(x1, y1, x2, y2)
}
}
}
pub fn normalized_bounds(&self) -> (f32, f32, f32, f32) {
let (x1, y1, x2, y2) = self.bounds();
(x1.min(x2), y1.min(y2), x1.max(x2), y1.max(y2))
}
pub fn size(&self) -> (f32, f32) {
let (x1, y1, x2, y2) = self.normalized_bounds();
(x2 - x1, y2 - y1)
}
pub fn color(&self) -> [u8; 4] {
match self {
VectorShape::Line { color, .. }
| VectorShape::Rect { color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { color, .. }
| VectorShape::Arrow { color, .. }
| VectorShape::Star { color, .. }
| VectorShape::Polygon { color, .. }
| VectorShape::Rhombus { color, .. }
| VectorShape::Cylinder { color, .. }
| VectorShape::Heart { color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { color, .. }
| VectorShape::Gear { color, .. }
| VectorShape::Cross { color, .. }
| VectorShape::Crescent { color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { color, .. }
| VectorShape::Bolt { color, .. }
| VectorShape::SvgShape { color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::FreePath { color, .. } => *color,
}
}
pub fn set_color(&mut self, new_color: [u8; 4]) {
match self {
VectorShape::Line { color, .. }
| VectorShape::Rect { color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { color, .. }
| VectorShape::Arrow { color, .. }
| VectorShape::Star { color, .. }
| VectorShape::Polygon { color, .. }
| VectorShape::Rhombus { color, .. }
| VectorShape::Cylinder { color, .. }
| VectorShape::Heart { color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { color, .. }
| VectorShape::Gear { color, .. }
| VectorShape::Cross { color, .. }
| VectorShape::Crescent { color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { color, .. }
| VectorShape::Bolt { color, .. }
| VectorShape::SvgShape { color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::FreePath { color, .. } => *color = new_color,
}
}
pub fn shape_opacity(&self) -> f32 {
match self {
VectorShape::Line { opacity, .. }
| VectorShape::Rect { opacity, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { opacity, .. }
| VectorShape::Arrow { opacity, .. }
| VectorShape::Star { opacity, .. }
| VectorShape::Polygon { opacity, .. }
| VectorShape::Rhombus { opacity, .. }
| VectorShape::Cylinder { opacity, .. }
| VectorShape::Heart { opacity, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { opacity, .. }
| VectorShape::Gear { opacity, .. }
| VectorShape::Cross { opacity, .. }
| VectorShape::Crescent { opacity, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { opacity, .. }
| VectorShape::Bolt { opacity, .. }
| VectorShape::SvgShape { opacity, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::FreePath { opacity, .. } => *opacity,
}
}
pub fn set_shape_opacity(&mut self, new_opacity: f32) {
let clamped = new_opacity.clamp(0.0, 1.0);
match self {
VectorShape::Line { opacity, .. }
| VectorShape::Rect { opacity, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { opacity, .. }
| VectorShape::Arrow { opacity, .. }
| VectorShape::Star { opacity, .. }
| VectorShape::Polygon { opacity, .. }
| VectorShape::Rhombus { opacity, .. }
| VectorShape::Cylinder { opacity, .. }
| VectorShape::Heart { opacity, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { opacity, .. }
| VectorShape::Gear { opacity, .. }
| VectorShape::Cross { opacity, .. }
| VectorShape::Crescent { opacity, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { opacity, .. }
| VectorShape::Bolt { opacity, .. }
| VectorShape::SvgShape { opacity, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::FreePath { opacity, .. } => *opacity = clamped,
}
}
pub fn hardness(&self) -> f32 {
match self {
VectorShape::Line { hardness, .. }
| VectorShape::Rect { hardness, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { hardness, .. }
| VectorShape::Arrow { hardness, .. }
| VectorShape::Star { hardness, .. }
| VectorShape::Polygon { hardness, .. }
| VectorShape::Rhombus { hardness, .. }
| VectorShape::Cylinder { hardness, .. }
| VectorShape::Heart { hardness, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { hardness, .. }
| VectorShape::Gear { hardness, .. }
| VectorShape::Cross { hardness, .. }
| VectorShape::Crescent { hardness, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { hardness, .. }
| VectorShape::Bolt { hardness, .. }
| VectorShape::SvgShape { hardness, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::FreePath { hardness, .. } => *hardness,
}
}
pub fn set_hardness(&mut self, new_hardness: f32) {
let clamped = new_hardness.clamp(0.0, 1.0);
match self {
VectorShape::Line { hardness, .. }
| VectorShape::Rect { hardness, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { hardness, .. }
| VectorShape::Arrow { hardness, .. }
| VectorShape::Star { hardness, .. }
| VectorShape::Polygon { hardness, .. }
| VectorShape::Rhombus { hardness, .. }
| VectorShape::Cylinder { hardness, .. }
| VectorShape::Heart { hardness, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { hardness, .. }
| VectorShape::Gear { hardness, .. }
| VectorShape::Cross { hardness, .. }
| VectorShape::Crescent { hardness, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { hardness, .. }
| VectorShape::Bolt { hardness, .. }
| VectorShape::SvgShape { hardness, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::FreePath { hardness, .. } => *hardness = clamped,
}
}
pub fn stroke(&self) -> f32 {
match self {
VectorShape::Line { stroke, .. }
| VectorShape::Rect { stroke, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { stroke, .. }
| VectorShape::Arrow { stroke, .. }
| VectorShape::Star { stroke, .. }
| VectorShape::Polygon { stroke, .. }
| VectorShape::Rhombus { stroke, .. }
| VectorShape::Cylinder { stroke, .. }
| VectorShape::Heart { stroke, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { stroke, .. }
| VectorShape::Gear { stroke, .. }
| VectorShape::Cross { stroke, .. }
| VectorShape::Crescent { stroke, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { stroke, .. }
| VectorShape::Bolt { stroke, .. }
| VectorShape::SvgShape { stroke, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::FreePath { stroke, .. } => *stroke,
}
}
pub fn set_stroke(&mut self, new_stroke: f32) {
let clamped = new_stroke.max(0.0);
match self {
VectorShape::Line { stroke, .. }
| VectorShape::Rect { stroke, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { stroke, .. }
| VectorShape::Arrow { stroke, .. }
| VectorShape::Star { stroke, .. }
| VectorShape::Polygon { stroke, .. }
| VectorShape::Rhombus { stroke, .. }
| VectorShape::Cylinder { stroke, .. }
| VectorShape::Heart { stroke, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { stroke, .. }
| VectorShape::Gear { stroke, .. }
| VectorShape::Cross { stroke, .. }
| VectorShape::Crescent { stroke, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { stroke, .. }
| VectorShape::Bolt { stroke, .. }
| VectorShape::SvgShape { stroke, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::FreePath { stroke, .. } => *stroke = clamped,
}
}
pub fn apply_handle(
&mut self,
handle: VectorEditHandle,
dx: f32,
dy: f32,
locked_ratio: Option<f32>,
) {
2026-07-09 02:59:53 +03:00
use VectorEditHandle::*;
if matches!(handle, None | Rotate) {
return;
}
2026-07-09 02:59:53 +03:00
if let VectorShape::FreePath { pts, .. } = self {
if handle == Move {
for p in pts.iter_mut() {
p[0] += dx;
p[1] += dy;
}
2026-07-09 02:59:53 +03:00
}
return;
}
let angle = self.angle();
let (sin_a, cos_a) = angle.sin_cos();
let (raw_x1, raw_y1, raw_x2, raw_y2) = self.coords_mut();
let x1_orig = *raw_x1;
let y1_orig = *raw_y1;
let x2_orig = *raw_x2;
let y2_orig = *raw_y2;
let mut left = x1_orig.min(x2_orig);
let mut top = y1_orig.min(y2_orig);
let mut right = x1_orig.max(x2_orig);
2026-07-09 02:59:53 +03:00
let mut bottom = y1_orig.max(y2_orig);
let (left0, top0, right0, bottom0) = (left, top, right, bottom);
let anchor_local = match handle {
TopLeft => (right0, bottom0),
Top => ((left0 + right0) / 2.0, bottom0),
TopRight => (left0, bottom0),
Left => (right0, (top0 + bottom0) / 2.0),
Right => (left0, (top0 + bottom0) / 2.0),
BottomLeft => (right0, top0),
Bottom => ((left0 + right0) / 2.0, top0),
2026-07-09 02:59:53 +03:00
BottomRight => (left0, top0),
Move => (0.0, 0.0),
_ => (0.0, 0.0),
2026-07-09 02:59:53 +03:00
};
match handle {
Move => {
left += dx;
top += dy;
right += dx;
bottom += dy;
2026-07-09 02:59:53 +03:00
}
_ => {
let mut new_left = left;
let mut new_top = top;
let mut new_right = right;
let mut new_bottom = bottom;
match handle {
TopLeft => {
new_left += dx;
new_top += dy;
}
Top => {
new_top += dy;
}
TopRight => {
new_right += dx;
new_top += dy;
}
Left => {
new_left += dx;
}
Right => {
new_right += dx;
}
BottomLeft => {
new_left += dx;
new_bottom += dy;
}
Bottom => {
new_bottom += dy;
}
BottomRight => {
new_right += dx;
new_bottom += dy;
}
2026-07-09 02:59:53 +03:00
_ => {}
}
if let Some(ratio) = locked_ratio {
let _w0 = (right0 - left0).abs().max(1.0);
let _h0 = (bottom0 - top0).abs().max(1.0);
let mut nw = (new_right - new_left).abs();
let mut nh = (new_bottom - new_top).abs();
if matches!(handle, Top | Bottom) {
nw = nh * ratio;
let dw = nw - (right0 - left0);
new_left = left0 - dw / 2.0;
new_right = right0 + dw / 2.0;
2026-07-09 02:59:53 +03:00
} else if matches!(handle, Left | Right) {
nh = nw / ratio;
let dh = nh - (bottom0 - top0);
new_top = top0 - dh / 2.0;
new_bottom = bottom0 + dh / 2.0;
2026-07-09 02:59:53 +03:00
} else {
if nw / nh > ratio {
nw = nh * ratio;
} else {
nh = nw / ratio;
}
match handle {
TopLeft => {
new_left = right0 - nw;
new_top = bottom0 - nh;
}
TopRight => {
new_right = left0 + nw;
new_top = bottom0 - nh;
}
BottomLeft => {
new_left = right0 - nw;
new_bottom = top0 + nh;
}
BottomRight => {
new_right = left0 + nw;
new_bottom = top0 + nh;
}
2026-07-09 02:59:53 +03:00
_ => {}
}
}
}
left = new_left;
top = new_top;
right = new_right;
bottom = new_bottom;
}
}
if handle != Move {
let cx0 = (left0 + right0) / 2.0;
let cy0 = (top0 + bottom0) / 2.0;
let cx1 = (left + right) / 2.0;
let cy1 = (top + bottom) / 2.0;
let ax_rel = anchor_local.0 - cx0;
let ay_rel = anchor_local.1 - cy0;
let awx = cx0 + ax_rel * cos_a - ay_rel * sin_a;
let awy = cy0 + ax_rel * sin_a + ay_rel * cos_a;
let anchor_local_new = match handle {
Top | Bottom => ((left + right) / 2.0, anchor_local.1),
Left | Right => (anchor_local.0, (top + bottom) / 2.0),
_ => anchor_local,
};
let ax_rel1 = anchor_local_new.0 - cx1;
let ay_rel1 = anchor_local_new.1 - cy1;
let awx1 = cx1 + ax_rel1 * cos_a - ay_rel1 * sin_a;
let awy1 = cy1 + ax_rel1 * sin_a + ay_rel1 * cos_a;
let dx_w = awx - awx1;
let dy_w = awy - awy1;
left += dx_w;
right += dx_w;
top += dy_w;
bottom += dy_w;
}
if x1_orig <= x2_orig {
*raw_x1 = left;
*raw_x2 = right;
2026-07-09 02:59:53 +03:00
} else {
*raw_x1 = right;
*raw_x2 = left;
2026-07-09 02:59:53 +03:00
}
if y1_orig <= y2_orig {
*raw_y1 = top;
*raw_y2 = bottom;
2026-07-09 02:59:53 +03:00
} else {
*raw_y1 = bottom;
*raw_y2 = top;
2026-07-09 02:59:53 +03:00
}
}
pub fn name(&self) -> String {
match self {
VectorShape::Line { name, .. }
| VectorShape::Rect { name, .. }
| VectorShape::Circle { name, .. }
| VectorShape::Arrow { name, .. }
| VectorShape::Star { name, .. }
| VectorShape::Polygon { name, .. }
| VectorShape::Rhombus { name, .. }
| VectorShape::Cylinder { name, .. }
| VectorShape::Heart { name, .. }
| VectorShape::Bubble { name, .. }
| VectorShape::Gear { name, .. }
| VectorShape::Cross { name, .. }
| VectorShape::Crescent { name, .. }
| VectorShape::Bolt { name, .. }
| VectorShape::Arrow4 { name, .. }
| VectorShape::SvgShape { name, .. }
| VectorShape::FreePath { name, .. } => name.clone(),
}
}
pub fn set_name(&mut self, new_name: String) {
match self {
VectorShape::Line { name, .. }
| VectorShape::Rect { name, .. }
| VectorShape::Circle { name, .. }
| VectorShape::Arrow { name, .. }
| VectorShape::Star { name, .. }
| VectorShape::Polygon { name, .. }
| VectorShape::Rhombus { name, .. }
| VectorShape::Cylinder { name, .. }
| VectorShape::Heart { name, .. }
| VectorShape::Bubble { name, .. }
| VectorShape::Gear { name, .. }
| VectorShape::Cross { name, .. }
| VectorShape::Crescent { name, .. }
| VectorShape::Bolt { name, .. }
| VectorShape::Arrow4 { name, .. }
| VectorShape::SvgShape { name, .. }
| VectorShape::FreePath { name, .. } => *name = new_name,
}
}
2026-07-09 02:59:53 +03:00
pub fn angle(&self) -> f32 {
match self {
VectorShape::Line { angle, .. }
| VectorShape::Rect { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { angle, .. }
| VectorShape::Arrow { angle, .. }
| VectorShape::Star { angle, .. }
| VectorShape::Polygon { angle, .. }
| VectorShape::Rhombus { angle, .. }
| VectorShape::Cylinder { angle, .. }
| VectorShape::Heart { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { angle, .. }
| VectorShape::Gear { angle, .. }
| VectorShape::Cross { angle, .. }
| VectorShape::Crescent { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { angle, .. }
| VectorShape::SvgShape { angle, .. }
| VectorShape::Bolt { angle, .. } => *angle,
2026-07-09 02:59:53 +03:00
VectorShape::FreePath { .. } => 0.0,
}
}
pub fn rotate(&mut self, delta: f32) {
match self {
VectorShape::Line { angle, .. }
| VectorShape::Rect { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { angle, .. }
| VectorShape::Arrow { angle, .. }
| VectorShape::Star { angle, .. }
| VectorShape::Polygon { angle, .. }
| VectorShape::Rhombus { angle, .. }
| VectorShape::Cylinder { angle, .. }
| VectorShape::Heart { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { angle, .. }
| VectorShape::Gear { angle, .. }
| VectorShape::Cross { angle, .. }
| VectorShape::Crescent { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { angle, .. }
| VectorShape::SvgShape { angle, .. }
| VectorShape::Bolt { angle, .. } => {
*angle += delta;
}
2026-07-09 02:59:53 +03:00
VectorShape::FreePath { .. } => {}
}
}
pub fn set_angle(&mut self, new_angle: f32) {
match self {
VectorShape::Line { angle, .. }
| VectorShape::Rect { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { angle, .. }
| VectorShape::Arrow { angle, .. }
| VectorShape::Star { angle, .. }
| VectorShape::Polygon { angle, .. }
| VectorShape::Rhombus { angle, .. }
| VectorShape::Cylinder { angle, .. }
| VectorShape::Heart { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { angle, .. }
| VectorShape::Gear { angle, .. }
| VectorShape::Cross { angle, .. }
| VectorShape::Crescent { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { angle, .. }
| VectorShape::SvgShape { angle, .. }
| VectorShape::Bolt { angle, .. } => {
*angle = new_angle;
}
2026-07-09 02:59:53 +03:00
VectorShape::FreePath { .. } => {}
}
}
pub fn get_angle_mut(&mut self) -> Option<&mut f32> {
match self {
VectorShape::Line { angle, .. }
| VectorShape::Rect { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { angle, .. }
| VectorShape::Arrow { angle, .. }
| VectorShape::Star { angle, .. }
| VectorShape::Polygon { angle, .. }
| VectorShape::Rhombus { angle, .. }
| VectorShape::Cylinder { angle, .. }
| VectorShape::Heart { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { angle, .. }
| VectorShape::Gear { angle, .. }
| VectorShape::Cross { angle, .. }
| VectorShape::Crescent { angle, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { angle, .. }
| VectorShape::SvgShape { angle, .. }
| VectorShape::Bolt { angle, .. } => Some(angle),
2026-07-09 02:59:53 +03:00
VectorShape::FreePath { .. } => None,
}
}
pub fn is_filled(&self) -> bool {
match self {
VectorShape::Rect { fill, .. }
| VectorShape::Circle { fill, .. }
| VectorShape::Arrow { fill, .. }
| VectorShape::Star { fill, .. }
| VectorShape::Polygon { fill, .. }
| VectorShape::Rhombus { fill, .. }
| VectorShape::Cylinder { fill, .. }
| VectorShape::Heart { fill, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { fill, .. }
| VectorShape::Gear { fill, .. }
| VectorShape::Cross { fill, .. }
| VectorShape::Crescent { fill, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { fill, .. }
| VectorShape::SvgShape { fill, .. }
| VectorShape::Bolt { fill, .. } => *fill,
2026-07-09 02:59:53 +03:00
_ => false,
}
}
pub fn fill_color(&self) -> Option<[u8; 4]> {
match self {
VectorShape::Rect { fill_color, .. }
| VectorShape::Circle { fill_color, .. }
| VectorShape::Arrow { fill_color, .. }
| VectorShape::Star { fill_color, .. }
| VectorShape::Polygon { fill_color, .. }
| VectorShape::Rhombus { fill_color, .. }
| VectorShape::Cylinder { fill_color, .. }
| VectorShape::Heart { fill_color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { fill_color, .. }
| VectorShape::Gear { fill_color, .. }
| VectorShape::Cross { fill_color, .. }
| VectorShape::Crescent { fill_color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { fill_color, .. }
| VectorShape::SvgShape { fill_color, .. }
| VectorShape::Bolt { fill_color, .. } => Some(*fill_color),
2026-07-09 02:59:53 +03:00
_ => None,
}
}
pub fn fill(&self) -> Option<bool> {
match self {
VectorShape::Rect { fill, .. }
| VectorShape::Circle { fill, .. }
| VectorShape::Arrow { fill, .. }
| VectorShape::Star { fill, .. }
| VectorShape::Polygon { fill, .. }
| VectorShape::Rhombus { fill, .. }
| VectorShape::Cylinder { fill, .. }
| VectorShape::Heart { fill, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { fill, .. }
| VectorShape::Gear { fill, .. }
| VectorShape::Cross { fill, .. }
| VectorShape::Crescent { fill, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { fill, .. }
| VectorShape::SvgShape { fill, .. }
| VectorShape::Bolt { fill, .. } => Some(*fill),
2026-07-09 02:59:53 +03:00
_ => None,
}
}
pub fn get_fill_mut(&mut self) -> Option<&mut bool> {
match self {
VectorShape::Rect { fill, .. }
| VectorShape::Circle { fill, .. }
| VectorShape::Arrow { fill, .. }
| VectorShape::Star { fill, .. }
| VectorShape::Polygon { fill, .. }
| VectorShape::Rhombus { fill, .. }
| VectorShape::Cylinder { fill, .. }
| VectorShape::Heart { fill, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { fill, .. }
| VectorShape::Gear { fill, .. }
| VectorShape::Cross { fill, .. }
| VectorShape::Crescent { fill, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { fill, .. }
| VectorShape::SvgShape { fill, .. }
| VectorShape::Bolt { fill, .. } => Some(fill),
2026-07-09 02:59:53 +03:00
_ => None,
}
}
pub fn get_fill_color_mut(&mut self) -> Option<&mut [u8; 4]> {
match self {
VectorShape::Rect { fill_color, .. }
| VectorShape::Circle { fill_color, .. }
| VectorShape::Arrow { fill_color, .. }
| VectorShape::Star { fill_color, .. }
| VectorShape::Polygon { fill_color, .. }
| VectorShape::Rhombus { fill_color, .. }
| VectorShape::Cylinder { fill_color, .. }
| VectorShape::Heart { fill_color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { fill_color, .. }
| VectorShape::Gear { fill_color, .. }
| VectorShape::Cross { fill_color, .. }
| VectorShape::Crescent { fill_color, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { fill_color, .. }
| VectorShape::SvgShape { fill_color, .. }
| VectorShape::Bolt { fill_color, .. } => Some(fill_color),
2026-07-09 02:59:53 +03:00
_ => None,
}
}
pub fn get_stroke_mut(&mut self) -> Option<&mut f32> {
match self {
VectorShape::Line { stroke, .. }
| VectorShape::Rect { stroke, .. }
| VectorShape::Circle { stroke, .. }
| VectorShape::Arrow { stroke, .. }
| VectorShape::Star { stroke, .. }
| VectorShape::Polygon { stroke, .. }
| VectorShape::Rhombus { stroke, .. }
| VectorShape::Cylinder { stroke, .. }
| VectorShape::Heart { stroke, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { stroke, .. }
| VectorShape::Gear { stroke, .. }
| VectorShape::Cross { stroke, .. }
| VectorShape::Crescent { stroke, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { stroke, .. }
| VectorShape::Bolt { stroke, .. }
| VectorShape::SvgShape { stroke, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::FreePath { stroke, .. } => Some(stroke),
}
}
pub fn points(&self) -> Option<u32> {
match self {
VectorShape::Star { points, .. } => Some(*points),
_ => None,
}
}
pub fn inner_radius(&self) -> Option<f32> {
match self {
VectorShape::Star { inner_radius, .. } => Some(*inner_radius),
_ => None,
}
}
pub fn sides(&self) -> Option<u32> {
match self {
VectorShape::Polygon { sides, .. } => Some(*sides),
_ => None,
}
}
pub fn get_points_mut(&mut self) -> Option<&mut u32> {
match self {
VectorShape::Star { points, .. } => Some(points),
_ => None,
}
}
pub fn get_inner_radius_mut(&mut self) -> Option<&mut f32> {
match self {
VectorShape::Star { inner_radius, .. } => Some(inner_radius),
_ => None,
}
}
pub fn get_sides_mut(&mut self) -> Option<&mut u32> {
match self {
VectorShape::Polygon { sides, .. } => Some(sides),
_ => None,
}
}
pub fn set_bounds(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
if let VectorShape::FreePath { pts, .. } = self {
let ox1 = pts.iter().map(|p| p[0]).fold(f32::INFINITY, f32::min);
let oy1 = pts.iter().map(|p| p[1]).fold(f32::INFINITY, f32::min);
let ox2 = pts.iter().map(|p| p[0]).fold(f32::NEG_INFINITY, f32::max);
let oy2 = pts.iter().map(|p| p[1]).fold(f32::NEG_INFINITY, f32::max);
let ow = (ox2 - ox1).max(1e-6);
let oh = (oy2 - oy1).max(1e-6);
for p in pts.iter_mut() {
p[0] = x1 + (p[0] - ox1) / ow * (x2 - x1);
p[1] = y1 + (p[1] - oy1) / oh * (y2 - y1);
}
return;
}
let (rx1, ry1, rx2, ry2) = self.coords_mut();
*rx1 = x1;
*ry1 = y1;
*rx2 = x2;
*ry2 = y2;
2026-07-09 02:59:53 +03:00
}
fn coords_mut(&mut self) -> (&mut f32, &mut f32, &mut f32, &mut f32) {
match self {
VectorShape::Line { x1, y1, x2, y2, .. }
| VectorShape::Rect { x1, y1, x2, y2, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Circle { x1, y1, x2, y2, .. }
| VectorShape::Arrow { x1, y1, x2, y2, .. }
| VectorShape::Star { x1, y1, x2, y2, .. }
| VectorShape::Polygon { x1, y1, x2, y2, .. }
| VectorShape::Rhombus { x1, y1, x2, y2, .. }
| VectorShape::Cylinder { x1, y1, x2, y2, .. }
| VectorShape::Heart { x1, y1, x2, y2, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Bubble { x1, y1, x2, y2, .. }
| VectorShape::Gear { x1, y1, x2, y2, .. }
| VectorShape::Cross { x1, y1, x2, y2, .. }
| VectorShape::Crescent { x1, y1, x2, y2, .. }
2026-07-09 02:59:53 +03:00
| VectorShape::Arrow4 { x1, y1, x2, y2, .. }
| VectorShape::SvgShape { x1, y1, x2, y2, .. }
| VectorShape::Bolt { x1, y1, x2, y2, .. } => (x1, y1, x2, y2),
2026-07-09 02:59:53 +03:00
VectorShape::FreePath { .. } => unreachable!("coords_mut not valid for FreePath"),
}
}
}