start of Appearance function (#4743)

* initial commit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add more samples

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updatres

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* regenerate docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* patterns and appearance samples

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fmt

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2024-12-10 17:51:51 -08:00
committed by GitHub
parent 671c01e36f
commit 53d40301dc
23 changed files with 3435 additions and 7 deletions

View File

@ -22,3 +22,5 @@ once fixed in engine will just start working here with no language changes.
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple - **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
chamfer cases work currently. chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work.

210
docs/kcl/appearance.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,7 @@ layout: manual
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects) * [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
* [`angledLineToX`](kcl/angledLineToX) * [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY) * [`angledLineToY`](kcl/angledLineToY)
* [`appearance`](kcl/appearance)
* [`arc`](kcl/arc) * [`arc`](kcl/arc)
* [`arcTo`](kcl/arcTo) * [`arcTo`](kcl/arcTo)
* [`asin`](kcl/asin) * [`asin`](kcl/asin)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
---
title: "AppearanceData"
excerpt: "Data for appearance."
layout: manual
---
Data for appearance.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |

View File

@ -68,7 +68,7 @@
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite --port=3000 --host=0.0.0.0",
"start:prod": "vite preview --port=3000", "start:prod": "vite preview --port=3000",
"serve": "vite serve --port=3000", "serve": "vite serve --port=3000",
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build", "build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",

View File

@ -1721,7 +1721,9 @@ dependencies = [
"parse-display 0.9.1", "parse-display 0.9.1",
"pretty_assertions", "pretty_assertions",
"pyo3", "pyo3",
"regex",
"reqwest", "reqwest",
"rgba_simple",
"ropey", "ropey",
"schemars", "schemars",
"serde", "serde",
@ -2971,6 +2973,12 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
] ]
[[package]]
name = "rgba_simple"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6cd655523701785087f69900df39892fb7b9b0721aa67682f571c38c32ac58a"
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.8" version = "0.17.8"

View File

