kcl_lib/std/
appearance.rs

1//! Standard library appearance.
2
3use 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, 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
23/// Construct a color from its red, blue and green components.
24pub async fn hex_string(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
25    let rgb: [TyF64; 3] = args.get_unlabeled_kw_arg(
26        "rgb",
27        &RuntimeType::Array(Box::new(RuntimeType::count()), ArrayLen::Known(3)),
28        exec_state,
29    )?;
30
31    // Make sure the color if set is valid.
32    if let Some(component) = rgb.iter().find(|component| component.n < 0.0 || component.n > 255.0) {
33        return Err(KclError::new_semantic(KclErrorDetails::new(
34            format!("Colors are given between 0 and 255, so {} is invalid", component.n),
35            vec![args.source_range],
36        )));
37    }
38
39    inner_hex_string(rgb, exec_state, args).await
40}
41
42async fn inner_hex_string(rgb: [TyF64; 3], _: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
43    let [r, g, b] = rgb.map(|n| n.n.floor() as u32);
44    let s = format!("#{r:02x}{g:02x}{b:02x}");
45    Ok(KclValue::String {
46        value: s,
47        meta: args.into(),
48    })
49}
50
51/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
52pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
53    let solids = args.get_unlabeled_kw_arg(
54        "solids",
55        &RuntimeType::Union(vec![RuntimeType::solids(), RuntimeType::imported()]),
56        exec_state,
57    )?;
58
59    let color: String = args.get_kw_arg("color", &RuntimeType::string(), exec_state)?;
60    let metalness: Option<TyF64> = args.get_kw_arg_opt("metalness", &RuntimeType::count(), exec_state)?;
61    let roughness: Option<TyF64> = args.get_kw_arg_opt("roughness", &RuntimeType::count(), exec_state)?;
62
63    // Make sure the color if set is valid.
64    if !HEX_REGEX.is_match(&color) {
65        return Err(KclError::new_semantic(KclErrorDetails::new(
66            format!("Invalid hex color (`{color}`), try something like `#fff000`"),
67            vec![args.source_range],
68        )));
69    }
70
71    let result = inner_appearance(
72        solids,
73        color,
74        metalness.map(|t| t.n),
75        roughness.map(|t| t.n),
76        exec_state,
77        args,
78    )
79    .await?;
80    Ok(result.into())
81}
82
83async fn inner_appearance(
84    solids: SolidOrImportedGeometry,
85    color: String,
86    metalness: Option<f64>,
87    roughness: Option<f64>,
88    exec_state: &mut ExecState,
89    args: Args,
90) -> Result<SolidOrImportedGeometry, KclError> {
91    let mut solids = solids.clone();
92
93    for solid_id in solids.ids(&args.ctx).await? {
94        // Set the material properties.
95        let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
96            KclError::new_semantic(KclErrorDetails::new(
97                format!("Invalid hex color (`{color}`): {err}"),
98                vec![args.source_range],
99            ))
100        })?;
101
102        let color = Color {
103            r: rgb.red,
104            g: rgb.green,
105            b: rgb.blue,
106            a: 100.0,
107        };
108
109        exec_state
110            .batch_modeling_cmd(
111                (&args).into(),
112                ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
113                    object_id: solid_id,
114                    color,
115                    metalness: metalness.unwrap_or_default() as f32 / 100.0,
116                    roughness: roughness.unwrap_or_default() as f32 / 100.0,
117                    ambient_occlusion: 0.0,
118                }),
119            )
120            .await?;
121
122        // Idk if we want to actually modify the memory for the colors, but I'm not right now since
123        // I can't think of a use case for it.
124    }
125
126    Ok(solids)
127}