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