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, KclError, 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 matches!(err.error, KclError::EngineHangup { .. }) {
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: Default::default(),
3501 abs_path: false,
3502 digest: None,
3503 }
3504}
3505
3506pub(crate) fn create_coincident_ast(expr1: ast::Expr, expr2: ast::Expr) -> ast::Expr {
3510 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3512 elements: vec![expr1, expr2],
3513 digest: None,
3514 non_code_meta: Default::default(),
3515 })));
3516
3517 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3519 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
3520 unlabeled: Some(array_expr),
3521 arguments: Default::default(),
3522 digest: None,
3523 non_code_meta: Default::default(),
3524 })))
3525}
3526
3527pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
3529 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3530 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
3531 unlabeled: None,
3532 arguments: vec![
3533 ast::LabeledArg {
3534 label: Some(ast::Identifier::new(LINE_START_PARAM)),
3535 arg: start_ast,
3536 },
3537 ast::LabeledArg {
3538 label: Some(ast::Identifier::new(LINE_END_PARAM)),
3539 arg: end_ast,
3540 },
3541 ],
3542 digest: None,
3543 non_code_meta: Default::default(),
3544 })))
3545}
3546
3547pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
3549 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3550 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
3551 unlabeled: Some(line_expr),
3552 arguments: Default::default(),
3553 digest: None,
3554 non_code_meta: Default::default(),
3555 })))
3556}
3557
3558pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
3560 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3561 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
3562 unlabeled: Some(line_expr),
3563 arguments: Default::default(),
3564 digest: None,
3565 non_code_meta: Default::default(),
3566 })))
3567}
3568
3569pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
3571 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
3572 object: object_expr,
3573 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
3574 name: ast::Node::no_src(ast::Identifier {
3575 name: property.to_string(),
3576 digest: None,
3577 }),
3578 path: Vec::new(),
3579 abs_path: false,
3580 digest: None,
3581 }))),
3582 computed: false,
3583 digest: None,
3584 })))
3585}
3586
3587pub(crate) fn create_equal_length_ast(line1_expr: ast::Expr, line2_expr: ast::Expr) -> ast::Expr {
3589 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3591 elements: vec![line1_expr, line2_expr],
3592 digest: None,
3593 non_code_meta: Default::default(),
3594 })));
3595
3596 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3598 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
3599 unlabeled: Some(array_expr),
3600 arguments: Default::default(),
3601 digest: None,
3602 non_code_meta: Default::default(),
3603 })))
3604}
3605
3606#[cfg(test)]
3607mod tests {
3608 use super::*;
3609 use crate::{
3610 engine::PlaneName,
3611 front::{Distance, Object, Plane, Sketch},
3612 frontend::sketch::Vertical,
3613 pretty::NumericSuffix,
3614 };
3615
3616 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
3617 for object in &scene_graph.objects {
3618 if let ObjectKind::Sketch(_) = &object.kind {
3619 return Some(object);
3620 }
3621 }
3622 None
3623 }
3624
3625 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
3626 for object in &scene_graph.objects {
3627 if let ObjectKind::Face(_) = &object.kind {
3628 return Some(object);
3629 }
3630 }
3631 None
3632 }
3633
3634 #[track_caller]
3635 fn expect_sketch(object: &Object) -> &Sketch {
3636 if let ObjectKind::Sketch(sketch) = &object.kind {
3637 sketch
3638 } else {
3639 panic!("Object is not a sketch: {:?}", object);
3640 }
3641 }
3642
3643 #[tokio::test(flavor = "multi_thread")]
3644 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
3645 let source = "\
3646@settings(experimentalFeatures = allow)
3647
3648sketch(on = XY) {
3649 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
3650}
3651
3652bad = missing_name
3653";
3654 let program = Program::parse(source).unwrap().0.unwrap();
3655
3656 let mut frontend = FrontendState::new();
3657
3658 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3659 let mock_ctx = ExecutorContext::new_mock(None).await;
3660 let version = Version(0);
3661 let project_id = ProjectId(0);
3662 let file_id = FileId(0);
3663
3664 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
3665 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
3666 };
3667
3668 let sketch_id = frontend
3669 .scene_graph
3670 .objects
3671 .iter()
3672 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
3673 .expect("Expected sketch object from errored hack_set_program");
3674
3675 frontend
3676 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
3677 .await
3678 .unwrap();
3679
3680 ctx.close().await;
3681 mock_ctx.close().await;
3682 }
3683
3684 #[tokio::test(flavor = "multi_thread")]
3685 async fn test_new_sketch_add_point_edit_point() {
3686 let program = Program::empty();
3687
3688 let mut frontend = FrontendState::new();
3689 frontend.program = program;
3690
3691 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3692 let mock_ctx = ExecutorContext::new_mock(None).await;
3693 let version = Version(0);
3694
3695 let sketch_args = SketchCtor {
3696 on: PlaneName::Xy.to_string(),
3697 };
3698 let (_src_delta, scene_delta, sketch_id) = frontend
3699 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3700 .await
3701 .unwrap();
3702 assert_eq!(sketch_id, ObjectId(1));
3703 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3704 let sketch_object = &scene_delta.new_graph.objects[1];
3705 assert_eq!(sketch_object.id, ObjectId(1));
3706 assert_eq!(
3707 sketch_object.kind,
3708 ObjectKind::Sketch(Sketch {
3709 args: SketchCtor {
3710 on: PlaneName::Xy.to_string()
3711 },
3712 plane: ObjectId(0),
3713 segments: vec![],
3714 constraints: vec![],
3715 })
3716 );
3717 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3718
3719 let point_ctor = PointCtor {
3720 position: Point2d {
3721 x: Expr::Number(Number {
3722 value: 1.0,
3723 units: NumericSuffix::Inch,
3724 }),
3725 y: Expr::Number(Number {
3726 value: 2.0,
3727 units: NumericSuffix::Inch,
3728 }),
3729 },
3730 };
3731 let segment = SegmentCtor::Point(point_ctor);
3732 let (src_delta, scene_delta) = frontend
3733 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3734 .await
3735 .unwrap();
3736 assert_eq!(
3737 src_delta.text.as_str(),
3738 "@settings(experimentalFeatures = allow)
3739
3740sketch(on = XY) {
3741 point(at = [1in, 2in])
3742}
3743"
3744 );
3745 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
3746 assert_eq!(scene_delta.new_graph.objects.len(), 3);
3747 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3748 assert_eq!(scene_object.id.0, i);
3749 }
3750
3751 let point_id = *scene_delta.new_objects.last().unwrap();
3752
3753 let point_ctor = PointCtor {
3754 position: Point2d {
3755 x: Expr::Number(Number {
3756 value: 3.0,
3757 units: NumericSuffix::Inch,
3758 }),
3759 y: Expr::Number(Number {
3760 value: 4.0,
3761 units: NumericSuffix::Inch,
3762 }),
3763 },
3764 };
3765 let segments = vec![ExistingSegmentCtor {
3766 id: point_id,
3767 ctor: SegmentCtor::Point(point_ctor),
3768 }];
3769 let (src_delta, scene_delta) = frontend
3770 .edit_segments(&mock_ctx, version, sketch_id, segments)
3771 .await
3772 .unwrap();
3773 assert_eq!(
3774 src_delta.text.as_str(),
3775 "@settings(experimentalFeatures = allow)
3776
3777sketch(on = XY) {
3778 point(at = [3in, 4in])
3779}
3780"
3781 );
3782 assert_eq!(scene_delta.new_objects, vec![]);
3783 assert_eq!(scene_delta.new_graph.objects.len(), 3);
3784
3785 ctx.close().await;
3786 mock_ctx.close().await;
3787 }
3788
3789 #[tokio::test(flavor = "multi_thread")]
3790 async fn test_new_sketch_add_line_edit_line() {
3791 let program = Program::empty();
3792
3793 let mut frontend = FrontendState::new();
3794 frontend.program = program;
3795
3796 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3797 let mock_ctx = ExecutorContext::new_mock(None).await;
3798 let version = Version(0);
3799
3800 let sketch_args = SketchCtor {
3801 on: PlaneName::Xy.to_string(),
3802 };
3803 let (_src_delta, scene_delta, sketch_id) = frontend
3804 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3805 .await
3806 .unwrap();
3807 assert_eq!(sketch_id, ObjectId(1));
3808 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3809 let sketch_object = &scene_delta.new_graph.objects[1];
3810 assert_eq!(sketch_object.id, ObjectId(1));
3811 assert_eq!(
3812 sketch_object.kind,
3813 ObjectKind::Sketch(Sketch {
3814 args: SketchCtor {
3815 on: PlaneName::Xy.to_string()
3816 },
3817 plane: ObjectId(0),
3818 segments: vec![],
3819 constraints: vec![],
3820 })
3821 );
3822 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3823
3824 let line_ctor = LineCtor {
3825 start: Point2d {
3826 x: Expr::Number(Number {
3827 value: 0.0,
3828 units: NumericSuffix::Mm,
3829 }),
3830 y: Expr::Number(Number {
3831 value: 0.0,
3832 units: NumericSuffix::Mm,
3833 }),
3834 },
3835 end: Point2d {
3836 x: Expr::Number(Number {
3837 value: 10.0,
3838 units: NumericSuffix::Mm,
3839 }),
3840 y: Expr::Number(Number {
3841 value: 10.0,
3842 units: NumericSuffix::Mm,
3843 }),
3844 },
3845 construction: None,
3846 };
3847 let segment = SegmentCtor::Line(line_ctor);
3848 let (src_delta, scene_delta) = frontend
3849 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3850 .await
3851 .unwrap();
3852 assert_eq!(
3853 src_delta.text.as_str(),
3854 "@settings(experimentalFeatures = allow)
3855
3856sketch(on = XY) {
3857 line(start = [0mm, 0mm], end = [10mm, 10mm])
3858}
3859"
3860 );
3861 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
3862 assert_eq!(scene_delta.new_graph.objects.len(), 5);
3863 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3864 assert_eq!(scene_object.id.0, i);
3865 }
3866
3867 let line = *scene_delta.new_objects.last().unwrap();
3869
3870 let line_ctor = LineCtor {
3871 start: Point2d {
3872 x: Expr::Number(Number {
3873 value: 1.0,
3874 units: NumericSuffix::Mm,
3875 }),
3876 y: Expr::Number(Number {
3877 value: 2.0,
3878 units: NumericSuffix::Mm,
3879 }),
3880 },
3881 end: Point2d {
3882 x: Expr::Number(Number {
3883 value: 13.0,
3884 units: NumericSuffix::Mm,
3885 }),
3886 y: Expr::Number(Number {
3887 value: 14.0,
3888 units: NumericSuffix::Mm,
3889 }),
3890 },
3891 construction: None,
3892 };
3893 let segments = vec![ExistingSegmentCtor {
3894 id: line,
3895 ctor: SegmentCtor::Line(line_ctor),
3896 }];
3897 let (src_delta, scene_delta) = frontend
3898 .edit_segments(&mock_ctx, version, sketch_id, segments)
3899 .await
3900 .unwrap();
3901 assert_eq!(
3902 src_delta.text.as_str(),
3903 "@settings(experimentalFeatures = allow)
3904
3905sketch(on = XY) {
3906 line(start = [1mm, 2mm], end = [13mm, 14mm])
3907}
3908"
3909 );
3910 assert_eq!(scene_delta.new_objects, vec![]);
3911 assert_eq!(scene_delta.new_graph.objects.len(), 5);
3912
3913 ctx.close().await;
3914 mock_ctx.close().await;
3915 }
3916
3917 #[tokio::test(flavor = "multi_thread")]
3918 async fn test_new_sketch_add_arc_edit_arc() {
3919 let program = Program::empty();
3920
3921 let mut frontend = FrontendState::new();
3922 frontend.program = program;
3923
3924 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3925 let mock_ctx = ExecutorContext::new_mock(None).await;
3926 let version = Version(0);
3927
3928 let sketch_args = SketchCtor {
3929 on: PlaneName::Xy.to_string(),
3930 };
3931 let (_src_delta, scene_delta, sketch_id) = frontend
3932 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3933 .await
3934 .unwrap();
3935 assert_eq!(sketch_id, ObjectId(1));
3936 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3937 let sketch_object = &scene_delta.new_graph.objects[1];
3938 assert_eq!(sketch_object.id, ObjectId(1));
3939 assert_eq!(
3940 sketch_object.kind,
3941 ObjectKind::Sketch(Sketch {
3942 args: SketchCtor {
3943 on: PlaneName::Xy.to_string(),
3944 },
3945 plane: ObjectId(0),
3946 segments: vec![],
3947 constraints: vec![],
3948 })
3949 );
3950 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3951
3952 let arc_ctor = ArcCtor {
3953 start: Point2d {
3954 x: Expr::Var(Number {
3955 value: 0.0,
3956 units: NumericSuffix::Mm,
3957 }),
3958 y: Expr::Var(Number {
3959 value: 0.0,
3960 units: NumericSuffix::Mm,
3961 }),
3962 },
3963 end: Point2d {
3964 x: Expr::Var(Number {
3965 value: 10.0,
3966 units: NumericSuffix::Mm,
3967 }),
3968 y: Expr::Var(Number {
3969 value: 10.0,
3970 units: NumericSuffix::Mm,
3971 }),
3972 },
3973 center: Point2d {
3974 x: Expr::Var(Number {
3975 value: 10.0,
3976 units: NumericSuffix::Mm,
3977 }),
3978 y: Expr::Var(Number {
3979 value: 0.0,
3980 units: NumericSuffix::Mm,
3981 }),
3982 },
3983 construction: None,
3984 };
3985 let segment = SegmentCtor::Arc(arc_ctor);
3986 let (src_delta, scene_delta) = frontend
3987 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3988 .await
3989 .unwrap();
3990 assert_eq!(
3991 src_delta.text.as_str(),
3992 "@settings(experimentalFeatures = allow)
3993
3994sketch(on = XY) {
3995 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
3996}
3997"
3998 );
3999 assert_eq!(
4000 scene_delta.new_objects,
4001 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
4002 );
4003 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
4004 assert_eq!(scene_object.id.0, i);
4005 }
4006 assert_eq!(scene_delta.new_graph.objects.len(), 6);
4007
4008 let arc = *scene_delta.new_objects.last().unwrap();
4010
4011 let arc_ctor = ArcCtor {
4012 start: Point2d {
4013 x: Expr::Var(Number {
4014 value: 1.0,
4015 units: NumericSuffix::Mm,
4016 }),
4017 y: Expr::Var(Number {
4018 value: 2.0,
4019 units: NumericSuffix::Mm,
4020 }),
4021 },
4022 end: Point2d {
4023 x: Expr::Var(Number {
4024 value: 13.0,
4025 units: NumericSuffix::Mm,
4026 }),
4027 y: Expr::Var(Number {
4028 value: 14.0,
4029 units: NumericSuffix::Mm,
4030 }),
4031 },
4032 center: Point2d {
4033 x: Expr::Var(Number {
4034 value: 13.0,
4035 units: NumericSuffix::Mm,
4036 }),
4037 y: Expr::Var(Number {
4038 value: 2.0,
4039 units: NumericSuffix::Mm,
4040 }),
4041 },
4042 construction: None,
4043 };
4044 let segments = vec![ExistingSegmentCtor {
4045 id: arc,
4046 ctor: SegmentCtor::Arc(arc_ctor),
4047 }];
4048 let (src_delta, scene_delta) = frontend
4049 .edit_segments(&mock_ctx, version, sketch_id, segments)
4050 .await
4051 .unwrap();
4052 assert_eq!(
4053 src_delta.text.as_str(),
4054 "@settings(experimentalFeatures = allow)
4055
4056sketch(on = XY) {
4057 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
4058}
4059"
4060 );
4061 assert_eq!(scene_delta.new_objects, vec![]);
4062 assert_eq!(scene_delta.new_graph.objects.len(), 6);
4063
4064 ctx.close().await;
4065 mock_ctx.close().await;
4066 }
4067
4068 #[tokio::test(flavor = "multi_thread")]
4069 async fn test_add_line_when_sketch_block_uses_variable() {
4070 let initial_source = "@settings(experimentalFeatures = allow)
4071
4072s = sketch(on = XY) {}
4073";
4074
4075 let program = Program::parse(initial_source).unwrap().0.unwrap();
4076
4077 let mut frontend = FrontendState::new();
4078
4079 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4080 let mock_ctx = ExecutorContext::new_mock(None).await;
4081 let version = Version(0);
4082
4083 frontend.hack_set_program(&ctx, program).await.unwrap();
4084 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4085 let sketch_id = sketch_object.id;
4086
4087 let line_ctor = LineCtor {
4088 start: Point2d {
4089 x: Expr::Number(Number {
4090 value: 0.0,
4091 units: NumericSuffix::Mm,
4092 }),
4093 y: Expr::Number(Number {
4094 value: 0.0,
4095 units: NumericSuffix::Mm,
4096 }),
4097 },
4098 end: Point2d {
4099 x: Expr::Number(Number {
4100 value: 10.0,
4101 units: NumericSuffix::Mm,
4102 }),
4103 y: Expr::Number(Number {
4104 value: 10.0,
4105 units: NumericSuffix::Mm,
4106 }),
4107 },
4108 construction: None,
4109 };
4110 let segment = SegmentCtor::Line(line_ctor);
4111 let (src_delta, scene_delta) = frontend
4112 .add_segment(&mock_ctx, version, sketch_id, segment, None)
4113 .await
4114 .unwrap();
4115 assert_eq!(
4116 src_delta.text.as_str(),
4117 "@settings(experimentalFeatures = allow)
4118
4119s = sketch(on = XY) {
4120 line(start = [0mm, 0mm], end = [10mm, 10mm])
4121}
4122"
4123 );
4124 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
4125 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4126
4127 ctx.close().await;
4128 mock_ctx.close().await;
4129 }
4130
4131 #[tokio::test(flavor = "multi_thread")]
4132 async fn test_new_sketch_add_line_delete_sketch() {
4133 let program = Program::empty();
4134
4135 let mut frontend = FrontendState::new();
4136 frontend.program = program;
4137
4138 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4139 let mock_ctx = ExecutorContext::new_mock(None).await;
4140 let version = Version(0);
4141
4142 let sketch_args = SketchCtor {
4143 on: PlaneName::Xy.to_string(),
4144 };
4145 let (_src_delta, scene_delta, sketch_id) = frontend
4146 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4147 .await
4148 .unwrap();
4149 assert_eq!(sketch_id, ObjectId(1));
4150 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
4151 let sketch_object = &scene_delta.new_graph.objects[1];
4152 assert_eq!(sketch_object.id, ObjectId(1));
4153 assert_eq!(
4154 sketch_object.kind,
4155 ObjectKind::Sketch(Sketch {
4156 args: SketchCtor {
4157 on: PlaneName::Xy.to_string()
4158 },
4159 plane: ObjectId(0),
4160 segments: vec![],
4161 constraints: vec![],
4162 })
4163 );
4164 assert_eq!(scene_delta.new_graph.objects.len(), 2);
4165
4166 let line_ctor = LineCtor {
4167 start: Point2d {
4168 x: Expr::Number(Number {
4169 value: 0.0,
4170 units: NumericSuffix::Mm,
4171 }),
4172 y: Expr::Number(Number {
4173 value: 0.0,
4174 units: NumericSuffix::Mm,
4175 }),
4176 },
4177 end: Point2d {
4178 x: Expr::Number(Number {
4179 value: 10.0,
4180 units: NumericSuffix::Mm,
4181 }),
4182 y: Expr::Number(Number {
4183 value: 10.0,
4184 units: NumericSuffix::Mm,
4185 }),
4186 },
4187 construction: None,
4188 };
4189 let segment = SegmentCtor::Line(line_ctor);
4190 let (src_delta, scene_delta) = frontend
4191 .add_segment(&mock_ctx, version, sketch_id, segment, None)
4192 .await
4193 .unwrap();
4194 assert_eq!(
4195 src_delta.text.as_str(),
4196 "@settings(experimentalFeatures = allow)
4197
4198sketch(on = XY) {
4199 line(start = [0mm, 0mm], end = [10mm, 10mm])
4200}
4201"
4202 );
4203 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4204
4205 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
4206 assert_eq!(
4207 src_delta.text.as_str(),
4208 "@settings(experimentalFeatures = allow)
4209"
4210 );
4211 assert_eq!(scene_delta.new_graph.objects.len(), 0);
4212
4213 ctx.close().await;
4214 mock_ctx.close().await;
4215 }
4216
4217 #[tokio::test(flavor = "multi_thread")]
4218 async fn test_delete_sketch_when_sketch_block_uses_variable() {
4219 let initial_source = "@settings(experimentalFeatures = allow)
4220
4221s = sketch(on = XY) {}
4222";
4223
4224 let program = Program::parse(initial_source).unwrap().0.unwrap();
4225
4226 let mut frontend = FrontendState::new();
4227
4228 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4229 let mock_ctx = ExecutorContext::new_mock(None).await;
4230 let version = Version(0);
4231
4232 frontend.hack_set_program(&ctx, program).await.unwrap();
4233 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4234 let sketch_id = sketch_object.id;
4235
4236 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
4237 assert_eq!(
4238 src_delta.text.as_str(),
4239 "@settings(experimentalFeatures = allow)
4240"
4241 );
4242 assert_eq!(scene_delta.new_graph.objects.len(), 0);
4243
4244 ctx.close().await;
4245 mock_ctx.close().await;
4246 }
4247
4248 #[tokio::test(flavor = "multi_thread")]
4249 async fn test_edit_line_when_editing_its_start_point() {
4250 let initial_source = "\
4251@settings(experimentalFeatures = allow)
4252
4253sketch(on = XY) {
4254 line(start = [var 1, var 2], end = [var 3, var 4])
4255}
4256";
4257
4258 let program = Program::parse(initial_source).unwrap().0.unwrap();
4259
4260 let mut frontend = FrontendState::new();
4261
4262 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4263 let mock_ctx = ExecutorContext::new_mock(None).await;
4264 let version = Version(0);
4265
4266 frontend.hack_set_program(&ctx, program).await.unwrap();
4267 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4268 let sketch_id = sketch_object.id;
4269 let sketch = expect_sketch(sketch_object);
4270
4271 let point_id = *sketch.segments.first().unwrap();
4272
4273 let point_ctor = PointCtor {
4274 position: Point2d {
4275 x: Expr::Var(Number {
4276 value: 5.0,
4277 units: NumericSuffix::Inch,
4278 }),
4279 y: Expr::Var(Number {
4280 value: 6.0,
4281 units: NumericSuffix::Inch,
4282 }),
4283 },
4284 };
4285 let segments = vec![ExistingSegmentCtor {
4286 id: point_id,
4287 ctor: SegmentCtor::Point(point_ctor),
4288 }];
4289 let (src_delta, scene_delta) = frontend
4290 .edit_segments(&mock_ctx, version, sketch_id, segments)
4291 .await
4292 .unwrap();
4293 assert_eq!(
4294 src_delta.text.as_str(),
4295 "\
4296@settings(experimentalFeatures = allow)
4297
4298sketch(on = XY) {
4299 line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
4300}
4301"
4302 );
4303 assert_eq!(scene_delta.new_objects, vec![]);
4304 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4305
4306 ctx.close().await;
4307 mock_ctx.close().await;
4308 }
4309
4310 #[tokio::test(flavor = "multi_thread")]
4311 async fn test_edit_line_when_editing_its_end_point() {
4312 let initial_source = "\
4313@settings(experimentalFeatures = allow)
4314
4315sketch(on = XY) {
4316 line(start = [var 1, var 2], end = [var 3, var 4])
4317}
4318";
4319
4320 let program = Program::parse(initial_source).unwrap().0.unwrap();
4321
4322 let mut frontend = FrontendState::new();
4323
4324 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4325 let mock_ctx = ExecutorContext::new_mock(None).await;
4326 let version = Version(0);
4327
4328 frontend.hack_set_program(&ctx, program).await.unwrap();
4329 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4330 let sketch_id = sketch_object.id;
4331 let sketch = expect_sketch(sketch_object);
4332 let point_id = *sketch.segments.get(1).unwrap();
4333
4334 let point_ctor = PointCtor {
4335 position: Point2d {
4336 x: Expr::Var(Number {
4337 value: 5.0,
4338 units: NumericSuffix::Inch,
4339 }),
4340 y: Expr::Var(Number {
4341 value: 6.0,
4342 units: NumericSuffix::Inch,
4343 }),
4344 },
4345 };
4346 let segments = vec![ExistingSegmentCtor {
4347 id: point_id,
4348 ctor: SegmentCtor::Point(point_ctor),
4349 }];
4350 let (src_delta, scene_delta) = frontend
4351 .edit_segments(&mock_ctx, version, sketch_id, segments)
4352 .await
4353 .unwrap();
4354 assert_eq!(
4355 src_delta.text.as_str(),
4356 "\
4357@settings(experimentalFeatures = allow)
4358
4359sketch(on = XY) {
4360 line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
4361}
4362"
4363 );
4364 assert_eq!(scene_delta.new_objects, vec![]);
4365 assert_eq!(
4366 scene_delta.new_graph.objects.len(),
4367 5,
4368 "{:#?}",
4369 scene_delta.new_graph.objects
4370 );
4371
4372 ctx.close().await;
4373 mock_ctx.close().await;
4374 }
4375
4376 #[tokio::test(flavor = "multi_thread")]
4377 async fn test_edit_line_with_coincident_feedback() {
4378 let initial_source = "\
4379@settings(experimentalFeatures = allow)
4380
4381sketch(on = XY) {
4382 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
4383 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
4384 line1.start.at[0] == 0
4385 line1.start.at[1] == 0
4386 coincident([line1.end, line2.start])
4387 equalLength([line1, line2])
4388}
4389";
4390
4391 let program = Program::parse(initial_source).unwrap().0.unwrap();
4392
4393 let mut frontend = FrontendState::new();
4394
4395 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4396 let mock_ctx = ExecutorContext::new_mock(None).await;
4397 let version = Version(0);
4398
4399 frontend.hack_set_program(&ctx, program).await.unwrap();
4400 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4401 let sketch_id = sketch_object.id;
4402 let sketch = expect_sketch(sketch_object);
4403 let line2_end_id = *sketch.segments.get(4).unwrap();
4404
4405 let segments = vec![ExistingSegmentCtor {
4406 id: line2_end_id,
4407 ctor: SegmentCtor::Point(PointCtor {
4408 position: Point2d {
4409 x: Expr::Var(Number {
4410 value: 9.0,
4411 units: NumericSuffix::None,
4412 }),
4413 y: Expr::Var(Number {
4414 value: 10.0,
4415 units: NumericSuffix::None,
4416 }),
4417 },
4418 }),
4419 }];
4420 let (src_delta, scene_delta) = frontend
4421 .edit_segments(&mock_ctx, version, sketch_id, segments)
4422 .await
4423 .unwrap();
4424 assert_eq!(
4425 src_delta.text.as_str(),
4426 "\
4427@settings(experimentalFeatures = allow)
4428
4429sketch(on = XY) {
4430 line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
4431 line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
4432line1.start.at[0] == 0
4433line1.start.at[1] == 0
4434 coincident([line1.end, line2.start])
4435 equalLength([line1, line2])
4436}
4437"
4438 );
4439 assert_eq!(
4440 scene_delta.new_graph.objects.len(),
4441 10,
4442 "{:#?}",
4443 scene_delta.new_graph.objects
4444 );
4445
4446 ctx.close().await;
4447 mock_ctx.close().await;
4448 }
4449
4450 #[tokio::test(flavor = "multi_thread")]
4451 async fn test_delete_point_without_var() {
4452 let initial_source = "\
4453@settings(experimentalFeatures = allow)
4454
4455sketch(on = XY) {
4456 point(at = [var 1, var 2])
4457 point(at = [var 3, var 4])
4458 point(at = [var 5, var 6])
4459}
4460";
4461
4462 let program = Program::parse(initial_source).unwrap().0.unwrap();
4463
4464 let mut frontend = FrontendState::new();
4465
4466 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4467 let mock_ctx = ExecutorContext::new_mock(None).await;
4468 let version = Version(0);
4469
4470 frontend.hack_set_program(&ctx, program).await.unwrap();
4471 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4472 let sketch_id = sketch_object.id;
4473 let sketch = expect_sketch(sketch_object);
4474
4475 let point_id = *sketch.segments.get(1).unwrap();
4476
4477 let (src_delta, scene_delta) = frontend
4478 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
4479 .await
4480 .unwrap();
4481 assert_eq!(
4482 src_delta.text.as_str(),
4483 "\
4484@settings(experimentalFeatures = allow)
4485
4486sketch(on = XY) {
4487 point(at = [var 1mm, var 2mm])
4488 point(at = [var 5mm, var 6mm])
4489}
4490"
4491 );
4492 assert_eq!(scene_delta.new_objects, vec![]);
4493 assert_eq!(scene_delta.new_graph.objects.len(), 4);
4494
4495 ctx.close().await;
4496 mock_ctx.close().await;
4497 }
4498
4499 #[tokio::test(flavor = "multi_thread")]
4500 async fn test_delete_point_with_var() {
4501 let initial_source = "\
4502@settings(experimentalFeatures = allow)
4503
4504sketch(on = XY) {
4505 point(at = [var 1, var 2])
4506 point1 = point(at = [var 3, var 4])
4507 point(at = [var 5, var 6])
4508}
4509";
4510
4511 let program = Program::parse(initial_source).unwrap().0.unwrap();
4512
4513 let mut frontend = FrontendState::new();
4514
4515 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4516 let mock_ctx = ExecutorContext::new_mock(None).await;
4517 let version = Version(0);
4518
4519 frontend.hack_set_program(&ctx, program).await.unwrap();
4520 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4521 let sketch_id = sketch_object.id;
4522 let sketch = expect_sketch(sketch_object);
4523
4524 let point_id = *sketch.segments.get(1).unwrap();
4525
4526 let (src_delta, scene_delta) = frontend
4527 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
4528 .await
4529 .unwrap();
4530 assert_eq!(
4531 src_delta.text.as_str(),
4532 "\
4533@settings(experimentalFeatures = allow)
4534
4535sketch(on = XY) {
4536 point(at = [var 1mm, var 2mm])
4537 point(at = [var 5mm, var 6mm])
4538}
4539"
4540 );
4541 assert_eq!(scene_delta.new_objects, vec![]);
4542 assert_eq!(scene_delta.new_graph.objects.len(), 4);
4543
4544 ctx.close().await;
4545 mock_ctx.close().await;
4546 }
4547
4548 #[tokio::test(flavor = "multi_thread")]
4549 async fn test_delete_multiple_points() {
4550 let initial_source = "\
4551@settings(experimentalFeatures = allow)
4552
4553sketch(on = XY) {
4554 point(at = [var 1, var 2])
4555 point1 = point(at = [var 3, var 4])
4556 point(at = [var 5, var 6])
4557}
4558";
4559
4560 let program = Program::parse(initial_source).unwrap().0.unwrap();
4561
4562 let mut frontend = FrontendState::new();
4563
4564 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4565 let mock_ctx = ExecutorContext::new_mock(None).await;
4566 let version = Version(0);
4567
4568 frontend.hack_set_program(&ctx, program).await.unwrap();
4569 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4570 let sketch_id = sketch_object.id;
4571
4572 let sketch = expect_sketch(sketch_object);
4573
4574 let point1_id = *sketch.segments.first().unwrap();
4575 let point2_id = *sketch.segments.get(1).unwrap();
4576
4577 let (src_delta, scene_delta) = frontend
4578 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
4579 .await
4580 .unwrap();
4581 assert_eq!(
4582 src_delta.text.as_str(),
4583 "\
4584@settings(experimentalFeatures = allow)
4585
4586sketch(on = XY) {
4587 point(at = [var 5mm, var 6mm])
4588}
4589"
4590 );
4591 assert_eq!(scene_delta.new_objects, vec![]);
4592 assert_eq!(scene_delta.new_graph.objects.len(), 3);
4593
4594 ctx.close().await;
4595 mock_ctx.close().await;
4596 }
4597
4598 #[tokio::test(flavor = "multi_thread")]
4599 async fn test_delete_coincident_constraint() {
4600 let initial_source = "\
4601@settings(experimentalFeatures = allow)
4602
4603sketch(on = XY) {
4604 point1 = point(at = [var 1, var 2])
4605 point2 = point(at = [var 3, var 4])
4606 coincident([point1, point2])
4607 point(at = [var 5, var 6])
4608}
4609";
4610
4611 let program = Program::parse(initial_source).unwrap().0.unwrap();
4612
4613 let mut frontend = FrontendState::new();
4614
4615 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4616 let mock_ctx = ExecutorContext::new_mock(None).await;
4617 let version = Version(0);
4618
4619 frontend.hack_set_program(&ctx, program).await.unwrap();
4620 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4621 let sketch_id = sketch_object.id;
4622 let sketch = expect_sketch(sketch_object);
4623
4624 let coincident_id = *sketch.constraints.first().unwrap();
4625
4626 let (src_delta, scene_delta) = frontend
4627 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
4628 .await
4629 .unwrap();
4630 assert_eq!(
4631 src_delta.text.as_str(),
4632 "\
4633@settings(experimentalFeatures = allow)
4634
4635sketch(on = XY) {
4636 point1 = point(at = [var 1mm, var 2mm])
4637 point2 = point(at = [var 3mm, var 4mm])
4638 point(at = [var 5mm, var 6mm])
4639}
4640"
4641 );
4642 assert_eq!(scene_delta.new_objects, vec![]);
4643 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4644
4645 ctx.close().await;
4646 mock_ctx.close().await;
4647 }
4648
4649 #[tokio::test(flavor = "multi_thread")]
4650 async fn test_delete_line_cascades_to_coincident_constraint() {
4651 let initial_source = "\
4652@settings(experimentalFeatures = allow)
4653
4654sketch(on = XY) {
4655 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
4656 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
4657 coincident([line1.end, line2.start])
4658}
4659";
4660
4661 let program = Program::parse(initial_source).unwrap().0.unwrap();
4662
4663 let mut frontend = FrontendState::new();
4664
4665 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4666 let mock_ctx = ExecutorContext::new_mock(None).await;
4667 let version = Version(0);
4668
4669 frontend.hack_set_program(&ctx, program).await.unwrap();
4670 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4671 let sketch_id = sketch_object.id;
4672 let sketch = expect_sketch(sketch_object);
4673 let line_id = *sketch.segments.get(5).unwrap();
4674
4675 let (src_delta, scene_delta) = frontend
4676 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
4677 .await
4678 .unwrap();
4679 assert_eq!(
4680 src_delta.text.as_str(),
4681 "\
4682@settings(experimentalFeatures = allow)
4683
4684sketch(on = XY) {
4685 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4686}
4687"
4688 );
4689 assert_eq!(
4690 scene_delta.new_graph.objects.len(),
4691 5,
4692 "{:#?}",
4693 scene_delta.new_graph.objects
4694 );
4695
4696 ctx.close().await;
4697 mock_ctx.close().await;
4698 }
4699
4700 #[tokio::test(flavor = "multi_thread")]
4701 async fn test_delete_line_cascades_to_distance_constraint() {
4702 let initial_source = "\
4703@settings(experimentalFeatures = allow)
4704
4705sketch(on = XY) {
4706 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
4707 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
4708 distance([line1.end, line2.start]) == 10mm
4709}
4710";
4711
4712 let program = Program::parse(initial_source).unwrap().0.unwrap();
4713
4714 let mut frontend = FrontendState::new();
4715
4716 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4717 let mock_ctx = ExecutorContext::new_mock(None).await;
4718 let version = Version(0);
4719
4720 frontend.hack_set_program(&ctx, program).await.unwrap();
4721 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4722 let sketch_id = sketch_object.id;
4723 let sketch = expect_sketch(sketch_object);
4724 let line_id = *sketch.segments.get(5).unwrap();
4725
4726 let (src_delta, scene_delta) = frontend
4727 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
4728 .await
4729 .unwrap();
4730 assert_eq!(
4731 src_delta.text.as_str(),
4732 "\
4733@settings(experimentalFeatures = allow)
4734
4735sketch(on = XY) {
4736 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4737}
4738"
4739 );
4740 assert_eq!(
4741 scene_delta.new_graph.objects.len(),
4742 5,
4743 "{:#?}",
4744 scene_delta.new_graph.objects
4745 );
4746
4747 ctx.close().await;
4748 mock_ctx.close().await;
4749 }
4750
4751 #[tokio::test(flavor = "multi_thread")]
4752 async fn test_delete_line_line_coincident_constraint() {
4753 let initial_source = "\
4754@settings(experimentalFeatures = allow)
4755
4756sketch(on = XY) {
4757 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
4758 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
4759 coincident([line1, line2])
4760}
4761";
4762
4763 let program = Program::parse(initial_source).unwrap().0.unwrap();
4764
4765 let mut frontend = FrontendState::new();
4766
4767 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4768 let mock_ctx = ExecutorContext::new_mock(None).await;
4769 let version = Version(0);
4770
4771 frontend.hack_set_program(&ctx, program).await.unwrap();
4772 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4773 let sketch_id = sketch_object.id;
4774 let sketch = expect_sketch(sketch_object);
4775
4776 let coincident_id = *sketch.constraints.first().unwrap();
4777
4778 let (src_delta, scene_delta) = frontend
4779 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
4780 .await
4781 .unwrap();
4782 assert_eq!(
4783 src_delta.text.as_str(),
4784 "\
4785@settings(experimentalFeatures = allow)
4786
4787sketch(on = XY) {
4788 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4789 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
4790}
4791"
4792 );
4793 assert_eq!(scene_delta.new_objects, vec![]);
4794 assert_eq!(scene_delta.new_graph.objects.len(), 8);
4795
4796 ctx.close().await;
4797 mock_ctx.close().await;
4798 }
4799
4800 #[tokio::test(flavor = "multi_thread")]
4801 async fn test_two_points_coincident() {
4802 let initial_source = "\
4803@settings(experimentalFeatures = allow)
4804
4805sketch(on = XY) {
4806 point1 = point(at = [var 1, var 2])
4807 point(at = [3, 4])
4808}
4809";
4810
4811 let program = Program::parse(initial_source).unwrap().0.unwrap();
4812
4813 let mut frontend = FrontendState::new();
4814
4815 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4816 let mock_ctx = ExecutorContext::new_mock(None).await;
4817 let version = Version(0);
4818
4819 frontend.hack_set_program(&ctx, program).await.unwrap();
4820 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4821 let sketch_id = sketch_object.id;
4822 let sketch = expect_sketch(sketch_object);
4823 let point0_id = *sketch.segments.first().unwrap();
4824 let point1_id = *sketch.segments.get(1).unwrap();
4825
4826 let constraint = Constraint::Coincident(Coincident {
4827 segments: vec![point0_id, point1_id],
4828 });
4829 let (src_delta, scene_delta) = frontend
4830 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4831 .await
4832 .unwrap();
4833 assert_eq!(
4834 src_delta.text.as_str(),
4835 "\
4836@settings(experimentalFeatures = allow)
4837
4838sketch(on = XY) {
4839 point1 = point(at = [var 1, var 2])
4840 point2 = point(at = [3, 4])
4841 coincident([point1, point2])
4842}
4843"
4844 );
4845 assert_eq!(
4846 scene_delta.new_graph.objects.len(),
4847 5,
4848 "{:#?}",
4849 scene_delta.new_graph.objects
4850 );
4851
4852 ctx.close().await;
4853 mock_ctx.close().await;
4854 }
4855
4856 #[tokio::test(flavor = "multi_thread")]
4857 async fn test_coincident_of_line_end_points() {
4858 let initial_source = "\
4859@settings(experimentalFeatures = allow)
4860
4861sketch(on = XY) {
4862 line(start = [var 1, var 2], end = [var 3, var 4])
4863 line(start = [var 5, var 6], end = [var 7, var 8])
4864}
4865";
4866
4867 let program = Program::parse(initial_source).unwrap().0.unwrap();
4868
4869 let mut frontend = FrontendState::new();
4870
4871 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4872 let mock_ctx = ExecutorContext::new_mock(None).await;
4873 let version = Version(0);
4874
4875 frontend.hack_set_program(&ctx, program).await.unwrap();
4876 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4877 let sketch_id = sketch_object.id;
4878 let sketch = expect_sketch(sketch_object);
4879 let point0_id = *sketch.segments.get(1).unwrap();
4880 let point1_id = *sketch.segments.get(3).unwrap();
4881
4882 let constraint = Constraint::Coincident(Coincident {
4883 segments: vec![point0_id, point1_id],
4884 });
4885 let (src_delta, scene_delta) = frontend
4886 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4887 .await
4888 .unwrap();
4889 assert_eq!(
4890 src_delta.text.as_str(),
4891 "\
4892@settings(experimentalFeatures = allow)
4893
4894sketch(on = XY) {
4895 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
4896 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
4897 coincident([line1.end, line2.start])
4898}
4899"
4900 );
4901 assert_eq!(
4902 scene_delta.new_graph.objects.len(),
4903 9,
4904 "{:#?}",
4905 scene_delta.new_graph.objects
4906 );
4907
4908 ctx.close().await;
4909 mock_ctx.close().await;
4910 }
4911
4912 #[tokio::test(flavor = "multi_thread")]
4913 async fn test_invalid_coincident_arc_and_line_preserves_state() {
4914 let program = Program::empty();
4922
4923 let mut frontend = FrontendState::new();
4924 frontend.program = program;
4925
4926 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4927 let mock_ctx = ExecutorContext::new_mock(None).await;
4928 let version = Version(0);
4929
4930 let sketch_args = SketchCtor {
4931 on: PlaneName::Xy.to_string(),
4932 };
4933 let (_src_delta, _scene_delta, sketch_id) = frontend
4934 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4935 .await
4936 .unwrap();
4937
4938 let arc_ctor = ArcCtor {
4940 start: Point2d {
4941 x: Expr::Var(Number {
4942 value: 0.0,
4943 units: NumericSuffix::Mm,
4944 }),
4945 y: Expr::Var(Number {
4946 value: 0.0,
4947 units: NumericSuffix::Mm,
4948 }),
4949 },
4950 end: Point2d {
4951 x: Expr::Var(Number {
4952 value: 10.0,
4953 units: NumericSuffix::Mm,
4954 }),
4955 y: Expr::Var(Number {
4956 value: 10.0,
4957 units: NumericSuffix::Mm,
4958 }),
4959 },
4960 center: Point2d {
4961 x: Expr::Var(Number {
4962 value: 10.0,
4963 units: NumericSuffix::Mm,
4964 }),
4965 y: Expr::Var(Number {
4966 value: 0.0,
4967 units: NumericSuffix::Mm,
4968 }),
4969 },
4970 construction: None,
4971 };
4972 let (_src_delta, scene_delta) = frontend
4973 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
4974 .await
4975 .unwrap();
4976 let arc_id = *scene_delta.new_objects.last().unwrap();
4978
4979 let line_ctor = LineCtor {
4981 start: Point2d {
4982 x: Expr::Var(Number {
4983 value: 20.0,
4984 units: NumericSuffix::Mm,
4985 }),
4986 y: Expr::Var(Number {
4987 value: 0.0,
4988 units: NumericSuffix::Mm,
4989 }),
4990 },
4991 end: Point2d {
4992 x: Expr::Var(Number {
4993 value: 30.0,
4994 units: NumericSuffix::Mm,
4995 }),
4996 y: Expr::Var(Number {
4997 value: 10.0,
4998 units: NumericSuffix::Mm,
4999 }),
5000 },
5001 construction: None,
5002 };
5003 let (_src_delta, scene_delta) = frontend
5004 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
5005 .await
5006 .unwrap();
5007 let line_id = *scene_delta.new_objects.last().unwrap();
5009
5010 let constraint = Constraint::Coincident(Coincident {
5013 segments: vec![arc_id, line_id],
5014 });
5015 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
5016
5017 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
5019
5020 let sketch_object_after =
5023 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
5024 let sketch_after = expect_sketch(sketch_object_after);
5025
5026 assert!(
5028 sketch_after.segments.contains(&arc_id),
5029 "Arc segment should still exist after failed constraint"
5030 );
5031 assert!(
5032 sketch_after.segments.contains(&line_id),
5033 "Line segment should still exist after failed constraint"
5034 );
5035
5036 let arc_obj = frontend
5038 .scene_graph
5039 .objects
5040 .get(arc_id.0)
5041 .expect("Arc object should still be accessible");
5042 let line_obj = frontend
5043 .scene_graph
5044 .objects
5045 .get(line_id.0)
5046 .expect("Line object should still be accessible");
5047
5048 match &arc_obj.kind {
5051 ObjectKind::Segment {
5052 segment: Segment::Arc(_),
5053 } => {}
5054 _ => panic!("Arc object should still be an arc segment"),
5055 }
5056 match &line_obj.kind {
5057 ObjectKind::Segment {
5058 segment: Segment::Line(_),
5059 } => {}
5060 _ => panic!("Line object should still be a line segment"),
5061 }
5062
5063 ctx.close().await;
5064 mock_ctx.close().await;
5065 }
5066
5067 #[tokio::test(flavor = "multi_thread")]
5068 async fn test_distance_two_points() {
5069 let initial_source = "\
5070@settings(experimentalFeatures = allow)
5071
5072sketch(on = XY) {
5073 point(at = [var 1, var 2])
5074 point(at = [var 3, var 4])
5075}
5076";
5077
5078 let program = Program::parse(initial_source).unwrap().0.unwrap();
5079
5080 let mut frontend = FrontendState::new();
5081
5082 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5083 let mock_ctx = ExecutorContext::new_mock(None).await;
5084 let version = Version(0);
5085
5086 frontend.hack_set_program(&ctx, program).await.unwrap();
5087 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5088 let sketch_id = sketch_object.id;
5089 let sketch = expect_sketch(sketch_object);
5090 let point0_id = *sketch.segments.first().unwrap();
5091 let point1_id = *sketch.segments.get(1).unwrap();
5092
5093 let constraint = Constraint::Distance(Distance {
5094 points: vec![point0_id, point1_id],
5095 distance: Number {
5096 value: 2.0,
5097 units: NumericSuffix::Mm,
5098 },
5099 source: Default::default(),
5100 });
5101 let (src_delta, scene_delta) = frontend
5102 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5103 .await
5104 .unwrap();
5105 assert_eq!(
5106 src_delta.text.as_str(),
5107 "\
5109@settings(experimentalFeatures = allow)
5110
5111sketch(on = XY) {
5112 point1 = point(at = [var 1, var 2])
5113 point2 = point(at = [var 3, var 4])
5114distance([point1, point2]) == 2mm
5115}
5116"
5117 );
5118 assert_eq!(
5119 scene_delta.new_graph.objects.len(),
5120 5,
5121 "{:#?}",
5122 scene_delta.new_graph.objects
5123 );
5124
5125 ctx.close().await;
5126 mock_ctx.close().await;
5127 }
5128
5129 #[tokio::test(flavor = "multi_thread")]
5130 async fn test_horizontal_distance_two_points() {
5131 let initial_source = "\
5132@settings(experimentalFeatures = allow)
5133
5134sketch(on = XY) {
5135 point(at = [var 1, var 2])
5136 point(at = [var 3, var 4])
5137}
5138";
5139
5140 let program = Program::parse(initial_source).unwrap().0.unwrap();
5141
5142 let mut frontend = FrontendState::new();
5143
5144 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5145 let mock_ctx = ExecutorContext::new_mock(None).await;
5146 let version = Version(0);
5147
5148 frontend.hack_set_program(&ctx, program).await.unwrap();
5149 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5150 let sketch_id = sketch_object.id;
5151 let sketch = expect_sketch(sketch_object);
5152 let point0_id = *sketch.segments.first().unwrap();
5153 let point1_id = *sketch.segments.get(1).unwrap();
5154
5155 let constraint = Constraint::HorizontalDistance(Distance {
5156 points: vec![point0_id, point1_id],
5157 distance: Number {
5158 value: 2.0,
5159 units: NumericSuffix::Mm,
5160 },
5161 source: Default::default(),
5162 });
5163 let (src_delta, scene_delta) = frontend
5164 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5165 .await
5166 .unwrap();
5167 assert_eq!(
5168 src_delta.text.as_str(),
5169 "\
5171@settings(experimentalFeatures = allow)
5172
5173sketch(on = XY) {
5174 point1 = point(at = [var 1, var 2])
5175 point2 = point(at = [var 3, var 4])
5176horizontalDistance([point1, point2]) == 2mm
5177}
5178"
5179 );
5180 assert_eq!(
5181 scene_delta.new_graph.objects.len(),
5182 5,
5183 "{:#?}",
5184 scene_delta.new_graph.objects
5185 );
5186
5187 ctx.close().await;
5188 mock_ctx.close().await;
5189 }
5190
5191 #[tokio::test(flavor = "multi_thread")]
5192 async fn test_radius_single_arc_segment() {
5193 let initial_source = "\
5194@settings(experimentalFeatures = allow)
5195
5196sketch(on = XY) {
5197 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5198}
5199";
5200
5201 let program = Program::parse(initial_source).unwrap().0.unwrap();
5202
5203 let mut frontend = FrontendState::new();
5204
5205 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5206 let mock_ctx = ExecutorContext::new_mock(None).await;
5207 let version = Version(0);
5208
5209 frontend.hack_set_program(&ctx, program).await.unwrap();
5210 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5211 let sketch_id = sketch_object.id;
5212 let sketch = expect_sketch(sketch_object);
5213 let arc_id = sketch
5215 .segments
5216 .iter()
5217 .find(|&seg_id| {
5218 let obj = frontend.scene_graph.objects.get(seg_id.0);
5219 matches!(
5220 obj.map(|o| &o.kind),
5221 Some(ObjectKind::Segment {
5222 segment: Segment::Arc(_)
5223 })
5224 )
5225 })
5226 .unwrap();
5227
5228 let constraint = Constraint::Radius(Radius {
5229 arc: *arc_id,
5230 radius: Number {
5231 value: 5.0,
5232 units: NumericSuffix::Mm,
5233 },
5234 });
5235 let (src_delta, scene_delta) = frontend
5236 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5237 .await
5238 .unwrap();
5239 assert_eq!(
5240 src_delta.text.as_str(),
5241 "\
5243@settings(experimentalFeatures = allow)
5244
5245sketch(on = XY) {
5246 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5247radius(arc1) == 5mm
5248}
5249"
5250 );
5251 assert_eq!(
5252 scene_delta.new_graph.objects.len(),
5253 7, "{:#?}",
5255 scene_delta.new_graph.objects
5256 );
5257
5258 ctx.close().await;
5259 mock_ctx.close().await;
5260 }
5261
5262 #[tokio::test(flavor = "multi_thread")]
5263 async fn test_vertical_distance_two_points() {
5264 let initial_source = "\
5265@settings(experimentalFeatures = allow)
5266
5267sketch(on = XY) {
5268 point(at = [var 1, var 2])
5269 point(at = [var 3, var 4])
5270}
5271";
5272
5273 let program = Program::parse(initial_source).unwrap().0.unwrap();
5274
5275 let mut frontend = FrontendState::new();
5276
5277 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5278 let mock_ctx = ExecutorContext::new_mock(None).await;
5279 let version = Version(0);
5280
5281 frontend.hack_set_program(&ctx, program).await.unwrap();
5282 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5283 let sketch_id = sketch_object.id;
5284 let sketch = expect_sketch(sketch_object);
5285 let point0_id = *sketch.segments.first().unwrap();
5286 let point1_id = *sketch.segments.get(1).unwrap();
5287
5288 let constraint = Constraint::VerticalDistance(Distance {
5289 points: vec![point0_id, point1_id],
5290 distance: Number {
5291 value: 2.0,
5292 units: NumericSuffix::Mm,
5293 },
5294 source: Default::default(),
5295 });
5296 let (src_delta, scene_delta) = frontend
5297 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5298 .await
5299 .unwrap();
5300 assert_eq!(
5301 src_delta.text.as_str(),
5302 "\
5304@settings(experimentalFeatures = allow)
5305
5306sketch(on = XY) {
5307 point1 = point(at = [var 1, var 2])
5308 point2 = point(at = [var 3, var 4])
5309verticalDistance([point1, point2]) == 2mm
5310}
5311"
5312 );
5313 assert_eq!(
5314 scene_delta.new_graph.objects.len(),
5315 5,
5316 "{:#?}",
5317 scene_delta.new_graph.objects
5318 );
5319
5320 ctx.close().await;
5321 mock_ctx.close().await;
5322 }
5323
5324 #[tokio::test(flavor = "multi_thread")]
5325 async fn test_radius_error_cases() {
5326 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5327 let mock_ctx = ExecutorContext::new_mock(None).await;
5328 let version = Version(0);
5329
5330 let initial_source_point = "\
5332@settings(experimentalFeatures = allow)
5333
5334sketch(on = XY) {
5335 point(at = [var 1, var 2])
5336}
5337";
5338 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
5339 let mut frontend_point = FrontendState::new();
5340 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
5341 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
5342 let sketch_id_point = sketch_object_point.id;
5343 let sketch_point = expect_sketch(sketch_object_point);
5344 let point_id = *sketch_point.segments.first().unwrap();
5345
5346 let constraint_point = Constraint::Radius(Radius {
5347 arc: point_id,
5348 radius: Number {
5349 value: 5.0,
5350 units: NumericSuffix::Mm,
5351 },
5352 });
5353 let result_point = frontend_point
5354 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
5355 .await;
5356 assert!(result_point.is_err(), "Single point should error for radius");
5357
5358 let initial_source_line = "\
5360@settings(experimentalFeatures = allow)
5361
5362sketch(on = XY) {
5363 line(start = [var 1, var 2], end = [var 3, var 4])
5364}
5365";
5366 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
5367 let mut frontend_line = FrontendState::new();
5368 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
5369 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
5370 let sketch_id_line = sketch_object_line.id;
5371 let sketch_line = expect_sketch(sketch_object_line);
5372 let line_id = *sketch_line.segments.first().unwrap();
5373
5374 let constraint_line = Constraint::Radius(Radius {
5375 arc: line_id,
5376 radius: Number {
5377 value: 5.0,
5378 units: NumericSuffix::Mm,
5379 },
5380 });
5381 let result_line = frontend_line
5382 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
5383 .await;
5384 assert!(result_line.is_err(), "Single line segment should error for radius");
5385
5386 ctx.close().await;
5387 mock_ctx.close().await;
5388 }
5389
5390 #[tokio::test(flavor = "multi_thread")]
5391 async fn test_diameter_single_arc_segment() {
5392 let initial_source = "\
5393@settings(experimentalFeatures = allow)
5394
5395sketch(on = XY) {
5396 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5397}
5398";
5399
5400 let program = Program::parse(initial_source).unwrap().0.unwrap();
5401
5402 let mut frontend = FrontendState::new();
5403
5404 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5405 let mock_ctx = ExecutorContext::new_mock(None).await;
5406 let version = Version(0);
5407
5408 frontend.hack_set_program(&ctx, program).await.unwrap();
5409 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5410 let sketch_id = sketch_object.id;
5411 let sketch = expect_sketch(sketch_object);
5412 let arc_id = sketch
5414 .segments
5415 .iter()
5416 .find(|&seg_id| {
5417 let obj = frontend.scene_graph.objects.get(seg_id.0);
5418 matches!(
5419 obj.map(|o| &o.kind),
5420 Some(ObjectKind::Segment {
5421 segment: Segment::Arc(_)
5422 })
5423 )
5424 })
5425 .unwrap();
5426
5427 let constraint = Constraint::Diameter(Diameter {
5428 arc: *arc_id,
5429 diameter: Number {
5430 value: 10.0,
5431 units: NumericSuffix::Mm,
5432 },
5433 });
5434 let (src_delta, scene_delta) = frontend
5435 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5436 .await
5437 .unwrap();
5438 assert_eq!(
5439 src_delta.text.as_str(),
5440 "\
5442@settings(experimentalFeatures = allow)
5443
5444sketch(on = XY) {
5445 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5446diameter(arc1) == 10mm
5447}
5448"
5449 );
5450 assert_eq!(
5451 scene_delta.new_graph.objects.len(),
5452 7, "{:#?}",
5454 scene_delta.new_graph.objects
5455 );
5456
5457 ctx.close().await;
5458 mock_ctx.close().await;
5459 }
5460
5461 #[tokio::test(flavor = "multi_thread")]
5462 async fn test_diameter_error_cases() {
5463 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5464 let mock_ctx = ExecutorContext::new_mock(None).await;
5465 let version = Version(0);
5466
5467 let initial_source_point = "\
5469@settings(experimentalFeatures = allow)
5470
5471sketch(on = XY) {
5472 point(at = [var 1, var 2])
5473}
5474";
5475 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
5476 let mut frontend_point = FrontendState::new();
5477 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
5478 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
5479 let sketch_id_point = sketch_object_point.id;
5480 let sketch_point = expect_sketch(sketch_object_point);
5481 let point_id = *sketch_point.segments.first().unwrap();
5482
5483 let constraint_point = Constraint::Diameter(Diameter {
5484 arc: point_id,
5485 diameter: Number {
5486 value: 10.0,
5487 units: NumericSuffix::Mm,
5488 },
5489 });
5490 let result_point = frontend_point
5491 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
5492 .await;
5493 assert!(result_point.is_err(), "Single point should error for diameter");
5494
5495 let initial_source_line = "\
5497@settings(experimentalFeatures = allow)
5498
5499sketch(on = XY) {
5500 line(start = [var 1, var 2], end = [var 3, var 4])
5501}
5502";
5503 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
5504 let mut frontend_line = FrontendState::new();
5505 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
5506 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
5507 let sketch_id_line = sketch_object_line.id;
5508 let sketch_line = expect_sketch(sketch_object_line);
5509 let line_id = *sketch_line.segments.first().unwrap();
5510
5511 let constraint_line = Constraint::Diameter(Diameter {
5512 arc: line_id,
5513 diameter: Number {
5514 value: 10.0,
5515 units: NumericSuffix::Mm,
5516 },
5517 });
5518 let result_line = frontend_line
5519 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
5520 .await;
5521 assert!(result_line.is_err(), "Single line segment should error for diameter");
5522
5523 ctx.close().await;
5524 mock_ctx.close().await;
5525 }
5526
5527 #[tokio::test(flavor = "multi_thread")]
5528 async fn test_line_horizontal() {
5529 let initial_source = "\
5530@settings(experimentalFeatures = allow)
5531
5532sketch(on = XY) {
5533 line(start = [var 1, var 2], end = [var 3, var 4])
5534}
5535";
5536
5537 let program = Program::parse(initial_source).unwrap().0.unwrap();
5538
5539 let mut frontend = FrontendState::new();
5540
5541 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5542 let mock_ctx = ExecutorContext::new_mock(None).await;
5543 let version = Version(0);
5544
5545 frontend.hack_set_program(&ctx, program).await.unwrap();
5546 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5547 let sketch_id = sketch_object.id;
5548 let sketch = expect_sketch(sketch_object);
5549 let line1_id = *sketch.segments.get(2).unwrap();
5550
5551 let constraint = Constraint::Horizontal(Horizontal { line: line1_id });
5552 let (src_delta, scene_delta) = frontend
5553 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5554 .await
5555 .unwrap();
5556 assert_eq!(
5557 src_delta.text.as_str(),
5558 "\
5559@settings(experimentalFeatures = allow)
5560
5561sketch(on = XY) {
5562 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5563 horizontal(line1)
5564}
5565"
5566 );
5567 assert_eq!(
5568 scene_delta.new_graph.objects.len(),
5569 6,
5570 "{:#?}",
5571 scene_delta.new_graph.objects
5572 );
5573
5574 ctx.close().await;
5575 mock_ctx.close().await;
5576 }
5577
5578 #[tokio::test(flavor = "multi_thread")]
5579 async fn test_line_vertical() {
5580 let initial_source = "\
5581@settings(experimentalFeatures = allow)
5582
5583sketch(on = XY) {
5584 line(start = [var 1, var 2], end = [var 3, var 4])
5585}
5586";
5587
5588 let program = Program::parse(initial_source).unwrap().0.unwrap();
5589
5590 let mut frontend = FrontendState::new();
5591
5592 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5593 let mock_ctx = ExecutorContext::new_mock(None).await;
5594 let version = Version(0);
5595
5596 frontend.hack_set_program(&ctx, program).await.unwrap();
5597 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5598 let sketch_id = sketch_object.id;
5599 let sketch = expect_sketch(sketch_object);
5600 let line1_id = *sketch.segments.get(2).unwrap();
5601
5602 let constraint = Constraint::Vertical(Vertical { line: line1_id });
5603 let (src_delta, scene_delta) = frontend
5604 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5605 .await
5606 .unwrap();
5607 assert_eq!(
5608 src_delta.text.as_str(),
5609 "\
5610@settings(experimentalFeatures = allow)
5611
5612sketch(on = XY) {
5613 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5614 vertical(line1)
5615}
5616"
5617 );
5618 assert_eq!(
5619 scene_delta.new_graph.objects.len(),
5620 6,
5621 "{:#?}",
5622 scene_delta.new_graph.objects
5623 );
5624
5625 ctx.close().await;
5626 mock_ctx.close().await;
5627 }
5628
5629 #[tokio::test(flavor = "multi_thread")]
5630 async fn test_lines_equal_length() {
5631 let initial_source = "\
5632@settings(experimentalFeatures = allow)
5633
5634sketch(on = XY) {
5635 line(start = [var 1, var 2], end = [var 3, var 4])
5636 line(start = [var 5, var 6], end = [var 7, var 8])
5637}
5638";
5639
5640 let program = Program::parse(initial_source).unwrap().0.unwrap();
5641
5642 let mut frontend = FrontendState::new();
5643
5644 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5645 let mock_ctx = ExecutorContext::new_mock(None).await;
5646 let version = Version(0);
5647
5648 frontend.hack_set_program(&ctx, program).await.unwrap();
5649 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5650 let sketch_id = sketch_object.id;
5651 let sketch = expect_sketch(sketch_object);
5652 let line1_id = *sketch.segments.get(2).unwrap();
5653 let line2_id = *sketch.segments.get(5).unwrap();
5654
5655 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
5656 lines: vec![line1_id, line2_id],
5657 });
5658 let (src_delta, scene_delta) = frontend
5659 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5660 .await
5661 .unwrap();
5662 assert_eq!(
5663 src_delta.text.as_str(),
5664 "\
5665@settings(experimentalFeatures = allow)
5666
5667sketch(on = XY) {
5668 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5669 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5670 equalLength([line1, line2])
5671}
5672"
5673 );
5674 assert_eq!(
5675 scene_delta.new_graph.objects.len(),
5676 9,
5677 "{:#?}",
5678 scene_delta.new_graph.objects
5679 );
5680
5681 ctx.close().await;
5682 mock_ctx.close().await;
5683 }
5684
5685 #[tokio::test(flavor = "multi_thread")]
5686 async fn test_lines_parallel() {
5687 let initial_source = "\
5688@settings(experimentalFeatures = allow)
5689
5690sketch(on = XY) {
5691 line(start = [var 1, var 2], end = [var 3, var 4])
5692 line(start = [var 5, var 6], end = [var 7, var 8])
5693}
5694";
5695
5696 let program = Program::parse(initial_source).unwrap().0.unwrap();
5697
5698 let mut frontend = FrontendState::new();
5699
5700 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5701 let mock_ctx = ExecutorContext::new_mock(None).await;
5702 let version = Version(0);
5703
5704 frontend.hack_set_program(&ctx, program).await.unwrap();
5705 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5706 let sketch_id = sketch_object.id;
5707 let sketch = expect_sketch(sketch_object);
5708 let line1_id = *sketch.segments.get(2).unwrap();
5709 let line2_id = *sketch.segments.get(5).unwrap();
5710
5711 let constraint = Constraint::Parallel(Parallel {
5712 lines: vec![line1_id, line2_id],
5713 });
5714 let (src_delta, scene_delta) = frontend
5715 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5716 .await
5717 .unwrap();
5718 assert_eq!(
5719 src_delta.text.as_str(),
5720 "\
5721@settings(experimentalFeatures = allow)
5722
5723sketch(on = XY) {
5724 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5725 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5726 parallel([line1, line2])
5727}
5728"
5729 );
5730 assert_eq!(
5731 scene_delta.new_graph.objects.len(),
5732 9,
5733 "{:#?}",
5734 scene_delta.new_graph.objects
5735 );
5736
5737 ctx.close().await;
5738 mock_ctx.close().await;
5739 }
5740
5741 #[tokio::test(flavor = "multi_thread")]
5742 async fn test_lines_perpendicular() {
5743 let initial_source = "\
5744@settings(experimentalFeatures = allow)
5745
5746sketch(on = XY) {
5747 line(start = [var 1, var 2], end = [var 3, var 4])
5748 line(start = [var 5, var 6], end = [var 7, var 8])
5749}
5750";
5751
5752 let program = Program::parse(initial_source).unwrap().0.unwrap();
5753
5754 let mut frontend = FrontendState::new();
5755
5756 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5757 let mock_ctx = ExecutorContext::new_mock(None).await;
5758 let version = Version(0);
5759
5760 frontend.hack_set_program(&ctx, program).await.unwrap();
5761 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5762 let sketch_id = sketch_object.id;
5763 let sketch = expect_sketch(sketch_object);
5764 let line1_id = *sketch.segments.get(2).unwrap();
5765 let line2_id = *sketch.segments.get(5).unwrap();
5766
5767 let constraint = Constraint::Perpendicular(Perpendicular {
5768 lines: vec![line1_id, line2_id],
5769 });
5770 let (src_delta, scene_delta) = frontend
5771 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5772 .await
5773 .unwrap();
5774 assert_eq!(
5775 src_delta.text.as_str(),
5776 "\
5777@settings(experimentalFeatures = allow)
5778
5779sketch(on = XY) {
5780 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5781 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5782 perpendicular([line1, line2])
5783}
5784"
5785 );
5786 assert_eq!(
5787 scene_delta.new_graph.objects.len(),
5788 9,
5789 "{:#?}",
5790 scene_delta.new_graph.objects
5791 );
5792
5793 ctx.close().await;
5794 mock_ctx.close().await;
5795 }
5796
5797 #[tokio::test(flavor = "multi_thread")]
5798 async fn test_sketch_on_face_simple() {
5799 let initial_source = "\
5800@settings(experimentalFeatures = allow)
5801
5802len = 2mm
5803cube = startSketchOn(XY)
5804 |> startProfile(at = [0, 0])
5805 |> line(end = [len, 0], tag = $side)
5806 |> line(end = [0, len])
5807 |> line(end = [-len, 0])
5808 |> line(end = [0, -len])
5809 |> close()
5810 |> extrude(length = len)
5811
5812face = faceOf(cube, face = side)
5813";
5814
5815 let program = Program::parse(initial_source).unwrap().0.unwrap();
5816
5817 let mut frontend = FrontendState::new();
5818
5819 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5820 let mock_ctx = ExecutorContext::new_mock(None).await;
5821 let version = Version(0);
5822
5823 frontend.hack_set_program(&ctx, program).await.unwrap();
5824 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
5825 let face_id = face_object.id;
5826
5827 let sketch_args = SketchCtor { on: "face".to_owned() };
5828 let (_src_delta, scene_delta, sketch_id) = frontend
5829 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5830 .await
5831 .unwrap();
5832 assert_eq!(sketch_id, ObjectId(2));
5833 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
5834 let sketch_object = &scene_delta.new_graph.objects[2];
5835 assert_eq!(sketch_object.id, ObjectId(2));
5836 assert_eq!(
5837 sketch_object.kind,
5838 ObjectKind::Sketch(Sketch {
5839 args: SketchCtor { on: "face".to_owned() },
5840 plane: face_id,
5841 segments: vec![],
5842 constraints: vec![],
5843 })
5844 );
5845 assert_eq!(scene_delta.new_graph.objects.len(), 3);
5846
5847 ctx.close().await;
5848 mock_ctx.close().await;
5849 }
5850
5851 #[tokio::test(flavor = "multi_thread")]
5852 async fn test_sketch_on_plane_incremental() {
5853 let initial_source = "\
5854@settings(experimentalFeatures = allow)
5855
5856len = 2mm
5857cube = startSketchOn(XY)
5858 |> startProfile(at = [0, 0])
5859 |> line(end = [len, 0], tag = $side)
5860 |> line(end = [0, len])
5861 |> line(end = [-len, 0])
5862 |> line(end = [0, -len])
5863 |> close()
5864 |> extrude(length = len)
5865
5866plane = planeOf(cube, face = side)
5867";
5868
5869 let program = Program::parse(initial_source).unwrap().0.unwrap();
5870
5871 let mut frontend = FrontendState::new();
5872
5873 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5874 let mock_ctx = ExecutorContext::new_mock(None).await;
5875 let version = Version(0);
5876
5877 frontend.hack_set_program(&ctx, program).await.unwrap();
5878 let plane_object = frontend
5880 .scene_graph
5881 .objects
5882 .iter()
5883 .rev()
5884 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
5885 .unwrap();
5886 let plane_id = plane_object.id;
5887
5888 let sketch_args = SketchCtor { on: "plane".to_owned() };
5889 let (src_delta, scene_delta, sketch_id) = frontend
5890 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5891 .await
5892 .unwrap();
5893 assert_eq!(
5894 src_delta.text.as_str(),
5895 "\
5896@settings(experimentalFeatures = allow)
5897
5898len = 2mm
5899cube = startSketchOn(XY)
5900 |> startProfile(at = [0, 0])
5901 |> line(end = [len, 0], tag = $side)
5902 |> line(end = [0, len])
5903 |> line(end = [-len, 0])
5904 |> line(end = [0, -len])
5905 |> close()
5906 |> extrude(length = len)
5907
5908plane = planeOf(cube, face = side)
5909sketch(on = plane) {
5910}
5911"
5912 );
5913 assert_eq!(sketch_id, ObjectId(2));
5914 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
5915 let sketch_object = &scene_delta.new_graph.objects[2];
5916 assert_eq!(sketch_object.id, ObjectId(2));
5917 assert_eq!(
5918 sketch_object.kind,
5919 ObjectKind::Sketch(Sketch {
5920 args: SketchCtor { on: "plane".to_owned() },
5921 plane: plane_id,
5922 segments: vec![],
5923 constraints: vec![],
5924 })
5925 );
5926 assert_eq!(scene_delta.new_graph.objects.len(), 3);
5927
5928 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
5929 assert_eq!(plane_object.id, plane_id);
5930 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
5931
5932 ctx.close().await;
5933 mock_ctx.close().await;
5934 }
5935
5936 #[tokio::test(flavor = "multi_thread")]
5937 async fn test_multiple_sketch_blocks() {
5938 let initial_source = "\
5939@settings(experimentalFeatures = allow)
5940
5941// Cube that requires the engine.
5942width = 2
5943sketch001 = startSketchOn(XY)
5944profile001 = startProfile(sketch001, at = [0, 0])
5945 |> yLine(length = width, tag = $seg1)
5946 |> xLine(length = width)
5947 |> yLine(length = -width)
5948 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5949 |> close()
5950extrude001 = extrude(profile001, length = width)
5951
5952// Get a value that requires the engine.
5953x = segLen(seg1)
5954
5955// Triangle with side length 2*x.
5956sketch(on = XY) {
5957 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5958 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
5959 coincident([line1.end, line2.start])
5960 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
5961 coincident([line2.end, line3.start])
5962 coincident([line3.end, line1.start])
5963 equalLength([line3, line1])
5964 equalLength([line1, line2])
5965distance([line1.start, line1.end]) == 2*x
5966}
5967
5968// Line segment with length x.
5969sketch2 = sketch(on = XY) {
5970 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5971distance([line1.start, line1.end]) == x
5972}
5973";
5974
5975 let program = Program::parse(initial_source).unwrap().0.unwrap();
5976
5977 let mut frontend = FrontendState::new();
5978
5979 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5980 let mock_ctx = ExecutorContext::new_mock(None).await;
5981 let version = Version(0);
5982 let project_id = ProjectId(0);
5983 let file_id = FileId(0);
5984
5985 frontend.hack_set_program(&ctx, program).await.unwrap();
5986 let sketch_objects = frontend
5987 .scene_graph
5988 .objects
5989 .iter()
5990 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
5991 .collect::<Vec<_>>();
5992 let sketch1_id = sketch_objects.first().unwrap().id;
5993 let sketch2_id = sketch_objects.get(1).unwrap().id;
5994 let point1_id = ObjectId(sketch1_id.0 + 1);
5996 let point2_id = ObjectId(sketch2_id.0 + 1);
5998
5999 let scene_delta = frontend
6008 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
6009 .await
6010 .unwrap();
6011 assert_eq!(
6012 scene_delta.new_graph.objects.len(),
6013 18,
6014 "{:#?}",
6015 scene_delta.new_graph.objects
6016 );
6017
6018 let point_ctor = PointCtor {
6020 position: Point2d {
6021 x: Expr::Var(Number {
6022 value: 1.0,
6023 units: NumericSuffix::Mm,
6024 }),
6025 y: Expr::Var(Number {
6026 value: 2.0,
6027 units: NumericSuffix::Mm,
6028 }),
6029 },
6030 };
6031 let segments = vec![ExistingSegmentCtor {
6032 id: point1_id,
6033 ctor: SegmentCtor::Point(point_ctor),
6034 }];
6035 let (src_delta, _) = frontend
6036 .edit_segments(&mock_ctx, version, sketch1_id, segments)
6037 .await
6038 .unwrap();
6039 assert_eq!(
6041 src_delta.text.as_str(),
6042 "\
6043@settings(experimentalFeatures = allow)
6044
6045// Cube that requires the engine.
6046width = 2
6047sketch001 = startSketchOn(XY)
6048profile001 = startProfile(sketch001, at = [0, 0])
6049 |> yLine(length = width, tag = $seg1)
6050 |> xLine(length = width)
6051 |> yLine(length = -width)
6052 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
6053 |> close()
6054extrude001 = extrude(profile001, length = width)
6055
6056// Get a value that requires the engine.
6057x = segLen(seg1)
6058
6059// Triangle with side length 2*x.
6060sketch(on = XY) {
6061 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
6062 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
6063 coincident([line1.end, line2.start])
6064 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
6065 coincident([line2.end, line3.start])
6066 coincident([line3.end, line1.start])
6067 equalLength([line3, line1])
6068 equalLength([line1, line2])
6069distance([line1.start, line1.end]) == 2 * x
6070}
6071
6072// Line segment with length x.
6073sketch2 = sketch(on = XY) {
6074 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
6075distance([line1.start, line1.end]) == x
6076}
6077"
6078 );
6079
6080 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
6082 assert_eq!(
6084 src_delta.text.as_str(),
6085 "\
6086@settings(experimentalFeatures = allow)
6087
6088// Cube that requires the engine.
6089width = 2
6090sketch001 = startSketchOn(XY)
6091profile001 = startProfile(sketch001, at = [0, 0])
6092 |> yLine(length = width, tag = $seg1)
6093 |> xLine(length = width)
6094 |> yLine(length = -width)
6095 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
6096 |> close()
6097extrude001 = extrude(profile001, length = width)
6098
6099// Get a value that requires the engine.
6100x = segLen(seg1)
6101
6102// Triangle with side length 2*x.
6103sketch(on = XY) {
6104 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
6105 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
6106 coincident([line1.end, line2.start])
6107 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
6108 coincident([line2.end, line3.start])
6109 coincident([line3.end, line1.start])
6110 equalLength([line3, line1])
6111 equalLength([line1, line2])
6112distance([line1.start, line1.end]) == 2 * x
6113}
6114
6115// Line segment with length x.
6116sketch2 = sketch(on = XY) {
6117 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
6118distance([line1.start, line1.end]) == x
6119}
6120"
6121 );
6122 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
6130 assert_eq!(scene.objects.len(), 23, "{:#?}", scene.objects);
6131
6132 let scene_delta = frontend
6140 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
6141 .await
6142 .unwrap();
6143 assert_eq!(
6144 scene_delta.new_graph.objects.len(),
6145 23,
6146 "{:#?}",
6147 scene_delta.new_graph.objects
6148 );
6149
6150 let point_ctor = PointCtor {
6152 position: Point2d {
6153 x: Expr::Var(Number {
6154 value: 3.0,
6155 units: NumericSuffix::Mm,
6156 }),
6157 y: Expr::Var(Number {
6158 value: 4.0,
6159 units: NumericSuffix::Mm,
6160 }),
6161 },
6162 };
6163 let segments = vec![ExistingSegmentCtor {
6164 id: point2_id,
6165 ctor: SegmentCtor::Point(point_ctor),
6166 }];
6167 let (src_delta, _) = frontend
6168 .edit_segments(&mock_ctx, version, sketch2_id, segments)
6169 .await
6170 .unwrap();
6171 assert_eq!(
6173 src_delta.text.as_str(),
6174 "\
6175@settings(experimentalFeatures = allow)
6176
6177// Cube that requires the engine.
6178width = 2
6179sketch001 = startSketchOn(XY)
6180profile001 = startProfile(sketch001, at = [0, 0])
6181 |> yLine(length = width, tag = $seg1)
6182 |> xLine(length = width)
6183 |> yLine(length = -width)
6184 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
6185 |> close()
6186extrude001 = extrude(profile001, length = width)
6187
6188// Get a value that requires the engine.
6189x = segLen(seg1)
6190
6191// Triangle with side length 2*x.
6192sketch(on = XY) {
6193 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
6194 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
6195 coincident([line1.end, line2.start])
6196 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
6197 coincident([line2.end, line3.start])
6198 coincident([line3.end, line1.start])
6199 equalLength([line3, line1])
6200 equalLength([line1, line2])
6201distance([line1.start, line1.end]) == 2 * x
6202}
6203
6204// Line segment with length x.
6205sketch2 = sketch(on = XY) {
6206 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
6207distance([line1.start, line1.end]) == x
6208}
6209"
6210 );
6211
6212 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
6214 assert_eq!(
6216 src_delta.text.as_str(),
6217 "\
6218@settings(experimentalFeatures = allow)
6219
6220// Cube that requires the engine.
6221width = 2
6222sketch001 = startSketchOn(XY)
6223profile001 = startProfile(sketch001, at = [0, 0])
6224 |> yLine(length = width, tag = $seg1)
6225 |> xLine(length = width)
6226 |> yLine(length = -width)
6227 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
6228 |> close()
6229extrude001 = extrude(profile001, length = width)
6230
6231// Get a value that requires the engine.
6232x = segLen(seg1)
6233
6234// Triangle with side length 2*x.
6235sketch(on = XY) {
6236 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
6237 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
6238 coincident([line1.end, line2.start])
6239 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
6240 coincident([line2.end, line3.start])
6241 coincident([line3.end, line1.start])
6242 equalLength([line3, line1])
6243 equalLength([line1, line2])
6244distance([line1.start, line1.end]) == 2 * x
6245}
6246
6247// Line segment with length x.
6248sketch2 = sketch(on = XY) {
6249 line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
6250distance([line1.start, line1.end]) == x
6251}
6252"
6253 );
6254
6255 ctx.close().await;
6256 mock_ctx.close().await;
6257 }
6258}