File size: 4,135 Bytes
3d7d9b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager};

/// Export palette colors to various artist application formats
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorExportResult {
    pub format: String,
    pub content: String,
    pub filename: String,
}

#[tauri::command]
pub fn color_export(colors: Vec<String>, format: String) -> Result<ColorExportResult, String> {
    let content = match format.as_str() {
        "hex" => colors.join("\n"),
        "css" => colors.iter().enumerate()
            .map(|(i, c)| format!("  --color-{}: {};", i + 1, c))
            .collect::<Vec<_>>()
            .join("\n")
            .pipe(|body| format!(":root {{\n{body}\n}}")),
        "gpl" => {
            let mut lines = vec!["GIMP Palette".to_string(), "Name: Muse Export".to_string(), "#".to_string()];
            for color in &colors {
                if let Some((r, g, b)) = parse_hex(color) {
                    lines.push(format!("{r:3} {g:3} {b:3}\t{color}"));
                }
            }
            lines.join("\n")
        }
        "ase" => {
            let mut lines = vec!["// Adobe Swatch Exchange (text preview)".to_string()];
            for color in &colors {
                if let Some((r, g, b)) = parse_hex(color) {
                    lines.push(format!("RGB {:.4} {:.4} {:.4} // {color}", r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0));
                }
            }
            lines.join("\n")
        }
        "procreate" => {
            let swatches: Vec<serde_json::Value> = colors.iter().filter_map(|c| {
                parse_hex(c).map(|(r, g, b)| serde_json::json!({
                    "hue": 0, "saturation": 0, "brightness": 0,
                    "alpha": 1, "colorSpace": 0,
                    "components": [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0, 1.0]
                }))
            }).collect();
            serde_json::to_string_pretty(&serde_json::json!({
                "name": "Muse Export",
                "swatches": swatches
            })).unwrap_or_default()
        }
        _ => colors.join("\n"),
    };

    let ext = match format.as_str() {
        "css" => "css", "gpl" => "gpl", "ase" => "ase", "procreate" => "swatches", _ => "txt"
    };

    Ok(ColorExportResult {
        format: format.clone(),
        content,
        filename: format!("muse-palette.{ext}"),
    })
}

#[tauri::command]
pub fn color_search_library(app: AppHandle, hue_min: f32, hue_max: f32) -> Result<Vec<crate::library::LibraryItem>, String> {
    let state = app.state::<crate::library::LibraryState>();
    let items = state.items.lock().map_err(|_| "library lock poisoned")?;
    Ok(items.iter().filter(|item| {
        item.colors.iter().any(|c| {
            if let Some((r, g, b)) = parse_hex(c) {
                let hue = rgb_to_hue(r, g, b);
                if hue_min <= hue_max {
                    hue >= hue_min && hue <= hue_max
                } else {
                    hue >= hue_min || hue <= hue_max
                }
            } else { false }
        })
    }).cloned().collect())
}

fn parse_hex(hex: &str) -> Option<(u8, u8, u8)> {
    let h = hex.trim_start_matches('#');
    if h.len() != 6 { return None; }
    let r = u8::from_str_radix(&h[0..2], 16).ok()?;
    let g = u8::from_str_radix(&h[2..4], 16).ok()?;
    let b = u8::from_str_radix(&h[4..6], 16).ok()?;
    Some((r, g, b))
}

fn rgb_to_hue(r: u8, g: u8, b: u8) -> f32 {
    let rf = r as f32 / 255.0;
    let gf = g as f32 / 255.0;
    let bf = b as f32 / 255.0;
    let max = rf.max(gf).max(bf);
    let min = rf.min(gf).min(bf);
    if max == min { return 0.0; }
    let d = max - min;
    let h = if max == rf { ((gf - bf) / d) % 6.0 }
    else if max == gf { (bf - rf) / d + 2.0 }
    else { (rf - gf) / d + 4.0 };
    ((h * 60.0) + 360.0) % 360.0
}

trait PipeExt { fn pipe(self, f: impl FnOnce(String) -> String) -> String; }
impl PipeExt for String { fn pipe(self, f: impl FnOnce(String) -> String) -> String { f(self) } }