1use std::{cell::Cell, collections::HashSet, ops::ControlFlow};
2
3use kcl_error::SourceRange;
4
5use crate::{
6 ExecOutcome, ExecutorContext, Program,
7 collections::AhashIndexSet,
8 exec::WarningLevel,
9 execution::MockConfig,
10 fmt::format_number_literal,
11 front::{ArcCtor, Distance, Line, LinesEqualLength, Parallel, Perpendicular, PointCtor},
12 frontend::{
13 api::{
14 Error, Expr, FileId, Number, ObjectId, ObjectKind, ProjectId, SceneGraph, SceneGraphDelta, SourceDelta,
15 SourceRef, Version,
16 },
17 modify::{find_defined_names, next_free_name},
18 sketch::{
19 Coincident, Constraint, ExistingSegmentCtor, Horizontal, LineCtor, Point2d, Segment, SegmentCtor,
20 SketchApi, SketchArgs, Vertical,
21 },
22 traverse::{MutateBodyItem, TraversalReturn, Visitor, dfs_mut},
23 },
24 parsing::ast::types as ast,
25 std::constraints::LinesAtAngleKind,
26 walk::{NodeMut, Visitable},
27};
28
29pub(crate) mod api;
30mod modify;
31pub(crate) mod sketch;
32mod traverse;
33
34const POINT_FN: &str = "point";
35const POINT_AT_PARAM: &str = "at";
36const LINE_FN: &str = "line";
37const LINE_START_PARAM: &str = "start";
38const LINE_END_PARAM: &str = "end";
39const ARC_FN: &str = "arc";
40const ARC_START_PARAM: &str = "start";
41const ARC_END_PARAM: &str = "end";
42const ARC_CENTER_PARAM: &str = "center";
43
44const COINCIDENT_FN: &str = "coincident";
45const DISTANCE_FN: &str = "distance";
46const EQUAL_LENGTH_FN: &str = "equalLength";
47const HORIZONTAL_FN: &str = "horizontal";
48const VERTICAL_FN: &str = "vertical";
49
50const LINE_PROPERTY_START: &str = "start";
51const LINE_PROPERTY_END: &str = "end";
52
53#[derive(Debug, Clone)]
54pub struct FrontendState {
55 program: Program,
56 scene_graph: SceneGraph,
57}
58
59impl Default for FrontendState {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl FrontendState {
66 pub fn new() -> Self {
67 Self {
68 program: Program::empty(),
69 scene_graph: SceneGraph {
70 project: ProjectId(0),
71 file: FileId(0),
72 version: Version(0),
73 objects: Default::default(),
74 settings: Default::default(),
75 sketch_mode: Default::default(),
76 },
77 }
78 }
79}
80
81impl SketchApi for FrontendState {
82 async fn execute_mock(
83 &mut self,
84 ctx: &ExecutorContext,
85 _version: Version,
86 _sketch: ObjectId,
87 ) -> api::Result<(SceneGraph, ExecOutcome)> {
88 let outcome = ctx
90 .run_mock(&self.program, &MockConfig::default())
91 .await
92 .map_err(|err| Error {
93 msg: err.error.message().to_owned(),
94 })?;
95 let outcome = self.update_state_after_exec(outcome);
96 Ok((self.scene_graph.clone(), outcome))
97 }
98
99 async fn new_sketch(
100 &mut self,
101 ctx: &ExecutorContext,
102 _project: ProjectId,
103 _file: FileId,
104 _version: Version,
105 args: SketchArgs,
106 ) -> api::Result<(SourceDelta, SceneGraphDelta, ObjectId)> {
107 let plane_ast = match &args.on {
111 api::Plane::Object(_) => todo!(),
113 api::Plane::Default(plane) => ast_name_expr(plane.to_string()),
114 };
115 let sketch_ast = ast::SketchBlock {
116 arguments: vec![ast::LabeledArg {
117 label: Some(ast::Identifier::new("on")),
118 arg: plane_ast,
119 }],
120 body: Default::default(),
121 non_code_meta: Default::default(),
122 digest: None,
123 };
124 let mut new_ast = self.program.ast.clone();
125 new_ast.set_experimental_features(Some(WarningLevel::Allow));
128 new_ast.body.push(ast::BodyItem::ExpressionStatement(ast::Node {
130 inner: ast::ExpressionStatement {
131 expression: ast::Expr::SketchBlock(Box::new(ast::Node {
132 inner: sketch_ast,
133 start: Default::default(),
134 end: Default::default(),
135 module_id: Default::default(),
136 outer_attrs: Default::default(),
137 pre_comments: Default::default(),
138 comment_start: Default::default(),
139 })),
140 digest: None,
141 },
142 start: Default::default(),
143 end: Default::default(),
144 module_id: Default::default(),
145 outer_attrs: Default::default(),
146 pre_comments: Default::default(),
147 comment_start: Default::default(),
148 }));
149 let new_source = source_from_ast(&new_ast);
151 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
153 if !errors.is_empty() {
154 return Err(Error {
155 msg: format!("Error parsing KCL source after adding sketch: {errors:?}"),
156 });
157 }
158 let Some(new_program) = new_program else {
159 return Err(Error {
160 msg: "No AST produced after adding sketch".to_owned(),
161 });
162 };
163
164 let sketch_source_range = new_program
165 .ast
166 .body
167 .last()
168 .map(SourceRange::from)
169 .ok_or_else(|| Error {
170 msg: "No AST body items after adding sketch".to_owned(),
171 })?;
172 #[cfg(not(feature = "artifact-graph"))]
173 let _ = sketch_source_range;
174
175 self.program = new_program.clone();
177
178 let outcome = ctx
180 .run_mock(&new_program, &MockConfig::default())
181 .await
182 .map_err(|err| {
183 Error {
186 msg: err.error.message().to_owned(),
187 }
188 })?;
189
190 #[cfg(not(feature = "artifact-graph"))]
191 let sketch_id = ObjectId(0);
192 #[cfg(feature = "artifact-graph")]
193 let sketch_id = outcome
194 .source_range_to_object
195 .get(&sketch_source_range)
196 .copied()
197 .ok_or_else(|| Error {
198 msg: format!("Source range of sketch not found: {sketch_source_range:?}"),
199 })?;
200 let src_delta = SourceDelta { text: new_source };
201 self.scene_graph.sketch_mode = Some(sketch_id);
203 let outcome = self.update_state_after_exec(outcome);
204 let scene_graph_delta = SceneGraphDelta {
205 new_graph: self.scene_graph.clone(),
206 invalidates_ids: false,
207 new_objects: vec![sketch_id],
208 exec_outcome: outcome,
209 };
210 Ok((src_delta, scene_graph_delta, sketch_id))
211 }
212
213 async fn edit_sketch(
214 &mut self,
215 ctx: &ExecutorContext,
216 _project: ProjectId,
217 _file: FileId,
218 _version: Version,
219 sketch: ObjectId,
220 ) -> api::Result<SceneGraphDelta> {
221 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| Error {
225 msg: format!("Sketch not found: {sketch:?}"),
226 })?;
227 let ObjectKind::Sketch(_) = &sketch_object.kind else {
228 return Err(Error {
229 msg: format!("Object is not a sketch: {sketch_object:?}"),
230 });
231 };
232
233 self.scene_graph.sketch_mode = Some(sketch);
235
236 let mock_config = MockConfig {
239 freedom_analysis: true,
240 ..Default::default()
241 };
242 let outcome = ctx.run_mock(&self.program, &mock_config).await.map_err(|err| {
243 Error {
246 msg: err.error.message().to_owned(),
247 }
248 })?;
249
250 let outcome = self.update_state_after_exec(outcome);
251 let scene_graph_delta = SceneGraphDelta {
252 new_graph: self.scene_graph.clone(),
253 invalidates_ids: false,
254 new_objects: Vec::new(),
255 exec_outcome: outcome,
256 };
257 Ok(scene_graph_delta)
258 }
259
260 async fn exit_sketch(
261 &mut self,
262 ctx: &ExecutorContext,
263 _version: Version,
264 sketch: ObjectId,
265 ) -> api::Result<SceneGraph> {
266 #[cfg(not(target_arch = "wasm32"))]
268 let _ = sketch;
269 #[cfg(target_arch = "wasm32")]
270 if self.scene_graph.sketch_mode != Some(sketch) {
271 web_sys::console::warn_1(
272 &format!(
273 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
274 &self.scene_graph.sketch_mode
275 )
276 .into(),
277 );
278 }
279 self.scene_graph.sketch_mode = None;
280
281 let outcome = ctx.run_with_caching(self.program.clone()).await.map_err(|err| {
283 Error {
286 msg: err.error.message().to_owned(),
287 }
288 })?;
289
290 self.update_state_after_exec(outcome);
291
292 Ok(self.scene_graph.clone())
293 }
294
295 async fn delete_sketch(
296 &mut self,
297 ctx: &ExecutorContext,
298 _version: Version,
299 sketch: ObjectId,
300 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
301 let mut new_ast = self.program.ast.clone();
304
305 let sketch_id = sketch;
307 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
308 msg: format!("Sketch not found: {sketch:?}"),
309 })?;
310 let ObjectKind::Sketch(_) = &sketch_object.kind else {
311 return Err(Error {
312 msg: format!("Object is not a sketch: {sketch_object:?}"),
313 });
314 };
315
316 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)?;
318
319 self.execute_after_edit(ctx, Default::default(), true, &mut new_ast)
320 .await
321 }
322
323 async fn add_segment(
324 &mut self,
325 ctx: &ExecutorContext,
326 _version: Version,
327 sketch: ObjectId,
328 segment: SegmentCtor,
329 _label: Option<String>,
330 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
331 match segment {
333 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
334 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
335 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
336 _ => Err(Error {
337 msg: format!("segment ctor not implemented yet: {segment:?}"),
338 }),
339 }
340 }
341
342 async fn edit_segments(
343 &mut self,
344 ctx: &ExecutorContext,
345 _version: Version,
346 sketch: ObjectId,
347 segments: Vec<ExistingSegmentCtor>,
348 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
349 let mut new_ast = self.program.ast.clone();
351 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
352 for segment in segments {
353 segment_ids_edited.insert(segment.id);
354 match segment.ctor {
355 SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
356 SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
357 SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment.id, ctor)?,
358 _ => {
359 return Err(Error {
360 msg: format!("segment ctor not implemented yet: {segment:?}"),
361 });
362 }
363 }
364 }
365 self.execute_after_edit(ctx, segment_ids_edited, false, &mut new_ast)
366 .await
367 }
368
369 async fn delete_objects(
370 &mut self,
371 ctx: &ExecutorContext,
372 _version: Version,
373 sketch: ObjectId,
374 constraint_ids: Vec<ObjectId>,
375 segment_ids: Vec<ObjectId>,
376 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
377 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
381 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
382 self.add_dependent_constraints_to_delete(sketch, &segment_ids_set, &mut constraint_ids_set)?;
385
386 let mut new_ast = self.program.ast.clone();
387 for constraint_id in constraint_ids_set {
388 self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
389 }
390 for segment_id in segment_ids_set {
391 self.delete_segment(&mut new_ast, sketch, segment_id)?;
392 }
393 self.execute_after_edit(ctx, Default::default(), true, &mut new_ast)
394 .await
395 }
396
397 async fn add_constraint(
398 &mut self,
399 ctx: &ExecutorContext,
400 _version: Version,
401 sketch: ObjectId,
402 constraint: Constraint,
403 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
404 let mut new_ast = self.program.ast.clone();
407 let sketch_block_range = match constraint {
408 Constraint::Coincident(coincident) => self.add_coincident(sketch, coincident, &mut new_ast).await?,
409 Constraint::Distance(distance) => self.add_distance(sketch, distance, &mut new_ast).await?,
410 Constraint::Horizontal(horizontal) => self.add_horizontal(sketch, horizontal, &mut new_ast).await?,
411 Constraint::LinesEqualLength(lines_equal_length) => {
412 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
413 .await?
414 }
415 Constraint::Parallel(parallel) => self.add_parallel(sketch, parallel, &mut new_ast).await?,
416 Constraint::Perpendicular(perpendicular) => {
417 self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?
418 }
419 Constraint::Vertical(vertical) => self.add_vertical(sketch, vertical, &mut new_ast).await?,
420 };
421 self.execute_after_add_constraint(ctx, sketch, sketch_block_range, &mut new_ast)
422 .await
423 }
424
425 async fn edit_constraint(
426 &mut self,
427 _ctx: &ExecutorContext,
428 _version: Version,
429 _sketch: ObjectId,
430 _constraint_id: ObjectId,
431 _constraint: Constraint,
432 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
433 todo!()
434 }
435}
436
437impl FrontendState {
438 pub async fn hack_set_program(
439 &mut self,
440 ctx: &ExecutorContext,
441 program: Program,
442 ) -> api::Result<(SceneGraph, ExecOutcome)> {
443 self.program = program.clone();
444
445 let outcome = ctx.run_with_caching(program).await.map_err(|err| {
448 Error {
451 msg: err.error.message().to_owned(),
452 }
453 })?;
454
455 let outcome = self.update_state_after_exec(outcome);
456
457 Ok((self.scene_graph.clone(), outcome))
458 }
459
460 async fn add_point(
461 &mut self,
462 ctx: &ExecutorContext,
463 sketch: ObjectId,
464 ctor: PointCtor,
465 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
466 let at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
468 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
469 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
470 unlabeled: None,
471 arguments: vec![ast::LabeledArg {
472 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
473 arg: at_ast,
474 }],
475 digest: None,
476 non_code_meta: Default::default(),
477 })));
478
479 let sketch_id = sketch;
481 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
482 #[cfg(target_arch = "wasm32")]
483 web_sys::console::error_1(
484 &format!(
485 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
486 &self.scene_graph.objects
487 )
488 .into(),
489 );
490 Error {
491 msg: format!("Sketch not found: {sketch:?}"),
492 }
493 })?;
494 let ObjectKind::Sketch(_) = &sketch_object.kind else {
495 return Err(Error {
496 msg: format!("Object is not a sketch: {sketch_object:?}"),
497 });
498 };
499 let mut new_ast = self.program.ast.clone();
501 let (sketch_block_range, _) = self.mutate_ast(
502 &mut new_ast,
503 sketch_id,
504 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
505 )?;
506 let new_source = source_from_ast(&new_ast);
508 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
510 if !errors.is_empty() {
511 return Err(Error {
512 msg: format!("Error parsing KCL source after adding point: {errors:?}"),
513 });
514 }
515 let Some(new_program) = new_program else {
516 return Err(Error {
517 msg: "No AST produced after adding point".to_string(),
518 });
519 };
520
521 let point_source_range =
522 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
523 msg: format!("Source range of point not found in sketch block: {sketch_block_range:?}; {err:?}"),
524 })?;
525 #[cfg(not(feature = "artifact-graph"))]
526 let _ = point_source_range;
527
528 self.program = new_program.clone();
530
531 let outcome = ctx
533 .run_mock(&new_program, &MockConfig::default())
534 .await
535 .map_err(|err| {
536 Error {
539 msg: err.error.message().to_owned(),
540 }
541 })?;
542
543 #[cfg(not(feature = "artifact-graph"))]
544 let new_object_ids = Vec::new();
545 #[cfg(feature = "artifact-graph")]
546 let new_object_ids = {
547 let segment_id = outcome
548 .source_range_to_object
549 .get(&point_source_range)
550 .copied()
551 .ok_or_else(|| Error {
552 msg: format!("Source range of point not found: {point_source_range:?}"),
553 })?;
554 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
555 msg: format!("Segment not found: {segment_id:?}"),
556 })?;
557 let ObjectKind::Segment { segment } = &segment_object.kind else {
558 return Err(Error {
559 msg: format!("Object is not a segment: {segment_object:?}"),
560 });
561 };
562 let Segment::Point(_) = segment else {
563 return Err(Error {
564 msg: format!("Segment is not a point: {segment:?}"),
565 });
566 };
567 vec![segment_id]
568 };
569 let src_delta = SourceDelta { text: new_source };
570 let outcome = self.update_state_after_exec(outcome);
571 let scene_graph_delta = SceneGraphDelta {
572 new_graph: self.scene_graph.clone(),
573 invalidates_ids: false,
574 new_objects: new_object_ids,
575 exec_outcome: outcome,
576 };
577 Ok((src_delta, scene_graph_delta))
578 }
579
580 async fn add_line(
581 &mut self,
582 ctx: &ExecutorContext,
583 sketch: ObjectId,
584 ctor: LineCtor,
585 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
586 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
588 let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
589 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
590 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
591 unlabeled: None,
592 arguments: vec![
593 ast::LabeledArg {
594 label: Some(ast::Identifier::new(LINE_START_PARAM)),
595 arg: start_ast,
596 },
597 ast::LabeledArg {
598 label: Some(ast::Identifier::new(LINE_END_PARAM)),
599 arg: end_ast,
600 },
601 ],
602 digest: None,
603 non_code_meta: Default::default(),
604 })));
605
606 let sketch_id = sketch;
608 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
609 msg: format!("Sketch not found: {sketch:?}"),
610 })?;
611 let ObjectKind::Sketch(_) = &sketch_object.kind else {
612 return Err(Error {
613 msg: format!("Object is not a sketch: {sketch_object:?}"),
614 });
615 };
616 let mut new_ast = self.program.ast.clone();
618 let (sketch_block_range, _) = self.mutate_ast(
619 &mut new_ast,
620 sketch_id,
621 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
622 )?;
623 let new_source = source_from_ast(&new_ast);
625 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
627 if !errors.is_empty() {
628 return Err(Error {
629 msg: format!("Error parsing KCL source after adding line: {errors:?}"),
630 });
631 }
632 let Some(new_program) = new_program else {
633 return Err(Error {
634 msg: "No AST produced after adding line".to_string(),
635 });
636 };
637 let line_source_range =
638 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
639 msg: format!("Source range of line not found in sketch block: {sketch_block_range:?}; {err:?}"),
640 })?;
641 #[cfg(not(feature = "artifact-graph"))]
642 let _ = line_source_range;
643
644 self.program = new_program.clone();
646
647 let outcome = ctx
649 .run_mock(&new_program, &MockConfig::default())
650 .await
651 .map_err(|err| {
652 Error {
655 msg: err.error.message().to_owned(),
656 }
657 })?;
658
659 #[cfg(not(feature = "artifact-graph"))]
660 let new_object_ids = Vec::new();
661 #[cfg(feature = "artifact-graph")]
662 let new_object_ids = {
663 let segment_id = outcome
664 .source_range_to_object
665 .get(&line_source_range)
666 .copied()
667 .ok_or_else(|| Error {
668 msg: format!("Source range of line not found: {line_source_range:?}"),
669 })?;
670 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
671 msg: format!("Segment not found: {segment_id:?}"),
672 })?;
673 let ObjectKind::Segment { segment } = &segment_object.kind else {
674 return Err(Error {
675 msg: format!("Object is not a segment: {segment_object:?}"),
676 });
677 };
678 let Segment::Line(line) = segment else {
679 return Err(Error {
680 msg: format!("Segment is not a line: {segment:?}"),
681 });
682 };
683 vec![line.start, line.end, segment_id]
684 };
685 let src_delta = SourceDelta { text: new_source };
686 let outcome = self.update_state_after_exec(outcome);
687 let scene_graph_delta = SceneGraphDelta {
688 new_graph: self.scene_graph.clone(),
689 invalidates_ids: false,
690 new_objects: new_object_ids,
691 exec_outcome: outcome,
692 };
693 Ok((src_delta, scene_graph_delta))
694 }
695
696 async fn add_arc(
697 &mut self,
698 ctx: &ExecutorContext,
699 sketch: ObjectId,
700 ctor: ArcCtor,
701 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
702 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
704 let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
705 let center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
706 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
707 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
708 unlabeled: None,
709 arguments: vec![
710 ast::LabeledArg {
711 label: Some(ast::Identifier::new(ARC_START_PARAM)),
712 arg: start_ast,
713 },
714 ast::LabeledArg {
715 label: Some(ast::Identifier::new(ARC_END_PARAM)),
716 arg: end_ast,
717 },
718 ast::LabeledArg {
719 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
720 arg: center_ast,
721 },
722 ],
723 digest: None,
724 non_code_meta: Default::default(),
725 })));
726
727 let sketch_id = sketch;
729 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
730 msg: format!("Sketch not found: {sketch:?}"),
731 })?;
732 let ObjectKind::Sketch(_) = &sketch_object.kind else {
733 return Err(Error {
734 msg: format!("Object is not a sketch: {sketch_object:?}"),
735 });
736 };
737 let mut new_ast = self.program.ast.clone();
739 let (sketch_block_range, _) = self.mutate_ast(
740 &mut new_ast,
741 sketch_id,
742 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
743 )?;
744 let new_source = source_from_ast(&new_ast);
746 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
748 if !errors.is_empty() {
749 return Err(Error {
750 msg: format!("Error parsing KCL source after adding arc: {errors:?}"),
751 });
752 }
753 let Some(new_program) = new_program else {
754 return Err(Error {
755 msg: "No AST produced after adding arc".to_string(),
756 });
757 };
758 let arc_source_range =
759 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
760 msg: format!("Source range of arc not found in sketch block: {sketch_block_range:?}; {err:?}"),
761 })?;
762 #[cfg(not(feature = "artifact-graph"))]
763 let _ = arc_source_range;
764
765 self.program = new_program.clone();
767
768 let outcome = ctx
770 .run_mock(&new_program, &MockConfig::default())
771 .await
772 .map_err(|err| {
773 Error {
776 msg: err.error.message().to_owned(),
777 }
778 })?;
779
780 #[cfg(not(feature = "artifact-graph"))]
781 let new_object_ids = Vec::new();
782 #[cfg(feature = "artifact-graph")]
783 let new_object_ids = {
784 let segment_id = outcome
785 .source_range_to_object
786 .get(&arc_source_range)
787 .copied()
788 .ok_or_else(|| Error {
789 msg: format!("Source range of arc not found: {arc_source_range:?}"),
790 })?;
791 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
792 msg: format!("Segment not found: {segment_id:?}"),
793 })?;
794 let ObjectKind::Segment { segment } = &segment_object.kind else {
795 return Err(Error {
796 msg: format!("Object is not a segment: {segment_object:?}"),
797 });
798 };
799 let Segment::Arc(arc) = segment else {
800 return Err(Error {
801 msg: format!("Segment is not an arc: {segment:?}"),
802 });
803 };
804 vec![arc.start, arc.end, arc.center, segment_id]
805 };
806 let src_delta = SourceDelta { text: new_source };
807 let outcome = self.update_state_after_exec(outcome);
808 let scene_graph_delta = SceneGraphDelta {
809 new_graph: self.scene_graph.clone(),
810 invalidates_ids: false,
811 new_objects: new_object_ids,
812 exec_outcome: outcome,
813 };
814 Ok((src_delta, scene_graph_delta))
815 }
816
817 fn edit_point(
818 &mut self,
819 new_ast: &mut ast::Node<ast::Program>,
820 sketch: ObjectId,
821 point: ObjectId,
822 ctor: PointCtor,
823 ) -> api::Result<()> {
824 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
826
827 let sketch_id = sketch;
829 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
830 msg: format!("Sketch not found: {sketch:?}"),
831 })?;
832 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
833 return Err(Error {
834 msg: format!("Object is not a sketch: {sketch_object:?}"),
835 });
836 };
837 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| Error {
838 msg: format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"),
839 })?;
840 let point_id = point;
842 let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
843 msg: format!("Point not found in scene graph: point={point:?}"),
844 })?;
845 let ObjectKind::Segment {
846 segment: Segment::Point(point),
847 } = &point_object.kind
848 else {
849 return Err(Error {
850 msg: format!("Object is not a point segment: {point_object:?}"),
851 });
852 };
853
854 if let Some(line_id) = point.owner {
856 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
857 msg: format!("Internal: Line owner of point not found in scene graph: line={line_id:?}",),
858 })?;
859 let ObjectKind::Segment {
860 segment: Segment::Line(line),
861 } = &line_object.kind
862 else {
863 return Err(Error {
864 msg: format!("Internal: Owner of point is not actually a line segment: {line_object:?}"),
865 });
866 };
867 let SegmentCtor::Line(line_ctor) = &line.ctor else {
868 return Err(Error {
869 msg: format!("Internal: Owner of point does not have line ctor: {line_object:?}"),
870 });
871 };
872 let mut line_ctor = line_ctor.clone();
873 if line.start == point_id {
875 line_ctor.start = ctor.position;
876 } else if line.end == point_id {
877 line_ctor.end = ctor.position;
878 } else {
879 return Err(Error {
880 msg: format!(
881 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={line_id:?}"
882 ),
883 });
884 }
885 return self.edit_line(new_ast, sketch_id, line_id, line_ctor);
886 }
887
888 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
890 Ok(())
891 }
892
893 fn edit_line(
894 &mut self,
895 new_ast: &mut ast::Node<ast::Program>,
896 sketch: ObjectId,
897 line: ObjectId,
898 ctor: LineCtor,
899 ) -> api::Result<()> {
900 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
902 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
903
904 let sketch_id = sketch;
906 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
907 msg: format!("Sketch not found: {sketch:?}"),
908 })?;
909 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
910 return Err(Error {
911 msg: format!("Object is not a sketch: {sketch_object:?}"),
912 });
913 };
914 sketch.segments.iter().find(|o| **o == line).ok_or_else(|| Error {
915 msg: format!("Line not found in sketch: line={line:?}, sketch={sketch:?}"),
916 })?;
917 let line_id = line;
919 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
920 msg: format!("Line not found in scene graph: line={line:?}"),
921 })?;
922 let ObjectKind::Segment { .. } = &line_object.kind else {
923 return Err(Error {
924 msg: format!("Object is not a segment: {line_object:?}"),
925 });
926 };
927
928 self.mutate_ast(
930 new_ast,
931 line_id,
932 AstMutateCommand::EditLine {
933 start: new_start_ast,
934 end: new_end_ast,
935 },
936 )?;
937 Ok(())
938 }
939
940 fn edit_arc(
941 &mut self,
942 new_ast: &mut ast::Node<ast::Program>,
943 sketch: ObjectId,
944 arc: ObjectId,
945 ctor: ArcCtor,
946 ) -> api::Result<()> {
947 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
949 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
950 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
951
952 let sketch_id = sketch;
954 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
955 msg: format!("Sketch not found: {sketch:?}"),
956 })?;
957 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
958 return Err(Error {
959 msg: format!("Object is not a sketch: {sketch_object:?}"),
960 });
961 };
962 sketch.segments.iter().find(|o| **o == arc).ok_or_else(|| Error {
963 msg: format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}"),
964 })?;
965 let arc_id = arc;
967 let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
968 msg: format!("Arc not found in scene graph: arc={arc:?}"),
969 })?;
970 let ObjectKind::Segment { .. } = &arc_object.kind else {
971 return Err(Error {
972 msg: format!("Object is not a segment: {arc_object:?}"),
973 });
974 };
975
976 self.mutate_ast(
978 new_ast,
979 arc_id,
980 AstMutateCommand::EditArc {
981 start: new_start_ast,
982 end: new_end_ast,
983 center: new_center_ast,
984 },
985 )?;
986 Ok(())
987 }
988
989 fn delete_segment(
990 &mut self,
991 new_ast: &mut ast::Node<ast::Program>,
992 sketch: ObjectId,
993 segment_id: ObjectId,
994 ) -> api::Result<()> {
995 let sketch_id = sketch;
997 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
998 msg: format!("Sketch not found: {sketch:?}"),
999 })?;
1000 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1001 return Err(Error {
1002 msg: format!("Object is not a sketch: {sketch_object:?}"),
1003 });
1004 };
1005 sketch
1006 .segments
1007 .iter()
1008 .find(|o| **o == segment_id)
1009 .ok_or_else(|| Error {
1010 msg: format!("Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"),
1011 })?;
1012 let segment_object = self.scene_graph.objects.get(segment_id.0).ok_or_else(|| Error {
1014 msg: format!("Segment not found in scene graph: segment={segment_id:?}"),
1015 })?;
1016 let ObjectKind::Segment { .. } = &segment_object.kind else {
1017 return Err(Error {
1018 msg: format!("Object is not a segment: {segment_object:?}"),
1019 });
1020 };
1021
1022 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
1024 Ok(())
1025 }
1026
1027 fn delete_constraint(
1028 &mut self,
1029 new_ast: &mut ast::Node<ast::Program>,
1030 sketch: ObjectId,
1031 constraint_id: ObjectId,
1032 ) -> api::Result<()> {
1033 let sketch_id = sketch;
1035 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1036 msg: format!("Sketch not found: {sketch:?}"),
1037 })?;
1038 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1039 return Err(Error {
1040 msg: format!("Object is not a sketch: {sketch_object:?}"),
1041 });
1042 };
1043 sketch
1044 .constraints
1045 .iter()
1046 .find(|o| **o == constraint_id)
1047 .ok_or_else(|| Error {
1048 msg: format!("Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"),
1049 })?;
1050 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
1052 msg: format!("Constraint not found in scene graph: constraint={constraint_id:?}"),
1053 })?;
1054 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
1055 return Err(Error {
1056 msg: format!("Object is not a constraint: {constraint_object:?}"),
1057 });
1058 };
1059
1060 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
1062 Ok(())
1063 }
1064
1065 async fn execute_after_edit(
1066 &mut self,
1067 ctx: &ExecutorContext,
1068 segment_ids_edited: AhashIndexSet<ObjectId>,
1069 is_delete: bool,
1070 new_ast: &mut ast::Node<ast::Program>,
1071 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1072 let new_source = source_from_ast(new_ast);
1074 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1076 if !errors.is_empty() {
1077 return Err(Error {
1078 msg: format!("Error parsing KCL source after editing: {errors:?}"),
1079 });
1080 }
1081 let Some(new_program) = new_program else {
1082 return Err(Error {
1083 msg: "No AST produced after editing".to_string(),
1084 });
1085 };
1086
1087 self.program = new_program.clone();
1089
1090 #[cfg(not(feature = "artifact-graph"))]
1091 drop(segment_ids_edited);
1092
1093 let mock_config = MockConfig {
1095 use_prev_memory: !is_delete,
1096 freedom_analysis: is_delete,
1097 #[cfg(feature = "artifact-graph")]
1098 segment_ids_edited,
1099 };
1100 let outcome = ctx.run_mock(&new_program, &mock_config).await.map_err(|err| {
1101 Error {
1104 msg: err.error.message().to_owned(),
1105 }
1106 })?;
1107
1108 let outcome = self.update_state_after_exec(outcome);
1109
1110 #[cfg(feature = "artifact-graph")]
1111 let new_source = {
1112 let mut new_ast = self.program.ast.clone();
1118 for (var_range, value) in &outcome.var_solutions {
1119 let rounded = value.round(3);
1120 mutate_ast_node_by_source_range(
1121 &mut new_ast,
1122 *var_range,
1123 AstMutateCommand::EditVarInitialValue { value: rounded },
1124 )?;
1125 }
1126 source_from_ast(&new_ast)
1127 };
1128
1129 let src_delta = SourceDelta { text: new_source };
1130 let scene_graph_delta = SceneGraphDelta {
1131 new_graph: self.scene_graph.clone(),
1132 invalidates_ids: is_delete,
1133 new_objects: Vec::new(),
1134 exec_outcome: outcome,
1135 };
1136 Ok((src_delta, scene_graph_delta))
1137 }
1138
1139 async fn add_coincident(
1140 &mut self,
1141 sketch: ObjectId,
1142 coincident: Coincident,
1143 new_ast: &mut ast::Node<ast::Program>,
1144 ) -> api::Result<SourceRange> {
1145 let &[seg0_id, seg1_id] = coincident.segments.as_slice() else {
1146 return Err(Error {
1147 msg: format!(
1148 "Coincident constraint must have exactly 2 segments, got {}",
1149 coincident.segments.len()
1150 ),
1151 });
1152 };
1153 let sketch_id = sketch;
1154
1155 let seg0_object = self.scene_graph.objects.get(seg0_id.0).ok_or_else(|| Error {
1157 msg: format!("Object not found: {seg0_id:?}"),
1158 })?;
1159 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
1160 return Err(Error {
1161 msg: format!("Object is not a segment: {seg0_object:?}"),
1162 });
1163 };
1164 let seg0_ast = match seg0_segment {
1165 Segment::Point(point) => {
1166 if let Some(line_id) = point.owner {
1168 let line = self.expect_line(line_id)?;
1169 let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
1170 let property = if line.start == seg0_id {
1171 LINE_PROPERTY_START
1172 } else if line.end == seg0_id {
1173 LINE_PROPERTY_END
1174 } else {
1175 return Err(Error {
1176 msg: format!(
1177 "Internal: Point is not part of owner's line segment: point={seg0_id:?}, line={line_id:?}"
1178 ),
1179 });
1180 };
1181 get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
1182 } else {
1183 get_or_insert_ast_reference(new_ast, &seg0_object.source, "point", None)?
1185 }
1186 }
1187 Segment::Line(_) => {
1188 get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?
1190 }
1191 Segment::Arc(_) | Segment::Circle(_) => {
1192 return Err(Error {
1193 msg: "Coincident constraint with arcs or circles is not supported. Only points and line segments are.".to_owned(),
1194 });
1195 }
1196 };
1197
1198 let seg1_object = self.scene_graph.objects.get(seg1_id.0).ok_or_else(|| Error {
1200 msg: format!("Object not found: {seg1_id:?}"),
1201 })?;
1202 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
1203 return Err(Error {
1204 msg: format!("Object is not a segment: {seg1_object:?}"),
1205 });
1206 };
1207 let seg1_ast = match seg1_segment {
1208 Segment::Point(point) => {
1209 if let Some(line_id) = point.owner {
1211 let line = self.expect_line(line_id)?;
1212 let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
1213 let property = if line.start == seg1_id {
1214 LINE_PROPERTY_START
1215 } else if line.end == seg1_id {
1216 LINE_PROPERTY_END
1217 } else {
1218 return Err(Error {
1219 msg: format!(
1220 "Internal: Point is not part of owner's line segment: point={seg1_id:?}, line={line_id:?}"
1221 ),
1222 });
1223 };
1224 get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
1225 } else {
1226 get_or_insert_ast_reference(new_ast, &seg1_object.source, "point", None)?
1228 }
1229 }
1230 Segment::Line(_) => {
1231 get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?
1233 }
1234 Segment::Arc(_) | Segment::Circle(_) => {
1235 return Err(Error {
1236 msg: "Coincident constraint with arcs or circles is not supported. Only points and line segments are.".to_owned(),
1237 });
1238 }
1239 };
1240
1241 let coincident_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1243 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
1244 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1245 ast::ArrayExpression {
1246 elements: vec![seg0_ast, seg1_ast],
1247 digest: None,
1248 non_code_meta: Default::default(),
1249 },
1250 )))),
1251 arguments: Default::default(),
1252 digest: None,
1253 non_code_meta: Default::default(),
1254 })));
1255
1256 let (sketch_block_range, _) = self.mutate_ast(
1258 new_ast,
1259 sketch_id,
1260 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
1261 )?;
1262 Ok(sketch_block_range)
1263 }
1264
1265 async fn add_distance(
1266 &mut self,
1267 sketch: ObjectId,
1268 distance: Distance,
1269 new_ast: &mut ast::Node<ast::Program>,
1270 ) -> api::Result<SourceRange> {
1271 let &[pt0_id, pt1_id] = distance.points.as_slice() else {
1272 return Err(Error {
1273 msg: format!(
1274 "Distance constraint must have exactly 2 points, got {}",
1275 distance.points.len()
1276 ),
1277 });
1278 };
1279 let sketch_id = sketch;
1280
1281 let pt0_object = self.scene_graph.objects.get(pt0_id.0).ok_or_else(|| Error {
1283 msg: format!("Point not found: {pt0_id:?}"),
1284 })?;
1285 let ObjectKind::Segment { segment: pt0_segment } = &pt0_object.kind else {
1286 return Err(Error {
1287 msg: format!("Object is not a segment: {pt0_object:?}"),
1288 });
1289 };
1290 let Segment::Point(pt0) = pt0_segment else {
1291 return Err(Error {
1292 msg: format!("Only points are currently supported: {pt0_object:?}"),
1293 });
1294 };
1295 let pt0_ast = if let Some(line_id) = pt0.owner {
1297 let line = self.expect_line(line_id)?;
1298 let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
1299 let property = if line.start == pt0_id {
1300 LINE_PROPERTY_START
1301 } else if line.end == pt0_id {
1302 LINE_PROPERTY_END
1303 } else {
1304 return Err(Error {
1305 msg: format!(
1306 "Internal: Point is not part of owner's line segment: point={pt0_id:?}, line={line_id:?}"
1307 ),
1308 });
1309 };
1310 get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
1311 } else {
1312 get_or_insert_ast_reference(new_ast, &pt0_object.source, "point", None)?
1313 };
1314
1315 let pt1_object = self.scene_graph.objects.get(pt1_id.0).ok_or_else(|| Error {
1316 msg: format!("Point not found: {pt1_id:?}"),
1317 })?;
1318 let ObjectKind::Segment { segment: pt1_segment } = &pt1_object.kind else {
1319 return Err(Error {
1320 msg: format!("Object is not a segment: {pt1_object:?}"),
1321 });
1322 };
1323 let Segment::Point(pt1) = pt1_segment else {
1324 return Err(Error {
1325 msg: format!("Only points are currently supported: {pt1_object:?}"),
1326 });
1327 };
1328 let pt1_ast = if let Some(line_id) = pt1.owner {
1330 let line = self.expect_line(line_id)?;
1331 let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
1332 let property = if line.start == pt1_id {
1333 LINE_PROPERTY_START
1334 } else if line.end == pt1_id {
1335 LINE_PROPERTY_END
1336 } else {
1337 return Err(Error {
1338 msg: format!(
1339 "Internal: Point is not part of owner's line segment: point={pt1_id:?}, line={line_id:?}"
1340 ),
1341 });
1342 };
1343 get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
1344 } else {
1345 get_or_insert_ast_reference(new_ast, &pt1_object.source, "point", None)?
1346 };
1347
1348 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1350 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
1351 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1352 ast::ArrayExpression {
1353 elements: vec![pt0_ast, pt1_ast],
1354 digest: None,
1355 non_code_meta: Default::default(),
1356 },
1357 )))),
1358 arguments: Default::default(),
1359 digest: None,
1360 non_code_meta: Default::default(),
1361 })));
1362 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
1363 left: distance_call_ast,
1364 operator: ast::BinaryOperator::Eq,
1365 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
1366 value: ast::LiteralValue::Number {
1367 value: distance.distance.value,
1368 suffix: distance.distance.units,
1369 },
1370 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
1371 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
1372 })?,
1373 digest: None,
1374 }))),
1375 digest: None,
1376 })));
1377
1378 let (sketch_block_range, _) = self.mutate_ast(
1380 new_ast,
1381 sketch_id,
1382 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
1383 )?;
1384 Ok(sketch_block_range)
1385 }
1386
1387 async fn add_horizontal(
1388 &mut self,
1389 sketch: ObjectId,
1390 horizontal: Horizontal,
1391 new_ast: &mut ast::Node<ast::Program>,
1392 ) -> api::Result<SourceRange> {
1393 let sketch_id = sketch;
1394
1395 let line_id = horizontal.line;
1397 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1398 msg: format!("Line not found: {line_id:?}"),
1399 })?;
1400 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
1401 return Err(Error {
1402 msg: format!("Object is not a segment: {line_object:?}"),
1403 });
1404 };
1405 let Segment::Line(_) = line_segment else {
1406 return Err(Error {
1407 msg: format!("Only lines can be made horizontal: {line_object:?}"),
1408 });
1409 };
1410 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
1411
1412 let horizontal_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1414 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
1415 unlabeled: Some(line_ast),
1416 arguments: Default::default(),
1417 digest: None,
1418 non_code_meta: Default::default(),
1419 })));
1420
1421 let (sketch_block_range, _) = self.mutate_ast(
1423 new_ast,
1424 sketch_id,
1425 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
1426 )?;
1427 Ok(sketch_block_range)
1428 }
1429
1430 async fn add_lines_equal_length(
1431 &mut self,
1432 sketch: ObjectId,
1433 lines_equal_length: LinesEqualLength,
1434 new_ast: &mut ast::Node<ast::Program>,
1435 ) -> api::Result<SourceRange> {
1436 let &[line0_id, line1_id] = lines_equal_length.lines.as_slice() else {
1437 return Err(Error {
1438 msg: format!(
1439 "Lines equal length constraint must have exactly 2 lines, got {}",
1440 lines_equal_length.lines.len()
1441 ),
1442 });
1443 };
1444
1445 let sketch_id = sketch;
1446
1447 let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
1449 msg: format!("Line not found: {line0_id:?}"),
1450 })?;
1451 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
1452 return Err(Error {
1453 msg: format!("Object is not a segment: {line0_object:?}"),
1454 });
1455 };
1456 let Segment::Line(_) = line0_segment else {
1457 return Err(Error {
1458 msg: format!("Only lines can be made equal length: {line0_object:?}"),
1459 });
1460 };
1461 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
1462
1463 let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
1464 msg: format!("Line not found: {line1_id:?}"),
1465 })?;
1466 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
1467 return Err(Error {
1468 msg: format!("Object is not a segment: {line1_object:?}"),
1469 });
1470 };
1471 let Segment::Line(_) = line1_segment else {
1472 return Err(Error {
1473 msg: format!("Only lines can be made equal length: {line1_object:?}"),
1474 });
1475 };
1476 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
1477
1478 let equal_length_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1480 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
1481 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1482 ast::ArrayExpression {
1483 elements: vec![line0_ast, line1_ast],
1484 digest: None,
1485 non_code_meta: Default::default(),
1486 },
1487 )))),
1488 arguments: Default::default(),
1489 digest: None,
1490 non_code_meta: Default::default(),
1491 })));
1492
1493 let (sketch_block_range, _) = self.mutate_ast(
1495 new_ast,
1496 sketch_id,
1497 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
1498 )?;
1499 Ok(sketch_block_range)
1500 }
1501
1502 async fn add_parallel(
1503 &mut self,
1504 sketch: ObjectId,
1505 parallel: Parallel,
1506 new_ast: &mut ast::Node<ast::Program>,
1507 ) -> api::Result<SourceRange> {
1508 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Parallel, parallel.lines, new_ast)
1509 .await
1510 }
1511
1512 async fn add_perpendicular(
1513 &mut self,
1514 sketch: ObjectId,
1515 perpendicular: Perpendicular,
1516 new_ast: &mut ast::Node<ast::Program>,
1517 ) -> api::Result<SourceRange> {
1518 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
1519 .await
1520 }
1521
1522 async fn add_lines_at_angle_constraint(
1523 &mut self,
1524 sketch: ObjectId,
1525 angle_kind: LinesAtAngleKind,
1526 lines: Vec<ObjectId>,
1527 new_ast: &mut ast::Node<ast::Program>,
1528 ) -> api::Result<SourceRange> {
1529 let &[line0_id, line1_id] = lines.as_slice() else {
1530 return Err(Error {
1531 msg: format!(
1532 "{} constraint must have exactly 2 lines, got {}",
1533 angle_kind.to_function_name(),
1534 lines.len()
1535 ),
1536 });
1537 };
1538
1539 let sketch_id = sketch;
1540
1541 let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
1543 msg: format!("Line not found: {line0_id:?}"),
1544 })?;
1545 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
1546 return Err(Error {
1547 msg: format!("Object is not a segment: {line0_object:?}"),
1548 });
1549 };
1550 let Segment::Line(_) = line0_segment else {
1551 return Err(Error {
1552 msg: format!(
1553 "Only lines can be made {}: {line0_object:?}",
1554 angle_kind.to_function_name()
1555 ),
1556 });
1557 };
1558 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
1559
1560 let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
1561 msg: format!("Line not found: {line1_id:?}"),
1562 })?;
1563 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
1564 return Err(Error {
1565 msg: format!("Object is not a segment: {line1_object:?}"),
1566 });
1567 };
1568 let Segment::Line(_) = line1_segment else {
1569 return Err(Error {
1570 msg: format!(
1571 "Only lines can be made {}: {line1_object:?}",
1572 angle_kind.to_function_name()
1573 ),
1574 });
1575 };
1576 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
1577
1578 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1580 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
1581 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1582 ast::ArrayExpression {
1583 elements: vec![line0_ast, line1_ast],
1584 digest: None,
1585 non_code_meta: Default::default(),
1586 },
1587 )))),
1588 arguments: Default::default(),
1589 digest: None,
1590 non_code_meta: Default::default(),
1591 })));
1592
1593 let (sketch_block_range, _) = self.mutate_ast(
1595 new_ast,
1596 sketch_id,
1597 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
1598 )?;
1599 Ok(sketch_block_range)
1600 }
1601
1602 async fn add_vertical(
1603 &mut self,
1604 sketch: ObjectId,
1605 vertical: Vertical,
1606 new_ast: &mut ast::Node<ast::Program>,
1607 ) -> api::Result<SourceRange> {
1608 let sketch_id = sketch;
1609
1610 let line_id = vertical.line;
1612 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1613 msg: format!("Line not found: {line_id:?}"),
1614 })?;
1615 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
1616 return Err(Error {
1617 msg: format!("Object is not a segment: {line_object:?}"),
1618 });
1619 };
1620 let Segment::Line(_) = line_segment else {
1621 return Err(Error {
1622 msg: format!("Only lines can be made vertical: {line_object:?}"),
1623 });
1624 };
1625 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
1626
1627 let vertical_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1629 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
1630 unlabeled: Some(line_ast),
1631 arguments: Default::default(),
1632 digest: None,
1633 non_code_meta: Default::default(),
1634 })));
1635
1636 let (sketch_block_range, _) = self.mutate_ast(
1638 new_ast,
1639 sketch_id,
1640 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
1641 )?;
1642 Ok(sketch_block_range)
1643 }
1644
1645 async fn execute_after_add_constraint(
1646 &mut self,
1647 ctx: &ExecutorContext,
1648 _sketch_id: ObjectId,
1649 sketch_block_range: SourceRange,
1650 new_ast: &mut ast::Node<ast::Program>,
1651 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1652 let new_source = source_from_ast(new_ast);
1654 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1656 if !errors.is_empty() {
1657 return Err(Error {
1658 msg: format!("Error parsing KCL source after adding constraint: {errors:?}"),
1659 });
1660 }
1661 let Some(new_program) = new_program else {
1662 return Err(Error {
1663 msg: "No AST produced after adding constraint".to_string(),
1664 });
1665 };
1666 let _constraint_source_range =
1667 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1668 msg: format!(
1669 "Source range of new constraint not found in sketch block: {sketch_block_range:?}; {err:?}"
1670 ),
1671 })?;
1672
1673 self.program = new_program.clone();
1675
1676 let mock_config = MockConfig {
1678 freedom_analysis: true,
1679 ..Default::default()
1680 };
1681 let outcome = ctx.run_mock(&new_program, &mock_config).await.map_err(|err| {
1682 Error {
1685 msg: err.error.message().to_owned(),
1686 }
1687 })?;
1688
1689 let src_delta = SourceDelta { text: new_source };
1690 let outcome = self.update_state_after_exec(outcome);
1691 let scene_graph_delta = SceneGraphDelta {
1692 new_graph: self.scene_graph.clone(),
1693 invalidates_ids: false,
1694 new_objects: Vec::new(),
1695 exec_outcome: outcome,
1696 };
1697 Ok((src_delta, scene_graph_delta))
1698 }
1699
1700 fn add_dependent_constraints_to_delete(
1703 &self,
1704 sketch_id: ObjectId,
1705 segment_ids_set: &AhashIndexSet<ObjectId>,
1706 constraint_ids_set: &mut AhashIndexSet<ObjectId>,
1707 ) -> api::Result<()> {
1708 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1710 msg: format!("Sketch not found: {sketch_id:?}"),
1711 })?;
1712 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1713 return Err(Error {
1714 msg: format!("Object is not a sketch: {sketch_object:?}"),
1715 });
1716 };
1717 for constraint_id in &sketch.constraints {
1718 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
1719 msg: format!("Constraint not found: {constraint_id:?}"),
1720 })?;
1721 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1722 return Err(Error {
1723 msg: format!("Object is not a constraint: {constraint_object:?}"),
1724 });
1725 };
1726 let depends_on_segment = match constraint {
1727 Constraint::Coincident(c) => c.segments.iter().any(|pt_id| {
1728 if segment_ids_set.contains(pt_id) {
1729 return true;
1730 }
1731 let pt_object = self.scene_graph.objects.get(pt_id.0);
1732 if let Some(obj) = pt_object
1733 && let ObjectKind::Segment { segment } = &obj.kind
1734 && let Segment::Point(pt) = segment
1735 && let Some(owner_line_id) = pt.owner
1736 {
1737 return segment_ids_set.contains(&owner_line_id);
1738 }
1739 false
1740 }),
1741 Constraint::Distance(d) => d.points.iter().any(|pt_id| {
1742 let pt_object = self.scene_graph.objects.get(pt_id.0);
1743 if let Some(obj) = pt_object
1744 && let ObjectKind::Segment { segment } = &obj.kind
1745 && let Segment::Point(pt) = segment
1746 && let Some(owner_line_id) = pt.owner
1747 {
1748 return segment_ids_set.contains(&owner_line_id);
1749 }
1750 false
1751 }),
1752 Constraint::Horizontal(h) => segment_ids_set.contains(&h.line),
1753 Constraint::Vertical(v) => segment_ids_set.contains(&v.line),
1754 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
1755 .lines
1756 .iter()
1757 .any(|line_id| segment_ids_set.contains(line_id)),
1758 Constraint::Parallel(parallel) => {
1759 parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
1760 }
1761 Constraint::Perpendicular(perpendicular) => perpendicular
1762 .lines
1763 .iter()
1764 .any(|line_id| segment_ids_set.contains(line_id)),
1765 };
1766 if depends_on_segment {
1767 constraint_ids_set.insert(*constraint_id);
1768 }
1769 }
1770 Ok(())
1771 }
1772
1773 fn expect_line(&self, object_id: ObjectId) -> api::Result<&Line> {
1774 let object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
1775 msg: format!("Object not found: {object_id:?}"),
1776 })?;
1777 let ObjectKind::Segment { segment } = &object.kind else {
1778 return Err(Error {
1779 msg: format!("Object is not a segment: {object:?}"),
1780 });
1781 };
1782 let Segment::Line(line) = segment else {
1783 return Err(Error {
1784 msg: format!("Segment is not a line: {segment:?}"),
1785 });
1786 };
1787 Ok(line)
1788 }
1789
1790 fn update_state_after_exec(&mut self, outcome: ExecOutcome) -> ExecOutcome {
1791 #[cfg(not(feature = "artifact-graph"))]
1792 return outcome;
1793 #[cfg(feature = "artifact-graph")]
1794 {
1795 let mut outcome = outcome;
1796 self.scene_graph.objects = std::mem::take(&mut outcome.scene_objects);
1797 outcome
1798 }
1799 }
1800
1801 fn mutate_ast(
1802 &mut self,
1803 ast: &mut ast::Node<ast::Program>,
1804 object_id: ObjectId,
1805 command: AstMutateCommand,
1806 ) -> api::Result<(SourceRange, AstMutateCommandReturn)> {
1807 let sketch_object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
1808 msg: format!("Object not found: {object_id:?}"),
1809 })?;
1810 match &sketch_object.source {
1811 SourceRef::Simple { range } => mutate_ast_node_by_source_range(ast, *range, command),
1812 SourceRef::BackTrace { .. } => Err(Error {
1813 msg: "BackTrace source refs not supported yet".to_owned(),
1814 }),
1815 }
1816 }
1817}
1818
1819fn expect_single_source_range(source_ref: &SourceRef) -> api::Result<SourceRange> {
1820 match source_ref {
1821 SourceRef::Simple { range } => Ok(*range),
1822 SourceRef::BackTrace { ranges } => {
1823 if ranges.len() != 1 {
1824 return Err(Error {
1825 msg: format!(
1826 "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
1827 ranges.len(),
1828 ),
1829 });
1830 }
1831 Ok(ranges[0])
1832 }
1833 }
1834}
1835
1836fn get_or_insert_ast_reference(
1843 ast: &mut ast::Node<ast::Program>,
1844 source_ref: &SourceRef,
1845 prefix: &str,
1846 property: Option<&str>,
1847) -> api::Result<ast::Expr> {
1848 let range = expect_single_source_range(source_ref)?;
1849 let command = AstMutateCommand::AddVariableDeclaration {
1850 prefix: prefix.to_owned(),
1851 };
1852 let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
1853 let AstMutateCommandReturn::Name(var_name) = ret else {
1854 return Err(Error {
1855 msg: "Expected variable name returned from AddVariableDeclaration".to_owned(),
1856 });
1857 };
1858 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
1859 let Some(property) = property else {
1860 return Ok(var_expr);
1862 };
1863
1864 Ok(ast::Expr::MemberExpression(Box::new(ast::Node::no_src(
1865 ast::MemberExpression {
1866 object: var_expr,
1867 property: ast::Expr::Name(Box::new(ast::Name::new(property))),
1868 computed: false,
1869 digest: None,
1870 },
1871 ))))
1872}
1873
1874fn mutate_ast_node_by_source_range(
1875 ast: &mut ast::Node<ast::Program>,
1876 source_range: SourceRange,
1877 command: AstMutateCommand,
1878) -> Result<(SourceRange, AstMutateCommandReturn), Error> {
1879 let mut context = AstMutateContext {
1880 source_range,
1881 command,
1882 defined_names_stack: Default::default(),
1883 };
1884 let control = dfs_mut(ast, &mut context);
1885 match control {
1886 ControlFlow::Continue(_) => Err(Error {
1887 msg: format!("Source range not found: {source_range:?}"),
1888 }),
1889 ControlFlow::Break(break_value) => break_value,
1890 }
1891}
1892
1893#[derive(Debug)]
1894struct AstMutateContext {
1895 source_range: SourceRange,
1896 command: AstMutateCommand,
1897 defined_names_stack: Vec<HashSet<String>>,
1898}
1899
1900#[derive(Debug)]
1901#[allow(clippy::large_enum_variant)]
1902enum AstMutateCommand {
1903 AddSketchBlockExprStmt {
1905 expr: ast::Expr,
1906 },
1907 AddVariableDeclaration {
1908 prefix: String,
1909 },
1910 EditPoint {
1911 at: ast::Expr,
1912 },
1913 EditLine {
1914 start: ast::Expr,
1915 end: ast::Expr,
1916 },
1917 EditArc {
1918 start: ast::Expr,
1919 end: ast::Expr,
1920 center: ast::Expr,
1921 },
1922 #[cfg(feature = "artifact-graph")]
1923 EditVarInitialValue {
1924 value: Number,
1925 },
1926 DeleteNode,
1927}
1928
1929#[derive(Debug)]
1930enum AstMutateCommandReturn {
1931 None,
1932 Name(String),
1933}
1934
1935impl Visitor for AstMutateContext {
1936 type Break = Result<(SourceRange, AstMutateCommandReturn), Error>;
1937 type Continue = ();
1938
1939 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
1940 filter_and_process(self, node)
1941 }
1942
1943 fn finish(&mut self, node: NodeMut<'_>) {
1944 match &node {
1945 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
1946 self.defined_names_stack.pop();
1947 }
1948 _ => {}
1949 }
1950 }
1951}
1952fn filter_and_process(
1953 ctx: &mut AstMutateContext,
1954 node: NodeMut,
1955) -> TraversalReturn<Result<(SourceRange, AstMutateCommandReturn), Error>> {
1956 let Ok(node_range) = SourceRange::try_from(&node) else {
1957 return TraversalReturn::new_continue(());
1959 };
1960 if let NodeMut::VariableDeclaration(var_decl) = &node {
1965 let expr_range = SourceRange::from(&var_decl.declaration.init);
1966 if expr_range == ctx.source_range {
1967 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
1968 return TraversalReturn::new_break(Ok((
1971 node_range,
1972 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
1973 )));
1974 }
1975 if let AstMutateCommand::DeleteNode = &ctx.command {
1976 return TraversalReturn {
1979 mutate_body_item: MutateBodyItem::Delete,
1980 control_flow: ControlFlow::Break(Ok((ctx.source_range, AstMutateCommandReturn::None))),
1981 };
1982 }
1983 }
1984 }
1985
1986 if let NodeMut::Program(program) = &node {
1987 ctx.defined_names_stack.push(find_defined_names(*program));
1988 } else if let NodeMut::SketchBlock(block) = &node {
1989 ctx.defined_names_stack.push(find_defined_names(&block.body));
1990 }
1991
1992 if node_range != ctx.source_range {
1994 return TraversalReturn::new_continue(());
1995 }
1996 process(ctx, node).map_break(|result| result.map(|cmd_return| (ctx.source_range, cmd_return)))
1997}
1998
1999fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, Error>> {
2000 match &ctx.command {
2001 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
2002 if let NodeMut::SketchBlock(sketch_block) = node {
2003 sketch_block
2004 .body
2005 .items
2006 .push(ast::BodyItem::ExpressionStatement(ast::Node {
2007 inner: ast::ExpressionStatement {
2008 expression: expr.clone(),
2009 digest: None,
2010 },
2011 start: Default::default(),
2012 end: Default::default(),
2013 module_id: Default::default(),
2014 outer_attrs: Default::default(),
2015 pre_comments: Default::default(),
2016 comment_start: Default::default(),
2017 }));
2018 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2019 }
2020 }
2021 AstMutateCommand::AddVariableDeclaration { prefix } => {
2022 if let NodeMut::VariableDeclaration(inner) = node {
2023 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
2024 }
2025 if let NodeMut::ExpressionStatement(expr_stmt) = node {
2026 let empty_defined_names = HashSet::new();
2027 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
2028 let Ok(name) = next_free_name(prefix, defined_names) else {
2029 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2031 };
2032 let mutate_node =
2033 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
2034 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
2035 ast::ItemVisibility::Default,
2036 ast::VariableKind::Const,
2037 ))));
2038 return TraversalReturn {
2039 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
2040 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
2041 };
2042 }
2043 }
2044 AstMutateCommand::EditPoint { at } => {
2045 if let NodeMut::CallExpressionKw(call) = node {
2046 if call.callee.name.name != POINT_FN {
2047 return TraversalReturn::new_continue(());
2048 }
2049 for labeled_arg in &mut call.arguments {
2051 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
2052 labeled_arg.arg = at.clone();
2053 }
2054 }
2055 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2056 }
2057 }
2058 AstMutateCommand::EditLine { start, end } => {
2059 if let NodeMut::CallExpressionKw(call) = node {
2060 if call.callee.name.name != LINE_FN {
2061 return TraversalReturn::new_continue(());
2062 }
2063 for labeled_arg in &mut call.arguments {
2065 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
2066 labeled_arg.arg = start.clone();
2067 }
2068 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
2069 labeled_arg.arg = end.clone();
2070 }
2071 }
2072 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2073 }
2074 }
2075 AstMutateCommand::EditArc { start, end, center } => {
2076 if let NodeMut::CallExpressionKw(call) = node {
2077 if call.callee.name.name != ARC_FN {
2078 return TraversalReturn::new_continue(());
2079 }
2080 for labeled_arg in &mut call.arguments {
2082 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
2083 labeled_arg.arg = start.clone();
2084 }
2085 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
2086 labeled_arg.arg = end.clone();
2087 }
2088 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
2089 labeled_arg.arg = center.clone();
2090 }
2091 }
2092 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2093 }
2094 }
2095 #[cfg(feature = "artifact-graph")]
2096 AstMutateCommand::EditVarInitialValue { value } => {
2097 if let NodeMut::NumericLiteral(numeric_literal) = node {
2098 let Ok(literal) = to_source_number(*value) else {
2100 return TraversalReturn::new_break(Err(Error {
2101 msg: format!("Could not convert number to AST literal: {:?}", *value),
2102 }));
2103 };
2104 *numeric_literal = ast::Node::no_src(literal);
2105 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2106 }
2107 }
2108 AstMutateCommand::DeleteNode => {
2109 return TraversalReturn {
2110 mutate_body_item: MutateBodyItem::Delete,
2111 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
2112 };
2113 }
2114 }
2115 TraversalReturn::new_continue(())
2116}
2117
2118struct FindSketchBlockSourceRange {
2119 target_before_mutation: SourceRange,
2121 found: Cell<Option<SourceRange>>,
2125}
2126
2127impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
2128 type Error = crate::front::Error;
2129
2130 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
2131 let Ok(node_range) = SourceRange::try_from(&node) else {
2132 return Ok(true);
2133 };
2134
2135 if let crate::walk::Node::SketchBlock(sketch_block) = node {
2136 if node_range.module_id() == self.target_before_mutation.module_id()
2137 && node_range.start() == self.target_before_mutation.start()
2138 && node_range.end() >= self.target_before_mutation.end()
2140 {
2141 self.found.set(sketch_block.body.items.last().map(SourceRange::from));
2142 return Ok(false);
2143 } else {
2144 return Ok(true);
2147 }
2148 }
2149
2150 for child in node.children().iter() {
2151 if !child.visit(*self)? {
2152 return Ok(false);
2153 }
2154 }
2155
2156 Ok(true)
2157 }
2158}
2159
2160fn find_sketch_block_added_item(
2168 ast: &ast::Node<ast::Program>,
2169 range_before_mutation: SourceRange,
2170) -> api::Result<SourceRange> {
2171 let find = FindSketchBlockSourceRange {
2172 target_before_mutation: range_before_mutation,
2173 found: Cell::new(None),
2174 };
2175 let node = crate::walk::Node::from(ast);
2176 node.visit(&find)?;
2177 find.found.into_inner().ok_or_else(|| api::Error {
2178 msg: format!("Source range after mutation not found for range before mutation: {range_before_mutation:?}; Did you try formatting (i.e. call recast) before calling this?"),
2179 })
2180}
2181
2182fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
2183 ast.recast_top(&Default::default(), 0)
2185}
2186
2187fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
2188 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
2189 inner: ast::ArrayExpression {
2190 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
2191 non_code_meta: Default::default(),
2192 digest: None,
2193 },
2194 start: Default::default(),
2195 end: Default::default(),
2196 module_id: Default::default(),
2197 outer_attrs: Default::default(),
2198 pre_comments: Default::default(),
2199 comment_start: Default::default(),
2200 })))
2201}
2202
2203fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
2204 match expr {
2205 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
2206 inner: ast::Literal::from(to_source_number(*number)?),
2207 start: Default::default(),
2208 end: Default::default(),
2209 module_id: Default::default(),
2210 outer_attrs: Default::default(),
2211 pre_comments: Default::default(),
2212 comment_start: Default::default(),
2213 }))),
2214 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
2215 inner: ast::SketchVar {
2216 initial: Some(Box::new(ast::Node {
2217 inner: to_source_number(*number)?,
2218 start: Default::default(),
2219 end: Default::default(),
2220 module_id: Default::default(),
2221 outer_attrs: Default::default(),
2222 pre_comments: Default::default(),
2223 comment_start: Default::default(),
2224 })),
2225 digest: None,
2226 },
2227 start: Default::default(),
2228 end: Default::default(),
2229 module_id: Default::default(),
2230 outer_attrs: Default::default(),
2231 pre_comments: Default::default(),
2232 comment_start: Default::default(),
2233 }))),
2234 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
2235 }
2236}
2237
2238fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
2239 Ok(ast::NumericLiteral {
2240 value: number.value,
2241 suffix: number.units,
2242 raw: format_number_literal(number.value, number.units)?,
2243 digest: None,
2244 })
2245}
2246
2247fn ast_name_expr(name: String) -> ast::Expr {
2248 ast::Expr::Name(Box::new(ast_name(name)))
2249}
2250
2251fn ast_name(name: String) -> ast::Node<ast::Name> {
2252 ast::Node {
2253 inner: ast::Name {
2254 name: ast::Node {
2255 inner: ast::Identifier { name, digest: None },
2256 start: Default::default(),
2257 end: Default::default(),
2258 module_id: Default::default(),
2259 outer_attrs: Default::default(),
2260 pre_comments: Default::default(),
2261 comment_start: Default::default(),
2262 },
2263 path: Vec::new(),
2264 abs_path: false,
2265 digest: None,
2266 },
2267 start: Default::default(),
2268 end: Default::default(),
2269 module_id: Default::default(),
2270 outer_attrs: Default::default(),
2271 pre_comments: Default::default(),
2272 comment_start: Default::default(),
2273 }
2274}
2275
2276fn ast_sketch2_name(name: &str) -> ast::Name {
2277 ast::Name {
2278 name: ast::Node {
2279 inner: ast::Identifier {
2280 name: name.to_owned(),
2281 digest: None,
2282 },
2283 start: Default::default(),
2284 end: Default::default(),
2285 module_id: Default::default(),
2286 outer_attrs: Default::default(),
2287 pre_comments: Default::default(),
2288 comment_start: Default::default(),
2289 },
2290 path: vec![ast::Node::no_src(ast::Identifier {
2291 name: "sketch2".to_owned(),
2292 digest: None,
2293 })],
2294 abs_path: false,
2295 digest: None,
2296 }
2297}
2298
2299#[cfg(test)]
2300mod tests {
2301 use super::*;
2302 use crate::{
2303 engine::PlaneName,
2304 front::{Distance, Plane, Sketch},
2305 frontend::sketch::Vertical,
2306 pretty::NumericSuffix,
2307 };
2308
2309 #[tokio::test(flavor = "multi_thread")]
2310 async fn test_new_sketch_add_point_edit_point() {
2311 let program = Program::empty();
2312
2313 let mut frontend = FrontendState::new();
2314 frontend.program = program;
2315
2316 let mock_ctx = ExecutorContext::new_mock(None).await;
2317 let version = Version(0);
2318
2319 let sketch_args = SketchArgs {
2320 on: api::Plane::Default(PlaneName::Xy),
2321 };
2322 let (_src_delta, scene_delta, sketch_id) = frontend
2323 .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2324 .await
2325 .unwrap();
2326 assert_eq!(sketch_id, ObjectId(0));
2327 assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2328 let sketch_object = &scene_delta.new_graph.objects[0];
2329 assert_eq!(sketch_object.id, ObjectId(0));
2330 assert_eq!(
2331 sketch_object.kind,
2332 ObjectKind::Sketch(Sketch {
2333 args: SketchArgs {
2334 on: Plane::Default(PlaneName::Xy)
2335 },
2336 segments: vec![],
2337 constraints: vec![],
2338 })
2339 );
2340 assert_eq!(scene_delta.new_graph.objects.len(), 1);
2341
2342 let point_ctor = PointCtor {
2343 position: Point2d {
2344 x: Expr::Number(Number {
2345 value: 1.0,
2346 units: NumericSuffix::Inch,
2347 }),
2348 y: Expr::Number(Number {
2349 value: 2.0,
2350 units: NumericSuffix::Inch,
2351 }),
2352 },
2353 };
2354 let segment = SegmentCtor::Point(point_ctor);
2355 let (src_delta, scene_delta) = frontend
2356 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2357 .await
2358 .unwrap();
2359 assert_eq!(
2360 src_delta.text.as_str(),
2361 "@settings(experimentalFeatures = allow)
2362
2363sketch(on = XY) {
2364 sketch2::point(at = [1in, 2in])
2365}
2366"
2367 );
2368 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
2369 assert_eq!(scene_delta.new_graph.objects.len(), 2);
2370 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
2371 assert_eq!(scene_object.id.0, i);
2372 }
2373 assert_eq!(scene_delta.new_graph.objects.len(), 2);
2374
2375 let point_id = *scene_delta.new_objects.last().unwrap();
2376
2377 let point_ctor = PointCtor {
2378 position: Point2d {
2379 x: Expr::Number(Number {
2380 value: 3.0,
2381 units: NumericSuffix::Inch,
2382 }),
2383 y: Expr::Number(Number {
2384 value: 4.0,
2385 units: NumericSuffix::Inch,
2386 }),
2387 },
2388 };
2389 let segments = vec![ExistingSegmentCtor {
2390 id: point_id,
2391 ctor: SegmentCtor::Point(point_ctor),
2392 }];
2393 let (src_delta, scene_delta) = frontend
2394 .edit_segments(&mock_ctx, version, sketch_id, segments)
2395 .await
2396 .unwrap();
2397 assert_eq!(
2398 src_delta.text.as_str(),
2399 "@settings(experimentalFeatures = allow)
2400
2401sketch(on = XY) {
2402 sketch2::point(at = [3in, 4in])
2403}
2404"
2405 );
2406 assert_eq!(scene_delta.new_objects, vec![]);
2407 assert_eq!(scene_delta.new_graph.objects.len(), 2);
2408
2409 mock_ctx.close().await;
2410 }
2411
2412 #[tokio::test(flavor = "multi_thread")]
2413 async fn test_new_sketch_add_line_edit_line() {
2414 let program = Program::empty();
2415
2416 let mut frontend = FrontendState::new();
2417 frontend.program = program;
2418
2419 let mock_ctx = ExecutorContext::new_mock(None).await;
2420 let version = Version(0);
2421
2422 let sketch_args = SketchArgs {
2423 on: api::Plane::Default(PlaneName::Xy),
2424 };
2425 let (_src_delta, scene_delta, sketch_id) = frontend
2426 .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2427 .await
2428 .unwrap();
2429 assert_eq!(sketch_id, ObjectId(0));
2430 assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2431 let sketch_object = &scene_delta.new_graph.objects[0];
2432 assert_eq!(sketch_object.id, ObjectId(0));
2433 assert_eq!(
2434 sketch_object.kind,
2435 ObjectKind::Sketch(Sketch {
2436 args: SketchArgs {
2437 on: Plane::Default(PlaneName::Xy)
2438 },
2439 segments: vec![],
2440 constraints: vec![],
2441 })
2442 );
2443 assert_eq!(scene_delta.new_graph.objects.len(), 1);
2444
2445 let line_ctor = LineCtor {
2446 start: Point2d {
2447 x: Expr::Number(Number {
2448 value: 0.0,
2449 units: NumericSuffix::Mm,
2450 }),
2451 y: Expr::Number(Number {
2452 value: 0.0,
2453 units: NumericSuffix::Mm,
2454 }),
2455 },
2456 end: Point2d {
2457 x: Expr::Number(Number {
2458 value: 10.0,
2459 units: NumericSuffix::Mm,
2460 }),
2461 y: Expr::Number(Number {
2462 value: 10.0,
2463 units: NumericSuffix::Mm,
2464 }),
2465 },
2466 };
2467 let segment = SegmentCtor::Line(line_ctor);
2468 let (src_delta, scene_delta) = frontend
2469 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2470 .await
2471 .unwrap();
2472 assert_eq!(
2473 src_delta.text.as_str(),
2474 "@settings(experimentalFeatures = allow)
2475
2476sketch(on = XY) {
2477 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
2478}
2479"
2480 );
2481 assert_eq!(scene_delta.new_objects, vec![ObjectId(1), ObjectId(2), ObjectId(3)]);
2482 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
2483 assert_eq!(scene_object.id.0, i);
2484 }
2485 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2486
2487 let line = *scene_delta.new_objects.last().unwrap();
2489
2490 let line_ctor = LineCtor {
2491 start: Point2d {
2492 x: Expr::Number(Number {
2493 value: 1.0,
2494 units: NumericSuffix::Mm,
2495 }),
2496 y: Expr::Number(Number {
2497 value: 2.0,
2498 units: NumericSuffix::Mm,
2499 }),
2500 },
2501 end: Point2d {
2502 x: Expr::Number(Number {
2503 value: 13.0,
2504 units: NumericSuffix::Mm,
2505 }),
2506 y: Expr::Number(Number {
2507 value: 14.0,
2508 units: NumericSuffix::Mm,
2509 }),
2510 },
2511 };
2512 let segments = vec![ExistingSegmentCtor {
2513 id: line,
2514 ctor: SegmentCtor::Line(line_ctor),
2515 }];
2516 let (src_delta, scene_delta) = frontend
2517 .edit_segments(&mock_ctx, version, sketch_id, segments)
2518 .await
2519 .unwrap();
2520 assert_eq!(
2521 src_delta.text.as_str(),
2522 "@settings(experimentalFeatures = allow)
2523
2524sketch(on = XY) {
2525 sketch2::line(start = [1mm, 2mm], end = [13mm, 14mm])
2526}
2527"
2528 );
2529 assert_eq!(scene_delta.new_objects, vec![]);
2530 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2531
2532 mock_ctx.close().await;
2533 }
2534
2535 #[tokio::test(flavor = "multi_thread")]
2536 async fn test_new_sketch_add_arc_edit_arc() {
2537 let program = Program::empty();
2538
2539 let mut frontend = FrontendState::new();
2540 frontend.program = program;
2541
2542 let mock_ctx = ExecutorContext::new_mock(None).await;
2543 let version = Version(0);
2544
2545 let sketch_args = SketchArgs {
2546 on: api::Plane::Default(PlaneName::Xy),
2547 };
2548 let (_src_delta, scene_delta, sketch_id) = frontend
2549 .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2550 .await
2551 .unwrap();
2552 assert_eq!(sketch_id, ObjectId(0));
2553 assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2554 let sketch_object = &scene_delta.new_graph.objects[0];
2555 assert_eq!(sketch_object.id, ObjectId(0));
2556 assert_eq!(
2557 sketch_object.kind,
2558 ObjectKind::Sketch(Sketch {
2559 args: SketchArgs {
2560 on: Plane::Default(PlaneName::Xy)
2561 },
2562 segments: vec![],
2563 constraints: vec![],
2564 })
2565 );
2566 assert_eq!(scene_delta.new_graph.objects.len(), 1);
2567
2568 let arc_ctor = ArcCtor {
2569 start: Point2d {
2570 x: Expr::Var(Number {
2571 value: 0.0,
2572 units: NumericSuffix::Mm,
2573 }),
2574 y: Expr::Var(Number {
2575 value: 0.0,
2576 units: NumericSuffix::Mm,
2577 }),
2578 },
2579 end: Point2d {
2580 x: Expr::Var(Number {
2581 value: 10.0,
2582 units: NumericSuffix::Mm,
2583 }),
2584 y: Expr::Var(Number {
2585 value: 10.0,
2586 units: NumericSuffix::Mm,
2587 }),
2588 },
2589 center: Point2d {
2590 x: Expr::Var(Number {
2591 value: 10.0,
2592 units: NumericSuffix::Mm,
2593 }),
2594 y: Expr::Var(Number {
2595 value: 0.0,
2596 units: NumericSuffix::Mm,
2597 }),
2598 },
2599 };
2600 let segment = SegmentCtor::Arc(arc_ctor);
2601 let (src_delta, scene_delta) = frontend
2602 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2603 .await
2604 .unwrap();
2605 assert_eq!(
2606 src_delta.text.as_str(),
2607 "@settings(experimentalFeatures = allow)
2608
2609sketch(on = XY) {
2610 sketch2::arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
2611}
2612"
2613 );
2614 assert_eq!(
2615 scene_delta.new_objects,
2616 vec![ObjectId(1), ObjectId(2), ObjectId(3), ObjectId(4)]
2617 );
2618 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
2619 assert_eq!(scene_object.id.0, i);
2620 }
2621 assert_eq!(scene_delta.new_graph.objects.len(), 5);
2622
2623 let arc = *scene_delta.new_objects.last().unwrap();
2625
2626 let arc_ctor = ArcCtor {
2627 start: Point2d {
2628 x: Expr::Var(Number {
2629 value: 1.0,
2630 units: NumericSuffix::Mm,
2631 }),
2632 y: Expr::Var(Number {
2633 value: 2.0,
2634 units: NumericSuffix::Mm,
2635 }),
2636 },
2637 end: Point2d {
2638 x: Expr::Var(Number {
2639 value: 13.0,
2640 units: NumericSuffix::Mm,
2641 }),
2642 y: Expr::Var(Number {
2643 value: 14.0,
2644 units: NumericSuffix::Mm,
2645 }),
2646 },
2647 center: Point2d {
2648 x: Expr::Var(Number {
2649 value: 13.0,
2650 units: NumericSuffix::Mm,
2651 }),
2652 y: Expr::Var(Number {
2653 value: 2.0,
2654 units: NumericSuffix::Mm,
2655 }),
2656 },
2657 };
2658 let segments = vec![ExistingSegmentCtor {
2659 id: arc,
2660 ctor: SegmentCtor::Arc(arc_ctor),
2661 }];
2662 let (src_delta, scene_delta) = frontend
2663 .edit_segments(&mock_ctx, version, sketch_id, segments)
2664 .await
2665 .unwrap();
2666 assert_eq!(
2667 src_delta.text.as_str(),
2668 "@settings(experimentalFeatures = allow)
2669
2670sketch(on = XY) {
2671 sketch2::arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
2672}
2673"
2674 );
2675 assert_eq!(scene_delta.new_objects, vec![]);
2676 assert_eq!(scene_delta.new_graph.objects.len(), 5);
2677
2678 mock_ctx.close().await;
2679 }
2680
2681 #[tokio::test(flavor = "multi_thread")]
2682 async fn test_add_line_when_sketch_block_uses_variable() {
2683 let initial_source = "@settings(experimentalFeatures = allow)
2684
2685s = sketch(on = XY) {}
2686";
2687
2688 let program = Program::parse(initial_source).unwrap().0.unwrap();
2689
2690 let mut frontend = FrontendState::new();
2691
2692 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2693 let mock_ctx = ExecutorContext::new_mock(None).await;
2694 let version = Version(0);
2695
2696 frontend.hack_set_program(&ctx, program).await.unwrap();
2697 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2698
2699 let line_ctor = LineCtor {
2700 start: Point2d {
2701 x: Expr::Number(Number {
2702 value: 0.0,
2703 units: NumericSuffix::Mm,
2704 }),
2705 y: Expr::Number(Number {
2706 value: 0.0,
2707 units: NumericSuffix::Mm,
2708 }),
2709 },
2710 end: Point2d {
2711 x: Expr::Number(Number {
2712 value: 10.0,
2713 units: NumericSuffix::Mm,
2714 }),
2715 y: Expr::Number(Number {
2716 value: 10.0,
2717 units: NumericSuffix::Mm,
2718 }),
2719 },
2720 };
2721 let segment = SegmentCtor::Line(line_ctor);
2722 let (src_delta, scene_delta) = frontend
2723 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2724 .await
2725 .unwrap();
2726 assert_eq!(
2727 src_delta.text.as_str(),
2728 "@settings(experimentalFeatures = allow)
2729
2730s = sketch(on = XY) {
2731 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
2732}
2733"
2734 );
2735 assert_eq!(scene_delta.new_objects, vec![ObjectId(1), ObjectId(2), ObjectId(3)]);
2736 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2737
2738 ctx.close().await;
2739 mock_ctx.close().await;
2740 }
2741
2742 #[tokio::test(flavor = "multi_thread")]
2743 async fn test_new_sketch_add_line_delete_sketch() {
2744 let program = Program::empty();
2745
2746 let mut frontend = FrontendState::new();
2747 frontend.program = program;
2748
2749 let mock_ctx = ExecutorContext::new_mock(None).await;
2750 let version = Version(0);
2751
2752 let sketch_args = SketchArgs {
2753 on: api::Plane::Default(PlaneName::Xy),
2754 };
2755 let (_src_delta, scene_delta, sketch_id) = frontend
2756 .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2757 .await
2758 .unwrap();
2759 assert_eq!(sketch_id, ObjectId(0));
2760 assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2761 let sketch_object = &scene_delta.new_graph.objects[0];
2762 assert_eq!(sketch_object.id, ObjectId(0));
2763 assert_eq!(
2764 sketch_object.kind,
2765 ObjectKind::Sketch(Sketch {
2766 args: SketchArgs {
2767 on: Plane::Default(PlaneName::Xy)
2768 },
2769 segments: vec![],
2770 constraints: vec![],
2771 })
2772 );
2773 assert_eq!(scene_delta.new_graph.objects.len(), 1);
2774
2775 let line_ctor = LineCtor {
2776 start: Point2d {
2777 x: Expr::Number(Number {
2778 value: 0.0,
2779 units: NumericSuffix::Mm,
2780 }),
2781 y: Expr::Number(Number {
2782 value: 0.0,
2783 units: NumericSuffix::Mm,
2784 }),
2785 },
2786 end: Point2d {
2787 x: Expr::Number(Number {
2788 value: 10.0,
2789 units: NumericSuffix::Mm,
2790 }),
2791 y: Expr::Number(Number {
2792 value: 10.0,
2793 units: NumericSuffix::Mm,
2794 }),
2795 },
2796 };
2797 let segment = SegmentCtor::Line(line_ctor);
2798 let (src_delta, scene_delta) = frontend
2799 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2800 .await
2801 .unwrap();
2802 assert_eq!(
2803 src_delta.text.as_str(),
2804 "@settings(experimentalFeatures = allow)
2805
2806sketch(on = XY) {
2807 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
2808}
2809"
2810 );
2811 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2812
2813 let (src_delta, scene_delta) = frontend.delete_sketch(&mock_ctx, version, sketch_id).await.unwrap();
2814 assert_eq!(
2815 src_delta.text.as_str(),
2816 "@settings(experimentalFeatures = allow)
2817"
2818 );
2819 assert_eq!(scene_delta.new_graph.objects.len(), 0);
2820
2821 mock_ctx.close().await;
2822 }
2823
2824 #[tokio::test(flavor = "multi_thread")]
2825 async fn test_delete_sketch_when_sketch_block_uses_variable() {
2826 let initial_source = "@settings(experimentalFeatures = allow)
2827
2828s = sketch(on = XY) {}
2829";
2830
2831 let program = Program::parse(initial_source).unwrap().0.unwrap();
2832
2833 let mut frontend = FrontendState::new();
2834
2835 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2836 let mock_ctx = ExecutorContext::new_mock(None).await;
2837 let version = Version(0);
2838
2839 frontend.hack_set_program(&ctx, program).await.unwrap();
2840 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2841
2842 let (src_delta, scene_delta) = frontend.delete_sketch(&mock_ctx, version, sketch_id).await.unwrap();
2843 assert_eq!(
2844 src_delta.text.as_str(),
2845 "@settings(experimentalFeatures = allow)
2846"
2847 );
2848 assert_eq!(scene_delta.new_graph.objects.len(), 0);
2849
2850 ctx.close().await;
2851 mock_ctx.close().await;
2852 }
2853
2854 #[tokio::test(flavor = "multi_thread")]
2855 async fn test_edit_line_when_editing_its_start_point() {
2856 let initial_source = "\
2857@settings(experimentalFeatures = allow)
2858
2859sketch(on = XY) {
2860 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
2861}
2862";
2863
2864 let program = Program::parse(initial_source).unwrap().0.unwrap();
2865
2866 let mut frontend = FrontendState::new();
2867
2868 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2869 let mock_ctx = ExecutorContext::new_mock(None).await;
2870 let version = Version(0);
2871
2872 frontend.hack_set_program(&ctx, program).await.unwrap();
2873 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2874
2875 let point_id = frontend.scene_graph.objects.get(1).unwrap().id;
2876
2877 let point_ctor = PointCtor {
2878 position: Point2d {
2879 x: Expr::Var(Number {
2880 value: 5.0,
2881 units: NumericSuffix::Inch,
2882 }),
2883 y: Expr::Var(Number {
2884 value: 6.0,
2885 units: NumericSuffix::Inch,
2886 }),
2887 },
2888 };
2889 let segments = vec![ExistingSegmentCtor {
2890 id: point_id,
2891 ctor: SegmentCtor::Point(point_ctor),
2892 }];
2893 let (src_delta, scene_delta) = frontend
2894 .edit_segments(&mock_ctx, version, sketch_id, segments)
2895 .await
2896 .unwrap();
2897 assert_eq!(
2898 src_delta.text.as_str(),
2899 "\
2900@settings(experimentalFeatures = allow)
2901
2902sketch(on = XY) {
2903 sketch2::line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
2904}
2905"
2906 );
2907 assert_eq!(scene_delta.new_objects, vec![]);
2908 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2909
2910 ctx.close().await;
2911 mock_ctx.close().await;
2912 }
2913
2914 #[tokio::test(flavor = "multi_thread")]
2915 async fn test_edit_line_when_editing_its_end_point() {
2916 let initial_source = "\
2917@settings(experimentalFeatures = allow)
2918
2919sketch(on = XY) {
2920 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
2921}
2922";
2923
2924 let program = Program::parse(initial_source).unwrap().0.unwrap();
2925
2926 let mut frontend = FrontendState::new();
2927
2928 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2929 let mock_ctx = ExecutorContext::new_mock(None).await;
2930 let version = Version(0);
2931
2932 frontend.hack_set_program(&ctx, program).await.unwrap();
2933 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2934
2935 let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
2936
2937 let point_ctor = PointCtor {
2938 position: Point2d {
2939 x: Expr::Var(Number {
2940 value: 5.0,
2941 units: NumericSuffix::Inch,
2942 }),
2943 y: Expr::Var(Number {
2944 value: 6.0,
2945 units: NumericSuffix::Inch,
2946 }),
2947 },
2948 };
2949 let segments = vec![ExistingSegmentCtor {
2950 id: point_id,
2951 ctor: SegmentCtor::Point(point_ctor),
2952 }];
2953 let (src_delta, scene_delta) = frontend
2954 .edit_segments(&mock_ctx, version, sketch_id, segments)
2955 .await
2956 .unwrap();
2957 assert_eq!(
2958 src_delta.text.as_str(),
2959 "\
2960@settings(experimentalFeatures = allow)
2961
2962sketch(on = XY) {
2963 sketch2::line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
2964}
2965"
2966 );
2967 assert_eq!(scene_delta.new_objects, vec![]);
2968 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2969
2970 ctx.close().await;
2971 mock_ctx.close().await;
2972 }
2973
2974 #[tokio::test(flavor = "multi_thread")]
2975 async fn test_edit_line_with_coincident_feedback() {
2976 let initial_source = "\
2977@settings(experimentalFeatures = allow)
2978
2979sketch(on = XY) {
2980 line1 = sketch2::line(start = [var 1, var 2], end = [var 1, var 2])
2981 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
2982 line1.start.at[0] == 0
2983 line1.start.at[1] == 0
2984 sketch2::coincident([line1.end, line2.start])
2985 sketch2::equalLength([line1, line2])
2986}
2987";
2988
2989 let program = Program::parse(initial_source).unwrap().0.unwrap();
2990
2991 let mut frontend = FrontendState::new();
2992
2993 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2994 let mock_ctx = ExecutorContext::new_mock(None).await;
2995 let version = Version(0);
2996
2997 frontend.hack_set_program(&ctx, program).await.unwrap();
2998 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2999 let line2_end_id = frontend.scene_graph.objects.get(5).unwrap().id;
3000
3001 let segments = vec![ExistingSegmentCtor {
3002 id: line2_end_id,
3003 ctor: SegmentCtor::Point(PointCtor {
3004 position: Point2d {
3005 x: Expr::Var(Number {
3006 value: 9.0,
3007 units: NumericSuffix::None,
3008 }),
3009 y: Expr::Var(Number {
3010 value: 10.0,
3011 units: NumericSuffix::None,
3012 }),
3013 },
3014 }),
3015 }];
3016 let (src_delta, scene_delta) = frontend
3017 .edit_segments(&mock_ctx, version, sketch_id, segments)
3018 .await
3019 .unwrap();
3020 assert_eq!(
3021 src_delta.text.as_str(),
3022 "\
3023@settings(experimentalFeatures = allow)
3024
3025sketch(on = XY) {
3026 line1 = sketch2::line(start = [var -0mm, var -0mm], end = [var 4.145mm, var 5.32mm])
3027 line2 = sketch2::line(start = [var 4.145mm, var 5.32mm], end = [var 9mm, var 10mm])
3028line1.start.at[0] == 0
3029line1.start.at[1] == 0
3030 sketch2::coincident([line1.end, line2.start])
3031 sketch2::equalLength([line1, line2])
3032}
3033"
3034 );
3035 assert_eq!(
3036 scene_delta.new_graph.objects.len(),
3037 9,
3038 "{:#?}",
3039 scene_delta.new_graph.objects
3040 );
3041
3042 ctx.close().await;
3043 mock_ctx.close().await;
3044 }
3045
3046 #[tokio::test(flavor = "multi_thread")]
3047 async fn test_delete_point_without_var() {
3048 let initial_source = "\
3049@settings(experimentalFeatures = allow)
3050
3051sketch(on = XY) {
3052 sketch2::point(at = [var 1, var 2])
3053 sketch2::point(at = [var 3, var 4])
3054 sketch2::point(at = [var 5, var 6])
3055}
3056";
3057
3058 let program = Program::parse(initial_source).unwrap().0.unwrap();
3059
3060 let mut frontend = FrontendState::new();
3061
3062 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3063 let mock_ctx = ExecutorContext::new_mock(None).await;
3064 let version = Version(0);
3065
3066 frontend.hack_set_program(&ctx, program).await.unwrap();
3067 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3068
3069 let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
3070
3071 let (src_delta, scene_delta) = frontend
3072 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
3073 .await
3074 .unwrap();
3075 assert_eq!(
3076 src_delta.text.as_str(),
3077 "\
3078@settings(experimentalFeatures = allow)
3079
3080sketch(on = XY) {
3081 sketch2::point(at = [var 1mm, var 2mm])
3082 sketch2::point(at = [var 5mm, var 6mm])
3083}
3084"
3085 );
3086 assert_eq!(scene_delta.new_objects, vec![]);
3087 assert_eq!(scene_delta.new_graph.objects.len(), 3);
3088
3089 ctx.close().await;
3090 mock_ctx.close().await;
3091 }
3092
3093 #[tokio::test(flavor = "multi_thread")]
3094 async fn test_delete_point_with_var() {
3095 let initial_source = "\
3096@settings(experimentalFeatures = allow)
3097
3098sketch(on = XY) {
3099 sketch2::point(at = [var 1, var 2])
3100 point1 = sketch2::point(at = [var 3, var 4])
3101 sketch2::point(at = [var 5, var 6])
3102}
3103";
3104
3105 let program = Program::parse(initial_source).unwrap().0.unwrap();
3106
3107 let mut frontend = FrontendState::new();
3108
3109 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3110 let mock_ctx = ExecutorContext::new_mock(None).await;
3111 let version = Version(0);
3112
3113 frontend.hack_set_program(&ctx, program).await.unwrap();
3114 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3115
3116 let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
3117
3118 let (src_delta, scene_delta) = frontend
3119 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
3120 .await
3121 .unwrap();
3122 assert_eq!(
3123 src_delta.text.as_str(),
3124 "\
3125@settings(experimentalFeatures = allow)
3126
3127sketch(on = XY) {
3128 sketch2::point(at = [var 1mm, var 2mm])
3129 sketch2::point(at = [var 5mm, var 6mm])
3130}
3131"
3132 );
3133 assert_eq!(scene_delta.new_objects, vec![]);
3134 assert_eq!(scene_delta.new_graph.objects.len(), 3);
3135
3136 ctx.close().await;
3137 mock_ctx.close().await;
3138 }
3139
3140 #[tokio::test(flavor = "multi_thread")]
3141 async fn test_delete_multiple_points() {
3142 let initial_source = "\
3143@settings(experimentalFeatures = allow)
3144
3145sketch(on = XY) {
3146 sketch2::point(at = [var 1, var 2])
3147 point1 = sketch2::point(at = [var 3, var 4])
3148 sketch2::point(at = [var 5, var 6])
3149}
3150";
3151
3152 let program = Program::parse(initial_source).unwrap().0.unwrap();
3153
3154 let mut frontend = FrontendState::new();
3155
3156 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3157 let mock_ctx = ExecutorContext::new_mock(None).await;
3158 let version = Version(0);
3159
3160 frontend.hack_set_program(&ctx, program).await.unwrap();
3161 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3162
3163 let point1_id = frontend.scene_graph.objects.get(1).unwrap().id;
3164 let point2_id = frontend.scene_graph.objects.get(2).unwrap().id;
3165
3166 let (src_delta, scene_delta) = frontend
3167 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
3168 .await
3169 .unwrap();
3170 assert_eq!(
3171 src_delta.text.as_str(),
3172 "\
3173@settings(experimentalFeatures = allow)
3174
3175sketch(on = XY) {
3176 sketch2::point(at = [var 5mm, var 6mm])
3177}
3178"
3179 );
3180 assert_eq!(scene_delta.new_objects, vec![]);
3181 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3182
3183 ctx.close().await;
3184 mock_ctx.close().await;
3185 }
3186
3187 #[tokio::test(flavor = "multi_thread")]
3188 async fn test_delete_coincident_constraint() {
3189 let initial_source = "\
3190@settings(experimentalFeatures = allow)
3191
3192sketch(on = XY) {
3193 point1 = sketch2::point(at = [var 1, var 2])
3194 point2 = sketch2::point(at = [var 3, var 4])
3195 sketch2::coincident([point1, point2])
3196 sketch2::point(at = [var 5, var 6])
3197}
3198";
3199
3200 let program = Program::parse(initial_source).unwrap().0.unwrap();
3201
3202 let mut frontend = FrontendState::new();
3203
3204 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3205 let mock_ctx = ExecutorContext::new_mock(None).await;
3206 let version = Version(0);
3207
3208 frontend.hack_set_program(&ctx, program).await.unwrap();
3209 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3210
3211 let coincident_id = frontend.scene_graph.objects.get(3).unwrap().id;
3212
3213 let (src_delta, scene_delta) = frontend
3214 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
3215 .await
3216 .unwrap();
3217 assert_eq!(
3218 src_delta.text.as_str(),
3219 "\
3220@settings(experimentalFeatures = allow)
3221
3222sketch(on = XY) {
3223 point1 = sketch2::point(at = [var 1mm, var 2mm])
3224 point2 = sketch2::point(at = [var 3mm, var 4mm])
3225 sketch2::point(at = [var 5mm, var 6mm])
3226}
3227"
3228 );
3229 assert_eq!(scene_delta.new_objects, vec![]);
3230 assert_eq!(scene_delta.new_graph.objects.len(), 4);
3231
3232 ctx.close().await;
3233 mock_ctx.close().await;
3234 }
3235
3236 #[tokio::test(flavor = "multi_thread")]
3237 async fn test_delete_line_cascades_to_coincident_constraint() {
3238 let initial_source = "\
3239@settings(experimentalFeatures = allow)
3240
3241sketch(on = XY) {
3242 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3243 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3244 sketch2::coincident([line1.end, line2.start])
3245}
3246";
3247
3248 let program = Program::parse(initial_source).unwrap().0.unwrap();
3249
3250 let mut frontend = FrontendState::new();
3251
3252 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3253 let mock_ctx = ExecutorContext::new_mock(None).await;
3254 let version = Version(0);
3255
3256 frontend.hack_set_program(&ctx, program).await.unwrap();
3257 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3258 let line_id = frontend.scene_graph.objects.get(6).unwrap().id;
3259
3260 let (src_delta, scene_delta) = frontend
3261 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
3262 .await
3263 .unwrap();
3264 assert_eq!(
3265 src_delta.text.as_str(),
3266 "\
3267@settings(experimentalFeatures = allow)
3268
3269sketch(on = XY) {
3270 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3271}
3272"
3273 );
3274 assert_eq!(
3275 scene_delta.new_graph.objects.len(),
3276 4,
3277 "{:#?}",
3278 scene_delta.new_graph.objects
3279 );
3280
3281 ctx.close().await;
3282 mock_ctx.close().await;
3283 }
3284
3285 #[tokio::test(flavor = "multi_thread")]
3286 async fn test_delete_line_cascades_to_distance_constraint() {
3287 let initial_source = "\
3288@settings(experimentalFeatures = allow)
3289
3290sketch(on = XY) {
3291 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3292 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3293 sketch2::distance([line1.end, line2.start]) == 10mm
3294}
3295";
3296
3297 let program = Program::parse(initial_source).unwrap().0.unwrap();
3298
3299 let mut frontend = FrontendState::new();
3300
3301 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3302 let mock_ctx = ExecutorContext::new_mock(None).await;
3303 let version = Version(0);
3304
3305 frontend.hack_set_program(&ctx, program).await.unwrap();
3306 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3307 let line_id = frontend.scene_graph.objects.get(6).unwrap().id;
3308
3309 let (src_delta, scene_delta) = frontend
3310 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
3311 .await
3312 .unwrap();
3313 assert_eq!(
3314 src_delta.text.as_str(),
3315 "\
3316@settings(experimentalFeatures = allow)
3317
3318sketch(on = XY) {
3319 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3320}
3321"
3322 );
3323 assert_eq!(
3324 scene_delta.new_graph.objects.len(),
3325 4,
3326 "{:#?}",
3327 scene_delta.new_graph.objects
3328 );
3329
3330 ctx.close().await;
3331 mock_ctx.close().await;
3332 }
3333
3334 #[tokio::test(flavor = "multi_thread")]
3335 async fn test_two_points_coincident() {
3336 let initial_source = "\
3337@settings(experimentalFeatures = allow)
3338
3339sketch(on = XY) {
3340 point1 = sketch2::point(at = [var 1, var 2])
3341 sketch2::point(at = [3, 4])
3342}
3343";
3344
3345 let program = Program::parse(initial_source).unwrap().0.unwrap();
3346
3347 let mut frontend = FrontendState::new();
3348
3349 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3350 let mock_ctx = ExecutorContext::new_mock(None).await;
3351 let version = Version(0);
3352
3353 frontend.hack_set_program(&ctx, program).await.unwrap();
3354 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3355 let point0_id = frontend.scene_graph.objects.get(1).unwrap().id;
3356 let point1_id = frontend.scene_graph.objects.get(2).unwrap().id;
3357
3358 let constraint = Constraint::Coincident(Coincident {
3359 segments: vec![point0_id, point1_id],
3360 });
3361 let (src_delta, scene_delta) = frontend
3362 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3363 .await
3364 .unwrap();
3365 assert_eq!(
3366 src_delta.text.as_str(),
3367 "\
3368@settings(experimentalFeatures = allow)
3369
3370sketch(on = XY) {
3371 point1 = sketch2::point(at = [var 1, var 2])
3372 point2 = sketch2::point(at = [3, 4])
3373 sketch2::coincident([point1, point2])
3374}
3375"
3376 );
3377 assert_eq!(
3378 scene_delta.new_graph.objects.len(),
3379 4,
3380 "{:#?}",
3381 scene_delta.new_graph.objects
3382 );
3383
3384 ctx.close().await;
3385 mock_ctx.close().await;
3386 }
3387
3388 #[tokio::test(flavor = "multi_thread")]
3389 async fn test_coincident_of_line_end_points() {
3390 let initial_source = "\
3391@settings(experimentalFeatures = allow)
3392
3393sketch(on = XY) {
3394 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3395 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3396}
3397";
3398
3399 let program = Program::parse(initial_source).unwrap().0.unwrap();
3400
3401 let mut frontend = FrontendState::new();
3402
3403 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3404 let mock_ctx = ExecutorContext::new_mock(None).await;
3405 let version = Version(0);
3406
3407 frontend.hack_set_program(&ctx, program).await.unwrap();
3408 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3409 let point0_id = frontend.scene_graph.objects.get(2).unwrap().id;
3410 let point1_id = frontend.scene_graph.objects.get(4).unwrap().id;
3411
3412 let constraint = Constraint::Coincident(Coincident {
3413 segments: vec![point0_id, point1_id],
3414 });
3415 let (src_delta, scene_delta) = frontend
3416 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3417 .await
3418 .unwrap();
3419 assert_eq!(
3420 src_delta.text.as_str(),
3421 "\
3422@settings(experimentalFeatures = allow)
3423
3424sketch(on = XY) {
3425 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3426 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3427 sketch2::coincident([line1.end, line2.start])
3428}
3429"
3430 );
3431 assert_eq!(
3432 scene_delta.new_graph.objects.len(),
3433 8,
3434 "{:#?}",
3435 scene_delta.new_graph.objects
3436 );
3437
3438 ctx.close().await;
3439 mock_ctx.close().await;
3440 }
3441
3442 #[tokio::test(flavor = "multi_thread")]
3443 async fn test_distance_two_points() {
3444 let initial_source = "\
3445@settings(experimentalFeatures = allow)
3446
3447sketch(on = XY) {
3448 sketch2::point(at = [var 1, var 2])
3449 sketch2::point(at = [var 3, var 4])
3450}
3451";
3452
3453 let program = Program::parse(initial_source).unwrap().0.unwrap();
3454
3455 let mut frontend = FrontendState::new();
3456
3457 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3458 let mock_ctx = ExecutorContext::new_mock(None).await;
3459 let version = Version(0);
3460
3461 frontend.hack_set_program(&ctx, program).await.unwrap();
3462 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3463 let point0_id = frontend.scene_graph.objects.get(1).unwrap().id;
3464 let point1_id = frontend.scene_graph.objects.get(2).unwrap().id;
3465
3466 let constraint = Constraint::Distance(Distance {
3467 points: vec![point0_id, point1_id],
3468 distance: Number {
3469 value: 2.0,
3470 units: NumericSuffix::Mm,
3471 },
3472 });
3473 let (src_delta, scene_delta) = frontend
3474 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3475 .await
3476 .unwrap();
3477 assert_eq!(
3478 src_delta.text.as_str(),
3479 "\
3481@settings(experimentalFeatures = allow)
3482
3483sketch(on = XY) {
3484 point1 = sketch2::point(at = [var 1, var 2])
3485 point2 = sketch2::point(at = [var 3, var 4])
3486sketch2::distance([point1, point2]) == 2mm
3487}
3488"
3489 );
3490 assert_eq!(
3491 scene_delta.new_graph.objects.len(),
3492 4,
3493 "{:#?}",
3494 scene_delta.new_graph.objects
3495 );
3496
3497 ctx.close().await;
3498 mock_ctx.close().await;
3499 }
3500
3501 #[tokio::test(flavor = "multi_thread")]
3502 async fn test_line_horizontal() {
3503 let initial_source = "\
3504@settings(experimentalFeatures = allow)
3505
3506sketch(on = XY) {
3507 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3508}
3509";
3510
3511 let program = Program::parse(initial_source).unwrap().0.unwrap();
3512
3513 let mut frontend = FrontendState::new();
3514
3515 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3516 let mock_ctx = ExecutorContext::new_mock(None).await;
3517 let version = Version(0);
3518
3519 frontend.hack_set_program(&ctx, program).await.unwrap();
3520 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3521 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3522
3523 let constraint = Constraint::Horizontal(Horizontal { line: line1_id });
3524 let (src_delta, scene_delta) = frontend
3525 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3526 .await
3527 .unwrap();
3528 assert_eq!(
3529 src_delta.text.as_str(),
3530 "\
3531@settings(experimentalFeatures = allow)
3532
3533sketch(on = XY) {
3534 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3535 sketch2::horizontal(line1)
3536}
3537"
3538 );
3539 assert_eq!(
3540 scene_delta.new_graph.objects.len(),
3541 5,
3542 "{:#?}",
3543 scene_delta.new_graph.objects
3544 );
3545
3546 ctx.close().await;
3547 mock_ctx.close().await;
3548 }
3549
3550 #[tokio::test(flavor = "multi_thread")]
3551 async fn test_line_vertical() {
3552 let initial_source = "\
3553@settings(experimentalFeatures = allow)
3554
3555sketch(on = XY) {
3556 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3557}
3558";
3559
3560 let program = Program::parse(initial_source).unwrap().0.unwrap();
3561
3562 let mut frontend = FrontendState::new();
3563
3564 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3565 let mock_ctx = ExecutorContext::new_mock(None).await;
3566 let version = Version(0);
3567
3568 frontend.hack_set_program(&ctx, program).await.unwrap();
3569 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3570 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3571
3572 let constraint = Constraint::Vertical(Vertical { line: line1_id });
3573 let (src_delta, scene_delta) = frontend
3574 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3575 .await
3576 .unwrap();
3577 assert_eq!(
3578 src_delta.text.as_str(),
3579 "\
3580@settings(experimentalFeatures = allow)
3581
3582sketch(on = XY) {
3583 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3584 sketch2::vertical(line1)
3585}
3586"
3587 );
3588 assert_eq!(
3589 scene_delta.new_graph.objects.len(),
3590 5,
3591 "{:#?}",
3592 scene_delta.new_graph.objects
3593 );
3594
3595 ctx.close().await;
3596 mock_ctx.close().await;
3597 }
3598
3599 #[tokio::test(flavor = "multi_thread")]
3600 async fn test_lines_equal_length() {
3601 let initial_source = "\
3602@settings(experimentalFeatures = allow)
3603
3604sketch(on = XY) {
3605 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3606 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3607}
3608";
3609
3610 let program = Program::parse(initial_source).unwrap().0.unwrap();
3611
3612 let mut frontend = FrontendState::new();
3613
3614 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3615 let mock_ctx = ExecutorContext::new_mock(None).await;
3616 let version = Version(0);
3617
3618 frontend.hack_set_program(&ctx, program).await.unwrap();
3619 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3620 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3621 let line2_id = frontend.scene_graph.objects.get(6).unwrap().id;
3622
3623 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
3624 lines: vec![line1_id, line2_id],
3625 });
3626 let (src_delta, scene_delta) = frontend
3627 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3628 .await
3629 .unwrap();
3630 assert_eq!(
3631 src_delta.text.as_str(),
3632 "\
3633@settings(experimentalFeatures = allow)
3634
3635sketch(on = XY) {
3636 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3637 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3638 sketch2::equalLength([line1, line2])
3639}
3640"
3641 );
3642 assert_eq!(
3643 scene_delta.new_graph.objects.len(),
3644 8,
3645 "{:#?}",
3646 scene_delta.new_graph.objects
3647 );
3648
3649 ctx.close().await;
3650 mock_ctx.close().await;
3651 }
3652
3653 #[tokio::test(flavor = "multi_thread")]
3654 async fn test_lines_parallel() {
3655 let initial_source = "\
3656@settings(experimentalFeatures = allow)
3657
3658sketch(on = XY) {
3659 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3660 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3661}
3662";
3663
3664 let program = Program::parse(initial_source).unwrap().0.unwrap();
3665
3666 let mut frontend = FrontendState::new();
3667
3668 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3669 let mock_ctx = ExecutorContext::new_mock(None).await;
3670 let version = Version(0);
3671
3672 frontend.hack_set_program(&ctx, program).await.unwrap();
3673 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3674 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3675 let line2_id = frontend.scene_graph.objects.get(6).unwrap().id;
3676
3677 let constraint = Constraint::Parallel(Parallel {
3678 lines: vec![line1_id, line2_id],
3679 });
3680 let (src_delta, scene_delta) = frontend
3681 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3682 .await
3683 .unwrap();
3684 assert_eq!(
3685 src_delta.text.as_str(),
3686 "\
3687@settings(experimentalFeatures = allow)
3688
3689sketch(on = XY) {
3690 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3691 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3692 sketch2::parallel([line1, line2])
3693}
3694"
3695 );
3696 assert_eq!(
3697 scene_delta.new_graph.objects.len(),
3698 8,
3699 "{:#?}",
3700 scene_delta.new_graph.objects
3701 );
3702
3703 ctx.close().await;
3704 mock_ctx.close().await;
3705 }
3706
3707 #[tokio::test(flavor = "multi_thread")]
3708 async fn test_lines_perpendicular() {
3709 let initial_source = "\
3710@settings(experimentalFeatures = allow)
3711
3712sketch(on = XY) {
3713 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3714 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3715}
3716";
3717
3718 let program = Program::parse(initial_source).unwrap().0.unwrap();
3719
3720 let mut frontend = FrontendState::new();
3721
3722 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3723 let mock_ctx = ExecutorContext::new_mock(None).await;
3724 let version = Version(0);
3725
3726 frontend.hack_set_program(&ctx, program).await.unwrap();
3727 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3728 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3729 let line2_id = frontend.scene_graph.objects.get(6).unwrap().id;
3730
3731 let constraint = Constraint::Perpendicular(Perpendicular {
3732 lines: vec![line1_id, line2_id],
3733 });
3734 let (src_delta, scene_delta) = frontend
3735 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3736 .await
3737 .unwrap();
3738 assert_eq!(
3739 src_delta.text.as_str(),
3740 "\
3741@settings(experimentalFeatures = allow)
3742
3743sketch(on = XY) {
3744 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3745 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3746 sketch2::perpendicular([line1, line2])
3747}
3748"
3749 );
3750 assert_eq!(
3751 scene_delta.new_graph.objects.len(),
3752 8,
3753 "{:#?}",
3754 scene_delta.new_graph.objects
3755 );
3756
3757 ctx.close().await;
3758 mock_ctx.close().await;
3759 }
3760}