mirror of
https://github.com/SpaceManiac/SpacemanDMM.git
synced 2025-12-23 05:36:47 +00:00
Add 514 rgb() support (#254)
Allows for `rgb()` with arguments of form `[r,g,b|h,s,[v|l|y]],(a),(space)` Also allows the full names of the arguments. Technically, BYOND supports just the first letter so `halifax` works for `hue` but fuck that. Also now lints to see if the values are in the valid ranges. Co-authored-by: Tad Hardesty <tad@platymuus.com>
This commit is contained in:
parent
0a108b69af
commit
7e3731ea92
5 changed files with 321 additions and 21 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -1,5 +1,7 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.14.1"
|
||||
|
|
@ -319,6 +321,12 @@ dependencies = [
|
|||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_space"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3776b2bcc4e914db501bb9be9572dd706e344b9eb8f882894f3daa651d281381"
|
||||
|
||||
[[package]]
|
||||
name = "const_fn"
|
||||
version = "0.4.5"
|
||||
|
|
@ -533,6 +541,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"bitflags",
|
||||
"builtins-proc-macro",
|
||||
"color_space",
|
||||
"guard",
|
||||
"interval-tree",
|
||||
"linked-hash-map",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ serde_derive = "1.0.103"
|
|||
toml = "0.5.5"
|
||||
guard = "0.5.0"
|
||||
phf = { version = "0.8.0", features = ["macros"] }
|
||||
color_space = "0.5.3"
|
||||
|
||||
[dependencies.linked-hash-map]
|
||||
git = "https://github.com/SpaceManiac/linked-hash-map"
|
||||
|
|
|
|||
|
|
@ -513,7 +513,7 @@ pub fn register_builtins(tree: &mut ObjectTree) {
|
|||
proc/REGEX_QUOTE_REPLACEMENT(text);
|
||||
proc/replacetext(Haystack,Needle,Replacement,Start=1,End=0);
|
||||
proc/replacetextEx(Haystack,Needle,Replacement,Start=1,End=0);
|
||||
proc/rgb(R,G,B,A=null,space); // special form? - [r,g,b|x,y,z],(a),(space)
|
||||
proc/rgb(R,G,B,A=null,space,red,blue,green,alpha,h,hue,s,saturation,c,chroma,v,value,l,y,luminance); // [r,g,b|h,s,[v|l|y]],(a),(space)
|
||||
proc/rgb2num(color, space);
|
||||
proc/roll(ndice=1,sides); // +1 form
|
||||
proc/round(A,B=null);
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ use std::path::Path;
|
|||
|
||||
use linked_hash_map::LinkedHashMap;
|
||||
use ordered_float::OrderedFloat;
|
||||
use color_space::{Hsl, Hsv, Lch, Rgb};
|
||||
|
||||
use super::{DMError, Location, HasLocation, Context};
|
||||
use super::objtree::*;
|
||||
use super::ast::*;
|
||||
use super::objtree::*;
|
||||
use super::preprocessor::DefineMap;
|
||||
use super::{Context, DMError, HasLocation, Location, Severity};
|
||||
|
||||
/// An absolute typepath and optional variables.
|
||||
///
|
||||
|
|
@ -478,7 +479,7 @@ fn constant_ident_lookup(
|
|||
.cloned()
|
||||
{
|
||||
Some(decl) => decl,
|
||||
None => return Ok(ConstLookup::Continue(None)), // definitely doesn't exist
|
||||
None => return Ok(ConstLookup::Continue(None)), // definitely doesn't exist
|
||||
};
|
||||
|
||||
let type_ = &mut tree[ty];
|
||||
|
|
@ -742,23 +743,7 @@ impl<'a> ConstantFolder<'a> {
|
|||
"cos" => self.trig_op(args, f32::cos)?,
|
||||
"arcsin" => self.trig_op(args, f32::asin)?,
|
||||
"arccos" => self.trig_op(args, f32::acos)?,
|
||||
"rgb" => {
|
||||
use std::fmt::Write;
|
||||
if args.len() != 3 && args.len() != 4 {
|
||||
return Err(self.error(format!("malformed rgb() call, must have 3 or 4 arguments and instead has {}", args.len())));
|
||||
}
|
||||
let mut result = String::with_capacity(7);
|
||||
result.push('#');
|
||||
for each in args {
|
||||
if let Some(i) = self.expr(each, None)?.to_int() {
|
||||
let clamped = std::cmp::max(::std::cmp::min(i, 255), 0);
|
||||
let _ = write!(result, "{:02x}", clamped);
|
||||
} else {
|
||||
return Err(self.error("malformed rgb() call, argument wasn't an int"));
|
||||
}
|
||||
}
|
||||
Constant::String(result)
|
||||
},
|
||||
"rgb" => Constant::String(self.rgb(args)?),
|
||||
"defined" if self.defines.is_some() => {
|
||||
let defines = self.defines.unwrap(); // annoying, but keeps the match clean
|
||||
if args.len() != 1 {
|
||||
|
|
@ -856,4 +841,181 @@ impl<'a> ConstantFolder<'a> {
|
|||
}
|
||||
Err(self.error(format!("unknown variable: {}", ident)))
|
||||
}
|
||||
|
||||
fn rgb(&mut self, args: Vec<Expression>) -> Result<String, DMError> {
|
||||
enum ColorSpace {
|
||||
Rgb = 0,
|
||||
Hsv = 1,
|
||||
Hsl = 2,
|
||||
Hcy = 3,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ColorArgs {
|
||||
r: bool,
|
||||
g: bool,
|
||||
b: bool,
|
||||
h: bool,
|
||||
s: bool,
|
||||
v: bool,
|
||||
l: bool,
|
||||
c: bool,
|
||||
y: bool,
|
||||
a: Option<i32>,
|
||||
}
|
||||
|
||||
if args.len() != 3 && args.len() != 4 && args.len() != 5 {
|
||||
return Err(self.error(format!("malformed rgb() call, must have 3, 4, or 5 arguments and instead has {}", args.len())));
|
||||
}
|
||||
|
||||
let arguments = self.arguments(args)?;
|
||||
let mut space = None;
|
||||
let mut color_args = ColorArgs::default();
|
||||
|
||||
// Get the value of the `space` kwarg if present, or collect which kwargs are set to automatically determine the color space.
|
||||
for (value, potential_kwarg_value) in &arguments {
|
||||
// Check for kwargs if we're in the right form
|
||||
if let Some(kwarg_value) = potential_kwarg_value {
|
||||
if let Some(kwarg) = value.as_str() {
|
||||
match kwarg {
|
||||
"r" | "red" => color_args.r = true,
|
||||
"g" | "green" => color_args.g = true,
|
||||
"b" | "blue" => color_args.b = true,
|
||||
"h" | "hue" => color_args.h = true,
|
||||
"s" | "saturation" => color_args.s = true,
|
||||
"v" | "value" => color_args.v = true,
|
||||
"l" | "luminance" => color_args.l = true,
|
||||
"c" | "chroma" => color_args.c = true,
|
||||
"y" => color_args.y = true,
|
||||
"a" | "alpha" => color_args.a = kwarg_value.to_int(),
|
||||
"space" => match kwarg_value.to_int() { // Do we have an actual colorspace specified? Set the values.
|
||||
Some(0) => space = Some(ColorSpace::Rgb),
|
||||
Some(1) => space = Some(ColorSpace::Hsv),
|
||||
Some(2) => space = Some(ColorSpace::Hsl),
|
||||
Some(3) => space = Some(ColorSpace::Hcy),
|
||||
_ => {
|
||||
return Err(self.error(format!("malformed rgb() call, bad color space: {}", kwarg_value)))
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(self.error(format!("malformed rgb() call, bad kwarg passed: {}", kwarg)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(self.error(format!("malformed rgb() call, kwarg is not string: {}", value)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only set space if it wasn't set manually by the space arg
|
||||
let space = if let Some(space) = space {
|
||||
space
|
||||
} else {
|
||||
if color_args.r || color_args.g || color_args.b {
|
||||
// TODO: Add hint here for useless r/g/b kwarg
|
||||
ColorSpace::Rgb
|
||||
} else if color_args.h {
|
||||
if color_args.c && color_args.y {
|
||||
ColorSpace::Hcy
|
||||
} else if color_args.s {
|
||||
if color_args.v {
|
||||
ColorSpace::Hsv
|
||||
} else if color_args.l {
|
||||
ColorSpace::Hsl
|
||||
} else {
|
||||
return Err(self.error("malformed rgb() call, could not determine space: only h & s specified"));
|
||||
}
|
||||
} else {
|
||||
return Err(self.error("malformed rgb() call, could not determine space: only h specified"));
|
||||
}
|
||||
} else {
|
||||
ColorSpace::Rgb // Default
|
||||
}
|
||||
};
|
||||
|
||||
let mut value_vec: Vec<f64> = vec![];
|
||||
|
||||
for (arg_pos, (value, potential_kwarg_value)) in arguments.iter().enumerate() {
|
||||
let mut to_check = value;
|
||||
|
||||
// Determines the range based on predetermined colorspace
|
||||
let mut range = match arg_pos {
|
||||
0 => match space {
|
||||
ColorSpace::Rgb => 0..=255, //r
|
||||
ColorSpace::Hsv => 0..=360, //h
|
||||
ColorSpace::Hsl => 0..=360, //h
|
||||
ColorSpace::Hcy => 0..=360, //h
|
||||
},
|
||||
1 => match space {
|
||||
ColorSpace::Rgb => 0..=255, //g
|
||||
ColorSpace::Hsv => 0..=100, //s
|
||||
ColorSpace::Hsl => 0..=100, //s
|
||||
ColorSpace::Hcy => 0..=100, //c
|
||||
},
|
||||
2 => match space {
|
||||
ColorSpace::Rgb => 0..=255, //b
|
||||
ColorSpace::Hsv => 0..=100, //v
|
||||
ColorSpace::Hsl => 0..=100, //l
|
||||
ColorSpace::Hcy => 0..=100, //y
|
||||
},
|
||||
_ => 0..=255,
|
||||
};
|
||||
|
||||
// If we have a secondary value, it's a kwarg, we need to get the actual value. If this fails, it's normal.
|
||||
if let Some(kwarg_value) = potential_kwarg_value {
|
||||
if let Some(kwarg) = value.as_str() {
|
||||
to_check = kwarg_value; // Set the value to actually check to be our associated vaue
|
||||
|
||||
range = match kwarg {
|
||||
"r" | "red" | "g" | "green" | "b" | "blue" => 0..=255,
|
||||
"h" | "hue" => 0..=360,
|
||||
"s" | "saturation" => 0..=100,
|
||||
"v" | "value" => 0..=100,
|
||||
"c" | "chroma" => 0..=100,
|
||||
"l" | "y" | "luminance" => 0..=100,
|
||||
"a" | "alpha" => 0..=255,
|
||||
"space" => continue, // Don't range-check the value of the space
|
||||
_ => {
|
||||
return Err(self.error(format!("malformed rgb() call, bad kwarg passed: {}", kwarg)))
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return Err(self.error(format!("malformed rgb() call, kwarg is not string: {}", value)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(i) = to_check.to_int() {
|
||||
if !range.contains(&i) {
|
||||
return Err(self.error(format!("malformed rgb() call, {} is not within the valid range ({}..{})", i, range.start(), range.end()))
|
||||
.set_severity(Severity::Warning)
|
||||
.with_location(self.location)
|
||||
);
|
||||
}
|
||||
let clamped = std::cmp::max(::std::cmp::min(i, *range.end()), *range.start());
|
||||
value_vec.push(clamped.into());
|
||||
} else {
|
||||
return Err(self.error("malformed rgb() call, value wasn't an int"));
|
||||
}
|
||||
}
|
||||
|
||||
assert!(value_vec.len() >= 3); // Make sure we got 3+ values
|
||||
|
||||
// Convert our color given a space to a rgb hexcode
|
||||
let color: Rgb = match space {
|
||||
ColorSpace::Rgb => Rgb::new(value_vec[0], value_vec[1], value_vec[2]),
|
||||
ColorSpace::Hsv => Hsv::new(value_vec[0], value_vec[1] * 0.01, value_vec[2] * 0.01).into(),
|
||||
ColorSpace::Hsl => Hsl::new(value_vec[0], value_vec[1] * 0.01, value_vec[2] * 0.01).into(),
|
||||
ColorSpace::Hcy => Lch::new(value_vec[2], value_vec[1], value_vec[0]).into(),
|
||||
};
|
||||
|
||||
// Extract the raw 4th alpha positional argument if it wasn't a kwarg
|
||||
let alpha = color_args.a.or(value_vec.get(3).map(|&x| x as i32));
|
||||
|
||||
// APPARENTLY the author thinks fractional rgb is a thing, hence the rounding
|
||||
if let Some(alpha) = alpha {
|
||||
Ok(format!("#{:02x}{:02x}{:02x}{:02x}", color.r.round() as u8, color.g.round() as u8, color.b.round() as u8, alpha))
|
||||
} else {
|
||||
Ok(format!("#{:02x}{:02x}{:02x}", color.r.round() as u8, color.g.round() as u8, color.b.round() as u8))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,3 +13,131 @@ fn floating_point_rgb() {
|
|||
Constant::String("#7f7f7f".to_owned()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_base() {
|
||||
let code_good = "rgb(0, 255, 0)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_good.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#00ff00".to_owned()),
|
||||
);
|
||||
let code_good2 = "rgb(50, 50, 50)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_good2.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#323232".to_owned()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_range() {
|
||||
let code_rgb = "rgb(0, 300, 0)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_rgb.as_bytes())
|
||||
.unwrap_err().description(),
|
||||
"malformed rgb() call, 300 is not within the valid range (0..255)",
|
||||
);
|
||||
let code_hsv = "rgb(361, 0, 0, space=1)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsv.as_bytes())
|
||||
.unwrap_err().description(),
|
||||
"malformed rgb() call, 361 is not within the valid range (0..360)",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_args() {
|
||||
let code = "rgb(50)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code.as_bytes())
|
||||
.unwrap_err().description(),
|
||||
"malformed rgb() call, must have 3, 4, or 5 arguments and instead has 1",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_alpha() {
|
||||
let code_hsv = "rgb(h=0, s=0, v=100, a=50, space=1)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsv.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#ffffff32".to_owned()),
|
||||
);
|
||||
let code_hsv2 = "rgb(h=0, s=0, v=100, 50)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsv2.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#ffffff32".to_owned()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_hsv() {
|
||||
let code_hsv = "rgb(h=0, s=0, v=100)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsv.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#ffffff".to_owned()),
|
||||
);
|
||||
let code_hsv2 = "rgb(h=50, s=50, v=50)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsv2.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#807540".to_owned()),
|
||||
);
|
||||
|
||||
let code_hsv_space = "rgb(360, 0, 0, space=1)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsv_space.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#000000".to_owned()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_hsl() {
|
||||
let code_hsl = "rgb(h=0, s=0, l=100)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsl.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#ffffff".to_owned()),
|
||||
);
|
||||
let code_hsl2 = "rgb(h=50, s=50, l=50)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsl2.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#bfaa40".to_owned()),
|
||||
);
|
||||
|
||||
let code_hsl_space = "rgb(360, 0, 0, space=2)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsl_space.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#000000".to_owned()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn rgb_hcy() {
|
||||
let code_hsl = "rgb(h=0, c=0, y=100)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsl.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#ffffff".to_owned()),
|
||||
);
|
||||
let code_hsl2 = "rgb(h=50, c=50, y=50)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsl2.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#b65f37".to_owned()),
|
||||
);
|
||||
|
||||
let code_hsl_space = "rgb(360, 0, 0, space=3)";
|
||||
assert_eq!(
|
||||
dm::constants::evaluate_str(Default::default(), code_hsl_space.as_bytes())
|
||||
.expect("evaluation failed"),
|
||||
Constant::String("#000000".to_owned()),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue