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::{Distance, Line, LinesEqualLength, Parallel, 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 walk::{NodeMut, Visitable},
26};
27
28pub(crate) mod api;
29mod modify;
30pub(crate) mod sketch;
31mod traverse;
32
33const POINT_FN: &str = "point";
34const POINT_AT_PARAM: &str = "at";
35const LINE_FN: &str = "line";
36const LINE_START_PARAM: &str = "start";
37const LINE_END_PARAM: &str = "end";
38const COINCIDENT_FN: &str = "coincident";
39const DISTANCE_FN: &str = "distance";
40const EQUAL_LENGTH_FN: &str = "equalLength";
41const HORIZONTAL_FN: &str = "horizontal";
42const PARALLEL_FN: &str = "parallel";
43const VERTICAL_FN: &str = "vertical";
44
45const LINE_PROPERTY_START: &str = "start";
46const LINE_PROPERTY_END: &str = "end";
47
48#[derive(Debug, Clone)]
49pub struct FrontendState {
50 program: Program,
51 scene_graph: SceneGraph,
52}
53
54impl Default for FrontendState {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl FrontendState {
61 pub fn new() -> Self {
62 Self {
63 program: Program::empty(),
64 scene_graph: SceneGraph {
65 project: ProjectId(0),
66 file: FileId(0),
67 version: Version(0),
68 objects: Default::default(),
69 settings: Default::default(),
70 sketch_mode: Default::default(),
71 },
72 }
73 }
74}
75
76impl SketchApi for FrontendState {
77 async fn execute_mock(
78 &mut self,
79 ctx: &ExecutorContext,
80 _version: Version,
81 _sketch: ObjectId,
82 ) -> api::Result<(SceneGraph, ExecOutcome)> {
83 let outcome = ctx
85 .run_mock(&self.program, &MockConfig::default())
86 .await
87 .map_err(|err| Error {
88 msg: err.error.message().to_owned(),
89 })?;
90 let outcome = self.update_state_after_exec(outcome);
91 Ok((self.scene_graph.clone(), outcome))
92 }
93
94 async fn new_sketch(
95 &mut self,
96 ctx: &ExecutorContext,
97 _project: ProjectId,
98 _file: FileId,
99 _version: Version,
100 args: SketchArgs,
101 ) -> api::Result<(SourceDelta, SceneGraphDelta, ObjectId)> {
102 let plane_ast = match &args.on {
106 api::Plane::Object(_) => todo!(),
108 api::Plane::Default(plane) => ast_name_expr(plane.to_string()),
109 };
110 let sketch_ast = ast::SketchBlock {
111 arguments: vec![ast::LabeledArg {
112 label: Some(ast::Identifier::new("on")),
113 arg: plane_ast,
114 }],
115 body: Default::default(),
116 non_code_meta: Default::default(),
117 digest: None,
118 };
119 let mut new_ast = self.program.ast.clone();
120 new_ast.set_experimental_features(Some(WarningLevel::Allow));
123 new_ast.body.push(ast::BodyItem::ExpressionStatement(ast::Node {
125 inner: ast::ExpressionStatement {
126 expression: ast::Expr::SketchBlock(Box::new(ast::Node {
127 inner: sketch_ast,
128 start: Default::default(),
129 end: Default::default(),
130 module_id: Default::default(),
131 outer_attrs: Default::default(),
132 pre_comments: Default::default(),
133 comment_start: Default::default(),
134 })),
135 digest: None,
136 },
137 start: Default::default(),
138 end: Default::default(),
139 module_id: Default::default(),
140 outer_attrs: Default::default(),
141 pre_comments: Default::default(),
142 comment_start: Default::default(),
143 }));
144 let new_source = source_from_ast(&new_ast);
146 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
148 if !errors.is_empty() {
149 return Err(Error {
150 msg: format!("Error parsing KCL source after adding sketch: {errors:?}"),
151 });
152 }
153 let Some(new_program) = new_program else {
154 return Err(Error {
155 msg: "No AST produced after adding sketch".to_owned(),
156 });
157 };
158
159 let sketch_source_range = new_program
160 .ast
161 .body
162 .last()
163 .map(SourceRange::from)
164 .ok_or_else(|| Error {
165 msg: "No AST body items after adding sketch".to_owned(),
166 })?;
167 #[cfg(not(feature = "artifact-graph"))]
168 let _ = sketch_source_range;
169
170 self.program = new_program.clone();
172
173 let outcome = ctx
175 .run_mock(&new_program, &MockConfig::default())
176 .await
177 .map_err(|err| {
178 Error {
181 msg: err.error.message().to_owned(),
182 }
183 })?;
184
185 #[cfg(not(feature = "artifact-graph"))]
186 let sketch_id = ObjectId(0);
187 #[cfg(feature = "artifact-graph")]
188 let sketch_id = outcome
189 .source_range_to_object
190 .get(&sketch_source_range)
191 .copied()
192 .ok_or_else(|| Error {
193 msg: format!("Source range of sketch not found: {sketch_source_range:?}"),
194 })?;
195 let src_delta = SourceDelta { text: new_source };
196 self.scene_graph.sketch_mode = Some(sketch_id);
198 let outcome = self.update_state_after_exec(outcome);
199 let scene_graph_delta = SceneGraphDelta {
200 new_graph: self.scene_graph.clone(),
201 invalidates_ids: false,
202 new_objects: vec![sketch_id],
203 exec_outcome: outcome,
204 };
205 Ok((src_delta, scene_graph_delta, sketch_id))
206 }
207
208 async fn edit_sketch(
209 &mut self,
210 ctx: &ExecutorContext,
211 _project: ProjectId,
212 _file: FileId,
213 _version: Version,
214 sketch: ObjectId,
215 ) -> api::Result<SceneGraphDelta> {
216 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| Error {
220 msg: format!("Sketch not found: {sketch:?}"),
221 })?;
222 let ObjectKind::Sketch(_) = &sketch_object.kind else {
223 return Err(Error {
224 msg: format!("Object is not a sketch: {sketch_object:?}"),
225 });
226 };
227
228 self.scene_graph.sketch_mode = Some(sketch);
230
231 let mock_config = MockConfig {
234 freedom_analysis: true,
235 ..Default::default()
236 };
237 let outcome = ctx.run_mock(&self.program, &mock_config).await.map_err(|err| {
238 Error {
241 msg: err.error.message().to_owned(),
242 }
243 })?;
244
245 let outcome = self.update_state_after_exec(outcome);
246 let scene_graph_delta = SceneGraphDelta {
247 new_graph: self.scene_graph.clone(),
248 invalidates_ids: false,
249 new_objects: Vec::new(),
250 exec_outcome: outcome,
251 };
252 Ok(scene_graph_delta)
253 }
254
255 async fn exit_sketch(
256 &mut self,
257 ctx: &ExecutorContext,
258 _version: Version,
259 sketch: ObjectId,
260 ) -> api::Result<SceneGraph> {
261 #[cfg(not(target_arch = "wasm32"))]
263 let _ = sketch;
264 #[cfg(target_arch = "wasm32")]
265 if self.scene_graph.sketch_mode != Some(sketch) {
266 web_sys::console::warn_1(
267 &format!(
268 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
269 &self.scene_graph.sketch_mode
270 )
271 .into(),
272 );
273 }
274 self.scene_graph.sketch_mode = None;
275
276 let outcome = ctx.run_with_caching(self.program.clone()).await.map_err(|err| {
278 Error {
281 msg: err.error.message().to_owned(),
282 }
283 })?;
284
285 self.update_state_after_exec(outcome);
286
287 Ok(self.scene_graph.clone())
288 }
289
290 async fn add_segment(
291 &mut self,
292 ctx: &ExecutorContext,
293 _version: Version,
294 sketch: ObjectId,
295 segment: SegmentCtor,
296 _label: Option<String>,
297 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
298 match segment {
300 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
301 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
302 _ => Err(Error {
303 msg: format!("segment ctor not implemented yet: {segment:?}"),
304 }),
305 }
306 }
307
308 async fn edit_segments(
309 &mut self,
310 ctx: &ExecutorContext,
311 _version: Version,
312 sketch: ObjectId,
313 segments: Vec<ExistingSegmentCtor>,
314 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
315 let mut new_ast = self.program.ast.clone();
317 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
318 for segment in segments {
319 segment_ids_edited.insert(segment.id);
320 match segment.ctor {
321 SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
322 SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
323 _ => {
324 return Err(Error {
325 msg: format!("segment ctor not implemented yet: {segment:?}"),
326 });
327 }
328 }
329 }
330 self.execute_after_edit(ctx, sketch, segment_ids_edited, false, &mut new_ast)
331 .await
332 }
333
334 async fn delete_objects(
335 &mut self,
336 ctx: &ExecutorContext,
337 _version: Version,
338 sketch: ObjectId,
339 constraint_ids: Vec<ObjectId>,
340 segment_ids: Vec<ObjectId>,
341 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
342 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
346 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
347 self.add_dependent_constraints_to_delete(sketch, &segment_ids_set, &mut constraint_ids_set)?;
350
351 let mut new_ast = self.program.ast.clone();
352 for constraint_id in constraint_ids_set {
353 self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
354 }
355 for segment_id in segment_ids_set {
356 self.delete_segment(&mut new_ast, sketch, segment_id)?;
357 }
358 self.execute_after_edit(ctx, sketch, Default::default(), true, &mut new_ast)
359 .await
360 }
361
362 async fn add_constraint(
363 &mut self,
364 ctx: &ExecutorContext,
365 _version: Version,
366 sketch: ObjectId,
367 constraint: Constraint,
368 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
369 let mut new_ast = self.program.ast.clone();
372 let sketch_block_range = match constraint {
373 Constraint::Coincident(coincident) => self.add_coincident(sketch, coincident, &mut new_ast).await?,
374 Constraint::Distance(distance) => self.add_distance(sketch, distance, &mut new_ast).await?,
375 Constraint::Horizontal(horizontal) => self.add_horizontal(sketch, horizontal, &mut new_ast).await?,
376 Constraint::LinesEqualLength(lines_equal_length) => {
377 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
378 .await?
379 }
380 Constraint::Parallel(parallel) => self.add_parallel(sketch, parallel, &mut new_ast).await?,
381 Constraint::Vertical(vertical) => self.add_vertical(sketch, vertical, &mut new_ast).await?,
382 };
383 self.execute_after_add_constraint(ctx, sketch, sketch_block_range, &mut new_ast)
384 .await
385 }
386
387 async fn edit_constraint(
388 &mut self,
389 _ctx: &ExecutorContext,
390 _version: Version,
391 _sketch: ObjectId,
392 _constraint_id: ObjectId,
393 _constraint: Constraint,
394 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
395 todo!()
396 }
397}
398
399impl FrontendState {
400 pub async fn hack_set_program(
401 &mut self,
402 ctx: &ExecutorContext,
403 program: Program,
404 ) -> api::Result<(SceneGraph, ExecOutcome)> {
405 self.program = program.clone();
406
407 let outcome = ctx.run_with_caching(program).await.map_err(|err| {
410 Error {
413 msg: err.error.message().to_owned(),
414 }
415 })?;
416
417 let outcome = self.update_state_after_exec(outcome);
418
419 Ok((self.scene_graph.clone(), outcome))
420 }
421
422 async fn add_point(
423 &mut self,
424 ctx: &ExecutorContext,
425 sketch: ObjectId,
426 ctor: PointCtor,
427 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
428 let at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
430 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
431 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
432 unlabeled: None,
433 arguments: vec![ast::LabeledArg {
434 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
435 arg: at_ast,
436 }],
437 digest: None,
438 non_code_meta: Default::default(),
439 })));
440
441 let sketch_id = sketch;
443 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
444 #[cfg(target_arch = "wasm32")]
445 web_sys::console::error_1(
446 &format!(
447 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
448 &self.scene_graph.objects
449 )
450 .into(),
451 );
452 Error {
453 msg: format!("Sketch not found: {sketch:?}"),
454 }
455 })?;
456 let ObjectKind::Sketch(_) = &sketch_object.kind else {
457 return Err(Error {
458 msg: format!("Object is not a sketch: {sketch_object:?}"),
459 });
460 };
461 let mut new_ast = self.program.ast.clone();
463 let (sketch_block_range, _) = self.mutate_ast(
464 &mut new_ast,
465 sketch_id,
466 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
467 )?;
468 let new_source = source_from_ast(&new_ast);
470 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
472 if !errors.is_empty() {
473 return Err(Error {
474 msg: format!("Error parsing KCL source after adding point: {errors:?}"),
475 });
476 }
477 let Some(new_program) = new_program else {
478 return Err(Error {
479 msg: "No AST produced after adding point".to_string(),
480 });
481 };
482
483 let point_source_range =
484 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
485 msg: format!("Source range of point not found in sketch block: {sketch_block_range:?}; {err:?}"),
486 })?;
487 #[cfg(not(feature = "artifact-graph"))]
488 let _ = point_source_range;
489
490 self.program = new_program.clone();
492
493 let outcome = ctx
495 .run_mock(&new_program, &MockConfig::default())
496 .await
497 .map_err(|err| {
498 Error {
501 msg: err.error.message().to_owned(),
502 }
503 })?;
504
505 #[cfg(not(feature = "artifact-graph"))]
506 let new_object_ids = Vec::new();
507 #[cfg(feature = "artifact-graph")]
508 let new_object_ids = {
509 let segment_id = outcome
510 .source_range_to_object
511 .get(&point_source_range)
512 .copied()
513 .ok_or_else(|| Error {
514 msg: format!("Source range of point not found: {point_source_range:?}"),
515 })?;
516 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
517 msg: format!("Segment not found: {segment_id:?}"),
518 })?;
519 let ObjectKind::Segment { segment } = &segment_object.kind else {
520 return Err(Error {
521 msg: format!("Object is not a segment: {segment_object:?}"),
522 });
523 };
524 let Segment::Point(_) = segment else {
525 return Err(Error {
526 msg: format!("Segment is not a point: {segment:?}"),
527 });
528 };
529 vec![segment_id]
530 };
531 let src_delta = SourceDelta { text: new_source };
532 let outcome = self.update_state_after_exec(outcome);
533 let scene_graph_delta = SceneGraphDelta {
534 new_graph: self.scene_graph.clone(),
535 invalidates_ids: false,
536 new_objects: new_object_ids,
537 exec_outcome: outcome,
538 };
539 Ok((src_delta, scene_graph_delta))
540 }
541
542 async fn add_line(
543 &mut self,
544 ctx: &ExecutorContext,
545 sketch: ObjectId,
546 ctor: LineCtor,
547 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
548 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
550 let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
551 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
552 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
553 unlabeled: None,
554 arguments: vec![
555 ast::LabeledArg {
556 label: Some(ast::Identifier::new(LINE_START_PARAM)),
557 arg: start_ast,
558 },
559 ast::LabeledArg {
560 label: Some(ast::Identifier::new(LINE_END_PARAM)),
561 arg: end_ast,
562 },
563 ],
564 digest: None,
565 non_code_meta: Default::default(),
566 })));
567
568 let sketch_id = sketch;
570 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
571 msg: format!("Sketch not found: {sketch:?}"),
572 })?;
573 let ObjectKind::Sketch(_) = &sketch_object.kind else {
574 return Err(Error {
575 msg: format!("Object is not a sketch: {sketch_object:?}"),
576 });
577 };
578 let mut new_ast = self.program.ast.clone();
580 let (sketch_block_range, _) = self.mutate_ast(
581 &mut new_ast,
582 sketch_id,
583 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
584 )?;
585 let new_source = source_from_ast(&new_ast);
587 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
589 if !errors.is_empty() {
590 return Err(Error {
591 msg: format!("Error parsing KCL source after adding line: {errors:?}"),
592 });
593 }
594 let Some(new_program) = new_program else {
595 return Err(Error {
596 msg: "No AST produced after adding line".to_string(),
597 });
598 };
599 let line_source_range =
600 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
601 msg: format!("Source range of line not found in sketch block: {sketch_block_range:?}; {err:?}"),
602 })?;
603 #[cfg(not(feature = "artifact-graph"))]
604 let _ = line_source_range;
605
606 self.program = new_program.clone();
608
609 let outcome = ctx
611 .run_mock(&new_program, &MockConfig::default())
612 .await
613 .map_err(|err| {
614 Error {
617 msg: err.error.message().to_owned(),
618 }
619 })?;
620
621 #[cfg(not(feature = "artifact-graph"))]
622 let new_object_ids = Vec::new();
623 #[cfg(feature = "artifact-graph")]
624 let new_object_ids = {
625 let segment_id = outcome
626 .source_range_to_object
627 .get(&line_source_range)
628 .copied()
629 .ok_or_else(|| Error {
630 msg: format!("Source range of line not found: {line_source_range:?}"),
631 })?;
632 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
633 msg: format!("Segment not found: {segment_id:?}"),
634 })?;
635 let ObjectKind::Segment { segment } = &segment_object.kind else {
636 return Err(Error {
637 msg: format!("Object is not a segment: {segment_object:?}"),
638 });
639 };
640 let Segment::Line(line) = segment else {
641 return Err(Error {
642 msg: format!("Segment is not a line: {segment:?}"),
643 });
644 };
645 vec![line.start, line.end, segment_id]
646 };
647 let src_delta = SourceDelta { text: new_source };
648 let outcome = self.update_state_after_exec(outcome);
649 let scene_graph_delta = SceneGraphDelta {
650 new_graph: self.scene_graph.clone(),
651 invalidates_ids: false,
652 new_objects: new_object_ids,
653 exec_outcome: outcome,
654 };
655 Ok((src_delta, scene_graph_delta))
656 }
657
658 fn edit_point(
659 &mut self,
660 new_ast: &mut ast::Node<ast::Program>,
661 sketch: ObjectId,
662 point: ObjectId,
663 ctor: PointCtor,
664 ) -> api::Result<()> {
665 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
667
668 let sketch_id = sketch;
670 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
671 msg: format!("Sketch not found: {sketch:?}"),
672 })?;
673 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
674 return Err(Error {
675 msg: format!("Object is not a sketch: {sketch_object:?}"),
676 });
677 };
678 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| Error {
679 msg: format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"),
680 })?;
681 let point_id = point;
683 let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
684 msg: format!("Point not found in scene graph: point={point:?}"),
685 })?;
686 let ObjectKind::Segment {
687 segment: Segment::Point(point),
688 } = &point_object.kind
689 else {
690 return Err(Error {
691 msg: format!("Object is not a point segment: {point_object:?}"),
692 });
693 };
694
695 if let Some(line_id) = point.owner {
697 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
698 msg: format!("Internal: Line owner of point not found in scene graph: line={line_id:?}",),
699 })?;
700 let ObjectKind::Segment {
701 segment: Segment::Line(line),
702 } = &line_object.kind
703 else {
704 return Err(Error {
705 msg: format!("Internal: Owner of point is not actually a line segment: {line_object:?}"),
706 });
707 };
708 let SegmentCtor::Line(line_ctor) = &line.ctor else {
709 return Err(Error {
710 msg: format!("Internal: Owner of point does not have line ctor: {line_object:?}"),
711 });
712 };
713 let mut line_ctor = line_ctor.clone();
714 if line.start == point_id {
716 line_ctor.start = ctor.position;
717 } else if line.end == point_id {
718 line_ctor.end = ctor.position;
719 } else {
720 return Err(Error {
721 msg: format!(
722 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={line_id:?}"
723 ),
724 });
725 }
726 return self.edit_line(new_ast, sketch_id, line_id, line_ctor);
727 }
728
729 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
731 Ok(())
732 }
733
734 fn edit_line(
735 &mut self,
736 new_ast: &mut ast::Node<ast::Program>,
737 sketch: ObjectId,
738 line: ObjectId,
739 ctor: LineCtor,
740 ) -> api::Result<()> {
741 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
743 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
744
745 let sketch_id = sketch;
747 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
748 msg: format!("Sketch not found: {sketch:?}"),
749 })?;
750 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
751 return Err(Error {
752 msg: format!("Object is not a sketch: {sketch_object:?}"),
753 });
754 };
755 sketch.segments.iter().find(|o| **o == line).ok_or_else(|| Error {
756 msg: format!("Line not found in sketch: line={line:?}, sketch={sketch:?}"),
757 })?;
758 let line_id = line;
760 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
761 msg: format!("Line not found in scene graph: line={line:?}"),
762 })?;
763 let ObjectKind::Segment { .. } = &line_object.kind else {
764 return Err(Error {
765 msg: format!("Object is not a segment: {line_object:?}"),
766 });
767 };
768
769 self.mutate_ast(
771 new_ast,
772 line_id,
773 AstMutateCommand::EditLine {
774 start: new_start_ast,
775 end: new_end_ast,
776 },
777 )?;
778 Ok(())
779 }
780
781 fn delete_segment(
782 &mut self,
783 new_ast: &mut ast::Node<ast::Program>,
784 sketch: ObjectId,
785 segment_id: ObjectId,
786 ) -> api::Result<()> {
787 let sketch_id = sketch;
789 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
790 msg: format!("Sketch not found: {sketch:?}"),
791 })?;
792 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
793 return Err(Error {
794 msg: format!("Object is not a sketch: {sketch_object:?}"),
795 });
796 };
797 sketch
798 .segments
799 .iter()
800 .find(|o| **o == segment_id)
801 .ok_or_else(|| Error {
802 msg: format!("Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"),
803 })?;
804 let segment_object = self.scene_graph.objects.get(segment_id.0).ok_or_else(|| Error {
806 msg: format!("Segment not found in scene graph: segment={segment_id:?}"),
807 })?;
808 let ObjectKind::Segment { .. } = &segment_object.kind else {
809 return Err(Error {
810 msg: format!("Object is not a segment: {segment_object:?}"),
811 });
812 };
813
814 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
816 Ok(())
817 }
818
819 fn delete_constraint(
820 &mut self,
821 new_ast: &mut ast::Node<ast::Program>,
822 sketch: ObjectId,
823 constraint_id: ObjectId,
824 ) -> api::Result<()> {
825 let sketch_id = sketch;
827 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
828 msg: format!("Sketch not found: {sketch:?}"),
829 })?;
830 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
831 return Err(Error {
832 msg: format!("Object is not a sketch: {sketch_object:?}"),
833 });
834 };
835 sketch
836 .constraints
837 .iter()
838 .find(|o| **o == constraint_id)
839 .ok_or_else(|| Error {
840 msg: format!("Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"),
841 })?;
842 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
844 msg: format!("Constraint not found in scene graph: constraint={constraint_id:?}"),
845 })?;
846 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
847 return Err(Error {
848 msg: format!("Object is not a constraint: {constraint_object:?}"),
849 });
850 };
851
852 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
854 Ok(())
855 }
856
857 async fn execute_after_edit(
858 &mut self,
859 ctx: &ExecutorContext,
860 _sketch_id: ObjectId,
861 segment_ids_edited: AhashIndexSet<ObjectId>,
862 is_delete: bool,
863 new_ast: &mut ast::Node<ast::Program>,
864 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
865 let new_source = source_from_ast(new_ast);
867 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
869 if !errors.is_empty() {
870 return Err(Error {
871 msg: format!("Error parsing KCL source after editing: {errors:?}"),
872 });
873 }
874 let Some(new_program) = new_program else {
875 return Err(Error {
876 msg: "No AST produced after editing".to_string(),
877 });
878 };
879
880 self.program = new_program.clone();
882
883 #[cfg(not(feature = "artifact-graph"))]
884 drop(segment_ids_edited);
885
886 let mock_config = MockConfig {
888 use_prev_memory: !is_delete,
889 freedom_analysis: is_delete,
890 #[cfg(feature = "artifact-graph")]
891 segment_ids_edited,
892 };
893 let outcome = ctx.run_mock(&new_program, &mock_config).await.map_err(|err| {
894 Error {
897 msg: err.error.message().to_owned(),
898 }
899 })?;
900
901 let outcome = self.update_state_after_exec(outcome);
902
903 #[cfg(feature = "artifact-graph")]
904 let new_source = {
905 let mut new_ast = self.program.ast.clone();
911 for (var_range, value) in &outcome.var_solutions {
912 let rounded = value.round(3);
913 mutate_ast_node_by_source_range(
914 &mut new_ast,
915 *var_range,
916 AstMutateCommand::EditVarInitialValue { value: rounded },
917 )?;
918 }
919 source_from_ast(&new_ast)
920 };
921
922 let src_delta = SourceDelta { text: new_source };
923 let scene_graph_delta = SceneGraphDelta {
924 new_graph: self.scene_graph.clone(),
925 invalidates_ids: is_delete,
926 new_objects: Vec::new(),
927 exec_outcome: outcome,
928 };
929 Ok((src_delta, scene_graph_delta))
930 }
931
932 async fn add_coincident(
933 &mut self,
934 sketch: ObjectId,
935 coincident: Coincident,
936 new_ast: &mut ast::Node<ast::Program>,
937 ) -> api::Result<SourceRange> {
938 if coincident.points.len() != 2 {
939 return Err(Error {
940 msg: format!(
941 "Coincident constraint must have exactly 2 points, got {}",
942 coincident.points.len()
943 ),
944 });
945 }
946 let sketch_id = sketch;
947
948 let pt0_id = coincident.points[0];
950 let pt0_object = self.scene_graph.objects.get(pt0_id.0).ok_or_else(|| Error {
951 msg: format!("Point not found: {pt0_id:?}"),
952 })?;
953 let ObjectKind::Segment { segment: pt0_segment } = &pt0_object.kind else {
954 return Err(Error {
955 msg: format!("Object is not a segment: {pt0_object:?}"),
956 });
957 };
958 let Segment::Point(pt0) = pt0_segment else {
959 return Err(Error {
960 msg: format!("Only points are currently supported: {pt0_object:?}"),
961 });
962 };
963 let pt0_ast = if let Some(line_id) = pt0.owner {
965 let line = self.expect_line(line_id)?;
966 let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
967 let property = if line.start == pt0_id {
968 LINE_PROPERTY_START
969 } else if line.end == pt0_id {
970 LINE_PROPERTY_END
971 } else {
972 return Err(Error {
973 msg: format!(
974 "Internal: Point is not part of owner's line segment: point={pt0_id:?}, line={line_id:?}"
975 ),
976 });
977 };
978 get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
979 } else {
980 get_or_insert_ast_reference(new_ast, &pt0_object.source, "point", None)?
981 };
982
983 let pt1_id = coincident.points[1];
984 let pt1_object = self.scene_graph.objects.get(pt1_id.0).ok_or_else(|| Error {
985 msg: format!("Point not found: {pt1_id:?}"),
986 })?;
987 let ObjectKind::Segment { segment: pt1_segment } = &pt1_object.kind else {
988 return Err(Error {
989 msg: format!("Object is not a segment: {pt1_object:?}"),
990 });
991 };
992 let Segment::Point(pt1) = pt1_segment else {
993 return Err(Error {
994 msg: format!("Only points are currently supported: {pt1_object:?}"),
995 });
996 };
997 let pt1_ast = if let Some(line_id) = pt1.owner {
999 let line = self.expect_line(line_id)?;
1000 let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
1001 let property = if line.start == pt1_id {
1002 LINE_PROPERTY_START
1003 } else if line.end == pt1_id {
1004 LINE_PROPERTY_END
1005 } else {
1006 return Err(Error {
1007 msg: format!(
1008 "Internal: Point is not part of owner's line segment: point={pt1_id:?}, line={line_id:?}"
1009 ),
1010 });
1011 };
1012 get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
1013 } else {
1014 get_or_insert_ast_reference(new_ast, &pt1_object.source, "point", None)?
1015 };
1016
1017 let coincident_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1019 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
1020 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1021 ast::ArrayExpression {
1022 elements: vec![pt0_ast, pt1_ast],
1023 digest: None,
1024 non_code_meta: Default::default(),
1025 },
1026 )))),
1027 arguments: Default::default(),
1028 digest: None,
1029 non_code_meta: Default::default(),
1030 })));
1031
1032 let (sketch_block_range, _) = self.mutate_ast(
1034 new_ast,
1035 sketch_id,
1036 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
1037 )?;
1038 Ok(sketch_block_range)
1039 }
1040
1041 async fn add_distance(
1042 &mut self,
1043 sketch: ObjectId,
1044 distance: Distance,
1045 new_ast: &mut ast::Node<ast::Program>,
1046 ) -> api::Result<SourceRange> {
1047 if distance.points.len() != 2 {
1048 return Err(Error {
1049 msg: format!(
1050 "Distance constraint must have exactly 2 points, got {}",
1051 distance.points.len()
1052 ),
1053 });
1054 }
1055 let sketch_id = sketch;
1056
1057 let pt0_id = distance.points[0];
1059 let pt0_object = self.scene_graph.objects.get(pt0_id.0).ok_or_else(|| Error {
1060 msg: format!("Point not found: {pt0_id:?}"),
1061 })?;
1062 let ObjectKind::Segment { segment: pt0_segment } = &pt0_object.kind else {
1063 return Err(Error {
1064 msg: format!("Object is not a segment: {pt0_object:?}"),
1065 });
1066 };
1067 let Segment::Point(pt0) = pt0_segment else {
1068 return Err(Error {
1069 msg: format!("Only points are currently supported: {pt0_object:?}"),
1070 });
1071 };
1072 let pt0_ast = if let Some(line_id) = pt0.owner {
1074 let line = self.expect_line(line_id)?;
1075 let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
1076 let property = if line.start == pt0_id {
1077 LINE_PROPERTY_START
1078 } else if line.end == pt0_id {
1079 LINE_PROPERTY_END
1080 } else {
1081 return Err(Error {
1082 msg: format!(
1083 "Internal: Point is not part of owner's line segment: point={pt0_id:?}, line={line_id:?}"
1084 ),
1085 });
1086 };
1087 get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
1088 } else {
1089 get_or_insert_ast_reference(new_ast, &pt0_object.source, "point", None)?
1090 };
1091
1092 let pt1_id = distance.points[1];
1093 let pt1_object = self.scene_graph.objects.get(pt1_id.0).ok_or_else(|| Error {
1094 msg: format!("Point not found: {pt1_id:?}"),
1095 })?;
1096 let ObjectKind::Segment { segment: pt1_segment } = &pt1_object.kind else {
1097 return Err(Error {
1098 msg: format!("Object is not a segment: {pt1_object:?}"),
1099 });
1100 };
1101 let Segment::Point(pt1) = pt1_segment else {
1102 return Err(Error {
1103 msg: format!("Only points are currently supported: {pt1_object:?}"),
1104 });
1105 };
1106 let pt1_ast = if let Some(line_id) = pt1.owner {
1108 let line = self.expect_line(line_id)?;
1109 let line_source = &self.scene_graph.objects.get(line_id.0).unwrap().source;
1110 let property = if line.start == pt1_id {
1111 LINE_PROPERTY_START
1112 } else if line.end == pt1_id {
1113 LINE_PROPERTY_END
1114 } else {
1115 return Err(Error {
1116 msg: format!(
1117 "Internal: Point is not part of owner's line segment: point={pt1_id:?}, line={line_id:?}"
1118 ),
1119 });
1120 };
1121 get_or_insert_ast_reference(new_ast, line_source, "line", Some(property))?
1122 } else {
1123 get_or_insert_ast_reference(new_ast, &pt1_object.source, "point", None)?
1124 };
1125
1126 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1128 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
1129 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1130 ast::ArrayExpression {
1131 elements: vec![pt0_ast, pt1_ast],
1132 digest: None,
1133 non_code_meta: Default::default(),
1134 },
1135 )))),
1136 arguments: Default::default(),
1137 digest: None,
1138 non_code_meta: Default::default(),
1139 })));
1140 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
1141 left: distance_call_ast,
1142 operator: ast::BinaryOperator::Eq,
1143 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
1144 value: ast::LiteralValue::Number {
1145 value: distance.distance.value,
1146 suffix: distance.distance.units,
1147 },
1148 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
1149 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
1150 })?,
1151 digest: None,
1152 }))),
1153 digest: None,
1154 })));
1155
1156 let (sketch_block_range, _) = self.mutate_ast(
1158 new_ast,
1159 sketch_id,
1160 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
1161 )?;
1162 Ok(sketch_block_range)
1163 }
1164
1165 async fn add_horizontal(
1166 &mut self,
1167 sketch: ObjectId,
1168 horizontal: Horizontal,
1169 new_ast: &mut ast::Node<ast::Program>,
1170 ) -> api::Result<SourceRange> {
1171 let sketch_id = sketch;
1172
1173 let line_id = horizontal.line;
1175 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1176 msg: format!("Line not found: {line_id:?}"),
1177 })?;
1178 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
1179 return Err(Error {
1180 msg: format!("Object is not a segment: {line_object:?}"),
1181 });
1182 };
1183 let Segment::Line(_) = line_segment else {
1184 return Err(Error {
1185 msg: format!("Only lines can be made horizontal: {line_object:?}"),
1186 });
1187 };
1188 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
1189
1190 let horizontal_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1192 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
1193 unlabeled: Some(line_ast),
1194 arguments: Default::default(),
1195 digest: None,
1196 non_code_meta: Default::default(),
1197 })));
1198
1199 let (sketch_block_range, _) = self.mutate_ast(
1201 new_ast,
1202 sketch_id,
1203 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
1204 )?;
1205 Ok(sketch_block_range)
1206 }
1207
1208 async fn add_lines_equal_length(
1209 &mut self,
1210 sketch: ObjectId,
1211 lines_equal_length: LinesEqualLength,
1212 new_ast: &mut ast::Node<ast::Program>,
1213 ) -> api::Result<SourceRange> {
1214 if lines_equal_length.lines.len() != 2 {
1215 return Err(Error {
1216 msg: format!(
1217 "Lines equal length constraint must have exactly 2 lines, got {}",
1218 lines_equal_length.lines.len()
1219 ),
1220 });
1221 }
1222
1223 let sketch_id = sketch;
1224
1225 let line0_id = lines_equal_length.lines[0];
1227 let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
1228 msg: format!("Line not found: {line0_id:?}"),
1229 })?;
1230 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
1231 return Err(Error {
1232 msg: format!("Object is not a segment: {line0_object:?}"),
1233 });
1234 };
1235 let Segment::Line(_) = line0_segment else {
1236 return Err(Error {
1237 msg: format!("Only lines can be made equal length: {line0_object:?}"),
1238 });
1239 };
1240 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
1241
1242 let line1_id = lines_equal_length.lines[1];
1243 let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
1244 msg: format!("Line not found: {line1_id:?}"),
1245 })?;
1246 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
1247 return Err(Error {
1248 msg: format!("Object is not a segment: {line1_object:?}"),
1249 });
1250 };
1251 let Segment::Line(_) = line1_segment else {
1252 return Err(Error {
1253 msg: format!("Only lines can be made equal length: {line1_object:?}"),
1254 });
1255 };
1256 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
1257
1258 let equal_length_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1260 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
1261 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1262 ast::ArrayExpression {
1263 elements: vec![line0_ast, line1_ast],
1264 digest: None,
1265 non_code_meta: Default::default(),
1266 },
1267 )))),
1268 arguments: Default::default(),
1269 digest: None,
1270 non_code_meta: Default::default(),
1271 })));
1272
1273 let (sketch_block_range, _) = self.mutate_ast(
1275 new_ast,
1276 sketch_id,
1277 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
1278 )?;
1279 Ok(sketch_block_range)
1280 }
1281
1282 async fn add_parallel(
1283 &mut self,
1284 sketch: ObjectId,
1285 parallel: Parallel,
1286 new_ast: &mut ast::Node<ast::Program>,
1287 ) -> api::Result<SourceRange> {
1288 if parallel.lines.len() != 2 {
1289 return Err(Error {
1290 msg: format!(
1291 "Parallel constraint must have exactly 2 lines, got {}",
1292 parallel.lines.len()
1293 ),
1294 });
1295 }
1296
1297 let sketch_id = sketch;
1298
1299 let line0_id = parallel.lines[0];
1301 let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
1302 msg: format!("Line not found: {line0_id:?}"),
1303 })?;
1304 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
1305 return Err(Error {
1306 msg: format!("Object is not a segment: {line0_object:?}"),
1307 });
1308 };
1309 let Segment::Line(_) = line0_segment else {
1310 return Err(Error {
1311 msg: format!("Only lines can be made parallel: {line0_object:?}"),
1312 });
1313 };
1314 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
1315
1316 let line1_id = parallel.lines[1];
1317 let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
1318 msg: format!("Line not found: {line1_id:?}"),
1319 })?;
1320 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
1321 return Err(Error {
1322 msg: format!("Object is not a segment: {line1_object:?}"),
1323 });
1324 };
1325 let Segment::Line(_) = line1_segment else {
1326 return Err(Error {
1327 msg: format!("Only lines can be made parallel: {line1_object:?}"),
1328 });
1329 };
1330 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
1331
1332 let parallel_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1334 callee: ast::Node::no_src(ast_sketch2_name(PARALLEL_FN)),
1335 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1336 ast::ArrayExpression {
1337 elements: vec![line0_ast, line1_ast],
1338 digest: None,
1339 non_code_meta: Default::default(),
1340 },
1341 )))),
1342 arguments: Default::default(),
1343 digest: None,
1344 non_code_meta: Default::default(),
1345 })));
1346
1347 let (sketch_block_range, _) = self.mutate_ast(
1349 new_ast,
1350 sketch_id,
1351 AstMutateCommand::AddSketchBlockExprStmt { expr: parallel_ast },
1352 )?;
1353 Ok(sketch_block_range)
1354 }
1355
1356 async fn add_vertical(
1357 &mut self,
1358 sketch: ObjectId,
1359 vertical: Vertical,
1360 new_ast: &mut ast::Node<ast::Program>,
1361 ) -> api::Result<SourceRange> {
1362 let sketch_id = sketch;
1363
1364 let line_id = vertical.line;
1366 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1367 msg: format!("Line not found: {line_id:?}"),
1368 })?;
1369 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
1370 return Err(Error {
1371 msg: format!("Object is not a segment: {line_object:?}"),
1372 });
1373 };
1374 let Segment::Line(_) = line_segment else {
1375 return Err(Error {
1376 msg: format!("Only lines can be made vertical: {line_object:?}"),
1377 });
1378 };
1379 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
1380
1381 let vertical_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1383 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
1384 unlabeled: Some(line_ast),
1385 arguments: Default::default(),
1386 digest: None,
1387 non_code_meta: Default::default(),
1388 })));
1389
1390 let (sketch_block_range, _) = self.mutate_ast(
1392 new_ast,
1393 sketch_id,
1394 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
1395 )?;
1396 Ok(sketch_block_range)
1397 }
1398
1399 async fn execute_after_add_constraint(
1400 &mut self,
1401 ctx: &ExecutorContext,
1402 _sketch_id: ObjectId,
1403 sketch_block_range: SourceRange,
1404 new_ast: &mut ast::Node<ast::Program>,
1405 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1406 let new_source = source_from_ast(new_ast);
1408 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1410 if !errors.is_empty() {
1411 return Err(Error {
1412 msg: format!("Error parsing KCL source after adding constraint: {errors:?}"),
1413 });
1414 }
1415 let Some(new_program) = new_program else {
1416 return Err(Error {
1417 msg: "No AST produced after adding constraint".to_string(),
1418 });
1419 };
1420 let _constraint_source_range =
1421 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1422 msg: format!(
1423 "Source range of new constraint not found in sketch block: {sketch_block_range:?}; {err:?}"
1424 ),
1425 })?;
1426
1427 self.program = new_program.clone();
1429
1430 let mock_config = MockConfig {
1432 freedom_analysis: true,
1433 ..Default::default()
1434 };
1435 let outcome = ctx.run_mock(&new_program, &mock_config).await.map_err(|err| {
1436 Error {
1439 msg: err.error.message().to_owned(),
1440 }
1441 })?;
1442
1443 let src_delta = SourceDelta { text: new_source };
1444 let outcome = self.update_state_after_exec(outcome);
1445 let scene_graph_delta = SceneGraphDelta {
1446 new_graph: self.scene_graph.clone(),
1447 invalidates_ids: false,
1448 new_objects: Vec::new(),
1449 exec_outcome: outcome,
1450 };
1451 Ok((src_delta, scene_graph_delta))
1452 }
1453
1454 fn add_dependent_constraints_to_delete(
1457 &self,
1458 sketch_id: ObjectId,
1459 segment_ids_set: &AhashIndexSet<ObjectId>,
1460 constraint_ids_set: &mut AhashIndexSet<ObjectId>,
1461 ) -> api::Result<()> {
1462 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1464 msg: format!("Sketch not found: {sketch_id:?}"),
1465 })?;
1466 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1467 return Err(Error {
1468 msg: format!("Object is not a sketch: {sketch_object:?}"),
1469 });
1470 };
1471 for constraint_id in &sketch.constraints {
1472 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
1473 msg: format!("Constraint not found: {constraint_id:?}"),
1474 })?;
1475 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1476 return Err(Error {
1477 msg: format!("Object is not a constraint: {constraint_object:?}"),
1478 });
1479 };
1480 let depends_on_segment = match constraint {
1481 Constraint::Coincident(c) => c.points.iter().any(|pt_id| {
1482 if segment_ids_set.contains(pt_id) {
1483 return true;
1484 }
1485 let pt_object = self.scene_graph.objects.get(pt_id.0);
1486 if let Some(obj) = pt_object
1487 && let ObjectKind::Segment { segment } = &obj.kind
1488 && let Segment::Point(pt) = segment
1489 && let Some(owner_line_id) = pt.owner
1490 {
1491 return segment_ids_set.contains(&owner_line_id);
1492 }
1493 false
1494 }),
1495 Constraint::Distance(d) => d.points.iter().any(|pt_id| {
1496 let pt_object = self.scene_graph.objects.get(pt_id.0);
1497 if let Some(obj) = pt_object
1498 && let ObjectKind::Segment { segment } = &obj.kind
1499 && let Segment::Point(pt) = segment
1500 && let Some(owner_line_id) = pt.owner
1501 {
1502 return segment_ids_set.contains(&owner_line_id);
1503 }
1504 false
1505 }),
1506 Constraint::Horizontal(h) => segment_ids_set.contains(&h.line),
1507 Constraint::Vertical(v) => segment_ids_set.contains(&v.line),
1508 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
1509 .lines
1510 .iter()
1511 .any(|line_id| segment_ids_set.contains(line_id)),
1512 Constraint::Parallel(parallel) => {
1513 parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
1514 }
1515 };
1516 if depends_on_segment {
1517 constraint_ids_set.insert(*constraint_id);
1518 }
1519 }
1520 Ok(())
1521 }
1522
1523 fn expect_line(&self, object_id: ObjectId) -> api::Result<&Line> {
1524 let object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
1525 msg: format!("Object not found: {object_id:?}"),
1526 })?;
1527 let ObjectKind::Segment { segment } = &object.kind else {
1528 return Err(Error {
1529 msg: format!("Object is not a segment: {object:?}"),
1530 });
1531 };
1532 let Segment::Line(line) = segment else {
1533 return Err(Error {
1534 msg: format!("Segment is not a line: {segment:?}"),
1535 });
1536 };
1537 Ok(line)
1538 }
1539
1540 fn update_state_after_exec(&mut self, outcome: ExecOutcome) -> ExecOutcome {
1541 #[cfg(not(feature = "artifact-graph"))]
1542 return outcome;
1543 #[cfg(feature = "artifact-graph")]
1544 {
1545 let mut outcome = outcome;
1546 self.scene_graph.objects = std::mem::take(&mut outcome.scene_objects);
1547 outcome
1548 }
1549 }
1550
1551 fn mutate_ast(
1552 &mut self,
1553 ast: &mut ast::Node<ast::Program>,
1554 object_id: ObjectId,
1555 command: AstMutateCommand,
1556 ) -> api::Result<(SourceRange, AstMutateCommandReturn)> {
1557 let sketch_object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
1558 msg: format!("Object not found: {object_id:?}"),
1559 })?;
1560 match &sketch_object.source {
1561 SourceRef::Simple { range } => mutate_ast_node_by_source_range(ast, *range, command),
1562 SourceRef::BackTrace { .. } => Err(Error {
1563 msg: "BackTrace source refs not supported yet".to_owned(),
1564 }),
1565 }
1566 }
1567}
1568
1569fn expect_single_source_range(source_ref: &SourceRef) -> api::Result<SourceRange> {
1570 match source_ref {
1571 SourceRef::Simple { range } => Ok(*range),
1572 SourceRef::BackTrace { ranges } => {
1573 if ranges.len() != 1 {
1574 return Err(Error {
1575 msg: format!(
1576 "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
1577 ranges.len(),
1578 ),
1579 });
1580 }
1581 Ok(ranges[0])
1582 }
1583 }
1584}
1585
1586fn get_or_insert_ast_reference(
1593 ast: &mut ast::Node<ast::Program>,
1594 source_ref: &SourceRef,
1595 prefix: &str,
1596 property: Option<&str>,
1597) -> api::Result<ast::Expr> {
1598 let range = expect_single_source_range(source_ref)?;
1599 let command = AstMutateCommand::AddVariableDeclaration {
1600 prefix: prefix.to_owned(),
1601 };
1602 let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
1603 let AstMutateCommandReturn::Name(var_name) = ret else {
1604 return Err(Error {
1605 msg: "Expected variable name returned from AddVariableDeclaration".to_owned(),
1606 });
1607 };
1608 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
1609 let Some(property) = property else {
1610 return Ok(var_expr);
1612 };
1613
1614 Ok(ast::Expr::MemberExpression(Box::new(ast::Node::no_src(
1615 ast::MemberExpression {
1616 object: var_expr,
1617 property: ast::Expr::Name(Box::new(ast::Name::new(property))),
1618 computed: false,
1619 digest: None,
1620 },
1621 ))))
1622}
1623
1624fn mutate_ast_node_by_source_range(
1625 ast: &mut ast::Node<ast::Program>,
1626 source_range: SourceRange,
1627 command: AstMutateCommand,
1628) -> Result<(SourceRange, AstMutateCommandReturn), Error> {
1629 let mut context = AstMutateContext {
1630 source_range,
1631 command,
1632 defined_names_stack: Default::default(),
1633 };
1634 let control = dfs_mut(ast, &mut context);
1635 match control {
1636 ControlFlow::Continue(_) => Err(Error {
1637 msg: format!("Source range not found: {source_range:?}"),
1638 }),
1639 ControlFlow::Break(break_value) => break_value,
1640 }
1641}
1642
1643#[derive(Debug)]
1644struct AstMutateContext {
1645 source_range: SourceRange,
1646 command: AstMutateCommand,
1647 defined_names_stack: Vec<HashSet<String>>,
1648}
1649
1650#[derive(Debug)]
1651#[allow(clippy::large_enum_variant)]
1652enum AstMutateCommand {
1653 AddSketchBlockExprStmt {
1655 expr: ast::Expr,
1656 },
1657 AddVariableDeclaration {
1658 prefix: String,
1659 },
1660 EditPoint {
1661 at: ast::Expr,
1662 },
1663 EditLine {
1664 start: ast::Expr,
1665 end: ast::Expr,
1666 },
1667 #[cfg(feature = "artifact-graph")]
1668 EditVarInitialValue {
1669 value: Number,
1670 },
1671 DeleteNode,
1672}
1673
1674#[derive(Debug)]
1675enum AstMutateCommandReturn {
1676 None,
1677 Name(String),
1678}
1679
1680impl Visitor for AstMutateContext {
1681 type Break = Result<(SourceRange, AstMutateCommandReturn), Error>;
1682 type Continue = ();
1683
1684 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
1685 filter_and_process(self, node)
1686 }
1687
1688 fn finish(&mut self, node: NodeMut<'_>) {
1689 match &node {
1690 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
1691 self.defined_names_stack.pop();
1692 }
1693 _ => {}
1694 }
1695 }
1696}
1697fn filter_and_process(
1698 ctx: &mut AstMutateContext,
1699 node: NodeMut,
1700) -> TraversalReturn<Result<(SourceRange, AstMutateCommandReturn), Error>> {
1701 let Ok(node_range) = SourceRange::try_from(&node) else {
1702 return TraversalReturn::new_continue(());
1704 };
1705 if let NodeMut::VariableDeclaration(var_decl) = &node {
1710 let expr_range = SourceRange::from(&var_decl.declaration.init);
1711 if expr_range == ctx.source_range {
1712 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
1713 return TraversalReturn::new_break(Ok((
1716 node_range,
1717 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
1718 )));
1719 }
1720 if let AstMutateCommand::DeleteNode = &ctx.command {
1721 return TraversalReturn {
1724 mutate_body_item: MutateBodyItem::Delete,
1725 control_flow: ControlFlow::Break(Ok((ctx.source_range, AstMutateCommandReturn::None))),
1726 };
1727 }
1728 }
1729 }
1730
1731 if let NodeMut::Program(program) = &node {
1732 ctx.defined_names_stack.push(find_defined_names(*program));
1733 } else if let NodeMut::SketchBlock(block) = &node {
1734 ctx.defined_names_stack.push(find_defined_names(&block.body));
1735 }
1736
1737 if node_range != ctx.source_range {
1739 return TraversalReturn::new_continue(());
1740 }
1741 process(ctx, node).map_break(|result| result.map(|cmd_return| (ctx.source_range, cmd_return)))
1742}
1743
1744fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, Error>> {
1745 match &ctx.command {
1746 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
1747 if let NodeMut::SketchBlock(sketch_block) = node {
1748 sketch_block
1749 .body
1750 .items
1751 .push(ast::BodyItem::ExpressionStatement(ast::Node {
1752 inner: ast::ExpressionStatement {
1753 expression: expr.clone(),
1754 digest: None,
1755 },
1756 start: Default::default(),
1757 end: Default::default(),
1758 module_id: Default::default(),
1759 outer_attrs: Default::default(),
1760 pre_comments: Default::default(),
1761 comment_start: Default::default(),
1762 }));
1763 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
1764 }
1765 }
1766 AstMutateCommand::AddVariableDeclaration { prefix } => {
1767 if let NodeMut::VariableDeclaration(inner) = node {
1768 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
1769 }
1770 if let NodeMut::ExpressionStatement(expr_stmt) = node {
1771 let empty_defined_names = HashSet::new();
1772 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
1773 let Ok(name) = next_free_name(prefix, defined_names) else {
1774 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
1776 };
1777 let mutate_node =
1778 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
1779 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
1780 ast::ItemVisibility::Default,
1781 ast::VariableKind::Const,
1782 ))));
1783 return TraversalReturn {
1784 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
1785 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
1786 };
1787 }
1788 }
1789 AstMutateCommand::EditPoint { at } => {
1790 if let NodeMut::CallExpressionKw(call) = node {
1791 if call.callee.name.name != POINT_FN {
1792 return TraversalReturn::new_continue(());
1793 }
1794 for labeled_arg in &mut call.arguments {
1796 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
1797 labeled_arg.arg = at.clone();
1798 }
1799 }
1800 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
1801 }
1802 }
1803 AstMutateCommand::EditLine { start, end } => {
1804 if let NodeMut::CallExpressionKw(call) = node {
1805 if call.callee.name.name != LINE_FN {
1806 return TraversalReturn::new_continue(());
1807 }
1808 for labeled_arg in &mut call.arguments {
1810 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
1811 labeled_arg.arg = start.clone();
1812 }
1813 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
1814 labeled_arg.arg = end.clone();
1815 }
1816 }
1817 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
1818 }
1819 }
1820 #[cfg(feature = "artifact-graph")]
1821 AstMutateCommand::EditVarInitialValue { value } => {
1822 if let NodeMut::NumericLiteral(numeric_literal) = node {
1823 let Ok(literal) = to_source_number(*value) else {
1825 return TraversalReturn::new_break(Err(Error {
1826 msg: format!("Could not convert number to AST literal: {:?}", *value),
1827 }));
1828 };
1829 *numeric_literal = ast::Node::no_src(literal);
1830 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
1831 }
1832 }
1833 AstMutateCommand::DeleteNode => {
1834 return TraversalReturn {
1835 mutate_body_item: MutateBodyItem::Delete,
1836 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
1837 };
1838 }
1839 }
1840 TraversalReturn::new_continue(())
1841}
1842
1843struct FindSketchBlockSourceRange {
1844 target_before_mutation: SourceRange,
1846 found: Cell<Option<SourceRange>>,
1850}
1851
1852impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
1853 type Error = crate::front::Error;
1854
1855 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
1856 let Ok(node_range) = SourceRange::try_from(&node) else {
1857 return Ok(true);
1858 };
1859
1860 if let crate::walk::Node::SketchBlock(sketch_block) = node {
1861 if node_range.module_id() == self.target_before_mutation.module_id()
1862 && node_range.start() == self.target_before_mutation.start()
1863 && node_range.end() >= self.target_before_mutation.end()
1865 {
1866 self.found.set(sketch_block.body.items.last().map(SourceRange::from));
1867 return Ok(false);
1868 } else {
1869 return Ok(true);
1872 }
1873 }
1874
1875 for child in node.children().iter() {
1876 if !child.visit(*self)? {
1877 return Ok(false);
1878 }
1879 }
1880
1881 Ok(true)
1882 }
1883}
1884
1885fn find_sketch_block_added_item(
1893 ast: &ast::Node<ast::Program>,
1894 range_before_mutation: SourceRange,
1895) -> api::Result<SourceRange> {
1896 let find = FindSketchBlockSourceRange {
1897 target_before_mutation: range_before_mutation,
1898 found: Cell::new(None),
1899 };
1900 let node = crate::walk::Node::from(ast);
1901 node.visit(&find)?;
1902 find.found.into_inner().ok_or_else(|| api::Error {
1903 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?"),
1904 })
1905}
1906
1907fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
1908 ast.recast_top(&Default::default(), 0)
1910}
1911
1912fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
1913 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
1914 inner: ast::ArrayExpression {
1915 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
1916 non_code_meta: Default::default(),
1917 digest: None,
1918 },
1919 start: Default::default(),
1920 end: Default::default(),
1921 module_id: Default::default(),
1922 outer_attrs: Default::default(),
1923 pre_comments: Default::default(),
1924 comment_start: Default::default(),
1925 })))
1926}
1927
1928fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
1929 match expr {
1930 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
1931 inner: ast::Literal::from(to_source_number(*number)?),
1932 start: Default::default(),
1933 end: Default::default(),
1934 module_id: Default::default(),
1935 outer_attrs: Default::default(),
1936 pre_comments: Default::default(),
1937 comment_start: Default::default(),
1938 }))),
1939 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
1940 inner: ast::SketchVar {
1941 initial: Some(Box::new(ast::Node {
1942 inner: to_source_number(*number)?,
1943 start: Default::default(),
1944 end: Default::default(),
1945 module_id: Default::default(),
1946 outer_attrs: Default::default(),
1947 pre_comments: Default::default(),
1948 comment_start: Default::default(),
1949 })),
1950 digest: None,
1951 },
1952 start: Default::default(),
1953 end: Default::default(),
1954 module_id: Default::default(),
1955 outer_attrs: Default::default(),
1956 pre_comments: Default::default(),
1957 comment_start: Default::default(),
1958 }))),
1959 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
1960 }
1961}
1962
1963fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
1964 Ok(ast::NumericLiteral {
1965 value: number.value,
1966 suffix: number.units,
1967 raw: format_number_literal(number.value, number.units)?,
1968 digest: None,
1969 })
1970}
1971
1972fn ast_name_expr(name: String) -> ast::Expr {
1973 ast::Expr::Name(Box::new(ast_name(name)))
1974}
1975
1976fn ast_name(name: String) -> ast::Node<ast::Name> {
1977 ast::Node {
1978 inner: ast::Name {
1979 name: ast::Node {
1980 inner: ast::Identifier { name, digest: None },
1981 start: Default::default(),
1982 end: Default::default(),
1983 module_id: Default::default(),
1984 outer_attrs: Default::default(),
1985 pre_comments: Default::default(),
1986 comment_start: Default::default(),
1987 },
1988 path: Vec::new(),
1989 abs_path: false,
1990 digest: None,
1991 },
1992 start: Default::default(),
1993 end: Default::default(),
1994 module_id: Default::default(),
1995 outer_attrs: Default::default(),
1996 pre_comments: Default::default(),
1997 comment_start: Default::default(),
1998 }
1999}
2000
2001fn ast_sketch2_name(name: &str) -> ast::Name {
2002 ast::Name {
2003 name: ast::Node {
2004 inner: ast::Identifier {
2005 name: name.to_owned(),
2006 digest: None,
2007 },
2008 start: Default::default(),
2009 end: Default::default(),
2010 module_id: Default::default(),
2011 outer_attrs: Default::default(),
2012 pre_comments: Default::default(),
2013 comment_start: Default::default(),
2014 },
2015 path: vec![ast::Node::no_src(ast::Identifier {
2016 name: "sketch2".to_owned(),
2017 digest: None,
2018 })],
2019 abs_path: false,
2020 digest: None,
2021 }
2022}
2023
2024#[cfg(test)]
2025mod tests {
2026 use super::*;
2027 use crate::{
2028 engine::PlaneName,
2029 front::{Distance, Plane, Sketch},
2030 frontend::sketch::Vertical,
2031 pretty::NumericSuffix,
2032 };
2033
2034 #[tokio::test(flavor = "multi_thread")]
2035 async fn test_new_sketch_add_point_edit_point() {
2036 let program = Program::empty();
2037
2038 let mut frontend = FrontendState::new();
2039 frontend.program = program;
2040
2041 let mock_ctx = ExecutorContext::new_mock(None).await;
2042 let version = Version(0);
2043
2044 let sketch_args = SketchArgs {
2045 on: api::Plane::Default(PlaneName::Xy),
2046 };
2047 let (_src_delta, scene_delta, sketch_id) = frontend
2048 .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2049 .await
2050 .unwrap();
2051 assert_eq!(sketch_id, ObjectId(0));
2052 assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2053 let sketch_object = &scene_delta.new_graph.objects[0];
2054 assert_eq!(sketch_object.id, ObjectId(0));
2055 assert_eq!(
2056 sketch_object.kind,
2057 ObjectKind::Sketch(Sketch {
2058 args: SketchArgs {
2059 on: Plane::Default(PlaneName::Xy)
2060 },
2061 segments: vec![],
2062 constraints: vec![],
2063 is_underconstrained: None,
2064 })
2065 );
2066 assert_eq!(scene_delta.new_graph.objects.len(), 1);
2067
2068 let point_ctor = PointCtor {
2069 position: Point2d {
2070 x: Expr::Number(Number {
2071 value: 1.0,
2072 units: NumericSuffix::Inch,
2073 }),
2074 y: Expr::Number(Number {
2075 value: 2.0,
2076 units: NumericSuffix::Inch,
2077 }),
2078 },
2079 };
2080 let segment = SegmentCtor::Point(point_ctor);
2081 let (src_delta, scene_delta) = frontend
2082 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2083 .await
2084 .unwrap();
2085 assert_eq!(
2086 src_delta.text.as_str(),
2087 "@settings(experimentalFeatures = allow)
2088
2089sketch(on = XY) {
2090 sketch2::point(at = [1in, 2in])
2091}
2092"
2093 );
2094 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
2095 assert_eq!(scene_delta.new_graph.objects.len(), 2);
2096 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
2097 assert_eq!(scene_object.id.0, i);
2098 }
2099 assert_eq!(scene_delta.new_graph.objects.len(), 2);
2100
2101 let point_id = *scene_delta.new_objects.last().unwrap();
2102
2103 let point_ctor = PointCtor {
2104 position: Point2d {
2105 x: Expr::Number(Number {
2106 value: 3.0,
2107 units: NumericSuffix::Inch,
2108 }),
2109 y: Expr::Number(Number {
2110 value: 4.0,
2111 units: NumericSuffix::Inch,
2112 }),
2113 },
2114 };
2115 let segments = vec![ExistingSegmentCtor {
2116 id: point_id,
2117 ctor: SegmentCtor::Point(point_ctor),
2118 }];
2119 let (src_delta, scene_delta) = frontend
2120 .edit_segments(&mock_ctx, version, sketch_id, segments)
2121 .await
2122 .unwrap();
2123 assert_eq!(
2124 src_delta.text.as_str(),
2125 "@settings(experimentalFeatures = allow)
2126
2127sketch(on = XY) {
2128 sketch2::point(at = [3in, 4in])
2129}
2130"
2131 );
2132 assert_eq!(scene_delta.new_objects, vec![]);
2133 assert_eq!(scene_delta.new_graph.objects.len(), 2);
2134
2135 mock_ctx.close().await;
2136 }
2137
2138 #[tokio::test(flavor = "multi_thread")]
2139 async fn test_new_sketch_add_line_edit_line() {
2140 let program = Program::empty();
2141
2142 let mut frontend = FrontendState::new();
2143 frontend.program = program;
2144
2145 let mock_ctx = ExecutorContext::new_mock(None).await;
2146 let version = Version(0);
2147
2148 let sketch_args = SketchArgs {
2149 on: api::Plane::Default(PlaneName::Xy),
2150 };
2151 let (_src_delta, scene_delta, sketch_id) = frontend
2152 .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2153 .await
2154 .unwrap();
2155 assert_eq!(sketch_id, ObjectId(0));
2156 assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2157 let sketch_object = &scene_delta.new_graph.objects[0];
2158 assert_eq!(sketch_object.id, ObjectId(0));
2159 assert_eq!(
2160 sketch_object.kind,
2161 ObjectKind::Sketch(Sketch {
2162 args: SketchArgs {
2163 on: Plane::Default(PlaneName::Xy)
2164 },
2165 segments: vec![],
2166 constraints: vec![],
2167 is_underconstrained: None,
2168 })
2169 );
2170 assert_eq!(scene_delta.new_graph.objects.len(), 1);
2171
2172 let line_ctor = LineCtor {
2173 start: Point2d {
2174 x: Expr::Number(Number {
2175 value: 0.0,
2176 units: NumericSuffix::Mm,
2177 }),
2178 y: Expr::Number(Number {
2179 value: 0.0,
2180 units: NumericSuffix::Mm,
2181 }),
2182 },
2183 end: Point2d {
2184 x: Expr::Number(Number {
2185 value: 10.0,
2186 units: NumericSuffix::Mm,
2187 }),
2188 y: Expr::Number(Number {
2189 value: 10.0,
2190 units: NumericSuffix::Mm,
2191 }),
2192 },
2193 };
2194 let segment = SegmentCtor::Line(line_ctor);
2195 let (src_delta, scene_delta) = frontend
2196 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2197 .await
2198 .unwrap();
2199 assert_eq!(
2200 src_delta.text.as_str(),
2201 "@settings(experimentalFeatures = allow)
2202
2203sketch(on = XY) {
2204 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
2205}
2206"
2207 );
2208 assert_eq!(scene_delta.new_objects, vec![ObjectId(1), ObjectId(2), ObjectId(3)]);
2209 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
2210 assert_eq!(scene_object.id.0, i);
2211 }
2212 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2213
2214 let line = *scene_delta.new_objects.last().unwrap();
2216
2217 let line_ctor = LineCtor {
2218 start: Point2d {
2219 x: Expr::Number(Number {
2220 value: 1.0,
2221 units: NumericSuffix::Mm,
2222 }),
2223 y: Expr::Number(Number {
2224 value: 2.0,
2225 units: NumericSuffix::Mm,
2226 }),
2227 },
2228 end: Point2d {
2229 x: Expr::Number(Number {
2230 value: 13.0,
2231 units: NumericSuffix::Mm,
2232 }),
2233 y: Expr::Number(Number {
2234 value: 14.0,
2235 units: NumericSuffix::Mm,
2236 }),
2237 },
2238 };
2239 let segments = vec![ExistingSegmentCtor {
2240 id: line,
2241 ctor: SegmentCtor::Line(line_ctor),
2242 }];
2243 let (src_delta, scene_delta) = frontend
2244 .edit_segments(&mock_ctx, version, sketch_id, segments)
2245 .await
2246 .unwrap();
2247 assert_eq!(
2248 src_delta.text.as_str(),
2249 "@settings(experimentalFeatures = allow)
2250
2251sketch(on = XY) {
2252 sketch2::line(start = [1mm, 2mm], end = [13mm, 14mm])
2253}
2254"
2255 );
2256 assert_eq!(scene_delta.new_objects, vec![]);
2257 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2258
2259 mock_ctx.close().await;
2260 }
2261
2262 #[tokio::test(flavor = "multi_thread")]
2263 async fn test_add_line_when_sketch_block_uses_variable() {
2264 let initial_source = "@settings(experimentalFeatures = allow)
2265
2266s = sketch(on = XY) {}
2267";
2268
2269 let program = Program::parse(initial_source).unwrap().0.unwrap();
2270
2271 let mut frontend = FrontendState::new();
2272
2273 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2274 let mock_ctx = ExecutorContext::new_mock(None).await;
2275 let version = Version(0);
2276
2277 frontend.hack_set_program(&ctx, program).await.unwrap();
2278 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2279
2280 let line_ctor = LineCtor {
2281 start: Point2d {
2282 x: Expr::Number(Number {
2283 value: 0.0,
2284 units: NumericSuffix::Mm,
2285 }),
2286 y: Expr::Number(Number {
2287 value: 0.0,
2288 units: NumericSuffix::Mm,
2289 }),
2290 },
2291 end: Point2d {
2292 x: Expr::Number(Number {
2293 value: 10.0,
2294 units: NumericSuffix::Mm,
2295 }),
2296 y: Expr::Number(Number {
2297 value: 10.0,
2298 units: NumericSuffix::Mm,
2299 }),
2300 },
2301 };
2302 let segment = SegmentCtor::Line(line_ctor);
2303 let (src_delta, scene_delta) = frontend
2304 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2305 .await
2306 .unwrap();
2307 assert_eq!(
2308 src_delta.text.as_str(),
2309 "@settings(experimentalFeatures = allow)
2310
2311s = sketch(on = XY) {
2312 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
2313}
2314"
2315 );
2316 assert_eq!(scene_delta.new_objects, vec![ObjectId(1), ObjectId(2), ObjectId(3)]);
2317 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2318
2319 ctx.close().await;
2320 mock_ctx.close().await;
2321 }
2322
2323 #[tokio::test(flavor = "multi_thread")]
2324 async fn test_edit_line_when_editing_its_start_point() {
2325 let initial_source = "\
2326@settings(experimentalFeatures = allow)
2327
2328sketch(on = XY) {
2329 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
2330}
2331";
2332
2333 let program = Program::parse(initial_source).unwrap().0.unwrap();
2334
2335 let mut frontend = FrontendState::new();
2336
2337 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2338 let mock_ctx = ExecutorContext::new_mock(None).await;
2339 let version = Version(0);
2340
2341 frontend.hack_set_program(&ctx, program).await.unwrap();
2342 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2343
2344 let point_id = frontend.scene_graph.objects.get(1).unwrap().id;
2345
2346 let point_ctor = PointCtor {
2347 position: Point2d {
2348 x: Expr::Var(Number {
2349 value: 5.0,
2350 units: NumericSuffix::Inch,
2351 }),
2352 y: Expr::Var(Number {
2353 value: 6.0,
2354 units: NumericSuffix::Inch,
2355 }),
2356 },
2357 };
2358 let segments = vec![ExistingSegmentCtor {
2359 id: point_id,
2360 ctor: SegmentCtor::Point(point_ctor),
2361 }];
2362 let (src_delta, scene_delta) = frontend
2363 .edit_segments(&mock_ctx, version, sketch_id, segments)
2364 .await
2365 .unwrap();
2366 assert_eq!(
2367 src_delta.text.as_str(),
2368 "\
2369@settings(experimentalFeatures = allow)
2370
2371sketch(on = XY) {
2372 sketch2::line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
2373}
2374"
2375 );
2376 assert_eq!(scene_delta.new_objects, vec![]);
2377 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2378
2379 ctx.close().await;
2380 mock_ctx.close().await;
2381 }
2382
2383 #[tokio::test(flavor = "multi_thread")]
2384 async fn test_edit_line_when_editing_its_end_point() {
2385 let initial_source = "\
2386@settings(experimentalFeatures = allow)
2387
2388sketch(on = XY) {
2389 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
2390}
2391";
2392
2393 let program = Program::parse(initial_source).unwrap().0.unwrap();
2394
2395 let mut frontend = FrontendState::new();
2396
2397 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2398 let mock_ctx = ExecutorContext::new_mock(None).await;
2399 let version = Version(0);
2400
2401 frontend.hack_set_program(&ctx, program).await.unwrap();
2402 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2403
2404 let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
2405
2406 let point_ctor = PointCtor {
2407 position: Point2d {
2408 x: Expr::Var(Number {
2409 value: 5.0,
2410 units: NumericSuffix::Inch,
2411 }),
2412 y: Expr::Var(Number {
2413 value: 6.0,
2414 units: NumericSuffix::Inch,
2415 }),
2416 },
2417 };
2418 let segments = vec![ExistingSegmentCtor {
2419 id: point_id,
2420 ctor: SegmentCtor::Point(point_ctor),
2421 }];
2422 let (src_delta, scene_delta) = frontend
2423 .edit_segments(&mock_ctx, version, sketch_id, segments)
2424 .await
2425 .unwrap();
2426 assert_eq!(
2427 src_delta.text.as_str(),
2428 "\
2429@settings(experimentalFeatures = allow)
2430
2431sketch(on = XY) {
2432 sketch2::line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
2433}
2434"
2435 );
2436 assert_eq!(scene_delta.new_objects, vec![]);
2437 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2438
2439 ctx.close().await;
2440 mock_ctx.close().await;
2441 }
2442
2443 #[tokio::test(flavor = "multi_thread")]
2444 async fn test_edit_line_with_coincident_feedback() {
2445 let initial_source = "\
2446@settings(experimentalFeatures = allow)
2447
2448sketch(on = XY) {
2449 line1 = sketch2::line(start = [var 1, var 2], end = [var 1, var 2])
2450 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
2451 line1.start.at[0] == 0
2452 line1.start.at[1] == 0
2453 sketch2::coincident([line1.end, line2.start])
2454 sketch2::equalLength([line1, line2])
2455}
2456";
2457
2458 let program = Program::parse(initial_source).unwrap().0.unwrap();
2459
2460 let mut frontend = FrontendState::new();
2461
2462 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2463 let mock_ctx = ExecutorContext::new_mock(None).await;
2464 let version = Version(0);
2465
2466 frontend.hack_set_program(&ctx, program).await.unwrap();
2467 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2468 let line2_end_id = frontend.scene_graph.objects.get(5).unwrap().id;
2469
2470 let segments = vec![ExistingSegmentCtor {
2471 id: line2_end_id,
2472 ctor: SegmentCtor::Point(PointCtor {
2473 position: Point2d {
2474 x: Expr::Var(Number {
2475 value: 9.0,
2476 units: NumericSuffix::None,
2477 }),
2478 y: Expr::Var(Number {
2479 value: 10.0,
2480 units: NumericSuffix::None,
2481 }),
2482 },
2483 }),
2484 }];
2485 let (src_delta, scene_delta) = frontend
2486 .edit_segments(&mock_ctx, version, sketch_id, segments)
2487 .await
2488 .unwrap();
2489 assert_eq!(
2490 src_delta.text.as_str(),
2491 "\
2492@settings(experimentalFeatures = allow)
2493
2494sketch(on = XY) {
2495 line1 = sketch2::line(start = [var -0mm, var -0mm], end = [var 4.145mm, var 5.32mm])
2496 line2 = sketch2::line(start = [var 4.145mm, var 5.32mm], end = [var 9mm, var 10mm])
2497line1.start.at[0] == 0
2498line1.start.at[1] == 0
2499 sketch2::coincident([line1.end, line2.start])
2500 sketch2::equalLength([line1, line2])
2501}
2502"
2503 );
2504 assert_eq!(
2505 scene_delta.new_graph.objects.len(),
2506 9,
2507 "{:#?}",
2508 scene_delta.new_graph.objects
2509 );
2510
2511 ctx.close().await;
2512 mock_ctx.close().await;
2513 }
2514
2515 #[tokio::test(flavor = "multi_thread")]
2516 async fn test_delete_point_without_var() {
2517 let initial_source = "\
2518@settings(experimentalFeatures = allow)
2519
2520sketch(on = XY) {
2521 sketch2::point(at = [var 1, var 2])
2522 sketch2::point(at = [var 3, var 4])
2523 sketch2::point(at = [var 5, var 6])
2524}
2525";
2526
2527 let program = Program::parse(initial_source).unwrap().0.unwrap();
2528
2529 let mut frontend = FrontendState::new();
2530
2531 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2532 let mock_ctx = ExecutorContext::new_mock(None).await;
2533 let version = Version(0);
2534
2535 frontend.hack_set_program(&ctx, program).await.unwrap();
2536 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2537
2538 let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
2539
2540 let (src_delta, scene_delta) = frontend
2541 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
2542 .await
2543 .unwrap();
2544 assert_eq!(
2545 src_delta.text.as_str(),
2546 "\
2547@settings(experimentalFeatures = allow)
2548
2549sketch(on = XY) {
2550 sketch2::point(at = [var 1mm, var 2mm])
2551 sketch2::point(at = [var 5mm, var 6mm])
2552}
2553"
2554 );
2555 assert_eq!(scene_delta.new_objects, vec![]);
2556 assert_eq!(scene_delta.new_graph.objects.len(), 3);
2557
2558 ctx.close().await;
2559 mock_ctx.close().await;
2560 }
2561
2562 #[tokio::test(flavor = "multi_thread")]
2563 async fn test_delete_point_with_var() {
2564 let initial_source = "\
2565@settings(experimentalFeatures = allow)
2566
2567sketch(on = XY) {
2568 sketch2::point(at = [var 1, var 2])
2569 point1 = sketch2::point(at = [var 3, var 4])
2570 sketch2::point(at = [var 5, var 6])
2571}
2572";
2573
2574 let program = Program::parse(initial_source).unwrap().0.unwrap();
2575
2576 let mut frontend = FrontendState::new();
2577
2578 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2579 let mock_ctx = ExecutorContext::new_mock(None).await;
2580 let version = Version(0);
2581
2582 frontend.hack_set_program(&ctx, program).await.unwrap();
2583 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2584
2585 let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
2586
2587 let (src_delta, scene_delta) = frontend
2588 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
2589 .await
2590 .unwrap();
2591 assert_eq!(
2592 src_delta.text.as_str(),
2593 "\
2594@settings(experimentalFeatures = allow)
2595
2596sketch(on = XY) {
2597 sketch2::point(at = [var 1mm, var 2mm])
2598 sketch2::point(at = [var 5mm, var 6mm])
2599}
2600"
2601 );
2602 assert_eq!(scene_delta.new_objects, vec![]);
2603 assert_eq!(scene_delta.new_graph.objects.len(), 3);
2604
2605 ctx.close().await;
2606 mock_ctx.close().await;
2607 }
2608
2609 #[tokio::test(flavor = "multi_thread")]
2610 async fn test_delete_multiple_points() {
2611 let initial_source = "\
2612@settings(experimentalFeatures = allow)
2613
2614sketch(on = XY) {
2615 sketch2::point(at = [var 1, var 2])
2616 point1 = sketch2::point(at = [var 3, var 4])
2617 sketch2::point(at = [var 5, var 6])
2618}
2619";
2620
2621 let program = Program::parse(initial_source).unwrap().0.unwrap();
2622
2623 let mut frontend = FrontendState::new();
2624
2625 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2626 let mock_ctx = ExecutorContext::new_mock(None).await;
2627 let version = Version(0);
2628
2629 frontend.hack_set_program(&ctx, program).await.unwrap();
2630 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2631
2632 let point1_id = frontend.scene_graph.objects.get(1).unwrap().id;
2633 let point2_id = frontend.scene_graph.objects.get(2).unwrap().id;
2634
2635 let (src_delta, scene_delta) = frontend
2636 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
2637 .await
2638 .unwrap();
2639 assert_eq!(
2640 src_delta.text.as_str(),
2641 "\
2642@settings(experimentalFeatures = allow)
2643
2644sketch(on = XY) {
2645 sketch2::point(at = [var 5mm, var 6mm])
2646}
2647"
2648 );
2649 assert_eq!(scene_delta.new_objects, vec![]);
2650 assert_eq!(scene_delta.new_graph.objects.len(), 2);
2651
2652 ctx.close().await;
2653 mock_ctx.close().await;
2654 }
2655
2656 #[tokio::test(flavor = "multi_thread")]
2657 async fn test_delete_coincident_constraint() {
2658 let initial_source = "\
2659@settings(experimentalFeatures = allow)
2660
2661sketch(on = XY) {
2662 point1 = sketch2::point(at = [var 1, var 2])
2663 point2 = sketch2::point(at = [var 3, var 4])
2664 sketch2::coincident([point1, point2])
2665 sketch2::point(at = [var 5, var 6])
2666}
2667";
2668
2669 let program = Program::parse(initial_source).unwrap().0.unwrap();
2670
2671 let mut frontend = FrontendState::new();
2672
2673 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2674 let mock_ctx = ExecutorContext::new_mock(None).await;
2675 let version = Version(0);
2676
2677 frontend.hack_set_program(&ctx, program).await.unwrap();
2678 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2679
2680 let coincident_id = frontend.scene_graph.objects.get(3).unwrap().id;
2681
2682 let (src_delta, scene_delta) = frontend
2683 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
2684 .await
2685 .unwrap();
2686 assert_eq!(
2687 src_delta.text.as_str(),
2688 "\
2689@settings(experimentalFeatures = allow)
2690
2691sketch(on = XY) {
2692 point1 = sketch2::point(at = [var 1mm, var 2mm])
2693 point2 = sketch2::point(at = [var 3mm, var 4mm])
2694 sketch2::point(at = [var 5mm, var 6mm])
2695}
2696"
2697 );
2698 assert_eq!(scene_delta.new_objects, vec![]);
2699 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2700
2701 ctx.close().await;
2702 mock_ctx.close().await;
2703 }
2704
2705 #[tokio::test(flavor = "multi_thread")]
2706 async fn test_delete_line_cascades_to_coincident_constraint() {
2707 let initial_source = "\
2708@settings(experimentalFeatures = allow)
2709
2710sketch(on = XY) {
2711 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
2712 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
2713 sketch2::coincident([line1.end, line2.start])
2714}
2715";
2716
2717 let program = Program::parse(initial_source).unwrap().0.unwrap();
2718
2719 let mut frontend = FrontendState::new();
2720
2721 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2722 let mock_ctx = ExecutorContext::new_mock(None).await;
2723 let version = Version(0);
2724
2725 frontend.hack_set_program(&ctx, program).await.unwrap();
2726 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2727 let line_id = frontend.scene_graph.objects.get(6).unwrap().id;
2728
2729 let (src_delta, scene_delta) = frontend
2730 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
2731 .await
2732 .unwrap();
2733 assert_eq!(
2734 src_delta.text.as_str(),
2735 "\
2736@settings(experimentalFeatures = allow)
2737
2738sketch(on = XY) {
2739 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
2740}
2741"
2742 );
2743 assert_eq!(
2744 scene_delta.new_graph.objects.len(),
2745 4,
2746 "{:#?}",
2747 scene_delta.new_graph.objects
2748 );
2749
2750 ctx.close().await;
2751 mock_ctx.close().await;
2752 }
2753
2754 #[tokio::test(flavor = "multi_thread")]
2755 async fn test_delete_line_cascades_to_distance_constraint() {
2756 let initial_source = "\
2757@settings(experimentalFeatures = allow)
2758
2759sketch(on = XY) {
2760 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
2761 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
2762 sketch2::distance([line1.end, line2.start]) == 10mm
2763}
2764";
2765
2766 let program = Program::parse(initial_source).unwrap().0.unwrap();
2767
2768 let mut frontend = FrontendState::new();
2769
2770 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2771 let mock_ctx = ExecutorContext::new_mock(None).await;
2772 let version = Version(0);
2773
2774 frontend.hack_set_program(&ctx, program).await.unwrap();
2775 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2776 let line_id = frontend.scene_graph.objects.get(6).unwrap().id;
2777
2778 let (src_delta, scene_delta) = frontend
2779 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
2780 .await
2781 .unwrap();
2782 assert_eq!(
2783 src_delta.text.as_str(),
2784 "\
2785@settings(experimentalFeatures = allow)
2786
2787sketch(on = XY) {
2788 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
2789}
2790"
2791 );
2792 assert_eq!(
2793 scene_delta.new_graph.objects.len(),
2794 4,
2795 "{:#?}",
2796 scene_delta.new_graph.objects
2797 );
2798
2799 ctx.close().await;
2800 mock_ctx.close().await;
2801 }
2802
2803 #[tokio::test(flavor = "multi_thread")]
2804 async fn test_two_points_coincident() {
2805 let initial_source = "\
2806@settings(experimentalFeatures = allow)
2807
2808sketch(on = XY) {
2809 point1 = sketch2::point(at = [var 1, var 2])
2810 sketch2::point(at = [3, 4])
2811}
2812";
2813
2814 let program = Program::parse(initial_source).unwrap().0.unwrap();
2815
2816 let mut frontend = FrontendState::new();
2817
2818 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2819 let mock_ctx = ExecutorContext::new_mock(None).await;
2820 let version = Version(0);
2821
2822 frontend.hack_set_program(&ctx, program).await.unwrap();
2823 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2824 let point0_id = frontend.scene_graph.objects.get(1).unwrap().id;
2825 let point1_id = frontend.scene_graph.objects.get(2).unwrap().id;
2826
2827 let constraint = Constraint::Coincident(Coincident {
2828 points: vec![point0_id, point1_id],
2829 });
2830 let (src_delta, scene_delta) = frontend
2831 .add_constraint(&mock_ctx, version, sketch_id, constraint)
2832 .await
2833 .unwrap();
2834 assert_eq!(
2835 src_delta.text.as_str(),
2836 "\
2837@settings(experimentalFeatures = allow)
2838
2839sketch(on = XY) {
2840 point1 = sketch2::point(at = [var 1, var 2])
2841 point2 = sketch2::point(at = [3, 4])
2842 sketch2::coincident([point1, point2])
2843}
2844"
2845 );
2846 assert_eq!(
2847 scene_delta.new_graph.objects.len(),
2848 4,
2849 "{:#?}",
2850 scene_delta.new_graph.objects
2851 );
2852
2853 ctx.close().await;
2854 mock_ctx.close().await;
2855 }
2856
2857 #[tokio::test(flavor = "multi_thread")]
2858 async fn test_coincident_of_line_end_points() {
2859 let initial_source = "\
2860@settings(experimentalFeatures = allow)
2861
2862sketch(on = XY) {
2863 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
2864 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
2865}
2866";
2867
2868 let program = Program::parse(initial_source).unwrap().0.unwrap();
2869
2870 let mut frontend = FrontendState::new();
2871
2872 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2873 let mock_ctx = ExecutorContext::new_mock(None).await;
2874 let version = Version(0);
2875
2876 frontend.hack_set_program(&ctx, program).await.unwrap();
2877 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2878 let point0_id = frontend.scene_graph.objects.get(2).unwrap().id;
2879 let point1_id = frontend.scene_graph.objects.get(4).unwrap().id;
2880
2881 let constraint = Constraint::Coincident(Coincident {
2882 points: vec![point0_id, point1_id],
2883 });
2884 let (src_delta, scene_delta) = frontend
2885 .add_constraint(&mock_ctx, version, sketch_id, constraint)
2886 .await
2887 .unwrap();
2888 assert_eq!(
2889 src_delta.text.as_str(),
2890 "\
2891@settings(experimentalFeatures = allow)
2892
2893sketch(on = XY) {
2894 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
2895 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
2896 sketch2::coincident([line1.end, line2.start])
2897}
2898"
2899 );
2900 assert_eq!(
2901 scene_delta.new_graph.objects.len(),
2902 8,
2903 "{:#?}",
2904 scene_delta.new_graph.objects
2905 );
2906
2907 ctx.close().await;
2908 mock_ctx.close().await;
2909 }
2910
2911 #[tokio::test(flavor = "multi_thread")]
2912 async fn test_distance_two_points() {
2913 let initial_source = "\
2914@settings(experimentalFeatures = allow)
2915
2916sketch(on = XY) {
2917 sketch2::point(at = [var 1, var 2])
2918 sketch2::point(at = [var 3, var 4])
2919}
2920";
2921
2922 let program = Program::parse(initial_source).unwrap().0.unwrap();
2923
2924 let mut frontend = FrontendState::new();
2925
2926 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2927 let mock_ctx = ExecutorContext::new_mock(None).await;
2928 let version = Version(0);
2929
2930 frontend.hack_set_program(&ctx, program).await.unwrap();
2931 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2932 let point0_id = frontend.scene_graph.objects.get(1).unwrap().id;
2933 let point1_id = frontend.scene_graph.objects.get(2).unwrap().id;
2934
2935 let constraint = Constraint::Distance(Distance {
2936 points: vec![point0_id, point1_id],
2937 distance: Number {
2938 value: 2.0,
2939 units: NumericSuffix::Mm,
2940 },
2941 });
2942 let (src_delta, scene_delta) = frontend
2943 .add_constraint(&mock_ctx, version, sketch_id, constraint)
2944 .await
2945 .unwrap();
2946 assert_eq!(
2947 src_delta.text.as_str(),
2948 "\
2950@settings(experimentalFeatures = allow)
2951
2952sketch(on = XY) {
2953 point1 = sketch2::point(at = [var 1, var 2])
2954 point2 = sketch2::point(at = [var 3, var 4])
2955sketch2::distance([point1, point2]) == 2mm
2956}
2957"
2958 );
2959 assert_eq!(
2960 scene_delta.new_graph.objects.len(),
2961 4,
2962 "{:#?}",
2963 scene_delta.new_graph.objects
2964 );
2965
2966 ctx.close().await;
2967 mock_ctx.close().await;
2968 }
2969
2970 #[tokio::test(flavor = "multi_thread")]
2971 async fn test_line_horizontal() {
2972 let initial_source = "\
2973@settings(experimentalFeatures = allow)
2974
2975sketch(on = XY) {
2976 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
2977}
2978";
2979
2980 let program = Program::parse(initial_source).unwrap().0.unwrap();
2981
2982 let mut frontend = FrontendState::new();
2983
2984 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2985 let mock_ctx = ExecutorContext::new_mock(None).await;
2986 let version = Version(0);
2987
2988 frontend.hack_set_program(&ctx, program).await.unwrap();
2989 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2990 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
2991
2992 let constraint = Constraint::Horizontal(Horizontal { line: line1_id });
2993 let (src_delta, scene_delta) = frontend
2994 .add_constraint(&mock_ctx, version, sketch_id, constraint)
2995 .await
2996 .unwrap();
2997 assert_eq!(
2998 src_delta.text.as_str(),
2999 "\
3000@settings(experimentalFeatures = allow)
3001
3002sketch(on = XY) {
3003 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3004 sketch2::horizontal(line1)
3005}
3006"
3007 );
3008 assert_eq!(
3009 scene_delta.new_graph.objects.len(),
3010 5,
3011 "{:#?}",
3012 scene_delta.new_graph.objects
3013 );
3014
3015 ctx.close().await;
3016 mock_ctx.close().await;
3017 }
3018
3019 #[tokio::test(flavor = "multi_thread")]
3020 async fn test_line_vertical() {
3021 let initial_source = "\
3022@settings(experimentalFeatures = allow)
3023
3024sketch(on = XY) {
3025 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3026}
3027";
3028
3029 let program = Program::parse(initial_source).unwrap().0.unwrap();
3030
3031 let mut frontend = FrontendState::new();
3032
3033 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3034 let mock_ctx = ExecutorContext::new_mock(None).await;
3035 let version = Version(0);
3036
3037 frontend.hack_set_program(&ctx, program).await.unwrap();
3038 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3039 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3040
3041 let constraint = Constraint::Vertical(Vertical { line: line1_id });
3042 let (src_delta, scene_delta) = frontend
3043 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3044 .await
3045 .unwrap();
3046 assert_eq!(
3047 src_delta.text.as_str(),
3048 "\
3049@settings(experimentalFeatures = allow)
3050
3051sketch(on = XY) {
3052 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3053 sketch2::vertical(line1)
3054}
3055"
3056 );
3057 assert_eq!(
3058 scene_delta.new_graph.objects.len(),
3059 5,
3060 "{:#?}",
3061 scene_delta.new_graph.objects
3062 );
3063
3064 ctx.close().await;
3065 mock_ctx.close().await;
3066 }
3067
3068 #[tokio::test(flavor = "multi_thread")]
3069 async fn test_lines_equal_length() {
3070 let initial_source = "\
3071@settings(experimentalFeatures = allow)
3072
3073sketch(on = XY) {
3074 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3075 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3076}
3077";
3078
3079 let program = Program::parse(initial_source).unwrap().0.unwrap();
3080
3081 let mut frontend = FrontendState::new();
3082
3083 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3084 let mock_ctx = ExecutorContext::new_mock(None).await;
3085 let version = Version(0);
3086
3087 frontend.hack_set_program(&ctx, program).await.unwrap();
3088 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3089 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3090 let line2_id = frontend.scene_graph.objects.get(6).unwrap().id;
3091
3092 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
3093 lines: vec![line1_id, line2_id],
3094 });
3095 let (src_delta, scene_delta) = frontend
3096 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3097 .await
3098 .unwrap();
3099 assert_eq!(
3100 src_delta.text.as_str(),
3101 "\
3102@settings(experimentalFeatures = allow)
3103
3104sketch(on = XY) {
3105 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3106 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3107 sketch2::equalLength([line1, line2])
3108}
3109"
3110 );
3111 assert_eq!(
3112 scene_delta.new_graph.objects.len(),
3113 8,
3114 "{:#?}",
3115 scene_delta.new_graph.objects
3116 );
3117
3118 ctx.close().await;
3119 mock_ctx.close().await;
3120 }
3121
3122 #[tokio::test(flavor = "multi_thread")]
3123 async fn test_lines_parallel() {
3124 let initial_source = "\
3125@settings(experimentalFeatures = allow)
3126
3127sketch(on = XY) {
3128 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3129 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3130}
3131";
3132
3133 let program = Program::parse(initial_source).unwrap().0.unwrap();
3134
3135 let mut frontend = FrontendState::new();
3136
3137 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3138 let mock_ctx = ExecutorContext::new_mock(None).await;
3139 let version = Version(0);
3140
3141 frontend.hack_set_program(&ctx, program).await.unwrap();
3142 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3143 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3144 let line2_id = frontend.scene_graph.objects.get(6).unwrap().id;
3145
3146 let constraint = Constraint::Parallel(Parallel {
3147 lines: vec![line1_id, line2_id],
3148 });
3149 let (src_delta, scene_delta) = frontend
3150 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3151 .await
3152 .unwrap();
3153 assert_eq!(
3154 src_delta.text.as_str(),
3155 "\
3156@settings(experimentalFeatures = allow)
3157
3158sketch(on = XY) {
3159 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3160 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3161 sketch2::parallel([line1, line2])
3162}
3163"
3164 );
3165 assert_eq!(
3166 scene_delta.new_graph.objects.len(),
3167 8,
3168 "{:#?}",
3169 scene_delta.new_graph.objects
3170 );
3171
3172 ctx.close().await;
3173 mock_ctx.close().await;
3174 }
3175}