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