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