use deterministic ids in more places (#6064)

* dont redact the ids now that they are deterministic

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

pass arouund id generator more

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

change the anme space

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

updates and re-run

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

updates

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

* fixups

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

* fixes

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

* cleanup old files

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

* cleanup old files

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2025-03-29 21:23:11 -07:00
committed by GitHub
parent 0bdc50c78f
commit 017fac7041
10 changed files with 38 additions and 543 deletions

View File

@ -153,8 +153,3 @@ harness = false
name = "executor"
path = "e2e/executor/main.rs"
required-features = ["engine"]
[[test]]
name = "modify"
path = "e2e/modify/main.rs"
required-features = ["engine"]

View File

@ -1,232 +0,0 @@
use anyhow::Result;
use kcl_lib::{
exec::{KclValue, PlaneType},
modify_ast_for_sketch, ExecState, ExecutorContext, ModuleId, Program, SourceRange,
};
use kittycad_modeling_cmds::{each_cmd as mcmd, length_unit::LengthUnit, shared::Point3d, ModelingCmd};
use pretty_assertions::assert_eq;
/// Setup the engine and parse code for an ast.
async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, ModuleId, uuid::Uuid)> {
let program = Program::parse_no_errs(code)?;
let ctx = kcl_lib::ExecutorContext::new_with_default_client(Default::default()).await?;
let mut exec_state = ExecState::new(&ctx);
let result = ctx.run(&program, &mut exec_state).await?;
let outcome = exec_state.to_wasm_outcome(result.0).await;
// We need to get the sketch ID.
let KclValue::Sketch { value: sketch } = outcome.variables.get(name).unwrap() else {
anyhow::bail!("part001 not found in: {:?}", outcome.variables);
};
let sketch_id = sketch.id;
let plane_id = uuid::Uuid::new_v4();
ctx.engine
.send_modeling_cmd(
plane_id,
SourceRange::default(),
&ModelingCmd::from(mcmd::MakePlane {
clobber: false,
origin: Point3d::default(),
size: LengthUnit(60.0),
x_axis: Point3d { x: 1.0, y: 0.0, z: 0.0 },
y_axis: Point3d { x: 0.0, y: 1.0, z: 0.0 },
hide: Some(true),
}),
)
.await?;
// Enter sketch mode.
// We can't get control points without being in sketch mode.
// You can however get path info without sketch mode.
ctx.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
SourceRange::default(),
&ModelingCmd::from(mcmd::EnableSketchMode {
animated: false,
ortho: true,
entity_id: plane_id,
planar_normal: Some(Point3d { x: 0.0, y: 0.0, z: 1.0 }),
adjust_camera: false,
}),
)
.await?;
Ok((ctx, program, ModuleId::default(), sketch_id))
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_modify_sketch_part001() {
let name = "part001";
let code = format!(
r#"{} = startSketchOn(XY)
|> startProfileAt([8.41, 5.78], %)
|> line(end = [7.37, -11])
|> line(end = [-8.69, -3.75])
|> line(end = [-5, 4.25])
"#,
name
);
let (ctx, program, module_id, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let new_code = modify_ast_for_sketch(&ctx.engine, &mut new_program, module_id, name, PlaneType::XY, sketch_id)
.await
.unwrap();
// Make sure the code is the same.
assert_eq!(code, new_code);
// Make sure the program is the same.
assert_eq!(new_program, program);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_modify_sketch_part002() {
let name = "part002";
let code = format!(
r#"{} = startSketchOn(XY)
|> startProfileAt([8.41, 5.78], %)
|> line(end = [7.42, -8.62])
|> line(end = [-6.38, -3.51])
|> line(end = [-3.77, 3.56])
"#,
name
);
let (ctx, program, module_id, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let new_code = modify_ast_for_sketch(&ctx.engine, &mut new_program, module_id, name, PlaneType::XY, sketch_id)
.await
.unwrap();
// Make sure the code is the same.
assert_eq!(code, new_code);
// Make sure the program is the same.
assert_eq!(new_program, program);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] // until KittyCAD/engine#1434 is fixed.
async fn kcl_test_modify_close_sketch() {
let name = "part002";
let code = format!(
r#"{} = startSketchOn(XY)
|> startProfileAt([7.91, 3.89], %)
|> line(end = [7.42, -8.62])
|> line(end = [-6.38, -3.51])
|> line(end = [-3.77, 3.56])
|> close()
"#,
name
);
let (ctx, program, module_id, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let new_code = modify_ast_for_sketch(&ctx.engine, &mut new_program, module_id, name, PlaneType::XY, sketch_id)
.await
.unwrap();
// Make sure the code is the same.
assert_eq!(code, new_code);
// Make sure the program is the same.
assert_eq!(new_program, program);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_modify_line_to_close_sketch() {
let name = "part002";
let code = format!(
r#"const {} = startSketchOn(XY)
|> startProfileAt([7.91, 3.89], %)
|> line(end = [7.42, -8.62])
|> line(end = [-6.38, -3.51])
|> line(end = [-3.77, 3.56])
|> line(endAbsolute = [7.91, 3.89])
"#,
name
);
let (ctx, program, module_id, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let new_code = modify_ast_for_sketch(&ctx.engine, &mut new_program, module_id, name, PlaneType::XY, sketch_id)
.await
.unwrap();
// Make sure the code is the same.
assert_eq!(
new_code,
format!(
r#"{} = startSketchOn(XY)
|> startProfileAt([7.91, 3.89], %)
|> line(end = [7.42, -8.62])
|> line(end = [-6.38, -3.51])
|> line(end = [-3.77, 3.56])
|> close()
"#,
name
)
);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_modify_with_constraint() {
let name = "part002";
let code = format!(
r#"const thing = 12
const {} = startSketchOn(XY)
|> startProfileAt([7.91, 3.89], %)
|> line(end = [7.42, -8.62])
|> line(end = [-6.38, -3.51])
|> line(end = [-3.77, 3.56])
|> line(endAbsolute = [thing, 3.89])
"#,
name
);
let (ctx, program, module_id, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let result = modify_ast_for_sketch(&ctx.engine, &mut new_program, module_id, name, PlaneType::XY, sketch_id).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
"Sketch part002 is constrained `partial` and cannot be modified",
);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_modify_line_should_close_sketch() {
let name = "part003";
let code = format!(
r#"const {} = startSketchOn(XY)
|> startProfileAt([13.69, 3.8], %)
|> line(end = [4.23, -11.79])
|> line(end = [-10.7, -1.16])
|> line(end = [-3.72, 8.69])
|> line(end = [10.19, 4.26])
"#,
name
);
let (ctx, program, module_id, sketch_id) = setup(&code, name).await.unwrap();
let mut new_program = program.clone();
let new_code = modify_ast_for_sketch(&ctx.engine, &mut new_program, module_id, name, PlaneType::XY, sketch_id)
.await
.unwrap();
// Make sure the code is the same.
assert_eq!(
new_code,
format!(
r#"{} = startSketchOn(XY)
|> startProfileAt([13.69, 3.8], %)
|> line(end = [4.23, -11.79])
|> line(end = [-10.7, -1.16])
|> line(end = [-3.72, 8.69])
|> close()
"#,
name
)
);
}

View File

@ -170,7 +170,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
self.clear_queues().await;
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
id_generator.next_uuid(),
source_range,
&ModelingCmd::SceneClearAll(mcmd::SceneClearAll::default()),
)
@ -195,9 +195,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
&self,
visible: bool,
source_range: SourceRange,
id_generator: &mut IdGenerator,
) -> Result<(), crate::errors::KclError> {
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
id_generator.next_uuid(),
source_range,
&ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
)
@ -231,10 +232,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
&self,
units: crate::UnitLength,
source_range: SourceRange,
id_generator: &mut IdGenerator,
) -> Result<(), crate::errors::KclError> {
// Before we even start executing the program, set the units.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
id_generator.next_uuid(),
source_range,
&ModelingCmd::from(mcmd::SetSceneUnits { unit: units.into() }),
)
@ -248,15 +250,18 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
&self,
settings: &crate::ExecutorSettings,
source_range: SourceRange,
id_generator: &mut IdGenerator,
) -> Result<(), crate::errors::KclError> {
// Set the edge visibility.
self.set_edge_visibility(settings.highlight_edges, source_range).await?;
self.set_edge_visibility(settings.highlight_edges, source_range, id_generator)
.await?;
// Change the units.
self.set_units(settings.units, source_range).await?;
self.set_units(settings.units, source_range, id_generator).await?;
// Send the command to show the grid.
self.modify_grid(!settings.show_grid, source_range).await?;
self.modify_grid(!settings.show_grid, source_range, id_generator)
.await?;
// We do not have commands for changing ssao on the fly.
@ -502,6 +507,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
y_axis: Point3d,
color: Option<Color>,
source_range: SourceRange,
id_generator: &mut IdGenerator,
) -> Result<uuid::Uuid, KclError> {
// Create new default planes.
let default_size = 100.0;
@ -524,7 +530,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
if let Some(color) = color {
// Set the color.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
id_generator.next_uuid(),
source_range,
&ModelingCmd::from(mcmd::PlaneSetColor { color, plane_id }),
)
@ -615,7 +621,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
for (name, plane_id, x_axis, y_axis, color) in plane_settings {
planes.insert(
name,
self.make_default_plane(plane_id, x_axis, y_axis, color, source_range)
self.make_default_plane(plane_id, x_axis, y_axis, color, source_range, id_generator)
.await?,
);
}
@ -701,10 +707,15 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
}))
}
async fn modify_grid(&self, hidden: bool, source_range: SourceRange) -> Result<(), KclError> {
async fn modify_grid(
&self,
hidden: bool,
source_range: SourceRange,
id_generator: &mut IdGenerator,
) -> Result<(), KclError> {
// Hide/show the grid.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
id_generator.next_uuid(),
source_range,
&ModelingCmd::from(mcmd::ObjectVisible {
hidden,
@ -715,7 +726,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Hide/show the grid scale text.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
id_generator.next_uuid(),
source_range,
&ModelingCmd::from(mcmd::ObjectVisible {
hidden,

View File

@ -64,7 +64,11 @@ impl ExecutorContext {
let new_units = exec_state.length_unit();
if !self.engine.execution_kind().await.is_isolated() {
self.engine
.set_units(new_units.into(), annotation.as_source_range())
.set_units(
new_units.into(),
annotation.as_source_range(),
exec_state.id_generator(),
)
.await?;
}
} else {
@ -143,7 +147,9 @@ impl ExecutorContext {
// command and we'd need to flush the batch again.
// This avoids that.
if !exec_kind.is_isolated() && new_units != old_units && *path != ModulePath::Main {
self.engine.set_units(old_units.into(), Default::default()).await?;
self.engine
.set_units(old_units.into(), Default::default(), exec_state.id_generator())
.await?;
}
self.engine.replace_execution_kind(original_execution).await;

View File

@ -2,7 +2,7 @@
use crate::execution::ModuleId;
const NAMESPACE_KCL: uuid::Uuid = uuid::uuid!("efcd6508-4ce6-4a09-8317-e6a6994a3cd7");
const NAMESPACE_KCL: uuid::Uuid = uuid::uuid!("8bda3118-75eb-58c7-a866-bef1dcb495e7");
/// A generator for ArtifactIds that can be stable across executions.
#[derive(Debug, Clone, Default, PartialEq)]

View File

@ -618,7 +618,7 @@ impl ExecutorContext {
if reapply_settings
&& self
.engine
.reapply_settings(&self.settings, Default::default())
.reapply_settings(&self.settings, Default::default(), old_state.id_generator())
.await
.is_err()
{
@ -636,7 +636,7 @@ impl ExecutorContext {
CacheResult::NoAction(true) => {
if self
.engine
.reapply_settings(&self.settings, Default::default())
.reapply_settings(&self.settings, Default::default(), old_state.id_generator())
.await
.is_ok()
{
@ -737,7 +737,7 @@ impl ExecutorContext {
// Re-apply the settings, in case the cache was busted.
self.engine
.reapply_settings(&self.settings, Default::default())
.reapply_settings(&self.settings, Default::default(), exec_state.id_generator())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;

View File

@ -93,7 +93,7 @@ pub use lsp::{
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
};
pub use modules::ModuleId;
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
pub use parsing::ast::types::FormatOptions;
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
pub use source_range::SourceRange;
#[cfg(not(target_arch = "wasm32"))]

View File

@ -1,5 +1,4 @@
pub(crate) mod digest;
pub mod modify;
pub mod types;
use crate::{

View File

@ -1,285 +0,0 @@
use std::sync::Arc;
use kcmc::{
each_cmd as mcmd, ok_response::OkModelingCmdResponse, shared::PathCommand, websocket::OkWebSocketResponseData,
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use super::types::{CallExpressionKw, Identifier, LabeledArg, LiteralValue};
use crate::{
engine::EngineManager,
errors::{KclError, KclErrorDetails},
execution::Point2d,
parsing::ast::types::{
ArrayExpression, CallExpression, ConstraintLevel, FormatOptions, Literal, Node, PipeExpression,
PipeSubstitution, VariableDeclarator,
},
source_range::SourceRange,
ModuleId, Program,
};
type Point3d = kcmc::shared::Point3d<f64>;
#[derive(Debug)]
/// The control point data for a curve or line.
struct ControlPointData {
/// The control points for the curve or line.
points: Vec<Point3d>,
/// The command that created this curve or line.
_command: PathCommand,
/// The id of the curve or line.
_id: uuid::Uuid,
}
const EPSILON: f64 = 0.015625; // or 2^-6
/// Update the AST to reflect the new state of the program after something like
/// a move or a new line.
pub async fn modify_ast_for_sketch(
engine: &Arc<Box<dyn EngineManager>>,
program: &mut Program,
module_id: ModuleId,
// The name of the sketch.
sketch_name: &str,
// The type of plane the sketch is on. `XY` or `XZ`, etc
plane: crate::execution::PlaneType,
// The ID of the parent sketch.
sketch_id: uuid::Uuid,
) -> Result<String, KclError> {
// First we need to check if this sketch is constrained (even partially).
// If it is, we cannot modify it.
// Get the information about the sketch.
if let Some(ast_sketch) = program.ast.get_variable(sketch_name) {
let constraint_level = match ast_sketch {
super::types::Definition::Variable(var) => var.get_constraint_level(),
super::types::Definition::Import(import) => import.get_constraint_level(),
super::types::Definition::Type(_) => ConstraintLevel::Ignore {
source_ranges: Vec::new(),
},
};
match &constraint_level {
ConstraintLevel::None { source_ranges: _ } => {}
ConstraintLevel::Ignore { source_ranges: _ } => {}
ConstraintLevel::Partial {
source_ranges: _,
levels,
} => {
return Err(KclError::Engine(KclErrorDetails {
message: format!(
"Sketch {} is constrained `{}` and cannot be modified",
sketch_name, constraint_level
),
source_ranges: levels.get_all_partial_or_full_source_ranges(),
}));
}
ConstraintLevel::Full { source_ranges } => {
return Err(KclError::Engine(KclErrorDetails {
message: format!(
"Sketch {} is constrained `{}` and cannot be modified",
sketch_name, constraint_level
),
source_ranges: source_ranges.clone(),
}));
}
}
}
// Let's start by getting the path info.
// Let's get the path info.
let resp = engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
SourceRange::default(),
&ModelingCmd::PathGetInfo(mcmd::PathGetInfo { path_id: sketch_id }),
)
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::PathGetInfo(path_info),
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("Get path info response was not as expected: {:?}", resp),
source_ranges: vec![SourceRange::default()],
}));
};
// Now let's get the control points for all the segments.
// TODO: We should probably await all these at once so we aren't going one by one.
// But I guess this is fine for now.
// We absolutely have to preserve the order of the control points.
let mut control_points = Vec::new();
for segment in &path_info.segments {
if let Some(command_id) = &segment.command_id {
let cmd = ModelingCmd::from(mcmd::CurveGetControlPoints {
curve_id: (*command_id).into(),
});
let h = engine.send_modeling_cmd(uuid::Uuid::new_v4(), SourceRange::default(), &cmd);
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::CurveGetControlPoints(data),
} = h.await?
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("Curve get control points response was not as expected: {:?}", resp),
source_ranges: vec![SourceRange::default()],
}));
};
control_points.push(ControlPointData {
points: data.control_points.clone(),
_command: segment.command,
_id: (*command_id).into(),
});
}
}
if control_points.is_empty() {
return Err(KclError::Engine(KclErrorDetails {
message: format!("No control points found for sketch {}", sketch_name),
source_ranges: vec![SourceRange::default()],
}));
}
let first_control_points = control_points.first().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("No control points found for sketch {}", sketch_name),
source_ranges: vec![SourceRange::default()],
})
})?;
let mut additional_lines = Vec::new();
let mut last_point = first_control_points.points[1];
for control_point in control_points[1..].iter() {
additional_lines.push([
(control_point.points[1].x - last_point.x),
(control_point.points[1].y - last_point.y),
]);
last_point = Point3d {
x: control_point.points[1].x,
y: control_point.points[1].y,
z: control_point.points[1].z,
};
}
// Okay now let's recalculate the sketch from the control points.
let start_sketch_at_end = Point3d {
x: (first_control_points.points[1].x - first_control_points.points[0].x),
y: (first_control_points.points[1].y - first_control_points.points[0].y),
z: (first_control_points.points[1].z - first_control_points.points[0].z),
};
let sketch = create_start_sketch_on(
sketch_name,
[first_control_points.points[0].x, first_control_points.points[0].y],
[start_sketch_at_end.x, start_sketch_at_end.y],
plane,
additional_lines,
)?;
// Add the sketch back to the program.
program.ast.replace_variable(sketch_name, sketch);
let recasted = program.ast.recast(&FormatOptions::default(), 0);
// Re-parse the ast so we get the correct source ranges.
program.ast = crate::parsing::parse_str(&recasted, module_id).parse_errs_as_err()?;
Ok(recasted)
}
/// Create a pipe expression that starts a sketch at the given point and draws a line to the given point.
fn create_start_sketch_on(
name: &str,
start: [f64; 2],
end: [f64; 2],
plane: crate::execution::PlaneType,
additional_lines: Vec<[f64; 2]>,
) -> Result<Node<VariableDeclarator>, KclError> {
let start_sketch_on = CallExpression::new("startSketchOn", vec![Literal::new(plane.to_string().into()).into()])?;
let start_profile_at = CallExpression::new(
"startProfileAt",
vec![
ArrayExpression::new(vec![
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(start[0]))).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(start[1]))).into(),
])
.into(),
PipeSubstitution::new().into(),
],
)?;
// Keep track of where we are so we can close the sketch if we need to.
let mut current_position = Point2d {
x: start[0],
y: start[1],
};
current_position.x += end[0];
current_position.y += end[1];
let expr = ArrayExpression::new(vec![
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(end[0]))).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(end[1]))).into(),
])
.into();
let initial_line = CallExpressionKw::new(
"line",
None,
vec![LabeledArg {
label: Node::no_src(super::types::Identifier {
name: "end".to_owned(),
digest: None,
}),
arg: expr,
}],
)?;
let mut pipe_body = vec![start_sketch_on.into(), start_profile_at.into(), initial_line.into()];
for (index, line) in additional_lines.iter().enumerate() {
current_position.x += line[0];
current_position.y += line[1];
// If we are on the last line, check if we have to close the sketch.
if index == additional_lines.len() - 1 {
let diff_x = (current_position.x - start[0]).abs();
let diff_y = (current_position.y - start[1]).abs();
// Compare the end of the last line to the start of the first line.
// This is a bit more lenient if you look at the value of epsilon.
if diff_x <= EPSILON && diff_y <= EPSILON {
// We have to close the sketch.
let close = CallExpressionKw::new("close", None, vec![])?;
pipe_body.push(close.into());
break;
}
}
// TODO: we should check if we should close the sketch.
let expr = ArrayExpression::new(vec![
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(line[0]))).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(line[1]))).into(),
])
.into();
let line = CallExpressionKw::new(
"line",
None,
vec![LabeledArg {
arg: expr,
label: Node::no_src(Identifier {
name: "end".to_owned(),
digest: None,
}),
}],
)?;
pipe_body.push(line.into());
}
Ok(VariableDeclarator::new(name, PipeExpression::new(pipe_body).into()))
}
fn round_before_recast(num: f64) -> f64 {
// We use 2 decimal places.
(num * 100.0).round() / 100.0
}

View File

@ -66,7 +66,7 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
}));
}
let result = inner_appearance(solids, data.color, data.metalness, data.roughness, args).await?;
let result = inner_appearance(solids, data.color, data.metalness, data.roughness, exec_state, args).await?;
Ok(result.into())
}
@ -287,6 +287,7 @@ async fn inner_appearance(
color: String,
metalness: Option<f64>,
roughness: Option<f64>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Solid>, KclError> {
for solid in &solids {
@ -306,7 +307,7 @@ async fn inner_appearance(
};
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
exec_state.next_uuid(),
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
object_id: solid.id,
color,