kcl_lib/std/
appearance.rs1use anyhow::Result;
4use kcmc::{ModelingCmd, each_cmd as mcmd};
5use kittycad_modeling_cmds::{self as kcmc, shared::Color};
6use regex::Regex;
7use rgba_simple::Hex;
8
9use super::args::TyF64;
10use crate::{
11 errors::{KclError, KclErrorDetails},
12 execution::{
13 ExecState, KclValue, ModelingCmdMeta, SolidOrImportedGeometry,
14 types::{ArrayLen, RuntimeType},
15 },
16 std::Args,
17};
18
19lazy_static::lazy_static! {
20 static ref HEX_REGEX: Regex = Regex::new(r"^#[0-9a-fA-F]{6}$").unwrap();
21}
22
23const DEFAULT_ROUGHNESS: f64 = 1.0;
24const DEFAULT_METALNESS: f64 = 0.0;
25
26pub async fn hex_string(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
28 let rgb: [TyF64; 3] = args.get_unlabeled_kw_arg(
29 "rgb",
30 &RuntimeType::Array(Box::new(RuntimeType::count()), ArrayLen::Known(3)),
31 exec_state,
32 )?;
33
34 if let Some(component) = rgb.iter().find(|component| component.n < 0.0 || component.n > 255.0) {
36 return Err(KclError::new_semantic(KclErrorDetails::new(
37 format!("Colors are given between 0 and 255, so {} is invalid", component.n),
38 vec![args.source_range],
39 )));
40 }
41
42 inner_hex_string(rgb, exec_state, args).await
43}
44
45async fn inner_hex_string(rgb: [TyF64; 3], _: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
46 let [r, g, b] = rgb.map(|n| n.n.floor() as u32);
47 let s = format!("#{r:02x}{g:02x}{b:02x}");
48 Ok(KclValue::String {
49 value: s,
50 meta: args.into(),
51 })
52}
53
54pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
56 let solids = args.get_unlabeled_kw_arg(
57 "solids",
58 &RuntimeType::Union(vec![RuntimeType::solids(), RuntimeType::imported()]),
59 exec_state,
60 )?;
61
62 let color: String = args.get_kw_arg("color", &RuntimeType::string(), exec_state)?;
63 let metalness: Option<TyF64> = args.get_kw_arg_opt("metalness", &RuntimeType::count(), exec_state)?;
64 let roughness: Option<TyF64> = args.get_kw_arg_opt("roughness", &RuntimeType::count(), exec_state)?;
65
66 if !HEX_REGEX.is_match(&color) {
68 return Err(KclError::new_semantic(KclErrorDetails::new(
69 format!("Invalid hex color (`{color}`), try something like `#fff000`"),
70 vec![args.source_range],
71 )));
72 }
73
74 let result = inner_appearance(
75 solids,
76 color,
77 metalness.map(|t| t.n),
78 roughness.map(|t| t.n),
79 exec_state,
80 args,
81 )
82 .await?;
83 Ok(result.into())
84}
85
86async fn inner_appearance(
87 solids: SolidOrImportedGeometry,
88 color: String,
89 metalness: Option<f64>,
90 roughness: Option<f64>,
91 exec_state: &mut ExecState,
92 args: Args,
93) -> Result<SolidOrImportedGeometry, KclError> {
94 let mut solids = solids.clone();
95
96 for solid_id in solids.ids(&args.ctx).await? {
97 let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
99 KclError::new_semantic(KclErrorDetails::new(
100 format!("Invalid hex color (`{color}`): {err}"),
101 vec![args.source_range],
102 ))
103 })?;
104
105 let color = Color {
106 r: rgb.red,
107 g: rgb.green,
108 b: rgb.blue,
109 a: 100.0,
110 };
111
112 exec_state
113 .batch_modeling_cmd(
114 ModelingCmdMeta::from_args(exec_state, &args),
115 ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
116 object_id: solid_id,
117 color,
118 backface_color: None,
119 metalness: metalness.unwrap_or(DEFAULT_METALNESS) as f32 / 100.0,
120 roughness: roughness.unwrap_or(DEFAULT_ROUGHNESS) as f32 / 100.0,
121 ambient_occlusion: 0.0,
122 }),
123 )
124 .await?;
125
126 }
129
130 Ok(solids)
131}