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}