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;
9
10use super::args::TyF64;
11use crate::{
12    errors::{KclError, KclErrorDetails},
13    execution::{
14        types::{ArrayLen, RuntimeType},
15        ExecState, KclValue, SolidOrImportedGeometry,
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
24/// Construct a color from its red, blue and green components.
25pub async fn hex_string(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
26    let rgb: [TyF64; 3] = args.get_unlabeled_kw_arg_typed(
27        "rgb",
28        &RuntimeType::Array(Box::new(RuntimeType::count()), ArrayLen::Known(3)),
29        exec_state,
30    )?;
31
32    // Make sure the color if set is valid.
33    if let Some(component) = rgb.iter().find(|component| component.n < 0.0 || component.n > 255.0) {
34        return Err(KclError::Semantic(KclErrorDetails::new(
35            format!("Colors are given between 0 and 255, so {} is invalid", component.n),
36            vec![args.source_range],
37        )));
38    }
39
40    inner_hex_string(rgb, exec_state, args).await
41}
42
43async fn inner_hex_string(rgb: [TyF64; 3], _: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
44    let [r, g, b] = rgb.map(|n| n.n.floor() as u32);
45    let s = format!("#{r:02x}{g:02x}{b:02x}");
46    Ok(KclValue::String {
47        value: s,
48        meta: args.into(),
49    })
50}
51
52/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
53pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
54    let solids = args.get_unlabeled_kw_arg_typed(
55        "solids",
56        &RuntimeType::Union(vec![RuntimeType::solids(), RuntimeType::imported()]),
57        exec_state,
58    )?;
59
60    let color: String = args.get_kw_arg("color")?;
61    let metalness: Option<TyF64> = args.get_kw_arg_opt_typed("metalness", &RuntimeType::count(), exec_state)?;
62    let roughness: Option<TyF64> = args.get_kw_arg_opt_typed("roughness", &RuntimeType::count(), exec_state)?;
63
64    // Make sure the color if set is valid.
65    if !HEX_REGEX.is_match(&color) {
66        return Err(KclError::Semantic(KclErrorDetails::new(
67            format!("Invalid hex color (`{}`), try something like `#fff000`", color),
68            vec![args.source_range],
69        )));
70    }
71
72    let result = inner_appearance(
73        solids,
74        color,
75        metalness.map(|t| t.n),
76        roughness.map(|t| t.n),
77        exec_state,
78        args,
79    )
80    .await?;
81    Ok(result.into())
82}
83
84/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
85///
86/// This will work on any solid, including extruded solids, revolved solids, and shelled solids.
87/// ```no_run
88/// // Add color to an extruded solid.
89/// exampleSketch = startSketchOn(XZ)
90///   |> startProfile(at = [0, 0])
91///   |> line(endAbsolute = [10, 0])
92///   |> line(endAbsolute = [0, 10])
93///   |> line(endAbsolute = [-10, 0])
94///   |> close()
95///
96/// example = extrude(exampleSketch, length = 5)
97///  // There are other options besides 'color', but they're optional.
98///  |> appearance(color='#ff0000')
99/// ```
100///
101/// ```no_run
102/// // Add color to a revolved solid.
103/// sketch001 = startSketchOn(XY)
104///     |> circle( center = [15, 0], radius = 5 )
105///     |> revolve( angle = 360, axis = Y)
106///     |> appearance(
107///         color = '#ff0000',
108///         metalness = 90,
109///         roughness = 90
110///     )
111/// ```
112///
113/// ```no_run
114/// // Add color to different solids.
115/// fn cube(center) {
116///    return startSketchOn(XY)
117///    |> startProfile(at = [center[0] - 10, center[1] - 10])
118///    |> line(endAbsolute = [center[0] + 10, center[1] - 10])
119///     |> line(endAbsolute = [center[0] + 10, center[1] + 10])
120///     |> line(endAbsolute = [center[0] - 10, center[1] + 10])
121///     |> close()
122///    |> extrude(length = 10)
123/// }
124///
125/// example0 = cube(center = [0, 0])
126/// example1 = cube(center = [20, 0])
127/// example2 = cube(center = [40, 0])
128///
129///  appearance([example0, example1], color='#ff0000', metalness=50, roughness=50)
130///  appearance(example2, color='#00ff00', metalness=50, roughness=50)
131/// ```
132///
133/// ```no_run
134/// // You can set the appearance before or after you shell it will yield the same result.
135/// // This example shows setting the appearance _after_ the shell.
136/// firstSketch = startSketchOn(XY)
137///     |> startProfile(at = [-12, 12])
138///     |> line(end = [24, 0])
139///     |> line(end = [0, -24])
140///     |> line(end = [-24, 0])
141///     |> close()
142///     |> extrude(length = 6)
143///
144/// shell(
145///     firstSketch,
146///     faces = [END],
147///     thickness = 0.25,
148/// )
149///     |> appearance(
150///         color = '#ff0000',
151///         metalness = 90,
152///         roughness = 90
153///     )
154/// ```
155///
156/// ```no_run
157/// // You can set the appearance before or after you shell it will yield the same result.
158/// // This example shows setting the appearance _before_ the shell.
159/// firstSketch = startSketchOn(XY)
160///     |> startProfile(at = [-12, 12])
161///     |> line(end = [24, 0])
162///     |> line(end = [0, -24])
163///     |> line(end = [-24, 0])
164///     |> close()
165///     |> extrude(length = 6)
166///     |> appearance(
167///         color = '#ff0000',
168///         metalness = 90,
169///         roughness = 90
170///     )
171///
172/// shell(
173///     firstSketch,
174///     faces = [END],
175///     thickness = 0.25,
176/// )
177/// ```
178///
179/// ```no_run
180/// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
181/// // This example shows _before_ the pattern.
182/// exampleSketch = startSketchOn(XZ)
183///   |> startProfile(at = [0, 0])
184///   |> line(end = [0, 2])
185///   |> line(end = [3, 1])
186///   |> line(end = [0, -4])
187///   |> close()
188///
189/// example = extrude(exampleSketch, length = 1)
190///     |> appearance(
191///         color = '#ff0000',
192///         metalness = 90,
193///         roughness = 90
194///        )
195///     |> patternLinear3d(
196///         axis = [1, 0, 1],
197///         instances = 7,
198///         distance = 6
199///        )
200/// ```
201///
202/// ```no_run
203/// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
204/// // This example shows _after_ the pattern.
205/// exampleSketch = startSketchOn(XZ)
206///   |> startProfile(at = [0, 0])
207///   |> line(end = [0, 2])
208///   |> line(end = [3, 1])
209///   |> line(end = [0, -4])
210///   |> close()
211///
212/// example = extrude(exampleSketch, length = 1)
213///   |> patternLinear3d(
214///       axis = [1, 0, 1],
215///       instances = 7,
216///       distance = 6
217///      )
218///   |> appearance(
219///       color = '#ff0000',
220///       metalness = 90,
221///       roughness = 90
222///      )
223/// ```
224///
225/// ```no_run
226/// // Color the result of a 2D pattern that was extruded.
227/// exampleSketch = startSketchOn(XZ)
228///   |> startProfile(at = [.5, 25])
229///   |> line(end = [0, 5])
230///   |> line(end = [-1, 0])
231///   |> line(end = [0, -5])
232///   |> close()
233///   |> patternCircular2d(
234///        center = [0, 0],
235///        instances = 13,
236///        arcDegrees = 360,
237///        rotateDuplicates = true
238///      )
239///
240/// example = extrude(exampleSketch, length = 1)
241///     |> appearance(
242///         color = '#ff0000',
243///         metalness = 90,
244///         roughness = 90
245///     )
246/// ```
247///
248/// ```no_run
249/// // Color the result of a sweep.
250///
251/// // Create a path for the sweep.
252/// sweepPath = startSketchOn(XZ)
253///     |> startProfile(at = [0.05, 0.05])
254///     |> line(end = [0, 7])
255///     |> tangentialArc(angle = 90, radius = 5)
256///     |> line(end = [-3, 0])
257///     |> tangentialArc(angle = -90, radius = 5)
258///     |> line(end = [0, 7])
259///
260/// pipeHole = startSketchOn(XY)
261///     |> circle(
262///         center = [0, 0],
263///         radius = 1.5,
264///     )
265///
266/// sweepSketch = startSketchOn(XY)
267///     |> circle(
268///         center = [0, 0],
269///         radius = 2,
270///         )              
271///     |> subtract2d(tool = pipeHole)
272///     |> sweep(path = sweepPath)
273///     |> appearance(
274///         color = "#ff0000",
275///         metalness = 50,
276///         roughness = 50
277///     )
278/// ```
279///
280/// ```no_run
281/// // Change the appearance of an imported model.
282///
283/// import "tests/inputs/cube.sldprt" as cube
284///
285/// cube
286///    |> appearance(
287///        color = "#ff0000",
288///        metalness = 50,
289///        roughness = 50
290///    )
291/// ```
292#[stdlib {
293    name = "appearance",
294    unlabeled_first = true,
295    args = {
296        solids = { docs = "The solid(s) whose appearance is being set" },
297        color = { docs = "Color of the new material, a hex string like '#ff0000'"},
298        metalness = { docs = "Metalness of the new material, a percentage like 95.7." },
299        roughness = { docs = "Roughness of the new material, a percentage like 95.7." },
300    }
301}]
302async fn inner_appearance(
303    solids: SolidOrImportedGeometry,
304    color: String,
305    metalness: Option<f64>,
306    roughness: Option<f64>,
307    exec_state: &mut ExecState,
308    args: Args,
309) -> Result<SolidOrImportedGeometry, KclError> {
310    let mut solids = solids.clone();
311
312    for solid_id in solids.ids(&args.ctx).await? {
313        // Set the material properties.
314        let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
315            KclError::Semantic(KclErrorDetails::new(
316                format!("Invalid hex color (`{color}`): {err}"),
317                vec![args.source_range],
318            ))
319        })?;
320
321        let color = Color {
322            r: rgb.red,
323            g: rgb.green,
324            b: rgb.blue,
325            a: 100.0,
326        };
327
328        args.batch_modeling_cmd(
329            exec_state.next_uuid(),
330            ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
331                object_id: solid_id,
332                color,
333                metalness: metalness.unwrap_or_default() as f32 / 100.0,
334                roughness: roughness.unwrap_or_default() as f32 / 100.0,
335                ambient_occlusion: 0.0,
336            }),
337        )
338        .await?;
339
340        // Idk if we want to actually modify the memory for the colors, but I'm not right now since
341        // I can't think of a use case for it.
342    }
343
344    Ok(solids)
345}