@ -40,10 +40,12 @@ miette = "7.2.0"
mime_guess = "2.0.5" mime_guess = "2.0.5"
parse-display = "0.9.1" parse-display = "0.9.1"
pyo3 = { version = "0.22.6", optional = true } pyo3 = { version = "0.22.6", optional = true }
regex = "1.11.1"
reqwest = { version = "0.12", default-features = false, features = [ reqwest = { version = "0.12", default-features = false, features = [
"stream", "stream",
"rustls-tls", "rustls-tls",
] } ] }
rgba_simple = "0.10.0"
ropey = "1.6.1" ropey = "1.6.1"
schemars = { version = "0.8.17", features = [ schemars = { version = "0.8.17", features = [
"impl_json_schema", "impl_json_schema",

View File

@ -489,6 +489,12 @@ fn get_autocomplete_snippet_from_schema(
continue; continue;
} }
if prop_name == "color" {
fn_docs.push_str(&format!("\t{}: ${{{}:\"#ff0000\"}},\n", prop_name, i));
i += 1;
continue;
}
if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? { if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? {
fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet)); fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet));
i = new_index + 1; i = new_index + 1;
@ -946,6 +952,21 @@ mod tests {
); );
} }
#[test]
fn get_autocomplete_snippet_appearance() {
let appearance_fn: Box<dyn StdLibFn> = Box::new(crate::std::appearance::Appearance);
let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
assert_eq!(
snippet,
r#"appearance({
color: ${0:"#
.to_owned()
+ "\"#"
+ r#"ff0000"},
}, ${1:%})${}"#
);
}
// We want to test the snippets we compile at lsp start. // We want to test the snippets we compile at lsp start.
#[test] #[test]
fn get_all_stdlib_autocomplete_snippets() { fn get_all_stdlib_autocomplete_snippets() {

View File

@ -5,6 +5,7 @@ use std::{fmt, iter::Enumerate, num::NonZeroUsize};
use anyhow::Result; use anyhow::Result;
use parse_display::Display; use parse_display::Display;
use tokeniser::Input;
use tower_lsp::lsp_types::SemanticTokenType; use tower_lsp::lsp_types::SemanticTokenType;
use winnow::{ use winnow::{
self, self,
@ -17,7 +18,6 @@ use crate::{
parsing::ast::types::{ItemVisibility, VariableKind}, parsing::ast::types::{ItemVisibility, VariableKind},
source_range::{ModuleId, SourceRange}, source_range::{ModuleId, SourceRange},
}; };
use tokeniser::Input;
mod tokeniser; mod tokeniser;

View File

@ -10,13 +10,12 @@ use winnow::{
Located, Stateful, Located, Stateful,
}; };
use super::TokenStream;
use crate::{ use crate::{
parsing::token::{Token, TokenType}, parsing::token::{Token, TokenType},
source_range::ModuleId, source_range::ModuleId,
}; };
use super::TokenStream;
lazy_static! { lazy_static! {
pub(crate) static ref RESERVED_WORDS: FnvHashMap<&'static str, TokenType> = { pub(crate) static ref RESERVED_WORDS: FnvHashMap<&'static str, TokenType> = {
let mut set = FnvHashMap::default(); let mut set = FnvHashMap::default();
@ -365,9 +364,8 @@ fn keyword_type_or_word(i: &mut Input<'_>) -> PResult<Token> {
mod tests { mod tests {
use winnow::Located; use winnow::Located;
use crate::parsing::token::TokenSlice;
use super::*; use super::*;
use crate::parsing::token::TokenSlice;
fn assert_parse_err<'i, P, O, E>(mut p: P, s: &'i str) fn assert_parse_err<'i, P, O, E>(mut p: P, s: &'i str)
where where
O: std::fmt::Debug, O: std::fmt::Debug,

View File

@ -0,0 +1,263 @@
//! Standard library appearance.
use anyhow::Result;
use derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, ModelingCmd};
use kittycad_modeling_cmds::{self as kcmc, shared::Color};
use regex::Regex;
use rgba_simple::Hex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use validator::Validate;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Solid, SolidSet},
std::Args,
};
lazy_static::lazy_static! {
static ref HEX_REGEX: Regex = Regex::new(r"^#[0-9a-fA-F]{6}$").unwrap();
}
/// Data for appearance.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Validate)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct AppearanceData {
/// Color of the new material, a hex string like "#ff0000".
#[schemars(regex(pattern = "#[0-9a-fA-F]{6}"))]
pub color: String,
/// Metalness of the new material, a percentage like 95.7.
#[validate(range(min = 0.0, max = 100.0))]
pub metalness: Option<f64>,
/// Roughness of the new material, a percentage like 95.7.
#[validate(range(min = 0.0, max = 100.0))]
pub roughness: Option<f64>,
// TODO(jess): we can also ambient occlusion here I just don't know what it is.
}
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, solid_set): (AppearanceData, SolidSet) = args.get_data_and_solid_set()?;
// Validate the data.
data.validate().map_err(|err| {
KclError::Semantic(KclErrorDetails {
message: format!("Invalid appearance data: {}", err),
source_ranges: vec![args.source_range],
})
})?;
// Make sure the color if set is valid.
if !HEX_REGEX.is_match(&data.color) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Invalid hex color (`{}`), try something like `#fff000`", data.color),
source_ranges: vec![args.source_range],
}));
}
let result = inner_appearance(data, solid_set, args).await?;
Ok(result.into())
}
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
///
/// This will work on any solid, including extruded solids, revolved solids, and shelled solids.
/// ```no_run
/// /// Add color to an extruded solid.
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> lineTo([10, 0], %)
/// |> lineTo([0, 10], %)
/// |> lineTo([-10, 0], %)
/// |> close(%)
///
/// example = extrude(5, exampleSketch)
/// |> appearance({color= '#ff0000', metalness= 50, roughness= 50}, %)
/// ```
///
/// ```no_run
/// /// Add color to a revolved solid.
/// sketch001 = startSketchOn('XY')
/// |> circle({ center = [15, 0], radius = 5 }, %)
/// |> revolve({ angle = 360, axis = 'y' }, %)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// ```
///
/// ```no_run
/// /// Add color to different solids.
/// fn cube(center) {
/// return startSketchOn('XY')
/// |> startProfileAt([center[0] - 10, center[1] - 10], %)
/// |> lineTo([center[0] + 10, center[1] - 10], %)
/// |> lineTo([center[0] + 10, center[1] + 10], %)
/// |> lineTo([center[0] - 10, center[1] + 10], %)
/// |> close(%)
/// |> extrude(10, %)
/// }
///
/// example0 = cube([0, 0])
/// example1 = cube([20, 0])
/// example2 = cube([40, 0])
///
/// appearance({color= '#ff0000', metalness= 50, roughness= 50}, [example0, example1])
/// appearance({color= '#00ff00', metalness= 50, roughness= 50}, example2)
/// ```
///
/// ```no_run
/// /// You can set the appearance before or after you shell it will yield the same result.
/// /// This example shows setting the appearance _after_ the shell.
/// firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line([24, 0], %)
/// |> line([0, -24], %)
/// |> line([-24, 0], %)
/// |> close(%)
/// |> extrude(6, %)
///
/// shell({
/// faces = ['end'],
/// thickness = 0.25,
/// }, firstSketch)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// ```
///
/// ```no_run
/// /// You can set the appearance before or after you shell it will yield the same result.
/// /// This example shows setting the appearance _before_ the shell.
/// firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line([24, 0], %)
/// |> line([0, -24], %)
/// |> line([-24, 0], %)
/// |> close(%)
/// |> extrude(6, %)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
///
/// shell({
/// faces = ['end'],
/// thickness = 0.25,
/// }, firstSketch)
/// ```
///
/// ```no_run
/// /// Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
/// /// This example shows _before_ the pattern.
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line([0, 2], %)
/// |> line([3, 1], %)
/// |> line([0, -4], %)
/// |> close(%)
///
/// example = extrude(1, exampleSketch)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// |> patternLinear3d({
/// axis = [1, 0, 1],
/// instances = 7,
/// distance = 6
/// }, %)
/// ```
///
/// ```no_run
/// /// Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
/// /// This example shows _after_ the pattern.
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line([0, 2], %)
/// |> line([3, 1], %)
/// |> line([0, -4], %)
/// |> close(%)
///
/// example = extrude(1, exampleSketch)
/// |> patternLinear3d({
/// axis = [1, 0, 1],
/// instances = 7,
/// distance = 6
/// }, %)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// ```
///
/// ```no_run
/// /// Color the result of a 2D pattern that was extruded.
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([.5, 25], %)
/// |> line([0, 5], %)
/// |> line([-1, 0], %)
/// |> line([0, -5], %)
/// |> close(%)
/// |> patternCircular2d({
/// center = [0, 0],
/// instances = 13,
/// arcDegrees = 360,
/// rotateDuplicates = true
/// }, %)
///
/// example = extrude(1, exampleSketch)
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// ```
#[stdlib {
name = "appearance",
}]
async fn inner_appearance(data: AppearanceData, solid_set: SolidSet, args: Args) -> Result<SolidSet, KclError> {
let solids: Vec<Box<Solid>> = solid_set.into();
for solid in &solids {
// Set the material properties.
let rgb = rgba_simple::RGB::<f32>::from_hex(&data.color).map_err(|err| {
KclError::Semantic(KclErrorDetails {
message: format!("Invalid hex color (`{}`): {}", data.color, err),
source_ranges: vec![args.source_range],
})
})?;
let color = Color {
r: rgb.red,
g: rgb.green,
b: rgb.blue,
a: 100.0,
};
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
object_id: solid.id,
color,
metalness: data.metalness.unwrap_or_default() as f32 / 100.0,
roughness: data.roughness.unwrap_or_default() as f32 / 100.0,
ambient_occlusion: 0.0,
}),
)
.await?;
// Idk if we want to actually modify the memory for the colors, but I'm not right now since
// I can't think of a use case for it.
}
Ok(SolidSet::from(solids))
}

