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