1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::ops::ControlFlow;
5
6use indexmap::IndexMap;
7use kcl_error::CompilationError;
8use kcl_error::SourceRange;
9use kittycad_modeling_cmds::units::UnitLength;
10use serde::Serialize;
11
12use crate::ExecOutcome;
13use crate::ExecutorContext;
14use crate::KclError;
15use crate::KclErrorWithOutputs;
16use crate::Program;
17use crate::collections::AhashIndexSet;
18use crate::exec::WarningLevel;
19use crate::execution::MockConfig;
20use crate::execution::SKETCH_BLOCK_PARAM_ON;
21use crate::fmt::format_number_literal;
22use crate::front::Angle;
23use crate::front::ArcCtor;
24use crate::front::CircleCtor;
25use crate::front::Distance;
26use crate::front::Freedom;
27use crate::front::LinesEqualLength;
28use crate::front::Parallel;
29use crate::front::Perpendicular;
30use crate::front::PointCtor;
31use crate::front::Tangent;
32use crate::frontend::api::Error;
33use crate::frontend::api::Expr;
34use crate::frontend::api::FileId;
35use crate::frontend::api::Number;
36use crate::frontend::api::ObjectId;
37use crate::frontend::api::ObjectKind;
38use crate::frontend::api::Plane;
39use crate::frontend::api::ProjectId;
40use crate::frontend::api::SceneGraph;
41use crate::frontend::api::SceneGraphDelta;
42use crate::frontend::api::SourceDelta;
43use crate::frontend::api::SourceRef;
44use crate::frontend::api::Version;
45use crate::frontend::modify::find_defined_names;
46use crate::frontend::modify::next_free_name;
47use crate::frontend::modify::next_free_name_with_padding;
48use crate::frontend::sketch::Coincident;
49use crate::frontend::sketch::Constraint;
50use crate::frontend::sketch::Diameter;
51use crate::frontend::sketch::ExistingSegmentCtor;
52use crate::frontend::sketch::Horizontal;
53use crate::frontend::sketch::LineCtor;
54use crate::frontend::sketch::Point2d;
55use crate::frontend::sketch::Radius;
56use crate::frontend::sketch::Segment;
57use crate::frontend::sketch::SegmentCtor;
58use crate::frontend::sketch::SketchApi;
59use crate::frontend::sketch::SketchCtor;
60use crate::frontend::sketch::Vertical;
61use crate::frontend::traverse::MutateBodyItem;
62use crate::frontend::traverse::TraversalReturn;
63use crate::frontend::traverse::Visitor;
64use crate::frontend::traverse::dfs_mut;
65use crate::parsing::ast::types as ast;
66use crate::pretty::NumericSuffix;
67use crate::std::constraints::LinesAtAngleKind;
68use crate::walk::NodeMut;
69use crate::walk::Visitable;
70
71pub(crate) mod api;
72pub(crate) mod modify;
73pub(crate) mod sketch;
74mod traverse;
75pub(crate) mod trim;
76
77struct ArcSizeConstraintParams {
78 points: Vec<ObjectId>,
79 function_name: &'static str,
80 value: f64,
81 units: NumericSuffix,
82 constraint_type_name: &'static str,
83}
84
85const POINT_FN: &str = "point";
86const POINT_AT_PARAM: &str = "at";
87const LINE_FN: &str = "line";
88const LINE_START_PARAM: &str = "start";
89const LINE_END_PARAM: &str = "end";
90const ARC_FN: &str = "arc";
91const ARC_START_PARAM: &str = "start";
92const ARC_END_PARAM: &str = "end";
93const ARC_CENTER_PARAM: &str = "center";
94const CIRCLE_FN: &str = "circle";
95const CIRCLE_START_PARAM: &str = "start";
96const CIRCLE_CENTER_PARAM: &str = "center";
97
98const COINCIDENT_FN: &str = "coincident";
99const DIAMETER_FN: &str = "diameter";
100const DISTANCE_FN: &str = "distance";
101const ANGLE_FN: &str = "angle";
102const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
103const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
104const EQUAL_LENGTH_FN: &str = "equalLength";
105const HORIZONTAL_FN: &str = "horizontal";
106const RADIUS_FN: &str = "radius";
107const TANGENT_FN: &str = "tangent";
108const VERTICAL_FN: &str = "vertical";
109
110const LINE_PROPERTY_START: &str = "start";
111const LINE_PROPERTY_END: &str = "end";
112
113const ARC_PROPERTY_START: &str = "start";
114const ARC_PROPERTY_END: &str = "end";
115const ARC_PROPERTY_CENTER: &str = "center";
116const CIRCLE_PROPERTY_START: &str = "start";
117const CIRCLE_PROPERTY_CENTER: &str = "center";
118
119const CONSTRUCTION_PARAM: &str = "construction";
120
121#[derive(Debug, Clone, Copy)]
122enum EditDeleteKind {
123 Edit,
124 DeleteNonSketch,
125}
126
127impl EditDeleteKind {
128 fn is_delete(&self) -> bool {
130 match self {
131 EditDeleteKind::Edit => false,
132 EditDeleteKind::DeleteNonSketch => true,
133 }
134 }
135
136 fn to_change_kind(self) -> ChangeKind {
137 match self {
138 EditDeleteKind::Edit => ChangeKind::Edit,
139 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
140 }
141 }
142}
143
144#[derive(Debug, Clone, Copy)]
145enum ChangeKind {
146 Add,
147 Edit,
148 Delete,
149 None,
150}
151
152#[derive(Debug, Clone, Serialize, ts_rs::TS)]
153#[ts(export, export_to = "FrontendApi.ts")]
154#[serde(tag = "type")]
155pub enum SetProgramOutcome {
156 #[serde(rename_all = "camelCase")]
157 Success {
158 scene_graph: Box<SceneGraph>,
159 exec_outcome: Box<ExecOutcome>,
160 },
161 #[serde(rename_all = "camelCase")]
162 ExecFailure { error: Box<KclErrorWithOutputs> },
163}
164
165#[derive(Debug, Clone)]
166pub struct FrontendState {
167 program: Program,
168 scene_graph: SceneGraph,
169 point_freedom_cache: HashMap<ObjectId, Freedom>,
172}
173
174impl Default for FrontendState {
175 fn default() -> Self {
176 Self::new()
177 }
178}
179
180impl FrontendState {
181 pub fn new() -> Self {
182 Self {
183 program: Program::empty(),
184 scene_graph: SceneGraph {
185 project: ProjectId(0),
186 file: FileId(0),
187 version: Version(0),
188 objects: Default::default(),
189 settings: Default::default(),
190 sketch_mode: Default::default(),
191 },
192 point_freedom_cache: HashMap::new(),
193 }
194 }
195
196 pub fn scene_graph(&self) -> &SceneGraph {
198 &self.scene_graph
199 }
200
201 pub fn default_length_unit(&self) -> UnitLength {
202 self.program
203 .meta_settings()
204 .ok()
205 .flatten()
206 .map(|settings| settings.default_length_units)
207 .unwrap_or(UnitLength::Millimeters)
208 }
209}
210
211impl SketchApi for FrontendState {
212 async fn execute_mock(
213 &mut self,
214 ctx: &ExecutorContext,
215 _version: Version,
216 sketch: ObjectId,
217 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
218 let mut truncated_program = self.program.clone();
219 self.only_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
220
221 let outcome = ctx
223 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
224 .await
225 .map_err(|err| Error {
226 msg: err.error.message().to_owned(),
227 })?;
228 let new_source = source_from_ast(&self.program.ast);
229 let src_delta = SourceDelta { text: new_source };
230 let outcome = self.update_state_after_exec(outcome, true);
232 let scene_graph_delta = SceneGraphDelta {
233 new_graph: self.scene_graph.clone(),
234 new_objects: Default::default(),
235 invalidates_ids: false,
236 exec_outcome: outcome,
237 };
238 Ok((src_delta, scene_graph_delta))
239 }
240
241 async fn new_sketch(
242 &mut self,
243 ctx: &ExecutorContext,
244 _project: ProjectId,
245 _file: FileId,
246 _version: Version,
247 args: SketchCtor,
248 ) -> api::Result<(SourceDelta, SceneGraphDelta, ObjectId)> {
249 let mut new_ast = self.program.ast.clone();
252 let plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on)?;
254 let sketch_ast = ast::SketchBlock {
255 arguments: vec![ast::LabeledArg {
256 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
257 arg: plane_ast,
258 }],
259 body: Default::default(),
260 is_being_edited: false,
261 non_code_meta: Default::default(),
262 digest: None,
263 };
264 new_ast.set_experimental_features(Some(WarningLevel::Allow));
267 let defined_names = find_defined_names(&new_ast);
270 let sketch_name =
271 next_free_name_with_padding("sketch", &defined_names).map_err(|err| Error { msg: err.to_string() })?;
272 let sketch_decl = ast::VariableDeclaration::new(
273 ast::VariableDeclarator::new(
274 &sketch_name,
275 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
276 ),
277 ast::ItemVisibility::Default,
278 ast::VariableKind::Const,
279 );
280 new_ast
281 .body
282 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
283 sketch_decl,
284 ))));
285 let new_source = source_from_ast(&new_ast);
287 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
289 if !errors.is_empty() {
290 return Err(Error {
291 msg: format!("Error parsing KCL source after adding sketch: {errors:?}"),
292 });
293 }
294 let Some(new_program) = new_program else {
295 return Err(Error {
296 msg: "No AST produced after adding sketch".to_owned(),
297 });
298 };
299
300 self.program = new_program.clone();
302
303 let outcome = ctx.run_with_caching(new_program.clone()).await.map_err(|err| Error {
306 msg: err.error.message().to_owned(),
307 })?;
308 let freedom_analysis_ran = true;
309
310 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
311
312 let Some(sketch_id) = self.scene_graph.objects.last().map(|object| object.id) else {
313 return Err(Error {
314 msg: "No objects in scene graph after adding sketch".to_owned(),
315 });
316 };
317 self.scene_graph.sketch_mode = Some(sketch_id);
319
320 let src_delta = SourceDelta { text: new_source };
321 let scene_graph_delta = SceneGraphDelta {
322 new_graph: self.scene_graph.clone(),
323 invalidates_ids: false,
324 new_objects: vec![sketch_id],
325 exec_outcome: outcome,
326 };
327 Ok((src_delta, scene_graph_delta, sketch_id))
328 }
329
330 async fn edit_sketch(
331 &mut self,
332 ctx: &ExecutorContext,
333 _project: ProjectId,
334 _file: FileId,
335 _version: Version,
336 sketch: ObjectId,
337 ) -> api::Result<SceneGraphDelta> {
338 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| Error {
342 msg: format!("Sketch not found: {sketch:?}"),
343 })?;
344 let ObjectKind::Sketch(_) = &sketch_object.kind else {
345 return Err(Error {
346 msg: format!("Object is not a sketch: {sketch_object:?}"),
347 });
348 };
349
350 self.scene_graph.sketch_mode = Some(sketch);
352
353 let mut truncated_program = self.program.clone();
355 self.only_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
356
357 let outcome = ctx
360 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
361 .await
362 .map_err(|err| {
363 Error {
366 msg: err.error.message().to_owned(),
367 }
368 })?;
369
370 let outcome = self.update_state_after_exec(outcome, true);
372 let scene_graph_delta = SceneGraphDelta {
373 new_graph: self.scene_graph.clone(),
374 invalidates_ids: false,
375 new_objects: Vec::new(),
376 exec_outcome: outcome,
377 };
378 Ok(scene_graph_delta)
379 }
380
381 async fn exit_sketch(
382 &mut self,
383 ctx: &ExecutorContext,
384 _version: Version,
385 sketch: ObjectId,
386 ) -> api::Result<SceneGraph> {
387 #[cfg(not(target_arch = "wasm32"))]
389 let _ = sketch;
390 #[cfg(target_arch = "wasm32")]
391 if self.scene_graph.sketch_mode != Some(sketch) {
392 web_sys::console::warn_1(
393 &format!(
394 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
395 &self.scene_graph.sketch_mode
396 )
397 .into(),
398 );
399 }
400 self.scene_graph.sketch_mode = None;
401
402 let outcome = ctx.run_with_caching(self.program.clone()).await.map_err(|err| {
404 Error {
407 msg: err.error.message().to_owned(),
408 }
409 })?;
410
411 self.update_state_after_exec(outcome, false);
413
414 Ok(self.scene_graph.clone())
415 }
416
417 async fn delete_sketch(
418 &mut self,
419 ctx: &ExecutorContext,
420 _version: Version,
421 sketch: ObjectId,
422 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
423 let mut new_ast = self.program.ast.clone();
426
427 let sketch_id = sketch;
429 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
430 msg: format!("Sketch not found: {sketch:?}"),
431 })?;
432 let ObjectKind::Sketch(_) = &sketch_object.kind else {
433 return Err(Error {
434 msg: format!("Object is not a sketch: {sketch_object:?}"),
435 });
436 };
437
438 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)?;
440
441 self.execute_after_delete_sketch(ctx, &mut new_ast).await
442 }
443
444 async fn add_segment(
445 &mut self,
446 ctx: &ExecutorContext,
447 _version: Version,
448 sketch: ObjectId,
449 segment: SegmentCtor,
450 _label: Option<String>,
451 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
452 match segment {
454 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
455 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
456 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
457 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
458 }
459 }
460
461 async fn edit_segments(
462 &mut self,
463 ctx: &ExecutorContext,
464 _version: Version,
465 sketch: ObjectId,
466 segments: Vec<ExistingSegmentCtor>,
467 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
468 let mut new_ast = self.program.ast.clone();
470 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
471
472 for segment in &segments {
475 segment_ids_edited.insert(segment.id);
476 }
477
478 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
493
494 for segment in segments {
495 let segment_id = segment.id;
496 match segment.ctor {
497 SegmentCtor::Point(ctor) => {
498 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
500 && let ObjectKind::Segment { segment } = &segment_object.kind
501 && let Segment::Point(point) = segment
502 && let Some(owner_id) = point.owner
503 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
504 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
505 {
506 match owner_segment {
507 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
508 if let Some(existing) = final_edits.get_mut(&owner_id) {
509 let SegmentCtor::Line(line_ctor) = existing else {
510 return Err(Error {
511 msg: format!("Internal: Expected line ctor for owner: {owner_object:?}"),
512 });
513 };
514 if line.start == segment_id {
516 line_ctor.start = ctor.position;
517 } else {
518 line_ctor.end = ctor.position;
519 }
520 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
521 let mut line_ctor = line_ctor.clone();
523 if line.start == segment_id {
524 line_ctor.start = ctor.position;
525 } else {
526 line_ctor.end = ctor.position;
527 }
528 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
529 } else {
530 return Err(Error {
532 msg: format!("Internal: Line does not have line ctor: {owner_object:?}"),
533 });
534 }
535 continue;
536 }
537 Segment::Arc(arc)
538 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
539 {
540 if let Some(existing) = final_edits.get_mut(&owner_id) {
541 let SegmentCtor::Arc(arc_ctor) = existing else {
542 return Err(Error {
543 msg: format!("Internal: Expected arc ctor for owner: {owner_object:?}"),
544 });
545 };
546 if arc.start == segment_id {
547 arc_ctor.start = ctor.position;
548 } else if arc.end == segment_id {
549 arc_ctor.end = ctor.position;
550 } else {
551 arc_ctor.center = ctor.position;
552 }
553 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
554 let mut arc_ctor = arc_ctor.clone();
555 if arc.start == segment_id {
556 arc_ctor.start = ctor.position;
557 } else if arc.end == segment_id {
558 arc_ctor.end = ctor.position;
559 } else {
560 arc_ctor.center = ctor.position;
561 }
562 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
563 } else {
564 return Err(Error {
565 msg: format!("Internal: Arc does not have arc ctor: {owner_object:?}"),
566 });
567 }
568 continue;
569 }
570 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
571 if let Some(existing) = final_edits.get_mut(&owner_id) {
572 let SegmentCtor::Circle(circle_ctor) = existing else {
573 return Err(Error {
574 msg: format!("Internal: Expected circle ctor for owner: {owner_object:?}"),
575 });
576 };
577 if circle.start == segment_id {
578 circle_ctor.start = ctor.position;
579 } else {
580 circle_ctor.center = ctor.position;
581 }
582 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
583 let mut circle_ctor = circle_ctor.clone();
584 if circle.start == segment_id {
585 circle_ctor.start = ctor.position;
586 } else {
587 circle_ctor.center = ctor.position;
588 }
589 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
590 } else {
591 return Err(Error {
592 msg: format!("Internal: Circle does not have circle ctor: {owner_object:?}"),
593 });
594 }
595 continue;
596 }
597 _ => {}
598 }
599 }
600
601 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
603 }
604 SegmentCtor::Line(ctor) => {
605 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
606 }
607 SegmentCtor::Arc(ctor) => {
608 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
609 }
610 SegmentCtor::Circle(ctor) => {
611 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
612 }
613 }
614 }
615
616 for (segment_id, ctor) in final_edits {
617 match ctor {
618 SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment_id, ctor)?,
619 SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment_id, ctor)?,
620 SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment_id, ctor)?,
621 SegmentCtor::Circle(ctor) => self.edit_circle(&mut new_ast, sketch, segment_id, ctor)?,
622 }
623 }
624 self.execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
625 .await
626 }
627
628 async fn delete_objects(
629 &mut self,
630 ctx: &ExecutorContext,
631 _version: Version,
632 sketch: ObjectId,
633 constraint_ids: Vec<ObjectId>,
634 segment_ids: Vec<ObjectId>,
635 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
636 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
640 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
641
642 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
645
646 for segment_id in segment_ids_set.iter().copied() {
647 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
648 && let ObjectKind::Segment { segment } = &segment_object.kind
649 && let Segment::Point(point) = segment
650 && let Some(owner_id) = point.owner
651 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
652 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
653 && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
654 {
655 resolved_segment_ids_to_delete.insert(owner_id);
657 } else {
658 resolved_segment_ids_to_delete.insert(segment_id);
660 }
661 }
662 let referenced_constraint_ids = self.find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)?;
663
664 let mut new_ast = self.program.ast.clone();
665
666 for constraint_id in referenced_constraint_ids {
667 if constraint_ids_set.contains(&constraint_id) {
668 continue;
669 }
670
671 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
672 msg: format!("Constraint not found: {constraint_id:?}"),
673 })?;
674 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
675 return Err(Error {
676 msg: format!("Object is not a constraint: {constraint_object:?}"),
677 });
678 };
679
680 match constraint {
681 Constraint::LinesEqualLength(lines_equal_length) => {
682 let remaining_lines = lines_equal_length
683 .lines
684 .iter()
685 .copied()
686 .filter(|line_id| !resolved_segment_ids_to_delete.contains(line_id))
687 .collect::<Vec<_>>();
688
689 if remaining_lines.len() >= 2 {
691 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)?;
692 } else {
693 constraint_ids_set.insert(constraint_id);
694 }
695 }
696 _ => {
697 constraint_ids_set.insert(constraint_id);
699 }
700 }
701 }
702
703 for constraint_id in constraint_ids_set {
704 self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
705 }
706 for segment_id in resolved_segment_ids_to_delete {
707 self.delete_segment(&mut new_ast, sketch, segment_id)?;
708 }
709
710 self.execute_after_edit(
711 ctx,
712 sketch,
713 Default::default(),
714 EditDeleteKind::DeleteNonSketch,
715 &mut new_ast,
716 )
717 .await
718 }
719
720 async fn add_constraint(
721 &mut self,
722 ctx: &ExecutorContext,
723 _version: Version,
724 sketch: ObjectId,
725 constraint: Constraint,
726 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
727 let original_program = self.program.clone();
731 let original_scene_graph = self.scene_graph.clone();
732
733 let mut new_ast = self.program.ast.clone();
734 let sketch_block_range = match constraint {
735 Constraint::Coincident(coincident) => self.add_coincident(sketch, coincident, &mut new_ast).await?,
736 Constraint::Distance(distance) => self.add_distance(sketch, distance, &mut new_ast).await?,
737 Constraint::HorizontalDistance(distance) => {
738 self.add_horizontal_distance(sketch, distance, &mut new_ast).await?
739 }
740 Constraint::VerticalDistance(distance) => {
741 self.add_vertical_distance(sketch, distance, &mut new_ast).await?
742 }
743 Constraint::Horizontal(horizontal) => self.add_horizontal(sketch, horizontal, &mut new_ast).await?,
744 Constraint::LinesEqualLength(lines_equal_length) => {
745 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
746 .await?
747 }
748 Constraint::Parallel(parallel) => self.add_parallel(sketch, parallel, &mut new_ast).await?,
749 Constraint::Perpendicular(perpendicular) => {
750 self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?
751 }
752 Constraint::Radius(radius) => self.add_radius(sketch, radius, &mut new_ast).await?,
753 Constraint::Diameter(diameter) => self.add_diameter(sketch, diameter, &mut new_ast).await?,
754 Constraint::Vertical(vertical) => self.add_vertical(sketch, vertical, &mut new_ast).await?,
755 Constraint::Angle(lines_at_angle) => self.add_angle(sketch, lines_at_angle, &mut new_ast).await?,
756 Constraint::Tangent(tangent) => self.add_tangent(sketch, tangent, &mut new_ast).await?,
757 };
758
759 let result = self
760 .execute_after_add_constraint(ctx, sketch, sketch_block_range, &mut new_ast)
761 .await;
762
763 if result.is_err() {
765 self.program = original_program;
766 self.scene_graph = original_scene_graph;
767 }
768
769 result
770 }
771
772 async fn chain_segment(
773 &mut self,
774 ctx: &ExecutorContext,
775 version: Version,
776 sketch: ObjectId,
777 previous_segment_end_point_id: ObjectId,
778 segment: SegmentCtor,
779 _label: Option<String>,
780 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
781 let SegmentCtor::Line(line_ctor) = segment else {
785 return Err(Error {
786 msg: format!("chain_segment currently only supports Line segments, got: {segment:?}"),
787 });
788 };
789
790 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
792
793 let new_line_id = first_scene_delta
796 .new_objects
797 .iter()
798 .find(|&obj_id| {
799 let obj = self.scene_graph.objects.get(obj_id.0);
800 if let Some(obj) = obj {
801 matches!(
802 &obj.kind,
803 ObjectKind::Segment {
804 segment: Segment::Line(_)
805 }
806 )
807 } else {
808 false
809 }
810 })
811 .ok_or_else(|| Error {
812 msg: "Failed to find new line segment in scene graph".to_string(),
813 })?;
814
815 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| Error {
816 msg: format!("New line object not found: {new_line_id:?}"),
817 })?;
818
819 let ObjectKind::Segment {
820 segment: new_line_segment,
821 } = &new_line_obj.kind
822 else {
823 return Err(Error {
824 msg: format!("Object is not a segment: {new_line_obj:?}"),
825 });
826 };
827
828 let Segment::Line(new_line) = new_line_segment else {
829 return Err(Error {
830 msg: format!("Segment is not a line: {new_line_segment:?}"),
831 });
832 };
833
834 let new_line_start_point_id = new_line.start;
835
836 let coincident = Coincident {
838 segments: vec![previous_segment_end_point_id, new_line_start_point_id],
839 };
840
841 let (final_src_delta, final_scene_delta) = self
842 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
843 .await?;
844
845 let mut combined_new_objects = first_scene_delta.new_objects.clone();
848 combined_new_objects.extend(final_scene_delta.new_objects);
849
850 let scene_graph_delta = SceneGraphDelta {
851 new_graph: self.scene_graph.clone(),
852 invalidates_ids: false,
853 new_objects: combined_new_objects,
854 exec_outcome: final_scene_delta.exec_outcome,
855 };
856
857 Ok((final_src_delta, scene_graph_delta))
858 }
859
860 async fn edit_constraint(
861 &mut self,
862 ctx: &ExecutorContext,
863 _version: Version,
864 sketch: ObjectId,
865 constraint_id: ObjectId,
866 value_expression: String,
867 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
868 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
870 msg: format!("Object not found: {constraint_id:?}"),
871 })?;
872 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
873 return Err(Error {
874 msg: format!("Object is not a constraint: {constraint_id:?}"),
875 });
876 }
877
878 let mut new_ast = self.program.ast.clone();
879
880 let (parsed, errors) = Program::parse(&value_expression).map_err(|e| Error { msg: e.to_string() })?;
882 if !errors.is_empty() {
883 return Err(Error {
884 msg: format!("Error parsing value expression: {errors:?}"),
885 });
886 }
887 let mut parsed = parsed.ok_or_else(|| Error {
888 msg: "No AST produced from value expression".to_string(),
889 })?;
890 if parsed.ast.body.is_empty() {
891 return Err(Error {
892 msg: "Empty value expression".to_string(),
893 });
894 }
895 let first = parsed.ast.body.remove(0);
896 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
897 return Err(Error {
898 msg: "Value expression must be a simple expression".to_string(),
899 });
900 };
901
902 let new_value: ast::BinaryPart = expr_stmt
903 .inner
904 .expression
905 .try_into()
906 .map_err(|e: String| Error { msg: e })?;
907
908 self.mutate_ast(
909 &mut new_ast,
910 constraint_id,
911 AstMutateCommand::EditConstraintValue { value: new_value },
912 )?;
913
914 self.execute_after_edit(ctx, sketch, Default::default(), EditDeleteKind::Edit, &mut new_ast)
915 .await
916 }
917
918 async fn batch_split_segment_operations(
926 &mut self,
927 ctx: &ExecutorContext,
928 _version: Version,
929 sketch: ObjectId,
930 edit_segments: Vec<ExistingSegmentCtor>,
931 add_constraints: Vec<Constraint>,
932 delete_constraint_ids: Vec<ObjectId>,
933 _new_segment_info: sketch::NewSegmentInfo,
934 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
935 let mut new_ast = self.program.ast.clone();
937 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
938
939 for segment in edit_segments {
941 segment_ids_edited.insert(segment.id);
942 match segment.ctor {
943 SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
944 SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
945 SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment.id, ctor)?,
946 SegmentCtor::Circle(ctor) => self.edit_circle(&mut new_ast, sketch, segment.id, ctor)?,
947 }
948 }
949
950 for constraint in add_constraints {
952 match constraint {
953 Constraint::Coincident(coincident) => {
954 self.add_coincident(sketch, coincident, &mut new_ast).await?;
955 }
956 Constraint::Distance(distance) => {
957 self.add_distance(sketch, distance, &mut new_ast).await?;
958 }
959 Constraint::HorizontalDistance(distance) => {
960 self.add_horizontal_distance(sketch, distance, &mut new_ast).await?;
961 }
962 Constraint::VerticalDistance(distance) => {
963 self.add_vertical_distance(sketch, distance, &mut new_ast).await?;
964 }
965 Constraint::Horizontal(horizontal) => {
966 self.add_horizontal(sketch, horizontal, &mut new_ast).await?;
967 }
968 Constraint::LinesEqualLength(lines_equal_length) => {
969 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
970 .await?;
971 }
972 Constraint::Parallel(parallel) => {
973 self.add_parallel(sketch, parallel, &mut new_ast).await?;
974 }
975 Constraint::Perpendicular(perpendicular) => {
976 self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?;
977 }
978 Constraint::Vertical(vertical) => {
979 self.add_vertical(sketch, vertical, &mut new_ast).await?;
980 }
981 Constraint::Diameter(diameter) => {
982 self.add_diameter(sketch, diameter, &mut new_ast).await?;
983 }
984 Constraint::Radius(radius) => {
985 self.add_radius(sketch, radius, &mut new_ast).await?;
986 }
987 Constraint::Angle(angle) => {
988 self.add_angle(sketch, angle, &mut new_ast).await?;
989 }
990 Constraint::Tangent(tangent) => {
991 self.add_tangent(sketch, tangent, &mut new_ast).await?;
992 }
993 }
994 }
995
996 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
998
999 let has_constraint_deletions = !constraint_ids_set.is_empty();
1000 for constraint_id in constraint_ids_set {
1001 self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
1002 }
1003
1004 let (source_delta, mut scene_graph_delta) = self
1008 .execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
1009 .await?;
1010
1011 if has_constraint_deletions {
1014 scene_graph_delta.invalidates_ids = true;
1015 }
1016
1017 Ok((source_delta, scene_graph_delta))
1018 }
1019
1020 async fn batch_tail_cut_operations(
1021 &mut self,
1022 ctx: &ExecutorContext,
1023 _version: Version,
1024 sketch: ObjectId,
1025 edit_segments: Vec<ExistingSegmentCtor>,
1026 add_constraints: Vec<Constraint>,
1027 delete_constraint_ids: Vec<ObjectId>,
1028 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1029 let mut new_ast = self.program.ast.clone();
1030 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1031
1032 for segment in edit_segments {
1034 segment_ids_edited.insert(segment.id);
1035 match segment.ctor {
1036 SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
1037 SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
1038 SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment.id, ctor)?,
1039 SegmentCtor::Circle(ctor) => self.edit_circle(&mut new_ast, sketch, segment.id, ctor)?,
1040 }
1041 }
1042
1043 for constraint in add_constraints {
1045 match constraint {
1046 Constraint::Coincident(coincident) => {
1047 self.add_coincident(sketch, coincident, &mut new_ast).await?;
1048 }
1049 other => {
1050 return Err(Error {
1051 msg: format!("unsupported constraint in tail cut batch: {other:?}"),
1052 });
1053 }
1054 }
1055 }
1056
1057 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1059
1060 let has_constraint_deletions = !constraint_ids_set.is_empty();
1061 for constraint_id in constraint_ids_set {
1062 self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
1063 }
1064
1065 let (source_delta, mut scene_graph_delta) = self
1069 .execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
1070 .await?;
1071
1072 if has_constraint_deletions {
1075 scene_graph_delta.invalidates_ids = true;
1076 }
1077
1078 Ok((source_delta, scene_graph_delta))
1079 }
1080}
1081
1082impl FrontendState {
1083 pub async fn hack_set_program(
1084 &mut self,
1085 ctx: &ExecutorContext,
1086 program: Program,
1087 ) -> api::Result<SetProgramOutcome> {
1088 self.program = program.clone();
1089
1090 self.point_freedom_cache.clear();
1097 match ctx.run_with_caching(program).await {
1098 Ok(outcome) => {
1099 let outcome = self.update_state_after_exec(outcome, true);
1100 Ok(SetProgramOutcome::Success {
1101 scene_graph: Box::new(self.scene_graph.clone()),
1102 exec_outcome: Box::new(outcome),
1103 })
1104 }
1105 Err(mut err) => {
1106 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1109 self.update_state_after_exec(outcome, true);
1110 err.scene_graph = Some(self.scene_graph.clone());
1111 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1112 }
1113 }
1114 }
1115
1116 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> api::Result<ExecOutcome> {
1117 if matches!(err.error, KclError::EngineHangup { .. }) {
1118 return Err(Error {
1122 msg: err.error.message().to_owned(),
1123 });
1124 }
1125
1126 let KclErrorWithOutputs {
1127 error,
1128 mut non_fatal,
1129 variables,
1130 #[cfg(feature = "artifact-graph")]
1131 operations,
1132 #[cfg(feature = "artifact-graph")]
1133 artifact_graph,
1134 #[cfg(feature = "artifact-graph")]
1135 scene_objects,
1136 #[cfg(feature = "artifact-graph")]
1137 source_range_to_object,
1138 #[cfg(feature = "artifact-graph")]
1139 var_solutions,
1140 filenames,
1141 default_planes,
1142 ..
1143 } = err;
1144
1145 if let Some(source_range) = error.source_ranges().first() {
1146 non_fatal.push(CompilationError::fatal(*source_range, error.get_message()));
1147 } else {
1148 non_fatal.push(CompilationError::fatal(SourceRange::synthetic(), error.get_message()));
1149 }
1150
1151 Ok(ExecOutcome {
1152 variables,
1153 filenames,
1154 #[cfg(feature = "artifact-graph")]
1155 operations,
1156 #[cfg(feature = "artifact-graph")]
1157 artifact_graph,
1158 #[cfg(feature = "artifact-graph")]
1159 scene_objects,
1160 #[cfg(feature = "artifact-graph")]
1161 source_range_to_object,
1162 #[cfg(feature = "artifact-graph")]
1163 var_solutions,
1164 errors: non_fatal,
1165 default_planes,
1166 })
1167 }
1168
1169 async fn add_point(
1170 &mut self,
1171 ctx: &ExecutorContext,
1172 sketch: ObjectId,
1173 ctor: PointCtor,
1174 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1175 let at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
1177 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1178 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1179 unlabeled: None,
1180 arguments: vec![ast::LabeledArg {
1181 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1182 arg: at_ast,
1183 }],
1184 digest: None,
1185 non_code_meta: Default::default(),
1186 })));
1187
1188 let sketch_id = sketch;
1190 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1191 #[cfg(target_arch = "wasm32")]
1192 web_sys::console::error_1(
1193 &format!(
1194 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1195 &self.scene_graph.objects
1196 )
1197 .into(),
1198 );
1199 Error {
1200 msg: format!("Sketch not found: {sketch:?}"),
1201 }
1202 })?;
1203 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1204 return Err(Error {
1205 msg: format!("Object is not a sketch: {sketch_object:?}"),
1206 });
1207 };
1208 let mut new_ast = self.program.ast.clone();
1210 let (sketch_block_range, _) = self.mutate_ast(
1211 &mut new_ast,
1212 sketch_id,
1213 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1214 )?;
1215 let new_source = source_from_ast(&new_ast);
1217 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1219 if !errors.is_empty() {
1220 return Err(Error {
1221 msg: format!("Error parsing KCL source after adding point: {errors:?}"),
1222 });
1223 }
1224 let Some(new_program) = new_program else {
1225 return Err(Error {
1226 msg: "No AST produced after adding point".to_string(),
1227 });
1228 };
1229
1230 let point_source_range =
1231 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1232 msg: format!("Source range of point not found in sketch block: {sketch_block_range:?}; {err:?}"),
1233 })?;
1234 #[cfg(not(feature = "artifact-graph"))]
1235 let _ = point_source_range;
1236
1237 self.program = new_program.clone();
1239
1240 let mut truncated_program = new_program;
1242 self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1243
1244 let outcome = ctx
1246 .run_mock(
1247 &truncated_program,
1248 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1249 )
1250 .await
1251 .map_err(|err| {
1252 Error {
1255 msg: err.error.message().to_owned(),
1256 }
1257 })?;
1258
1259 #[cfg(not(feature = "artifact-graph"))]
1260 let new_object_ids = Vec::new();
1261 #[cfg(feature = "artifact-graph")]
1262 let new_object_ids = {
1263 let segment_id = outcome
1264 .source_range_to_object
1265 .get(&point_source_range)
1266 .copied()
1267 .ok_or_else(|| Error {
1268 msg: format!("Source range of point not found: {point_source_range:?}"),
1269 })?;
1270 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
1271 msg: format!("Segment not found: {segment_id:?}"),
1272 })?;
1273 let ObjectKind::Segment { segment } = &segment_object.kind else {
1274 return Err(Error {
1275 msg: format!("Object is not a segment: {segment_object:?}"),
1276 });
1277 };
1278 let Segment::Point(_) = segment else {
1279 return Err(Error {
1280 msg: format!("Segment is not a point: {segment:?}"),
1281 });
1282 };
1283 vec![segment_id]
1284 };
1285 let src_delta = SourceDelta { text: new_source };
1286 let outcome = self.update_state_after_exec(outcome, false);
1288 let scene_graph_delta = SceneGraphDelta {
1289 new_graph: self.scene_graph.clone(),
1290 invalidates_ids: false,
1291 new_objects: new_object_ids,
1292 exec_outcome: outcome,
1293 };
1294 Ok((src_delta, scene_graph_delta))
1295 }
1296
1297 async fn add_line(
1298 &mut self,
1299 ctx: &ExecutorContext,
1300 sketch: ObjectId,
1301 ctor: LineCtor,
1302 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1303 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1305 let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1306 let mut arguments = vec![
1307 ast::LabeledArg {
1308 label: Some(ast::Identifier::new(LINE_START_PARAM)),
1309 arg: start_ast,
1310 },
1311 ast::LabeledArg {
1312 label: Some(ast::Identifier::new(LINE_END_PARAM)),
1313 arg: end_ast,
1314 },
1315 ];
1316 if ctor.construction == Some(true) {
1318 arguments.push(ast::LabeledArg {
1319 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1320 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1321 value: ast::LiteralValue::Bool(true),
1322 raw: "true".to_string(),
1323 digest: None,
1324 }))),
1325 });
1326 }
1327 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1328 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1329 unlabeled: None,
1330 arguments,
1331 digest: None,
1332 non_code_meta: Default::default(),
1333 })));
1334
1335 let sketch_id = sketch;
1337 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1338 msg: format!("Sketch not found: {sketch:?}"),
1339 })?;
1340 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1341 return Err(Error {
1342 msg: format!("Object is not a sketch: {sketch_object:?}"),
1343 });
1344 };
1345 let mut new_ast = self.program.ast.clone();
1347 let (sketch_block_range, _) = self.mutate_ast(
1348 &mut new_ast,
1349 sketch_id,
1350 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1351 )?;
1352 let new_source = source_from_ast(&new_ast);
1354 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1356 if !errors.is_empty() {
1357 return Err(Error {
1358 msg: format!("Error parsing KCL source after adding line: {errors:?}"),
1359 });
1360 }
1361 let Some(new_program) = new_program else {
1362 return Err(Error {
1363 msg: "No AST produced after adding line".to_string(),
1364 });
1365 };
1366 let line_source_range =
1367 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1368 msg: format!("Source range of line not found in sketch block: {sketch_block_range:?}; {err:?}"),
1369 })?;
1370 #[cfg(not(feature = "artifact-graph"))]
1371 let _ = line_source_range;
1372
1373 self.program = new_program.clone();
1375
1376 let mut truncated_program = new_program;
1378 self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1379
1380 let outcome = ctx
1382 .run_mock(
1383 &truncated_program,
1384 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1385 )
1386 .await
1387 .map_err(|err| {
1388 Error {
1391 msg: err.error.message().to_owned(),
1392 }
1393 })?;
1394
1395 #[cfg(not(feature = "artifact-graph"))]
1396 let new_object_ids = Vec::new();
1397 #[cfg(feature = "artifact-graph")]
1398 let new_object_ids = {
1399 let segment_id = outcome
1400 .source_range_to_object
1401 .get(&line_source_range)
1402 .copied()
1403 .ok_or_else(|| Error {
1404 msg: format!("Source range of line not found: {line_source_range:?}"),
1405 })?;
1406 let segment_object = outcome.scene_object_by_id(segment_id).ok_or_else(|| Error {
1407 msg: format!("Segment not found: {segment_id:?}"),
1408 })?;
1409 let ObjectKind::Segment { segment } = &segment_object.kind else {
1410 return Err(Error {
1411 msg: format!("Object is not a segment: {segment_object:?}"),
1412 });
1413 };
1414 let Segment::Line(line) = segment else {
1415 return Err(Error {
1416 msg: format!("Segment is not a line: {segment:?}"),
1417 });
1418 };
1419 vec![line.start, line.end, segment_id]
1420 };
1421 let src_delta = SourceDelta { text: new_source };
1422 let outcome = self.update_state_after_exec(outcome, false);
1424 let scene_graph_delta = SceneGraphDelta {
1425 new_graph: self.scene_graph.clone(),
1426 invalidates_ids: false,
1427 new_objects: new_object_ids,
1428 exec_outcome: outcome,
1429 };
1430 Ok((src_delta, scene_graph_delta))
1431 }
1432
1433 async fn add_arc(
1434 &mut self,
1435 ctx: &ExecutorContext,
1436 sketch: ObjectId,
1437 ctor: ArcCtor,
1438 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1439 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1441 let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1442 let center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1443 let mut arguments = vec![
1444 ast::LabeledArg {
1445 label: Some(ast::Identifier::new(ARC_START_PARAM)),
1446 arg: start_ast,
1447 },
1448 ast::LabeledArg {
1449 label: Some(ast::Identifier::new(ARC_END_PARAM)),
1450 arg: end_ast,
1451 },
1452 ast::LabeledArg {
1453 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1454 arg: center_ast,
1455 },
1456 ];
1457 if ctor.construction == Some(true) {
1459 arguments.push(ast::LabeledArg {
1460 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1461 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1462 value: ast::LiteralValue::Bool(true),
1463 raw: "true".to_string(),
1464 digest: None,
1465 }))),
1466 });
1467 }
1468 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1469 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1470 unlabeled: None,
1471 arguments,
1472 digest: None,
1473 non_code_meta: Default::default(),
1474 })));
1475
1476 let sketch_id = sketch;
1478 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1479 msg: format!("Sketch not found: {sketch:?}"),
1480 })?;
1481 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1482 return Err(Error {
1483 msg: format!("Object is not a sketch: {sketch_object:?}"),
1484 });
1485 };
1486 let mut new_ast = self.program.ast.clone();
1488 let (sketch_block_range, _) = self.mutate_ast(
1489 &mut new_ast,
1490 sketch_id,
1491 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1492 )?;
1493 let new_source = source_from_ast(&new_ast);
1495 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1497 if !errors.is_empty() {
1498 return Err(Error {
1499 msg: format!("Error parsing KCL source after adding arc: {errors:?}"),
1500 });
1501 }
1502 let Some(new_program) = new_program else {
1503 return Err(Error {
1504 msg: "No AST produced after adding arc".to_string(),
1505 });
1506 };
1507 let arc_source_range =
1508 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1509 msg: format!("Source range of arc not found in sketch block: {sketch_block_range:?}; {err:?}"),
1510 })?;
1511 #[cfg(not(feature = "artifact-graph"))]
1512 let _ = arc_source_range;
1513
1514 self.program = new_program.clone();
1516
1517 let mut truncated_program = new_program;
1519 self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1520
1521 let outcome = ctx
1523 .run_mock(
1524 &truncated_program,
1525 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1526 )
1527 .await
1528 .map_err(|err| {
1529 Error {
1532 msg: err.error.message().to_owned(),
1533 }
1534 })?;
1535
1536 #[cfg(not(feature = "artifact-graph"))]
1537 let new_object_ids = Vec::new();
1538 #[cfg(feature = "artifact-graph")]
1539 let new_object_ids = {
1540 let segment_id = outcome
1541 .source_range_to_object
1542 .get(&arc_source_range)
1543 .copied()
1544 .ok_or_else(|| Error {
1545 msg: format!("Source range of arc not found: {arc_source_range:?}"),
1546 })?;
1547 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
1548 msg: format!("Segment not found: {segment_id:?}"),
1549 })?;
1550 let ObjectKind::Segment { segment } = &segment_object.kind else {
1551 return Err(Error {
1552 msg: format!("Object is not a segment: {segment_object:?}"),
1553 });
1554 };
1555 let Segment::Arc(arc) = segment else {
1556 return Err(Error {
1557 msg: format!("Segment is not an arc: {segment:?}"),
1558 });
1559 };
1560 vec![arc.start, arc.end, arc.center, segment_id]
1561 };
1562 let src_delta = SourceDelta { text: new_source };
1563 let outcome = self.update_state_after_exec(outcome, false);
1565 let scene_graph_delta = SceneGraphDelta {
1566 new_graph: self.scene_graph.clone(),
1567 invalidates_ids: false,
1568 new_objects: new_object_ids,
1569 exec_outcome: outcome,
1570 };
1571 Ok((src_delta, scene_graph_delta))
1572 }
1573
1574 async fn add_circle(
1575 &mut self,
1576 ctx: &ExecutorContext,
1577 sketch: ObjectId,
1578 ctor: CircleCtor,
1579 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1580 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1582 let center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1583 let mut arguments = vec![
1584 ast::LabeledArg {
1585 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
1586 arg: start_ast,
1587 },
1588 ast::LabeledArg {
1589 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
1590 arg: center_ast,
1591 },
1592 ];
1593 if ctor.construction == Some(true) {
1595 arguments.push(ast::LabeledArg {
1596 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1597 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1598 value: ast::LiteralValue::Bool(true),
1599 raw: "true".to_string(),
1600 digest: None,
1601 }))),
1602 });
1603 }
1604 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1605 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
1606 unlabeled: None,
1607 arguments,
1608 digest: None,
1609 non_code_meta: Default::default(),
1610 })));
1611
1612 let sketch_id = sketch;
1614 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1615 msg: format!("Sketch not found: {sketch:?}"),
1616 })?;
1617 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1618 return Err(Error {
1619 msg: format!("Object is not a sketch: {sketch_object:?}"),
1620 });
1621 };
1622 let mut new_ast = self.program.ast.clone();
1624 let (sketch_block_range, _) = self.mutate_ast(
1625 &mut new_ast,
1626 sketch_id,
1627 AstMutateCommand::AddSketchBlockExprStmt { expr: circle_ast },
1628 )?;
1629 let new_source = source_from_ast(&new_ast);
1631 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1633 if !errors.is_empty() {
1634 return Err(Error {
1635 msg: format!("Error parsing KCL source after adding circle: {errors:?}"),
1636 });
1637 }
1638 let Some(new_program) = new_program else {
1639 return Err(Error {
1640 msg: "No AST produced after adding circle".to_string(),
1641 });
1642 };
1643 let circle_source_range =
1644 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1645 msg: format!("Source range of circle not found in sketch block: {sketch_block_range:?}; {err:?}"),
1646 })?;
1647 #[cfg(not(feature = "artifact-graph"))]
1648 let _ = circle_source_range;
1649
1650 self.program = new_program.clone();
1652
1653 let mut truncated_program = new_program;
1655 self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1656
1657 let outcome = ctx
1659 .run_mock(
1660 &truncated_program,
1661 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1662 )
1663 .await
1664 .map_err(|err| Error {
1665 msg: err.error.message().to_owned(),
1666 })?;
1667
1668 #[cfg(not(feature = "artifact-graph"))]
1669 let new_object_ids = Vec::new();
1670 #[cfg(feature = "artifact-graph")]
1671 let new_object_ids = {
1672 let segment_id = outcome
1673 .source_range_to_object
1674 .get(&circle_source_range)
1675 .copied()
1676 .ok_or_else(|| Error {
1677 msg: format!("Source range of circle not found: {circle_source_range:?}"),
1678 })?;
1679 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
1680 msg: format!("Segment not found: {segment_id:?}"),
1681 })?;
1682 let ObjectKind::Segment { segment } = &segment_object.kind else {
1683 return Err(Error {
1684 msg: format!("Object is not a segment: {segment_object:?}"),
1685 });
1686 };
1687 let Segment::Circle(circle) = segment else {
1688 return Err(Error {
1689 msg: format!("Segment is not a circle: {segment:?}"),
1690 });
1691 };
1692 vec![circle.start, circle.center, segment_id]
1693 };
1694 let src_delta = SourceDelta { text: new_source };
1695 let outcome = self.update_state_after_exec(outcome, false);
1697 let scene_graph_delta = SceneGraphDelta {
1698 new_graph: self.scene_graph.clone(),
1699 invalidates_ids: false,
1700 new_objects: new_object_ids,
1701 exec_outcome: outcome,
1702 };
1703 Ok((src_delta, scene_graph_delta))
1704 }
1705
1706 fn edit_point(
1707 &mut self,
1708 new_ast: &mut ast::Node<ast::Program>,
1709 sketch: ObjectId,
1710 point: ObjectId,
1711 ctor: PointCtor,
1712 ) -> api::Result<()> {
1713 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
1715
1716 let sketch_id = sketch;
1718 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1719 msg: format!("Sketch not found: {sketch:?}"),
1720 })?;
1721 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1722 return Err(Error {
1723 msg: format!("Object is not a sketch: {sketch_object:?}"),
1724 });
1725 };
1726 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| Error {
1727 msg: format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"),
1728 })?;
1729 let point_id = point;
1731 let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
1732 msg: format!("Point not found in scene graph: point={point:?}"),
1733 })?;
1734 let ObjectKind::Segment {
1735 segment: Segment::Point(point),
1736 } = &point_object.kind
1737 else {
1738 return Err(Error {
1739 msg: format!("Object is not a point segment: {point_object:?}"),
1740 });
1741 };
1742
1743 if let Some(owner_id) = point.owner {
1745 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
1746 msg: format!("Internal: Owner of point not found in scene graph: owner={owner_id:?}",),
1747 })?;
1748 let ObjectKind::Segment { segment } = &owner_object.kind else {
1749 return Err(Error {
1750 msg: format!("Internal: Owner of point is not a segment: {owner_object:?}"),
1751 });
1752 };
1753
1754 if let Segment::Line(line) = segment {
1756 let SegmentCtor::Line(line_ctor) = &line.ctor else {
1757 return Err(Error {
1758 msg: format!("Internal: Owner of point does not have line ctor: {owner_object:?}"),
1759 });
1760 };
1761 let mut line_ctor = line_ctor.clone();
1762 if line.start == point_id {
1764 line_ctor.start = ctor.position;
1765 } else if line.end == point_id {
1766 line_ctor.end = ctor.position;
1767 } else {
1768 return Err(Error {
1769 msg: format!(
1770 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
1771 ),
1772 });
1773 }
1774 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
1775 }
1776
1777 if let Segment::Arc(arc) = segment {
1779 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
1780 return Err(Error {
1781 msg: format!("Internal: Owner of point does not have arc ctor: {owner_object:?}"),
1782 });
1783 };
1784 let mut arc_ctor = arc_ctor.clone();
1785 if arc.center == point_id {
1787 arc_ctor.center = ctor.position;
1788 } else if arc.start == point_id {
1789 arc_ctor.start = ctor.position;
1790 } else if arc.end == point_id {
1791 arc_ctor.end = ctor.position;
1792 } else {
1793 return Err(Error {
1794 msg: format!(
1795 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
1796 ),
1797 });
1798 }
1799 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
1800 }
1801
1802 if let Segment::Circle(circle) = segment {
1804 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
1805 return Err(Error {
1806 msg: format!("Internal: Owner of point does not have circle ctor: {owner_object:?}"),
1807 });
1808 };
1809 let mut circle_ctor = circle_ctor.clone();
1810 if circle.center == point_id {
1811 circle_ctor.center = ctor.position;
1812 } else if circle.start == point_id {
1813 circle_ctor.start = ctor.position;
1814 } else {
1815 return Err(Error {
1816 msg: format!(
1817 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
1818 ),
1819 });
1820 }
1821 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
1822 }
1823
1824 }
1827
1828 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
1830 Ok(())
1831 }
1832
1833 fn edit_line(
1834 &mut self,
1835 new_ast: &mut ast::Node<ast::Program>,
1836 sketch: ObjectId,
1837 line: ObjectId,
1838 ctor: LineCtor,
1839 ) -> api::Result<()> {
1840 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1842 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1843
1844 let sketch_id = sketch;
1846 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1847 msg: format!("Sketch not found: {sketch:?}"),
1848 })?;
1849 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1850 return Err(Error {
1851 msg: format!("Object is not a sketch: {sketch_object:?}"),
1852 });
1853 };
1854 sketch.segments.iter().find(|o| **o == line).ok_or_else(|| Error {
1855 msg: format!("Line not found in sketch: line={line:?}, sketch={sketch:?}"),
1856 })?;
1857 let line_id = line;
1859 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1860 msg: format!("Line not found in scene graph: line={line:?}"),
1861 })?;
1862 let ObjectKind::Segment { .. } = &line_object.kind else {
1863 return Err(Error {
1864 msg: format!("Object is not a segment: {line_object:?}"),
1865 });
1866 };
1867
1868 self.mutate_ast(
1870 new_ast,
1871 line_id,
1872 AstMutateCommand::EditLine {
1873 start: new_start_ast,
1874 end: new_end_ast,
1875 construction: ctor.construction,
1876 },
1877 )?;
1878 Ok(())
1879 }
1880
1881 fn edit_arc(
1882 &mut self,
1883 new_ast: &mut ast::Node<ast::Program>,
1884 sketch: ObjectId,
1885 arc: ObjectId,
1886 ctor: ArcCtor,
1887 ) -> api::Result<()> {
1888 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1890 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1891 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1892
1893 let sketch_id = sketch;
1895 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1896 msg: format!("Sketch not found: {sketch:?}"),
1897 })?;
1898 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1899 return Err(Error {
1900 msg: format!("Object is not a sketch: {sketch_object:?}"),
1901 });
1902 };
1903 sketch.segments.iter().find(|o| **o == arc).ok_or_else(|| Error {
1904 msg: format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}"),
1905 })?;
1906 let arc_id = arc;
1908 let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
1909 msg: format!("Arc not found in scene graph: arc={arc:?}"),
1910 })?;
1911 let ObjectKind::Segment { .. } = &arc_object.kind else {
1912 return Err(Error {
1913 msg: format!("Object is not a segment: {arc_object:?}"),
1914 });
1915 };
1916
1917 self.mutate_ast(
1919 new_ast,
1920 arc_id,
1921 AstMutateCommand::EditArc {
1922 start: new_start_ast,
1923 end: new_end_ast,
1924 center: new_center_ast,
1925 construction: ctor.construction,
1926 },
1927 )?;
1928 Ok(())
1929 }
1930
1931 fn edit_circle(
1932 &mut self,
1933 new_ast: &mut ast::Node<ast::Program>,
1934 sketch: ObjectId,
1935 circle: ObjectId,
1936 ctor: CircleCtor,
1937 ) -> api::Result<()> {
1938 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1940 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1941
1942 let sketch_id = sketch;
1944 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1945 msg: format!("Sketch not found: {sketch:?}"),
1946 })?;
1947 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1948 return Err(Error {
1949 msg: format!("Object is not a sketch: {sketch_object:?}"),
1950 });
1951 };
1952 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| Error {
1953 msg: format!("Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"),
1954 })?;
1955 let circle_id = circle;
1957 let circle_object = self.scene_graph.objects.get(circle_id.0).ok_or_else(|| Error {
1958 msg: format!("Circle not found in scene graph: circle={circle:?}"),
1959 })?;
1960 let ObjectKind::Segment { .. } = &circle_object.kind else {
1961 return Err(Error {
1962 msg: format!("Object is not a segment: {circle_object:?}"),
1963 });
1964 };
1965
1966 self.mutate_ast(
1968 new_ast,
1969 circle_id,
1970 AstMutateCommand::EditCircle {
1971 start: new_start_ast,
1972 center: new_center_ast,
1973 construction: ctor.construction,
1974 },
1975 )?;
1976 Ok(())
1977 }
1978
1979 fn delete_segment(
1980 &mut self,
1981 new_ast: &mut ast::Node<ast::Program>,
1982 sketch: ObjectId,
1983 segment_id: ObjectId,
1984 ) -> api::Result<()> {
1985 let sketch_id = sketch;
1987 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1988 msg: format!("Sketch not found: {sketch:?}"),
1989 })?;
1990 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1991 return Err(Error {
1992 msg: format!("Object is not a sketch: {sketch_object:?}"),
1993 });
1994 };
1995 sketch
1996 .segments
1997 .iter()
1998 .find(|o| **o == segment_id)
1999 .ok_or_else(|| Error {
2000 msg: format!("Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"),
2001 })?;
2002 let segment_object = self.scene_graph.objects.get(segment_id.0).ok_or_else(|| Error {
2004 msg: format!("Segment not found in scene graph: segment={segment_id:?}"),
2005 })?;
2006 let ObjectKind::Segment { .. } = &segment_object.kind else {
2007 return Err(Error {
2008 msg: format!("Object is not a segment: {segment_object:?}"),
2009 });
2010 };
2011
2012 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2014 Ok(())
2015 }
2016
2017 fn delete_constraint(
2018 &mut self,
2019 new_ast: &mut ast::Node<ast::Program>,
2020 sketch: ObjectId,
2021 constraint_id: ObjectId,
2022 ) -> api::Result<()> {
2023 let sketch_id = sketch;
2025 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
2026 msg: format!("Sketch not found: {sketch:?}"),
2027 })?;
2028 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2029 return Err(Error {
2030 msg: format!("Object is not a sketch: {sketch_object:?}"),
2031 });
2032 };
2033 sketch
2034 .constraints
2035 .iter()
2036 .find(|o| **o == constraint_id)
2037 .ok_or_else(|| Error {
2038 msg: format!("Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"),
2039 })?;
2040 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
2042 msg: format!("Constraint not found in scene graph: constraint={constraint_id:?}"),
2043 })?;
2044 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2045 return Err(Error {
2046 msg: format!("Object is not a constraint: {constraint_object:?}"),
2047 });
2048 };
2049
2050 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2052 Ok(())
2053 }
2054
2055 fn edit_equal_length_constraint(
2057 &mut self,
2058 new_ast: &mut ast::Node<ast::Program>,
2059 constraint_id: ObjectId,
2060 lines: Vec<ObjectId>,
2061 ) -> api::Result<()> {
2062 if lines.len() < 2 {
2063 return Err(Error {
2064 msg: format!(
2065 "Lines equal length constraint must have at least 2 lines, got {}",
2066 lines.len()
2067 ),
2068 });
2069 }
2070
2071 let line_asts = lines
2072 .iter()
2073 .map(|line_id| {
2074 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
2075 msg: format!("Line not found: {line_id:?}"),
2076 })?;
2077 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2078 return Err(Error {
2079 msg: format!("Object is not a segment: {line_object:?}"),
2080 });
2081 };
2082 let Segment::Line(_) = line_segment else {
2083 return Err(Error {
2084 msg: format!("Only lines can be made equal length: {line_object:?}"),
2085 });
2086 };
2087
2088 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
2089 })
2090 .collect::<Result<Vec<_>, _>>()?;
2091
2092 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2093 elements: line_asts,
2094 digest: None,
2095 non_code_meta: Default::default(),
2096 })));
2097
2098 self.mutate_ast(
2099 new_ast,
2100 constraint_id,
2101 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2102 )?;
2103 Ok(())
2104 }
2105
2106 async fn execute_after_edit(
2107 &mut self,
2108 ctx: &ExecutorContext,
2109 sketch: ObjectId,
2110 segment_ids_edited: AhashIndexSet<ObjectId>,
2111 edit_kind: EditDeleteKind,
2112 new_ast: &mut ast::Node<ast::Program>,
2113 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
2114 let new_source = source_from_ast(new_ast);
2116 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
2118 if !errors.is_empty() {
2119 return Err(Error {
2120 msg: format!("Error parsing KCL source after editing: {errors:?}"),
2121 });
2122 }
2123 let Some(new_program) = new_program else {
2124 return Err(Error {
2125 msg: "No AST produced after editing".to_string(),
2126 });
2127 };
2128
2129 self.program = new_program.clone();
2131
2132 let is_delete = edit_kind.is_delete();
2134 let truncated_program = {
2135 let mut truncated_program = new_program;
2136 self.only_sketch_block(sketch, edit_kind.to_change_kind(), &mut truncated_program.ast)?;
2137 truncated_program
2138 };
2139
2140 #[cfg(not(feature = "artifact-graph"))]
2141 drop(segment_ids_edited);
2142
2143 let mock_config = MockConfig {
2145 sketch_block_id: Some(sketch),
2146 freedom_analysis: is_delete,
2147 #[cfg(feature = "artifact-graph")]
2148 segment_ids_edited: segment_ids_edited.clone(),
2149 ..Default::default()
2150 };
2151 let outcome = ctx.run_mock(&truncated_program, &mock_config).await.map_err(|err| {
2152 Error {
2155 msg: err.error.message().to_owned(),
2156 }
2157 })?;
2158
2159 let outcome = self.update_state_after_exec(outcome, is_delete);
2161
2162 #[cfg(feature = "artifact-graph")]
2163 let new_source = {
2164 let mut new_ast = self.program.ast.clone();
2169 for (var_range, value) in &outcome.var_solutions {
2170 let rounded = value.round(3);
2171 mutate_ast_node_by_source_range(
2172 &mut new_ast,
2173 *var_range,
2174 AstMutateCommand::EditVarInitialValue { value: rounded },
2175 )?;
2176 }
2177 source_from_ast(&new_ast)
2178 };
2179
2180 let src_delta = SourceDelta { text: new_source };
2181 let scene_graph_delta = SceneGraphDelta {
2182 new_graph: self.scene_graph.clone(),
2183 invalidates_ids: is_delete,
2184 new_objects: Vec::new(),
2185 exec_outcome: outcome,
2186 };
2187 Ok((src_delta, scene_graph_delta))
2188 }
2189
2190 async fn execute_after_delete_sketch(
2191 &mut self,
2192 ctx: &ExecutorContext,
2193 new_ast: &mut ast::Node<ast::Program>,
2194 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
2195 let new_source = source_from_ast(new_ast);
2197 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
2199 if !errors.is_empty() {
2200 return Err(Error {
2201 msg: format!("Error parsing KCL source after editing: {errors:?}"),
2202 });
2203 }
2204 let Some(new_program) = new_program else {
2205 return Err(Error {
2206 msg: "No AST produced after editing".to_string(),
2207 });
2208 };
2209
2210 self.program = new_program.clone();
2212
2213 let outcome = ctx.run_with_caching(new_program).await.map_err(|err| {
2219 Error {
2222 msg: err.error.message().to_owned(),
2223 }
2224 })?;
2225 let freedom_analysis_ran = true;
2226
2227 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2228
2229 let src_delta = SourceDelta { text: new_source };
2230 let scene_graph_delta = SceneGraphDelta {
2231 new_graph: self.scene_graph.clone(),
2232 invalidates_ids: true,
2233 new_objects: Vec::new(),
2234 exec_outcome: outcome,
2235 };
2236 Ok((src_delta, scene_graph_delta))
2237 }
2238
2239 fn point_id_to_ast_reference(
2244 &self,
2245 point_id: ObjectId,
2246 new_ast: &mut ast::Node<ast::Program>,
2247 ) -> api::Result<ast::Expr> {
2248 let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
2249 msg: format!("Point not found: {point_id:?}"),
2250 })?;
2251 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2252 return Err(Error {
2253 msg: format!("Object is not a segment: {point_object:?}"),
2254 });
2255 };
2256 let Segment::Point(point) = point_segment else {
2257 return Err(Error {
2258 msg: format!("Only points are currently supported: {point_object:?}"),
2259 });
2260 };
2261
2262 if let Some(owner_id) = point.owner {
2263 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
2264 msg: format!("Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"),
2265 })?;
2266 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2267 return Err(Error {
2268 msg: format!("Owner of point is not a segment: {owner_object:?}"),
2269 });
2270 };
2271
2272 match owner_segment {
2273 Segment::Line(line) => {
2274 let property = if line.start == point_id {
2275 LINE_PROPERTY_START
2276 } else if line.end == point_id {
2277 LINE_PROPERTY_END
2278 } else {
2279 return Err(Error {
2280 msg: format!(
2281 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2282 ),
2283 });
2284 };
2285 get_or_insert_ast_reference(new_ast, &owner_object.source, "line", Some(property))
2286 }
2287 Segment::Arc(arc) => {
2288 let property = if arc.start == point_id {
2289 ARC_PROPERTY_START
2290 } else if arc.end == point_id {
2291 ARC_PROPERTY_END
2292 } else if arc.center == point_id {
2293 ARC_PROPERTY_CENTER
2294 } else {
2295 return Err(Error {
2296 msg: format!(
2297 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2298 ),
2299 });
2300 };
2301 get_or_insert_ast_reference(new_ast, &owner_object.source, "arc", Some(property))
2302 }
2303 Segment::Circle(circle) => {
2304 let property = if circle.start == point_id {
2305 CIRCLE_PROPERTY_START
2306 } else if circle.center == point_id {
2307 CIRCLE_PROPERTY_CENTER
2308 } else {
2309 return Err(Error {
2310 msg: format!(
2311 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2312 ),
2313 });
2314 };
2315 get_or_insert_ast_reference(new_ast, &owner_object.source, "circle", Some(property))
2316 }
2317 _ => Err(Error {
2318 msg: format!(
2319 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
2320 ),
2321 }),
2322 }
2323 } else {
2324 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
2326 }
2327 }
2328
2329 async fn add_coincident(
2330 &mut self,
2331 sketch: ObjectId,
2332 coincident: Coincident,
2333 new_ast: &mut ast::Node<ast::Program>,
2334 ) -> api::Result<SourceRange> {
2335 let &[seg0_id, seg1_id] = coincident.segments.as_slice() else {
2336 return Err(Error {
2337 msg: format!(
2338 "Coincident constraint must have exactly 2 segments, got {}",
2339 coincident.segments.len()
2340 ),
2341 });
2342 };
2343 let sketch_id = sketch;
2344
2345 let seg0_object = self.scene_graph.objects.get(seg0_id.0).ok_or_else(|| Error {
2347 msg: format!("Object not found: {seg0_id:?}"),
2348 })?;
2349 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
2350 return Err(Error {
2351 msg: format!("Object is not a segment: {seg0_object:?}"),
2352 });
2353 };
2354 let seg0_ast = match seg0_segment {
2355 Segment::Point(_) => {
2356 self.point_id_to_ast_reference(seg0_id, new_ast)?
2358 }
2359 Segment::Line(_) => {
2360 get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?
2362 }
2363 Segment::Arc(_) => {
2364 get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?
2366 }
2367 Segment::Circle(_) => {
2368 get_or_insert_ast_reference(new_ast, &seg0_object.source, "circle", None)?
2370 }
2371 };
2372
2373 let seg1_object = self.scene_graph.objects.get(seg1_id.0).ok_or_else(|| Error {
2375 msg: format!("Object not found: {seg1_id:?}"),
2376 })?;
2377 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
2378 return Err(Error {
2379 msg: format!("Object is not a segment: {seg1_object:?}"),
2380 });
2381 };
2382 let seg1_ast = match seg1_segment {
2383 Segment::Point(_) => {
2384 self.point_id_to_ast_reference(seg1_id, new_ast)?
2386 }
2387 Segment::Line(_) => {
2388 get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?
2390 }
2391 Segment::Arc(_) => {
2392 get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?
2394 }
2395 Segment::Circle(_) => {
2396 get_or_insert_ast_reference(new_ast, &seg1_object.source, "circle", None)?
2398 }
2399 };
2400
2401 let coincident_ast = create_coincident_ast(seg0_ast, seg1_ast);
2403
2404 let (sketch_block_range, _) = self.mutate_ast(
2406 new_ast,
2407 sketch_id,
2408 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
2409 )?;
2410 Ok(sketch_block_range)
2411 }
2412
2413 async fn add_distance(
2414 &mut self,
2415 sketch: ObjectId,
2416 distance: Distance,
2417 new_ast: &mut ast::Node<ast::Program>,
2418 ) -> api::Result<SourceRange> {
2419 let &[pt0_id, pt1_id] = distance.points.as_slice() else {
2420 return Err(Error {
2421 msg: format!(
2422 "Distance constraint must have exactly 2 points, got {}",
2423 distance.points.len()
2424 ),
2425 });
2426 };
2427 let sketch_id = sketch;
2428
2429 let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
2431 let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
2432
2433 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2435 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
2436 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2437 ast::ArrayExpression {
2438 elements: vec![pt0_ast, pt1_ast],
2439 digest: None,
2440 non_code_meta: Default::default(),
2441 },
2442 )))),
2443 arguments: Default::default(),
2444 digest: None,
2445 non_code_meta: Default::default(),
2446 })));
2447 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2448 left: distance_call_ast,
2449 operator: ast::BinaryOperator::Eq,
2450 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2451 value: ast::LiteralValue::Number {
2452 value: distance.distance.value,
2453 suffix: distance.distance.units,
2454 },
2455 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
2456 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
2457 })?,
2458 digest: None,
2459 }))),
2460 digest: None,
2461 })));
2462
2463 let (sketch_block_range, _) = self.mutate_ast(
2465 new_ast,
2466 sketch_id,
2467 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
2468 )?;
2469 Ok(sketch_block_range)
2470 }
2471
2472 async fn add_angle(
2473 &mut self,
2474 sketch: ObjectId,
2475 angle: Angle,
2476 new_ast: &mut ast::Node<ast::Program>,
2477 ) -> api::Result<SourceRange> {
2478 let &[l0_id, l1_id] = angle.lines.as_slice() else {
2479 return Err(Error {
2480 msg: format!("Angle constraint must have exactly 2 lines, got {}", angle.lines.len()),
2481 });
2482 };
2483 let sketch_id = sketch;
2484
2485 let line0_object = self.scene_graph.objects.get(l0_id.0).ok_or_else(|| Error {
2487 msg: format!("Line not found: {l0_id:?}"),
2488 })?;
2489 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
2490 return Err(Error {
2491 msg: format!("Object is not a segment: {line0_object:?}"),
2492 });
2493 };
2494 let Segment::Line(_) = line0_segment else {
2495 return Err(Error {
2496 msg: format!("Only lines can be constrained to meet at an angle: {line0_object:?}",),
2497 });
2498 };
2499 let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
2500
2501 let line1_object = self.scene_graph.objects.get(l1_id.0).ok_or_else(|| Error {
2502 msg: format!("Line not found: {l1_id:?}"),
2503 })?;
2504 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
2505 return Err(Error {
2506 msg: format!("Object is not a segment: {line1_object:?}"),
2507 });
2508 };
2509 let Segment::Line(_) = line1_segment else {
2510 return Err(Error {
2511 msg: format!("Only lines can be constrained to meet at an angle: {line1_object:?}",),
2512 });
2513 };
2514 let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
2515
2516 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2518 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
2519 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2520 ast::ArrayExpression {
2521 elements: vec![l0_ast, l1_ast],
2522 digest: None,
2523 non_code_meta: Default::default(),
2524 },
2525 )))),
2526 arguments: Default::default(),
2527 digest: None,
2528 non_code_meta: Default::default(),
2529 })));
2530 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2531 left: angle_call_ast,
2532 operator: ast::BinaryOperator::Eq,
2533 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2534 value: ast::LiteralValue::Number {
2535 value: angle.angle.value,
2536 suffix: angle.angle.units,
2537 },
2538 raw: format_number_literal(angle.angle.value, angle.angle.units).map_err(|_| Error {
2539 msg: format!("Could not format numeric suffix: {:?}", angle.angle.units),
2540 })?,
2541 digest: None,
2542 }))),
2543 digest: None,
2544 })));
2545
2546 let (sketch_block_range, _) = self.mutate_ast(
2548 new_ast,
2549 sketch_id,
2550 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
2551 )?;
2552 Ok(sketch_block_range)
2553 }
2554
2555 async fn add_tangent(
2556 &mut self,
2557 sketch: ObjectId,
2558 tangent: Tangent,
2559 new_ast: &mut ast::Node<ast::Program>,
2560 ) -> api::Result<SourceRange> {
2561 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
2562 return Err(Error {
2563 msg: format!(
2564 "Tangent constraint must have exactly 2 segments, got {}",
2565 tangent.input.len()
2566 ),
2567 });
2568 };
2569 let sketch_id = sketch;
2570
2571 let seg0_object = self.scene_graph.objects.get(seg0_id.0).ok_or_else(|| Error {
2572 msg: format!("Segment not found: {seg0_id:?}"),
2573 })?;
2574 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
2575 return Err(Error {
2576 msg: format!("Object is not a segment: {seg0_object:?}"),
2577 });
2578 };
2579 let seg0_ast = match seg0_segment {
2580 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?,
2581 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?,
2582 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "circle", None)?,
2583 _ => {
2584 return Err(Error {
2585 msg: format!("Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"),
2586 });
2587 }
2588 };
2589
2590 let seg1_object = self.scene_graph.objects.get(seg1_id.0).ok_or_else(|| Error {
2591 msg: format!("Segment not found: {seg1_id:?}"),
2592 })?;
2593 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
2594 return Err(Error {
2595 msg: format!("Object is not a segment: {seg1_object:?}"),
2596 });
2597 };
2598 let seg1_ast = match seg1_segment {
2599 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?,
2600 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?,
2601 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "circle", None)?,
2602 _ => {
2603 return Err(Error {
2604 msg: format!("Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"),
2605 });
2606 }
2607 };
2608
2609 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
2610 let (sketch_block_range, _) = self.mutate_ast(
2611 new_ast,
2612 sketch_id,
2613 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
2614 )?;
2615 Ok(sketch_block_range)
2616 }
2617
2618 async fn add_radius(
2619 &mut self,
2620 sketch: ObjectId,
2621 radius: Radius,
2622 new_ast: &mut ast::Node<ast::Program>,
2623 ) -> api::Result<SourceRange> {
2624 let params = ArcSizeConstraintParams {
2625 points: vec![radius.arc],
2626 function_name: RADIUS_FN,
2627 value: radius.radius.value,
2628 units: radius.radius.units,
2629 constraint_type_name: "Radius",
2630 };
2631 self.add_arc_size_constraint(sketch, params, new_ast).await
2632 }
2633
2634 async fn add_diameter(
2635 &mut self,
2636 sketch: ObjectId,
2637 diameter: Diameter,
2638 new_ast: &mut ast::Node<ast::Program>,
2639 ) -> api::Result<SourceRange> {
2640 let params = ArcSizeConstraintParams {
2641 points: vec![diameter.arc],
2642 function_name: DIAMETER_FN,
2643 value: diameter.diameter.value,
2644 units: diameter.diameter.units,
2645 constraint_type_name: "Diameter",
2646 };
2647 self.add_arc_size_constraint(sketch, params, new_ast).await
2648 }
2649
2650 async fn add_arc_size_constraint(
2651 &mut self,
2652 sketch: ObjectId,
2653 params: ArcSizeConstraintParams,
2654 new_ast: &mut ast::Node<ast::Program>,
2655 ) -> api::Result<SourceRange> {
2656 let sketch_id = sketch;
2657
2658 if params.points.len() != 1 {
2660 return Err(Error {
2661 msg: format!(
2662 "{} constraint must have exactly 1 argument (an arc segment), got {}",
2663 params.constraint_type_name,
2664 params.points.len()
2665 ),
2666 });
2667 }
2668
2669 let arc_id = params.points[0];
2670 let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
2671 msg: format!("Arc segment not found: {arc_id:?}"),
2672 })?;
2673 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
2674 return Err(Error {
2675 msg: format!("Object is not a segment: {arc_object:?}"),
2676 });
2677 };
2678 let ref_type = match arc_segment {
2679 Segment::Arc(_) => "arc",
2680 Segment::Circle(_) => "circle",
2681 _ => {
2682 return Err(Error {
2683 msg: format!(
2684 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
2685 params.constraint_type_name
2686 ),
2687 });
2688 }
2689 };
2690 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
2692
2693 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2695 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
2696 unlabeled: Some(arc_ast),
2697 arguments: Default::default(),
2698 digest: None,
2699 non_code_meta: Default::default(),
2700 })));
2701 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2702 left: call_ast,
2703 operator: ast::BinaryOperator::Eq,
2704 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2705 value: ast::LiteralValue::Number {
2706 value: params.value,
2707 suffix: params.units,
2708 },
2709 raw: format_number_literal(params.value, params.units).map_err(|_| Error {
2710 msg: format!("Could not format numeric suffix: {:?}", params.units),
2711 })?,
2712 digest: None,
2713 }))),
2714 digest: None,
2715 })));
2716
2717 let (sketch_block_range, _) = self.mutate_ast(
2719 new_ast,
2720 sketch_id,
2721 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
2722 )?;
2723 Ok(sketch_block_range)
2724 }
2725
2726 async fn add_horizontal_distance(
2727 &mut self,
2728 sketch: ObjectId,
2729 distance: Distance,
2730 new_ast: &mut ast::Node<ast::Program>,
2731 ) -> api::Result<SourceRange> {
2732 let &[pt0_id, pt1_id] = distance.points.as_slice() else {
2733 return Err(Error {
2734 msg: format!(
2735 "Horizontal distance constraint must have exactly 2 points, got {}",
2736 distance.points.len()
2737 ),
2738 });
2739 };
2740 let sketch_id = sketch;
2741
2742 let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
2744 let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
2745
2746 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2748 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
2749 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2750 ast::ArrayExpression {
2751 elements: vec![pt0_ast, pt1_ast],
2752 digest: None,
2753 non_code_meta: Default::default(),
2754 },
2755 )))),
2756 arguments: Default::default(),
2757 digest: None,
2758 non_code_meta: Default::default(),
2759 })));
2760 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2761 left: distance_call_ast,
2762 operator: ast::BinaryOperator::Eq,
2763 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2764 value: ast::LiteralValue::Number {
2765 value: distance.distance.value,
2766 suffix: distance.distance.units,
2767 },
2768 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
2769 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
2770 })?,
2771 digest: None,
2772 }))),
2773 digest: None,
2774 })));
2775
2776 let (sketch_block_range, _) = self.mutate_ast(
2778 new_ast,
2779 sketch_id,
2780 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
2781 )?;
2782 Ok(sketch_block_range)
2783 }
2784
2785 async fn add_vertical_distance(
2786 &mut self,
2787 sketch: ObjectId,
2788 distance: Distance,
2789 new_ast: &mut ast::Node<ast::Program>,
2790 ) -> api::Result<SourceRange> {
2791 let &[pt0_id, pt1_id] = distance.points.as_slice() else {
2792 return Err(Error {
2793 msg: format!(
2794 "Vertical distance constraint must have exactly 2 points, got {}",
2795 distance.points.len()
2796 ),
2797 });
2798 };
2799 let sketch_id = sketch;
2800
2801 let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
2803 let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
2804
2805 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2807 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
2808 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2809 ast::ArrayExpression {
2810 elements: vec![pt0_ast, pt1_ast],
2811 digest: None,
2812 non_code_meta: Default::default(),
2813 },
2814 )))),
2815 arguments: Default::default(),
2816 digest: None,
2817 non_code_meta: Default::default(),
2818 })));
2819 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2820 left: distance_call_ast,
2821 operator: ast::BinaryOperator::Eq,
2822 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2823 value: ast::LiteralValue::Number {
2824 value: distance.distance.value,
2825 suffix: distance.distance.units,
2826 },
2827 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
2828 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
2829 })?,
2830 digest: None,
2831 }))),
2832 digest: None,
2833 })));
2834
2835 let (sketch_block_range, _) = self.mutate_ast(
2837 new_ast,
2838 sketch_id,
2839 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
2840 )?;
2841 Ok(sketch_block_range)
2842 }
2843
2844 async fn add_horizontal(
2845 &mut self,
2846 sketch: ObjectId,
2847 horizontal: Horizontal,
2848 new_ast: &mut ast::Node<ast::Program>,
2849 ) -> api::Result<SourceRange> {
2850 let sketch_id = sketch;
2851
2852 let line_id = horizontal.line;
2854 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
2855 msg: format!("Line not found: {line_id:?}"),
2856 })?;
2857 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2858 return Err(Error {
2859 msg: format!("Object is not a segment: {line_object:?}"),
2860 });
2861 };
2862 let Segment::Line(_) = line_segment else {
2863 return Err(Error {
2864 msg: format!("Only lines can be made horizontal: {line_object:?}"),
2865 });
2866 };
2867 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
2868
2869 let horizontal_ast = create_horizontal_ast(line_ast);
2871
2872 let (sketch_block_range, _) = self.mutate_ast(
2874 new_ast,
2875 sketch_id,
2876 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
2877 )?;
2878 Ok(sketch_block_range)
2879 }
2880
2881 async fn add_lines_equal_length(
2882 &mut self,
2883 sketch: ObjectId,
2884 lines_equal_length: LinesEqualLength,
2885 new_ast: &mut ast::Node<ast::Program>,
2886 ) -> api::Result<SourceRange> {
2887 if lines_equal_length.lines.len() < 2 {
2888 return Err(Error {
2889 msg: format!(
2890 "Lines equal length constraint must have at least 2 lines, got {}",
2891 lines_equal_length.lines.len()
2892 ),
2893 });
2894 };
2895
2896 let sketch_id = sketch;
2897
2898 let line_asts = lines_equal_length
2900 .lines
2901 .iter()
2902 .map(|line_id| {
2903 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
2904 msg: format!("Line not found: {line_id:?}"),
2905 })?;
2906 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2907 return Err(Error {
2908 msg: format!("Object is not a segment: {line_object:?}"),
2909 });
2910 };
2911 let Segment::Line(_) = line_segment else {
2912 return Err(Error {
2913 msg: format!("Only lines can be made equal length: {line_object:?}"),
2914 });
2915 };
2916
2917 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
2918 })
2919 .collect::<Result<Vec<_>, _>>()?;
2920
2921 let equal_length_ast = create_equal_length_ast(line_asts);
2923
2924 let (sketch_block_range, _) = self.mutate_ast(
2926 new_ast,
2927 sketch_id,
2928 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
2929 )?;
2930 Ok(sketch_block_range)
2931 }
2932
2933 async fn add_parallel(
2934 &mut self,
2935 sketch: ObjectId,
2936 parallel: Parallel,
2937 new_ast: &mut ast::Node<ast::Program>,
2938 ) -> api::Result<SourceRange> {
2939 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Parallel, parallel.lines, new_ast)
2940 .await
2941 }
2942
2943 async fn add_perpendicular(
2944 &mut self,
2945 sketch: ObjectId,
2946 perpendicular: Perpendicular,
2947 new_ast: &mut ast::Node<ast::Program>,
2948 ) -> api::Result<SourceRange> {
2949 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
2950 .await
2951 }
2952
2953 async fn add_lines_at_angle_constraint(
2954 &mut self,
2955 sketch: ObjectId,
2956 angle_kind: LinesAtAngleKind,
2957 lines: Vec<ObjectId>,
2958 new_ast: &mut ast::Node<ast::Program>,
2959 ) -> api::Result<SourceRange> {
2960 let &[line0_id, line1_id] = lines.as_slice() else {
2961 return Err(Error {
2962 msg: format!(
2963 "{} constraint must have exactly 2 lines, got {}",
2964 angle_kind.to_function_name(),
2965 lines.len()
2966 ),
2967 });
2968 };
2969
2970 let sketch_id = sketch;
2971
2972 let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
2974 msg: format!("Line not found: {line0_id:?}"),
2975 })?;
2976 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
2977 return Err(Error {
2978 msg: format!("Object is not a segment: {line0_object:?}"),
2979 });
2980 };
2981 let Segment::Line(_) = line0_segment else {
2982 return Err(Error {
2983 msg: format!(
2984 "Only lines can be made {}: {line0_object:?}",
2985 angle_kind.to_function_name()
2986 ),
2987 });
2988 };
2989 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
2990
2991 let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
2992 msg: format!("Line not found: {line1_id:?}"),
2993 })?;
2994 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
2995 return Err(Error {
2996 msg: format!("Object is not a segment: {line1_object:?}"),
2997 });
2998 };
2999 let Segment::Line(_) = line1_segment else {
3000 return Err(Error {
3001 msg: format!(
3002 "Only lines can be made {}: {line1_object:?}",
3003 angle_kind.to_function_name()
3004 ),
3005 });
3006 };
3007 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
3008
3009 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3011 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3012 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3013 ast::ArrayExpression {
3014 elements: vec![line0_ast, line1_ast],
3015 digest: None,
3016 non_code_meta: Default::default(),
3017 },
3018 )))),
3019 arguments: Default::default(),
3020 digest: None,
3021 non_code_meta: Default::default(),
3022 })));
3023
3024 let (sketch_block_range, _) = self.mutate_ast(
3026 new_ast,
3027 sketch_id,
3028 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3029 )?;
3030 Ok(sketch_block_range)
3031 }
3032
3033 async fn add_vertical(
3034 &mut self,
3035 sketch: ObjectId,
3036 vertical: Vertical,
3037 new_ast: &mut ast::Node<ast::Program>,
3038 ) -> api::Result<SourceRange> {
3039 let sketch_id = sketch;
3040
3041 let line_id = vertical.line;
3043 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
3044 msg: format!("Line not found: {line_id:?}"),
3045 })?;
3046 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3047 return Err(Error {
3048 msg: format!("Object is not a segment: {line_object:?}"),
3049 });
3050 };
3051 let Segment::Line(_) = line_segment else {
3052 return Err(Error {
3053 msg: format!("Only lines can be made vertical: {line_object:?}"),
3054 });
3055 };
3056 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
3057
3058 let vertical_ast = create_vertical_ast(line_ast);
3060
3061 let (sketch_block_range, _) = self.mutate_ast(
3063 new_ast,
3064 sketch_id,
3065 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
3066 )?;
3067 Ok(sketch_block_range)
3068 }
3069
3070 async fn execute_after_add_constraint(
3071 &mut self,
3072 ctx: &ExecutorContext,
3073 sketch_id: ObjectId,
3074 #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_range: SourceRange,
3075 new_ast: &mut ast::Node<ast::Program>,
3076 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
3077 let new_source = source_from_ast(new_ast);
3079 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
3081 if !errors.is_empty() {
3082 return Err(Error {
3083 msg: format!("Error parsing KCL source after adding constraint: {errors:?}"),
3084 });
3085 }
3086 let Some(new_program) = new_program else {
3087 return Err(Error {
3088 msg: "No AST produced after adding constraint".to_string(),
3089 });
3090 };
3091 #[cfg(feature = "artifact-graph")]
3092 let constraint_source_range =
3093 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
3094 msg: format!(
3095 "Source range of new constraint not found in sketch block: {sketch_block_range:?}; {err:?}"
3096 ),
3097 })?;
3098
3099 let mut truncated_program = new_program.clone();
3102 self.only_sketch_block(sketch_id, ChangeKind::Add, &mut truncated_program.ast)?;
3103
3104 let outcome = ctx
3106 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
3107 .await
3108 .map_err(|err| {
3109 Error {
3112 msg: err.error.message().to_owned(),
3113 }
3114 })?;
3115
3116 #[cfg(not(feature = "artifact-graph"))]
3117 let new_object_ids = Vec::new();
3118 #[cfg(feature = "artifact-graph")]
3119 let new_object_ids = {
3120 let constraint_id = outcome
3122 .source_range_to_object
3123 .get(&constraint_source_range)
3124 .copied()
3125 .ok_or_else(|| Error {
3126 msg: format!("Source range of constraint not found: {constraint_source_range:?}"),
3127 })?;
3128 vec![constraint_id]
3129 };
3130
3131 self.program = new_program;
3134
3135 let outcome = self.update_state_after_exec(outcome, true);
3137
3138 let src_delta = SourceDelta { text: new_source };
3139 let scene_graph_delta = SceneGraphDelta {
3140 new_graph: self.scene_graph.clone(),
3141 invalidates_ids: false,
3142 new_objects: new_object_ids,
3143 exec_outcome: outcome,
3144 };
3145 Ok((src_delta, scene_graph_delta))
3146 }
3147
3148 fn find_referenced_constraints(
3150 &self,
3151 sketch_id: ObjectId,
3152 segment_ids_set: &AhashIndexSet<ObjectId>,
3153 ) -> api::Result<AhashIndexSet<ObjectId>> {
3154 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
3156 msg: format!("Sketch not found: {sketch_id:?}"),
3157 })?;
3158 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3159 return Err(Error {
3160 msg: format!("Object is not a sketch: {sketch_object:?}"),
3161 });
3162 };
3163 let mut constraint_ids_set = AhashIndexSet::default();
3164 for constraint_id in &sketch.constraints {
3165 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
3166 msg: format!("Constraint not found: {constraint_id:?}"),
3167 })?;
3168 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
3169 return Err(Error {
3170 msg: format!("Object is not a constraint: {constraint_object:?}"),
3171 });
3172 };
3173 let depends_on_segment = match constraint {
3174 Constraint::Coincident(c) => c.segments.iter().any(|seg_id| {
3175 if segment_ids_set.contains(seg_id) {
3177 return true;
3178 }
3179 let seg_object = self.scene_graph.objects.get(seg_id.0);
3181 if let Some(obj) = seg_object
3182 && let ObjectKind::Segment { segment } = &obj.kind
3183 && let Segment::Point(pt) = segment
3184 && let Some(owner_line_id) = pt.owner
3185 {
3186 return segment_ids_set.contains(&owner_line_id);
3187 }
3188 false
3189 }),
3190 Constraint::Distance(d) => d.points.iter().any(|pt_id| {
3191 if segment_ids_set.contains(pt_id) {
3192 return true;
3193 }
3194 let pt_object = self.scene_graph.objects.get(pt_id.0);
3195 if let Some(obj) = pt_object
3196 && let ObjectKind::Segment { segment } = &obj.kind
3197 && let Segment::Point(pt) = segment
3198 && let Some(owner_line_id) = pt.owner
3199 {
3200 return segment_ids_set.contains(&owner_line_id);
3201 }
3202 false
3203 }),
3204 Constraint::Radius(r) => segment_ids_set.contains(&r.arc),
3205 Constraint::Diameter(d) => segment_ids_set.contains(&d.arc),
3206 Constraint::HorizontalDistance(d) => d.points.iter().any(|pt_id| {
3207 let pt_object = self.scene_graph.objects.get(pt_id.0);
3208 if let Some(obj) = pt_object
3209 && let ObjectKind::Segment { segment } = &obj.kind
3210 && let Segment::Point(pt) = segment
3211 && let Some(owner_line_id) = pt.owner
3212 {
3213 return segment_ids_set.contains(&owner_line_id);
3214 }
3215 false
3216 }),
3217 Constraint::VerticalDistance(d) => d.points.iter().any(|pt_id| {
3218 let pt_object = self.scene_graph.objects.get(pt_id.0);
3219 if let Some(obj) = pt_object
3220 && let ObjectKind::Segment { segment } = &obj.kind
3221 && let Segment::Point(pt) = segment
3222 && let Some(owner_line_id) = pt.owner
3223 {
3224 return segment_ids_set.contains(&owner_line_id);
3225 }
3226 false
3227 }),
3228 Constraint::Horizontal(h) => segment_ids_set.contains(&h.line),
3229 Constraint::Vertical(v) => segment_ids_set.contains(&v.line),
3230 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
3231 .lines
3232 .iter()
3233 .any(|line_id| segment_ids_set.contains(line_id)),
3234 Constraint::Parallel(parallel) => {
3235 parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
3236 }
3237 Constraint::Perpendicular(perpendicular) => perpendicular
3238 .lines
3239 .iter()
3240 .any(|line_id| segment_ids_set.contains(line_id)),
3241 Constraint::Angle(angle) => angle.lines.iter().any(|line_id| segment_ids_set.contains(line_id)),
3242 Constraint::Tangent(tangent) => tangent.input.iter().any(|seg_id| segment_ids_set.contains(seg_id)),
3243 };
3244 if depends_on_segment {
3245 constraint_ids_set.insert(*constraint_id);
3246 }
3247 }
3248 Ok(constraint_ids_set)
3249 }
3250
3251 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
3252 #[cfg(not(feature = "artifact-graph"))]
3253 {
3254 let _ = freedom_analysis_ran; outcome
3256 }
3257 #[cfg(feature = "artifact-graph")]
3258 {
3259 let mut outcome = outcome;
3260 let new_objects = std::mem::take(&mut outcome.scene_objects);
3261
3262 if freedom_analysis_ran {
3263 self.point_freedom_cache.clear();
3266 for new_obj in &new_objects {
3267 if let ObjectKind::Segment {
3268 segment: crate::front::Segment::Point(point),
3269 } = &new_obj.kind
3270 {
3271 self.point_freedom_cache.insert(new_obj.id, point.freedom);
3272 }
3273 }
3274 self.scene_graph.objects = new_objects;
3276 } else {
3277 for old_obj in &self.scene_graph.objects {
3280 if let ObjectKind::Segment {
3281 segment: crate::front::Segment::Point(point),
3282 } = &old_obj.kind
3283 {
3284 self.point_freedom_cache.insert(old_obj.id, point.freedom);
3285 }
3286 }
3287
3288 let mut updated_objects = Vec::with_capacity(new_objects.len());
3290 for new_obj in new_objects {
3291 let mut obj = new_obj;
3292 if let ObjectKind::Segment {
3293 segment: crate::front::Segment::Point(point),
3294 } = &mut obj.kind
3295 {
3296 let new_freedom = point.freedom;
3297 match new_freedom {
3303 Freedom::Free => {
3304 match self.point_freedom_cache.get(&obj.id).copied() {
3305 Some(Freedom::Conflict) => {
3306 }
3309 Some(Freedom::Fixed) => {
3310 point.freedom = Freedom::Fixed;
3312 }
3313 Some(Freedom::Free) => {
3314 }
3316 None => {
3317 }
3319 }
3320 }
3321 Freedom::Fixed => {
3322 }
3324 Freedom::Conflict => {
3325 }
3327 }
3328 self.point_freedom_cache.insert(obj.id, point.freedom);
3330 }
3331 updated_objects.push(obj);
3332 }
3333
3334 self.scene_graph.objects = updated_objects;
3335 }
3336 outcome
3337 }
3338 }
3339
3340 fn only_sketch_block(
3341 &self,
3342 sketch_id: ObjectId,
3343 edit_kind: ChangeKind,
3344 ast: &mut ast::Node<ast::Program>,
3345 ) -> api::Result<()> {
3346 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
3347 msg: format!("Sketch not found: {sketch_id:?}"),
3348 })?;
3349 let ObjectKind::Sketch(_) = &sketch_object.kind else {
3350 return Err(Error {
3351 msg: format!("Object is not a sketch: {sketch_object:?}"),
3352 });
3353 };
3354 let sketch_block_range = expect_single_source_range(&sketch_object.source)?;
3355 only_sketch_block(ast, sketch_block_range, edit_kind)
3356 }
3357
3358 fn mutate_ast(
3359 &mut self,
3360 ast: &mut ast::Node<ast::Program>,
3361 object_id: ObjectId,
3362 command: AstMutateCommand,
3363 ) -> api::Result<(SourceRange, AstMutateCommandReturn)> {
3364 let sketch_object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
3365 msg: format!("Object not found: {object_id:?}"),
3366 })?;
3367 match &sketch_object.source {
3368 SourceRef::Simple { range } => mutate_ast_node_by_source_range(ast, *range, command),
3369 SourceRef::BackTrace { .. } => Err(Error {
3370 msg: "BackTrace source refs not supported yet".to_owned(),
3371 }),
3372 }
3373 }
3374}
3375
3376fn expect_single_source_range(source_ref: &SourceRef) -> api::Result<SourceRange> {
3377 match source_ref {
3378 SourceRef::Simple { range } => Ok(*range),
3379 SourceRef::BackTrace { ranges } => {
3380 if ranges.len() != 1 {
3381 return Err(Error {
3382 msg: format!(
3383 "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
3384 ranges.len(),
3385 ),
3386 });
3387 }
3388 Ok(ranges[0])
3389 }
3390 }
3391}
3392
3393fn only_sketch_block(
3394 ast: &mut ast::Node<ast::Program>,
3395 sketch_block_range: SourceRange,
3396 edit_kind: ChangeKind,
3397) -> api::Result<()> {
3398 let r1 = sketch_block_range;
3399 let matches_range = |r2: SourceRange| -> bool {
3400 match edit_kind {
3403 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
3404 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
3406 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
3407 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
3409 }
3410 };
3411 let mut found = false;
3412 for item in ast.body.iter_mut() {
3413 match item {
3414 ast::BodyItem::ImportStatement(_) => {}
3415 ast::BodyItem::ExpressionStatement(node) => {
3416 if matches_range(SourceRange::from(&*node))
3417 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
3418 {
3419 sketch_block.is_being_edited = true;
3420 found = true;
3421 break;
3422 }
3423 }
3424 ast::BodyItem::VariableDeclaration(node) => {
3425 if matches_range(SourceRange::from(&node.declaration.init))
3426 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
3427 {
3428 sketch_block.is_being_edited = true;
3429 found = true;
3430 break;
3431 }
3432 }
3433 ast::BodyItem::TypeDeclaration(_) => {}
3434 ast::BodyItem::ReturnStatement(node) => {
3435 if matches_range(SourceRange::from(&node.argument))
3436 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
3437 {
3438 sketch_block.is_being_edited = true;
3439 found = true;
3440 break;
3441 }
3442 }
3443 }
3444 }
3445 if !found {
3446 return Err(Error {
3447 msg: format!("Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"),
3448 });
3449 }
3450
3451 Ok(())
3452}
3453
3454fn sketch_on_ast_expr(
3455 ast: &mut ast::Node<ast::Program>,
3456 scene_graph: &SceneGraph,
3457 on: &Plane,
3458) -> api::Result<ast::Expr> {
3459 match on {
3460 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
3461 Plane::Object(object_id) => {
3462 let on_object = scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
3463 msg: format!("Sketch plane object not found: {object_id:?}"),
3464 })?;
3465 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
3466 }
3467 }
3468}
3469
3470fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
3471 use crate::engine::PlaneName;
3472
3473 match name {
3474 PlaneName::Xy => ast_name_expr("XY".to_owned()),
3475 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
3476 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
3477 PlaneName::NegXy => negated_plane_ast_expr("XY"),
3478 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
3479 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
3480 }
3481}
3482
3483fn negated_plane_ast_expr(name: &str) -> ast::Expr {
3484 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
3485 ast::UnaryOperator::Neg,
3486 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
3487 )))
3488}
3489
3490fn get_or_insert_ast_reference(
3497 ast: &mut ast::Node<ast::Program>,
3498 source_ref: &SourceRef,
3499 prefix: &str,
3500 property: Option<&str>,
3501) -> api::Result<ast::Expr> {
3502 let range = expect_single_source_range(source_ref)?;
3503 let command = AstMutateCommand::AddVariableDeclaration {
3504 prefix: prefix.to_owned(),
3505 };
3506 let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
3507 let AstMutateCommandReturn::Name(var_name) = ret else {
3508 return Err(Error {
3509 msg: "Expected variable name returned from AddVariableDeclaration".to_owned(),
3510 });
3511 };
3512 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
3513 let Some(property) = property else {
3514 return Ok(var_expr);
3516 };
3517
3518 Ok(create_member_expression(var_expr, property))
3519}
3520
3521fn mutate_ast_node_by_source_range(
3522 ast: &mut ast::Node<ast::Program>,
3523 source_range: SourceRange,
3524 command: AstMutateCommand,
3525) -> Result<(SourceRange, AstMutateCommandReturn), Error> {
3526 let mut context = AstMutateContext {
3527 source_range,
3528 command,
3529 defined_names_stack: Default::default(),
3530 };
3531 let control = dfs_mut(ast, &mut context);
3532 match control {
3533 ControlFlow::Continue(_) => Err(Error {
3534 msg: format!("Source range not found: {source_range:?}"),
3535 }),
3536 ControlFlow::Break(break_value) => break_value,
3537 }
3538}
3539
3540#[derive(Debug)]
3541struct AstMutateContext {
3542 source_range: SourceRange,
3543 command: AstMutateCommand,
3544 defined_names_stack: Vec<HashSet<String>>,
3545}
3546
3547#[derive(Debug)]
3548#[allow(clippy::large_enum_variant)]
3549enum AstMutateCommand {
3550 AddSketchBlockExprStmt {
3552 expr: ast::Expr,
3553 },
3554 AddVariableDeclaration {
3555 prefix: String,
3556 },
3557 EditPoint {
3558 at: ast::Expr,
3559 },
3560 EditLine {
3561 start: ast::Expr,
3562 end: ast::Expr,
3563 construction: Option<bool>,
3564 },
3565 EditArc {
3566 start: ast::Expr,
3567 end: ast::Expr,
3568 center: ast::Expr,
3569 construction: Option<bool>,
3570 },
3571 EditCircle {
3572 start: ast::Expr,
3573 center: ast::Expr,
3574 construction: Option<bool>,
3575 },
3576 EditConstraintValue {
3577 value: ast::BinaryPart,
3578 },
3579 EditCallUnlabeled {
3580 arg: ast::Expr,
3581 },
3582 #[cfg(feature = "artifact-graph")]
3583 EditVarInitialValue {
3584 value: Number,
3585 },
3586 DeleteNode,
3587}
3588
3589#[derive(Debug)]
3590enum AstMutateCommandReturn {
3591 None,
3592 Name(String),
3593}
3594
3595impl Visitor for AstMutateContext {
3596 type Break = Result<(SourceRange, AstMutateCommandReturn), Error>;
3597 type Continue = ();
3598
3599 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
3600 filter_and_process(self, node)
3601 }
3602
3603 fn finish(&mut self, node: NodeMut<'_>) {
3604 match &node {
3605 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
3606 self.defined_names_stack.pop();
3607 }
3608 _ => {}
3609 }
3610 }
3611}
3612
3613fn filter_and_process(
3614 ctx: &mut AstMutateContext,
3615 node: NodeMut,
3616) -> TraversalReturn<Result<(SourceRange, AstMutateCommandReturn), Error>> {
3617 let Ok(node_range) = SourceRange::try_from(&node) else {
3618 return TraversalReturn::new_continue(());
3620 };
3621 if let NodeMut::VariableDeclaration(var_decl) = &node {
3626 let expr_range = SourceRange::from(&var_decl.declaration.init);
3627 if expr_range == ctx.source_range {
3628 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
3629 return TraversalReturn::new_break(Ok((
3632 node_range,
3633 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
3634 )));
3635 }
3636 if let AstMutateCommand::DeleteNode = &ctx.command {
3637 return TraversalReturn {
3640 mutate_body_item: MutateBodyItem::Delete,
3641 control_flow: ControlFlow::Break(Ok((ctx.source_range, AstMutateCommandReturn::None))),
3642 };
3643 }
3644 }
3645 }
3646
3647 if let NodeMut::Program(program) = &node {
3648 ctx.defined_names_stack.push(find_defined_names(*program));
3649 } else if let NodeMut::SketchBlock(block) = &node {
3650 ctx.defined_names_stack.push(find_defined_names(&block.body));
3651 }
3652
3653 if node_range != ctx.source_range {
3655 return TraversalReturn::new_continue(());
3656 }
3657 process(ctx, node).map_break(|result| result.map(|cmd_return| (ctx.source_range, cmd_return)))
3658}
3659
3660fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, Error>> {
3661 match &ctx.command {
3662 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
3663 if let NodeMut::SketchBlock(sketch_block) = node {
3664 sketch_block
3665 .body
3666 .items
3667 .push(ast::BodyItem::ExpressionStatement(ast::Node {
3668 inner: ast::ExpressionStatement {
3669 expression: expr.clone(),
3670 digest: None,
3671 },
3672 start: Default::default(),
3673 end: Default::default(),
3674 module_id: Default::default(),
3675 outer_attrs: Default::default(),
3676 pre_comments: Default::default(),
3677 comment_start: Default::default(),
3678 }));
3679 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3680 }
3681 }
3682 AstMutateCommand::AddVariableDeclaration { prefix } => {
3683 if let NodeMut::VariableDeclaration(inner) = node {
3684 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
3685 }
3686 if let NodeMut::ExpressionStatement(expr_stmt) = node {
3687 let empty_defined_names = HashSet::new();
3688 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
3689 let Ok(name) = next_free_name(prefix, defined_names) else {
3690 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3692 };
3693 let mutate_node =
3694 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
3695 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
3696 ast::ItemVisibility::Default,
3697 ast::VariableKind::Const,
3698 ))));
3699 return TraversalReturn {
3700 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
3701 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
3702 };
3703 }
3704 }
3705 AstMutateCommand::EditPoint { at } => {
3706 if let NodeMut::CallExpressionKw(call) = node {
3707 if call.callee.name.name != POINT_FN {
3708 return TraversalReturn::new_continue(());
3709 }
3710 for labeled_arg in &mut call.arguments {
3712 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
3713 labeled_arg.arg = at.clone();
3714 }
3715 }
3716 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3717 }
3718 }
3719 AstMutateCommand::EditLine {
3720 start,
3721 end,
3722 construction,
3723 } => {
3724 if let NodeMut::CallExpressionKw(call) = node {
3725 if call.callee.name.name != LINE_FN {
3726 return TraversalReturn::new_continue(());
3727 }
3728 for labeled_arg in &mut call.arguments {
3730 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
3731 labeled_arg.arg = start.clone();
3732 }
3733 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
3734 labeled_arg.arg = end.clone();
3735 }
3736 }
3737 if let Some(construction_value) = construction {
3739 let construction_exists = call
3740 .arguments
3741 .iter()
3742 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
3743 if *construction_value {
3744 if construction_exists {
3746 for labeled_arg in &mut call.arguments {
3748 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
3749 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3750 value: ast::LiteralValue::Bool(true),
3751 raw: "true".to_string(),
3752 digest: None,
3753 })));
3754 }
3755 }
3756 } else {
3757 call.arguments.push(ast::LabeledArg {
3759 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
3760 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3761 value: ast::LiteralValue::Bool(true),
3762 raw: "true".to_string(),
3763 digest: None,
3764 }))),
3765 });
3766 }
3767 } else {
3768 call.arguments
3770 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
3771 }
3772 }
3773 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3774 }
3775 }
3776 AstMutateCommand::EditArc {
3777 start,
3778 end,
3779 center,
3780 construction,
3781 } => {
3782 if let NodeMut::CallExpressionKw(call) = node {
3783 if call.callee.name.name != ARC_FN {
3784 return TraversalReturn::new_continue(());
3785 }
3786 for labeled_arg in &mut call.arguments {
3788 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
3789 labeled_arg.arg = start.clone();
3790 }
3791 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
3792 labeled_arg.arg = end.clone();
3793 }
3794 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
3795 labeled_arg.arg = center.clone();
3796 }
3797 }
3798 if let Some(construction_value) = construction {
3800 let construction_exists = call
3801 .arguments
3802 .iter()
3803 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
3804 if *construction_value {
3805 if construction_exists {
3807 for labeled_arg in &mut call.arguments {
3809 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
3810 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3811 value: ast::LiteralValue::Bool(true),
3812 raw: "true".to_string(),
3813 digest: None,
3814 })));
3815 }
3816 }
3817 } else {
3818 call.arguments.push(ast::LabeledArg {
3820 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
3821 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3822 value: ast::LiteralValue::Bool(true),
3823 raw: "true".to_string(),
3824 digest: None,
3825 }))),
3826 });
3827 }
3828 } else {
3829 call.arguments
3831 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
3832 }
3833 }
3834 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3835 }
3836 }
3837 AstMutateCommand::EditCircle {
3838 start,
3839 center,
3840 construction,
3841 } => {
3842 if let NodeMut::CallExpressionKw(call) = node {
3843 if call.callee.name.name != CIRCLE_FN {
3844 return TraversalReturn::new_continue(());
3845 }
3846 for labeled_arg in &mut call.arguments {
3848 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
3849 labeled_arg.arg = start.clone();
3850 }
3851 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
3852 labeled_arg.arg = center.clone();
3853 }
3854 }
3855 if let Some(construction_value) = construction {
3857 let construction_exists = call
3858 .arguments
3859 .iter()
3860 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
3861 if *construction_value {
3862 if construction_exists {
3863 for labeled_arg in &mut call.arguments {
3864 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
3865 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3866 value: ast::LiteralValue::Bool(true),
3867 raw: "true".to_string(),
3868 digest: None,
3869 })));
3870 }
3871 }
3872 } else {
3873 call.arguments.push(ast::LabeledArg {
3874 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
3875 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3876 value: ast::LiteralValue::Bool(true),
3877 raw: "true".to_string(),
3878 digest: None,
3879 }))),
3880 });
3881 }
3882 } else {
3883 call.arguments
3884 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
3885 }
3886 }
3887 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3888 }
3889 }
3890 AstMutateCommand::EditConstraintValue { value } => {
3891 if let NodeMut::BinaryExpression(binary_expr) = node {
3892 let left_is_constraint = matches!(
3893 &binary_expr.left,
3894 ast::BinaryPart::CallExpressionKw(call)
3895 if matches!(
3896 call.callee.name.name.as_str(),
3897 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
3898 )
3899 );
3900 if left_is_constraint {
3901 binary_expr.right = value.clone();
3902 } else {
3903 binary_expr.left = value.clone();
3904 }
3905
3906 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3907 }
3908 }
3909 AstMutateCommand::EditCallUnlabeled { arg } => {
3910 if let NodeMut::CallExpressionKw(call) = node {
3911 call.unlabeled = Some(arg.clone());
3912 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3913 }
3914 }
3915 #[cfg(feature = "artifact-graph")]
3916 AstMutateCommand::EditVarInitialValue { value } => {
3917 if let NodeMut::NumericLiteral(numeric_literal) = node {
3918 let Ok(literal) = to_source_number(*value) else {
3920 return TraversalReturn::new_break(Err(Error {
3921 msg: format!("Could not convert number to AST literal: {:?}", *value),
3922 }));
3923 };
3924 *numeric_literal = ast::Node::no_src(literal);
3925 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3926 }
3927 }
3928 AstMutateCommand::DeleteNode => {
3929 return TraversalReturn {
3930 mutate_body_item: MutateBodyItem::Delete,
3931 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
3932 };
3933 }
3934 }
3935 TraversalReturn::new_continue(())
3936}
3937
3938struct FindSketchBlockSourceRange {
3939 target_before_mutation: SourceRange,
3941 found: Cell<Option<SourceRange>>,
3945}
3946
3947impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
3948 type Error = crate::front::Error;
3949
3950 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
3951 let Ok(node_range) = SourceRange::try_from(&node) else {
3952 return Ok(true);
3953 };
3954
3955 if let crate::walk::Node::SketchBlock(sketch_block) = node {
3956 if node_range.module_id() == self.target_before_mutation.module_id()
3957 && node_range.start() == self.target_before_mutation.start()
3958 && node_range.end() >= self.target_before_mutation.end()
3960 {
3961 self.found.set(sketch_block.body.items.last().map(SourceRange::from));
3962 return Ok(false);
3963 } else {
3964 return Ok(true);
3967 }
3968 }
3969
3970 for child in node.children().iter() {
3971 if !child.visit(*self)? {
3972 return Ok(false);
3973 }
3974 }
3975
3976 Ok(true)
3977 }
3978}
3979
3980fn find_sketch_block_added_item(
3988 ast: &ast::Node<ast::Program>,
3989 range_before_mutation: SourceRange,
3990) -> api::Result<SourceRange> {
3991 let find = FindSketchBlockSourceRange {
3992 target_before_mutation: range_before_mutation,
3993 found: Cell::new(None),
3994 };
3995 let node = crate::walk::Node::from(ast);
3996 node.visit(&find)?;
3997 find.found.into_inner().ok_or_else(|| api::Error {
3998 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?"),
3999 })
4000}
4001
4002fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
4003 ast.recast_top(&Default::default(), 0)
4005}
4006
4007pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
4008 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
4009 inner: ast::ArrayExpression {
4010 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
4011 non_code_meta: Default::default(),
4012 digest: None,
4013 },
4014 start: Default::default(),
4015 end: Default::default(),
4016 module_id: Default::default(),
4017 outer_attrs: Default::default(),
4018 pre_comments: Default::default(),
4019 comment_start: Default::default(),
4020 })))
4021}
4022
4023fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
4024 match expr {
4025 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
4026 inner: ast::Literal::from(to_source_number(*number)?),
4027 start: Default::default(),
4028 end: Default::default(),
4029 module_id: Default::default(),
4030 outer_attrs: Default::default(),
4031 pre_comments: Default::default(),
4032 comment_start: Default::default(),
4033 }))),
4034 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
4035 inner: ast::SketchVar {
4036 initial: Some(Box::new(ast::Node {
4037 inner: to_source_number(*number)?,
4038 start: Default::default(),
4039 end: Default::default(),
4040 module_id: Default::default(),
4041 outer_attrs: Default::default(),
4042 pre_comments: Default::default(),
4043 comment_start: Default::default(),
4044 })),
4045 digest: None,
4046 },
4047 start: Default::default(),
4048 end: Default::default(),
4049 module_id: Default::default(),
4050 outer_attrs: Default::default(),
4051 pre_comments: Default::default(),
4052 comment_start: Default::default(),
4053 }))),
4054 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
4055 }
4056}
4057
4058fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
4059 Ok(ast::NumericLiteral {
4060 value: number.value,
4061 suffix: number.units,
4062 raw: format_number_literal(number.value, number.units)?,
4063 digest: None,
4064 })
4065}
4066
4067pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
4068 ast::Expr::Name(Box::new(ast_name(name)))
4069}
4070
4071fn ast_name(name: String) -> ast::Node<ast::Name> {
4072 ast::Node {
4073 inner: ast::Name {
4074 name: ast::Node {
4075 inner: ast::Identifier { name, digest: None },
4076 start: Default::default(),
4077 end: Default::default(),
4078 module_id: Default::default(),
4079 outer_attrs: Default::default(),
4080 pre_comments: Default::default(),
4081 comment_start: Default::default(),
4082 },
4083 path: Vec::new(),
4084 abs_path: false,
4085 digest: None,
4086 },
4087 start: Default::default(),
4088 end: Default::default(),
4089 module_id: Default::default(),
4090 outer_attrs: Default::default(),
4091 pre_comments: Default::default(),
4092 comment_start: Default::default(),
4093 }
4094}
4095
4096pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
4097 ast::Name {
4098 name: ast::Node {
4099 inner: ast::Identifier {
4100 name: name.to_owned(),
4101 digest: None,
4102 },
4103 start: Default::default(),
4104 end: Default::default(),
4105 module_id: Default::default(),
4106 outer_attrs: Default::default(),
4107 pre_comments: Default::default(),
4108 comment_start: Default::default(),
4109 },
4110 path: Default::default(),
4111 abs_path: false,
4112 digest: None,
4113 }
4114}
4115
4116pub(crate) fn create_coincident_ast(expr1: ast::Expr, expr2: ast::Expr) -> ast::Expr {
4120 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
4122 elements: vec![expr1, expr2],
4123 digest: None,
4124 non_code_meta: Default::default(),
4125 })));
4126
4127 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4129 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
4130 unlabeled: Some(array_expr),
4131 arguments: Default::default(),
4132 digest: None,
4133 non_code_meta: Default::default(),
4134 })))
4135}
4136
4137pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
4139 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4140 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
4141 unlabeled: None,
4142 arguments: vec![
4143 ast::LabeledArg {
4144 label: Some(ast::Identifier::new(LINE_START_PARAM)),
4145 arg: start_ast,
4146 },
4147 ast::LabeledArg {
4148 label: Some(ast::Identifier::new(LINE_END_PARAM)),
4149 arg: end_ast,
4150 },
4151 ],
4152 digest: None,
4153 non_code_meta: Default::default(),
4154 })))
4155}
4156
4157pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
4159 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4160 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
4161 unlabeled: None,
4162 arguments: vec![
4163 ast::LabeledArg {
4164 label: Some(ast::Identifier::new(ARC_START_PARAM)),
4165 arg: start_ast,
4166 },
4167 ast::LabeledArg {
4168 label: Some(ast::Identifier::new(ARC_END_PARAM)),
4169 arg: end_ast,
4170 },
4171 ast::LabeledArg {
4172 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
4173 arg: center_ast,
4174 },
4175 ],
4176 digest: None,
4177 non_code_meta: Default::default(),
4178 })))
4179}
4180
4181pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
4183 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4184 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
4185 unlabeled: None,
4186 arguments: vec![
4187 ast::LabeledArg {
4188 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
4189 arg: start_ast,
4190 },
4191 ast::LabeledArg {
4192 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
4193 arg: center_ast,
4194 },
4195 ],
4196 digest: None,
4197 non_code_meta: Default::default(),
4198 })))
4199}
4200
4201pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
4203 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4204 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
4205 unlabeled: Some(line_expr),
4206 arguments: Default::default(),
4207 digest: None,
4208 non_code_meta: Default::default(),
4209 })))
4210}
4211
4212pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
4214 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4215 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
4216 unlabeled: Some(line_expr),
4217 arguments: Default::default(),
4218 digest: None,
4219 non_code_meta: Default::default(),
4220 })))
4221}
4222
4223pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
4225 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
4226 object: object_expr,
4227 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
4228 name: ast::Node::no_src(ast::Identifier {
4229 name: property.to_string(),
4230 digest: None,
4231 }),
4232 path: Vec::new(),
4233 abs_path: false,
4234 digest: None,
4235 }))),
4236 computed: false,
4237 digest: None,
4238 })))
4239}
4240
4241pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
4243 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
4244 elements: line_exprs,
4245 digest: None,
4246 non_code_meta: Default::default(),
4247 })));
4248
4249 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4251 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
4252 unlabeled: Some(array_expr),
4253 arguments: Default::default(),
4254 digest: None,
4255 non_code_meta: Default::default(),
4256 })))
4257}
4258
4259pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
4261 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
4262 elements: vec![seg1_expr, seg2_expr],
4263 digest: None,
4264 non_code_meta: Default::default(),
4265 })));
4266
4267 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4268 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
4269 unlabeled: Some(array_expr),
4270 arguments: Default::default(),
4271 digest: None,
4272 non_code_meta: Default::default(),
4273 })))
4274}
4275
4276#[cfg(all(feature = "artifact-graph", test))]
4277mod tests {
4278 use super::*;
4279 use crate::engine::PlaneName;
4280 use crate::front::Distance;
4281 use crate::front::Object;
4282 use crate::front::Plane;
4283 use crate::front::Sketch;
4284 use crate::front::Tangent;
4285 use crate::frontend::sketch::Vertical;
4286 use crate::pretty::NumericSuffix;
4287
4288 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
4289 for object in &scene_graph.objects {
4290 if let ObjectKind::Sketch(_) = &object.kind {
4291 return Some(object);
4292 }
4293 }
4294 None
4295 }
4296
4297 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
4298 for object in &scene_graph.objects {
4299 if let ObjectKind::Face(_) = &object.kind {
4300 return Some(object);
4301 }
4302 }
4303 None
4304 }
4305
4306 #[track_caller]
4307 fn expect_sketch(object: &Object) -> &Sketch {
4308 if let ObjectKind::Sketch(sketch) = &object.kind {
4309 sketch
4310 } else {
4311 panic!("Object is not a sketch: {:?}", object);
4312 }
4313 }
4314
4315 #[tokio::test(flavor = "multi_thread")]
4316 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
4317 let source = "\
4318@settings(experimentalFeatures = allow)
4319
4320sketch(on = XY) {
4321 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
4322}
4323
4324bad = missing_name
4325";
4326 let program = Program::parse(source).unwrap().0.unwrap();
4327
4328 let mut frontend = FrontendState::new();
4329
4330 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4331 let mock_ctx = ExecutorContext::new_mock(None).await;
4332 let version = Version(0);
4333 let project_id = ProjectId(0);
4334 let file_id = FileId(0);
4335
4336 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
4337 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
4338 };
4339
4340 let sketch_id = frontend
4341 .scene_graph
4342 .objects
4343 .iter()
4344 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
4345 .expect("Expected sketch object from errored hack_set_program");
4346
4347 frontend
4348 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
4349 .await
4350 .unwrap();
4351
4352 ctx.close().await;
4353 mock_ctx.close().await;
4354 }
4355
4356 #[tokio::test(flavor = "multi_thread")]
4357 async fn test_new_sketch_add_point_edit_point() {
4358 let program = Program::empty();
4359
4360 let mut frontend = FrontendState::new();
4361 frontend.program = program;
4362
4363 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4364 let mock_ctx = ExecutorContext::new_mock(None).await;
4365 let version = Version(0);
4366
4367 let sketch_args = SketchCtor {
4368 on: Plane::Default(PlaneName::Xy),
4369 };
4370 let (_src_delta, scene_delta, sketch_id) = frontend
4371 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4372 .await
4373 .unwrap();
4374 assert_eq!(sketch_id, ObjectId(1));
4375 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
4376 let sketch_object = &scene_delta.new_graph.objects[1];
4377 assert_eq!(sketch_object.id, ObjectId(1));
4378 assert_eq!(
4379 sketch_object.kind,
4380 ObjectKind::Sketch(Sketch {
4381 args: SketchCtor {
4382 on: Plane::Default(PlaneName::Xy)
4383 },
4384 plane: ObjectId(0),
4385 segments: vec![],
4386 constraints: vec![],
4387 })
4388 );
4389 assert_eq!(scene_delta.new_graph.objects.len(), 2);
4390
4391 let point_ctor = PointCtor {
4392 position: Point2d {
4393 x: Expr::Number(Number {
4394 value: 1.0,
4395 units: NumericSuffix::Inch,
4396 }),
4397 y: Expr::Number(Number {
4398 value: 2.0,
4399 units: NumericSuffix::Inch,
4400 }),
4401 },
4402 };
4403 let segment = SegmentCtor::Point(point_ctor);
4404 let (src_delta, scene_delta) = frontend
4405 .add_segment(&mock_ctx, version, sketch_id, segment, None)
4406 .await
4407 .unwrap();
4408 assert_eq!(
4409 src_delta.text.as_str(),
4410 "@settings(experimentalFeatures = allow)
4411
4412sketch001 = sketch(on = XY) {
4413 point(at = [1in, 2in])
4414}
4415"
4416 );
4417 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
4418 assert_eq!(scene_delta.new_graph.objects.len(), 3);
4419 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
4420 assert_eq!(scene_object.id.0, i);
4421 }
4422
4423 let point_id = *scene_delta.new_objects.last().unwrap();
4424
4425 let point_ctor = PointCtor {
4426 position: Point2d {
4427 x: Expr::Number(Number {
4428 value: 3.0,
4429 units: NumericSuffix::Inch,
4430 }),
4431 y: Expr::Number(Number {
4432 value: 4.0,
4433 units: NumericSuffix::Inch,
4434 }),
4435 },
4436 };
4437 let segments = vec![ExistingSegmentCtor {
4438 id: point_id,
4439 ctor: SegmentCtor::Point(point_ctor),
4440 }];
4441 let (src_delta, scene_delta) = frontend
4442 .edit_segments(&mock_ctx, version, sketch_id, segments)
4443 .await
4444 .unwrap();
4445 assert_eq!(
4446 src_delta.text.as_str(),
4447 "@settings(experimentalFeatures = allow)
4448
4449sketch001 = sketch(on = XY) {
4450 point(at = [3in, 4in])
4451}
4452"
4453 );
4454 assert_eq!(scene_delta.new_objects, vec![]);
4455 assert_eq!(scene_delta.new_graph.objects.len(), 3);
4456
4457 ctx.close().await;
4458 mock_ctx.close().await;
4459 }
4460
4461 #[tokio::test(flavor = "multi_thread")]
4462 async fn test_new_sketch_add_line_edit_line() {
4463 let program = Program::empty();
4464
4465 let mut frontend = FrontendState::new();
4466 frontend.program = program;
4467
4468 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4469 let mock_ctx = ExecutorContext::new_mock(None).await;
4470 let version = Version(0);
4471
4472 let sketch_args = SketchCtor {
4473 on: Plane::Default(PlaneName::Xy),
4474 };
4475 let (_src_delta, scene_delta, sketch_id) = frontend
4476 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4477 .await
4478 .unwrap();
4479 assert_eq!(sketch_id, ObjectId(1));
4480 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
4481 let sketch_object = &scene_delta.new_graph.objects[1];
4482 assert_eq!(sketch_object.id, ObjectId(1));
4483 assert_eq!(
4484 sketch_object.kind,
4485 ObjectKind::Sketch(Sketch {
4486 args: SketchCtor {
4487 on: Plane::Default(PlaneName::Xy)
4488 },
4489 plane: ObjectId(0),
4490 segments: vec![],
4491 constraints: vec![],
4492 })
4493 );
4494 assert_eq!(scene_delta.new_graph.objects.len(), 2);
4495
4496 let line_ctor = LineCtor {
4497 start: Point2d {
4498 x: Expr::Number(Number {
4499 value: 0.0,
4500 units: NumericSuffix::Mm,
4501 }),
4502 y: Expr::Number(Number {
4503 value: 0.0,
4504 units: NumericSuffix::Mm,
4505 }),
4506 },
4507 end: Point2d {
4508 x: Expr::Number(Number {
4509 value: 10.0,
4510 units: NumericSuffix::Mm,
4511 }),
4512 y: Expr::Number(Number {
4513 value: 10.0,
4514 units: NumericSuffix::Mm,
4515 }),
4516 },
4517 construction: None,
4518 };
4519 let segment = SegmentCtor::Line(line_ctor);
4520 let (src_delta, scene_delta) = frontend
4521 .add_segment(&mock_ctx, version, sketch_id, segment, None)
4522 .await
4523 .unwrap();
4524 assert_eq!(
4525 src_delta.text.as_str(),
4526 "@settings(experimentalFeatures = allow)
4527
4528sketch001 = sketch(on = XY) {
4529 line(start = [0mm, 0mm], end = [10mm, 10mm])
4530}
4531"
4532 );
4533 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
4534 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4535 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
4536 assert_eq!(scene_object.id.0, i);
4537 }
4538
4539 let line = *scene_delta.new_objects.last().unwrap();
4541
4542 let line_ctor = LineCtor {
4543 start: Point2d {
4544 x: Expr::Number(Number {
4545 value: 1.0,
4546 units: NumericSuffix::Mm,
4547 }),
4548 y: Expr::Number(Number {
4549 value: 2.0,
4550 units: NumericSuffix::Mm,
4551 }),
4552 },
4553 end: Point2d {
4554 x: Expr::Number(Number {
4555 value: 13.0,
4556 units: NumericSuffix::Mm,
4557 }),
4558 y: Expr::Number(Number {
4559 value: 14.0,
4560 units: NumericSuffix::Mm,
4561 }),
4562 },
4563 construction: None,
4564 };
4565 let segments = vec![ExistingSegmentCtor {
4566 id: line,
4567 ctor: SegmentCtor::Line(line_ctor),
4568 }];
4569 let (src_delta, scene_delta) = frontend
4570 .edit_segments(&mock_ctx, version, sketch_id, segments)
4571 .await
4572 .unwrap();
4573 assert_eq!(
4574 src_delta.text.as_str(),
4575 "@settings(experimentalFeatures = allow)
4576
4577sketch001 = sketch(on = XY) {
4578 line(start = [1mm, 2mm], end = [13mm, 14mm])
4579}
4580"
4581 );
4582 assert_eq!(scene_delta.new_objects, vec![]);
4583 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4584
4585 ctx.close().await;
4586 mock_ctx.close().await;
4587 }
4588
4589 #[tokio::test(flavor = "multi_thread")]
4590 async fn test_new_sketch_add_arc_edit_arc() {
4591 let program = Program::empty();
4592
4593 let mut frontend = FrontendState::new();
4594 frontend.program = program;
4595
4596 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4597 let mock_ctx = ExecutorContext::new_mock(None).await;
4598 let version = Version(0);
4599
4600 let sketch_args = SketchCtor {
4601 on: Plane::Default(PlaneName::Xy),
4602 };
4603 let (_src_delta, scene_delta, sketch_id) = frontend
4604 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4605 .await
4606 .unwrap();
4607 assert_eq!(sketch_id, ObjectId(1));
4608 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
4609 let sketch_object = &scene_delta.new_graph.objects[1];
4610 assert_eq!(sketch_object.id, ObjectId(1));
4611 assert_eq!(
4612 sketch_object.kind,
4613 ObjectKind::Sketch(Sketch {
4614 args: SketchCtor {
4615 on: Plane::Default(PlaneName::Xy),
4616 },
4617 plane: ObjectId(0),
4618 segments: vec![],
4619 constraints: vec![],
4620 })
4621 );
4622 assert_eq!(scene_delta.new_graph.objects.len(), 2);
4623
4624 let arc_ctor = ArcCtor {
4625 start: Point2d {
4626 x: Expr::Var(Number {
4627 value: 0.0,
4628 units: NumericSuffix::Mm,
4629 }),
4630 y: Expr::Var(Number {
4631 value: 0.0,
4632 units: NumericSuffix::Mm,
4633 }),
4634 },
4635 end: Point2d {
4636 x: Expr::Var(Number {
4637 value: 10.0,
4638 units: NumericSuffix::Mm,
4639 }),
4640 y: Expr::Var(Number {
4641 value: 10.0,
4642 units: NumericSuffix::Mm,
4643 }),
4644 },
4645 center: Point2d {
4646 x: Expr::Var(Number {
4647 value: 10.0,
4648 units: NumericSuffix::Mm,
4649 }),
4650 y: Expr::Var(Number {
4651 value: 0.0,
4652 units: NumericSuffix::Mm,
4653 }),
4654 },
4655 construction: None,
4656 };
4657 let segment = SegmentCtor::Arc(arc_ctor);
4658 let (src_delta, scene_delta) = frontend
4659 .add_segment(&mock_ctx, version, sketch_id, segment, None)
4660 .await
4661 .unwrap();
4662 assert_eq!(
4663 src_delta.text.as_str(),
4664 "@settings(experimentalFeatures = allow)
4665
4666sketch001 = sketch(on = XY) {
4667 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
4668}
4669"
4670 );
4671 assert_eq!(
4672 scene_delta.new_objects,
4673 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
4674 );
4675 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
4676 assert_eq!(scene_object.id.0, i);
4677 }
4678 assert_eq!(scene_delta.new_graph.objects.len(), 6);
4679
4680 let arc = *scene_delta.new_objects.last().unwrap();
4682
4683 let arc_ctor = ArcCtor {
4684 start: Point2d {
4685 x: Expr::Var(Number {
4686 value: 1.0,
4687 units: NumericSuffix::Mm,
4688 }),
4689 y: Expr::Var(Number {
4690 value: 2.0,
4691 units: NumericSuffix::Mm,
4692 }),
4693 },
4694 end: Point2d {
4695 x: Expr::Var(Number {
4696 value: 13.0,
4697 units: NumericSuffix::Mm,
4698 }),
4699 y: Expr::Var(Number {
4700 value: 14.0,
4701 units: NumericSuffix::Mm,
4702 }),
4703 },
4704 center: Point2d {
4705 x: Expr::Var(Number {
4706 value: 13.0,
4707 units: NumericSuffix::Mm,
4708 }),
4709 y: Expr::Var(Number {
4710 value: 2.0,
4711 units: NumericSuffix::Mm,
4712 }),
4713 },
4714 construction: None,
4715 };
4716 let segments = vec![ExistingSegmentCtor {
4717 id: arc,
4718 ctor: SegmentCtor::Arc(arc_ctor),
4719 }];
4720 let (src_delta, scene_delta) = frontend
4721 .edit_segments(&mock_ctx, version, sketch_id, segments)
4722 .await
4723 .unwrap();
4724 assert_eq!(
4725 src_delta.text.as_str(),
4726 "@settings(experimentalFeatures = allow)
4727
4728sketch001 = sketch(on = XY) {
4729 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
4730}
4731"
4732 );
4733 assert_eq!(scene_delta.new_objects, vec![]);
4734 assert_eq!(scene_delta.new_graph.objects.len(), 6);
4735
4736 ctx.close().await;
4737 mock_ctx.close().await;
4738 }
4739
4740 #[tokio::test(flavor = "multi_thread")]
4741 async fn test_new_sketch_add_circle_edit_circle() {
4742 let program = Program::empty();
4743
4744 let mut frontend = FrontendState::new();
4745 frontend.program = program;
4746
4747 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4748 let mock_ctx = ExecutorContext::new_mock(None).await;
4749 let version = Version(0);
4750
4751 let sketch_args = SketchCtor {
4752 on: Plane::Default(PlaneName::Xy),
4753 };
4754 let (_src_delta, _scene_delta, sketch_id) = frontend
4755 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4756 .await
4757 .unwrap();
4758
4759 let circle_ctor = CircleCtor {
4761 start: Point2d {
4762 x: Expr::Var(Number {
4763 value: 5.0,
4764 units: NumericSuffix::Mm,
4765 }),
4766 y: Expr::Var(Number {
4767 value: 0.0,
4768 units: NumericSuffix::Mm,
4769 }),
4770 },
4771 center: Point2d {
4772 x: Expr::Var(Number {
4773 value: 0.0,
4774 units: NumericSuffix::Mm,
4775 }),
4776 y: Expr::Var(Number {
4777 value: 0.0,
4778 units: NumericSuffix::Mm,
4779 }),
4780 },
4781 construction: None,
4782 };
4783 let segment = SegmentCtor::Circle(circle_ctor);
4784 let (src_delta, scene_delta) = frontend
4785 .add_segment(&mock_ctx, version, sketch_id, segment, None)
4786 .await
4787 .unwrap();
4788 assert_eq!(
4789 src_delta.text.as_str(),
4790 "@settings(experimentalFeatures = allow)
4791
4792sketch001 = sketch(on = XY) {
4793 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
4794}
4795"
4796 );
4797 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
4799 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4800
4801 let circle = *scene_delta.new_objects.last().unwrap();
4802
4803 let circle_ctor = CircleCtor {
4805 start: Point2d {
4806 x: Expr::Var(Number {
4807 value: 10.0,
4808 units: NumericSuffix::Mm,
4809 }),
4810 y: Expr::Var(Number {
4811 value: 0.0,
4812 units: NumericSuffix::Mm,
4813 }),
4814 },
4815 center: Point2d {
4816 x: Expr::Var(Number {
4817 value: 3.0,
4818 units: NumericSuffix::Mm,
4819 }),
4820 y: Expr::Var(Number {
4821 value: 4.0,
4822 units: NumericSuffix::Mm,
4823 }),
4824 },
4825 construction: None,
4826 };
4827 let segments = vec![ExistingSegmentCtor {
4828 id: circle,
4829 ctor: SegmentCtor::Circle(circle_ctor),
4830 }];
4831 let (src_delta, scene_delta) = frontend
4832 .edit_segments(&mock_ctx, version, sketch_id, segments)
4833 .await
4834 .unwrap();
4835 assert_eq!(
4836 src_delta.text.as_str(),
4837 "@settings(experimentalFeatures = allow)
4838
4839sketch001 = sketch(on = XY) {
4840 circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
4841}
4842"
4843 );
4844 assert_eq!(scene_delta.new_objects, vec![]);
4845 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4846
4847 ctx.close().await;
4848 mock_ctx.close().await;
4849 }
4850
4851 #[tokio::test(flavor = "multi_thread")]
4852 async fn test_delete_circle() {
4853 let initial_source = "@settings(experimentalFeatures = allow)
4854
4855sketch001 = sketch(on = XY) {
4856 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
4857}
4858";
4859
4860 let program = Program::parse(initial_source).unwrap().0.unwrap();
4861 let mut frontend = FrontendState::new();
4862
4863 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4864 let mock_ctx = ExecutorContext::new_mock(None).await;
4865 let version = Version(0);
4866
4867 frontend.hack_set_program(&ctx, program).await.unwrap();
4868 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4869 let sketch_id = sketch_object.id;
4870 let sketch = expect_sketch(sketch_object);
4871
4872 assert_eq!(sketch.segments.len(), 3);
4874 let circle_id = sketch.segments[2];
4875
4876 let (src_delta, scene_delta) = frontend
4878 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
4879 .await
4880 .unwrap();
4881 assert_eq!(
4882 src_delta.text.as_str(),
4883 "@settings(experimentalFeatures = allow)
4884
4885sketch001 = sketch(on = XY) {
4886}
4887"
4888 );
4889 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
4890 let new_sketch = expect_sketch(new_sketch_object);
4891 assert_eq!(new_sketch.segments.len(), 0);
4892
4893 ctx.close().await;
4894 mock_ctx.close().await;
4895 }
4896
4897 #[tokio::test(flavor = "multi_thread")]
4898 async fn test_edit_circle_via_point() {
4899 let initial_source = "@settings(experimentalFeatures = allow)
4900
4901sketch001 = sketch(on = XY) {
4902 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
4903}
4904";
4905
4906 let program = Program::parse(initial_source).unwrap().0.unwrap();
4907 let mut frontend = FrontendState::new();
4908
4909 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4910 let mock_ctx = ExecutorContext::new_mock(None).await;
4911 let version = Version(0);
4912
4913 frontend.hack_set_program(&ctx, program).await.unwrap();
4914 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4915 let sketch_id = sketch_object.id;
4916 let sketch = expect_sketch(sketch_object);
4917
4918 let circle_id = sketch
4920 .segments
4921 .iter()
4922 .copied()
4923 .find(|seg_id| {
4924 matches!(
4925 &frontend.scene_graph.objects[seg_id.0].kind,
4926 ObjectKind::Segment {
4927 segment: Segment::Circle(_)
4928 }
4929 )
4930 })
4931 .expect("Expected a circle segment in sketch");
4932 let circle_object = &frontend.scene_graph.objects[circle_id.0];
4933 let ObjectKind::Segment {
4934 segment: Segment::Circle(circle),
4935 } = &circle_object.kind
4936 else {
4937 panic!("Expected circle segment, got: {:?}", circle_object.kind);
4938 };
4939 let start_point_id = circle.start;
4940
4941 let segments = vec![ExistingSegmentCtor {
4943 id: start_point_id,
4944 ctor: SegmentCtor::Point(PointCtor {
4945 position: Point2d {
4946 x: Expr::Var(Number {
4947 value: 7.0,
4948 units: NumericSuffix::Mm,
4949 }),
4950 y: Expr::Var(Number {
4951 value: 1.0,
4952 units: NumericSuffix::Mm,
4953 }),
4954 },
4955 }),
4956 }];
4957 let (src_delta, _scene_delta) = frontend
4958 .edit_segments(&mock_ctx, version, sketch_id, segments)
4959 .await
4960 .unwrap();
4961 assert_eq!(
4962 src_delta.text.as_str(),
4963 "@settings(experimentalFeatures = allow)
4964
4965sketch001 = sketch(on = XY) {
4966 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
4967}
4968"
4969 );
4970
4971 ctx.close().await;
4972 mock_ctx.close().await;
4973 }
4974
4975 #[tokio::test(flavor = "multi_thread")]
4976 async fn test_add_line_when_sketch_block_uses_variable() {
4977 let initial_source = "@settings(experimentalFeatures = allow)
4978
4979s = sketch(on = XY) {}
4980";
4981
4982 let program = Program::parse(initial_source).unwrap().0.unwrap();
4983
4984 let mut frontend = FrontendState::new();
4985
4986 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4987 let mock_ctx = ExecutorContext::new_mock(None).await;
4988 let version = Version(0);
4989
4990 frontend.hack_set_program(&ctx, program).await.unwrap();
4991 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4992 let sketch_id = sketch_object.id;
4993
4994 let line_ctor = LineCtor {
4995 start: Point2d {
4996 x: Expr::Number(Number {
4997 value: 0.0,
4998 units: NumericSuffix::Mm,
4999 }),
5000 y: Expr::Number(Number {
5001 value: 0.0,
5002 units: NumericSuffix::Mm,
5003 }),
5004 },
5005 end: Point2d {
5006 x: Expr::Number(Number {
5007 value: 10.0,
5008 units: NumericSuffix::Mm,
5009 }),
5010 y: Expr::Number(Number {
5011 value: 10.0,
5012 units: NumericSuffix::Mm,
5013 }),
5014 },
5015 construction: None,
5016 };
5017 let segment = SegmentCtor::Line(line_ctor);
5018 let (src_delta, scene_delta) = frontend
5019 .add_segment(&mock_ctx, version, sketch_id, segment, None)
5020 .await
5021 .unwrap();
5022 assert_eq!(
5023 src_delta.text.as_str(),
5024 "@settings(experimentalFeatures = allow)
5025
5026s = sketch(on = XY) {
5027 line(start = [0mm, 0mm], end = [10mm, 10mm])
5028}
5029"
5030 );
5031 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
5032 assert_eq!(scene_delta.new_graph.objects.len(), 5);
5033
5034 ctx.close().await;
5035 mock_ctx.close().await;
5036 }
5037
5038 #[tokio::test(flavor = "multi_thread")]
5039 async fn test_new_sketch_add_line_delete_sketch() {
5040 let program = Program::empty();
5041
5042 let mut frontend = FrontendState::new();
5043 frontend.program = program;
5044
5045 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5046 let mock_ctx = ExecutorContext::new_mock(None).await;
5047 let version = Version(0);
5048
5049 let sketch_args = SketchCtor {
5050 on: Plane::Default(PlaneName::Xy),
5051 };
5052 let (_src_delta, scene_delta, sketch_id) = frontend
5053 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5054 .await
5055 .unwrap();
5056 assert_eq!(sketch_id, ObjectId(1));
5057 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
5058 let sketch_object = &scene_delta.new_graph.objects[1];
5059 assert_eq!(sketch_object.id, ObjectId(1));
5060 assert_eq!(
5061 sketch_object.kind,
5062 ObjectKind::Sketch(Sketch {
5063 args: SketchCtor {
5064 on: Plane::Default(PlaneName::Xy)
5065 },
5066 plane: ObjectId(0),
5067 segments: vec![],
5068 constraints: vec![],
5069 })
5070 );
5071 assert_eq!(scene_delta.new_graph.objects.len(), 2);
5072
5073 let line_ctor = LineCtor {
5074 start: Point2d {
5075 x: Expr::Number(Number {
5076 value: 0.0,
5077 units: NumericSuffix::Mm,
5078 }),
5079 y: Expr::Number(Number {
5080 value: 0.0,
5081 units: NumericSuffix::Mm,
5082 }),
5083 },
5084 end: Point2d {
5085 x: Expr::Number(Number {
5086 value: 10.0,
5087 units: NumericSuffix::Mm,
5088 }),
5089 y: Expr::Number(Number {
5090 value: 10.0,
5091 units: NumericSuffix::Mm,
5092 }),
5093 },
5094 construction: None,
5095 };
5096 let segment = SegmentCtor::Line(line_ctor);
5097 let (src_delta, scene_delta) = frontend
5098 .add_segment(&mock_ctx, version, sketch_id, segment, None)
5099 .await
5100 .unwrap();
5101 assert_eq!(
5102 src_delta.text.as_str(),
5103 "@settings(experimentalFeatures = allow)
5104
5105sketch001 = sketch(on = XY) {
5106 line(start = [0mm, 0mm], end = [10mm, 10mm])
5107}
5108"
5109 );
5110 assert_eq!(scene_delta.new_graph.objects.len(), 5);
5111
5112 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
5113 assert_eq!(
5114 src_delta.text.as_str(),
5115 "@settings(experimentalFeatures = allow)
5116"
5117 );
5118 assert_eq!(scene_delta.new_graph.objects.len(), 0);
5119
5120 ctx.close().await;
5121 mock_ctx.close().await;
5122 }
5123
5124 #[tokio::test(flavor = "multi_thread")]
5125 async fn test_delete_sketch_when_sketch_block_uses_variable() {
5126 let initial_source = "@settings(experimentalFeatures = allow)
5127
5128s = sketch(on = XY) {}
5129";
5130
5131 let program = Program::parse(initial_source).unwrap().0.unwrap();
5132
5133 let mut frontend = FrontendState::new();
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 frontend.hack_set_program(&ctx, program).await.unwrap();
5140 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5141 let sketch_id = sketch_object.id;
5142
5143 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
5144 assert_eq!(
5145 src_delta.text.as_str(),
5146 "@settings(experimentalFeatures = allow)
5147"
5148 );
5149 assert_eq!(scene_delta.new_graph.objects.len(), 0);
5150
5151 ctx.close().await;
5152 mock_ctx.close().await;
5153 }
5154
5155 #[tokio::test(flavor = "multi_thread")]
5156 async fn test_edit_line_when_editing_its_start_point() {
5157 let initial_source = "\
5158@settings(experimentalFeatures = allow)
5159
5160sketch(on = XY) {
5161 line(start = [var 1, var 2], end = [var 3, var 4])
5162}
5163";
5164
5165 let program = Program::parse(initial_source).unwrap().0.unwrap();
5166
5167 let mut frontend = FrontendState::new();
5168
5169 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5170 let mock_ctx = ExecutorContext::new_mock(None).await;
5171 let version = Version(0);
5172
5173 frontend.hack_set_program(&ctx, program).await.unwrap();
5174 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5175 let sketch_id = sketch_object.id;
5176 let sketch = expect_sketch(sketch_object);
5177
5178 let point_id = *sketch.segments.first().unwrap();
5179
5180 let point_ctor = PointCtor {
5181 position: Point2d {
5182 x: Expr::Var(Number {
5183 value: 5.0,
5184 units: NumericSuffix::Inch,
5185 }),
5186 y: Expr::Var(Number {
5187 value: 6.0,
5188 units: NumericSuffix::Inch,
5189 }),
5190 },
5191 };
5192 let segments = vec![ExistingSegmentCtor {
5193 id: point_id,
5194 ctor: SegmentCtor::Point(point_ctor),
5195 }];
5196 let (src_delta, scene_delta) = frontend
5197 .edit_segments(&mock_ctx, version, sketch_id, segments)
5198 .await
5199 .unwrap();
5200 assert_eq!(
5201 src_delta.text.as_str(),
5202 "\
5203@settings(experimentalFeatures = allow)
5204
5205sketch(on = XY) {
5206 line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
5207}
5208"
5209 );
5210 assert_eq!(scene_delta.new_objects, vec![]);
5211 assert_eq!(scene_delta.new_graph.objects.len(), 5);
5212
5213 ctx.close().await;
5214 mock_ctx.close().await;
5215 }
5216
5217 #[tokio::test(flavor = "multi_thread")]
5218 async fn test_edit_line_when_editing_its_end_point() {
5219 let initial_source = "\
5220@settings(experimentalFeatures = allow)
5221
5222sketch(on = XY) {
5223 line(start = [var 1, var 2], end = [var 3, var 4])
5224}
5225";
5226
5227 let program = Program::parse(initial_source).unwrap().0.unwrap();
5228
5229 let mut frontend = FrontendState::new();
5230
5231 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5232 let mock_ctx = ExecutorContext::new_mock(None).await;
5233 let version = Version(0);
5234
5235 frontend.hack_set_program(&ctx, program).await.unwrap();
5236 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5237 let sketch_id = sketch_object.id;
5238 let sketch = expect_sketch(sketch_object);
5239 let point_id = *sketch.segments.get(1).unwrap();
5240
5241 let point_ctor = PointCtor {
5242 position: Point2d {
5243 x: Expr::Var(Number {
5244 value: 5.0,
5245 units: NumericSuffix::Inch,
5246 }),
5247 y: Expr::Var(Number {
5248 value: 6.0,
5249 units: NumericSuffix::Inch,
5250 }),
5251 },
5252 };
5253 let segments = vec![ExistingSegmentCtor {
5254 id: point_id,
5255 ctor: SegmentCtor::Point(point_ctor),
5256 }];
5257 let (src_delta, scene_delta) = frontend
5258 .edit_segments(&mock_ctx, version, sketch_id, segments)
5259 .await
5260 .unwrap();
5261 assert_eq!(
5262 src_delta.text.as_str(),
5263 "\
5264@settings(experimentalFeatures = allow)
5265
5266sketch(on = XY) {
5267 line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
5268}
5269"
5270 );
5271 assert_eq!(scene_delta.new_objects, vec![]);
5272 assert_eq!(
5273 scene_delta.new_graph.objects.len(),
5274 5,
5275 "{:#?}",
5276 scene_delta.new_graph.objects
5277 );
5278
5279 ctx.close().await;
5280 mock_ctx.close().await;
5281 }
5282
5283 #[tokio::test(flavor = "multi_thread")]
5284 async fn test_edit_line_with_coincident_feedback() {
5285 let initial_source = "\
5286@settings(experimentalFeatures = allow)
5287
5288sketch(on = XY) {
5289 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
5290 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5291 line1.start.at[0] == 0
5292 line1.start.at[1] == 0
5293 coincident([line1.end, line2.start])
5294 equalLength([line1, line2])
5295}
5296";
5297
5298 let program = Program::parse(initial_source).unwrap().0.unwrap();
5299
5300 let mut frontend = FrontendState::new();
5301
5302 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5303 let mock_ctx = ExecutorContext::new_mock(None).await;
5304 let version = Version(0);
5305
5306 frontend.hack_set_program(&ctx, program).await.unwrap();
5307 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5308 let sketch_id = sketch_object.id;
5309 let sketch = expect_sketch(sketch_object);
5310 let line2_end_id = *sketch.segments.get(4).unwrap();
5311
5312 let segments = vec![ExistingSegmentCtor {
5313 id: line2_end_id,
5314 ctor: SegmentCtor::Point(PointCtor {
5315 position: Point2d {
5316 x: Expr::Var(Number {
5317 value: 9.0,
5318 units: NumericSuffix::None,
5319 }),
5320 y: Expr::Var(Number {
5321 value: 10.0,
5322 units: NumericSuffix::None,
5323 }),
5324 },
5325 }),
5326 }];
5327 let (src_delta, scene_delta) = frontend
5328 .edit_segments(&mock_ctx, version, sketch_id, segments)
5329 .await
5330 .unwrap();
5331 assert_eq!(
5332 src_delta.text.as_str(),
5333 "\
5334@settings(experimentalFeatures = allow)
5335
5336sketch(on = XY) {
5337 line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
5338 line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
5339 line1.start.at[0] == 0
5340 line1.start.at[1] == 0
5341 coincident([line1.end, line2.start])
5342 equalLength([line1, line2])
5343}
5344"
5345 );
5346 assert_eq!(
5347 scene_delta.new_graph.objects.len(),
5348 10,
5349 "{:#?}",
5350 scene_delta.new_graph.objects
5351 );
5352
5353 ctx.close().await;
5354 mock_ctx.close().await;
5355 }
5356
5357 #[tokio::test(flavor = "multi_thread")]
5358 async fn test_delete_point_without_var() {
5359 let initial_source = "\
5360@settings(experimentalFeatures = allow)
5361
5362sketch(on = XY) {
5363 point(at = [var 1, var 2])
5364 point(at = [var 3, var 4])
5365 point(at = [var 5, var 6])
5366}
5367";
5368
5369 let program = Program::parse(initial_source).unwrap().0.unwrap();
5370
5371 let mut frontend = FrontendState::new();
5372
5373 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5374 let mock_ctx = ExecutorContext::new_mock(None).await;
5375 let version = Version(0);
5376
5377 frontend.hack_set_program(&ctx, program).await.unwrap();
5378 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5379 let sketch_id = sketch_object.id;
5380 let sketch = expect_sketch(sketch_object);
5381
5382 let point_id = *sketch.segments.get(1).unwrap();
5383
5384 let (src_delta, scene_delta) = frontend
5385 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
5386 .await
5387 .unwrap();
5388 assert_eq!(
5389 src_delta.text.as_str(),
5390 "\
5391@settings(experimentalFeatures = allow)
5392
5393sketch(on = XY) {
5394 point(at = [var 1mm, var 2mm])
5395 point(at = [var 5mm, var 6mm])
5396}
5397"
5398 );
5399 assert_eq!(scene_delta.new_objects, vec![]);
5400 assert_eq!(scene_delta.new_graph.objects.len(), 4);
5401
5402 ctx.close().await;
5403 mock_ctx.close().await;
5404 }
5405
5406 #[tokio::test(flavor = "multi_thread")]
5407 async fn test_delete_point_with_var() {
5408 let initial_source = "\
5409@settings(experimentalFeatures = allow)
5410
5411sketch(on = XY) {
5412 point(at = [var 1, var 2])
5413 point1 = point(at = [var 3, var 4])
5414 point(at = [var 5, var 6])
5415}
5416";
5417
5418 let program = Program::parse(initial_source).unwrap().0.unwrap();
5419
5420 let mut frontend = FrontendState::new();
5421
5422 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5423 let mock_ctx = ExecutorContext::new_mock(None).await;
5424 let version = Version(0);
5425
5426 frontend.hack_set_program(&ctx, program).await.unwrap();
5427 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5428 let sketch_id = sketch_object.id;
5429 let sketch = expect_sketch(sketch_object);
5430
5431 let point_id = *sketch.segments.get(1).unwrap();
5432
5433 let (src_delta, scene_delta) = frontend
5434 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
5435 .await
5436 .unwrap();
5437 assert_eq!(
5438 src_delta.text.as_str(),
5439 "\
5440@settings(experimentalFeatures = allow)
5441
5442sketch(on = XY) {
5443 point(at = [var 1mm, var 2mm])
5444 point(at = [var 5mm, var 6mm])
5445}
5446"
5447 );
5448 assert_eq!(scene_delta.new_objects, vec![]);
5449 assert_eq!(scene_delta.new_graph.objects.len(), 4);
5450
5451 ctx.close().await;
5452 mock_ctx.close().await;
5453 }
5454
5455 #[tokio::test(flavor = "multi_thread")]
5456 async fn test_delete_multiple_points() {
5457 let initial_source = "\
5458@settings(experimentalFeatures = allow)
5459
5460sketch(on = XY) {
5461 point(at = [var 1, var 2])
5462 point1 = point(at = [var 3, var 4])
5463 point(at = [var 5, var 6])
5464}
5465";
5466
5467 let program = Program::parse(initial_source).unwrap().0.unwrap();
5468
5469 let mut frontend = FrontendState::new();
5470
5471 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5472 let mock_ctx = ExecutorContext::new_mock(None).await;
5473 let version = Version(0);
5474
5475 frontend.hack_set_program(&ctx, program).await.unwrap();
5476 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5477 let sketch_id = sketch_object.id;
5478
5479 let sketch = expect_sketch(sketch_object);
5480
5481 let point1_id = *sketch.segments.first().unwrap();
5482 let point2_id = *sketch.segments.get(1).unwrap();
5483
5484 let (src_delta, scene_delta) = frontend
5485 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
5486 .await
5487 .unwrap();
5488 assert_eq!(
5489 src_delta.text.as_str(),
5490 "\
5491@settings(experimentalFeatures = allow)
5492
5493sketch(on = XY) {
5494 point(at = [var 5mm, var 6mm])
5495}
5496"
5497 );
5498 assert_eq!(scene_delta.new_objects, vec![]);
5499 assert_eq!(scene_delta.new_graph.objects.len(), 3);
5500
5501 ctx.close().await;
5502 mock_ctx.close().await;
5503 }
5504
5505 #[tokio::test(flavor = "multi_thread")]
5506 async fn test_delete_coincident_constraint() {
5507 let initial_source = "\
5508@settings(experimentalFeatures = allow)
5509
5510sketch(on = XY) {
5511 point1 = point(at = [var 1, var 2])
5512 point2 = point(at = [var 3, var 4])
5513 coincident([point1, point2])
5514 point(at = [var 5, var 6])
5515}
5516";
5517
5518 let program = Program::parse(initial_source).unwrap().0.unwrap();
5519
5520 let mut frontend = FrontendState::new();
5521
5522 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5523 let mock_ctx = ExecutorContext::new_mock(None).await;
5524 let version = Version(0);
5525
5526 frontend.hack_set_program(&ctx, program).await.unwrap();
5527 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5528 let sketch_id = sketch_object.id;
5529 let sketch = expect_sketch(sketch_object);
5530
5531 let coincident_id = *sketch.constraints.first().unwrap();
5532
5533 let (src_delta, scene_delta) = frontend
5534 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
5535 .await
5536 .unwrap();
5537 assert_eq!(
5538 src_delta.text.as_str(),
5539 "\
5540@settings(experimentalFeatures = allow)
5541
5542sketch(on = XY) {
5543 point1 = point(at = [var 1mm, var 2mm])
5544 point2 = point(at = [var 3mm, var 4mm])
5545 point(at = [var 5mm, var 6mm])
5546}
5547"
5548 );
5549 assert_eq!(scene_delta.new_objects, vec![]);
5550 assert_eq!(scene_delta.new_graph.objects.len(), 5);
5551
5552 ctx.close().await;
5553 mock_ctx.close().await;
5554 }
5555
5556 #[tokio::test(flavor = "multi_thread")]
5557 async fn test_delete_line_cascades_to_coincident_constraint() {
5558 let initial_source = "\
5559@settings(experimentalFeatures = allow)
5560
5561sketch(on = XY) {
5562 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5563 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5564 coincident([line1.end, line2.start])
5565}
5566";
5567
5568 let program = Program::parse(initial_source).unwrap().0.unwrap();
5569
5570 let mut frontend = FrontendState::new();
5571
5572 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5573 let mock_ctx = ExecutorContext::new_mock(None).await;
5574 let version = Version(0);
5575
5576 frontend.hack_set_program(&ctx, program).await.unwrap();
5577 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5578 let sketch_id = sketch_object.id;
5579 let sketch = expect_sketch(sketch_object);
5580 let line_id = *sketch.segments.get(5).unwrap();
5581
5582 let (src_delta, scene_delta) = frontend
5583 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
5584 .await
5585 .unwrap();
5586 assert_eq!(
5587 src_delta.text.as_str(),
5588 "\
5589@settings(experimentalFeatures = allow)
5590
5591sketch(on = XY) {
5592 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5593}
5594"
5595 );
5596 assert_eq!(
5597 scene_delta.new_graph.objects.len(),
5598 5,
5599 "{:#?}",
5600 scene_delta.new_graph.objects
5601 );
5602
5603 ctx.close().await;
5604 mock_ctx.close().await;
5605 }
5606
5607 #[tokio::test(flavor = "multi_thread")]
5608 async fn test_delete_line_cascades_to_distance_constraint() {
5609 let initial_source = "\
5610@settings(experimentalFeatures = allow)
5611
5612sketch(on = XY) {
5613 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5614 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5615 distance([line1.end, line2.start]) == 10mm
5616}
5617";
5618
5619 let program = Program::parse(initial_source).unwrap().0.unwrap();
5620
5621 let mut frontend = FrontendState::new();
5622
5623 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5624 let mock_ctx = ExecutorContext::new_mock(None).await;
5625 let version = Version(0);
5626
5627 frontend.hack_set_program(&ctx, program).await.unwrap();
5628 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5629 let sketch_id = sketch_object.id;
5630 let sketch = expect_sketch(sketch_object);
5631 let line_id = *sketch.segments.get(5).unwrap();
5632
5633 let (src_delta, scene_delta) = frontend
5634 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
5635 .await
5636 .unwrap();
5637 assert_eq!(
5638 src_delta.text.as_str(),
5639 "\
5640@settings(experimentalFeatures = allow)
5641
5642sketch(on = XY) {
5643 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5644}
5645"
5646 );
5647 assert_eq!(
5648 scene_delta.new_graph.objects.len(),
5649 5,
5650 "{:#?}",
5651 scene_delta.new_graph.objects
5652 );
5653
5654 ctx.close().await;
5655 mock_ctx.close().await;
5656 }
5657
5658 #[tokio::test(flavor = "multi_thread")]
5659 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
5660 let initial_source = "\
5661@settings(experimentalFeatures = allow)
5662
5663sketch(on = XY) {
5664 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5665 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5666 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
5667 equalLength([line1, line2, line3])
5668}
5669";
5670
5671 let program = Program::parse(initial_source).unwrap().0.unwrap();
5672
5673 let mut frontend = FrontendState::new();
5674
5675 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5676 let mock_ctx = ExecutorContext::new_mock(None).await;
5677 let version = Version(0);
5678
5679 frontend.hack_set_program(&ctx, program).await.unwrap();
5680 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5681 let sketch_id = sketch_object.id;
5682 let sketch = expect_sketch(sketch_object);
5683 let line3_id = *sketch.segments.get(8).unwrap();
5684
5685 let (src_delta, scene_delta) = frontend
5686 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
5687 .await
5688 .unwrap();
5689 assert_eq!(
5690 src_delta.text.as_str(),
5691 "\
5692@settings(experimentalFeatures = allow)
5693
5694sketch(on = XY) {
5695 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5696 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
5697 equalLength([line1, line2])
5698}
5699"
5700 );
5701
5702 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
5703 let sketch = expect_sketch(sketch_object);
5704 assert_eq!(sketch.constraints.len(), 1);
5705
5706 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
5707 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
5708 panic!("Expected constraint object");
5709 };
5710 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
5711 panic!("Expected lines equal length constraint");
5712 };
5713 assert_eq!(lines_equal_length.lines.len(), 2);
5714
5715 ctx.close().await;
5716 mock_ctx.close().await;
5717 }
5718
5719 #[tokio::test(flavor = "multi_thread")]
5720 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
5721 let initial_source = "\
5722@settings(experimentalFeatures = allow)
5723
5724sketch(on = XY) {
5725 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5726 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5727 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
5728 equalLength([line1, line2, line3])
5729}
5730";
5731
5732 let program = Program::parse(initial_source).unwrap().0.unwrap();
5733
5734 let mut frontend = FrontendState::new();
5735
5736 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5737 let mock_ctx = ExecutorContext::new_mock(None).await;
5738 let version = Version(0);
5739
5740 frontend.hack_set_program(&ctx, program).await.unwrap();
5741 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5742 let sketch_id = sketch_object.id;
5743 let sketch = expect_sketch(sketch_object);
5744 let line2_id = *sketch.segments.get(5).unwrap();
5745 let line3_id = *sketch.segments.get(8).unwrap();
5746
5747 let (src_delta, scene_delta) = frontend
5748 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
5749 .await
5750 .unwrap();
5751 assert_eq!(
5752 src_delta.text.as_str(),
5753 "\
5754@settings(experimentalFeatures = allow)
5755
5756sketch(on = XY) {
5757 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5758}
5759"
5760 );
5761
5762 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
5763 let sketch = expect_sketch(sketch_object);
5764 assert!(sketch.constraints.is_empty());
5765
5766 ctx.close().await;
5767 mock_ctx.close().await;
5768 }
5769
5770 #[tokio::test(flavor = "multi_thread")]
5771 async fn test_delete_line_line_coincident_constraint() {
5772 let initial_source = "\
5773@settings(experimentalFeatures = allow)
5774
5775sketch(on = XY) {
5776 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5777 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5778 coincident([line1, line2])
5779}
5780";
5781
5782 let program = Program::parse(initial_source).unwrap().0.unwrap();
5783
5784 let mut frontend = FrontendState::new();
5785
5786 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5787 let mock_ctx = ExecutorContext::new_mock(None).await;
5788 let version = Version(0);
5789
5790 frontend.hack_set_program(&ctx, program).await.unwrap();
5791 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5792 let sketch_id = sketch_object.id;
5793 let sketch = expect_sketch(sketch_object);
5794
5795 let coincident_id = *sketch.constraints.first().unwrap();
5796
5797 let (src_delta, scene_delta) = frontend
5798 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
5799 .await
5800 .unwrap();
5801 assert_eq!(
5802 src_delta.text.as_str(),
5803 "\
5804@settings(experimentalFeatures = allow)
5805
5806sketch(on = XY) {
5807 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5808 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
5809}
5810"
5811 );
5812 assert_eq!(scene_delta.new_objects, vec![]);
5813 assert_eq!(scene_delta.new_graph.objects.len(), 8);
5814
5815 ctx.close().await;
5816 mock_ctx.close().await;
5817 }
5818
5819 #[tokio::test(flavor = "multi_thread")]
5820 async fn test_two_points_coincident() {
5821 let initial_source = "\
5822@settings(experimentalFeatures = allow)
5823
5824sketch(on = XY) {
5825 point1 = point(at = [var 1, var 2])
5826 point(at = [3, 4])
5827}
5828";
5829
5830 let program = Program::parse(initial_source).unwrap().0.unwrap();
5831
5832 let mut frontend = FrontendState::new();
5833
5834 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5835 let mock_ctx = ExecutorContext::new_mock(None).await;
5836 let version = Version(0);
5837
5838 frontend.hack_set_program(&ctx, program).await.unwrap();
5839 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5840 let sketch_id = sketch_object.id;
5841 let sketch = expect_sketch(sketch_object);
5842 let point0_id = *sketch.segments.first().unwrap();
5843 let point1_id = *sketch.segments.get(1).unwrap();
5844
5845 let constraint = Constraint::Coincident(Coincident {
5846 segments: vec![point0_id, point1_id],
5847 });
5848 let (src_delta, scene_delta) = frontend
5849 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5850 .await
5851 .unwrap();
5852 assert_eq!(
5853 src_delta.text.as_str(),
5854 "\
5855@settings(experimentalFeatures = allow)
5856
5857sketch(on = XY) {
5858 point1 = point(at = [var 1, var 2])
5859 point2 = point(at = [3, 4])
5860 coincident([point1, point2])
5861}
5862"
5863 );
5864 assert_eq!(
5865 scene_delta.new_graph.objects.len(),
5866 5,
5867 "{:#?}",
5868 scene_delta.new_graph.objects
5869 );
5870
5871 ctx.close().await;
5872 mock_ctx.close().await;
5873 }
5874
5875 #[tokio::test(flavor = "multi_thread")]
5876 async fn test_coincident_of_line_end_points() {
5877 let initial_source = "\
5878@settings(experimentalFeatures = allow)
5879
5880sketch(on = XY) {
5881 line(start = [var 1, var 2], end = [var 3, var 4])
5882 line(start = [var 5, var 6], end = [var 7, var 8])
5883}
5884";
5885
5886 let program = Program::parse(initial_source).unwrap().0.unwrap();
5887
5888 let mut frontend = FrontendState::new();
5889
5890 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5891 let mock_ctx = ExecutorContext::new_mock(None).await;
5892 let version = Version(0);
5893
5894 frontend.hack_set_program(&ctx, program).await.unwrap();
5895 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5896 let sketch_id = sketch_object.id;
5897 let sketch = expect_sketch(sketch_object);
5898 let point0_id = *sketch.segments.get(1).unwrap();
5899 let point1_id = *sketch.segments.get(3).unwrap();
5900
5901 let constraint = Constraint::Coincident(Coincident {
5902 segments: vec![point0_id, point1_id],
5903 });
5904 let (src_delta, scene_delta) = frontend
5905 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5906 .await
5907 .unwrap();
5908 assert_eq!(
5909 src_delta.text.as_str(),
5910 "\
5911@settings(experimentalFeatures = allow)
5912
5913sketch(on = XY) {
5914 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
5915 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
5916 coincident([line1.end, line2.start])
5917}
5918"
5919 );
5920 assert_eq!(
5921 scene_delta.new_graph.objects.len(),
5922 9,
5923 "{:#?}",
5924 scene_delta.new_graph.objects
5925 );
5926
5927 ctx.close().await;
5928 mock_ctx.close().await;
5929 }
5930
5931 #[tokio::test(flavor = "multi_thread")]
5932 async fn test_invalid_coincident_arc_and_line_preserves_state() {
5933 let program = Program::empty();
5941
5942 let mut frontend = FrontendState::new();
5943 frontend.program = program;
5944
5945 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5946 let mock_ctx = ExecutorContext::new_mock(None).await;
5947 let version = Version(0);
5948
5949 let sketch_args = SketchCtor {
5950 on: Plane::Default(PlaneName::Xy),
5951 };
5952 let (_src_delta, _scene_delta, sketch_id) = frontend
5953 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5954 .await
5955 .unwrap();
5956
5957 let arc_ctor = ArcCtor {
5959 start: Point2d {
5960 x: Expr::Var(Number {
5961 value: 0.0,
5962 units: NumericSuffix::Mm,
5963 }),
5964 y: Expr::Var(Number {
5965 value: 0.0,
5966 units: NumericSuffix::Mm,
5967 }),
5968 },
5969 end: Point2d {
5970 x: Expr::Var(Number {
5971 value: 10.0,
5972 units: NumericSuffix::Mm,
5973 }),
5974 y: Expr::Var(Number {
5975 value: 10.0,
5976 units: NumericSuffix::Mm,
5977 }),
5978 },
5979 center: Point2d {
5980 x: Expr::Var(Number {
5981 value: 10.0,
5982 units: NumericSuffix::Mm,
5983 }),
5984 y: Expr::Var(Number {
5985 value: 0.0,
5986 units: NumericSuffix::Mm,
5987 }),
5988 },
5989 construction: None,
5990 };
5991 let (_src_delta, scene_delta) = frontend
5992 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
5993 .await
5994 .unwrap();
5995 let arc_id = *scene_delta.new_objects.last().unwrap();
5997
5998 let line_ctor = LineCtor {
6000 start: Point2d {
6001 x: Expr::Var(Number {
6002 value: 20.0,
6003 units: NumericSuffix::Mm,
6004 }),
6005 y: Expr::Var(Number {
6006 value: 0.0,
6007 units: NumericSuffix::Mm,
6008 }),
6009 },
6010 end: Point2d {
6011 x: Expr::Var(Number {
6012 value: 30.0,
6013 units: NumericSuffix::Mm,
6014 }),
6015 y: Expr::Var(Number {
6016 value: 10.0,
6017 units: NumericSuffix::Mm,
6018 }),
6019 },
6020 construction: None,
6021 };
6022 let (_src_delta, scene_delta) = frontend
6023 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
6024 .await
6025 .unwrap();
6026 let line_id = *scene_delta.new_objects.last().unwrap();
6028
6029 let constraint = Constraint::Coincident(Coincident {
6032 segments: vec![arc_id, line_id],
6033 });
6034 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
6035
6036 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
6038
6039 let sketch_object_after =
6042 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
6043 let sketch_after = expect_sketch(sketch_object_after);
6044
6045 assert!(
6047 sketch_after.segments.contains(&arc_id),
6048 "Arc segment should still exist after failed constraint"
6049 );
6050 assert!(
6051 sketch_after.segments.contains(&line_id),
6052 "Line segment should still exist after failed constraint"
6053 );
6054
6055 let arc_obj = frontend
6057 .scene_graph
6058 .objects
6059 .get(arc_id.0)
6060 .expect("Arc object should still be accessible");
6061 let line_obj = frontend
6062 .scene_graph
6063 .objects
6064 .get(line_id.0)
6065 .expect("Line object should still be accessible");
6066
6067 match &arc_obj.kind {
6070 ObjectKind::Segment {
6071 segment: Segment::Arc(_),
6072 } => {}
6073 _ => panic!("Arc object should still be an arc segment"),
6074 }
6075 match &line_obj.kind {
6076 ObjectKind::Segment {
6077 segment: Segment::Line(_),
6078 } => {}
6079 _ => panic!("Line object should still be a line segment"),
6080 }
6081
6082 ctx.close().await;
6083 mock_ctx.close().await;
6084 }
6085
6086 #[tokio::test(flavor = "multi_thread")]
6087 async fn test_distance_two_points() {
6088 let initial_source = "\
6089@settings(experimentalFeatures = allow)
6090
6091sketch(on = XY) {
6092 point(at = [var 1, var 2])
6093 point(at = [var 3, var 4])
6094}
6095";
6096
6097 let program = Program::parse(initial_source).unwrap().0.unwrap();
6098
6099 let mut frontend = FrontendState::new();
6100
6101 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6102 let mock_ctx = ExecutorContext::new_mock(None).await;
6103 let version = Version(0);
6104
6105 frontend.hack_set_program(&ctx, program).await.unwrap();
6106 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6107 let sketch_id = sketch_object.id;
6108 let sketch = expect_sketch(sketch_object);
6109 let point0_id = *sketch.segments.first().unwrap();
6110 let point1_id = *sketch.segments.get(1).unwrap();
6111
6112 let constraint = Constraint::Distance(Distance {
6113 points: vec![point0_id, point1_id],
6114 distance: Number {
6115 value: 2.0,
6116 units: NumericSuffix::Mm,
6117 },
6118 source: Default::default(),
6119 });
6120 let (src_delta, scene_delta) = frontend
6121 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6122 .await
6123 .unwrap();
6124 assert_eq!(
6125 src_delta.text.as_str(),
6126 "\
6128@settings(experimentalFeatures = allow)
6129
6130sketch(on = XY) {
6131 point1 = point(at = [var 1, var 2])
6132 point2 = point(at = [var 3, var 4])
6133 distance([point1, point2]) == 2mm
6134}
6135"
6136 );
6137 assert_eq!(
6138 scene_delta.new_graph.objects.len(),
6139 5,
6140 "{:#?}",
6141 scene_delta.new_graph.objects
6142 );
6143
6144 ctx.close().await;
6145 mock_ctx.close().await;
6146 }
6147
6148 #[tokio::test(flavor = "multi_thread")]
6149 async fn test_horizontal_distance_two_points() {
6150 let initial_source = "\
6151@settings(experimentalFeatures = allow)
6152
6153sketch(on = XY) {
6154 point(at = [var 1, var 2])
6155 point(at = [var 3, var 4])
6156}
6157";
6158
6159 let program = Program::parse(initial_source).unwrap().0.unwrap();
6160
6161 let mut frontend = FrontendState::new();
6162
6163 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6164 let mock_ctx = ExecutorContext::new_mock(None).await;
6165 let version = Version(0);
6166
6167 frontend.hack_set_program(&ctx, program).await.unwrap();
6168 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6169 let sketch_id = sketch_object.id;
6170 let sketch = expect_sketch(sketch_object);
6171 let point0_id = *sketch.segments.first().unwrap();
6172 let point1_id = *sketch.segments.get(1).unwrap();
6173
6174 let constraint = Constraint::HorizontalDistance(Distance {
6175 points: vec![point0_id, point1_id],
6176 distance: Number {
6177 value: 2.0,
6178 units: NumericSuffix::Mm,
6179 },
6180 source: Default::default(),
6181 });
6182 let (src_delta, scene_delta) = frontend
6183 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6184 .await
6185 .unwrap();
6186 assert_eq!(
6187 src_delta.text.as_str(),
6188 "\
6190@settings(experimentalFeatures = allow)
6191
6192sketch(on = XY) {
6193 point1 = point(at = [var 1, var 2])
6194 point2 = point(at = [var 3, var 4])
6195 horizontalDistance([point1, point2]) == 2mm
6196}
6197"
6198 );
6199 assert_eq!(
6200 scene_delta.new_graph.objects.len(),
6201 5,
6202 "{:#?}",
6203 scene_delta.new_graph.objects
6204 );
6205
6206 ctx.close().await;
6207 mock_ctx.close().await;
6208 }
6209
6210 #[tokio::test(flavor = "multi_thread")]
6211 async fn test_radius_single_arc_segment() {
6212 let initial_source = "\
6213@settings(experimentalFeatures = allow)
6214
6215sketch(on = XY) {
6216 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
6217}
6218";
6219
6220 let program = Program::parse(initial_source).unwrap().0.unwrap();
6221
6222 let mut frontend = FrontendState::new();
6223
6224 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6225 let mock_ctx = ExecutorContext::new_mock(None).await;
6226 let version = Version(0);
6227
6228 frontend.hack_set_program(&ctx, program).await.unwrap();
6229 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6230 let sketch_id = sketch_object.id;
6231 let sketch = expect_sketch(sketch_object);
6232 let arc_id = sketch
6234 .segments
6235 .iter()
6236 .find(|&seg_id| {
6237 let obj = frontend.scene_graph.objects.get(seg_id.0);
6238 matches!(
6239 obj.map(|o| &o.kind),
6240 Some(ObjectKind::Segment {
6241 segment: Segment::Arc(_)
6242 })
6243 )
6244 })
6245 .unwrap();
6246
6247 let constraint = Constraint::Radius(Radius {
6248 arc: *arc_id,
6249 radius: Number {
6250 value: 5.0,
6251 units: NumericSuffix::Mm,
6252 },
6253 source: Default::default(),
6254 });
6255 let (src_delta, scene_delta) = frontend
6256 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6257 .await
6258 .unwrap();
6259 assert_eq!(
6260 src_delta.text.as_str(),
6261 "\
6263@settings(experimentalFeatures = allow)
6264
6265sketch(on = XY) {
6266 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
6267 radius(arc1) == 5mm
6268}
6269"
6270 );
6271 assert_eq!(
6272 scene_delta.new_graph.objects.len(),
6273 7, "{:#?}",
6275 scene_delta.new_graph.objects
6276 );
6277
6278 ctx.close().await;
6279 mock_ctx.close().await;
6280 }
6281
6282 #[tokio::test(flavor = "multi_thread")]
6283 async fn test_vertical_distance_two_points() {
6284 let initial_source = "\
6285@settings(experimentalFeatures = allow)
6286
6287sketch(on = XY) {
6288 point(at = [var 1, var 2])
6289 point(at = [var 3, var 4])
6290}
6291";
6292
6293 let program = Program::parse(initial_source).unwrap().0.unwrap();
6294
6295 let mut frontend = FrontendState::new();
6296
6297 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6298 let mock_ctx = ExecutorContext::new_mock(None).await;
6299 let version = Version(0);
6300
6301 frontend.hack_set_program(&ctx, program).await.unwrap();
6302 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6303 let sketch_id = sketch_object.id;
6304 let sketch = expect_sketch(sketch_object);
6305 let point0_id = *sketch.segments.first().unwrap();
6306 let point1_id = *sketch.segments.get(1).unwrap();
6307
6308 let constraint = Constraint::VerticalDistance(Distance {
6309 points: vec![point0_id, point1_id],
6310 distance: Number {
6311 value: 2.0,
6312 units: NumericSuffix::Mm,
6313 },
6314 source: Default::default(),
6315 });
6316 let (src_delta, scene_delta) = frontend
6317 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6318 .await
6319 .unwrap();
6320 assert_eq!(
6321 src_delta.text.as_str(),
6322 "\
6324@settings(experimentalFeatures = allow)
6325
6326sketch(on = XY) {
6327 point1 = point(at = [var 1, var 2])
6328 point2 = point(at = [var 3, var 4])
6329 verticalDistance([point1, point2]) == 2mm
6330}
6331"
6332 );
6333 assert_eq!(
6334 scene_delta.new_graph.objects.len(),
6335 5,
6336 "{:#?}",
6337 scene_delta.new_graph.objects
6338 );
6339
6340 ctx.close().await;
6341 mock_ctx.close().await;
6342 }
6343
6344 #[tokio::test(flavor = "multi_thread")]
6345 async fn test_radius_error_cases() {
6346 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6347 let mock_ctx = ExecutorContext::new_mock(None).await;
6348 let version = Version(0);
6349
6350 let initial_source_point = "\
6352@settings(experimentalFeatures = allow)
6353
6354sketch(on = XY) {
6355 point(at = [var 1, var 2])
6356}
6357";
6358 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
6359 let mut frontend_point = FrontendState::new();
6360 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
6361 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
6362 let sketch_id_point = sketch_object_point.id;
6363 let sketch_point = expect_sketch(sketch_object_point);
6364 let point_id = *sketch_point.segments.first().unwrap();
6365
6366 let constraint_point = Constraint::Radius(Radius {
6367 arc: point_id,
6368 radius: Number {
6369 value: 5.0,
6370 units: NumericSuffix::Mm,
6371 },
6372 source: Default::default(),
6373 });
6374 let result_point = frontend_point
6375 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
6376 .await;
6377 assert!(result_point.is_err(), "Single point should error for radius");
6378
6379 let initial_source_line = "\
6381@settings(experimentalFeatures = allow)
6382
6383sketch(on = XY) {
6384 line(start = [var 1, var 2], end = [var 3, var 4])
6385}
6386";
6387 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
6388 let mut frontend_line = FrontendState::new();
6389 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
6390 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
6391 let sketch_id_line = sketch_object_line.id;
6392 let sketch_line = expect_sketch(sketch_object_line);
6393 let line_id = *sketch_line.segments.first().unwrap();
6394
6395 let constraint_line = Constraint::Radius(Radius {
6396 arc: line_id,
6397 radius: Number {
6398 value: 5.0,
6399 units: NumericSuffix::Mm,
6400 },
6401 source: Default::default(),
6402 });
6403 let result_line = frontend_line
6404 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
6405 .await;
6406 assert!(result_line.is_err(), "Single line segment should error for radius");
6407
6408 ctx.close().await;
6409 mock_ctx.close().await;
6410 }
6411
6412 #[tokio::test(flavor = "multi_thread")]
6413 async fn test_diameter_single_arc_segment() {
6414 let initial_source = "\
6415@settings(experimentalFeatures = allow)
6416
6417sketch(on = XY) {
6418 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
6419}
6420";
6421
6422 let program = Program::parse(initial_source).unwrap().0.unwrap();
6423
6424 let mut frontend = FrontendState::new();
6425
6426 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6427 let mock_ctx = ExecutorContext::new_mock(None).await;
6428 let version = Version(0);
6429
6430 frontend.hack_set_program(&ctx, program).await.unwrap();
6431 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6432 let sketch_id = sketch_object.id;
6433 let sketch = expect_sketch(sketch_object);
6434 let arc_id = sketch
6436 .segments
6437 .iter()
6438 .find(|&seg_id| {
6439 let obj = frontend.scene_graph.objects.get(seg_id.0);
6440 matches!(
6441 obj.map(|o| &o.kind),
6442 Some(ObjectKind::Segment {
6443 segment: Segment::Arc(_)
6444 })
6445 )
6446 })
6447 .unwrap();
6448
6449 let constraint = Constraint::Diameter(Diameter {
6450 arc: *arc_id,
6451 diameter: Number {
6452 value: 10.0,
6453 units: NumericSuffix::Mm,
6454 },
6455 source: Default::default(),
6456 });
6457 let (src_delta, scene_delta) = frontend
6458 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6459 .await
6460 .unwrap();
6461 assert_eq!(
6462 src_delta.text.as_str(),
6463 "\
6465@settings(experimentalFeatures = allow)
6466
6467sketch(on = XY) {
6468 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
6469 diameter(arc1) == 10mm
6470}
6471"
6472 );
6473 assert_eq!(
6474 scene_delta.new_graph.objects.len(),
6475 7, "{:#?}",
6477 scene_delta.new_graph.objects
6478 );
6479
6480 ctx.close().await;
6481 mock_ctx.close().await;
6482 }
6483
6484 #[tokio::test(flavor = "multi_thread")]
6485 async fn test_diameter_error_cases() {
6486 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6487 let mock_ctx = ExecutorContext::new_mock(None).await;
6488 let version = Version(0);
6489
6490 let initial_source_point = "\
6492@settings(experimentalFeatures = allow)
6493
6494sketch(on = XY) {
6495 point(at = [var 1, var 2])
6496}
6497";
6498 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
6499 let mut frontend_point = FrontendState::new();
6500 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
6501 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
6502 let sketch_id_point = sketch_object_point.id;
6503 let sketch_point = expect_sketch(sketch_object_point);
6504 let point_id = *sketch_point.segments.first().unwrap();
6505
6506 let constraint_point = Constraint::Diameter(Diameter {
6507 arc: point_id,
6508 diameter: Number {
6509 value: 10.0,
6510 units: NumericSuffix::Mm,
6511 },
6512 source: Default::default(),
6513 });
6514 let result_point = frontend_point
6515 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
6516 .await;
6517 assert!(result_point.is_err(), "Single point should error for diameter");
6518
6519 let initial_source_line = "\
6521@settings(experimentalFeatures = allow)
6522
6523sketch(on = XY) {
6524 line(start = [var 1, var 2], end = [var 3, var 4])
6525}
6526";
6527 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
6528 let mut frontend_line = FrontendState::new();
6529 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
6530 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
6531 let sketch_id_line = sketch_object_line.id;
6532 let sketch_line = expect_sketch(sketch_object_line);
6533 let line_id = *sketch_line.segments.first().unwrap();
6534
6535 let constraint_line = Constraint::Diameter(Diameter {
6536 arc: line_id,
6537 diameter: Number {
6538 value: 10.0,
6539 units: NumericSuffix::Mm,
6540 },
6541 source: Default::default(),
6542 });
6543 let result_line = frontend_line
6544 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
6545 .await;
6546 assert!(result_line.is_err(), "Single line segment should error for diameter");
6547
6548 ctx.close().await;
6549 mock_ctx.close().await;
6550 }
6551
6552 #[tokio::test(flavor = "multi_thread")]
6553 async fn test_line_horizontal() {
6554 let initial_source = "\
6555@settings(experimentalFeatures = allow)
6556
6557sketch(on = XY) {
6558 line(start = [var 1, var 2], end = [var 3, var 4])
6559}
6560";
6561
6562 let program = Program::parse(initial_source).unwrap().0.unwrap();
6563
6564 let mut frontend = FrontendState::new();
6565
6566 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6567 let mock_ctx = ExecutorContext::new_mock(None).await;
6568 let version = Version(0);
6569
6570 frontend.hack_set_program(&ctx, program).await.unwrap();
6571 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6572 let sketch_id = sketch_object.id;
6573 let sketch = expect_sketch(sketch_object);
6574 let line1_id = *sketch.segments.get(2).unwrap();
6575
6576 let constraint = Constraint::Horizontal(Horizontal { line: line1_id });
6577 let (src_delta, scene_delta) = frontend
6578 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6579 .await
6580 .unwrap();
6581 assert_eq!(
6582 src_delta.text.as_str(),
6583 "\
6584@settings(experimentalFeatures = allow)
6585
6586sketch(on = XY) {
6587 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6588 horizontal(line1)
6589}
6590"
6591 );
6592 assert_eq!(
6593 scene_delta.new_graph.objects.len(),
6594 6,
6595 "{:#?}",
6596 scene_delta.new_graph.objects
6597 );
6598
6599 ctx.close().await;
6600 mock_ctx.close().await;
6601 }
6602
6603 #[tokio::test(flavor = "multi_thread")]
6604 async fn test_line_vertical() {
6605 let initial_source = "\
6606@settings(experimentalFeatures = allow)
6607
6608sketch(on = XY) {
6609 line(start = [var 1, var 2], end = [var 3, var 4])
6610}
6611";
6612
6613 let program = Program::parse(initial_source).unwrap().0.unwrap();
6614
6615 let mut frontend = FrontendState::new();
6616
6617 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6618 let mock_ctx = ExecutorContext::new_mock(None).await;
6619 let version = Version(0);
6620
6621 frontend.hack_set_program(&ctx, program).await.unwrap();
6622 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6623 let sketch_id = sketch_object.id;
6624 let sketch = expect_sketch(sketch_object);
6625 let line1_id = *sketch.segments.get(2).unwrap();
6626
6627 let constraint = Constraint::Vertical(Vertical { line: line1_id });
6628 let (src_delta, scene_delta) = frontend
6629 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6630 .await
6631 .unwrap();
6632 assert_eq!(
6633 src_delta.text.as_str(),
6634 "\
6635@settings(experimentalFeatures = allow)
6636
6637sketch(on = XY) {
6638 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6639 vertical(line1)
6640}
6641"
6642 );
6643 assert_eq!(
6644 scene_delta.new_graph.objects.len(),
6645 6,
6646 "{:#?}",
6647 scene_delta.new_graph.objects
6648 );
6649
6650 ctx.close().await;
6651 mock_ctx.close().await;
6652 }
6653
6654 #[tokio::test(flavor = "multi_thread")]
6655 async fn test_lines_equal_length() {
6656 let initial_source = "\
6657@settings(experimentalFeatures = allow)
6658
6659sketch(on = XY) {
6660 line(start = [var 1, var 2], end = [var 3, var 4])
6661 line(start = [var 5, var 6], end = [var 7, var 8])
6662}
6663";
6664
6665 let program = Program::parse(initial_source).unwrap().0.unwrap();
6666
6667 let mut frontend = FrontendState::new();
6668
6669 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6670 let mock_ctx = ExecutorContext::new_mock(None).await;
6671 let version = Version(0);
6672
6673 frontend.hack_set_program(&ctx, program).await.unwrap();
6674 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6675 let sketch_id = sketch_object.id;
6676 let sketch = expect_sketch(sketch_object);
6677 let line1_id = *sketch.segments.get(2).unwrap();
6678 let line2_id = *sketch.segments.get(5).unwrap();
6679
6680 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
6681 lines: vec![line1_id, line2_id],
6682 });
6683 let (src_delta, scene_delta) = frontend
6684 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6685 .await
6686 .unwrap();
6687 assert_eq!(
6688 src_delta.text.as_str(),
6689 "\
6690@settings(experimentalFeatures = allow)
6691
6692sketch(on = XY) {
6693 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6694 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6695 equalLength([line1, line2])
6696}
6697"
6698 );
6699 assert_eq!(
6700 scene_delta.new_graph.objects.len(),
6701 9,
6702 "{:#?}",
6703 scene_delta.new_graph.objects
6704 );
6705
6706 ctx.close().await;
6707 mock_ctx.close().await;
6708 }
6709
6710 #[tokio::test(flavor = "multi_thread")]
6711 async fn test_add_constraint_multi_line_equal_length() {
6712 let initial_source = "\
6713@settings(experimentalFeatures = allow)
6714
6715sketch(on = XY) {
6716 line(start = [var 1, var 2], end = [var 3, var 4])
6717 line(start = [var 5, var 6], end = [var 7, var 8])
6718 line(start = [var 9, var 10], end = [var 11, var 12])
6719}
6720";
6721
6722 let program = Program::parse(initial_source).unwrap().0.unwrap();
6723
6724 let mut frontend = FrontendState::new();
6725 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6726 let mock_ctx = ExecutorContext::new_mock(None).await;
6727 let version = Version(0);
6728
6729 frontend.hack_set_program(&ctx, program).await.unwrap();
6730 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6731 let sketch_id = sketch_object.id;
6732 let sketch = expect_sketch(sketch_object);
6733 let line1_id = *sketch.segments.get(2).unwrap();
6734 let line2_id = *sketch.segments.get(5).unwrap();
6735 let line3_id = *sketch.segments.get(8).unwrap();
6736
6737 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
6738 lines: vec![line1_id, line2_id, line3_id],
6739 });
6740 let (src_delta, scene_delta) = frontend
6741 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6742 .await
6743 .unwrap();
6744 assert_eq!(
6745 src_delta.text.as_str(),
6746 "\
6747@settings(experimentalFeatures = allow)
6748
6749sketch(on = XY) {
6750 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6751 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6752 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
6753 equalLength([line1, line2, line3])
6754}
6755"
6756 );
6757 let constraints = scene_delta
6758 .new_graph
6759 .objects
6760 .iter()
6761 .filter_map(|obj| {
6762 let ObjectKind::Constraint { constraint } = &obj.kind else {
6763 return None;
6764 };
6765 Some(constraint)
6766 })
6767 .collect::<Vec<_>>();
6768
6769 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
6770 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
6771 panic!("expected equal length constraint, got {:?}", constraints[0]);
6772 };
6773 assert_eq!(lines_equal_length.lines.len(), 3);
6774
6775 ctx.close().await;
6776 mock_ctx.close().await;
6777 }
6778
6779 #[tokio::test(flavor = "multi_thread")]
6780 async fn test_lines_parallel() {
6781 let initial_source = "\
6782@settings(experimentalFeatures = allow)
6783
6784sketch(on = XY) {
6785 line(start = [var 1, var 2], end = [var 3, var 4])
6786 line(start = [var 5, var 6], end = [var 7, var 8])
6787}
6788";
6789
6790 let program = Program::parse(initial_source).unwrap().0.unwrap();
6791
6792 let mut frontend = FrontendState::new();
6793
6794 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6795 let mock_ctx = ExecutorContext::new_mock(None).await;
6796 let version = Version(0);
6797
6798 frontend.hack_set_program(&ctx, program).await.unwrap();
6799 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6800 let sketch_id = sketch_object.id;
6801 let sketch = expect_sketch(sketch_object);
6802 let line1_id = *sketch.segments.get(2).unwrap();
6803 let line2_id = *sketch.segments.get(5).unwrap();
6804
6805 let constraint = Constraint::Parallel(Parallel {
6806 lines: vec![line1_id, line2_id],
6807 });
6808 let (src_delta, scene_delta) = frontend
6809 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6810 .await
6811 .unwrap();
6812 assert_eq!(
6813 src_delta.text.as_str(),
6814 "\
6815@settings(experimentalFeatures = allow)
6816
6817sketch(on = XY) {
6818 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6819 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6820 parallel([line1, line2])
6821}
6822"
6823 );
6824 assert_eq!(
6825 scene_delta.new_graph.objects.len(),
6826 9,
6827 "{:#?}",
6828 scene_delta.new_graph.objects
6829 );
6830
6831 ctx.close().await;
6832 mock_ctx.close().await;
6833 }
6834
6835 #[tokio::test(flavor = "multi_thread")]
6836 async fn test_lines_perpendicular() {
6837 let initial_source = "\
6838@settings(experimentalFeatures = allow)
6839
6840sketch(on = XY) {
6841 line(start = [var 1, var 2], end = [var 3, var 4])
6842 line(start = [var 5, var 6], end = [var 7, var 8])
6843}
6844";
6845
6846 let program = Program::parse(initial_source).unwrap().0.unwrap();
6847
6848 let mut frontend = FrontendState::new();
6849
6850 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6851 let mock_ctx = ExecutorContext::new_mock(None).await;
6852 let version = Version(0);
6853
6854 frontend.hack_set_program(&ctx, program).await.unwrap();
6855 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6856 let sketch_id = sketch_object.id;
6857 let sketch = expect_sketch(sketch_object);
6858 let line1_id = *sketch.segments.get(2).unwrap();
6859 let line2_id = *sketch.segments.get(5).unwrap();
6860
6861 let constraint = Constraint::Perpendicular(Perpendicular {
6862 lines: vec![line1_id, line2_id],
6863 });
6864 let (src_delta, scene_delta) = frontend
6865 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6866 .await
6867 .unwrap();
6868 assert_eq!(
6869 src_delta.text.as_str(),
6870 "\
6871@settings(experimentalFeatures = allow)
6872
6873sketch(on = XY) {
6874 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6875 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6876 perpendicular([line1, line2])
6877}
6878"
6879 );
6880 assert_eq!(
6881 scene_delta.new_graph.objects.len(),
6882 9,
6883 "{:#?}",
6884 scene_delta.new_graph.objects
6885 );
6886
6887 ctx.close().await;
6888 mock_ctx.close().await;
6889 }
6890
6891 #[tokio::test(flavor = "multi_thread")]
6892 async fn test_lines_angle() {
6893 let initial_source = "\
6894@settings(experimentalFeatures = allow)
6895
6896sketch(on = XY) {
6897 line(start = [var 1, var 2], end = [var 3, var 4])
6898 line(start = [var 5, var 6], end = [var 7, var 8])
6899}
6900";
6901
6902 let program = Program::parse(initial_source).unwrap().0.unwrap();
6903
6904 let mut frontend = FrontendState::new();
6905
6906 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6907 let mock_ctx = ExecutorContext::new_mock(None).await;
6908 let version = Version(0);
6909
6910 frontend.hack_set_program(&ctx, program).await.unwrap();
6911 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6912 let sketch_id = sketch_object.id;
6913 let sketch = expect_sketch(sketch_object);
6914 let line1_id = *sketch.segments.get(2).unwrap();
6915 let line2_id = *sketch.segments.get(5).unwrap();
6916
6917 let constraint = Constraint::Angle(Angle {
6918 lines: vec![line1_id, line2_id],
6919 angle: Number {
6920 value: 30.0,
6921 units: NumericSuffix::Deg,
6922 },
6923 source: Default::default(),
6924 });
6925 let (src_delta, scene_delta) = frontend
6926 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6927 .await
6928 .unwrap();
6929 assert_eq!(
6930 src_delta.text.as_str(),
6931 "\
6933@settings(experimentalFeatures = allow)
6934
6935sketch(on = XY) {
6936 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6937 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6938 angle([line1, line2]) == 30deg
6939}
6940"
6941 );
6942 assert_eq!(
6943 scene_delta.new_graph.objects.len(),
6944 9,
6945 "{:#?}",
6946 scene_delta.new_graph.objects
6947 );
6948
6949 ctx.close().await;
6950 mock_ctx.close().await;
6951 }
6952
6953 #[tokio::test(flavor = "multi_thread")]
6954 async fn test_segments_tangent() {
6955 let initial_source = "\
6956@settings(experimentalFeatures = allow)
6957
6958sketch(on = XY) {
6959 line(start = [var 1, var 2], end = [var 3, var 4])
6960 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
6961}
6962";
6963
6964 let program = Program::parse(initial_source).unwrap().0.unwrap();
6965
6966 let mut frontend = FrontendState::new();
6967
6968 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6969 let mock_ctx = ExecutorContext::new_mock(None).await;
6970 let version = Version(0);
6971
6972 frontend.hack_set_program(&ctx, program).await.unwrap();
6973 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6974 let sketch_id = sketch_object.id;
6975 let sketch = expect_sketch(sketch_object);
6976 let line1_id = *sketch.segments.get(2).unwrap();
6977 let arc1_id = *sketch.segments.get(6).unwrap();
6978
6979 let constraint = Constraint::Tangent(Tangent {
6980 input: vec![line1_id, arc1_id],
6981 });
6982 let (src_delta, scene_delta) = frontend
6983 .add_constraint(&mock_ctx, version, sketch_id, constraint)
6984 .await
6985 .unwrap();
6986 assert_eq!(
6987 src_delta.text.as_str(),
6988 "\
6989@settings(experimentalFeatures = allow)
6990
6991sketch(on = XY) {
6992 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
6993 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
6994 tangent([line1, arc1])
6995}
6996"
6997 );
6998 assert_eq!(
6999 scene_delta.new_graph.objects.len(),
7000 10,
7001 "{:#?}",
7002 scene_delta.new_graph.objects
7003 );
7004
7005 ctx.close().await;
7006 mock_ctx.close().await;
7007 }
7008
7009 #[tokio::test(flavor = "multi_thread")]
7010 async fn test_sketch_on_face_simple() {
7011 let initial_source = "\
7012@settings(experimentalFeatures = allow)
7013
7014len = 2mm
7015cube = startSketchOn(XY)
7016 |> startProfile(at = [0, 0])
7017 |> line(end = [len, 0], tag = $side)
7018 |> line(end = [0, len])
7019 |> line(end = [-len, 0])
7020 |> line(end = [0, -len])
7021 |> close()
7022 |> extrude(length = len)
7023
7024face = faceOf(cube, face = side)
7025";
7026
7027 let program = Program::parse(initial_source).unwrap().0.unwrap();
7028
7029 let mut frontend = FrontendState::new();
7030
7031 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7032 let mock_ctx = ExecutorContext::new_mock(None).await;
7033 let version = Version(0);
7034
7035 frontend.hack_set_program(&ctx, program).await.unwrap();
7036 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
7037 let face_id = face_object.id;
7038
7039 let sketch_args = SketchCtor {
7040 on: Plane::Object(face_id),
7041 };
7042 let (_src_delta, scene_delta, sketch_id) = frontend
7043 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7044 .await
7045 .unwrap();
7046 assert_eq!(sketch_id, ObjectId(2));
7047 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7048 let sketch_object = &scene_delta.new_graph.objects[2];
7049 assert_eq!(sketch_object.id, ObjectId(2));
7050 assert_eq!(
7051 sketch_object.kind,
7052 ObjectKind::Sketch(Sketch {
7053 args: SketchCtor {
7054 on: Plane::Object(face_id),
7055 },
7056 plane: face_id,
7057 segments: vec![],
7058 constraints: vec![],
7059 })
7060 );
7061 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7062
7063 ctx.close().await;
7064 mock_ctx.close().await;
7065 }
7066
7067 #[tokio::test(flavor = "multi_thread")]
7068 async fn test_sketch_on_plane_incremental() {
7069 let initial_source = "\
7070@settings(experimentalFeatures = allow)
7071
7072len = 2mm
7073cube = startSketchOn(XY)
7074 |> startProfile(at = [0, 0])
7075 |> line(end = [len, 0], tag = $side)
7076 |> line(end = [0, len])
7077 |> line(end = [-len, 0])
7078 |> line(end = [0, -len])
7079 |> close()
7080 |> extrude(length = len)
7081
7082plane = planeOf(cube, face = side)
7083";
7084
7085 let program = Program::parse(initial_source).unwrap().0.unwrap();
7086
7087 let mut frontend = FrontendState::new();
7088
7089 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7090 let mock_ctx = ExecutorContext::new_mock(None).await;
7091 let version = Version(0);
7092
7093 frontend.hack_set_program(&ctx, program).await.unwrap();
7094 let plane_object = frontend
7096 .scene_graph
7097 .objects
7098 .iter()
7099 .rev()
7100 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
7101 .unwrap();
7102 let plane_id = plane_object.id;
7103
7104 let sketch_args = SketchCtor {
7105 on: Plane::Object(plane_id),
7106 };
7107 let (src_delta, scene_delta, sketch_id) = frontend
7108 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7109 .await
7110 .unwrap();
7111 assert_eq!(
7112 src_delta.text.as_str(),
7113 "\
7114@settings(experimentalFeatures = allow)
7115
7116len = 2mm
7117cube = startSketchOn(XY)
7118 |> startProfile(at = [0, 0])
7119 |> line(end = [len, 0], tag = $side)
7120 |> line(end = [0, len])
7121 |> line(end = [-len, 0])
7122 |> line(end = [0, -len])
7123 |> close()
7124 |> extrude(length = len)
7125
7126plane = planeOf(cube, face = side)
7127sketch001 = sketch(on = plane) {
7128}
7129"
7130 );
7131 assert_eq!(sketch_id, ObjectId(2));
7132 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7133 let sketch_object = &scene_delta.new_graph.objects[2];
7134 assert_eq!(sketch_object.id, ObjectId(2));
7135 assert_eq!(
7136 sketch_object.kind,
7137 ObjectKind::Sketch(Sketch {
7138 args: SketchCtor {
7139 on: Plane::Object(plane_id),
7140 },
7141 plane: plane_id,
7142 segments: vec![],
7143 constraints: vec![],
7144 })
7145 );
7146 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7147
7148 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
7149 assert_eq!(plane_object.id, plane_id);
7150 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
7151
7152 ctx.close().await;
7153 mock_ctx.close().await;
7154 }
7155
7156 #[tokio::test(flavor = "multi_thread")]
7157 async fn test_new_sketch_uses_unique_variable_name() {
7158 let initial_source = "\
7159@settings(experimentalFeatures = allow)
7160
7161sketch1 = sketch(on = XY) {
7162}
7163";
7164
7165 let program = Program::parse(initial_source).unwrap().0.unwrap();
7166
7167 let mut frontend = FrontendState::new();
7168 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7169 let version = Version(0);
7170
7171 frontend.hack_set_program(&ctx, program).await.unwrap();
7172
7173 let sketch_args = SketchCtor {
7174 on: Plane::Default(PlaneName::Yz),
7175 };
7176 let (src_delta, _, _) = frontend
7177 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7178 .await
7179 .unwrap();
7180
7181 assert_eq!(
7182 src_delta.text.as_str(),
7183 "\
7184@settings(experimentalFeatures = allow)
7185
7186sketch1 = sketch(on = XY) {
7187}
7188sketch001 = sketch(on = YZ) {
7189}
7190"
7191 );
7192
7193 ctx.close().await;
7194 }
7195
7196 #[tokio::test(flavor = "multi_thread")]
7197 async fn test_new_sketch_twice_using_same_plane() {
7198 let initial_source = "\
7199@settings(experimentalFeatures = allow)
7200
7201sketch1 = sketch(on = XY) {
7202}
7203";
7204
7205 let program = Program::parse(initial_source).unwrap().0.unwrap();
7206
7207 let mut frontend = FrontendState::new();
7208 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7209 let version = Version(0);
7210
7211 frontend.hack_set_program(&ctx, program).await.unwrap();
7212
7213 let sketch_args = SketchCtor {
7214 on: Plane::Default(PlaneName::Xy),
7215 };
7216 let (src_delta, _, _) = frontend
7217 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7218 .await
7219 .unwrap();
7220
7221 assert_eq!(
7222 src_delta.text.as_str(),
7223 "\
7224@settings(experimentalFeatures = allow)
7225
7226sketch1 = sketch(on = XY) {
7227}
7228sketch001 = sketch(on = XY) {
7229}
7230"
7231 );
7232
7233 ctx.close().await;
7234 }
7235
7236 #[tokio::test(flavor = "multi_thread")]
7237 async fn test_sketch_mode_reuses_cached_on_expression() {
7238 let initial_source = "\
7239@settings(experimentalFeatures = allow)
7240
7241width = 2mm
7242sketch(on = offsetPlane(XY, offset = width)) {
7243 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
7244 distance([line1.start, line1.end]) == width
7245}
7246";
7247 let program = Program::parse(initial_source).unwrap().0.unwrap();
7248
7249 let mut frontend = FrontendState::new();
7250 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7251 let mock_ctx = ExecutorContext::new_mock(None).await;
7252 let version = Version(0);
7253 let project_id = ProjectId(0);
7254 let file_id = FileId(0);
7255
7256 frontend.hack_set_program(&ctx, program).await.unwrap();
7257 let initial_object_count = frontend.scene_graph.objects.len();
7258 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
7259 .expect("Expected sketch object to exist")
7260 .id;
7261
7262 let scene_delta = frontend
7265 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7266 .await
7267 .unwrap();
7268 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
7269
7270 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
7273 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
7274
7275 ctx.close().await;
7276 mock_ctx.close().await;
7277 }
7278
7279 #[tokio::test(flavor = "multi_thread")]
7280 async fn test_multiple_sketch_blocks() {
7281 let initial_source = "\
7282@settings(experimentalFeatures = allow)
7283
7284// Cube that requires the engine.
7285width = 2
7286sketch001 = startSketchOn(XY)
7287profile001 = startProfile(sketch001, at = [0, 0])
7288 |> yLine(length = width, tag = $seg1)
7289 |> xLine(length = width)
7290 |> yLine(length = -width)
7291 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
7292 |> close()
7293extrude001 = extrude(profile001, length = width)
7294
7295// Get a value that requires the engine.
7296x = segLen(seg1)
7297
7298// Triangle with side length 2*x.
7299sketch(on = XY) {
7300 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
7301 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
7302 coincident([line1.end, line2.start])
7303 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
7304 coincident([line2.end, line3.start])
7305 coincident([line3.end, line1.start])
7306 equalLength([line3, line1])
7307 equalLength([line1, line2])
7308 distance([line1.start, line1.end]) == 2*x
7309}
7310
7311// Line segment with length x.
7312sketch2 = sketch(on = XY) {
7313 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
7314 distance([line1.start, line1.end]) == x
7315}
7316";
7317
7318 let program = Program::parse(initial_source).unwrap().0.unwrap();
7319
7320 let mut frontend = FrontendState::new();
7321
7322 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7323 let mock_ctx = ExecutorContext::new_mock(None).await;
7324 let version = Version(0);
7325 let project_id = ProjectId(0);
7326 let file_id = FileId(0);
7327
7328 frontend.hack_set_program(&ctx, program).await.unwrap();
7329 let sketch_objects = frontend
7330 .scene_graph
7331 .objects
7332 .iter()
7333 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
7334 .collect::<Vec<_>>();
7335 let sketch1_id = sketch_objects.first().unwrap().id;
7336 let sketch2_id = sketch_objects.get(1).unwrap().id;
7337 let point1_id = ObjectId(sketch1_id.0 + 1);
7339 let point2_id = ObjectId(sketch2_id.0 + 1);
7341
7342 let scene_delta = frontend
7351 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
7352 .await
7353 .unwrap();
7354 assert_eq!(
7355 scene_delta.new_graph.objects.len(),
7356 18,
7357 "{:#?}",
7358 scene_delta.new_graph.objects
7359 );
7360
7361 let point_ctor = PointCtor {
7363 position: Point2d {
7364 x: Expr::Var(Number {
7365 value: 1.0,
7366 units: NumericSuffix::Mm,
7367 }),
7368 y: Expr::Var(Number {
7369 value: 2.0,
7370 units: NumericSuffix::Mm,
7371 }),
7372 },
7373 };
7374 let segments = vec![ExistingSegmentCtor {
7375 id: point1_id,
7376 ctor: SegmentCtor::Point(point_ctor),
7377 }];
7378 let (src_delta, _) = frontend
7379 .edit_segments(&mock_ctx, version, sketch1_id, segments)
7380 .await
7381 .unwrap();
7382 assert_eq!(
7384 src_delta.text.as_str(),
7385 "\
7386@settings(experimentalFeatures = allow)
7387
7388// Cube that requires the engine.
7389width = 2
7390sketch001 = startSketchOn(XY)
7391profile001 = startProfile(sketch001, at = [0, 0])
7392 |> yLine(length = width, tag = $seg1)
7393 |> xLine(length = width)
7394 |> yLine(length = -width)
7395 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
7396 |> close()
7397extrude001 = extrude(profile001, length = width)
7398
7399// Get a value that requires the engine.
7400x = segLen(seg1)
7401
7402// Triangle with side length 2*x.
7403sketch(on = XY) {
7404 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
7405 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
7406 coincident([line1.end, line2.start])
7407 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
7408 coincident([line2.end, line3.start])
7409 coincident([line3.end, line1.start])
7410 equalLength([line3, line1])
7411 equalLength([line1, line2])
7412 distance([line1.start, line1.end]) == 2 * x
7413}
7414
7415// Line segment with length x.
7416sketch2 = sketch(on = XY) {
7417 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
7418 distance([line1.start, line1.end]) == x
7419}
7420"
7421 );
7422
7423 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
7425 assert_eq!(
7427 src_delta.text.as_str(),
7428 "\
7429@settings(experimentalFeatures = allow)
7430
7431// Cube that requires the engine.
7432width = 2
7433sketch001 = startSketchOn(XY)
7434profile001 = startProfile(sketch001, at = [0, 0])
7435 |> yLine(length = width, tag = $seg1)
7436 |> xLine(length = width)
7437 |> yLine(length = -width)
7438 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
7439 |> close()
7440extrude001 = extrude(profile001, length = width)
7441
7442// Get a value that requires the engine.
7443x = segLen(seg1)
7444
7445// Triangle with side length 2*x.
7446sketch(on = XY) {
7447 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
7448 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
7449 coincident([line1.end, line2.start])
7450 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
7451 coincident([line2.end, line3.start])
7452 coincident([line3.end, line1.start])
7453 equalLength([line3, line1])
7454 equalLength([line1, line2])
7455 distance([line1.start, line1.end]) == 2 * x
7456}
7457
7458// Line segment with length x.
7459sketch2 = sketch(on = XY) {
7460 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
7461 distance([line1.start, line1.end]) == x
7462}
7463"
7464 );
7465 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
7473 assert_eq!(scene.objects.len(), 23, "{:#?}", scene.objects);
7474
7475 let scene_delta = frontend
7483 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
7484 .await
7485 .unwrap();
7486 assert_eq!(
7487 scene_delta.new_graph.objects.len(),
7488 23,
7489 "{:#?}",
7490 scene_delta.new_graph.objects
7491 );
7492
7493 let point_ctor = PointCtor {
7495 position: Point2d {
7496 x: Expr::Var(Number {
7497 value: 3.0,
7498 units: NumericSuffix::Mm,
7499 }),
7500 y: Expr::Var(Number {
7501 value: 4.0,
7502 units: NumericSuffix::Mm,
7503 }),
7504 },
7505 };
7506 let segments = vec![ExistingSegmentCtor {
7507 id: point2_id,
7508 ctor: SegmentCtor::Point(point_ctor),
7509 }];
7510 let (src_delta, _) = frontend
7511 .edit_segments(&mock_ctx, version, sketch2_id, segments)
7512 .await
7513 .unwrap();
7514 assert_eq!(
7516 src_delta.text.as_str(),
7517 "\
7518@settings(experimentalFeatures = allow)
7519
7520// Cube that requires the engine.
7521width = 2
7522sketch001 = startSketchOn(XY)
7523profile001 = startProfile(sketch001, at = [0, 0])
7524 |> yLine(length = width, tag = $seg1)
7525 |> xLine(length = width)
7526 |> yLine(length = -width)
7527 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
7528 |> close()
7529extrude001 = extrude(profile001, length = width)
7530
7531// Get a value that requires the engine.
7532x = segLen(seg1)
7533
7534// Triangle with side length 2*x.
7535sketch(on = XY) {
7536 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
7537 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
7538 coincident([line1.end, line2.start])
7539 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
7540 coincident([line2.end, line3.start])
7541 coincident([line3.end, line1.start])
7542 equalLength([line3, line1])
7543 equalLength([line1, line2])
7544 distance([line1.start, line1.end]) == 2 * x
7545}
7546
7547// Line segment with length x.
7548sketch2 = sketch(on = XY) {
7549 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
7550 distance([line1.start, line1.end]) == x
7551}
7552"
7553 );
7554
7555 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
7557 assert_eq!(
7559 src_delta.text.as_str(),
7560 "\
7561@settings(experimentalFeatures = allow)
7562
7563// Cube that requires the engine.
7564width = 2
7565sketch001 = startSketchOn(XY)
7566profile001 = startProfile(sketch001, at = [0, 0])
7567 |> yLine(length = width, tag = $seg1)
7568 |> xLine(length = width)
7569 |> yLine(length = -width)
7570 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
7571 |> close()
7572extrude001 = extrude(profile001, length = width)
7573
7574// Get a value that requires the engine.
7575x = segLen(seg1)
7576
7577// Triangle with side length 2*x.
7578sketch(on = XY) {
7579 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
7580 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
7581 coincident([line1.end, line2.start])
7582 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
7583 coincident([line2.end, line3.start])
7584 coincident([line3.end, line1.start])
7585 equalLength([line3, line1])
7586 equalLength([line1, line2])
7587 distance([line1.start, line1.end]) == 2 * x
7588}
7589
7590// Line segment with length x.
7591sketch2 = sketch(on = XY) {
7592 line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
7593 distance([line1.start, line1.end]) == x
7594}
7595"
7596 );
7597
7598 ctx.close().await;
7599 mock_ctx.close().await;
7600 }
7601}