use std::cell::Cell;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::ops::ControlFlow;
use indexmap::IndexMap;
use kcl_error::CompilationIssue;
use kcl_error::SourceRange;
use kittycad_modeling_cmds::units::UnitLength;
use serde::Serialize;
use crate::ExecOutcome;
use crate::ExecutorContext;
use crate::KclError;
use crate::KclErrorWithOutputs;
use crate::Program;
use crate::collections::AhashIndexSet;
#[cfg(feature = "artifact-graph")]
use crate::execution::Artifact;
#[cfg(feature = "artifact-graph")]
use crate::execution::ArtifactGraph;
#[cfg(feature = "artifact-graph")]
use crate::execution::CapSubType;
use crate::execution::MockConfig;
use crate::execution::SKETCH_BLOCK_PARAM_ON;
use crate::execution::cache::SketchModeState;
use crate::execution::cache::clear_mem_cache;
use crate::execution::cache::read_old_memory;
use crate::execution::cache::write_old_memory;
use crate::fmt::format_number_literal;
use crate::front::Angle;
use crate::front::ArcCtor;
use crate::front::CircleCtor;
use crate::front::Distance;
use crate::front::EqualRadius;
use crate::front::Error;
use crate::front::ExecResult;
use crate::front::FixedPoint;
use crate::front::Freedom;
use crate::front::LinesEqualLength;
use crate::front::Object;
use crate::front::Parallel;
use crate::front::Perpendicular;
use crate::front::PointCtor;
use crate::front::Tangent;
use crate::frontend::api::Expr;
use crate::frontend::api::FileId;
use crate::frontend::api::Number;
use crate::frontend::api::ObjectId;
use crate::frontend::api::ObjectKind;
use crate::frontend::api::Plane;
use crate::frontend::api::ProjectId;
use crate::frontend::api::RestoreSketchCheckpointOutcome;
use crate::frontend::api::SceneGraph;
use crate::frontend::api::SceneGraphDelta;
use crate::frontend::api::SketchCheckpointId;
use crate::frontend::api::SourceDelta;
use crate::frontend::api::SourceRef;
use crate::frontend::api::Version;
use crate::frontend::modify::find_defined_names;
use crate::frontend::modify::next_free_name;
use crate::frontend::modify::next_free_name_with_padding;
use crate::frontend::sketch::Coincident;
use crate::frontend::sketch::Constraint;
use crate::frontend::sketch::ConstraintSegment;
use crate::frontend::sketch::Diameter;
use crate::frontend::sketch::ExistingSegmentCtor;
use crate::frontend::sketch::Horizontal;
use crate::frontend::sketch::LineCtor;
use crate::frontend::sketch::Point2d;
use crate::frontend::sketch::Radius;
use crate::frontend::sketch::Segment;
use crate::frontend::sketch::SegmentCtor;
use crate::frontend::sketch::SketchApi;
use crate::frontend::sketch::SketchCtor;
use crate::frontend::sketch::Vertical;
use crate::frontend::traverse::MutateBodyItem;
use crate::frontend::traverse::TraversalReturn;
use crate::frontend::traverse::Visitor;
use crate::frontend::traverse::dfs_mut;
use crate::id::IncIdGenerator;
use crate::parsing::ast::types as ast;
use crate::pretty::NumericSuffix;
use crate::std::constraints::LinesAtAngleKind;
use crate::walk::NodeMut;
use crate::walk::Visitable;
pub(crate) mod api;
pub(crate) mod modify;
pub(crate) mod sketch;
pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
#[derive(Debug, Clone)]
struct SketchCheckpoint {
id: SketchCheckpointId,
source: SourceDelta,
program: Program,
scene_graph: SceneGraph,
exec_outcome: ExecOutcome,
point_freedom_cache: HashMap<ObjectId, Freedom>,
mock_memory: Option<SketchModeState>,
}
mod traverse;
pub(crate) mod trim;
struct ArcSizeConstraintParams {
points: Vec<ObjectId>,
function_name: &'static str,
value: f64,
units: NumericSuffix,
constraint_type_name: &'static str,
}
const POINT_FN: &str = "point";
const POINT_AT_PARAM: &str = "at";
const LINE_FN: &str = "line";
const LINE_START_PARAM: &str = "start";
const LINE_END_PARAM: &str = "end";
const ARC_FN: &str = "arc";
const ARC_START_PARAM: &str = "start";
const ARC_END_PARAM: &str = "end";
const ARC_CENTER_PARAM: &str = "center";
const CIRCLE_FN: &str = "circle";
const CIRCLE_VARIABLE: &str = "circle";
const CIRCLE_START_PARAM: &str = "start";
const CIRCLE_CENTER_PARAM: &str = "center";
const COINCIDENT_FN: &str = "coincident";
const DIAMETER_FN: &str = "diameter";
const DISTANCE_FN: &str = "distance";
const FIXED_FN: &str = "fixed";
const ANGLE_FN: &str = "angle";
const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
const EQUAL_LENGTH_FN: &str = "equalLength";
const EQUAL_RADIUS_FN: &str = "equalRadius";
const HORIZONTAL_FN: &str = "horizontal";
const RADIUS_FN: &str = "radius";
const TANGENT_FN: &str = "tangent";
const VERTICAL_FN: &str = "vertical";
const LINE_PROPERTY_START: &str = "start";
const LINE_PROPERTY_END: &str = "end";
const ARC_PROPERTY_START: &str = "start";
const ARC_PROPERTY_END: &str = "end";
const ARC_PROPERTY_CENTER: &str = "center";
const CIRCLE_PROPERTY_START: &str = "start";
const CIRCLE_PROPERTY_CENTER: &str = "center";
const CONSTRUCTION_PARAM: &str = "construction";
#[derive(Debug, Clone, Copy)]
enum EditDeleteKind {
Edit,
DeleteNonSketch,
}
impl EditDeleteKind {
fn is_delete(&self) -> bool {
match self {
EditDeleteKind::Edit => false,
EditDeleteKind::DeleteNonSketch => true,
}
}
fn to_change_kind(self) -> ChangeKind {
match self {
EditDeleteKind::Edit => ChangeKind::Edit,
EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
}
}
}
#[derive(Debug, Clone, Copy)]
enum ChangeKind {
Add,
Edit,
Delete,
None,
}
#[derive(Debug, Clone, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts")]
#[serde(tag = "type")]
pub enum SetProgramOutcome {
#[serde(rename_all = "camelCase")]
Success {
scene_graph: Box<SceneGraph>,
exec_outcome: Box<ExecOutcome>,
checkpoint_id: Option<SketchCheckpointId>,
},
#[serde(rename_all = "camelCase")]
ExecFailure { error: Box<KclErrorWithOutputs> },
}
#[derive(Debug, Clone)]
pub struct FrontendState {
program: Program,
scene_graph: SceneGraph,
point_freedom_cache: HashMap<ObjectId, Freedom>,
sketch_checkpoints: VecDeque<SketchCheckpoint>,
sketch_checkpoint_id_gen: IncIdGenerator<u64>,
}
impl Default for FrontendState {
fn default() -> Self {
Self::new()
}
}
impl FrontendState {
pub fn new() -> Self {
Self {
program: Program::empty(),
scene_graph: SceneGraph {
project: ProjectId(0),
file: FileId(0),
version: Version(0),
objects: Default::default(),
settings: Default::default(),
sketch_mode: Default::default(),
},
point_freedom_cache: HashMap::new(),
sketch_checkpoints: VecDeque::new(),
sketch_checkpoint_id_gen: IncIdGenerator::new(1),
}
}
pub fn scene_graph(&self) -> &SceneGraph {
&self.scene_graph
}
pub fn default_length_unit(&self) -> UnitLength {
self.program
.meta_settings()
.ok()
.flatten()
.map(|settings| settings.default_length_units)
.unwrap_or(UnitLength::Millimeters)
}
pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
let checkpoint = SketchCheckpoint {
id: checkpoint_id,
source: SourceDelta {
text: source_from_ast(&self.program.ast),
},
program: self.program.clone(),
scene_graph: self.scene_graph.clone(),
exec_outcome,
point_freedom_cache: self.point_freedom_cache.clone(),
mock_memory: read_old_memory().await,
};
self.sketch_checkpoints.push_back(checkpoint);
while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
self.sketch_checkpoints.pop_front();
}
Ok(checkpoint_id)
}
pub async fn restore_sketch_checkpoint(
&mut self,
checkpoint_id: SketchCheckpointId,
) -> api::Result<RestoreSketchCheckpointOutcome> {
let checkpoint = self
.sketch_checkpoints
.iter()
.find(|checkpoint| checkpoint.id == checkpoint_id)
.cloned()
.ok_or_else(|| Error {
msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
})?;
self.program = checkpoint.program;
self.scene_graph = checkpoint.scene_graph.clone();
self.point_freedom_cache = checkpoint.point_freedom_cache;
if let Some(mock_memory) = checkpoint.mock_memory {
write_old_memory(mock_memory).await;
} else {
clear_mem_cache().await;
}
Ok(RestoreSketchCheckpointOutcome {
source_delta: checkpoint.source,
scene_graph_delta: SceneGraphDelta {
new_graph: checkpoint.scene_graph,
new_objects: Vec::new(),
invalidates_ids: true,
exec_outcome: checkpoint.exec_outcome,
},
})
}
pub fn clear_sketch_checkpoints(&mut self) {
self.sketch_checkpoints.clear();
}
}
impl SketchApi for FrontendState {
async fn execute_mock(
&mut self,
ctx: &ExecutorContext,
_version: Version,
sketch: ObjectId,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let sketch_block_ref =
sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
let mut truncated_program = self.program.clone();
only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
.map_err(KclErrorWithOutputs::no_outputs)?;
let outcome = ctx
.run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
.await?;
let new_source = source_from_ast(&self.program.ast);
let src_delta = SourceDelta { text: new_source };
let outcome = self.update_state_after_exec(outcome, true);
let scene_graph_delta = SceneGraphDelta {
new_graph: self.scene_graph.clone(),
new_objects: Default::default(),
invalidates_ids: false,
exec_outcome: outcome,
};
Ok((src_delta, scene_graph_delta))
}
async fn new_sketch(
&mut self,
ctx: &ExecutorContext,
_project: ProjectId,
_file: FileId,
_version: Version,
args: SketchCtor,
) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
let mut new_ast = self.program.ast.clone();
let mut plane_ast =
sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
let mut defined_names = find_defined_names(&new_ast);
let is_face_of_expr = matches!(
&plane_ast,
ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
);
if is_face_of_expr {
let face_name = next_free_name_with_padding("face", &defined_names)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
let face_decl = ast::VariableDeclaration::new(
ast::VariableDeclarator::new(&face_name, plane_ast),
ast::ItemVisibility::Default,
ast::VariableKind::Const,
);
new_ast
.body
.push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
face_decl,
))));
defined_names.insert(face_name.clone());
plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
}
let sketch_ast = ast::SketchBlock {
arguments: vec![ast::LabeledArg {
label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
arg: plane_ast,
}],
body: Default::default(),
is_being_edited: false,
non_code_meta: Default::default(),
digest: None,
};
let sketch_name = next_free_name_with_padding("sketch", &defined_names)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
let sketch_decl = ast::VariableDeclaration::new(
ast::VariableDeclarator::new(
&sketch_name,
ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
),
ast::ItemVisibility::Default,
ast::VariableKind::Const,
);
new_ast
.body
.push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
sketch_decl,
))));
let new_source = source_from_ast(&new_ast);
let (new_program, errors) = Program::parse(&new_source)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
if !errors.is_empty() {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Error parsing KCL source after adding sketch: {errors:?}"
))));
}
let Some(new_program) = new_program else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
"No AST produced after adding sketch".to_owned(),
)));
};
self.program = new_program.clone();
let outcome = ctx.run_with_caching(new_program.clone()).await?;
let freedom_analysis_ran = true;
let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
let Some(sketch_id) = self
.scene_graph
.objects
.iter()
.filter_map(|object| match object.kind {
ObjectKind::Sketch(_) => Some(object.id),
_ => None,
})
.max_by_key(|id| id.0)
else {
return Err(KclErrorWithOutputs::from_error_outcome(
KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
outcome,
));
};
self.scene_graph.sketch_mode = Some(sketch_id);
let src_delta = SourceDelta { text: new_source };
let scene_graph_delta = SceneGraphDelta {
new_graph: self.scene_graph.clone(),
invalidates_ids: false,
new_objects: vec![sketch_id],
exec_outcome: outcome,
};
Ok((src_delta, scene_graph_delta, sketch_id))
}
async fn edit_sketch(
&mut self,
ctx: &ExecutorContext,
_project: ProjectId,
_file: FileId,
_version: Version,
sketch: ObjectId,
) -> ExecResult<SceneGraphDelta> {
let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
})?;
let ObjectKind::Sketch(_) = &sketch_object.kind else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Object is not a sketch, it is {}",
sketch_object.kind.human_friendly_kind_with_article()
))));
};
let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
self.scene_graph.sketch_mode = Some(sketch);
let mut truncated_program = self.program.clone();
only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
.map_err(KclErrorWithOutputs::no_outputs)?;
let outcome = ctx
.run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
.await?;
let outcome = self.update_state_after_exec(outcome, true);
let scene_graph_delta = SceneGraphDelta {
new_graph: self.scene_graph.clone(),
invalidates_ids: false,
new_objects: Vec::new(),
exec_outcome: outcome,
};
Ok(scene_graph_delta)
}
async fn exit_sketch(
&mut self,
ctx: &ExecutorContext,
_version: Version,
sketch: ObjectId,
) -> ExecResult<SceneGraph> {
#[cfg(not(target_arch = "wasm32"))]
let _ = sketch;
#[cfg(target_arch = "wasm32")]
if self.scene_graph.sketch_mode != Some(sketch) {
web_sys::console::warn_1(
&format!(
"WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
&self.scene_graph.sketch_mode
)
.into(),
);
}
self.scene_graph.sketch_mode = None;
let outcome = ctx.run_with_caching(self.program.clone()).await?;
self.update_state_after_exec(outcome, false);
Ok(self.scene_graph.clone())
}
async fn delete_sketch(
&mut self,
ctx: &ExecutorContext,
_version: Version,
sketch: ObjectId,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let mut new_ast = self.program.ast.clone();
let sketch_id = sketch;
let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
})?;
let ObjectKind::Sketch(_) = &sketch_object.kind else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Object is not a sketch, it is {}",
sketch_object.kind.human_friendly_kind_with_article(),
))));
};
self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
.map_err(KclErrorWithOutputs::no_outputs)?;
self.execute_after_delete_sketch(ctx, &mut new_ast).await
}
async fn add_segment(
&mut self,
ctx: &ExecutorContext,
_version: Version,
sketch: ObjectId,
segment: SegmentCtor,
_label: Option<String>,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
match segment {
SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
}
}
async fn edit_segments(
&mut self,
ctx: &ExecutorContext,
_version: Version,
sketch: ObjectId,
segments: Vec<ExistingSegmentCtor>,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let sketch_block_ref =
sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
let mut new_ast = self.program.ast.clone();
let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
for segment in &segments {
segment_ids_edited.insert(segment.id);
}
let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
for segment in segments {
let segment_id = segment.id;
match segment.ctor {
SegmentCtor::Point(ctor) => {
if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
&& let ObjectKind::Segment { segment } = &segment_object.kind
&& let Segment::Point(point) = segment
&& let Some(owner_id) = point.owner
&& let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
&& let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
{
match owner_segment {
Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
if let Some(existing) = final_edits.get_mut(&owner_id) {
let SegmentCtor::Line(line_ctor) = existing else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Internal: Expected line ctor for owner, but found {}",
existing.human_friendly_kind_with_article()
))));
};
if line.start == segment_id {
line_ctor.start = ctor.position;
} else {
line_ctor.end = ctor.position;
}
} else if let SegmentCtor::Line(line_ctor) = &line.ctor {
let mut line_ctor = line_ctor.clone();
if line.start == segment_id {
line_ctor.start = ctor.position;
} else {
line_ctor.end = ctor.position;
}
final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
} else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Internal: Line does not have line ctor, but found {}",
line.ctor.human_friendly_kind_with_article()
))));
}
continue;
}
Segment::Arc(arc)
if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
{
if let Some(existing) = final_edits.get_mut(&owner_id) {
let SegmentCtor::Arc(arc_ctor) = existing else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Internal: Expected arc ctor for owner, but found {}",
existing.human_friendly_kind_with_article()
))));
};
if arc.start == segment_id {
arc_ctor.start = ctor.position;
} else if arc.end == segment_id {
arc_ctor.end = ctor.position;
} else {
arc_ctor.center = ctor.position;
}
} else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
let mut arc_ctor = arc_ctor.clone();
if arc.start == segment_id {
arc_ctor.start = ctor.position;
} else if arc.end == segment_id {
arc_ctor.end = ctor.position;
} else {
arc_ctor.center = ctor.position;
}
final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
} else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Internal: Arc does not have arc ctor, but found {}",
arc.ctor.human_friendly_kind_with_article()
))));
}
continue;
}
Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
if let Some(existing) = final_edits.get_mut(&owner_id) {
let SegmentCtor::Circle(circle_ctor) = existing else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Internal: Expected circle ctor for owner, but found {}",
existing.human_friendly_kind_with_article()
))));
};
if circle.start == segment_id {
circle_ctor.start = ctor.position;
} else {
circle_ctor.center = ctor.position;
}
} else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
let mut circle_ctor = circle_ctor.clone();
if circle.start == segment_id {
circle_ctor.start = ctor.position;
} else {
circle_ctor.center = ctor.position;
}
final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
} else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Internal: Circle does not have circle ctor, but found {}",
circle.ctor.human_friendly_kind_with_article()
))));
}
continue;
}
_ => {}
}
}
final_edits.insert(segment_id, SegmentCtor::Point(ctor));
}
SegmentCtor::Line(ctor) => {
final_edits.insert(segment_id, SegmentCtor::Line(ctor));
}
SegmentCtor::Arc(ctor) => {
final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
}
SegmentCtor::Circle(ctor) => {
final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
}
}
}
for (segment_id, ctor) in final_edits {
match ctor {
SegmentCtor::Point(ctor) => self
.edit_point(&mut new_ast, sketch, segment_id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
SegmentCtor::Line(ctor) => self
.edit_line(&mut new_ast, sketch, segment_id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
SegmentCtor::Arc(ctor) => self
.edit_arc(&mut new_ast, sketch, segment_id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
SegmentCtor::Circle(ctor) => self
.edit_circle(&mut new_ast, sketch, segment_id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
}
}
self.execute_after_edit(
ctx,
sketch,
sketch_block_ref,
segment_ids_edited,
EditDeleteKind::Edit,
&mut new_ast,
)
.await
}
async fn delete_objects(
&mut self,
ctx: &ExecutorContext,
_version: Version,
sketch: ObjectId,
constraint_ids: Vec<ObjectId>,
segment_ids: Vec<ObjectId>,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let sketch_block_ref =
sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
for segment_id in segment_ids_set.iter().copied() {
if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
&& let ObjectKind::Segment { segment } = &segment_object.kind
&& let Segment::Point(point) = segment
&& let Some(owner_id) = point.owner
&& let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
&& let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
&& matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
{
resolved_segment_ids_to_delete.insert(owner_id);
} else {
resolved_segment_ids_to_delete.insert(segment_id);
}
}
let referenced_constraint_ids = self
.find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
.map_err(KclErrorWithOutputs::no_outputs)?;
let mut new_ast = self.program.ast.clone();
for constraint_id in referenced_constraint_ids {
if constraint_ids_set.contains(&constraint_id) {
continue;
}
let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
})?;
let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Object is not a constraint, it is {}",
constraint_object.kind.human_friendly_kind_with_article()
))));
};
match constraint {
Constraint::Coincident(coincident) => {
let remaining_segments = coincident
.segments
.iter()
.copied()
.filter(|segment| match segment {
ConstraintSegment::Segment(point_id) => {
if resolved_segment_ids_to_delete.contains(point_id) {
return false;
}
let point_object = self.scene_graph.objects.get(point_id.0);
if let Some(object) = point_object
&& let ObjectKind::Segment { segment } = &object.kind
&& let Segment::Point(point) = segment
&& let Some(owner_id) = point.owner
{
return !resolved_segment_ids_to_delete.contains(&owner_id);
}
true
}
ConstraintSegment::Origin(_) => true,
})
.collect::<Vec<_>>();
if remaining_segments.len() >= 2 {
self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
.map_err(KclErrorWithOutputs::no_outputs)?;
} else {
constraint_ids_set.insert(constraint_id);
}
}
Constraint::EqualRadius(equal_radius) => {
let remaining_input = equal_radius
.input
.iter()
.copied()
.filter(|segment_id| !resolved_segment_ids_to_delete.contains(segment_id))
.collect::<Vec<_>>();
if remaining_input.len() >= 2 {
self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
.map_err(KclErrorWithOutputs::no_outputs)?;
} else {
constraint_ids_set.insert(constraint_id);
}
}
Constraint::LinesEqualLength(lines_equal_length) => {
let remaining_lines = lines_equal_length
.lines
.iter()
.copied()
.filter(|line_id| !resolved_segment_ids_to_delete.contains(line_id))
.collect::<Vec<_>>();
if remaining_lines.len() >= 2 {
self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
.map_err(KclErrorWithOutputs::no_outputs)?;
} else {
constraint_ids_set.insert(constraint_id);
}
}
Constraint::Parallel(parallel) => {
let remaining_lines = parallel
.lines
.iter()
.copied()
.filter(|line_id| !resolved_segment_ids_to_delete.contains(line_id))
.collect::<Vec<_>>();
if remaining_lines.len() >= 2 {
self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
.map_err(KclErrorWithOutputs::no_outputs)?;
} else {
constraint_ids_set.insert(constraint_id);
}
}
_ => {
constraint_ids_set.insert(constraint_id);
}
}
}
for constraint_id in constraint_ids_set {
self.delete_constraint(&mut new_ast, sketch, constraint_id)
.map_err(KclErrorWithOutputs::no_outputs)?;
}
for segment_id in resolved_segment_ids_to_delete {
self.delete_segment(&mut new_ast, sketch, segment_id)
.map_err(KclErrorWithOutputs::no_outputs)?;
}
self.execute_after_edit(
ctx,
sketch,
sketch_block_ref,
Default::default(),
EditDeleteKind::DeleteNonSketch,
&mut new_ast,
)
.await
}
async fn add_constraint(
&mut self,
ctx: &ExecutorContext,
_version: Version,
sketch: ObjectId,
constraint: Constraint,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let original_program = self.program.clone();
let original_scene_graph = self.scene_graph.clone();
let mut new_ast = self.program.ast.clone();
let sketch_block_ref = match constraint {
Constraint::Coincident(coincident) => self
.add_coincident(sketch, coincident, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::Distance(distance) => self
.add_distance(sketch, distance, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::EqualRadius(equal_radius) => self
.add_equal_radius(sketch, equal_radius, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::Fixed(fixed) => self
.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::HorizontalDistance(distance) => self
.add_horizontal_distance(sketch, distance, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::VerticalDistance(distance) => self
.add_vertical_distance(sketch, distance, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::Horizontal(horizontal) => self
.add_horizontal(sketch, horizontal, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::LinesEqualLength(lines_equal_length) => self
.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::Parallel(parallel) => self
.add_parallel(sketch, parallel, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::Perpendicular(perpendicular) => self
.add_perpendicular(sketch, perpendicular, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::Radius(radius) => self
.add_radius(sketch, radius, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::Diameter(diameter) => self
.add_diameter(sketch, diameter, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::Vertical(vertical) => self
.add_vertical(sketch, vertical, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::Angle(lines_at_angle) => self
.add_angle(sketch, lines_at_angle, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
Constraint::Tangent(tangent) => self
.add_tangent(sketch, tangent, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?,
};
let result = self
.execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
.await;
if result.is_err() {
self.program = original_program;
self.scene_graph = original_scene_graph;
}
result
}
async fn chain_segment(
&mut self,
ctx: &ExecutorContext,
version: Version,
sketch: ObjectId,
previous_segment_end_point_id: ObjectId,
segment: SegmentCtor,
_label: Option<String>,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let SegmentCtor::Line(line_ctor) = segment else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"chain_segment currently only supports Line segments, got {}",
segment.human_friendly_kind_with_article(),
))));
};
let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
let new_line_id = first_scene_delta
.new_objects
.iter()
.find(|&obj_id| {
let obj = self.scene_graph.objects.get(obj_id.0);
if let Some(obj) = obj {
matches!(
&obj.kind,
ObjectKind::Segment {
segment: Segment::Line(_)
}
)
} else {
false
}
})
.ok_or_else(|| {
KclErrorWithOutputs::no_outputs(KclError::refactor(
"Failed to find new line segment in scene graph".to_string(),
))
})?;
let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"New line object not found: {new_line_id:?}"
)))
})?;
let ObjectKind::Segment {
segment: new_line_segment,
} = &new_line_obj.kind
else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Object is not a segment: {new_line_obj:?}"
))));
};
let Segment::Line(new_line) = new_line_segment else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Segment is not a line: {new_line_segment:?}"
))));
};
let new_line_start_point_id = new_line.start;
let coincident = Coincident {
segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
};
let (final_src_delta, final_scene_delta) = self
.add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
.await?;
let mut combined_new_objects = first_scene_delta.new_objects.clone();
combined_new_objects.extend(final_scene_delta.new_objects);
let scene_graph_delta = SceneGraphDelta {
new_graph: self.scene_graph.clone(),
invalidates_ids: false,
new_objects: combined_new_objects,
exec_outcome: final_scene_delta.exec_outcome,
};
Ok((final_src_delta, scene_graph_delta))
}
async fn edit_constraint(
&mut self,
ctx: &ExecutorContext,
_version: Version,
sketch: ObjectId,
constraint_id: ObjectId,
value_expression: String,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let sketch_block_ref =
sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
})?;
if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Object is not a constraint: {constraint_id:?}"
))));
}
let mut new_ast = self.program.ast.clone();
let (parsed, errors) = Program::parse(&value_expression)
.map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
if !errors.is_empty() {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Error parsing value expression: {errors:?}"
))));
}
let mut parsed = parsed.ok_or_else(|| {
KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
})?;
if parsed.ast.body.is_empty() {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
"Empty value expression".to_string(),
)));
}
let first = parsed.ast.body.remove(0);
let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
"Value expression must be a simple expression".to_string(),
)));
};
let new_value: ast::BinaryPart = expr_stmt
.inner
.expression
.try_into()
.map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
self.mutate_ast(
&mut new_ast,
constraint_id,
AstMutateCommand::EditConstraintValue { value: new_value },
)
.map_err(KclErrorWithOutputs::no_outputs)?;
self.execute_after_edit(
ctx,
sketch,
sketch_block_ref,
Default::default(),
EditDeleteKind::Edit,
&mut new_ast,
)
.await
}
async fn batch_split_segment_operations(
&mut self,
ctx: &ExecutorContext,
_version: Version,
sketch: ObjectId,
edit_segments: Vec<ExistingSegmentCtor>,
add_constraints: Vec<Constraint>,
delete_constraint_ids: Vec<ObjectId>,
_new_segment_info: sketch::NewSegmentInfo,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let sketch_block_ref =
sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
let mut new_ast = self.program.ast.clone();
let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
for segment in edit_segments {
segment_ids_edited.insert(segment.id);
match segment.ctor {
SegmentCtor::Point(ctor) => self
.edit_point(&mut new_ast, sketch, segment.id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
SegmentCtor::Line(ctor) => self
.edit_line(&mut new_ast, sketch, segment.id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
SegmentCtor::Arc(ctor) => self
.edit_arc(&mut new_ast, sketch, segment.id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
SegmentCtor::Circle(ctor) => self
.edit_circle(&mut new_ast, sketch, segment.id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
}
}
for constraint in add_constraints {
match constraint {
Constraint::Coincident(coincident) => {
self.add_coincident(sketch, coincident, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::Distance(distance) => {
self.add_distance(sketch, distance, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::EqualRadius(equal_radius) => {
self.add_equal_radius(sketch, equal_radius, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::Fixed(fixed) => {
self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::HorizontalDistance(distance) => {
self.add_horizontal_distance(sketch, distance, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::VerticalDistance(distance) => {
self.add_vertical_distance(sketch, distance, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::Horizontal(horizontal) => {
self.add_horizontal(sketch, horizontal, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::LinesEqualLength(lines_equal_length) => {
self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::Parallel(parallel) => {
self.add_parallel(sketch, parallel, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::Perpendicular(perpendicular) => {
self.add_perpendicular(sketch, perpendicular, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::Vertical(vertical) => {
self.add_vertical(sketch, vertical, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::Diameter(diameter) => {
self.add_diameter(sketch, diameter, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::Radius(radius) => {
self.add_radius(sketch, radius, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::Angle(angle) => {
self.add_angle(sketch, angle, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
Constraint::Tangent(tangent) => {
self.add_tangent(sketch, tangent, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
}
}
let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
let has_constraint_deletions = !constraint_ids_set.is_empty();
for constraint_id in constraint_ids_set {
self.delete_constraint(&mut new_ast, sketch, constraint_id)
.map_err(KclErrorWithOutputs::no_outputs)?;
}
let (source_delta, mut scene_graph_delta) = self
.execute_after_edit(
ctx,
sketch,
sketch_block_ref,
segment_ids_edited,
EditDeleteKind::Edit,
&mut new_ast,
)
.await?;
if has_constraint_deletions {
scene_graph_delta.invalidates_ids = true;
}
Ok((source_delta, scene_graph_delta))
}
async fn batch_tail_cut_operations(
&mut self,
ctx: &ExecutorContext,
_version: Version,
sketch: ObjectId,
edit_segments: Vec<ExistingSegmentCtor>,
add_constraints: Vec<Constraint>,
delete_constraint_ids: Vec<ObjectId>,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let sketch_block_ref =
sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
let mut new_ast = self.program.ast.clone();
let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
for segment in edit_segments {
segment_ids_edited.insert(segment.id);
match segment.ctor {
SegmentCtor::Point(ctor) => self
.edit_point(&mut new_ast, sketch, segment.id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
SegmentCtor::Line(ctor) => self
.edit_line(&mut new_ast, sketch, segment.id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
SegmentCtor::Arc(ctor) => self
.edit_arc(&mut new_ast, sketch, segment.id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
SegmentCtor::Circle(ctor) => self
.edit_circle(&mut new_ast, sketch, segment.id, ctor)
.map_err(KclErrorWithOutputs::no_outputs)?,
}
}
for constraint in add_constraints {
match constraint {
Constraint::Coincident(coincident) => {
self.add_coincident(sketch, coincident, &mut new_ast)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
}
other => {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"unsupported constraint in tail cut batch: {other:?}"
))));
}
}
}
let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
let has_constraint_deletions = !constraint_ids_set.is_empty();
for constraint_id in constraint_ids_set {
self.delete_constraint(&mut new_ast, sketch, constraint_id)
.map_err(KclErrorWithOutputs::no_outputs)?;
}
let (source_delta, mut scene_graph_delta) = self
.execute_after_edit(
ctx,
sketch,
sketch_block_ref,
segment_ids_edited,
EditDeleteKind::Edit,
&mut new_ast,
)
.await?;
if has_constraint_deletions {
scene_graph_delta.invalidates_ids = true;
}
Ok((source_delta, scene_graph_delta))
}
}
impl FrontendState {
pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
self.program = program.clone();
self.point_freedom_cache.clear();
match ctx.run_with_caching(program).await {
Ok(outcome) => {
let outcome = self.update_state_after_exec(outcome, true);
let checkpoint_id = self
.create_sketch_checkpoint(outcome.clone())
.await
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
Ok(SetProgramOutcome::Success {
scene_graph: Box::new(self.scene_graph.clone()),
exec_outcome: Box::new(outcome),
checkpoint_id: Some(checkpoint_id),
})
}
Err(mut err) => {
let outcome = self.exec_outcome_from_exec_error(err.clone())?;
self.update_state_after_exec(outcome, true);
err.scene_graph = Some(self.scene_graph.clone());
Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
}
}
}
pub async fn engine_execute(
&mut self,
ctx: &ExecutorContext,
program: Program,
) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
self.program = program.clone();
self.point_freedom_cache.clear();
match ctx.run_with_caching(program).await {
Ok(outcome) => {
let outcome = self.update_state_after_exec(outcome, true);
Ok(SceneGraphDelta {
new_graph: self.scene_graph.clone(),
exec_outcome: outcome,
new_objects: Default::default(),
invalidates_ids: Default::default(),
})
}
Err(mut err) => {
let outcome = self.exec_outcome_from_exec_error(err.clone())?;
self.update_state_after_exec(outcome, true);
err.scene_graph = Some(self.scene_graph.clone());
Err(err)
}
}
}
fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
if matches!(err.error, KclError::EngineHangup { .. }) {
return Err(err);
}
let KclErrorWithOutputs {
error,
mut non_fatal,
variables,
#[cfg(feature = "artifact-graph")]
operations,
#[cfg(feature = "artifact-graph")]
artifact_graph,
#[cfg(feature = "artifact-graph")]
scene_objects,
#[cfg(feature = "artifact-graph")]
source_range_to_object,
#[cfg(feature = "artifact-graph")]
var_solutions,
filenames,
default_planes,
..
} = err;
if let Some(source_range) = error.source_ranges().first() {
non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
} else {
non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
}
Ok(ExecOutcome {
variables,
filenames,
#[cfg(feature = "artifact-graph")]
operations,
#[cfg(feature = "artifact-graph")]
artifact_graph,
#[cfg(feature = "artifact-graph")]
scene_objects,
#[cfg(feature = "artifact-graph")]
source_range_to_object,
#[cfg(feature = "artifact-graph")]
var_solutions,
issues: non_fatal,
default_planes,
})
}
async fn add_point(
&mut self,
ctx: &ExecutorContext,
sketch: ObjectId,
ctor: PointCtor,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let at_ast = to_ast_point2d(&ctor.position)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
unlabeled: None,
arguments: vec![ast::LabeledArg {
label: Some(ast::Identifier::new(POINT_AT_PARAM)),
arg: at_ast,
}],
digest: None,
non_code_meta: Default::default(),
})));
let sketch_id = sketch;
let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
#[cfg(target_arch = "wasm32")]
web_sys::console::error_1(
&format!(
"Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
&self.scene_graph.objects
)
.into(),
);
KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
})?;
let ObjectKind::Sketch(_) = &sketch_object.kind else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Object is not a sketch, it is {}",
sketch_object.kind.human_friendly_kind_with_article(),
))));
};
let mut new_ast = self.program.ast.clone();
let (sketch_block_ref, _) = self
.mutate_ast(
&mut new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
)
.map_err(KclErrorWithOutputs::no_outputs)?;
let new_source = source_from_ast(&new_ast);
let (new_program, errors) = Program::parse(&new_source)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
if !errors.is_empty() {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Error parsing KCL source after adding point: {errors:?}"
))));
}
let Some(new_program) = new_program else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
"No AST produced after adding point".to_string(),
)));
};
let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
)))
})?;
#[cfg(not(feature = "artifact-graph"))]
let _ = point_node_ref;
self.program = new_program.clone();
let mut truncated_program = new_program;
only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
.map_err(KclErrorWithOutputs::no_outputs)?;
let outcome = ctx
.run_mock(
&truncated_program,
&MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
)
.await?;
#[cfg(not(feature = "artifact-graph"))]
let new_object_ids = Vec::new();
#[cfg(feature = "artifact-graph")]
let new_object_ids = {
let make_err =
|msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
let segment_id = outcome
.source_range_to_object
.get(&point_node_ref.range)
.copied()
.ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
let segment_object = outcome
.scene_objects
.get(segment_id.0)
.ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
let ObjectKind::Segment { segment } = &segment_object.kind else {
return Err(make_err(format!(
"Object is not a segment, it is {}",
segment_object.kind.human_friendly_kind_with_article()
)));
};
let Segment::Point(_) = segment else {
return Err(make_err(format!(
"Segment is not a point, it is {}",
segment.human_friendly_kind_with_article()
)));
};
vec![segment_id]
};
let src_delta = SourceDelta { text: new_source };
let outcome = self.update_state_after_exec(outcome, false);
let scene_graph_delta = SceneGraphDelta {
new_graph: self.scene_graph.clone(),
invalidates_ids: false,
new_objects: new_object_ids,
exec_outcome: outcome,
};
Ok((src_delta, scene_graph_delta))
}
async fn add_line(
&mut self,
ctx: &ExecutorContext,
sketch: ObjectId,
ctor: LineCtor,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let start_ast = to_ast_point2d(&ctor.start)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
let end_ast = to_ast_point2d(&ctor.end)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
let mut arguments = vec![
ast::LabeledArg {
label: Some(ast::Identifier::new(LINE_START_PARAM)),
arg: start_ast,
},
ast::LabeledArg {
label: Some(ast::Identifier::new(LINE_END_PARAM)),
arg: end_ast,
},
];
if ctor.construction == Some(true) {
arguments.push(ast::LabeledArg {
label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Bool(true),
raw: "true".to_string(),
digest: None,
}))),
});
}
let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
unlabeled: None,
arguments,
digest: None,
non_code_meta: Default::default(),
})));
let sketch_id = sketch;
let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
})?;
let ObjectKind::Sketch(_) = &sketch_object.kind else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Object is not a sketch, it is {}",
sketch_object.kind.human_friendly_kind_with_article(),
))));
};
let mut new_ast = self.program.ast.clone();
let (sketch_block_ref, _) = self
.mutate_ast(
&mut new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
)
.map_err(KclErrorWithOutputs::no_outputs)?;
let new_source = source_from_ast(&new_ast);
let (new_program, errors) = Program::parse(&new_source)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
if !errors.is_empty() {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Error parsing KCL source after adding line: {errors:?}"
))));
}
let Some(new_program) = new_program else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
"No AST produced after adding line".to_string(),
)));
};
let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
)))
})?;
#[cfg(not(feature = "artifact-graph"))]
let _ = line_node_ref;
self.program = new_program.clone();
let mut truncated_program = new_program;
only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
.map_err(KclErrorWithOutputs::no_outputs)?;
let outcome = ctx
.run_mock(
&truncated_program,
&MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
)
.await?;
#[cfg(not(feature = "artifact-graph"))]
let new_object_ids = Vec::new();
#[cfg(feature = "artifact-graph")]
let new_object_ids = {
let make_err =
|msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
let segment_id = outcome
.source_range_to_object
.get(&line_node_ref.range)
.copied()
.ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
let segment_object = outcome
.scene_object_by_id(segment_id)
.ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
let ObjectKind::Segment { segment } = &segment_object.kind else {
return Err(make_err(format!(
"Object is not a segment, it is {}",
segment_object.kind.human_friendly_kind_with_article()
)));
};
let Segment::Line(line) = segment else {
return Err(make_err(format!(
"Segment is not a line, it is {}",
segment.human_friendly_kind_with_article()
)));
};
vec![line.start, line.end, segment_id]
};
let src_delta = SourceDelta { text: new_source };
let outcome = self.update_state_after_exec(outcome, false);
let scene_graph_delta = SceneGraphDelta {
new_graph: self.scene_graph.clone(),
invalidates_ids: false,
new_objects: new_object_ids,
exec_outcome: outcome,
};
Ok((src_delta, scene_graph_delta))
}
async fn add_arc(
&mut self,
ctx: &ExecutorContext,
sketch: ObjectId,
ctor: ArcCtor,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let start_ast = to_ast_point2d(&ctor.start)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
let end_ast = to_ast_point2d(&ctor.end)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
let center_ast = to_ast_point2d(&ctor.center)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
let mut arguments = vec![
ast::LabeledArg {
label: Some(ast::Identifier::new(ARC_START_PARAM)),
arg: start_ast,
},
ast::LabeledArg {
label: Some(ast::Identifier::new(ARC_END_PARAM)),
arg: end_ast,
},
ast::LabeledArg {
label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
arg: center_ast,
},
];
if ctor.construction == Some(true) {
arguments.push(ast::LabeledArg {
label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Bool(true),
raw: "true".to_string(),
digest: None,
}))),
});
}
let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
unlabeled: None,
arguments,
digest: None,
non_code_meta: Default::default(),
})));
let sketch_id = sketch;
let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
})?;
let ObjectKind::Sketch(_) = &sketch_object.kind else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Object is not a sketch, it is {}",
sketch_object.kind.human_friendly_kind_with_article(),
))));
};
let mut new_ast = self.program.ast.clone();
let (sketch_block_ref, _) = self
.mutate_ast(
&mut new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
)
.map_err(KclErrorWithOutputs::no_outputs)?;
let new_source = source_from_ast(&new_ast);
let (new_program, errors) = Program::parse(&new_source)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
if !errors.is_empty() {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Error parsing KCL source after adding arc: {errors:?}"
))));
}
let Some(new_program) = new_program else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
"No AST produced after adding arc".to_string(),
)));
};
let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
)))
})?;
#[cfg(not(feature = "artifact-graph"))]
let _ = arc_node_ref;
self.program = new_program.clone();
let mut truncated_program = new_program;
only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
.map_err(KclErrorWithOutputs::no_outputs)?;
let outcome = ctx
.run_mock(
&truncated_program,
&MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
)
.await?;
#[cfg(not(feature = "artifact-graph"))]
let new_object_ids = Vec::new();
#[cfg(feature = "artifact-graph")]
let new_object_ids = {
let make_err =
|msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
let segment_id = outcome
.source_range_to_object
.get(&arc_node_ref.range)
.copied()
.ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
let segment_object = outcome
.scene_objects
.get(segment_id.0)
.ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
let ObjectKind::Segment { segment } = &segment_object.kind else {
return Err(make_err(format!(
"Object is not a segment, it is {}",
segment_object.kind.human_friendly_kind_with_article()
)));
};
let Segment::Arc(arc) = segment else {
return Err(make_err(format!(
"Segment is not an arc, it is {}",
segment.human_friendly_kind_with_article()
)));
};
vec![arc.start, arc.end, arc.center, segment_id]
};
let src_delta = SourceDelta { text: new_source };
let outcome = self.update_state_after_exec(outcome, false);
let scene_graph_delta = SceneGraphDelta {
new_graph: self.scene_graph.clone(),
invalidates_ids: false,
new_objects: new_object_ids,
exec_outcome: outcome,
};
Ok((src_delta, scene_graph_delta))
}
async fn add_circle(
&mut self,
ctx: &ExecutorContext,
sketch: ObjectId,
ctor: CircleCtor,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let start_ast = to_ast_point2d(&ctor.start)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
let center_ast = to_ast_point2d(&ctor.center)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
let mut arguments = vec![
ast::LabeledArg {
label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
arg: start_ast,
},
ast::LabeledArg {
label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
arg: center_ast,
},
];
if ctor.construction == Some(true) {
arguments.push(ast::LabeledArg {
label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Bool(true),
raw: "true".to_string(),
digest: None,
}))),
});
}
let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
unlabeled: None,
arguments,
digest: None,
non_code_meta: Default::default(),
})));
let sketch_id = sketch;
let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
})?;
let ObjectKind::Sketch(_) = &sketch_object.kind else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Object is not a sketch, it is {}",
sketch_object.kind.human_friendly_kind_with_article(),
))));
};
let mut new_ast = self.program.ast.clone();
let (sketch_block_ref, _) = self
.mutate_ast(
&mut new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockVarDecl {
prefix: CIRCLE_VARIABLE.to_owned(),
expr: circle_ast,
},
)
.map_err(KclErrorWithOutputs::no_outputs)?;
let new_source = source_from_ast(&new_ast);
let (new_program, errors) = Program::parse(&new_source)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
if !errors.is_empty() {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Error parsing KCL source after adding circle: {errors:?}"
))));
}
let Some(new_program) = new_program else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
"No AST produced after adding circle".to_string(),
)));
};
let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
)))
})?;
#[cfg(not(feature = "artifact-graph"))]
let _ = circle_node_ref;
self.program = new_program.clone();
let mut truncated_program = new_program;
only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
.map_err(KclErrorWithOutputs::no_outputs)?;
let outcome = ctx
.run_mock(
&truncated_program,
&MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
)
.await?;
#[cfg(not(feature = "artifact-graph"))]
let new_object_ids = Vec::new();
#[cfg(feature = "artifact-graph")]
let new_object_ids = {
let make_err =
|msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
let segment_id = outcome
.source_range_to_object
.get(&circle_node_ref.range)
.copied()
.ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
let segment_object = outcome
.scene_objects
.get(segment_id.0)
.ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
let ObjectKind::Segment { segment } = &segment_object.kind else {
return Err(make_err(format!(
"Object is not a segment, it is {}",
segment_object.kind.human_friendly_kind_with_article()
)));
};
let Segment::Circle(circle) = segment else {
return Err(make_err(format!(
"Segment is not a circle, it is {}",
segment.human_friendly_kind_with_article()
)));
};
vec![circle.start, circle.center, segment_id]
};
let src_delta = SourceDelta { text: new_source };
let outcome = self.update_state_after_exec(outcome, false);
let scene_graph_delta = SceneGraphDelta {
new_graph: self.scene_graph.clone(),
invalidates_ids: false,
new_objects: new_object_ids,
exec_outcome: outcome,
};
Ok((src_delta, scene_graph_delta))
}
fn edit_point(
&mut self,
new_ast: &mut ast::Node<ast::Program>,
sketch: ObjectId,
point: ObjectId,
ctor: PointCtor,
) -> Result<(), KclError> {
let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
let sketch_id = sketch;
let sketch_object = self
.scene_graph
.objects
.get(sketch_id.0)
.ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
};
sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
})?;
let point_id = point;
let point_object = self
.scene_graph
.objects
.get(point_id.0)
.ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
let ObjectKind::Segment {
segment: Segment::Point(point),
} = &point_object.kind
else {
return Err(KclError::refactor(format!(
"Object is not a point segment: {point_object:?}"
)));
};
if let Some(owner_id) = point.owner {
let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
KclError::refactor(format!(
"Internal: Owner of point not found in scene graph: owner={owner_id:?}",
))
})?;
let ObjectKind::Segment { segment } = &owner_object.kind else {
return Err(KclError::refactor(format!(
"Internal: Owner of point is not a segment, but found {}",
owner_object.kind.human_friendly_kind_with_article()
)));
};
if let Segment::Line(line) = segment {
let SegmentCtor::Line(line_ctor) = &line.ctor else {
return Err(KclError::refactor(format!(
"Internal: Owner of point does not have line ctor, but found {}",
line.ctor.human_friendly_kind_with_article()
)));
};
let mut line_ctor = line_ctor.clone();
if line.start == point_id {
line_ctor.start = ctor.position;
} else if line.end == point_id {
line_ctor.end = ctor.position;
} else {
return Err(KclError::refactor(format!(
"Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
)));
}
return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
}
if let Segment::Arc(arc) = segment {
let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
return Err(KclError::refactor(format!(
"Internal: Owner of point does not have arc ctor, but found {}",
arc.ctor.human_friendly_kind_with_article()
)));
};
let mut arc_ctor = arc_ctor.clone();
if arc.center == point_id {
arc_ctor.center = ctor.position;
} else if arc.start == point_id {
arc_ctor.start = ctor.position;
} else if arc.end == point_id {
arc_ctor.end = ctor.position;
} else {
return Err(KclError::refactor(format!(
"Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
)));
}
return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
}
if let Segment::Circle(circle) = segment {
let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
return Err(KclError::refactor(format!(
"Internal: Owner of point does not have circle ctor, but found {}",
circle.ctor.human_friendly_kind_with_article()
)));
};
let mut circle_ctor = circle_ctor.clone();
if circle.center == point_id {
circle_ctor.center = ctor.position;
} else if circle.start == point_id {
circle_ctor.start = ctor.position;
} else {
return Err(KclError::refactor(format!(
"Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
)));
}
return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
}
}
self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
Ok(())
}
fn edit_line(
&mut self,
new_ast: &mut ast::Node<ast::Program>,
sketch: ObjectId,
line: ObjectId,
ctor: LineCtor,
) -> Result<(), KclError> {
let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
let sketch_id = sketch;
let sketch_object = self
.scene_graph
.objects
.get(sketch_id.0)
.ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
};
sketch
.segments
.iter()
.find(|o| **o == line)
.ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
let line_id = line;
let line_object = self
.scene_graph
.objects
.get(line_id.0)
.ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
let ObjectKind::Segment { .. } = &line_object.kind else {
let kind = line_object.kind.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"This constraint only works on Segments, but you selected {kind}"
)));
};
self.mutate_ast(
new_ast,
line_id,
AstMutateCommand::EditLine {
start: new_start_ast,
end: new_end_ast,
construction: ctor.construction,
},
)?;
Ok(())
}
fn edit_arc(
&mut self,
new_ast: &mut ast::Node<ast::Program>,
sketch: ObjectId,
arc: ObjectId,
ctor: ArcCtor,
) -> Result<(), KclError> {
let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
let sketch_id = sketch;
let sketch_object = self
.scene_graph
.objects
.get(sketch_id.0)
.ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
};
sketch
.segments
.iter()
.find(|o| **o == arc)
.ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
let arc_id = arc;
let arc_object = self
.scene_graph
.objects
.get(arc_id.0)
.ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
let ObjectKind::Segment { .. } = &arc_object.kind else {
return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
};
self.mutate_ast(
new_ast,
arc_id,
AstMutateCommand::EditArc {
start: new_start_ast,
end: new_end_ast,
center: new_center_ast,
construction: ctor.construction,
},
)?;
Ok(())
}
fn edit_circle(
&mut self,
new_ast: &mut ast::Node<ast::Program>,
sketch: ObjectId,
circle: ObjectId,
ctor: CircleCtor,
) -> Result<(), KclError> {
let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
let sketch_id = sketch;
let sketch_object = self
.scene_graph
.objects
.get(sketch_id.0)
.ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
};
sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
KclError::refactor(format!(
"Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
))
})?;
let circle_id = circle;
let circle_object = self
.scene_graph
.objects
.get(circle_id.0)
.ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
let ObjectKind::Segment { .. } = &circle_object.kind else {
return Err(KclError::refactor(format!(
"Object is not a segment: {circle_object:?}"
)));
};
self.mutate_ast(
new_ast,
circle_id,
AstMutateCommand::EditCircle {
start: new_start_ast,
center: new_center_ast,
construction: ctor.construction,
},
)?;
Ok(())
}
fn delete_segment(
&mut self,
new_ast: &mut ast::Node<ast::Program>,
sketch: ObjectId,
segment_id: ObjectId,
) -> Result<(), KclError> {
let sketch_id = sketch;
let sketch_object = self
.scene_graph
.objects
.get(sketch_id.0)
.ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
};
sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
KclError::refactor(format!(
"Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
))
})?;
let segment_object =
self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
})?;
let ObjectKind::Segment { .. } = &segment_object.kind else {
return Err(KclError::refactor(format!(
"Object is not a segment, it is {}",
segment_object.kind.human_friendly_kind_with_article()
)));
};
self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
Ok(())
}
fn delete_constraint(
&mut self,
new_ast: &mut ast::Node<ast::Program>,
sketch: ObjectId,
constraint_id: ObjectId,
) -> Result<(), KclError> {
let sketch_id = sketch;
let sketch_object = self
.scene_graph
.objects
.get(sketch_id.0)
.ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
};
sketch
.constraints
.iter()
.find(|o| **o == constraint_id)
.ok_or_else(|| {
KclError::refactor(format!(
"Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
))
})?;
let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
KclError::refactor(format!(
"Constraint not found in scene graph: constraint={constraint_id:?}"
))
})?;
let ObjectKind::Constraint { .. } = &constraint_object.kind else {
return Err(KclError::refactor(format!(
"Object is not a constraint, it is {}",
constraint_object.kind.human_friendly_kind_with_article()
)));
};
self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
Ok(())
}
fn edit_coincident_constraint(
&mut self,
new_ast: &mut ast::Node<ast::Program>,
constraint_id: ObjectId,
segments: Vec<ConstraintSegment>,
) -> Result<(), KclError> {
if segments.len() < 2 {
return Err(KclError::refactor(format!(
"Coincident constraint must have at least 2 inputs, got {}",
segments.len()
)));
}
let segment_asts = segments
.iter()
.map(|segment| self.coincident_segment_to_ast(segment, new_ast))
.collect::<Result<Vec<_>, _>>()?;
let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
elements: segment_asts,
digest: None,
non_code_meta: Default::default(),
})));
self.mutate_ast(
new_ast,
constraint_id,
AstMutateCommand::EditCallUnlabeled { arg: array_expr },
)?;
Ok(())
}
fn edit_equal_length_constraint(
&mut self,
new_ast: &mut ast::Node<ast::Program>,
constraint_id: ObjectId,
lines: Vec<ObjectId>,
) -> Result<(), KclError> {
if lines.len() < 2 {
return Err(KclError::refactor(format!(
"Lines equal length constraint must have at least 2 lines, got {}",
lines.len()
)));
}
let line_asts = lines
.iter()
.map(|line_id| {
let line_object = self
.scene_graph
.objects
.get(line_id.0)
.ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
let kind = line_object.kind.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"This constraint only works on Segments, but you selected {kind}"
)));
};
let Segment::Line(_) = line_segment else {
let kind = line_segment.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"Only lines can be made equal length, but you selected {kind}"
)));
};
get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
})
.collect::<Result<Vec<_>, _>>()?;
let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
elements: line_asts,
digest: None,
non_code_meta: Default::default(),
})));
self.mutate_ast(
new_ast,
constraint_id,
AstMutateCommand::EditCallUnlabeled { arg: array_expr },
)?;
Ok(())
}
fn edit_parallel_constraint(
&mut self,
new_ast: &mut ast::Node<ast::Program>,
constraint_id: ObjectId,
lines: Vec<ObjectId>,
) -> Result<(), KclError> {
if lines.len() < 2 {
return Err(KclError::refactor(format!(
"Parallel constraint must have at least 2 lines, got {}",
lines.len()
)));
}
let line_asts = lines
.iter()
.map(|line_id| {
let line_object = self
.scene_graph
.objects
.get(line_id.0)
.ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
let kind = line_object.kind.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"This constraint only works on Segments, but you selected {kind}"
)));
};
let Segment::Line(_) = line_segment else {
let kind = line_segment.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"Only lines can be made parallel, but you selected {kind}"
)));
};
get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
})
.collect::<Result<Vec<_>, _>>()?;
let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
elements: line_asts,
digest: None,
non_code_meta: Default::default(),
})));
self.mutate_ast(
new_ast,
constraint_id,
AstMutateCommand::EditCallUnlabeled { arg: array_expr },
)?;
Ok(())
}
fn edit_equal_radius_constraint(
&mut self,
new_ast: &mut ast::Node<ast::Program>,
constraint_id: ObjectId,
input: Vec<ObjectId>,
) -> Result<(), KclError> {
if input.len() < 2 {
return Err(KclError::refactor(format!(
"equalRadius constraint must have at least 2 segments, got {}",
input.len()
)));
}
let input_asts = input
.iter()
.map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
.collect::<Result<Vec<_>, _>>()?;
let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
elements: input_asts,
digest: None,
non_code_meta: Default::default(),
})));
self.mutate_ast(
new_ast,
constraint_id,
AstMutateCommand::EditCallUnlabeled { arg: array_expr },
)?;
Ok(())
}
async fn execute_after_edit(
&mut self,
ctx: &ExecutorContext,
sketch: ObjectId,
sketch_block_ref: AstNodeRef,
segment_ids_edited: AhashIndexSet<ObjectId>,
edit_kind: EditDeleteKind,
new_ast: &mut ast::Node<ast::Program>,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let new_source = source_from_ast(new_ast);
let (new_program, errors) = Program::parse(&new_source)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
if !errors.is_empty() {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Error parsing KCL source after editing: {errors:?}"
))));
}
let Some(new_program) = new_program else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
"No AST produced after editing".to_string(),
)));
};
self.program = new_program.clone();
let is_delete = edit_kind.is_delete();
let truncated_program = {
let mut truncated_program = new_program;
only_sketch_block(
&mut truncated_program.ast,
&sketch_block_ref,
edit_kind.to_change_kind(),
)
.map_err(KclErrorWithOutputs::no_outputs)?;
truncated_program
};
#[cfg(not(feature = "artifact-graph"))]
drop(segment_ids_edited);
let mock_config = MockConfig {
sketch_block_id: Some(sketch),
freedom_analysis: is_delete,
#[cfg(feature = "artifact-graph")]
segment_ids_edited: segment_ids_edited.clone(),
..Default::default()
};
let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
let outcome = self.update_state_after_exec(outcome, is_delete);
#[cfg(feature = "artifact-graph")]
let new_source = {
let mut new_ast = self.program.ast.clone();
for (var_range, value) in &outcome.var_solutions {
let rounded = value.round(3);
mutate_ast_node_by_source_range(
&mut new_ast,
*var_range,
AstMutateCommand::EditVarInitialValue { value: rounded },
)
.map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
}
source_from_ast(&new_ast)
};
let src_delta = SourceDelta { text: new_source };
let scene_graph_delta = SceneGraphDelta {
new_graph: self.scene_graph.clone(),
invalidates_ids: is_delete,
new_objects: Vec::new(),
exec_outcome: outcome,
};
Ok((src_delta, scene_graph_delta))
}
async fn execute_after_delete_sketch(
&mut self,
ctx: &ExecutorContext,
new_ast: &mut ast::Node<ast::Program>,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let new_source = source_from_ast(new_ast);
let (new_program, errors) = Program::parse(&new_source)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
if !errors.is_empty() {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Error parsing KCL source after editing: {errors:?}"
))));
}
let Some(new_program) = new_program else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
"No AST produced after editing".to_string(),
)));
};
self.program = new_program.clone();
let outcome = ctx.run_with_caching(new_program).await?;
let freedom_analysis_ran = true;
let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
let src_delta = SourceDelta { text: new_source };
let scene_graph_delta = SceneGraphDelta {
new_graph: self.scene_graph.clone(),
invalidates_ids: true,
new_objects: Vec::new(),
exec_outcome: outcome,
};
Ok((src_delta, scene_graph_delta))
}
fn point_id_to_ast_reference(
&self,
point_id: ObjectId,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<ast::Expr, KclError> {
let point_object = self
.scene_graph
.objects
.get(point_id.0)
.ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
};
let Segment::Point(point) = point_segment else {
return Err(KclError::refactor(format!(
"Only points are currently supported: {point_object:?}"
)));
};
if let Some(owner_id) = point.owner {
let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
KclError::refactor(format!(
"Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
))
})?;
let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
return Err(KclError::refactor(format!(
"Owner of point is not a segment, but found {}",
owner_object.kind.human_friendly_kind_with_article()
)));
};
match owner_segment {
Segment::Line(line) => {
let property = if line.start == point_id {
LINE_PROPERTY_START
} else if line.end == point_id {
LINE_PROPERTY_END
} else {
return Err(KclError::refactor(format!(
"Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
)));
};
get_or_insert_ast_reference(new_ast, &owner_object.source, "line", Some(property))
}
Segment::Arc(arc) => {
let property = if arc.start == point_id {
ARC_PROPERTY_START
} else if arc.end == point_id {
ARC_PROPERTY_END
} else if arc.center == point_id {
ARC_PROPERTY_CENTER
} else {
return Err(KclError::refactor(format!(
"Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
)));
};
get_or_insert_ast_reference(new_ast, &owner_object.source, "arc", Some(property))
}
Segment::Circle(circle) => {
let property = if circle.start == point_id {
CIRCLE_PROPERTY_START
} else if circle.center == point_id {
CIRCLE_PROPERTY_CENTER
} else {
return Err(KclError::refactor(format!(
"Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
)));
};
get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
}
_ => Err(KclError::refactor(format!(
"Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
))),
}
} else {
get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
}
}
fn coincident_segment_to_ast(
&self,
segment: &ConstraintSegment,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<ast::Expr, KclError> {
match segment {
ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
ConstraintSegment::Segment(segment_id) => {
let segment_object = self
.scene_graph
.objects
.get(segment_id.0)
.ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
let ObjectKind::Segment { segment } = &segment_object.kind else {
return Err(KclError::refactor(format!(
"Object is not a segment, it is {}",
segment_object.kind.human_friendly_kind_with_article()
)));
};
match segment {
Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None),
Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
Segment::Circle(_) => {
get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
}
}
}
}
}
fn axis_constraint_segment_to_ast(
&self,
segment: &ConstraintSegment,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<ast::Expr, KclError> {
match segment {
ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
}
}
async fn add_coincident(
&mut self,
sketch: ObjectId,
coincident: Coincident,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let sketch_id = sketch;
let segment_asts = coincident
.segments
.iter()
.map(|segment| self.coincident_segment_to_ast(segment, new_ast))
.collect::<Result<Vec<_>, _>>()?;
if segment_asts.len() < 2 {
return Err(KclError::refactor(format!(
"Coincident constraint must have at least 2 inputs, got {}",
segment_asts.len()
)));
}
let coincident_ast = create_coincident_ast(segment_asts);
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
)?;
Ok(sketch_block_ref)
}
async fn add_distance(
&mut self,
sketch: ObjectId,
distance: Distance,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let sketch_id = sketch;
let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
[pt0, pt1] => [
self.coincident_segment_to_ast(pt0, new_ast)?,
self.coincident_segment_to_ast(pt1, new_ast)?,
],
_ => {
return Err(KclError::refactor(format!(
"Distance constraint must have exactly 2 points, got {}",
distance.points.len()
)));
}
};
let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
ast::ArrayExpression {
elements: vec![pt0_ast, pt1_ast],
digest: None,
non_code_meta: Default::default(),
},
)))),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})));
let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
left: distance_call_ast,
operator: ast::BinaryOperator::Eq,
right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Number {
value: distance.distance.value,
suffix: distance.distance.units,
},
raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
KclError::refactor(format!(
"Could not format numeric suffix: {:?}",
distance.distance.units
))
})?,
digest: None,
}))),
digest: None,
})));
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
)?;
Ok(sketch_block_ref)
}
async fn add_angle(
&mut self,
sketch: ObjectId,
angle: Angle,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let &[l0_id, l1_id] = angle.lines.as_slice() else {
return Err(KclError::refactor(format!(
"Angle constraint must have exactly 2 lines, got {}",
angle.lines.len()
)));
};
let sketch_id = sketch;
let line0_object = self
.scene_graph
.objects
.get(l0_id.0)
.ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
};
let Segment::Line(_) = line0_segment else {
return Err(KclError::refactor(format!(
"Only lines can be constrained to meet at an angle: {line0_object:?}",
)));
};
let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
let line1_object = self
.scene_graph
.objects
.get(l1_id.0)
.ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
};
let Segment::Line(_) = line1_segment else {
return Err(KclError::refactor(format!(
"Only lines can be constrained to meet at an angle: {line1_object:?}",
)));
};
let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
ast::ArrayExpression {
elements: vec![l0_ast, l1_ast],
digest: None,
non_code_meta: Default::default(),
},
)))),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})));
let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
left: angle_call_ast,
operator: ast::BinaryOperator::Eq,
right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Number {
value: angle.angle.value,
suffix: angle.angle.units,
},
raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
})?,
digest: None,
}))),
digest: None,
})));
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
)?;
Ok(sketch_block_ref)
}
async fn add_tangent(
&mut self,
sketch: ObjectId,
tangent: Tangent,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
return Err(KclError::refactor(format!(
"Tangent constraint must have exactly 2 segments, got {}",
tangent.input.len()
)));
};
let sketch_id = sketch;
let seg0_object = self
.scene_graph
.objects
.get(seg0_id.0)
.ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
};
let seg0_ast = match seg0_segment {
Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?,
Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?,
Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
_ => {
return Err(KclError::refactor(format!(
"Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
)));
}
};
let seg1_object = self
.scene_graph
.objects
.get(seg1_id.0)
.ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
};
let seg1_ast = match seg1_segment {
Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?,
Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?,
Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
_ => {
return Err(KclError::refactor(format!(
"Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
)));
}
};
let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
)?;
Ok(sketch_block_ref)
}
async fn add_equal_radius(
&mut self,
sketch: ObjectId,
equal_radius: EqualRadius,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
if equal_radius.input.len() < 2 {
return Err(KclError::refactor(format!(
"equalRadius constraint must have at least 2 segments, got {}",
equal_radius.input.len()
)));
}
let sketch_id = sketch;
let input_asts = equal_radius
.input
.iter()
.map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
.collect::<Result<Vec<_>, _>>()?;
let equal_radius_ast = create_equal_radius_ast(input_asts);
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
)?;
Ok(sketch_block_ref)
}
async fn add_radius(
&mut self,
sketch: ObjectId,
radius: Radius,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let params = ArcSizeConstraintParams {
points: vec![radius.arc],
function_name: RADIUS_FN,
value: radius.radius.value,
units: radius.radius.units,
constraint_type_name: "Radius",
};
self.add_arc_size_constraint(sketch, params, new_ast).await
}
async fn add_diameter(
&mut self,
sketch: ObjectId,
diameter: Diameter,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let params = ArcSizeConstraintParams {
points: vec![diameter.arc],
function_name: DIAMETER_FN,
value: diameter.diameter.value,
units: diameter.diameter.units,
constraint_type_name: "Diameter",
};
self.add_arc_size_constraint(sketch, params, new_ast).await
}
async fn add_fixed_constraints(
&mut self,
sketch: ObjectId,
points: Vec<FixedPoint>,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let mut sketch_block_ref = None;
for fixed_point in points {
let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
.map_err(|err| KclError::refactor(err.to_string()))?;
let (sketch_ref, _) = self.mutate_ast(
new_ast,
sketch,
AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
)?;
sketch_block_ref = Some(sketch_ref);
}
sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
}
async fn add_arc_size_constraint(
&mut self,
sketch: ObjectId,
params: ArcSizeConstraintParams,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let sketch_id = sketch;
if params.points.len() != 1 {
return Err(KclError::refactor(format!(
"{} constraint must have exactly 1 argument (an arc segment), got {}",
params.constraint_type_name,
params.points.len()
)));
}
let arc_id = params.points[0];
let arc_object = self
.scene_graph
.objects
.get(arc_id.0)
.ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
};
let ref_type = match arc_segment {
Segment::Arc(_) => "arc",
Segment::Circle(_) => CIRCLE_VARIABLE,
_ => {
return Err(KclError::refactor(format!(
"{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
params.constraint_type_name
)));
}
};
let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
unlabeled: Some(arc_ast),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})));
let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
left: call_ast,
operator: ast::BinaryOperator::Eq,
right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Number {
value: params.value,
suffix: params.units,
},
raw: format_number_literal(params.value, params.units, None)
.map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
digest: None,
}))),
digest: None,
})));
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
)?;
Ok(sketch_block_ref)
}
async fn add_horizontal_distance(
&mut self,
sketch: ObjectId,
distance: Distance,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let sketch_id = sketch;
let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
[pt0, pt1] => [
self.coincident_segment_to_ast(pt0, new_ast)?,
self.coincident_segment_to_ast(pt1, new_ast)?,
],
_ => {
return Err(KclError::refactor(format!(
"Horizontal distance constraint must have exactly 2 points, got {}",
distance.points.len()
)));
}
};
let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
ast::ArrayExpression {
elements: vec![pt0_ast, pt1_ast],
digest: None,
non_code_meta: Default::default(),
},
)))),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})));
let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
left: distance_call_ast,
operator: ast::BinaryOperator::Eq,
right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Number {
value: distance.distance.value,
suffix: distance.distance.units,
},
raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
KclError::refactor(format!(
"Could not format numeric suffix: {:?}",
distance.distance.units
))
})?,
digest: None,
}))),
digest: None,
})));
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
)?;
Ok(sketch_block_ref)
}
async fn add_vertical_distance(
&mut self,
sketch: ObjectId,
distance: Distance,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let sketch_id = sketch;
let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
[pt0, pt1] => [
self.coincident_segment_to_ast(pt0, new_ast)?,
self.coincident_segment_to_ast(pt1, new_ast)?,
],
_ => {
return Err(KclError::refactor(format!(
"Vertical distance constraint must have exactly 2 points, got {}",
distance.points.len()
)));
}
};
let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
ast::ArrayExpression {
elements: vec![pt0_ast, pt1_ast],
digest: None,
non_code_meta: Default::default(),
},
)))),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})));
let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
left: distance_call_ast,
operator: ast::BinaryOperator::Eq,
right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Number {
value: distance.distance.value,
suffix: distance.distance.units,
},
raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
KclError::refactor(format!(
"Could not format numeric suffix: {:?}",
distance.distance.units
))
})?,
digest: None,
}))),
digest: None,
})));
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
)?;
Ok(sketch_block_ref)
}
async fn add_horizontal(
&mut self,
sketch: ObjectId,
horizontal: Horizontal,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let sketch_id = sketch;
let first_arg_ast = match horizontal {
Horizontal::Line { line } => {
let line_object = self
.scene_graph
.objects
.get(line.0)
.ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
let kind = line_object.kind.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"This constraint only works on Segments, but you selected {kind}"
)));
};
let Segment::Line(_) = line_segment else {
return Err(KclError::refactor(format!(
"Only lines can be made horizontal, but you selected {}",
line_segment.human_friendly_kind_with_article(),
)));
};
get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?
}
Horizontal::Points { points } => {
let point_asts = points
.iter()
.map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
.collect::<Result<Vec<_>, _>>()?;
ast::ArrayExpression::new(point_asts).into()
}
};
let horizontal_ast = create_horizontal_ast(first_arg_ast);
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
)?;
Ok(sketch_block_ref)
}
async fn add_lines_equal_length(
&mut self,
sketch: ObjectId,
lines_equal_length: LinesEqualLength,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
if lines_equal_length.lines.len() < 2 {
return Err(KclError::refactor(format!(
"Lines equal length constraint must have at least 2 lines, got {}",
lines_equal_length.lines.len()
)));
};
let sketch_id = sketch;
let line_asts = lines_equal_length
.lines
.iter()
.map(|line_id| {
let line_object = self
.scene_graph
.objects
.get(line_id.0)
.ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
let kind = line_object.kind.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"This constraint only works on Segments, but you selected {kind}"
)));
};
let Segment::Line(_) = line_segment else {
let kind = line_segment.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"Only lines can be made equal length, but you selected {kind}"
)));
};
get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
})
.collect::<Result<Vec<_>, _>>()?;
let equal_length_ast = create_equal_length_ast(line_asts);
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
)?;
Ok(sketch_block_ref)
}
fn equal_radius_segment_id_to_ast_reference(
&mut self,
segment_id: ObjectId,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<ast::Expr, KclError> {
let segment_object = self
.scene_graph
.objects
.get(segment_id.0)
.ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
let ObjectKind::Segment { segment } = &segment_object.kind else {
return Err(KclError::refactor(format!(
"Object is not a segment, it was {}",
segment_object.kind.human_friendly_kind_with_article()
)));
};
let ref_type = match segment {
Segment::Arc(_) => "arc",
Segment::Circle(_) => CIRCLE_VARIABLE,
_ => {
return Err(KclError::refactor(format!(
"equalRadius supports only arc/circle segments, got {}",
segment.human_friendly_kind_with_article()
)));
}
};
get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
}
async fn add_parallel(
&mut self,
sketch: ObjectId,
parallel: Parallel,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
if parallel.lines.len() < 2 {
return Err(KclError::refactor(format!(
"Parallel constraint must have at least 2 lines, got {}",
parallel.lines.len()
)));
};
let sketch_id = sketch;
let line_asts = parallel
.lines
.iter()
.map(|line_id| {
let line_object = self
.scene_graph
.objects
.get(line_id.0)
.ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
let kind = line_object.kind.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"This constraint only works on Segments, but you selected {kind}"
)));
};
let Segment::Line(_) = line_segment else {
let kind = line_segment.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"Only lines can be made parallel, but you selected {kind}"
)));
};
get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
})
.collect::<Result<Vec<_>, _>>()?;
let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
ast::ArrayExpression {
elements: line_asts,
digest: None,
non_code_meta: Default::default(),
},
)))),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})));
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
)?;
Ok(sketch_block_ref)
}
async fn add_perpendicular(
&mut self,
sketch: ObjectId,
perpendicular: Perpendicular,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
.await
}
async fn add_lines_at_angle_constraint(
&mut self,
sketch: ObjectId,
angle_kind: LinesAtAngleKind,
lines: Vec<ObjectId>,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let &[line0_id, line1_id] = lines.as_slice() else {
return Err(KclError::refactor(format!(
"{} constraint must have exactly 2 lines, got {}",
angle_kind.to_function_name(),
lines.len()
)));
};
let sketch_id = sketch;
let line0_object = self
.scene_graph
.objects
.get(line0_id.0)
.ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
let kind = line0_object.kind.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"This constraint only works on Segments, but you selected {kind}"
)));
};
let Segment::Line(_) = line0_segment else {
return Err(KclError::refactor(format!(
"Only lines can be made {}, but you selected {}",
angle_kind.to_function_name(),
line0_segment.human_friendly_kind_with_article(),
)));
};
let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
let line1_object = self
.scene_graph
.objects
.get(line1_id.0)
.ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
let kind = line1_object.kind.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"This constraint only works on Segments, but you selected {kind}"
)));
};
let Segment::Line(_) = line1_segment else {
return Err(KclError::refactor(format!(
"Only lines can be made {}, but you selected {}",
angle_kind.to_function_name(),
line1_segment.human_friendly_kind_with_article(),
)));
};
let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
ast::ArrayExpression {
elements: vec![line0_ast, line1_ast],
digest: None,
non_code_meta: Default::default(),
},
)))),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})));
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
)?;
Ok(sketch_block_ref)
}
async fn add_vertical(
&mut self,
sketch: ObjectId,
vertical: Vertical,
new_ast: &mut ast::Node<ast::Program>,
) -> Result<AstNodeRef, KclError> {
let sketch_id = sketch;
let first_arg_ast = match vertical {
Vertical::Line { line } => {
let line_object = self
.scene_graph
.objects
.get(line.0)
.ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
let kind = line_object.kind.human_friendly_kind_with_article();
return Err(KclError::refactor(format!(
"This constraint only works on Segments, but you selected {kind}"
)));
};
let Segment::Line(_) = line_segment else {
return Err(KclError::refactor(format!(
"Only lines can be made vertical, but you selected {}",
line_segment.human_friendly_kind_with_article()
)));
};
get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?
}
Vertical::Points { points } => {
let point_asts = points
.iter()
.map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
.collect::<Result<Vec<_>, _>>()?;
ast::ArrayExpression::new(point_asts).into()
}
};
let vertical_ast = create_vertical_ast(first_arg_ast);
let (sketch_block_ref, _) = self.mutate_ast(
new_ast,
sketch_id,
AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
)?;
Ok(sketch_block_ref)
}
async fn execute_after_add_constraint(
&mut self,
ctx: &ExecutorContext,
sketch_id: ObjectId,
#[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_ref: AstNodeRef,
new_ast: &mut ast::Node<ast::Program>,
) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
let new_source = source_from_ast(new_ast);
let (new_program, errors) = Program::parse(&new_source)
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
if !errors.is_empty() {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Error parsing KCL source after adding constraint: {errors:?}"
))));
}
let Some(new_program) = new_program else {
return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
"No AST produced after adding constraint".to_string(),
)));
};
#[cfg(feature = "artifact-graph")]
let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
"Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
)))
})?;
let mut truncated_program = new_program.clone();
only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
.map_err(KclErrorWithOutputs::no_outputs)?;
let outcome = ctx
.run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
.await?;
#[cfg(not(feature = "artifact-graph"))]
let new_object_ids = Vec::new();
#[cfg(feature = "artifact-graph")]
let new_object_ids = {
let constraint_id = outcome
.source_range_to_object
.get(&constraint_node_ref.range)
.copied()
.ok_or_else(|| {
KclErrorWithOutputs::from_error_outcome(
KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
outcome.clone(),
)
})?;
vec![constraint_id]
};
self.program = new_program;
let outcome = self.update_state_after_exec(outcome, true);
let src_delta = SourceDelta { text: new_source };
let scene_graph_delta = SceneGraphDelta {
new_graph: self.scene_graph.clone(),
invalidates_ids: false,
new_objects: new_object_ids,
exec_outcome: outcome,
};
Ok((src_delta, scene_graph_delta))
}
fn find_referenced_constraints(
&self,
sketch_id: ObjectId,
segment_ids_set: &AhashIndexSet<ObjectId>,
) -> Result<AhashIndexSet<ObjectId>, KclError> {
let sketch_object = self
.scene_graph
.objects
.get(sketch_id.0)
.ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
};
let mut constraint_ids_set = AhashIndexSet::default();
for constraint_id in &sketch.constraints {
let constraint_object = self
.scene_graph
.objects
.get(constraint_id.0)
.ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
return Err(KclError::refactor(format!(
"Object is not a constraint, it is {}",
constraint_object.kind.human_friendly_kind_with_article()
)));
};
let depends_on_segment = match constraint {
Constraint::Coincident(c) => c.segment_ids().any(|seg_id| {
if segment_ids_set.contains(&seg_id) {
return true;
}
let seg_object = self.scene_graph.objects.get(seg_id.0);
if let Some(obj) = seg_object
&& let ObjectKind::Segment { segment } = &obj.kind
&& let Segment::Point(pt) = segment
&& let Some(owner_line_id) = pt.owner
{
return segment_ids_set.contains(&owner_line_id);
}
false
}),
Constraint::Distance(d) => d.point_ids().any(|pt_id| {
if segment_ids_set.contains(&pt_id) {
return true;
}
let pt_object = self.scene_graph.objects.get(pt_id.0);
if let Some(obj) = pt_object
&& let ObjectKind::Segment { segment } = &obj.kind
&& let Segment::Point(pt) = segment
&& let Some(owner_line_id) = pt.owner
{
return segment_ids_set.contains(&owner_line_id);
}
false
}),
Constraint::Fixed(_) => false,
Constraint::Radius(r) => segment_ids_set.contains(&r.arc),
Constraint::Diameter(d) => segment_ids_set.contains(&d.arc),
Constraint::EqualRadius(equal_radius) => {
equal_radius.input.iter().any(|seg_id| segment_ids_set.contains(seg_id))
}
Constraint::HorizontalDistance(d) => d.point_ids().any(|pt_id| {
let pt_object = self.scene_graph.objects.get(pt_id.0);
if let Some(obj) = pt_object
&& let ObjectKind::Segment { segment } = &obj.kind
&& let Segment::Point(pt) = segment
&& let Some(owner_line_id) = pt.owner
{
return segment_ids_set.contains(&owner_line_id);
}
false
}),
Constraint::VerticalDistance(d) => d.point_ids().any(|pt_id| {
let pt_object = self.scene_graph.objects.get(pt_id.0);
if let Some(obj) = pt_object
&& let ObjectKind::Segment { segment } = &obj.kind
&& let Segment::Point(pt) = segment
&& let Some(owner_line_id) = pt.owner
{
return segment_ids_set.contains(&owner_line_id);
}
false
}),
Constraint::Horizontal(h) => match h {
Horizontal::Line { line } => segment_ids_set.contains(line),
Horizontal::Points { points } => points.iter().any(|point| match point {
ConstraintSegment::Segment(point) => segment_ids_set.contains(point),
ConstraintSegment::Origin(_) => false,
}),
},
Constraint::Vertical(v) => match v {
Vertical::Line { line } => segment_ids_set.contains(line),
Vertical::Points { points } => points.iter().any(|point| match point {
ConstraintSegment::Segment(point) => segment_ids_set.contains(point),
ConstraintSegment::Origin(_) => false,
}),
},
Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
.lines
.iter()
.any(|line_id| segment_ids_set.contains(line_id)),
Constraint::Parallel(parallel) => {
parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
}
Constraint::Perpendicular(perpendicular) => perpendicular
.lines
.iter()
.any(|line_id| segment_ids_set.contains(line_id)),
Constraint::Angle(angle) => angle.lines.iter().any(|line_id| segment_ids_set.contains(line_id)),
Constraint::Tangent(tangent) => tangent.input.iter().any(|seg_id| segment_ids_set.contains(seg_id)),
};
if depends_on_segment {
constraint_ids_set.insert(*constraint_id);
}
}
Ok(constraint_ids_set)
}
fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
#[cfg(not(feature = "artifact-graph"))]
{
let _ = freedom_analysis_ran; outcome
}
#[cfg(feature = "artifact-graph")]
{
let mut outcome = outcome;
let mut new_objects = std::mem::take(&mut outcome.scene_objects);
if freedom_analysis_ran {
self.point_freedom_cache.clear();
for new_obj in &new_objects {
if let ObjectKind::Segment {
segment: crate::front::Segment::Point(point),
} = &new_obj.kind
{
self.point_freedom_cache.insert(new_obj.id, point.freedom);
}
}
add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
self.scene_graph.objects = new_objects;
} else {
for old_obj in &self.scene_graph.objects {
if let ObjectKind::Segment {
segment: crate::front::Segment::Point(point),
} = &old_obj.kind
{
self.point_freedom_cache.insert(old_obj.id, point.freedom);
}
}
let mut updated_objects = Vec::with_capacity(new_objects.len());
for new_obj in new_objects {
let mut obj = new_obj;
if let ObjectKind::Segment {
segment: crate::front::Segment::Point(point),
} = &mut obj.kind
{
let new_freedom = point.freedom;
match new_freedom {
Freedom::Free => {
match self.point_freedom_cache.get(&obj.id).copied() {
Some(Freedom::Conflict) => {
}
Some(Freedom::Fixed) => {
point.freedom = Freedom::Fixed;
}
Some(Freedom::Free) => {
}
None => {
}
}
}
Freedom::Fixed => {
}
Freedom::Conflict => {
}
}
self.point_freedom_cache.insert(obj.id, point.freedom);
}
updated_objects.push(obj);
}
add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
self.scene_graph.objects = updated_objects;
}
outcome
}
}
fn mutate_ast(
&mut self,
ast: &mut ast::Node<ast::Program>,
object_id: ObjectId,
command: AstMutateCommand,
) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
let sketch_object = self
.scene_graph
.objects
.get(object_id.0)
.ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
match &sketch_object.source {
SourceRef::Simple { range, node_path: _ } => mutate_ast_node_by_source_range(ast, *range, command),
SourceRef::BackTrace { .. } => {
Err(KclError::refactor("BackTrace source refs not supported yet".to_owned()))
}
}
}
}
fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
let sketch_object = scene_graph
.objects
.get(sketch_id.0)
.ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
let ObjectKind::Sketch(_) = &sketch_object.kind else {
return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
};
expect_single_node_ref(sketch_object)
}
fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
match &object.source {
SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
range: *range,
node_path: node_path.clone(),
}),
SourceRef::BackTrace { ranges } => {
let [range] = ranges.as_slice() else {
return Err(KclError::refactor(format!(
"Expected single location in SourceRef, got {}; ranges={ranges:#?}",
ranges.len()
)));
};
Ok(AstNodeRef {
range: range.0,
node_path: range.1.clone(),
})
}
}
}
fn expect_single_source_range(source_ref: &SourceRef) -> Result<SourceRange, KclError> {
match source_ref {
SourceRef::Simple { range, node_path: _ } => Ok(*range),
SourceRef::BackTrace { ranges } => {
if ranges.len() != 1 {
return Err(KclError::refactor(format!(
"Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
ranges.len(),
)));
}
Ok(ranges[0].0)
}
}
}
fn only_sketch_block_from_range(
ast: &mut ast::Node<ast::Program>,
sketch_block_range: SourceRange,
edit_kind: ChangeKind,
) -> Result<(), KclError> {
let r1 = sketch_block_range;
let matches_range = |r2: SourceRange| -> bool {
match edit_kind {
ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
}
};
let mut found = false;
for item in ast.body.iter_mut() {
match item {
ast::BodyItem::ImportStatement(_) => {}
ast::BodyItem::ExpressionStatement(node) => {
if matches_range(SourceRange::from(&*node))
&& let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
{
sketch_block.is_being_edited = true;
found = true;
break;
}
}
ast::BodyItem::VariableDeclaration(node) => {
if matches_range(SourceRange::from(&node.declaration.init))
&& let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
{
sketch_block.is_being_edited = true;
found = true;
break;
}
}
ast::BodyItem::TypeDeclaration(_) => {}
ast::BodyItem::ReturnStatement(node) => {
if matches_range(SourceRange::from(&node.argument))
&& let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
{
sketch_block.is_being_edited = true;
found = true;
break;
}
}
}
}
if !found {
return Err(KclError::refactor(format!(
"Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
)));
}
Ok(())
}
fn only_sketch_block(
ast: &mut ast::Node<ast::Program>,
sketch_block_ref: &AstNodeRef,
edit_kind: ChangeKind,
) -> Result<(), KclError> {
let Some(target_node_path) = &sketch_block_ref.node_path else {
#[cfg(target_arch = "wasm32")]
web_sys::console::warn_1(
&format!(
"only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
&sketch_block_ref
)
.into(),
);
return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
};
let mut found = false;
for item in ast.body.iter_mut() {
match item {
ast::BodyItem::ImportStatement(_) => {}
ast::BodyItem::ExpressionStatement(node) => {
if let Some(node_path) = &node.node_path
&& node_path == target_node_path
&& let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
{
sketch_block.is_being_edited = true;
found = true;
break;
}
if let Some(node_path) = node.expression.node_path()
&& node_path == target_node_path
&& let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
{
sketch_block.is_being_edited = true;
found = true;
break;
}
}
ast::BodyItem::VariableDeclaration(node) => {
if let Some(node_path) = node.declaration.init.node_path()
&& node_path == target_node_path
&& let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
{
sketch_block.is_being_edited = true;
found = true;
break;
}
}
ast::BodyItem::TypeDeclaration(_) => {}
ast::BodyItem::ReturnStatement(node) => {
if let Some(node_path) = node.argument.node_path()
&& node_path == target_node_path
&& let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
{
sketch_block.is_being_edited = true;
found = true;
break;
}
}
}
}
if !found {
return Err(KclError::refactor(format!(
"Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
)));
}
Ok(())
}
fn sketch_on_ast_expr(
ast: &mut ast::Node<ast::Program>,
scene_graph: &SceneGraph,
on: &Plane,
) -> Result<ast::Expr, KclError> {
match on {
Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
Plane::Object(object_id) => {
let on_object = scene_graph
.objects
.get(object_id.0)
.ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
#[cfg(feature = "artifact-graph")]
{
if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
return Ok(face_expr);
}
}
get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
}
}
}
#[cfg(feature = "artifact-graph")]
fn sketch_face_of_scene_object_ast_expr(
ast: &mut ast::Node<ast::Program>,
on_object: &crate::front::Object,
) -> Result<Option<ast::Expr>, KclError> {
let SourceRef::BackTrace { ranges } = &on_object.source else {
return Ok(None);
};
match &on_object.kind {
ObjectKind::Wall(_) => {
let [sweep_range, segment_range] = ranges.as_slice() else {
return Err(KclError::refactor(format!(
"Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
ranges.len(),
on_object.artifact_id
)));
};
let sweep_ref = get_or_insert_ast_reference(
ast,
&SourceRef::Simple {
range: sweep_range.0,
node_path: sweep_range.1.clone(),
},
"solid",
None,
)?;
let ast::Expr::Name(solid_name_expr) = sweep_ref else {
return Err(KclError::refactor(format!(
"Could not resolve sweep reference for selected wall: artifact_id={:?}",
on_object.artifact_id
)));
};
let solid_name = solid_name_expr.name.name.clone();
let solid_expr = ast_name_expr(solid_name.clone());
let segment_ref = get_or_insert_ast_reference(
ast,
&SourceRef::Simple {
range: segment_range.0,
node_path: segment_range.1.clone(),
},
"line",
None,
)?;
let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
let ast::Expr::Name(segment_name_expr) = segment_ref else {
return Err(KclError::refactor(format!(
"Could not resolve source segment reference for selected region wall: artifact_id={:?}",
on_object.artifact_id
)));
};
create_member_expression(
create_member_expression(ast_name_expr(region_name), "tags"),
&segment_name_expr.name.name,
)
} else {
segment_ref
};
Ok(Some(create_face_of_ast(solid_expr, face_expr)))
}
ObjectKind::Cap(cap) => {
let [range] = ranges.as_slice() else {
return Err(KclError::refactor(format!(
"Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
ranges.len(),
on_object.artifact_id
)));
};
let sweep_ref = get_or_insert_ast_reference(
ast,
&SourceRef::Simple {
range: range.0,
node_path: range.1.clone(),
},
"solid",
None,
)?;
let ast::Expr::Name(solid_name_expr) = sweep_ref else {
return Err(KclError::refactor(format!(
"Could not resolve sweep reference for selected cap: artifact_id={:?}",
on_object.artifact_id
)));
};
let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
let face_expr = match cap.kind {
crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
};
Ok(Some(create_face_of_ast(solid_expr, face_expr)))
}
_ => Ok(None),
}
}
#[cfg(feature = "artifact-graph")]
fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
let mut existing_artifact_ids = scene_objects
.iter()
.map(|object| object.artifact_id)
.collect::<HashSet<_>>();
for artifact in artifact_graph.values() {
match artifact {
Artifact::Wall(wall) => {
if existing_artifact_ids.contains(&wall.id) {
continue;
}
let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
Artifact::Segment(segment) => Some(segment),
_ => None,
}) else {
continue;
};
let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
Artifact::Sweep(sweep) => Some(sweep),
_ => None,
}) else {
continue;
};
let source_segment = segment
.original_seg_id
.and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
.and_then(|artifact| match artifact {
Artifact::Segment(segment) => Some(segment),
_ => None,
})
.unwrap_or(segment);
let id = ObjectId(scene_objects.len());
scene_objects.push(crate::front::Object {
id,
kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
label: Default::default(),
comments: Default::default(),
artifact_id: wall.id,
source: SourceRef::BackTrace {
ranges: vec![
(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
(
source_segment.code_ref.range,
Some(source_segment.code_ref.node_path.clone()),
),
],
},
});
existing_artifact_ids.insert(wall.id);
}
Artifact::Cap(cap) => {
if existing_artifact_ids.contains(&cap.id) {
continue;
}
let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
Artifact::Sweep(sweep) => Some(sweep),
_ => None,
}) else {
continue;
};
let id = ObjectId(scene_objects.len());
let kind = match cap.sub_type {
CapSubType::Start => crate::frontend::api::CapKind::Start,
CapSubType::End => crate::frontend::api::CapKind::End,
};
scene_objects.push(crate::front::Object {
id,
kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
label: Default::default(),
comments: Default::default(),
artifact_id: cap.id,
source: SourceRef::BackTrace {
ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
},
});
existing_artifact_ids.insert(cap.id);
}
_ => {}
}
}
}
fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
use crate::engine::PlaneName;
match name {
PlaneName::Xy => ast_name_expr("XY".to_owned()),
PlaneName::Xz => ast_name_expr("XZ".to_owned()),
PlaneName::Yz => ast_name_expr("YZ".to_owned()),
PlaneName::NegXy => negated_plane_ast_expr("XY"),
PlaneName::NegXz => negated_plane_ast_expr("XZ"),
PlaneName::NegYz => negated_plane_ast_expr("YZ"),
}
}
fn negated_plane_ast_expr(name: &str) -> ast::Expr {
ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
ast::UnaryOperator::Neg,
ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
)))
}
#[cfg(feature = "artifact-graph")]
fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
unlabeled: Some(solid_expr),
arguments: vec![ast::LabeledArg {
label: Some(ast::Identifier::new("face")),
arg: face_expr,
}],
digest: None,
non_code_meta: Default::default(),
})))
}
#[cfg(feature = "artifact-graph")]
fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
return None;
};
let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
return None;
};
if !matches!(
sweep_call.callee.name.name.as_str(),
"extrude" | "revolve" | "sweep" | "loft"
) {
return None;
}
let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
return None;
};
let candidate = region_name_expr.name.name.clone();
let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
return None;
};
let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
return None;
};
if region_call.callee.name.name != "region" {
return None;
}
Some(candidate)
}
fn get_or_insert_ast_reference(
ast: &mut ast::Node<ast::Program>,
source_ref: &SourceRef,
prefix: &str,
property: Option<&str>,
) -> Result<ast::Expr, KclError> {
let range = expect_single_source_range(source_ref)?;
let command = AstMutateCommand::AddVariableDeclaration {
prefix: prefix.to_owned(),
};
let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
let AstMutateCommandReturn::Name(var_name) = ret else {
return Err(KclError::refactor(
"Expected variable name returned from AddVariableDeclaration".to_owned(),
));
};
let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
let Some(property) = property else {
return Ok(var_expr);
};
Ok(create_member_expression(var_expr, property))
}
fn mutate_ast_node_by_source_range(
ast: &mut ast::Node<ast::Program>,
source_range: SourceRange,
command: AstMutateCommand,
) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
let mut context = AstMutateContext {
source_range,
node_path: None,
command,
defined_names_stack: Default::default(),
};
let control = dfs_mut(ast, &mut context);
match control {
ControlFlow::Continue(_) => Err(KclError::refactor(format!("Source range not found: {source_range:?}"))),
ControlFlow::Break(break_value) => break_value,
}
}
#[derive(Debug)]
struct AstMutateContext {
source_range: SourceRange,
node_path: Option<ast::NodePath>,
command: AstMutateCommand,
defined_names_stack: Vec<HashSet<String>>,
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
enum AstMutateCommand {
AddSketchBlockExprStmt {
expr: ast::Expr,
},
AddSketchBlockVarDecl {
prefix: String,
expr: ast::Expr,
},
AddVariableDeclaration {
prefix: String,
},
EditPoint {
at: ast::Expr,
},
EditLine {
start: ast::Expr,
end: ast::Expr,
construction: Option<bool>,
},
EditArc {
start: ast::Expr,
end: ast::Expr,
center: ast::Expr,
construction: Option<bool>,
},
EditCircle {
start: ast::Expr,
center: ast::Expr,
construction: Option<bool>,
},
EditConstraintValue {
value: ast::BinaryPart,
},
EditCallUnlabeled {
arg: ast::Expr,
},
#[cfg(feature = "artifact-graph")]
EditVarInitialValue {
value: Number,
},
DeleteNode,
}
impl AstMutateCommand {
fn needs_defined_names_stack(&self) -> bool {
matches!(
self,
AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
)
}
}
#[derive(Debug)]
enum AstMutateCommandReturn {
None,
Name(String),
}
#[derive(Debug, Clone)]
struct AstNodeRef {
range: SourceRange,
node_path: Option<ast::NodePath>,
}
impl<T> From<&ast::Node<T>> for AstNodeRef {
fn from(value: &ast::Node<T>) -> Self {
AstNodeRef {
range: value.into(),
node_path: value.node_path.clone(),
}
}
}
impl From<&ast::BodyItem> for AstNodeRef {
fn from(value: &ast::BodyItem) -> Self {
match value {
ast::BodyItem::ImportStatement(node) => AstNodeRef {
range: node.into(),
node_path: node.node_path.clone(),
},
ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
range: node.into(),
node_path: node.node_path.clone(),
},
ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
range: node.into(),
node_path: node.node_path.clone(),
},
ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
range: node.into(),
node_path: node.node_path.clone(),
},
ast::BodyItem::ReturnStatement(node) => AstNodeRef {
range: node.into(),
node_path: node.node_path.clone(),
},
}
}
}
impl From<&ast::Expr> for AstNodeRef {
fn from(value: &ast::Expr) -> Self {
AstNodeRef {
range: SourceRange::from(value),
node_path: value.node_path().cloned(),
}
}
}
impl From<&AstMutateContext> for AstNodeRef {
fn from(value: &AstMutateContext) -> Self {
AstNodeRef {
range: value.source_range,
node_path: value.node_path.clone(),
}
}
}
impl TryFrom<&NodeMut<'_>> for AstNodeRef {
type Error = crate::walk::AstNodeError;
fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
Ok(AstNodeRef {
range: SourceRange::try_from(value)?,
node_path: value.try_into()?,
})
}
}
impl From<AstNodeRef> for SourceRange {
fn from(value: AstNodeRef) -> Self {
value.range
}
}
impl Visitor for AstMutateContext {
type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
type Continue = ();
fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
filter_and_process(self, node)
}
fn finish(&mut self, node: NodeMut<'_>) {
match &node {
NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
self.defined_names_stack.pop();
}
_ => {}
}
}
}
fn filter_and_process(
ctx: &mut AstMutateContext,
node: NodeMut,
) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
let Ok(node_range) = SourceRange::try_from(&node) else {
return TraversalReturn::new_continue(());
};
if let NodeMut::VariableDeclaration(var_decl) = &node {
let expr_range = SourceRange::from(&var_decl.declaration.init);
if expr_range == ctx.source_range {
if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
return TraversalReturn::new_break(Ok((
AstNodeRef::from(&**var_decl),
AstMutateCommandReturn::Name(var_decl.name().to_owned()),
)));
}
if let AstMutateCommand::DeleteNode = &ctx.command {
return TraversalReturn {
mutate_body_item: MutateBodyItem::Delete,
control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
};
}
}
}
if ctx.command.needs_defined_names_stack() {
if let NodeMut::Program(program) = &node {
ctx.defined_names_stack.push(find_defined_names(*program));
} else if let NodeMut::SketchBlock(block) = &node {
ctx.defined_names_stack.push(find_defined_names(&block.body));
}
}
if node_range != ctx.source_range {
return TraversalReturn::new_continue(());
}
let Ok(node_ref) = AstNodeRef::try_from(&node) else {
return TraversalReturn::new_continue(());
};
process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
}
fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
match &ctx.command {
AstMutateCommand::AddSketchBlockExprStmt { expr } => {
if let NodeMut::SketchBlock(sketch_block) = node {
sketch_block
.body
.items
.push(ast::BodyItem::ExpressionStatement(ast::Node {
inner: ast::ExpressionStatement {
expression: expr.clone(),
digest: None,
},
start: Default::default(),
end: Default::default(),
module_id: Default::default(),
node_path: None,
outer_attrs: Default::default(),
pre_comments: Default::default(),
comment_start: Default::default(),
}));
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
}
}
AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
if let NodeMut::SketchBlock(sketch_block) = node {
let empty_defined_names = HashSet::new();
let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
let Ok(name) = next_free_name(prefix, defined_names) else {
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
};
sketch_block
.body
.items
.push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
ast::VariableDeclaration::new(
ast::VariableDeclarator::new(&name, expr.clone()),
ast::ItemVisibility::Default,
ast::VariableKind::Const,
),
))));
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
}
}
AstMutateCommand::AddVariableDeclaration { prefix } => {
if let NodeMut::VariableDeclaration(inner) = node {
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
}
if let NodeMut::ExpressionStatement(expr_stmt) = node {
let empty_defined_names = HashSet::new();
let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
let Ok(name) = next_free_name(prefix, defined_names) else {
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
};
let mutate_node =
ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
ast::ItemVisibility::Default,
ast::VariableKind::Const,
))));
return TraversalReturn {
mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
};
}
}
AstMutateCommand::EditPoint { at } => {
if let NodeMut::CallExpressionKw(call) = node {
if call.callee.name.name != POINT_FN {
return TraversalReturn::new_continue(());
}
for labeled_arg in &mut call.arguments {
if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
labeled_arg.arg = at.clone();
}
}
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
}
}
AstMutateCommand::EditLine {
start,
end,
construction,
} => {
if let NodeMut::CallExpressionKw(call) = node {
if call.callee.name.name != LINE_FN {
return TraversalReturn::new_continue(());
}
for labeled_arg in &mut call.arguments {
if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
labeled_arg.arg = start.clone();
}
if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
labeled_arg.arg = end.clone();
}
}
if let Some(construction_value) = construction {
let construction_exists = call
.arguments
.iter()
.any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
if *construction_value {
if construction_exists {
for labeled_arg in &mut call.arguments {
if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Bool(true),
raw: "true".to_string(),
digest: None,
})));
}
}
} else {
call.arguments.push(ast::LabeledArg {
label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Bool(true),
raw: "true".to_string(),
digest: None,
}))),
});
}
} else {
call.arguments
.retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
}
}
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
}
}
AstMutateCommand::EditArc {
start,
end,
center,
construction,
} => {
if let NodeMut::CallExpressionKw(call) = node {
if call.callee.name.name != ARC_FN {
return TraversalReturn::new_continue(());
}
for labeled_arg in &mut call.arguments {
if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
labeled_arg.arg = start.clone();
}
if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
labeled_arg.arg = end.clone();
}
if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
labeled_arg.arg = center.clone();
}
}
if let Some(construction_value) = construction {
let construction_exists = call
.arguments
.iter()
.any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
if *construction_value {
if construction_exists {
for labeled_arg in &mut call.arguments {
if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Bool(true),
raw: "true".to_string(),
digest: None,
})));
}
}
} else {
call.arguments.push(ast::LabeledArg {
label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Bool(true),
raw: "true".to_string(),
digest: None,
}))),
});
}
} else {
call.arguments
.retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
}
}
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
}
}
AstMutateCommand::EditCircle {
start,
center,
construction,
} => {
if let NodeMut::CallExpressionKw(call) = node {
if call.callee.name.name != CIRCLE_FN {
return TraversalReturn::new_continue(());
}
for labeled_arg in &mut call.arguments {
if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
labeled_arg.arg = start.clone();
}
if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
labeled_arg.arg = center.clone();
}
}
if let Some(construction_value) = construction {
let construction_exists = call
.arguments
.iter()
.any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
if *construction_value {
if construction_exists {
for labeled_arg in &mut call.arguments {
if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Bool(true),
raw: "true".to_string(),
digest: None,
})));
}
}
} else {
call.arguments.push(ast::LabeledArg {
label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
value: ast::LiteralValue::Bool(true),
raw: "true".to_string(),
digest: None,
}))),
});
}
} else {
call.arguments
.retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
}
}
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
}
}
AstMutateCommand::EditConstraintValue { value } => {
if let NodeMut::BinaryExpression(binary_expr) = node {
let left_is_constraint = matches!(
&binary_expr.left,
ast::BinaryPart::CallExpressionKw(call)
if matches!(
call.callee.name.name.as_str(),
DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
)
);
if left_is_constraint {
binary_expr.right = value.clone();
} else {
binary_expr.left = value.clone();
}
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
}
}
AstMutateCommand::EditCallUnlabeled { arg } => {
if let NodeMut::CallExpressionKw(call) = node {
call.unlabeled = Some(arg.clone());
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
}
}
#[cfg(feature = "artifact-graph")]
AstMutateCommand::EditVarInitialValue { value } => {
if let NodeMut::NumericLiteral(numeric_literal) = node {
let Ok(literal) = to_source_number(*value) else {
return TraversalReturn::new_break(Err(KclError::refactor(format!(
"Could not convert number to AST literal: {:?}",
*value
))));
};
*numeric_literal = ast::Node::no_src(literal);
return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
}
}
AstMutateCommand::DeleteNode => {
return TraversalReturn {
mutate_body_item: MutateBodyItem::Delete,
control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
};
}
}
TraversalReturn::new_continue(())
}
struct FindSketchBlockSourceRange {
target_before_mutation: SourceRange,
found: Cell<Option<AstNodeRef>>,
}
impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
type Error = crate::front::Error;
fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
let Ok(node_range) = SourceRange::try_from(&node) else {
return Ok(true);
};
if let crate::walk::Node::SketchBlock(sketch_block) = node {
if node_range.module_id() == self.target_before_mutation.module_id()
&& node_range.start() == self.target_before_mutation.start()
&& node_range.end() >= self.target_before_mutation.end()
{
self.found.set(sketch_block.body.items.last().map(|item| match item {
ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
_ => AstNodeRef::from(item),
}));
return Ok(false);
} else {
return Ok(true);
}
}
for child in node.children().iter() {
if !child.visit(*self)? {
return Ok(false);
}
}
Ok(true)
}
}
struct FindSketchBlockByNodePath {
target_node_path: ast::NodePath,
found: Cell<Option<AstNodeRef>>,
}
impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
type Error = crate::front::Error;
fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
return Ok(true);
};
if let crate::walk::Node::SketchBlock(sketch_block) = node {
if let Some(node_path) = node_path
&& node_path == self.target_node_path
{
self.found.set(sketch_block.body.items.last().map(|item| match item {
ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
_ => AstNodeRef::from(item),
}));
return Ok(false);
} else {
return Ok(true);
}
}
for child in node.children().iter() {
if !child.visit(*self)? {
return Ok(false);
}
}
Ok(true)
}
}
fn find_sketch_block_added_item(
ast: &ast::Node<ast::Program>,
sketch_block_before_mutation: &AstNodeRef,
) -> Result<AstNodeRef, KclError> {
if let Some(node_path) = &sketch_block_before_mutation.node_path {
let find = FindSketchBlockByNodePath {
target_node_path: node_path.clone(),
found: Cell::new(None),
};
let node = crate::walk::Node::from(ast);
node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
find.found.into_inner().ok_or_else(|| {
KclError::refactor(format!(
"Node ID after mutation not found for Node ID before mutation: {node_path:?}"
))
})
} else {
let find = FindSketchBlockSourceRange {
target_before_mutation: sketch_block_before_mutation.range,
found: Cell::new(None),
};
let node = crate::walk::Node::from(ast);
node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
find.found.into_inner().ok_or_else(|| KclError::refactor(
format!("Source range after mutation not found for range before mutation: {sketch_block_before_mutation:?}; Did you try formatting (i.e. call recast) before calling this?"),
))
}
}
fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
ast.recast_top(&Default::default(), 0)
}
pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
inner: ast::ArrayExpression {
elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
non_code_meta: Default::default(),
digest: None,
},
start: Default::default(),
end: Default::default(),
module_id: Default::default(),
node_path: None,
outer_attrs: Default::default(),
pre_comments: Default::default(),
comment_start: Default::default(),
})))
}
fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
match expr {
Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
inner: ast::Literal::from(to_source_number(*number)?),
start: Default::default(),
end: Default::default(),
module_id: Default::default(),
node_path: None,
outer_attrs: Default::default(),
pre_comments: Default::default(),
comment_start: Default::default(),
}))),
Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
inner: ast::SketchVar {
initial: Some(Box::new(ast::Node {
inner: to_source_number(*number)?,
start: Default::default(),
end: Default::default(),
module_id: Default::default(),
node_path: None,
outer_attrs: Default::default(),
pre_comments: Default::default(),
comment_start: Default::default(),
})),
digest: None,
},
start: Default::default(),
end: Default::default(),
module_id: Default::default(),
node_path: None,
outer_attrs: Default::default(),
pre_comments: Default::default(),
comment_start: Default::default(),
}))),
Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
}
}
fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
Ok(ast::NumericLiteral {
value: number.value,
suffix: number.units,
raw: format_number_literal(number.value, number.units, None)?,
digest: None,
})
}
pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
ast::Expr::Name(Box::new(ast_name(name)))
}
fn ast_name(name: String) -> ast::Node<ast::Name> {
ast::Node {
inner: ast::Name {
name: ast::Node {
inner: ast::Identifier { name, digest: None },
start: Default::default(),
end: Default::default(),
module_id: Default::default(),
node_path: None,
outer_attrs: Default::default(),
pre_comments: Default::default(),
comment_start: Default::default(),
},
path: Vec::new(),
abs_path: false,
digest: None,
},
start: Default::default(),
end: Default::default(),
module_id: Default::default(),
node_path: None,
outer_attrs: Default::default(),
pre_comments: Default::default(),
comment_start: Default::default(),
}
}
pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
ast::Name {
name: ast::Node {
inner: ast::Identifier {
name: name.to_owned(),
digest: None,
},
start: Default::default(),
end: Default::default(),
module_id: Default::default(),
node_path: None,
outer_attrs: Default::default(),
pre_comments: Default::default(),
comment_start: Default::default(),
},
path: Default::default(),
abs_path: false,
digest: None,
}
}
pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
let elements = exprs.into_iter().collect::<Vec<_>>();
debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
elements,
digest: None,
non_code_meta: Default::default(),
})));
ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
unlabeled: Some(array_expr),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})))
}
pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
unlabeled: None,
arguments: vec![
ast::LabeledArg {
label: Some(ast::Identifier::new(LINE_START_PARAM)),
arg: start_ast,
},
ast::LabeledArg {
label: Some(ast::Identifier::new(LINE_END_PARAM)),
arg: end_ast,
},
],
digest: None,
non_code_meta: Default::default(),
})))
}
pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
unlabeled: None,
arguments: vec![
ast::LabeledArg {
label: Some(ast::Identifier::new(ARC_START_PARAM)),
arg: start_ast,
},
ast::LabeledArg {
label: Some(ast::Identifier::new(ARC_END_PARAM)),
arg: end_ast,
},
ast::LabeledArg {
label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
arg: center_ast,
},
],
digest: None,
non_code_meta: Default::default(),
})))
}
pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
unlabeled: None,
arguments: vec![
ast::LabeledArg {
label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
arg: start_ast,
},
ast::LabeledArg {
label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
arg: center_ast,
},
],
digest: None,
non_code_meta: Default::default(),
})))
}
pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
unlabeled: Some(line_expr),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})))
}
pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
unlabeled: Some(line_expr),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})))
}
pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
object: object_expr,
property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
name: ast::Node::no_src(ast::Identifier {
name: property.to_string(),
digest: None,
}),
path: Vec::new(),
abs_path: false,
digest: None,
}))),
computed: false,
digest: None,
})))
}
fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
position.x,
)?))));
let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
position.y,
)?))));
let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
elements: vec![x_literal, y_literal],
digest: None,
non_code_meta: Default::default(),
})));
let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
elements: vec![point_expr, point_array],
digest: None,
non_code_meta: Default::default(),
})));
Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
unlabeled: Some(array_expr),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
},
))))
}
pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
elements: line_exprs,
digest: None,
non_code_meta: Default::default(),
})));
ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
unlabeled: Some(array_expr),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})))
}
pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
elements: segment_exprs,
digest: None,
non_code_meta: Default::default(),
})));
ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
unlabeled: Some(array_expr),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})))
}
pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
elements: vec![seg1_expr, seg2_expr],
digest: None,
non_code_meta: Default::default(),
})));
ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
unlabeled: Some(array_expr),
arguments: Default::default(),
digest: None,
non_code_meta: Default::default(),
})))
}
#[cfg(all(feature = "artifact-graph", test))]
mod tests {
use super::*;
use crate::engine::PlaneName;
use crate::execution::cache::SketchModeState;
use crate::execution::cache::clear_mem_cache;
use crate::execution::cache::read_old_memory;
use crate::execution::cache::write_old_memory;
use crate::front::Distance;
use crate::front::Fixed;
use crate::front::FixedPoint;
use crate::front::Object;
use crate::front::Plane;
use crate::front::Sketch;
use crate::front::Tangent;
use crate::frontend::sketch::Vertical;
use crate::pretty::NumericSuffix;
fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
for object in &scene_graph.objects {
if let ObjectKind::Sketch(_) = &object.kind {
return Some(object);
}
}
None
}
fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
for object in &scene_graph.objects {
if let ObjectKind::Face(_) = &object.kind {
return Some(object);
}
}
None
}
fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
for object in &scene_graph.objects {
if matches!(&object.kind, ObjectKind::Wall(_)) {
return Some(object.id);
}
}
None
}
#[test]
fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
let source = "\
region001 = region(point = [0.1, 0.1], sketch = s)
extrude001 = extrude(region001, length = 5)
revolve001 = revolve(region001, axis = Y)
sweep001 = sweep(region001, path = path001)
loft001 = loft(region001)
not_sweep001 = shell(extrude001, faces = [], thickness = 1)
";
let program = Program::parse(source).unwrap().0.unwrap();
assert_eq!(
region_name_from_sweep_variable(&program.ast, "extrude001"),
Some("region001".to_owned())
);
assert_eq!(
region_name_from_sweep_variable(&program.ast, "revolve001"),
Some("region001".to_owned())
);
assert_eq!(
region_name_from_sweep_variable(&program.ast, "sweep001"),
Some("region001".to_owned())
);
assert_eq!(
region_name_from_sweep_variable(&program.ast, "loft001"),
Some("region001".to_owned())
);
assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
}
#[track_caller]
fn expect_sketch(object: &Object) -> &Sketch {
if let ObjectKind::Sketch(sketch) = &object.kind {
sketch
} else {
panic!("Object is not a sketch: {:?}", object);
}
}
fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
LineCtor {
start: Point2d {
x: Expr::Number(Number { value: start_x, units }),
y: Expr::Number(Number { value: start_y, units }),
},
end: Point2d {
x: Expr::Number(Number { value: end_x, units }),
y: Expr::Number(Number { value: end_y, units }),
},
construction: None,
}
}
async fn create_sketch_with_single_line(
frontend: &mut FrontendState,
ctx: &ExecutorContext,
mock_ctx: &ExecutorContext,
version: Version,
) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
frontend.program = Program::empty();
let sketch_args = SketchCtor {
on: Plane::Default(PlaneName::Xy),
};
let (_src_delta, _scene_delta, sketch_id) = frontend
.new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
let (source_delta, scene_graph_delta) = frontend
.add_segment(mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
let line_id = *scene_graph_delta
.new_objects
.last()
.expect("Expected line object id to be created");
(sketch_id, line_id, source_delta, scene_graph_delta)
}
#[tokio::test(flavor = "multi_thread")]
async fn test_sketch_checkpoint_round_trip_restores_state() {
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let (sketch_id, line_id, source_delta, scene_graph_delta) =
create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
let expected_source = source_delta.text.clone();
let expected_scene_graph = frontend.scene_graph.clone();
let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
let checkpoint_id = frontend
.create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
.await
.unwrap();
let edited_segments = vec![ExistingSegmentCtor {
id: line_id,
ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
}];
let (edited_source, _edited_scene) = frontend
.edit_segments(&mock_ctx, version, sketch_id, edited_segments)
.await
.unwrap();
assert_ne!(edited_source.text, expected_source);
let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
assert_eq!(restored.source_delta.text, expected_source);
assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
assert!(restored.scene_graph_delta.invalidates_ids);
assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
assert_eq!(frontend.scene_graph, expected_scene_graph);
assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_sketch_checkpoints_prune_oldest_entries() {
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
let mut checkpoint_ids = Vec::new();
for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
checkpoint_ids.push(
frontend
.create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
.await
.unwrap(),
);
}
assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
let oldest_retained = checkpoint_ids[3];
assert_eq!(
frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
Some(oldest_retained)
);
let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
assert!(evicted_restore.is_err());
assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
frontend
.restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
.await
.unwrap();
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
let mut frontend = FrontendState::new();
let missing_checkpoint = SketchCheckpointId::new(999);
let err = frontend
.restore_sketch_checkpoint(missing_checkpoint)
.await
.expect_err("Expected restore to fail for missing checkpoint");
assert!(err.msg.contains("Sketch checkpoint not found"));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
let checkpoint_a = frontend
.create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
.await
.unwrap();
let checkpoint_b = frontend
.create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
.await
.unwrap();
assert_eq!(frontend.sketch_checkpoints.len(), 2);
frontend.clear_sketch_checkpoints();
assert!(frontend.sketch_checkpoints.is_empty());
frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
let old_source = source_delta.text.clone();
let old_checkpoint = frontend
.create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
.await
.unwrap();
let initial_checkpoint_count = frontend.sketch_checkpoints.len();
let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
.unwrap()
.0
.unwrap();
let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
let SetProgramOutcome::Success {
checkpoint_id: Some(new_checkpoint),
..
} = result
else {
panic!("Expected Success with a fresh checkpoint baseline");
};
assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
assert_eq!(old_restore.source_delta.text, old_source);
let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
let old_checkpoint = frontend
.create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
.await
.unwrap();
let checkpoint_count_before = frontend.sketch_checkpoints.len();
let failing_program = Program::parse(
"sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
)
.unwrap()
.0
.unwrap();
let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let program = Program::parse(
"width = 2mm\nsketch001 = sketch(on = offsetPlane(XY, offset = width)) {\n line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])\n distance([line1.start, line1.end]) == width\n}\n",
)
.unwrap()
.0
.unwrap();
let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
panic!("Expected successful baseline program execution");
};
clear_mem_cache().await;
assert!(read_old_memory().await.is_none());
let checkpoint_without_mock_memory = frontend
.create_sketch_checkpoint((*exec_outcome).clone())
.await
.unwrap();
write_old_memory(SketchModeState::new_for_tests()).await;
assert!(read_old_memory().await.is_some());
let checkpoint_with_mock_memory = frontend
.create_sketch_checkpoint((*exec_outcome).clone())
.await
.unwrap();
clear_mem_cache().await;
assert!(read_old_memory().await.is_none());
frontend
.restore_sketch_checkpoint(checkpoint_with_mock_memory)
.await
.unwrap();
assert!(read_old_memory().await.is_some());
frontend
.restore_sketch_checkpoint(checkpoint_without_mock_memory)
.await
.unwrap();
assert!(read_old_memory().await.is_none());
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
let source = "\
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
}
bad = missing_name
";
let program = Program::parse(source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let project_id = ProjectId(0);
let file_id = FileId(0);
let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
};
let sketch_id = frontend
.scene_graph
.objects
.iter()
.find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
.expect("Expected sketch object from errored hack_set_program");
frontend
.edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
.await
.unwrap();
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_new_sketch_add_point_edit_point() {
let program = Program::empty();
let mut frontend = FrontendState::new();
frontend.program = program;
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let sketch_args = SketchCtor {
on: Plane::Default(PlaneName::Xy),
};
let (_src_delta, scene_delta, sketch_id) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
assert_eq!(sketch_id, ObjectId(1));
assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
let sketch_object = &scene_delta.new_graph.objects[1];
assert_eq!(sketch_object.id, ObjectId(1));
assert_eq!(
sketch_object.kind,
ObjectKind::Sketch(Sketch {
args: SketchCtor {
on: Plane::Default(PlaneName::Xy)
},
plane: ObjectId(0),
segments: vec![],
constraints: vec![],
})
);
assert_eq!(scene_delta.new_graph.objects.len(), 2);
let point_ctor = PointCtor {
position: Point2d {
x: Expr::Number(Number {
value: 1.0,
units: NumericSuffix::Inch,
}),
y: Expr::Number(Number {
value: 2.0,
units: NumericSuffix::Inch,
}),
},
};
let segment = SegmentCtor::Point(point_ctor);
let (src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"sketch001 = sketch(on = XY) {
point(at = [1in, 2in])
}
"
);
assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
assert_eq!(scene_delta.new_graph.objects.len(), 3);
for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
assert_eq!(scene_object.id.0, i);
}
let point_id = *scene_delta.new_objects.last().unwrap();
let point_ctor = PointCtor {
position: Point2d {
x: Expr::Number(Number {
value: 3.0,
units: NumericSuffix::Inch,
}),
y: Expr::Number(Number {
value: 4.0,
units: NumericSuffix::Inch,
}),
},
};
let segments = vec![ExistingSegmentCtor {
id: point_id,
ctor: SegmentCtor::Point(point_ctor),
}];
let (src_delta, scene_delta) = frontend
.edit_segments(&mock_ctx, version, sketch_id, segments)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"sketch001 = sketch(on = XY) {
point(at = [3in, 4in])
}
"
);
assert_eq!(scene_delta.new_objects, vec![]);
assert_eq!(scene_delta.new_graph.objects.len(), 3);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_new_sketch_add_line_edit_line() {
let program = Program::empty();
let mut frontend = FrontendState::new();
frontend.program = program;
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let sketch_args = SketchCtor {
on: Plane::Default(PlaneName::Xy),
};
let (_src_delta, scene_delta, sketch_id) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
assert_eq!(sketch_id, ObjectId(1));
assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
let sketch_object = &scene_delta.new_graph.objects[1];
assert_eq!(sketch_object.id, ObjectId(1));
assert_eq!(
sketch_object.kind,
ObjectKind::Sketch(Sketch {
args: SketchCtor {
on: Plane::Default(PlaneName::Xy)
},
plane: ObjectId(0),
segments: vec![],
constraints: vec![],
})
);
assert_eq!(scene_delta.new_graph.objects.len(), 2);
let line_ctor = LineCtor {
start: Point2d {
x: Expr::Number(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Number(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segment = SegmentCtor::Line(line_ctor);
let (src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"sketch001 = sketch(on = XY) {
line(start = [0mm, 0mm], end = [10mm, 10mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
assert_eq!(scene_delta.new_graph.objects.len(), 5);
for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
assert_eq!(scene_object.id.0, i);
}
let line = *scene_delta.new_objects.last().unwrap();
let line_ctor = LineCtor {
start: Point2d {
x: Expr::Number(Number {
value: 1.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 2.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Number(Number {
value: 13.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 14.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segments = vec![ExistingSegmentCtor {
id: line,
ctor: SegmentCtor::Line(line_ctor),
}];
let (src_delta, scene_delta) = frontend
.edit_segments(&mock_ctx, version, sketch_id, segments)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"sketch001 = sketch(on = XY) {
line(start = [1mm, 2mm], end = [13mm, 14mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![]);
assert_eq!(scene_delta.new_graph.objects.len(), 5);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_new_sketch_add_arc_edit_arc() {
let program = Program::empty();
let mut frontend = FrontendState::new();
frontend.program = program;
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let sketch_args = SketchCtor {
on: Plane::Default(PlaneName::Xy),
};
let (_src_delta, scene_delta, sketch_id) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
assert_eq!(sketch_id, ObjectId(1));
assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
let sketch_object = &scene_delta.new_graph.objects[1];
assert_eq!(sketch_object.id, ObjectId(1));
assert_eq!(
sketch_object.kind,
ObjectKind::Sketch(Sketch {
args: SketchCtor {
on: Plane::Default(PlaneName::Xy),
},
plane: ObjectId(0),
segments: vec![],
constraints: vec![],
})
);
assert_eq!(scene_delta.new_graph.objects.len(), 2);
let arc_ctor = ArcCtor {
start: Point2d {
x: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Var(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
},
center: Point2d {
x: Expr::Var(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segment = SegmentCtor::Arc(arc_ctor);
let (src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"sketch001 = sketch(on = XY) {
arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
}
"
);
assert_eq!(
scene_delta.new_objects,
vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
);
for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
assert_eq!(scene_object.id.0, i);
}
assert_eq!(scene_delta.new_graph.objects.len(), 6);
let arc = *scene_delta.new_objects.last().unwrap();
let arc_ctor = ArcCtor {
start: Point2d {
x: Expr::Var(Number {
value: 1.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 2.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Var(Number {
value: 13.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 14.0,
units: NumericSuffix::Mm,
}),
},
center: Point2d {
x: Expr::Var(Number {
value: 13.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 2.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segments = vec![ExistingSegmentCtor {
id: arc,
ctor: SegmentCtor::Arc(arc_ctor),
}];
let (src_delta, scene_delta) = frontend
.edit_segments(&mock_ctx, version, sketch_id, segments)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"sketch001 = sketch(on = XY) {
arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![]);
assert_eq!(scene_delta.new_graph.objects.len(), 6);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_new_sketch_add_circle_edit_circle() {
let program = Program::empty();
let mut frontend = FrontendState::new();
frontend.program = program;
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let sketch_args = SketchCtor {
on: Plane::Default(PlaneName::Xy),
};
let (_src_delta, _scene_delta, sketch_id) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
let circle_ctor = CircleCtor {
start: Point2d {
x: Expr::Var(Number {
value: 5.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
center: Point2d {
x: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segment = SegmentCtor::Circle(circle_ctor);
let (src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"sketch001 = sketch(on = XY) {
circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
assert_eq!(scene_delta.new_graph.objects.len(), 5);
let circle = *scene_delta.new_objects.last().unwrap();
let circle_ctor = CircleCtor {
start: Point2d {
x: Expr::Var(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
center: Point2d {
x: Expr::Var(Number {
value: 3.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 4.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segments = vec![ExistingSegmentCtor {
id: circle,
ctor: SegmentCtor::Circle(circle_ctor),
}];
let (src_delta, scene_delta) = frontend
.edit_segments(&mock_ctx, version, sketch_id, segments)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"sketch001 = sketch(on = XY) {
circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![]);
assert_eq!(scene_delta.new_graph.objects.len(), 5);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_circle() {
let initial_source = "sketch001 = sketch(on = XY) {
circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
assert_eq!(sketch.segments.len(), 3);
let circle_id = sketch.segments[2];
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"sketch001 = sketch(on = XY) {
}
"
);
let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
let new_sketch = expect_sketch(new_sketch_object);
assert_eq!(new_sketch.segments.len(), 0);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_edit_circle_via_point() {
let initial_source = "sketch001 = sketch(on = XY) {
circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let circle_id = sketch
.segments
.iter()
.copied()
.find(|seg_id| {
matches!(
&frontend.scene_graph.objects[seg_id.0].kind,
ObjectKind::Segment {
segment: Segment::Circle(_)
}
)
})
.expect("Expected a circle segment in sketch");
let circle_object = &frontend.scene_graph.objects[circle_id.0];
let ObjectKind::Segment {
segment: Segment::Circle(circle),
} = &circle_object.kind
else {
panic!("Expected circle segment, got: {:?}", circle_object.kind);
};
let start_point_id = circle.start;
let segments = vec![ExistingSegmentCtor {
id: start_point_id,
ctor: SegmentCtor::Point(PointCtor {
position: Point2d {
x: Expr::Var(Number {
value: 7.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 1.0,
units: NumericSuffix::Mm,
}),
},
}),
}];
let (src_delta, _scene_delta) = frontend
.edit_segments(&mock_ctx, version, sketch_id, segments)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"sketch001 = sketch(on = XY) {
circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
}
"
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_add_line_when_sketch_block_uses_variable() {
let initial_source = "s = sketch(on = XY) {}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let line_ctor = LineCtor {
start: Point2d {
x: Expr::Number(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Number(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segment = SegmentCtor::Line(line_ctor);
let (src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"s = sketch(on = XY) {
line(start = [0mm, 0mm], end = [10mm, 10mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
assert_eq!(scene_delta.new_graph.objects.len(), 5);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_new_sketch_add_line_delete_sketch() {
let program = Program::empty();
let mut frontend = FrontendState::new();
frontend.program = program;
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let sketch_args = SketchCtor {
on: Plane::Default(PlaneName::Xy),
};
let (_src_delta, scene_delta, sketch_id) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
assert_eq!(sketch_id, ObjectId(1));
assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
let sketch_object = &scene_delta.new_graph.objects[1];
assert_eq!(sketch_object.id, ObjectId(1));
assert_eq!(
sketch_object.kind,
ObjectKind::Sketch(Sketch {
args: SketchCtor {
on: Plane::Default(PlaneName::Xy)
},
plane: ObjectId(0),
segments: vec![],
constraints: vec![],
})
);
assert_eq!(scene_delta.new_graph.objects.len(), 2);
let line_ctor = LineCtor {
start: Point2d {
x: Expr::Number(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Number(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segment = SegmentCtor::Line(line_ctor);
let (src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"sketch001 = sketch(on = XY) {
line(start = [0mm, 0mm], end = [10mm, 10mm])
}
"
);
assert_eq!(scene_delta.new_graph.objects.len(), 5);
let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
assert_eq!(src_delta.text.as_str(), "");
assert_eq!(scene_delta.new_graph.objects.len(), 0);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_sketch_when_sketch_block_uses_variable() {
let initial_source = "s = sketch(on = XY) {}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
assert_eq!(src_delta.text.as_str(), "");
assert_eq!(scene_delta.new_graph.objects.len(), 0);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_edit_line_when_editing_its_start_point() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point_id = *sketch.segments.first().unwrap();
let point_ctor = PointCtor {
position: Point2d {
x: Expr::Var(Number {
value: 5.0,
units: NumericSuffix::Inch,
}),
y: Expr::Var(Number {
value: 6.0,
units: NumericSuffix::Inch,
}),
},
};
let segments = vec![ExistingSegmentCtor {
id: point_id,
ctor: SegmentCtor::Point(point_ctor),
}];
let (src_delta, scene_delta) = frontend
.edit_segments(&mock_ctx, version, sketch_id, segments)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![]);
assert_eq!(scene_delta.new_graph.objects.len(), 5);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_edit_line_when_editing_its_end_point() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point_id = *sketch.segments.get(1).unwrap();
let point_ctor = PointCtor {
position: Point2d {
x: Expr::Var(Number {
value: 5.0,
units: NumericSuffix::Inch,
}),
y: Expr::Var(Number {
value: 6.0,
units: NumericSuffix::Inch,
}),
},
};
let segments = vec![ExistingSegmentCtor {
id: point_id,
ctor: SegmentCtor::Point(point_ctor),
}];
let (src_delta, scene_delta) = frontend
.edit_segments(&mock_ctx, version, sketch_id, segments)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![]);
assert_eq!(
scene_delta.new_graph.objects.len(),
5,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_edit_line_with_coincident_feedback() {
let initial_source = "\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 1, var 2])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
fixed([line1.start, [0, 0]])
coincident([line1.end, line2.start])
equalLength([line1, line2])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line2_end_id = *sketch.segments.get(4).unwrap();
let segments = vec![ExistingSegmentCtor {
id: line2_end_id,
ctor: SegmentCtor::Point(PointCtor {
position: Point2d {
x: Expr::Var(Number {
value: 9.0,
units: NumericSuffix::None,
}),
y: Expr::Var(Number {
value: 10.0,
units: NumericSuffix::None,
}),
},
}),
}];
let (src_delta, scene_delta) = frontend
.edit_segments(&mock_ctx, version, sketch_id, segments)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
fixed([line1.start, [0, 0]])
coincident([line1.end, line2.start])
equalLength([line1, line2])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
11,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_point_without_var() {
let initial_source = "\
sketch(on = XY) {
point(at = [var 1, var 2])
point(at = [var 3, var 4])
point(at = [var 5, var 6])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point_id = *sketch.segments.get(1).unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
point(at = [var 1mm, var 2mm])
point(at = [var 5mm, var 6mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![]);
assert_eq!(scene_delta.new_graph.objects.len(), 4);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_point_with_var() {
let initial_source = "\
sketch(on = XY) {
point(at = [var 1, var 2])
point1 = point(at = [var 3, var 4])
point(at = [var 5, var 6])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point_id = *sketch.segments.get(1).unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
point(at = [var 1mm, var 2mm])
point(at = [var 5mm, var 6mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![]);
assert_eq!(scene_delta.new_graph.objects.len(), 4);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_multiple_points() {
let initial_source = "\
sketch(on = XY) {
point(at = [var 1, var 2])
point1 = point(at = [var 3, var 4])
point(at = [var 5, var 6])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point1_id = *sketch.segments.first().unwrap();
let point2_id = *sketch.segments.get(1).unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
point(at = [var 5mm, var 6mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![]);
assert_eq!(scene_delta.new_graph.objects.len(), 3);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_coincident_constraint() {
let initial_source = "\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
point2 = point(at = [var 3, var 4])
coincident([point1, point2])
point(at = [var 5, var 6])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let coincident_id = *sketch.constraints.first().unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
point1 = point(at = [var 1mm, var 2mm])
point2 = point(at = [var 3mm, var 4mm])
point(at = [var 5mm, var 6mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![]);
assert_eq!(scene_delta.new_graph.objects.len(), 5);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_line_cascades_to_coincident_constraint() {
let initial_source = "\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
coincident([line1.end, line2.start])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line_id = *sketch.segments.get(5).unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
5,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_line_cascades_to_distance_constraint() {
let initial_source = "\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
distance([line1.end, line2.start]) == 10mm
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line_id = *sketch.segments.get(5).unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
5,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_point_preserves_multiline_coincident_constraint() {
let initial_source = "\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
point2 = point(at = [var 3, var 4])
point3 = point(at = [var 5, var 6])
coincident([point1, point2, point3])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.program = program.clone();
let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
frontend.update_state_after_exec(outcome, true);
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point3_id = *sketch.segments.get(2).unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
.await
.unwrap();
assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
assert!(
src_delta.text.contains("coincident([point1, point2])"),
"{}",
src_delta.text
);
let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
let sketch = expect_sketch(sketch_object);
assert_eq!(sketch.segments.len(), 2);
assert_eq!(sketch.constraints.len(), 1);
let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
panic!("Expected constraint object");
};
let Constraint::Coincident(coincident) = constraint else {
panic!("Expected coincident constraint");
};
assert_eq!(
coincident.segments,
sketch
.segments
.iter()
.copied()
.map(Into::into)
.collect::<Vec<ConstraintSegment>>()
);
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_line_preserves_multiline_equal_length_constraint() {
let initial_source = "\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
line3 = line(start = [var 9, var 10], end = [var 11, var 12])
equalLength([line1, line2, line3])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line3_id = *sketch.segments.get(8).unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
equalLength([line1, line2])
}
"
);
let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
let sketch = expect_sketch(sketch_object);
assert_eq!(sketch.constraints.len(), 1);
let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
panic!("Expected constraint object");
};
let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
panic!("Expected lines equal length constraint");
};
assert_eq!(lines_equal_length.lines.len(), 2);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_line_preserves_multiline_coincident_constraint() {
let initial_source = "\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
line3 = line(start = [var 9, var 10], end = [var 11, var 12])
coincident([line1.end, line2.start, line3.start])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.program = program.clone();
let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
frontend.update_state_after_exec(outcome, true);
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line1_id = *sketch.segments.get(2).unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
.await
.unwrap();
assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
assert!(
src_delta.text.contains("coincident([line2.start, line3.start])"),
"{}",
src_delta.text
);
let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
let sketch = expect_sketch(sketch_object);
assert_eq!(sketch.constraints.len(), 1);
let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
panic!("Expected constraint object");
};
let Constraint::Coincident(coincident) = constraint else {
panic!("Expected coincident constraint");
};
let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
assert_eq!(coincident.segments, remaining_segments);
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
let initial_source = "\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
line3 = line(start = [var 9, var 10], end = [var 11, var 12])
equalLength([line1, line2, line3])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line2_id = *sketch.segments.get(5).unwrap();
let line3_id = *sketch.segments.get(8).unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
}
"
);
let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
let sketch = expect_sketch(sketch_object);
assert!(sketch.constraints.is_empty());
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_line_preserves_multiline_parallel_constraint() {
let initial_source = "\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
line3 = line(start = [var 9, var 10], end = [var 11, var 12])
parallel([line1, line2, line3])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line3_id = *sketch.segments.get(8).unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
parallel([line1, line2])
}
"
);
let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
let sketch = expect_sketch(sketch_object);
assert_eq!(sketch.constraints.len(), 1);
let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
panic!("Expected constraint object");
};
let Constraint::Parallel(parallel) = constraint else {
panic!("Expected parallel constraint");
};
assert_eq!(parallel.lines.len(), 2);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
let initial_source = "\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
line3 = line(start = [var 9, var 10], end = [var 11, var 12])
parallel([line1, line2, line3])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line2_id = *sketch.segments.get(5).unwrap();
let line3_id = *sketch.segments.get(8).unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
}
"
);
let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
let sketch = expect_sketch(sketch_object);
assert!(sketch.constraints.is_empty());
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_line_line_coincident_constraint() {
let initial_source = "\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
coincident([line1, line2])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let coincident_id = *sketch.constraints.first().unwrap();
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
}
"
);
assert_eq!(scene_delta.new_objects, vec![]);
assert_eq!(scene_delta.new_graph.objects.len(), 8);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_two_points_coincident() {
let initial_source = "\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
point(at = [3, 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point0_id = *sketch.segments.first().unwrap();
let point1_id = *sketch.segments.get(1).unwrap();
let constraint = Constraint::Coincident(Coincident {
segments: vec![point0_id.into(), point1_id.into()],
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
point2 = point(at = [3, 4])
coincident([point1, point2])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
5,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_three_points_coincident() {
let initial_source = "\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
point(at = [var 3, var 4])
point(at = [var 5, var 6])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.program = program.clone();
let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
frontend.update_state_after_exec(outcome, true);
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let segments = sketch
.segments
.iter()
.take(3)
.copied()
.map(Into::into)
.collect::<Vec<ConstraintSegment>>();
let constraint = Constraint::Coincident(Coincident {
segments: segments.clone(),
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
point2 = point(at = [var 3, var 4])
point3 = point(at = [var 5, var 6])
coincident([point1, point2, point3])
}
"
);
let constraint_object = scene_delta
.new_graph
.objects
.iter()
.find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
.unwrap();
let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
panic!("expected a constraint object");
};
assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_source_with_three_point_coincident_tracks_all_segments() {
let initial_source = "\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
point2 = point(at = [var 3, var 4])
point3 = point(at = [var 5, var 6])
coincident([point1, point2, point3])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_mock(None).await;
frontend.program = program.clone();
let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
frontend.update_state_after_exec(outcome, true);
let constraint_object = frontend
.scene_graph
.objects
.iter()
.find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
.unwrap();
let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
panic!("expected a constraint object");
};
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch = expect_sketch(sketch_object);
let expected_segments = sketch
.segments
.iter()
.take(3)
.copied()
.map(Into::into)
.collect::<Vec<ConstraintSegment>>();
assert_eq!(
constraint,
&Constraint::Coincident(Coincident {
segments: expected_segments,
})
);
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_point_origin_coincident_preserves_order() {
let initial_source = "\
sketch(on = XY) {
point(at = [var 1, var 2])
}
";
for (origin_first, expected_source) in [
(
true,
"\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
coincident([ORIGIN, point1])
}
",
),
(
false,
"\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
coincident([point1, ORIGIN])
}
",
),
] {
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point_id = *sketch.segments.first().unwrap();
let segments = if origin_first {
vec![ConstraintSegment::ORIGIN, point_id.into()]
} else {
vec![point_id.into(), ConstraintSegment::ORIGIN]
};
let constraint = Constraint::Coincident(Coincident {
segments: segments.clone(),
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(src_delta.text.as_str(), expected_source);
let constraint_object = scene_delta
.new_graph
.objects
.iter()
.find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
.unwrap();
let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
panic!("expected a constraint object");
};
assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
ctx.close().await;
mock_ctx.close().await;
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_coincident_of_line_end_points() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
line(start = [var 5, var 6], end = [var 7, var 8])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point0_id = *sketch.segments.get(1).unwrap();
let point1_id = *sketch.segments.get(3).unwrap();
let constraint = Constraint::Coincident(Coincident {
segments: vec![point0_id.into(), point1_id.into()],
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
coincident([line1.end, line2.start])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
9,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_coincident_of_line_point_and_circle_segment() {
let initial_source = "\
sketch(on = XY) {
circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
frontend.program = program;
frontend.update_state_after_exec(outcome, true);
let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let circle_id = sketch
.segments
.iter()
.copied()
.find(|seg_id| {
matches!(
&frontend.scene_graph.objects[seg_id.0].kind,
ObjectKind::Segment {
segment: Segment::Circle(_)
}
)
})
.expect("Expected a circle segment in sketch");
let line_id = sketch
.segments
.iter()
.copied()
.find(|seg_id| {
matches!(
&frontend.scene_graph.objects[seg_id.0].kind,
ObjectKind::Segment {
segment: Segment::Line(_)
}
)
})
.expect("Expected a line segment in sketch");
let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
ObjectKind::Segment {
segment: Segment::Line(line),
} => line.start,
_ => panic!("Expected line segment object"),
};
let constraint = Constraint::Coincident(Coincident {
segments: vec![line_start_point_id.into(), circle_id.into()],
});
let (src_delta, _scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
coincident([line1.start, circle1])
}
"
);
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_invalid_coincident_arc_and_line_preserves_state() {
let program = Program::empty();
let mut frontend = FrontendState::new();
frontend.program = program;
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let sketch_args = SketchCtor {
on: Plane::Default(PlaneName::Xy),
};
let (_src_delta, _scene_delta, sketch_id) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
let arc_ctor = ArcCtor {
start: Point2d {
x: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Var(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
},
center: Point2d {
x: Expr::Var(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let (_src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
.await
.unwrap();
let arc_id = *scene_delta.new_objects.last().unwrap();
let line_ctor = LineCtor {
start: Point2d {
x: Expr::Var(Number {
value: 20.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Var(Number {
value: 30.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let (_src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
.await
.unwrap();
let line_id = *scene_delta.new_objects.last().unwrap();
let constraint = Constraint::Coincident(Coincident {
segments: vec![arc_id.into(), line_id.into()],
});
let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
assert!(result.is_err(), "Expected invalid coincident constraint to fail");
let sketch_object_after =
find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
let sketch_after = expect_sketch(sketch_object_after);
assert!(
sketch_after.segments.contains(&arc_id),
"Arc segment should still exist after failed constraint"
);
assert!(
sketch_after.segments.contains(&line_id),
"Line segment should still exist after failed constraint"
);
let arc_obj = frontend
.scene_graph
.objects
.get(arc_id.0)
.expect("Arc object should still be accessible");
let line_obj = frontend
.scene_graph
.objects
.get(line_id.0)
.expect("Line object should still be accessible");
match &arc_obj.kind {
ObjectKind::Segment {
segment: Segment::Arc(_),
} => {}
_ => panic!("Arc object should still be an arc segment"),
}
match &line_obj.kind {
ObjectKind::Segment {
segment: Segment::Line(_),
} => {}
_ => panic!("Line object should still be a line segment"),
}
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_distance_two_points() {
let initial_source = "\
sketch(on = XY) {
point(at = [var 1, var 2])
point(at = [var 3, var 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point0_id = *sketch.segments.first().unwrap();
let point1_id = *sketch.segments.get(1).unwrap();
let constraint = Constraint::Distance(Distance {
points: vec![point0_id.into(), point1_id.into()],
distance: Number {
value: 2.0,
units: NumericSuffix::Mm,
},
source: Default::default(),
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
point2 = point(at = [var 3, var 4])
distance([point1, point2]) == 2mm
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
5,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_horizontal_distance_two_points() {
let initial_source = "\
sketch(on = XY) {
point(at = [var 1, var 2])
point(at = [var 3, var 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point0_id = *sketch.segments.first().unwrap();
let point1_id = *sketch.segments.get(1).unwrap();
let constraint = Constraint::HorizontalDistance(Distance {
points: vec![point0_id.into(), point1_id.into()],
distance: Number {
value: 2.0,
units: NumericSuffix::Mm,
},
source: Default::default(),
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
point2 = point(at = [var 3, var 4])
horizontalDistance([point1, point2]) == 2mm
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
5,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_radius_single_arc_segment() {
let initial_source = "\
sketch(on = XY) {
arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let arc_id = sketch
.segments
.iter()
.find(|&seg_id| {
let obj = frontend.scene_graph.objects.get(seg_id.0);
matches!(
obj.map(|o| &o.kind),
Some(ObjectKind::Segment {
segment: Segment::Arc(_)
})
)
})
.unwrap();
let constraint = Constraint::Radius(Radius {
arc: *arc_id,
radius: Number {
value: 5.0,
units: NumericSuffix::Mm,
},
source: Default::default(),
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
radius(arc1) == 5mm
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
7, "{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_vertical_distance_two_points() {
let initial_source = "\
sketch(on = XY) {
point(at = [var 1, var 2])
point(at = [var 3, var 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point0_id = *sketch.segments.first().unwrap();
let point1_id = *sketch.segments.get(1).unwrap();
let constraint = Constraint::VerticalDistance(Distance {
points: vec![point0_id.into(), point1_id.into()],
distance: Number {
value: 2.0,
units: NumericSuffix::Mm,
},
source: Default::default(),
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
point2 = point(at = [var 3, var 4])
verticalDistance([point1, point2]) == 2mm
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
5,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_add_fixed_standalone_point() {
let initial_source = "\
sketch(on = XY) {
point(at = [var 1, var 2])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point_id = *sketch.segments.first().unwrap();
let (src_delta, scene_delta) = frontend
.add_constraint(
&mock_ctx,
version,
sketch_id,
Constraint::Fixed(Fixed {
points: vec![FixedPoint {
point: point_id,
position: Point2d {
x: Number {
value: 2.0,
units: NumericSuffix::Mm,
},
y: Number {
value: 3.0,
units: NumericSuffix::Mm,
},
},
}],
}),
)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
fixed([point1, [2mm, 3mm]])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
4,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_add_fixed_multiple_points() {
let initial_source = "\
sketch(on = XY) {
point(at = [var 1, var 2])
point(at = [var 3, var 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point0_id = *sketch.segments.first().unwrap();
let point1_id = *sketch.segments.get(1).unwrap();
let (src_delta, scene_delta) = frontend
.add_constraint(
&mock_ctx,
version,
sketch_id,
Constraint::Fixed(Fixed {
points: vec![
FixedPoint {
point: point0_id,
position: Point2d {
x: Number {
value: 2.0,
units: NumericSuffix::Mm,
},
y: Number {
value: 3.0,
units: NumericSuffix::Mm,
},
},
},
FixedPoint {
point: point1_id,
position: Point2d {
x: Number {
value: 4.0,
units: NumericSuffix::Mm,
},
y: Number {
value: 5.0,
units: NumericSuffix::Mm,
},
},
},
],
}),
)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
point1 = point(at = [var 1, var 2])
point2 = point(at = [var 3, var 4])
fixed([point1, [2mm, 3mm]])
fixed([point2, [4mm, 5mm]])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
6,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_add_fixed_owned_point() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line_start_id = *sketch.segments.first().unwrap();
let (src_delta, scene_delta) = frontend
.add_constraint(
&mock_ctx,
version,
sketch_id,
Constraint::Fixed(Fixed {
points: vec![FixedPoint {
point: line_start_id,
position: Point2d {
x: Number {
value: 2.0,
units: NumericSuffix::Mm,
},
y: Number {
value: 3.0,
units: NumericSuffix::Mm,
},
},
}],
}),
)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
fixed([line1.start, [2mm, 3mm]])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
6,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_radius_error_cases() {
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let initial_source_point = "\
sketch(on = XY) {
point(at = [var 1, var 2])
}
";
let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
let mut frontend_point = FrontendState::new();
frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
let sketch_id_point = sketch_object_point.id;
let sketch_point = expect_sketch(sketch_object_point);
let point_id = *sketch_point.segments.first().unwrap();
let constraint_point = Constraint::Radius(Radius {
arc: point_id,
radius: Number {
value: 5.0,
units: NumericSuffix::Mm,
},
source: Default::default(),
});
let result_point = frontend_point
.add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
.await;
assert!(result_point.is_err(), "Single point should error for radius");
let initial_source_line = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
}
";
let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
let mut frontend_line = FrontendState::new();
frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
let sketch_id_line = sketch_object_line.id;
let sketch_line = expect_sketch(sketch_object_line);
let line_id = *sketch_line.segments.first().unwrap();
let constraint_line = Constraint::Radius(Radius {
arc: line_id,
radius: Number {
value: 5.0,
units: NumericSuffix::Mm,
},
source: Default::default(),
});
let result_line = frontend_line
.add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
.await;
assert!(result_line.is_err(), "Single line segment should error for radius");
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_diameter_single_arc_segment() {
let initial_source = "\
sketch(on = XY) {
arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let arc_id = sketch
.segments
.iter()
.find(|&seg_id| {
let obj = frontend.scene_graph.objects.get(seg_id.0);
matches!(
obj.map(|o| &o.kind),
Some(ObjectKind::Segment {
segment: Segment::Arc(_)
})
)
})
.unwrap();
let constraint = Constraint::Diameter(Diameter {
arc: *arc_id,
diameter: Number {
value: 10.0,
units: NumericSuffix::Mm,
},
source: Default::default(),
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
diameter(arc1) == 10mm
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
7, "{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_diameter_error_cases() {
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let initial_source_point = "\
sketch(on = XY) {
point(at = [var 1, var 2])
}
";
let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
let mut frontend_point = FrontendState::new();
frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
let sketch_id_point = sketch_object_point.id;
let sketch_point = expect_sketch(sketch_object_point);
let point_id = *sketch_point.segments.first().unwrap();
let constraint_point = Constraint::Diameter(Diameter {
arc: point_id,
diameter: Number {
value: 10.0,
units: NumericSuffix::Mm,
},
source: Default::default(),
});
let result_point = frontend_point
.add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
.await;
assert!(result_point.is_err(), "Single point should error for diameter");
let initial_source_line = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
}
";
let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
let mut frontend_line = FrontendState::new();
frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
let sketch_id_line = sketch_object_line.id;
let sketch_line = expect_sketch(sketch_object_line);
let line_id = *sketch_line.segments.first().unwrap();
let constraint_line = Constraint::Diameter(Diameter {
arc: line_id,
diameter: Number {
value: 10.0,
units: NumericSuffix::Mm,
},
source: Default::default(),
});
let result_line = frontend_line
.add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
.await;
assert!(result_line.is_err(), "Single line segment should error for diameter");
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_line_horizontal() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line1_id = *sketch.segments.get(2).unwrap();
let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
horizontal(line1)
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
6,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_line_vertical() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line1_id = *sketch.segments.get(2).unwrap();
let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
vertical(line1)
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
6,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_points_vertical() {
let initial_source = "\
sketch001 = sketch(on = XY) {
p0 = point(at = [var -2.23mm, var 3.1mm])
pf = point(at = [4, 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point_ids = vec![
sketch.segments.first().unwrap().to_owned(),
sketch.segments.get(1).unwrap().to_owned(),
];
let constraint = Constraint::Vertical(Vertical::Points {
points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch001 = sketch(on = XY) {
p0 = point(at = [var -2.23mm, var 3.1mm])
pf = point(at = [4, 4])
vertical([p0, pf])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
5,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_points_horizontal() {
let initial_source = "\
sketch001 = sketch(on = XY) {
p0 = point(at = [var -2.23mm, var 3.1mm])
pf = point(at = [4, 4])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point_ids = vec![
sketch.segments.first().unwrap().to_owned(),
sketch.segments.get(1).unwrap().to_owned(),
];
let constraint = Constraint::Horizontal(Horizontal::Points {
points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch001 = sketch(on = XY) {
p0 = point(at = [var -2.23mm, var 3.1mm])
pf = point(at = [4, 4])
horizontal([p0, pf])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
5,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_point_horizontal_with_origin() {
let initial_source = "\
sketch001 = sketch(on = XY) {
p0 = point(at = [var -2.23mm, var 3.1mm])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let point_id = *sketch.segments.first().unwrap();
let constraint = Constraint::Horizontal(Horizontal::Points {
points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch001 = sketch(on = XY) {
p0 = point(at = [var -2.23mm, var 3.1mm])
horizontal([p0, ORIGIN])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
4,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_lines_equal_length() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
line(start = [var 5, var 6], end = [var 7, var 8])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line1_id = *sketch.segments.get(2).unwrap();
let line2_id = *sketch.segments.get(5).unwrap();
let constraint = Constraint::LinesEqualLength(LinesEqualLength {
lines: vec![line1_id, line2_id],
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
equalLength([line1, line2])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
9,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_add_constraint_multi_line_equal_length() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
line(start = [var 5, var 6], end = [var 7, var 8])
line(start = [var 9, var 10], end = [var 11, var 12])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line1_id = *sketch.segments.get(2).unwrap();
let line2_id = *sketch.segments.get(5).unwrap();
let line3_id = *sketch.segments.get(8).unwrap();
let constraint = Constraint::LinesEqualLength(LinesEqualLength {
lines: vec![line1_id, line2_id, line3_id],
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
line3 = line(start = [var 9, var 10], end = [var 11, var 12])
equalLength([line1, line2, line3])
}
"
);
let constraints = scene_delta
.new_graph
.objects
.iter()
.filter_map(|obj| {
let ObjectKind::Constraint { constraint } = &obj.kind else {
return None;
};
Some(constraint)
})
.collect::<Vec<_>>();
assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
panic!("expected equal length constraint, got {:?}", constraints[0]);
};
assert_eq!(lines_equal_length.lines.len(), 3);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_lines_parallel() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
line(start = [var 5, var 6], end = [var 7, var 8])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line1_id = *sketch.segments.get(2).unwrap();
let line2_id = *sketch.segments.get(5).unwrap();
let constraint = Constraint::Parallel(Parallel {
lines: vec![line1_id, line2_id],
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
parallel([line1, line2])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
9,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_lines_parallel_multiline() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
line(start = [var 5, var 6], end = [var 7, var 8])
line(start = [var 9, var 10], end = [var 11, var 12])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line1_id = *sketch.segments.get(2).unwrap();
let line2_id = *sketch.segments.get(5).unwrap();
let line3_id = *sketch.segments.get(8).unwrap();
let constraint = Constraint::Parallel(Parallel {
lines: vec![line1_id, line2_id, line3_id],
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
line3 = line(start = [var 9, var 10], end = [var 11, var 12])
parallel([line1, line2, line3])
}
"
);
let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
let sketch = expect_sketch(sketch_object);
assert_eq!(sketch.constraints.len(), 1);
let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
panic!("Expected constraint object");
};
let Constraint::Parallel(parallel) = constraint else {
panic!("Expected parallel constraint");
};
assert_eq!(parallel.lines.len(), 3);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_lines_perpendicular() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
line(start = [var 5, var 6], end = [var 7, var 8])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line1_id = *sketch.segments.get(2).unwrap();
let line2_id = *sketch.segments.get(5).unwrap();
let constraint = Constraint::Perpendicular(Perpendicular {
lines: vec![line1_id, line2_id],
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
perpendicular([line1, line2])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
9,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_lines_angle() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
line(start = [var 5, var 6], end = [var 7, var 8])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line1_id = *sketch.segments.get(2).unwrap();
let line2_id = *sketch.segments.get(5).unwrap();
let constraint = Constraint::Angle(Angle {
lines: vec![line1_id, line2_id],
angle: Number {
value: 30.0,
units: NumericSuffix::Deg,
},
source: Default::default(),
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
line2 = line(start = [var 5, var 6], end = [var 7, var 8])
angle([line1, line2]) == 30deg
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
9,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_segments_tangent() {
let initial_source = "\
sketch(on = XY) {
line(start = [var 1, var 2], end = [var 3, var 4])
arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line1_id = *sketch.segments.get(2).unwrap();
let arc1_id = *sketch.segments.get(6).unwrap();
let constraint = Constraint::Tangent(Tangent {
input: vec![line1_id, arc1_id],
});
let (src_delta, scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch(on = XY) {
line1 = line(start = [var 1, var 2], end = [var 3, var 4])
arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
tangent([line1, arc1])
}
"
);
assert_eq!(
scene_delta.new_graph.objects.len(),
10,
"{:#?}",
scene_delta.new_graph.objects
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_sketch_on_face_simple() {
let initial_source = "\
len = 2mm
cube = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [len, 0], tag = $side)
|> line(end = [0, len])
|> line(end = [-len, 0])
|> line(end = [0, -len])
|> close()
|> extrude(length = len)
face = faceOf(cube, face = side)
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
let face_id = face_object.id;
let sketch_args = SketchCtor {
on: Plane::Object(face_id),
};
let (_src_delta, scene_delta, sketch_id) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
assert_eq!(sketch_id, ObjectId(2));
assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
let sketch_object = &scene_delta.new_graph.objects[2];
assert_eq!(sketch_object.id, ObjectId(2));
assert_eq!(
sketch_object.kind,
ObjectKind::Sketch(Sketch {
args: SketchCtor {
on: Plane::Object(face_id),
},
plane: face_id,
segments: vec![],
constraints: vec![],
})
);
assert_eq!(scene_delta.new_graph.objects.len(), 8);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_sketch_on_wall_artifact_from_region_extrude() {
let initial_source = "\
s = sketch(on = YZ) {
line1 = line(start = [0, 0], end = [0, 1])
line2 = line(start = [0, 1], end = [1, 1])
line3 = line(start = [1, 1], end = [0, 0])
}
region001 = region(point = [0.1, 0.1], sketch = s)
extrude001 = extrude(region001, length = 5)
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
let sketch_args = SketchCtor {
on: Plane::Object(wall_object_id),
};
let (src_delta, _scene_delta, _sketch_id) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
let initial_source = "\
sketch001 = sketch(on = YZ) {
line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
coincident([line1.end, line2.start])
coincident([line2.end, line3.start])
coincident([line3.end, line4.start])
coincident([line4.end, line1.start])
parallel([line2, line4])
parallel([line3, line1])
perpendicular([line1, line2])
horizontal(line3)
line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
}
region001 = region(point = [3.1, 3.74], sketch = sketch001)
extrude001 = extrude(region001, length = 5)
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
let sketch_args = SketchCtor {
on: Plane::Object(wall_object_id),
};
let (src_delta, _scene_delta, _sketch_id) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_sketch_on_plane_incremental() {
let initial_source = "\
len = 2mm
cube = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [len, 0], tag = $side)
|> line(end = [0, len])
|> line(end = [-len, 0])
|> line(end = [0, -len])
|> close()
|> extrude(length = len)
plane = planeOf(cube, face = side)
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let plane_object = frontend
.scene_graph
.objects
.iter()
.rev()
.find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
.unwrap();
let plane_id = plane_object.id;
let sketch_args = SketchCtor {
on: Plane::Object(plane_id),
};
let (src_delta, scene_delta, sketch_id) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
len = 2mm
cube = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [len, 0], tag = $side)
|> line(end = [0, len])
|> line(end = [-len, 0])
|> line(end = [0, -len])
|> close()
|> extrude(length = len)
plane = planeOf(cube, face = side)
sketch001 = sketch(on = plane) {
}
"
);
assert_eq!(sketch_id, ObjectId(2));
assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
let sketch_object = &scene_delta.new_graph.objects[2];
assert_eq!(sketch_object.id, ObjectId(2));
assert_eq!(
sketch_object.kind,
ObjectKind::Sketch(Sketch {
args: SketchCtor {
on: Plane::Object(plane_id),
},
plane: plane_id,
segments: vec![],
constraints: vec![],
})
);
assert_eq!(scene_delta.new_graph.objects.len(), 9);
let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
assert_eq!(plane_object.id, plane_id);
assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_new_sketch_uses_unique_variable_name() {
let initial_source = "\
sketch1 = sketch(on = XY) {
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_args = SketchCtor {
on: Plane::Default(PlaneName::Yz),
};
let (src_delta, _, _) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch1 = sketch(on = XY) {
}
sketch001 = sketch(on = YZ) {
}
"
);
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_new_sketch_twice_using_same_plane() {
let initial_source = "\
sketch1 = sketch(on = XY) {
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_args = SketchCtor {
on: Plane::Default(PlaneName::Xy),
};
let (src_delta, _, _) = frontend
.new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
sketch1 = sketch(on = XY) {
}
sketch001 = sketch(on = XY) {
}
"
);
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_sketch_mode_reuses_cached_on_expression() {
let initial_source = "\
width = 2mm
sketch(on = offsetPlane(XY, offset = width)) {
line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
distance([line1.start, line1.end]) == width
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let project_id = ProjectId(0);
let file_id = FileId(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let initial_object_count = frontend.scene_graph.objects.len();
let sketch_id = find_first_sketch_object(&frontend.scene_graph)
.expect("Expected sketch object to exist")
.id;
let scene_delta = frontend
.edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
.await
.unwrap();
assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_multiple_sketch_blocks() {
let initial_source = "\
// Cube that requires the engine.
width = 2
sketch001 = startSketchOn(XY)
profile001 = startProfile(sketch001, at = [0, 0])
|> yLine(length = width, tag = $seg1)
|> xLine(length = width)
|> yLine(length = -width)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = width)
// Get a value that requires the engine.
x = segLen(seg1)
// Triangle with side length 2*x.
sketch(on = XY) {
line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
coincident([line1.end, line2.start])
line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
coincident([line2.end, line3.start])
coincident([line3.end, line1.start])
equalLength([line3, line1])
equalLength([line1, line2])
distance([line1.start, line1.end]) == 2*x
}
// Line segment with length x.
sketch2 = sketch(on = XY) {
line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
distance([line1.start, line1.end]) == x
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let project_id = ProjectId(0);
let file_id = FileId(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_objects = frontend
.scene_graph
.objects
.iter()
.filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
.collect::<Vec<_>>();
let sketch1_id = sketch_objects.first().unwrap().id;
let sketch2_id = sketch_objects.get(1).unwrap().id;
let point1_id = ObjectId(sketch1_id.0 + 1);
let point2_id = ObjectId(sketch2_id.0 + 1);
let scene_delta = frontend
.edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
.await
.unwrap();
assert_eq!(
scene_delta.new_graph.objects.len(),
18,
"{:#?}",
scene_delta.new_graph.objects
);
let point_ctor = PointCtor {
position: Point2d {
x: Expr::Var(Number {
value: 1.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 2.0,
units: NumericSuffix::Mm,
}),
},
};
let segments = vec![ExistingSegmentCtor {
id: point1_id,
ctor: SegmentCtor::Point(point_ctor),
}];
let (src_delta, _) = frontend
.edit_segments(&mock_ctx, version, sketch1_id, segments)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
// Cube that requires the engine.
width = 2
sketch001 = startSketchOn(XY)
profile001 = startProfile(sketch001, at = [0, 0])
|> yLine(length = width, tag = $seg1)
|> xLine(length = width)
|> yLine(length = -width)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = width)
// Get a value that requires the engine.
x = segLen(seg1)
// Triangle with side length 2*x.
sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
coincident([line1.end, line2.start])
line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
coincident([line2.end, line3.start])
coincident([line3.end, line1.start])
equalLength([line3, line1])
equalLength([line1, line2])
distance([line1.start, line1.end]) == 2 * x
}
// Line segment with length x.
sketch2 = sketch(on = XY) {
line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
distance([line1.start, line1.end]) == x
}
"
);
let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
// Cube that requires the engine.
width = 2
sketch001 = startSketchOn(XY)
profile001 = startProfile(sketch001, at = [0, 0])
|> yLine(length = width, tag = $seg1)
|> xLine(length = width)
|> yLine(length = -width)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = width)
// Get a value that requires the engine.
x = segLen(seg1)
// Triangle with side length 2*x.
sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
coincident([line1.end, line2.start])
line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
coincident([line2.end, line3.start])
coincident([line3.end, line1.start])
equalLength([line3, line1])
equalLength([line1, line2])
distance([line1.start, line1.end]) == 2 * x
}
// Line segment with length x.
sketch2 = sketch(on = XY) {
line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
distance([line1.start, line1.end]) == x
}
"
);
let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
let scene_delta = frontend
.edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
.await
.unwrap();
assert_eq!(
scene_delta.new_graph.objects.len(),
24,
"{:#?}",
scene_delta.new_graph.objects
);
let point_ctor = PointCtor {
position: Point2d {
x: Expr::Var(Number {
value: 3.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 4.0,
units: NumericSuffix::Mm,
}),
},
};
let segments = vec![ExistingSegmentCtor {
id: point2_id,
ctor: SegmentCtor::Point(point_ctor),
}];
let (src_delta, _) = frontend
.edit_segments(&mock_ctx, version, sketch2_id, segments)
.await
.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
// Cube that requires the engine.
width = 2
sketch001 = startSketchOn(XY)
profile001 = startProfile(sketch001, at = [0, 0])
|> yLine(length = width, tag = $seg1)
|> xLine(length = width)
|> yLine(length = -width)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = width)
// Get a value that requires the engine.
x = segLen(seg1)
// Triangle with side length 2*x.
sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
coincident([line1.end, line2.start])
line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
coincident([line2.end, line3.start])
coincident([line3.end, line1.start])
equalLength([line3, line1])
equalLength([line1, line2])
distance([line1.start, line1.end]) == 2 * x
}
// Line segment with length x.
sketch2 = sketch(on = XY) {
line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
distance([line1.start, line1.end]) == x
}
"
);
let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
assert_eq!(
src_delta.text.as_str(),
"\
// Cube that requires the engine.
width = 2
sketch001 = startSketchOn(XY)
profile001 = startProfile(sketch001, at = [0, 0])
|> yLine(length = width, tag = $seg1)
|> xLine(length = width)
|> yLine(length = -width)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = width)
// Get a value that requires the engine.
x = segLen(seg1)
// Triangle with side length 2*x.
sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
coincident([line1.end, line2.start])
line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
coincident([line2.end, line3.start])
coincident([line3.end, line1.start])
equalLength([line3, line1])
equalLength([line1, line2])
distance([line1.start, line1.end]) == 2 * x
}
// Line segment with length x.
sketch2 = sketch(on = XY) {
line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
distance([line1.start, line1.end]) == x
}
"
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
clear_mem_cache().await;
let source = r#"sketch001 = sketch(on = XZ) {
circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
}
sketch002 = sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
coincident([line1.end, line2.start])
coincident([line2.end, line3.start])
coincident([line3.end, line4.start])
coincident([line4.end, line1.start])
parallel([line2, line4])
parallel([line3, line1])
perpendicular([line1, line2])
horizontal(line3)
coincident([line1.start, ORIGIN])
}
"#;
let program = Program::parse(source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_engine(
std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
Default::default(),
);
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let project_id = ProjectId(0);
let file_id = FileId(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_objects = frontend
.scene_graph
.objects
.iter()
.filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
.collect::<Vec<_>>();
assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
let sketch1_id = sketch_objects[0].id;
let sketch2_id = sketch_objects[1].id;
frontend
.edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
.await
.unwrap();
frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
let scene_delta = frontend
.edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
.await
.unwrap();
assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
clear_mem_cache().await;
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
let initial_source = "@settings(defaultLengthUnit = mm)
sketch001 = sketch(on = XY) {
point(at = [1in, 2in])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let project_id = ProjectId(0);
let file_id = FileId(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
frontend
.edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
.await
.unwrap();
let point_ctor = PointCtor {
position: Point2d {
x: Expr::Number(Number {
value: 5.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 6.0,
units: NumericSuffix::Mm,
}),
},
};
let segment = SegmentCtor::Point(point_ctor);
let (src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
assert!(
src_delta.text.contains("point(at = [5mm, 6mm])"),
"Expected new point in source, got: {}",
src_delta.text
);
assert!(!scene_delta.new_objects.is_empty());
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
let initial_source = "@settings(defaultLengthUnit = mm)
s = sketch(on = XY) {}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let line_ctor = LineCtor {
start: Point2d {
x: Expr::Number(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Number(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segment = SegmentCtor::Line(line_ctor);
let (src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
assert!(
src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
"Expected line in source, got: {}",
src_delta.text
);
assert_eq!(scene_delta.new_objects.len(), 3);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_extra_newlines_between_operations_edit_line() {
let initial_source = "@settings(defaultLengthUnit = mm)
sketch001 = sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let project_id = ProjectId(0);
let file_id = FileId(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line_id = sketch
.segments
.iter()
.copied()
.find(|seg_id| {
matches!(
&frontend.scene_graph.objects[seg_id.0].kind,
ObjectKind::Segment {
segment: Segment::Line(_)
}
)
})
.expect("Expected a line segment in sketch");
frontend
.edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
.await
.unwrap();
let line_ctor = LineCtor {
start: Point2d {
x: Expr::Var(Number {
value: 1.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 2.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Var(Number {
value: 13.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 14.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segments = vec![ExistingSegmentCtor {
id: line_id,
ctor: SegmentCtor::Line(line_ctor),
}];
let (src_delta, _scene_delta) = frontend
.edit_segments(&mock_ctx, version, sketch_id, segments)
.await
.unwrap();
assert!(
src_delta
.text
.contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
"Expected edited line in source, got: {}",
src_delta.text
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_extra_newlines_delete_segment() {
let initial_source = "@settings(defaultLengthUnit = mm)
sketch001 = sketch(on = XY) {
circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
assert_eq!(sketch.segments.len(), 3);
let circle_id = sketch.segments[2];
let (src_delta, scene_delta) = frontend
.delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
.await
.unwrap();
assert!(
src_delta.text.contains("sketch(on = XY) {"),
"Expected sketch block in source, got: {}",
src_delta.text
);
let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
let new_sketch = expect_sketch(new_sketch_object);
assert_eq!(new_sketch.segments.len(), 0);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_unformatted_source_add_arc() {
let initial_source = "@settings(defaultLengthUnit = mm)
sketch001 = sketch(on = XY) {
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let arc_ctor = ArcCtor {
start: Point2d {
x: Expr::Var(Number {
value: 5.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 5.0,
units: NumericSuffix::Mm,
}),
},
center: Point2d {
x: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segment = SegmentCtor::Arc(arc_ctor);
let (src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
assert!(
src_delta
.text
.contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
"Expected arc in source, got: {}",
src_delta.text
);
assert!(!scene_delta.new_objects.is_empty());
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_extra_newlines_add_circle() {
let initial_source = "@settings(defaultLengthUnit = mm)
sketch001 = sketch(on = XY) {
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let circle_ctor = CircleCtor {
start: Point2d {
x: Expr::Var(Number {
value: 5.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
center: Point2d {
x: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
y: Expr::Var(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segment = SegmentCtor::Circle(circle_ctor);
let (src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
assert!(
src_delta
.text
.contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
"Expected circle in source, got: {}",
src_delta.text
);
assert!(!scene_delta.new_objects.is_empty());
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_extra_newlines_add_constraint() {
let initial_source = "@settings(defaultLengthUnit = mm)
sketch001 = sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
let project_id = ProjectId(0);
let file_id = FileId(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let sketch = expect_sketch(sketch_object);
let line_ids: Vec<ObjectId> = sketch
.segments
.iter()
.copied()
.filter(|seg_id| {
matches!(
&frontend.scene_graph.objects[seg_id.0].kind,
ObjectKind::Segment {
segment: Segment::Line(_)
}
)
})
.collect();
assert_eq!(line_ids.len(), 2, "Expected two line segments");
let line1 = &frontend.scene_graph.objects[line_ids[0].0];
let ObjectKind::Segment {
segment: Segment::Line(line1_data),
} = &line1.kind
else {
panic!("Expected line");
};
let line2 = &frontend.scene_graph.objects[line_ids[1].0];
let ObjectKind::Segment {
segment: Segment::Line(line2_data),
} = &line2.kind
else {
panic!("Expected line");
};
let constraint = Constraint::Coincident(Coincident {
segments: vec![line1_data.end.into(), line2_data.start.into()],
});
frontend
.edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
.await
.unwrap();
let (src_delta, _scene_delta) = frontend
.add_constraint(&mock_ctx, version, sketch_id, constraint)
.await
.unwrap();
assert!(
src_delta.text.contains("coincident("),
"Expected coincident constraint in source, got: {}",
src_delta.text
);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_extra_newlines_add_line_then_edit_line() {
let initial_source = "@settings(defaultLengthUnit = mm)
sketch001 = sketch(on = XY) {
}
";
let program = Program::parse(initial_source).unwrap().0.unwrap();
let mut frontend = FrontendState::new();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mock_ctx = ExecutorContext::new_mock(None).await;
let version = Version(0);
frontend.hack_set_program(&ctx, program).await.unwrap();
let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
let sketch_id = sketch_object.id;
let line_ctor = LineCtor {
start: Point2d {
x: Expr::Number(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 0.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Number(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 10.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segment = SegmentCtor::Line(line_ctor);
let (src_delta, scene_delta) = frontend
.add_segment(&mock_ctx, version, sketch_id, segment, None)
.await
.unwrap();
assert!(
src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
"Expected line in source after add, got: {}",
src_delta.text
);
let line_id = *scene_delta.new_objects.last().unwrap();
let line_ctor = LineCtor {
start: Point2d {
x: Expr::Number(Number {
value: 1.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 2.0,
units: NumericSuffix::Mm,
}),
},
end: Point2d {
x: Expr::Number(Number {
value: 13.0,
units: NumericSuffix::Mm,
}),
y: Expr::Number(Number {
value: 14.0,
units: NumericSuffix::Mm,
}),
},
construction: None,
};
let segments = vec![ExistingSegmentCtor {
id: line_id,
ctor: SegmentCtor::Line(line_ctor),
}];
let (src_delta, scene_delta) = frontend
.edit_segments(&mock_ctx, version, sketch_id, segments)
.await
.unwrap();
assert!(
src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
"Expected edited line in source, got: {}",
src_delta.text
);
assert_eq!(scene_delta.new_objects, vec![]);
ctx.close().await;
mock_ctx.close().await;
}
}