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, 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
26/// Construct a color from its red, blue and green components.
27pub 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    // Make sure the color if set is valid.
35    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
54/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
55pub 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    // Make sure the color if set is valid.
67    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        // Set the material properties.
98        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        // Idk if we want to actually modify the memory for the colors, but I'm not right now since
127        // I can't think of a use case for it.
128    }
129
130    Ok(solids)
131}