1use std::{
2 cell::Cell,
3 collections::{HashMap, HashSet},
4 ops::ControlFlow,
5};
6
7use indexmap::IndexMap;
8use kcl_error::SourceRange;
9
10use crate::{
11 ExecOutcome, ExecutorContext, Program,
12 collections::AhashIndexSet,
13 exec::WarningLevel,
14 execution::MockConfig,
15 fmt::format_number_literal,
16 front::{ArcCtor, Distance, Freedom, LinesEqualLength, Parallel, Perpendicular, PointCtor},
17 frontend::{
18 api::{
19 Error, Expr, FileId, Number, ObjectId, ObjectKind, ProjectId, SceneGraph, SceneGraphDelta, SourceDelta,
20 SourceRef, Version,
21 },
22 modify::{find_defined_names, next_free_name},
23 sketch::{
24 Coincident, Constraint, ExistingSegmentCtor, Horizontal, LineCtor, Point2d, Segment, SegmentCtor,
25 SketchApi, SketchCtor, Vertical,
26 },
27 traverse::{MutateBodyItem, TraversalReturn, Visitor, dfs_mut},
28 },
29 parsing::ast::types as ast,
30 std::constraints::LinesAtAngleKind,
31 walk::{NodeMut, Visitable},
32};
33
34pub(crate) mod api;
35pub(crate) mod modify;
36pub(crate) mod sketch;
37mod traverse;
38
39const POINT_FN: &str = "point";
40const POINT_AT_PARAM: &str = "at";
41const LINE_FN: &str = "line";
42const LINE_START_PARAM: &str = "start";
43const LINE_END_PARAM: &str = "end";
44const ARC_FN: &str = "arc";
45const ARC_START_PARAM: &str = "start";
46const ARC_END_PARAM: &str = "end";
47const ARC_CENTER_PARAM: &str = "center";
48
49const COINCIDENT_FN: &str = "coincident";
50const DISTANCE_FN: &str = "distance";
51const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
52const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
53const EQUAL_LENGTH_FN: &str = "equalLength";
54const HORIZONTAL_FN: &str = "horizontal";
55const VERTICAL_FN: &str = "vertical";
56
57const LINE_PROPERTY_START: &str = "start";
58const LINE_PROPERTY_END: &str = "end";
59
60const ARC_PROPERTY_START: &str = "start";
61const ARC_PROPERTY_END: &str = "end";
62const ARC_PROPERTY_CENTER: &str = "center";
63
64const CONSTRUCTION_PARAM: &str = "construction";
65
66#[derive(Debug, Clone, Copy)]
67enum EditDeleteKind {
68 Edit,
69 DeleteNonSketch,
70}
71
72impl EditDeleteKind {
73 fn is_delete(&self) -> bool {
75 match self {
76 EditDeleteKind::Edit => false,
77 EditDeleteKind::DeleteNonSketch => true,
78 }
79 }
80
81 fn to_change_kind(self) -> ChangeKind {
82 match self {
83 EditDeleteKind::Edit => ChangeKind::Edit,
84 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy)]
90enum ChangeKind {
91 Add,
92 Edit,
93 Delete,
94 None,
95}
96
97#[derive(Debug, Clone)]
98pub struct FrontendState {
99 program: Program,
100 scene_graph: SceneGraph,
101 point_freedom_cache: HashMap<ObjectId, Freedom>,
104}
105
106impl Default for FrontendState {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl FrontendState {
113 pub fn new() -> Self {
114 Self {
115 program: Program::empty(),
116 scene_graph: SceneGraph {
117 project: ProjectId(0),
118 file: FileId(0),
119 version: Version(0),
120 objects: Default::default(),
121 settings: Default::default(),
122 sketch_mode: Default::default(),
123 },
124 point_freedom_cache: HashMap::new(),
125 }
126 }
127}
128
129impl SketchApi for FrontendState {
130 async fn execute_mock(
131 &mut self,
132 ctx: &ExecutorContext,
133 _version: Version,
134 sketch: ObjectId,
135 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
136 let mut truncated_program = self.program.clone();
137 self.only_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
138
139 let outcome = ctx
141 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
142 .await
143 .map_err(|err| Error {
144 msg: err.error.message().to_owned(),
145 })?;
146 let new_source = source_from_ast(&self.program.ast);
147 let src_delta = SourceDelta { text: new_source };
148 let outcome = self.update_state_after_exec(outcome, true);
150 let scene_graph_delta = SceneGraphDelta {
151 new_graph: self.scene_graph.clone(),
152 new_objects: Default::default(),
153 invalidates_ids: false,
154 exec_outcome: outcome,
155 };
156 Ok((src_delta, scene_graph_delta))
157 }
158
159 async fn new_sketch(
160 &mut self,
161 ctx: &ExecutorContext,
162 _project: ProjectId,
163 _file: FileId,
164 _version: Version,
165 args: SketchCtor,
166 ) -> api::Result<(SourceDelta, SceneGraphDelta, ObjectId)> {
167 let plane_ast = ast_name_expr(args.on);
171 let sketch_ast = ast::SketchBlock {
172 arguments: vec![ast::LabeledArg {
173 label: Some(ast::Identifier::new("on")),
174 arg: plane_ast,
175 }],
176 body: Default::default(),
177 is_being_edited: false,
178 non_code_meta: Default::default(),
179 digest: None,
180 };
181 let mut new_ast = self.program.ast.clone();
182 new_ast.set_experimental_features(Some(WarningLevel::Allow));
185 new_ast.body.push(ast::BodyItem::ExpressionStatement(ast::Node {
187 inner: ast::ExpressionStatement {
188 expression: ast::Expr::SketchBlock(Box::new(ast::Node {
189 inner: sketch_ast,
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 digest: None,
198 },
199 start: Default::default(),
200 end: Default::default(),
201 module_id: Default::default(),
202 outer_attrs: Default::default(),
203 pre_comments: Default::default(),
204 comment_start: Default::default(),
205 }));
206 let new_source = source_from_ast(&new_ast);
208 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
210 if !errors.is_empty() {
211 return Err(Error {
212 msg: format!("Error parsing KCL source after adding sketch: {errors:?}"),
213 });
214 }
215 let Some(new_program) = new_program else {
216 return Err(Error {
217 msg: "No AST produced after adding sketch".to_owned(),
218 });
219 };
220
221 self.program = new_program.clone();
223
224 let outcome = ctx.run_with_caching(new_program.clone()).await.map_err(|err| Error {
227 msg: err.error.message().to_owned(),
228 })?;
229 let freedom_analysis_ran = true;
230
231 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
232
233 let Some(sketch_id) = self.scene_graph.objects.last().map(|object| object.id) else {
234 return Err(Error {
235 msg: "No objects in scene graph after adding sketch".to_owned(),
236 });
237 };
238 self.scene_graph.sketch_mode = Some(sketch_id);
240
241 let src_delta = SourceDelta { text: new_source };
242 let scene_graph_delta = SceneGraphDelta {
243 new_graph: self.scene_graph.clone(),
244 invalidates_ids: false,
245 new_objects: vec![sketch_id],
246 exec_outcome: outcome,
247 };
248 Ok((src_delta, scene_graph_delta, sketch_id))
249 }
250
251 async fn edit_sketch(
252 &mut self,
253 ctx: &ExecutorContext,
254 _project: ProjectId,
255 _file: FileId,
256 _version: Version,
257 sketch: ObjectId,
258 ) -> api::Result<SceneGraphDelta> {
259 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| Error {
263 msg: format!("Sketch not found: {sketch:?}"),
264 })?;
265 let ObjectKind::Sketch(_) = &sketch_object.kind else {
266 return Err(Error {
267 msg: format!("Object is not a sketch: {sketch_object:?}"),
268 });
269 };
270
271 self.scene_graph.sketch_mode = Some(sketch);
273
274 let mut truncated_program = self.program.clone();
276 self.only_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
277
278 let outcome = ctx
281 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
282 .await
283 .map_err(|err| {
284 Error {
287 msg: err.error.message().to_owned(),
288 }
289 })?;
290
291 let outcome = self.update_state_after_exec(outcome, true);
293 let scene_graph_delta = SceneGraphDelta {
294 new_graph: self.scene_graph.clone(),
295 invalidates_ids: false,
296 new_objects: Vec::new(),
297 exec_outcome: outcome,
298 };
299 Ok(scene_graph_delta)
300 }
301
302 async fn exit_sketch(
303 &mut self,
304 ctx: &ExecutorContext,
305 _version: Version,
306 sketch: ObjectId,
307 ) -> api::Result<SceneGraph> {
308 #[cfg(not(target_arch = "wasm32"))]
310 let _ = sketch;
311 #[cfg(target_arch = "wasm32")]
312 if self.scene_graph.sketch_mode != Some(sketch) {
313 web_sys::console::warn_1(
314 &format!(
315 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
316 &self.scene_graph.sketch_mode
317 )
318 .into(),
319 );
320 }
321 self.scene_graph.sketch_mode = None;
322
323 let outcome = ctx.run_with_caching(self.program.clone()).await.map_err(|err| {
325 Error {
328 msg: err.error.message().to_owned(),
329 }
330 })?;
331
332 self.update_state_after_exec(outcome, false);
334
335 Ok(self.scene_graph.clone())
336 }
337
338 async fn delete_sketch(
339 &mut self,
340 ctx: &ExecutorContext,
341 _version: Version,
342 sketch: ObjectId,
343 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
344 let mut new_ast = self.program.ast.clone();
347
348 let sketch_id = sketch;
350 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
351 msg: format!("Sketch not found: {sketch:?}"),
352 })?;
353 let ObjectKind::Sketch(_) = &sketch_object.kind else {
354 return Err(Error {
355 msg: format!("Object is not a sketch: {sketch_object:?}"),
356 });
357 };
358
359 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)?;
361
362 self.execute_after_delete_sketch(ctx, &mut new_ast).await
363 }
364
365 async fn add_segment(
366 &mut self,
367 ctx: &ExecutorContext,
368 _version: Version,
369 sketch: ObjectId,
370 segment: SegmentCtor,
371 _label: Option<String>,
372 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
373 match segment {
375 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
376 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
377 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
378 _ => Err(Error {
379 msg: format!("segment ctor not implemented yet: {segment:?}"),
380 }),
381 }
382 }
383
384 async fn edit_segments(
385 &mut self,
386 ctx: &ExecutorContext,
387 _version: Version,
388 sketch: ObjectId,
389 segments: Vec<ExistingSegmentCtor>,
390 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
391 let mut new_ast = self.program.ast.clone();
393 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
394
395 for segment in &segments {
398 segment_ids_edited.insert(segment.id);
399 }
400
401 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
416
417 for segment in segments {
418 let segment_id = segment.id;
419 match segment.ctor {
420 SegmentCtor::Point(ctor) => {
421 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
423 && let ObjectKind::Segment { segment } = &segment_object.kind
424 && let Segment::Point(point) = segment
425 && let Some(owner_id) = point.owner
426 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
427 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
428 {
429 match owner_segment {
430 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
431 if let Some(existing) = final_edits.get_mut(&owner_id) {
432 let SegmentCtor::Line(line_ctor) = existing else {
433 return Err(Error {
434 msg: format!("Internal: Expected line ctor for owner: {owner_object:?}"),
435 });
436 };
437 if line.start == segment_id {
439 line_ctor.start = ctor.position;
440 } else {
441 line_ctor.end = ctor.position;
442 }
443 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
444 let mut line_ctor = line_ctor.clone();
446 if line.start == segment_id {
447 line_ctor.start = ctor.position;
448 } else {
449 line_ctor.end = ctor.position;
450 }
451 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
452 } else {
453 return Err(Error {
455 msg: format!("Internal: Line does not have line ctor: {owner_object:?}"),
456 });
457 }
458 continue;
459 }
460 Segment::Arc(arc)
461 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
462 {
463 if let Some(existing) = final_edits.get_mut(&owner_id) {
464 let SegmentCtor::Arc(arc_ctor) = existing else {
465 return Err(Error {
466 msg: format!("Internal: Expected arc ctor for owner: {owner_object:?}"),
467 });
468 };
469 if arc.start == segment_id {
470 arc_ctor.start = ctor.position;
471 } else if arc.end == segment_id {
472 arc_ctor.end = ctor.position;
473 } else {
474 arc_ctor.center = ctor.position;
475 }
476 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
477 let mut arc_ctor = arc_ctor.clone();
478 if arc.start == segment_id {
479 arc_ctor.start = ctor.position;
480 } else if arc.end == segment_id {
481 arc_ctor.end = ctor.position;
482 } else {
483 arc_ctor.center = ctor.position;
484 }
485 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
486 } else {
487 return Err(Error {
488 msg: format!("Internal: Arc does not have arc ctor: {owner_object:?}"),
489 });
490 }
491 continue;
492 }
493 _ => {}
494 }
495 }
496
497 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
499 }
500 SegmentCtor::Line(ctor) => {
501 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
502 }
503 SegmentCtor::Arc(ctor) => {
504 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
505 }
506 other_ctor => {
507 final_edits.insert(segment_id, other_ctor);
508 }
509 }
510 }
511
512 for (segment_id, ctor) in final_edits {
513 match ctor {
514 SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment_id, ctor)?,
515 SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment_id, ctor)?,
516 SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment_id, ctor)?,
517 _ => {
518 return Err(Error {
519 msg: format!("segment ctor not implemented yet: {ctor:?}"),
520 });
521 }
522 }
523 }
524 self.execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
525 .await
526 }
527
528 async fn delete_objects(
529 &mut self,
530 ctx: &ExecutorContext,
531 _version: Version,
532 sketch: ObjectId,
533 constraint_ids: Vec<ObjectId>,
534 segment_ids: Vec<ObjectId>,
535 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
536 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
540 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
541
542 let mut delete_ids = AhashIndexSet::default();
545
546 for segment_id in segment_ids_set.iter().copied() {
547 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
548 && let ObjectKind::Segment { segment } = &segment_object.kind
549 && let Segment::Point(point) = segment
550 && let Some(owner_id) = point.owner
551 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
552 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
553 && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_))
554 {
555 delete_ids.insert(owner_id);
557 } else {
558 delete_ids.insert(segment_id);
560 }
561 }
562 self.add_dependent_constraints_to_delete(sketch, &delete_ids, &mut constraint_ids_set)?;
565
566 let mut new_ast = self.program.ast.clone();
567 for constraint_id in constraint_ids_set {
568 self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
569 }
570 for segment_id in delete_ids {
571 self.delete_segment(&mut new_ast, sketch, segment_id)?;
572 }
573 self.execute_after_edit(
574 ctx,
575 sketch,
576 Default::default(),
577 EditDeleteKind::DeleteNonSketch,
578 &mut new_ast,
579 )
580 .await
581 }
582
583 async fn add_constraint(
584 &mut self,
585 ctx: &ExecutorContext,
586 _version: Version,
587 sketch: ObjectId,
588 constraint: Constraint,
589 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
590 let original_program = self.program.clone();
594 let original_scene_graph = self.scene_graph.clone();
595
596 let mut new_ast = self.program.ast.clone();
597 let sketch_block_range = match constraint {
598 Constraint::Coincident(coincident) => self.add_coincident(sketch, coincident, &mut new_ast).await?,
599 Constraint::Distance(distance) => self.add_distance(sketch, distance, &mut new_ast).await?,
600 Constraint::HorizontalDistance(distance) => {
601 self.add_horizontal_distance(sketch, distance, &mut new_ast).await?
602 }
603 Constraint::VerticalDistance(distance) => {
604 self.add_vertical_distance(sketch, distance, &mut new_ast).await?
605 }
606 Constraint::Horizontal(horizontal) => self.add_horizontal(sketch, horizontal, &mut new_ast).await?,
607 Constraint::LinesEqualLength(lines_equal_length) => {
608 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
609 .await?
610 }
611 Constraint::Parallel(parallel) => self.add_parallel(sketch, parallel, &mut new_ast).await?,
612 Constraint::Perpendicular(perpendicular) => {
613 self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?
614 }
615 Constraint::Vertical(vertical) => self.add_vertical(sketch, vertical, &mut new_ast).await?,
616 };
617
618 let result = self
619 .execute_after_add_constraint(ctx, sketch, sketch_block_range, &mut new_ast)
620 .await;
621
622 if result.is_err() {
624 self.program = original_program;
625 self.scene_graph = original_scene_graph;
626 }
627
628 result
629 }
630
631 async fn chain_segment(
632 &mut self,
633 ctx: &ExecutorContext,
634 version: Version,
635 sketch: ObjectId,
636 previous_segment_end_point_id: ObjectId,
637 segment: SegmentCtor,
638 _label: Option<String>,
639 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
640 let SegmentCtor::Line(line_ctor) = segment else {
644 return Err(Error {
645 msg: format!("chain_segment currently only supports Line segments, got: {segment:?}"),
646 });
647 };
648
649 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
651
652 let new_line_id = first_scene_delta
655 .new_objects
656 .iter()
657 .find(|&obj_id| {
658 let obj = self.scene_graph.objects.get(obj_id.0);
659 if let Some(obj) = obj {
660 matches!(
661 &obj.kind,
662 ObjectKind::Segment {
663 segment: Segment::Line(_)
664 }
665 )
666 } else {
667 false
668 }
669 })
670 .ok_or_else(|| Error {
671 msg: "Failed to find new line segment in scene graph".to_string(),
672 })?;
673
674 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| Error {
675 msg: format!("New line object not found: {new_line_id:?}"),
676 })?;
677
678 let ObjectKind::Segment {
679 segment: new_line_segment,
680 } = &new_line_obj.kind
681 else {
682 return Err(Error {
683 msg: format!("Object is not a segment: {new_line_obj:?}"),
684 });
685 };
686
687 let Segment::Line(new_line) = new_line_segment else {
688 return Err(Error {
689 msg: format!("Segment is not a line: {new_line_segment:?}"),
690 });
691 };
692
693 let new_line_start_point_id = new_line.start;
694
695 let coincident = Coincident {
697 segments: vec![previous_segment_end_point_id, new_line_start_point_id],
698 };
699
700 let (final_src_delta, final_scene_delta) = self
701 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
702 .await?;
703
704 let mut combined_new_objects = first_scene_delta.new_objects.clone();
707 combined_new_objects.extend(final_scene_delta.new_objects);
708
709 let scene_graph_delta = SceneGraphDelta {
710 new_graph: self.scene_graph.clone(),
711 invalidates_ids: false,
712 new_objects: combined_new_objects,
713 exec_outcome: final_scene_delta.exec_outcome,
714 };
715
716 Ok((final_src_delta, scene_graph_delta))
717 }
718
719 async fn edit_constraint(
720 &mut self,
721 _ctx: &ExecutorContext,
722 _version: Version,
723 _sketch: ObjectId,
724 _constraint_id: ObjectId,
725 _constraint: Constraint,
726 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
727 todo!()
728 }
729}
730
731impl FrontendState {
732 pub async fn hack_set_program(
733 &mut self,
734 ctx: &ExecutorContext,
735 program: Program,
736 ) -> api::Result<(SceneGraph, ExecOutcome)> {
737 self.program = program.clone();
738
739 self.point_freedom_cache.clear();
746 let outcome = ctx.run_with_caching(program).await.map_err(|err| Error {
747 msg: err.error.message().to_owned(),
748 })?;
749 let freedom_analysis_ran = true;
750
751 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
752
753 Ok((self.scene_graph.clone(), outcome))
754 }
755
756 async fn add_point(
757 &mut self,
758 ctx: &ExecutorContext,
759 sketch: ObjectId,
760 ctor: PointCtor,
761 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
762 let at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
764 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
765 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
766 unlabeled: None,
767 arguments: vec![ast::LabeledArg {
768 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
769 arg: at_ast,
770 }],
771 digest: None,
772 non_code_meta: Default::default(),
773 })));
774
775 let sketch_id = sketch;
777 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
778 #[cfg(target_arch = "wasm32")]
779 web_sys::console::error_1(
780 &format!(
781 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
782 &self.scene_graph.objects
783 )
784 .into(),
785 );
786 Error {
787 msg: format!("Sketch not found: {sketch:?}"),
788 }
789 })?;
790 let ObjectKind::Sketch(_) = &sketch_object.kind else {
791 return Err(Error {
792 msg: format!("Object is not a sketch: {sketch_object:?}"),
793 });
794 };
795 let mut new_ast = self.program.ast.clone();
797 let (sketch_block_range, _) = self.mutate_ast(
798 &mut new_ast,
799 sketch_id,
800 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
801 )?;
802 let new_source = source_from_ast(&new_ast);
804 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
806 if !errors.is_empty() {
807 return Err(Error {
808 msg: format!("Error parsing KCL source after adding point: {errors:?}"),
809 });
810 }
811 let Some(new_program) = new_program else {
812 return Err(Error {
813 msg: "No AST produced after adding point".to_string(),
814 });
815 };
816
817 let point_source_range =
818 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
819 msg: format!("Source range of point not found in sketch block: {sketch_block_range:?}; {err:?}"),
820 })?;
821 #[cfg(not(feature = "artifact-graph"))]
822 let _ = point_source_range;
823
824 self.program = new_program.clone();
826
827 let mut truncated_program = new_program;
829 self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
830
831 let outcome = ctx
833 .run_mock(
834 &truncated_program,
835 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
836 )
837 .await
838 .map_err(|err| {
839 Error {
842 msg: err.error.message().to_owned(),
843 }
844 })?;
845
846 #[cfg(not(feature = "artifact-graph"))]
847 let new_object_ids = Vec::new();
848 #[cfg(feature = "artifact-graph")]
849 let new_object_ids = {
850 let segment_id = outcome
851 .source_range_to_object
852 .get(&point_source_range)
853 .copied()
854 .ok_or_else(|| Error {
855 msg: format!("Source range of point not found: {point_source_range:?}"),
856 })?;
857 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
858 msg: format!("Segment not found: {segment_id:?}"),
859 })?;
860 let ObjectKind::Segment { segment } = &segment_object.kind else {
861 return Err(Error {
862 msg: format!("Object is not a segment: {segment_object:?}"),
863 });
864 };
865 let Segment::Point(_) = segment else {
866 return Err(Error {
867 msg: format!("Segment is not a point: {segment:?}"),
868 });
869 };
870 vec![segment_id]
871 };
872 let src_delta = SourceDelta { text: new_source };
873 let outcome = self.update_state_after_exec(outcome, false);
875 let scene_graph_delta = SceneGraphDelta {
876 new_graph: self.scene_graph.clone(),
877 invalidates_ids: false,
878 new_objects: new_object_ids,
879 exec_outcome: outcome,
880 };
881 Ok((src_delta, scene_graph_delta))
882 }
883
884 async fn add_line(
885 &mut self,
886 ctx: &ExecutorContext,
887 sketch: ObjectId,
888 ctor: LineCtor,
889 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
890 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
892 let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
893 let mut arguments = vec![
894 ast::LabeledArg {
895 label: Some(ast::Identifier::new(LINE_START_PARAM)),
896 arg: start_ast,
897 },
898 ast::LabeledArg {
899 label: Some(ast::Identifier::new(LINE_END_PARAM)),
900 arg: end_ast,
901 },
902 ];
903 if ctor.construction == Some(true) {
905 arguments.push(ast::LabeledArg {
906 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
907 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
908 value: ast::LiteralValue::Bool(true),
909 raw: "true".to_string(),
910 digest: None,
911 }))),
912 });
913 }
914 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
915 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
916 unlabeled: None,
917 arguments,
918 digest: None,
919 non_code_meta: Default::default(),
920 })));
921
922 let sketch_id = sketch;
924 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
925 msg: format!("Sketch not found: {sketch:?}"),
926 })?;
927 let ObjectKind::Sketch(_) = &sketch_object.kind else {
928 return Err(Error {
929 msg: format!("Object is not a sketch: {sketch_object:?}"),
930 });
931 };
932 let mut new_ast = self.program.ast.clone();
934 let (sketch_block_range, _) = self.mutate_ast(
935 &mut new_ast,
936 sketch_id,
937 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
938 )?;
939 let new_source = source_from_ast(&new_ast);
941 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
943 if !errors.is_empty() {
944 return Err(Error {
945 msg: format!("Error parsing KCL source after adding line: {errors:?}"),
946 });
947 }
948 let Some(new_program) = new_program else {
949 return Err(Error {
950 msg: "No AST produced after adding line".to_string(),
951 });
952 };
953 let line_source_range =
954 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
955 msg: format!("Source range of line not found in sketch block: {sketch_block_range:?}; {err:?}"),
956 })?;
957 #[cfg(not(feature = "artifact-graph"))]
958 let _ = line_source_range;
959
960 self.program = new_program.clone();
962
963 let mut truncated_program = new_program;
965 self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
966
967 let outcome = ctx
969 .run_mock(
970 &truncated_program,
971 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
972 )
973 .await
974 .map_err(|err| {
975 Error {
978 msg: err.error.message().to_owned(),
979 }
980 })?;
981
982 #[cfg(not(feature = "artifact-graph"))]
983 let new_object_ids = Vec::new();
984 #[cfg(feature = "artifact-graph")]
985 let new_object_ids = {
986 let segment_id = outcome
987 .source_range_to_object
988 .get(&line_source_range)
989 .copied()
990 .ok_or_else(|| Error {
991 msg: format!("Source range of line not found: {line_source_range:?}"),
992 })?;
993 let segment_object = outcome.scene_object_by_id(segment_id).ok_or_else(|| Error {
994 msg: format!("Segment not found: {segment_id:?}"),
995 })?;
996 let ObjectKind::Segment { segment } = &segment_object.kind else {
997 return Err(Error {
998 msg: format!("Object is not a segment: {segment_object:?}"),
999 });
1000 };
1001 let Segment::Line(line) = segment else {
1002 return Err(Error {
1003 msg: format!("Segment is not a line: {segment:?}"),
1004 });
1005 };
1006 vec![line.start, line.end, segment_id]
1007 };
1008 let src_delta = SourceDelta { text: new_source };
1009 let outcome = self.update_state_after_exec(outcome, false);
1011 let scene_graph_delta = SceneGraphDelta {
1012 new_graph: self.scene_graph.clone(),
1013 invalidates_ids: false,
1014 new_objects: new_object_ids,
1015 exec_outcome: outcome,
1016 };
1017 Ok((src_delta, scene_graph_delta))
1018 }
1019
1020 async fn add_arc(
1021 &mut self,
1022 ctx: &ExecutorContext,
1023 sketch: ObjectId,
1024 ctor: ArcCtor,
1025 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1026 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1028 let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1029 let center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1030 let mut arguments = vec![
1031 ast::LabeledArg {
1032 label: Some(ast::Identifier::new(ARC_START_PARAM)),
1033 arg: start_ast,
1034 },
1035 ast::LabeledArg {
1036 label: Some(ast::Identifier::new(ARC_END_PARAM)),
1037 arg: end_ast,
1038 },
1039 ast::LabeledArg {
1040 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1041 arg: center_ast,
1042 },
1043 ];
1044 if ctor.construction == Some(true) {
1046 arguments.push(ast::LabeledArg {
1047 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1048 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1049 value: ast::LiteralValue::Bool(true),
1050 raw: "true".to_string(),
1051 digest: None,
1052 }))),
1053 });
1054 }
1055 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1056 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1057 unlabeled: None,
1058 arguments,
1059 digest: None,
1060 non_code_meta: Default::default(),
1061 })));
1062
1063 let sketch_id = sketch;
1065 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1066 msg: format!("Sketch not found: {sketch:?}"),
1067 })?;
1068 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1069 return Err(Error {
1070 msg: format!("Object is not a sketch: {sketch_object:?}"),
1071 });
1072 };
1073 let mut new_ast = self.program.ast.clone();
1075 let (sketch_block_range, _) = self.mutate_ast(
1076 &mut new_ast,
1077 sketch_id,
1078 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1079 )?;
1080 let new_source = source_from_ast(&new_ast);
1082 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1084 if !errors.is_empty() {
1085 return Err(Error {
1086 msg: format!("Error parsing KCL source after adding arc: {errors:?}"),
1087 });
1088 }
1089 let Some(new_program) = new_program else {
1090 return Err(Error {
1091 msg: "No AST produced after adding arc".to_string(),
1092 });
1093 };
1094 let arc_source_range =
1095 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1096 msg: format!("Source range of arc not found in sketch block: {sketch_block_range:?}; {err:?}"),
1097 })?;
1098 #[cfg(not(feature = "artifact-graph"))]
1099 let _ = arc_source_range;
1100
1101 self.program = new_program.clone();
1103
1104 let mut truncated_program = new_program;
1106 self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1107
1108 let outcome = ctx
1110 .run_mock(
1111 &truncated_program,
1112 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1113 )
1114 .await
1115 .map_err(|err| {
1116 Error {
1119 msg: err.error.message().to_owned(),
1120 }
1121 })?;
1122
1123 #[cfg(not(feature = "artifact-graph"))]
1124 let new_object_ids = Vec::new();
1125 #[cfg(feature = "artifact-graph")]
1126 let new_object_ids = {
1127 let segment_id = outcome
1128 .source_range_to_object
1129 .get(&arc_source_range)
1130 .copied()
1131 .ok_or_else(|| Error {
1132 msg: format!("Source range of arc not found: {arc_source_range:?}"),
1133 })?;
1134 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
1135 msg: format!("Segment not found: {segment_id:?}"),
1136 })?;
1137 let ObjectKind::Segment { segment } = &segment_object.kind else {
1138 return Err(Error {
1139 msg: format!("Object is not a segment: {segment_object:?}"),
1140 });
1141 };
1142 let Segment::Arc(arc) = segment else {
1143 return Err(Error {
1144 msg: format!("Segment is not an arc: {segment:?}"),
1145 });
1146 };
1147 vec![arc.start, arc.end, arc.center, segment_id]
1148 };
1149 let src_delta = SourceDelta { text: new_source };
1150 let outcome = self.update_state_after_exec(outcome, false);
1152 let scene_graph_delta = SceneGraphDelta {
1153 new_graph: self.scene_graph.clone(),
1154 invalidates_ids: false,
1155 new_objects: new_object_ids,
1156 exec_outcome: outcome,
1157 };
1158 Ok((src_delta, scene_graph_delta))
1159 }
1160
1161 fn edit_point(
1162 &mut self,
1163 new_ast: &mut ast::Node<ast::Program>,
1164 sketch: ObjectId,
1165 point: ObjectId,
1166 ctor: PointCtor,
1167 ) -> api::Result<()> {
1168 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
1170
1171 let sketch_id = sketch;
1173 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1174 msg: format!("Sketch not found: {sketch:?}"),
1175 })?;
1176 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1177 return Err(Error {
1178 msg: format!("Object is not a sketch: {sketch_object:?}"),
1179 });
1180 };
1181 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| Error {
1182 msg: format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"),
1183 })?;
1184 let point_id = point;
1186 let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
1187 msg: format!("Point not found in scene graph: point={point:?}"),
1188 })?;
1189 let ObjectKind::Segment {
1190 segment: Segment::Point(point),
1191 } = &point_object.kind
1192 else {
1193 return Err(Error {
1194 msg: format!("Object is not a point segment: {point_object:?}"),
1195 });
1196 };
1197
1198 if let Some(owner_id) = point.owner {
1200 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
1201 msg: format!("Internal: Owner of point not found in scene graph: owner={owner_id:?}",),
1202 })?;
1203 let ObjectKind::Segment { segment } = &owner_object.kind else {
1204 return Err(Error {
1205 msg: format!("Internal: Owner of point is not a segment: {owner_object:?}"),
1206 });
1207 };
1208
1209 if let Segment::Line(line) = segment {
1211 let SegmentCtor::Line(line_ctor) = &line.ctor else {
1212 return Err(Error {
1213 msg: format!("Internal: Owner of point does not have line ctor: {owner_object:?}"),
1214 });
1215 };
1216 let mut line_ctor = line_ctor.clone();
1217 if line.start == point_id {
1219 line_ctor.start = ctor.position;
1220 } else if line.end == point_id {
1221 line_ctor.end = ctor.position;
1222 } else {
1223 return Err(Error {
1224 msg: format!(
1225 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
1226 ),
1227 });
1228 }
1229 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
1230 }
1231
1232 if let Segment::Arc(arc) = segment {
1234 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
1235 return Err(Error {
1236 msg: format!("Internal: Owner of point does not have arc ctor: {owner_object:?}"),
1237 });
1238 };
1239 let mut arc_ctor = arc_ctor.clone();
1240 if arc.center == point_id {
1242 arc_ctor.center = ctor.position;
1243 } else if arc.start == point_id {
1244 arc_ctor.start = ctor.position;
1245 } else if arc.end == point_id {
1246 arc_ctor.end = ctor.position;
1247 } else {
1248 return Err(Error {
1249 msg: format!(
1250 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
1251 ),
1252 });
1253 }
1254 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
1255 }
1256
1257 }
1260
1261 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
1263 Ok(())
1264 }
1265
1266 fn edit_line(
1267 &mut self,
1268 new_ast: &mut ast::Node<ast::Program>,
1269 sketch: ObjectId,
1270 line: ObjectId,
1271 ctor: LineCtor,
1272 ) -> api::Result<()> {
1273 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1275 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1276
1277 let sketch_id = sketch;
1279 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1280 msg: format!("Sketch not found: {sketch:?}"),
1281 })?;
1282 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1283 return Err(Error {
1284 msg: format!("Object is not a sketch: {sketch_object:?}"),
1285 });
1286 };
1287 sketch.segments.iter().find(|o| **o == line).ok_or_else(|| Error {
1288 msg: format!("Line not found in sketch: line={line:?}, sketch={sketch:?}"),
1289 })?;
1290 let line_id = line;
1292 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1293 msg: format!("Line not found in scene graph: line={line:?}"),
1294 })?;
1295 let ObjectKind::Segment { .. } = &line_object.kind else {
1296 return Err(Error {
1297 msg: format!("Object is not a segment: {line_object:?}"),
1298 });
1299 };
1300
1301 self.mutate_ast(
1303 new_ast,
1304 line_id,
1305 AstMutateCommand::EditLine {
1306 start: new_start_ast,
1307 end: new_end_ast,
1308 construction: ctor.construction,
1309 },
1310 )?;
1311 Ok(())
1312 }
1313
1314 fn edit_arc(
1315 &mut self,
1316 new_ast: &mut ast::Node<ast::Program>,
1317 sketch: ObjectId,
1318 arc: ObjectId,
1319 ctor: ArcCtor,
1320 ) -> api::Result<()> {
1321 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1323 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1324 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1325
1326 let sketch_id = sketch;
1328 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1329 msg: format!("Sketch not found: {sketch:?}"),
1330 })?;
1331 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1332 return Err(Error {
1333 msg: format!("Object is not a sketch: {sketch_object:?}"),
1334 });
1335 };
1336 sketch.segments.iter().find(|o| **o == arc).ok_or_else(|| Error {
1337 msg: format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}"),
1338 })?;
1339 let arc_id = arc;
1341 let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
1342 msg: format!("Arc not found in scene graph: arc={arc:?}"),
1343 })?;
1344 let ObjectKind::Segment { .. } = &arc_object.kind else {
1345 return Err(Error {
1346 msg: format!("Object is not a segment: {arc_object:?}"),
1347 });
1348 };
1349
1350 self.mutate_ast(
1352 new_ast,
1353 arc_id,
1354 AstMutateCommand::EditArc {
1355 start: new_start_ast,
1356 end: new_end_ast,
1357 center: new_center_ast,
1358 construction: ctor.construction,
1359 },
1360 )?;
1361 Ok(())
1362 }
1363
1364 fn delete_segment(
1365 &mut self,
1366 new_ast: &mut ast::Node<ast::Program>,
1367 sketch: ObjectId,
1368 segment_id: ObjectId,
1369 ) -> api::Result<()> {
1370 let sketch_id = sketch;
1372 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1373 msg: format!("Sketch not found: {sketch:?}"),
1374 })?;
1375 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1376 return Err(Error {
1377 msg: format!("Object is not a sketch: {sketch_object:?}"),
1378 });
1379 };
1380 sketch
1381 .segments
1382 .iter()
1383 .find(|o| **o == segment_id)
1384 .ok_or_else(|| Error {
1385 msg: format!("Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"),
1386 })?;
1387 let segment_object = self.scene_graph.objects.get(segment_id.0).ok_or_else(|| Error {
1389 msg: format!("Segment not found in scene graph: segment={segment_id:?}"),
1390 })?;
1391 let ObjectKind::Segment { .. } = &segment_object.kind else {
1392 return Err(Error {
1393 msg: format!("Object is not a segment: {segment_object:?}"),
1394 });
1395 };
1396
1397 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
1399 Ok(())
1400 }
1401
1402 fn delete_constraint(
1403 &mut self,
1404 new_ast: &mut ast::Node<ast::Program>,
1405 sketch: ObjectId,
1406 constraint_id: ObjectId,
1407 ) -> api::Result<()> {
1408 let sketch_id = sketch;
1410 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1411 msg: format!("Sketch not found: {sketch:?}"),
1412 })?;
1413 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1414 return Err(Error {
1415 msg: format!("Object is not a sketch: {sketch_object:?}"),
1416 });
1417 };
1418 sketch
1419 .constraints
1420 .iter()
1421 .find(|o| **o == constraint_id)
1422 .ok_or_else(|| Error {
1423 msg: format!("Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"),
1424 })?;
1425 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
1427 msg: format!("Constraint not found in scene graph: constraint={constraint_id:?}"),
1428 })?;
1429 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
1430 return Err(Error {
1431 msg: format!("Object is not a constraint: {constraint_object:?}"),
1432 });
1433 };
1434
1435 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
1437 Ok(())
1438 }
1439
1440 async fn execute_after_edit(
1441 &mut self,
1442 ctx: &ExecutorContext,
1443 sketch: ObjectId,
1444 segment_ids_edited: AhashIndexSet<ObjectId>,
1445 edit_kind: EditDeleteKind,
1446 new_ast: &mut ast::Node<ast::Program>,
1447 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1448 let new_source = source_from_ast(new_ast);
1450 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1452 if !errors.is_empty() {
1453 return Err(Error {
1454 msg: format!("Error parsing KCL source after editing: {errors:?}"),
1455 });
1456 }
1457 let Some(new_program) = new_program else {
1458 return Err(Error {
1459 msg: "No AST produced after editing".to_string(),
1460 });
1461 };
1462
1463 self.program = new_program.clone();
1465
1466 let is_delete = edit_kind.is_delete();
1468 let truncated_program = {
1469 let mut truncated_program = new_program;
1470 self.only_sketch_block(sketch, edit_kind.to_change_kind(), &mut truncated_program.ast)?;
1471 truncated_program
1472 };
1473
1474 #[cfg(not(feature = "artifact-graph"))]
1475 drop(segment_ids_edited);
1476
1477 let mock_config = MockConfig {
1479 sketch_block_id: Some(sketch),
1480 freedom_analysis: is_delete,
1481 #[cfg(feature = "artifact-graph")]
1482 segment_ids_edited: segment_ids_edited.clone(),
1483 ..Default::default()
1484 };
1485 let outcome = ctx.run_mock(&truncated_program, &mock_config).await.map_err(|err| {
1486 Error {
1489 msg: err.error.message().to_owned(),
1490 }
1491 })?;
1492
1493 let outcome = self.update_state_after_exec(outcome, is_delete);
1495
1496 #[cfg(feature = "artifact-graph")]
1497 let new_source = {
1498 let mut new_ast = self.program.ast.clone();
1503 for (var_range, value) in &outcome.var_solutions {
1504 let rounded = value.round(3);
1505 mutate_ast_node_by_source_range(
1506 &mut new_ast,
1507 *var_range,
1508 AstMutateCommand::EditVarInitialValue { value: rounded },
1509 )?;
1510 }
1511 source_from_ast(&new_ast)
1512 };
1513
1514 let src_delta = SourceDelta { text: new_source };
1515 let scene_graph_delta = SceneGraphDelta {
1516 new_graph: self.scene_graph.clone(),
1517 invalidates_ids: is_delete,
1518 new_objects: Vec::new(),
1519 exec_outcome: outcome,
1520 };
1521 Ok((src_delta, scene_graph_delta))
1522 }
1523
1524 async fn execute_after_delete_sketch(
1525 &mut self,
1526 ctx: &ExecutorContext,
1527 new_ast: &mut ast::Node<ast::Program>,
1528 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1529 let new_source = source_from_ast(new_ast);
1531 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1533 if !errors.is_empty() {
1534 return Err(Error {
1535 msg: format!("Error parsing KCL source after editing: {errors:?}"),
1536 });
1537 }
1538 let Some(new_program) = new_program else {
1539 return Err(Error {
1540 msg: "No AST produced after editing".to_string(),
1541 });
1542 };
1543
1544 self.program = new_program.clone();
1546
1547 let outcome = ctx.run_with_caching(new_program).await.map_err(|err| {
1553 Error {
1556 msg: err.error.message().to_owned(),
1557 }
1558 })?;
1559 let freedom_analysis_ran = true;
1560
1561 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
1562
1563 let src_delta = SourceDelta { text: new_source };
1564 let scene_graph_delta = SceneGraphDelta {
1565 new_graph: self.scene_graph.clone(),
1566 invalidates_ids: true,
1567 new_objects: Vec::new(),
1568 exec_outcome: outcome,
1569 };
1570 Ok((src_delta, scene_graph_delta))
1571 }
1572
1573 fn point_id_to_ast_reference(
1578 &self,
1579 point_id: ObjectId,
1580 new_ast: &mut ast::Node<ast::Program>,
1581 ) -> api::Result<ast::Expr> {
1582 let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
1583 msg: format!("Point not found: {point_id:?}"),
1584 })?;
1585 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
1586 return Err(Error {
1587 msg: format!("Object is not a segment: {point_object:?}"),
1588 });
1589 };
1590 let Segment::Point(point) = point_segment else {
1591 return Err(Error {
1592 msg: format!("Only points are currently supported: {point_object:?}"),
1593 });
1594 };
1595
1596 if let Some(owner_id) = point.owner {
1597 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
1598 msg: format!("Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"),
1599 })?;
1600 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
1601 return Err(Error {
1602 msg: format!("Owner of point is not a segment: {owner_object:?}"),
1603 });
1604 };
1605
1606 match owner_segment {
1607 Segment::Line(line) => {
1608 let property = if line.start == point_id {
1609 LINE_PROPERTY_START
1610 } else if line.end == point_id {
1611 LINE_PROPERTY_END
1612 } else {
1613 return Err(Error {
1614 msg: format!(
1615 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
1616 ),
1617 });
1618 };
1619 get_or_insert_ast_reference(new_ast, &owner_object.source, "line", Some(property))
1620 }
1621 Segment::Arc(arc) => {
1622 let property = if arc.start == point_id {
1623 ARC_PROPERTY_START
1624 } else if arc.end == point_id {
1625 ARC_PROPERTY_END
1626 } else if arc.center == point_id {
1627 ARC_PROPERTY_CENTER
1628 } else {
1629 return Err(Error {
1630 msg: format!(
1631 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
1632 ),
1633 });
1634 };
1635 get_or_insert_ast_reference(new_ast, &owner_object.source, "arc", Some(property))
1636 }
1637 _ => Err(Error {
1638 msg: format!(
1639 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
1640 ),
1641 }),
1642 }
1643 } else {
1644 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
1646 }
1647 }
1648
1649 async fn add_coincident(
1650 &mut self,
1651 sketch: ObjectId,
1652 coincident: Coincident,
1653 new_ast: &mut ast::Node<ast::Program>,
1654 ) -> api::Result<SourceRange> {
1655 let &[seg0_id, seg1_id] = coincident.segments.as_slice() else {
1656 return Err(Error {
1657 msg: format!(
1658 "Coincident constraint must have exactly 2 segments, got {}",
1659 coincident.segments.len()
1660 ),
1661 });
1662 };
1663 let sketch_id = sketch;
1664
1665 let seg0_object = self.scene_graph.objects.get(seg0_id.0).ok_or_else(|| Error {
1667 msg: format!("Object not found: {seg0_id:?}"),
1668 })?;
1669 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
1670 return Err(Error {
1671 msg: format!("Object is not a segment: {seg0_object:?}"),
1672 });
1673 };
1674 let seg0_ast = match seg0_segment {
1675 Segment::Point(_) => {
1676 self.point_id_to_ast_reference(seg0_id, new_ast)?
1678 }
1679 Segment::Line(_) => {
1680 get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?
1682 }
1683 Segment::Arc(_) | Segment::Circle(_) => {
1684 get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?
1686 }
1687 };
1688
1689 let seg1_object = self.scene_graph.objects.get(seg1_id.0).ok_or_else(|| Error {
1691 msg: format!("Object not found: {seg1_id:?}"),
1692 })?;
1693 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
1694 return Err(Error {
1695 msg: format!("Object is not a segment: {seg1_object:?}"),
1696 });
1697 };
1698 let seg1_ast = match seg1_segment {
1699 Segment::Point(_) => {
1700 self.point_id_to_ast_reference(seg1_id, new_ast)?
1702 }
1703 Segment::Line(_) => {
1704 get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?
1706 }
1707 Segment::Arc(_) | Segment::Circle(_) => {
1708 get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?
1710 }
1711 };
1712
1713 let coincident_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1715 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
1716 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1717 ast::ArrayExpression {
1718 elements: vec![seg0_ast, seg1_ast],
1719 digest: None,
1720 non_code_meta: Default::default(),
1721 },
1722 )))),
1723 arguments: Default::default(),
1724 digest: None,
1725 non_code_meta: Default::default(),
1726 })));
1727
1728 let (sketch_block_range, _) = self.mutate_ast(
1730 new_ast,
1731 sketch_id,
1732 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
1733 )?;
1734 Ok(sketch_block_range)
1735 }
1736
1737 async fn add_distance(
1738 &mut self,
1739 sketch: ObjectId,
1740 distance: Distance,
1741 new_ast: &mut ast::Node<ast::Program>,
1742 ) -> api::Result<SourceRange> {
1743 let &[pt0_id, pt1_id] = distance.points.as_slice() else {
1744 return Err(Error {
1745 msg: format!(
1746 "Distance constraint must have exactly 2 points, got {}",
1747 distance.points.len()
1748 ),
1749 });
1750 };
1751 let sketch_id = sketch;
1752
1753 let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
1755 let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
1756
1757 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1759 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
1760 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1761 ast::ArrayExpression {
1762 elements: vec![pt0_ast, pt1_ast],
1763 digest: None,
1764 non_code_meta: Default::default(),
1765 },
1766 )))),
1767 arguments: Default::default(),
1768 digest: None,
1769 non_code_meta: Default::default(),
1770 })));
1771 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
1772 left: distance_call_ast,
1773 operator: ast::BinaryOperator::Eq,
1774 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
1775 value: ast::LiteralValue::Number {
1776 value: distance.distance.value,
1777 suffix: distance.distance.units,
1778 },
1779 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
1780 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
1781 })?,
1782 digest: None,
1783 }))),
1784 digest: None,
1785 })));
1786
1787 let (sketch_block_range, _) = self.mutate_ast(
1789 new_ast,
1790 sketch_id,
1791 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
1792 )?;
1793 Ok(sketch_block_range)
1794 }
1795
1796 async fn add_horizontal_distance(
1797 &mut self,
1798 sketch: ObjectId,
1799 distance: Distance,
1800 new_ast: &mut ast::Node<ast::Program>,
1801 ) -> api::Result<SourceRange> {
1802 let &[pt0_id, pt1_id] = distance.points.as_slice() else {
1803 return Err(Error {
1804 msg: format!(
1805 "Horizontal distance constraint must have exactly 2 points, got {}",
1806 distance.points.len()
1807 ),
1808 });
1809 };
1810 let sketch_id = sketch;
1811
1812 let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
1814 let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
1815
1816 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1818 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
1819 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1820 ast::ArrayExpression {
1821 elements: vec![pt0_ast, pt1_ast],
1822 digest: None,
1823 non_code_meta: Default::default(),
1824 },
1825 )))),
1826 arguments: Default::default(),
1827 digest: None,
1828 non_code_meta: Default::default(),
1829 })));
1830 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
1831 left: distance_call_ast,
1832 operator: ast::BinaryOperator::Eq,
1833 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
1834 value: ast::LiteralValue::Number {
1835 value: distance.distance.value,
1836 suffix: distance.distance.units,
1837 },
1838 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
1839 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
1840 })?,
1841 digest: None,
1842 }))),
1843 digest: None,
1844 })));
1845
1846 let (sketch_block_range, _) = self.mutate_ast(
1848 new_ast,
1849 sketch_id,
1850 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
1851 )?;
1852 Ok(sketch_block_range)
1853 }
1854
1855 async fn add_vertical_distance(
1856 &mut self,
1857 sketch: ObjectId,
1858 distance: Distance,
1859 new_ast: &mut ast::Node<ast::Program>,
1860 ) -> api::Result<SourceRange> {
1861 let &[pt0_id, pt1_id] = distance.points.as_slice() else {
1862 return Err(Error {
1863 msg: format!(
1864 "Vertical distance constraint must have exactly 2 points, got {}",
1865 distance.points.len()
1866 ),
1867 });
1868 };
1869 let sketch_id = sketch;
1870
1871 let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
1873 let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
1874
1875 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1877 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
1878 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1879 ast::ArrayExpression {
1880 elements: vec![pt0_ast, pt1_ast],
1881 digest: None,
1882 non_code_meta: Default::default(),
1883 },
1884 )))),
1885 arguments: Default::default(),
1886 digest: None,
1887 non_code_meta: Default::default(),
1888 })));
1889 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
1890 left: distance_call_ast,
1891 operator: ast::BinaryOperator::Eq,
1892 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
1893 value: ast::LiteralValue::Number {
1894 value: distance.distance.value,
1895 suffix: distance.distance.units,
1896 },
1897 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
1898 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
1899 })?,
1900 digest: None,
1901 }))),
1902 digest: None,
1903 })));
1904
1905 let (sketch_block_range, _) = self.mutate_ast(
1907 new_ast,
1908 sketch_id,
1909 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
1910 )?;
1911 Ok(sketch_block_range)
1912 }
1913
1914 async fn add_horizontal(
1915 &mut self,
1916 sketch: ObjectId,
1917 horizontal: Horizontal,
1918 new_ast: &mut ast::Node<ast::Program>,
1919 ) -> api::Result<SourceRange> {
1920 let sketch_id = sketch;
1921
1922 let line_id = horizontal.line;
1924 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1925 msg: format!("Line not found: {line_id:?}"),
1926 })?;
1927 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
1928 return Err(Error {
1929 msg: format!("Object is not a segment: {line_object:?}"),
1930 });
1931 };
1932 let Segment::Line(_) = line_segment else {
1933 return Err(Error {
1934 msg: format!("Only lines can be made horizontal: {line_object:?}"),
1935 });
1936 };
1937 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
1938
1939 let horizontal_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1941 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
1942 unlabeled: Some(line_ast),
1943 arguments: Default::default(),
1944 digest: None,
1945 non_code_meta: Default::default(),
1946 })));
1947
1948 let (sketch_block_range, _) = self.mutate_ast(
1950 new_ast,
1951 sketch_id,
1952 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
1953 )?;
1954 Ok(sketch_block_range)
1955 }
1956
1957 async fn add_lines_equal_length(
1958 &mut self,
1959 sketch: ObjectId,
1960 lines_equal_length: LinesEqualLength,
1961 new_ast: &mut ast::Node<ast::Program>,
1962 ) -> api::Result<SourceRange> {
1963 let &[line0_id, line1_id] = lines_equal_length.lines.as_slice() else {
1964 return Err(Error {
1965 msg: format!(
1966 "Lines equal length constraint must have exactly 2 lines, got {}",
1967 lines_equal_length.lines.len()
1968 ),
1969 });
1970 };
1971
1972 let sketch_id = sketch;
1973
1974 let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
1976 msg: format!("Line not found: {line0_id:?}"),
1977 })?;
1978 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
1979 return Err(Error {
1980 msg: format!("Object is not a segment: {line0_object:?}"),
1981 });
1982 };
1983 let Segment::Line(_) = line0_segment else {
1984 return Err(Error {
1985 msg: format!("Only lines can be made equal length: {line0_object:?}"),
1986 });
1987 };
1988 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
1989
1990 let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
1991 msg: format!("Line not found: {line1_id:?}"),
1992 })?;
1993 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
1994 return Err(Error {
1995 msg: format!("Object is not a segment: {line1_object:?}"),
1996 });
1997 };
1998 let Segment::Line(_) = line1_segment else {
1999 return Err(Error {
2000 msg: format!("Only lines can be made equal length: {line1_object:?}"),
2001 });
2002 };
2003 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
2004
2005 let equal_length_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2007 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
2008 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2009 ast::ArrayExpression {
2010 elements: vec![line0_ast, line1_ast],
2011 digest: None,
2012 non_code_meta: Default::default(),
2013 },
2014 )))),
2015 arguments: Default::default(),
2016 digest: None,
2017 non_code_meta: Default::default(),
2018 })));
2019
2020 let (sketch_block_range, _) = self.mutate_ast(
2022 new_ast,
2023 sketch_id,
2024 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
2025 )?;
2026 Ok(sketch_block_range)
2027 }
2028
2029 async fn add_parallel(
2030 &mut self,
2031 sketch: ObjectId,
2032 parallel: Parallel,
2033 new_ast: &mut ast::Node<ast::Program>,
2034 ) -> api::Result<SourceRange> {
2035 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Parallel, parallel.lines, new_ast)
2036 .await
2037 }
2038
2039 async fn add_perpendicular(
2040 &mut self,
2041 sketch: ObjectId,
2042 perpendicular: Perpendicular,
2043 new_ast: &mut ast::Node<ast::Program>,
2044 ) -> api::Result<SourceRange> {
2045 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
2046 .await
2047 }
2048
2049 async fn add_lines_at_angle_constraint(
2050 &mut self,
2051 sketch: ObjectId,
2052 angle_kind: LinesAtAngleKind,
2053 lines: Vec<ObjectId>,
2054 new_ast: &mut ast::Node<ast::Program>,
2055 ) -> api::Result<SourceRange> {
2056 let &[line0_id, line1_id] = lines.as_slice() else {
2057 return Err(Error {
2058 msg: format!(
2059 "{} constraint must have exactly 2 lines, got {}",
2060 angle_kind.to_function_name(),
2061 lines.len()
2062 ),
2063 });
2064 };
2065
2066 let sketch_id = sketch;
2067
2068 let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
2070 msg: format!("Line not found: {line0_id:?}"),
2071 })?;
2072 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
2073 return Err(Error {
2074 msg: format!("Object is not a segment: {line0_object:?}"),
2075 });
2076 };
2077 let Segment::Line(_) = line0_segment else {
2078 return Err(Error {
2079 msg: format!(
2080 "Only lines can be made {}: {line0_object:?}",
2081 angle_kind.to_function_name()
2082 ),
2083 });
2084 };
2085 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
2086
2087 let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
2088 msg: format!("Line not found: {line1_id:?}"),
2089 })?;
2090 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
2091 return Err(Error {
2092 msg: format!("Object is not a segment: {line1_object:?}"),
2093 });
2094 };
2095 let Segment::Line(_) = line1_segment else {
2096 return Err(Error {
2097 msg: format!(
2098 "Only lines can be made {}: {line1_object:?}",
2099 angle_kind.to_function_name()
2100 ),
2101 });
2102 };
2103 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
2104
2105 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2107 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
2108 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2109 ast::ArrayExpression {
2110 elements: vec![line0_ast, line1_ast],
2111 digest: None,
2112 non_code_meta: Default::default(),
2113 },
2114 )))),
2115 arguments: Default::default(),
2116 digest: None,
2117 non_code_meta: Default::default(),
2118 })));
2119
2120 let (sketch_block_range, _) = self.mutate_ast(
2122 new_ast,
2123 sketch_id,
2124 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
2125 )?;
2126 Ok(sketch_block_range)
2127 }
2128
2129 async fn add_vertical(
2130 &mut self,
2131 sketch: ObjectId,
2132 vertical: Vertical,
2133 new_ast: &mut ast::Node<ast::Program>,
2134 ) -> api::Result<SourceRange> {
2135 let sketch_id = sketch;
2136
2137 let line_id = vertical.line;
2139 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
2140 msg: format!("Line not found: {line_id:?}"),
2141 })?;
2142 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2143 return Err(Error {
2144 msg: format!("Object is not a segment: {line_object:?}"),
2145 });
2146 };
2147 let Segment::Line(_) = line_segment else {
2148 return Err(Error {
2149 msg: format!("Only lines can be made vertical: {line_object:?}"),
2150 });
2151 };
2152 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
2153
2154 let vertical_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2156 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
2157 unlabeled: Some(line_ast),
2158 arguments: Default::default(),
2159 digest: None,
2160 non_code_meta: Default::default(),
2161 })));
2162
2163 let (sketch_block_range, _) = self.mutate_ast(
2165 new_ast,
2166 sketch_id,
2167 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
2168 )?;
2169 Ok(sketch_block_range)
2170 }
2171
2172 async fn execute_after_add_constraint(
2173 &mut self,
2174 ctx: &ExecutorContext,
2175 sketch_id: ObjectId,
2176 #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_range: SourceRange,
2177 new_ast: &mut ast::Node<ast::Program>,
2178 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
2179 let new_source = source_from_ast(new_ast);
2181 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
2183 if !errors.is_empty() {
2184 return Err(Error {
2185 msg: format!("Error parsing KCL source after adding constraint: {errors:?}"),
2186 });
2187 }
2188 let Some(new_program) = new_program else {
2189 return Err(Error {
2190 msg: "No AST produced after adding constraint".to_string(),
2191 });
2192 };
2193 #[cfg(feature = "artifact-graph")]
2194 let constraint_source_range =
2195 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
2196 msg: format!(
2197 "Source range of new constraint not found in sketch block: {sketch_block_range:?}; {err:?}"
2198 ),
2199 })?;
2200
2201 let mut truncated_program = new_program.clone();
2204 self.only_sketch_block(sketch_id, ChangeKind::Add, &mut truncated_program.ast)?;
2205
2206 let outcome = ctx
2208 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
2209 .await
2210 .map_err(|err| {
2211 Error {
2214 msg: err.error.message().to_owned(),
2215 }
2216 })?;
2217
2218 #[cfg(not(feature = "artifact-graph"))]
2219 let new_object_ids = Vec::new();
2220 #[cfg(feature = "artifact-graph")]
2221 let new_object_ids = {
2222 let constraint_id = outcome
2224 .source_range_to_object
2225 .get(&constraint_source_range)
2226 .copied()
2227 .ok_or_else(|| Error {
2228 msg: format!("Source range of constraint not found: {constraint_source_range:?}"),
2229 })?;
2230 vec![constraint_id]
2231 };
2232
2233 self.program = new_program;
2236
2237 let outcome = self.update_state_after_exec(outcome, true);
2239
2240 let src_delta = SourceDelta { text: new_source };
2241 let scene_graph_delta = SceneGraphDelta {
2242 new_graph: self.scene_graph.clone(),
2243 invalidates_ids: false,
2244 new_objects: new_object_ids,
2245 exec_outcome: outcome,
2246 };
2247 Ok((src_delta, scene_graph_delta))
2248 }
2249
2250 fn add_dependent_constraints_to_delete(
2253 &self,
2254 sketch_id: ObjectId,
2255 segment_ids_set: &AhashIndexSet<ObjectId>,
2256 constraint_ids_set: &mut AhashIndexSet<ObjectId>,
2257 ) -> api::Result<()> {
2258 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
2260 msg: format!("Sketch not found: {sketch_id:?}"),
2261 })?;
2262 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2263 return Err(Error {
2264 msg: format!("Object is not a sketch: {sketch_object:?}"),
2265 });
2266 };
2267 for constraint_id in &sketch.constraints {
2268 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
2269 msg: format!("Constraint not found: {constraint_id:?}"),
2270 })?;
2271 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
2272 return Err(Error {
2273 msg: format!("Object is not a constraint: {constraint_object:?}"),
2274 });
2275 };
2276 let depends_on_segment = match constraint {
2277 Constraint::Coincident(c) => c.segments.iter().any(|seg_id| {
2278 if segment_ids_set.contains(seg_id) {
2280 return true;
2281 }
2282 let seg_object = self.scene_graph.objects.get(seg_id.0);
2284 if let Some(obj) = seg_object
2285 && let ObjectKind::Segment { segment } = &obj.kind
2286 && let Segment::Point(pt) = segment
2287 && let Some(owner_line_id) = pt.owner
2288 {
2289 return segment_ids_set.contains(&owner_line_id);
2290 }
2291 false
2292 }),
2293 Constraint::Distance(d) => d.points.iter().any(|pt_id| {
2294 let pt_object = self.scene_graph.objects.get(pt_id.0);
2295 if let Some(obj) = pt_object
2296 && let ObjectKind::Segment { segment } = &obj.kind
2297 && let Segment::Point(pt) = segment
2298 && let Some(owner_line_id) = pt.owner
2299 {
2300 return segment_ids_set.contains(&owner_line_id);
2301 }
2302 false
2303 }),
2304 Constraint::HorizontalDistance(d) => d.points.iter().any(|pt_id| {
2305 let pt_object = self.scene_graph.objects.get(pt_id.0);
2306 if let Some(obj) = pt_object
2307 && let ObjectKind::Segment { segment } = &obj.kind
2308 && let Segment::Point(pt) = segment
2309 && let Some(owner_line_id) = pt.owner
2310 {
2311 return segment_ids_set.contains(&owner_line_id);
2312 }
2313 false
2314 }),
2315 Constraint::VerticalDistance(d) => d.points.iter().any(|pt_id| {
2316 let pt_object = self.scene_graph.objects.get(pt_id.0);
2317 if let Some(obj) = pt_object
2318 && let ObjectKind::Segment { segment } = &obj.kind
2319 && let Segment::Point(pt) = segment
2320 && let Some(owner_line_id) = pt.owner
2321 {
2322 return segment_ids_set.contains(&owner_line_id);
2323 }
2324 false
2325 }),
2326 Constraint::Horizontal(h) => segment_ids_set.contains(&h.line),
2327 Constraint::Vertical(v) => segment_ids_set.contains(&v.line),
2328 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
2329 .lines
2330 .iter()
2331 .any(|line_id| segment_ids_set.contains(line_id)),
2332 Constraint::Parallel(parallel) => {
2333 parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
2334 }
2335 Constraint::Perpendicular(perpendicular) => perpendicular
2336 .lines
2337 .iter()
2338 .any(|line_id| segment_ids_set.contains(line_id)),
2339 };
2340 if depends_on_segment {
2341 constraint_ids_set.insert(*constraint_id);
2342 }
2343 }
2344 Ok(())
2345 }
2346
2347 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
2348 #[cfg(not(feature = "artifact-graph"))]
2349 {
2350 let _ = freedom_analysis_ran; outcome
2352 }
2353 #[cfg(feature = "artifact-graph")]
2354 {
2355 let mut outcome = outcome;
2356 let new_objects = std::mem::take(&mut outcome.scene_objects);
2357
2358 if freedom_analysis_ran {
2359 self.point_freedom_cache.clear();
2362 for new_obj in &new_objects {
2363 if let ObjectKind::Segment {
2364 segment: crate::front::Segment::Point(point),
2365 } = &new_obj.kind
2366 {
2367 self.point_freedom_cache.insert(new_obj.id, point.freedom);
2368 }
2369 }
2370 self.scene_graph.objects = new_objects;
2372 } else {
2373 for old_obj in &self.scene_graph.objects {
2376 if let ObjectKind::Segment {
2377 segment: crate::front::Segment::Point(point),
2378 } = &old_obj.kind
2379 {
2380 self.point_freedom_cache.insert(old_obj.id, point.freedom);
2381 }
2382 }
2383
2384 let mut updated_objects = Vec::with_capacity(new_objects.len());
2386 for new_obj in new_objects {
2387 let mut obj = new_obj;
2388 if let ObjectKind::Segment {
2389 segment: crate::front::Segment::Point(point),
2390 } = &mut obj.kind
2391 {
2392 let new_freedom = point.freedom;
2393 match new_freedom {
2399 Freedom::Free => {
2400 match self.point_freedom_cache.get(&obj.id).copied() {
2401 Some(Freedom::Conflict) => {
2402 }
2405 Some(Freedom::Fixed) => {
2406 point.freedom = Freedom::Fixed;
2408 }
2409 Some(Freedom::Free) => {
2410 }
2412 None => {
2413 }
2415 }
2416 }
2417 Freedom::Fixed => {
2418 }
2420 Freedom::Conflict => {
2421 }
2423 }
2424 self.point_freedom_cache.insert(obj.id, point.freedom);
2426 }
2427 updated_objects.push(obj);
2428 }
2429
2430 self.scene_graph.objects = updated_objects;
2431 }
2432 outcome
2433 }
2434 }
2435
2436 fn only_sketch_block(
2437 &self,
2438 sketch_id: ObjectId,
2439 edit_kind: ChangeKind,
2440 ast: &mut ast::Node<ast::Program>,
2441 ) -> api::Result<()> {
2442 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
2443 msg: format!("Sketch not found: {sketch_id:?}"),
2444 })?;
2445 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2446 return Err(Error {
2447 msg: format!("Object is not a sketch: {sketch_object:?}"),
2448 });
2449 };
2450 let sketch_block_range = expect_single_source_range(&sketch_object.source)?;
2451 only_sketch_block(ast, sketch_block_range, edit_kind)
2452 }
2453
2454 fn mutate_ast(
2455 &mut self,
2456 ast: &mut ast::Node<ast::Program>,
2457 object_id: ObjectId,
2458 command: AstMutateCommand,
2459 ) -> api::Result<(SourceRange, AstMutateCommandReturn)> {
2460 let sketch_object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
2461 msg: format!("Object not found: {object_id:?}"),
2462 })?;
2463 match &sketch_object.source {
2464 SourceRef::Simple { range } => mutate_ast_node_by_source_range(ast, *range, command),
2465 SourceRef::BackTrace { .. } => Err(Error {
2466 msg: "BackTrace source refs not supported yet".to_owned(),
2467 }),
2468 }
2469 }
2470}
2471
2472fn expect_single_source_range(source_ref: &SourceRef) -> api::Result<SourceRange> {
2473 match source_ref {
2474 SourceRef::Simple { range } => Ok(*range),
2475 SourceRef::BackTrace { ranges } => {
2476 if ranges.len() != 1 {
2477 return Err(Error {
2478 msg: format!(
2479 "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
2480 ranges.len(),
2481 ),
2482 });
2483 }
2484 Ok(ranges[0])
2485 }
2486 }
2487}
2488
2489fn only_sketch_block(
2490 ast: &mut ast::Node<ast::Program>,
2491 sketch_block_range: SourceRange,
2492 edit_kind: ChangeKind,
2493) -> api::Result<()> {
2494 let r1 = sketch_block_range;
2495 let matches_range = |r2: SourceRange| -> bool {
2496 match edit_kind {
2499 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
2500 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
2502 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
2503 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
2505 }
2506 };
2507 let mut found = false;
2508 for item in ast.body.iter_mut() {
2509 match item {
2510 ast::BodyItem::ImportStatement(_) => {}
2511 ast::BodyItem::ExpressionStatement(node) => {
2512 if matches_range(SourceRange::from(&*node))
2513 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
2514 {
2515 sketch_block.is_being_edited = true;
2516 found = true;
2517 break;
2518 }
2519 }
2520 ast::BodyItem::VariableDeclaration(node) => {
2521 if matches_range(SourceRange::from(&node.declaration.init))
2522 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
2523 {
2524 sketch_block.is_being_edited = true;
2525 found = true;
2526 break;
2527 }
2528 }
2529 ast::BodyItem::TypeDeclaration(_) => {}
2530 ast::BodyItem::ReturnStatement(node) => {
2531 if matches_range(SourceRange::from(&node.argument))
2532 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
2533 {
2534 sketch_block.is_being_edited = true;
2535 found = true;
2536 break;
2537 }
2538 }
2539 }
2540 }
2541 if !found {
2542 return Err(Error {
2543 msg: format!("Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"),
2544 });
2545 }
2546
2547 Ok(())
2548}
2549
2550fn get_or_insert_ast_reference(
2557 ast: &mut ast::Node<ast::Program>,
2558 source_ref: &SourceRef,
2559 prefix: &str,
2560 property: Option<&str>,
2561) -> api::Result<ast::Expr> {
2562 let range = expect_single_source_range(source_ref)?;
2563 let command = AstMutateCommand::AddVariableDeclaration {
2564 prefix: prefix.to_owned(),
2565 };
2566 let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
2567 let AstMutateCommandReturn::Name(var_name) = ret else {
2568 return Err(Error {
2569 msg: "Expected variable name returned from AddVariableDeclaration".to_owned(),
2570 });
2571 };
2572 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
2573 let Some(property) = property else {
2574 return Ok(var_expr);
2576 };
2577
2578 Ok(ast::Expr::MemberExpression(Box::new(ast::Node::no_src(
2579 ast::MemberExpression {
2580 object: var_expr,
2581 property: ast::Expr::Name(Box::new(ast::Name::new(property))),
2582 computed: false,
2583 digest: None,
2584 },
2585 ))))
2586}
2587
2588fn mutate_ast_node_by_source_range(
2589 ast: &mut ast::Node<ast::Program>,
2590 source_range: SourceRange,
2591 command: AstMutateCommand,
2592) -> Result<(SourceRange, AstMutateCommandReturn), Error> {
2593 let mut context = AstMutateContext {
2594 source_range,
2595 command,
2596 defined_names_stack: Default::default(),
2597 };
2598 let control = dfs_mut(ast, &mut context);
2599 match control {
2600 ControlFlow::Continue(_) => Err(Error {
2601 msg: format!("Source range not found: {source_range:?}"),
2602 }),
2603 ControlFlow::Break(break_value) => break_value,
2604 }
2605}
2606
2607#[derive(Debug)]
2608struct AstMutateContext {
2609 source_range: SourceRange,
2610 command: AstMutateCommand,
2611 defined_names_stack: Vec<HashSet<String>>,
2612}
2613
2614#[derive(Debug)]
2615#[allow(clippy::large_enum_variant)]
2616enum AstMutateCommand {
2617 AddSketchBlockExprStmt {
2619 expr: ast::Expr,
2620 },
2621 AddVariableDeclaration {
2622 prefix: String,
2623 },
2624 EditPoint {
2625 at: ast::Expr,
2626 },
2627 EditLine {
2628 start: ast::Expr,
2629 end: ast::Expr,
2630 construction: Option<bool>,
2631 },
2632 EditArc {
2633 start: ast::Expr,
2634 end: ast::Expr,
2635 center: ast::Expr,
2636 construction: Option<bool>,
2637 },
2638 #[cfg(feature = "artifact-graph")]
2639 EditVarInitialValue {
2640 value: Number,
2641 },
2642 DeleteNode,
2643}
2644
2645#[derive(Debug)]
2646enum AstMutateCommandReturn {
2647 None,
2648 Name(String),
2649}
2650
2651impl Visitor for AstMutateContext {
2652 type Break = Result<(SourceRange, AstMutateCommandReturn), Error>;
2653 type Continue = ();
2654
2655 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
2656 filter_and_process(self, node)
2657 }
2658
2659 fn finish(&mut self, node: NodeMut<'_>) {
2660 match &node {
2661 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
2662 self.defined_names_stack.pop();
2663 }
2664 _ => {}
2665 }
2666 }
2667}
2668
2669fn filter_and_process(
2670 ctx: &mut AstMutateContext,
2671 node: NodeMut,
2672) -> TraversalReturn<Result<(SourceRange, AstMutateCommandReturn), Error>> {
2673 let Ok(node_range) = SourceRange::try_from(&node) else {
2674 return TraversalReturn::new_continue(());
2676 };
2677 if let NodeMut::VariableDeclaration(var_decl) = &node {
2682 let expr_range = SourceRange::from(&var_decl.declaration.init);
2683 if expr_range == ctx.source_range {
2684 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
2685 return TraversalReturn::new_break(Ok((
2688 node_range,
2689 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
2690 )));
2691 }
2692 if let AstMutateCommand::DeleteNode = &ctx.command {
2693 return TraversalReturn {
2696 mutate_body_item: MutateBodyItem::Delete,
2697 control_flow: ControlFlow::Break(Ok((ctx.source_range, AstMutateCommandReturn::None))),
2698 };
2699 }
2700 }
2701 }
2702
2703 if let NodeMut::Program(program) = &node {
2704 ctx.defined_names_stack.push(find_defined_names(*program));
2705 } else if let NodeMut::SketchBlock(block) = &node {
2706 ctx.defined_names_stack.push(find_defined_names(&block.body));
2707 }
2708
2709 if node_range != ctx.source_range {
2711 return TraversalReturn::new_continue(());
2712 }
2713 process(ctx, node).map_break(|result| result.map(|cmd_return| (ctx.source_range, cmd_return)))
2714}
2715
2716fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, Error>> {
2717 match &ctx.command {
2718 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
2719 if let NodeMut::SketchBlock(sketch_block) = node {
2720 sketch_block
2721 .body
2722 .items
2723 .push(ast::BodyItem::ExpressionStatement(ast::Node {
2724 inner: ast::ExpressionStatement {
2725 expression: expr.clone(),
2726 digest: None,
2727 },
2728 start: Default::default(),
2729 end: Default::default(),
2730 module_id: Default::default(),
2731 outer_attrs: Default::default(),
2732 pre_comments: Default::default(),
2733 comment_start: Default::default(),
2734 }));
2735 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2736 }
2737 }
2738 AstMutateCommand::AddVariableDeclaration { prefix } => {
2739 if let NodeMut::VariableDeclaration(inner) = node {
2740 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
2741 }
2742 if let NodeMut::ExpressionStatement(expr_stmt) = node {
2743 let empty_defined_names = HashSet::new();
2744 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
2745 let Ok(name) = next_free_name(prefix, defined_names) else {
2746 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2748 };
2749 let mutate_node =
2750 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
2751 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
2752 ast::ItemVisibility::Default,
2753 ast::VariableKind::Const,
2754 ))));
2755 return TraversalReturn {
2756 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
2757 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
2758 };
2759 }
2760 }
2761 AstMutateCommand::EditPoint { at } => {
2762 if let NodeMut::CallExpressionKw(call) = node {
2763 if call.callee.name.name != POINT_FN {
2764 return TraversalReturn::new_continue(());
2765 }
2766 for labeled_arg in &mut call.arguments {
2768 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
2769 labeled_arg.arg = at.clone();
2770 }
2771 }
2772 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2773 }
2774 }
2775 AstMutateCommand::EditLine {
2776 start,
2777 end,
2778 construction,
2779 } => {
2780 if let NodeMut::CallExpressionKw(call) = node {
2781 if call.callee.name.name != LINE_FN {
2782 return TraversalReturn::new_continue(());
2783 }
2784 for labeled_arg in &mut call.arguments {
2786 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
2787 labeled_arg.arg = start.clone();
2788 }
2789 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
2790 labeled_arg.arg = end.clone();
2791 }
2792 }
2793 if let Some(construction_value) = construction {
2795 let construction_exists = call
2796 .arguments
2797 .iter()
2798 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
2799 if *construction_value {
2800 if construction_exists {
2802 for labeled_arg in &mut call.arguments {
2804 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
2805 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2806 value: ast::LiteralValue::Bool(true),
2807 raw: "true".to_string(),
2808 digest: None,
2809 })));
2810 }
2811 }
2812 } else {
2813 call.arguments.push(ast::LabeledArg {
2815 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2816 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2817 value: ast::LiteralValue::Bool(true),
2818 raw: "true".to_string(),
2819 digest: None,
2820 }))),
2821 });
2822 }
2823 } else {
2824 call.arguments
2826 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
2827 }
2828 }
2829 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2830 }
2831 }
2832 AstMutateCommand::EditArc {
2833 start,
2834 end,
2835 center,
2836 construction,
2837 } => {
2838 if let NodeMut::CallExpressionKw(call) = node {
2839 if call.callee.name.name != ARC_FN {
2840 return TraversalReturn::new_continue(());
2841 }
2842 for labeled_arg in &mut call.arguments {
2844 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
2845 labeled_arg.arg = start.clone();
2846 }
2847 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
2848 labeled_arg.arg = end.clone();
2849 }
2850 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
2851 labeled_arg.arg = center.clone();
2852 }
2853 }
2854 if let Some(construction_value) = construction {
2856 let construction_exists = call
2857 .arguments
2858 .iter()
2859 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
2860 if *construction_value {
2861 if construction_exists {
2863 for labeled_arg in &mut call.arguments {
2865 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
2866 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2867 value: ast::LiteralValue::Bool(true),
2868 raw: "true".to_string(),
2869 digest: None,
2870 })));
2871 }
2872 }
2873 } else {
2874 call.arguments.push(ast::LabeledArg {
2876 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2877 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2878 value: ast::LiteralValue::Bool(true),
2879 raw: "true".to_string(),
2880 digest: None,
2881 }))),
2882 });
2883 }
2884 } else {
2885 call.arguments
2887 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
2888 }
2889 }
2890 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2891 }
2892 }
2893 #[cfg(feature = "artifact-graph")]
2894 AstMutateCommand::EditVarInitialValue { value } => {
2895 if let NodeMut::NumericLiteral(numeric_literal) = node {
2896 let Ok(literal) = to_source_number(*value) else {
2898 return TraversalReturn::new_break(Err(Error {
2899 msg: format!("Could not convert number to AST literal: {:?}", *value),
2900 }));
2901 };
2902 *numeric_literal = ast::Node::no_src(literal);
2903 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
2904 }
2905 }
2906 AstMutateCommand::DeleteNode => {
2907 return TraversalReturn {
2908 mutate_body_item: MutateBodyItem::Delete,
2909 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
2910 };
2911 }
2912 }
2913 TraversalReturn::new_continue(())
2914}
2915
2916struct FindSketchBlockSourceRange {
2917 target_before_mutation: SourceRange,
2919 found: Cell<Option<SourceRange>>,
2923}
2924
2925impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
2926 type Error = crate::front::Error;
2927
2928 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
2929 let Ok(node_range) = SourceRange::try_from(&node) else {
2930 return Ok(true);
2931 };
2932
2933 if let crate::walk::Node::SketchBlock(sketch_block) = node {
2934 if node_range.module_id() == self.target_before_mutation.module_id()
2935 && node_range.start() == self.target_before_mutation.start()
2936 && node_range.end() >= self.target_before_mutation.end()
2938 {
2939 self.found.set(sketch_block.body.items.last().map(SourceRange::from));
2940 return Ok(false);
2941 } else {
2942 return Ok(true);
2945 }
2946 }
2947
2948 for child in node.children().iter() {
2949 if !child.visit(*self)? {
2950 return Ok(false);
2951 }
2952 }
2953
2954 Ok(true)
2955 }
2956}
2957
2958fn find_sketch_block_added_item(
2966 ast: &ast::Node<ast::Program>,
2967 range_before_mutation: SourceRange,
2968) -> api::Result<SourceRange> {
2969 let find = FindSketchBlockSourceRange {
2970 target_before_mutation: range_before_mutation,
2971 found: Cell::new(None),
2972 };
2973 let node = crate::walk::Node::from(ast);
2974 node.visit(&find)?;
2975 find.found.into_inner().ok_or_else(|| api::Error {
2976 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?"),
2977 })
2978}
2979
2980fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
2981 ast.recast_top(&Default::default(), 0)
2983}
2984
2985fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
2986 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
2987 inner: ast::ArrayExpression {
2988 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
2989 non_code_meta: Default::default(),
2990 digest: None,
2991 },
2992 start: Default::default(),
2993 end: Default::default(),
2994 module_id: Default::default(),
2995 outer_attrs: Default::default(),
2996 pre_comments: Default::default(),
2997 comment_start: Default::default(),
2998 })))
2999}
3000
3001fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
3002 match expr {
3003 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
3004 inner: ast::Literal::from(to_source_number(*number)?),
3005 start: Default::default(),
3006 end: Default::default(),
3007 module_id: Default::default(),
3008 outer_attrs: Default::default(),
3009 pre_comments: Default::default(),
3010 comment_start: Default::default(),
3011 }))),
3012 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
3013 inner: ast::SketchVar {
3014 initial: Some(Box::new(ast::Node {
3015 inner: to_source_number(*number)?,
3016 start: Default::default(),
3017 end: Default::default(),
3018 module_id: Default::default(),
3019 outer_attrs: Default::default(),
3020 pre_comments: Default::default(),
3021 comment_start: Default::default(),
3022 })),
3023 digest: None,
3024 },
3025 start: Default::default(),
3026 end: Default::default(),
3027 module_id: Default::default(),
3028 outer_attrs: Default::default(),
3029 pre_comments: Default::default(),
3030 comment_start: Default::default(),
3031 }))),
3032 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
3033 }
3034}
3035
3036fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
3037 Ok(ast::NumericLiteral {
3038 value: number.value,
3039 suffix: number.units,
3040 raw: format_number_literal(number.value, number.units)?,
3041 digest: None,
3042 })
3043}
3044
3045fn ast_name_expr(name: String) -> ast::Expr {
3046 ast::Expr::Name(Box::new(ast_name(name)))
3047}
3048
3049fn ast_name(name: String) -> ast::Node<ast::Name> {
3050 ast::Node {
3051 inner: ast::Name {
3052 name: ast::Node {
3053 inner: ast::Identifier { name, digest: None },
3054 start: Default::default(),
3055 end: Default::default(),
3056 module_id: Default::default(),
3057 outer_attrs: Default::default(),
3058 pre_comments: Default::default(),
3059 comment_start: Default::default(),
3060 },
3061 path: Vec::new(),
3062 abs_path: false,
3063 digest: None,
3064 },
3065 start: Default::default(),
3066 end: Default::default(),
3067 module_id: Default::default(),
3068 outer_attrs: Default::default(),
3069 pre_comments: Default::default(),
3070 comment_start: Default::default(),
3071 }
3072}
3073
3074fn ast_sketch2_name(name: &str) -> ast::Name {
3075 ast::Name {
3076 name: ast::Node {
3077 inner: ast::Identifier {
3078 name: name.to_owned(),
3079 digest: None,
3080 },
3081 start: Default::default(),
3082 end: Default::default(),
3083 module_id: Default::default(),
3084 outer_attrs: Default::default(),
3085 pre_comments: Default::default(),
3086 comment_start: Default::default(),
3087 },
3088 path: vec![ast::Node::no_src(ast::Identifier {
3089 name: "sketch2".to_owned(),
3090 digest: None,
3091 })],
3092 abs_path: false,
3093 digest: None,
3094 }
3095}
3096
3097#[cfg(test)]
3098mod tests {
3099 use super::*;
3100 use crate::{
3101 engine::PlaneName,
3102 front::{Distance, Object, Plane, Sketch},
3103 frontend::sketch::Vertical,
3104 pretty::NumericSuffix,
3105 };
3106
3107 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
3108 for object in &scene_graph.objects {
3109 if let ObjectKind::Sketch(_) = &object.kind {
3110 return Some(object);
3111 }
3112 }
3113 None
3114 }
3115
3116 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
3117 for object in &scene_graph.objects {
3118 if let ObjectKind::Face(_) = &object.kind {
3119 return Some(object);
3120 }
3121 }
3122 None
3123 }
3124
3125 #[track_caller]
3126 fn expect_sketch(object: &Object) -> &Sketch {
3127 if let ObjectKind::Sketch(sketch) = &object.kind {
3128 sketch
3129 } else {
3130 panic!("Object is not a sketch: {:?}", object);
3131 }
3132 }
3133
3134 #[tokio::test(flavor = "multi_thread")]
3135 async fn test_new_sketch_add_point_edit_point() {
3136 let program = Program::empty();
3137
3138 let mut frontend = FrontendState::new();
3139 frontend.program = program;
3140
3141 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3142 let mock_ctx = ExecutorContext::new_mock(None).await;
3143 let version = Version(0);
3144
3145 let sketch_args = SketchCtor {
3146 on: PlaneName::Xy.to_string(),
3147 };
3148 let (_src_delta, scene_delta, sketch_id) = frontend
3149 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3150 .await
3151 .unwrap();
3152 assert_eq!(sketch_id, ObjectId(1));
3153 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3154 let sketch_object = &scene_delta.new_graph.objects[1];
3155 assert_eq!(sketch_object.id, ObjectId(1));
3156 assert_eq!(
3157 sketch_object.kind,
3158 ObjectKind::Sketch(Sketch {
3159 args: SketchCtor {
3160 on: PlaneName::Xy.to_string()
3161 },
3162 plane: ObjectId(0),
3163 segments: vec![],
3164 constraints: vec![],
3165 })
3166 );
3167 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3168
3169 let point_ctor = PointCtor {
3170 position: Point2d {
3171 x: Expr::Number(Number {
3172 value: 1.0,
3173 units: NumericSuffix::Inch,
3174 }),
3175 y: Expr::Number(Number {
3176 value: 2.0,
3177 units: NumericSuffix::Inch,
3178 }),
3179 },
3180 };
3181 let segment = SegmentCtor::Point(point_ctor);
3182 let (src_delta, scene_delta) = frontend
3183 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3184 .await
3185 .unwrap();
3186 assert_eq!(
3187 src_delta.text.as_str(),
3188 "@settings(experimentalFeatures = allow)
3189
3190sketch(on = XY) {
3191 sketch2::point(at = [1in, 2in])
3192}
3193"
3194 );
3195 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
3196 assert_eq!(scene_delta.new_graph.objects.len(), 3);
3197 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3198 assert_eq!(scene_object.id.0, i);
3199 }
3200
3201 let point_id = *scene_delta.new_objects.last().unwrap();
3202
3203 let point_ctor = PointCtor {
3204 position: Point2d {
3205 x: Expr::Number(Number {
3206 value: 3.0,
3207 units: NumericSuffix::Inch,
3208 }),
3209 y: Expr::Number(Number {
3210 value: 4.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 "@settings(experimentalFeatures = allow)
3226
3227sketch(on = XY) {
3228 sketch2::point(at = [3in, 4in])
3229}
3230"
3231 );
3232 assert_eq!(scene_delta.new_objects, vec![]);
3233 assert_eq!(scene_delta.new_graph.objects.len(), 3);
3234
3235 ctx.close().await;
3236 mock_ctx.close().await;
3237 }
3238
3239 #[tokio::test(flavor = "multi_thread")]
3240 async fn test_new_sketch_add_line_edit_line() {
3241 let program = Program::empty();
3242
3243 let mut frontend = FrontendState::new();
3244 frontend.program = program;
3245
3246 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3247 let mock_ctx = ExecutorContext::new_mock(None).await;
3248 let version = Version(0);
3249
3250 let sketch_args = SketchCtor {
3251 on: PlaneName::Xy.to_string(),
3252 };
3253 let (_src_delta, scene_delta, sketch_id) = frontend
3254 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3255 .await
3256 .unwrap();
3257 assert_eq!(sketch_id, ObjectId(1));
3258 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3259 let sketch_object = &scene_delta.new_graph.objects[1];
3260 assert_eq!(sketch_object.id, ObjectId(1));
3261 assert_eq!(
3262 sketch_object.kind,
3263 ObjectKind::Sketch(Sketch {
3264 args: SketchCtor {
3265 on: PlaneName::Xy.to_string()
3266 },
3267 plane: ObjectId(0),
3268 segments: vec![],
3269 constraints: vec![],
3270 })
3271 );
3272 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3273
3274 let line_ctor = LineCtor {
3275 start: Point2d {
3276 x: Expr::Number(Number {
3277 value: 0.0,
3278 units: NumericSuffix::Mm,
3279 }),
3280 y: Expr::Number(Number {
3281 value: 0.0,
3282 units: NumericSuffix::Mm,
3283 }),
3284 },
3285 end: Point2d {
3286 x: Expr::Number(Number {
3287 value: 10.0,
3288 units: NumericSuffix::Mm,
3289 }),
3290 y: Expr::Number(Number {
3291 value: 10.0,
3292 units: NumericSuffix::Mm,
3293 }),
3294 },
3295 construction: None,
3296 };
3297 let segment = SegmentCtor::Line(line_ctor);
3298 let (src_delta, scene_delta) = frontend
3299 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3300 .await
3301 .unwrap();
3302 assert_eq!(
3303 src_delta.text.as_str(),
3304 "@settings(experimentalFeatures = allow)
3305
3306sketch(on = XY) {
3307 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
3308}
3309"
3310 );
3311 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
3312 assert_eq!(scene_delta.new_graph.objects.len(), 5);
3313 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3314 assert_eq!(scene_object.id.0, i);
3315 }
3316
3317 let line = *scene_delta.new_objects.last().unwrap();
3319
3320 let line_ctor = LineCtor {
3321 start: Point2d {
3322 x: Expr::Number(Number {
3323 value: 1.0,
3324 units: NumericSuffix::Mm,
3325 }),
3326 y: Expr::Number(Number {
3327 value: 2.0,
3328 units: NumericSuffix::Mm,
3329 }),
3330 },
3331 end: Point2d {
3332 x: Expr::Number(Number {
3333 value: 13.0,
3334 units: NumericSuffix::Mm,
3335 }),
3336 y: Expr::Number(Number {
3337 value: 14.0,
3338 units: NumericSuffix::Mm,
3339 }),
3340 },
3341 construction: None,
3342 };
3343 let segments = vec![ExistingSegmentCtor {
3344 id: line,
3345 ctor: SegmentCtor::Line(line_ctor),
3346 }];
3347 let (src_delta, scene_delta) = frontend
3348 .edit_segments(&mock_ctx, version, sketch_id, segments)
3349 .await
3350 .unwrap();
3351 assert_eq!(
3352 src_delta.text.as_str(),
3353 "@settings(experimentalFeatures = allow)
3354
3355sketch(on = XY) {
3356 sketch2::line(start = [1mm, 2mm], end = [13mm, 14mm])
3357}
3358"
3359 );
3360 assert_eq!(scene_delta.new_objects, vec![]);
3361 assert_eq!(scene_delta.new_graph.objects.len(), 5);
3362
3363 ctx.close().await;
3364 mock_ctx.close().await;
3365 }
3366
3367 #[tokio::test(flavor = "multi_thread")]
3368 async fn test_new_sketch_add_arc_edit_arc() {
3369 let program = Program::empty();
3370
3371 let mut frontend = FrontendState::new();
3372 frontend.program = program;
3373
3374 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3375 let mock_ctx = ExecutorContext::new_mock(None).await;
3376 let version = Version(0);
3377
3378 let sketch_args = SketchCtor {
3379 on: PlaneName::Xy.to_string(),
3380 };
3381 let (_src_delta, scene_delta, sketch_id) = frontend
3382 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3383 .await
3384 .unwrap();
3385 assert_eq!(sketch_id, ObjectId(1));
3386 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3387 let sketch_object = &scene_delta.new_graph.objects[1];
3388 assert_eq!(sketch_object.id, ObjectId(1));
3389 assert_eq!(
3390 sketch_object.kind,
3391 ObjectKind::Sketch(Sketch {
3392 args: SketchCtor {
3393 on: PlaneName::Xy.to_string(),
3394 },
3395 plane: ObjectId(0),
3396 segments: vec![],
3397 constraints: vec![],
3398 })
3399 );
3400 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3401
3402 let arc_ctor = ArcCtor {
3403 start: Point2d {
3404 x: Expr::Var(Number {
3405 value: 0.0,
3406 units: NumericSuffix::Mm,
3407 }),
3408 y: Expr::Var(Number {
3409 value: 0.0,
3410 units: NumericSuffix::Mm,
3411 }),
3412 },
3413 end: Point2d {
3414 x: Expr::Var(Number {
3415 value: 10.0,
3416 units: NumericSuffix::Mm,
3417 }),
3418 y: Expr::Var(Number {
3419 value: 10.0,
3420 units: NumericSuffix::Mm,
3421 }),
3422 },
3423 center: Point2d {
3424 x: Expr::Var(Number {
3425 value: 10.0,
3426 units: NumericSuffix::Mm,
3427 }),
3428 y: Expr::Var(Number {
3429 value: 0.0,
3430 units: NumericSuffix::Mm,
3431 }),
3432 },
3433 construction: None,
3434 };
3435 let segment = SegmentCtor::Arc(arc_ctor);
3436 let (src_delta, scene_delta) = frontend
3437 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3438 .await
3439 .unwrap();
3440 assert_eq!(
3441 src_delta.text.as_str(),
3442 "@settings(experimentalFeatures = allow)
3443
3444sketch(on = XY) {
3445 sketch2::arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
3446}
3447"
3448 );
3449 assert_eq!(
3450 scene_delta.new_objects,
3451 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
3452 );
3453 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3454 assert_eq!(scene_object.id.0, i);
3455 }
3456 assert_eq!(scene_delta.new_graph.objects.len(), 6);
3457
3458 let arc = *scene_delta.new_objects.last().unwrap();
3460
3461 let arc_ctor = ArcCtor {
3462 start: Point2d {
3463 x: Expr::Var(Number {
3464 value: 1.0,
3465 units: NumericSuffix::Mm,
3466 }),
3467 y: Expr::Var(Number {
3468 value: 2.0,
3469 units: NumericSuffix::Mm,
3470 }),
3471 },
3472 end: Point2d {
3473 x: Expr::Var(Number {
3474 value: 13.0,
3475 units: NumericSuffix::Mm,
3476 }),
3477 y: Expr::Var(Number {
3478 value: 14.0,
3479 units: NumericSuffix::Mm,
3480 }),
3481 },
3482 center: Point2d {
3483 x: Expr::Var(Number {
3484 value: 13.0,
3485 units: NumericSuffix::Mm,
3486 }),
3487 y: Expr::Var(Number {
3488 value: 2.0,
3489 units: NumericSuffix::Mm,
3490 }),
3491 },
3492 construction: None,
3493 };
3494 let segments = vec![ExistingSegmentCtor {
3495 id: arc,
3496 ctor: SegmentCtor::Arc(arc_ctor),
3497 }];
3498 let (src_delta, scene_delta) = frontend
3499 .edit_segments(&mock_ctx, version, sketch_id, segments)
3500 .await
3501 .unwrap();
3502 assert_eq!(
3503 src_delta.text.as_str(),
3504 "@settings(experimentalFeatures = allow)
3505
3506sketch(on = XY) {
3507 sketch2::arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
3508}
3509"
3510 );
3511 assert_eq!(scene_delta.new_objects, vec![]);
3512 assert_eq!(scene_delta.new_graph.objects.len(), 6);
3513
3514 ctx.close().await;
3515 mock_ctx.close().await;
3516 }
3517
3518 #[tokio::test(flavor = "multi_thread")]
3519 async fn test_add_line_when_sketch_block_uses_variable() {
3520 let initial_source = "@settings(experimentalFeatures = allow)
3521
3522s = sketch(on = XY) {}
3523";
3524
3525 let program = Program::parse(initial_source).unwrap().0.unwrap();
3526
3527 let mut frontend = FrontendState::new();
3528
3529 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3530 let mock_ctx = ExecutorContext::new_mock(None).await;
3531 let version = Version(0);
3532
3533 frontend.hack_set_program(&ctx, program).await.unwrap();
3534 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
3535 let sketch_id = sketch_object.id;
3536
3537 let line_ctor = LineCtor {
3538 start: Point2d {
3539 x: Expr::Number(Number {
3540 value: 0.0,
3541 units: NumericSuffix::Mm,
3542 }),
3543 y: Expr::Number(Number {
3544 value: 0.0,
3545 units: NumericSuffix::Mm,
3546 }),
3547 },
3548 end: Point2d {
3549 x: Expr::Number(Number {
3550 value: 10.0,
3551 units: NumericSuffix::Mm,
3552 }),
3553 y: Expr::Number(Number {
3554 value: 10.0,
3555 units: NumericSuffix::Mm,
3556 }),
3557 },
3558 construction: None,
3559 };
3560 let segment = SegmentCtor::Line(line_ctor);
3561 let (src_delta, scene_delta) = frontend
3562 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3563 .await
3564 .unwrap();
3565 assert_eq!(
3566 src_delta.text.as_str(),
3567 "@settings(experimentalFeatures = allow)
3568
3569s = sketch(on = XY) {
3570 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
3571}
3572"
3573 );
3574 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
3575 assert_eq!(scene_delta.new_graph.objects.len(), 5);
3576
3577 ctx.close().await;
3578 mock_ctx.close().await;
3579 }
3580
3581 #[tokio::test(flavor = "multi_thread")]
3582 async fn test_new_sketch_add_line_delete_sketch() {
3583 let program = Program::empty();
3584
3585 let mut frontend = FrontendState::new();
3586 frontend.program = program;
3587
3588 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3589 let mock_ctx = ExecutorContext::new_mock(None).await;
3590 let version = Version(0);
3591
3592 let sketch_args = SketchCtor {
3593 on: PlaneName::Xy.to_string(),
3594 };
3595 let (_src_delta, scene_delta, sketch_id) = frontend
3596 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3597 .await
3598 .unwrap();
3599 assert_eq!(sketch_id, ObjectId(1));
3600 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3601 let sketch_object = &scene_delta.new_graph.objects[1];
3602 assert_eq!(sketch_object.id, ObjectId(1));
3603 assert_eq!(
3604 sketch_object.kind,
3605 ObjectKind::Sketch(Sketch {
3606 args: SketchCtor {
3607 on: PlaneName::Xy.to_string()
3608 },
3609 plane: ObjectId(0),
3610 segments: vec![],
3611 constraints: vec![],
3612 })
3613 );
3614 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3615
3616 let line_ctor = LineCtor {
3617 start: Point2d {
3618 x: Expr::Number(Number {
3619 value: 0.0,
3620 units: NumericSuffix::Mm,
3621 }),
3622 y: Expr::Number(Number {
3623 value: 0.0,
3624 units: NumericSuffix::Mm,
3625 }),
3626 },
3627 end: Point2d {
3628 x: Expr::Number(Number {
3629 value: 10.0,
3630 units: NumericSuffix::Mm,
3631 }),
3632 y: Expr::Number(Number {
3633 value: 10.0,
3634 units: NumericSuffix::Mm,
3635 }),
3636 },
3637 construction: None,
3638 };
3639 let segment = SegmentCtor::Line(line_ctor);
3640 let (src_delta, scene_delta) = frontend
3641 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3642 .await
3643 .unwrap();
3644 assert_eq!(
3645 src_delta.text.as_str(),
3646 "@settings(experimentalFeatures = allow)
3647
3648sketch(on = XY) {
3649 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
3650}
3651"
3652 );
3653 assert_eq!(scene_delta.new_graph.objects.len(), 5);
3654
3655 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
3656 assert_eq!(
3657 src_delta.text.as_str(),
3658 "@settings(experimentalFeatures = allow)
3659"
3660 );
3661 assert_eq!(scene_delta.new_graph.objects.len(), 0);
3662
3663 ctx.close().await;
3664 mock_ctx.close().await;
3665 }
3666
3667 #[tokio::test(flavor = "multi_thread")]
3668 async fn test_delete_sketch_when_sketch_block_uses_variable() {
3669 let initial_source = "@settings(experimentalFeatures = allow)
3670
3671s = sketch(on = XY) {}
3672";
3673
3674 let program = Program::parse(initial_source).unwrap().0.unwrap();
3675
3676 let mut frontend = FrontendState::new();
3677
3678 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3679 let mock_ctx = ExecutorContext::new_mock(None).await;
3680 let version = Version(0);
3681
3682 frontend.hack_set_program(&ctx, program).await.unwrap();
3683 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
3684 let sketch_id = sketch_object.id;
3685
3686 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
3687 assert_eq!(
3688 src_delta.text.as_str(),
3689 "@settings(experimentalFeatures = allow)
3690"
3691 );
3692 assert_eq!(scene_delta.new_graph.objects.len(), 0);
3693
3694 ctx.close().await;
3695 mock_ctx.close().await;
3696 }
3697
3698 #[tokio::test(flavor = "multi_thread")]
3699 async fn test_edit_line_when_editing_its_start_point() {
3700 let initial_source = "\
3701@settings(experimentalFeatures = allow)
3702
3703sketch(on = XY) {
3704 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3705}
3706";
3707
3708 let program = Program::parse(initial_source).unwrap().0.unwrap();
3709
3710 let mut frontend = FrontendState::new();
3711
3712 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3713 let mock_ctx = ExecutorContext::new_mock(None).await;
3714 let version = Version(0);
3715
3716 frontend.hack_set_program(&ctx, program).await.unwrap();
3717 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
3718 let sketch_id = sketch_object.id;
3719 let sketch = expect_sketch(sketch_object);
3720
3721 let point_id = *sketch.segments.first().unwrap();
3722
3723 let point_ctor = PointCtor {
3724 position: Point2d {
3725 x: Expr::Var(Number {
3726 value: 5.0,
3727 units: NumericSuffix::Inch,
3728 }),
3729 y: Expr::Var(Number {
3730 value: 6.0,
3731 units: NumericSuffix::Inch,
3732 }),
3733 },
3734 };
3735 let segments = vec![ExistingSegmentCtor {
3736 id: point_id,
3737 ctor: SegmentCtor::Point(point_ctor),
3738 }];
3739 let (src_delta, scene_delta) = frontend
3740 .edit_segments(&mock_ctx, version, sketch_id, segments)
3741 .await
3742 .unwrap();
3743 assert_eq!(
3744 src_delta.text.as_str(),
3745 "\
3746@settings(experimentalFeatures = allow)
3747
3748sketch(on = XY) {
3749 sketch2::line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
3750}
3751"
3752 );
3753 assert_eq!(scene_delta.new_objects, vec![]);
3754 assert_eq!(scene_delta.new_graph.objects.len(), 5);
3755
3756 ctx.close().await;
3757 mock_ctx.close().await;
3758 }
3759
3760 #[tokio::test(flavor = "multi_thread")]
3761 async fn test_edit_line_when_editing_its_end_point() {
3762 let initial_source = "\
3763@settings(experimentalFeatures = allow)
3764
3765sketch(on = XY) {
3766 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
3767}
3768";
3769
3770 let program = Program::parse(initial_source).unwrap().0.unwrap();
3771
3772 let mut frontend = FrontendState::new();
3773
3774 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3775 let mock_ctx = ExecutorContext::new_mock(None).await;
3776 let version = Version(0);
3777
3778 frontend.hack_set_program(&ctx, program).await.unwrap();
3779 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
3780 let sketch_id = sketch_object.id;
3781 let sketch = expect_sketch(sketch_object);
3782 let point_id = *sketch.segments.get(1).unwrap();
3783
3784 let point_ctor = PointCtor {
3785 position: Point2d {
3786 x: Expr::Var(Number {
3787 value: 5.0,
3788 units: NumericSuffix::Inch,
3789 }),
3790 y: Expr::Var(Number {
3791 value: 6.0,
3792 units: NumericSuffix::Inch,
3793 }),
3794 },
3795 };
3796 let segments = vec![ExistingSegmentCtor {
3797 id: point_id,
3798 ctor: SegmentCtor::Point(point_ctor),
3799 }];
3800 let (src_delta, scene_delta) = frontend
3801 .edit_segments(&mock_ctx, version, sketch_id, segments)
3802 .await
3803 .unwrap();
3804 assert_eq!(
3805 src_delta.text.as_str(),
3806 "\
3807@settings(experimentalFeatures = allow)
3808
3809sketch(on = XY) {
3810 sketch2::line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
3811}
3812"
3813 );
3814 assert_eq!(scene_delta.new_objects, vec![]);
3815 assert_eq!(
3816 scene_delta.new_graph.objects.len(),
3817 5,
3818 "{:#?}",
3819 scene_delta.new_graph.objects
3820 );
3821
3822 ctx.close().await;
3823 mock_ctx.close().await;
3824 }
3825
3826 #[tokio::test(flavor = "multi_thread")]
3827 async fn test_edit_line_with_coincident_feedback() {
3828 let initial_source = "\
3829@settings(experimentalFeatures = allow)
3830
3831sketch(on = XY) {
3832 line1 = sketch2::line(start = [var 1, var 2], end = [var 1, var 2])
3833 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
3834 line1.start.at[0] == 0
3835 line1.start.at[1] == 0
3836 sketch2::coincident([line1.end, line2.start])
3837 sketch2::equalLength([line1, line2])
3838}
3839";
3840
3841 let program = Program::parse(initial_source).unwrap().0.unwrap();
3842
3843 let mut frontend = FrontendState::new();
3844
3845 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3846 let mock_ctx = ExecutorContext::new_mock(None).await;
3847 let version = Version(0);
3848
3849 frontend.hack_set_program(&ctx, program).await.unwrap();
3850 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
3851 let sketch_id = sketch_object.id;
3852 let sketch = expect_sketch(sketch_object);
3853 let line2_end_id = *sketch.segments.get(4).unwrap();
3854
3855 let segments = vec![ExistingSegmentCtor {
3856 id: line2_end_id,
3857 ctor: SegmentCtor::Point(PointCtor {
3858 position: Point2d {
3859 x: Expr::Var(Number {
3860 value: 9.0,
3861 units: NumericSuffix::None,
3862 }),
3863 y: Expr::Var(Number {
3864 value: 10.0,
3865 units: NumericSuffix::None,
3866 }),
3867 },
3868 }),
3869 }];
3870 let (src_delta, scene_delta) = frontend
3871 .edit_segments(&mock_ctx, version, sketch_id, segments)
3872 .await
3873 .unwrap();
3874 assert_eq!(
3875 src_delta.text.as_str(),
3876 "\
3877@settings(experimentalFeatures = allow)
3878
3879sketch(on = XY) {
3880 line1 = sketch2::line(start = [var 0mm, var 0mm], end = [var 4.145mm, var 5.32mm])
3881 line2 = sketch2::line(start = [var 4.145mm, var 5.32mm], end = [var 9mm, var 10mm])
3882line1.start.at[0] == 0
3883line1.start.at[1] == 0
3884 sketch2::coincident([line1.end, line2.start])
3885 sketch2::equalLength([line1, line2])
3886}
3887"
3888 );
3889 assert_eq!(
3890 scene_delta.new_graph.objects.len(),
3891 10,
3892 "{:#?}",
3893 scene_delta.new_graph.objects
3894 );
3895
3896 ctx.close().await;
3897 mock_ctx.close().await;
3898 }
3899
3900 #[tokio::test(flavor = "multi_thread")]
3901 async fn test_delete_point_without_var() {
3902 let initial_source = "\
3903@settings(experimentalFeatures = allow)
3904
3905sketch(on = XY) {
3906 sketch2::point(at = [var 1, var 2])
3907 sketch2::point(at = [var 3, var 4])
3908 sketch2::point(at = [var 5, var 6])
3909}
3910";
3911
3912 let program = Program::parse(initial_source).unwrap().0.unwrap();
3913
3914 let mut frontend = FrontendState::new();
3915
3916 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3917 let mock_ctx = ExecutorContext::new_mock(None).await;
3918 let version = Version(0);
3919
3920 frontend.hack_set_program(&ctx, program).await.unwrap();
3921 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
3922 let sketch_id = sketch_object.id;
3923 let sketch = expect_sketch(sketch_object);
3924
3925 let point_id = *sketch.segments.get(1).unwrap();
3926
3927 let (src_delta, scene_delta) = frontend
3928 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
3929 .await
3930 .unwrap();
3931 assert_eq!(
3932 src_delta.text.as_str(),
3933 "\
3934@settings(experimentalFeatures = allow)
3935
3936sketch(on = XY) {
3937 sketch2::point(at = [var 1mm, var 2mm])
3938 sketch2::point(at = [var 5mm, var 6mm])
3939}
3940"
3941 );
3942 assert_eq!(scene_delta.new_objects, vec![]);
3943 assert_eq!(scene_delta.new_graph.objects.len(), 4);
3944
3945 ctx.close().await;
3946 mock_ctx.close().await;
3947 }
3948
3949 #[tokio::test(flavor = "multi_thread")]
3950 async fn test_delete_point_with_var() {
3951 let initial_source = "\
3952@settings(experimentalFeatures = allow)
3953
3954sketch(on = XY) {
3955 sketch2::point(at = [var 1, var 2])
3956 point1 = sketch2::point(at = [var 3, var 4])
3957 sketch2::point(at = [var 5, var 6])
3958}
3959";
3960
3961 let program = Program::parse(initial_source).unwrap().0.unwrap();
3962
3963 let mut frontend = FrontendState::new();
3964
3965 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3966 let mock_ctx = ExecutorContext::new_mock(None).await;
3967 let version = Version(0);
3968
3969 frontend.hack_set_program(&ctx, program).await.unwrap();
3970 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
3971 let sketch_id = sketch_object.id;
3972 let sketch = expect_sketch(sketch_object);
3973
3974 let point_id = *sketch.segments.get(1).unwrap();
3975
3976 let (src_delta, scene_delta) = frontend
3977 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
3978 .await
3979 .unwrap();
3980 assert_eq!(
3981 src_delta.text.as_str(),
3982 "\
3983@settings(experimentalFeatures = allow)
3984
3985sketch(on = XY) {
3986 sketch2::point(at = [var 1mm, var 2mm])
3987 sketch2::point(at = [var 5mm, var 6mm])
3988}
3989"
3990 );
3991 assert_eq!(scene_delta.new_objects, vec![]);
3992 assert_eq!(scene_delta.new_graph.objects.len(), 4);
3993
3994 ctx.close().await;
3995 mock_ctx.close().await;
3996 }
3997
3998 #[tokio::test(flavor = "multi_thread")]
3999 async fn test_delete_multiple_points() {
4000 let initial_source = "\
4001@settings(experimentalFeatures = allow)
4002
4003sketch(on = XY) {
4004 sketch2::point(at = [var 1, var 2])
4005 point1 = sketch2::point(at = [var 3, var 4])
4006 sketch2::point(at = [var 5, var 6])
4007}
4008";
4009
4010 let program = Program::parse(initial_source).unwrap().0.unwrap();
4011
4012 let mut frontend = FrontendState::new();
4013
4014 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4015 let mock_ctx = ExecutorContext::new_mock(None).await;
4016 let version = Version(0);
4017
4018 frontend.hack_set_program(&ctx, program).await.unwrap();
4019 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4020 let sketch_id = sketch_object.id;
4021
4022 let sketch = expect_sketch(sketch_object);
4023
4024 let point1_id = *sketch.segments.first().unwrap();
4025 let point2_id = *sketch.segments.get(1).unwrap();
4026
4027 let (src_delta, scene_delta) = frontend
4028 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
4029 .await
4030 .unwrap();
4031 assert_eq!(
4032 src_delta.text.as_str(),
4033 "\
4034@settings(experimentalFeatures = allow)
4035
4036sketch(on = XY) {
4037 sketch2::point(at = [var 5mm, var 6mm])
4038}
4039"
4040 );
4041 assert_eq!(scene_delta.new_objects, vec![]);
4042 assert_eq!(scene_delta.new_graph.objects.len(), 3);
4043
4044 ctx.close().await;
4045 mock_ctx.close().await;
4046 }
4047
4048 #[tokio::test(flavor = "multi_thread")]
4049 async fn test_delete_coincident_constraint() {
4050 let initial_source = "\
4051@settings(experimentalFeatures = allow)
4052
4053sketch(on = XY) {
4054 point1 = sketch2::point(at = [var 1, var 2])
4055 point2 = sketch2::point(at = [var 3, var 4])
4056 sketch2::coincident([point1, point2])
4057 sketch2::point(at = [var 5, var 6])
4058}
4059";
4060
4061 let program = Program::parse(initial_source).unwrap().0.unwrap();
4062
4063 let mut frontend = FrontendState::new();
4064
4065 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4066 let mock_ctx = ExecutorContext::new_mock(None).await;
4067 let version = Version(0);
4068
4069 frontend.hack_set_program(&ctx, program).await.unwrap();
4070 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4071 let sketch_id = sketch_object.id;
4072 let sketch = expect_sketch(sketch_object);
4073
4074 let coincident_id = *sketch.constraints.first().unwrap();
4075
4076 let (src_delta, scene_delta) = frontend
4077 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
4078 .await
4079 .unwrap();
4080 assert_eq!(
4081 src_delta.text.as_str(),
4082 "\
4083@settings(experimentalFeatures = allow)
4084
4085sketch(on = XY) {
4086 point1 = sketch2::point(at = [var 1mm, var 2mm])
4087 point2 = sketch2::point(at = [var 3mm, var 4mm])
4088 sketch2::point(at = [var 5mm, var 6mm])
4089}
4090"
4091 );
4092 assert_eq!(scene_delta.new_objects, vec![]);
4093 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4094
4095 ctx.close().await;
4096 mock_ctx.close().await;
4097 }
4098
4099 #[tokio::test(flavor = "multi_thread")]
4100 async fn test_delete_line_cascades_to_coincident_constraint() {
4101 let initial_source = "\
4102@settings(experimentalFeatures = allow)
4103
4104sketch(on = XY) {
4105 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4106 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4107 sketch2::coincident([line1.end, line2.start])
4108}
4109";
4110
4111 let program = Program::parse(initial_source).unwrap().0.unwrap();
4112
4113 let mut frontend = FrontendState::new();
4114
4115 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4116 let mock_ctx = ExecutorContext::new_mock(None).await;
4117 let version = Version(0);
4118
4119 frontend.hack_set_program(&ctx, program).await.unwrap();
4120 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4121 let sketch_id = sketch_object.id;
4122 let sketch = expect_sketch(sketch_object);
4123 let line_id = *sketch.segments.get(5).unwrap();
4124
4125 let (src_delta, scene_delta) = frontend
4126 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
4127 .await
4128 .unwrap();
4129 assert_eq!(
4130 src_delta.text.as_str(),
4131 "\
4132@settings(experimentalFeatures = allow)
4133
4134sketch(on = XY) {
4135 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4136}
4137"
4138 );
4139 assert_eq!(
4140 scene_delta.new_graph.objects.len(),
4141 5,
4142 "{:#?}",
4143 scene_delta.new_graph.objects
4144 );
4145
4146 ctx.close().await;
4147 mock_ctx.close().await;
4148 }
4149
4150 #[tokio::test(flavor = "multi_thread")]
4151 async fn test_delete_line_cascades_to_distance_constraint() {
4152 let initial_source = "\
4153@settings(experimentalFeatures = allow)
4154
4155sketch(on = XY) {
4156 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4157 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4158 sketch2::distance([line1.end, line2.start]) == 10mm
4159}
4160";
4161
4162 let program = Program::parse(initial_source).unwrap().0.unwrap();
4163
4164 let mut frontend = FrontendState::new();
4165
4166 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4167 let mock_ctx = ExecutorContext::new_mock(None).await;
4168 let version = Version(0);
4169
4170 frontend.hack_set_program(&ctx, program).await.unwrap();
4171 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4172 let sketch_id = sketch_object.id;
4173 let sketch = expect_sketch(sketch_object);
4174 let line_id = *sketch.segments.get(5).unwrap();
4175
4176 let (src_delta, scene_delta) = frontend
4177 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
4178 .await
4179 .unwrap();
4180 assert_eq!(
4181 src_delta.text.as_str(),
4182 "\
4183@settings(experimentalFeatures = allow)
4184
4185sketch(on = XY) {
4186 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4187}
4188"
4189 );
4190 assert_eq!(
4191 scene_delta.new_graph.objects.len(),
4192 5,
4193 "{:#?}",
4194 scene_delta.new_graph.objects
4195 );
4196
4197 ctx.close().await;
4198 mock_ctx.close().await;
4199 }
4200
4201 #[tokio::test(flavor = "multi_thread")]
4202 async fn test_delete_line_line_coincident_constraint() {
4203 let initial_source = "\
4204@settings(experimentalFeatures = allow)
4205
4206sketch(on = XY) {
4207 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4208 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4209 sketch2::coincident([line1, line2])
4210}
4211";
4212
4213 let program = Program::parse(initial_source).unwrap().0.unwrap();
4214
4215 let mut frontend = FrontendState::new();
4216
4217 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4218 let mock_ctx = ExecutorContext::new_mock(None).await;
4219 let version = Version(0);
4220
4221 frontend.hack_set_program(&ctx, program).await.unwrap();
4222 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4223 let sketch_id = sketch_object.id;
4224 let sketch = expect_sketch(sketch_object);
4225
4226 let coincident_id = *sketch.constraints.first().unwrap();
4227
4228 let (src_delta, scene_delta) = frontend
4229 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
4230 .await
4231 .unwrap();
4232 assert_eq!(
4233 src_delta.text.as_str(),
4234 "\
4235@settings(experimentalFeatures = allow)
4236
4237sketch(on = XY) {
4238 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4239 line2 = sketch2::line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
4240}
4241"
4242 );
4243 assert_eq!(scene_delta.new_objects, vec![]);
4244 assert_eq!(scene_delta.new_graph.objects.len(), 8);
4245
4246 ctx.close().await;
4247 mock_ctx.close().await;
4248 }
4249
4250 #[tokio::test(flavor = "multi_thread")]
4251 async fn test_two_points_coincident() {
4252 let initial_source = "\
4253@settings(experimentalFeatures = allow)
4254
4255sketch(on = XY) {
4256 point1 = sketch2::point(at = [var 1, var 2])
4257 sketch2::point(at = [3, 4])
4258}
4259";
4260
4261 let program = Program::parse(initial_source).unwrap().0.unwrap();
4262
4263 let mut frontend = FrontendState::new();
4264
4265 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4266 let mock_ctx = ExecutorContext::new_mock(None).await;
4267 let version = Version(0);
4268
4269 frontend.hack_set_program(&ctx, program).await.unwrap();
4270 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4271 let sketch_id = sketch_object.id;
4272 let sketch = expect_sketch(sketch_object);
4273 let point0_id = *sketch.segments.first().unwrap();
4274 let point1_id = *sketch.segments.get(1).unwrap();
4275
4276 let constraint = Constraint::Coincident(Coincident {
4277 segments: vec![point0_id, point1_id],
4278 });
4279 let (src_delta, scene_delta) = frontend
4280 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4281 .await
4282 .unwrap();
4283 assert_eq!(
4284 src_delta.text.as_str(),
4285 "\
4286@settings(experimentalFeatures = allow)
4287
4288sketch(on = XY) {
4289 point1 = sketch2::point(at = [var 1, var 2])
4290 point2 = sketch2::point(at = [3, 4])
4291 sketch2::coincident([point1, point2])
4292}
4293"
4294 );
4295 assert_eq!(
4296 scene_delta.new_graph.objects.len(),
4297 5,
4298 "{:#?}",
4299 scene_delta.new_graph.objects
4300 );
4301
4302 ctx.close().await;
4303 mock_ctx.close().await;
4304 }
4305
4306 #[tokio::test(flavor = "multi_thread")]
4307 async fn test_coincident_of_line_end_points() {
4308 let initial_source = "\
4309@settings(experimentalFeatures = allow)
4310
4311sketch(on = XY) {
4312 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4313 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4314}
4315";
4316
4317 let program = Program::parse(initial_source).unwrap().0.unwrap();
4318
4319 let mut frontend = FrontendState::new();
4320
4321 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4322 let mock_ctx = ExecutorContext::new_mock(None).await;
4323 let version = Version(0);
4324
4325 frontend.hack_set_program(&ctx, program).await.unwrap();
4326 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4327 let sketch_id = sketch_object.id;
4328 let sketch = expect_sketch(sketch_object);
4329 let point0_id = *sketch.segments.get(1).unwrap();
4330 let point1_id = *sketch.segments.get(3).unwrap();
4331
4332 let constraint = Constraint::Coincident(Coincident {
4333 segments: vec![point0_id, point1_id],
4334 });
4335 let (src_delta, scene_delta) = frontend
4336 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4337 .await
4338 .unwrap();
4339 assert_eq!(
4340 src_delta.text.as_str(),
4341 "\
4342@settings(experimentalFeatures = allow)
4343
4344sketch(on = XY) {
4345 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4346 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4347 sketch2::coincident([line1.end, line2.start])
4348}
4349"
4350 );
4351 assert_eq!(
4352 scene_delta.new_graph.objects.len(),
4353 9,
4354 "{:#?}",
4355 scene_delta.new_graph.objects
4356 );
4357
4358 ctx.close().await;
4359 mock_ctx.close().await;
4360 }
4361
4362 #[tokio::test(flavor = "multi_thread")]
4363 async fn test_invalid_coincident_arc_and_line_preserves_state() {
4364 let program = Program::empty();
4372
4373 let mut frontend = FrontendState::new();
4374 frontend.program = program;
4375
4376 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4377 let mock_ctx = ExecutorContext::new_mock(None).await;
4378 let version = Version(0);
4379
4380 let sketch_args = SketchCtor {
4381 on: PlaneName::Xy.to_string(),
4382 };
4383 let (_src_delta, _scene_delta, sketch_id) = frontend
4384 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4385 .await
4386 .unwrap();
4387
4388 let arc_ctor = ArcCtor {
4390 start: Point2d {
4391 x: Expr::Var(Number {
4392 value: 0.0,
4393 units: NumericSuffix::Mm,
4394 }),
4395 y: Expr::Var(Number {
4396 value: 0.0,
4397 units: NumericSuffix::Mm,
4398 }),
4399 },
4400 end: Point2d {
4401 x: Expr::Var(Number {
4402 value: 10.0,
4403 units: NumericSuffix::Mm,
4404 }),
4405 y: Expr::Var(Number {
4406 value: 10.0,
4407 units: NumericSuffix::Mm,
4408 }),
4409 },
4410 center: Point2d {
4411 x: Expr::Var(Number {
4412 value: 10.0,
4413 units: NumericSuffix::Mm,
4414 }),
4415 y: Expr::Var(Number {
4416 value: 0.0,
4417 units: NumericSuffix::Mm,
4418 }),
4419 },
4420 construction: None,
4421 };
4422 let (_src_delta, scene_delta) = frontend
4423 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
4424 .await
4425 .unwrap();
4426 let arc_id = *scene_delta.new_objects.last().unwrap();
4428
4429 let line_ctor = LineCtor {
4431 start: Point2d {
4432 x: Expr::Var(Number {
4433 value: 20.0,
4434 units: NumericSuffix::Mm,
4435 }),
4436 y: Expr::Var(Number {
4437 value: 0.0,
4438 units: NumericSuffix::Mm,
4439 }),
4440 },
4441 end: Point2d {
4442 x: Expr::Var(Number {
4443 value: 30.0,
4444 units: NumericSuffix::Mm,
4445 }),
4446 y: Expr::Var(Number {
4447 value: 10.0,
4448 units: NumericSuffix::Mm,
4449 }),
4450 },
4451 construction: None,
4452 };
4453 let (_src_delta, scene_delta) = frontend
4454 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
4455 .await
4456 .unwrap();
4457 let line_id = *scene_delta.new_objects.last().unwrap();
4459
4460 let constraint = Constraint::Coincident(Coincident {
4463 segments: vec![arc_id, line_id],
4464 });
4465 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
4466
4467 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
4469
4470 let sketch_object_after =
4473 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
4474 let sketch_after = expect_sketch(sketch_object_after);
4475
4476 assert!(
4478 sketch_after.segments.contains(&arc_id),
4479 "Arc segment should still exist after failed constraint"
4480 );
4481 assert!(
4482 sketch_after.segments.contains(&line_id),
4483 "Line segment should still exist after failed constraint"
4484 );
4485
4486 let arc_obj = frontend
4488 .scene_graph
4489 .objects
4490 .get(arc_id.0)
4491 .expect("Arc object should still be accessible");
4492 let line_obj = frontend
4493 .scene_graph
4494 .objects
4495 .get(line_id.0)
4496 .expect("Line object should still be accessible");
4497
4498 match &arc_obj.kind {
4501 ObjectKind::Segment {
4502 segment: Segment::Arc(_),
4503 } => {}
4504 _ => panic!("Arc object should still be an arc segment"),
4505 }
4506 match &line_obj.kind {
4507 ObjectKind::Segment {
4508 segment: Segment::Line(_),
4509 } => {}
4510 _ => panic!("Line object should still be a line segment"),
4511 }
4512
4513 ctx.close().await;
4514 mock_ctx.close().await;
4515 }
4516
4517 #[tokio::test(flavor = "multi_thread")]
4518 async fn test_distance_two_points() {
4519 let initial_source = "\
4520@settings(experimentalFeatures = allow)
4521
4522sketch(on = XY) {
4523 sketch2::point(at = [var 1, var 2])
4524 sketch2::point(at = [var 3, var 4])
4525}
4526";
4527
4528 let program = Program::parse(initial_source).unwrap().0.unwrap();
4529
4530 let mut frontend = FrontendState::new();
4531
4532 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4533 let mock_ctx = ExecutorContext::new_mock(None).await;
4534 let version = Version(0);
4535
4536 frontend.hack_set_program(&ctx, program).await.unwrap();
4537 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4538 let sketch_id = sketch_object.id;
4539 let sketch = expect_sketch(sketch_object);
4540 let point0_id = *sketch.segments.first().unwrap();
4541 let point1_id = *sketch.segments.get(1).unwrap();
4542
4543 let constraint = Constraint::Distance(Distance {
4544 points: vec![point0_id, point1_id],
4545 distance: Number {
4546 value: 2.0,
4547 units: NumericSuffix::Mm,
4548 },
4549 });
4550 let (src_delta, scene_delta) = frontend
4551 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4552 .await
4553 .unwrap();
4554 assert_eq!(
4555 src_delta.text.as_str(),
4556 "\
4558@settings(experimentalFeatures = allow)
4559
4560sketch(on = XY) {
4561 point1 = sketch2::point(at = [var 1, var 2])
4562 point2 = sketch2::point(at = [var 3, var 4])
4563sketch2::distance([point1, point2]) == 2mm
4564}
4565"
4566 );
4567 assert_eq!(
4568 scene_delta.new_graph.objects.len(),
4569 5,
4570 "{:#?}",
4571 scene_delta.new_graph.objects
4572 );
4573
4574 ctx.close().await;
4575 mock_ctx.close().await;
4576 }
4577
4578 #[tokio::test(flavor = "multi_thread")]
4579 async fn test_horizontal_distance_two_points() {
4580 let initial_source = "\
4581@settings(experimentalFeatures = allow)
4582
4583sketch(on = XY) {
4584 sketch2::point(at = [var 1, var 2])
4585 sketch2::point(at = [var 3, var 4])
4586}
4587";
4588
4589 let program = Program::parse(initial_source).unwrap().0.unwrap();
4590
4591 let mut frontend = FrontendState::new();
4592
4593 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4594 let mock_ctx = ExecutorContext::new_mock(None).await;
4595 let version = Version(0);
4596
4597 frontend.hack_set_program(&ctx, program).await.unwrap();
4598 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4599 let sketch_id = sketch_object.id;
4600 let sketch = expect_sketch(sketch_object);
4601 let point0_id = *sketch.segments.first().unwrap();
4602 let point1_id = *sketch.segments.get(1).unwrap();
4603
4604 let constraint = Constraint::HorizontalDistance(Distance {
4605 points: vec![point0_id, point1_id],
4606 distance: Number {
4607 value: 2.0,
4608 units: NumericSuffix::Mm,
4609 },
4610 });
4611 let (src_delta, scene_delta) = frontend
4612 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4613 .await
4614 .unwrap();
4615 assert_eq!(
4616 src_delta.text.as_str(),
4617 "\
4619@settings(experimentalFeatures = allow)
4620
4621sketch(on = XY) {
4622 point1 = sketch2::point(at = [var 1, var 2])
4623 point2 = sketch2::point(at = [var 3, var 4])
4624sketch2::horizontalDistance([point1, point2]) == 2mm
4625}
4626"
4627 );
4628 assert_eq!(
4629 scene_delta.new_graph.objects.len(),
4630 5,
4631 "{:#?}",
4632 scene_delta.new_graph.objects
4633 );
4634
4635 ctx.close().await;
4636 mock_ctx.close().await;
4637 }
4638
4639 #[tokio::test(flavor = "multi_thread")]
4640 async fn test_vertical_distance_two_points() {
4641 let initial_source = "\
4642@settings(experimentalFeatures = allow)
4643
4644sketch(on = XY) {
4645 sketch2::point(at = [var 1, var 2])
4646 sketch2::point(at = [var 3, var 4])
4647}
4648";
4649
4650 let program = Program::parse(initial_source).unwrap().0.unwrap();
4651
4652 let mut frontend = FrontendState::new();
4653
4654 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4655 let mock_ctx = ExecutorContext::new_mock(None).await;
4656 let version = Version(0);
4657
4658 frontend.hack_set_program(&ctx, program).await.unwrap();
4659 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4660 let sketch_id = sketch_object.id;
4661 let sketch = expect_sketch(sketch_object);
4662 let point0_id = *sketch.segments.first().unwrap();
4663 let point1_id = *sketch.segments.get(1).unwrap();
4664
4665 let constraint = Constraint::VerticalDistance(Distance {
4666 points: vec![point0_id, point1_id],
4667 distance: Number {
4668 value: 2.0,
4669 units: NumericSuffix::Mm,
4670 },
4671 });
4672 let (src_delta, scene_delta) = frontend
4673 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4674 .await
4675 .unwrap();
4676 assert_eq!(
4677 src_delta.text.as_str(),
4678 "\
4680@settings(experimentalFeatures = allow)
4681
4682sketch(on = XY) {
4683 point1 = sketch2::point(at = [var 1, var 2])
4684 point2 = sketch2::point(at = [var 3, var 4])
4685sketch2::verticalDistance([point1, point2]) == 2mm
4686}
4687"
4688 );
4689 assert_eq!(
4690 scene_delta.new_graph.objects.len(),
4691 5,
4692 "{:#?}",
4693 scene_delta.new_graph.objects
4694 );
4695
4696 ctx.close().await;
4697 mock_ctx.close().await;
4698 }
4699
4700 #[tokio::test(flavor = "multi_thread")]
4701 async fn test_line_horizontal() {
4702 let initial_source = "\
4703@settings(experimentalFeatures = allow)
4704
4705sketch(on = XY) {
4706 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4707}
4708";
4709
4710 let program = Program::parse(initial_source).unwrap().0.unwrap();
4711
4712 let mut frontend = FrontendState::new();
4713
4714 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4715 let mock_ctx = ExecutorContext::new_mock(None).await;
4716 let version = Version(0);
4717
4718 frontend.hack_set_program(&ctx, program).await.unwrap();
4719 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4720 let sketch_id = sketch_object.id;
4721 let sketch = expect_sketch(sketch_object);
4722 let line1_id = *sketch.segments.get(2).unwrap();
4723
4724 let constraint = Constraint::Horizontal(Horizontal { line: line1_id });
4725 let (src_delta, scene_delta) = frontend
4726 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4727 .await
4728 .unwrap();
4729 assert_eq!(
4730 src_delta.text.as_str(),
4731 "\
4732@settings(experimentalFeatures = allow)
4733
4734sketch(on = XY) {
4735 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4736 sketch2::horizontal(line1)
4737}
4738"
4739 );
4740 assert_eq!(
4741 scene_delta.new_graph.objects.len(),
4742 6,
4743 "{:#?}",
4744 scene_delta.new_graph.objects
4745 );
4746
4747 ctx.close().await;
4748 mock_ctx.close().await;
4749 }
4750
4751 #[tokio::test(flavor = "multi_thread")]
4752 async fn test_line_vertical() {
4753 let initial_source = "\
4754@settings(experimentalFeatures = allow)
4755
4756sketch(on = XY) {
4757 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4758}
4759";
4760
4761 let program = Program::parse(initial_source).unwrap().0.unwrap();
4762
4763 let mut frontend = FrontendState::new();
4764
4765 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4766 let mock_ctx = ExecutorContext::new_mock(None).await;
4767 let version = Version(0);
4768
4769 frontend.hack_set_program(&ctx, program).await.unwrap();
4770 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4771 let sketch_id = sketch_object.id;
4772 let sketch = expect_sketch(sketch_object);
4773 let line1_id = *sketch.segments.get(2).unwrap();
4774
4775 let constraint = Constraint::Vertical(Vertical { line: line1_id });
4776 let (src_delta, scene_delta) = frontend
4777 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4778 .await
4779 .unwrap();
4780 assert_eq!(
4781 src_delta.text.as_str(),
4782 "\
4783@settings(experimentalFeatures = allow)
4784
4785sketch(on = XY) {
4786 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4787 sketch2::vertical(line1)
4788}
4789"
4790 );
4791 assert_eq!(
4792 scene_delta.new_graph.objects.len(),
4793 6,
4794 "{:#?}",
4795 scene_delta.new_graph.objects
4796 );
4797
4798 ctx.close().await;
4799 mock_ctx.close().await;
4800 }
4801
4802 #[tokio::test(flavor = "multi_thread")]
4803 async fn test_lines_equal_length() {
4804 let initial_source = "\
4805@settings(experimentalFeatures = allow)
4806
4807sketch(on = XY) {
4808 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4809 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4810}
4811";
4812
4813 let program = Program::parse(initial_source).unwrap().0.unwrap();
4814
4815 let mut frontend = FrontendState::new();
4816
4817 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4818 let mock_ctx = ExecutorContext::new_mock(None).await;
4819 let version = Version(0);
4820
4821 frontend.hack_set_program(&ctx, program).await.unwrap();
4822 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4823 let sketch_id = sketch_object.id;
4824 let sketch = expect_sketch(sketch_object);
4825 let line1_id = *sketch.segments.get(2).unwrap();
4826 let line2_id = *sketch.segments.get(5).unwrap();
4827
4828 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
4829 lines: vec![line1_id, line2_id],
4830 });
4831 let (src_delta, scene_delta) = frontend
4832 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4833 .await
4834 .unwrap();
4835 assert_eq!(
4836 src_delta.text.as_str(),
4837 "\
4838@settings(experimentalFeatures = allow)
4839
4840sketch(on = XY) {
4841 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4842 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4843 sketch2::equalLength([line1, line2])
4844}
4845"
4846 );
4847 assert_eq!(
4848 scene_delta.new_graph.objects.len(),
4849 9,
4850 "{:#?}",
4851 scene_delta.new_graph.objects
4852 );
4853
4854 ctx.close().await;
4855 mock_ctx.close().await;
4856 }
4857
4858 #[tokio::test(flavor = "multi_thread")]
4859 async fn test_lines_parallel() {
4860 let initial_source = "\
4861@settings(experimentalFeatures = allow)
4862
4863sketch(on = XY) {
4864 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4865 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4866}
4867";
4868
4869 let program = Program::parse(initial_source).unwrap().0.unwrap();
4870
4871 let mut frontend = FrontendState::new();
4872
4873 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4874 let mock_ctx = ExecutorContext::new_mock(None).await;
4875 let version = Version(0);
4876
4877 frontend.hack_set_program(&ctx, program).await.unwrap();
4878 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4879 let sketch_id = sketch_object.id;
4880 let sketch = expect_sketch(sketch_object);
4881 let line1_id = *sketch.segments.get(2).unwrap();
4882 let line2_id = *sketch.segments.get(5).unwrap();
4883
4884 let constraint = Constraint::Parallel(Parallel {
4885 lines: vec![line1_id, line2_id],
4886 });
4887 let (src_delta, scene_delta) = frontend
4888 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4889 .await
4890 .unwrap();
4891 assert_eq!(
4892 src_delta.text.as_str(),
4893 "\
4894@settings(experimentalFeatures = allow)
4895
4896sketch(on = XY) {
4897 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4898 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4899 sketch2::parallel([line1, line2])
4900}
4901"
4902 );
4903 assert_eq!(
4904 scene_delta.new_graph.objects.len(),
4905 9,
4906 "{:#?}",
4907 scene_delta.new_graph.objects
4908 );
4909
4910 ctx.close().await;
4911 mock_ctx.close().await;
4912 }
4913
4914 #[tokio::test(flavor = "multi_thread")]
4915 async fn test_lines_perpendicular() {
4916 let initial_source = "\
4917@settings(experimentalFeatures = allow)
4918
4919sketch(on = XY) {
4920 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4921 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4922}
4923";
4924
4925 let program = Program::parse(initial_source).unwrap().0.unwrap();
4926
4927 let mut frontend = FrontendState::new();
4928
4929 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4930 let mock_ctx = ExecutorContext::new_mock(None).await;
4931 let version = Version(0);
4932
4933 frontend.hack_set_program(&ctx, program).await.unwrap();
4934 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4935 let sketch_id = sketch_object.id;
4936 let sketch = expect_sketch(sketch_object);
4937 let line1_id = *sketch.segments.get(2).unwrap();
4938 let line2_id = *sketch.segments.get(5).unwrap();
4939
4940 let constraint = Constraint::Perpendicular(Perpendicular {
4941 lines: vec![line1_id, line2_id],
4942 });
4943 let (src_delta, scene_delta) = frontend
4944 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4945 .await
4946 .unwrap();
4947 assert_eq!(
4948 src_delta.text.as_str(),
4949 "\
4950@settings(experimentalFeatures = allow)
4951
4952sketch(on = XY) {
4953 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4954 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4955 sketch2::perpendicular([line1, line2])
4956}
4957"
4958 );
4959 assert_eq!(
4960 scene_delta.new_graph.objects.len(),
4961 9,
4962 "{:#?}",
4963 scene_delta.new_graph.objects
4964 );
4965
4966 ctx.close().await;
4967 mock_ctx.close().await;
4968 }
4969
4970 #[tokio::test(flavor = "multi_thread")]
4971 async fn test_sketch_on_face_simple() {
4972 let initial_source = "\
4973@settings(experimentalFeatures = allow)
4974
4975len = 2mm
4976cube = startSketchOn(XY)
4977 |> startProfile(at = [0, 0])
4978 |> line(end = [len, 0], tag = $side)
4979 |> line(end = [0, len])
4980 |> line(end = [-len, 0])
4981 |> line(end = [0, -len])
4982 |> close()
4983 |> extrude(length = len)
4984
4985face = faceOf(cube, face = side)
4986";
4987
4988 let program = Program::parse(initial_source).unwrap().0.unwrap();
4989
4990 let mut frontend = FrontendState::new();
4991
4992 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4993 let mock_ctx = ExecutorContext::new_mock(None).await;
4994 let version = Version(0);
4995
4996 frontend.hack_set_program(&ctx, program).await.unwrap();
4997 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
4998 let face_id = face_object.id;
4999
5000 let sketch_args = SketchCtor { on: "face".to_owned() };
5001 let (_src_delta, scene_delta, sketch_id) = frontend
5002 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5003 .await
5004 .unwrap();
5005 assert_eq!(sketch_id, ObjectId(2));
5006 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
5007 let sketch_object = &scene_delta.new_graph.objects[2];
5008 assert_eq!(sketch_object.id, ObjectId(2));
5009 assert_eq!(
5010 sketch_object.kind,
5011 ObjectKind::Sketch(Sketch {
5012 args: SketchCtor { on: "face".to_owned() },
5013 plane: face_id,
5014 segments: vec![],
5015 constraints: vec![],
5016 })
5017 );
5018 assert_eq!(scene_delta.new_graph.objects.len(), 3);
5019
5020 ctx.close().await;
5021 mock_ctx.close().await;
5022 }
5023
5024 #[tokio::test(flavor = "multi_thread")]
5025 async fn test_sketch_on_plane_incremental() {
5026 let initial_source = "\
5027@settings(experimentalFeatures = allow)
5028
5029len = 2mm
5030cube = startSketchOn(XY)
5031 |> startProfile(at = [0, 0])
5032 |> line(end = [len, 0], tag = $side)
5033 |> line(end = [0, len])
5034 |> line(end = [-len, 0])
5035 |> line(end = [0, -len])
5036 |> close()
5037 |> extrude(length = len)
5038
5039plane = planeOf(cube, face = side)
5040";
5041
5042 let program = Program::parse(initial_source).unwrap().0.unwrap();
5043
5044 let mut frontend = FrontendState::new();
5045
5046 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5047 let mock_ctx = ExecutorContext::new_mock(None).await;
5048 let version = Version(0);
5049
5050 frontend.hack_set_program(&ctx, program).await.unwrap();
5051 let plane_object = frontend
5053 .scene_graph
5054 .objects
5055 .iter()
5056 .rev()
5057 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
5058 .unwrap();
5059 let plane_id = plane_object.id;
5060
5061 let sketch_args = SketchCtor { on: "plane".to_owned() };
5062 let (src_delta, scene_delta, sketch_id) = frontend
5063 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5064 .await
5065 .unwrap();
5066 assert_eq!(
5067 src_delta.text.as_str(),
5068 "\
5069@settings(experimentalFeatures = allow)
5070
5071len = 2mm
5072cube = startSketchOn(XY)
5073 |> startProfile(at = [0, 0])
5074 |> line(end = [len, 0], tag = $side)
5075 |> line(end = [0, len])
5076 |> line(end = [-len, 0])
5077 |> line(end = [0, -len])
5078 |> close()
5079 |> extrude(length = len)
5080
5081plane = planeOf(cube, face = side)
5082sketch(on = plane) {
5083}
5084"
5085 );
5086 assert_eq!(sketch_id, ObjectId(2));
5087 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
5088 let sketch_object = &scene_delta.new_graph.objects[2];
5089 assert_eq!(sketch_object.id, ObjectId(2));
5090 assert_eq!(
5091 sketch_object.kind,
5092 ObjectKind::Sketch(Sketch {
5093 args: SketchCtor { on: "plane".to_owned() },
5094 plane: plane_id,
5095 segments: vec![],
5096 constraints: vec![],
5097 })
5098 );
5099 assert_eq!(scene_delta.new_graph.objects.len(), 3);
5100
5101 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
5102 assert_eq!(plane_object.id, plane_id);
5103 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
5104
5105 ctx.close().await;
5106 mock_ctx.close().await;
5107 }
5108
5109 #[tokio::test(flavor = "multi_thread")]
5110 async fn test_multiple_sketch_blocks() {
5111 let initial_source = "\
5112@settings(experimentalFeatures = allow)
5113
5114// Cube that requires the engine.
5115width = 2
5116sketch001 = startSketchOn(XY)
5117profile001 = startProfile(sketch001, at = [0, 0])
5118 |> yLine(length = width, tag = $seg1)
5119 |> xLine(length = width)
5120 |> yLine(length = -width)
5121 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5122 |> close()
5123extrude001 = extrude(profile001, length = width)
5124
5125// Get a value that requires the engine.
5126x = segLen(seg1)
5127
5128// Triangle with side length 2*x.
5129sketch(on = XY) {
5130 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5131 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
5132 sketch2::coincident([line1.end, line2.start])
5133 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
5134 sketch2::coincident([line2.end, line3.start])
5135 sketch2::coincident([line3.end, line1.start])
5136 sketch2::equalLength([line3, line1])
5137 sketch2::equalLength([line1, line2])
5138sketch2::distance([line1.start, line1.end]) == 2*x
5139}
5140
5141// Line segment with length x.
5142sketch2 = sketch(on = XY) {
5143 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5144sketch2::distance([line1.start, line1.end]) == x
5145}
5146";
5147
5148 let program = Program::parse(initial_source).unwrap().0.unwrap();
5149
5150 let mut frontend = FrontendState::new();
5151
5152 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5153 let mock_ctx = ExecutorContext::new_mock(None).await;
5154 let version = Version(0);
5155 let project_id = ProjectId(0);
5156 let file_id = FileId(0);
5157
5158 frontend.hack_set_program(&ctx, program).await.unwrap();
5159 let sketch_objects = frontend
5160 .scene_graph
5161 .objects
5162 .iter()
5163 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
5164 .collect::<Vec<_>>();
5165 let sketch1_id = sketch_objects.first().unwrap().id;
5166 let sketch2_id = sketch_objects.get(1).unwrap().id;
5167 let point1_id = ObjectId(sketch1_id.0 + 1);
5169 let point2_id = ObjectId(sketch2_id.0 + 1);
5171
5172 let scene_delta = frontend
5181 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
5182 .await
5183 .unwrap();
5184 assert_eq!(
5185 scene_delta.new_graph.objects.len(),
5186 18,
5187 "{:#?}",
5188 scene_delta.new_graph.objects
5189 );
5190
5191 let point_ctor = PointCtor {
5193 position: Point2d {
5194 x: Expr::Var(Number {
5195 value: 1.0,
5196 units: NumericSuffix::Mm,
5197 }),
5198 y: Expr::Var(Number {
5199 value: 2.0,
5200 units: NumericSuffix::Mm,
5201 }),
5202 },
5203 };
5204 let segments = vec![ExistingSegmentCtor {
5205 id: point1_id,
5206 ctor: SegmentCtor::Point(point_ctor),
5207 }];
5208 let (src_delta, _) = frontend
5209 .edit_segments(&mock_ctx, version, sketch1_id, segments)
5210 .await
5211 .unwrap();
5212 assert_eq!(
5214 src_delta.text.as_str(),
5215 "\
5216@settings(experimentalFeatures = allow)
5217
5218// Cube that requires the engine.
5219width = 2
5220sketch001 = startSketchOn(XY)
5221profile001 = startProfile(sketch001, at = [0, 0])
5222 |> yLine(length = width, tag = $seg1)
5223 |> xLine(length = width)
5224 |> yLine(length = -width)
5225 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5226 |> close()
5227extrude001 = extrude(profile001, length = width)
5228
5229// Get a value that requires the engine.
5230x = segLen(seg1)
5231
5232// Triangle with side length 2*x.
5233sketch(on = XY) {
5234 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 2.317mm, var -1.777mm])
5235 line2 = sketch2::line(start = [var 2.317mm, var -1.777mm], end = [var -1.613mm, var -1.029mm])
5236 sketch2::coincident([line1.end, line2.start])
5237 line3 = sketch2::line(start = [var -1.613mm, var -1.029mm], end = [var 1mm, var 2mm])
5238 sketch2::coincident([line2.end, line3.start])
5239 sketch2::coincident([line3.end, line1.start])
5240 sketch2::equalLength([line3, line1])
5241 sketch2::equalLength([line1, line2])
5242sketch2::distance([line1.start, line1.end]) == 2 * x
5243}
5244
5245// Line segment with length x.
5246sketch2 = sketch(on = XY) {
5247 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5248sketch2::distance([line1.start, line1.end]) == x
5249}
5250"
5251 );
5252
5253 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
5255 assert_eq!(
5257 src_delta.text.as_str(),
5258 "\
5259@settings(experimentalFeatures = allow)
5260
5261// Cube that requires the engine.
5262width = 2
5263sketch001 = startSketchOn(XY)
5264profile001 = startProfile(sketch001, at = [0, 0])
5265 |> yLine(length = width, tag = $seg1)
5266 |> xLine(length = width)
5267 |> yLine(length = -width)
5268 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5269 |> close()
5270extrude001 = extrude(profile001, length = width)
5271
5272// Get a value that requires the engine.
5273x = segLen(seg1)
5274
5275// Triangle with side length 2*x.
5276sketch(on = XY) {
5277 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.283mm, var -0.781mm])
5278 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
5279 sketch2::coincident([line1.end, line2.start])
5280 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
5281 sketch2::coincident([line2.end, line3.start])
5282 sketch2::coincident([line3.end, line1.start])
5283 sketch2::equalLength([line3, line1])
5284 sketch2::equalLength([line1, line2])
5285sketch2::distance([line1.start, line1.end]) == 2 * x
5286}
5287
5288// Line segment with length x.
5289sketch2 = sketch(on = XY) {
5290 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5291sketch2::distance([line1.start, line1.end]) == x
5292}
5293"
5294 );
5295 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
5303 assert_eq!(scene.objects.len(), 24, "{:#?}", scene.objects);
5304
5305 let scene_delta = frontend
5313 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
5314 .await
5315 .unwrap();
5316 assert_eq!(
5317 scene_delta.new_graph.objects.len(),
5318 24,
5319 "{:#?}",
5320 scene_delta.new_graph.objects
5321 );
5322
5323 let point_ctor = PointCtor {
5325 position: Point2d {
5326 x: Expr::Var(Number {
5327 value: 3.0,
5328 units: NumericSuffix::Mm,
5329 }),
5330 y: Expr::Var(Number {
5331 value: 4.0,
5332 units: NumericSuffix::Mm,
5333 }),
5334 },
5335 };
5336 let segments = vec![ExistingSegmentCtor {
5337 id: point2_id,
5338 ctor: SegmentCtor::Point(point_ctor),
5339 }];
5340 let (src_delta, _) = frontend
5341 .edit_segments(&mock_ctx, version, sketch2_id, segments)
5342 .await
5343 .unwrap();
5344 assert_eq!(
5346 src_delta.text.as_str(),
5347 "\
5348@settings(experimentalFeatures = allow)
5349
5350// Cube that requires the engine.
5351width = 2
5352sketch001 = startSketchOn(XY)
5353profile001 = startProfile(sketch001, at = [0, 0])
5354 |> yLine(length = width, tag = $seg1)
5355 |> xLine(length = width)
5356 |> yLine(length = -width)
5357 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5358 |> close()
5359extrude001 = extrude(profile001, length = width)
5360
5361// Get a value that requires the engine.
5362x = segLen(seg1)
5363
5364// Triangle with side length 2*x.
5365sketch(on = XY) {
5366 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.283mm, var -0.781mm])
5367 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
5368 sketch2::coincident([line1.end, line2.start])
5369 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
5370 sketch2::coincident([line2.end, line3.start])
5371 sketch2::coincident([line3.end, line1.start])
5372 sketch2::equalLength([line3, line1])
5373 sketch2::equalLength([line1, line2])
5374sketch2::distance([line1.start, line1.end]) == 2 * x
5375}
5376
5377// Line segment with length x.
5378sketch2 = sketch(on = XY) {
5379 line1 = sketch2::line(start = [var 3mm, var 4mm], end = [var 2.324mm, var 2.118mm])
5380sketch2::distance([line1.start, line1.end]) == x
5381}
5382"
5383 );
5384
5385 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
5387 assert_eq!(
5389 src_delta.text.as_str(),
5390 "\
5391@settings(experimentalFeatures = allow)
5392
5393// Cube that requires the engine.
5394width = 2
5395sketch001 = startSketchOn(XY)
5396profile001 = startProfile(sketch001, at = [0, 0])
5397 |> yLine(length = width, tag = $seg1)
5398 |> xLine(length = width)
5399 |> yLine(length = -width)
5400 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5401 |> close()
5402extrude001 = extrude(profile001, length = width)
5403
5404// Get a value that requires the engine.
5405x = segLen(seg1)
5406
5407// Triangle with side length 2*x.
5408sketch(on = XY) {
5409 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.283mm, var -0.781mm])
5410 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
5411 sketch2::coincident([line1.end, line2.start])
5412 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
5413 sketch2::coincident([line2.end, line3.start])
5414 sketch2::coincident([line3.end, line1.start])
5415 sketch2::equalLength([line3, line1])
5416 sketch2::equalLength([line1, line2])
5417sketch2::distance([line1.start, line1.end]) == 2 * x
5418}
5419
5420// Line segment with length x.
5421sketch2 = sketch(on = XY) {
5422 line1 = sketch2::line(start = [var 3mm, var 4mm], end = [var 1.283mm, var -0.781mm])
5423sketch2::distance([line1.start, line1.end]) == x
5424}
5425"
5426 );
5427
5428 ctx.close().await;
5429 mock_ctx.close().await;
5430 }
5431}