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