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