Skip to main content

kcl_lib/std/
appearance.rs

1//! Standard library appearance.
2
3use 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
27/// Construct a color from its red, blue and green components.
28pub 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    // Make sure the color if set is valid.
36    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
55/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
56pub 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    // Make sure the color if set is valid.
69    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    // Set the material properties.
101    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    // OIT (order-independent transparency) is required to show transparency.
127    // But it degrades engine performance. So only enable it if necessary,
128    // i.e. if user has chosen to make something transparent.
129    let mut needs_oit = false;
130    let opacity_param = if let Some(opacity) = opacity {
131        // The engine errors out if you toggle OIT with SSAO off.
132        // So ignore OIT settings if SSAO is off.
133        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        // TODO: Emit a warning annotation if SSAO is disabled.
144        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        // Idk if we want to actually modify the memory for the colors, but I'm not right now since
169        // I can't think of a use case for it.
170    }
171
172    Ok(solids)
173}