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}