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