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}