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