This commit is contained in:
2026-07-09 02:59:53 +03:00
commit 7ace048d94
817 changed files with 234289 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "hcie-protocol"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
hcie-blend = { path = "../hcie-blend" }
[lib]
crate-type = ["rlib"]
[dev-dependencies]
rstest = "0.23"
proptest = "1.5"
approx = "0.5"
+192
View File
@@ -0,0 +1,192 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ContourCurve {
pub points: Vec<(f32, f32)>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GradientDef {
pub color_stops: Vec<(u32, [u8; 4])>,
pub transparency_stops: Vec<(u32, u8)>,
pub midpoint: f32,
pub angle: f32,
pub scale: f32,
pub gradient_type: u32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum BevelStyle {
InnerBevel,
OuterBevel,
Emboss,
PillowEmboss,
StrokeEmboss,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum Technique {
Smooth,
ChiselHard,
ChiselSoft,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum Direction {
Up,
Down,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum StrokePosition {
Inside,
Center,
Outside,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum StrokeFillType {
Color,
Gradient,
Pattern,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LayerEffect {
DropShadow {
enabled: bool,
blend_mode: String,
color: [u8; 4],
opacity: f32,
angle: f32,
distance: f32,
spread: f32,
size: f32,
noise: f32,
contour: Option<ContourCurve>,
},
InnerShadow {
enabled: bool,
blend_mode: String,
color: [u8; 4],
opacity: f32,
angle: f32,
distance: f32,
choke: f32,
size: f32,
noise: f32,
contour: Option<ContourCurve>,
},
OuterGlow {
enabled: bool,
blend_mode: String,
color: [u8; 4],
opacity: f32,
spread: f32,
size: f32,
noise: f32,
contour: Option<ContourCurve>,
},
InnerGlow {
enabled: bool,
blend_mode: String,
color: [u8; 4],
opacity: f32,
choke: f32,
size: f32,
noise: f32,
contour: Option<ContourCurve>,
source: u32,
},
BevelEmboss {
enabled: bool,
style: BevelStyle,
technique: Technique,
depth: f32,
direction: Direction,
size: f32,
soften: f32,
angle: f32,
altitude: f32,
highlight_blend: String,
highlight_color: [u8; 4],
highlight_opacity: f32,
shadow_blend: String,
shadow_color: [u8; 4],
shadow_opacity: f32,
contour: Option<ContourCurve>,
},
Satin {
enabled: bool,
blend_mode: String,
color: [u8; 4],
opacity: f32,
angle: f32,
distance: f32,
size: f32,
invert: bool,
contour: Option<ContourCurve>,
},
ColorOverlay {
enabled: bool,
blend_mode: String,
color: [u8; 4],
opacity: f32,
},
GradientOverlay {
enabled: bool,
blend_mode: String,
opacity: f32,
angle: f32,
scale: f32,
gradient: GradientDef,
},
PatternOverlay {
enabled: bool,
blend_mode: String,
opacity: f32,
scale: f32,
pattern_id: String,
},
Stroke {
enabled: bool,
position: StrokePosition,
fill_type: StrokeFillType,
blend_mode: String,
opacity: f32,
size: f32,
color: [u8; 4],
},
}
impl LayerEffect {
pub fn is_enabled(&self) -> bool {
match self {
Self::DropShadow { enabled, .. }
| Self::InnerShadow { enabled, .. }
| Self::OuterGlow { enabled, .. }
| Self::InnerGlow { enabled, .. }
| Self::BevelEmboss { enabled, .. }
| Self::Satin { enabled, .. }
| Self::ColorOverlay { enabled, .. }
| Self::GradientOverlay { enabled, .. }
| Self::PatternOverlay { enabled, .. }
| Self::Stroke { enabled, .. } => *enabled,
}
}
pub fn effect_name(&self) -> &'static str {
match self {
Self::DropShadow { .. } => "Drop Shadow",
Self::InnerShadow { .. } => "Inner Shadow",
Self::OuterGlow { .. } => "Outer Glow",
Self::InnerGlow { .. } => "Inner Glow",
Self::BevelEmboss { .. } => "Bevel & Emboss",
Self::Satin { .. } => "Satin",
Self::ColorOverlay { .. } => "Color Overlay",
Self::GradientOverlay { .. } => "Gradient Overlay",
Self::PatternOverlay { .. } => "Pattern Overlay",
Self::Stroke { .. } => "Stroke",
}
}
}
File diff suppressed because it is too large Load Diff
+767
View File
@@ -0,0 +1,767 @@
use serde::{Deserialize, Serialize};
// ─────────────────────────────────────────────────────────────────────────────
// Tool — Application tools
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum Tool {
#[default]
Pen,
Brush,
Eraser,
Spray,
FloodFill,
Eyedropper,
VectorLine,
VectorRect,
VectorCircle,
VectorArrow,
VectorStar,
VectorPolygon,
VectorRhombus,
VectorCylinder,
VectorHeart,
VectorBubble,
VectorGear,
VectorCross,
VectorCrescent,
VectorBolt,
VectorArrow4,
VectorSelect,
Text,
Select,
MagicWand,
Lasso,
PolygonSelect,
Gradient,
Move,
Crop,
RedEyeRemoval,
SpotRemoval,
SmartPatch,
AiObjectRemoval,
SmartSelect,
VisionSelect,
}
impl Tool {
pub const ALL: &[Tool] = &[
Tool::Pen, Tool::Brush, Tool::Eraser, Tool::Spray,
Tool::FloodFill, Tool::Eyedropper,
Tool::VectorLine, Tool::VectorRect, Tool::VectorCircle,
Tool::VectorArrow, Tool::VectorStar, Tool::VectorPolygon,
Tool::VectorRhombus, Tool::VectorCylinder, Tool::VectorHeart,
Tool::VectorBubble, Tool::VectorGear, Tool::VectorCross,
Tool::VectorCrescent, Tool::VectorBolt, Tool::VectorArrow4,
Tool::VectorSelect,
Tool::Text,
Tool::Select, Tool::MagicWand, Tool::Lasso, Tool::PolygonSelect,
Tool::Gradient, Tool::Move, Tool::Crop,
Tool::RedEyeRemoval, Tool::SpotRemoval, Tool::SmartPatch,
Tool::AiObjectRemoval, Tool::SmartSelect, Tool::VisionSelect,
];
pub fn label(self) -> &'static str {
match self {
Tool::Pen => "Pen",
Tool::Brush => "Brush",
Tool::Eraser => "Eraser",
Tool::Spray => "Spray",
Tool::FloodFill => "Flood Fill",
Tool::Eyedropper => "Eyedropper",
Tool::VectorLine => "Line",
Tool::VectorRect => "Rectangle",
Tool::VectorCircle => "Circle",
Tool::VectorArrow => "Arrow",
Tool::VectorStar => "Star",
Tool::VectorPolygon => "Polygon",
Tool::VectorRhombus => "Rhombus",
Tool::VectorCylinder => "Cylinder",
Tool::VectorHeart => "Heart",
Tool::VectorBubble => "Speech Bubble",
Tool::VectorGear => "Gear",
Tool::VectorCross => "Cross",
Tool::VectorCrescent => "Crescent",
Tool::VectorBolt => "Bolt",
Tool::VectorArrow4 => "4-Way Arrow",
Tool::VectorSelect => "Vector Select",
Tool::Text => "Text",
Tool::Select => "Select",
Tool::MagicWand => "Magic Wand",
Tool::Lasso => "Lasso",
Tool::PolygonSelect => "Polygon Select",
Tool::Gradient => "Gradient",
Tool::Move => "Move",
Tool::Crop => "Crop",
Tool::RedEyeRemoval => "Red-eye Removal",
Tool::SpotRemoval => "Spot Removal",
Tool::SmartPatch => "Smart Patch",
Tool::AiObjectRemoval => "AI Object Removal",
Tool::SmartSelect => "Smart Select",
Tool::VisionSelect => "Vision Tools",
}
}
pub fn icon(self) -> &'static str {
match self {
Tool::Pen => "",
Tool::Brush => "🖌",
Tool::Eraser => "",
Tool::Spray => "💨",
Tool::FloodFill => "🪣",
Tool::Eyedropper => "🔍",
Tool::VectorLine => "/",
Tool::VectorRect => "",
Tool::VectorCircle => "",
Tool::VectorArrow => "",
Tool::VectorStar => "",
Tool::VectorPolygon => "",
Tool::VectorRhombus => "",
Tool::VectorCylinder => "",
Tool::VectorHeart => "",
Tool::VectorBubble => "💬",
Tool::VectorGear => "",
Tool::VectorCross => "",
Tool::VectorCrescent => "🌙",
Tool::VectorBolt => "",
Tool::VectorArrow4 => "",
Tool::VectorSelect => "",
Tool::Text => "T",
Tool::Select => "",
Tool::MagicWand => "🪄",
Tool::Lasso => "",
Tool::PolygonSelect => "polygon",
Tool::Gradient => "🌈",
Tool::Move => "",
Tool::Crop => "",
Tool::RedEyeRemoval => "👁",
Tool::SpotRemoval => "🩹",
Tool::SmartPatch => "",
Tool::AiObjectRemoval => "🪄",
Tool::SmartSelect => "🎯",
Tool::VisionSelect => "👁",
}
}
pub fn is_raster(self) -> bool {
matches!(self, Tool::Pen | Tool::Brush | Tool::Eraser | Tool::FloodFill | Tool::Spray)
}
pub fn is_ai_tool(self) -> bool {
matches!(self, Tool::SmartPatch | Tool::AiObjectRemoval | Tool::SmartSelect | Tool::VisionSelect)
}
pub fn is_raster_compatible(self) -> bool {
self.is_raster() || self.is_ai_tool() || matches!(self, Tool::SpotRemoval | Tool::RedEyeRemoval)
}
pub fn allowed_layer_type(self) -> Option<super::LayerType> {
if self.is_raster() || self == Tool::Gradient || self.is_ai_tool() {
Some(super::LayerType::Raster)
} else if self.is_shape_tool() {
Some(super::LayerType::Vector)
} else if self == Tool::Text {
Some(super::LayerType::Text)
} else {
None
}
}
pub fn shows_pen_tips(self) -> bool {
matches!(self, Tool::Pen | Tool::Brush | Tool::Eraser | Tool::Spray)
}
pub fn is_selection_tool(self) -> bool {
matches!(self, Tool::Select | Tool::MagicWand | Tool::Lasso | Tool::SmartSelect | Tool::VisionSelect)
}
pub fn is_vector_select(self) -> bool {
self == Tool::VectorSelect
}
pub fn is_shape_tool(self) -> bool {
matches!(self,
Tool::VectorLine | Tool::VectorRect | Tool::VectorCircle |
Tool::VectorArrow | Tool::VectorStar |
Tool::VectorPolygon | Tool::VectorRhombus | Tool::VectorCylinder |
Tool::VectorHeart | Tool::VectorBubble | Tool::VectorGear |
Tool::VectorCross | Tool::VectorCrescent | Tool::VectorBolt |
Tool::VectorArrow4
)
}
pub fn has_separator_before(self) -> bool {
matches!(self, Tool::FloodFill | Tool::VectorLine | Tool::Text | Tool::Select | Tool::Gradient | Tool::RedEyeRemoval | Tool::SmartPatch)
}
pub fn is_vector(self) -> bool {
self.is_shape_tool() || self == Tool::VectorSelect
}
pub fn has_size(self) -> bool {
matches!(self, Tool::Pen | Tool::Brush | Tool::Eraser | Tool::Spray | Tool::SpotRemoval)
}
pub fn has_opacity(self) -> bool {
matches!(self, Tool::Pen | Tool::Brush | Tool::Eraser | Tool::Spray | Tool::Gradient)
}
}
// ─────────────────────────────────────────────────────────────────────────────
// BrushStyle
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum BrushStyle {
#[default]
Default,
Round,
Square,
HardRound,
SoftRound,
Noise,
Texture,
Spray,
Pen,
Oil,
Charcoal,
Leaf,
Rock,
Meadow,
Wood,
Watercolor,
Calligraphy,
Marker,
Sketch,
Hatch,
Star,
Glow,
Airbrush,
Pencil,
Crayon,
WetPaint,
InkPen,
Clouds,
Dirt,
Tree,
Bristle,
Mixer,
Blender,
Bitmap,
}
// ─────────────────────────────────────────────────────────────────────────────
// FilterType
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FilterType {
BoxBlur,
GaussianBlur,
MotionBlur,
UnsharpMask,
Mosaic,
Pinch,
Twirl,
OilPaint,
Crystallize,
Levels,
Vibrance,
Exposure,
Posterize,
Threshold,
ChannelMixer,
BlackAndWhite,
SelectiveColor,
GammaCorrection,
ExtractChannel,
GradientMap,
GaussianBlurGamma,
BoxBlurGamma,
MedianFilter,
Dehaze,
NoisePattern,
}
impl Default for FilterType {
fn default() -> Self { FilterType::GaussianBlur }
}
// ─────────────────────────────────────────────────────────────────────────────
// Config structs
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PenConfig {
pub size: f32,
pub opacity: f32,
pub hardness: f32,
pub blend_mode: super::BlendMode,
}
impl Default for PenConfig {
fn default() -> Self {
Self {
size: 10.0,
opacity: 1.0,
hardness: 1.0,
blend_mode: super::BlendMode::Normal,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrushConfig {
pub size: f32,
pub opacity: f32,
pub hardness: f32,
pub flow: f32,
pub spacing: f32,
pub style: BrushStyle,
pub blend_mode: super::BlendMode,
pub cyclic_color: bool,
pub cyclic_speed: f32,
pub color_variant: bool,
pub variant_amount: f32,
pub density: f32,
pub jitter_amount: f32,
pub scatter_amount: f32,
pub angle: f32,
pub roundness: f32,
pub drawing_angle: bool,
pub rotation_random: f32,
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 BrushConfig {
fn default() -> Self {
Self {
size: 20.0,
opacity: 1.0,
hardness: 0.8,
flow: 1.0,
spacing: 0.25,
style: BrushStyle::Default,
blend_mode: super::BlendMode::Normal,
cyclic_color: false,
cyclic_speed: 0.5,
color_variant: false,
variant_amount: 0.0,
density: 1.0,
jitter_amount: 0.0,
scatter_amount: 0.0,
angle: 0.0,
roundness: 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,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EraserConfig {
pub size: f32,
pub opacity: f32,
pub hardness: f32,
}
impl Default for EraserConfig {
fn default() -> Self {
Self { size: 20.0, opacity: 1.0, hardness: 1.0 }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SprayConfig {
pub radius: f32,
pub dot_size: f32,
pub density: u32,
pub opacity: f32,
pub hardness: f32,
}
impl Default for SprayConfig {
fn default() -> Self {
Self { radius: 50.0, dot_size: 2.0, density: 100, opacity: 1.0, hardness: 0.3 }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FloodFillConfig {
pub tolerance: u8,
}
impl Default for FloodFillConfig {
fn default() -> Self { Self { tolerance: 32 } }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MagicWandConfig {
pub tolerance: u8,
}
impl Default for MagicWandConfig {
fn default() -> Self { Self { tolerance: 32 } }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VectorConfig {
pub stroke_size: f32,
pub opacity: f32,
pub fill_shape: bool,
pub round_rect_radius: f32,
pub hardness: f32,
pub blend_mode: super::BlendMode,
pub line_cap: super::LineCap,
pub star_points: u32,
pub star_inner_radius: f32,
pub polygon_sides: u32,
}
impl Default for VectorConfig {
fn default() -> Self {
Self {
stroke_size: 2.0,
opacity: 1.0,
fill_shape: false,
round_rect_radius: 0.0,
hardness: 0.8,
blend_mode: super::BlendMode::Normal,
line_cap: super::LineCap::Round,
star_points: 5,
star_inner_radius: 0.5,
polygon_sides: 6,
}
}
}
/// Horizontal alignment of a text layer.
///
/// Determines how multi-line text is anchored relative to the layer origin.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum TextAlignment {
#[default]
Left,
Center,
Right,
Justify,
}
impl TextAlignment {
pub const ALL: &[TextAlignment] = &[
TextAlignment::Left,
TextAlignment::Center,
TextAlignment::Right,
TextAlignment::Justify,
];
pub fn label(self) -> &'static str {
match self {
TextAlignment::Left => "Left",
TextAlignment::Center => "Center",
TextAlignment::Right => "Right",
TextAlignment::Justify => "Justify",
}
}
}
/// Text orientation controls whether glyphs are laid out horizontally
/// or stacked vertically.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum TextOrientation {
#[default]
Horizontal,
Vertical,
}
impl TextOrientation {
pub const ALL: &[TextOrientation] = &[
TextOrientation::Horizontal,
TextOrientation::Vertical,
];
pub fn label(self) -> &'static str {
match self {
TextOrientation::Horizontal => "Horizontal",
TextOrientation::Vertical => "Vertical",
}
}
}
/// Non-destructive effect that can be applied to a text layer.
///
/// These effects are stored as editable parameters and rasterized only
/// during compositing, so the original text, font and size remain intact.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum TextEffect {
/// 2D rotation around the layer origin in degrees.
Rotation { angle: f32 },
/// Simple planar warp: kind is a stable identifier (e.g. "arc",
/// "bulge", "flag") and amount controls intensity.
Warp { kind: String, amount: f32 },
/// 3D extrusion parameters. Depth and light direction are preserved
/// as metadata so the effect can be re-rendered with different values.
Extrude3D { depth: f32, angle: f32, light: [f32; 3] },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextToolConfig {
pub size: f32,
pub font: String,
pub color: [u8; 4],
pub angle: f32,
pub alignment: TextAlignment,
pub orientation: TextOrientation,
}
impl Default for TextToolConfig {
fn default() -> Self {
Self {
size: 24.0,
font: "default".to_string(),
color: [0, 0, 0, 255],
angle: 0.0,
alignment: TextAlignment::default(),
orientation: TextOrientation::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum GradientType {
#[default]
Linear,
Radial,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GradientConfig {
pub gradient_type: GradientType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilterConfig {
pub filter_type: FilterType,
pub radius: f32,
pub sigma: f32,
pub amount: f32,
pub angle: f32,
pub distance: u32,
pub cell_size: u32,
}
impl Default for FilterConfig {
fn default() -> Self {
Self {
filter_type: FilterType::GaussianBlur,
radius: 3.0,
sigma: 2.0,
amount: 1.0,
angle: 0.0,
distance: 10,
cell_size: 10,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AdjTab { HueSat, BrightnessContrast }
impl Default for AdjTab {
fn default() -> Self { AdjTab::HueSat }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdjustmentConfig {
pub hue_shift: f32,
pub saturation: f32,
pub lightness: f32,
pub brightness: f32,
pub contrast: f32,
pub tab: AdjTab,
}
impl Default for AdjustmentConfig {
fn default() -> Self {
Self {
hue_shift: 0.0,
saturation: 1.0,
lightness: 0.0,
brightness: 0.0,
contrast: 1.0,
tab: AdjTab::HueSat,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmartSelectConfig {
pub use_ai: bool,
pub tolerance: i32,
}
impl Default for SmartSelectConfig {
fn default() -> Self { Self { use_ai: false, tolerance: 32 } }
}
// ─────────────────────────────────────────────────────────────────────────────
// ToolConfigs — per-tool isolated configuration
// ─────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ToolConfigs {
pub pen: PenConfig,
pub brush: BrushConfig,
pub eraser: EraserConfig,
pub spray: SprayConfig,
pub flood_fill: FloodFillConfig,
pub magic_wand: MagicWandConfig,
pub vector: VectorConfig,
pub text: TextToolConfig,
pub gradient: GradientConfig,
pub smart_select: SmartSelectConfig,
pub filter: FilterConfig,
pub adjustment: AdjustmentConfig,
pub shared: SharedConfig,
}
/// Shared tool configuration (chain/link mechanism).
///
/// Purpose: Allows linking size, opacity, and hardness across tools.
/// When a tool is in the corresponding HashSet, that tool reads/writes
/// the shared global value instead of its own per-tool config value.
///
/// Logic: Each HashSet contains Tool variants that are "linked" for
/// that property. UI toggle buttons (🔗/🔓) add/remove from these sets.
/// Accessor methods (active_size, active_opacity, active_hardness)
/// check the set membership and route to shared or per-tool value.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SharedConfig {
pub size: f32,
pub opacity: f32,
pub hardness: f32,
pub use_shared_size: std::collections::HashSet<Tool>,
pub use_shared_opacity: std::collections::HashSet<Tool>,
pub use_shared_hardness: std::collections::HashSet<Tool>,
}
impl Default for SharedConfig {
fn default() -> Self {
Self {
size: 20.0,
opacity: 1.0,
hardness: 1.0,
use_shared_size: std::collections::HashSet::new(),
use_shared_opacity: std::collections::HashSet::new(),
use_shared_hardness: std::collections::HashSet::new(),
}
}
}
impl ToolConfigs {
/// Get the active size for the current tool, considering shared config.
pub fn active_size(&self, tool: Tool) -> f32 {
if self.shared.use_shared_size.contains(&tool) {
return self.shared.size;
}
match tool {
Tool::Pen => self.pen.size,
Tool::Brush => self.brush.size,
Tool::Eraser => self.eraser.size,
Tool::Spray => self.spray.radius,
t if t.is_shape_tool() => self.vector.stroke_size,
Tool::Text => self.text.size,
_ => self.brush.size,
}
}
/// Get mutable reference to the active size, writing through to shared if linked.
pub fn active_size_mut(&mut self, tool: Tool) -> &mut f32 {
if self.shared.use_shared_size.contains(&tool) {
return &mut self.shared.size;
}
match tool {
Tool::Pen => &mut self.pen.size,
Tool::Brush => &mut self.brush.size,
Tool::Eraser => &mut self.eraser.size,
Tool::Spray => &mut self.spray.radius,
t if t.is_shape_tool() => &mut self.vector.stroke_size,
Tool::Text => &mut self.text.size,
_ => &mut self.brush.size,
}
}
/// Get the active opacity for the current tool, considering shared config.
pub fn active_opacity(&self, tool: Tool) -> f32 {
if self.shared.use_shared_opacity.contains(&tool) {
return self.shared.opacity;
}
match tool {
Tool::Pen => self.pen.opacity,
Tool::Brush => self.brush.opacity,
Tool::Eraser => self.eraser.opacity,
Tool::Spray => self.spray.opacity,
t if t.is_shape_tool() => self.vector.opacity,
_ => self.brush.opacity,
}
}
/// Get mutable reference to the active opacity, writing through to shared if linked.
pub fn active_opacity_mut(&mut self, tool: Tool) -> &mut f32 {
if self.shared.use_shared_opacity.contains(&tool) {
return &mut self.shared.opacity;
}
match tool {
Tool::Pen => &mut self.pen.opacity,
Tool::Brush => &mut self.brush.opacity,
Tool::Eraser => &mut self.eraser.opacity,
Tool::Spray => &mut self.spray.opacity,
t if t.is_shape_tool() => &mut self.vector.opacity,
_ => &mut self.brush.opacity,
}
}
/// Get the active hardness for the current tool, considering shared config.
pub fn active_hardness(&self, tool: Tool) -> f32 {
if self.shared.use_shared_hardness.contains(&tool) {
return self.shared.hardness;
}
match tool {
Tool::Pen => self.pen.hardness,
Tool::Brush => self.brush.hardness,
Tool::Eraser => self.eraser.hardness,
Tool::Spray => self.spray.hardness,
t if t.is_shape_tool() => self.vector.hardness,
_ => self.brush.hardness,
}
}
/// Get mutable reference to the active hardness, writing through to shared if linked.
pub fn active_hardness_mut(&mut self, tool: Tool) -> &mut f32 {
if self.shared.use_shared_hardness.contains(&tool) {
return &mut self.shared.hardness;
}
match tool {
Tool::Pen => &mut self.pen.hardness,
Tool::Brush => &mut self.brush.hardness,
Tool::Eraser => &mut self.eraser.hardness,
Tool::Spray => &mut self.spray.hardness,
t if t.is_shape_tool() => &mut self.vector.hardness,
_ => &mut self.brush.hardness,
}
}
}
+131
View File
@@ -0,0 +1,131 @@
use hcie_protocol::{BlendMode, Tool};
#[test]
fn test_to_u32_from_u32_roundtrip() {
let colors = [
[0, 0, 0, 0],
[255, 255, 255, 255],
[128, 64, 32, 200],
[1, 2, 3, 4],
[255, 0, 0, 128],
];
for &c in &colors {
let packed = hcie_protocol::colors::to_u32(c);
let unpacked = hcie_protocol::colors::from_u32(packed);
assert_eq!(c, unpacked, "round-trip failed for {:?}", c);
}
}
#[test]
fn test_color_constants() {
assert_eq!(hcie_protocol::colors::TRANSPARENT, [0, 0, 0, 0]);
assert_eq!(hcie_protocol::colors::BLACK, [0, 0, 0, 255]);
assert_eq!(hcie_protocol::colors::WHITE, [255, 255, 255, 255]);
assert_eq!(hcie_protocol::colors::RED, [255, 0, 0, 255]);
}
#[test]
fn test_lerp_boundaries() {
assert_eq!(hcie_protocol::lerp(0.0, 100.0, 0.0), 0.0);
assert_eq!(hcie_protocol::lerp(0.0, 100.0, 1.0), 100.0);
assert_eq!(hcie_protocol::lerp(0.0, 100.0, 0.5), 50.0);
}
#[test]
fn test_color_lerp() {
let a = [0, 0, 0, 0];
let b = [100, 200, 50, 255];
let mid = hcie_protocol::colors::lerp(a, b, 0.5);
assert_eq!(mid[0], 50);
assert_eq!(mid[1], 100);
assert_eq!(mid[2], 25);
assert_eq!(mid[3], 128);
}
#[test]
fn test_lerp_clamp() {
assert_eq!(hcie_protocol::lerp(0.0, 100.0, -0.5), 0.0);
assert_eq!(hcie_protocol::lerp(0.0, 100.0, 1.5), 100.0);
}
#[test]
fn test_clamp() {
assert_eq!(hcie_protocol::clamp(5.0, 0.0, 10.0), 5.0);
assert_eq!(hcie_protocol::clamp(-5.0, 0.0, 10.0), 0.0);
assert_eq!(hcie_protocol::clamp(15.0, 0.0, 10.0), 10.0);
}
#[test]
fn test_distance() {
assert_eq!(hcie_protocol::distance(0.0, 0.0, 3.0, 4.0), 5.0);
assert_eq!(hcie_protocol::distance(1.0, 1.0, 1.0, 1.0), 0.0);
}
#[test]
fn test_point_in_rect() {
assert!(hcie_protocol::point_in_rect(5.0, 5.0, 0.0, 0.0, 10.0, 10.0));
assert!(!hcie_protocol::point_in_rect(15.0, 5.0, 0.0, 0.0, 10.0, 10.0));
assert!(hcie_protocol::point_in_rect(5.0, 5.0, 10.0, 10.0, 0.0, 0.0));
}
#[test]
fn test_thumbnail_nearest() {
let input = vec![
255, 0, 0, 255, 0, 255, 0, 255,
0, 0, 255, 255, 255, 255, 0, 255,
];
let thumb = hcie_protocol::thumbnail_nearest(&input, 2, 1, 1, 1);
assert_eq!(thumb.len(), 4, "should produce 1x1 RGBA");
assert_eq!(thumb[3], 255, "alpha should be 255");
}
#[test]
fn test_selection_rect_from_mask() {
let mask = vec![
0, 0, 0, 0,
0, 255, 255, 0,
0, 255, 255, 0,
0, 0, 0, 0,
];
let rect = hcie_protocol::selection_rect_from_mask(&mask, 4, 4);
assert_eq!(rect, Some([1, 1, 2, 2]));
}
#[test]
fn test_blank_mask_no_selection() {
let mask = vec![0u8; 16];
assert!(hcie_protocol::selection_rect_from_mask(&mask, 4, 4).is_none());
}
#[test]
fn test_blend_mode_all_contains_all() {
assert_eq!(BlendMode::ALL.len(), 28, "should have 28 blend modes");
}
#[test]
fn test_all_tools_listed() {
assert_eq!(Tool::ALL.len(), 36, "should have 36 tools");
}
#[test]
fn test_expand_dirty_bounds() {
let mut bounds = None;
hcie_protocol::expand_dirty_bounds(&mut bounds, 10, 10, 5.0, 100, 100);
assert_eq!(bounds, Some([5, 5, 16, 16]));
}
#[test]
fn test_version_string_format() {
let v = hcie_protocol::version_string();
assert!(v.starts_with("3."), "version should start with 3.x.x");
}
proptest::proptest! {
#[test]
fn from_u32_to_u32_roundtrip(r: u8, g: u8, b: u8, a: u8) {
let rgba = [r, g, b, a];
let packed = hcie_protocol::colors::to_u32(rgba);
let unpacked = hcie_protocol::colors::from_u32(packed);
proptest::prop_assert_eq!(rgba, unpacked);
}
}