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

1914 lines
82 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,
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",
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,
];
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",
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 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];
pub fn label(self) -> &'static str {
match self { LineCap::Round => "Round", LineCap::Square => "Square", LineCap::Arrow => "Arrow", LineCap::Flat => "Flat" }
}
}
// ─────────────────────────────────────────────────────────────────────────────
// VectorEditHandle
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum VectorEditHandle {
#[default]
None,
Move,
TopLeft, Top, TopRight,
Left, Right,
BottomLeft, Bottom, BottomRight,
Rotate,
}
// ─────────────────────────────────────────────────────────────────────────────
// VectorShape
// ─────────────────────────────────────────────────────────────────────────────
fn default_fill_color() -> [u8; 4] { [255, 255, 255, 255] }
fn default_opacity() -> f32 { 1.0 }
fn default_hardness() -> f32 { 1.0 }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum VectorShape {
Line { 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 { 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 { 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 },
Arrow { 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 },
Star { 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{ 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 },
Rhombus{ 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 },
Cylinder{ 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 },
Heart { 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 },
Bubble { 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 },
Gear { 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 },
Cross { 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 },
Crescent{ 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 },
Bolt { 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 },
Arrow4 { 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 },
FreePath { 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 },
}
// ─────────────────────────────────────────────────────────────────────────────
// LayerData
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Clone, Serialize, Deserialize, PartialEq)]
pub enum LayerData {
Raster,
Vector { shapes: Vec<VectorShape> },
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 }
}
// ─────────────────────────────────────────────────────────────────────────────
// 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 },
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],
},
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 },
}
// ─────────────────────────────────────────────────────────────────────────────
// 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 },
Undo,
Redo,
JumpToHistory { index: i32 },
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)
/// 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 },
/// Append sampled points to the stroke currently in progress.
/// Points are processed sequentially by `Engine::stroke_to`.
StrokeTo { points: Vec<(f32, f32, f32)> },
/// 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 },
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 },
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 },
ApplyFilter { filter_id: String, params: serde_json::Value },
ResetTool,
AlignLayer { id: u64, axis: AlignAxis },
AddText { text: String, font: String, size: f32, x: f32, y: f32, color: [u8; 4] },
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 },
OpenImage(String),
SaveAs { path: String, format: String },
SaveNative(String),
ExportPsd(String),
ExportKra(String),
ExportLayer { layer_id: u64, path: String, format: String },
ExportSelection { path: String, format: String },
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 },
/// 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 },
/// 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 },
AllLayersReplaced,
UndoPerformed { current_step: i32 },
RedoPerformed { current_step: i32 },
HistoryJumped { target_step: i32 },
CanvasRendered { width: u32, height: u32 },
CanvasResized { w: u32, h: u32 },
SelectionChanged,
BrushStrokeFinished,
ToolFinished,
ImageOpened { path: String },
Saved { path: String, success: bool },
ZoomChanged(f32),
PanChanged(f32, f32),
ModifiedChanged(bool),
DocumentClosed { id: u64 },
DocumentSwitched { id: u64 },
AiActionCompleted { result: String },
AiActionFailed { error: String },
/// 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 },
Separator,
Group { label: String, children: Vec<WidgetDescription> },
}
// ─────────────────────────────────────────────────────────────────────────────
// Layer (Data-only struct, no complex methods)
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Serialize, Deserialize)]
pub struct Layer {
pub name: String,
pub layer_type: LayerType,
pub data: LayerData,
pub pixels: Vec<u8>,
pub width: u32,
pub height: u32,
pub visible: bool,
pub opacity: f32,
pub blend_mode: BlendMode,
pub locked: bool,
pub dirty: bool,
pub id: u64,
pub parent_id: Option<u64>,
pub styles: Vec<LayerStyle>,
pub clipping_mask: bool,
pub collapsed: bool,
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)),
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;
}
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, .. } => {
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]; }
(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]]
}
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);
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)
} 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::BrushTip;
use super::BrushStyle;
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];
/// 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)
}
/// 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,
((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) }
}
// ─────────────────────────────────────────────────────────────────────────────
// 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,
) {
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> {
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,
]
}
// ─────────────────────────────────────────────────────────────────────────────
// 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, .. }
| 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, .. }
| VectorShape::Bubble { x1, y1, x2, y2, .. }
| VectorShape::Gear { x1, y1, x2, y2, .. }
| VectorShape::Cross { x1, y1, x2, y2, .. }
| VectorShape::Crescent{ x1, y1, x2, y2, .. }
| VectorShape::Arrow4 { x1, y1, x2, y2, .. }
| VectorShape::Bolt { x1, y1, x2, y2, .. } => (*x1, *y1, *x2, *y2),
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, .. }
| VectorShape::Circle { color, .. }
| VectorShape::Arrow { color, .. }
| VectorShape::Star { color, .. }
| VectorShape::Polygon{ color, .. }
| VectorShape::Rhombus{ color, .. }
| VectorShape::Cylinder{ color, .. }
| VectorShape::Heart { color, .. }
| VectorShape::Bubble { color, .. }
| VectorShape::Gear { color, .. }
| VectorShape::Cross { color, .. }
| VectorShape::Crescent{ color, .. }
| VectorShape::Arrow4 { color, .. }
| VectorShape::Bolt { color, .. }
| VectorShape::FreePath { color, .. } => *color,
}
}
pub fn set_color(&mut self, new_color: [u8; 4]) {
match self {
VectorShape::Line { color, .. }
| VectorShape::Rect { color, .. }
| VectorShape::Circle { color, .. }
| VectorShape::Arrow { color, .. }
| VectorShape::Star { color, .. }
| VectorShape::Polygon{ color, .. }
| VectorShape::Rhombus{ color, .. }
| VectorShape::Cylinder{ color, .. }
| VectorShape::Heart { color, .. }
| VectorShape::Bubble { color, .. }
| VectorShape::Gear { color, .. }
| VectorShape::Cross { color, .. }
| VectorShape::Crescent{ color, .. }
| VectorShape::Arrow4 { color, .. }
| VectorShape::Bolt { color, .. }
| VectorShape::FreePath { color, .. } => *color = new_color,
}
}
pub fn shape_opacity(&self) -> f32 {
match self {
VectorShape::Line { opacity, .. }
| VectorShape::Rect { opacity, .. }
| VectorShape::Circle { opacity, .. }
| VectorShape::Arrow { opacity, .. }
| VectorShape::Star { opacity, .. }
| VectorShape::Polygon{ opacity, .. }
| VectorShape::Rhombus{ opacity, .. }
| VectorShape::Cylinder{ opacity, .. }
| VectorShape::Heart { opacity, .. }
| VectorShape::Bubble { opacity, .. }
| VectorShape::Gear { opacity, .. }
| VectorShape::Cross { opacity, .. }
| VectorShape::Crescent{ opacity, .. }
| VectorShape::Arrow4 { opacity, .. }
| VectorShape::Bolt { opacity, .. }
| 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, .. }
| VectorShape::Circle { opacity, .. }
| VectorShape::Arrow { opacity, .. }
| VectorShape::Star { opacity, .. }
| VectorShape::Polygon{ opacity, .. }
| VectorShape::Rhombus{ opacity, .. }
| VectorShape::Cylinder{ opacity, .. }
| VectorShape::Heart { opacity, .. }
| VectorShape::Bubble { opacity, .. }
| VectorShape::Gear { opacity, .. }
| VectorShape::Cross { opacity, .. }
| VectorShape::Crescent{ opacity, .. }
| VectorShape::Arrow4 { opacity, .. }
| VectorShape::Bolt { opacity, .. }
| VectorShape::FreePath { opacity, .. } => *opacity = clamped,
}
}
pub fn hardness(&self) -> f32 {
match self {
VectorShape::Line { hardness, .. }
| VectorShape::Rect { hardness, .. }
| VectorShape::Circle { hardness, .. }
| VectorShape::Arrow { hardness, .. }
| VectorShape::Star { hardness, .. }
| VectorShape::Polygon{ hardness, .. }
| VectorShape::Rhombus{ hardness, .. }
| VectorShape::Cylinder{ hardness, .. }
| VectorShape::Heart { hardness, .. }
| VectorShape::Bubble { hardness, .. }
| VectorShape::Gear { hardness, .. }
| VectorShape::Cross { hardness, .. }
| VectorShape::Crescent{ hardness, .. }
| VectorShape::Arrow4 { hardness, .. }
| VectorShape::Bolt { hardness, .. }
| 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, .. }
| VectorShape::Circle { hardness, .. }
| VectorShape::Arrow { hardness, .. }
| VectorShape::Star { hardness, .. }
| VectorShape::Polygon{ hardness, .. }
| VectorShape::Rhombus{ hardness, .. }
| VectorShape::Cylinder{ hardness, .. }
| VectorShape::Heart { hardness, .. }
| VectorShape::Bubble { hardness, .. }
| VectorShape::Gear { hardness, .. }
| VectorShape::Cross { hardness, .. }
| VectorShape::Crescent{ hardness, .. }
| VectorShape::Arrow4 { hardness, .. }
| VectorShape::Bolt { hardness, .. }
| VectorShape::FreePath { hardness, .. } => *hardness = clamped,
}
}
pub fn stroke(&self) -> 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, .. }
| VectorShape::Bubble { stroke, .. }
| VectorShape::Gear { stroke, .. }
| VectorShape::Cross { stroke, .. }
| VectorShape::Crescent{ stroke, .. }
| VectorShape::Arrow4 { stroke, .. }
| VectorShape::Bolt { stroke, .. }
| 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, .. }
| VectorShape::Circle { stroke, .. }
| VectorShape::Arrow { stroke, .. }
| VectorShape::Star { stroke, .. }
| VectorShape::Polygon{ stroke, .. }
| VectorShape::Rhombus{ stroke, .. }
| VectorShape::Cylinder{ stroke, .. }
| VectorShape::Heart { stroke, .. }
| VectorShape::Bubble { stroke, .. }
| VectorShape::Gear { stroke, .. }
| VectorShape::Cross { stroke, .. }
| VectorShape::Crescent{ stroke, .. }
| VectorShape::Arrow4 { stroke, .. }
| VectorShape::Bolt { stroke, .. }
| VectorShape::FreePath { stroke, .. } => *stroke = clamped,
}
}
pub fn apply_handle(&mut self, handle: VectorEditHandle, dx: f32, dy: f32, locked_ratio: Option<f32>) {
use VectorEditHandle::*;
if matches!(handle, None | Rotate) { return; }
if let VectorShape::FreePath { pts, .. } = self {
if handle == Move {
for p in pts.iter_mut() { p[0] += dx; p[1] += dy; }
}
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);
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),
BottomRight => (left0, top0),
Move => (0.0, 0.0),
_ => (0.0, 0.0),
};
match handle {
Move => {
left += dx; top += dy; right += dx; bottom += dy;
}
_ => {
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; }
_ => {}
}
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;
} 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;
} 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; }
_ => {}
}
}
}
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;
} else {
*raw_x1 = right; *raw_x2 = left;
}
if y1_orig <= y2_orig {
*raw_y1 = top; *raw_y2 = bottom;
} else {
*raw_y1 = bottom; *raw_y2 = top;
}
}
pub fn angle(&self) -> f32 {
match self {
VectorShape::Line { angle, .. }
| VectorShape::Rect { angle, .. }
| VectorShape::Circle { angle, .. }
| VectorShape::Arrow { angle, .. }
| VectorShape::Star { angle, .. }
| VectorShape::Polygon{ angle, .. }
| VectorShape::Rhombus{ angle, .. }
| VectorShape::Cylinder{ angle, .. }
| VectorShape::Heart { angle, .. }
| VectorShape::Bubble { angle, .. }
| VectorShape::Gear { angle, .. }
| VectorShape::Cross { angle, .. }
| VectorShape::Crescent{ angle, .. }
| VectorShape::Arrow4 { angle, .. }
| VectorShape::Bolt { angle, .. } => *angle,
VectorShape::FreePath { .. } => 0.0,
}
}
pub fn rotate(&mut self, delta: f32) {
match self {
VectorShape::Line { angle, .. }
| VectorShape::Rect { angle, .. }
| VectorShape::Circle { angle, .. }
| VectorShape::Arrow { angle, .. }
| VectorShape::Star { angle, .. }
| VectorShape::Polygon{ angle, .. }
| VectorShape::Rhombus{ angle, .. }
| VectorShape::Cylinder{ angle, .. }
| VectorShape::Heart { angle, .. }
| VectorShape::Bubble { angle, .. }
| VectorShape::Gear { angle, .. }
| VectorShape::Cross { angle, .. }
| VectorShape::Crescent{ angle, .. }
| VectorShape::Arrow4 { angle, .. }
| VectorShape::Bolt { angle, .. } => { *angle += delta; }
VectorShape::FreePath { .. } => {}
}
}
pub fn set_angle(&mut self, new_angle: f32) {
match self {
VectorShape::Line { angle, .. }
| VectorShape::Rect { angle, .. }
| VectorShape::Circle { angle, .. }
| VectorShape::Arrow { angle, .. }
| VectorShape::Star { angle, .. }
| VectorShape::Polygon{ angle, .. }
| VectorShape::Rhombus{ angle, .. }
| VectorShape::Cylinder{ angle, .. }
| VectorShape::Heart { angle, .. }
| VectorShape::Bubble { angle, .. }
| VectorShape::Gear { angle, .. }
| VectorShape::Cross { angle, .. }
| VectorShape::Crescent{ angle, .. }
| VectorShape::Arrow4 { angle, .. }
| VectorShape::Bolt { angle, .. } => { *angle = new_angle; }
VectorShape::FreePath { .. } => {}
}
}
pub fn get_angle_mut(&mut self) -> Option<&mut f32> {
match self {
VectorShape::Line { angle, .. }
| VectorShape::Rect { angle, .. }
| VectorShape::Circle { angle, .. }
| VectorShape::Arrow { angle, .. }
| VectorShape::Star { angle, .. }
| VectorShape::Polygon{ angle, .. }
| VectorShape::Rhombus{ angle, .. }
| VectorShape::Cylinder{ angle, .. }
| VectorShape::Heart { angle, .. }
| VectorShape::Bubble { angle, .. }
| VectorShape::Gear { angle, .. }
| VectorShape::Cross { angle, .. }
| VectorShape::Crescent{ angle, .. }
| VectorShape::Arrow4 { angle, .. }
| VectorShape::Bolt { angle, .. } => Some(angle),
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, .. }
| VectorShape::Bubble { fill, .. }
| VectorShape::Gear { fill, .. }
| VectorShape::Cross { fill, .. }
| VectorShape::Crescent{ fill, .. }
| VectorShape::Arrow4 { fill, .. }
| VectorShape::Bolt { fill, .. } => *fill,
_ => 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, .. }
| VectorShape::Bubble { fill_color, .. }
| VectorShape::Gear { fill_color, .. }
| VectorShape::Cross { fill_color, .. }
| VectorShape::Crescent{ fill_color, .. }
| VectorShape::Arrow4 { fill_color, .. }
| VectorShape::Bolt { fill_color, .. } => Some(*fill_color),
_ => 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, .. }
| VectorShape::Bubble { fill, .. }
| VectorShape::Gear { fill, .. }
| VectorShape::Cross { fill, .. }
| VectorShape::Crescent{ fill, .. }
| VectorShape::Arrow4 { fill, .. }
| VectorShape::Bolt { fill, .. } => Some(*fill),
_ => 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, .. }
| VectorShape::Bubble { fill, .. }
| VectorShape::Gear { fill, .. }
| VectorShape::Cross { fill, .. }
| VectorShape::Crescent{ fill, .. }
| VectorShape::Arrow4 { fill, .. }
| VectorShape::Bolt { fill, .. } => Some(fill),
_ => 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, .. }
| VectorShape::Bubble { fill_color, .. }
| VectorShape::Gear { fill_color, .. }
| VectorShape::Cross { fill_color, .. }
| VectorShape::Crescent{ fill_color, .. }
| VectorShape::Arrow4 { fill_color, .. }
| VectorShape::Bolt { fill_color, .. } => Some(fill_color),
_ => 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, .. }
| VectorShape::Bubble { stroke, .. }
| VectorShape::Gear { stroke, .. }
| VectorShape::Cross { stroke, .. }
| VectorShape::Crescent{ stroke, .. }
| VectorShape::Arrow4 { stroke, .. }
| VectorShape::Bolt { stroke, .. }
| 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;
}
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, .. }
| 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, .. }
| VectorShape::Bubble { x1, y1, x2, y2, .. }
| VectorShape::Gear { x1, y1, x2, y2, .. }
| VectorShape::Cross { x1, y1, x2, y2, .. }
| VectorShape::Crescent{ x1, y1, x2, y2, .. }
| VectorShape::Arrow4 { x1, y1, x2, y2, .. }
| VectorShape::Bolt { x1, y1, x2, y2, .. } => (x1, y1, x2, y2),
VectorShape::FreePath { .. } => unreachable!("coords_mut not valid for FreePath"),
}
}
}