View File

@ -1096,6 +1096,20 @@ impl<'a> FromKclValue<'a> for super::fillet::FilletData {
} }
} }
impl<'a> FromKclValue<'a> for super::appearance::AppearanceData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
let_field_of!(obj, color);
let_field_of!(obj, metalness?);
let_field_of!(obj, roughness?);
Some(Self {
color,
metalness,
roughness,
})
}
}
impl<'a> FromKclValue<'a> for super::helix::HelixData { impl<'a> FromKclValue<'a> for super::helix::HelixData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?; let obj = arg.as_object()?;

View File

@ -1,5 +1,6 @@
//! Functions implemented for language execution. //! Functions implemented for language execution.
pub mod appearance;
pub mod args; pub mod args;
pub mod array; pub mod array;
pub mod assert; pub mod assert;
@ -50,6 +51,7 @@ lazy_static! {
Box::new(LegLen), Box::new(LegLen),
Box::new(LegAngX), Box::new(LegAngX),
Box::new(LegAngY), Box::new(LegAngY),
Box::new(crate::std::appearance::Appearance),
Box::new(crate::std::convert::Int), Box::new(crate::std::convert::Int),
Box::new(crate::std::extrude::Extrude), Box::new(crate::std::extrude::Extrude),
Box::new(crate::std::segment::SegEnd), Box::new(crate::std::segment::SegEnd),

View File

@ -141,9 +141,10 @@ impl<'tree> Visitable<'tree> for Node<'tree> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use std::sync::Mutex; use std::sync::Mutex;
use super::*;
macro_rules! kcl { macro_rules! kcl {
( $kcl:expr ) => {{ ( $kcl:expr ) => {{
$crate::parsing::top_level_parse($kcl).unwrap() $crate::parsing::top_level_parse($kcl).unwrap()

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB