1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_error::CompilationIssue;
9use kcl_error::SourceRange;
10use kittycad_modeling_cmds::units::UnitLength;
11use serde::Serialize;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::collections::AhashIndexSet;
19#[cfg(feature = "artifact-graph")]
20use crate::execution::Artifact;
21#[cfg(feature = "artifact-graph")]
22use crate::execution::ArtifactGraph;
23#[cfg(feature = "artifact-graph")]
24use crate::execution::CapSubType;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::cache::SketchModeState;
28use crate::execution::cache::clear_mem_cache;
29use crate::execution::cache::read_old_memory;
30use crate::execution::cache::write_old_memory;
31use crate::fmt::format_number_literal;
32use crate::front::Angle;
33use crate::front::ArcCtor;
34use crate::front::CircleCtor;
35use crate::front::Distance;
36use crate::front::EqualRadius;
37use crate::front::Error;
38use crate::front::ExecResult;
39use crate::front::FixedPoint;
40use crate::front::Freedom;
41use crate::front::LinesEqualLength;
42use crate::front::Object;
43use crate::front::Parallel;
44use crate::front::Perpendicular;
45use crate::front::PointCtor;
46use crate::front::Tangent;
47use crate::frontend::api::Expr;
48use crate::frontend::api::FileId;
49use crate::frontend::api::Number;
50use crate::frontend::api::ObjectId;
51use crate::frontend::api::ObjectKind;
52use crate::frontend::api::Plane;
53use crate::frontend::api::ProjectId;
54use crate::frontend::api::RestoreSketchCheckpointOutcome;
55use crate::frontend::api::SceneGraph;
56use crate::frontend::api::SceneGraphDelta;
57use crate::frontend::api::SketchCheckpointId;
58use crate::frontend::api::SourceDelta;
59use crate::frontend::api::SourceRef;
60use crate::frontend::api::Version;
61use crate::frontend::modify::find_defined_names;
62use crate::frontend::modify::next_free_name;
63use crate::frontend::modify::next_free_name_with_padding;
64use crate::frontend::sketch::Coincident;
65use crate::frontend::sketch::Constraint;
66use crate::frontend::sketch::ConstraintSegment;
67use crate::frontend::sketch::Diameter;
68use crate::frontend::sketch::ExistingSegmentCtor;
69use crate::frontend::sketch::Horizontal;
70use crate::frontend::sketch::LineCtor;
71use crate::frontend::sketch::Point2d;
72use crate::frontend::sketch::Radius;
73use crate::frontend::sketch::Segment;
74use crate::frontend::sketch::SegmentCtor;
75use crate::frontend::sketch::SketchApi;
76use crate::frontend::sketch::SketchCtor;
77use crate::frontend::sketch::Vertical;
78use crate::frontend::traverse::MutateBodyItem;
79use crate::frontend::traverse::TraversalReturn;
80use crate::frontend::traverse::Visitor;
81use crate::frontend::traverse::dfs_mut;
82use crate::id::IncIdGenerator;
83use crate::parsing::ast::types as ast;
84use crate::pretty::NumericSuffix;
85use crate::std::constraints::LinesAtAngleKind;
86use crate::walk::NodeMut;
87use crate::walk::Visitable;
88
89pub(crate) mod api;
90pub(crate) mod modify;
91pub(crate) mod sketch;
92
93pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
94
95#[derive(Debug, Clone)]
96struct SketchCheckpoint {
97 id: SketchCheckpointId,
98 source: SourceDelta,
99 program: Program,
100 scene_graph: SceneGraph,
101 exec_outcome: ExecOutcome,
102 point_freedom_cache: HashMap<ObjectId, Freedom>,
103 mock_memory: Option<SketchModeState>,
104}
105mod traverse;
106pub(crate) mod trim;
107
108struct ArcSizeConstraintParams {
109 points: Vec<ObjectId>,
110 function_name: &'static str,
111 value: f64,
112 units: NumericSuffix,
113 constraint_type_name: &'static str,
114}
115
116const POINT_FN: &str = "point";
117const POINT_AT_PARAM: &str = "at";
118const LINE_FN: &str = "line";
119const LINE_START_PARAM: &str = "start";
120const LINE_END_PARAM: &str = "end";
121const ARC_FN: &str = "arc";
122const ARC_START_PARAM: &str = "start";
123const ARC_END_PARAM: &str = "end";
124const ARC_CENTER_PARAM: &str = "center";
125const CIRCLE_FN: &str = "circle";
126const CIRCLE_VARIABLE: &str = "circle";
127const CIRCLE_START_PARAM: &str = "start";
128const CIRCLE_CENTER_PARAM: &str = "center";
129
130const COINCIDENT_FN: &str = "coincident";
131const DIAMETER_FN: &str = "diameter";
132const DISTANCE_FN: &str = "distance";
133const FIXED_FN: &str = "fixed";
134const ANGLE_FN: &str = "angle";
135const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
136const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
137const EQUAL_LENGTH_FN: &str = "equalLength";
138const EQUAL_RADIUS_FN: &str = "equalRadius";
139const HORIZONTAL_FN: &str = "horizontal";
140const RADIUS_FN: &str = "radius";
141const TANGENT_FN: &str = "tangent";
142const VERTICAL_FN: &str = "vertical";
143
144const LINE_PROPERTY_START: &str = "start";
145const LINE_PROPERTY_END: &str = "end";
146
147const ARC_PROPERTY_START: &str = "start";
148const ARC_PROPERTY_END: &str = "end";
149const ARC_PROPERTY_CENTER: &str = "center";
150const CIRCLE_PROPERTY_START: &str = "start";
151const CIRCLE_PROPERTY_CENTER: &str = "center";
152
153const CONSTRUCTION_PARAM: &str = "construction";
154
155#[derive(Debug, Clone, Copy)]
156enum EditDeleteKind {
157 Edit,
158 DeleteNonSketch,
159}
160
161impl EditDeleteKind {
162 fn is_delete(&self) -> bool {
164 match self {
165 EditDeleteKind::Edit => false,
166 EditDeleteKind::DeleteNonSketch => true,
167 }
168 }
169
170 fn to_change_kind(self) -> ChangeKind {
171 match self {
172 EditDeleteKind::Edit => ChangeKind::Edit,
173 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
174 }
175 }
176}
177
178#[derive(Debug, Clone, Copy)]
179enum ChangeKind {
180 Add,
181 Edit,
182 Delete,
183 None,
184}
185
186#[derive(Debug, Clone, Serialize, ts_rs::TS)]
187#[ts(export, export_to = "FrontendApi.ts")]
188#[serde(tag = "type")]
189pub enum SetProgramOutcome {
190 #[serde(rename_all = "camelCase")]
191 Success {
192 scene_graph: Box<SceneGraph>,
193 exec_outcome: Box<ExecOutcome>,
194 checkpoint_id: Option<SketchCheckpointId>,
195 },
196 #[serde(rename_all = "camelCase")]
197 ExecFailure { error: Box<KclErrorWithOutputs> },
198}
199
200#[derive(Debug, Clone)]
201pub struct FrontendState {
202 program: Program,
203 scene_graph: SceneGraph,
204 point_freedom_cache: HashMap<ObjectId, Freedom>,
207 sketch_checkpoints: VecDeque<SketchCheckpoint>,
208 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
209}
210
211impl Default for FrontendState {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217impl FrontendState {
218 pub fn new() -> Self {
219 Self {
220 program: Program::empty(),
221 scene_graph: SceneGraph {
222 project: ProjectId(0),
223 file: FileId(0),
224 version: Version(0),
225 objects: Default::default(),
226 settings: Default::default(),
227 sketch_mode: Default::default(),
228 },
229 point_freedom_cache: HashMap::new(),
230 sketch_checkpoints: VecDeque::new(),
231 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
232 }
233 }
234
235 pub fn scene_graph(&self) -> &SceneGraph {
237 &self.scene_graph
238 }
239
240 pub fn default_length_unit(&self) -> UnitLength {
241 self.program
242 .meta_settings()
243 .ok()
244 .flatten()
245 .map(|settings| settings.default_length_units)
246 .unwrap_or(UnitLength::Millimeters)
247 }
248
249 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
250 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
251
252 let checkpoint = SketchCheckpoint {
253 id: checkpoint_id,
254 source: SourceDelta {
255 text: source_from_ast(&self.program.ast),
256 },
257 program: self.program.clone(),
258 scene_graph: self.scene_graph.clone(),
259 exec_outcome,
260 point_freedom_cache: self.point_freedom_cache.clone(),
261 mock_memory: read_old_memory().await,
262 };
263
264 self.sketch_checkpoints.push_back(checkpoint);
265 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
266 self.sketch_checkpoints.pop_front();
267 }
268
269 Ok(checkpoint_id)
270 }
271
272 pub async fn restore_sketch_checkpoint(
273 &mut self,
274 checkpoint_id: SketchCheckpointId,
275 ) -> api::Result<RestoreSketchCheckpointOutcome> {
276 let checkpoint = self
277 .sketch_checkpoints
278 .iter()
279 .find(|checkpoint| checkpoint.id == checkpoint_id)
280 .cloned()
281 .ok_or_else(|| Error {
282 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
283 })?;
284
285 self.program = checkpoint.program;
286 self.scene_graph = checkpoint.scene_graph.clone();
287 self.point_freedom_cache = checkpoint.point_freedom_cache;
288
289 if let Some(mock_memory) = checkpoint.mock_memory {
290 write_old_memory(mock_memory).await;
291 } else {
292 clear_mem_cache().await;
293 }
294
295 Ok(RestoreSketchCheckpointOutcome {
296 source_delta: checkpoint.source,
297 scene_graph_delta: SceneGraphDelta {
298 new_graph: checkpoint.scene_graph,
299 new_objects: Vec::new(),
300 invalidates_ids: true,
301 exec_outcome: checkpoint.exec_outcome,
302 },
303 })
304 }
305
306 pub fn clear_sketch_checkpoints(&mut self) {
307 self.sketch_checkpoints.clear();
308 }
309}
310
311impl SketchApi for FrontendState {
312 async fn execute_mock(
313 &mut self,
314 ctx: &ExecutorContext,
315 _version: Version,
316 sketch: ObjectId,
317 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
318 let sketch_block_ref =
319 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
320
321 let mut truncated_program = self.program.clone();
322 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
323 .map_err(KclErrorWithOutputs::no_outputs)?;
324
325 let outcome = ctx
327 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
328 .await?;
329 let new_source = source_from_ast(&self.program.ast);
330 let src_delta = SourceDelta { text: new_source };
331 let outcome = self.update_state_after_exec(outcome, true);
333 let scene_graph_delta = SceneGraphDelta {
334 new_graph: self.scene_graph.clone(),
335 new_objects: Default::default(),
336 invalidates_ids: false,
337 exec_outcome: outcome,
338 };
339 Ok((src_delta, scene_graph_delta))
340 }
341
342 async fn new_sketch(
343 &mut self,
344 ctx: &ExecutorContext,
345 _project: ProjectId,
346 _file: FileId,
347 _version: Version,
348 args: SketchCtor,
349 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
350 let mut new_ast = self.program.ast.clone();
353 let mut plane_ast =
355 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
356 let mut defined_names = find_defined_names(&new_ast);
357 let is_face_of_expr = matches!(
358 &plane_ast,
359 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
360 );
361 if is_face_of_expr {
362 let face_name = next_free_name_with_padding("face", &defined_names)
363 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
364 let face_decl = ast::VariableDeclaration::new(
365 ast::VariableDeclarator::new(&face_name, plane_ast),
366 ast::ItemVisibility::Default,
367 ast::VariableKind::Const,
368 );
369 new_ast
370 .body
371 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
372 face_decl,
373 ))));
374 defined_names.insert(face_name.clone());
375 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
376 }
377 let sketch_ast = ast::SketchBlock {
378 arguments: vec![ast::LabeledArg {
379 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
380 arg: plane_ast,
381 }],
382 body: Default::default(),
383 is_being_edited: false,
384 non_code_meta: Default::default(),
385 digest: None,
386 };
387 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
390 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
391 let sketch_decl = ast::VariableDeclaration::new(
392 ast::VariableDeclarator::new(
393 &sketch_name,
394 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
395 ),
396 ast::ItemVisibility::Default,
397 ast::VariableKind::Const,
398 );
399 new_ast
400 .body
401 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
402 sketch_decl,
403 ))));
404 let new_source = source_from_ast(&new_ast);
406 let (new_program, errors) = Program::parse(&new_source)
408 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
409 if !errors.is_empty() {
410 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
411 "Error parsing KCL source after adding sketch: {errors:?}"
412 ))));
413 }
414 let Some(new_program) = new_program else {
415 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
416 "No AST produced after adding sketch".to_owned(),
417 )));
418 };
419
420 self.program = new_program.clone();
422
423 let outcome = ctx.run_with_caching(new_program.clone()).await?;
426 let freedom_analysis_ran = true;
427
428 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
429
430 let Some(sketch_id) = self
431 .scene_graph
432 .objects
433 .iter()
434 .filter_map(|object| match object.kind {
435 ObjectKind::Sketch(_) => Some(object.id),
436 _ => None,
437 })
438 .max_by_key(|id| id.0)
439 else {
440 return Err(KclErrorWithOutputs::from_error_outcome(
441 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
442 outcome,
443 ));
444 };
445 self.scene_graph.sketch_mode = Some(sketch_id);
447
448 let src_delta = SourceDelta { text: new_source };
449 let scene_graph_delta = SceneGraphDelta {
450 new_graph: self.scene_graph.clone(),
451 invalidates_ids: false,
452 new_objects: vec![sketch_id],
453 exec_outcome: outcome,
454 };
455 Ok((src_delta, scene_graph_delta, sketch_id))
456 }
457
458 async fn edit_sketch(
459 &mut self,
460 ctx: &ExecutorContext,
461 _project: ProjectId,
462 _file: FileId,
463 _version: Version,
464 sketch: ObjectId,
465 ) -> ExecResult<SceneGraphDelta> {
466 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
470 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
471 })?;
472 let ObjectKind::Sketch(_) = &sketch_object.kind else {
473 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
474 "Object is not a sketch, it is {}",
475 sketch_object.kind.human_friendly_kind_with_article()
476 ))));
477 };
478 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
479
480 self.scene_graph.sketch_mode = Some(sketch);
482
483 let mut truncated_program = self.program.clone();
485 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
486 .map_err(KclErrorWithOutputs::no_outputs)?;
487
488 let outcome = ctx
491 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
492 .await?;
493
494 let outcome = self.update_state_after_exec(outcome, true);
496 let scene_graph_delta = SceneGraphDelta {
497 new_graph: self.scene_graph.clone(),
498 invalidates_ids: false,
499 new_objects: Vec::new(),
500 exec_outcome: outcome,
501 };
502 Ok(scene_graph_delta)
503 }
504
505 async fn exit_sketch(
506 &mut self,
507 ctx: &ExecutorContext,
508 _version: Version,
509 sketch: ObjectId,
510 ) -> ExecResult<SceneGraph> {
511 #[cfg(not(target_arch = "wasm32"))]
513 let _ = sketch;
514 #[cfg(target_arch = "wasm32")]
515 if self.scene_graph.sketch_mode != Some(sketch) {
516 web_sys::console::warn_1(
517 &format!(
518 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
519 &self.scene_graph.sketch_mode
520 )
521 .into(),
522 );
523 }
524 self.scene_graph.sketch_mode = None;
525
526 let outcome = ctx.run_with_caching(self.program.clone()).await?;
528
529 self.update_state_after_exec(outcome, false);
531
532 Ok(self.scene_graph.clone())
533 }
534
535 async fn delete_sketch(
536 &mut self,
537 ctx: &ExecutorContext,
538 _version: Version,
539 sketch: ObjectId,
540 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
541 let mut new_ast = self.program.ast.clone();
544
545 let sketch_id = sketch;
547 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
548 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
549 })?;
550 let ObjectKind::Sketch(_) = &sketch_object.kind else {
551 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
552 "Object is not a sketch, it is {}",
553 sketch_object.kind.human_friendly_kind_with_article(),
554 ))));
555 };
556
557 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
559 .map_err(KclErrorWithOutputs::no_outputs)?;
560
561 self.execute_after_delete_sketch(ctx, &mut new_ast).await
562 }
563
564 async fn add_segment(
565 &mut self,
566 ctx: &ExecutorContext,
567 _version: Version,
568 sketch: ObjectId,
569 segment: SegmentCtor,
570 _label: Option<String>,
571 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
572 match segment {
574 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
575 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
576 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
577 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
578 }
579 }
580
581 async fn edit_segments(
582 &mut self,
583 ctx: &ExecutorContext,
584 _version: Version,
585 sketch: ObjectId,
586 segments: Vec<ExistingSegmentCtor>,
587 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
588 let sketch_block_ref =
590 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
591
592 let mut new_ast = self.program.ast.clone();
593 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
594
595 for segment in &segments {
598 segment_ids_edited.insert(segment.id);
599 }
600
601 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
616
617 for segment in segments {
618 let segment_id = segment.id;
619 match segment.ctor {
620 SegmentCtor::Point(ctor) => {
621 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
623 && let ObjectKind::Segment { segment } = &segment_object.kind
624 && let Segment::Point(point) = segment
625 && let Some(owner_id) = point.owner
626 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
627 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
628 {
629 match owner_segment {
630 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
631 if let Some(existing) = final_edits.get_mut(&owner_id) {
632 let SegmentCtor::Line(line_ctor) = existing else {
633 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
634 "Internal: Expected line ctor for owner, but found {}",
635 existing.human_friendly_kind_with_article()
636 ))));
637 };
638 if line.start == segment_id {
640 line_ctor.start = ctor.position;
641 } else {
642 line_ctor.end = ctor.position;
643 }
644 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
645 let mut line_ctor = line_ctor.clone();
647 if line.start == segment_id {
648 line_ctor.start = ctor.position;
649 } else {
650 line_ctor.end = ctor.position;
651 }
652 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
653 } else {
654 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
656 "Internal: Line does not have line ctor, but found {}",
657 line.ctor.human_friendly_kind_with_article()
658 ))));
659 }
660 continue;
661 }
662 Segment::Arc(arc)
663 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
664 {
665 if let Some(existing) = final_edits.get_mut(&owner_id) {
666 let SegmentCtor::Arc(arc_ctor) = existing else {
667 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
668 "Internal: Expected arc ctor for owner, but found {}",
669 existing.human_friendly_kind_with_article()
670 ))));
671 };
672 if arc.start == segment_id {
673 arc_ctor.start = ctor.position;
674 } else if arc.end == segment_id {
675 arc_ctor.end = ctor.position;
676 } else {
677 arc_ctor.center = ctor.position;
678 }
679 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
680 let mut arc_ctor = arc_ctor.clone();
681 if arc.start == segment_id {
682 arc_ctor.start = ctor.position;
683 } else if arc.end == segment_id {
684 arc_ctor.end = ctor.position;
685 } else {
686 arc_ctor.center = ctor.position;
687 }
688 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
689 } else {
690 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
691 "Internal: Arc does not have arc ctor, but found {}",
692 arc.ctor.human_friendly_kind_with_article()
693 ))));
694 }
695 continue;
696 }
697 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
698 if let Some(existing) = final_edits.get_mut(&owner_id) {
699 let SegmentCtor::Circle(circle_ctor) = existing else {
700 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
701 "Internal: Expected circle ctor for owner, but found {}",
702 existing.human_friendly_kind_with_article()
703 ))));
704 };
705 if circle.start == segment_id {
706 circle_ctor.start = ctor.position;
707 } else {
708 circle_ctor.center = ctor.position;
709 }
710 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
711 let mut circle_ctor = circle_ctor.clone();
712 if circle.start == segment_id {
713 circle_ctor.start = ctor.position;
714 } else {
715 circle_ctor.center = ctor.position;
716 }
717 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
718 } else {
719 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
720 "Internal: Circle does not have circle ctor, but found {}",
721 circle.ctor.human_friendly_kind_with_article()
722 ))));
723 }
724 continue;
725 }
726 _ => {}
727 }
728 }
729
730 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
732 }
733 SegmentCtor::Line(ctor) => {
734 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
735 }
736 SegmentCtor::Arc(ctor) => {
737 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
738 }
739 SegmentCtor::Circle(ctor) => {
740 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
741 }
742 }
743 }
744
745 for (segment_id, ctor) in final_edits {
746 match ctor {
747 SegmentCtor::Point(ctor) => self
748 .edit_point(&mut new_ast, sketch, segment_id, ctor)
749 .map_err(KclErrorWithOutputs::no_outputs)?,
750 SegmentCtor::Line(ctor) => self
751 .edit_line(&mut new_ast, sketch, segment_id, ctor)
752 .map_err(KclErrorWithOutputs::no_outputs)?,
753 SegmentCtor::Arc(ctor) => self
754 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
755 .map_err(KclErrorWithOutputs::no_outputs)?,
756 SegmentCtor::Circle(ctor) => self
757 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
758 .map_err(KclErrorWithOutputs::no_outputs)?,
759 }
760 }
761 self.execute_after_edit(
762 ctx,
763 sketch,
764 sketch_block_ref,
765 segment_ids_edited,
766 EditDeleteKind::Edit,
767 &mut new_ast,
768 )
769 .await
770 }
771
772 async fn delete_objects(
773 &mut self,
774 ctx: &ExecutorContext,
775 _version: Version,
776 sketch: ObjectId,
777 constraint_ids: Vec<ObjectId>,
778 segment_ids: Vec<ObjectId>,
779 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
780 let sketch_block_ref =
782 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
783
784 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
786 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
787
788 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
791
792 for segment_id in segment_ids_set.iter().copied() {
793 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
794 && let ObjectKind::Segment { segment } = &segment_object.kind
795 && let Segment::Point(point) = segment
796 && let Some(owner_id) = point.owner
797 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
798 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
799 && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
800 {
801 resolved_segment_ids_to_delete.insert(owner_id);
803 } else {
804 resolved_segment_ids_to_delete.insert(segment_id);
806 }
807 }
808 let referenced_constraint_ids = self
809 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
810 .map_err(KclErrorWithOutputs::no_outputs)?;
811
812 let mut new_ast = self.program.ast.clone();
813
814 for constraint_id in referenced_constraint_ids {
815 if constraint_ids_set.contains(&constraint_id) {
816 continue;
817 }
818
819 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
820 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
821 })?;
822 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
823 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
824 "Object is not a constraint, it is {}",
825 constraint_object.kind.human_friendly_kind_with_article()
826 ))));
827 };
828
829 match constraint {
830 Constraint::Coincident(coincident) => {
831 let remaining_segments = coincident
833 .segments
834 .iter()
835 .copied()
836 .filter(|segment| match segment {
837 ConstraintSegment::Segment(point_id) => {
838 if resolved_segment_ids_to_delete.contains(point_id) {
839 return false;
841 }
842 let point_object = self.scene_graph.objects.get(point_id.0);
843 if let Some(object) = point_object
844 && let ObjectKind::Segment { segment } = &object.kind
845 && let Segment::Point(point) = segment
846 && let Some(owner_id) = point.owner
847 {
848 return !resolved_segment_ids_to_delete.contains(&owner_id);
851 }
852 true
853 }
854 ConstraintSegment::Origin(_) => true,
855 })
856 .collect::<Vec<_>>();
857
858 if remaining_segments.len() >= 2 {
860 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
861 .map_err(KclErrorWithOutputs::no_outputs)?;
862 } else {
863 constraint_ids_set.insert(constraint_id);
864 }
865 }
866 Constraint::EqualRadius(equal_radius) => {
867 let remaining_input = equal_radius
868 .input
869 .iter()
870 .copied()
871 .filter(|segment_id| !resolved_segment_ids_to_delete.contains(segment_id))
872 .collect::<Vec<_>>();
873
874 if remaining_input.len() >= 2 {
875 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
876 .map_err(KclErrorWithOutputs::no_outputs)?;
877 } else {
878 constraint_ids_set.insert(constraint_id);
879 }
880 }
881 Constraint::LinesEqualLength(lines_equal_length) => {
882 let remaining_lines = lines_equal_length
883 .lines
884 .iter()
885 .copied()
886 .filter(|line_id| !resolved_segment_ids_to_delete.contains(line_id))
887 .collect::<Vec<_>>();
888
889 if remaining_lines.len() >= 2 {
891 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
892 .map_err(KclErrorWithOutputs::no_outputs)?;
893 } else {
894 constraint_ids_set.insert(constraint_id);
895 }
896 }
897 Constraint::Parallel(parallel) => {
898 let remaining_lines = parallel
899 .lines
900 .iter()
901 .copied()
902 .filter(|line_id| !resolved_segment_ids_to_delete.contains(line_id))
903 .collect::<Vec<_>>();
904
905 if remaining_lines.len() >= 2 {
906 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
907 .map_err(KclErrorWithOutputs::no_outputs)?;
908 } else {
909 constraint_ids_set.insert(constraint_id);
910 }
911 }
912 _ => {
913 constraint_ids_set.insert(constraint_id);
915 }
916 }
917 }
918
919 for constraint_id in constraint_ids_set {
920 self.delete_constraint(&mut new_ast, sketch, constraint_id)
921 .map_err(KclErrorWithOutputs::no_outputs)?;
922 }
923 for segment_id in resolved_segment_ids_to_delete {
924 self.delete_segment(&mut new_ast, sketch, segment_id)
925 .map_err(KclErrorWithOutputs::no_outputs)?;
926 }
927
928 self.execute_after_edit(
929 ctx,
930 sketch,
931 sketch_block_ref,
932 Default::default(),
933 EditDeleteKind::DeleteNonSketch,
934 &mut new_ast,
935 )
936 .await
937 }
938
939 async fn add_constraint(
940 &mut self,
941 ctx: &ExecutorContext,
942 _version: Version,
943 sketch: ObjectId,
944 constraint: Constraint,
945 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
946 let original_program = self.program.clone();
950 let original_scene_graph = self.scene_graph.clone();
951
952 let mut new_ast = self.program.ast.clone();
953 let sketch_block_ref = match constraint {
954 Constraint::Coincident(coincident) => self
955 .add_coincident(sketch, coincident, &mut new_ast)
956 .await
957 .map_err(KclErrorWithOutputs::no_outputs)?,
958 Constraint::Distance(distance) => self
959 .add_distance(sketch, distance, &mut new_ast)
960 .await
961 .map_err(KclErrorWithOutputs::no_outputs)?,
962 Constraint::EqualRadius(equal_radius) => self
963 .add_equal_radius(sketch, equal_radius, &mut new_ast)
964 .await
965 .map_err(KclErrorWithOutputs::no_outputs)?,
966 Constraint::Fixed(fixed) => self
967 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
968 .await
969 .map_err(KclErrorWithOutputs::no_outputs)?,
970 Constraint::HorizontalDistance(distance) => self
971 .add_horizontal_distance(sketch, distance, &mut new_ast)
972 .await
973 .map_err(KclErrorWithOutputs::no_outputs)?,
974 Constraint::VerticalDistance(distance) => self
975 .add_vertical_distance(sketch, distance, &mut new_ast)
976 .await
977 .map_err(KclErrorWithOutputs::no_outputs)?,
978 Constraint::Horizontal(horizontal) => self
979 .add_horizontal(sketch, horizontal, &mut new_ast)
980 .await
981 .map_err(KclErrorWithOutputs::no_outputs)?,
982 Constraint::LinesEqualLength(lines_equal_length) => self
983 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
984 .await
985 .map_err(KclErrorWithOutputs::no_outputs)?,
986 Constraint::Parallel(parallel) => self
987 .add_parallel(sketch, parallel, &mut new_ast)
988 .await
989 .map_err(KclErrorWithOutputs::no_outputs)?,
990 Constraint::Perpendicular(perpendicular) => self
991 .add_perpendicular(sketch, perpendicular, &mut new_ast)
992 .await
993 .map_err(KclErrorWithOutputs::no_outputs)?,
994 Constraint::Radius(radius) => self
995 .add_radius(sketch, radius, &mut new_ast)
996 .await
997 .map_err(KclErrorWithOutputs::no_outputs)?,
998 Constraint::Diameter(diameter) => self
999 .add_diameter(sketch, diameter, &mut new_ast)
1000 .await
1001 .map_err(KclErrorWithOutputs::no_outputs)?,
1002 Constraint::Vertical(vertical) => self
1003 .add_vertical(sketch, vertical, &mut new_ast)
1004 .await
1005 .map_err(KclErrorWithOutputs::no_outputs)?,
1006 Constraint::Angle(lines_at_angle) => self
1007 .add_angle(sketch, lines_at_angle, &mut new_ast)
1008 .await
1009 .map_err(KclErrorWithOutputs::no_outputs)?,
1010 Constraint::Tangent(tangent) => self
1011 .add_tangent(sketch, tangent, &mut new_ast)
1012 .await
1013 .map_err(KclErrorWithOutputs::no_outputs)?,
1014 };
1015
1016 let result = self
1017 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1018 .await;
1019
1020 if result.is_err() {
1022 self.program = original_program;
1023 self.scene_graph = original_scene_graph;
1024 }
1025
1026 result
1027 }
1028
1029 async fn chain_segment(
1030 &mut self,
1031 ctx: &ExecutorContext,
1032 version: Version,
1033 sketch: ObjectId,
1034 previous_segment_end_point_id: ObjectId,
1035 segment: SegmentCtor,
1036 _label: Option<String>,
1037 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1038 let SegmentCtor::Line(line_ctor) = segment else {
1042 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1043 "chain_segment currently only supports Line segments, got {}",
1044 segment.human_friendly_kind_with_article(),
1045 ))));
1046 };
1047
1048 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1050
1051 let new_line_id = first_scene_delta
1054 .new_objects
1055 .iter()
1056 .find(|&obj_id| {
1057 let obj = self.scene_graph.objects.get(obj_id.0);
1058 if let Some(obj) = obj {
1059 matches!(
1060 &obj.kind,
1061 ObjectKind::Segment {
1062 segment: Segment::Line(_)
1063 }
1064 )
1065 } else {
1066 false
1067 }
1068 })
1069 .ok_or_else(|| {
1070 KclErrorWithOutputs::no_outputs(KclError::refactor(
1071 "Failed to find new line segment in scene graph".to_string(),
1072 ))
1073 })?;
1074
1075 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1076 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1077 "New line object not found: {new_line_id:?}"
1078 )))
1079 })?;
1080
1081 let ObjectKind::Segment {
1082 segment: new_line_segment,
1083 } = &new_line_obj.kind
1084 else {
1085 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1086 "Object is not a segment: {new_line_obj:?}"
1087 ))));
1088 };
1089
1090 let Segment::Line(new_line) = new_line_segment else {
1091 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1092 "Segment is not a line: {new_line_segment:?}"
1093 ))));
1094 };
1095
1096 let new_line_start_point_id = new_line.start;
1097
1098 let coincident = Coincident {
1100 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1101 };
1102
1103 let (final_src_delta, final_scene_delta) = self
1104 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1105 .await?;
1106
1107 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1110 combined_new_objects.extend(final_scene_delta.new_objects);
1111
1112 let scene_graph_delta = SceneGraphDelta {
1113 new_graph: self.scene_graph.clone(),
1114 invalidates_ids: false,
1115 new_objects: combined_new_objects,
1116 exec_outcome: final_scene_delta.exec_outcome,
1117 };
1118
1119 Ok((final_src_delta, scene_graph_delta))
1120 }
1121
1122 async fn edit_constraint(
1123 &mut self,
1124 ctx: &ExecutorContext,
1125 _version: Version,
1126 sketch: ObjectId,
1127 constraint_id: ObjectId,
1128 value_expression: String,
1129 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1130 let sketch_block_ref =
1132 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1133
1134 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1135 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1136 })?;
1137 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1138 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1139 "Object is not a constraint: {constraint_id:?}"
1140 ))));
1141 }
1142
1143 let mut new_ast = self.program.ast.clone();
1144
1145 let (parsed, errors) = Program::parse(&value_expression)
1147 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1148 if !errors.is_empty() {
1149 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1150 "Error parsing value expression: {errors:?}"
1151 ))));
1152 }
1153 let mut parsed = parsed.ok_or_else(|| {
1154 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1155 })?;
1156 if parsed.ast.body.is_empty() {
1157 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1158 "Empty value expression".to_string(),
1159 )));
1160 }
1161 let first = parsed.ast.body.remove(0);
1162 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1163 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1164 "Value expression must be a simple expression".to_string(),
1165 )));
1166 };
1167
1168 let new_value: ast::BinaryPart = expr_stmt
1169 .inner
1170 .expression
1171 .try_into()
1172 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1173
1174 self.mutate_ast(
1175 &mut new_ast,
1176 constraint_id,
1177 AstMutateCommand::EditConstraintValue { value: new_value },
1178 )
1179 .map_err(KclErrorWithOutputs::no_outputs)?;
1180
1181 self.execute_after_edit(
1182 ctx,
1183 sketch,
1184 sketch_block_ref,
1185 Default::default(),
1186 EditDeleteKind::Edit,
1187 &mut new_ast,
1188 )
1189 .await
1190 }
1191
1192 async fn batch_split_segment_operations(
1200 &mut self,
1201 ctx: &ExecutorContext,
1202 _version: Version,
1203 sketch: ObjectId,
1204 edit_segments: Vec<ExistingSegmentCtor>,
1205 add_constraints: Vec<Constraint>,
1206 delete_constraint_ids: Vec<ObjectId>,
1207 _new_segment_info: sketch::NewSegmentInfo,
1208 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1209 let sketch_block_ref =
1211 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1212
1213 let mut new_ast = self.program.ast.clone();
1214 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1215
1216 for segment in edit_segments {
1218 segment_ids_edited.insert(segment.id);
1219 match segment.ctor {
1220 SegmentCtor::Point(ctor) => self
1221 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1222 .map_err(KclErrorWithOutputs::no_outputs)?,
1223 SegmentCtor::Line(ctor) => self
1224 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1225 .map_err(KclErrorWithOutputs::no_outputs)?,
1226 SegmentCtor::Arc(ctor) => self
1227 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1228 .map_err(KclErrorWithOutputs::no_outputs)?,
1229 SegmentCtor::Circle(ctor) => self
1230 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1231 .map_err(KclErrorWithOutputs::no_outputs)?,
1232 }
1233 }
1234
1235 for constraint in add_constraints {
1237 match constraint {
1238 Constraint::Coincident(coincident) => {
1239 self.add_coincident(sketch, coincident, &mut new_ast)
1240 .await
1241 .map_err(KclErrorWithOutputs::no_outputs)?;
1242 }
1243 Constraint::Distance(distance) => {
1244 self.add_distance(sketch, distance, &mut new_ast)
1245 .await
1246 .map_err(KclErrorWithOutputs::no_outputs)?;
1247 }
1248 Constraint::EqualRadius(equal_radius) => {
1249 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1250 .await
1251 .map_err(KclErrorWithOutputs::no_outputs)?;
1252 }
1253 Constraint::Fixed(fixed) => {
1254 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1255 .await
1256 .map_err(KclErrorWithOutputs::no_outputs)?;
1257 }
1258 Constraint::HorizontalDistance(distance) => {
1259 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1260 .await
1261 .map_err(KclErrorWithOutputs::no_outputs)?;
1262 }
1263 Constraint::VerticalDistance(distance) => {
1264 self.add_vertical_distance(sketch, distance, &mut new_ast)
1265 .await
1266 .map_err(KclErrorWithOutputs::no_outputs)?;
1267 }
1268 Constraint::Horizontal(horizontal) => {
1269 self.add_horizontal(sketch, horizontal, &mut new_ast)
1270 .await
1271 .map_err(KclErrorWithOutputs::no_outputs)?;
1272 }
1273 Constraint::LinesEqualLength(lines_equal_length) => {
1274 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1275 .await
1276 .map_err(KclErrorWithOutputs::no_outputs)?;
1277 }
1278 Constraint::Parallel(parallel) => {
1279 self.add_parallel(sketch, parallel, &mut new_ast)
1280 .await
1281 .map_err(KclErrorWithOutputs::no_outputs)?;
1282 }
1283 Constraint::Perpendicular(perpendicular) => {
1284 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1285 .await
1286 .map_err(KclErrorWithOutputs::no_outputs)?;
1287 }
1288 Constraint::Vertical(vertical) => {
1289 self.add_vertical(sketch, vertical, &mut new_ast)
1290 .await
1291 .map_err(KclErrorWithOutputs::no_outputs)?;
1292 }
1293 Constraint::Diameter(diameter) => {
1294 self.add_diameter(sketch, diameter, &mut new_ast)
1295 .await
1296 .map_err(KclErrorWithOutputs::no_outputs)?;
1297 }
1298 Constraint::Radius(radius) => {
1299 self.add_radius(sketch, radius, &mut new_ast)
1300 .await
1301 .map_err(KclErrorWithOutputs::no_outputs)?;
1302 }
1303 Constraint::Angle(angle) => {
1304 self.add_angle(sketch, angle, &mut new_ast)
1305 .await
1306 .map_err(KclErrorWithOutputs::no_outputs)?;
1307 }
1308 Constraint::Tangent(tangent) => {
1309 self.add_tangent(sketch, tangent, &mut new_ast)
1310 .await
1311 .map_err(KclErrorWithOutputs::no_outputs)?;
1312 }
1313 }
1314 }
1315
1316 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1318
1319 let has_constraint_deletions = !constraint_ids_set.is_empty();
1320 for constraint_id in constraint_ids_set {
1321 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1322 .map_err(KclErrorWithOutputs::no_outputs)?;
1323 }
1324
1325 let (source_delta, mut scene_graph_delta) = self
1329 .execute_after_edit(
1330 ctx,
1331 sketch,
1332 sketch_block_ref,
1333 segment_ids_edited,
1334 EditDeleteKind::Edit,
1335 &mut new_ast,
1336 )
1337 .await?;
1338
1339 if has_constraint_deletions {
1342 scene_graph_delta.invalidates_ids = true;
1343 }
1344
1345 Ok((source_delta, scene_graph_delta))
1346 }
1347
1348 async fn batch_tail_cut_operations(
1349 &mut self,
1350 ctx: &ExecutorContext,
1351 _version: Version,
1352 sketch: ObjectId,
1353 edit_segments: Vec<ExistingSegmentCtor>,
1354 add_constraints: Vec<Constraint>,
1355 delete_constraint_ids: Vec<ObjectId>,
1356 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1357 let sketch_block_ref =
1358 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1359
1360 let mut new_ast = self.program.ast.clone();
1361 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1362
1363 for segment in edit_segments {
1365 segment_ids_edited.insert(segment.id);
1366 match segment.ctor {
1367 SegmentCtor::Point(ctor) => self
1368 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1369 .map_err(KclErrorWithOutputs::no_outputs)?,
1370 SegmentCtor::Line(ctor) => self
1371 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1372 .map_err(KclErrorWithOutputs::no_outputs)?,
1373 SegmentCtor::Arc(ctor) => self
1374 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1375 .map_err(KclErrorWithOutputs::no_outputs)?,
1376 SegmentCtor::Circle(ctor) => self
1377 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1378 .map_err(KclErrorWithOutputs::no_outputs)?,
1379 }
1380 }
1381
1382 for constraint in add_constraints {
1384 match constraint {
1385 Constraint::Coincident(coincident) => {
1386 self.add_coincident(sketch, coincident, &mut new_ast)
1387 .await
1388 .map_err(KclErrorWithOutputs::no_outputs)?;
1389 }
1390 other => {
1391 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1392 "unsupported constraint in tail cut batch: {other:?}"
1393 ))));
1394 }
1395 }
1396 }
1397
1398 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1400
1401 let has_constraint_deletions = !constraint_ids_set.is_empty();
1402 for constraint_id in constraint_ids_set {
1403 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1404 .map_err(KclErrorWithOutputs::no_outputs)?;
1405 }
1406
1407 let (source_delta, mut scene_graph_delta) = self
1411 .execute_after_edit(
1412 ctx,
1413 sketch,
1414 sketch_block_ref,
1415 segment_ids_edited,
1416 EditDeleteKind::Edit,
1417 &mut new_ast,
1418 )
1419 .await?;
1420
1421 if has_constraint_deletions {
1424 scene_graph_delta.invalidates_ids = true;
1425 }
1426
1427 Ok((source_delta, scene_graph_delta))
1428 }
1429}
1430
1431impl FrontendState {
1432 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1433 self.program = program.clone();
1434
1435 self.point_freedom_cache.clear();
1446 match ctx.run_with_caching(program).await {
1447 Ok(outcome) => {
1448 let outcome = self.update_state_after_exec(outcome, true);
1449 let checkpoint_id = self
1450 .create_sketch_checkpoint(outcome.clone())
1451 .await
1452 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1453 Ok(SetProgramOutcome::Success {
1454 scene_graph: Box::new(self.scene_graph.clone()),
1455 exec_outcome: Box::new(outcome),
1456 checkpoint_id: Some(checkpoint_id),
1457 })
1458 }
1459 Err(mut err) => {
1460 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1463 self.update_state_after_exec(outcome, true);
1464 err.scene_graph = Some(self.scene_graph.clone());
1465 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1466 }
1467 }
1468 }
1469
1470 pub async fn engine_execute(
1473 &mut self,
1474 ctx: &ExecutorContext,
1475 program: Program,
1476 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1477 self.program = program.clone();
1478
1479 self.point_freedom_cache.clear();
1483 match ctx.run_with_caching(program).await {
1484 Ok(outcome) => {
1485 let outcome = self.update_state_after_exec(outcome, true);
1486 Ok(SceneGraphDelta {
1487 new_graph: self.scene_graph.clone(),
1488 exec_outcome: outcome,
1489 new_objects: Default::default(),
1491 invalidates_ids: Default::default(),
1493 })
1494 }
1495 Err(mut err) => {
1496 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1498 self.update_state_after_exec(outcome, true);
1499 err.scene_graph = Some(self.scene_graph.clone());
1500 Err(err)
1501 }
1502 }
1503 }
1504
1505 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1506 if matches!(err.error, KclError::EngineHangup { .. }) {
1507 return Err(err);
1511 }
1512
1513 let KclErrorWithOutputs {
1514 error,
1515 mut non_fatal,
1516 variables,
1517 #[cfg(feature = "artifact-graph")]
1518 operations,
1519 #[cfg(feature = "artifact-graph")]
1520 artifact_graph,
1521 #[cfg(feature = "artifact-graph")]
1522 scene_objects,
1523 #[cfg(feature = "artifact-graph")]
1524 source_range_to_object,
1525 #[cfg(feature = "artifact-graph")]
1526 var_solutions,
1527 filenames,
1528 default_planes,
1529 ..
1530 } = err;
1531
1532 if let Some(source_range) = error.source_ranges().first() {
1533 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1534 } else {
1535 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1536 }
1537
1538 Ok(ExecOutcome {
1539 variables,
1540 filenames,
1541 #[cfg(feature = "artifact-graph")]
1542 operations,
1543 #[cfg(feature = "artifact-graph")]
1544 artifact_graph,
1545 #[cfg(feature = "artifact-graph")]
1546 scene_objects,
1547 #[cfg(feature = "artifact-graph")]
1548 source_range_to_object,
1549 #[cfg(feature = "artifact-graph")]
1550 var_solutions,
1551 issues: non_fatal,
1552 default_planes,
1553 })
1554 }
1555
1556 async fn add_point(
1557 &mut self,
1558 ctx: &ExecutorContext,
1559 sketch: ObjectId,
1560 ctor: PointCtor,
1561 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1562 let at_ast = to_ast_point2d(&ctor.position)
1564 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1565 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1566 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1567 unlabeled: None,
1568 arguments: vec![ast::LabeledArg {
1569 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1570 arg: at_ast,
1571 }],
1572 digest: None,
1573 non_code_meta: Default::default(),
1574 })));
1575
1576 let sketch_id = sketch;
1578 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1579 #[cfg(target_arch = "wasm32")]
1580 web_sys::console::error_1(
1581 &format!(
1582 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1583 &self.scene_graph.objects
1584 )
1585 .into(),
1586 );
1587 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1588 })?;
1589 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1590 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1591 "Object is not a sketch, it is {}",
1592 sketch_object.kind.human_friendly_kind_with_article(),
1593 ))));
1594 };
1595 let mut new_ast = self.program.ast.clone();
1597 let (sketch_block_ref, _) = self
1598 .mutate_ast(
1599 &mut new_ast,
1600 sketch_id,
1601 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1602 )
1603 .map_err(KclErrorWithOutputs::no_outputs)?;
1604 let new_source = source_from_ast(&new_ast);
1606 let (new_program, errors) = Program::parse(&new_source)
1608 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1609 if !errors.is_empty() {
1610 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1611 "Error parsing KCL source after adding point: {errors:?}"
1612 ))));
1613 }
1614 let Some(new_program) = new_program else {
1615 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1616 "No AST produced after adding point".to_string(),
1617 )));
1618 };
1619
1620 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1621 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1622 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1623 )))
1624 })?;
1625 #[cfg(not(feature = "artifact-graph"))]
1626 let _ = point_node_ref;
1627
1628 self.program = new_program.clone();
1630
1631 let mut truncated_program = new_program;
1633 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1634 .map_err(KclErrorWithOutputs::no_outputs)?;
1635
1636 let outcome = ctx
1638 .run_mock(
1639 &truncated_program,
1640 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1641 )
1642 .await?;
1643
1644 #[cfg(not(feature = "artifact-graph"))]
1645 let new_object_ids = Vec::new();
1646 #[cfg(feature = "artifact-graph")]
1647 let new_object_ids = {
1648 let make_err =
1649 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1650 let segment_id = outcome
1651 .source_range_to_object
1652 .get(&point_node_ref.range)
1653 .copied()
1654 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1655 let segment_object = outcome
1656 .scene_objects
1657 .get(segment_id.0)
1658 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1659 let ObjectKind::Segment { segment } = &segment_object.kind else {
1660 return Err(make_err(format!(
1661 "Object is not a segment, it is {}",
1662 segment_object.kind.human_friendly_kind_with_article()
1663 )));
1664 };
1665 let Segment::Point(_) = segment else {
1666 return Err(make_err(format!(
1667 "Segment is not a point, it is {}",
1668 segment.human_friendly_kind_with_article()
1669 )));
1670 };
1671 vec![segment_id]
1672 };
1673 let src_delta = SourceDelta { text: new_source };
1674 let outcome = self.update_state_after_exec(outcome, false);
1676 let scene_graph_delta = SceneGraphDelta {
1677 new_graph: self.scene_graph.clone(),
1678 invalidates_ids: false,
1679 new_objects: new_object_ids,
1680 exec_outcome: outcome,
1681 };
1682 Ok((src_delta, scene_graph_delta))
1683 }
1684
1685 async fn add_line(
1686 &mut self,
1687 ctx: &ExecutorContext,
1688 sketch: ObjectId,
1689 ctor: LineCtor,
1690 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1691 let start_ast = to_ast_point2d(&ctor.start)
1693 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1694 let end_ast = to_ast_point2d(&ctor.end)
1695 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1696 let mut arguments = vec![
1697 ast::LabeledArg {
1698 label: Some(ast::Identifier::new(LINE_START_PARAM)),
1699 arg: start_ast,
1700 },
1701 ast::LabeledArg {
1702 label: Some(ast::Identifier::new(LINE_END_PARAM)),
1703 arg: end_ast,
1704 },
1705 ];
1706 if ctor.construction == Some(true) {
1708 arguments.push(ast::LabeledArg {
1709 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1710 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1711 value: ast::LiteralValue::Bool(true),
1712 raw: "true".to_string(),
1713 digest: None,
1714 }))),
1715 });
1716 }
1717 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1718 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1719 unlabeled: None,
1720 arguments,
1721 digest: None,
1722 non_code_meta: Default::default(),
1723 })));
1724
1725 let sketch_id = sketch;
1727 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1728 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1729 })?;
1730 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1731 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1732 "Object is not a sketch, it is {}",
1733 sketch_object.kind.human_friendly_kind_with_article(),
1734 ))));
1735 };
1736 let mut new_ast = self.program.ast.clone();
1738 let (sketch_block_ref, _) = self
1739 .mutate_ast(
1740 &mut new_ast,
1741 sketch_id,
1742 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1743 )
1744 .map_err(KclErrorWithOutputs::no_outputs)?;
1745 let new_source = source_from_ast(&new_ast);
1747 let (new_program, errors) = Program::parse(&new_source)
1749 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1750 if !errors.is_empty() {
1751 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1752 "Error parsing KCL source after adding line: {errors:?}"
1753 ))));
1754 }
1755 let Some(new_program) = new_program else {
1756 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1757 "No AST produced after adding line".to_string(),
1758 )));
1759 };
1760
1761 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1762 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1763 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1764 )))
1765 })?;
1766 #[cfg(not(feature = "artifact-graph"))]
1767 let _ = line_node_ref;
1768
1769 self.program = new_program.clone();
1771
1772 let mut truncated_program = new_program;
1774 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1775 .map_err(KclErrorWithOutputs::no_outputs)?;
1776
1777 let outcome = ctx
1779 .run_mock(
1780 &truncated_program,
1781 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1782 )
1783 .await?;
1784
1785 #[cfg(not(feature = "artifact-graph"))]
1786 let new_object_ids = Vec::new();
1787 #[cfg(feature = "artifact-graph")]
1788 let new_object_ids = {
1789 let make_err =
1790 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1791 let segment_id = outcome
1792 .source_range_to_object
1793 .get(&line_node_ref.range)
1794 .copied()
1795 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1796 let segment_object = outcome
1797 .scene_object_by_id(segment_id)
1798 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1799 let ObjectKind::Segment { segment } = &segment_object.kind else {
1800 return Err(make_err(format!(
1801 "Object is not a segment, it is {}",
1802 segment_object.kind.human_friendly_kind_with_article()
1803 )));
1804 };
1805 let Segment::Line(line) = segment else {
1806 return Err(make_err(format!(
1807 "Segment is not a line, it is {}",
1808 segment.human_friendly_kind_with_article()
1809 )));
1810 };
1811 vec![line.start, line.end, segment_id]
1812 };
1813 let src_delta = SourceDelta { text: new_source };
1814 let outcome = self.update_state_after_exec(outcome, false);
1816 let scene_graph_delta = SceneGraphDelta {
1817 new_graph: self.scene_graph.clone(),
1818 invalidates_ids: false,
1819 new_objects: new_object_ids,
1820 exec_outcome: outcome,
1821 };
1822 Ok((src_delta, scene_graph_delta))
1823 }
1824
1825 async fn add_arc(
1826 &mut self,
1827 ctx: &ExecutorContext,
1828 sketch: ObjectId,
1829 ctor: ArcCtor,
1830 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1831 let start_ast = to_ast_point2d(&ctor.start)
1833 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1834 let end_ast = to_ast_point2d(&ctor.end)
1835 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1836 let center_ast = to_ast_point2d(&ctor.center)
1837 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1838 let mut arguments = vec![
1839 ast::LabeledArg {
1840 label: Some(ast::Identifier::new(ARC_START_PARAM)),
1841 arg: start_ast,
1842 },
1843 ast::LabeledArg {
1844 label: Some(ast::Identifier::new(ARC_END_PARAM)),
1845 arg: end_ast,
1846 },
1847 ast::LabeledArg {
1848 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1849 arg: center_ast,
1850 },
1851 ];
1852 if ctor.construction == Some(true) {
1854 arguments.push(ast::LabeledArg {
1855 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1856 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1857 value: ast::LiteralValue::Bool(true),
1858 raw: "true".to_string(),
1859 digest: None,
1860 }))),
1861 });
1862 }
1863 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1864 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1865 unlabeled: None,
1866 arguments,
1867 digest: None,
1868 non_code_meta: Default::default(),
1869 })));
1870
1871 let sketch_id = sketch;
1873 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1874 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1875 })?;
1876 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1877 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1878 "Object is not a sketch, it is {}",
1879 sketch_object.kind.human_friendly_kind_with_article(),
1880 ))));
1881 };
1882 let mut new_ast = self.program.ast.clone();
1884 let (sketch_block_ref, _) = self
1885 .mutate_ast(
1886 &mut new_ast,
1887 sketch_id,
1888 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1889 )
1890 .map_err(KclErrorWithOutputs::no_outputs)?;
1891 let new_source = source_from_ast(&new_ast);
1893 let (new_program, errors) = Program::parse(&new_source)
1895 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1896 if !errors.is_empty() {
1897 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1898 "Error parsing KCL source after adding arc: {errors:?}"
1899 ))));
1900 }
1901 let Some(new_program) = new_program else {
1902 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1903 "No AST produced after adding arc".to_string(),
1904 )));
1905 };
1906
1907 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1908 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1909 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
1910 )))
1911 })?;
1912 #[cfg(not(feature = "artifact-graph"))]
1913 let _ = arc_node_ref;
1914
1915 self.program = new_program.clone();
1917
1918 let mut truncated_program = new_program;
1920 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1921 .map_err(KclErrorWithOutputs::no_outputs)?;
1922
1923 let outcome = ctx
1925 .run_mock(
1926 &truncated_program,
1927 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1928 )
1929 .await?;
1930
1931 #[cfg(not(feature = "artifact-graph"))]
1932 let new_object_ids = Vec::new();
1933 #[cfg(feature = "artifact-graph")]
1934 let new_object_ids = {
1935 let make_err =
1936 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1937 let segment_id = outcome
1938 .source_range_to_object
1939 .get(&arc_node_ref.range)
1940 .copied()
1941 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
1942 let segment_object = outcome
1943 .scene_objects
1944 .get(segment_id.0)
1945 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1946 let ObjectKind::Segment { segment } = &segment_object.kind else {
1947 return Err(make_err(format!(
1948 "Object is not a segment, it is {}",
1949 segment_object.kind.human_friendly_kind_with_article()
1950 )));
1951 };
1952 let Segment::Arc(arc) = segment else {
1953 return Err(make_err(format!(
1954 "Segment is not an arc, it is {}",
1955 segment.human_friendly_kind_with_article()
1956 )));
1957 };
1958 vec![arc.start, arc.end, arc.center, segment_id]
1959 };
1960 let src_delta = SourceDelta { text: new_source };
1961 let outcome = self.update_state_after_exec(outcome, false);
1963 let scene_graph_delta = SceneGraphDelta {
1964 new_graph: self.scene_graph.clone(),
1965 invalidates_ids: false,
1966 new_objects: new_object_ids,
1967 exec_outcome: outcome,
1968 };
1969 Ok((src_delta, scene_graph_delta))
1970 }
1971
1972 async fn add_circle(
1973 &mut self,
1974 ctx: &ExecutorContext,
1975 sketch: ObjectId,
1976 ctor: CircleCtor,
1977 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1978 let start_ast = to_ast_point2d(&ctor.start)
1980 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1981 let center_ast = to_ast_point2d(&ctor.center)
1982 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1983 let mut arguments = vec![
1984 ast::LabeledArg {
1985 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
1986 arg: start_ast,
1987 },
1988 ast::LabeledArg {
1989 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
1990 arg: center_ast,
1991 },
1992 ];
1993 if ctor.construction == Some(true) {
1995 arguments.push(ast::LabeledArg {
1996 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1997 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1998 value: ast::LiteralValue::Bool(true),
1999 raw: "true".to_string(),
2000 digest: None,
2001 }))),
2002 });
2003 }
2004 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2005 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2006 unlabeled: None,
2007 arguments,
2008 digest: None,
2009 non_code_meta: Default::default(),
2010 })));
2011
2012 let sketch_id = sketch;
2014 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2015 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2016 })?;
2017 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2018 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2019 "Object is not a sketch, it is {}",
2020 sketch_object.kind.human_friendly_kind_with_article(),
2021 ))));
2022 };
2023 let mut new_ast = self.program.ast.clone();
2025 let (sketch_block_ref, _) = self
2026 .mutate_ast(
2027 &mut new_ast,
2028 sketch_id,
2029 AstMutateCommand::AddSketchBlockVarDecl {
2030 prefix: CIRCLE_VARIABLE.to_owned(),
2031 expr: circle_ast,
2032 },
2033 )
2034 .map_err(KclErrorWithOutputs::no_outputs)?;
2035 let new_source = source_from_ast(&new_ast);
2037 let (new_program, errors) = Program::parse(&new_source)
2039 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2040 if !errors.is_empty() {
2041 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2042 "Error parsing KCL source after adding circle: {errors:?}"
2043 ))));
2044 }
2045 let Some(new_program) = new_program else {
2046 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2047 "No AST produced after adding circle".to_string(),
2048 )));
2049 };
2050
2051 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2052 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2053 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2054 )))
2055 })?;
2056 #[cfg(not(feature = "artifact-graph"))]
2057 let _ = circle_node_ref;
2058
2059 self.program = new_program.clone();
2061
2062 let mut truncated_program = new_program;
2064 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2065 .map_err(KclErrorWithOutputs::no_outputs)?;
2066
2067 let outcome = ctx
2069 .run_mock(
2070 &truncated_program,
2071 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2072 )
2073 .await?;
2074
2075 #[cfg(not(feature = "artifact-graph"))]
2076 let new_object_ids = Vec::new();
2077 #[cfg(feature = "artifact-graph")]
2078 let new_object_ids = {
2079 let make_err =
2080 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2081 let segment_id = outcome
2082 .source_range_to_object
2083 .get(&circle_node_ref.range)
2084 .copied()
2085 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2086 let segment_object = outcome
2087 .scene_objects
2088 .get(segment_id.0)
2089 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2090 let ObjectKind::Segment { segment } = &segment_object.kind else {
2091 return Err(make_err(format!(
2092 "Object is not a segment, it is {}",
2093 segment_object.kind.human_friendly_kind_with_article()
2094 )));
2095 };
2096 let Segment::Circle(circle) = segment else {
2097 return Err(make_err(format!(
2098 "Segment is not a circle, it is {}",
2099 segment.human_friendly_kind_with_article()
2100 )));
2101 };
2102 vec![circle.start, circle.center, segment_id]
2103 };
2104 let src_delta = SourceDelta { text: new_source };
2105 let outcome = self.update_state_after_exec(outcome, false);
2107 let scene_graph_delta = SceneGraphDelta {
2108 new_graph: self.scene_graph.clone(),
2109 invalidates_ids: false,
2110 new_objects: new_object_ids,
2111 exec_outcome: outcome,
2112 };
2113 Ok((src_delta, scene_graph_delta))
2114 }
2115
2116 fn edit_point(
2117 &mut self,
2118 new_ast: &mut ast::Node<ast::Program>,
2119 sketch: ObjectId,
2120 point: ObjectId,
2121 ctor: PointCtor,
2122 ) -> Result<(), KclError> {
2123 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2125
2126 let sketch_id = sketch;
2128 let sketch_object = self
2129 .scene_graph
2130 .objects
2131 .get(sketch_id.0)
2132 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2133 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2134 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2135 };
2136 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2137 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2138 })?;
2139 let point_id = point;
2141 let point_object = self
2142 .scene_graph
2143 .objects
2144 .get(point_id.0)
2145 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2146 let ObjectKind::Segment {
2147 segment: Segment::Point(point),
2148 } = &point_object.kind
2149 else {
2150 return Err(KclError::refactor(format!(
2151 "Object is not a point segment: {point_object:?}"
2152 )));
2153 };
2154
2155 if let Some(owner_id) = point.owner {
2157 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2158 KclError::refactor(format!(
2159 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2160 ))
2161 })?;
2162 let ObjectKind::Segment { segment } = &owner_object.kind else {
2163 return Err(KclError::refactor(format!(
2164 "Internal: Owner of point is not a segment, but found {}",
2165 owner_object.kind.human_friendly_kind_with_article()
2166 )));
2167 };
2168
2169 if let Segment::Line(line) = segment {
2171 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2172 return Err(KclError::refactor(format!(
2173 "Internal: Owner of point does not have line ctor, but found {}",
2174 line.ctor.human_friendly_kind_with_article()
2175 )));
2176 };
2177 let mut line_ctor = line_ctor.clone();
2178 if line.start == point_id {
2180 line_ctor.start = ctor.position;
2181 } else if line.end == point_id {
2182 line_ctor.end = ctor.position;
2183 } else {
2184 return Err(KclError::refactor(format!(
2185 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2186 )));
2187 }
2188 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2189 }
2190
2191 if let Segment::Arc(arc) = segment {
2193 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2194 return Err(KclError::refactor(format!(
2195 "Internal: Owner of point does not have arc ctor, but found {}",
2196 arc.ctor.human_friendly_kind_with_article()
2197 )));
2198 };
2199 let mut arc_ctor = arc_ctor.clone();
2200 if arc.center == point_id {
2202 arc_ctor.center = ctor.position;
2203 } else if arc.start == point_id {
2204 arc_ctor.start = ctor.position;
2205 } else if arc.end == point_id {
2206 arc_ctor.end = ctor.position;
2207 } else {
2208 return Err(KclError::refactor(format!(
2209 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2210 )));
2211 }
2212 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2213 }
2214
2215 if let Segment::Circle(circle) = segment {
2217 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2218 return Err(KclError::refactor(format!(
2219 "Internal: Owner of point does not have circle ctor, but found {}",
2220 circle.ctor.human_friendly_kind_with_article()
2221 )));
2222 };
2223 let mut circle_ctor = circle_ctor.clone();
2224 if circle.center == point_id {
2225 circle_ctor.center = ctor.position;
2226 } else if circle.start == point_id {
2227 circle_ctor.start = ctor.position;
2228 } else {
2229 return Err(KclError::refactor(format!(
2230 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2231 )));
2232 }
2233 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2234 }
2235
2236 }
2239
2240 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2242 Ok(())
2243 }
2244
2245 fn edit_line(
2246 &mut self,
2247 new_ast: &mut ast::Node<ast::Program>,
2248 sketch: ObjectId,
2249 line: ObjectId,
2250 ctor: LineCtor,
2251 ) -> Result<(), KclError> {
2252 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2254 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2255
2256 let sketch_id = sketch;
2258 let sketch_object = self
2259 .scene_graph
2260 .objects
2261 .get(sketch_id.0)
2262 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2263 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2264 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2265 };
2266 sketch
2267 .segments
2268 .iter()
2269 .find(|o| **o == line)
2270 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2271 let line_id = line;
2273 let line_object = self
2274 .scene_graph
2275 .objects
2276 .get(line_id.0)
2277 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2278 let ObjectKind::Segment { .. } = &line_object.kind else {
2279 let kind = line_object.kind.human_friendly_kind_with_article();
2280 return Err(KclError::refactor(format!(
2281 "This constraint only works on Segments, but you selected {kind}"
2282 )));
2283 };
2284
2285 self.mutate_ast(
2287 new_ast,
2288 line_id,
2289 AstMutateCommand::EditLine {
2290 start: new_start_ast,
2291 end: new_end_ast,
2292 construction: ctor.construction,
2293 },
2294 )?;
2295 Ok(())
2296 }
2297
2298 fn edit_arc(
2299 &mut self,
2300 new_ast: &mut ast::Node<ast::Program>,
2301 sketch: ObjectId,
2302 arc: ObjectId,
2303 ctor: ArcCtor,
2304 ) -> Result<(), KclError> {
2305 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2307 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2308 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2309
2310 let sketch_id = sketch;
2312 let sketch_object = self
2313 .scene_graph
2314 .objects
2315 .get(sketch_id.0)
2316 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2317 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2318 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2319 };
2320 sketch
2321 .segments
2322 .iter()
2323 .find(|o| **o == arc)
2324 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2325 let arc_id = arc;
2327 let arc_object = self
2328 .scene_graph
2329 .objects
2330 .get(arc_id.0)
2331 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2332 let ObjectKind::Segment { .. } = &arc_object.kind else {
2333 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2334 };
2335
2336 self.mutate_ast(
2338 new_ast,
2339 arc_id,
2340 AstMutateCommand::EditArc {
2341 start: new_start_ast,
2342 end: new_end_ast,
2343 center: new_center_ast,
2344 construction: ctor.construction,
2345 },
2346 )?;
2347 Ok(())
2348 }
2349
2350 fn edit_circle(
2351 &mut self,
2352 new_ast: &mut ast::Node<ast::Program>,
2353 sketch: ObjectId,
2354 circle: ObjectId,
2355 ctor: CircleCtor,
2356 ) -> Result<(), KclError> {
2357 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2359 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2360
2361 let sketch_id = sketch;
2363 let sketch_object = self
2364 .scene_graph
2365 .objects
2366 .get(sketch_id.0)
2367 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2368 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2369 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2370 };
2371 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2372 KclError::refactor(format!(
2373 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2374 ))
2375 })?;
2376 let circle_id = circle;
2378 let circle_object = self
2379 .scene_graph
2380 .objects
2381 .get(circle_id.0)
2382 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2383 let ObjectKind::Segment { .. } = &circle_object.kind else {
2384 return Err(KclError::refactor(format!(
2385 "Object is not a segment: {circle_object:?}"
2386 )));
2387 };
2388
2389 self.mutate_ast(
2391 new_ast,
2392 circle_id,
2393 AstMutateCommand::EditCircle {
2394 start: new_start_ast,
2395 center: new_center_ast,
2396 construction: ctor.construction,
2397 },
2398 )?;
2399 Ok(())
2400 }
2401
2402 fn delete_segment(
2403 &mut self,
2404 new_ast: &mut ast::Node<ast::Program>,
2405 sketch: ObjectId,
2406 segment_id: ObjectId,
2407 ) -> Result<(), KclError> {
2408 let sketch_id = sketch;
2410 let sketch_object = self
2411 .scene_graph
2412 .objects
2413 .get(sketch_id.0)
2414 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2415 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2416 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2417 };
2418 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2419 KclError::refactor(format!(
2420 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2421 ))
2422 })?;
2423 let segment_object =
2425 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2426 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2427 })?;
2428 let ObjectKind::Segment { .. } = &segment_object.kind else {
2429 return Err(KclError::refactor(format!(
2430 "Object is not a segment, it is {}",
2431 segment_object.kind.human_friendly_kind_with_article()
2432 )));
2433 };
2434
2435 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2437 Ok(())
2438 }
2439
2440 fn delete_constraint(
2441 &mut self,
2442 new_ast: &mut ast::Node<ast::Program>,
2443 sketch: ObjectId,
2444 constraint_id: ObjectId,
2445 ) -> Result<(), KclError> {
2446 let sketch_id = sketch;
2448 let sketch_object = self
2449 .scene_graph
2450 .objects
2451 .get(sketch_id.0)
2452 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2453 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2454 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2455 };
2456 sketch
2457 .constraints
2458 .iter()
2459 .find(|o| **o == constraint_id)
2460 .ok_or_else(|| {
2461 KclError::refactor(format!(
2462 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2463 ))
2464 })?;
2465 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2467 KclError::refactor(format!(
2468 "Constraint not found in scene graph: constraint={constraint_id:?}"
2469 ))
2470 })?;
2471 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2472 return Err(KclError::refactor(format!(
2473 "Object is not a constraint, it is {}",
2474 constraint_object.kind.human_friendly_kind_with_article()
2475 )));
2476 };
2477
2478 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2480 Ok(())
2481 }
2482
2483 fn edit_coincident_constraint(
2484 &mut self,
2485 new_ast: &mut ast::Node<ast::Program>,
2486 constraint_id: ObjectId,
2487 segments: Vec<ConstraintSegment>,
2488 ) -> Result<(), KclError> {
2489 if segments.len() < 2 {
2490 return Err(KclError::refactor(format!(
2491 "Coincident constraint must have at least 2 inputs, got {}",
2492 segments.len()
2493 )));
2494 }
2495
2496 let segment_asts = segments
2497 .iter()
2498 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2499 .collect::<Result<Vec<_>, _>>()?;
2500
2501 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2502 elements: segment_asts,
2503 digest: None,
2504 non_code_meta: Default::default(),
2505 })));
2506
2507 self.mutate_ast(
2508 new_ast,
2509 constraint_id,
2510 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2511 )?;
2512 Ok(())
2513 }
2514
2515 fn edit_equal_length_constraint(
2517 &mut self,
2518 new_ast: &mut ast::Node<ast::Program>,
2519 constraint_id: ObjectId,
2520 lines: Vec<ObjectId>,
2521 ) -> Result<(), KclError> {
2522 if lines.len() < 2 {
2523 return Err(KclError::refactor(format!(
2524 "Lines equal length constraint must have at least 2 lines, got {}",
2525 lines.len()
2526 )));
2527 }
2528
2529 let line_asts = lines
2530 .iter()
2531 .map(|line_id| {
2532 let line_object = self
2533 .scene_graph
2534 .objects
2535 .get(line_id.0)
2536 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2537 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2538 let kind = line_object.kind.human_friendly_kind_with_article();
2539 return Err(KclError::refactor(format!(
2540 "This constraint only works on Segments, but you selected {kind}"
2541 )));
2542 };
2543 let Segment::Line(_) = line_segment else {
2544 let kind = line_segment.human_friendly_kind_with_article();
2545 return Err(KclError::refactor(format!(
2546 "Only lines can be made equal length, but you selected {kind}"
2547 )));
2548 };
2549
2550 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
2551 })
2552 .collect::<Result<Vec<_>, _>>()?;
2553
2554 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2555 elements: line_asts,
2556 digest: None,
2557 non_code_meta: Default::default(),
2558 })));
2559
2560 self.mutate_ast(
2561 new_ast,
2562 constraint_id,
2563 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2564 )?;
2565 Ok(())
2566 }
2567
2568 fn edit_parallel_constraint(
2570 &mut self,
2571 new_ast: &mut ast::Node<ast::Program>,
2572 constraint_id: ObjectId,
2573 lines: Vec<ObjectId>,
2574 ) -> Result<(), KclError> {
2575 if lines.len() < 2 {
2576 return Err(KclError::refactor(format!(
2577 "Parallel constraint must have at least 2 lines, got {}",
2578 lines.len()
2579 )));
2580 }
2581
2582 let line_asts = lines
2583 .iter()
2584 .map(|line_id| {
2585 let line_object = self
2586 .scene_graph
2587 .objects
2588 .get(line_id.0)
2589 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2590 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2591 let kind = line_object.kind.human_friendly_kind_with_article();
2592 return Err(KclError::refactor(format!(
2593 "This constraint only works on Segments, but you selected {kind}"
2594 )));
2595 };
2596 let Segment::Line(_) = line_segment else {
2597 let kind = line_segment.human_friendly_kind_with_article();
2598 return Err(KclError::refactor(format!(
2599 "Only lines can be made parallel, but you selected {kind}"
2600 )));
2601 };
2602
2603 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
2604 })
2605 .collect::<Result<Vec<_>, _>>()?;
2606
2607 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2608 elements: line_asts,
2609 digest: None,
2610 non_code_meta: Default::default(),
2611 })));
2612
2613 self.mutate_ast(
2614 new_ast,
2615 constraint_id,
2616 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2617 )?;
2618 Ok(())
2619 }
2620
2621 fn edit_equal_radius_constraint(
2623 &mut self,
2624 new_ast: &mut ast::Node<ast::Program>,
2625 constraint_id: ObjectId,
2626 input: Vec<ObjectId>,
2627 ) -> Result<(), KclError> {
2628 if input.len() < 2 {
2629 return Err(KclError::refactor(format!(
2630 "equalRadius constraint must have at least 2 segments, got {}",
2631 input.len()
2632 )));
2633 }
2634
2635 let input_asts = input
2636 .iter()
2637 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2638 .collect::<Result<Vec<_>, _>>()?;
2639
2640 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2641 elements: input_asts,
2642 digest: None,
2643 non_code_meta: Default::default(),
2644 })));
2645
2646 self.mutate_ast(
2647 new_ast,
2648 constraint_id,
2649 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2650 )?;
2651 Ok(())
2652 }
2653
2654 async fn execute_after_edit(
2655 &mut self,
2656 ctx: &ExecutorContext,
2657 sketch: ObjectId,
2658 sketch_block_ref: AstNodeRef,
2659 segment_ids_edited: AhashIndexSet<ObjectId>,
2660 edit_kind: EditDeleteKind,
2661 new_ast: &mut ast::Node<ast::Program>,
2662 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2663 let new_source = source_from_ast(new_ast);
2665 let (new_program, errors) = Program::parse(&new_source)
2667 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2668 if !errors.is_empty() {
2669 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2670 "Error parsing KCL source after editing: {errors:?}"
2671 ))));
2672 }
2673 let Some(new_program) = new_program else {
2674 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2675 "No AST produced after editing".to_string(),
2676 )));
2677 };
2678
2679 self.program = new_program.clone();
2681
2682 let is_delete = edit_kind.is_delete();
2684 let truncated_program = {
2685 let mut truncated_program = new_program;
2686 only_sketch_block(
2687 &mut truncated_program.ast,
2688 &sketch_block_ref,
2689 edit_kind.to_change_kind(),
2690 )
2691 .map_err(KclErrorWithOutputs::no_outputs)?;
2692 truncated_program
2693 };
2694
2695 #[cfg(not(feature = "artifact-graph"))]
2696 drop(segment_ids_edited);
2697
2698 let mock_config = MockConfig {
2700 sketch_block_id: Some(sketch),
2701 freedom_analysis: is_delete,
2702 #[cfg(feature = "artifact-graph")]
2703 segment_ids_edited: segment_ids_edited.clone(),
2704 ..Default::default()
2705 };
2706 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2707
2708 let outcome = self.update_state_after_exec(outcome, is_delete);
2710
2711 #[cfg(feature = "artifact-graph")]
2712 let new_source = {
2713 let mut new_ast = self.program.ast.clone();
2718 for (var_range, value) in &outcome.var_solutions {
2719 let rounded = value.round(3);
2720 mutate_ast_node_by_source_range(
2721 &mut new_ast,
2722 *var_range,
2723 AstMutateCommand::EditVarInitialValue { value: rounded },
2724 )
2725 .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2726 }
2727 source_from_ast(&new_ast)
2728 };
2729
2730 let src_delta = SourceDelta { text: new_source };
2731 let scene_graph_delta = SceneGraphDelta {
2732 new_graph: self.scene_graph.clone(),
2733 invalidates_ids: is_delete,
2734 new_objects: Vec::new(),
2735 exec_outcome: outcome,
2736 };
2737 Ok((src_delta, scene_graph_delta))
2738 }
2739
2740 async fn execute_after_delete_sketch(
2741 &mut self,
2742 ctx: &ExecutorContext,
2743 new_ast: &mut ast::Node<ast::Program>,
2744 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2745 let new_source = source_from_ast(new_ast);
2747 let (new_program, errors) = Program::parse(&new_source)
2749 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2750 if !errors.is_empty() {
2751 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2752 "Error parsing KCL source after editing: {errors:?}"
2753 ))));
2754 }
2755 let Some(new_program) = new_program else {
2756 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2757 "No AST produced after editing".to_string(),
2758 )));
2759 };
2760
2761 self.program = new_program.clone();
2763
2764 let outcome = ctx.run_with_caching(new_program).await?;
2770 let freedom_analysis_ran = true;
2771
2772 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2773
2774 let src_delta = SourceDelta { text: new_source };
2775 let scene_graph_delta = SceneGraphDelta {
2776 new_graph: self.scene_graph.clone(),
2777 invalidates_ids: true,
2778 new_objects: Vec::new(),
2779 exec_outcome: outcome,
2780 };
2781 Ok((src_delta, scene_graph_delta))
2782 }
2783
2784 fn point_id_to_ast_reference(
2789 &self,
2790 point_id: ObjectId,
2791 new_ast: &mut ast::Node<ast::Program>,
2792 ) -> Result<ast::Expr, KclError> {
2793 let point_object = self
2794 .scene_graph
2795 .objects
2796 .get(point_id.0)
2797 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2798 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2799 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2800 };
2801 let Segment::Point(point) = point_segment else {
2802 return Err(KclError::refactor(format!(
2803 "Only points are currently supported: {point_object:?}"
2804 )));
2805 };
2806
2807 if let Some(owner_id) = point.owner {
2808 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2809 KclError::refactor(format!(
2810 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2811 ))
2812 })?;
2813 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2814 return Err(KclError::refactor(format!(
2815 "Owner of point is not a segment, but found {}",
2816 owner_object.kind.human_friendly_kind_with_article()
2817 )));
2818 };
2819
2820 match owner_segment {
2821 Segment::Line(line) => {
2822 let property = if line.start == point_id {
2823 LINE_PROPERTY_START
2824 } else if line.end == point_id {
2825 LINE_PROPERTY_END
2826 } else {
2827 return Err(KclError::refactor(format!(
2828 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2829 )));
2830 };
2831 get_or_insert_ast_reference(new_ast, &owner_object.source, "line", Some(property))
2832 }
2833 Segment::Arc(arc) => {
2834 let property = if arc.start == point_id {
2835 ARC_PROPERTY_START
2836 } else if arc.end == point_id {
2837 ARC_PROPERTY_END
2838 } else if arc.center == point_id {
2839 ARC_PROPERTY_CENTER
2840 } else {
2841 return Err(KclError::refactor(format!(
2842 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2843 )));
2844 };
2845 get_or_insert_ast_reference(new_ast, &owner_object.source, "arc", Some(property))
2846 }
2847 Segment::Circle(circle) => {
2848 let property = if circle.start == point_id {
2849 CIRCLE_PROPERTY_START
2850 } else if circle.center == point_id {
2851 CIRCLE_PROPERTY_CENTER
2852 } else {
2853 return Err(KclError::refactor(format!(
2854 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2855 )));
2856 };
2857 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
2858 }
2859 _ => Err(KclError::refactor(format!(
2860 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
2861 ))),
2862 }
2863 } else {
2864 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
2866 }
2867 }
2868
2869 fn coincident_segment_to_ast(
2870 &self,
2871 segment: &ConstraintSegment,
2872 new_ast: &mut ast::Node<ast::Program>,
2873 ) -> Result<ast::Expr, KclError> {
2874 match segment {
2875 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
2876 ConstraintSegment::Segment(segment_id) => {
2877 let segment_object = self
2878 .scene_graph
2879 .objects
2880 .get(segment_id.0)
2881 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
2882 let ObjectKind::Segment { segment } = &segment_object.kind else {
2883 return Err(KclError::refactor(format!(
2884 "Object is not a segment, it is {}",
2885 segment_object.kind.human_friendly_kind_with_article()
2886 )));
2887 };
2888
2889 match segment {
2890 Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
2891 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None),
2892 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
2893 Segment::Circle(_) => {
2894 get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
2895 }
2896 }
2897 }
2898 }
2899 }
2900
2901 fn axis_constraint_segment_to_ast(
2902 &self,
2903 segment: &ConstraintSegment,
2904 new_ast: &mut ast::Node<ast::Program>,
2905 ) -> Result<ast::Expr, KclError> {
2906 match segment {
2907 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
2908 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
2909 }
2910 }
2911
2912 async fn add_coincident(
2913 &mut self,
2914 sketch: ObjectId,
2915 coincident: Coincident,
2916 new_ast: &mut ast::Node<ast::Program>,
2917 ) -> Result<AstNodeRef, KclError> {
2918 let sketch_id = sketch;
2919 let segment_asts = coincident
2920 .segments
2921 .iter()
2922 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2923 .collect::<Result<Vec<_>, _>>()?;
2924 if segment_asts.len() < 2 {
2925 return Err(KclError::refactor(format!(
2926 "Coincident constraint must have at least 2 inputs, got {}",
2927 segment_asts.len()
2928 )));
2929 }
2930
2931 let coincident_ast = create_coincident_ast(segment_asts);
2933
2934 let (sketch_block_ref, _) = self.mutate_ast(
2936 new_ast,
2937 sketch_id,
2938 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
2939 )?;
2940 Ok(sketch_block_ref)
2941 }
2942
2943 async fn add_distance(
2944 &mut self,
2945 sketch: ObjectId,
2946 distance: Distance,
2947 new_ast: &mut ast::Node<ast::Program>,
2948 ) -> Result<AstNodeRef, KclError> {
2949 let sketch_id = sketch;
2950 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
2951 [pt0, pt1] => [
2952 self.coincident_segment_to_ast(pt0, new_ast)?,
2953 self.coincident_segment_to_ast(pt1, new_ast)?,
2954 ],
2955 _ => {
2956 return Err(KclError::refactor(format!(
2957 "Distance constraint must have exactly 2 points, got {}",
2958 distance.points.len()
2959 )));
2960 }
2961 };
2962
2963 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2965 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
2966 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2967 ast::ArrayExpression {
2968 elements: vec![pt0_ast, pt1_ast],
2969 digest: None,
2970 non_code_meta: Default::default(),
2971 },
2972 )))),
2973 arguments: Default::default(),
2974 digest: None,
2975 non_code_meta: Default::default(),
2976 })));
2977 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2978 left: distance_call_ast,
2979 operator: ast::BinaryOperator::Eq,
2980 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2981 value: ast::LiteralValue::Number {
2982 value: distance.distance.value,
2983 suffix: distance.distance.units,
2984 },
2985 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
2986 KclError::refactor(format!(
2987 "Could not format numeric suffix: {:?}",
2988 distance.distance.units
2989 ))
2990 })?,
2991 digest: None,
2992 }))),
2993 digest: None,
2994 })));
2995
2996 let (sketch_block_ref, _) = self.mutate_ast(
2998 new_ast,
2999 sketch_id,
3000 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3001 )?;
3002 Ok(sketch_block_ref)
3003 }
3004
3005 async fn add_angle(
3006 &mut self,
3007 sketch: ObjectId,
3008 angle: Angle,
3009 new_ast: &mut ast::Node<ast::Program>,
3010 ) -> Result<AstNodeRef, KclError> {
3011 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3012 return Err(KclError::refactor(format!(
3013 "Angle constraint must have exactly 2 lines, got {}",
3014 angle.lines.len()
3015 )));
3016 };
3017 let sketch_id = sketch;
3018
3019 let line0_object = self
3021 .scene_graph
3022 .objects
3023 .get(l0_id.0)
3024 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3025 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3026 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3027 };
3028 let Segment::Line(_) = line0_segment else {
3029 return Err(KclError::refactor(format!(
3030 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3031 )));
3032 };
3033 let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
3034
3035 let line1_object = self
3036 .scene_graph
3037 .objects
3038 .get(l1_id.0)
3039 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3040 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3041 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3042 };
3043 let Segment::Line(_) = line1_segment else {
3044 return Err(KclError::refactor(format!(
3045 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3046 )));
3047 };
3048 let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
3049
3050 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3052 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3053 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3054 ast::ArrayExpression {
3055 elements: vec![l0_ast, l1_ast],
3056 digest: None,
3057 non_code_meta: Default::default(),
3058 },
3059 )))),
3060 arguments: Default::default(),
3061 digest: None,
3062 non_code_meta: Default::default(),
3063 })));
3064 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3065 left: angle_call_ast,
3066 operator: ast::BinaryOperator::Eq,
3067 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3068 value: ast::LiteralValue::Number {
3069 value: angle.angle.value,
3070 suffix: angle.angle.units,
3071 },
3072 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3073 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3074 })?,
3075 digest: None,
3076 }))),
3077 digest: None,
3078 })));
3079
3080 let (sketch_block_ref, _) = self.mutate_ast(
3082 new_ast,
3083 sketch_id,
3084 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3085 )?;
3086 Ok(sketch_block_ref)
3087 }
3088
3089 async fn add_tangent(
3090 &mut self,
3091 sketch: ObjectId,
3092 tangent: Tangent,
3093 new_ast: &mut ast::Node<ast::Program>,
3094 ) -> Result<AstNodeRef, KclError> {
3095 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3096 return Err(KclError::refactor(format!(
3097 "Tangent constraint must have exactly 2 segments, got {}",
3098 tangent.input.len()
3099 )));
3100 };
3101 let sketch_id = sketch;
3102
3103 let seg0_object = self
3104 .scene_graph
3105 .objects
3106 .get(seg0_id.0)
3107 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3108 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3109 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3110 };
3111 let seg0_ast = match seg0_segment {
3112 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?,
3113 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?,
3114 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3115 _ => {
3116 return Err(KclError::refactor(format!(
3117 "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3118 )));
3119 }
3120 };
3121
3122 let seg1_object = self
3123 .scene_graph
3124 .objects
3125 .get(seg1_id.0)
3126 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3127 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3128 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3129 };
3130 let seg1_ast = match seg1_segment {
3131 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?,
3132 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?,
3133 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3134 _ => {
3135 return Err(KclError::refactor(format!(
3136 "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3137 )));
3138 }
3139 };
3140
3141 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3142 let (sketch_block_ref, _) = self.mutate_ast(
3143 new_ast,
3144 sketch_id,
3145 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3146 )?;
3147 Ok(sketch_block_ref)
3148 }
3149
3150 async fn add_equal_radius(
3151 &mut self,
3152 sketch: ObjectId,
3153 equal_radius: EqualRadius,
3154 new_ast: &mut ast::Node<ast::Program>,
3155 ) -> Result<AstNodeRef, KclError> {
3156 if equal_radius.input.len() < 2 {
3157 return Err(KclError::refactor(format!(
3158 "equalRadius constraint must have at least 2 segments, got {}",
3159 equal_radius.input.len()
3160 )));
3161 }
3162
3163 let sketch_id = sketch;
3164 let input_asts = equal_radius
3165 .input
3166 .iter()
3167 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3168 .collect::<Result<Vec<_>, _>>()?;
3169
3170 let equal_radius_ast = create_equal_radius_ast(input_asts);
3171 let (sketch_block_ref, _) = self.mutate_ast(
3172 new_ast,
3173 sketch_id,
3174 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3175 )?;
3176 Ok(sketch_block_ref)
3177 }
3178
3179 async fn add_radius(
3180 &mut self,
3181 sketch: ObjectId,
3182 radius: Radius,
3183 new_ast: &mut ast::Node<ast::Program>,
3184 ) -> Result<AstNodeRef, KclError> {
3185 let params = ArcSizeConstraintParams {
3186 points: vec![radius.arc],
3187 function_name: RADIUS_FN,
3188 value: radius.radius.value,
3189 units: radius.radius.units,
3190 constraint_type_name: "Radius",
3191 };
3192 self.add_arc_size_constraint(sketch, params, new_ast).await
3193 }
3194
3195 async fn add_diameter(
3196 &mut self,
3197 sketch: ObjectId,
3198 diameter: Diameter,
3199 new_ast: &mut ast::Node<ast::Program>,
3200 ) -> Result<AstNodeRef, KclError> {
3201 let params = ArcSizeConstraintParams {
3202 points: vec![diameter.arc],
3203 function_name: DIAMETER_FN,
3204 value: diameter.diameter.value,
3205 units: diameter.diameter.units,
3206 constraint_type_name: "Diameter",
3207 };
3208 self.add_arc_size_constraint(sketch, params, new_ast).await
3209 }
3210
3211 async fn add_fixed_constraints(
3212 &mut self,
3213 sketch: ObjectId,
3214 points: Vec<FixedPoint>,
3215 new_ast: &mut ast::Node<ast::Program>,
3216 ) -> Result<AstNodeRef, KclError> {
3217 let mut sketch_block_ref = None;
3218
3219 for fixed_point in points {
3220 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3221 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3222 .map_err(|err| KclError::refactor(err.to_string()))?;
3223
3224 let (sketch_ref, _) = self.mutate_ast(
3225 new_ast,
3226 sketch,
3227 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3228 )?;
3229 sketch_block_ref = Some(sketch_ref);
3230 }
3231
3232 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3233 }
3234
3235 async fn add_arc_size_constraint(
3236 &mut self,
3237 sketch: ObjectId,
3238 params: ArcSizeConstraintParams,
3239 new_ast: &mut ast::Node<ast::Program>,
3240 ) -> Result<AstNodeRef, KclError> {
3241 let sketch_id = sketch;
3242
3243 if params.points.len() != 1 {
3245 return Err(KclError::refactor(format!(
3246 "{} constraint must have exactly 1 argument (an arc segment), got {}",
3247 params.constraint_type_name,
3248 params.points.len()
3249 )));
3250 }
3251
3252 let arc_id = params.points[0];
3253 let arc_object = self
3254 .scene_graph
3255 .objects
3256 .get(arc_id.0)
3257 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3258 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3259 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3260 };
3261 let ref_type = match arc_segment {
3262 Segment::Arc(_) => "arc",
3263 Segment::Circle(_) => CIRCLE_VARIABLE,
3264 _ => {
3265 return Err(KclError::refactor(format!(
3266 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3267 params.constraint_type_name
3268 )));
3269 }
3270 };
3271 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3273
3274 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3276 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3277 unlabeled: Some(arc_ast),
3278 arguments: Default::default(),
3279 digest: None,
3280 non_code_meta: Default::default(),
3281 })));
3282 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3283 left: call_ast,
3284 operator: ast::BinaryOperator::Eq,
3285 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3286 value: ast::LiteralValue::Number {
3287 value: params.value,
3288 suffix: params.units,
3289 },
3290 raw: format_number_literal(params.value, params.units, None)
3291 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3292 digest: None,
3293 }))),
3294 digest: None,
3295 })));
3296
3297 let (sketch_block_ref, _) = self.mutate_ast(
3299 new_ast,
3300 sketch_id,
3301 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3302 )?;
3303 Ok(sketch_block_ref)
3304 }
3305
3306 async fn add_horizontal_distance(
3307 &mut self,
3308 sketch: ObjectId,
3309 distance: Distance,
3310 new_ast: &mut ast::Node<ast::Program>,
3311 ) -> Result<AstNodeRef, KclError> {
3312 let sketch_id = sketch;
3313 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3314 [pt0, pt1] => [
3315 self.coincident_segment_to_ast(pt0, new_ast)?,
3316 self.coincident_segment_to_ast(pt1, new_ast)?,
3317 ],
3318 _ => {
3319 return Err(KclError::refactor(format!(
3320 "Horizontal distance constraint must have exactly 2 points, got {}",
3321 distance.points.len()
3322 )));
3323 }
3324 };
3325
3326 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3328 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3329 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3330 ast::ArrayExpression {
3331 elements: vec![pt0_ast, pt1_ast],
3332 digest: None,
3333 non_code_meta: Default::default(),
3334 },
3335 )))),
3336 arguments: Default::default(),
3337 digest: None,
3338 non_code_meta: Default::default(),
3339 })));
3340 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3341 left: distance_call_ast,
3342 operator: ast::BinaryOperator::Eq,
3343 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3344 value: ast::LiteralValue::Number {
3345 value: distance.distance.value,
3346 suffix: distance.distance.units,
3347 },
3348 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3349 KclError::refactor(format!(
3350 "Could not format numeric suffix: {:?}",
3351 distance.distance.units
3352 ))
3353 })?,
3354 digest: None,
3355 }))),
3356 digest: None,
3357 })));
3358
3359 let (sketch_block_ref, _) = self.mutate_ast(
3361 new_ast,
3362 sketch_id,
3363 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3364 )?;
3365 Ok(sketch_block_ref)
3366 }
3367
3368 async fn add_vertical_distance(
3369 &mut self,
3370 sketch: ObjectId,
3371 distance: Distance,
3372 new_ast: &mut ast::Node<ast::Program>,
3373 ) -> Result<AstNodeRef, KclError> {
3374 let sketch_id = sketch;
3375 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3376 [pt0, pt1] => [
3377 self.coincident_segment_to_ast(pt0, new_ast)?,
3378 self.coincident_segment_to_ast(pt1, new_ast)?,
3379 ],
3380 _ => {
3381 return Err(KclError::refactor(format!(
3382 "Vertical distance constraint must have exactly 2 points, got {}",
3383 distance.points.len()
3384 )));
3385 }
3386 };
3387
3388 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3390 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3391 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3392 ast::ArrayExpression {
3393 elements: vec![pt0_ast, pt1_ast],
3394 digest: None,
3395 non_code_meta: Default::default(),
3396 },
3397 )))),
3398 arguments: Default::default(),
3399 digest: None,
3400 non_code_meta: Default::default(),
3401 })));
3402 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3403 left: distance_call_ast,
3404 operator: ast::BinaryOperator::Eq,
3405 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3406 value: ast::LiteralValue::Number {
3407 value: distance.distance.value,
3408 suffix: distance.distance.units,
3409 },
3410 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3411 KclError::refactor(format!(
3412 "Could not format numeric suffix: {:?}",
3413 distance.distance.units
3414 ))
3415 })?,
3416 digest: None,
3417 }))),
3418 digest: None,
3419 })));
3420
3421 let (sketch_block_ref, _) = self.mutate_ast(
3423 new_ast,
3424 sketch_id,
3425 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3426 )?;
3427 Ok(sketch_block_ref)
3428 }
3429
3430 async fn add_horizontal(
3431 &mut self,
3432 sketch: ObjectId,
3433 horizontal: Horizontal,
3434 new_ast: &mut ast::Node<ast::Program>,
3435 ) -> Result<AstNodeRef, KclError> {
3436 let sketch_id = sketch;
3437
3438 let first_arg_ast = match horizontal {
3440 Horizontal::Line { line } => {
3441 let line_object = self
3442 .scene_graph
3443 .objects
3444 .get(line.0)
3445 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3446 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3447 let kind = line_object.kind.human_friendly_kind_with_article();
3448 return Err(KclError::refactor(format!(
3449 "This constraint only works on Segments, but you selected {kind}"
3450 )));
3451 };
3452 let Segment::Line(_) = line_segment else {
3453 return Err(KclError::refactor(format!(
3454 "Only lines can be made horizontal, but you selected {}",
3455 line_segment.human_friendly_kind_with_article(),
3456 )));
3457 };
3458 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?
3459 }
3460 Horizontal::Points { points } => {
3461 let point_asts = points
3462 .iter()
3463 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3464 .collect::<Result<Vec<_>, _>>()?;
3465 ast::ArrayExpression::new(point_asts).into()
3466 }
3467 };
3468
3469 let horizontal_ast = create_horizontal_ast(first_arg_ast);
3471
3472 let (sketch_block_ref, _) = self.mutate_ast(
3474 new_ast,
3475 sketch_id,
3476 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3477 )?;
3478 Ok(sketch_block_ref)
3479 }
3480
3481 async fn add_lines_equal_length(
3482 &mut self,
3483 sketch: ObjectId,
3484 lines_equal_length: LinesEqualLength,
3485 new_ast: &mut ast::Node<ast::Program>,
3486 ) -> Result<AstNodeRef, KclError> {
3487 if lines_equal_length.lines.len() < 2 {
3488 return Err(KclError::refactor(format!(
3489 "Lines equal length constraint must have at least 2 lines, got {}",
3490 lines_equal_length.lines.len()
3491 )));
3492 };
3493
3494 let sketch_id = sketch;
3495
3496 let line_asts = lines_equal_length
3498 .lines
3499 .iter()
3500 .map(|line_id| {
3501 let line_object = self
3502 .scene_graph
3503 .objects
3504 .get(line_id.0)
3505 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3506 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3507 let kind = line_object.kind.human_friendly_kind_with_article();
3508 return Err(KclError::refactor(format!(
3509 "This constraint only works on Segments, but you selected {kind}"
3510 )));
3511 };
3512 let Segment::Line(_) = line_segment else {
3513 let kind = line_segment.human_friendly_kind_with_article();
3514 return Err(KclError::refactor(format!(
3515 "Only lines can be made equal length, but you selected {kind}"
3516 )));
3517 };
3518
3519 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
3520 })
3521 .collect::<Result<Vec<_>, _>>()?;
3522
3523 let equal_length_ast = create_equal_length_ast(line_asts);
3525
3526 let (sketch_block_ref, _) = self.mutate_ast(
3528 new_ast,
3529 sketch_id,
3530 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3531 )?;
3532 Ok(sketch_block_ref)
3533 }
3534
3535 fn equal_radius_segment_id_to_ast_reference(
3536 &mut self,
3537 segment_id: ObjectId,
3538 new_ast: &mut ast::Node<ast::Program>,
3539 ) -> Result<ast::Expr, KclError> {
3540 let segment_object = self
3541 .scene_graph
3542 .objects
3543 .get(segment_id.0)
3544 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3545 let ObjectKind::Segment { segment } = &segment_object.kind else {
3546 return Err(KclError::refactor(format!(
3547 "Object is not a segment, it was {}",
3548 segment_object.kind.human_friendly_kind_with_article()
3549 )));
3550 };
3551
3552 let ref_type = match segment {
3553 Segment::Arc(_) => "arc",
3554 Segment::Circle(_) => CIRCLE_VARIABLE,
3555 _ => {
3556 return Err(KclError::refactor(format!(
3557 "equalRadius supports only arc/circle segments, got {}",
3558 segment.human_friendly_kind_with_article()
3559 )));
3560 }
3561 };
3562
3563 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3564 }
3565
3566 async fn add_parallel(
3567 &mut self,
3568 sketch: ObjectId,
3569 parallel: Parallel,
3570 new_ast: &mut ast::Node<ast::Program>,
3571 ) -> Result<AstNodeRef, KclError> {
3572 if parallel.lines.len() < 2 {
3573 return Err(KclError::refactor(format!(
3574 "Parallel constraint must have at least 2 lines, got {}",
3575 parallel.lines.len()
3576 )));
3577 };
3578
3579 let sketch_id = sketch;
3580
3581 let line_asts = parallel
3582 .lines
3583 .iter()
3584 .map(|line_id| {
3585 let line_object = self
3586 .scene_graph
3587 .objects
3588 .get(line_id.0)
3589 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3590 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3591 let kind = line_object.kind.human_friendly_kind_with_article();
3592 return Err(KclError::refactor(format!(
3593 "This constraint only works on Segments, but you selected {kind}"
3594 )));
3595 };
3596 let Segment::Line(_) = line_segment else {
3597 let kind = line_segment.human_friendly_kind_with_article();
3598 return Err(KclError::refactor(format!(
3599 "Only lines can be made parallel, but you selected {kind}"
3600 )));
3601 };
3602
3603 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
3604 })
3605 .collect::<Result<Vec<_>, _>>()?;
3606
3607 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3608 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3609 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3610 ast::ArrayExpression {
3611 elements: line_asts,
3612 digest: None,
3613 non_code_meta: Default::default(),
3614 },
3615 )))),
3616 arguments: Default::default(),
3617 digest: None,
3618 non_code_meta: Default::default(),
3619 })));
3620
3621 let (sketch_block_ref, _) = self.mutate_ast(
3622 new_ast,
3623 sketch_id,
3624 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3625 )?;
3626 Ok(sketch_block_ref)
3627 }
3628
3629 async fn add_perpendicular(
3630 &mut self,
3631 sketch: ObjectId,
3632 perpendicular: Perpendicular,
3633 new_ast: &mut ast::Node<ast::Program>,
3634 ) -> Result<AstNodeRef, KclError> {
3635 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3636 .await
3637 }
3638
3639 async fn add_lines_at_angle_constraint(
3640 &mut self,
3641 sketch: ObjectId,
3642 angle_kind: LinesAtAngleKind,
3643 lines: Vec<ObjectId>,
3644 new_ast: &mut ast::Node<ast::Program>,
3645 ) -> Result<AstNodeRef, KclError> {
3646 let &[line0_id, line1_id] = lines.as_slice() else {
3647 return Err(KclError::refactor(format!(
3648 "{} constraint must have exactly 2 lines, got {}",
3649 angle_kind.to_function_name(),
3650 lines.len()
3651 )));
3652 };
3653
3654 let sketch_id = sketch;
3655
3656 let line0_object = self
3658 .scene_graph
3659 .objects
3660 .get(line0_id.0)
3661 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3662 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3663 let kind = line0_object.kind.human_friendly_kind_with_article();
3664 return Err(KclError::refactor(format!(
3665 "This constraint only works on Segments, but you selected {kind}"
3666 )));
3667 };
3668 let Segment::Line(_) = line0_segment else {
3669 return Err(KclError::refactor(format!(
3670 "Only lines can be made {}, but you selected {}",
3671 angle_kind.to_function_name(),
3672 line0_segment.human_friendly_kind_with_article(),
3673 )));
3674 };
3675 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
3676
3677 let line1_object = self
3678 .scene_graph
3679 .objects
3680 .get(line1_id.0)
3681 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3682 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3683 let kind = line1_object.kind.human_friendly_kind_with_article();
3684 return Err(KclError::refactor(format!(
3685 "This constraint only works on Segments, but you selected {kind}"
3686 )));
3687 };
3688 let Segment::Line(_) = line1_segment else {
3689 return Err(KclError::refactor(format!(
3690 "Only lines can be made {}, but you selected {}",
3691 angle_kind.to_function_name(),
3692 line1_segment.human_friendly_kind_with_article(),
3693 )));
3694 };
3695 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
3696
3697 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3699 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3700 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3701 ast::ArrayExpression {
3702 elements: vec![line0_ast, line1_ast],
3703 digest: None,
3704 non_code_meta: Default::default(),
3705 },
3706 )))),
3707 arguments: Default::default(),
3708 digest: None,
3709 non_code_meta: Default::default(),
3710 })));
3711
3712 let (sketch_block_ref, _) = self.mutate_ast(
3714 new_ast,
3715 sketch_id,
3716 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3717 )?;
3718 Ok(sketch_block_ref)
3719 }
3720
3721 async fn add_vertical(
3722 &mut self,
3723 sketch: ObjectId,
3724 vertical: Vertical,
3725 new_ast: &mut ast::Node<ast::Program>,
3726 ) -> Result<AstNodeRef, KclError> {
3727 let sketch_id = sketch;
3728
3729 let first_arg_ast = match vertical {
3730 Vertical::Line { line } => {
3731 let line_object = self
3733 .scene_graph
3734 .objects
3735 .get(line.0)
3736 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3737 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3738 let kind = line_object.kind.human_friendly_kind_with_article();
3739 return Err(KclError::refactor(format!(
3740 "This constraint only works on Segments, but you selected {kind}"
3741 )));
3742 };
3743 let Segment::Line(_) = line_segment else {
3744 return Err(KclError::refactor(format!(
3745 "Only lines can be made vertical, but you selected {}",
3746 line_segment.human_friendly_kind_with_article()
3747 )));
3748 };
3749 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?
3750 }
3751 Vertical::Points { points } => {
3752 let point_asts = points
3753 .iter()
3754 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3755 .collect::<Result<Vec<_>, _>>()?;
3756 ast::ArrayExpression::new(point_asts).into()
3757 }
3758 };
3759
3760 let vertical_ast = create_vertical_ast(first_arg_ast);
3762
3763 let (sketch_block_ref, _) = self.mutate_ast(
3765 new_ast,
3766 sketch_id,
3767 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
3768 )?;
3769 Ok(sketch_block_ref)
3770 }
3771
3772 async fn execute_after_add_constraint(
3773 &mut self,
3774 ctx: &ExecutorContext,
3775 sketch_id: ObjectId,
3776 #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_ref: AstNodeRef,
3777 new_ast: &mut ast::Node<ast::Program>,
3778 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3779 let new_source = source_from_ast(new_ast);
3781 let (new_program, errors) = Program::parse(&new_source)
3783 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3784 if !errors.is_empty() {
3785 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3786 "Error parsing KCL source after adding constraint: {errors:?}"
3787 ))));
3788 }
3789 let Some(new_program) = new_program else {
3790 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3791 "No AST produced after adding constraint".to_string(),
3792 )));
3793 };
3794 #[cfg(feature = "artifact-graph")]
3795 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
3796 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3797 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
3798 )))
3799 })?;
3800
3801 let mut truncated_program = new_program.clone();
3804 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
3805 .map_err(KclErrorWithOutputs::no_outputs)?;
3806
3807 let outcome = ctx
3809 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
3810 .await?;
3811
3812 #[cfg(not(feature = "artifact-graph"))]
3813 let new_object_ids = Vec::new();
3814 #[cfg(feature = "artifact-graph")]
3815 let new_object_ids = {
3816 let constraint_id = outcome
3818 .source_range_to_object
3819 .get(&constraint_node_ref.range)
3820 .copied()
3821 .ok_or_else(|| {
3822 KclErrorWithOutputs::from_error_outcome(
3823 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
3824 outcome.clone(),
3825 )
3826 })?;
3827 vec![constraint_id]
3828 };
3829
3830 self.program = new_program;
3833
3834 let outcome = self.update_state_after_exec(outcome, true);
3836
3837 let src_delta = SourceDelta { text: new_source };
3838 let scene_graph_delta = SceneGraphDelta {
3839 new_graph: self.scene_graph.clone(),
3840 invalidates_ids: false,
3841 new_objects: new_object_ids,
3842 exec_outcome: outcome,
3843 };
3844 Ok((src_delta, scene_graph_delta))
3845 }
3846
3847 fn find_referenced_constraints(
3849 &self,
3850 sketch_id: ObjectId,
3851 segment_ids_set: &AhashIndexSet<ObjectId>,
3852 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
3853 let sketch_object = self
3855 .scene_graph
3856 .objects
3857 .get(sketch_id.0)
3858 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
3859 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3860 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3861 };
3862 let mut constraint_ids_set = AhashIndexSet::default();
3863 for constraint_id in &sketch.constraints {
3864 let constraint_object = self
3865 .scene_graph
3866 .objects
3867 .get(constraint_id.0)
3868 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
3869 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
3870 return Err(KclError::refactor(format!(
3871 "Object is not a constraint, it is {}",
3872 constraint_object.kind.human_friendly_kind_with_article()
3873 )));
3874 };
3875 let depends_on_segment = match constraint {
3876 Constraint::Coincident(c) => c.segment_ids().any(|seg_id| {
3877 if segment_ids_set.contains(&seg_id) {
3879 return true;
3880 }
3881 let seg_object = self.scene_graph.objects.get(seg_id.0);
3883 if let Some(obj) = seg_object
3884 && let ObjectKind::Segment { segment } = &obj.kind
3885 && let Segment::Point(pt) = segment
3886 && let Some(owner_line_id) = pt.owner
3887 {
3888 return segment_ids_set.contains(&owner_line_id);
3889 }
3890 false
3891 }),
3892 Constraint::Distance(d) => d.point_ids().any(|pt_id| {
3893 if segment_ids_set.contains(&pt_id) {
3894 return true;
3895 }
3896 let pt_object = self.scene_graph.objects.get(pt_id.0);
3897 if let Some(obj) = pt_object
3898 && let ObjectKind::Segment { segment } = &obj.kind
3899 && let Segment::Point(pt) = segment
3900 && let Some(owner_line_id) = pt.owner
3901 {
3902 return segment_ids_set.contains(&owner_line_id);
3903 }
3904 false
3905 }),
3906 Constraint::Fixed(_) => false,
3907 Constraint::Radius(r) => segment_ids_set.contains(&r.arc),
3908 Constraint::Diameter(d) => segment_ids_set.contains(&d.arc),
3909 Constraint::EqualRadius(equal_radius) => {
3910 equal_radius.input.iter().any(|seg_id| segment_ids_set.contains(seg_id))
3911 }
3912 Constraint::HorizontalDistance(d) => d.point_ids().any(|pt_id| {
3913 let pt_object = self.scene_graph.objects.get(pt_id.0);
3914 if let Some(obj) = pt_object
3915 && let ObjectKind::Segment { segment } = &obj.kind
3916 && let Segment::Point(pt) = segment
3917 && let Some(owner_line_id) = pt.owner
3918 {
3919 return segment_ids_set.contains(&owner_line_id);
3920 }
3921 false
3922 }),
3923 Constraint::VerticalDistance(d) => d.point_ids().any(|pt_id| {
3924 let pt_object = self.scene_graph.objects.get(pt_id.0);
3925 if let Some(obj) = pt_object
3926 && let ObjectKind::Segment { segment } = &obj.kind
3927 && let Segment::Point(pt) = segment
3928 && let Some(owner_line_id) = pt.owner
3929 {
3930 return segment_ids_set.contains(&owner_line_id);
3931 }
3932 false
3933 }),
3934 Constraint::Horizontal(h) => match h {
3935 Horizontal::Line { line } => segment_ids_set.contains(line),
3936 Horizontal::Points { points } => points.iter().any(|point| match point {
3937 ConstraintSegment::Segment(point) => segment_ids_set.contains(point),
3938 ConstraintSegment::Origin(_) => false,
3939 }),
3940 },
3941 Constraint::Vertical(v) => match v {
3942 Vertical::Line { line } => segment_ids_set.contains(line),
3943 Vertical::Points { points } => points.iter().any(|point| match point {
3944 ConstraintSegment::Segment(point) => segment_ids_set.contains(point),
3945 ConstraintSegment::Origin(_) => false,
3946 }),
3947 },
3948 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
3949 .lines
3950 .iter()
3951 .any(|line_id| segment_ids_set.contains(line_id)),
3952 Constraint::Parallel(parallel) => {
3953 parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
3954 }
3955 Constraint::Perpendicular(perpendicular) => perpendicular
3956 .lines
3957 .iter()
3958 .any(|line_id| segment_ids_set.contains(line_id)),
3959 Constraint::Angle(angle) => angle.lines.iter().any(|line_id| segment_ids_set.contains(line_id)),
3960 Constraint::Tangent(tangent) => tangent.input.iter().any(|seg_id| segment_ids_set.contains(seg_id)),
3961 };
3962 if depends_on_segment {
3963 constraint_ids_set.insert(*constraint_id);
3964 }
3965 }
3966 Ok(constraint_ids_set)
3967 }
3968
3969 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
3970 #[cfg(not(feature = "artifact-graph"))]
3971 {
3972 let _ = freedom_analysis_ran; outcome
3974 }
3975 #[cfg(feature = "artifact-graph")]
3976 {
3977 let mut outcome = outcome;
3978 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
3979
3980 if freedom_analysis_ran {
3981 self.point_freedom_cache.clear();
3984 for new_obj in &new_objects {
3985 if let ObjectKind::Segment {
3986 segment: crate::front::Segment::Point(point),
3987 } = &new_obj.kind
3988 {
3989 self.point_freedom_cache.insert(new_obj.id, point.freedom);
3990 }
3991 }
3992 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
3993 self.scene_graph.objects = new_objects;
3995 } else {
3996 for old_obj in &self.scene_graph.objects {
3999 if let ObjectKind::Segment {
4000 segment: crate::front::Segment::Point(point),
4001 } = &old_obj.kind
4002 {
4003 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4004 }
4005 }
4006
4007 let mut updated_objects = Vec::with_capacity(new_objects.len());
4009 for new_obj in new_objects {
4010 let mut obj = new_obj;
4011 if let ObjectKind::Segment {
4012 segment: crate::front::Segment::Point(point),
4013 } = &mut obj.kind
4014 {
4015 let new_freedom = point.freedom;
4016 match new_freedom {
4022 Freedom::Free => {
4023 match self.point_freedom_cache.get(&obj.id).copied() {
4024 Some(Freedom::Conflict) => {
4025 }
4028 Some(Freedom::Fixed) => {
4029 point.freedom = Freedom::Fixed;
4031 }
4032 Some(Freedom::Free) => {
4033 }
4035 None => {
4036 }
4038 }
4039 }
4040 Freedom::Fixed => {
4041 }
4043 Freedom::Conflict => {
4044 }
4046 }
4047 self.point_freedom_cache.insert(obj.id, point.freedom);
4049 }
4050 updated_objects.push(obj);
4051 }
4052
4053 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4054 self.scene_graph.objects = updated_objects;
4055 }
4056 outcome
4057 }
4058 }
4059
4060 fn mutate_ast(
4061 &mut self,
4062 ast: &mut ast::Node<ast::Program>,
4063 object_id: ObjectId,
4064 command: AstMutateCommand,
4065 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4066 let sketch_object = self
4067 .scene_graph
4068 .objects
4069 .get(object_id.0)
4070 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4071 match &sketch_object.source {
4072 SourceRef::Simple { range, node_path: _ } => mutate_ast_node_by_source_range(ast, *range, command),
4073 SourceRef::BackTrace { .. } => {
4074 Err(KclError::refactor("BackTrace source refs not supported yet".to_owned()))
4075 }
4076 }
4077 }
4078}
4079
4080fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4081 let sketch_object = scene_graph
4083 .objects
4084 .get(sketch_id.0)
4085 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4086 let ObjectKind::Sketch(_) = &sketch_object.kind else {
4087 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4088 };
4089 expect_single_node_ref(sketch_object)
4090}
4091
4092fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4093 match &object.source {
4094 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4095 range: *range,
4096 node_path: node_path.clone(),
4097 }),
4098 SourceRef::BackTrace { ranges } => {
4099 let [range] = ranges.as_slice() else {
4100 return Err(KclError::refactor(format!(
4101 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4102 ranges.len()
4103 )));
4104 };
4105 Ok(AstNodeRef {
4106 range: range.0,
4107 node_path: range.1.clone(),
4108 })
4109 }
4110 }
4111}
4112
4113fn expect_single_source_range(source_ref: &SourceRef) -> Result<SourceRange, KclError> {
4114 match source_ref {
4115 SourceRef::Simple { range, node_path: _ } => Ok(*range),
4116 SourceRef::BackTrace { ranges } => {
4117 if ranges.len() != 1 {
4118 return Err(KclError::refactor(format!(
4119 "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
4120 ranges.len(),
4121 )));
4122 }
4123 Ok(ranges[0].0)
4124 }
4125 }
4126}
4127
4128fn only_sketch_block_from_range(
4131 ast: &mut ast::Node<ast::Program>,
4132 sketch_block_range: SourceRange,
4133 edit_kind: ChangeKind,
4134) -> Result<(), KclError> {
4135 let r1 = sketch_block_range;
4136 let matches_range = |r2: SourceRange| -> bool {
4137 match edit_kind {
4140 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4141 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4143 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4144 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4146 }
4147 };
4148 let mut found = false;
4149 for item in ast.body.iter_mut() {
4150 match item {
4151 ast::BodyItem::ImportStatement(_) => {}
4152 ast::BodyItem::ExpressionStatement(node) => {
4153 if matches_range(SourceRange::from(&*node))
4154 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4155 {
4156 sketch_block.is_being_edited = true;
4157 found = true;
4158 break;
4159 }
4160 }
4161 ast::BodyItem::VariableDeclaration(node) => {
4162 if matches_range(SourceRange::from(&node.declaration.init))
4163 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4164 {
4165 sketch_block.is_being_edited = true;
4166 found = true;
4167 break;
4168 }
4169 }
4170 ast::BodyItem::TypeDeclaration(_) => {}
4171 ast::BodyItem::ReturnStatement(node) => {
4172 if matches_range(SourceRange::from(&node.argument))
4173 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4174 {
4175 sketch_block.is_being_edited = true;
4176 found = true;
4177 break;
4178 }
4179 }
4180 }
4181 }
4182 if !found {
4183 return Err(KclError::refactor(format!(
4184 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4185 )));
4186 }
4187
4188 Ok(())
4189}
4190
4191fn only_sketch_block(
4192 ast: &mut ast::Node<ast::Program>,
4193 sketch_block_ref: &AstNodeRef,
4194 edit_kind: ChangeKind,
4195) -> Result<(), KclError> {
4196 let Some(target_node_path) = &sketch_block_ref.node_path else {
4197 #[cfg(target_arch = "wasm32")]
4198 web_sys::console::warn_1(
4199 &format!(
4200 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4201 &sketch_block_ref
4202 )
4203 .into(),
4204 );
4205 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4206 };
4207 let mut found = false;
4208 for item in ast.body.iter_mut() {
4209 match item {
4210 ast::BodyItem::ImportStatement(_) => {}
4211 ast::BodyItem::ExpressionStatement(node) => {
4212 if let Some(node_path) = &node.node_path
4214 && node_path == target_node_path
4215 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4216 {
4217 sketch_block.is_being_edited = true;
4218 found = true;
4219 break;
4220 }
4221 if let Some(node_path) = node.expression.node_path()
4223 && node_path == target_node_path
4224 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4225 {
4226 sketch_block.is_being_edited = true;
4227 found = true;
4228 break;
4229 }
4230 }
4231 ast::BodyItem::VariableDeclaration(node) => {
4232 if let Some(node_path) = node.declaration.init.node_path()
4233 && node_path == target_node_path
4234 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4235 {
4236 sketch_block.is_being_edited = true;
4237 found = true;
4238 break;
4239 }
4240 }
4241 ast::BodyItem::TypeDeclaration(_) => {}
4242 ast::BodyItem::ReturnStatement(node) => {
4243 if let Some(node_path) = node.argument.node_path()
4244 && node_path == target_node_path
4245 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4246 {
4247 sketch_block.is_being_edited = true;
4248 found = true;
4249 break;
4250 }
4251 }
4252 }
4253 }
4254 if !found {
4255 return Err(KclError::refactor(format!(
4256 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4257 )));
4258 }
4259
4260 Ok(())
4261}
4262
4263fn sketch_on_ast_expr(
4264 ast: &mut ast::Node<ast::Program>,
4265 scene_graph: &SceneGraph,
4266 on: &Plane,
4267) -> Result<ast::Expr, KclError> {
4268 match on {
4269 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4270 Plane::Object(object_id) => {
4271 let on_object = scene_graph
4272 .objects
4273 .get(object_id.0)
4274 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4275 #[cfg(feature = "artifact-graph")]
4276 {
4277 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4278 return Ok(face_expr);
4279 }
4280 }
4281 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4282 }
4283 }
4284}
4285
4286#[cfg(feature = "artifact-graph")]
4287fn sketch_face_of_scene_object_ast_expr(
4288 ast: &mut ast::Node<ast::Program>,
4289 on_object: &crate::front::Object,
4290) -> Result<Option<ast::Expr>, KclError> {
4291 let SourceRef::BackTrace { ranges } = &on_object.source else {
4292 return Ok(None);
4293 };
4294
4295 match &on_object.kind {
4296 ObjectKind::Wall(_) => {
4297 let [sweep_range, segment_range] = ranges.as_slice() else {
4298 return Err(KclError::refactor(format!(
4299 "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4300 ranges.len(),
4301 on_object.artifact_id
4302 )));
4303 };
4304 let sweep_ref = get_or_insert_ast_reference(
4305 ast,
4306 &SourceRef::Simple {
4307 range: sweep_range.0,
4308 node_path: sweep_range.1.clone(),
4309 },
4310 "solid",
4311 None,
4312 )?;
4313 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4314 return Err(KclError::refactor(format!(
4315 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4316 on_object.artifact_id
4317 )));
4318 };
4319 let solid_name = solid_name_expr.name.name.clone();
4320 let solid_expr = ast_name_expr(solid_name.clone());
4321 let segment_ref = get_or_insert_ast_reference(
4322 ast,
4323 &SourceRef::Simple {
4324 range: segment_range.0,
4325 node_path: segment_range.1.clone(),
4326 },
4327 "line",
4328 None,
4329 )?;
4330
4331 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4332 let ast::Expr::Name(segment_name_expr) = segment_ref else {
4333 return Err(KclError::refactor(format!(
4334 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4335 on_object.artifact_id
4336 )));
4337 };
4338 create_member_expression(
4339 create_member_expression(ast_name_expr(region_name), "tags"),
4340 &segment_name_expr.name.name,
4341 )
4342 } else {
4343 segment_ref
4344 };
4345
4346 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4347 }
4348 ObjectKind::Cap(cap) => {
4349 let [range] = ranges.as_slice() else {
4350 return Err(KclError::refactor(format!(
4351 "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4352 ranges.len(),
4353 on_object.artifact_id
4354 )));
4355 };
4356 let sweep_ref = get_or_insert_ast_reference(
4357 ast,
4358 &SourceRef::Simple {
4359 range: range.0,
4360 node_path: range.1.clone(),
4361 },
4362 "solid",
4363 None,
4364 )?;
4365 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4366 return Err(KclError::refactor(format!(
4367 "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4368 on_object.artifact_id
4369 )));
4370 };
4371 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4372 let face_expr = match cap.kind {
4374 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4375 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4376 };
4377
4378 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4379 }
4380 _ => Ok(None),
4381 }
4382}
4383
4384#[cfg(feature = "artifact-graph")]
4385fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4386 let mut existing_artifact_ids = scene_objects
4387 .iter()
4388 .map(|object| object.artifact_id)
4389 .collect::<HashSet<_>>();
4390
4391 for artifact in artifact_graph.values() {
4392 match artifact {
4393 Artifact::Wall(wall) => {
4394 if existing_artifact_ids.contains(&wall.id) {
4395 continue;
4396 }
4397
4398 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4399 Artifact::Segment(segment) => Some(segment),
4400 _ => None,
4401 }) else {
4402 continue;
4403 };
4404 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4405 Artifact::Sweep(sweep) => Some(sweep),
4406 _ => None,
4407 }) else {
4408 continue;
4409 };
4410 let source_segment = segment
4411 .original_seg_id
4412 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4413 .and_then(|artifact| match artifact {
4414 Artifact::Segment(segment) => Some(segment),
4415 _ => None,
4416 })
4417 .unwrap_or(segment);
4418 let id = ObjectId(scene_objects.len());
4419 scene_objects.push(crate::front::Object {
4420 id,
4421 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4422 label: Default::default(),
4423 comments: Default::default(),
4424 artifact_id: wall.id,
4425 source: SourceRef::BackTrace {
4426 ranges: vec![
4427 (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4428 (
4429 source_segment.code_ref.range,
4430 Some(source_segment.code_ref.node_path.clone()),
4431 ),
4432 ],
4433 },
4434 });
4435 existing_artifact_ids.insert(wall.id);
4436 }
4437 Artifact::Cap(cap) => {
4438 if existing_artifact_ids.contains(&cap.id) {
4439 continue;
4440 }
4441
4442 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4443 Artifact::Sweep(sweep) => Some(sweep),
4444 _ => None,
4445 }) else {
4446 continue;
4447 };
4448 let id = ObjectId(scene_objects.len());
4449 let kind = match cap.sub_type {
4450 CapSubType::Start => crate::frontend::api::CapKind::Start,
4451 CapSubType::End => crate::frontend::api::CapKind::End,
4452 };
4453 scene_objects.push(crate::front::Object {
4454 id,
4455 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4456 label: Default::default(),
4457 comments: Default::default(),
4458 artifact_id: cap.id,
4459 source: SourceRef::BackTrace {
4460 ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4461 },
4462 });
4463 existing_artifact_ids.insert(cap.id);
4464 }
4465 _ => {}
4466 }
4467 }
4468}
4469
4470fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4471 use crate::engine::PlaneName;
4472
4473 match name {
4474 PlaneName::Xy => ast_name_expr("XY".to_owned()),
4475 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4476 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4477 PlaneName::NegXy => negated_plane_ast_expr("XY"),
4478 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4479 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4480 }
4481}
4482
4483fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4484 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4485 ast::UnaryOperator::Neg,
4486 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4487 )))
4488}
4489
4490#[cfg(feature = "artifact-graph")]
4491fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4492 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4493 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4494 unlabeled: Some(solid_expr),
4495 arguments: vec![ast::LabeledArg {
4496 label: Some(ast::Identifier::new("face")),
4497 arg: face_expr,
4498 }],
4499 digest: None,
4500 non_code_meta: Default::default(),
4501 })))
4502}
4503
4504#[cfg(feature = "artifact-graph")]
4505fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4506 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4507 return None;
4508 };
4509 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4510 return None;
4511 };
4512 if !matches!(
4513 sweep_call.callee.name.name.as_str(),
4514 "extrude" | "revolve" | "sweep" | "loft"
4515 ) {
4516 return None;
4517 }
4518 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4519 return None;
4520 };
4521 let candidate = region_name_expr.name.name.clone();
4522 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4523 return None;
4524 };
4525 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
4526 return None;
4527 };
4528 if region_call.callee.name.name != "region" {
4529 return None;
4530 }
4531 Some(candidate)
4532}
4533
4534fn get_or_insert_ast_reference(
4541 ast: &mut ast::Node<ast::Program>,
4542 source_ref: &SourceRef,
4543 prefix: &str,
4544 property: Option<&str>,
4545) -> Result<ast::Expr, KclError> {
4546 let range = expect_single_source_range(source_ref)?;
4547 let command = AstMutateCommand::AddVariableDeclaration {
4548 prefix: prefix.to_owned(),
4549 };
4550 let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
4551 let AstMutateCommandReturn::Name(var_name) = ret else {
4552 return Err(KclError::refactor(
4553 "Expected variable name returned from AddVariableDeclaration".to_owned(),
4554 ));
4555 };
4556 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4557 let Some(property) = property else {
4558 return Ok(var_expr);
4560 };
4561
4562 Ok(create_member_expression(var_expr, property))
4563}
4564
4565fn mutate_ast_node_by_source_range(
4566 ast: &mut ast::Node<ast::Program>,
4567 source_range: SourceRange,
4568 command: AstMutateCommand,
4569) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4570 let mut context = AstMutateContext {
4571 source_range,
4572 node_path: None,
4573 command,
4574 defined_names_stack: Default::default(),
4575 };
4576 let control = dfs_mut(ast, &mut context);
4577 match control {
4578 ControlFlow::Continue(_) => Err(KclError::refactor(format!("Source range not found: {source_range:?}"))),
4579 ControlFlow::Break(break_value) => break_value,
4580 }
4581}
4582
4583#[derive(Debug)]
4584struct AstMutateContext {
4585 source_range: SourceRange,
4586 node_path: Option<ast::NodePath>,
4587 command: AstMutateCommand,
4588 defined_names_stack: Vec<HashSet<String>>,
4589}
4590
4591#[derive(Debug)]
4592#[allow(clippy::large_enum_variant)]
4593enum AstMutateCommand {
4594 AddSketchBlockExprStmt {
4596 expr: ast::Expr,
4597 },
4598 AddSketchBlockVarDecl {
4600 prefix: String,
4601 expr: ast::Expr,
4602 },
4603 AddVariableDeclaration {
4604 prefix: String,
4605 },
4606 EditPoint {
4607 at: ast::Expr,
4608 },
4609 EditLine {
4610 start: ast::Expr,
4611 end: ast::Expr,
4612 construction: Option<bool>,
4613 },
4614 EditArc {
4615 start: ast::Expr,
4616 end: ast::Expr,
4617 center: ast::Expr,
4618 construction: Option<bool>,
4619 },
4620 EditCircle {
4621 start: ast::Expr,
4622 center: ast::Expr,
4623 construction: Option<bool>,
4624 },
4625 EditConstraintValue {
4626 value: ast::BinaryPart,
4627 },
4628 EditCallUnlabeled {
4629 arg: ast::Expr,
4630 },
4631 #[cfg(feature = "artifact-graph")]
4632 EditVarInitialValue {
4633 value: Number,
4634 },
4635 DeleteNode,
4636}
4637
4638impl AstMutateCommand {
4639 fn needs_defined_names_stack(&self) -> bool {
4640 matches!(
4641 self,
4642 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4643 )
4644 }
4645}
4646
4647#[derive(Debug)]
4648enum AstMutateCommandReturn {
4649 None,
4650 Name(String),
4651}
4652
4653#[derive(Debug, Clone)]
4654struct AstNodeRef {
4655 range: SourceRange,
4656 node_path: Option<ast::NodePath>,
4657}
4658
4659impl<T> From<&ast::Node<T>> for AstNodeRef {
4660 fn from(value: &ast::Node<T>) -> Self {
4661 AstNodeRef {
4662 range: value.into(),
4663 node_path: value.node_path.clone(),
4664 }
4665 }
4666}
4667
4668impl From<&ast::BodyItem> for AstNodeRef {
4669 fn from(value: &ast::BodyItem) -> Self {
4670 match value {
4671 ast::BodyItem::ImportStatement(node) => AstNodeRef {
4672 range: node.into(),
4673 node_path: node.node_path.clone(),
4674 },
4675 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4676 range: node.into(),
4677 node_path: node.node_path.clone(),
4678 },
4679 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4680 range: node.into(),
4681 node_path: node.node_path.clone(),
4682 },
4683 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4684 range: node.into(),
4685 node_path: node.node_path.clone(),
4686 },
4687 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
4688 range: node.into(),
4689 node_path: node.node_path.clone(),
4690 },
4691 }
4692 }
4693}
4694
4695impl From<&ast::Expr> for AstNodeRef {
4696 fn from(value: &ast::Expr) -> Self {
4697 AstNodeRef {
4698 range: SourceRange::from(value),
4699 node_path: value.node_path().cloned(),
4700 }
4701 }
4702}
4703
4704impl From<&AstMutateContext> for AstNodeRef {
4705 fn from(value: &AstMutateContext) -> Self {
4706 AstNodeRef {
4707 range: value.source_range,
4708 node_path: value.node_path.clone(),
4709 }
4710 }
4711}
4712
4713impl TryFrom<&NodeMut<'_>> for AstNodeRef {
4714 type Error = crate::walk::AstNodeError;
4715
4716 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
4717 Ok(AstNodeRef {
4718 range: SourceRange::try_from(value)?,
4719 node_path: value.try_into()?,
4720 })
4721 }
4722}
4723
4724impl From<AstNodeRef> for SourceRange {
4725 fn from(value: AstNodeRef) -> Self {
4726 value.range
4727 }
4728}
4729
4730impl Visitor for AstMutateContext {
4731 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
4732 type Continue = ();
4733
4734 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
4735 filter_and_process(self, node)
4736 }
4737
4738 fn finish(&mut self, node: NodeMut<'_>) {
4739 match &node {
4740 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
4741 self.defined_names_stack.pop();
4742 }
4743 _ => {}
4744 }
4745 }
4746}
4747
4748fn filter_and_process(
4749 ctx: &mut AstMutateContext,
4750 node: NodeMut,
4751) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
4752 let Ok(node_range) = SourceRange::try_from(&node) else {
4753 return TraversalReturn::new_continue(());
4755 };
4756 if let NodeMut::VariableDeclaration(var_decl) = &node {
4761 let expr_range = SourceRange::from(&var_decl.declaration.init);
4762 if expr_range == ctx.source_range {
4763 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
4764 return TraversalReturn::new_break(Ok((
4767 AstNodeRef::from(&**var_decl),
4768 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
4769 )));
4770 }
4771 if let AstMutateCommand::DeleteNode = &ctx.command {
4772 return TraversalReturn {
4775 mutate_body_item: MutateBodyItem::Delete,
4776 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
4777 };
4778 }
4779 }
4780 }
4781
4782 if ctx.command.needs_defined_names_stack() {
4783 if let NodeMut::Program(program) = &node {
4784 ctx.defined_names_stack.push(find_defined_names(*program));
4785 } else if let NodeMut::SketchBlock(block) = &node {
4786 ctx.defined_names_stack.push(find_defined_names(&block.body));
4787 }
4788 }
4789
4790 if node_range != ctx.source_range {
4793 return TraversalReturn::new_continue(());
4794 }
4795 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
4796 return TraversalReturn::new_continue(());
4797 };
4798 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
4799}
4800
4801fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
4802 match &ctx.command {
4803 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
4804 if let NodeMut::SketchBlock(sketch_block) = node {
4805 sketch_block
4806 .body
4807 .items
4808 .push(ast::BodyItem::ExpressionStatement(ast::Node {
4809 inner: ast::ExpressionStatement {
4810 expression: expr.clone(),
4811 digest: None,
4812 },
4813 start: Default::default(),
4814 end: Default::default(),
4815 module_id: Default::default(),
4816 node_path: None,
4817 outer_attrs: Default::default(),
4818 pre_comments: Default::default(),
4819 comment_start: Default::default(),
4820 }));
4821 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4822 }
4823 }
4824 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
4825 if let NodeMut::SketchBlock(sketch_block) = node {
4826 let empty_defined_names = HashSet::new();
4827 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
4828 let Ok(name) = next_free_name(prefix, defined_names) else {
4829 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4830 };
4831 sketch_block
4832 .body
4833 .items
4834 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
4835 ast::VariableDeclaration::new(
4836 ast::VariableDeclarator::new(&name, expr.clone()),
4837 ast::ItemVisibility::Default,
4838 ast::VariableKind::Const,
4839 ),
4840 ))));
4841 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
4842 }
4843 }
4844 AstMutateCommand::AddVariableDeclaration { prefix } => {
4845 if let NodeMut::VariableDeclaration(inner) = node {
4846 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
4847 }
4848 if let NodeMut::ExpressionStatement(expr_stmt) = node {
4849 let empty_defined_names = HashSet::new();
4850 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
4851 let Ok(name) = next_free_name(prefix, defined_names) else {
4852 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4854 };
4855 let mutate_node =
4856 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
4857 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
4858 ast::ItemVisibility::Default,
4859 ast::VariableKind::Const,
4860 ))));
4861 return TraversalReturn {
4862 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
4863 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
4864 };
4865 }
4866 }
4867 AstMutateCommand::EditPoint { at } => {
4868 if let NodeMut::CallExpressionKw(call) = node {
4869 if call.callee.name.name != POINT_FN {
4870 return TraversalReturn::new_continue(());
4871 }
4872 for labeled_arg in &mut call.arguments {
4874 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
4875 labeled_arg.arg = at.clone();
4876 }
4877 }
4878 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4879 }
4880 }
4881 AstMutateCommand::EditLine {
4882 start,
4883 end,
4884 construction,
4885 } => {
4886 if let NodeMut::CallExpressionKw(call) = node {
4887 if call.callee.name.name != LINE_FN {
4888 return TraversalReturn::new_continue(());
4889 }
4890 for labeled_arg in &mut call.arguments {
4892 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
4893 labeled_arg.arg = start.clone();
4894 }
4895 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
4896 labeled_arg.arg = end.clone();
4897 }
4898 }
4899 if let Some(construction_value) = construction {
4901 let construction_exists = call
4902 .arguments
4903 .iter()
4904 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
4905 if *construction_value {
4906 if construction_exists {
4908 for labeled_arg in &mut call.arguments {
4910 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
4911 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4912 value: ast::LiteralValue::Bool(true),
4913 raw: "true".to_string(),
4914 digest: None,
4915 })));
4916 }
4917 }
4918 } else {
4919 call.arguments.push(ast::LabeledArg {
4921 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
4922 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4923 value: ast::LiteralValue::Bool(true),
4924 raw: "true".to_string(),
4925 digest: None,
4926 }))),
4927 });
4928 }
4929 } else {
4930 call.arguments
4932 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
4933 }
4934 }
4935 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4936 }
4937 }
4938 AstMutateCommand::EditArc {
4939 start,
4940 end,
4941 center,
4942 construction,
4943 } => {
4944 if let NodeMut::CallExpressionKw(call) = node {
4945 if call.callee.name.name != ARC_FN {
4946 return TraversalReturn::new_continue(());
4947 }
4948 for labeled_arg in &mut call.arguments {
4950 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
4951 labeled_arg.arg = start.clone();
4952 }
4953 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
4954 labeled_arg.arg = end.clone();
4955 }
4956 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
4957 labeled_arg.arg = center.clone();
4958 }
4959 }
4960 if let Some(construction_value) = construction {
4962 let construction_exists = call
4963 .arguments
4964 .iter()
4965 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
4966 if *construction_value {
4967 if construction_exists {
4969 for labeled_arg in &mut call.arguments {
4971 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
4972 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4973 value: ast::LiteralValue::Bool(true),
4974 raw: "true".to_string(),
4975 digest: None,
4976 })));
4977 }
4978 }
4979 } else {
4980 call.arguments.push(ast::LabeledArg {
4982 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
4983 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4984 value: ast::LiteralValue::Bool(true),
4985 raw: "true".to_string(),
4986 digest: None,
4987 }))),
4988 });
4989 }
4990 } else {
4991 call.arguments
4993 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
4994 }
4995 }
4996 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4997 }
4998 }
4999 AstMutateCommand::EditCircle {
5000 start,
5001 center,
5002 construction,
5003 } => {
5004 if let NodeMut::CallExpressionKw(call) = node {
5005 if call.callee.name.name != CIRCLE_FN {
5006 return TraversalReturn::new_continue(());
5007 }
5008 for labeled_arg in &mut call.arguments {
5010 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5011 labeled_arg.arg = start.clone();
5012 }
5013 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5014 labeled_arg.arg = center.clone();
5015 }
5016 }
5017 if let Some(construction_value) = construction {
5019 let construction_exists = call
5020 .arguments
5021 .iter()
5022 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5023 if *construction_value {
5024 if construction_exists {
5025 for labeled_arg in &mut call.arguments {
5026 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5027 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5028 value: ast::LiteralValue::Bool(true),
5029 raw: "true".to_string(),
5030 digest: None,
5031 })));
5032 }
5033 }
5034 } else {
5035 call.arguments.push(ast::LabeledArg {
5036 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5037 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5038 value: ast::LiteralValue::Bool(true),
5039 raw: "true".to_string(),
5040 digest: None,
5041 }))),
5042 });
5043 }
5044 } else {
5045 call.arguments
5046 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5047 }
5048 }
5049 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5050 }
5051 }
5052 AstMutateCommand::EditConstraintValue { value } => {
5053 if let NodeMut::BinaryExpression(binary_expr) = node {
5054 let left_is_constraint = matches!(
5055 &binary_expr.left,
5056 ast::BinaryPart::CallExpressionKw(call)
5057 if matches!(
5058 call.callee.name.name.as_str(),
5059 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5060 )
5061 );
5062 if left_is_constraint {
5063 binary_expr.right = value.clone();
5064 } else {
5065 binary_expr.left = value.clone();
5066 }
5067
5068 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5069 }
5070 }
5071 AstMutateCommand::EditCallUnlabeled { arg } => {
5072 if let NodeMut::CallExpressionKw(call) = node {
5073 call.unlabeled = Some(arg.clone());
5074 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5075 }
5076 }
5077 #[cfg(feature = "artifact-graph")]
5078 AstMutateCommand::EditVarInitialValue { value } => {
5079 if let NodeMut::NumericLiteral(numeric_literal) = node {
5080 let Ok(literal) = to_source_number(*value) else {
5082 return TraversalReturn::new_break(Err(KclError::refactor(format!(
5083 "Could not convert number to AST literal: {:?}",
5084 *value
5085 ))));
5086 };
5087 *numeric_literal = ast::Node::no_src(literal);
5088 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5089 }
5090 }
5091 AstMutateCommand::DeleteNode => {
5092 return TraversalReturn {
5093 mutate_body_item: MutateBodyItem::Delete,
5094 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5095 };
5096 }
5097 }
5098 TraversalReturn::new_continue(())
5099}
5100
5101struct FindSketchBlockSourceRange {
5102 target_before_mutation: SourceRange,
5104 found: Cell<Option<AstNodeRef>>,
5108}
5109
5110impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5111 type Error = crate::front::Error;
5112
5113 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5114 let Ok(node_range) = SourceRange::try_from(&node) else {
5115 return Ok(true);
5116 };
5117
5118 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5119 if node_range.module_id() == self.target_before_mutation.module_id()
5120 && node_range.start() == self.target_before_mutation.start()
5121 && node_range.end() >= self.target_before_mutation.end()
5123 {
5124 self.found.set(sketch_block.body.items.last().map(|item| match item {
5125 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5129 _ => AstNodeRef::from(item),
5130 }));
5131 return Ok(false);
5132 } else {
5133 return Ok(true);
5136 }
5137 }
5138
5139 for child in node.children().iter() {
5140 if !child.visit(*self)? {
5141 return Ok(false);
5142 }
5143 }
5144
5145 Ok(true)
5146 }
5147}
5148
5149struct FindSketchBlockByNodePath {
5150 target_node_path: ast::NodePath,
5152 found: Cell<Option<AstNodeRef>>,
5156}
5157
5158impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5159 type Error = crate::front::Error;
5160
5161 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5162 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5163 return Ok(true);
5164 };
5165
5166 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5167 if let Some(node_path) = node_path
5168 && node_path == self.target_node_path
5169 {
5170 self.found.set(sketch_block.body.items.last().map(|item| match item {
5171 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5175 _ => AstNodeRef::from(item),
5176 }));
5177
5178 return Ok(false);
5179 } else {
5180 return Ok(true);
5183 }
5184 }
5185
5186 for child in node.children().iter() {
5187 if !child.visit(*self)? {
5188 return Ok(false);
5189 }
5190 }
5191
5192 Ok(true)
5193 }
5194}
5195
5196fn find_sketch_block_added_item(
5204 ast: &ast::Node<ast::Program>,
5205 sketch_block_before_mutation: &AstNodeRef,
5206) -> Result<AstNodeRef, KclError> {
5207 if let Some(node_path) = &sketch_block_before_mutation.node_path {
5208 let find = FindSketchBlockByNodePath {
5209 target_node_path: node_path.clone(),
5210 found: Cell::new(None),
5211 };
5212 let node = crate::walk::Node::from(ast);
5213 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5214 find.found.into_inner().ok_or_else(|| {
5215 KclError::refactor(format!(
5216 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5217 ))
5218 })
5219 } else {
5220 let find = FindSketchBlockSourceRange {
5222 target_before_mutation: sketch_block_before_mutation.range,
5223 found: Cell::new(None),
5224 };
5225 let node = crate::walk::Node::from(ast);
5226 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5227 find.found.into_inner().ok_or_else(|| KclError::refactor(
5228 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?"),
5229 ))
5230 }
5231}
5232
5233fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5234 ast.recast_top(&Default::default(), 0)
5236}
5237
5238pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5239 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5240 inner: ast::ArrayExpression {
5241 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5242 non_code_meta: Default::default(),
5243 digest: None,
5244 },
5245 start: Default::default(),
5246 end: Default::default(),
5247 module_id: Default::default(),
5248 node_path: None,
5249 outer_attrs: Default::default(),
5250 pre_comments: Default::default(),
5251 comment_start: Default::default(),
5252 })))
5253}
5254
5255fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5256 match expr {
5257 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5258 inner: ast::Literal::from(to_source_number(*number)?),
5259 start: Default::default(),
5260 end: Default::default(),
5261 module_id: Default::default(),
5262 node_path: None,
5263 outer_attrs: Default::default(),
5264 pre_comments: Default::default(),
5265 comment_start: Default::default(),
5266 }))),
5267 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5268 inner: ast::SketchVar {
5269 initial: Some(Box::new(ast::Node {
5270 inner: to_source_number(*number)?,
5271 start: Default::default(),
5272 end: Default::default(),
5273 module_id: Default::default(),
5274 node_path: None,
5275 outer_attrs: Default::default(),
5276 pre_comments: Default::default(),
5277 comment_start: Default::default(),
5278 })),
5279 digest: None,
5280 },
5281 start: Default::default(),
5282 end: Default::default(),
5283 module_id: Default::default(),
5284 node_path: None,
5285 outer_attrs: Default::default(),
5286 pre_comments: Default::default(),
5287 comment_start: Default::default(),
5288 }))),
5289 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5290 }
5291}
5292
5293fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5294 Ok(ast::NumericLiteral {
5295 value: number.value,
5296 suffix: number.units,
5297 raw: format_number_literal(number.value, number.units, None)?,
5298 digest: None,
5299 })
5300}
5301
5302pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5303 ast::Expr::Name(Box::new(ast_name(name)))
5304}
5305
5306fn ast_name(name: String) -> ast::Node<ast::Name> {
5307 ast::Node {
5308 inner: ast::Name {
5309 name: ast::Node {
5310 inner: ast::Identifier { name, digest: None },
5311 start: Default::default(),
5312 end: Default::default(),
5313 module_id: Default::default(),
5314 node_path: None,
5315 outer_attrs: Default::default(),
5316 pre_comments: Default::default(),
5317 comment_start: Default::default(),
5318 },
5319 path: Vec::new(),
5320 abs_path: false,
5321 digest: None,
5322 },
5323 start: Default::default(),
5324 end: Default::default(),
5325 module_id: Default::default(),
5326 node_path: None,
5327 outer_attrs: Default::default(),
5328 pre_comments: Default::default(),
5329 comment_start: Default::default(),
5330 }
5331}
5332
5333pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5334 ast::Name {
5335 name: ast::Node {
5336 inner: ast::Identifier {
5337 name: name.to_owned(),
5338 digest: None,
5339 },
5340 start: Default::default(),
5341 end: Default::default(),
5342 module_id: Default::default(),
5343 node_path: None,
5344 outer_attrs: Default::default(),
5345 pre_comments: Default::default(),
5346 comment_start: Default::default(),
5347 },
5348 path: Default::default(),
5349 abs_path: false,
5350 digest: None,
5351 }
5352}
5353
5354pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
5358 let elements = exprs.into_iter().collect::<Vec<_>>();
5359 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
5360
5361 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5363 elements,
5364 digest: None,
5365 non_code_meta: Default::default(),
5366 })));
5367
5368 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5370 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5371 unlabeled: Some(array_expr),
5372 arguments: Default::default(),
5373 digest: None,
5374 non_code_meta: Default::default(),
5375 })))
5376}
5377
5378pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5380 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5381 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5382 unlabeled: None,
5383 arguments: vec![
5384 ast::LabeledArg {
5385 label: Some(ast::Identifier::new(LINE_START_PARAM)),
5386 arg: start_ast,
5387 },
5388 ast::LabeledArg {
5389 label: Some(ast::Identifier::new(LINE_END_PARAM)),
5390 arg: end_ast,
5391 },
5392 ],
5393 digest: None,
5394 non_code_meta: Default::default(),
5395 })))
5396}
5397
5398pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5400 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5401 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5402 unlabeled: None,
5403 arguments: vec![
5404 ast::LabeledArg {
5405 label: Some(ast::Identifier::new(ARC_START_PARAM)),
5406 arg: start_ast,
5407 },
5408 ast::LabeledArg {
5409 label: Some(ast::Identifier::new(ARC_END_PARAM)),
5410 arg: end_ast,
5411 },
5412 ast::LabeledArg {
5413 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5414 arg: center_ast,
5415 },
5416 ],
5417 digest: None,
5418 non_code_meta: Default::default(),
5419 })))
5420}
5421
5422pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5424 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5425 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
5426 unlabeled: None,
5427 arguments: vec![
5428 ast::LabeledArg {
5429 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5430 arg: start_ast,
5431 },
5432 ast::LabeledArg {
5433 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5434 arg: center_ast,
5435 },
5436 ],
5437 digest: None,
5438 non_code_meta: Default::default(),
5439 })))
5440}
5441
5442pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
5444 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5445 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
5446 unlabeled: Some(line_expr),
5447 arguments: Default::default(),
5448 digest: None,
5449 non_code_meta: Default::default(),
5450 })))
5451}
5452
5453pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5455 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5456 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5457 unlabeled: Some(line_expr),
5458 arguments: Default::default(),
5459 digest: None,
5460 non_code_meta: Default::default(),
5461 })))
5462}
5463
5464pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5466 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5467 object: object_expr,
5468 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5469 name: ast::Node::no_src(ast::Identifier {
5470 name: property.to_string(),
5471 digest: None,
5472 }),
5473 path: Vec::new(),
5474 abs_path: false,
5475 digest: None,
5476 }))),
5477 computed: false,
5478 digest: None,
5479 })))
5480}
5481
5482fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5484 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5486 position.x,
5487 )?))));
5488 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5489 position.y,
5490 )?))));
5491 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5492 elements: vec![x_literal, y_literal],
5493 digest: None,
5494 non_code_meta: Default::default(),
5495 })));
5496
5497 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5499 elements: vec![point_expr, point_array],
5500 digest: None,
5501 non_code_meta: Default::default(),
5502 })));
5503
5504 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5506 ast::CallExpressionKw {
5507 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5508 unlabeled: Some(array_expr),
5509 arguments: Default::default(),
5510 digest: None,
5511 non_code_meta: Default::default(),
5512 },
5513 ))))
5514}
5515
5516pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5518 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5519 elements: line_exprs,
5520 digest: None,
5521 non_code_meta: Default::default(),
5522 })));
5523
5524 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5526 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5527 unlabeled: Some(array_expr),
5528 arguments: Default::default(),
5529 digest: None,
5530 non_code_meta: Default::default(),
5531 })))
5532}
5533
5534pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5536 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5537 elements: segment_exprs,
5538 digest: None,
5539 non_code_meta: Default::default(),
5540 })));
5541
5542 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5543 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5544 unlabeled: Some(array_expr),
5545 arguments: Default::default(),
5546 digest: None,
5547 non_code_meta: Default::default(),
5548 })))
5549}
5550
5551pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5553 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5554 elements: vec![seg1_expr, seg2_expr],
5555 digest: None,
5556 non_code_meta: Default::default(),
5557 })));
5558
5559 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5560 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5561 unlabeled: Some(array_expr),
5562 arguments: Default::default(),
5563 digest: None,
5564 non_code_meta: Default::default(),
5565 })))
5566}
5567
5568#[cfg(all(feature = "artifact-graph", test))]
5569mod tests {
5570 use super::*;
5571 use crate::engine::PlaneName;
5572 use crate::execution::cache::SketchModeState;
5573 use crate::execution::cache::clear_mem_cache;
5574 use crate::execution::cache::read_old_memory;
5575 use crate::execution::cache::write_old_memory;
5576 use crate::front::Distance;
5577 use crate::front::Fixed;
5578 use crate::front::FixedPoint;
5579 use crate::front::Object;
5580 use crate::front::Plane;
5581 use crate::front::Sketch;
5582 use crate::front::Tangent;
5583 use crate::frontend::sketch::Vertical;
5584 use crate::pretty::NumericSuffix;
5585
5586 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
5587 for object in &scene_graph.objects {
5588 if let ObjectKind::Sketch(_) = &object.kind {
5589 return Some(object);
5590 }
5591 }
5592 None
5593 }
5594
5595 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
5596 for object in &scene_graph.objects {
5597 if let ObjectKind::Face(_) = &object.kind {
5598 return Some(object);
5599 }
5600 }
5601 None
5602 }
5603
5604 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
5605 for object in &scene_graph.objects {
5606 if matches!(&object.kind, ObjectKind::Wall(_)) {
5607 return Some(object.id);
5608 }
5609 }
5610 None
5611 }
5612
5613 #[test]
5614 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
5615 let source = "\
5616region001 = region(point = [0.1, 0.1], sketch = s)
5617extrude001 = extrude(region001, length = 5)
5618revolve001 = revolve(region001, axis = Y)
5619sweep001 = sweep(region001, path = path001)
5620loft001 = loft(region001)
5621not_sweep001 = shell(extrude001, faces = [], thickness = 1)
5622";
5623
5624 let program = Program::parse(source).unwrap().0.unwrap();
5625
5626 assert_eq!(
5627 region_name_from_sweep_variable(&program.ast, "extrude001"),
5628 Some("region001".to_owned())
5629 );
5630 assert_eq!(
5631 region_name_from_sweep_variable(&program.ast, "revolve001"),
5632 Some("region001".to_owned())
5633 );
5634 assert_eq!(
5635 region_name_from_sweep_variable(&program.ast, "sweep001"),
5636 Some("region001".to_owned())
5637 );
5638 assert_eq!(
5639 region_name_from_sweep_variable(&program.ast, "loft001"),
5640 Some("region001".to_owned())
5641 );
5642 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
5643 }
5644
5645 #[track_caller]
5646 fn expect_sketch(object: &Object) -> &Sketch {
5647 if let ObjectKind::Sketch(sketch) = &object.kind {
5648 sketch
5649 } else {
5650 panic!("Object is not a sketch: {:?}", object);
5651 }
5652 }
5653
5654 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
5655 LineCtor {
5656 start: Point2d {
5657 x: Expr::Number(Number { value: start_x, units }),
5658 y: Expr::Number(Number { value: start_y, units }),
5659 },
5660 end: Point2d {
5661 x: Expr::Number(Number { value: end_x, units }),
5662 y: Expr::Number(Number { value: end_y, units }),
5663 },
5664 construction: None,
5665 }
5666 }
5667
5668 async fn create_sketch_with_single_line(
5669 frontend: &mut FrontendState,
5670 ctx: &ExecutorContext,
5671 mock_ctx: &ExecutorContext,
5672 version: Version,
5673 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
5674 frontend.program = Program::empty();
5675
5676 let sketch_args = SketchCtor {
5677 on: Plane::Default(PlaneName::Xy),
5678 };
5679 let (_src_delta, _scene_delta, sketch_id) = frontend
5680 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
5681 .await
5682 .unwrap();
5683
5684 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
5685 let (source_delta, scene_graph_delta) = frontend
5686 .add_segment(mock_ctx, version, sketch_id, segment, None)
5687 .await
5688 .unwrap();
5689 let line_id = *scene_graph_delta
5690 .new_objects
5691 .last()
5692 .expect("Expected line object id to be created");
5693
5694 (sketch_id, line_id, source_delta, scene_graph_delta)
5695 }
5696
5697 #[tokio::test(flavor = "multi_thread")]
5698 async fn test_sketch_checkpoint_round_trip_restores_state() {
5699 let mut frontend = FrontendState::new();
5700 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5701 let mock_ctx = ExecutorContext::new_mock(None).await;
5702 let version = Version(0);
5703
5704 let (sketch_id, line_id, source_delta, scene_graph_delta) =
5705 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5706
5707 let expected_source = source_delta.text.clone();
5708 let expected_scene_graph = frontend.scene_graph.clone();
5709 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
5710 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
5711
5712 let checkpoint_id = frontend
5713 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5714 .await
5715 .unwrap();
5716
5717 let edited_segments = vec![ExistingSegmentCtor {
5718 id: line_id,
5719 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
5720 }];
5721 let (edited_source, _edited_scene) = frontend
5722 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
5723 .await
5724 .unwrap();
5725 assert_ne!(edited_source.text, expected_source);
5726
5727 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
5728
5729 assert_eq!(restored.source_delta.text, expected_source);
5730 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
5731 assert!(restored.scene_graph_delta.invalidates_ids);
5732 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
5733 assert_eq!(frontend.scene_graph, expected_scene_graph);
5734 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
5735
5736 ctx.close().await;
5737 mock_ctx.close().await;
5738 }
5739
5740 #[tokio::test(flavor = "multi_thread")]
5741 async fn test_sketch_checkpoints_prune_oldest_entries() {
5742 let mut frontend = FrontendState::new();
5743 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5744 let mock_ctx = ExecutorContext::new_mock(None).await;
5745 let version = Version(0);
5746
5747 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
5748 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5749
5750 let mut checkpoint_ids = Vec::new();
5751 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
5752 checkpoint_ids.push(
5753 frontend
5754 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5755 .await
5756 .unwrap(),
5757 );
5758 }
5759
5760 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
5761 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
5762
5763 let oldest_retained = checkpoint_ids[3];
5764 assert_eq!(
5765 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
5766 Some(oldest_retained)
5767 );
5768
5769 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
5770 assert!(evicted_restore.is_err());
5771 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
5772
5773 frontend
5774 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
5775 .await
5776 .unwrap();
5777
5778 ctx.close().await;
5779 mock_ctx.close().await;
5780 }
5781
5782 #[tokio::test(flavor = "multi_thread")]
5783 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
5784 let mut frontend = FrontendState::new();
5785 let missing_checkpoint = SketchCheckpointId::new(999);
5786
5787 let err = frontend
5788 .restore_sketch_checkpoint(missing_checkpoint)
5789 .await
5790 .expect_err("Expected restore to fail for missing checkpoint");
5791
5792 assert!(err.msg.contains("Sketch checkpoint not found"));
5793 }
5794
5795 #[tokio::test(flavor = "multi_thread")]
5796 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
5797 let mut frontend = FrontendState::new();
5798 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5799 let mock_ctx = ExecutorContext::new_mock(None).await;
5800 let version = Version(0);
5801
5802 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
5803 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5804
5805 let checkpoint_a = frontend
5806 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5807 .await
5808 .unwrap();
5809 let checkpoint_b = frontend
5810 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5811 .await
5812 .unwrap();
5813 assert_eq!(frontend.sketch_checkpoints.len(), 2);
5814
5815 frontend.clear_sketch_checkpoints();
5816 assert!(frontend.sketch_checkpoints.is_empty());
5817 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
5818 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
5819
5820 ctx.close().await;
5821 mock_ctx.close().await;
5822 }
5823
5824 #[tokio::test(flavor = "multi_thread")]
5825 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
5826 let mut frontend = FrontendState::new();
5827 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5828 let mock_ctx = ExecutorContext::new_mock(None).await;
5829 let version = Version(0);
5830
5831 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
5832 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5833 let old_source = source_delta.text.clone();
5834 let old_checkpoint = frontend
5835 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5836 .await
5837 .unwrap();
5838 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
5839
5840 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
5841 .unwrap()
5842 .0
5843 .unwrap();
5844
5845 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
5846 let SetProgramOutcome::Success {
5847 checkpoint_id: Some(new_checkpoint),
5848 ..
5849 } = result
5850 else {
5851 panic!("Expected Success with a fresh checkpoint baseline");
5852 };
5853
5854 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
5855
5856 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
5857 assert_eq!(old_restore.source_delta.text, old_source);
5858
5859 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
5860 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
5861
5862 ctx.close().await;
5863 mock_ctx.close().await;
5864 }
5865
5866 #[tokio::test(flavor = "multi_thread")]
5867 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
5868 let mut frontend = FrontendState::new();
5869 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5870 let mock_ctx = ExecutorContext::new_mock(None).await;
5871 let version = Version(0);
5872
5873 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
5874 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5875 let old_checkpoint = frontend
5876 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5877 .await
5878 .unwrap();
5879 let checkpoint_count_before = frontend.sketch_checkpoints.len();
5880
5881 let failing_program = Program::parse(
5882 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
5883 )
5884 .unwrap()
5885 .0
5886 .unwrap();
5887
5888 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
5889 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
5890 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
5891 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
5892
5893 ctx.close().await;
5894 mock_ctx.close().await;
5895 }
5896
5897 #[tokio::test(flavor = "multi_thread")]
5898 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
5899 let mut frontend = FrontendState::new();
5900 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5901
5902 let program = Program::parse(
5903 "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",
5904 )
5905 .unwrap()
5906 .0
5907 .unwrap();
5908 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
5909 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
5910 panic!("Expected successful baseline program execution");
5911 };
5912
5913 clear_mem_cache().await;
5914 assert!(read_old_memory().await.is_none());
5915
5916 let checkpoint_without_mock_memory = frontend
5917 .create_sketch_checkpoint((*exec_outcome).clone())
5918 .await
5919 .unwrap();
5920
5921 write_old_memory(SketchModeState::new_for_tests()).await;
5922 assert!(read_old_memory().await.is_some());
5923
5924 let checkpoint_with_mock_memory = frontend
5925 .create_sketch_checkpoint((*exec_outcome).clone())
5926 .await
5927 .unwrap();
5928
5929 clear_mem_cache().await;
5930 assert!(read_old_memory().await.is_none());
5931
5932 frontend
5933 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
5934 .await
5935 .unwrap();
5936 assert!(read_old_memory().await.is_some());
5937
5938 frontend
5939 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
5940 .await
5941 .unwrap();
5942 assert!(read_old_memory().await.is_none());
5943
5944 ctx.close().await;
5945 }
5946
5947 #[tokio::test(flavor = "multi_thread")]
5948 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
5949 let source = "\
5950sketch(on = XY) {
5951 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
5952}
5953
5954bad = missing_name
5955";
5956 let program = Program::parse(source).unwrap().0.unwrap();
5957
5958 let mut frontend = FrontendState::new();
5959
5960 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5961 let mock_ctx = ExecutorContext::new_mock(None).await;
5962 let version = Version(0);
5963 let project_id = ProjectId(0);
5964 let file_id = FileId(0);
5965
5966 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
5967 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
5968 };
5969
5970 let sketch_id = frontend
5971 .scene_graph
5972 .objects
5973 .iter()
5974 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
5975 .expect("Expected sketch object from errored hack_set_program");
5976
5977 frontend
5978 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
5979 .await
5980 .unwrap();
5981
5982 ctx.close().await;
5983 mock_ctx.close().await;
5984 }
5985
5986 #[tokio::test(flavor = "multi_thread")]
5987 async fn test_new_sketch_add_point_edit_point() {
5988 let program = Program::empty();
5989
5990 let mut frontend = FrontendState::new();
5991 frontend.program = program;
5992
5993 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5994 let mock_ctx = ExecutorContext::new_mock(None).await;
5995 let version = Version(0);
5996
5997 let sketch_args = SketchCtor {
5998 on: Plane::Default(PlaneName::Xy),
5999 };
6000 let (_src_delta, scene_delta, sketch_id) = frontend
6001 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6002 .await
6003 .unwrap();
6004 assert_eq!(sketch_id, ObjectId(1));
6005 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6006 let sketch_object = &scene_delta.new_graph.objects[1];
6007 assert_eq!(sketch_object.id, ObjectId(1));
6008 assert_eq!(
6009 sketch_object.kind,
6010 ObjectKind::Sketch(Sketch {
6011 args: SketchCtor {
6012 on: Plane::Default(PlaneName::Xy)
6013 },
6014 plane: ObjectId(0),
6015 segments: vec![],
6016 constraints: vec![],
6017 })
6018 );
6019 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6020
6021 let point_ctor = PointCtor {
6022 position: Point2d {
6023 x: Expr::Number(Number {
6024 value: 1.0,
6025 units: NumericSuffix::Inch,
6026 }),
6027 y: Expr::Number(Number {
6028 value: 2.0,
6029 units: NumericSuffix::Inch,
6030 }),
6031 },
6032 };
6033 let segment = SegmentCtor::Point(point_ctor);
6034 let (src_delta, scene_delta) = frontend
6035 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6036 .await
6037 .unwrap();
6038 assert_eq!(
6039 src_delta.text.as_str(),
6040 "sketch001 = sketch(on = XY) {
6041 point(at = [1in, 2in])
6042}
6043"
6044 );
6045 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6046 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6047 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6048 assert_eq!(scene_object.id.0, i);
6049 }
6050
6051 let point_id = *scene_delta.new_objects.last().unwrap();
6052
6053 let point_ctor = PointCtor {
6054 position: Point2d {
6055 x: Expr::Number(Number {
6056 value: 3.0,
6057 units: NumericSuffix::Inch,
6058 }),
6059 y: Expr::Number(Number {
6060 value: 4.0,
6061 units: NumericSuffix::Inch,
6062 }),
6063 },
6064 };
6065 let segments = vec![ExistingSegmentCtor {
6066 id: point_id,
6067 ctor: SegmentCtor::Point(point_ctor),
6068 }];
6069 let (src_delta, scene_delta) = frontend
6070 .edit_segments(&mock_ctx, version, sketch_id, segments)
6071 .await
6072 .unwrap();
6073 assert_eq!(
6074 src_delta.text.as_str(),
6075 "sketch001 = sketch(on = XY) {
6076 point(at = [3in, 4in])
6077}
6078"
6079 );
6080 assert_eq!(scene_delta.new_objects, vec![]);
6081 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6082
6083 ctx.close().await;
6084 mock_ctx.close().await;
6085 }
6086
6087 #[tokio::test(flavor = "multi_thread")]
6088 async fn test_new_sketch_add_line_edit_line() {
6089 let program = Program::empty();
6090
6091 let mut frontend = FrontendState::new();
6092 frontend.program = program;
6093
6094 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6095 let mock_ctx = ExecutorContext::new_mock(None).await;
6096 let version = Version(0);
6097
6098 let sketch_args = SketchCtor {
6099 on: Plane::Default(PlaneName::Xy),
6100 };
6101 let (_src_delta, scene_delta, sketch_id) = frontend
6102 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6103 .await
6104 .unwrap();
6105 assert_eq!(sketch_id, ObjectId(1));
6106 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6107 let sketch_object = &scene_delta.new_graph.objects[1];
6108 assert_eq!(sketch_object.id, ObjectId(1));
6109 assert_eq!(
6110 sketch_object.kind,
6111 ObjectKind::Sketch(Sketch {
6112 args: SketchCtor {
6113 on: Plane::Default(PlaneName::Xy)
6114 },
6115 plane: ObjectId(0),
6116 segments: vec![],
6117 constraints: vec![],
6118 })
6119 );
6120 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6121
6122 let line_ctor = LineCtor {
6123 start: Point2d {
6124 x: Expr::Number(Number {
6125 value: 0.0,
6126 units: NumericSuffix::Mm,
6127 }),
6128 y: Expr::Number(Number {
6129 value: 0.0,
6130 units: NumericSuffix::Mm,
6131 }),
6132 },
6133 end: Point2d {
6134 x: Expr::Number(Number {
6135 value: 10.0,
6136 units: NumericSuffix::Mm,
6137 }),
6138 y: Expr::Number(Number {
6139 value: 10.0,
6140 units: NumericSuffix::Mm,
6141 }),
6142 },
6143 construction: None,
6144 };
6145 let segment = SegmentCtor::Line(line_ctor);
6146 let (src_delta, scene_delta) = frontend
6147 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6148 .await
6149 .unwrap();
6150 assert_eq!(
6151 src_delta.text.as_str(),
6152 "sketch001 = sketch(on = XY) {
6153 line(start = [0mm, 0mm], end = [10mm, 10mm])
6154}
6155"
6156 );
6157 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6158 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6159 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6160 assert_eq!(scene_object.id.0, i);
6161 }
6162
6163 let line = *scene_delta.new_objects.last().unwrap();
6165
6166 let line_ctor = LineCtor {
6167 start: Point2d {
6168 x: Expr::Number(Number {
6169 value: 1.0,
6170 units: NumericSuffix::Mm,
6171 }),
6172 y: Expr::Number(Number {
6173 value: 2.0,
6174 units: NumericSuffix::Mm,
6175 }),
6176 },
6177 end: Point2d {
6178 x: Expr::Number(Number {
6179 value: 13.0,
6180 units: NumericSuffix::Mm,
6181 }),
6182 y: Expr::Number(Number {
6183 value: 14.0,
6184 units: NumericSuffix::Mm,
6185 }),
6186 },
6187 construction: None,
6188 };
6189 let segments = vec![ExistingSegmentCtor {
6190 id: line,
6191 ctor: SegmentCtor::Line(line_ctor),
6192 }];
6193 let (src_delta, scene_delta) = frontend
6194 .edit_segments(&mock_ctx, version, sketch_id, segments)
6195 .await
6196 .unwrap();
6197 assert_eq!(
6198 src_delta.text.as_str(),
6199 "sketch001 = sketch(on = XY) {
6200 line(start = [1mm, 2mm], end = [13mm, 14mm])
6201}
6202"
6203 );
6204 assert_eq!(scene_delta.new_objects, vec![]);
6205 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6206
6207 ctx.close().await;
6208 mock_ctx.close().await;
6209 }
6210
6211 #[tokio::test(flavor = "multi_thread")]
6212 async fn test_new_sketch_add_arc_edit_arc() {
6213 let program = Program::empty();
6214
6215 let mut frontend = FrontendState::new();
6216 frontend.program = program;
6217
6218 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6219 let mock_ctx = ExecutorContext::new_mock(None).await;
6220 let version = Version(0);
6221
6222 let sketch_args = SketchCtor {
6223 on: Plane::Default(PlaneName::Xy),
6224 };
6225 let (_src_delta, scene_delta, sketch_id) = frontend
6226 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6227 .await
6228 .unwrap();
6229 assert_eq!(sketch_id, ObjectId(1));
6230 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6231 let sketch_object = &scene_delta.new_graph.objects[1];
6232 assert_eq!(sketch_object.id, ObjectId(1));
6233 assert_eq!(
6234 sketch_object.kind,
6235 ObjectKind::Sketch(Sketch {
6236 args: SketchCtor {
6237 on: Plane::Default(PlaneName::Xy),
6238 },
6239 plane: ObjectId(0),
6240 segments: vec![],
6241 constraints: vec![],
6242 })
6243 );
6244 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6245
6246 let arc_ctor = ArcCtor {
6247 start: Point2d {
6248 x: Expr::Var(Number {
6249 value: 0.0,
6250 units: NumericSuffix::Mm,
6251 }),
6252 y: Expr::Var(Number {
6253 value: 0.0,
6254 units: NumericSuffix::Mm,
6255 }),
6256 },
6257 end: Point2d {
6258 x: Expr::Var(Number {
6259 value: 10.0,
6260 units: NumericSuffix::Mm,
6261 }),
6262 y: Expr::Var(Number {
6263 value: 10.0,
6264 units: NumericSuffix::Mm,
6265 }),
6266 },
6267 center: Point2d {
6268 x: Expr::Var(Number {
6269 value: 10.0,
6270 units: NumericSuffix::Mm,
6271 }),
6272 y: Expr::Var(Number {
6273 value: 0.0,
6274 units: NumericSuffix::Mm,
6275 }),
6276 },
6277 construction: None,
6278 };
6279 let segment = SegmentCtor::Arc(arc_ctor);
6280 let (src_delta, scene_delta) = frontend
6281 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6282 .await
6283 .unwrap();
6284 assert_eq!(
6285 src_delta.text.as_str(),
6286 "sketch001 = sketch(on = XY) {
6287 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6288}
6289"
6290 );
6291 assert_eq!(
6292 scene_delta.new_objects,
6293 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6294 );
6295 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6296 assert_eq!(scene_object.id.0, i);
6297 }
6298 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6299
6300 let arc = *scene_delta.new_objects.last().unwrap();
6302
6303 let arc_ctor = ArcCtor {
6304 start: Point2d {
6305 x: Expr::Var(Number {
6306 value: 1.0,
6307 units: NumericSuffix::Mm,
6308 }),
6309 y: Expr::Var(Number {
6310 value: 2.0,
6311 units: NumericSuffix::Mm,
6312 }),
6313 },
6314 end: Point2d {
6315 x: Expr::Var(Number {
6316 value: 13.0,
6317 units: NumericSuffix::Mm,
6318 }),
6319 y: Expr::Var(Number {
6320 value: 14.0,
6321 units: NumericSuffix::Mm,
6322 }),
6323 },
6324 center: Point2d {
6325 x: Expr::Var(Number {
6326 value: 13.0,
6327 units: NumericSuffix::Mm,
6328 }),
6329 y: Expr::Var(Number {
6330 value: 2.0,
6331 units: NumericSuffix::Mm,
6332 }),
6333 },
6334 construction: None,
6335 };
6336 let segments = vec![ExistingSegmentCtor {
6337 id: arc,
6338 ctor: SegmentCtor::Arc(arc_ctor),
6339 }];
6340 let (src_delta, scene_delta) = frontend
6341 .edit_segments(&mock_ctx, version, sketch_id, segments)
6342 .await
6343 .unwrap();
6344 assert_eq!(
6345 src_delta.text.as_str(),
6346 "sketch001 = sketch(on = XY) {
6347 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6348}
6349"
6350 );
6351 assert_eq!(scene_delta.new_objects, vec![]);
6352 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6353
6354 ctx.close().await;
6355 mock_ctx.close().await;
6356 }
6357
6358 #[tokio::test(flavor = "multi_thread")]
6359 async fn test_new_sketch_add_circle_edit_circle() {
6360 let program = Program::empty();
6361
6362 let mut frontend = FrontendState::new();
6363 frontend.program = program;
6364
6365 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6366 let mock_ctx = ExecutorContext::new_mock(None).await;
6367 let version = Version(0);
6368
6369 let sketch_args = SketchCtor {
6370 on: Plane::Default(PlaneName::Xy),
6371 };
6372 let (_src_delta, _scene_delta, sketch_id) = frontend
6373 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6374 .await
6375 .unwrap();
6376
6377 let circle_ctor = CircleCtor {
6379 start: Point2d {
6380 x: Expr::Var(Number {
6381 value: 5.0,
6382 units: NumericSuffix::Mm,
6383 }),
6384 y: Expr::Var(Number {
6385 value: 0.0,
6386 units: NumericSuffix::Mm,
6387 }),
6388 },
6389 center: Point2d {
6390 x: Expr::Var(Number {
6391 value: 0.0,
6392 units: NumericSuffix::Mm,
6393 }),
6394 y: Expr::Var(Number {
6395 value: 0.0,
6396 units: NumericSuffix::Mm,
6397 }),
6398 },
6399 construction: None,
6400 };
6401 let segment = SegmentCtor::Circle(circle_ctor);
6402 let (src_delta, scene_delta) = frontend
6403 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6404 .await
6405 .unwrap();
6406 assert_eq!(
6407 src_delta.text.as_str(),
6408 "sketch001 = sketch(on = XY) {
6409 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6410}
6411"
6412 );
6413 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6415 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6416
6417 let circle = *scene_delta.new_objects.last().unwrap();
6418
6419 let circle_ctor = CircleCtor {
6421 start: Point2d {
6422 x: Expr::Var(Number {
6423 value: 10.0,
6424 units: NumericSuffix::Mm,
6425 }),
6426 y: Expr::Var(Number {
6427 value: 0.0,
6428 units: NumericSuffix::Mm,
6429 }),
6430 },
6431 center: Point2d {
6432 x: Expr::Var(Number {
6433 value: 3.0,
6434 units: NumericSuffix::Mm,
6435 }),
6436 y: Expr::Var(Number {
6437 value: 4.0,
6438 units: NumericSuffix::Mm,
6439 }),
6440 },
6441 construction: None,
6442 };
6443 let segments = vec![ExistingSegmentCtor {
6444 id: circle,
6445 ctor: SegmentCtor::Circle(circle_ctor),
6446 }];
6447 let (src_delta, scene_delta) = frontend
6448 .edit_segments(&mock_ctx, version, sketch_id, segments)
6449 .await
6450 .unwrap();
6451 assert_eq!(
6452 src_delta.text.as_str(),
6453 "sketch001 = sketch(on = XY) {
6454 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6455}
6456"
6457 );
6458 assert_eq!(scene_delta.new_objects, vec![]);
6459 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6460
6461 ctx.close().await;
6462 mock_ctx.close().await;
6463 }
6464
6465 #[tokio::test(flavor = "multi_thread")]
6466 async fn test_delete_circle() {
6467 let initial_source = "sketch001 = sketch(on = XY) {
6468 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6469}
6470";
6471
6472 let program = Program::parse(initial_source).unwrap().0.unwrap();
6473 let mut frontend = FrontendState::new();
6474
6475 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6476 let mock_ctx = ExecutorContext::new_mock(None).await;
6477 let version = Version(0);
6478
6479 frontend.hack_set_program(&ctx, program).await.unwrap();
6480 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6481 let sketch_id = sketch_object.id;
6482 let sketch = expect_sketch(sketch_object);
6483
6484 assert_eq!(sketch.segments.len(), 3);
6486 let circle_id = sketch.segments[2];
6487
6488 let (src_delta, scene_delta) = frontend
6490 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6491 .await
6492 .unwrap();
6493 assert_eq!(
6494 src_delta.text.as_str(),
6495 "sketch001 = sketch(on = XY) {
6496}
6497"
6498 );
6499 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6500 let new_sketch = expect_sketch(new_sketch_object);
6501 assert_eq!(new_sketch.segments.len(), 0);
6502
6503 ctx.close().await;
6504 mock_ctx.close().await;
6505 }
6506
6507 #[tokio::test(flavor = "multi_thread")]
6508 async fn test_edit_circle_via_point() {
6509 let initial_source = "sketch001 = sketch(on = XY) {
6510 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6511}
6512";
6513
6514 let program = Program::parse(initial_source).unwrap().0.unwrap();
6515 let mut frontend = FrontendState::new();
6516
6517 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6518 let mock_ctx = ExecutorContext::new_mock(None).await;
6519 let version = Version(0);
6520
6521 frontend.hack_set_program(&ctx, program).await.unwrap();
6522 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6523 let sketch_id = sketch_object.id;
6524 let sketch = expect_sketch(sketch_object);
6525
6526 let circle_id = sketch
6528 .segments
6529 .iter()
6530 .copied()
6531 .find(|seg_id| {
6532 matches!(
6533 &frontend.scene_graph.objects[seg_id.0].kind,
6534 ObjectKind::Segment {
6535 segment: Segment::Circle(_)
6536 }
6537 )
6538 })
6539 .expect("Expected a circle segment in sketch");
6540 let circle_object = &frontend.scene_graph.objects[circle_id.0];
6541 let ObjectKind::Segment {
6542 segment: Segment::Circle(circle),
6543 } = &circle_object.kind
6544 else {
6545 panic!("Expected circle segment, got: {:?}", circle_object.kind);
6546 };
6547 let start_point_id = circle.start;
6548
6549 let segments = vec![ExistingSegmentCtor {
6551 id: start_point_id,
6552 ctor: SegmentCtor::Point(PointCtor {
6553 position: Point2d {
6554 x: Expr::Var(Number {
6555 value: 7.0,
6556 units: NumericSuffix::Mm,
6557 }),
6558 y: Expr::Var(Number {
6559 value: 1.0,
6560 units: NumericSuffix::Mm,
6561 }),
6562 },
6563 }),
6564 }];
6565 let (src_delta, _scene_delta) = frontend
6566 .edit_segments(&mock_ctx, version, sketch_id, segments)
6567 .await
6568 .unwrap();
6569 assert_eq!(
6570 src_delta.text.as_str(),
6571 "sketch001 = sketch(on = XY) {
6572 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
6573}
6574"
6575 );
6576
6577 ctx.close().await;
6578 mock_ctx.close().await;
6579 }
6580
6581 #[tokio::test(flavor = "multi_thread")]
6582 async fn test_add_line_when_sketch_block_uses_variable() {
6583 let initial_source = "s = sketch(on = XY) {}
6584";
6585
6586 let program = Program::parse(initial_source).unwrap().0.unwrap();
6587
6588 let mut frontend = FrontendState::new();
6589
6590 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6591 let mock_ctx = ExecutorContext::new_mock(None).await;
6592 let version = Version(0);
6593
6594 frontend.hack_set_program(&ctx, program).await.unwrap();
6595 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6596 let sketch_id = sketch_object.id;
6597
6598 let line_ctor = LineCtor {
6599 start: Point2d {
6600 x: Expr::Number(Number {
6601 value: 0.0,
6602 units: NumericSuffix::Mm,
6603 }),
6604 y: Expr::Number(Number {
6605 value: 0.0,
6606 units: NumericSuffix::Mm,
6607 }),
6608 },
6609 end: Point2d {
6610 x: Expr::Number(Number {
6611 value: 10.0,
6612 units: NumericSuffix::Mm,
6613 }),
6614 y: Expr::Number(Number {
6615 value: 10.0,
6616 units: NumericSuffix::Mm,
6617 }),
6618 },
6619 construction: None,
6620 };
6621 let segment = SegmentCtor::Line(line_ctor);
6622 let (src_delta, scene_delta) = frontend
6623 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6624 .await
6625 .unwrap();
6626 assert_eq!(
6627 src_delta.text.as_str(),
6628 "s = sketch(on = XY) {
6629 line(start = [0mm, 0mm], end = [10mm, 10mm])
6630}
6631"
6632 );
6633 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6634 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6635
6636 ctx.close().await;
6637 mock_ctx.close().await;
6638 }
6639
6640 #[tokio::test(flavor = "multi_thread")]
6641 async fn test_new_sketch_add_line_delete_sketch() {
6642 let program = Program::empty();
6643
6644 let mut frontend = FrontendState::new();
6645 frontend.program = program;
6646
6647 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6648 let mock_ctx = ExecutorContext::new_mock(None).await;
6649 let version = Version(0);
6650
6651 let sketch_args = SketchCtor {
6652 on: Plane::Default(PlaneName::Xy),
6653 };
6654 let (_src_delta, scene_delta, sketch_id) = frontend
6655 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6656 .await
6657 .unwrap();
6658 assert_eq!(sketch_id, ObjectId(1));
6659 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6660 let sketch_object = &scene_delta.new_graph.objects[1];
6661 assert_eq!(sketch_object.id, ObjectId(1));
6662 assert_eq!(
6663 sketch_object.kind,
6664 ObjectKind::Sketch(Sketch {
6665 args: SketchCtor {
6666 on: Plane::Default(PlaneName::Xy)
6667 },
6668 plane: ObjectId(0),
6669 segments: vec![],
6670 constraints: vec![],
6671 })
6672 );
6673 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6674
6675 let line_ctor = LineCtor {
6676 start: Point2d {
6677 x: Expr::Number(Number {
6678 value: 0.0,
6679 units: NumericSuffix::Mm,
6680 }),
6681 y: Expr::Number(Number {
6682 value: 0.0,
6683 units: NumericSuffix::Mm,
6684 }),
6685 },
6686 end: Point2d {
6687 x: Expr::Number(Number {
6688 value: 10.0,
6689 units: NumericSuffix::Mm,
6690 }),
6691 y: Expr::Number(Number {
6692 value: 10.0,
6693 units: NumericSuffix::Mm,
6694 }),
6695 },
6696 construction: None,
6697 };
6698 let segment = SegmentCtor::Line(line_ctor);
6699 let (src_delta, scene_delta) = frontend
6700 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6701 .await
6702 .unwrap();
6703 assert_eq!(
6704 src_delta.text.as_str(),
6705 "sketch001 = sketch(on = XY) {
6706 line(start = [0mm, 0mm], end = [10mm, 10mm])
6707}
6708"
6709 );
6710 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6711
6712 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
6713 assert_eq!(src_delta.text.as_str(), "");
6714 assert_eq!(scene_delta.new_graph.objects.len(), 0);
6715
6716 ctx.close().await;
6717 mock_ctx.close().await;
6718 }
6719
6720 #[tokio::test(flavor = "multi_thread")]
6721 async fn test_delete_sketch_when_sketch_block_uses_variable() {
6722 let initial_source = "s = sketch(on = XY) {}
6723";
6724
6725 let program = Program::parse(initial_source).unwrap().0.unwrap();
6726
6727 let mut frontend = FrontendState::new();
6728
6729 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6730 let mock_ctx = ExecutorContext::new_mock(None).await;
6731 let version = Version(0);
6732
6733 frontend.hack_set_program(&ctx, program).await.unwrap();
6734 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6735 let sketch_id = sketch_object.id;
6736
6737 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
6738 assert_eq!(src_delta.text.as_str(), "");
6739 assert_eq!(scene_delta.new_graph.objects.len(), 0);
6740
6741 ctx.close().await;
6742 mock_ctx.close().await;
6743 }
6744
6745 #[tokio::test(flavor = "multi_thread")]
6746 async fn test_edit_line_when_editing_its_start_point() {
6747 let initial_source = "\
6748sketch(on = XY) {
6749 line(start = [var 1, var 2], end = [var 3, var 4])
6750}
6751";
6752
6753 let program = Program::parse(initial_source).unwrap().0.unwrap();
6754
6755 let mut frontend = FrontendState::new();
6756
6757 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6758 let mock_ctx = ExecutorContext::new_mock(None).await;
6759 let version = Version(0);
6760
6761 frontend.hack_set_program(&ctx, program).await.unwrap();
6762 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6763 let sketch_id = sketch_object.id;
6764 let sketch = expect_sketch(sketch_object);
6765
6766 let point_id = *sketch.segments.first().unwrap();
6767
6768 let point_ctor = PointCtor {
6769 position: Point2d {
6770 x: Expr::Var(Number {
6771 value: 5.0,
6772 units: NumericSuffix::Inch,
6773 }),
6774 y: Expr::Var(Number {
6775 value: 6.0,
6776 units: NumericSuffix::Inch,
6777 }),
6778 },
6779 };
6780 let segments = vec![ExistingSegmentCtor {
6781 id: point_id,
6782 ctor: SegmentCtor::Point(point_ctor),
6783 }];
6784 let (src_delta, scene_delta) = frontend
6785 .edit_segments(&mock_ctx, version, sketch_id, segments)
6786 .await
6787 .unwrap();
6788 assert_eq!(
6789 src_delta.text.as_str(),
6790 "\
6791sketch(on = XY) {
6792 line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
6793}
6794"
6795 );
6796 assert_eq!(scene_delta.new_objects, vec![]);
6797 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6798
6799 ctx.close().await;
6800 mock_ctx.close().await;
6801 }
6802
6803 #[tokio::test(flavor = "multi_thread")]
6804 async fn test_edit_line_when_editing_its_end_point() {
6805 let initial_source = "\
6806sketch(on = XY) {
6807 line(start = [var 1, var 2], end = [var 3, var 4])
6808}
6809";
6810
6811 let program = Program::parse(initial_source).unwrap().0.unwrap();
6812
6813 let mut frontend = FrontendState::new();
6814
6815 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6816 let mock_ctx = ExecutorContext::new_mock(None).await;
6817 let version = Version(0);
6818
6819 frontend.hack_set_program(&ctx, program).await.unwrap();
6820 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6821 let sketch_id = sketch_object.id;
6822 let sketch = expect_sketch(sketch_object);
6823 let point_id = *sketch.segments.get(1).unwrap();
6824
6825 let point_ctor = PointCtor {
6826 position: Point2d {
6827 x: Expr::Var(Number {
6828 value: 5.0,
6829 units: NumericSuffix::Inch,
6830 }),
6831 y: Expr::Var(Number {
6832 value: 6.0,
6833 units: NumericSuffix::Inch,
6834 }),
6835 },
6836 };
6837 let segments = vec![ExistingSegmentCtor {
6838 id: point_id,
6839 ctor: SegmentCtor::Point(point_ctor),
6840 }];
6841 let (src_delta, scene_delta) = frontend
6842 .edit_segments(&mock_ctx, version, sketch_id, segments)
6843 .await
6844 .unwrap();
6845 assert_eq!(
6846 src_delta.text.as_str(),
6847 "\
6848sketch(on = XY) {
6849 line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
6850}
6851"
6852 );
6853 assert_eq!(scene_delta.new_objects, vec![]);
6854 assert_eq!(
6855 scene_delta.new_graph.objects.len(),
6856 5,
6857 "{:#?}",
6858 scene_delta.new_graph.objects
6859 );
6860
6861 ctx.close().await;
6862 mock_ctx.close().await;
6863 }
6864
6865 #[tokio::test(flavor = "multi_thread")]
6866 async fn test_edit_line_with_coincident_feedback() {
6867 let initial_source = "\
6868sketch(on = XY) {
6869 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
6870 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6871 fixed([line1.start, [0, 0]])
6872 coincident([line1.end, line2.start])
6873 equalLength([line1, line2])
6874}
6875";
6876
6877 let program = Program::parse(initial_source).unwrap().0.unwrap();
6878
6879 let mut frontend = FrontendState::new();
6880
6881 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6882 let mock_ctx = ExecutorContext::new_mock(None).await;
6883 let version = Version(0);
6884
6885 frontend.hack_set_program(&ctx, program).await.unwrap();
6886 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6887 let sketch_id = sketch_object.id;
6888 let sketch = expect_sketch(sketch_object);
6889 let line2_end_id = *sketch.segments.get(4).unwrap();
6890
6891 let segments = vec![ExistingSegmentCtor {
6892 id: line2_end_id,
6893 ctor: SegmentCtor::Point(PointCtor {
6894 position: Point2d {
6895 x: Expr::Var(Number {
6896 value: 9.0,
6897 units: NumericSuffix::None,
6898 }),
6899 y: Expr::Var(Number {
6900 value: 10.0,
6901 units: NumericSuffix::None,
6902 }),
6903 },
6904 }),
6905 }];
6906 let (src_delta, scene_delta) = frontend
6907 .edit_segments(&mock_ctx, version, sketch_id, segments)
6908 .await
6909 .unwrap();
6910 assert_eq!(
6911 src_delta.text.as_str(),
6912 "\
6913sketch(on = XY) {
6914 line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
6915 line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
6916 fixed([line1.start, [0, 0]])
6917 coincident([line1.end, line2.start])
6918 equalLength([line1, line2])
6919}
6920"
6921 );
6922 assert_eq!(
6923 scene_delta.new_graph.objects.len(),
6924 11,
6925 "{:#?}",
6926 scene_delta.new_graph.objects
6927 );
6928
6929 ctx.close().await;
6930 mock_ctx.close().await;
6931 }
6932
6933 #[tokio::test(flavor = "multi_thread")]
6934 async fn test_delete_point_without_var() {
6935 let initial_source = "\
6936sketch(on = XY) {
6937 point(at = [var 1, var 2])
6938 point(at = [var 3, var 4])
6939 point(at = [var 5, var 6])
6940}
6941";
6942
6943 let program = Program::parse(initial_source).unwrap().0.unwrap();
6944
6945 let mut frontend = FrontendState::new();
6946
6947 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6948 let mock_ctx = ExecutorContext::new_mock(None).await;
6949 let version = Version(0);
6950
6951 frontend.hack_set_program(&ctx, program).await.unwrap();
6952 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6953 let sketch_id = sketch_object.id;
6954 let sketch = expect_sketch(sketch_object);
6955
6956 let point_id = *sketch.segments.get(1).unwrap();
6957
6958 let (src_delta, scene_delta) = frontend
6959 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
6960 .await
6961 .unwrap();
6962 assert_eq!(
6963 src_delta.text.as_str(),
6964 "\
6965sketch(on = XY) {
6966 point(at = [var 1mm, var 2mm])
6967 point(at = [var 5mm, var 6mm])
6968}
6969"
6970 );
6971 assert_eq!(scene_delta.new_objects, vec![]);
6972 assert_eq!(scene_delta.new_graph.objects.len(), 4);
6973
6974 ctx.close().await;
6975 mock_ctx.close().await;
6976 }
6977
6978 #[tokio::test(flavor = "multi_thread")]
6979 async fn test_delete_point_with_var() {
6980 let initial_source = "\
6981sketch(on = XY) {
6982 point(at = [var 1, var 2])
6983 point1 = point(at = [var 3, var 4])
6984 point(at = [var 5, var 6])
6985}
6986";
6987
6988 let program = Program::parse(initial_source).unwrap().0.unwrap();
6989
6990 let mut frontend = FrontendState::new();
6991
6992 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6993 let mock_ctx = ExecutorContext::new_mock(None).await;
6994 let version = Version(0);
6995
6996 frontend.hack_set_program(&ctx, program).await.unwrap();
6997 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6998 let sketch_id = sketch_object.id;
6999 let sketch = expect_sketch(sketch_object);
7000
7001 let point_id = *sketch.segments.get(1).unwrap();
7002
7003 let (src_delta, scene_delta) = frontend
7004 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7005 .await
7006 .unwrap();
7007 assert_eq!(
7008 src_delta.text.as_str(),
7009 "\
7010sketch(on = XY) {
7011 point(at = [var 1mm, var 2mm])
7012 point(at = [var 5mm, var 6mm])
7013}
7014"
7015 );
7016 assert_eq!(scene_delta.new_objects, vec![]);
7017 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7018
7019 ctx.close().await;
7020 mock_ctx.close().await;
7021 }
7022
7023 #[tokio::test(flavor = "multi_thread")]
7024 async fn test_delete_multiple_points() {
7025 let initial_source = "\
7026sketch(on = XY) {
7027 point(at = [var 1, var 2])
7028 point1 = point(at = [var 3, var 4])
7029 point(at = [var 5, var 6])
7030}
7031";
7032
7033 let program = Program::parse(initial_source).unwrap().0.unwrap();
7034
7035 let mut frontend = FrontendState::new();
7036
7037 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7038 let mock_ctx = ExecutorContext::new_mock(None).await;
7039 let version = Version(0);
7040
7041 frontend.hack_set_program(&ctx, program).await.unwrap();
7042 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7043 let sketch_id = sketch_object.id;
7044
7045 let sketch = expect_sketch(sketch_object);
7046
7047 let point1_id = *sketch.segments.first().unwrap();
7048 let point2_id = *sketch.segments.get(1).unwrap();
7049
7050 let (src_delta, scene_delta) = frontend
7051 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
7052 .await
7053 .unwrap();
7054 assert_eq!(
7055 src_delta.text.as_str(),
7056 "\
7057sketch(on = XY) {
7058 point(at = [var 5mm, var 6mm])
7059}
7060"
7061 );
7062 assert_eq!(scene_delta.new_objects, vec![]);
7063 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7064
7065 ctx.close().await;
7066 mock_ctx.close().await;
7067 }
7068
7069 #[tokio::test(flavor = "multi_thread")]
7070 async fn test_delete_coincident_constraint() {
7071 let initial_source = "\
7072sketch(on = XY) {
7073 point1 = point(at = [var 1, var 2])
7074 point2 = point(at = [var 3, var 4])
7075 coincident([point1, point2])
7076 point(at = [var 5, var 6])
7077}
7078";
7079
7080 let program = Program::parse(initial_source).unwrap().0.unwrap();
7081
7082 let mut frontend = FrontendState::new();
7083
7084 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7085 let mock_ctx = ExecutorContext::new_mock(None).await;
7086 let version = Version(0);
7087
7088 frontend.hack_set_program(&ctx, program).await.unwrap();
7089 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7090 let sketch_id = sketch_object.id;
7091 let sketch = expect_sketch(sketch_object);
7092
7093 let coincident_id = *sketch.constraints.first().unwrap();
7094
7095 let (src_delta, scene_delta) = frontend
7096 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7097 .await
7098 .unwrap();
7099 assert_eq!(
7100 src_delta.text.as_str(),
7101 "\
7102sketch(on = XY) {
7103 point1 = point(at = [var 1mm, var 2mm])
7104 point2 = point(at = [var 3mm, var 4mm])
7105 point(at = [var 5mm, var 6mm])
7106}
7107"
7108 );
7109 assert_eq!(scene_delta.new_objects, vec![]);
7110 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7111
7112 ctx.close().await;
7113 mock_ctx.close().await;
7114 }
7115
7116 #[tokio::test(flavor = "multi_thread")]
7117 async fn test_delete_line_cascades_to_coincident_constraint() {
7118 let initial_source = "\
7119sketch(on = XY) {
7120 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7121 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7122 coincident([line1.end, line2.start])
7123}
7124";
7125
7126 let program = Program::parse(initial_source).unwrap().0.unwrap();
7127
7128 let mut frontend = FrontendState::new();
7129
7130 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7131 let mock_ctx = ExecutorContext::new_mock(None).await;
7132 let version = Version(0);
7133
7134 frontend.hack_set_program(&ctx, program).await.unwrap();
7135 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7136 let sketch_id = sketch_object.id;
7137 let sketch = expect_sketch(sketch_object);
7138 let line_id = *sketch.segments.get(5).unwrap();
7139
7140 let (src_delta, scene_delta) = frontend
7141 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7142 .await
7143 .unwrap();
7144 assert_eq!(
7145 src_delta.text.as_str(),
7146 "\
7147sketch(on = XY) {
7148 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7149}
7150"
7151 );
7152 assert_eq!(
7153 scene_delta.new_graph.objects.len(),
7154 5,
7155 "{:#?}",
7156 scene_delta.new_graph.objects
7157 );
7158
7159 ctx.close().await;
7160 mock_ctx.close().await;
7161 }
7162
7163 #[tokio::test(flavor = "multi_thread")]
7164 async fn test_delete_line_cascades_to_distance_constraint() {
7165 let initial_source = "\
7166sketch(on = XY) {
7167 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7168 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7169 distance([line1.end, line2.start]) == 10mm
7170}
7171";
7172
7173 let program = Program::parse(initial_source).unwrap().0.unwrap();
7174
7175 let mut frontend = FrontendState::new();
7176
7177 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7178 let mock_ctx = ExecutorContext::new_mock(None).await;
7179 let version = Version(0);
7180
7181 frontend.hack_set_program(&ctx, program).await.unwrap();
7182 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7183 let sketch_id = sketch_object.id;
7184 let sketch = expect_sketch(sketch_object);
7185 let line_id = *sketch.segments.get(5).unwrap();
7186
7187 let (src_delta, scene_delta) = frontend
7188 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7189 .await
7190 .unwrap();
7191 assert_eq!(
7192 src_delta.text.as_str(),
7193 "\
7194sketch(on = XY) {
7195 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7196}
7197"
7198 );
7199 assert_eq!(
7200 scene_delta.new_graph.objects.len(),
7201 5,
7202 "{:#?}",
7203 scene_delta.new_graph.objects
7204 );
7205
7206 ctx.close().await;
7207 mock_ctx.close().await;
7208 }
7209
7210 #[tokio::test(flavor = "multi_thread")]
7211 async fn test_delete_point_preserves_multiline_coincident_constraint() {
7212 let initial_source = "\
7213sketch(on = XY) {
7214 point1 = point(at = [var 1, var 2])
7215 point2 = point(at = [var 3, var 4])
7216 point3 = point(at = [var 5, var 6])
7217 coincident([point1, point2, point3])
7218}
7219";
7220
7221 let program = Program::parse(initial_source).unwrap().0.unwrap();
7222
7223 let mut frontend = FrontendState::new();
7224
7225 let mock_ctx = ExecutorContext::new_mock(None).await;
7226 let version = Version(0);
7227
7228 frontend.program = program.clone();
7229 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7230 frontend.update_state_after_exec(outcome, true);
7231 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7232 let sketch_id = sketch_object.id;
7233 let sketch = expect_sketch(sketch_object);
7234 let point3_id = *sketch.segments.get(2).unwrap();
7235
7236 let (src_delta, scene_delta) = frontend
7237 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
7238 .await
7239 .unwrap();
7240 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
7241 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
7242 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
7243 assert!(
7244 src_delta.text.contains("coincident([point1, point2])"),
7245 "{}",
7246 src_delta.text
7247 );
7248
7249 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7250 let sketch = expect_sketch(sketch_object);
7251 assert_eq!(sketch.segments.len(), 2);
7252 assert_eq!(sketch.constraints.len(), 1);
7253
7254 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7255 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7256 panic!("Expected constraint object");
7257 };
7258 let Constraint::Coincident(coincident) = constraint else {
7259 panic!("Expected coincident constraint");
7260 };
7261 assert_eq!(
7262 coincident.segments,
7263 sketch
7264 .segments
7265 .iter()
7266 .copied()
7267 .map(Into::into)
7268 .collect::<Vec<ConstraintSegment>>()
7269 );
7270
7271 mock_ctx.close().await;
7272 }
7273
7274 #[tokio::test(flavor = "multi_thread")]
7275 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
7276 let initial_source = "\
7277sketch(on = XY) {
7278 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7279 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7280 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7281 equalLength([line1, line2, line3])
7282}
7283";
7284
7285 let program = Program::parse(initial_source).unwrap().0.unwrap();
7286
7287 let mut frontend = FrontendState::new();
7288
7289 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7290 let mock_ctx = ExecutorContext::new_mock(None).await;
7291 let version = Version(0);
7292
7293 frontend.hack_set_program(&ctx, program).await.unwrap();
7294 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7295 let sketch_id = sketch_object.id;
7296 let sketch = expect_sketch(sketch_object);
7297 let line3_id = *sketch.segments.get(8).unwrap();
7298
7299 let (src_delta, scene_delta) = frontend
7300 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
7301 .await
7302 .unwrap();
7303 assert_eq!(
7304 src_delta.text.as_str(),
7305 "\
7306sketch(on = XY) {
7307 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7308 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7309 equalLength([line1, line2])
7310}
7311"
7312 );
7313
7314 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7315 let sketch = expect_sketch(sketch_object);
7316 assert_eq!(sketch.constraints.len(), 1);
7317
7318 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7319 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7320 panic!("Expected constraint object");
7321 };
7322 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
7323 panic!("Expected lines equal length constraint");
7324 };
7325 assert_eq!(lines_equal_length.lines.len(), 2);
7326
7327 ctx.close().await;
7328 mock_ctx.close().await;
7329 }
7330
7331 #[tokio::test(flavor = "multi_thread")]
7332 async fn test_delete_line_preserves_multiline_coincident_constraint() {
7333 let initial_source = "\
7334sketch(on = XY) {
7335 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7336 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7337 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7338 coincident([line1.end, line2.start, line3.start])
7339}
7340";
7341
7342 let program = Program::parse(initial_source).unwrap().0.unwrap();
7343
7344 let mut frontend = FrontendState::new();
7345
7346 let mock_ctx = ExecutorContext::new_mock(None).await;
7347 let version = Version(0);
7348
7349 frontend.program = program.clone();
7350 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7351 frontend.update_state_after_exec(outcome, true);
7352 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7353 let sketch_id = sketch_object.id;
7354 let sketch = expect_sketch(sketch_object);
7355 let line1_id = *sketch.segments.get(2).unwrap();
7356
7357 let (src_delta, scene_delta) = frontend
7358 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7359 .await
7360 .unwrap();
7361 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
7362 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
7363 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
7364 assert!(
7365 src_delta.text.contains("coincident([line2.start, line3.start])"),
7366 "{}",
7367 src_delta.text
7368 );
7369
7370 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7371 let sketch = expect_sketch(sketch_object);
7372 assert_eq!(sketch.constraints.len(), 1);
7373
7374 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7375 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7376 panic!("Expected constraint object");
7377 };
7378 let Constraint::Coincident(coincident) = constraint else {
7379 panic!("Expected coincident constraint");
7380 };
7381 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
7382 assert_eq!(coincident.segments, remaining_segments);
7383
7384 mock_ctx.close().await;
7385 }
7386
7387 #[tokio::test(flavor = "multi_thread")]
7388 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
7389 let initial_source = "\
7390sketch(on = XY) {
7391 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7392 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7393 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7394 equalLength([line1, line2, line3])
7395}
7396";
7397
7398 let program = Program::parse(initial_source).unwrap().0.unwrap();
7399
7400 let mut frontend = FrontendState::new();
7401
7402 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7403 let mock_ctx = ExecutorContext::new_mock(None).await;
7404 let version = Version(0);
7405
7406 frontend.hack_set_program(&ctx, program).await.unwrap();
7407 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7408 let sketch_id = sketch_object.id;
7409 let sketch = expect_sketch(sketch_object);
7410 let line2_id = *sketch.segments.get(5).unwrap();
7411 let line3_id = *sketch.segments.get(8).unwrap();
7412
7413 let (src_delta, scene_delta) = frontend
7414 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
7415 .await
7416 .unwrap();
7417 assert_eq!(
7418 src_delta.text.as_str(),
7419 "\
7420sketch(on = XY) {
7421 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7422}
7423"
7424 );
7425
7426 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7427 let sketch = expect_sketch(sketch_object);
7428 assert!(sketch.constraints.is_empty());
7429
7430 ctx.close().await;
7431 mock_ctx.close().await;
7432 }
7433
7434 #[tokio::test(flavor = "multi_thread")]
7435 async fn test_delete_line_preserves_multiline_parallel_constraint() {
7436 let initial_source = "\
7437sketch(on = XY) {
7438 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7439 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7440 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7441 parallel([line1, line2, line3])
7442}
7443";
7444
7445 let program = Program::parse(initial_source).unwrap().0.unwrap();
7446
7447 let mut frontend = FrontendState::new();
7448
7449 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7450 let mock_ctx = ExecutorContext::new_mock(None).await;
7451 let version = Version(0);
7452
7453 frontend.hack_set_program(&ctx, program).await.unwrap();
7454 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7455 let sketch_id = sketch_object.id;
7456 let sketch = expect_sketch(sketch_object);
7457 let line3_id = *sketch.segments.get(8).unwrap();
7458
7459 let (src_delta, scene_delta) = frontend
7460 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
7461 .await
7462 .unwrap();
7463 assert_eq!(
7464 src_delta.text.as_str(),
7465 "\
7466sketch(on = XY) {
7467 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7468 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7469 parallel([line1, line2])
7470}
7471"
7472 );
7473
7474 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7475 let sketch = expect_sketch(sketch_object);
7476 assert_eq!(sketch.constraints.len(), 1);
7477
7478 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7479 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7480 panic!("Expected constraint object");
7481 };
7482 let Constraint::Parallel(parallel) = constraint else {
7483 panic!("Expected parallel constraint");
7484 };
7485 assert_eq!(parallel.lines.len(), 2);
7486
7487 ctx.close().await;
7488 mock_ctx.close().await;
7489 }
7490
7491 #[tokio::test(flavor = "multi_thread")]
7492 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
7493 let initial_source = "\
7494sketch(on = XY) {
7495 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7496 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7497 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7498 parallel([line1, line2, line3])
7499}
7500";
7501
7502 let program = Program::parse(initial_source).unwrap().0.unwrap();
7503
7504 let mut frontend = FrontendState::new();
7505
7506 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7507 let mock_ctx = ExecutorContext::new_mock(None).await;
7508 let version = Version(0);
7509
7510 frontend.hack_set_program(&ctx, program).await.unwrap();
7511 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7512 let sketch_id = sketch_object.id;
7513 let sketch = expect_sketch(sketch_object);
7514 let line2_id = *sketch.segments.get(5).unwrap();
7515 let line3_id = *sketch.segments.get(8).unwrap();
7516
7517 let (src_delta, scene_delta) = frontend
7518 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
7519 .await
7520 .unwrap();
7521 assert_eq!(
7522 src_delta.text.as_str(),
7523 "\
7524sketch(on = XY) {
7525 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7526}
7527"
7528 );
7529
7530 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7531 let sketch = expect_sketch(sketch_object);
7532 assert!(sketch.constraints.is_empty());
7533
7534 ctx.close().await;
7535 mock_ctx.close().await;
7536 }
7537
7538 #[tokio::test(flavor = "multi_thread")]
7539 async fn test_delete_line_line_coincident_constraint() {
7540 let initial_source = "\
7541sketch(on = XY) {
7542 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7543 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7544 coincident([line1, line2])
7545}
7546";
7547
7548 let program = Program::parse(initial_source).unwrap().0.unwrap();
7549
7550 let mut frontend = FrontendState::new();
7551
7552 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7553 let mock_ctx = ExecutorContext::new_mock(None).await;
7554 let version = Version(0);
7555
7556 frontend.hack_set_program(&ctx, program).await.unwrap();
7557 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7558 let sketch_id = sketch_object.id;
7559 let sketch = expect_sketch(sketch_object);
7560
7561 let coincident_id = *sketch.constraints.first().unwrap();
7562
7563 let (src_delta, scene_delta) = frontend
7564 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7565 .await
7566 .unwrap();
7567 assert_eq!(
7568 src_delta.text.as_str(),
7569 "\
7570sketch(on = XY) {
7571 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7572 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7573}
7574"
7575 );
7576 assert_eq!(scene_delta.new_objects, vec![]);
7577 assert_eq!(scene_delta.new_graph.objects.len(), 8);
7578
7579 ctx.close().await;
7580 mock_ctx.close().await;
7581 }
7582
7583 #[tokio::test(flavor = "multi_thread")]
7584 async fn test_two_points_coincident() {
7585 let initial_source = "\
7586sketch(on = XY) {
7587 point1 = point(at = [var 1, var 2])
7588 point(at = [3, 4])
7589}
7590";
7591
7592 let program = Program::parse(initial_source).unwrap().0.unwrap();
7593
7594 let mut frontend = FrontendState::new();
7595
7596 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7597 let mock_ctx = ExecutorContext::new_mock(None).await;
7598 let version = Version(0);
7599
7600 frontend.hack_set_program(&ctx, program).await.unwrap();
7601 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7602 let sketch_id = sketch_object.id;
7603 let sketch = expect_sketch(sketch_object);
7604 let point0_id = *sketch.segments.first().unwrap();
7605 let point1_id = *sketch.segments.get(1).unwrap();
7606
7607 let constraint = Constraint::Coincident(Coincident {
7608 segments: vec![point0_id.into(), point1_id.into()],
7609 });
7610 let (src_delta, scene_delta) = frontend
7611 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7612 .await
7613 .unwrap();
7614 assert_eq!(
7615 src_delta.text.as_str(),
7616 "\
7617sketch(on = XY) {
7618 point1 = point(at = [var 1, var 2])
7619 point2 = point(at = [3, 4])
7620 coincident([point1, point2])
7621}
7622"
7623 );
7624 assert_eq!(
7625 scene_delta.new_graph.objects.len(),
7626 5,
7627 "{:#?}",
7628 scene_delta.new_graph.objects
7629 );
7630
7631 ctx.close().await;
7632 mock_ctx.close().await;
7633 }
7634
7635 #[tokio::test(flavor = "multi_thread")]
7636 async fn test_three_points_coincident() {
7637 let initial_source = "\
7638sketch(on = XY) {
7639 point1 = point(at = [var 1, var 2])
7640 point(at = [var 3, var 4])
7641 point(at = [var 5, var 6])
7642}
7643";
7644
7645 let program = Program::parse(initial_source).unwrap().0.unwrap();
7646
7647 let mut frontend = FrontendState::new();
7648
7649 let mock_ctx = ExecutorContext::new_mock(None).await;
7650 let version = Version(0);
7651
7652 frontend.program = program.clone();
7653 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7654 frontend.update_state_after_exec(outcome, true);
7655 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7656 let sketch_id = sketch_object.id;
7657 let sketch = expect_sketch(sketch_object);
7658 let segments = sketch
7659 .segments
7660 .iter()
7661 .take(3)
7662 .copied()
7663 .map(Into::into)
7664 .collect::<Vec<ConstraintSegment>>();
7665
7666 let constraint = Constraint::Coincident(Coincident {
7667 segments: segments.clone(),
7668 });
7669 let (src_delta, scene_delta) = frontend
7670 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7671 .await
7672 .unwrap();
7673 assert_eq!(
7674 src_delta.text.as_str(),
7675 "\
7676sketch(on = XY) {
7677 point1 = point(at = [var 1, var 2])
7678 point2 = point(at = [var 3, var 4])
7679 point3 = point(at = [var 5, var 6])
7680 coincident([point1, point2, point3])
7681}
7682"
7683 );
7684
7685 let constraint_object = scene_delta
7686 .new_graph
7687 .objects
7688 .iter()
7689 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
7690 .unwrap();
7691
7692 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7693 panic!("expected a constraint object");
7694 };
7695
7696 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
7697
7698 mock_ctx.close().await;
7699 }
7700
7701 #[tokio::test(flavor = "multi_thread")]
7702 async fn test_source_with_three_point_coincident_tracks_all_segments() {
7703 let initial_source = "\
7704sketch(on = XY) {
7705 point1 = point(at = [var 1, var 2])
7706 point2 = point(at = [var 3, var 4])
7707 point3 = point(at = [var 5, var 6])
7708 coincident([point1, point2, point3])
7709}
7710";
7711
7712 let program = Program::parse(initial_source).unwrap().0.unwrap();
7713
7714 let mut frontend = FrontendState::new();
7715
7716 let ctx = ExecutorContext::new_mock(None).await;
7717 frontend.program = program.clone();
7718 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7719 frontend.update_state_after_exec(outcome, true);
7720
7721 let constraint_object = frontend
7722 .scene_graph
7723 .objects
7724 .iter()
7725 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
7726 .unwrap();
7727 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7728 panic!("expected a constraint object");
7729 };
7730
7731 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7732 let sketch = expect_sketch(sketch_object);
7733 let expected_segments = sketch
7734 .segments
7735 .iter()
7736 .take(3)
7737 .copied()
7738 .map(Into::into)
7739 .collect::<Vec<ConstraintSegment>>();
7740
7741 assert_eq!(
7742 constraint,
7743 &Constraint::Coincident(Coincident {
7744 segments: expected_segments,
7745 })
7746 );
7747
7748 ctx.close().await;
7749 }
7750
7751 #[tokio::test(flavor = "multi_thread")]
7752 async fn test_point_origin_coincident_preserves_order() {
7753 let initial_source = "\
7754sketch(on = XY) {
7755 point(at = [var 1, var 2])
7756}
7757";
7758
7759 for (origin_first, expected_source) in [
7760 (
7761 true,
7762 "\
7763sketch(on = XY) {
7764 point1 = point(at = [var 1, var 2])
7765 coincident([ORIGIN, point1])
7766}
7767",
7768 ),
7769 (
7770 false,
7771 "\
7772sketch(on = XY) {
7773 point1 = point(at = [var 1, var 2])
7774 coincident([point1, ORIGIN])
7775}
7776",
7777 ),
7778 ] {
7779 let program = Program::parse(initial_source).unwrap().0.unwrap();
7780
7781 let mut frontend = FrontendState::new();
7782
7783 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7784 let mock_ctx = ExecutorContext::new_mock(None).await;
7785 let version = Version(0);
7786
7787 frontend.hack_set_program(&ctx, program).await.unwrap();
7788 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7789 let sketch_id = sketch_object.id;
7790 let sketch = expect_sketch(sketch_object);
7791 let point_id = *sketch.segments.first().unwrap();
7792
7793 let segments = if origin_first {
7794 vec![ConstraintSegment::ORIGIN, point_id.into()]
7795 } else {
7796 vec![point_id.into(), ConstraintSegment::ORIGIN]
7797 };
7798 let constraint = Constraint::Coincident(Coincident {
7799 segments: segments.clone(),
7800 });
7801 let (src_delta, scene_delta) = frontend
7802 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7803 .await
7804 .unwrap();
7805 assert_eq!(src_delta.text.as_str(), expected_source);
7806
7807 let constraint_object = scene_delta
7808 .new_graph
7809 .objects
7810 .iter()
7811 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
7812 .unwrap();
7813
7814 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7815 panic!("expected a constraint object");
7816 };
7817
7818 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
7819
7820 ctx.close().await;
7821 mock_ctx.close().await;
7822 }
7823 }
7824
7825 #[tokio::test(flavor = "multi_thread")]
7826 async fn test_coincident_of_line_end_points() {
7827 let initial_source = "\
7828sketch(on = XY) {
7829 line(start = [var 1, var 2], end = [var 3, var 4])
7830 line(start = [var 5, var 6], end = [var 7, var 8])
7831}
7832";
7833
7834 let program = Program::parse(initial_source).unwrap().0.unwrap();
7835
7836 let mut frontend = FrontendState::new();
7837
7838 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7839 let mock_ctx = ExecutorContext::new_mock(None).await;
7840 let version = Version(0);
7841
7842 frontend.hack_set_program(&ctx, program).await.unwrap();
7843 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7844 let sketch_id = sketch_object.id;
7845 let sketch = expect_sketch(sketch_object);
7846 let point0_id = *sketch.segments.get(1).unwrap();
7847 let point1_id = *sketch.segments.get(3).unwrap();
7848
7849 let constraint = Constraint::Coincident(Coincident {
7850 segments: vec![point0_id.into(), point1_id.into()],
7851 });
7852 let (src_delta, scene_delta) = frontend
7853 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7854 .await
7855 .unwrap();
7856 assert_eq!(
7857 src_delta.text.as_str(),
7858 "\
7859sketch(on = XY) {
7860 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7861 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7862 coincident([line1.end, line2.start])
7863}
7864"
7865 );
7866 assert_eq!(
7867 scene_delta.new_graph.objects.len(),
7868 9,
7869 "{:#?}",
7870 scene_delta.new_graph.objects
7871 );
7872
7873 ctx.close().await;
7874 mock_ctx.close().await;
7875 }
7876
7877 #[tokio::test(flavor = "multi_thread")]
7878 async fn test_coincident_of_line_point_and_circle_segment() {
7879 let initial_source = "\
7880sketch(on = XY) {
7881 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7882 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
7883}
7884";
7885 let program = Program::parse(initial_source).unwrap().0.unwrap();
7886 let mut frontend = FrontendState::new();
7887
7888 let mock_ctx = ExecutorContext::new_mock(None).await;
7889 let version = Version(0);
7890
7891 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7892 frontend.program = program;
7893 frontend.update_state_after_exec(outcome, true);
7894 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
7895 let sketch_id = sketch_object.id;
7896 let sketch = expect_sketch(sketch_object);
7897
7898 let circle_id = sketch
7899 .segments
7900 .iter()
7901 .copied()
7902 .find(|seg_id| {
7903 matches!(
7904 &frontend.scene_graph.objects[seg_id.0].kind,
7905 ObjectKind::Segment {
7906 segment: Segment::Circle(_)
7907 }
7908 )
7909 })
7910 .expect("Expected a circle segment in sketch");
7911 let line_id = sketch
7912 .segments
7913 .iter()
7914 .copied()
7915 .find(|seg_id| {
7916 matches!(
7917 &frontend.scene_graph.objects[seg_id.0].kind,
7918 ObjectKind::Segment {
7919 segment: Segment::Line(_)
7920 }
7921 )
7922 })
7923 .expect("Expected a line segment in sketch");
7924
7925 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
7926 ObjectKind::Segment {
7927 segment: Segment::Line(line),
7928 } => line.start,
7929 _ => panic!("Expected line segment object"),
7930 };
7931
7932 let constraint = Constraint::Coincident(Coincident {
7933 segments: vec![line_start_point_id.into(), circle_id.into()],
7934 });
7935 let (src_delta, _scene_delta) = frontend
7936 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7937 .await
7938 .unwrap();
7939 assert_eq!(
7940 src_delta.text.as_str(),
7941 "\
7942sketch(on = XY) {
7943 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7944 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
7945 coincident([line1.start, circle1])
7946}
7947"
7948 );
7949
7950 mock_ctx.close().await;
7951 }
7952
7953 #[tokio::test(flavor = "multi_thread")]
7954 async fn test_invalid_coincident_arc_and_line_preserves_state() {
7955 let program = Program::empty();
7963
7964 let mut frontend = FrontendState::new();
7965 frontend.program = program;
7966
7967 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7968 let mock_ctx = ExecutorContext::new_mock(None).await;
7969 let version = Version(0);
7970
7971 let sketch_args = SketchCtor {
7972 on: Plane::Default(PlaneName::Xy),
7973 };
7974 let (_src_delta, _scene_delta, sketch_id) = frontend
7975 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7976 .await
7977 .unwrap();
7978
7979 let arc_ctor = ArcCtor {
7981 start: Point2d {
7982 x: Expr::Var(Number {
7983 value: 0.0,
7984 units: NumericSuffix::Mm,
7985 }),
7986 y: Expr::Var(Number {
7987 value: 0.0,
7988 units: NumericSuffix::Mm,
7989 }),
7990 },
7991 end: Point2d {
7992 x: Expr::Var(Number {
7993 value: 10.0,
7994 units: NumericSuffix::Mm,
7995 }),
7996 y: Expr::Var(Number {
7997 value: 10.0,
7998 units: NumericSuffix::Mm,
7999 }),
8000 },
8001 center: Point2d {
8002 x: Expr::Var(Number {
8003 value: 10.0,
8004 units: NumericSuffix::Mm,
8005 }),
8006 y: Expr::Var(Number {
8007 value: 0.0,
8008 units: NumericSuffix::Mm,
8009 }),
8010 },
8011 construction: None,
8012 };
8013 let (_src_delta, scene_delta) = frontend
8014 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
8015 .await
8016 .unwrap();
8017 let arc_id = *scene_delta.new_objects.last().unwrap();
8019
8020 let line_ctor = LineCtor {
8022 start: Point2d {
8023 x: Expr::Var(Number {
8024 value: 20.0,
8025 units: NumericSuffix::Mm,
8026 }),
8027 y: Expr::Var(Number {
8028 value: 0.0,
8029 units: NumericSuffix::Mm,
8030 }),
8031 },
8032 end: Point2d {
8033 x: Expr::Var(Number {
8034 value: 30.0,
8035 units: NumericSuffix::Mm,
8036 }),
8037 y: Expr::Var(Number {
8038 value: 10.0,
8039 units: NumericSuffix::Mm,
8040 }),
8041 },
8042 construction: None,
8043 };
8044 let (_src_delta, scene_delta) = frontend
8045 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
8046 .await
8047 .unwrap();
8048 let line_id = *scene_delta.new_objects.last().unwrap();
8050
8051 let constraint = Constraint::Coincident(Coincident {
8054 segments: vec![arc_id.into(), line_id.into()],
8055 });
8056 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
8057
8058 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
8060
8061 let sketch_object_after =
8064 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
8065 let sketch_after = expect_sketch(sketch_object_after);
8066
8067 assert!(
8069 sketch_after.segments.contains(&arc_id),
8070 "Arc segment should still exist after failed constraint"
8071 );
8072 assert!(
8073 sketch_after.segments.contains(&line_id),
8074 "Line segment should still exist after failed constraint"
8075 );
8076
8077 let arc_obj = frontend
8079 .scene_graph
8080 .objects
8081 .get(arc_id.0)
8082 .expect("Arc object should still be accessible");
8083 let line_obj = frontend
8084 .scene_graph
8085 .objects
8086 .get(line_id.0)
8087 .expect("Line object should still be accessible");
8088
8089 match &arc_obj.kind {
8092 ObjectKind::Segment {
8093 segment: Segment::Arc(_),
8094 } => {}
8095 _ => panic!("Arc object should still be an arc segment"),
8096 }
8097 match &line_obj.kind {
8098 ObjectKind::Segment {
8099 segment: Segment::Line(_),
8100 } => {}
8101 _ => panic!("Line object should still be a line segment"),
8102 }
8103
8104 ctx.close().await;
8105 mock_ctx.close().await;
8106 }
8107
8108 #[tokio::test(flavor = "multi_thread")]
8109 async fn test_distance_two_points() {
8110 let initial_source = "\
8111sketch(on = XY) {
8112 point(at = [var 1, var 2])
8113 point(at = [var 3, var 4])
8114}
8115";
8116
8117 let program = Program::parse(initial_source).unwrap().0.unwrap();
8118
8119 let mut frontend = FrontendState::new();
8120
8121 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8122 let mock_ctx = ExecutorContext::new_mock(None).await;
8123 let version = Version(0);
8124
8125 frontend.hack_set_program(&ctx, program).await.unwrap();
8126 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8127 let sketch_id = sketch_object.id;
8128 let sketch = expect_sketch(sketch_object);
8129 let point0_id = *sketch.segments.first().unwrap();
8130 let point1_id = *sketch.segments.get(1).unwrap();
8131
8132 let constraint = Constraint::Distance(Distance {
8133 points: vec![point0_id.into(), point1_id.into()],
8134 distance: Number {
8135 value: 2.0,
8136 units: NumericSuffix::Mm,
8137 },
8138 source: Default::default(),
8139 });
8140 let (src_delta, scene_delta) = frontend
8141 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8142 .await
8143 .unwrap();
8144 assert_eq!(
8145 src_delta.text.as_str(),
8146 "\
8148sketch(on = XY) {
8149 point1 = point(at = [var 1, var 2])
8150 point2 = point(at = [var 3, var 4])
8151 distance([point1, point2]) == 2mm
8152}
8153"
8154 );
8155 assert_eq!(
8156 scene_delta.new_graph.objects.len(),
8157 5,
8158 "{:#?}",
8159 scene_delta.new_graph.objects
8160 );
8161
8162 ctx.close().await;
8163 mock_ctx.close().await;
8164 }
8165
8166 #[tokio::test(flavor = "multi_thread")]
8167 async fn test_horizontal_distance_two_points() {
8168 let initial_source = "\
8169sketch(on = XY) {
8170 point(at = [var 1, var 2])
8171 point(at = [var 3, var 4])
8172}
8173";
8174
8175 let program = Program::parse(initial_source).unwrap().0.unwrap();
8176
8177 let mut frontend = FrontendState::new();
8178
8179 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8180 let mock_ctx = ExecutorContext::new_mock(None).await;
8181 let version = Version(0);
8182
8183 frontend.hack_set_program(&ctx, program).await.unwrap();
8184 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8185 let sketch_id = sketch_object.id;
8186 let sketch = expect_sketch(sketch_object);
8187 let point0_id = *sketch.segments.first().unwrap();
8188 let point1_id = *sketch.segments.get(1).unwrap();
8189
8190 let constraint = Constraint::HorizontalDistance(Distance {
8191 points: vec![point0_id.into(), point1_id.into()],
8192 distance: Number {
8193 value: 2.0,
8194 units: NumericSuffix::Mm,
8195 },
8196 source: Default::default(),
8197 });
8198 let (src_delta, scene_delta) = frontend
8199 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8200 .await
8201 .unwrap();
8202 assert_eq!(
8203 src_delta.text.as_str(),
8204 "\
8206sketch(on = XY) {
8207 point1 = point(at = [var 1, var 2])
8208 point2 = point(at = [var 3, var 4])
8209 horizontalDistance([point1, point2]) == 2mm
8210}
8211"
8212 );
8213 assert_eq!(
8214 scene_delta.new_graph.objects.len(),
8215 5,
8216 "{:#?}",
8217 scene_delta.new_graph.objects
8218 );
8219
8220 ctx.close().await;
8221 mock_ctx.close().await;
8222 }
8223
8224 #[tokio::test(flavor = "multi_thread")]
8225 async fn test_radius_single_arc_segment() {
8226 let initial_source = "\
8227sketch(on = XY) {
8228 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8229}
8230";
8231
8232 let program = Program::parse(initial_source).unwrap().0.unwrap();
8233
8234 let mut frontend = FrontendState::new();
8235
8236 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8237 let mock_ctx = ExecutorContext::new_mock(None).await;
8238 let version = Version(0);
8239
8240 frontend.hack_set_program(&ctx, program).await.unwrap();
8241 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8242 let sketch_id = sketch_object.id;
8243 let sketch = expect_sketch(sketch_object);
8244 let arc_id = sketch
8246 .segments
8247 .iter()
8248 .find(|&seg_id| {
8249 let obj = frontend.scene_graph.objects.get(seg_id.0);
8250 matches!(
8251 obj.map(|o| &o.kind),
8252 Some(ObjectKind::Segment {
8253 segment: Segment::Arc(_)
8254 })
8255 )
8256 })
8257 .unwrap();
8258
8259 let constraint = Constraint::Radius(Radius {
8260 arc: *arc_id,
8261 radius: Number {
8262 value: 5.0,
8263 units: NumericSuffix::Mm,
8264 },
8265 source: Default::default(),
8266 });
8267 let (src_delta, scene_delta) = frontend
8268 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8269 .await
8270 .unwrap();
8271 assert_eq!(
8272 src_delta.text.as_str(),
8273 "\
8275sketch(on = XY) {
8276 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8277 radius(arc1) == 5mm
8278}
8279"
8280 );
8281 assert_eq!(
8282 scene_delta.new_graph.objects.len(),
8283 7, "{:#?}",
8285 scene_delta.new_graph.objects
8286 );
8287
8288 ctx.close().await;
8289 mock_ctx.close().await;
8290 }
8291
8292 #[tokio::test(flavor = "multi_thread")]
8293 async fn test_vertical_distance_two_points() {
8294 let initial_source = "\
8295sketch(on = XY) {
8296 point(at = [var 1, var 2])
8297 point(at = [var 3, var 4])
8298}
8299";
8300
8301 let program = Program::parse(initial_source).unwrap().0.unwrap();
8302
8303 let mut frontend = FrontendState::new();
8304
8305 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8306 let mock_ctx = ExecutorContext::new_mock(None).await;
8307 let version = Version(0);
8308
8309 frontend.hack_set_program(&ctx, program).await.unwrap();
8310 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8311 let sketch_id = sketch_object.id;
8312 let sketch = expect_sketch(sketch_object);
8313 let point0_id = *sketch.segments.first().unwrap();
8314 let point1_id = *sketch.segments.get(1).unwrap();
8315
8316 let constraint = Constraint::VerticalDistance(Distance {
8317 points: vec![point0_id.into(), point1_id.into()],
8318 distance: Number {
8319 value: 2.0,
8320 units: NumericSuffix::Mm,
8321 },
8322 source: Default::default(),
8323 });
8324 let (src_delta, scene_delta) = frontend
8325 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8326 .await
8327 .unwrap();
8328 assert_eq!(
8329 src_delta.text.as_str(),
8330 "\
8332sketch(on = XY) {
8333 point1 = point(at = [var 1, var 2])
8334 point2 = point(at = [var 3, var 4])
8335 verticalDistance([point1, point2]) == 2mm
8336}
8337"
8338 );
8339 assert_eq!(
8340 scene_delta.new_graph.objects.len(),
8341 5,
8342 "{:#?}",
8343 scene_delta.new_graph.objects
8344 );
8345
8346 ctx.close().await;
8347 mock_ctx.close().await;
8348 }
8349
8350 #[tokio::test(flavor = "multi_thread")]
8351 async fn test_add_fixed_standalone_point() {
8352 let initial_source = "\
8353sketch(on = XY) {
8354 point(at = [var 1, var 2])
8355}
8356";
8357
8358 let program = Program::parse(initial_source).unwrap().0.unwrap();
8359
8360 let mut frontend = FrontendState::new();
8361
8362 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8363 let mock_ctx = ExecutorContext::new_mock(None).await;
8364 let version = Version(0);
8365
8366 frontend.hack_set_program(&ctx, program).await.unwrap();
8367 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8368 let sketch_id = sketch_object.id;
8369 let sketch = expect_sketch(sketch_object);
8370 let point_id = *sketch.segments.first().unwrap();
8371
8372 let (src_delta, scene_delta) = frontend
8373 .add_constraint(
8374 &mock_ctx,
8375 version,
8376 sketch_id,
8377 Constraint::Fixed(Fixed {
8378 points: vec![FixedPoint {
8379 point: point_id,
8380 position: Point2d {
8381 x: Number {
8382 value: 2.0,
8383 units: NumericSuffix::Mm,
8384 },
8385 y: Number {
8386 value: 3.0,
8387 units: NumericSuffix::Mm,
8388 },
8389 },
8390 }],
8391 }),
8392 )
8393 .await
8394 .unwrap();
8395 assert_eq!(
8396 src_delta.text.as_str(),
8397 "\
8398sketch(on = XY) {
8399 point1 = point(at = [var 1, var 2])
8400 fixed([point1, [2mm, 3mm]])
8401}
8402"
8403 );
8404 assert_eq!(
8405 scene_delta.new_graph.objects.len(),
8406 4,
8407 "{:#?}",
8408 scene_delta.new_graph.objects
8409 );
8410
8411 ctx.close().await;
8412 mock_ctx.close().await;
8413 }
8414
8415 #[tokio::test(flavor = "multi_thread")]
8416 async fn test_add_fixed_multiple_points() {
8417 let initial_source = "\
8418sketch(on = XY) {
8419 point(at = [var 1, var 2])
8420 point(at = [var 3, var 4])
8421}
8422";
8423
8424 let program = Program::parse(initial_source).unwrap().0.unwrap();
8425
8426 let mut frontend = FrontendState::new();
8427
8428 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8429 let mock_ctx = ExecutorContext::new_mock(None).await;
8430 let version = Version(0);
8431
8432 frontend.hack_set_program(&ctx, program).await.unwrap();
8433 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8434 let sketch_id = sketch_object.id;
8435 let sketch = expect_sketch(sketch_object);
8436 let point0_id = *sketch.segments.first().unwrap();
8437 let point1_id = *sketch.segments.get(1).unwrap();
8438
8439 let (src_delta, scene_delta) = frontend
8440 .add_constraint(
8441 &mock_ctx,
8442 version,
8443 sketch_id,
8444 Constraint::Fixed(Fixed {
8445 points: vec![
8446 FixedPoint {
8447 point: point0_id,
8448 position: Point2d {
8449 x: Number {
8450 value: 2.0,
8451 units: NumericSuffix::Mm,
8452 },
8453 y: Number {
8454 value: 3.0,
8455 units: NumericSuffix::Mm,
8456 },
8457 },
8458 },
8459 FixedPoint {
8460 point: point1_id,
8461 position: Point2d {
8462 x: Number {
8463 value: 4.0,
8464 units: NumericSuffix::Mm,
8465 },
8466 y: Number {
8467 value: 5.0,
8468 units: NumericSuffix::Mm,
8469 },
8470 },
8471 },
8472 ],
8473 }),
8474 )
8475 .await
8476 .unwrap();
8477 assert_eq!(
8478 src_delta.text.as_str(),
8479 "\
8480sketch(on = XY) {
8481 point1 = point(at = [var 1, var 2])
8482 point2 = point(at = [var 3, var 4])
8483 fixed([point1, [2mm, 3mm]])
8484 fixed([point2, [4mm, 5mm]])
8485}
8486"
8487 );
8488 assert_eq!(
8489 scene_delta.new_graph.objects.len(),
8490 6,
8491 "{:#?}",
8492 scene_delta.new_graph.objects
8493 );
8494
8495 ctx.close().await;
8496 mock_ctx.close().await;
8497 }
8498
8499 #[tokio::test(flavor = "multi_thread")]
8500 async fn test_add_fixed_owned_point() {
8501 let initial_source = "\
8502sketch(on = XY) {
8503 line(start = [var 1, var 2], end = [var 3, var 4])
8504}
8505";
8506
8507 let program = Program::parse(initial_source).unwrap().0.unwrap();
8508
8509 let mut frontend = FrontendState::new();
8510
8511 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8512 let mock_ctx = ExecutorContext::new_mock(None).await;
8513 let version = Version(0);
8514
8515 frontend.hack_set_program(&ctx, program).await.unwrap();
8516 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8517 let sketch_id = sketch_object.id;
8518 let sketch = expect_sketch(sketch_object);
8519 let line_start_id = *sketch.segments.first().unwrap();
8520
8521 let (src_delta, scene_delta) = frontend
8522 .add_constraint(
8523 &mock_ctx,
8524 version,
8525 sketch_id,
8526 Constraint::Fixed(Fixed {
8527 points: vec![FixedPoint {
8528 point: line_start_id,
8529 position: Point2d {
8530 x: Number {
8531 value: 2.0,
8532 units: NumericSuffix::Mm,
8533 },
8534 y: Number {
8535 value: 3.0,
8536 units: NumericSuffix::Mm,
8537 },
8538 },
8539 }],
8540 }),
8541 )
8542 .await
8543 .unwrap();
8544 assert_eq!(
8545 src_delta.text.as_str(),
8546 "\
8547sketch(on = XY) {
8548 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8549 fixed([line1.start, [2mm, 3mm]])
8550}
8551"
8552 );
8553 assert_eq!(
8554 scene_delta.new_graph.objects.len(),
8555 6,
8556 "{:#?}",
8557 scene_delta.new_graph.objects
8558 );
8559
8560 ctx.close().await;
8561 mock_ctx.close().await;
8562 }
8563
8564 #[tokio::test(flavor = "multi_thread")]
8565 async fn test_radius_error_cases() {
8566 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8567 let mock_ctx = ExecutorContext::new_mock(None).await;
8568 let version = Version(0);
8569
8570 let initial_source_point = "\
8572sketch(on = XY) {
8573 point(at = [var 1, var 2])
8574}
8575";
8576 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
8577 let mut frontend_point = FrontendState::new();
8578 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
8579 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
8580 let sketch_id_point = sketch_object_point.id;
8581 let sketch_point = expect_sketch(sketch_object_point);
8582 let point_id = *sketch_point.segments.first().unwrap();
8583
8584 let constraint_point = Constraint::Radius(Radius {
8585 arc: point_id,
8586 radius: Number {
8587 value: 5.0,
8588 units: NumericSuffix::Mm,
8589 },
8590 source: Default::default(),
8591 });
8592 let result_point = frontend_point
8593 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
8594 .await;
8595 assert!(result_point.is_err(), "Single point should error for radius");
8596
8597 let initial_source_line = "\
8599sketch(on = XY) {
8600 line(start = [var 1, var 2], end = [var 3, var 4])
8601}
8602";
8603 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
8604 let mut frontend_line = FrontendState::new();
8605 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
8606 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
8607 let sketch_id_line = sketch_object_line.id;
8608 let sketch_line = expect_sketch(sketch_object_line);
8609 let line_id = *sketch_line.segments.first().unwrap();
8610
8611 let constraint_line = Constraint::Radius(Radius {
8612 arc: line_id,
8613 radius: Number {
8614 value: 5.0,
8615 units: NumericSuffix::Mm,
8616 },
8617 source: Default::default(),
8618 });
8619 let result_line = frontend_line
8620 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
8621 .await;
8622 assert!(result_line.is_err(), "Single line segment should error for radius");
8623
8624 ctx.close().await;
8625 mock_ctx.close().await;
8626 }
8627
8628 #[tokio::test(flavor = "multi_thread")]
8629 async fn test_diameter_single_arc_segment() {
8630 let initial_source = "\
8631sketch(on = XY) {
8632 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8633}
8634";
8635
8636 let program = Program::parse(initial_source).unwrap().0.unwrap();
8637
8638 let mut frontend = FrontendState::new();
8639
8640 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8641 let mock_ctx = ExecutorContext::new_mock(None).await;
8642 let version = Version(0);
8643
8644 frontend.hack_set_program(&ctx, program).await.unwrap();
8645 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8646 let sketch_id = sketch_object.id;
8647 let sketch = expect_sketch(sketch_object);
8648 let arc_id = sketch
8650 .segments
8651 .iter()
8652 .find(|&seg_id| {
8653 let obj = frontend.scene_graph.objects.get(seg_id.0);
8654 matches!(
8655 obj.map(|o| &o.kind),
8656 Some(ObjectKind::Segment {
8657 segment: Segment::Arc(_)
8658 })
8659 )
8660 })
8661 .unwrap();
8662
8663 let constraint = Constraint::Diameter(Diameter {
8664 arc: *arc_id,
8665 diameter: Number {
8666 value: 10.0,
8667 units: NumericSuffix::Mm,
8668 },
8669 source: Default::default(),
8670 });
8671 let (src_delta, scene_delta) = frontend
8672 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8673 .await
8674 .unwrap();
8675 assert_eq!(
8676 src_delta.text.as_str(),
8677 "\
8679sketch(on = XY) {
8680 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8681 diameter(arc1) == 10mm
8682}
8683"
8684 );
8685 assert_eq!(
8686 scene_delta.new_graph.objects.len(),
8687 7, "{:#?}",
8689 scene_delta.new_graph.objects
8690 );
8691
8692 ctx.close().await;
8693 mock_ctx.close().await;
8694 }
8695
8696 #[tokio::test(flavor = "multi_thread")]
8697 async fn test_diameter_error_cases() {
8698 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8699 let mock_ctx = ExecutorContext::new_mock(None).await;
8700 let version = Version(0);
8701
8702 let initial_source_point = "\
8704sketch(on = XY) {
8705 point(at = [var 1, var 2])
8706}
8707";
8708 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
8709 let mut frontend_point = FrontendState::new();
8710 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
8711 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
8712 let sketch_id_point = sketch_object_point.id;
8713 let sketch_point = expect_sketch(sketch_object_point);
8714 let point_id = *sketch_point.segments.first().unwrap();
8715
8716 let constraint_point = Constraint::Diameter(Diameter {
8717 arc: point_id,
8718 diameter: Number {
8719 value: 10.0,
8720 units: NumericSuffix::Mm,
8721 },
8722 source: Default::default(),
8723 });
8724 let result_point = frontend_point
8725 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
8726 .await;
8727 assert!(result_point.is_err(), "Single point should error for diameter");
8728
8729 let initial_source_line = "\
8731sketch(on = XY) {
8732 line(start = [var 1, var 2], end = [var 3, var 4])
8733}
8734";
8735 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
8736 let mut frontend_line = FrontendState::new();
8737 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
8738 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
8739 let sketch_id_line = sketch_object_line.id;
8740 let sketch_line = expect_sketch(sketch_object_line);
8741 let line_id = *sketch_line.segments.first().unwrap();
8742
8743 let constraint_line = Constraint::Diameter(Diameter {
8744 arc: line_id,
8745 diameter: Number {
8746 value: 10.0,
8747 units: NumericSuffix::Mm,
8748 },
8749 source: Default::default(),
8750 });
8751 let result_line = frontend_line
8752 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
8753 .await;
8754 assert!(result_line.is_err(), "Single line segment should error for diameter");
8755
8756 ctx.close().await;
8757 mock_ctx.close().await;
8758 }
8759
8760 #[tokio::test(flavor = "multi_thread")]
8761 async fn test_line_horizontal() {
8762 let initial_source = "\
8763sketch(on = XY) {
8764 line(start = [var 1, var 2], end = [var 3, var 4])
8765}
8766";
8767
8768 let program = Program::parse(initial_source).unwrap().0.unwrap();
8769
8770 let mut frontend = FrontendState::new();
8771
8772 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8773 let mock_ctx = ExecutorContext::new_mock(None).await;
8774 let version = Version(0);
8775
8776 frontend.hack_set_program(&ctx, program).await.unwrap();
8777 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8778 let sketch_id = sketch_object.id;
8779 let sketch = expect_sketch(sketch_object);
8780 let line1_id = *sketch.segments.get(2).unwrap();
8781
8782 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
8783 let (src_delta, scene_delta) = frontend
8784 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8785 .await
8786 .unwrap();
8787 assert_eq!(
8788 src_delta.text.as_str(),
8789 "\
8790sketch(on = XY) {
8791 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8792 horizontal(line1)
8793}
8794"
8795 );
8796 assert_eq!(
8797 scene_delta.new_graph.objects.len(),
8798 6,
8799 "{:#?}",
8800 scene_delta.new_graph.objects
8801 );
8802
8803 ctx.close().await;
8804 mock_ctx.close().await;
8805 }
8806
8807 #[tokio::test(flavor = "multi_thread")]
8808 async fn test_line_vertical() {
8809 let initial_source = "\
8810sketch(on = XY) {
8811 line(start = [var 1, var 2], end = [var 3, var 4])
8812}
8813";
8814
8815 let program = Program::parse(initial_source).unwrap().0.unwrap();
8816
8817 let mut frontend = FrontendState::new();
8818
8819 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8820 let mock_ctx = ExecutorContext::new_mock(None).await;
8821 let version = Version(0);
8822
8823 frontend.hack_set_program(&ctx, program).await.unwrap();
8824 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8825 let sketch_id = sketch_object.id;
8826 let sketch = expect_sketch(sketch_object);
8827 let line1_id = *sketch.segments.get(2).unwrap();
8828
8829 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
8830 let (src_delta, scene_delta) = frontend
8831 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8832 .await
8833 .unwrap();
8834 assert_eq!(
8835 src_delta.text.as_str(),
8836 "\
8837sketch(on = XY) {
8838 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8839 vertical(line1)
8840}
8841"
8842 );
8843 assert_eq!(
8844 scene_delta.new_graph.objects.len(),
8845 6,
8846 "{:#?}",
8847 scene_delta.new_graph.objects
8848 );
8849
8850 ctx.close().await;
8851 mock_ctx.close().await;
8852 }
8853
8854 #[tokio::test(flavor = "multi_thread")]
8855 async fn test_points_vertical() {
8856 let initial_source = "\
8857sketch001 = sketch(on = XY) {
8858 p0 = point(at = [var -2.23mm, var 3.1mm])
8859 pf = point(at = [4, 4])
8860}
8861";
8862
8863 let program = Program::parse(initial_source).unwrap().0.unwrap();
8864
8865 let mut frontend = FrontendState::new();
8866
8867 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8868 let mock_ctx = ExecutorContext::new_mock(None).await;
8869 let version = Version(0);
8870
8871 frontend.hack_set_program(&ctx, program).await.unwrap();
8872 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8873 let sketch_id = sketch_object.id;
8874 let sketch = expect_sketch(sketch_object);
8875 let point_ids = vec![
8876 sketch.segments.first().unwrap().to_owned(),
8877 sketch.segments.get(1).unwrap().to_owned(),
8878 ];
8879
8880 let constraint = Constraint::Vertical(Vertical::Points {
8881 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
8882 });
8883 let (src_delta, scene_delta) = frontend
8884 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8885 .await
8886 .unwrap();
8887 assert_eq!(
8888 src_delta.text.as_str(),
8889 "\
8890sketch001 = sketch(on = XY) {
8891 p0 = point(at = [var -2.23mm, var 3.1mm])
8892 pf = point(at = [4, 4])
8893 vertical([p0, pf])
8894}
8895"
8896 );
8897 assert_eq!(
8898 scene_delta.new_graph.objects.len(),
8899 5,
8900 "{:#?}",
8901 scene_delta.new_graph.objects
8902 );
8903
8904 ctx.close().await;
8905 mock_ctx.close().await;
8906 }
8907
8908 #[tokio::test(flavor = "multi_thread")]
8909 async fn test_points_horizontal() {
8910 let initial_source = "\
8911sketch001 = sketch(on = XY) {
8912 p0 = point(at = [var -2.23mm, var 3.1mm])
8913 pf = point(at = [4, 4])
8914}
8915";
8916
8917 let program = Program::parse(initial_source).unwrap().0.unwrap();
8918
8919 let mut frontend = FrontendState::new();
8920
8921 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8922 let mock_ctx = ExecutorContext::new_mock(None).await;
8923 let version = Version(0);
8924
8925 frontend.hack_set_program(&ctx, program).await.unwrap();
8926 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8927 let sketch_id = sketch_object.id;
8928 let sketch = expect_sketch(sketch_object);
8929 let point_ids = vec![
8930 sketch.segments.first().unwrap().to_owned(),
8931 sketch.segments.get(1).unwrap().to_owned(),
8932 ];
8933
8934 let constraint = Constraint::Horizontal(Horizontal::Points {
8935 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
8936 });
8937 let (src_delta, scene_delta) = frontend
8938 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8939 .await
8940 .unwrap();
8941 assert_eq!(
8942 src_delta.text.as_str(),
8943 "\
8944sketch001 = sketch(on = XY) {
8945 p0 = point(at = [var -2.23mm, var 3.1mm])
8946 pf = point(at = [4, 4])
8947 horizontal([p0, pf])
8948}
8949"
8950 );
8951 assert_eq!(
8952 scene_delta.new_graph.objects.len(),
8953 5,
8954 "{:#?}",
8955 scene_delta.new_graph.objects
8956 );
8957
8958 ctx.close().await;
8959 mock_ctx.close().await;
8960 }
8961
8962 #[tokio::test(flavor = "multi_thread")]
8963 async fn test_point_horizontal_with_origin() {
8964 let initial_source = "\
8965sketch001 = sketch(on = XY) {
8966 p0 = point(at = [var -2.23mm, var 3.1mm])
8967}
8968";
8969
8970 let program = Program::parse(initial_source).unwrap().0.unwrap();
8971
8972 let mut frontend = FrontendState::new();
8973
8974 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8975 let mock_ctx = ExecutorContext::new_mock(None).await;
8976 let version = Version(0);
8977
8978 frontend.hack_set_program(&ctx, program).await.unwrap();
8979 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8980 let sketch_id = sketch_object.id;
8981 let sketch = expect_sketch(sketch_object);
8982 let point_id = *sketch.segments.first().unwrap();
8983
8984 let constraint = Constraint::Horizontal(Horizontal::Points {
8985 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
8986 });
8987 let (src_delta, scene_delta) = frontend
8988 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8989 .await
8990 .unwrap();
8991 assert_eq!(
8992 src_delta.text.as_str(),
8993 "\
8994sketch001 = sketch(on = XY) {
8995 p0 = point(at = [var -2.23mm, var 3.1mm])
8996 horizontal([p0, ORIGIN])
8997}
8998"
8999 );
9000 assert_eq!(
9001 scene_delta.new_graph.objects.len(),
9002 4,
9003 "{:#?}",
9004 scene_delta.new_graph.objects
9005 );
9006
9007 ctx.close().await;
9008 mock_ctx.close().await;
9009 }
9010
9011 #[tokio::test(flavor = "multi_thread")]
9012 async fn test_lines_equal_length() {
9013 let initial_source = "\
9014sketch(on = XY) {
9015 line(start = [var 1, var 2], end = [var 3, var 4])
9016 line(start = [var 5, var 6], end = [var 7, var 8])
9017}
9018";
9019
9020 let program = Program::parse(initial_source).unwrap().0.unwrap();
9021
9022 let mut frontend = FrontendState::new();
9023
9024 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9025 let mock_ctx = ExecutorContext::new_mock(None).await;
9026 let version = Version(0);
9027
9028 frontend.hack_set_program(&ctx, program).await.unwrap();
9029 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9030 let sketch_id = sketch_object.id;
9031 let sketch = expect_sketch(sketch_object);
9032 let line1_id = *sketch.segments.get(2).unwrap();
9033 let line2_id = *sketch.segments.get(5).unwrap();
9034
9035 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
9036 lines: vec![line1_id, line2_id],
9037 });
9038 let (src_delta, scene_delta) = frontend
9039 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9040 .await
9041 .unwrap();
9042 assert_eq!(
9043 src_delta.text.as_str(),
9044 "\
9045sketch(on = XY) {
9046 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9047 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9048 equalLength([line1, line2])
9049}
9050"
9051 );
9052 assert_eq!(
9053 scene_delta.new_graph.objects.len(),
9054 9,
9055 "{:#?}",
9056 scene_delta.new_graph.objects
9057 );
9058
9059 ctx.close().await;
9060 mock_ctx.close().await;
9061 }
9062
9063 #[tokio::test(flavor = "multi_thread")]
9064 async fn test_add_constraint_multi_line_equal_length() {
9065 let initial_source = "\
9066sketch(on = XY) {
9067 line(start = [var 1, var 2], end = [var 3, var 4])
9068 line(start = [var 5, var 6], end = [var 7, var 8])
9069 line(start = [var 9, var 10], end = [var 11, var 12])
9070}
9071";
9072
9073 let program = Program::parse(initial_source).unwrap().0.unwrap();
9074
9075 let mut frontend = FrontendState::new();
9076 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9077 let mock_ctx = ExecutorContext::new_mock(None).await;
9078 let version = Version(0);
9079
9080 frontend.hack_set_program(&ctx, program).await.unwrap();
9081 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9082 let sketch_id = sketch_object.id;
9083 let sketch = expect_sketch(sketch_object);
9084 let line1_id = *sketch.segments.get(2).unwrap();
9085 let line2_id = *sketch.segments.get(5).unwrap();
9086 let line3_id = *sketch.segments.get(8).unwrap();
9087
9088 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
9089 lines: vec![line1_id, line2_id, line3_id],
9090 });
9091 let (src_delta, scene_delta) = frontend
9092 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9093 .await
9094 .unwrap();
9095 assert_eq!(
9096 src_delta.text.as_str(),
9097 "\
9098sketch(on = XY) {
9099 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9100 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9101 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9102 equalLength([line1, line2, line3])
9103}
9104"
9105 );
9106 let constraints = scene_delta
9107 .new_graph
9108 .objects
9109 .iter()
9110 .filter_map(|obj| {
9111 let ObjectKind::Constraint { constraint } = &obj.kind else {
9112 return None;
9113 };
9114 Some(constraint)
9115 })
9116 .collect::<Vec<_>>();
9117
9118 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
9119 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
9120 panic!("expected equal length constraint, got {:?}", constraints[0]);
9121 };
9122 assert_eq!(lines_equal_length.lines.len(), 3);
9123
9124 ctx.close().await;
9125 mock_ctx.close().await;
9126 }
9127
9128 #[tokio::test(flavor = "multi_thread")]
9129 async fn test_lines_parallel() {
9130 let initial_source = "\
9131sketch(on = XY) {
9132 line(start = [var 1, var 2], end = [var 3, var 4])
9133 line(start = [var 5, var 6], end = [var 7, var 8])
9134}
9135";
9136
9137 let program = Program::parse(initial_source).unwrap().0.unwrap();
9138
9139 let mut frontend = FrontendState::new();
9140
9141 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9142 let mock_ctx = ExecutorContext::new_mock(None).await;
9143 let version = Version(0);
9144
9145 frontend.hack_set_program(&ctx, program).await.unwrap();
9146 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9147 let sketch_id = sketch_object.id;
9148 let sketch = expect_sketch(sketch_object);
9149 let line1_id = *sketch.segments.get(2).unwrap();
9150 let line2_id = *sketch.segments.get(5).unwrap();
9151
9152 let constraint = Constraint::Parallel(Parallel {
9153 lines: vec![line1_id, line2_id],
9154 });
9155 let (src_delta, scene_delta) = frontend
9156 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9157 .await
9158 .unwrap();
9159 assert_eq!(
9160 src_delta.text.as_str(),
9161 "\
9162sketch(on = XY) {
9163 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9164 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9165 parallel([line1, line2])
9166}
9167"
9168 );
9169 assert_eq!(
9170 scene_delta.new_graph.objects.len(),
9171 9,
9172 "{:#?}",
9173 scene_delta.new_graph.objects
9174 );
9175
9176 ctx.close().await;
9177 mock_ctx.close().await;
9178 }
9179
9180 #[tokio::test(flavor = "multi_thread")]
9181 async fn test_lines_parallel_multiline() {
9182 let initial_source = "\
9183sketch(on = XY) {
9184 line(start = [var 1, var 2], end = [var 3, var 4])
9185 line(start = [var 5, var 6], end = [var 7, var 8])
9186 line(start = [var 9, var 10], end = [var 11, var 12])
9187}
9188";
9189
9190 let program = Program::parse(initial_source).unwrap().0.unwrap();
9191
9192 let mut frontend = FrontendState::new();
9193
9194 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9195 let mock_ctx = ExecutorContext::new_mock(None).await;
9196 let version = Version(0);
9197
9198 frontend.hack_set_program(&ctx, program).await.unwrap();
9199 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9200 let sketch_id = sketch_object.id;
9201 let sketch = expect_sketch(sketch_object);
9202 let line1_id = *sketch.segments.get(2).unwrap();
9203 let line2_id = *sketch.segments.get(5).unwrap();
9204 let line3_id = *sketch.segments.get(8).unwrap();
9205
9206 let constraint = Constraint::Parallel(Parallel {
9207 lines: vec![line1_id, line2_id, line3_id],
9208 });
9209 let (src_delta, scene_delta) = frontend
9210 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9211 .await
9212 .unwrap();
9213 assert_eq!(
9214 src_delta.text.as_str(),
9215 "\
9216sketch(on = XY) {
9217 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9218 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9219 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9220 parallel([line1, line2, line3])
9221}
9222"
9223 );
9224
9225 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9226 let sketch = expect_sketch(sketch_object);
9227 assert_eq!(sketch.constraints.len(), 1);
9228
9229 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9230 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9231 panic!("Expected constraint object");
9232 };
9233 let Constraint::Parallel(parallel) = constraint else {
9234 panic!("Expected parallel constraint");
9235 };
9236 assert_eq!(parallel.lines.len(), 3);
9237
9238 ctx.close().await;
9239 mock_ctx.close().await;
9240 }
9241
9242 #[tokio::test(flavor = "multi_thread")]
9243 async fn test_lines_perpendicular() {
9244 let initial_source = "\
9245sketch(on = XY) {
9246 line(start = [var 1, var 2], end = [var 3, var 4])
9247 line(start = [var 5, var 6], end = [var 7, var 8])
9248}
9249";
9250
9251 let program = Program::parse(initial_source).unwrap().0.unwrap();
9252
9253 let mut frontend = FrontendState::new();
9254
9255 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9256 let mock_ctx = ExecutorContext::new_mock(None).await;
9257 let version = Version(0);
9258
9259 frontend.hack_set_program(&ctx, program).await.unwrap();
9260 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9261 let sketch_id = sketch_object.id;
9262 let sketch = expect_sketch(sketch_object);
9263 let line1_id = *sketch.segments.get(2).unwrap();
9264 let line2_id = *sketch.segments.get(5).unwrap();
9265
9266 let constraint = Constraint::Perpendicular(Perpendicular {
9267 lines: vec![line1_id, line2_id],
9268 });
9269 let (src_delta, scene_delta) = frontend
9270 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9271 .await
9272 .unwrap();
9273 assert_eq!(
9274 src_delta.text.as_str(),
9275 "\
9276sketch(on = XY) {
9277 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9278 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9279 perpendicular([line1, line2])
9280}
9281"
9282 );
9283 assert_eq!(
9284 scene_delta.new_graph.objects.len(),
9285 9,
9286 "{:#?}",
9287 scene_delta.new_graph.objects
9288 );
9289
9290 ctx.close().await;
9291 mock_ctx.close().await;
9292 }
9293
9294 #[tokio::test(flavor = "multi_thread")]
9295 async fn test_lines_angle() {
9296 let initial_source = "\
9297sketch(on = XY) {
9298 line(start = [var 1, var 2], end = [var 3, var 4])
9299 line(start = [var 5, var 6], end = [var 7, var 8])
9300}
9301";
9302
9303 let program = Program::parse(initial_source).unwrap().0.unwrap();
9304
9305 let mut frontend = FrontendState::new();
9306
9307 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9308 let mock_ctx = ExecutorContext::new_mock(None).await;
9309 let version = Version(0);
9310
9311 frontend.hack_set_program(&ctx, program).await.unwrap();
9312 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9313 let sketch_id = sketch_object.id;
9314 let sketch = expect_sketch(sketch_object);
9315 let line1_id = *sketch.segments.get(2).unwrap();
9316 let line2_id = *sketch.segments.get(5).unwrap();
9317
9318 let constraint = Constraint::Angle(Angle {
9319 lines: vec![line1_id, line2_id],
9320 angle: Number {
9321 value: 30.0,
9322 units: NumericSuffix::Deg,
9323 },
9324 source: Default::default(),
9325 });
9326 let (src_delta, scene_delta) = frontend
9327 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9328 .await
9329 .unwrap();
9330 assert_eq!(
9331 src_delta.text.as_str(),
9332 "\
9334sketch(on = XY) {
9335 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9336 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9337 angle([line1, line2]) == 30deg
9338}
9339"
9340 );
9341 assert_eq!(
9342 scene_delta.new_graph.objects.len(),
9343 9,
9344 "{:#?}",
9345 scene_delta.new_graph.objects
9346 );
9347
9348 ctx.close().await;
9349 mock_ctx.close().await;
9350 }
9351
9352 #[tokio::test(flavor = "multi_thread")]
9353 async fn test_segments_tangent() {
9354 let initial_source = "\
9355sketch(on = XY) {
9356 line(start = [var 1, var 2], end = [var 3, var 4])
9357 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
9358}
9359";
9360
9361 let program = Program::parse(initial_source).unwrap().0.unwrap();
9362
9363 let mut frontend = FrontendState::new();
9364
9365 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9366 let mock_ctx = ExecutorContext::new_mock(None).await;
9367 let version = Version(0);
9368
9369 frontend.hack_set_program(&ctx, program).await.unwrap();
9370 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9371 let sketch_id = sketch_object.id;
9372 let sketch = expect_sketch(sketch_object);
9373 let line1_id = *sketch.segments.get(2).unwrap();
9374 let arc1_id = *sketch.segments.get(6).unwrap();
9375
9376 let constraint = Constraint::Tangent(Tangent {
9377 input: vec![line1_id, arc1_id],
9378 });
9379 let (src_delta, scene_delta) = frontend
9380 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9381 .await
9382 .unwrap();
9383 assert_eq!(
9384 src_delta.text.as_str(),
9385 "\
9386sketch(on = XY) {
9387 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9388 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
9389 tangent([line1, arc1])
9390}
9391"
9392 );
9393 assert_eq!(
9394 scene_delta.new_graph.objects.len(),
9395 10,
9396 "{:#?}",
9397 scene_delta.new_graph.objects
9398 );
9399
9400 ctx.close().await;
9401 mock_ctx.close().await;
9402 }
9403
9404 #[tokio::test(flavor = "multi_thread")]
9405 async fn test_sketch_on_face_simple() {
9406 let initial_source = "\
9407len = 2mm
9408cube = startSketchOn(XY)
9409 |> startProfile(at = [0, 0])
9410 |> line(end = [len, 0], tag = $side)
9411 |> line(end = [0, len])
9412 |> line(end = [-len, 0])
9413 |> line(end = [0, -len])
9414 |> close()
9415 |> extrude(length = len)
9416
9417face = faceOf(cube, face = side)
9418";
9419
9420 let program = Program::parse(initial_source).unwrap().0.unwrap();
9421
9422 let mut frontend = FrontendState::new();
9423
9424 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9425 let mock_ctx = ExecutorContext::new_mock(None).await;
9426 let version = Version(0);
9427
9428 frontend.hack_set_program(&ctx, program).await.unwrap();
9429 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
9430 let face_id = face_object.id;
9431
9432 let sketch_args = SketchCtor {
9433 on: Plane::Object(face_id),
9434 };
9435 let (_src_delta, scene_delta, sketch_id) = frontend
9436 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9437 .await
9438 .unwrap();
9439 assert_eq!(sketch_id, ObjectId(2));
9440 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
9441 let sketch_object = &scene_delta.new_graph.objects[2];
9442 assert_eq!(sketch_object.id, ObjectId(2));
9443 assert_eq!(
9444 sketch_object.kind,
9445 ObjectKind::Sketch(Sketch {
9446 args: SketchCtor {
9447 on: Plane::Object(face_id),
9448 },
9449 plane: face_id,
9450 segments: vec![],
9451 constraints: vec![],
9452 })
9453 );
9454 assert_eq!(scene_delta.new_graph.objects.len(), 8);
9455
9456 ctx.close().await;
9457 mock_ctx.close().await;
9458 }
9459
9460 #[tokio::test(flavor = "multi_thread")]
9461 async fn test_sketch_on_wall_artifact_from_region_extrude() {
9462 let initial_source = "\
9463s = sketch(on = YZ) {
9464 line1 = line(start = [0, 0], end = [0, 1])
9465 line2 = line(start = [0, 1], end = [1, 1])
9466 line3 = line(start = [1, 1], end = [0, 0])
9467}
9468region001 = region(point = [0.1, 0.1], sketch = s)
9469extrude001 = extrude(region001, length = 5)
9470";
9471
9472 let program = Program::parse(initial_source).unwrap().0.unwrap();
9473
9474 let mut frontend = FrontendState::new();
9475 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9476 let version = Version(0);
9477
9478 frontend.hack_set_program(&ctx, program).await.unwrap();
9479 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
9480
9481 let sketch_args = SketchCtor {
9482 on: Plane::Object(wall_object_id),
9483 };
9484 let (src_delta, _scene_delta, _sketch_id) = frontend
9485 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9486 .await
9487 .unwrap();
9488 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
9489
9490 ctx.close().await;
9491 }
9492
9493 #[tokio::test(flavor = "multi_thread")]
9494 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
9495 let initial_source = "\
9496sketch001 = sketch(on = YZ) {
9497 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
9498 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
9499 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
9500 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
9501 coincident([line1.end, line2.start])
9502 coincident([line2.end, line3.start])
9503 coincident([line3.end, line4.start])
9504 coincident([line4.end, line1.start])
9505 parallel([line2, line4])
9506 parallel([line3, line1])
9507 perpendicular([line1, line2])
9508 horizontal(line3)
9509 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
9510}
9511region001 = region(point = [3.1, 3.74], sketch = sketch001)
9512extrude001 = extrude(region001, length = 5)
9513";
9514
9515 let program = Program::parse(initial_source).unwrap().0.unwrap();
9516
9517 let mut frontend = FrontendState::new();
9518 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9519 let version = Version(0);
9520
9521 frontend.hack_set_program(&ctx, program).await.unwrap();
9522 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
9523
9524 let sketch_args = SketchCtor {
9525 on: Plane::Object(wall_object_id),
9526 };
9527 let (src_delta, _scene_delta, _sketch_id) = frontend
9528 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9529 .await
9530 .unwrap();
9531 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
9532
9533 ctx.close().await;
9534 }
9535
9536 #[tokio::test(flavor = "multi_thread")]
9537 async fn test_sketch_on_plane_incremental() {
9538 let initial_source = "\
9539len = 2mm
9540cube = startSketchOn(XY)
9541 |> startProfile(at = [0, 0])
9542 |> line(end = [len, 0], tag = $side)
9543 |> line(end = [0, len])
9544 |> line(end = [-len, 0])
9545 |> line(end = [0, -len])
9546 |> close()
9547 |> extrude(length = len)
9548
9549plane = planeOf(cube, face = side)
9550";
9551
9552 let program = Program::parse(initial_source).unwrap().0.unwrap();
9553
9554 let mut frontend = FrontendState::new();
9555
9556 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9557 let mock_ctx = ExecutorContext::new_mock(None).await;
9558 let version = Version(0);
9559
9560 frontend.hack_set_program(&ctx, program).await.unwrap();
9561 let plane_object = frontend
9563 .scene_graph
9564 .objects
9565 .iter()
9566 .rev()
9567 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
9568 .unwrap();
9569 let plane_id = plane_object.id;
9570
9571 let sketch_args = SketchCtor {
9572 on: Plane::Object(plane_id),
9573 };
9574 let (src_delta, scene_delta, sketch_id) = frontend
9575 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9576 .await
9577 .unwrap();
9578 assert_eq!(
9579 src_delta.text.as_str(),
9580 "\
9581len = 2mm
9582cube = startSketchOn(XY)
9583 |> startProfile(at = [0, 0])
9584 |> line(end = [len, 0], tag = $side)
9585 |> line(end = [0, len])
9586 |> line(end = [-len, 0])
9587 |> line(end = [0, -len])
9588 |> close()
9589 |> extrude(length = len)
9590
9591plane = planeOf(cube, face = side)
9592sketch001 = sketch(on = plane) {
9593}
9594"
9595 );
9596 assert_eq!(sketch_id, ObjectId(2));
9597 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
9598 let sketch_object = &scene_delta.new_graph.objects[2];
9599 assert_eq!(sketch_object.id, ObjectId(2));
9600 assert_eq!(
9601 sketch_object.kind,
9602 ObjectKind::Sketch(Sketch {
9603 args: SketchCtor {
9604 on: Plane::Object(plane_id),
9605 },
9606 plane: plane_id,
9607 segments: vec![],
9608 constraints: vec![],
9609 })
9610 );
9611 assert_eq!(scene_delta.new_graph.objects.len(), 9);
9612
9613 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
9614 assert_eq!(plane_object.id, plane_id);
9615 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
9616
9617 ctx.close().await;
9618 mock_ctx.close().await;
9619 }
9620
9621 #[tokio::test(flavor = "multi_thread")]
9622 async fn test_new_sketch_uses_unique_variable_name() {
9623 let initial_source = "\
9624sketch1 = sketch(on = XY) {
9625}
9626";
9627
9628 let program = Program::parse(initial_source).unwrap().0.unwrap();
9629
9630 let mut frontend = FrontendState::new();
9631 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9632 let version = Version(0);
9633
9634 frontend.hack_set_program(&ctx, program).await.unwrap();
9635
9636 let sketch_args = SketchCtor {
9637 on: Plane::Default(PlaneName::Yz),
9638 };
9639 let (src_delta, _, _) = frontend
9640 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9641 .await
9642 .unwrap();
9643
9644 assert_eq!(
9645 src_delta.text.as_str(),
9646 "\
9647sketch1 = sketch(on = XY) {
9648}
9649sketch001 = sketch(on = YZ) {
9650}
9651"
9652 );
9653
9654 ctx.close().await;
9655 }
9656
9657 #[tokio::test(flavor = "multi_thread")]
9658 async fn test_new_sketch_twice_using_same_plane() {
9659 let initial_source = "\
9660sketch1 = sketch(on = XY) {
9661}
9662";
9663
9664 let program = Program::parse(initial_source).unwrap().0.unwrap();
9665
9666 let mut frontend = FrontendState::new();
9667 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9668 let version = Version(0);
9669
9670 frontend.hack_set_program(&ctx, program).await.unwrap();
9671
9672 let sketch_args = SketchCtor {
9673 on: Plane::Default(PlaneName::Xy),
9674 };
9675 let (src_delta, _, _) = frontend
9676 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9677 .await
9678 .unwrap();
9679
9680 assert_eq!(
9681 src_delta.text.as_str(),
9682 "\
9683sketch1 = sketch(on = XY) {
9684}
9685sketch001 = sketch(on = XY) {
9686}
9687"
9688 );
9689
9690 ctx.close().await;
9691 }
9692
9693 #[tokio::test(flavor = "multi_thread")]
9694 async fn test_sketch_mode_reuses_cached_on_expression() {
9695 let initial_source = "\
9696width = 2mm
9697sketch(on = offsetPlane(XY, offset = width)) {
9698 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
9699 distance([line1.start, line1.end]) == width
9700}
9701";
9702 let program = Program::parse(initial_source).unwrap().0.unwrap();
9703
9704 let mut frontend = FrontendState::new();
9705 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9706 let mock_ctx = ExecutorContext::new_mock(None).await;
9707 let version = Version(0);
9708 let project_id = ProjectId(0);
9709 let file_id = FileId(0);
9710
9711 frontend.hack_set_program(&ctx, program).await.unwrap();
9712 let initial_object_count = frontend.scene_graph.objects.len();
9713 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
9714 .expect("Expected sketch object to exist")
9715 .id;
9716
9717 let scene_delta = frontend
9720 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
9721 .await
9722 .unwrap();
9723 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
9724
9725 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9728 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
9729
9730 ctx.close().await;
9731 mock_ctx.close().await;
9732 }
9733
9734 #[tokio::test(flavor = "multi_thread")]
9735 async fn test_multiple_sketch_blocks() {
9736 let initial_source = "\
9737// Cube that requires the engine.
9738width = 2
9739sketch001 = startSketchOn(XY)
9740profile001 = startProfile(sketch001, at = [0, 0])
9741 |> yLine(length = width, tag = $seg1)
9742 |> xLine(length = width)
9743 |> yLine(length = -width)
9744 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
9745 |> close()
9746extrude001 = extrude(profile001, length = width)
9747
9748// Get a value that requires the engine.
9749x = segLen(seg1)
9750
9751// Triangle with side length 2*x.
9752sketch(on = XY) {
9753 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
9754 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
9755 coincident([line1.end, line2.start])
9756 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
9757 coincident([line2.end, line3.start])
9758 coincident([line3.end, line1.start])
9759 equalLength([line3, line1])
9760 equalLength([line1, line2])
9761 distance([line1.start, line1.end]) == 2*x
9762}
9763
9764// Line segment with length x.
9765sketch2 = sketch(on = XY) {
9766 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
9767 distance([line1.start, line1.end]) == x
9768}
9769";
9770
9771 let program = Program::parse(initial_source).unwrap().0.unwrap();
9772
9773 let mut frontend = FrontendState::new();
9774
9775 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9776 let mock_ctx = ExecutorContext::new_mock(None).await;
9777 let version = Version(0);
9778 let project_id = ProjectId(0);
9779 let file_id = FileId(0);
9780
9781 frontend.hack_set_program(&ctx, program).await.unwrap();
9782 let sketch_objects = frontend
9783 .scene_graph
9784 .objects
9785 .iter()
9786 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
9787 .collect::<Vec<_>>();
9788 let sketch1_id = sketch_objects.first().unwrap().id;
9789 let sketch2_id = sketch_objects.get(1).unwrap().id;
9790 let point1_id = ObjectId(sketch1_id.0 + 1);
9792 let point2_id = ObjectId(sketch2_id.0 + 1);
9794
9795 let scene_delta = frontend
9804 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
9805 .await
9806 .unwrap();
9807 assert_eq!(
9808 scene_delta.new_graph.objects.len(),
9809 18,
9810 "{:#?}",
9811 scene_delta.new_graph.objects
9812 );
9813
9814 let point_ctor = PointCtor {
9816 position: Point2d {
9817 x: Expr::Var(Number {
9818 value: 1.0,
9819 units: NumericSuffix::Mm,
9820 }),
9821 y: Expr::Var(Number {
9822 value: 2.0,
9823 units: NumericSuffix::Mm,
9824 }),
9825 },
9826 };
9827 let segments = vec![ExistingSegmentCtor {
9828 id: point1_id,
9829 ctor: SegmentCtor::Point(point_ctor),
9830 }];
9831 let (src_delta, _) = frontend
9832 .edit_segments(&mock_ctx, version, sketch1_id, segments)
9833 .await
9834 .unwrap();
9835 assert_eq!(
9837 src_delta.text.as_str(),
9838 "\
9839// Cube that requires the engine.
9840width = 2
9841sketch001 = startSketchOn(XY)
9842profile001 = startProfile(sketch001, at = [0, 0])
9843 |> yLine(length = width, tag = $seg1)
9844 |> xLine(length = width)
9845 |> yLine(length = -width)
9846 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
9847 |> close()
9848extrude001 = extrude(profile001, length = width)
9849
9850// Get a value that requires the engine.
9851x = segLen(seg1)
9852
9853// Triangle with side length 2*x.
9854sketch(on = XY) {
9855 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
9856 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
9857 coincident([line1.end, line2.start])
9858 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
9859 coincident([line2.end, line3.start])
9860 coincident([line3.end, line1.start])
9861 equalLength([line3, line1])
9862 equalLength([line1, line2])
9863 distance([line1.start, line1.end]) == 2 * x
9864}
9865
9866// Line segment with length x.
9867sketch2 = sketch(on = XY) {
9868 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
9869 distance([line1.start, line1.end]) == x
9870}
9871"
9872 );
9873
9874 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
9876 assert_eq!(
9878 src_delta.text.as_str(),
9879 "\
9880// Cube that requires the engine.
9881width = 2
9882sketch001 = startSketchOn(XY)
9883profile001 = startProfile(sketch001, at = [0, 0])
9884 |> yLine(length = width, tag = $seg1)
9885 |> xLine(length = width)
9886 |> yLine(length = -width)
9887 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
9888 |> close()
9889extrude001 = extrude(profile001, length = width)
9890
9891// Get a value that requires the engine.
9892x = segLen(seg1)
9893
9894// Triangle with side length 2*x.
9895sketch(on = XY) {
9896 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
9897 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
9898 coincident([line1.end, line2.start])
9899 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
9900 coincident([line2.end, line3.start])
9901 coincident([line3.end, line1.start])
9902 equalLength([line3, line1])
9903 equalLength([line1, line2])
9904 distance([line1.start, line1.end]) == 2 * x
9905}
9906
9907// Line segment with length x.
9908sketch2 = sketch(on = XY) {
9909 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
9910 distance([line1.start, line1.end]) == x
9911}
9912"
9913 );
9914 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
9922 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
9923
9924 let scene_delta = frontend
9932 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
9933 .await
9934 .unwrap();
9935 assert_eq!(
9936 scene_delta.new_graph.objects.len(),
9937 24,
9938 "{:#?}",
9939 scene_delta.new_graph.objects
9940 );
9941
9942 let point_ctor = PointCtor {
9944 position: Point2d {
9945 x: Expr::Var(Number {
9946 value: 3.0,
9947 units: NumericSuffix::Mm,
9948 }),
9949 y: Expr::Var(Number {
9950 value: 4.0,
9951 units: NumericSuffix::Mm,
9952 }),
9953 },
9954 };
9955 let segments = vec![ExistingSegmentCtor {
9956 id: point2_id,
9957 ctor: SegmentCtor::Point(point_ctor),
9958 }];
9959 let (src_delta, _) = frontend
9960 .edit_segments(&mock_ctx, version, sketch2_id, segments)
9961 .await
9962 .unwrap();
9963 assert_eq!(
9965 src_delta.text.as_str(),
9966 "\
9967// Cube that requires the engine.
9968width = 2
9969sketch001 = startSketchOn(XY)
9970profile001 = startProfile(sketch001, at = [0, 0])
9971 |> yLine(length = width, tag = $seg1)
9972 |> xLine(length = width)
9973 |> yLine(length = -width)
9974 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
9975 |> close()
9976extrude001 = extrude(profile001, length = width)
9977
9978// Get a value that requires the engine.
9979x = segLen(seg1)
9980
9981// Triangle with side length 2*x.
9982sketch(on = XY) {
9983 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
9984 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
9985 coincident([line1.end, line2.start])
9986 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
9987 coincident([line2.end, line3.start])
9988 coincident([line3.end, line1.start])
9989 equalLength([line3, line1])
9990 equalLength([line1, line2])
9991 distance([line1.start, line1.end]) == 2 * x
9992}
9993
9994// Line segment with length x.
9995sketch2 = sketch(on = XY) {
9996 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
9997 distance([line1.start, line1.end]) == x
9998}
9999"
10000 );
10001
10002 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
10004 assert_eq!(
10006 src_delta.text.as_str(),
10007 "\
10008// Cube that requires the engine.
10009width = 2
10010sketch001 = startSketchOn(XY)
10011profile001 = startProfile(sketch001, at = [0, 0])
10012 |> yLine(length = width, tag = $seg1)
10013 |> xLine(length = width)
10014 |> yLine(length = -width)
10015 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
10016 |> close()
10017extrude001 = extrude(profile001, length = width)
10018
10019// Get a value that requires the engine.
10020x = segLen(seg1)
10021
10022// Triangle with side length 2*x.
10023sketch(on = XY) {
10024 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
10025 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
10026 coincident([line1.end, line2.start])
10027 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
10028 coincident([line2.end, line3.start])
10029 coincident([line3.end, line1.start])
10030 equalLength([line3, line1])
10031 equalLength([line1, line2])
10032 distance([line1.start, line1.end]) == 2 * x
10033}
10034
10035// Line segment with length x.
10036sketch2 = sketch(on = XY) {
10037 line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
10038 distance([line1.start, line1.end]) == x
10039}
10040"
10041 );
10042
10043 ctx.close().await;
10044 mock_ctx.close().await;
10045 }
10046
10047 #[tokio::test(flavor = "multi_thread")]
10048 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
10049 clear_mem_cache().await;
10050
10051 let source = r#"sketch001 = sketch(on = XZ) {
10052 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
10053}
10054sketch002 = sketch(on = XY) {
10055 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
10056 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
10057 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
10058 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
10059 coincident([line1.end, line2.start])
10060 coincident([line2.end, line3.start])
10061 coincident([line3.end, line4.start])
10062 coincident([line4.end, line1.start])
10063 parallel([line2, line4])
10064 parallel([line3, line1])
10065 perpendicular([line1, line2])
10066 horizontal(line3)
10067 coincident([line1.start, ORIGIN])
10068}
10069"#;
10070
10071 let program = Program::parse(source).unwrap().0.unwrap();
10072 let mut frontend = FrontendState::new();
10073 let ctx = ExecutorContext::new_with_engine(
10074 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
10075 Default::default(),
10076 );
10077 let mock_ctx = ExecutorContext::new_mock(None).await;
10078 let version = Version(0);
10079 let project_id = ProjectId(0);
10080 let file_id = FileId(0);
10081
10082 frontend.hack_set_program(&ctx, program).await.unwrap();
10083 let sketch_objects = frontend
10084 .scene_graph
10085 .objects
10086 .iter()
10087 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
10088 .collect::<Vec<_>>();
10089 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
10090
10091 let sketch1_id = sketch_objects[0].id;
10092 let sketch2_id = sketch_objects[1].id;
10093
10094 frontend
10095 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
10096 .await
10097 .unwrap();
10098 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
10099
10100 let scene_delta = frontend
10101 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
10102 .await
10103 .unwrap();
10104 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
10105
10106 clear_mem_cache().await;
10107 ctx.close().await;
10108 mock_ctx.close().await;
10109 }
10110
10111 #[tokio::test(flavor = "multi_thread")]
10116 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
10117 let initial_source = "@settings(defaultLengthUnit = mm)
10119
10120
10121
10122sketch001 = sketch(on = XY) {
10123 point(at = [1in, 2in])
10124}
10125";
10126
10127 let program = Program::parse(initial_source).unwrap().0.unwrap();
10128 let mut frontend = FrontendState::new();
10129
10130 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10131 let mock_ctx = ExecutorContext::new_mock(None).await;
10132 let version = Version(0);
10133 let project_id = ProjectId(0);
10134 let file_id = FileId(0);
10135
10136 frontend.hack_set_program(&ctx, program).await.unwrap();
10137 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10138 let sketch_id = sketch_object.id;
10139
10140 frontend
10142 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
10143 .await
10144 .unwrap();
10145
10146 let point_ctor = PointCtor {
10148 position: Point2d {
10149 x: Expr::Number(Number {
10150 value: 5.0,
10151 units: NumericSuffix::Mm,
10152 }),
10153 y: Expr::Number(Number {
10154 value: 6.0,
10155 units: NumericSuffix::Mm,
10156 }),
10157 },
10158 };
10159 let segment = SegmentCtor::Point(point_ctor);
10160 let (src_delta, scene_delta) = frontend
10161 .add_segment(&mock_ctx, version, sketch_id, segment, None)
10162 .await
10163 .unwrap();
10164 assert!(
10166 src_delta.text.contains("point(at = [5mm, 6mm])"),
10167 "Expected new point in source, got: {}",
10168 src_delta.text
10169 );
10170 assert!(!scene_delta.new_objects.is_empty());
10171
10172 ctx.close().await;
10173 mock_ctx.close().await;
10174 }
10175
10176 #[tokio::test(flavor = "multi_thread")]
10177 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
10178 let initial_source = "@settings(defaultLengthUnit = mm)
10180
10181
10182
10183s = sketch(on = XY) {}
10184";
10185
10186 let program = Program::parse(initial_source).unwrap().0.unwrap();
10187 let mut frontend = FrontendState::new();
10188
10189 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10190 let mock_ctx = ExecutorContext::new_mock(None).await;
10191 let version = Version(0);
10192
10193 frontend.hack_set_program(&ctx, program).await.unwrap();
10194 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10195 let sketch_id = sketch_object.id;
10196
10197 let line_ctor = LineCtor {
10198 start: Point2d {
10199 x: Expr::Number(Number {
10200 value: 0.0,
10201 units: NumericSuffix::Mm,
10202 }),
10203 y: Expr::Number(Number {
10204 value: 0.0,
10205 units: NumericSuffix::Mm,
10206 }),
10207 },
10208 end: Point2d {
10209 x: Expr::Number(Number {
10210 value: 10.0,
10211 units: NumericSuffix::Mm,
10212 }),
10213 y: Expr::Number(Number {
10214 value: 10.0,
10215 units: NumericSuffix::Mm,
10216 }),
10217 },
10218 construction: None,
10219 };
10220 let segment = SegmentCtor::Line(line_ctor);
10221 let (src_delta, scene_delta) = frontend
10222 .add_segment(&mock_ctx, version, sketch_id, segment, None)
10223 .await
10224 .unwrap();
10225 assert!(
10226 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
10227 "Expected line in source, got: {}",
10228 src_delta.text
10229 );
10230 assert_eq!(scene_delta.new_objects.len(), 3);
10232
10233 ctx.close().await;
10234 mock_ctx.close().await;
10235 }
10236
10237 #[tokio::test(flavor = "multi_thread")]
10238 async fn test_extra_newlines_between_operations_edit_line() {
10239 let initial_source = "@settings(defaultLengthUnit = mm)
10241
10242
10243sketch001 = sketch(on = XY) {
10244
10245 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
10246
10247}
10248";
10249
10250 let program = Program::parse(initial_source).unwrap().0.unwrap();
10251 let mut frontend = FrontendState::new();
10252
10253 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10254 let mock_ctx = ExecutorContext::new_mock(None).await;
10255 let version = Version(0);
10256 let project_id = ProjectId(0);
10257 let file_id = FileId(0);
10258
10259 frontend.hack_set_program(&ctx, program).await.unwrap();
10260 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10261 let sketch_id = sketch_object.id;
10262 let sketch = expect_sketch(sketch_object);
10263
10264 let line_id = sketch
10266 .segments
10267 .iter()
10268 .copied()
10269 .find(|seg_id| {
10270 matches!(
10271 &frontend.scene_graph.objects[seg_id.0].kind,
10272 ObjectKind::Segment {
10273 segment: Segment::Line(_)
10274 }
10275 )
10276 })
10277 .expect("Expected a line segment in sketch");
10278
10279 frontend
10281 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
10282 .await
10283 .unwrap();
10284
10285 let line_ctor = LineCtor {
10287 start: Point2d {
10288 x: Expr::Var(Number {
10289 value: 1.0,
10290 units: NumericSuffix::Mm,
10291 }),
10292 y: Expr::Var(Number {
10293 value: 2.0,
10294 units: NumericSuffix::Mm,
10295 }),
10296 },
10297 end: Point2d {
10298 x: Expr::Var(Number {
10299 value: 13.0,
10300 units: NumericSuffix::Mm,
10301 }),
10302 y: Expr::Var(Number {
10303 value: 14.0,
10304 units: NumericSuffix::Mm,
10305 }),
10306 },
10307 construction: None,
10308 };
10309 let segments = vec![ExistingSegmentCtor {
10310 id: line_id,
10311 ctor: SegmentCtor::Line(line_ctor),
10312 }];
10313 let (src_delta, _scene_delta) = frontend
10314 .edit_segments(&mock_ctx, version, sketch_id, segments)
10315 .await
10316 .unwrap();
10317 assert!(
10318 src_delta
10319 .text
10320 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
10321 "Expected edited line in source, got: {}",
10322 src_delta.text
10323 );
10324
10325 ctx.close().await;
10326 mock_ctx.close().await;
10327 }
10328
10329 #[tokio::test(flavor = "multi_thread")]
10330 async fn test_extra_newlines_delete_segment() {
10331 let initial_source = "@settings(defaultLengthUnit = mm)
10333
10334
10335
10336sketch001 = sketch(on = XY) {
10337 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10338}
10339";
10340
10341 let program = Program::parse(initial_source).unwrap().0.unwrap();
10342 let mut frontend = FrontendState::new();
10343
10344 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10345 let mock_ctx = ExecutorContext::new_mock(None).await;
10346 let version = Version(0);
10347
10348 frontend.hack_set_program(&ctx, program).await.unwrap();
10349 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10350 let sketch_id = sketch_object.id;
10351 let sketch = expect_sketch(sketch_object);
10352
10353 assert_eq!(sketch.segments.len(), 3);
10355 let circle_id = sketch.segments[2];
10356
10357 let (src_delta, scene_delta) = frontend
10359 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
10360 .await
10361 .unwrap();
10362 assert!(
10363 src_delta.text.contains("sketch(on = XY) {"),
10364 "Expected sketch block in source, got: {}",
10365 src_delta.text
10366 );
10367 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10368 let new_sketch = expect_sketch(new_sketch_object);
10369 assert_eq!(new_sketch.segments.len(), 0);
10370
10371 ctx.close().await;
10372 mock_ctx.close().await;
10373 }
10374
10375 #[tokio::test(flavor = "multi_thread")]
10376 async fn test_unformatted_source_add_arc() {
10377 let initial_source = "@settings(defaultLengthUnit = mm)
10379
10380
10381
10382
10383sketch001 = sketch(on = XY) {
10384}
10385";
10386
10387 let program = Program::parse(initial_source).unwrap().0.unwrap();
10388 let mut frontend = FrontendState::new();
10389
10390 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10391 let mock_ctx = ExecutorContext::new_mock(None).await;
10392 let version = Version(0);
10393
10394 frontend.hack_set_program(&ctx, program).await.unwrap();
10395 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10396 let sketch_id = sketch_object.id;
10397
10398 let arc_ctor = ArcCtor {
10399 start: Point2d {
10400 x: Expr::Var(Number {
10401 value: 5.0,
10402 units: NumericSuffix::Mm,
10403 }),
10404 y: Expr::Var(Number {
10405 value: 0.0,
10406 units: NumericSuffix::Mm,
10407 }),
10408 },
10409 end: Point2d {
10410 x: Expr::Var(Number {
10411 value: 0.0,
10412 units: NumericSuffix::Mm,
10413 }),
10414 y: Expr::Var(Number {
10415 value: 5.0,
10416 units: NumericSuffix::Mm,
10417 }),
10418 },
10419 center: Point2d {
10420 x: Expr::Var(Number {
10421 value: 0.0,
10422 units: NumericSuffix::Mm,
10423 }),
10424 y: Expr::Var(Number {
10425 value: 0.0,
10426 units: NumericSuffix::Mm,
10427 }),
10428 },
10429 construction: None,
10430 };
10431 let segment = SegmentCtor::Arc(arc_ctor);
10432 let (src_delta, scene_delta) = frontend
10433 .add_segment(&mock_ctx, version, sketch_id, segment, None)
10434 .await
10435 .unwrap();
10436 assert!(
10437 src_delta
10438 .text
10439 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
10440 "Expected arc in source, got: {}",
10441 src_delta.text
10442 );
10443 assert!(!scene_delta.new_objects.is_empty());
10444
10445 ctx.close().await;
10446 mock_ctx.close().await;
10447 }
10448
10449 #[tokio::test(flavor = "multi_thread")]
10450 async fn test_extra_newlines_add_circle() {
10451 let initial_source = "@settings(defaultLengthUnit = mm)
10453
10454
10455
10456sketch001 = sketch(on = XY) {
10457}
10458";
10459
10460 let program = Program::parse(initial_source).unwrap().0.unwrap();
10461 let mut frontend = FrontendState::new();
10462
10463 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10464 let mock_ctx = ExecutorContext::new_mock(None).await;
10465 let version = Version(0);
10466
10467 frontend.hack_set_program(&ctx, program).await.unwrap();
10468 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10469 let sketch_id = sketch_object.id;
10470
10471 let circle_ctor = CircleCtor {
10472 start: Point2d {
10473 x: Expr::Var(Number {
10474 value: 5.0,
10475 units: NumericSuffix::Mm,
10476 }),
10477 y: Expr::Var(Number {
10478 value: 0.0,
10479 units: NumericSuffix::Mm,
10480 }),
10481 },
10482 center: Point2d {
10483 x: Expr::Var(Number {
10484 value: 0.0,
10485 units: NumericSuffix::Mm,
10486 }),
10487 y: Expr::Var(Number {
10488 value: 0.0,
10489 units: NumericSuffix::Mm,
10490 }),
10491 },
10492 construction: None,
10493 };
10494 let segment = SegmentCtor::Circle(circle_ctor);
10495 let (src_delta, scene_delta) = frontend
10496 .add_segment(&mock_ctx, version, sketch_id, segment, None)
10497 .await
10498 .unwrap();
10499 assert!(
10500 src_delta
10501 .text
10502 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
10503 "Expected circle in source, got: {}",
10504 src_delta.text
10505 );
10506 assert!(!scene_delta.new_objects.is_empty());
10507
10508 ctx.close().await;
10509 mock_ctx.close().await;
10510 }
10511
10512 #[tokio::test(flavor = "multi_thread")]
10513 async fn test_extra_newlines_add_constraint() {
10514 let initial_source = "@settings(defaultLengthUnit = mm)
10516
10517
10518
10519sketch001 = sketch(on = XY) {
10520 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
10521 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
10522}
10523";
10524
10525 let program = Program::parse(initial_source).unwrap().0.unwrap();
10526 let mut frontend = FrontendState::new();
10527
10528 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10529 let mock_ctx = ExecutorContext::new_mock(None).await;
10530 let version = Version(0);
10531 let project_id = ProjectId(0);
10532 let file_id = FileId(0);
10533
10534 frontend.hack_set_program(&ctx, program).await.unwrap();
10535 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10536 let sketch_id = sketch_object.id;
10537 let sketch = expect_sketch(sketch_object);
10538
10539 let line_ids: Vec<ObjectId> = sketch
10541 .segments
10542 .iter()
10543 .copied()
10544 .filter(|seg_id| {
10545 matches!(
10546 &frontend.scene_graph.objects[seg_id.0].kind,
10547 ObjectKind::Segment {
10548 segment: Segment::Line(_)
10549 }
10550 )
10551 })
10552 .collect();
10553 assert_eq!(line_ids.len(), 2, "Expected two line segments");
10554
10555 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
10556 let ObjectKind::Segment {
10557 segment: Segment::Line(line1_data),
10558 } = &line1.kind
10559 else {
10560 panic!("Expected line");
10561 };
10562 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
10563 let ObjectKind::Segment {
10564 segment: Segment::Line(line2_data),
10565 } = &line2.kind
10566 else {
10567 panic!("Expected line");
10568 };
10569
10570 let constraint = Constraint::Coincident(Coincident {
10572 segments: vec![line1_data.end.into(), line2_data.start.into()],
10573 });
10574
10575 frontend
10577 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
10578 .await
10579 .unwrap();
10580 let (src_delta, _scene_delta) = frontend
10581 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10582 .await
10583 .unwrap();
10584 assert!(
10585 src_delta.text.contains("coincident("),
10586 "Expected coincident constraint in source, got: {}",
10587 src_delta.text
10588 );
10589
10590 ctx.close().await;
10591 mock_ctx.close().await;
10592 }
10593
10594 #[tokio::test(flavor = "multi_thread")]
10595 async fn test_extra_newlines_add_line_then_edit_line() {
10596 let initial_source = "@settings(defaultLengthUnit = mm)
10598
10599
10600
10601sketch001 = sketch(on = XY) {
10602}
10603";
10604
10605 let program = Program::parse(initial_source).unwrap().0.unwrap();
10606 let mut frontend = FrontendState::new();
10607
10608 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10609 let mock_ctx = ExecutorContext::new_mock(None).await;
10610 let version = Version(0);
10611
10612 frontend.hack_set_program(&ctx, program).await.unwrap();
10613 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10614 let sketch_id = sketch_object.id;
10615
10616 let line_ctor = LineCtor {
10618 start: Point2d {
10619 x: Expr::Number(Number {
10620 value: 0.0,
10621 units: NumericSuffix::Mm,
10622 }),
10623 y: Expr::Number(Number {
10624 value: 0.0,
10625 units: NumericSuffix::Mm,
10626 }),
10627 },
10628 end: Point2d {
10629 x: Expr::Number(Number {
10630 value: 10.0,
10631 units: NumericSuffix::Mm,
10632 }),
10633 y: Expr::Number(Number {
10634 value: 10.0,
10635 units: NumericSuffix::Mm,
10636 }),
10637 },
10638 construction: None,
10639 };
10640 let segment = SegmentCtor::Line(line_ctor);
10641 let (src_delta, scene_delta) = frontend
10642 .add_segment(&mock_ctx, version, sketch_id, segment, None)
10643 .await
10644 .unwrap();
10645 assert!(
10646 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
10647 "Expected line in source after add, got: {}",
10648 src_delta.text
10649 );
10650 let line_id = *scene_delta.new_objects.last().unwrap();
10652
10653 let line_ctor = LineCtor {
10655 start: Point2d {
10656 x: Expr::Number(Number {
10657 value: 1.0,
10658 units: NumericSuffix::Mm,
10659 }),
10660 y: Expr::Number(Number {
10661 value: 2.0,
10662 units: NumericSuffix::Mm,
10663 }),
10664 },
10665 end: Point2d {
10666 x: Expr::Number(Number {
10667 value: 13.0,
10668 units: NumericSuffix::Mm,
10669 }),
10670 y: Expr::Number(Number {
10671 value: 14.0,
10672 units: NumericSuffix::Mm,
10673 }),
10674 },
10675 construction: None,
10676 };
10677 let segments = vec![ExistingSegmentCtor {
10678 id: line_id,
10679 ctor: SegmentCtor::Line(line_ctor),
10680 }];
10681 let (src_delta, scene_delta) = frontend
10682 .edit_segments(&mock_ctx, version, sketch_id, segments)
10683 .await
10684 .unwrap();
10685 assert!(
10686 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
10687 "Expected edited line in source, got: {}",
10688 src_delta.text
10689 );
10690 assert_eq!(scene_delta.new_objects, vec![]);
10691
10692 ctx.close().await;
10693 mock_ctx.close().await;
10694 }
10695}