1use std::{cell::Cell, collections::HashSet, ops::ControlFlow};
2
3use kcl_error::SourceRange;
4
5use crate::{
6 ExecOutcome, ExecutorContext, Program,
7 collections::AhashIndexSet,
8 exec::WarningLevel,
9 execution::MockConfig,
10 fmt::format_number_literal,
11 front::{ArcCtor, Distance, LinesEqualLength, Parallel, Perpendicular, PointCtor},
12 frontend::{
13 api::{
14 Error, Expr, FileId, Number, ObjectId, ObjectKind, ProjectId, SceneGraph, SceneGraphDelta, SourceDelta,
15 SourceRef, Version,
16 },
17 modify::{find_defined_names, next_free_name},
18 sketch::{
19 Coincident, Constraint, ExistingSegmentCtor, Horizontal, LineCtor, Point2d, Segment, SegmentCtor,
20 SketchApi, SketchArgs, Vertical,
21 },
22 traverse::{MutateBodyItem, TraversalReturn, Visitor, dfs_mut},
23 },
24 parsing::ast::types as ast,
25 std::constraints::LinesAtAngleKind,
26 walk::{NodeMut, Visitable},
27};
28
29pub(crate) mod api;
30pub(crate) mod modify;
31pub(crate) mod sketch;
32mod traverse;
33
34const POINT_FN: &str = "point";
35const POINT_AT_PARAM: &str = "at";
36const LINE_FN: &str = "line";
37const LINE_START_PARAM: &str = "start";
38const LINE_END_PARAM: &str = "end";
39const ARC_FN: &str = "arc";
40const ARC_START_PARAM: &str = "start";
41const ARC_END_PARAM: &str = "end";
42const ARC_CENTER_PARAM: &str = "center";
43
44const COINCIDENT_FN: &str = "coincident";
45const DISTANCE_FN: &str = "distance";
46const EQUAL_LENGTH_FN: &str = "equalLength";
47const HORIZONTAL_FN: &str = "horizontal";
48const VERTICAL_FN: &str = "vertical";
49
50const LINE_PROPERTY_START: &str = "start";
51const LINE_PROPERTY_END: &str = "end";
52
53const ARC_PROPERTY_START: &str = "start";
54const ARC_PROPERTY_END: &str = "end";
55const ARC_PROPERTY_CENTER: &str = "center";
56
57#[derive(Debug, Clone, Copy)]
58enum EditDeleteKind {
59 Edit,
60 DeleteSketch,
61 DeleteNonSketch,
62}
63
64impl EditDeleteKind {
65 fn is_delete(&self) -> bool {
67 match self {
68 EditDeleteKind::Edit => false,
69 EditDeleteKind::DeleteSketch | EditDeleteKind::DeleteNonSketch => true,
70 }
71 }
72
73 fn to_change_kind(self) -> ChangeKind {
74 match self {
75 EditDeleteKind::Edit => ChangeKind::Edit,
76 EditDeleteKind::DeleteSketch | EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy)]
82enum ChangeKind {
83 Add,
84 Edit,
85 Delete,
86 None,
87}
88
89#[derive(Debug, Clone)]
90pub struct FrontendState {
91 program: Program,
92 scene_graph: SceneGraph,
93}
94
95impl Default for FrontendState {
96 fn default() -> Self {
97 Self::new()
98 }
99}
100
101impl FrontendState {
102 pub fn new() -> Self {
103 Self {
104 program: Program::empty(),
105 scene_graph: SceneGraph {
106 project: ProjectId(0),
107 file: FileId(0),
108 version: Version(0),
109 objects: Default::default(),
110 settings: Default::default(),
111 sketch_mode: Default::default(),
112 },
113 }
114 }
115}
116
117impl SketchApi for FrontendState {
118 async fn execute_mock(
119 &mut self,
120 ctx: &ExecutorContext,
121 _version: Version,
122 sketch: ObjectId,
123 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
124 let mut truncated_program = self.program.clone();
125 self.exit_after_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
126
127 let outcome = ctx
129 .run_mock(&truncated_program, &MockConfig::default())
130 .await
131 .map_err(|err| Error {
132 msg: err.error.message().to_owned(),
133 })?;
134 let new_source = source_from_ast(&self.program.ast);
135 let src_delta = SourceDelta { text: new_source };
136 let outcome = self.update_state_after_exec(outcome);
137 let scene_graph_delta = SceneGraphDelta {
138 new_graph: self.scene_graph.clone(),
139 new_objects: Default::default(),
140 invalidates_ids: false,
141 exec_outcome: outcome,
142 };
143 Ok((src_delta, scene_graph_delta))
144 }
145
146 async fn new_sketch(
147 &mut self,
148 ctx: &ExecutorContext,
149 _project: ProjectId,
150 _file: FileId,
151 _version: Version,
152 args: SketchArgs,
153 ) -> api::Result<(SourceDelta, SceneGraphDelta, ObjectId)> {
154 let plane_ast = match &args.on {
158 api::Plane::Object(_) => todo!(),
160 api::Plane::Default(plane) => ast_name_expr(plane.to_string()),
161 };
162 let sketch_ast = ast::SketchBlock {
163 arguments: vec![ast::LabeledArg {
164 label: Some(ast::Identifier::new("on")),
165 arg: plane_ast,
166 }],
167 body: Default::default(),
168 is_being_edited: false,
169 non_code_meta: Default::default(),
170 digest: None,
171 };
172 let mut new_ast = self.program.ast.clone();
173 new_ast.set_experimental_features(Some(WarningLevel::Allow));
176 new_ast.body.push(ast::BodyItem::ExpressionStatement(ast::Node {
178 inner: ast::ExpressionStatement {
179 expression: ast::Expr::SketchBlock(Box::new(ast::Node {
180 inner: sketch_ast,
181 start: Default::default(),
182 end: Default::default(),
183 module_id: Default::default(),
184 outer_attrs: Default::default(),
185 pre_comments: Default::default(),
186 comment_start: Default::default(),
187 })),
188 digest: None,
189 },
190 start: Default::default(),
191 end: Default::default(),
192 module_id: Default::default(),
193 outer_attrs: Default::default(),
194 pre_comments: Default::default(),
195 comment_start: Default::default(),
196 }));
197 let new_source = source_from_ast(&new_ast);
199 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
201 if !errors.is_empty() {
202 return Err(Error {
203 msg: format!("Error parsing KCL source after adding sketch: {errors:?}"),
204 });
205 }
206 let Some(new_program) = new_program else {
207 return Err(Error {
208 msg: "No AST produced after adding sketch".to_owned(),
209 });
210 };
211
212 let sketch_source_range = new_program
213 .ast
214 .body
215 .last()
216 .map(SourceRange::from)
217 .ok_or_else(|| Error {
218 msg: "No AST body items after adding sketch".to_owned(),
219 })?;
220 #[cfg(not(feature = "artifact-graph"))]
221 let _ = sketch_source_range;
222
223 self.program = new_program.clone();
225
226 let outcome = ctx
231 .run_mock(&new_program, &MockConfig::default())
232 .await
233 .map_err(|err| {
234 Error {
237 msg: err.error.message().to_owned(),
238 }
239 })?;
240
241 #[cfg(not(feature = "artifact-graph"))]
242 let sketch_id = ObjectId(0);
243 #[cfg(feature = "artifact-graph")]
244 let sketch_id = outcome
245 .source_range_to_object
246 .get(&sketch_source_range)
247 .copied()
248 .ok_or_else(|| Error {
249 msg: format!("Source range of sketch not found: {sketch_source_range:?}"),
250 })?;
251 let src_delta = SourceDelta { text: new_source };
252 self.scene_graph.sketch_mode = Some(sketch_id);
254 let outcome = self.update_state_after_exec(outcome);
255 let scene_graph_delta = SceneGraphDelta {
256 new_graph: self.scene_graph.clone(),
257 invalidates_ids: false,
258 new_objects: vec![sketch_id],
259 exec_outcome: outcome,
260 };
261 Ok((src_delta, scene_graph_delta, sketch_id))
262 }
263
264 async fn edit_sketch(
265 &mut self,
266 ctx: &ExecutorContext,
267 _project: ProjectId,
268 _file: FileId,
269 _version: Version,
270 sketch: ObjectId,
271 ) -> api::Result<SceneGraphDelta> {
272 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| Error {
276 msg: format!("Sketch not found: {sketch:?}"),
277 })?;
278 let ObjectKind::Sketch(_) = &sketch_object.kind else {
279 return Err(Error {
280 msg: format!("Object is not a sketch: {sketch_object:?}"),
281 });
282 };
283
284 self.scene_graph.sketch_mode = Some(sketch);
286
287 let mut truncated_program = self.program.clone();
289 self.exit_after_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
290
291 let mock_config = MockConfig {
294 freedom_analysis: true,
295 ..Default::default()
296 };
297 let outcome = ctx.run_mock(&truncated_program, &mock_config).await.map_err(|err| {
298 Error {
301 msg: err.error.message().to_owned(),
302 }
303 })?;
304
305 let outcome = self.update_state_after_exec(outcome);
306 let scene_graph_delta = SceneGraphDelta {
307 new_graph: self.scene_graph.clone(),
308 invalidates_ids: false,
309 new_objects: Vec::new(),
310 exec_outcome: outcome,
311 };
312 Ok(scene_graph_delta)
313 }
314
315 async fn exit_sketch(
316 &mut self,
317 ctx: &ExecutorContext,
318 _version: Version,
319 sketch: ObjectId,
320 ) -> api::Result<SceneGraph> {
321 #[cfg(not(target_arch = "wasm32"))]
323 let _ = sketch;
324 #[cfg(target_arch = "wasm32")]
325 if self.scene_graph.sketch_mode != Some(sketch) {
326 web_sys::console::warn_1(
327 &format!(
328 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
329 &self.scene_graph.sketch_mode
330 )
331 .into(),
332 );
333 }
334 self.scene_graph.sketch_mode = None;
335
336 let outcome = ctx.run_with_caching(self.program.clone()).await.map_err(|err| {
338 Error {
341 msg: err.error.message().to_owned(),
342 }
343 })?;
344
345 self.update_state_after_exec(outcome);
346
347 Ok(self.scene_graph.clone())
348 }
349
350 async fn delete_sketch(
351 &mut self,
352 ctx: &ExecutorContext,
353 _version: Version,
354 sketch: ObjectId,
355 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
356 let mut new_ast = self.program.ast.clone();
359
360 let sketch_id = sketch;
362 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
363 msg: format!("Sketch not found: {sketch:?}"),
364 })?;
365 let ObjectKind::Sketch(_) = &sketch_object.kind else {
366 return Err(Error {
367 msg: format!("Object is not a sketch: {sketch_object:?}"),
368 });
369 };
370
371 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)?;
373
374 self.execute_after_edit(
375 ctx,
376 sketch,
377 Default::default(),
378 EditDeleteKind::DeleteSketch,
379 &mut new_ast,
380 )
381 .await
382 }
383
384 async fn add_segment(
385 &mut self,
386 ctx: &ExecutorContext,
387 _version: Version,
388 sketch: ObjectId,
389 segment: SegmentCtor,
390 _label: Option<String>,
391 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
392 match segment {
394 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
395 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
396 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
397 _ => Err(Error {
398 msg: format!("segment ctor not implemented yet: {segment:?}"),
399 }),
400 }
401 }
402
403 async fn edit_segments(
404 &mut self,
405 ctx: &ExecutorContext,
406 _version: Version,
407 sketch: ObjectId,
408 segments: Vec<ExistingSegmentCtor>,
409 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
410 let mut new_ast = self.program.ast.clone();
412 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
413 for segment in segments {
414 segment_ids_edited.insert(segment.id);
415 match segment.ctor {
416 SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
417 SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
418 SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment.id, ctor)?,
419 _ => {
420 return Err(Error {
421 msg: format!("segment ctor not implemented yet: {segment:?}"),
422 });
423 }
424 }
425 }
426 self.execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
427 .await
428 }
429
430 async fn delete_objects(
431 &mut self,
432 ctx: &ExecutorContext,
433 _version: Version,
434 sketch: ObjectId,
435 constraint_ids: Vec<ObjectId>,
436 segment_ids: Vec<ObjectId>,
437 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
438 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
442 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
443 self.add_dependent_constraints_to_delete(sketch, &segment_ids_set, &mut constraint_ids_set)?;
446
447 let mut new_ast = self.program.ast.clone();
448 for constraint_id in constraint_ids_set {
449 self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
450 }
451 for segment_id in segment_ids_set {
452 self.delete_segment(&mut new_ast, sketch, segment_id)?;
453 }
454 self.execute_after_edit(
455 ctx,
456 sketch,
457 Default::default(),
458 EditDeleteKind::DeleteNonSketch,
459 &mut new_ast,
460 )
461 .await
462 }
463
464 async fn add_constraint(
465 &mut self,
466 ctx: &ExecutorContext,
467 _version: Version,
468 sketch: ObjectId,
469 constraint: Constraint,
470 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
471 let mut new_ast = self.program.ast.clone();
474 let sketch_block_range = match constraint {
475 Constraint::Coincident(coincident) => self.add_coincident(sketch, coincident, &mut new_ast).await?,
476 Constraint::Distance(distance) => self.add_distance(sketch, distance, &mut new_ast).await?,
477 Constraint::Horizontal(horizontal) => self.add_horizontal(sketch, horizontal, &mut new_ast).await?,
478 Constraint::LinesEqualLength(lines_equal_length) => {
479 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
480 .await?
481 }
482 Constraint::Parallel(parallel) => self.add_parallel(sketch, parallel, &mut new_ast).await?,
483 Constraint::Perpendicular(perpendicular) => {
484 self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?
485 }
486 Constraint::Vertical(vertical) => self.add_vertical(sketch, vertical, &mut new_ast).await?,
487 };
488 self.execute_after_add_constraint(ctx, sketch, sketch_block_range, &mut new_ast)
489 .await
490 }
491
492 async fn chain_segment(
493 &mut self,
494 ctx: &ExecutorContext,
495 version: Version,
496 sketch: ObjectId,
497 previous_segment_end_point_id: ObjectId,
498 segment: SegmentCtor,
499 _label: Option<String>,
500 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
501 let SegmentCtor::Line(line_ctor) = segment else {
505 return Err(Error {
506 msg: format!("chain_segment currently only supports Line segments, got: {segment:?}"),
507 });
508 };
509
510 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
512
513 let new_line_id = first_scene_delta
516 .new_objects
517 .iter()
518 .find(|&obj_id| {
519 let obj = self.scene_graph.objects.get(obj_id.0);
520 if let Some(obj) = obj {
521 matches!(
522 &obj.kind,
523 ObjectKind::Segment {
524 segment: Segment::Line(_)
525 }
526 )
527 } else {
528 false
529 }
530 })
531 .ok_or_else(|| Error {
532 msg: "Failed to find new line segment in scene graph".to_string(),
533 })?;
534
535 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| Error {
536 msg: format!("New line object not found: {new_line_id:?}"),
537 })?;
538
539 let ObjectKind::Segment {
540 segment: new_line_segment,
541 } = &new_line_obj.kind
542 else {
543 return Err(Error {
544 msg: format!("Object is not a segment: {new_line_obj:?}"),
545 });
546 };
547
548 let Segment::Line(new_line) = new_line_segment else {
549 return Err(Error {
550 msg: format!("Segment is not a line: {new_line_segment:?}"),
551 });
552 };
553
554 let new_line_start_point_id = new_line.start;
555
556 let coincident = Coincident {
558 segments: vec![previous_segment_end_point_id, new_line_start_point_id],
559 };
560
561 let (final_src_delta, final_scene_delta) = self
562 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
563 .await?;
564
565 let mut combined_new_objects = first_scene_delta.new_objects.clone();
568 combined_new_objects.extend(final_scene_delta.new_objects);
569
570 let scene_graph_delta = SceneGraphDelta {
571 new_graph: self.scene_graph.clone(),
572 invalidates_ids: false,
573 new_objects: combined_new_objects,
574 exec_outcome: final_scene_delta.exec_outcome,
575 };
576
577 Ok((final_src_delta, scene_graph_delta))
578 }
579
580 async fn edit_constraint(
581 &mut self,
582 _ctx: &ExecutorContext,
583 _version: Version,
584 _sketch: ObjectId,
585 _constraint_id: ObjectId,
586 _constraint: Constraint,
587 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
588 todo!()
589 }
590}
591
592impl FrontendState {
593 pub async fn hack_set_program(
594 &mut self,
595 ctx: &ExecutorContext,
596 program: Program,
597 ) -> api::Result<(SceneGraph, ExecOutcome)> {
598 self.program = program.clone();
599
600 let outcome = ctx.run_with_caching(program).await.map_err(|err| {
603 Error {
606 msg: err.error.message().to_owned(),
607 }
608 })?;
609
610 let outcome = self.update_state_after_exec(outcome);
611
612 Ok((self.scene_graph.clone(), outcome))
613 }
614
615 async fn add_point(
616 &mut self,
617 ctx: &ExecutorContext,
618 sketch: ObjectId,
619 ctor: PointCtor,
620 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
621 let at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
623 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
624 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
625 unlabeled: None,
626 arguments: vec![ast::LabeledArg {
627 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
628 arg: at_ast,
629 }],
630 digest: None,
631 non_code_meta: Default::default(),
632 })));
633
634 let sketch_id = sketch;
636 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
637 #[cfg(target_arch = "wasm32")]
638 web_sys::console::error_1(
639 &format!(
640 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
641 &self.scene_graph.objects
642 )
643 .into(),
644 );
645 Error {
646 msg: format!("Sketch not found: {sketch:?}"),
647 }
648 })?;
649 let ObjectKind::Sketch(_) = &sketch_object.kind else {
650 return Err(Error {
651 msg: format!("Object is not a sketch: {sketch_object:?}"),
652 });
653 };
654 let mut new_ast = self.program.ast.clone();
656 let (sketch_block_range, _) = self.mutate_ast(
657 &mut new_ast,
658 sketch_id,
659 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
660 )?;
661 let new_source = source_from_ast(&new_ast);
663 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
665 if !errors.is_empty() {
666 return Err(Error {
667 msg: format!("Error parsing KCL source after adding point: {errors:?}"),
668 });
669 }
670 let Some(new_program) = new_program else {
671 return Err(Error {
672 msg: "No AST produced after adding point".to_string(),
673 });
674 };
675
676 let point_source_range =
677 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
678 msg: format!("Source range of point not found in sketch block: {sketch_block_range:?}; {err:?}"),
679 })?;
680 #[cfg(not(feature = "artifact-graph"))]
681 let _ = point_source_range;
682
683 self.program = new_program.clone();
685
686 let mut truncated_program = new_program;
688 self.exit_after_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
689
690 let outcome = ctx
692 .run_mock(&truncated_program, &MockConfig::default())
693 .await
694 .map_err(|err| {
695 Error {
698 msg: err.error.message().to_owned(),
699 }
700 })?;
701
702 #[cfg(not(feature = "artifact-graph"))]
703 let new_object_ids = Vec::new();
704 #[cfg(feature = "artifact-graph")]
705 let new_object_ids = {
706 let segment_id = outcome
707 .source_range_to_object
708 .get(&point_source_range)
709 .copied()
710 .ok_or_else(|| Error {
711 msg: format!("Source range of point not found: {point_source_range:?}"),
712 })?;
713 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
714 msg: format!("Segment not found: {segment_id:?}"),
715 })?;
716 let ObjectKind::Segment { segment } = &segment_object.kind else {
717 return Err(Error {
718 msg: format!("Object is not a segment: {segment_object:?}"),
719 });
720 };
721 let Segment::Point(_) = segment else {
722 return Err(Error {
723 msg: format!("Segment is not a point: {segment:?}"),
724 });
725 };
726 vec![segment_id]
727 };
728 let src_delta = SourceDelta { text: new_source };
729 let outcome = self.update_state_after_exec(outcome);
730 let scene_graph_delta = SceneGraphDelta {
731 new_graph: self.scene_graph.clone(),
732 invalidates_ids: false,
733 new_objects: new_object_ids,
734 exec_outcome: outcome,
735 };
736 Ok((src_delta, scene_graph_delta))
737 }
738
739 async fn add_line(
740 &mut self,
741 ctx: &ExecutorContext,
742 sketch: ObjectId,
743 ctor: LineCtor,
744 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
745 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
747 let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
748 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
749 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
750 unlabeled: None,
751 arguments: vec![
752 ast::LabeledArg {
753 label: Some(ast::Identifier::new(LINE_START_PARAM)),
754 arg: start_ast,
755 },
756 ast::LabeledArg {
757 label: Some(ast::Identifier::new(LINE_END_PARAM)),
758 arg: end_ast,
759 },
760 ],
761 digest: None,
762 non_code_meta: Default::default(),
763 })));
764
765 let sketch_id = sketch;
767 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
768 msg: format!("Sketch not found: {sketch:?}"),
769 })?;
770 let ObjectKind::Sketch(_) = &sketch_object.kind else {
771 return Err(Error {
772 msg: format!("Object is not a sketch: {sketch_object:?}"),
773 });
774 };
775 let mut new_ast = self.program.ast.clone();
777 let (sketch_block_range, _) = self.mutate_ast(
778 &mut new_ast,
779 sketch_id,
780 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
781 )?;
782 let new_source = source_from_ast(&new_ast);
784 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
786 if !errors.is_empty() {
787 return Err(Error {
788 msg: format!("Error parsing KCL source after adding line: {errors:?}"),
789 });
790 }
791 let Some(new_program) = new_program else {
792 return Err(Error {
793 msg: "No AST produced after adding line".to_string(),
794 });
795 };
796 let line_source_range =
797 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
798 msg: format!("Source range of line not found in sketch block: {sketch_block_range:?}; {err:?}"),
799 })?;
800 #[cfg(not(feature = "artifact-graph"))]
801 let _ = line_source_range;
802
803 self.program = new_program.clone();
805
806 let mut truncated_program = new_program;
808 self.exit_after_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
809
810 let outcome = ctx
812 .run_mock(&truncated_program, &MockConfig::default())
813 .await
814 .map_err(|err| {
815 Error {
818 msg: err.error.message().to_owned(),
819 }
820 })?;
821
822 #[cfg(not(feature = "artifact-graph"))]
823 let new_object_ids = Vec::new();
824 #[cfg(feature = "artifact-graph")]
825 let new_object_ids = {
826 let segment_id = outcome
827 .source_range_to_object
828 .get(&line_source_range)
829 .copied()
830 .ok_or_else(|| Error {
831 msg: format!("Source range of line not found: {line_source_range:?}"),
832 })?;
833 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
834 msg: format!("Segment not found: {segment_id:?}"),
835 })?;
836 let ObjectKind::Segment { segment } = &segment_object.kind else {
837 return Err(Error {
838 msg: format!("Object is not a segment: {segment_object:?}"),
839 });
840 };
841 let Segment::Line(line) = segment else {
842 return Err(Error {
843 msg: format!("Segment is not a line: {segment:?}"),
844 });
845 };
846 vec![line.start, line.end, segment_id]
847 };
848 let src_delta = SourceDelta { text: new_source };
849 let outcome = self.update_state_after_exec(outcome);
850 let scene_graph_delta = SceneGraphDelta {
851 new_graph: self.scene_graph.clone(),
852 invalidates_ids: false,
853 new_objects: new_object_ids,
854 exec_outcome: outcome,
855 };
856 Ok((src_delta, scene_graph_delta))
857 }
858
859 async fn add_arc(
860 &mut self,
861 ctx: &ExecutorContext,
862 sketch: ObjectId,
863 ctor: ArcCtor,
864 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
865 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
867 let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
868 let center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
869 let arguments = vec![
870 ast::LabeledArg {
871 label: Some(ast::Identifier::new(ARC_START_PARAM)),
872 arg: start_ast,
873 },
874 ast::LabeledArg {
875 label: Some(ast::Identifier::new(ARC_END_PARAM)),
876 arg: end_ast,
877 },
878 ast::LabeledArg {
879 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
880 arg: center_ast,
881 },
882 ];
883 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
884 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
885 unlabeled: None,
886 arguments,
887 digest: None,
888 non_code_meta: Default::default(),
889 })));
890
891 let sketch_id = sketch;
893 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
894 msg: format!("Sketch not found: {sketch:?}"),
895 })?;
896 let ObjectKind::Sketch(_) = &sketch_object.kind else {
897 return Err(Error {
898 msg: format!("Object is not a sketch: {sketch_object:?}"),
899 });
900 };
901 let mut new_ast = self.program.ast.clone();
903 let (sketch_block_range, _) = self.mutate_ast(
904 &mut new_ast,
905 sketch_id,
906 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
907 )?;
908 let new_source = source_from_ast(&new_ast);
910 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
912 if !errors.is_empty() {
913 return Err(Error {
914 msg: format!("Error parsing KCL source after adding arc: {errors:?}"),
915 });
916 }
917 let Some(new_program) = new_program else {
918 return Err(Error {
919 msg: "No AST produced after adding arc".to_string(),
920 });
921 };
922 let arc_source_range =
923 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
924 msg: format!("Source range of arc not found in sketch block: {sketch_block_range:?}; {err:?}"),
925 })?;
926 #[cfg(not(feature = "artifact-graph"))]
927 let _ = arc_source_range;
928
929 self.program = new_program.clone();
931
932 let mut truncated_program = new_program;
934 self.exit_after_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
935
936 let outcome = ctx
938 .run_mock(&truncated_program, &MockConfig::default())
939 .await
940 .map_err(|err| {
941 Error {
944 msg: err.error.message().to_owned(),
945 }
946 })?;
947
948 #[cfg(not(feature = "artifact-graph"))]
949 let new_object_ids = Vec::new();
950 #[cfg(feature = "artifact-graph")]
951 let new_object_ids = {
952 let segment_id = outcome
953 .source_range_to_object
954 .get(&arc_source_range)
955 .copied()
956 .ok_or_else(|| Error {
957 msg: format!("Source range of arc not found: {arc_source_range:?}"),
958 })?;
959 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
960 msg: format!("Segment not found: {segment_id:?}"),
961 })?;
962 let ObjectKind::Segment { segment } = &segment_object.kind else {
963 return Err(Error {
964 msg: format!("Object is not a segment: {segment_object:?}"),
965 });
966 };
967 let Segment::Arc(arc) = segment else {
968 return Err(Error {
969 msg: format!("Segment is not an arc: {segment:?}"),
970 });
971 };
972 vec![arc.start, arc.end, arc.center, segment_id]
973 };
974 let src_delta = SourceDelta { text: new_source };
975 let outcome = self.update_state_after_exec(outcome);
976 let scene_graph_delta = SceneGraphDelta {
977 new_graph: self.scene_graph.clone(),
978 invalidates_ids: false,
979 new_objects: new_object_ids,
980 exec_outcome: outcome,
981 };
982 Ok((src_delta, scene_graph_delta))
983 }
984
985 fn edit_point(
986 &mut self,
987 new_ast: &mut ast::Node<ast::Program>,
988 sketch: ObjectId,
989 point: ObjectId,
990 ctor: PointCtor,
991 ) -> api::Result<()> {
992 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
994
995 let sketch_id = sketch;
997 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
998 msg: format!("Sketch not found: {sketch:?}"),
999 })?;
1000 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1001 return Err(Error {
1002 msg: format!("Object is not a sketch: {sketch_object:?}"),
1003 });
1004 };
1005 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| Error {
1006 msg: format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"),
1007 })?;
1008 let point_id = point;
1010 let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
1011 msg: format!("Point not found in scene graph: point={point:?}"),
1012 })?;
1013 let ObjectKind::Segment {
1014 segment: Segment::Point(point),
1015 } = &point_object.kind
1016 else {
1017 return Err(Error {
1018 msg: format!("Object is not a point segment: {point_object:?}"),
1019 });
1020 };
1021
1022 if let Some(owner_id) = point.owner {
1024 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
1025 msg: format!("Internal: Owner of point not found in scene graph: owner={owner_id:?}",),
1026 })?;
1027 let ObjectKind::Segment { segment } = &owner_object.kind else {
1028 return Err(Error {
1029 msg: format!("Internal: Owner of point is not a segment: {owner_object:?}"),
1030 });
1031 };
1032
1033 if let Segment::Line(line) = segment {
1035 let SegmentCtor::Line(line_ctor) = &line.ctor else {
1036 return Err(Error {
1037 msg: format!("Internal: Owner of point does not have line ctor: {owner_object:?}"),
1038 });
1039 };
1040 let mut line_ctor = line_ctor.clone();
1041 if line.start == point_id {
1043 line_ctor.start = ctor.position;
1044 } else if line.end == point_id {
1045 line_ctor.end = ctor.position;
1046 } else {
1047 return Err(Error {
1048 msg: format!(
1049 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
1050 ),
1051 });
1052 }
1053 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
1054 }
1055
1056 if let Segment::Arc(arc) = segment {
1058 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
1059 return Err(Error {
1060 msg: format!("Internal: Owner of point does not have arc ctor: {owner_object:?}"),
1061 });
1062 };
1063 let mut arc_ctor = arc_ctor.clone();
1064 if arc.center == point_id {
1066 arc_ctor.center = ctor.position;
1067 } else if arc.start == point_id {
1068 arc_ctor.start = ctor.position;
1069 } else if arc.end == point_id {
1070 arc_ctor.end = ctor.position;
1071 } else {
1072 return Err(Error {
1073 msg: format!(
1074 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
1075 ),
1076 });
1077 }
1078 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
1079 }
1080
1081 }
1084
1085 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
1087 Ok(())
1088 }
1089
1090 fn edit_line(
1091 &mut self,
1092 new_ast: &mut ast::Node<ast::Program>,
1093 sketch: ObjectId,
1094 line: ObjectId,
1095 ctor: LineCtor,
1096 ) -> api::Result<()> {
1097 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1099 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1100
1101 let sketch_id = sketch;
1103 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1104 msg: format!("Sketch not found: {sketch:?}"),
1105 })?;
1106 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1107 return Err(Error {
1108 msg: format!("Object is not a sketch: {sketch_object:?}"),
1109 });
1110 };
1111 sketch.segments.iter().find(|o| **o == line).ok_or_else(|| Error {
1112 msg: format!("Line not found in sketch: line={line:?}, sketch={sketch:?}"),
1113 })?;
1114 let line_id = line;
1116 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1117 msg: format!("Line not found in scene graph: line={line:?}"),
1118 })?;
1119 let ObjectKind::Segment { .. } = &line_object.kind else {
1120 return Err(Error {
1121 msg: format!("Object is not a segment: {line_object:?}"),
1122 });
1123 };
1124
1125 self.mutate_ast(
1127 new_ast,
1128 line_id,
1129 AstMutateCommand::EditLine {
1130 start: new_start_ast,
1131 end: new_end_ast,
1132 },
1133 )?;
1134 Ok(())
1135 }
1136
1137 fn edit_arc(
1138 &mut self,
1139 new_ast: &mut ast::Node<ast::Program>,
1140 sketch: ObjectId,
1141 arc: ObjectId,
1142 ctor: ArcCtor,
1143 ) -> api::Result<()> {
1144 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1146 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1147 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1148
1149 let sketch_id = sketch;
1151 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1152 msg: format!("Sketch not found: {sketch:?}"),
1153 })?;
1154 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1155 return Err(Error {
1156 msg: format!("Object is not a sketch: {sketch_object:?}"),
1157 });
1158 };
1159 sketch.segments.iter().find(|o| **o == arc).ok_or_else(|| Error {
1160 msg: format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}"),
1161 })?;
1162 let arc_id = arc;
1164 let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
1165 msg: format!("Arc not found in scene graph: arc={arc:?}"),
1166 })?;
1167 let ObjectKind::Segment { .. } = &arc_object.kind else {
1168 return Err(Error {
1169 msg: format!("Object is not a segment: {arc_object:?}"),
1170 });
1171 };
1172
1173 self.mutate_ast(
1175 new_ast,
1176 arc_id,
1177 AstMutateCommand::EditArc {
1178 start: new_start_ast,
1179 end: new_end_ast,
1180 center: new_center_ast,
1181 },
1182 )?;
1183 Ok(())
1184 }
1185
1186 fn delete_segment(
1187 &mut self,
1188 new_ast: &mut ast::Node<ast::Program>,
1189 sketch: ObjectId,
1190 segment_id: ObjectId,
1191 ) -> api::Result<()> {
1192 let sketch_id = sketch;
1194 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1195 msg: format!("Sketch not found: {sketch:?}"),
1196 })?;
1197 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1198 return Err(Error {
1199 msg: format!("Object is not a sketch: {sketch_object:?}"),
1200 });
1201 };
1202 sketch
1203 .segments
1204 .iter()
1205 .find(|o| **o == segment_id)
1206 .ok_or_else(|| Error {
1207 msg: format!("Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"),
1208 })?;
1209 let segment_object = self.scene_graph.objects.get(segment_id.0).ok_or_else(|| Error {
1211 msg: format!("Segment not found in scene graph: segment={segment_id:?}"),
1212 })?;
1213 let ObjectKind::Segment { .. } = &segment_object.kind else {
1214 return Err(Error {
1215 msg: format!("Object is not a segment: {segment_object:?}"),
1216 });
1217 };
1218
1219 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
1221 Ok(())
1222 }
1223
1224 fn delete_constraint(
1225 &mut self,
1226 new_ast: &mut ast::Node<ast::Program>,
1227 sketch: ObjectId,
1228 constraint_id: ObjectId,
1229 ) -> api::Result<()> {
1230 let sketch_id = sketch;
1232 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1233 msg: format!("Sketch not found: {sketch:?}"),
1234 })?;
1235 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1236 return Err(Error {
1237 msg: format!("Object is not a sketch: {sketch_object:?}"),
1238 });
1239 };
1240 sketch
1241 .constraints
1242 .iter()
1243 .find(|o| **o == constraint_id)
1244 .ok_or_else(|| Error {
1245 msg: format!("Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"),
1246 })?;
1247 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
1249 msg: format!("Constraint not found in scene graph: constraint={constraint_id:?}"),
1250 })?;
1251 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
1252 return Err(Error {
1253 msg: format!("Object is not a constraint: {constraint_object:?}"),
1254 });
1255 };
1256
1257 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
1259 Ok(())
1260 }
1261
1262 async fn execute_after_edit(
1263 &mut self,
1264 ctx: &ExecutorContext,
1265 sketch: ObjectId,
1266 segment_ids_edited: AhashIndexSet<ObjectId>,
1267 edit_kind: EditDeleteKind,
1268 new_ast: &mut ast::Node<ast::Program>,
1269 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1270 let new_source = source_from_ast(new_ast);
1272 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1274 if !errors.is_empty() {
1275 return Err(Error {
1276 msg: format!("Error parsing KCL source after editing: {errors:?}"),
1277 });
1278 }
1279 let Some(new_program) = new_program else {
1280 return Err(Error {
1281 msg: "No AST produced after editing".to_string(),
1282 });
1283 };
1284
1285 self.program = new_program.clone();
1287
1288 let is_delete = edit_kind.is_delete();
1290 let truncated_program = match edit_kind {
1291 EditDeleteKind::DeleteSketch => new_program,
1292 EditDeleteKind::Edit | EditDeleteKind::DeleteNonSketch => {
1293 let mut truncated_program = new_program;
1294 self.exit_after_sketch_block(sketch, edit_kind.to_change_kind(), &mut truncated_program.ast)?;
1295 truncated_program
1296 }
1297 };
1298
1299 #[cfg(not(feature = "artifact-graph"))]
1300 drop(segment_ids_edited);
1301
1302 let mock_config = MockConfig {
1304 use_prev_memory: !is_delete,
1305 freedom_analysis: is_delete,
1306 #[cfg(feature = "artifact-graph")]
1307 segment_ids_edited,
1308 };
1309 let outcome = ctx.run_mock(&truncated_program, &mock_config).await.map_err(|err| {
1310 Error {
1313 msg: err.error.message().to_owned(),
1314 }
1315 })?;
1316
1317 let outcome = self.update_state_after_exec(outcome);
1318
1319 #[cfg(feature = "artifact-graph")]
1320 let new_source = {
1321 let mut new_ast = self.program.ast.clone();
1326 for (var_range, value) in &outcome.var_solutions {
1327 let rounded = value.round(3);
1328 mutate_ast_node_by_source_range(
1329 &mut new_ast,
1330 *var_range,
1331 AstMutateCommand::EditVarInitialValue { value: rounded },
1332 )?;
1333 }
1334 source_from_ast(&new_ast)
1335 };
1336
1337 let src_delta = SourceDelta { text: new_source };
1338 let scene_graph_delta = SceneGraphDelta {
1339 new_graph: self.scene_graph.clone(),
1340 invalidates_ids: is_delete,
1341 new_objects: Vec::new(),
1342 exec_outcome: outcome,
1343 };
1344 Ok((src_delta, scene_graph_delta))
1345 }
1346
1347 fn point_id_to_ast_reference(
1352 &self,
1353 point_id: ObjectId,
1354 new_ast: &mut ast::Node<ast::Program>,
1355 ) -> api::Result<ast::Expr> {
1356 let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
1357 msg: format!("Point not found: {point_id:?}"),
1358 })?;
1359 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
1360 return Err(Error {
1361 msg: format!("Object is not a segment: {point_object:?}"),
1362 });
1363 };
1364 let Segment::Point(point) = point_segment else {
1365 return Err(Error {
1366 msg: format!("Only points are currently supported: {point_object:?}"),
1367 });
1368 };
1369
1370 if let Some(owner_id) = point.owner {
1371 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
1372 msg: format!("Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"),
1373 })?;
1374 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
1375 return Err(Error {
1376 msg: format!("Owner of point is not a segment: {owner_object:?}"),
1377 });
1378 };
1379
1380 match owner_segment {
1381 Segment::Line(line) => {
1382 let property = if line.start == point_id {
1383 LINE_PROPERTY_START
1384 } else if line.end == point_id {
1385 LINE_PROPERTY_END
1386 } else {
1387 return Err(Error {
1388 msg: format!(
1389 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
1390 ),
1391 });
1392 };
1393 get_or_insert_ast_reference(new_ast, &owner_object.source, "line", Some(property))
1394 }
1395 Segment::Arc(arc) => {
1396 let property = if arc.start == point_id {
1397 ARC_PROPERTY_START
1398 } else if arc.end == point_id {
1399 ARC_PROPERTY_END
1400 } else if arc.center == point_id {
1401 ARC_PROPERTY_CENTER
1402 } else {
1403 return Err(Error {
1404 msg: format!(
1405 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
1406 ),
1407 });
1408 };
1409 get_or_insert_ast_reference(new_ast, &owner_object.source, "arc", Some(property))
1410 }
1411 _ => Err(Error {
1412 msg: format!(
1413 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
1414 ),
1415 }),
1416 }
1417 } else {
1418 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
1420 }
1421 }
1422
1423 async fn add_coincident(
1424 &mut self,
1425 sketch: ObjectId,
1426 coincident: Coincident,
1427 new_ast: &mut ast::Node<ast::Program>,
1428 ) -> api::Result<SourceRange> {
1429 let &[seg0_id, seg1_id] = coincident.segments.as_slice() else {
1430 return Err(Error {
1431 msg: format!(
1432 "Coincident constraint must have exactly 2 segments, got {}",
1433 coincident.segments.len()
1434 ),
1435 });
1436 };
1437 let sketch_id = sketch;
1438
1439 let seg0_object = self.scene_graph.objects.get(seg0_id.0).ok_or_else(|| Error {
1441 msg: format!("Object not found: {seg0_id:?}"),
1442 })?;
1443 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
1444 return Err(Error {
1445 msg: format!("Object is not a segment: {seg0_object:?}"),
1446 });
1447 };
1448 let seg0_ast = match seg0_segment {
1449 Segment::Point(_) => {
1450 self.point_id_to_ast_reference(seg0_id, new_ast)?
1452 }
1453 Segment::Line(_) => {
1454 get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?
1456 }
1457 Segment::Arc(_) | Segment::Circle(_) => {
1458 get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?
1460 }
1461 };
1462
1463 let seg1_object = self.scene_graph.objects.get(seg1_id.0).ok_or_else(|| Error {
1465 msg: format!("Object not found: {seg1_id:?}"),
1466 })?;
1467 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
1468 return Err(Error {
1469 msg: format!("Object is not a segment: {seg1_object:?}"),
1470 });
1471 };
1472 let seg1_ast = match seg1_segment {
1473 Segment::Point(_) => {
1474 self.point_id_to_ast_reference(seg1_id, new_ast)?
1476 }
1477 Segment::Line(_) => {
1478 get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?
1480 }
1481 Segment::Arc(_) | Segment::Circle(_) => {
1482 get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?
1484 }
1485 };
1486
1487 let coincident_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1489 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
1490 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1491 ast::ArrayExpression {
1492 elements: vec![seg0_ast, seg1_ast],
1493 digest: None,
1494 non_code_meta: Default::default(),
1495 },
1496 )))),
1497 arguments: Default::default(),
1498 digest: None,
1499 non_code_meta: Default::default(),
1500 })));
1501
1502 let (sketch_block_range, _) = self.mutate_ast(
1504 new_ast,
1505 sketch_id,
1506 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
1507 )?;
1508 Ok(sketch_block_range)
1509 }
1510
1511 async fn add_distance(
1512 &mut self,
1513 sketch: ObjectId,
1514 distance: Distance,
1515 new_ast: &mut ast::Node<ast::Program>,
1516 ) -> api::Result<SourceRange> {
1517 let &[pt0_id, pt1_id] = distance.points.as_slice() else {
1518 return Err(Error {
1519 msg: format!(
1520 "Distance constraint must have exactly 2 points, got {}",
1521 distance.points.len()
1522 ),
1523 });
1524 };
1525 let sketch_id = sketch;
1526
1527 let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
1529 let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
1530
1531 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1533 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
1534 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1535 ast::ArrayExpression {
1536 elements: vec![pt0_ast, pt1_ast],
1537 digest: None,
1538 non_code_meta: Default::default(),
1539 },
1540 )))),
1541 arguments: Default::default(),
1542 digest: None,
1543 non_code_meta: Default::default(),
1544 })));
1545 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
1546 left: distance_call_ast,
1547 operator: ast::BinaryOperator::Eq,
1548 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
1549 value: ast::LiteralValue::Number {
1550 value: distance.distance.value,
1551 suffix: distance.distance.units,
1552 },
1553 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
1554 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
1555 })?,
1556 digest: None,
1557 }))),
1558 digest: None,
1559 })));
1560
1561 let (sketch_block_range, _) = self.mutate_ast(
1563 new_ast,
1564 sketch_id,
1565 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
1566 )?;
1567 Ok(sketch_block_range)
1568 }
1569
1570 async fn add_horizontal(
1571 &mut self,
1572 sketch: ObjectId,
1573 horizontal: Horizontal,
1574 new_ast: &mut ast::Node<ast::Program>,
1575 ) -> api::Result<SourceRange> {
1576 let sketch_id = sketch;
1577
1578 let line_id = horizontal.line;
1580 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1581 msg: format!("Line not found: {line_id:?}"),
1582 })?;
1583 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
1584 return Err(Error {
1585 msg: format!("Object is not a segment: {line_object:?}"),
1586 });
1587 };
1588 let Segment::Line(_) = line_segment else {
1589 return Err(Error {
1590 msg: format!("Only lines can be made horizontal: {line_object:?}"),
1591 });
1592 };
1593 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
1594
1595 let horizontal_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1597 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
1598 unlabeled: Some(line_ast),
1599 arguments: Default::default(),
1600 digest: None,
1601 non_code_meta: Default::default(),
1602 })));
1603
1604 let (sketch_block_range, _) = self.mutate_ast(
1606 new_ast,
1607 sketch_id,
1608 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
1609 )?;
1610 Ok(sketch_block_range)
1611 }
1612
1613 async fn add_lines_equal_length(
1614 &mut self,
1615 sketch: ObjectId,
1616 lines_equal_length: LinesEqualLength,
1617 new_ast: &mut ast::Node<ast::Program>,
1618 ) -> api::Result<SourceRange> {
1619 let &[line0_id, line1_id] = lines_equal_length.lines.as_slice() else {
1620 return Err(Error {
1621 msg: format!(
1622 "Lines equal length constraint must have exactly 2 lines, got {}",
1623 lines_equal_length.lines.len()
1624 ),
1625 });
1626 };
1627
1628 let sketch_id = sketch;
1629
1630 let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
1632 msg: format!("Line not found: {line0_id:?}"),
1633 })?;
1634 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
1635 return Err(Error {
1636 msg: format!("Object is not a segment: {line0_object:?}"),
1637 });
1638 };
1639 let Segment::Line(_) = line0_segment else {
1640 return Err(Error {
1641 msg: format!("Only lines can be made equal length: {line0_object:?}"),
1642 });
1643 };
1644 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
1645
1646 let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
1647 msg: format!("Line not found: {line1_id:?}"),
1648 })?;
1649 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
1650 return Err(Error {
1651 msg: format!("Object is not a segment: {line1_object:?}"),
1652 });
1653 };
1654 let Segment::Line(_) = line1_segment else {
1655 return Err(Error {
1656 msg: format!("Only lines can be made equal length: {line1_object:?}"),
1657 });
1658 };
1659 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
1660
1661 let equal_length_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1663 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
1664 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1665 ast::ArrayExpression {
1666 elements: vec![line0_ast, line1_ast],
1667 digest: None,
1668 non_code_meta: Default::default(),
1669 },
1670 )))),
1671 arguments: Default::default(),
1672 digest: None,
1673 non_code_meta: Default::default(),
1674 })));
1675
1676 let (sketch_block_range, _) = self.mutate_ast(
1678 new_ast,
1679 sketch_id,
1680 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
1681 )?;
1682 Ok(sketch_block_range)
1683 }
1684
1685 async fn add_parallel(
1686 &mut self,
1687 sketch: ObjectId,
1688 parallel: Parallel,
1689 new_ast: &mut ast::Node<ast::Program>,
1690 ) -> api::Result<SourceRange> {
1691 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Parallel, parallel.lines, new_ast)
1692 .await
1693 }
1694
1695 async fn add_perpendicular(
1696 &mut self,
1697 sketch: ObjectId,
1698 perpendicular: Perpendicular,
1699 new_ast: &mut ast::Node<ast::Program>,
1700 ) -> api::Result<SourceRange> {
1701 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
1702 .await
1703 }
1704
1705 async fn add_lines_at_angle_constraint(
1706 &mut self,
1707 sketch: ObjectId,
1708 angle_kind: LinesAtAngleKind,
1709 lines: Vec<ObjectId>,
1710 new_ast: &mut ast::Node<ast::Program>,
1711 ) -> api::Result<SourceRange> {
1712 let &[line0_id, line1_id] = lines.as_slice() else {
1713 return Err(Error {
1714 msg: format!(
1715 "{} constraint must have exactly 2 lines, got {}",
1716 angle_kind.to_function_name(),
1717 lines.len()
1718 ),
1719 });
1720 };
1721
1722 let sketch_id = sketch;
1723
1724 let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
1726 msg: format!("Line not found: {line0_id:?}"),
1727 })?;
1728 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
1729 return Err(Error {
1730 msg: format!("Object is not a segment: {line0_object:?}"),
1731 });
1732 };
1733 let Segment::Line(_) = line0_segment else {
1734 return Err(Error {
1735 msg: format!(
1736 "Only lines can be made {}: {line0_object:?}",
1737 angle_kind.to_function_name()
1738 ),
1739 });
1740 };
1741 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
1742
1743 let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
1744 msg: format!("Line not found: {line1_id:?}"),
1745 })?;
1746 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
1747 return Err(Error {
1748 msg: format!("Object is not a segment: {line1_object:?}"),
1749 });
1750 };
1751 let Segment::Line(_) = line1_segment else {
1752 return Err(Error {
1753 msg: format!(
1754 "Only lines can be made {}: {line1_object:?}",
1755 angle_kind.to_function_name()
1756 ),
1757 });
1758 };
1759 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
1760
1761 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1763 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
1764 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1765 ast::ArrayExpression {
1766 elements: vec![line0_ast, line1_ast],
1767 digest: None,
1768 non_code_meta: Default::default(),
1769 },
1770 )))),
1771 arguments: Default::default(),
1772 digest: None,
1773 non_code_meta: Default::default(),
1774 })));
1775
1776 let (sketch_block_range, _) = self.mutate_ast(
1778 new_ast,
1779 sketch_id,
1780 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
1781 )?;
1782 Ok(sketch_block_range)
1783 }
1784
1785 async fn add_vertical(
1786 &mut self,
1787 sketch: ObjectId,
1788 vertical: Vertical,
1789 new_ast: &mut ast::Node<ast::Program>,
1790 ) -> api::Result<SourceRange> {
1791 let sketch_id = sketch;
1792
1793 let line_id = vertical.line;
1795 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1796 msg: format!("Line not found: {line_id:?}"),
1797 })?;
1798 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
1799 return Err(Error {
1800 msg: format!("Object is not a segment: {line_object:?}"),
1801 });
1802 };
1803 let Segment::Line(_) = line_segment else {
1804 return Err(Error {
1805 msg: format!("Only lines can be made vertical: {line_object:?}"),
1806 });
1807 };
1808 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
1809
1810 let vertical_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1812 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
1813 unlabeled: Some(line_ast),
1814 arguments: Default::default(),
1815 digest: None,
1816 non_code_meta: Default::default(),
1817 })));
1818
1819 let (sketch_block_range, _) = self.mutate_ast(
1821 new_ast,
1822 sketch_id,
1823 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
1824 )?;
1825 Ok(sketch_block_range)
1826 }
1827
1828 async fn execute_after_add_constraint(
1829 &mut self,
1830 ctx: &ExecutorContext,
1831 sketch_id: ObjectId,
1832 #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_range: SourceRange,
1833 new_ast: &mut ast::Node<ast::Program>,
1834 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1835 let new_source = source_from_ast(new_ast);
1837 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1839 if !errors.is_empty() {
1840 return Err(Error {
1841 msg: format!("Error parsing KCL source after adding constraint: {errors:?}"),
1842 });
1843 }
1844 let Some(new_program) = new_program else {
1845 return Err(Error {
1846 msg: "No AST produced after adding constraint".to_string(),
1847 });
1848 };
1849 #[cfg(feature = "artifact-graph")]
1850 let constraint_source_range =
1851 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1852 msg: format!(
1853 "Source range of new constraint not found in sketch block: {sketch_block_range:?}; {err:?}"
1854 ),
1855 })?;
1856
1857 self.program = new_program.clone();
1859
1860 let mut truncated_program = new_program;
1862 self.exit_after_sketch_block(sketch_id, ChangeKind::Add, &mut truncated_program.ast)?;
1863
1864 let mock_config = MockConfig {
1866 freedom_analysis: true,
1867 ..Default::default()
1868 };
1869 let outcome = ctx.run_mock(&truncated_program, &mock_config).await.map_err(|err| {
1870 Error {
1873 msg: err.error.message().to_owned(),
1874 }
1875 })?;
1876
1877 #[cfg(not(feature = "artifact-graph"))]
1878 let new_object_ids = Vec::new();
1879 #[cfg(feature = "artifact-graph")]
1880 let new_object_ids = {
1881 let constraint_id = outcome
1883 .source_range_to_object
1884 .get(&constraint_source_range)
1885 .copied()
1886 .ok_or_else(|| Error {
1887 msg: format!("Source range of constraint not found: {constraint_source_range:?}"),
1888 })?;
1889 vec![constraint_id]
1890 };
1891
1892 let src_delta = SourceDelta { text: new_source };
1893 let outcome = self.update_state_after_exec(outcome);
1894 let scene_graph_delta = SceneGraphDelta {
1895 new_graph: self.scene_graph.clone(),
1896 invalidates_ids: false,
1897 new_objects: new_object_ids,
1898 exec_outcome: outcome,
1899 };
1900 Ok((src_delta, scene_graph_delta))
1901 }
1902
1903 fn add_dependent_constraints_to_delete(
1906 &self,
1907 sketch_id: ObjectId,
1908 segment_ids_set: &AhashIndexSet<ObjectId>,
1909 constraint_ids_set: &mut AhashIndexSet<ObjectId>,
1910 ) -> api::Result<()> {
1911 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1913 msg: format!("Sketch not found: {sketch_id:?}"),
1914 })?;
1915 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1916 return Err(Error {
1917 msg: format!("Object is not a sketch: {sketch_object:?}"),
1918 });
1919 };
1920 for constraint_id in &sketch.constraints {
1921 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
1922 msg: format!("Constraint not found: {constraint_id:?}"),
1923 })?;
1924 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1925 return Err(Error {
1926 msg: format!("Object is not a constraint: {constraint_object:?}"),
1927 });
1928 };
1929 let depends_on_segment = match constraint {
1930 Constraint::Coincident(c) => c.segments.iter().any(|pt_id| {
1931 if segment_ids_set.contains(pt_id) {
1932 return true;
1933 }
1934 let pt_object = self.scene_graph.objects.get(pt_id.0);
1935 if let Some(obj) = pt_object
1936 && let ObjectKind::Segment { segment } = &obj.kind
1937 && let Segment::Point(pt) = segment
1938 && let Some(owner_line_id) = pt.owner
1939 {
1940 return segment_ids_set.contains(&owner_line_id);
1941 }
1942 false
1943 }),
1944 Constraint::Distance(d) => d.points.iter().any(|pt_id| {
1945 let pt_object = self.scene_graph.objects.get(pt_id.0);
1946 if let Some(obj) = pt_object
1947 && let ObjectKind::Segment { segment } = &obj.kind
1948 && let Segment::Point(pt) = segment
1949 && let Some(owner_line_id) = pt.owner
1950 {
1951 return segment_ids_set.contains(&owner_line_id);
1952 }
1953 false
1954 }),
1955 Constraint::Horizontal(h) => segment_ids_set.contains(&h.line),
1956 Constraint::Vertical(v) => segment_ids_set.contains(&v.line),
1957 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
1958 .lines
1959 .iter()
1960 .any(|line_id| segment_ids_set.contains(line_id)),
1961 Constraint::Parallel(parallel) => {
1962 parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
1963 }
1964 Constraint::Perpendicular(perpendicular) => perpendicular
1965 .lines
1966 .iter()
1967 .any(|line_id| segment_ids_set.contains(line_id)),
1968 };
1969 if depends_on_segment {
1970 constraint_ids_set.insert(*constraint_id);
1971 }
1972 }
1973 Ok(())
1974 }
1975
1976 fn update_state_after_exec(&mut self, outcome: ExecOutcome) -> ExecOutcome {
1977 #[cfg(not(feature = "artifact-graph"))]
1978 return outcome;
1979 #[cfg(feature = "artifact-graph")]
1980 {
1981 let mut outcome = outcome;
1982 self.scene_graph.objects = std::mem::take(&mut outcome.scene_objects);
1983 outcome
1984 }
1985 }
1986
1987 fn exit_after_sketch_block(
1988 &self,
1989 sketch_id: ObjectId,
1990 edit_kind: ChangeKind,
1991 ast: &mut ast::Node<ast::Program>,
1992 ) -> api::Result<()> {
1993 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1994 msg: format!("Sketch not found: {sketch_id:?}"),
1995 })?;
1996 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1997 return Err(Error {
1998 msg: format!("Object is not a sketch: {sketch_object:?}"),
1999 });
2000 };
2001 let sketch_block_range = expect_single_source_range(&sketch_object.source)?;
2002 exit_after_sketch_block(ast, sketch_block_range, edit_kind)
2003 }
2004
2005 fn mutate_ast(
2006 &mut self,
2007 ast: &mut ast::Node<ast::Program>,
2008 object_id: ObjectId,
2009 command: AstMutateCommand,
2010 ) -> api::Result<(SourceRange, AstMutateCommandReturn)> {
2011 let sketch_object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
2012 msg: format!("Object not found: {object_id:?}"),
2013 })?;
2014 match &sketch_object.source {
2015 SourceRef::Simple { range } => mutate_ast_node_by_source_range(ast, *range, command),
2016 SourceRef::BackTrace { .. } => Err(Error {
2017 msg: "BackTrace source refs not supported yet".to_owned(),
2018 }),
2019 }
2020 }
2021}
2022
2023fn expect_single_source_range(source_ref: &SourceRef) -> api::Result<SourceRange> {
2024 match source_ref {
2025 SourceRef::Simple { range } => Ok(*range),
2026 SourceRef::BackTrace { ranges } => {
2027 if ranges.len() != 1 {
2028 return Err(Error {
2029 msg: format!(
2030 "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
2031 ranges.len(),
2032 ),
2033 });
2034 }
2035 Ok(ranges[0])
2036 }
2037 }
2038}
2039
2040fn exit_after_sketch_block(
2041 ast: &mut ast::Node<ast::Program>,
2042 sketch_block_range: SourceRange,
2043 edit_kind: ChangeKind,
2044) -> api::Result<()> {
2045 let r1 = sketch_block_range;
2046 let matches_range = |r2: SourceRange| -> bool {
2047 match edit_kind {
2050 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
2051 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
2053 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
2054 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
2056 }
2057 };
2058 let mut found = false;
2059 for item in ast.body.iter_mut() {
2060 match item {
2061 ast::BodyItem::ImportStatement(_) => {}
2062 ast::BodyItem::ExpressionStatement(node) => {
2063 if matches_range(SourceRange::from(&*node))
2064 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
2065 {
2066 sketch_block.is_being_edited = true;
2067 found = true;
2068 break;
2069 }
2070 }
2071 ast::BodyItem::VariableDeclaration(node) => {
2072 if matches_range(SourceRange::from(&node.declaration.init))
2073 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
2074 {
2075 sketch_block.is_being_edited = true;
2076 found = true;
2077 break;
2078 }
2079 }
2080 ast::BodyItem::TypeDeclaration(_) => {}
2081 ast::BodyItem::ReturnStatement(node) => {
2082 if matches_range(SourceRange::from(&node.argument))
2083 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
2084 {
2085 sketch_block.is_being_edited = true;
2086 found = true;
2087 break;
2088 }
2089 }
2090 }
2091 }
2092 if !found {
2093 return Err(Error {
2094 msg: format!("Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"),
2095 });
2096 }
2097
2098 Ok(())
2099}
2100
2101fn get_or_insert_ast_reference(
2108 ast: &mut ast::Node<ast::Program>,
2109 source_ref: &SourceRef,
2110 prefix: &str,
2111 property: Option<&str>,
2112) -> api::Result<ast::Expr> {
2113 let range = expect_single_source_range(source_ref)?;
2114 let command = AstMutateCommand::AddVariableDeclaration {
2115 prefix: prefix.to_owned(),
2116 };
2117 let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
2118 let AstMutateCommandReturn::Name(var_name) = ret else {
2119 return Err(Error {
2120 msg: "Expected variable name returned from AddVariableDeclaration".to_owned(),
2121 });
2122 };
2123 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
2124 let Some(property) = property else {
2125 return Ok(var_expr);
2127 };
2128
2129 Ok(ast::Expr::MemberExpression(Box::new(ast::Node::no_src(
2130 ast::MemberExpression {
2131 object: var_expr,
2132 property: ast::Expr::Name(Box::new(ast::Name::new(property))),
2133 computed: false,
2134 digest: None,
2135 },
2136 ))))
2137}
2138
2139fn mutate_ast_node_by_source_range(
2140 ast: &mut ast::Node<ast::Program>,
2141 source_range: SourceRange,
2142 command: AstMutateCommand,
2143) -> Result<(SourceRange, AstMutateCommandReturn), Error> {
2144 let mut context = AstMutateContext {
2145 source_range,
2146 command,
2147 defined_names_stack: Default::default(),
2148 };
2149 let control = dfs_mut(ast, &mut context);
2150 match control {
2151 ControlFlow::Continue(_) => Err(Error {
2152 msg: format!("Source range not found: {source_range:?}"),
2153 }),
2154 ControlFlow::Break(break_value) => break_value,
2155 }
2156}
2157
2158#[derive(Debug)]
2159struct AstMutateContext {
2160 source_range: SourceRange,
2161 command: AstMutateCommand,
2162 defined_names_stack: Vec<HashSet<String>>,
2163}
2164
2165#[derive(Debug)]
2166#[allow(clippy::large_enum_variant)]
2167enum AstMutateCommand {
2168 AddSketchBlockExprStmt {
2170 expr: ast::Expr,
2171 },
2172 AddVariableDeclaration {
2173 prefix: String,
2174 },
2175 EditPoint {
2176 at: ast::Expr,
2177 },
2178 EditLine {
2179 start: ast::Expr,
2180 end: ast::Expr,
2181 },
2182 EditArc {
2183 start: ast::Expr,
2184 end: ast::Expr,
2185 center: ast::Expr,
2186 },
2187 #[cfg(feature = "artifact-graph")]
2188 EditVarInitialValue {
2189 value: Number,
2190 },
2191 DeleteNode,
2192}
2193
2194#[derive(Debug)]
2195enum AstMutateCommandReturn {
2196 None,
2197 Name(String),
2198}
2199
2200impl Visitor for AstMutateContext {
2201 type Break = Result<(SourceRange, AstMutateCommandReturn), Error>;
2202 type Continue = ();
2203
2204 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
2205 filter_and_process(self, node)
2206 }
2207
2208 fn finish(&mut self, node: NodeMut<'_>) {
2209 match &node {
2210 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
2211 self.defined_names_stack.pop();
2212 }
2213 _ => {}
2214 }
2215 }
2216}
2217
2218fn filter_and_process(
2219 ctx: &mut AstMutateContext,
2220 node: NodeMut,
2221) -> TraversalReturn<Result<(SourceRange, AstMutateCommandReturn), Error>> {
2222 let Ok(node_range) = SourceRange::try_from(&node) else {
2223 return TraversalReturn::new_continue(());
2225 };
2226 if let NodeMut::VariableDeclaration(var_decl) = &node {
2231 let expr_range = SourceRange::from(&var_decl.declaration.init);
2232 if expr_range == ctx.source_range {
2233 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
2234 return TraversalReturn::new_break(Ok((
2237 node_range,
2238 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
2239 )));
2240 }
2241 if let AstMutateCommand::DeleteNode = &ctx.command {
2242 return TraversalReturn {
2245 mutate_body_item: MutateBodyItem::Delete,
2246 control_flow: ControlFlow::Break(Ok((ctx.source_range, AstMutateCommandReturn::None))),
2247 };
2248 }
2249 }
2250 }
2251
2252 if let NodeMut::Program(program) = &node {
2253 ctx.defined_names_stack.push(find_defined_names(*program));
2254 } else if let NodeMut::SketchBlock(block) = &node {
2255 ctx.defined_names_stack.push(find_defined_names(&block.body));
2256 }
2257
2258 if node_range != ctx.source_range {
2260 return TraversalReturn::new_continue(());
2261 }
2262 process(ctx, node).map_break(|result| result.map(|cmd_return| (ctx.source_range, cmd_return)))
2263}
2264
2265fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, Error>> {
2266 match &ctx.command {
2267 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
2268 if let NodeMut::SketchBlock(sketch_block) = node {
2269 sketch_block
2270 .body
2271 .items
2272 .push(ast::BodyItem::ExpressionStatement(ast::Node {
2273 inner: ast::ExpressionStatement {
2274 expression: expr.clone(),
2275 digest: None,
2276 },
2277 start: Default::default(),
2278 end: Default::default(),
2279 module_id: Default::default(),
2280 outer_attrs: Default::default(),
2281 pre_comments: Default::default(),
2282 comment_start: Default::default(),
2283 }));
2284 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2285 }
2286 }
2287 AstMutateCommand::AddVariableDeclaration { prefix } => {
2288 if let NodeMut::VariableDeclaration(inner) = node {
2289 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
2290 }
2291 if let NodeMut::ExpressionStatement(expr_stmt) = node {
2292 let empty_defined_names = HashSet::new();
2293 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
2294 let Ok(name) = next_free_name(prefix, defined_names) else {
2295 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2297 };
2298 let mutate_node =
2299 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
2300 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
2301 ast::ItemVisibility::Default,
2302 ast::VariableKind::Const,
2303 ))));
2304 return TraversalReturn {
2305 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
2306 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
2307 };
2308 }
2309 }
2310 AstMutateCommand::EditPoint { at } => {
2311 if let NodeMut::CallExpressionKw(call) = node {
2312 if call.callee.name.name != POINT_FN {
2313 return TraversalReturn::new_continue(());
2314 }
2315 for labeled_arg in &mut call.arguments {
2317 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
2318 labeled_arg.arg = at.clone();
2319 }
2320 }
2321 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2322 }
2323 }
2324 AstMutateCommand::EditLine { start, end } => {
2325 if let NodeMut::CallExpressionKw(call) = node {
2326 if call.callee.name.name != LINE_FN {
2327 return TraversalReturn::new_continue(());
2328 }
2329 for labeled_arg in &mut call.arguments {
2331 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
2332 labeled_arg.arg = start.clone();
2333 }
2334 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
2335 labeled_arg.arg = end.clone();
2336 }
2337 }
2338 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2339 }
2340 }
2341 AstMutateCommand::EditArc { start, end, center } => {
2342 if let NodeMut::CallExpressionKw(call) = node {
2343 if call.callee.name.name != ARC_FN {
2344 return TraversalReturn::new_continue(());
2345 }
2346 for labeled_arg in &mut call.arguments {
2348 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
2349 labeled_arg.arg = start.clone();
2350 }
2351 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
2352 labeled_arg.arg = end.clone();
2353 }
2354 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
2355 labeled_arg.arg = center.clone();
2356 }
2357 }
2358 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2359 }
2360 }
2361 #[cfg(feature = "artifact-graph")]
2362 AstMutateCommand::EditVarInitialValue { value } => {
2363 if let NodeMut::NumericLiteral(numeric_literal) = node {
2364 let Ok(literal) = to_source_number(*value) else {
2366 return TraversalReturn::new_break(Err(Error {
2367 msg: format!("Could not convert number to AST literal: {:?}", *value),
2368 }));
2369 };
2370 *numeric_literal = ast::Node::no_src(literal);
2371 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2372 }
2373 }
2374 AstMutateCommand::DeleteNode => {
2375 return TraversalReturn {
2376 mutate_body_item: MutateBodyItem::Delete,
2377 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
2378 };
2379 }
2380 }
2381 TraversalReturn::new_continue(())
2382}
2383
2384struct FindSketchBlockSourceRange {
2385 target_before_mutation: SourceRange,
2387 found: Cell<Option<SourceRange>>,
2391}
2392
2393impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
2394 type Error = crate::front::Error;
2395
2396 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
2397 let Ok(node_range) = SourceRange::try_from(&node) else {
2398 return Ok(true);
2399 };
2400
2401 if let crate::walk::Node::SketchBlock(sketch_block) = node {
2402 if node_range.module_id() == self.target_before_mutation.module_id()
2403 && node_range.start() == self.target_before_mutation.start()
2404 && node_range.end() >= self.target_before_mutation.end()
2406 {
2407 self.found.set(sketch_block.body.items.last().map(SourceRange::from));
2408 return Ok(false);
2409 } else {
2410 return Ok(true);
2413 }
2414 }
2415
2416 for child in node.children().iter() {
2417 if !child.visit(*self)? {
2418 return Ok(false);
2419 }
2420 }
2421
2422 Ok(true)
2423 }
2424}
2425
2426fn find_sketch_block_added_item(
2434 ast: &ast::Node<ast::Program>,
2435 range_before_mutation: SourceRange,
2436) -> api::Result<SourceRange> {
2437 let find = FindSketchBlockSourceRange {
2438 target_before_mutation: range_before_mutation,
2439 found: Cell::new(None),
2440 };
2441 let node = crate::walk::Node::from(ast);
2442 node.visit(&find)?;
2443 find.found.into_inner().ok_or_else(|| api::Error {
2444 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?"),
2445 })
2446}
2447
2448fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
2449 ast.recast_top(&Default::default(), 0)
2451}
2452
2453fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
2454 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
2455 inner: ast::ArrayExpression {
2456 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
2457 non_code_meta: Default::default(),
2458 digest: None,
2459 },
2460 start: Default::default(),
2461 end: Default::default(),
2462 module_id: Default::default(),
2463 outer_attrs: Default::default(),
2464 pre_comments: Default::default(),
2465 comment_start: Default::default(),
2466 })))
2467}
2468
2469fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
2470 match expr {
2471 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
2472 inner: ast::Literal::from(to_source_number(*number)?),
2473 start: Default::default(),
2474 end: Default::default(),
2475 module_id: Default::default(),
2476 outer_attrs: Default::default(),
2477 pre_comments: Default::default(),
2478 comment_start: Default::default(),
2479 }))),
2480 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
2481 inner: ast::SketchVar {
2482 initial: Some(Box::new(ast::Node {
2483 inner: to_source_number(*number)?,
2484 start: Default::default(),
2485 end: Default::default(),
2486 module_id: Default::default(),
2487 outer_attrs: Default::default(),
2488 pre_comments: Default::default(),
2489 comment_start: Default::default(),
2490 })),
2491 digest: None,
2492 },
2493 start: Default::default(),
2494 end: Default::default(),
2495 module_id: Default::default(),
2496 outer_attrs: Default::default(),
2497 pre_comments: Default::default(),
2498 comment_start: Default::default(),
2499 }))),
2500 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
2501 }
2502}
2503
2504fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
2505 Ok(ast::NumericLiteral {
2506 value: number.value,
2507 suffix: number.units,
2508 raw: format_number_literal(number.value, number.units)?,
2509 digest: None,
2510 })
2511}
2512
2513fn ast_name_expr(name: String) -> ast::Expr {
2514 ast::Expr::Name(Box::new(ast_name(name)))
2515}
2516
2517fn ast_name(name: String) -> ast::Node<ast::Name> {
2518 ast::Node {
2519 inner: ast::Name {
2520 name: ast::Node {
2521 inner: ast::Identifier { name, digest: None },
2522 start: Default::default(),
2523 end: Default::default(),
2524 module_id: Default::default(),
2525 outer_attrs: Default::default(),
2526 pre_comments: Default::default(),
2527 comment_start: Default::default(),
2528 },
2529 path: Vec::new(),
2530 abs_path: false,
2531 digest: None,
2532 },
2533 start: Default::default(),
2534 end: Default::default(),
2535 module_id: Default::default(),
2536 outer_attrs: Default::default(),
2537 pre_comments: Default::default(),
2538 comment_start: Default::default(),
2539 }
2540}
2541
2542fn ast_sketch2_name(name: &str) -> ast::Name {
2543 ast::Name {
2544 name: ast::Node {
2545 inner: ast::Identifier {
2546 name: name.to_owned(),
2547 digest: None,
2548 },
2549 start: Default::default(),
2550 end: Default::default(),
2551 module_id: Default::default(),
2552 outer_attrs: Default::default(),
2553 pre_comments: Default::default(),
2554 comment_start: Default::default(),
2555 },
2556 path: vec![ast::Node::no_src(ast::Identifier {
2557 name: "sketch2".to_owned(),
2558 digest: None,
2559 })],
2560 abs_path: false,
2561 digest: None,
2562 }
2563}
2564
2565#[cfg(test)]
2566mod tests {
2567 use super::*;
2568 use crate::{
2569 engine::PlaneName,
2570 front::{Distance, Plane, Sketch},
2571 frontend::sketch::Vertical,
2572 pretty::NumericSuffix,
2573 };
2574
2575 #[tokio::test(flavor = "multi_thread")]
2576 async fn test_new_sketch_add_point_edit_point() {
2577 let program = Program::empty();
2578
2579 let mut frontend = FrontendState::new();
2580 frontend.program = program;
2581
2582 let mock_ctx = ExecutorContext::new_mock(None).await;
2583 let version = Version(0);
2584
2585 let sketch_args = SketchArgs {
2586 on: api::Plane::Default(PlaneName::Xy),
2587 };
2588 let (_src_delta, scene_delta, sketch_id) = frontend
2589 .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2590 .await
2591 .unwrap();
2592 assert_eq!(sketch_id, ObjectId(0));
2593 assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2594 let sketch_object = &scene_delta.new_graph.objects[0];
2595 assert_eq!(sketch_object.id, ObjectId(0));
2596 assert_eq!(
2597 sketch_object.kind,
2598 ObjectKind::Sketch(Sketch {
2599 args: SketchArgs {
2600 on: Plane::Default(PlaneName::Xy)
2601 },
2602 segments: vec![],
2603 constraints: vec![],
2604 })
2605 );
2606 assert_eq!(scene_delta.new_graph.objects.len(), 1);
2607
2608 let point_ctor = PointCtor {
2609 position: Point2d {
2610 x: Expr::Number(Number {
2611 value: 1.0,
2612 units: NumericSuffix::Inch,
2613 }),
2614 y: Expr::Number(Number {
2615 value: 2.0,
2616 units: NumericSuffix::Inch,
2617 }),
2618 },
2619 };
2620 let segment = SegmentCtor::Point(point_ctor);
2621 let (src_delta, scene_delta) = frontend
2622 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2623 .await
2624 .unwrap();
2625 assert_eq!(
2626 src_delta.text.as_str(),
2627 "@settings(experimentalFeatures = allow)
2628
2629sketch(on = XY) {
2630 sketch2::point(at = [1in, 2in])
2631}
2632"
2633 );
2634 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
2635 assert_eq!(scene_delta.new_graph.objects.len(), 2);
2636 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
2637 assert_eq!(scene_object.id.0, i);
2638 }
2639 assert_eq!(scene_delta.new_graph.objects.len(), 2);
2640
2641 let point_id = *scene_delta.new_objects.last().unwrap();
2642
2643 let point_ctor = PointCtor {
2644 position: Point2d {
2645 x: Expr::Number(Number {
2646 value: 3.0,
2647 units: NumericSuffix::Inch,
2648 }),
2649 y: Expr::Number(Number {
2650 value: 4.0,
2651 units: NumericSuffix::Inch,
2652 }),
2653 },
2654 };
2655 let segments = vec![ExistingSegmentCtor {
2656 id: point_id,
2657 ctor: SegmentCtor::Point(point_ctor),
2658 }];
2659 let (src_delta, scene_delta) = frontend
2660 .edit_segments(&mock_ctx, version, sketch_id, segments)
2661 .await
2662 .unwrap();
2663 assert_eq!(
2664 src_delta.text.as_str(),
2665 "@settings(experimentalFeatures = allow)
2666
2667sketch(on = XY) {
2668 sketch2::point(at = [3in, 4in])
2669}
2670"
2671 );
2672 assert_eq!(scene_delta.new_objects, vec![]);
2673 assert_eq!(scene_delta.new_graph.objects.len(), 2);
2674
2675 mock_ctx.close().await;
2676 }
2677
2678 #[tokio::test(flavor = "multi_thread")]
2679 async fn test_new_sketch_add_line_edit_line() {
2680 let program = Program::empty();
2681
2682 let mut frontend = FrontendState::new();
2683 frontend.program = program;
2684
2685 let mock_ctx = ExecutorContext::new_mock(None).await;
2686 let version = Version(0);
2687
2688 let sketch_args = SketchArgs {
2689 on: api::Plane::Default(PlaneName::Xy),
2690 };
2691 let (_src_delta, scene_delta, sketch_id) = frontend
2692 .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2693 .await
2694 .unwrap();
2695 assert_eq!(sketch_id, ObjectId(0));
2696 assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2697 let sketch_object = &scene_delta.new_graph.objects[0];
2698 assert_eq!(sketch_object.id, ObjectId(0));
2699 assert_eq!(
2700 sketch_object.kind,
2701 ObjectKind::Sketch(Sketch {
2702 args: SketchArgs {
2703 on: Plane::Default(PlaneName::Xy)
2704 },
2705 segments: vec![],
2706 constraints: vec![],
2707 })
2708 );
2709 assert_eq!(scene_delta.new_graph.objects.len(), 1);
2710
2711 let line_ctor = LineCtor {
2712 start: Point2d {
2713 x: Expr::Number(Number {
2714 value: 0.0,
2715 units: NumericSuffix::Mm,
2716 }),
2717 y: Expr::Number(Number {
2718 value: 0.0,
2719 units: NumericSuffix::Mm,
2720 }),
2721 },
2722 end: Point2d {
2723 x: Expr::Number(Number {
2724 value: 10.0,
2725 units: NumericSuffix::Mm,
2726 }),
2727 y: Expr::Number(Number {
2728 value: 10.0,
2729 units: NumericSuffix::Mm,
2730 }),
2731 },
2732 };
2733 let segment = SegmentCtor::Line(line_ctor);
2734 let (src_delta, scene_delta) = frontend
2735 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2736 .await
2737 .unwrap();
2738 assert_eq!(
2739 src_delta.text.as_str(),
2740 "@settings(experimentalFeatures = allow)
2741
2742sketch(on = XY) {
2743 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
2744}
2745"
2746 );
2747 assert_eq!(scene_delta.new_objects, vec![ObjectId(1), ObjectId(2), ObjectId(3)]);
2748 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
2749 assert_eq!(scene_object.id.0, i);
2750 }
2751 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2752
2753 let line = *scene_delta.new_objects.last().unwrap();
2755
2756 let line_ctor = LineCtor {
2757 start: Point2d {
2758 x: Expr::Number(Number {
2759 value: 1.0,
2760 units: NumericSuffix::Mm,
2761 }),
2762 y: Expr::Number(Number {
2763 value: 2.0,
2764 units: NumericSuffix::Mm,
2765 }),
2766 },
2767 end: Point2d {
2768 x: Expr::Number(Number {
2769 value: 13.0,
2770 units: NumericSuffix::Mm,
2771 }),
2772 y: Expr::Number(Number {
2773 value: 14.0,
2774 units: NumericSuffix::Mm,
2775 }),
2776 },
2777 };
2778 let segments = vec![ExistingSegmentCtor {
2779 id: line,
2780 ctor: SegmentCtor::Line(line_ctor),
2781 }];
2782 let (src_delta, scene_delta) = frontend
2783 .edit_segments(&mock_ctx, version, sketch_id, segments)
2784 .await
2785 .unwrap();
2786 assert_eq!(
2787 src_delta.text.as_str(),
2788 "@settings(experimentalFeatures = allow)
2789
2790sketch(on = XY) {
2791 sketch2::line(start = [1mm, 2mm], end = [13mm, 14mm])
2792}
2793"
2794 );
2795 assert_eq!(scene_delta.new_objects, vec![]);
2796 assert_eq!(scene_delta.new_graph.objects.len(), 4);
2797
2798 mock_ctx.close().await;
2799 }
2800
2801 #[tokio::test(flavor = "multi_thread")]
2802 async fn test_new_sketch_add_arc_edit_arc() {
2803 let program = Program::empty();
2804
2805 let mut frontend = FrontendState::new();
2806 frontend.program = program;
2807
2808 let mock_ctx = ExecutorContext::new_mock(None).await;
2809 let version = Version(0);
2810
2811 let sketch_args = SketchArgs {
2812 on: api::Plane::Default(PlaneName::Xy),
2813 };
2814 let (_src_delta, scene_delta, sketch_id) = frontend
2815 .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
2816 .await
2817 .unwrap();
2818 assert_eq!(sketch_id, ObjectId(0));
2819 assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
2820 let sketch_object = &scene_delta.new_graph.objects[0];
2821 assert_eq!(sketch_object.id, ObjectId(0));
2822 assert_eq!(
2823 sketch_object.kind,
2824 ObjectKind::Sketch(Sketch {
2825 args: SketchArgs {
2826 on: Plane::Default(PlaneName::Xy)
2827 },
2828 segments: vec![],
2829 constraints: vec![],
2830 })
2831 );
2832 assert_eq!(scene_delta.new_graph.objects.len(), 1);
2833
2834 let arc_ctor = ArcCtor {
2835 start: Point2d {
2836 x: Expr::Var(Number {
2837 value: 0.0,
2838 units: NumericSuffix::Mm,
2839 }),
2840 y: Expr::Var(Number {
2841 value: 0.0,
2842 units: NumericSuffix::Mm,
2843 }),
2844 },
2845 end: Point2d {
2846 x: Expr::Var(Number {
2847 value: 10.0,
2848 units: NumericSuffix::Mm,
2849 }),
2850 y: Expr::Var(Number {
2851 value: 10.0,
2852 units: NumericSuffix::Mm,
2853 }),
2854 },
2855 center: Point2d {
2856 x: Expr::Var(Number {
2857 value: 10.0,
2858 units: NumericSuffix::Mm,
2859 }),
2860 y: Expr::Var(Number {
2861 value: 0.0,
2862 units: NumericSuffix::Mm,
2863 }),
2864 },
2865 };
2866 let segment = SegmentCtor::Arc(arc_ctor);
2867 let (src_delta, scene_delta) = frontend
2868 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2869 .await
2870 .unwrap();
2871 assert_eq!(
2872 src_delta.text.as_str(),
2873 "@settings(experimentalFeatures = allow)
2874
2875sketch(on = XY) {
2876 sketch2::arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
2877}
2878"
2879 );
2880 assert_eq!(
2881 scene_delta.new_objects,
2882 vec![ObjectId(1), ObjectId(2), ObjectId(3), ObjectId(4)]
2883 );
2884 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
2885 assert_eq!(scene_object.id.0, i);
2886 }
2887 assert_eq!(scene_delta.new_graph.objects.len(), 5);
2888
2889 let arc = *scene_delta.new_objects.last().unwrap();
2891
2892 let arc_ctor = ArcCtor {
2893 start: Point2d {
2894 x: Expr::Var(Number {
2895 value: 1.0,
2896 units: NumericSuffix::Mm,
2897 }),
2898 y: Expr::Var(Number {
2899 value: 2.0,
2900 units: NumericSuffix::Mm,
2901 }),
2902 },
2903 end: Point2d {
2904 x: Expr::Var(Number {
2905 value: 13.0,
2906 units: NumericSuffix::Mm,
2907 }),
2908 y: Expr::Var(Number {
2909 value: 14.0,
2910 units: NumericSuffix::Mm,
2911 }),
2912 },
2913 center: Point2d {
2914 x: Expr::Var(Number {
2915 value: 13.0,
2916 units: NumericSuffix::Mm,
2917 }),
2918 y: Expr::Var(Number {
2919 value: 2.0,
2920 units: NumericSuffix::Mm,
2921 }),
2922 },
2923 };
2924 let segments = vec![ExistingSegmentCtor {
2925 id: arc,
2926 ctor: SegmentCtor::Arc(arc_ctor),
2927 }];
2928 let (src_delta, scene_delta) = frontend
2929 .edit_segments(&mock_ctx, version, sketch_id, segments)
2930 .await
2931 .unwrap();
2932 assert_eq!(
2933 src_delta.text.as_str(),
2934 "@settings(experimentalFeatures = allow)
2935
2936sketch(on = XY) {
2937 sketch2::arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
2938}
2939"
2940 );
2941 assert_eq!(scene_delta.new_objects, vec![]);
2942 assert_eq!(scene_delta.new_graph.objects.len(), 5);
2943
2944 mock_ctx.close().await;
2945 }
2946
2947 #[tokio::test(flavor = "multi_thread")]
2948 async fn test_add_line_when_sketch_block_uses_variable() {
2949 let initial_source = "@settings(experimentalFeatures = allow)
2950
2951s = sketch(on = XY) {}
2952";
2953
2954 let program = Program::parse(initial_source).unwrap().0.unwrap();
2955
2956 let mut frontend = FrontendState::new();
2957
2958 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2959 let mock_ctx = ExecutorContext::new_mock(None).await;
2960 let version = Version(0);
2961
2962 frontend.hack_set_program(&ctx, program).await.unwrap();
2963 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
2964
2965 let line_ctor = LineCtor {
2966 start: Point2d {
2967 x: Expr::Number(Number {
2968 value: 0.0,
2969 units: NumericSuffix::Mm,
2970 }),
2971 y: Expr::Number(Number {
2972 value: 0.0,
2973 units: NumericSuffix::Mm,
2974 }),
2975 },
2976 end: Point2d {
2977 x: Expr::Number(Number {
2978 value: 10.0,
2979 units: NumericSuffix::Mm,
2980 }),
2981 y: Expr::Number(Number {
2982 value: 10.0,
2983 units: NumericSuffix::Mm,
2984 }),
2985 },
2986 };
2987 let segment = SegmentCtor::Line(line_ctor);
2988 let (src_delta, scene_delta) = frontend
2989 .add_segment(&mock_ctx, version, sketch_id, segment, None)
2990 .await
2991 .unwrap();
2992 assert_eq!(
2993 src_delta.text.as_str(),
2994 "@settings(experimentalFeatures = allow)
2995
2996s = sketch(on = XY) {
2997 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
2998}
2999"
3000 );
3001 assert_eq!(scene_delta.new_objects, vec![ObjectId(1), ObjectId(2), ObjectId(3)]);
3002 assert_eq!(scene_delta.new_graph.objects.len(), 4);
3003
3004 ctx.close().await;
3005 mock_ctx.close().await;
3006 }
3007
3008 #[tokio::test(flavor = "multi_thread")]
3009 async fn test_new_sketch_add_line_delete_sketch() {
3010 let program = Program::empty();
3011
3012 let mut frontend = FrontendState::new();
3013 frontend.program = program;
3014
3015 let mock_ctx = ExecutorContext::new_mock(None).await;
3016 let version = Version(0);
3017
3018 let sketch_args = SketchArgs {
3019 on: api::Plane::Default(PlaneName::Xy),
3020 };
3021 let (_src_delta, scene_delta, sketch_id) = frontend
3022 .new_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_args)
3023 .await
3024 .unwrap();
3025 assert_eq!(sketch_id, ObjectId(0));
3026 assert_eq!(scene_delta.new_objects, vec![ObjectId(0)]);
3027 let sketch_object = &scene_delta.new_graph.objects[0];
3028 assert_eq!(sketch_object.id, ObjectId(0));
3029 assert_eq!(
3030 sketch_object.kind,
3031 ObjectKind::Sketch(Sketch {
3032 args: SketchArgs {
3033 on: Plane::Default(PlaneName::Xy)
3034 },
3035 segments: vec![],
3036 constraints: vec![],
3037 })
3038 );
3039 assert_eq!(scene_delta.new_graph.objects.len(), 1);
3040
3041 let line_ctor = LineCtor {
3042 start: Point2d {
3043 x: Expr::Number(Number {
3044 value: 0.0,
3045 units: NumericSuffix::Mm,
3046 }),
3047 y: Expr::Number(Number {
3048 value: 0.0,
3049 units: NumericSuffix::Mm,
3050 }),
3051 },
3052 end: Point2d {
3053 x: Expr::Number(Number {
3054 value: 10.0,
3055 units: NumericSuffix::Mm,
3056 }),
3057 y: Expr::Number(Number {
3058 value: 10.0,
3059 units: NumericSuffix::Mm,
3060 }),
3061 },
3062 };
3063 let segment = SegmentCtor::Line(line_ctor);
3064 let (src_delta, scene_delta) = frontend
3065 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3066 .await
3067 .unwrap();
3068 assert_eq!(
3069 src_delta.text.as_str(),
3070 "@settings(experimentalFeatures = allow)
3071
3072sketch(on = XY) {
3073 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
3074}
3075"
3076 );
3077 assert_eq!(scene_delta.new_graph.objects.len(), 4);
3078
3079 let (src_delta, scene_delta) = frontend.delete_sketch(&mock_ctx, version, sketch_id).await.unwrap();
3080 assert_eq!(
3081 src_delta.text.as_str(),
3082 "@settings(experimentalFeatures = allow)
3083"
3084 );
3085 assert_eq!(scene_delta.new_graph.objects.len(), 0);
3086
3087 mock_ctx.close().await;
3088 }
3089
3090 #[tokio::test(flavor = "multi_thread")]
3091 async fn test_delete_sketch_when_sketch_block_uses_variable() {
3092 let initial_source = "@settings(experimentalFeatures = allow)
3093
3094s = sketch(on = XY) {}
3095";
3096
3097 let program = Program::parse(initial_source).unwrap().0.unwrap();
3098
3099 let mut frontend = FrontendState::new();
3100
3101 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3102 let mock_ctx = ExecutorContext::new_mock(None).await;
3103 let version = Version(0);
3104
3105 frontend.hack_set_program(&ctx, program).await.unwrap();
3106 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3107
3108 let (src_delta, scene_delta) = frontend.delete_sketch(&mock_ctx, version, sketch_id).await.unwrap();
3109 assert_eq!(
3110 src_delta.text.as_str(),
3111 "@settings(experimentalFeatures = allow)
3112"
3113 );
3114 assert_eq!(scene_delta.new_graph.objects.len(), 0);
3115
3116 ctx.close().await;
3117 mock_ctx.close().await;
3118 }
3119
3120 #[tokio::test(flavor = "multi_thread")]
3121 async fn test_edit_line_when_editing_its_start_point() {
3122 let initial_source = "\
3123@settings(experimentalFeatures = allow)
3124
3125sketch(on = XY) {
3126 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3127}
3128";
3129
3130 let program = Program::parse(initial_source).unwrap().0.unwrap();
3131
3132 let mut frontend = FrontendState::new();
3133
3134 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3135 let mock_ctx = ExecutorContext::new_mock(None).await;
3136 let version = Version(0);
3137
3138 frontend.hack_set_program(&ctx, program).await.unwrap();
3139 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3140
3141 let point_id = frontend.scene_graph.objects.get(1).unwrap().id;
3142
3143 let point_ctor = PointCtor {
3144 position: Point2d {
3145 x: Expr::Var(Number {
3146 value: 5.0,
3147 units: NumericSuffix::Inch,
3148 }),
3149 y: Expr::Var(Number {
3150 value: 6.0,
3151 units: NumericSuffix::Inch,
3152 }),
3153 },
3154 };
3155 let segments = vec![ExistingSegmentCtor {
3156 id: point_id,
3157 ctor: SegmentCtor::Point(point_ctor),
3158 }];
3159 let (src_delta, scene_delta) = frontend
3160 .edit_segments(&mock_ctx, version, sketch_id, segments)
3161 .await
3162 .unwrap();
3163 assert_eq!(
3164 src_delta.text.as_str(),
3165 "\
3166@settings(experimentalFeatures = allow)
3167
3168sketch(on = XY) {
3169 sketch2::line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
3170}
3171"
3172 );
3173 assert_eq!(scene_delta.new_objects, vec![]);
3174 assert_eq!(scene_delta.new_graph.objects.len(), 4);
3175
3176 ctx.close().await;
3177 mock_ctx.close().await;
3178 }
3179
3180 #[tokio::test(flavor = "multi_thread")]
3181 async fn test_edit_line_when_editing_its_end_point() {
3182 let initial_source = "\
3183@settings(experimentalFeatures = allow)
3184
3185sketch(on = XY) {
3186 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3187}
3188";
3189
3190 let program = Program::parse(initial_source).unwrap().0.unwrap();
3191
3192 let mut frontend = FrontendState::new();
3193
3194 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3195 let mock_ctx = ExecutorContext::new_mock(None).await;
3196 let version = Version(0);
3197
3198 frontend.hack_set_program(&ctx, program).await.unwrap();
3199 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3200
3201 let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
3202
3203 let point_ctor = PointCtor {
3204 position: Point2d {
3205 x: Expr::Var(Number {
3206 value: 5.0,
3207 units: NumericSuffix::Inch,
3208 }),
3209 y: Expr::Var(Number {
3210 value: 6.0,
3211 units: NumericSuffix::Inch,
3212 }),
3213 },
3214 };
3215 let segments = vec![ExistingSegmentCtor {
3216 id: point_id,
3217 ctor: SegmentCtor::Point(point_ctor),
3218 }];
3219 let (src_delta, scene_delta) = frontend
3220 .edit_segments(&mock_ctx, version, sketch_id, segments)
3221 .await
3222 .unwrap();
3223 assert_eq!(
3224 src_delta.text.as_str(),
3225 "\
3226@settings(experimentalFeatures = allow)
3227
3228sketch(on = XY) {
3229 sketch2::line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
3230}
3231"
3232 );
3233 assert_eq!(scene_delta.new_objects, vec![]);
3234 assert_eq!(scene_delta.new_graph.objects.len(), 4);
3235
3236 ctx.close().await;
3237 mock_ctx.close().await;
3238 }
3239
3240 #[tokio::test(flavor = "multi_thread")]
3241 async fn test_edit_line_with_coincident_feedback() {
3242 let initial_source = "\
3243@settings(experimentalFeatures = allow)
3244
3245sketch(on = XY) {
3246 line1 = sketch2::line(start = [var 1, var 2], end = [var 1, var 2])
3247 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3248 line1.start.at[0] == 0
3249 line1.start.at[1] == 0
3250 sketch2::coincident([line1.end, line2.start])
3251 sketch2::equalLength([line1, line2])
3252}
3253";
3254
3255 let program = Program::parse(initial_source).unwrap().0.unwrap();
3256
3257 let mut frontend = FrontendState::new();
3258
3259 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3260 let mock_ctx = ExecutorContext::new_mock(None).await;
3261 let version = Version(0);
3262
3263 frontend.hack_set_program(&ctx, program).await.unwrap();
3264 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3265 let line2_end_id = frontend.scene_graph.objects.get(5).unwrap().id;
3266
3267 let segments = vec![ExistingSegmentCtor {
3268 id: line2_end_id,
3269 ctor: SegmentCtor::Point(PointCtor {
3270 position: Point2d {
3271 x: Expr::Var(Number {
3272 value: 9.0,
3273 units: NumericSuffix::None,
3274 }),
3275 y: Expr::Var(Number {
3276 value: 10.0,
3277 units: NumericSuffix::None,
3278 }),
3279 },
3280 }),
3281 }];
3282 let (src_delta, scene_delta) = frontend
3283 .edit_segments(&mock_ctx, version, sketch_id, segments)
3284 .await
3285 .unwrap();
3286 assert_eq!(
3287 src_delta.text.as_str(),
3288 "\
3289@settings(experimentalFeatures = allow)
3290
3291sketch(on = XY) {
3292 line1 = sketch2::line(start = [var -0mm, var -0mm], end = [var 4.145mm, var 5.32mm])
3293 line2 = sketch2::line(start = [var 4.145mm, var 5.32mm], end = [var 9mm, var 10mm])
3294line1.start.at[0] == 0
3295line1.start.at[1] == 0
3296 sketch2::coincident([line1.end, line2.start])
3297 sketch2::equalLength([line1, line2])
3298}
3299"
3300 );
3301 assert_eq!(
3302 scene_delta.new_graph.objects.len(),
3303 9,
3304 "{:#?}",
3305 scene_delta.new_graph.objects
3306 );
3307
3308 ctx.close().await;
3309 mock_ctx.close().await;
3310 }
3311
3312 #[tokio::test(flavor = "multi_thread")]
3313 async fn test_delete_point_without_var() {
3314 let initial_source = "\
3315@settings(experimentalFeatures = allow)
3316
3317sketch(on = XY) {
3318 sketch2::point(at = [var 1, var 2])
3319 sketch2::point(at = [var 3, var 4])
3320 sketch2::point(at = [var 5, var 6])
3321}
3322";
3323
3324 let program = Program::parse(initial_source).unwrap().0.unwrap();
3325
3326 let mut frontend = FrontendState::new();
3327
3328 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3329 let mock_ctx = ExecutorContext::new_mock(None).await;
3330 let version = Version(0);
3331
3332 frontend.hack_set_program(&ctx, program).await.unwrap();
3333 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3334
3335 let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
3336
3337 let (src_delta, scene_delta) = frontend
3338 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
3339 .await
3340 .unwrap();
3341 assert_eq!(
3342 src_delta.text.as_str(),
3343 "\
3344@settings(experimentalFeatures = allow)
3345
3346sketch(on = XY) {
3347 sketch2::point(at = [var 1mm, var 2mm])
3348 sketch2::point(at = [var 5mm, var 6mm])
3349}
3350"
3351 );
3352 assert_eq!(scene_delta.new_objects, vec![]);
3353 assert_eq!(scene_delta.new_graph.objects.len(), 3);
3354
3355 ctx.close().await;
3356 mock_ctx.close().await;
3357 }
3358
3359 #[tokio::test(flavor = "multi_thread")]
3360 async fn test_delete_point_with_var() {
3361 let initial_source = "\
3362@settings(experimentalFeatures = allow)
3363
3364sketch(on = XY) {
3365 sketch2::point(at = [var 1, var 2])
3366 point1 = sketch2::point(at = [var 3, var 4])
3367 sketch2::point(at = [var 5, var 6])
3368}
3369";
3370
3371 let program = Program::parse(initial_source).unwrap().0.unwrap();
3372
3373 let mut frontend = FrontendState::new();
3374
3375 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3376 let mock_ctx = ExecutorContext::new_mock(None).await;
3377 let version = Version(0);
3378
3379 frontend.hack_set_program(&ctx, program).await.unwrap();
3380 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3381
3382 let point_id = frontend.scene_graph.objects.get(2).unwrap().id;
3383
3384 let (src_delta, scene_delta) = frontend
3385 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
3386 .await
3387 .unwrap();
3388 assert_eq!(
3389 src_delta.text.as_str(),
3390 "\
3391@settings(experimentalFeatures = allow)
3392
3393sketch(on = XY) {
3394 sketch2::point(at = [var 1mm, var 2mm])
3395 sketch2::point(at = [var 5mm, var 6mm])
3396}
3397"
3398 );
3399 assert_eq!(scene_delta.new_objects, vec![]);
3400 assert_eq!(scene_delta.new_graph.objects.len(), 3);
3401
3402 ctx.close().await;
3403 mock_ctx.close().await;
3404 }
3405
3406 #[tokio::test(flavor = "multi_thread")]
3407 async fn test_delete_multiple_points() {
3408 let initial_source = "\
3409@settings(experimentalFeatures = allow)
3410
3411sketch(on = XY) {
3412 sketch2::point(at = [var 1, var 2])
3413 point1 = sketch2::point(at = [var 3, var 4])
3414 sketch2::point(at = [var 5, var 6])
3415}
3416";
3417
3418 let program = Program::parse(initial_source).unwrap().0.unwrap();
3419
3420 let mut frontend = FrontendState::new();
3421
3422 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3423 let mock_ctx = ExecutorContext::new_mock(None).await;
3424 let version = Version(0);
3425
3426 frontend.hack_set_program(&ctx, program).await.unwrap();
3427 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3428
3429 let point1_id = frontend.scene_graph.objects.get(1).unwrap().id;
3430 let point2_id = frontend.scene_graph.objects.get(2).unwrap().id;
3431
3432 let (src_delta, scene_delta) = frontend
3433 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
3434 .await
3435 .unwrap();
3436 assert_eq!(
3437 src_delta.text.as_str(),
3438 "\
3439@settings(experimentalFeatures = allow)
3440
3441sketch(on = XY) {
3442 sketch2::point(at = [var 5mm, var 6mm])
3443}
3444"
3445 );
3446 assert_eq!(scene_delta.new_objects, vec![]);
3447 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3448
3449 ctx.close().await;
3450 mock_ctx.close().await;
3451 }
3452
3453 #[tokio::test(flavor = "multi_thread")]
3454 async fn test_delete_coincident_constraint() {
3455 let initial_source = "\
3456@settings(experimentalFeatures = allow)
3457
3458sketch(on = XY) {
3459 point1 = sketch2::point(at = [var 1, var 2])
3460 point2 = sketch2::point(at = [var 3, var 4])
3461 sketch2::coincident([point1, point2])
3462 sketch2::point(at = [var 5, var 6])
3463}
3464";
3465
3466 let program = Program::parse(initial_source).unwrap().0.unwrap();
3467
3468 let mut frontend = FrontendState::new();
3469
3470 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3471 let mock_ctx = ExecutorContext::new_mock(None).await;
3472 let version = Version(0);
3473
3474 frontend.hack_set_program(&ctx, program).await.unwrap();
3475 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3476
3477 let coincident_id = frontend.scene_graph.objects.get(3).unwrap().id;
3478
3479 let (src_delta, scene_delta) = frontend
3480 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
3481 .await
3482 .unwrap();
3483 assert_eq!(
3484 src_delta.text.as_str(),
3485 "\
3486@settings(experimentalFeatures = allow)
3487
3488sketch(on = XY) {
3489 point1 = sketch2::point(at = [var 1mm, var 2mm])
3490 point2 = sketch2::point(at = [var 3mm, var 4mm])
3491 sketch2::point(at = [var 5mm, var 6mm])
3492}
3493"
3494 );
3495 assert_eq!(scene_delta.new_objects, vec![]);
3496 assert_eq!(scene_delta.new_graph.objects.len(), 4);
3497
3498 ctx.close().await;
3499 mock_ctx.close().await;
3500 }
3501
3502 #[tokio::test(flavor = "multi_thread")]
3503 async fn test_delete_line_cascades_to_coincident_constraint() {
3504 let initial_source = "\
3505@settings(experimentalFeatures = allow)
3506
3507sketch(on = XY) {
3508 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3509 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3510 sketch2::coincident([line1.end, line2.start])
3511}
3512";
3513
3514 let program = Program::parse(initial_source).unwrap().0.unwrap();
3515
3516 let mut frontend = FrontendState::new();
3517
3518 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3519 let mock_ctx = ExecutorContext::new_mock(None).await;
3520 let version = Version(0);
3521
3522 frontend.hack_set_program(&ctx, program).await.unwrap();
3523 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3524 let line_id = frontend.scene_graph.objects.get(6).unwrap().id;
3525
3526 let (src_delta, scene_delta) = frontend
3527 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
3528 .await
3529 .unwrap();
3530 assert_eq!(
3531 src_delta.text.as_str(),
3532 "\
3533@settings(experimentalFeatures = allow)
3534
3535sketch(on = XY) {
3536 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3537}
3538"
3539 );
3540 assert_eq!(
3541 scene_delta.new_graph.objects.len(),
3542 4,
3543 "{:#?}",
3544 scene_delta.new_graph.objects
3545 );
3546
3547 ctx.close().await;
3548 mock_ctx.close().await;
3549 }
3550
3551 #[tokio::test(flavor = "multi_thread")]
3552 async fn test_delete_line_cascades_to_distance_constraint() {
3553 let initial_source = "\
3554@settings(experimentalFeatures = allow)
3555
3556sketch(on = XY) {
3557 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3558 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3559 sketch2::distance([line1.end, line2.start]) == 10mm
3560}
3561";
3562
3563 let program = Program::parse(initial_source).unwrap().0.unwrap();
3564
3565 let mut frontend = FrontendState::new();
3566
3567 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3568 let mock_ctx = ExecutorContext::new_mock(None).await;
3569 let version = Version(0);
3570
3571 frontend.hack_set_program(&ctx, program).await.unwrap();
3572 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3573 let line_id = frontend.scene_graph.objects.get(6).unwrap().id;
3574
3575 let (src_delta, scene_delta) = frontend
3576 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
3577 .await
3578 .unwrap();
3579 assert_eq!(
3580 src_delta.text.as_str(),
3581 "\
3582@settings(experimentalFeatures = allow)
3583
3584sketch(on = XY) {
3585 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3586}
3587"
3588 );
3589 assert_eq!(
3590 scene_delta.new_graph.objects.len(),
3591 4,
3592 "{:#?}",
3593 scene_delta.new_graph.objects
3594 );
3595
3596 ctx.close().await;
3597 mock_ctx.close().await;
3598 }
3599
3600 #[tokio::test(flavor = "multi_thread")]
3601 async fn test_two_points_coincident() {
3602 let initial_source = "\
3603@settings(experimentalFeatures = allow)
3604
3605sketch(on = XY) {
3606 point1 = sketch2::point(at = [var 1, var 2])
3607 sketch2::point(at = [3, 4])
3608}
3609";
3610
3611 let program = Program::parse(initial_source).unwrap().0.unwrap();
3612
3613 let mut frontend = FrontendState::new();
3614
3615 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3616 let mock_ctx = ExecutorContext::new_mock(None).await;
3617 let version = Version(0);
3618
3619 frontend.hack_set_program(&ctx, program).await.unwrap();
3620 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3621 let point0_id = frontend.scene_graph.objects.get(1).unwrap().id;
3622 let point1_id = frontend.scene_graph.objects.get(2).unwrap().id;
3623
3624 let constraint = Constraint::Coincident(Coincident {
3625 segments: vec![point0_id, point1_id],
3626 });
3627 let (src_delta, scene_delta) = frontend
3628 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3629 .await
3630 .unwrap();
3631 assert_eq!(
3632 src_delta.text.as_str(),
3633 "\
3634@settings(experimentalFeatures = allow)
3635
3636sketch(on = XY) {
3637 point1 = sketch2::point(at = [var 1, var 2])
3638 point2 = sketch2::point(at = [3, 4])
3639 sketch2::coincident([point1, point2])
3640}
3641"
3642 );
3643 assert_eq!(
3644 scene_delta.new_graph.objects.len(),
3645 4,
3646 "{:#?}",
3647 scene_delta.new_graph.objects
3648 );
3649
3650 ctx.close().await;
3651 mock_ctx.close().await;
3652 }
3653
3654 #[tokio::test(flavor = "multi_thread")]
3655 async fn test_coincident_of_line_end_points() {
3656 let initial_source = "\
3657@settings(experimentalFeatures = allow)
3658
3659sketch(on = XY) {
3660 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3661 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3662}
3663";
3664
3665 let program = Program::parse(initial_source).unwrap().0.unwrap();
3666
3667 let mut frontend = FrontendState::new();
3668
3669 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3670 let mock_ctx = ExecutorContext::new_mock(None).await;
3671 let version = Version(0);
3672
3673 frontend.hack_set_program(&ctx, program).await.unwrap();
3674 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3675 let point0_id = frontend.scene_graph.objects.get(2).unwrap().id;
3676 let point1_id = frontend.scene_graph.objects.get(4).unwrap().id;
3677
3678 let constraint = Constraint::Coincident(Coincident {
3679 segments: vec![point0_id, point1_id],
3680 });
3681 let (src_delta, scene_delta) = frontend
3682 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3683 .await
3684 .unwrap();
3685 assert_eq!(
3686 src_delta.text.as_str(),
3687 "\
3688@settings(experimentalFeatures = allow)
3689
3690sketch(on = XY) {
3691 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3692 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3693 sketch2::coincident([line1.end, line2.start])
3694}
3695"
3696 );
3697 assert_eq!(
3698 scene_delta.new_graph.objects.len(),
3699 8,
3700 "{:#?}",
3701 scene_delta.new_graph.objects
3702 );
3703
3704 ctx.close().await;
3705 mock_ctx.close().await;
3706 }
3707
3708 #[tokio::test(flavor = "multi_thread")]
3709 async fn test_distance_two_points() {
3710 let initial_source = "\
3711@settings(experimentalFeatures = allow)
3712
3713sketch(on = XY) {
3714 sketch2::point(at = [var 1, var 2])
3715 sketch2::point(at = [var 3, var 4])
3716}
3717";
3718
3719 let program = Program::parse(initial_source).unwrap().0.unwrap();
3720
3721 let mut frontend = FrontendState::new();
3722
3723 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3724 let mock_ctx = ExecutorContext::new_mock(None).await;
3725 let version = Version(0);
3726
3727 frontend.hack_set_program(&ctx, program).await.unwrap();
3728 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3729 let point0_id = frontend.scene_graph.objects.get(1).unwrap().id;
3730 let point1_id = frontend.scene_graph.objects.get(2).unwrap().id;
3731
3732 let constraint = Constraint::Distance(Distance {
3733 points: vec![point0_id, point1_id],
3734 distance: Number {
3735 value: 2.0,
3736 units: NumericSuffix::Mm,
3737 },
3738 });
3739 let (src_delta, scene_delta) = frontend
3740 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3741 .await
3742 .unwrap();
3743 assert_eq!(
3744 src_delta.text.as_str(),
3745 "\
3747@settings(experimentalFeatures = allow)
3748
3749sketch(on = XY) {
3750 point1 = sketch2::point(at = [var 1, var 2])
3751 point2 = sketch2::point(at = [var 3, var 4])
3752sketch2::distance([point1, point2]) == 2mm
3753}
3754"
3755 );
3756 assert_eq!(
3757 scene_delta.new_graph.objects.len(),
3758 4,
3759 "{:#?}",
3760 scene_delta.new_graph.objects
3761 );
3762
3763 ctx.close().await;
3764 mock_ctx.close().await;
3765 }
3766
3767 #[tokio::test(flavor = "multi_thread")]
3768 async fn test_line_horizontal() {
3769 let initial_source = "\
3770@settings(experimentalFeatures = allow)
3771
3772sketch(on = XY) {
3773 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3774}
3775";
3776
3777 let program = Program::parse(initial_source).unwrap().0.unwrap();
3778
3779 let mut frontend = FrontendState::new();
3780
3781 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3782 let mock_ctx = ExecutorContext::new_mock(None).await;
3783 let version = Version(0);
3784
3785 frontend.hack_set_program(&ctx, program).await.unwrap();
3786 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3787 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3788
3789 let constraint = Constraint::Horizontal(Horizontal { line: line1_id });
3790 let (src_delta, scene_delta) = frontend
3791 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3792 .await
3793 .unwrap();
3794 assert_eq!(
3795 src_delta.text.as_str(),
3796 "\
3797@settings(experimentalFeatures = allow)
3798
3799sketch(on = XY) {
3800 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3801 sketch2::horizontal(line1)
3802}
3803"
3804 );
3805 assert_eq!(
3806 scene_delta.new_graph.objects.len(),
3807 5,
3808 "{:#?}",
3809 scene_delta.new_graph.objects
3810 );
3811
3812 ctx.close().await;
3813 mock_ctx.close().await;
3814 }
3815
3816 #[tokio::test(flavor = "multi_thread")]
3817 async fn test_line_vertical() {
3818 let initial_source = "\
3819@settings(experimentalFeatures = allow)
3820
3821sketch(on = XY) {
3822 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3823}
3824";
3825
3826 let program = Program::parse(initial_source).unwrap().0.unwrap();
3827
3828 let mut frontend = FrontendState::new();
3829
3830 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3831 let mock_ctx = ExecutorContext::new_mock(None).await;
3832 let version = Version(0);
3833
3834 frontend.hack_set_program(&ctx, program).await.unwrap();
3835 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3836 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3837
3838 let constraint = Constraint::Vertical(Vertical { line: line1_id });
3839 let (src_delta, scene_delta) = frontend
3840 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3841 .await
3842 .unwrap();
3843 assert_eq!(
3844 src_delta.text.as_str(),
3845 "\
3846@settings(experimentalFeatures = allow)
3847
3848sketch(on = XY) {
3849 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3850 sketch2::vertical(line1)
3851}
3852"
3853 );
3854 assert_eq!(
3855 scene_delta.new_graph.objects.len(),
3856 5,
3857 "{:#?}",
3858 scene_delta.new_graph.objects
3859 );
3860
3861 ctx.close().await;
3862 mock_ctx.close().await;
3863 }
3864
3865 #[tokio::test(flavor = "multi_thread")]
3866 async fn test_lines_equal_length() {
3867 let initial_source = "\
3868@settings(experimentalFeatures = allow)
3869
3870sketch(on = XY) {
3871 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3872 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3873}
3874";
3875
3876 let program = Program::parse(initial_source).unwrap().0.unwrap();
3877
3878 let mut frontend = FrontendState::new();
3879
3880 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3881 let mock_ctx = ExecutorContext::new_mock(None).await;
3882 let version = Version(0);
3883
3884 frontend.hack_set_program(&ctx, program).await.unwrap();
3885 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3886 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3887 let line2_id = frontend.scene_graph.objects.get(6).unwrap().id;
3888
3889 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
3890 lines: vec![line1_id, line2_id],
3891 });
3892 let (src_delta, scene_delta) = frontend
3893 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3894 .await
3895 .unwrap();
3896 assert_eq!(
3897 src_delta.text.as_str(),
3898 "\
3899@settings(experimentalFeatures = allow)
3900
3901sketch(on = XY) {
3902 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3903 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3904 sketch2::equalLength([line1, line2])
3905}
3906"
3907 );
3908 assert_eq!(
3909 scene_delta.new_graph.objects.len(),
3910 8,
3911 "{:#?}",
3912 scene_delta.new_graph.objects
3913 );
3914
3915 ctx.close().await;
3916 mock_ctx.close().await;
3917 }
3918
3919 #[tokio::test(flavor = "multi_thread")]
3920 async fn test_lines_parallel() {
3921 let initial_source = "\
3922@settings(experimentalFeatures = allow)
3923
3924sketch(on = XY) {
3925 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3926 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3927}
3928";
3929
3930 let program = Program::parse(initial_source).unwrap().0.unwrap();
3931
3932 let mut frontend = FrontendState::new();
3933
3934 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3935 let mock_ctx = ExecutorContext::new_mock(None).await;
3936 let version = Version(0);
3937
3938 frontend.hack_set_program(&ctx, program).await.unwrap();
3939 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3940 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3941 let line2_id = frontend.scene_graph.objects.get(6).unwrap().id;
3942
3943 let constraint = Constraint::Parallel(Parallel {
3944 lines: vec![line1_id, line2_id],
3945 });
3946 let (src_delta, scene_delta) = frontend
3947 .add_constraint(&mock_ctx, version, sketch_id, constraint)
3948 .await
3949 .unwrap();
3950 assert_eq!(
3951 src_delta.text.as_str(),
3952 "\
3953@settings(experimentalFeatures = allow)
3954
3955sketch(on = XY) {
3956 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3957 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3958 sketch2::parallel([line1, line2])
3959}
3960"
3961 );
3962 assert_eq!(
3963 scene_delta.new_graph.objects.len(),
3964 8,
3965 "{:#?}",
3966 scene_delta.new_graph.objects
3967 );
3968
3969 ctx.close().await;
3970 mock_ctx.close().await;
3971 }
3972
3973 #[tokio::test(flavor = "multi_thread")]
3974 async fn test_lines_perpendicular() {
3975 let initial_source = "\
3976@settings(experimentalFeatures = allow)
3977
3978sketch(on = XY) {
3979 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3980 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3981}
3982";
3983
3984 let program = Program::parse(initial_source).unwrap().0.unwrap();
3985
3986 let mut frontend = FrontendState::new();
3987
3988 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3989 let mock_ctx = ExecutorContext::new_mock(None).await;
3990 let version = Version(0);
3991
3992 frontend.hack_set_program(&ctx, program).await.unwrap();
3993 let sketch_id = frontend.scene_graph.objects.first().unwrap().id;
3994 let line1_id = frontend.scene_graph.objects.get(3).unwrap().id;
3995 let line2_id = frontend.scene_graph.objects.get(6).unwrap().id;
3996
3997 let constraint = Constraint::Perpendicular(Perpendicular {
3998 lines: vec![line1_id, line2_id],
3999 });
4000 let (src_delta, scene_delta) = frontend
4001 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4002 .await
4003 .unwrap();
4004 assert_eq!(
4005 src_delta.text.as_str(),
4006 "\
4007@settings(experimentalFeatures = allow)
4008
4009sketch(on = XY) {
4010 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4011 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4012 sketch2::perpendicular([line1, line2])
4013}
4014"
4015 );
4016 assert_eq!(
4017 scene_delta.new_graph.objects.len(),
4018 8,
4019 "{:#?}",
4020 scene_delta.new_graph.objects
4021 );
4022
4023 ctx.close().await;
4024 mock_ctx.close().await;
4025 }
4026
4027 #[tokio::test(flavor = "multi_thread")]
4028 async fn test_multiple_sketch_blocks() {
4029 let initial_source = "\
4030@settings(experimentalFeatures = allow)
4031
4032// Cube that requires the engine.
4033width = 2
4034sketch001 = startSketchOn(XY)
4035profile001 = startProfile(sketch001, at = [0, 0])
4036 |> yLine(length = width, tag = $seg1)
4037 |> xLine(length = width)
4038 |> yLine(length = -width)
4039 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4040 |> close()
4041extrude001 = extrude(profile001, length = width)
4042
4043// Get a value that requires the engine.
4044x = segLen(seg1)
4045
4046// Triangle with side length 2*x.
4047sketch(on = XY) {
4048 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
4049 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
4050 sketch2::coincident([line1.end, line2.start])
4051 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
4052 sketch2::coincident([line2.end, line3.start])
4053 sketch2::coincident([line3.end, line1.start])
4054 sketch2::equalLength([line3, line1])
4055 sketch2::equalLength([line1, line2])
4056sketch2::distance([line1.start, line1.end]) == 2*x
4057}
4058
4059// Line segment with length x.
4060sketch2 = sketch(on = XY) {
4061 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
4062sketch2::distance([line1.start, line1.end]) == x
4063}
4064";
4065
4066 let program = Program::parse(initial_source).unwrap().0.unwrap();
4067
4068 let mut frontend = FrontendState::new();
4069
4070 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4071 let mock_ctx = ExecutorContext::new_mock(None).await;
4072 let version = Version(0);
4073 let project_id = ProjectId(0);
4074 let file_id = FileId(0);
4075
4076 frontend.hack_set_program(&ctx, program).await.unwrap();
4077 let sketch_objects = frontend
4078 .scene_graph
4079 .objects
4080 .iter()
4081 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
4082 .collect::<Vec<_>>();
4083 let sketch1_id = sketch_objects.first().unwrap().id;
4084 let sketch2_id = sketch_objects.get(1).unwrap().id;
4085 let point1_id = ObjectId(sketch1_id.0 + 1);
4087 let point2_id = ObjectId(sketch2_id.0 + 1);
4089
4090 let scene_delta = frontend
4096 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
4097 .await
4098 .unwrap();
4099 assert_eq!(
4100 scene_delta.new_graph.objects.len(),
4101 17,
4102 "{:#?}",
4103 scene_delta.new_graph.objects
4104 );
4105
4106 let point_ctor = PointCtor {
4108 position: Point2d {
4109 x: Expr::Var(Number {
4110 value: 1.0,
4111 units: NumericSuffix::Mm,
4112 }),
4113 y: Expr::Var(Number {
4114 value: 2.0,
4115 units: NumericSuffix::Mm,
4116 }),
4117 },
4118 };
4119 let segments = vec![ExistingSegmentCtor {
4120 id: point1_id,
4121 ctor: SegmentCtor::Point(point_ctor),
4122 }];
4123 let (src_delta, _) = frontend
4124 .edit_segments(&mock_ctx, version, sketch1_id, segments)
4125 .await
4126 .unwrap();
4127 assert_eq!(
4129 src_delta.text.as_str(),
4130 "\
4131@settings(experimentalFeatures = allow)
4132
4133// Cube that requires the engine.
4134width = 2
4135sketch001 = startSketchOn(XY)
4136profile001 = startProfile(sketch001, at = [0, 0])
4137 |> yLine(length = width, tag = $seg1)
4138 |> xLine(length = width)
4139 |> yLine(length = -width)
4140 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4141 |> close()
4142extrude001 = extrude(profile001, length = width)
4143
4144// Get a value that requires the engine.
4145x = segLen(seg1)
4146
4147// Triangle with side length 2*x.
4148sketch(on = XY) {
4149 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 2.317mm, var -1.777mm])
4150 line2 = sketch2::line(start = [var 2.317mm, var -1.777mm], end = [var -1.613mm, var -1.029mm])
4151 sketch2::coincident([line1.end, line2.start])
4152 line3 = sketch2::line(start = [var -1.613mm, var -1.029mm], end = [var 1mm, var 2mm])
4153 sketch2::coincident([line2.end, line3.start])
4154 sketch2::coincident([line3.end, line1.start])
4155 sketch2::equalLength([line3, line1])
4156 sketch2::equalLength([line1, line2])
4157sketch2::distance([line1.start, line1.end]) == 2 * x
4158}
4159
4160// Line segment with length x.
4161sketch2 = sketch(on = XY) {
4162 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
4163sketch2::distance([line1.start, line1.end]) == x
4164}
4165"
4166 );
4167
4168 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
4170 assert_eq!(
4172 src_delta.text.as_str(),
4173 "\
4174@settings(experimentalFeatures = allow)
4175
4176// Cube that requires the engine.
4177width = 2
4178sketch001 = startSketchOn(XY)
4179profile001 = startProfile(sketch001, at = [0, 0])
4180 |> yLine(length = width, tag = $seg1)
4181 |> xLine(length = width)
4182 |> yLine(length = -width)
4183 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4184 |> close()
4185extrude001 = extrude(profile001, length = width)
4186
4187// Get a value that requires the engine.
4188x = segLen(seg1)
4189
4190// Triangle with side length 2*x.
4191sketch(on = XY) {
4192 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.283mm, var -0.781mm])
4193 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
4194 sketch2::coincident([line1.end, line2.start])
4195 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
4196 sketch2::coincident([line2.end, line3.start])
4197 sketch2::coincident([line3.end, line1.start])
4198 sketch2::equalLength([line3, line1])
4199 sketch2::equalLength([line1, line2])
4200sketch2::distance([line1.start, line1.end]) == 2 * x
4201}
4202
4203// Line segment with length x.
4204sketch2 = sketch(on = XY) {
4205 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
4206sketch2::distance([line1.start, line1.end]) == x
4207}
4208"
4209 );
4210 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
4216 assert_eq!(scene.objects.len(), 22, "{:#?}", scene.objects);
4217
4218 let scene_delta = frontend
4225 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
4226 .await
4227 .unwrap();
4228 assert_eq!(
4229 scene_delta.new_graph.objects.len(),
4230 22,
4231 "{:#?}",
4232 scene_delta.new_graph.objects
4233 );
4234
4235 let point_ctor = PointCtor {
4237 position: Point2d {
4238 x: Expr::Var(Number {
4239 value: 3.0,
4240 units: NumericSuffix::Mm,
4241 }),
4242 y: Expr::Var(Number {
4243 value: 4.0,
4244 units: NumericSuffix::Mm,
4245 }),
4246 },
4247 };
4248 let segments = vec![ExistingSegmentCtor {
4249 id: point2_id,
4250 ctor: SegmentCtor::Point(point_ctor),
4251 }];
4252 let (src_delta, _) = frontend
4253 .edit_segments(&mock_ctx, version, sketch2_id, segments)
4254 .await
4255 .unwrap();
4256 assert_eq!(
4258 src_delta.text.as_str(),
4259 "\
4260@settings(experimentalFeatures = allow)
4261
4262// Cube that requires the engine.
4263width = 2
4264sketch001 = startSketchOn(XY)
4265profile001 = startProfile(sketch001, at = [0, 0])
4266 |> yLine(length = width, tag = $seg1)
4267 |> xLine(length = width)
4268 |> yLine(length = -width)
4269 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4270 |> close()
4271extrude001 = extrude(profile001, length = width)
4272
4273// Get a value that requires the engine.
4274x = segLen(seg1)
4275
4276// Triangle with side length 2*x.
4277sketch(on = XY) {
4278 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.283mm, var -0.781mm])
4279 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
4280 sketch2::coincident([line1.end, line2.start])
4281 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
4282 sketch2::coincident([line2.end, line3.start])
4283 sketch2::coincident([line3.end, line1.start])
4284 sketch2::equalLength([line3, line1])
4285 sketch2::equalLength([line1, line2])
4286sketch2::distance([line1.start, line1.end]) == 2 * x
4287}
4288
4289// Line segment with length x.
4290sketch2 = sketch(on = XY) {
4291 line1 = sketch2::line(start = [var 3mm, var 4mm], end = [var 2.324mm, var 2.118mm])
4292sketch2::distance([line1.start, line1.end]) == x
4293}
4294"
4295 );
4296
4297 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
4299 assert_eq!(
4301 src_delta.text.as_str(),
4302 "\
4303@settings(experimentalFeatures = allow)
4304
4305// Cube that requires the engine.
4306width = 2
4307sketch001 = startSketchOn(XY)
4308profile001 = startProfile(sketch001, at = [0, 0])
4309 |> yLine(length = width, tag = $seg1)
4310 |> xLine(length = width)
4311 |> yLine(length = -width)
4312 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4313 |> close()
4314extrude001 = extrude(profile001, length = width)
4315
4316// Get a value that requires the engine.
4317x = segLen(seg1)
4318
4319// Triangle with side length 2*x.
4320sketch(on = XY) {
4321 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.283mm, var -0.781mm])
4322 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
4323 sketch2::coincident([line1.end, line2.start])
4324 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
4325 sketch2::coincident([line2.end, line3.start])
4326 sketch2::coincident([line3.end, line1.start])
4327 sketch2::equalLength([line3, line1])
4328 sketch2::equalLength([line1, line2])
4329sketch2::distance([line1.start, line1.end]) == 2 * x
4330}
4331
4332// Line segment with length x.
4333sketch2 = sketch(on = XY) {
4334 line1 = sketch2::line(start = [var 3mm, var 4mm], end = [var 1.283mm, var -0.781mm])
4335sketch2::distance([line1.start, line1.end]) == x
4336}
4337"
4338 );
4339
4340 ctx.close().await;
4341 mock_ctx.close().await;
4342 }
4343}