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