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 crate::{
ast::types::{
ArrayExpression, CallExpression, ConstraintLevel, FormatOptions, Literal, PipeExpression, PipeSubstitution,
Program, VariableDeclarator,
},
engine::EngineManager,
errors::{KclError, KclErrorDetails},
executor::{Point2d, SourceRange},
};
type Point3d = kcmc::shared::Point3d<f64>;
#[derive(Debug)]
pub struct ControlPointData {
pub points: Vec<Point3d>,
pub command: PathCommand,
pub id: uuid::Uuid,
}
const EPSILON: f64 = 0.015625; pub async fn modify_ast_for_sketch(
engine: &Arc<Box<dyn EngineManager>>,
program: &mut Program,
sketch_name: &str,
plane: crate::executor::PlaneType,
sketch_id: uuid::Uuid,
) -> Result<String, KclError> {
if let Some(ast_sketch) = program.get_variable(sketch_name) {
let constraint_level = ast_sketch.get_constraint_level();
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 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()],
}));
};
let mut control_points = Vec::new();
for segment in &path_info.segments {
if let Some(command_id) = &segment.command_id {
let h = engine.send_modeling_cmd(
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::from(mcmd::CurveGetControlPoints {
curve_id: (*command_id).into(),
}),
);
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,
};
}
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,
)?;
program.replace_variable(sketch_name, sketch);
let recasted = program.recast(&FormatOptions::default(), 0);
let tokens = crate::token::lexer(&recasted)?;
let parser = crate::parser::Parser::new(tokens);
*program = parser.ast()?;
Ok(recasted)
}
fn create_start_sketch_on(
name: &str,
start: [f64; 2],
end: [f64; 2],
plane: crate::executor::PlaneType,
additional_lines: Vec<[f64; 2]>,
) -> Result<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(round_before_recast(start[0]).into()).into(),
Literal::new(round_before_recast(start[1]).into()).into(),
])
.into(),
PipeSubstitution::new().into(),
],
)?;
let mut current_position = Point2d {
x: start[0],
y: start[1],
};
current_position.x += end[0];
current_position.y += end[1];
let initial_line = CallExpression::new(
"line",
vec![
ArrayExpression::new(vec![
Literal::new(round_before_recast(end[0]).into()).into(),
Literal::new(round_before_recast(end[1]).into()).into(),
])
.into(),
PipeSubstitution::new().into(),
],
)?;
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 index == additional_lines.len() - 1 {
let diff_x = (current_position.x - start[0]).abs();
let diff_y = (current_position.y - start[1]).abs();
if diff_x <= EPSILON && diff_y <= EPSILON {
let close = CallExpression::new("close", vec![PipeSubstitution::new().into()])?;
pipe_body.push(close.into());
break;
}
}
let line = CallExpression::new(
"line",
vec![
ArrayExpression::new(vec![
Literal::new(round_before_recast(line[0]).into()).into(),
Literal::new(round_before_recast(line[1]).into()).into(),
])
.into(),
PipeSubstitution::new().into(),
],
)?;
pipe_body.push(line.into());
}
Ok(VariableDeclarator::new(name, PipeExpression::new(pipe_body).into()))
}
fn round_before_recast(num: f64) -> f64 {
(num * 100.0).round() / 100.0
}