KCL: Appearance stdlib fn is now kwargs (#5308)

This commit is contained in:
Adam Chalmers
2025-02-07 16:36:51 -06:00
committed by GitHub
parent 67e60cb832
commit 9a3ac64603
11 changed files with 4521 additions and 174 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,7 @@ extrude003 = extrude(sketch003, length = 20)
`
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
test.describe('Check the happy path, for basic changing color', () => {
test.fixme('Check the happy path, for basic changing color', () => {
const cases = [
{
desc: 'User accepts change',
@ -106,7 +106,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await test.step('verify initial change', async () => {
await scene.expectPixelColor(green, greenCheckCoords, 15)
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
await editor.expectEditor.toContain('appearance({')
await editor.expectEditor.toContain('appearance(')
})
if (!shouldReject) {
@ -115,13 +115,13 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await expect(successToast).not.toBeVisible()
await scene.expectPixelColor(green, greenCheckCoords, 15)
await editor.expectEditor.toContain('appearance({')
await editor.expectEditor.toContain('appearance(')
// ctrl-z works after accepting
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta')
await editor.expectEditor.not.toContain('appearance({')
await editor.expectEditor.not.toContain('appearance(')
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
})
} else {
@ -130,7 +130,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await expect(successToast).not.toBeVisible()
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
await editor.expectEditor.not.toContain('appearance({')
await editor.expectEditor.not.toContain('appearance(')
})
}
})

View File

@ -1188,11 +1188,11 @@ sweepSketch = startSketchOn('XY')
radius = 2
}, %)
|> sweep(path = sweepPath)
|> appearance({
|> appearance(
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
)
`
)
})
@ -1234,11 +1234,11 @@ sweepSketch = startSketchOn('XY')
radius = 2
}, %)
|> sweep(path = sweepPath)
|> appearance({
|> appearance(
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
)
`
)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

@ -85,7 +85,7 @@
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./get-latest-wasm-bundle.sh",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/kw-shell/manifest.json",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/kw-appearance/manifest.json",
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",

View File

@ -32,7 +32,7 @@ child_process.spawnSync('git', [
'clone',
'--single-branch',
'--branch',
'achalmers/kw-shell',
'achalmers/kw-appearance',
URL_GIT_KCL_SAMPLES,
DIR_KCL_SAMPLES,
])

View File

@ -118,7 +118,7 @@ impl StdLibFnArg {
} else if self.type_ == "KclValue" && self.required {
return Ok(Some((index, format!("{label}${{{}:{}}}", index, "3"))));
}
self.get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index, in_keyword_fn)
self.get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index, in_keyword_fn, &self.name)
.map(|maybe| maybe.map(|(index, snippet)| (index, format!("{label}{snippet}"))))
}
@ -136,6 +136,7 @@ impl StdLibFnArg {
schema: &schemars::schema::Schema,
index: usize,
in_keyword_fn: bool,
name: &str,
) -> Result<Option<(usize, String)>> {
match schema {
schemars::schema::Schema::Object(o) => {
@ -149,6 +150,10 @@ impl StdLibFnArg {
return Ok(Some((index, format!("${{{}:sketch{}}}", index, "000"))));
}
if name == "color" {
let snippet = format!("${{{}:\"#ff0000\"}}", index);
return Ok(Some((index, snippet)));
}
if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") {
if (!in_keyword_fn && *nullable) || (in_keyword_fn && !self.include_in_snippet) {
return Ok(None);
@ -192,13 +197,9 @@ impl StdLibFnArg {
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)) = self.get_autocomplete_snippet_from_schema(prop, i, false)? {
if let Some((new_index, snippet)) =
self.get_autocomplete_snippet_from_schema(prop, i, false, name)?
{
fn_docs.push_str(&format!("\t{} = {},\n", prop_name, snippet));
i = new_index + 1;
}
@ -223,7 +224,8 @@ impl StdLibFnArg {
.get_autocomplete_snippet_from_schema(
items,
index + (v as usize),
in_keyword_fn
in_keyword_fn,
name
)
.unwrap()
.unwrap()
@ -238,7 +240,7 @@ impl StdLibFnArg {
index,
format!(
"[{}]",
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn)?
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn, name)?
.ok_or_else(|| anyhow::anyhow!("expected snippet"))?
.1
),
@ -250,7 +252,7 @@ impl StdLibFnArg {
index,
format!(
"[{}]",
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn)?
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn, name)?
.ok_or_else(|| anyhow::anyhow!("expected snippet"))?
.1
),
@ -293,7 +295,7 @@ impl StdLibFnArg {
return Ok(Some((index, parsed_enum_values[0].to_string())));
} else if let Some(item) = items.iter().next() {
if let Some((new_index, snippet)) =
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn)?
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn, name)?
{
i = new_index + 1;
fn_docs.push_str(&snippet);
@ -302,7 +304,7 @@ impl StdLibFnArg {
} else if let Some(items) = &subschemas.any_of {
if let Some(item) = items.iter().next() {
if let Some((new_index, snippet)) =
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn)?
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn, name)?
{
i = new_index + 1;
fn_docs.push_str(&snippet);
@ -1018,12 +1020,7 @@ mod tests {
let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
assert_eq!(
snippet,
r#"appearance({
color = ${0:"#
.to_owned()
+ "\"#"
+ r#"ff0000"},
}, ${1:%})${}"#
r#"appearance(${0:%}, color = ${1:"#.to_owned() + "\"#" + r#"ff0000"})${}"#
);
}

View File

@ -24,7 +24,7 @@ lazy_static::lazy_static! {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Validate)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct AppearanceData {
struct AppearanceData {
/// Color of the new material, a hex string like "#ff0000".
#[schemars(regex(pattern = "#[0-9a-fA-F]{6}"))]
pub color: String,
@ -39,7 +39,16 @@ pub struct AppearanceData {
/// 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()?;
let solid_set: SolidSet = args.get_unlabeled_kw_arg("solidSet")?;
let color: String = args.get_kw_arg("color")?;
let metalness: Option<f64> = args.get_kw_arg_opt("metalness")?;
let roughness: Option<f64> = args.get_kw_arg_opt("roughness")?;
let data = AppearanceData {
color,
metalness,
roughness,
};
// Validate the data.
data.validate().map_err(|err| {
@ -57,7 +66,7 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
}));
}
let result = inner_appearance(data, solid_set, args).await?;
let result = inner_appearance(solid_set, data.color, data.metalness, data.roughness, args).await?;
Ok(result.into())
}
@ -74,7 +83,8 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// |> appearance({color= '#ff0000', metalness= 50, roughness= 50}, %)
/// // There are other options besides 'color', but they're optional.
/// |> appearance(color='#ff0000')
/// ```
///
/// ```no_run
@ -82,11 +92,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// sketch001 = startSketchOn('XY')
/// |> circle({ center = [15, 0], radius = 5 }, %)
/// |> revolve({ angle = 360, axis = 'y' }, %)
/// |> appearance({
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// )
/// ```
///
/// ```no_run
@ -105,8 +115,8 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// 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)
/// appearance([example0, example1], color='#ff0000', metalness=50, roughness=50)
/// appearance(example2, color='#00ff00', metalness=50, roughness=50)
/// ```
///
/// ```no_run
@ -125,11 +135,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// faces = ['end'],
/// thickness = 0.25,
/// )
/// |> appearance({
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// )
/// ```
///
/// ```no_run
@ -142,11 +152,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// |> line(end = [-24, 0])
/// |> close()
/// |> extrude(length = 6)
/// |> appearance({
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// )
///
/// shell(
/// firstSketch,
@ -166,11 +176,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// |> close()
///
/// example = extrude(exampleSketch, length = 1)
/// |> appearance({
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// )
/// |> patternLinear3d({
/// axis = [1, 0, 1],
/// instances = 7,
@ -194,11 +204,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// instances = 7,
/// distance = 6
/// }, %)
/// |> appearance({
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// )
/// ```
///
/// ```no_run
@ -217,11 +227,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// }, %)
///
/// example = extrude(exampleSketch, length = 1)
/// |> appearance({
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// }, %)
/// )
/// ```
///
/// ```no_run
@ -255,23 +265,37 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// }, %)
/// |> hole(pipeHole, %)
/// |> sweep(path = sweepPath)
/// |> appearance({
/// color: "#ff0000",
/// metalness: 50,
/// roughness: 50
/// }, %)
/// |> appearance(
/// color = "#ff0000",
/// metalness = 50,
/// roughness = 50
/// )
/// ```
#[stdlib {
name = "appearance",
keywords = true,
unlabeled_first = true,
args = {
solid_set = { docs = "The solid(s) whose appearance is being set" },
color = { docs = "Color of the new material, a hex string like '#ff0000'"},
metalness = { docs = "Metalness of the new material, a percentage like 95.7." },
roughness = { docs = "Roughness of the new material, a percentage like 95.7." },
}
}]
async fn inner_appearance(data: AppearanceData, solid_set: SolidSet, args: Args) -> Result<SolidSet, KclError> {
async fn inner_appearance(
solid_set: SolidSet,
color: String,
metalness: Option<f64>,
roughness: Option<f64>,
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| {
let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
KclError::Semantic(KclErrorDetails {
message: format!("Invalid hex color (`{}`): {}", data.color, err),
message: format!("Invalid hex color (`{color}`): {err}"),
source_ranges: vec![args.source_range],
})
})?;
@ -288,8 +312,8 @@ async fn inner_appearance(data: AppearanceData, solid_set: SolidSet, args: Args)
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,
metalness: metalness.unwrap_or_default() as f32 / 100.0,
roughness: roughness.unwrap_or_default() as f32 / 100.0,
ambient_occlusion: 0.0,
}),
)

View File

@ -1042,20 +1042,6 @@ 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::HelixRevolutionsData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;