1914 lines
82 KiB
Rust
1914 lines
82 KiB
Rust
|
|
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"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|