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