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