Declare appearance function in KCL (#7220)

Move appearance to KCL

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2025-05-28 11:25:27 +12:00
committed by GitHub
parent 889c72ec60
commit 9dfb67cf61
68 changed files with 3999 additions and 11936 deletions

View File

@ -463,7 +463,11 @@ impl ModData {
}
pub fn find_by_name(&self, name: &str) -> Option<&DocData> {
if let Some(result) = self.children.values().find(|dd| dd.name() == name) {
if let Some(result) = self
.children
.values()
.find(|dd| dd.name() == name && !matches!(dd, DocData::Mod(_)))
{
return Some(result);
}
@ -824,7 +828,13 @@ impl ArgData {
Some("[Edge; 1+]") => Some((index, format!(r#"{label}[${{{index}:tag_or_edge_fn}}]"#))),
Some("Plane") => Some((index, format!(r#"{label}${{{}:XY}}"#, index))),
Some("string") => Some((index, format!(r#"{label}${{{}:"string"}}"#, index))),
Some("string") => {
if self.name == "color" {
Some((index, format!(r#"{label}${{{}:"ff0000"}}"#, index)))
} else {
Some((index, format!(r#"{label}${{{}:"string"}}"#, index)))
}
}
Some("bool") => Some((index, format!(r#"{label}${{{}:false}}"#, index))),
_ => None,
}

View File

@ -1074,12 +1074,12 @@ 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(${0:%}, color = ${1:"#.to_owned() + "\"#" + r#"ff0000"})"#
);
let data = kcl_doc::walk_prelude();
let DocData::Fn(helix_fn) = data.find_by_name("appearance").unwrap() else {
panic!();
};
let snippet = helix_fn.to_autocomplete_snippet();
assert_eq!(snippet, r#"appearance(color = ${0:"ff0000"})"#);
}
#[test]

View File

@ -160,8 +160,10 @@ impl ExecutorContext {
.cloned();
let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.name.name);
let mut ty = mem.get_from(&ty_name, env_ref, import_item.into(), 0).cloned();
let mod_name = format!("{}{}", memory::MODULE_PREFIX, import_item.name.name);
let mut mod_value = mem.get_from(&mod_name, env_ref, import_item.into(), 0).cloned();
if value.is_err() && ty.is_err() {
if value.is_err() && ty.is_err() && mod_value.is_err() {
return Err(KclError::UndefinedValue(KclErrorDetails::new(
format!("{} is not defined in module", import_item.name.name),
vec![SourceRange::from(&import_item.name)],
@ -180,10 +182,22 @@ impl ExecutorContext {
}
if ty.is_ok() && !module_exports.contains(&ty_name) {
ty = Err(KclError::Semantic(KclErrorDetails::new(String::new(), vec![])));
ty = Err(KclError::Semantic(KclErrorDetails::new(format!(
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
import_item.name.name
),
vec![SourceRange::from(&import_item.name)],)));
}
if value.is_err() && ty.is_err() {
if mod_value.is_ok() && !module_exports.contains(&mod_name) {
mod_value = Err(KclError::Semantic(KclErrorDetails::new(format!(
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
import_item.name.name
),
vec![SourceRange::from(&import_item.name)],)));
}
if value.is_err() && ty.is_err() && mod_value.is_err() {
return value.map(Option::Some);
}
@ -205,7 +219,6 @@ impl ExecutorContext {
if let Ok(ty) = ty {
let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.identifier());
// Add the item to the current module.
exec_state.mut_stack().add(
ty_name.clone(),
ty,
@ -216,6 +229,19 @@ impl ExecutorContext {
exec_state.mod_local.module_exports.push(ty_name);
}
}
if let Ok(mod_value) = mod_value {
let mod_name = format!("{}{}", memory::MODULE_PREFIX, import_item.identifier());
exec_state.mut_stack().add(
mod_name.clone(),
mod_value,
SourceRange::from(&import_item.name),
)?;
if let ItemVisibility::Export = import_stmt.visibility {
exec_state.mod_local.module_exports.push(mod_name);
}
}
}
}
ImportSelector::Glob(_) => {
@ -246,7 +272,11 @@ impl ExecutorContext {
value: module_id,
meta: vec![source_range.into()],
};
exec_state.mut_stack().add(name, item, source_range)?;
exec_state.mut_stack().add(
format!("{}{}", memory::MODULE_PREFIX, name),
item,
source_range,
)?;
}
}
last_expr = None;
@ -780,8 +810,14 @@ impl Node<Name> {
)));
}
let mod_name = format!("{}{}", memory::MODULE_PREFIX, self.name.name);
if self.path.is_empty() {
return exec_state.stack().get(&self.name.name, self.into());
let item_value = exec_state.stack().get(&self.name.name, self.into());
if item_value.is_ok() {
return item_value;
}
return exec_state.stack().get(&mod_name, self.into());
}
let mut mem_spec: Option<(EnvironmentRef, Vec<String>)> = None;
@ -800,7 +836,9 @@ impl Node<Name> {
.memory
.get_from(&p.name, env, p.as_source_range(), 0)?
}
None => exec_state.stack().get(&p.name, self.into())?,
None => exec_state
.stack()
.get(&format!("{}{}", memory::MODULE_PREFIX, p.name), self.into())?,
};
let KclValue::Module { value: module_id, .. } = value else {
@ -820,17 +858,40 @@ impl Node<Name> {
}
let (env, exports) = mem_spec.unwrap();
if !exports.contains(&self.name.name) {
return Err(KclError::Semantic(KclErrorDetails::new(
format!("Item {} not found in module's exported items", self.name.name),
self.name.as_source_ranges(),
)));
}
exec_state
let item_exported = exports.contains(&self.name.name);
let item_value = exec_state
.stack()
.memory
.get_from(&self.name.name, env, self.name.as_source_range(), 0)
.get_from(&self.name.name, env, self.name.as_source_range(), 0);
// Item is defined and exported.
if item_exported && item_value.is_ok() {
return item_value;
}
let mod_exported = exports.contains(&mod_name);
let mod_value = exec_state
.stack()
.memory
.get_from(&mod_name, env, self.name.as_source_range(), 0);
// Module is defined and exported.
if mod_exported && mod_value.is_ok() {
return mod_value;
}
// Neither item or module is defined.
if item_value.is_err() && mod_value.is_err() {
return item_value;
}
// Either item or module is defined, but not exported.
debug_assert!((item_value.is_ok() && !item_exported) || (mod_value.is_ok() && !mod_exported));
Err(KclError::Semantic(KclErrorDetails::new(
format!("Item {} not found in module's exported items", self.name.name),
self.name.as_source_ranges(),
)))
}
}

View File

@ -226,8 +226,9 @@ use crate::{
/// The distinguished name of the return value of a function.
pub(crate) const RETURN_NAME: &str = "__return";
/// Low-budget namespacing for types.
/// Low-budget namespacing for types and modules.
pub(crate) const TYPE_PREFIX: &str = "__ty_";
pub(crate) const MODULE_PREFIX: &str = "__mod_";
/// KCL memory. There should be only one ProgramMemory for the interpretation of a program (
/// including other modules). Multiple interpretation runs should have fresh instances.
@ -364,8 +365,10 @@ impl ProgramMemory {
};
}
let name = var.trim_start_matches(TYPE_PREFIX).trim_start_matches(MODULE_PREFIX);
Err(KclError::UndefinedValue(KclErrorDetails::new(
format!("`{}` is not defined", var),
format!("`{name}` is not defined"),
vec![source_range],
)))
}

View File

@ -194,7 +194,10 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
".**[].x[]" => rounded_redaction(3),
".**[].y[]" => rounded_redaction(3),
".**[].z[]" => rounded_redaction(3),
".**.sourceRange" => Vec::new(),
".**.x" => rounded_redaction(3),
".**.y" => rounded_redaction(3),
".**.z" => rounded_redaction(3),
".**.sourceRange" => Vec::new(),
})
})
}));

View File

@ -1,7 +1,6 @@
//! Standard library appearance.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, ModelingCmd};
use kittycad_modeling_cmds::{self as kcmc, shared::Color};
use regex::Regex;
@ -57,7 +56,7 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
exec_state,
)?;
let color: String = args.get_kw_arg("color")?;
let color: String = args.get_kw_arg_typed("color", &RuntimeType::string(), exec_state)?;
let metalness: Option<TyF64> = args.get_kw_arg_opt_typed("metalness", &RuntimeType::count(), exec_state)?;
let roughness: Option<TyF64> = args.get_kw_arg_opt_typed("roughness", &RuntimeType::count(), exec_state)?;
@ -81,224 +80,6 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
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)
/// |> startProfile(at = [0, 0])
/// |> line(endAbsolute = [10, 0])
/// |> line(endAbsolute = [0, 10])
/// |> line(endAbsolute = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// // There are other options besides 'color', but they're optional.
/// |> appearance(color='#ff0000')
/// ```
///
/// ```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)
/// |> startProfile(at = [center[0] - 10, center[1] - 10])
/// |> line(endAbsolute = [center[0] + 10, center[1] - 10])
/// |> line(endAbsolute = [center[0] + 10, center[1] + 10])
/// |> line(endAbsolute = [center[0] - 10, center[1] + 10])
/// |> close()
/// |> extrude(length = 10)
/// }
///
/// example0 = cube(center = [0, 0])
/// example1 = cube(center = [20, 0])
/// example2 = cube(center = [40, 0])
///
/// appearance([example0, example1], color='#ff0000', metalness=50, roughness=50)
/// appearance(example2, color='#00ff00', metalness=50, roughness=50)
/// ```
///
/// ```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)
/// |> startProfile(at = [-12, 12])
/// |> line(end = [24, 0])
/// |> line(end = [0, -24])
/// |> line(end = [-24, 0])
/// |> close()
/// |> extrude(length = 6)
///
/// shell(
/// firstSketch,
/// faces = [END],
/// thickness = 0.25,
/// )
/// |> 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)
/// |> startProfile(at = [-12, 12])
/// |> line(end = [24, 0])
/// |> line(end = [0, -24])
/// |> line(end = [-24, 0])
/// |> close()
/// |> extrude(length = 6)
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// )
///
/// shell(
/// firstSketch,
/// faces = [END],
/// thickness = 0.25,
/// )
/// ```
///
/// ```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)
/// |> startProfile(at = [0, 0])
/// |> line(end = [0, 2])
/// |> line(end = [3, 1])
/// |> line(end = [0, -4])
/// |> close()
///
/// example = extrude(exampleSketch, length = 1)
/// |> 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)
/// |> startProfile(at = [0, 0])
/// |> line(end = [0, 2])
/// |> line(end = [3, 1])
/// |> line(end = [0, -4])
/// |> close()
///
/// example = extrude(exampleSketch, length = 1)
/// |> 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)
/// |> startProfile(at = [.5, 25])
/// |> line(end = [0, 5])
/// |> line(end = [-1, 0])
/// |> line(end = [0, -5])
/// |> close()
/// |> patternCircular2d(
/// center = [0, 0],
/// instances = 13,
/// arcDegrees = 360,
/// rotateDuplicates = true
/// )
///
/// example = extrude(exampleSketch, length = 1)
/// |> appearance(
/// color = '#ff0000',
/// metalness = 90,
/// roughness = 90
/// )
/// ```
///
/// ```no_run
/// // Color the result of a sweep.
///
/// // Create a path for the sweep.
/// sweepPath = startSketchOn(XZ)
/// |> startProfile(at = [0.05, 0.05])
/// |> line(end = [0, 7])
/// |> tangentialArc(angle = 90, radius = 5)
/// |> line(end = [-3, 0])
/// |> tangentialArc(angle = -90, radius = 5)
/// |> line(end = [0, 7])
///
/// pipeHole = startSketchOn(XY)
/// |> circle(
/// center = [0, 0],
/// radius = 1.5,
/// )
///
/// sweepSketch = startSketchOn(XY)
/// |> circle(
/// center = [0, 0],
/// radius = 2,
/// )
/// |> subtract2d(tool = pipeHole)
/// |> sweep(path = sweepPath)
/// |> appearance(
/// color = "#ff0000",
/// metalness = 50,
/// roughness = 50
/// )
/// ```
///
/// ```no_run
/// // Change the appearance of an imported model.
///
/// import "tests/inputs/cube.sldprt" as cube
///
/// cube
/// |> appearance(
/// color = "#ff0000",
/// metalness = 50,
/// roughness = 50
/// )
/// ```
#[stdlib {
name = "appearance",
unlabeled_first = true,
args = {
solids = { 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(
solids: SolidOrImportedGeometry,
color: String,

View File

@ -45,7 +45,6 @@ pub type StdFn = fn(
lazy_static! {
static ref CORE_FNS: Vec<Box<dyn StdLibFn>> = vec![
Box::new(crate::std::appearance::Appearance),
Box::new(crate::std::extrude::Extrude),
Box::new(crate::std::segment::SegEnd),
Box::new(crate::std::segment::SegEndX),
@ -277,6 +276,10 @@ pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProp
|e, a| Box::pin(crate::std::patterns::pattern_circular_3d(e, a)),
StdFnProps::default("std::solid::patternCircular3d").include_in_feature_tree(),
),
("solid", "appearance") => (
|e, a| Box::pin(crate::std::appearance::appearance(e, a)),
StdFnProps::default("std::solid::appearance"),
),
("array", "map") => (
|e, a| Box::pin(crate::std::array::map(e, a)),
StdFnProps::default("std::array::map"),