1use anyhow::Result;
4use kcl_error::CompilationError;
5use kcmc::{ModelingCmd, each_cmd as mcmd};
6use kittycad_modeling_cmds::{self as kcmc, shared::Color};
7use regex::Regex;
8use rgba_simple::Hex;
9
10use super::args::TyF64;
11use crate::{
12 errors::{KclError, KclErrorDetails},
13 execution::{
14 ExecState, KclValue, ModelingCmdMeta, SolidOrImportedGeometry, annotations,
15 types::{ArrayLen, RuntimeType},
16 },
17 std::Args,
18};
19
20lazy_static::lazy_static! {
21 static ref HEX_REGEX: Regex = Regex::new(r"^#[0-9a-fA-F]{6}$").unwrap();
22}
23
24const DEFAULT_ROUGHNESS: f64 = 1.0;
25const DEFAULT_METALNESS: f64 = 0.0;
26
27pub async fn hex_string(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
29 let rgb: [TyF64; 3] = args.get_unlabeled_kw_arg(
30 "rgb",
31 &RuntimeType::Array(Box::new(RuntimeType::count()), ArrayLen::Known(3)),
32 exec_state,
33 )?;
34
35 if let Some(component) = rgb.iter().find(|component| component.n < 0.0 || component.n > 255.0) {
37 return Err(KclError::new_semantic(KclErrorDetails::new(
38 format!("Colors are given between 0 and 255, so {} is invalid", component.n),
39 vec![args.source_range],
40 )));
41 }
42
43 inner_hex_string(rgb, exec_state, args).await
44}
45
46async fn inner_hex_string(rgb: [TyF64; 3], _: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
47 let [r, g, b] = rgb.map(|n| n.n.floor() as u32);
48 let s = format!("#{r:02x}{g:02x}{b:02x}");
49 Ok(KclValue::String {
50 value: s,
51 meta: args.into(),
52 })
53}
54
55pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
57 let solids = args.get_unlabeled_kw_arg(
58 "solids",
59 &RuntimeType::Union(vec![RuntimeType::solids(), RuntimeType::imported()]),
60 exec_state,
61 )?;
62
63 let color: String = args.get_kw_arg("color", &RuntimeType::string(), exec_state)?;
64 let metalness: Option<TyF64> = args.get_kw_arg_opt("metalness", &RuntimeType::count(), exec_state)?;
65 let roughness: Option<TyF64> = args.get_kw_arg_opt("roughness", &RuntimeType::count(), exec_state)?;
66 let opacity: Option<TyF64> = args.get_kw_arg_opt("opacity", &RuntimeType::count(), exec_state)?;
67
68 if !HEX_REGEX.is_match(&color) {
70 return Err(KclError::new_semantic(KclErrorDetails::new(
71 format!("Invalid hex color (`{color}`), try something like `#fff000`"),
72 vec![args.source_range],
73 )));
74 }
75
76 let result = inner_appearance(
77 solids,
78 color,
79 metalness.map(|t| t.n),
80 roughness.map(|t| t.n),
81 opacity.map(|t| t.n),
82 exec_state,
83 args,
84 )
85 .await?;
86 Ok(result.into())
87}
88
89async fn inner_appearance(
90 solids: SolidOrImportedGeometry,
91 color: String,
92 metalness: Option<f64>,
93 roughness: Option<f64>,
94 opacity: Option<f64>,
95 exec_state: &mut ExecState,
96 args: Args,
97) -> Result<SolidOrImportedGeometry, KclError> {
98 let mut solids = solids.clone();
99
100 let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
102 KclError::new_semantic(KclErrorDetails::new(
103 format!("Invalid hex color (`{color}`): {err}"),
104 vec![args.source_range],
105 ))
106 })?;
107 let percent_range = (0.0)..=100.0;
108 let zero_one_range = (0.0)..=1.0;
109 for (prop, val) in [("Metalness", metalness), ("Roughness", roughness), ("Opacity", opacity)] {
110 if let Some(x) = val {
111 if !(percent_range.contains(&x)) {
112 return Err(KclError::new_semantic(KclErrorDetails::new(
113 format!("{prop} must be between 0 and 100, but it was {x}"),
114 vec![args.source_range],
115 )));
116 }
117 if zero_one_range.contains(&x) && x != 0.0 {
118 exec_state.warn(
119 CompilationError::err(args.source_range, "This looks like you're setting a property to a number between 0 and 1, but the property should be between 0 and 100.".to_string()),
120 annotations::WARN_SHOULD_BE_PERCENTAGE,
121 );
122 }
123 }
124 }
125
126 let mut needs_oit = false;
130 let opacity_param = if let Some(opacity) = opacity {
131 if opacity < 100.0 && args.ctx.settings.enable_ssao {
134 needs_oit = true;
135 }
136 opacity / 100.0
137 } else {
138 1.0
139 };
140 let color = Color::from_rgba(rgb.red, rgb.green, rgb.blue, opacity_param as f32);
141
142 if needs_oit {
143 exec_state
145 .batch_modeling_cmd(
146 ModelingCmdMeta::from_args(exec_state, &args),
147 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(true).build()),
148 )
149 .await?;
150 }
151
152 for solid_id in solids.ids(&args.ctx).await? {
153 exec_state
154 .batch_modeling_cmd(
155 ModelingCmdMeta::from_args(exec_state, &args),
156 ModelingCmd::from(
157 mcmd::ObjectSetMaterialParamsPbr::builder()
158 .object_id(solid_id)
159 .color(color)
160 .metalness(metalness.unwrap_or(DEFAULT_METALNESS) as f32 / 100.0)
161 .roughness(roughness.unwrap_or(DEFAULT_ROUGHNESS) as f32 / 100.0)
162 .ambient_occlusion(0.0)
163 .build(),
164 ),
165 )
166 .await?;
167
168 }
171
172 Ok(solids)
173}