kcl_lib/std/
appearance.rs

1//! Standard library appearance.
2
3use anyhow::Result;
4use kcl_derive_docs::stdlib;
5use kcmc::{each_cmd as mcmd, ModelingCmd};
6use kittycad_modeling_cmds::{self as kcmc, shared::Color};
7use regex::Regex;
8use rgba_simple::Hex;
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use validator::Validate;
12
13use crate::{
14    errors::{KclError, KclErrorDetails},
15    execution::{ExecState, KclValue, Solid, SolidSet},
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/// Data for appearance.
24#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Validate)]
25#[ts(export)]
26#[serde(rename_all = "camelCase")]
27struct AppearanceData {
28    /// Color of the new material, a hex string like "#ff0000".
29    #[schemars(regex(pattern = "#[0-9a-fA-F]{6}"))]
30    pub color: String,
31    /// Metalness of the new material, a percentage like 95.7.
32    #[validate(range(min = 0.0, max = 100.0))]
33    pub metalness: Option<f64>,
34    /// Roughness of the new material, a percentage like 95.7.
35    #[validate(range(min = 0.0, max = 100.0))]
36    pub roughness: Option<f64>,
37    // TODO(jess): we can also ambient occlusion here I just don't know what it is.
38}
39
40/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
41pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
42    let solid_set: SolidSet = args.get_unlabeled_kw_arg("solidSet")?;
43
44    let color: String = args.get_kw_arg("color")?;
45    let metalness: Option<f64> = args.get_kw_arg_opt("metalness")?;
46    let roughness: Option<f64> = args.get_kw_arg_opt("roughness")?;
47    let data = AppearanceData {
48        color,
49        metalness,
50        roughness,
51    };
52
53    // Validate the data.
54    data.validate().map_err(|err| {
55        KclError::Semantic(KclErrorDetails {
56            message: format!("Invalid appearance data: {}", err),
57            source_ranges: vec![args.source_range],
58        })
59    })?;
60
61    // Make sure the color if set is valid.
62    if !HEX_REGEX.is_match(&data.color) {
63        return Err(KclError::Semantic(KclErrorDetails {
64            message: format!("Invalid hex color (`{}`), try something like `#fff000`", data.color),
65            source_ranges: vec![args.source_range],
66        }));
67    }
68
69    let result = inner_appearance(solid_set, data.color, data.metalness, data.roughness, args).await?;
70    Ok(result.into())
71}
72
73/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
74///
75/// This will work on any solid, including extruded solids, revolved solids, and shelled solids.
76/// ```no_run
77/// // Add color to an extruded solid.
78/// exampleSketch = startSketchOn("XZ")
79///   |> startProfileAt([0, 0], %)
80///   |> line(endAbsolute = [10, 0])
81///   |> line(endAbsolute = [0, 10])
82///   |> line(endAbsolute = [-10, 0])
83///   |> close()
84///
85/// example = extrude(exampleSketch, length = 5)
86///  // There are other options besides 'color', but they're optional.
87///  |> appearance(color='#ff0000')
88/// ```
89///
90/// ```no_run
91/// // Add color to a revolved solid.
92/// sketch001 = startSketchOn('XY')
93///     |> circle( center = [15, 0], radius = 5 )
94///     |> revolve({ angle = 360, axis = 'y' }, %)
95///     |> appearance(
96///         color = '#ff0000',
97///         metalness = 90,
98///         roughness = 90
99///     )
100/// ```
101///
102/// ```no_run
103/// // Add color to different solids.
104/// fn cube(center) {
105///    return startSketchOn('XY')
106///    |> startProfileAt([center[0] - 10, center[1] - 10], %)
107///    |> line(endAbsolute = [center[0] + 10, center[1] - 10])
108///     |> line(endAbsolute = [center[0] + 10, center[1] + 10])
109///     |> line(endAbsolute = [center[0] - 10, center[1] + 10])
110///     |> close()
111///    |> extrude(length = 10)
112/// }
113///
114/// example0 = cube([0, 0])
115/// example1 = cube([20, 0])
116/// example2 = cube([40, 0])
117///
118///  appearance([example0, example1], color='#ff0000', metalness=50, roughness=50)
119///  appearance(example2, color='#00ff00', metalness=50, roughness=50)
120/// ```
121///
122/// ```no_run
123/// // You can set the appearance before or after you shell it will yield the same result.
124/// // This example shows setting the appearance _after_ the shell.
125/// firstSketch = startSketchOn('XY')
126///     |> startProfileAt([-12, 12], %)
127///     |> line(end = [24, 0])
128///     |> line(end = [0, -24])
129///     |> line(end = [-24, 0])
130///     |> close()
131///     |> extrude(length = 6)
132///
133/// shell(
134///     firstSketch,
135///     faces = ['end'],
136///     thickness = 0.25,
137/// )
138///     |> appearance(
139///         color = '#ff0000',
140///         metalness = 90,
141///         roughness = 90
142///     )
143/// ```
144///
145/// ```no_run
146/// // You can set the appearance before or after you shell it will yield the same result.
147/// // This example shows setting the appearance _before_ the shell.
148/// firstSketch = startSketchOn('XY')
149///     |> startProfileAt([-12, 12], %)
150///     |> line(end = [24, 0])
151///     |> line(end = [0, -24])
152///     |> line(end = [-24, 0])
153///     |> close()
154///     |> extrude(length = 6)
155///     |> appearance(
156///         color = '#ff0000',
157///         metalness = 90,
158///         roughness = 90
159///     )
160///
161/// shell(
162///     firstSketch,
163///     faces = ['end'],
164///     thickness = 0.25,
165/// )
166/// ```
167///
168/// ```no_run
169/// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
170/// // This example shows _before_ the pattern.
171/// exampleSketch = startSketchOn('XZ')
172///   |> startProfileAt([0, 0], %)
173///   |> line(end = [0, 2])
174///   |> line(end = [3, 1])
175///   |> line(end = [0, -4])
176///   |> close()
177///
178/// example = extrude(exampleSketch, length = 1)
179///     |> appearance(
180///         color = '#ff0000',
181///         metalness = 90,
182///         roughness = 90
183///        )
184///     |> patternLinear3d(
185///         axis = [1, 0, 1],
186///         instances = 7,
187///         distance = 6
188///        )
189/// ```
190///
191/// ```no_run
192/// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
193/// // This example shows _after_ the pattern.
194/// exampleSketch = startSketchOn('XZ')
195///   |> startProfileAt([0, 0], %)
196///   |> line(end = [0, 2])
197///   |> line(end = [3, 1])
198///   |> line(end = [0, -4])
199///   |> close()
200///
201/// example = extrude(exampleSketch, length = 1)
202///   |> patternLinear3d(
203///       axis = [1, 0, 1],
204///       instances = 7,
205///       distance = 6
206///      )
207///   |> appearance(
208///       color = '#ff0000',
209///       metalness = 90,
210///       roughness = 90
211///      )
212/// ```
213///
214/// ```no_run
215/// // Color the result of a 2D pattern that was extruded.
216/// exampleSketch = startSketchOn('XZ')
217///   |> startProfileAt([.5, 25], %)
218///   |> line(end = [0, 5])
219///   |> line(end = [-1, 0])
220///   |> line(end = [0, -5])
221///   |> close()
222///   |> patternCircular2d(
223///        center = [0, 0],
224///        instances = 13,
225///        arcDegrees = 360,
226///        rotateDuplicates = true
227///      )
228///
229/// example = extrude(exampleSketch, length = 1)
230///     |> appearance(
231///         color = '#ff0000',
232///         metalness = 90,
233///         roughness = 90
234///     )
235/// ```
236///
237/// ```no_run
238/// // Color the result of a sweep.
239///
240/// // Create a path for the sweep.
241/// sweepPath = startSketchOn('XZ')
242///     |> startProfileAt([0.05, 0.05], %)
243///     |> line(end = [0, 7])
244///     |> tangentialArc({
245///         offset: 90,
246///         radius: 5
247///     }, %)
248///     |> line(end = [-3, 0])
249///     |> tangentialArc({
250///         offset: -90,
251///         radius: 5
252///     }, %)
253///     |> line(end = [0, 7])
254///
255/// pipeHole = startSketchOn('XY')
256///     |> circle(
257///         center = [0, 0],
258///         radius = 1.5,
259///     )
260///
261/// sweepSketch = startSketchOn('XY')
262///     |> circle(
263///         center = [0, 0],
264///         radius = 2,
265///         )              
266///     |> hole(pipeHole, %)
267///     |> sweep(path = sweepPath)
268///     |> appearance(
269///         color = "#ff0000",
270///         metalness = 50,
271///         roughness = 50
272///     )
273/// ```
274#[stdlib {
275    name = "appearance",
276    keywords = true,
277    unlabeled_first = true,
278    args = {
279        solid_set = { docs = "The solid(s) whose appearance is being set" },
280        color = { docs = "Color of the new material, a hex string like '#ff0000'"},
281        metalness = { docs = "Metalness of the new material, a percentage like 95.7." },
282        roughness = { docs = "Roughness of the new material, a percentage like 95.7." },
283    }
284}]
285async fn inner_appearance(
286    solid_set: SolidSet,
287    color: String,
288    metalness: Option<f64>,
289    roughness: Option<f64>,
290    args: Args,
291) -> Result<SolidSet, KclError> {
292    let solids: Vec<Box<Solid>> = solid_set.into();
293
294    for solid in &solids {
295        // Set the material properties.
296        let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
297            KclError::Semantic(KclErrorDetails {
298                message: format!("Invalid hex color (`{color}`): {err}"),
299                source_ranges: vec![args.source_range],
300            })
301        })?;
302
303        let color = Color {
304            r: rgb.red,
305            g: rgb.green,
306            b: rgb.blue,
307            a: 100.0,
308        };
309
310        args.batch_modeling_cmd(
311            uuid::Uuid::new_v4(),
312            ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
313                object_id: solid.id,
314                color,
315                metalness: metalness.unwrap_or_default() as f32 / 100.0,
316                roughness: roughness.unwrap_or_default() as f32 / 100.0,
317                ambient_occlusion: 0.0,
318            }),
319        )
320        .await?;
321
322        // Idk if we want to actually modify the memory for the colors, but I'm not right now since
323        // I can't think of a use case for it.
324    }
325
326    Ok(SolidSet::from(solids))
327}