Skip to main content

beacon_calculator/
lib.rs

1//! This is a library for calculating the ideal combination of glass colors to color a Minecraft
2//! beacon.
3//!
4//! # Features
5//! - **Color Approxmation**: Find the best combination of glass colors to approximate a given
6//!     color
7//! - **Pane Calculation**: Calculate the color of the beacon beam with some given glass colors
8//! - **Standard Colors**: Acces the standard Minecraft colors
9//! ## Color Approxmation
10//! `find_combination_default` and it's custom variants are used for approximating a color using
11//! the limited palette of Minecraft colors.
12//! ```
13//! use beacon_calculator::find_combination_default;
14//!
15//! find_combination_default([254, 44, 84]);
16//!
17//! ```
18//! Returns:
19//! ```ignore
20//! Some(Panes {
21//!     panes: [
22//!         "pink",
23//!         "magenta",
24//!         "orange",
25//!         "pink",
26//!         "pink",
27//!         "red",
28//!     ],
29//!     distance: 7.9557647705078125,
30//!     color: PreciseRGB {
31//!         red: 208.5,
32//!         green: 89.90625,
33//!         blue: 95.78125,
34//!     },
35//! })
36//! ```
37//! ### Performance Considerations
38//! When using the custom variants pay attention to what limits you set, as they can get out of
39//! hand rather quickly. A `depth` and `cutoff` of 7 already take longer than 30 seconds on my
40//! machine for example. The accuracy doesn't get much better with values over 6 anyway.
41//!
42//!
43//! ## Get color from panes
44//! `calculate_color_from_panes_default` and it's custom counterpart are used for getting the color
45//! of a `Vec<String>` representing a list of glass colors. Order is important here!
46//! ```
47//! use beacon_calculator::calculate_color_from_panes_default;
48//!
49//! calculate_color_from_panes_default(&[
50//!     "red".to_string(),
51//!     "green".to_string(),
52//!     "red".to_string(),
53//! ]);
54//! ```
55//! Returns:
56//! ```ignore
57//! PreciseRGB {
58//!     red: 155.5,
59//!     green: 65.5,
60//!     blue: 34.0,
61//! }
62//! ```
63//! ## Standard Colors
64//! `get_standard_colors` just gets the standard Minecraft colors in form of a `HashMap<String,
65//! [u8;3]>`
66//! # Optional Features
67//! - `serde`: Derives Serialize and Deserialize for custom datatypes
68
69use color_utils::{calculate_distance, RGB};
70use core::f64;
71use std::{
72    cmp::Ordering,
73    collections::HashMap,
74    sync::mpsc::channel,
75    thread::{self},
76};
77mod color_utils;
78
79/// Calculates the color of some glass panes using custom colors
80pub use color_utils::calculate_color_from_panes as calculate_color_from_panes_custom;
81
82/// Represents a RGB color using f64
83pub use color_utils::PreciseRGB;
84
85/// Represents some glass panes, their DE2000 distance to the target and their calculated color
86#[allow(dead_code)]
87#[derive(Debug, Clone)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub struct Panes {
90    panes: Vec<String>,
91    distance: f64,
92    color: PreciseRGB,
93}
94
95impl PartialEq for Panes {
96    fn eq(&self, other: &Self) -> bool {
97        self.distance == other.distance
98    }
99}
100
101impl Eq for Panes {}
102
103impl PartialOrd for Panes {
104    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
105        Some(self.cmp(other))
106    }
107}
108
109impl Ord for Panes {
110    fn cmp(&self, other: &Self) -> Ordering {
111        self.distance.partial_cmp(&other.distance).unwrap()
112    }
113}
114impl Panes {
115    fn from_panes_vec(
116        panes: &[String],
117        available_colors: &HashMap<String, RGB>,
118        target: RGB,
119    ) -> Self {
120        let color = calculate_color_from_panes_custom(panes, available_colors);
121        let dist = calculate_distance(color, target);
122        Self {
123            panes: panes.to_vec(),
124            distance: dist,
125            color,
126        }
127    }
128}
129/// Gets the standard Minecraft colors
130pub fn get_standard_colors() -> HashMap<String, [u8; 3]> {
131    HashMap::from([
132        ("white".to_string(), [249, 255, 254]),
133        ("orange".to_string(), [249, 128, 29]),
134        ("magenta".to_string(), [199, 78, 189]),
135        ("light_blue".to_string(), [58, 179, 218]),
136        ("yellow".to_string(), [254, 216, 61]),
137        ("lime".to_string(), [128, 199, 31]),
138        ("pink".to_string(), [243, 139, 170]),
139        ("gray".to_string(), [71, 79, 82]),
140        ("light_gray".to_string(), [157, 157, 151]),
141        ("cyan".to_string(), [22, 156, 156]),
142        ("purple".to_string(), [137, 50, 184]),
143        ("blue".to_string(), [60, 68, 170]),
144        ("brown".to_string(), [131, 84, 50]),
145        ("green".to_string(), [94, 124, 22]),
146        ("red".to_string(), [176, 46, 38]),
147        ("black".to_string(), [29, 29, 33]),
148    ])
149}
150
151/// Calculates the color of some glass panes using the default Minecraft colors
152pub fn calculate_color_from_panes_default(panes: &[String]) -> PreciseRGB {
153    calculate_color_from_panes_custom(panes, &convert_colors(&get_standard_colors()))
154}
155
156/// Calculates the most precise combination with the given values (incl. custom Colors)
157#[allow(clippy::implicit_hasher)]
158pub fn find_combination_custom_colors(
159    color: [u8; 3],
160    colors: &HashMap<String, [u8; 3]>,
161    depth: u8,
162    cutoff: u8,
163) -> Option<Panes> {
164    find_combination(color, &convert_colors(colors), depth, cutoff)
165}
166
167/// Calculates the most precise combination with the given values (excl. custom Colors)
168pub fn find_combination_custom(color: [u8; 3], depth: u8, cutoff: u8) -> Option<Panes> {
169    find_combination(
170        color,
171        &convert_colors(&get_standard_colors()),
172        depth,
173        cutoff,
174    )
175}
176
177/// Calculates the most precise combination with the default values
178pub fn find_combination_default(color: [u8; 3]) -> Option<Panes> {
179    let depth = 6;
180    let cutoff = 6;
181    find_combination(
182        color,
183        &convert_colors(&get_standard_colors()),
184        depth,
185        cutoff,
186    )
187}
188
189fn convert_colors(colors: &HashMap<String, [u8; 3]>) -> HashMap<String, RGB> {
190    let mut new_colors = HashMap::new();
191
192    for x in colors {
193        new_colors.insert(x.0.clone(), RGB::new(*x.1));
194    }
195    new_colors
196}
197
198#[allow(clippy::implicit_hasher)]
199fn find_combination(
200    color: [u8; 3],
201    colors: &HashMap<String, RGB>,
202    depth: u8,
203    cutoff: u8,
204) -> Option<Panes> {
205    let color = RGB::new(color);
206    if depth == 0 {
207        return None;
208    }
209    if usize::from(cutoff) >= colors.len() {
210        return None;
211    }
212    let mut all = calculate_combinations_recursively(colors, 0, depth, color, cutoff, &Vec::new());
213    all.sort();
214    //dbg!(&all[0]);
215    Some(all[0].clone())
216}
217
218fn calculate_combinations_recursively(
219    colors: &HashMap<String, RGB>,
220    depth: u8,
221    max_depth: u8,
222    target: RGB,
223    cutoff: u8,
224    base_panes: &[String],
225) -> Vec<Panes> {
226    if depth == max_depth {
227        return vec![Panes::from_panes_vec(base_panes, colors, target)];
228    }
229
230    let mut possibilities = Vec::new();
231
232    let dists = get_distances(base_panes, colors, target);
233    let trimmed_dists = drop_entries(&dists, cutoff);
234    for dist in trimmed_dists {
235        let mut possibility: Vec<String> = base_panes.to_vec();
236        possibility.push(dist.0);
237        possibilities.push(possibility);
238    }
239
240    let mut collection = Vec::new();
241    for possibility in possibilities {
242        collection.extend(calculate_combinations_recursively(
243            colors,
244            depth + 1,
245            max_depth,
246            target,
247            cutoff,
248            &possibility,
249        ));
250    }
251    collection
252}
253
254fn drop_entries(distance_pairs: &[(String, f64)], cutoff: u8) -> Vec<(String, f64)> {
255    let mut distance_pairs = distance_pairs.to_owned();
256    distance_pairs.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
257    distance_pairs
258        .into_iter()
259        .take(usize::from(cutoff))
260        .collect()
261}
262
263fn get_distances(
264    panes: &[String],
265    available_colors: &HashMap<String, RGB>,
266    target: RGB,
267) -> Vec<(String, f64)> {
268    let (sender, reciever) = channel();
269    thread::scope(|x| {
270        let mut panes = panes.to_vec();
271        for color in available_colors {
272            let sender_clone = sender.clone();
273            panes.push(color.0.clone());
274            let new_panes = panes.clone();
275            x.spawn(move || {
276                let _ = sender_clone.send((
277                    color.0.clone(),
278                    calculate_distance(
279                        calculate_color_from_panes_custom(&new_panes, available_colors),
280                        target,
281                    ),
282                ));
283            });
284            panes.pop();
285        }
286        drop(sender);
287    });
288    let mut distance_pairs = Vec::new();
289    for message in reciever {
290        distance_pairs.push(message);
291    }
292    distance_pairs
293}