1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_error::CompilationIssue;
9use kcl_error::SourceRange;
10use kittycad_modeling_cmds::units::UnitLength;
11use serde::Serialize;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::collections::AhashIndexSet;
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::execution::cache::SketchModeState;
28use crate::execution::cache::clear_mem_cache;
29use crate::execution::cache::read_old_memory;
30use crate::execution::cache::write_old_memory;
31use crate::fmt::format_number_literal;
32use crate::front::Angle;
33use crate::front::ArcCtor;
34use crate::front::CircleCtor;
35use crate::front::Distance;
36use crate::front::EqualRadius;
37use crate::front::Error;
38use crate::front::ExecResult;
39use crate::front::FixedPoint;
40use crate::front::Freedom;
41use crate::front::LinesEqualLength;
42use crate::front::Midpoint;
43use crate::front::Object;
44use crate::front::Parallel;
45use crate::front::Perpendicular;
46use crate::front::PointCtor;
47use crate::front::Symmetric;
48use crate::front::Tangent;
49use crate::frontend::api::Expr;
50use crate::frontend::api::FileId;
51use crate::frontend::api::Number;
52use crate::frontend::api::ObjectId;
53use crate::frontend::api::ObjectKind;
54use crate::frontend::api::Plane;
55use crate::frontend::api::ProjectId;
56use crate::frontend::api::RestoreSketchCheckpointOutcome;
57use crate::frontend::api::SceneGraph;
58use crate::frontend::api::SceneGraphDelta;
59use crate::frontend::api::SketchCheckpointId;
60use crate::frontend::api::SourceDelta;
61use crate::frontend::api::SourceRef;
62use crate::frontend::api::Version;
63use crate::frontend::modify::find_defined_names;
64use crate::frontend::modify::next_free_name;
65use crate::frontend::modify::next_free_name_with_padding;
66use crate::frontend::sketch::Coincident;
67use crate::frontend::sketch::Constraint;
68use crate::frontend::sketch::ConstraintSegment;
69use crate::frontend::sketch::Diameter;
70use crate::frontend::sketch::ExistingSegmentCtor;
71use crate::frontend::sketch::Horizontal;
72use crate::frontend::sketch::LineCtor;
73use crate::frontend::sketch::Point2d;
74use crate::frontend::sketch::Radius;
75use crate::frontend::sketch::Segment;
76use crate::frontend::sketch::SegmentCtor;
77use crate::frontend::sketch::SketchApi;
78use crate::frontend::sketch::SketchCtor;
79use crate::frontend::sketch::Vertical;
80use crate::frontend::traverse::MutateBodyItem;
81use crate::frontend::traverse::TraversalReturn;
82use crate::frontend::traverse::Visitor;
83use crate::frontend::traverse::dfs_mut;
84use crate::id::IncIdGenerator;
85use crate::parsing::ast::types as ast;
86use crate::pretty::NumericSuffix;
87use crate::std::constraints::LinesAtAngleKind;
88use crate::walk::NodeMut;
89use crate::walk::Visitable;
90
91pub(crate) mod api;
92pub(crate) mod modify;
93pub(crate) mod sketch;
94
95pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
96
97#[derive(Debug, Clone)]
98struct SketchCheckpoint {
99 id: SketchCheckpointId,
100 source: SourceDelta,
101 program: Program,
102 scene_graph: SceneGraph,
103 exec_outcome: ExecOutcome,
104 point_freedom_cache: HashMap<ObjectId, Freedom>,
105 mock_memory: Option<SketchModeState>,
106}
107mod traverse;
108pub(crate) mod trim;
109
110struct ArcSizeConstraintParams {
111 points: Vec<ObjectId>,
112 function_name: &'static str,
113 value: f64,
114 units: NumericSuffix,
115 constraint_type_name: &'static str,
116}
117
118const POINT_FN: &str = "point";
119const POINT_AT_PARAM: &str = "at";
120const LINE_FN: &str = "line";
121const LINE_VARIABLE: &str = "line";
122const LINE_START_PARAM: &str = "start";
123const LINE_END_PARAM: &str = "end";
124const ARC_FN: &str = "arc";
125const ARC_VARIABLE: &str = "arc";
126const ARC_START_PARAM: &str = "start";
127const ARC_END_PARAM: &str = "end";
128const ARC_CENTER_PARAM: &str = "center";
129const CIRCLE_FN: &str = "circle";
130const CIRCLE_VARIABLE: &str = "circle";
131const CIRCLE_START_PARAM: &str = "start";
132const CIRCLE_CENTER_PARAM: &str = "center";
133
134const COINCIDENT_FN: &str = "coincident";
135const DIAMETER_FN: &str = "diameter";
136const DISTANCE_FN: &str = "distance";
137const FIXED_FN: &str = "fixed";
138const ANGLE_FN: &str = "angle";
139const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
140const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
141const EQUAL_LENGTH_FN: &str = "equalLength";
142const EQUAL_RADIUS_FN: &str = "equalRadius";
143const HORIZONTAL_FN: &str = "horizontal";
144const MIDPOINT_FN: &str = "midpoint";
145const MIDPOINT_POINT_PARAM: &str = "point";
146const RADIUS_FN: &str = "radius";
147const SYMMETRIC_FN: &str = "symmetric";
148const SYMMETRIC_AXIS_PARAM: &str = "axis";
149const TANGENT_FN: &str = "tangent";
150const VERTICAL_FN: &str = "vertical";
151
152const LINE_PROPERTY_START: &str = "start";
153const LINE_PROPERTY_END: &str = "end";
154
155const ARC_PROPERTY_START: &str = "start";
156const ARC_PROPERTY_END: &str = "end";
157const ARC_PROPERTY_CENTER: &str = "center";
158const CIRCLE_PROPERTY_START: &str = "start";
159const CIRCLE_PROPERTY_CENTER: &str = "center";
160
161const CONSTRUCTION_PARAM: &str = "construction";
162
163#[derive(Debug, Clone, Copy)]
164enum EditDeleteKind {
165 Edit,
166 DeleteNonSketch,
167}
168
169impl EditDeleteKind {
170 fn is_delete(&self) -> bool {
172 match self {
173 EditDeleteKind::Edit => false,
174 EditDeleteKind::DeleteNonSketch => true,
175 }
176 }
177
178 fn to_change_kind(self) -> ChangeKind {
179 match self {
180 EditDeleteKind::Edit => ChangeKind::Edit,
181 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
182 }
183 }
184}
185
186#[derive(Debug, Clone, Copy)]
187enum ChangeKind {
188 Add,
189 Edit,
190 Delete,
191 None,
192}
193
194#[derive(Debug, Clone, Serialize, ts_rs::TS)]
195#[ts(export, export_to = "FrontendApi.ts")]
196#[serde(tag = "type")]
197pub enum SetProgramOutcome {
198 #[serde(rename_all = "camelCase")]
199 Success {
200 scene_graph: Box<SceneGraph>,
201 exec_outcome: Box<ExecOutcome>,
202 checkpoint_id: Option<SketchCheckpointId>,
203 },
204 #[serde(rename_all = "camelCase")]
205 ExecFailure { error: Box<KclErrorWithOutputs> },
206}
207
208#[derive(Debug, Clone)]
209pub struct FrontendState {
210 program: Program,
211 scene_graph: SceneGraph,
212 point_freedom_cache: HashMap<ObjectId, Freedom>,
215 sketch_checkpoints: VecDeque<SketchCheckpoint>,
216 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
217}
218
219impl Default for FrontendState {
220 fn default() -> Self {
221 Self::new()
222 }
223}
224
225impl FrontendState {
226 pub fn new() -> Self {
227 Self {
228 program: Program::empty(),
229 scene_graph: SceneGraph {
230 project: ProjectId(0),
231 file: FileId(0),
232 version: Version(0),
233 objects: Default::default(),
234 settings: Default::default(),
235 sketch_mode: Default::default(),
236 },
237 point_freedom_cache: HashMap::new(),
238 sketch_checkpoints: VecDeque::new(),
239 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
240 }
241 }
242
243 pub fn scene_graph(&self) -> &SceneGraph {
245 &self.scene_graph
246 }
247
248 pub fn default_length_unit(&self) -> UnitLength {
249 self.program
250 .meta_settings()
251 .ok()
252 .flatten()
253 .map(|settings| settings.default_length_units)
254 .unwrap_or(UnitLength::Millimeters)
255 }
256
257 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
258 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
259
260 let checkpoint = SketchCheckpoint {
261 id: checkpoint_id,
262 source: SourceDelta {
263 text: source_from_ast(&self.program.ast),
264 },
265 program: self.program.clone(),
266 scene_graph: self.scene_graph.clone(),
267 exec_outcome,
268 point_freedom_cache: self.point_freedom_cache.clone(),
269 mock_memory: read_old_memory().await,
270 };
271
272 self.sketch_checkpoints.push_back(checkpoint);
273 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
274 self.sketch_checkpoints.pop_front();
275 }
276
277 Ok(checkpoint_id)
278 }
279
280 pub async fn restore_sketch_checkpoint(
281 &mut self,
282 checkpoint_id: SketchCheckpointId,
283 ) -> api::Result<RestoreSketchCheckpointOutcome> {
284 let checkpoint = self
285 .sketch_checkpoints
286 .iter()
287 .find(|checkpoint| checkpoint.id == checkpoint_id)
288 .cloned()
289 .ok_or_else(|| Error {
290 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
291 })?;
292
293 self.program = checkpoint.program;
294 self.scene_graph = checkpoint.scene_graph.clone();
295 self.point_freedom_cache = checkpoint.point_freedom_cache;
296
297 if let Some(mock_memory) = checkpoint.mock_memory {
298 write_old_memory(mock_memory).await;
299 } else {
300 clear_mem_cache().await;
301 }
302
303 Ok(RestoreSketchCheckpointOutcome {
304 source_delta: checkpoint.source,
305 scene_graph_delta: SceneGraphDelta {
306 new_graph: checkpoint.scene_graph,
307 new_objects: Vec::new(),
308 invalidates_ids: true,
309 exec_outcome: checkpoint.exec_outcome,
310 },
311 })
312 }
313
314 pub fn clear_sketch_checkpoints(&mut self) {
315 self.sketch_checkpoints.clear();
316 }
317}
318
319impl SketchApi for FrontendState {
320 async fn execute_mock(
321 &mut self,
322 ctx: &ExecutorContext,
323 _version: Version,
324 sketch: ObjectId,
325 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
326 let sketch_block_ref =
327 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
328
329 let mut truncated_program = self.program.clone();
330 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
331 .map_err(KclErrorWithOutputs::no_outputs)?;
332
333 let outcome = ctx
335 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
336 .await?;
337 let new_source = source_from_ast(&self.program.ast);
338 let src_delta = SourceDelta { text: new_source };
339 let outcome = self.update_state_after_exec(outcome, true);
341 let scene_graph_delta = SceneGraphDelta {
342 new_graph: self.scene_graph.clone(),
343 new_objects: Default::default(),
344 invalidates_ids: false,
345 exec_outcome: outcome,
346 };
347 Ok((src_delta, scene_graph_delta))
348 }
349
350 async fn new_sketch(
351 &mut self,
352 ctx: &ExecutorContext,
353 _project: ProjectId,
354 _file: FileId,
355 _version: Version,
356 args: SketchCtor,
357 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
358 let mut new_ast = self.program.ast.clone();
361 let mut plane_ast =
363 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
364 let mut defined_names = find_defined_names(&new_ast);
365 let is_face_of_expr = matches!(
366 &plane_ast,
367 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
368 );
369 if is_face_of_expr {
370 let face_name = next_free_name_with_padding("face", &defined_names)
371 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
372 let face_decl = ast::VariableDeclaration::new(
373 ast::VariableDeclarator::new(&face_name, plane_ast),
374 ast::ItemVisibility::Default,
375 ast::VariableKind::Const,
376 );
377 new_ast
378 .body
379 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
380 face_decl,
381 ))));
382 defined_names.insert(face_name.clone());
383 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
384 }
385 let sketch_ast = ast::SketchBlock {
386 arguments: vec![ast::LabeledArg {
387 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
388 arg: plane_ast,
389 }],
390 body: Default::default(),
391 is_being_edited: false,
392 non_code_meta: Default::default(),
393 digest: None,
394 };
395 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
398 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
399 let sketch_decl = ast::VariableDeclaration::new(
400 ast::VariableDeclarator::new(
401 &sketch_name,
402 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
403 ),
404 ast::ItemVisibility::Default,
405 ast::VariableKind::Const,
406 );
407 new_ast
408 .body
409 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
410 sketch_decl,
411 ))));
412 let new_source = source_from_ast(&new_ast);
414 let (new_program, errors) = Program::parse(&new_source)
416 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
417 if !errors.is_empty() {
418 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
419 "Error parsing KCL source after adding sketch: {errors:?}"
420 ))));
421 }
422 let Some(new_program) = new_program else {
423 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
424 "No AST produced after adding sketch".to_owned(),
425 )));
426 };
427
428 self.program = new_program.clone();
430
431 let outcome = ctx.run_with_caching(new_program.clone()).await?;
434 let freedom_analysis_ran = true;
435
436 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
437
438 let Some(sketch_id) = self
439 .scene_graph
440 .objects
441 .iter()
442 .filter_map(|object| match object.kind {
443 ObjectKind::Sketch(_) => Some(object.id),
444 _ => None,
445 })
446 .max_by_key(|id| id.0)
447 else {
448 return Err(KclErrorWithOutputs::from_error_outcome(
449 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
450 outcome,
451 ));
452 };
453 self.scene_graph.sketch_mode = Some(sketch_id);
455
456 let src_delta = SourceDelta { text: new_source };
457 let scene_graph_delta = SceneGraphDelta {
458 new_graph: self.scene_graph.clone(),
459 invalidates_ids: false,
460 new_objects: vec![sketch_id],
461 exec_outcome: outcome,
462 };
463 Ok((src_delta, scene_graph_delta, sketch_id))
464 }
465
466 async fn edit_sketch(
467 &mut self,
468 ctx: &ExecutorContext,
469 _project: ProjectId,
470 _file: FileId,
471 _version: Version,
472 sketch: ObjectId,
473 ) -> ExecResult<SceneGraphDelta> {
474 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
478 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
479 })?;
480 let ObjectKind::Sketch(_) = &sketch_object.kind else {
481 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
482 "Object is not a sketch, it is {}",
483 sketch_object.kind.human_friendly_kind_with_article()
484 ))));
485 };
486 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
487
488 self.scene_graph.sketch_mode = Some(sketch);
490
491 let mut truncated_program = self.program.clone();
493 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
494 .map_err(KclErrorWithOutputs::no_outputs)?;
495
496 let outcome = ctx
499 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
500 .await?;
501
502 let outcome = self.update_state_after_exec(outcome, true);
504 let scene_graph_delta = SceneGraphDelta {
505 new_graph: self.scene_graph.clone(),
506 invalidates_ids: false,
507 new_objects: Vec::new(),
508 exec_outcome: outcome,
509 };
510 Ok(scene_graph_delta)
511 }
512
513 async fn exit_sketch(
514 &mut self,
515 ctx: &ExecutorContext,
516 _version: Version,
517 sketch: ObjectId,
518 ) -> ExecResult<SceneGraph> {
519 #[cfg(not(target_arch = "wasm32"))]
521 let _ = sketch;
522 #[cfg(target_arch = "wasm32")]
523 if self.scene_graph.sketch_mode != Some(sketch) {
524 web_sys::console::warn_1(
525 &format!(
526 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
527 &self.scene_graph.sketch_mode
528 )
529 .into(),
530 );
531 }
532 self.scene_graph.sketch_mode = None;
533
534 let outcome = ctx.run_with_caching(self.program.clone()).await?;
536
537 self.update_state_after_exec(outcome, false);
539
540 Ok(self.scene_graph.clone())
541 }
542
543 async fn delete_sketch(
544 &mut self,
545 ctx: &ExecutorContext,
546 _version: Version,
547 sketch: ObjectId,
548 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
549 let mut new_ast = self.program.ast.clone();
552
553 let sketch_id = sketch;
555 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
556 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
557 })?;
558 let ObjectKind::Sketch(_) = &sketch_object.kind else {
559 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
560 "Object is not a sketch, it is {}",
561 sketch_object.kind.human_friendly_kind_with_article(),
562 ))));
563 };
564
565 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
567 .map_err(KclErrorWithOutputs::no_outputs)?;
568
569 self.execute_after_delete_sketch(ctx, &mut new_ast).await
570 }
571
572 async fn add_segment(
573 &mut self,
574 ctx: &ExecutorContext,
575 _version: Version,
576 sketch: ObjectId,
577 segment: SegmentCtor,
578 _label: Option<String>,
579 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
580 match segment {
582 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
583 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
584 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
585 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
586 }
587 }
588
589 async fn edit_segments(
590 &mut self,
591 ctx: &ExecutorContext,
592 _version: Version,
593 sketch: ObjectId,
594 segments: Vec<ExistingSegmentCtor>,
595 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
596 let sketch_block_ref =
598 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
599
600 let mut new_ast = self.program.ast.clone();
601 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
602
603 for segment in &segments {
606 segment_ids_edited.insert(segment.id);
607 }
608
609 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
624
625 for segment in segments {
626 let segment_id = segment.id;
627 match segment.ctor {
628 SegmentCtor::Point(ctor) => {
629 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
631 && let ObjectKind::Segment { segment } = &segment_object.kind
632 && let Segment::Point(point) = segment
633 && let Some(owner_id) = point.owner
634 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
635 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
636 {
637 match owner_segment {
638 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
639 if let Some(existing) = final_edits.get_mut(&owner_id) {
640 let SegmentCtor::Line(line_ctor) = existing else {
641 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
642 "Internal: Expected line ctor for owner, but found {}",
643 existing.human_friendly_kind_with_article()
644 ))));
645 };
646 if line.start == segment_id {
648 line_ctor.start = ctor.position;
649 } else {
650 line_ctor.end = ctor.position;
651 }
652 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
653 let mut line_ctor = line_ctor.clone();
655 if line.start == segment_id {
656 line_ctor.start = ctor.position;
657 } else {
658 line_ctor.end = ctor.position;
659 }
660 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
661 } else {
662 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
664 "Internal: Line does not have line ctor, but found {}",
665 line.ctor.human_friendly_kind_with_article()
666 ))));
667 }
668 continue;
669 }
670 Segment::Arc(arc)
671 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
672 {
673 if let Some(existing) = final_edits.get_mut(&owner_id) {
674 let SegmentCtor::Arc(arc_ctor) = existing else {
675 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
676 "Internal: Expected arc ctor for owner, but found {}",
677 existing.human_friendly_kind_with_article()
678 ))));
679 };
680 if arc.start == segment_id {
681 arc_ctor.start = ctor.position;
682 } else if arc.end == segment_id {
683 arc_ctor.end = ctor.position;
684 } else {
685 arc_ctor.center = ctor.position;
686 }
687 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
688 let mut arc_ctor = arc_ctor.clone();
689 if arc.start == segment_id {
690 arc_ctor.start = ctor.position;
691 } else if arc.end == segment_id {
692 arc_ctor.end = ctor.position;
693 } else {
694 arc_ctor.center = ctor.position;
695 }
696 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
697 } else {
698 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
699 "Internal: Arc does not have arc ctor, but found {}",
700 arc.ctor.human_friendly_kind_with_article()
701 ))));
702 }
703 continue;
704 }
705 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
706 if let Some(existing) = final_edits.get_mut(&owner_id) {
707 let SegmentCtor::Circle(circle_ctor) = existing else {
708 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
709 "Internal: Expected circle ctor for owner, but found {}",
710 existing.human_friendly_kind_with_article()
711 ))));
712 };
713 if circle.start == segment_id {
714 circle_ctor.start = ctor.position;
715 } else {
716 circle_ctor.center = ctor.position;
717 }
718 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
719 let mut circle_ctor = circle_ctor.clone();
720 if circle.start == segment_id {
721 circle_ctor.start = ctor.position;
722 } else {
723 circle_ctor.center = ctor.position;
724 }
725 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
726 } else {
727 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
728 "Internal: Circle does not have circle ctor, but found {}",
729 circle.ctor.human_friendly_kind_with_article()
730 ))));
731 }
732 continue;
733 }
734 _ => {}
735 }
736 }
737
738 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
740 }
741 SegmentCtor::Line(ctor) => {
742 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
743 }
744 SegmentCtor::Arc(ctor) => {
745 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
746 }
747 SegmentCtor::Circle(ctor) => {
748 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
749 }
750 }
751 }
752
753 for (segment_id, ctor) in final_edits {
754 match ctor {
755 SegmentCtor::Point(ctor) => self
756 .edit_point(&mut new_ast, sketch, segment_id, ctor)
757 .map_err(KclErrorWithOutputs::no_outputs)?,
758 SegmentCtor::Line(ctor) => self
759 .edit_line(&mut new_ast, sketch, segment_id, ctor)
760 .map_err(KclErrorWithOutputs::no_outputs)?,
761 SegmentCtor::Arc(ctor) => self
762 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
763 .map_err(KclErrorWithOutputs::no_outputs)?,
764 SegmentCtor::Circle(ctor) => self
765 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
766 .map_err(KclErrorWithOutputs::no_outputs)?,
767 }
768 }
769 self.execute_after_edit(
770 ctx,
771 sketch,
772 sketch_block_ref,
773 segment_ids_edited,
774 EditDeleteKind::Edit,
775 &mut new_ast,
776 )
777 .await
778 }
779
780 async fn delete_objects(
781 &mut self,
782 ctx: &ExecutorContext,
783 _version: Version,
784 sketch: ObjectId,
785 constraint_ids: Vec<ObjectId>,
786 segment_ids: Vec<ObjectId>,
787 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
788 let sketch_block_ref =
790 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
791
792 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
794 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
795
796 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
799
800 for segment_id in segment_ids_set.iter().copied() {
801 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
802 && let ObjectKind::Segment { segment } = &segment_object.kind
803 && let Segment::Point(point) = segment
804 && let Some(owner_id) = point.owner
805 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
806 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
807 && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
808 {
809 resolved_segment_ids_to_delete.insert(owner_id);
811 } else {
812 resolved_segment_ids_to_delete.insert(segment_id);
814 }
815 }
816 let referenced_constraint_ids = self
817 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
818 .map_err(KclErrorWithOutputs::no_outputs)?;
819
820 let mut new_ast = self.program.ast.clone();
821
822 for constraint_id in referenced_constraint_ids {
823 if constraint_ids_set.contains(&constraint_id) {
824 continue;
825 }
826
827 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
828 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
829 })?;
830 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
831 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
832 "Object is not a constraint, it is {}",
833 constraint_object.kind.human_friendly_kind_with_article()
834 ))));
835 };
836
837 match constraint {
838 Constraint::Coincident(coincident) => {
839 let remaining_segments =
840 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
841
842 if remaining_segments.len() >= 2 {
844 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
845 .map_err(KclErrorWithOutputs::no_outputs)?;
846 } else {
847 constraint_ids_set.insert(constraint_id);
848 }
849 }
850 Constraint::EqualRadius(equal_radius) => {
851 let remaining_input = equal_radius
852 .input
853 .iter()
854 .copied()
855 .filter(|segment_id| {
856 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
857 })
858 .collect::<Vec<_>>();
859
860 if remaining_input.len() >= 2 {
861 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
862 .map_err(KclErrorWithOutputs::no_outputs)?;
863 } else {
864 constraint_ids_set.insert(constraint_id);
865 }
866 }
867 Constraint::LinesEqualLength(lines_equal_length) => {
868 let remaining_lines = lines_equal_length
869 .lines
870 .iter()
871 .copied()
872 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
873 .collect::<Vec<_>>();
874
875 if remaining_lines.len() >= 2 {
877 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
878 .map_err(KclErrorWithOutputs::no_outputs)?;
879 } else {
880 constraint_ids_set.insert(constraint_id);
881 }
882 }
883 Constraint::Parallel(parallel) => {
884 let remaining_lines = parallel
885 .lines
886 .iter()
887 .copied()
888 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
889 .collect::<Vec<_>>();
890
891 if remaining_lines.len() >= 2 {
892 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
893 .map_err(KclErrorWithOutputs::no_outputs)?;
894 } else {
895 constraint_ids_set.insert(constraint_id);
896 }
897 }
898 Constraint::Horizontal(Horizontal::Points { points }) => {
899 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
900
901 if remaining_points.len() >= 2 {
902 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
903 .map_err(KclErrorWithOutputs::no_outputs)?;
904 } else {
905 constraint_ids_set.insert(constraint_id);
906 }
907 }
908 Constraint::Vertical(Vertical::Points { points }) => {
909 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
910
911 if remaining_points.len() >= 2 {
912 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
913 .map_err(KclErrorWithOutputs::no_outputs)?;
914 } else {
915 constraint_ids_set.insert(constraint_id);
916 }
917 }
918 Constraint::Fixed(fixed) => {
919 if fixed.points.iter().any(|fixed_point| {
920 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
921 }) {
922 constraint_ids_set.insert(constraint_id);
923 }
924 }
925 _ => {
926 constraint_ids_set.insert(constraint_id);
928 }
929 }
930 }
931
932 for constraint_id in constraint_ids_set {
933 self.delete_constraint(&mut new_ast, sketch, constraint_id)
934 .map_err(KclErrorWithOutputs::no_outputs)?;
935 }
936 for segment_id in resolved_segment_ids_to_delete {
937 self.delete_segment(&mut new_ast, sketch, segment_id)
938 .map_err(KclErrorWithOutputs::no_outputs)?;
939 }
940
941 self.execute_after_edit(
942 ctx,
943 sketch,
944 sketch_block_ref,
945 Default::default(),
946 EditDeleteKind::DeleteNonSketch,
947 &mut new_ast,
948 )
949 .await
950 }
951
952 async fn add_constraint(
953 &mut self,
954 ctx: &ExecutorContext,
955 _version: Version,
956 sketch: ObjectId,
957 constraint: Constraint,
958 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
959 let original_program = self.program.clone();
963 let original_scene_graph = self.scene_graph.clone();
964
965 let mut new_ast = self.program.ast.clone();
966 let sketch_block_ref = match constraint {
967 Constraint::Coincident(coincident) => self
968 .add_coincident(sketch, coincident, &mut new_ast)
969 .await
970 .map_err(KclErrorWithOutputs::no_outputs)?,
971 Constraint::Distance(distance) => self
972 .add_distance(sketch, distance, &mut new_ast)
973 .await
974 .map_err(KclErrorWithOutputs::no_outputs)?,
975 Constraint::EqualRadius(equal_radius) => self
976 .add_equal_radius(sketch, equal_radius, &mut new_ast)
977 .await
978 .map_err(KclErrorWithOutputs::no_outputs)?,
979 Constraint::Fixed(fixed) => self
980 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
981 .await
982 .map_err(KclErrorWithOutputs::no_outputs)?,
983 Constraint::HorizontalDistance(distance) => self
984 .add_horizontal_distance(sketch, distance, &mut new_ast)
985 .await
986 .map_err(KclErrorWithOutputs::no_outputs)?,
987 Constraint::VerticalDistance(distance) => self
988 .add_vertical_distance(sketch, distance, &mut new_ast)
989 .await
990 .map_err(KclErrorWithOutputs::no_outputs)?,
991 Constraint::Horizontal(horizontal) => self
992 .add_horizontal(sketch, horizontal, &mut new_ast)
993 .await
994 .map_err(KclErrorWithOutputs::no_outputs)?,
995 Constraint::LinesEqualLength(lines_equal_length) => self
996 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
997 .await
998 .map_err(KclErrorWithOutputs::no_outputs)?,
999 Constraint::Midpoint(midpoint) => self
1000 .add_midpoint(sketch, midpoint, &mut new_ast)
1001 .await
1002 .map_err(KclErrorWithOutputs::no_outputs)?,
1003 Constraint::Parallel(parallel) => self
1004 .add_parallel(sketch, parallel, &mut new_ast)
1005 .await
1006 .map_err(KclErrorWithOutputs::no_outputs)?,
1007 Constraint::Perpendicular(perpendicular) => self
1008 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1009 .await
1010 .map_err(KclErrorWithOutputs::no_outputs)?,
1011 Constraint::Radius(radius) => self
1012 .add_radius(sketch, radius, &mut new_ast)
1013 .await
1014 .map_err(KclErrorWithOutputs::no_outputs)?,
1015 Constraint::Diameter(diameter) => self
1016 .add_diameter(sketch, diameter, &mut new_ast)
1017 .await
1018 .map_err(KclErrorWithOutputs::no_outputs)?,
1019 Constraint::Symmetric(symmetric) => self
1020 .add_symmetric(sketch, symmetric, &mut new_ast)
1021 .await
1022 .map_err(KclErrorWithOutputs::no_outputs)?,
1023 Constraint::Vertical(vertical) => self
1024 .add_vertical(sketch, vertical, &mut new_ast)
1025 .await
1026 .map_err(KclErrorWithOutputs::no_outputs)?,
1027 Constraint::Angle(lines_at_angle) => self
1028 .add_angle(sketch, lines_at_angle, &mut new_ast)
1029 .await
1030 .map_err(KclErrorWithOutputs::no_outputs)?,
1031 Constraint::Tangent(tangent) => self
1032 .add_tangent(sketch, tangent, &mut new_ast)
1033 .await
1034 .map_err(KclErrorWithOutputs::no_outputs)?,
1035 };
1036
1037 let result = self
1038 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1039 .await;
1040
1041 if result.is_err() {
1043 self.program = original_program;
1044 self.scene_graph = original_scene_graph;
1045 }
1046
1047 result
1048 }
1049
1050 async fn chain_segment(
1051 &mut self,
1052 ctx: &ExecutorContext,
1053 version: Version,
1054 sketch: ObjectId,
1055 previous_segment_end_point_id: ObjectId,
1056 segment: SegmentCtor,
1057 _label: Option<String>,
1058 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1059 let SegmentCtor::Line(line_ctor) = segment else {
1063 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1064 "chain_segment currently only supports Line segments, got {}",
1065 segment.human_friendly_kind_with_article(),
1066 ))));
1067 };
1068
1069 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1071
1072 let new_line_id = first_scene_delta
1075 .new_objects
1076 .iter()
1077 .find(|&obj_id| {
1078 let obj = self.scene_graph.objects.get(obj_id.0);
1079 if let Some(obj) = obj {
1080 matches!(
1081 &obj.kind,
1082 ObjectKind::Segment {
1083 segment: Segment::Line(_)
1084 }
1085 )
1086 } else {
1087 false
1088 }
1089 })
1090 .ok_or_else(|| {
1091 KclErrorWithOutputs::no_outputs(KclError::refactor(
1092 "Failed to find new line segment in scene graph".to_string(),
1093 ))
1094 })?;
1095
1096 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1097 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1098 "New line object not found: {new_line_id:?}"
1099 )))
1100 })?;
1101
1102 let ObjectKind::Segment {
1103 segment: new_line_segment,
1104 } = &new_line_obj.kind
1105 else {
1106 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1107 "Object is not a segment: {new_line_obj:?}"
1108 ))));
1109 };
1110
1111 let Segment::Line(new_line) = new_line_segment else {
1112 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1113 "Segment is not a line: {new_line_segment:?}"
1114 ))));
1115 };
1116
1117 let new_line_start_point_id = new_line.start;
1118
1119 let coincident = Coincident {
1121 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1122 };
1123
1124 let (final_src_delta, final_scene_delta) = self
1125 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1126 .await?;
1127
1128 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1131 combined_new_objects.extend(final_scene_delta.new_objects);
1132
1133 let scene_graph_delta = SceneGraphDelta {
1134 new_graph: self.scene_graph.clone(),
1135 invalidates_ids: false,
1136 new_objects: combined_new_objects,
1137 exec_outcome: final_scene_delta.exec_outcome,
1138 };
1139
1140 Ok((final_src_delta, scene_graph_delta))
1141 }
1142
1143 async fn edit_constraint(
1144 &mut self,
1145 ctx: &ExecutorContext,
1146 _version: Version,
1147 sketch: ObjectId,
1148 constraint_id: ObjectId,
1149 value_expression: String,
1150 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1151 let sketch_block_ref =
1153 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1154
1155 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1156 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1157 })?;
1158 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1159 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1160 "Object is not a constraint: {constraint_id:?}"
1161 ))));
1162 }
1163
1164 let mut new_ast = self.program.ast.clone();
1165
1166 let (parsed, errors) = Program::parse(&value_expression)
1168 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1169 if !errors.is_empty() {
1170 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1171 "Error parsing value expression: {errors:?}"
1172 ))));
1173 }
1174 let mut parsed = parsed.ok_or_else(|| {
1175 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1176 })?;
1177 if parsed.ast.body.is_empty() {
1178 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1179 "Empty value expression".to_string(),
1180 )));
1181 }
1182 let first = parsed.ast.body.remove(0);
1183 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1184 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1185 "Value expression must be a simple expression".to_string(),
1186 )));
1187 };
1188
1189 let new_value: ast::BinaryPart = expr_stmt
1190 .inner
1191 .expression
1192 .try_into()
1193 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1194
1195 self.mutate_ast(
1196 &mut new_ast,
1197 constraint_id,
1198 AstMutateCommand::EditConstraintValue { value: new_value },
1199 )
1200 .map_err(KclErrorWithOutputs::no_outputs)?;
1201
1202 self.execute_after_edit(
1203 ctx,
1204 sketch,
1205 sketch_block_ref,
1206 Default::default(),
1207 EditDeleteKind::Edit,
1208 &mut new_ast,
1209 )
1210 .await
1211 }
1212
1213 async fn batch_split_segment_operations(
1221 &mut self,
1222 ctx: &ExecutorContext,
1223 _version: Version,
1224 sketch: ObjectId,
1225 edit_segments: Vec<ExistingSegmentCtor>,
1226 add_constraints: Vec<Constraint>,
1227 delete_constraint_ids: Vec<ObjectId>,
1228 _new_segment_info: sketch::NewSegmentInfo,
1229 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1230 let sketch_block_ref =
1232 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1233
1234 let mut new_ast = self.program.ast.clone();
1235 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1236
1237 for segment in edit_segments {
1239 segment_ids_edited.insert(segment.id);
1240 match segment.ctor {
1241 SegmentCtor::Point(ctor) => self
1242 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1243 .map_err(KclErrorWithOutputs::no_outputs)?,
1244 SegmentCtor::Line(ctor) => self
1245 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1246 .map_err(KclErrorWithOutputs::no_outputs)?,
1247 SegmentCtor::Arc(ctor) => self
1248 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1249 .map_err(KclErrorWithOutputs::no_outputs)?,
1250 SegmentCtor::Circle(ctor) => self
1251 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1252 .map_err(KclErrorWithOutputs::no_outputs)?,
1253 }
1254 }
1255
1256 for constraint in add_constraints {
1258 match constraint {
1259 Constraint::Coincident(coincident) => {
1260 self.add_coincident(sketch, coincident, &mut new_ast)
1261 .await
1262 .map_err(KclErrorWithOutputs::no_outputs)?;
1263 }
1264 Constraint::Distance(distance) => {
1265 self.add_distance(sketch, distance, &mut new_ast)
1266 .await
1267 .map_err(KclErrorWithOutputs::no_outputs)?;
1268 }
1269 Constraint::EqualRadius(equal_radius) => {
1270 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1271 .await
1272 .map_err(KclErrorWithOutputs::no_outputs)?;
1273 }
1274 Constraint::Fixed(fixed) => {
1275 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1276 .await
1277 .map_err(KclErrorWithOutputs::no_outputs)?;
1278 }
1279 Constraint::HorizontalDistance(distance) => {
1280 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1281 .await
1282 .map_err(KclErrorWithOutputs::no_outputs)?;
1283 }
1284 Constraint::VerticalDistance(distance) => {
1285 self.add_vertical_distance(sketch, distance, &mut new_ast)
1286 .await
1287 .map_err(KclErrorWithOutputs::no_outputs)?;
1288 }
1289 Constraint::Horizontal(horizontal) => {
1290 self.add_horizontal(sketch, horizontal, &mut new_ast)
1291 .await
1292 .map_err(KclErrorWithOutputs::no_outputs)?;
1293 }
1294 Constraint::LinesEqualLength(lines_equal_length) => {
1295 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1296 .await
1297 .map_err(KclErrorWithOutputs::no_outputs)?;
1298 }
1299 Constraint::Midpoint(midpoint) => {
1300 self.add_midpoint(sketch, midpoint, &mut new_ast)
1301 .await
1302 .map_err(KclErrorWithOutputs::no_outputs)?;
1303 }
1304 Constraint::Parallel(parallel) => {
1305 self.add_parallel(sketch, parallel, &mut new_ast)
1306 .await
1307 .map_err(KclErrorWithOutputs::no_outputs)?;
1308 }
1309 Constraint::Perpendicular(perpendicular) => {
1310 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1311 .await
1312 .map_err(KclErrorWithOutputs::no_outputs)?;
1313 }
1314 Constraint::Vertical(vertical) => {
1315 self.add_vertical(sketch, vertical, &mut new_ast)
1316 .await
1317 .map_err(KclErrorWithOutputs::no_outputs)?;
1318 }
1319 Constraint::Diameter(diameter) => {
1320 self.add_diameter(sketch, diameter, &mut new_ast)
1321 .await
1322 .map_err(KclErrorWithOutputs::no_outputs)?;
1323 }
1324 Constraint::Radius(radius) => {
1325 self.add_radius(sketch, radius, &mut new_ast)
1326 .await
1327 .map_err(KclErrorWithOutputs::no_outputs)?;
1328 }
1329 Constraint::Symmetric(symmetric) => {
1330 self.add_symmetric(sketch, symmetric, &mut new_ast)
1331 .await
1332 .map_err(KclErrorWithOutputs::no_outputs)?;
1333 }
1334 Constraint::Angle(angle) => {
1335 self.add_angle(sketch, angle, &mut new_ast)
1336 .await
1337 .map_err(KclErrorWithOutputs::no_outputs)?;
1338 }
1339 Constraint::Tangent(tangent) => {
1340 self.add_tangent(sketch, tangent, &mut new_ast)
1341 .await
1342 .map_err(KclErrorWithOutputs::no_outputs)?;
1343 }
1344 }
1345 }
1346
1347 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1349
1350 let has_constraint_deletions = !constraint_ids_set.is_empty();
1351 for constraint_id in constraint_ids_set {
1352 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1353 .map_err(KclErrorWithOutputs::no_outputs)?;
1354 }
1355
1356 let (source_delta, mut scene_graph_delta) = self
1360 .execute_after_edit(
1361 ctx,
1362 sketch,
1363 sketch_block_ref,
1364 segment_ids_edited,
1365 EditDeleteKind::Edit,
1366 &mut new_ast,
1367 )
1368 .await?;
1369
1370 if has_constraint_deletions {
1373 scene_graph_delta.invalidates_ids = true;
1374 }
1375
1376 Ok((source_delta, scene_graph_delta))
1377 }
1378
1379 async fn batch_tail_cut_operations(
1380 &mut self,
1381 ctx: &ExecutorContext,
1382 _version: Version,
1383 sketch: ObjectId,
1384 edit_segments: Vec<ExistingSegmentCtor>,
1385 add_constraints: Vec<Constraint>,
1386 delete_constraint_ids: Vec<ObjectId>,
1387 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1388 let sketch_block_ref =
1389 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1390
1391 let mut new_ast = self.program.ast.clone();
1392 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1393
1394 for segment in edit_segments {
1396 segment_ids_edited.insert(segment.id);
1397 match segment.ctor {
1398 SegmentCtor::Point(ctor) => self
1399 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1400 .map_err(KclErrorWithOutputs::no_outputs)?,
1401 SegmentCtor::Line(ctor) => self
1402 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1403 .map_err(KclErrorWithOutputs::no_outputs)?,
1404 SegmentCtor::Arc(ctor) => self
1405 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1406 .map_err(KclErrorWithOutputs::no_outputs)?,
1407 SegmentCtor::Circle(ctor) => self
1408 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1409 .map_err(KclErrorWithOutputs::no_outputs)?,
1410 }
1411 }
1412
1413 for constraint in add_constraints {
1415 match constraint {
1416 Constraint::Coincident(coincident) => {
1417 self.add_coincident(sketch, coincident, &mut new_ast)
1418 .await
1419 .map_err(KclErrorWithOutputs::no_outputs)?;
1420 }
1421 other => {
1422 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1423 "unsupported constraint in tail cut batch: {other:?}"
1424 ))));
1425 }
1426 }
1427 }
1428
1429 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1431
1432 let has_constraint_deletions = !constraint_ids_set.is_empty();
1433 for constraint_id in constraint_ids_set {
1434 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1435 .map_err(KclErrorWithOutputs::no_outputs)?;
1436 }
1437
1438 let (source_delta, mut scene_graph_delta) = self
1442 .execute_after_edit(
1443 ctx,
1444 sketch,
1445 sketch_block_ref,
1446 segment_ids_edited,
1447 EditDeleteKind::Edit,
1448 &mut new_ast,
1449 )
1450 .await?;
1451
1452 if has_constraint_deletions {
1455 scene_graph_delta.invalidates_ids = true;
1456 }
1457
1458 Ok((source_delta, scene_graph_delta))
1459 }
1460}
1461
1462impl FrontendState {
1463 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1464 self.program = program.clone();
1465
1466 self.point_freedom_cache.clear();
1477 match ctx.run_with_caching(program).await {
1478 Ok(outcome) => {
1479 let outcome = self.update_state_after_exec(outcome, true);
1480 let checkpoint_id = self
1481 .create_sketch_checkpoint(outcome.clone())
1482 .await
1483 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1484 Ok(SetProgramOutcome::Success {
1485 scene_graph: Box::new(self.scene_graph.clone()),
1486 exec_outcome: Box::new(outcome),
1487 checkpoint_id: Some(checkpoint_id),
1488 })
1489 }
1490 Err(mut err) => {
1491 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1494 self.update_state_after_exec(outcome, true);
1495 err.scene_graph = Some(self.scene_graph.clone());
1496 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1497 }
1498 }
1499 }
1500
1501 pub async fn engine_execute(
1504 &mut self,
1505 ctx: &ExecutorContext,
1506 program: Program,
1507 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1508 self.program = program.clone();
1509
1510 self.point_freedom_cache.clear();
1514 match ctx.run_with_caching(program).await {
1515 Ok(outcome) => {
1516 let outcome = self.update_state_after_exec(outcome, true);
1517 Ok(SceneGraphDelta {
1518 new_graph: self.scene_graph.clone(),
1519 exec_outcome: outcome,
1520 new_objects: Default::default(),
1522 invalidates_ids: Default::default(),
1524 })
1525 }
1526 Err(mut err) => {
1527 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1529 self.update_state_after_exec(outcome, true);
1530 err.scene_graph = Some(self.scene_graph.clone());
1531 Err(err)
1532 }
1533 }
1534 }
1535
1536 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1537 if matches!(err.error, KclError::EngineHangup { .. }) {
1538 return Err(err);
1542 }
1543
1544 let KclErrorWithOutputs {
1545 error,
1546 mut non_fatal,
1547 variables,
1548 #[cfg(feature = "artifact-graph")]
1549 operations,
1550 #[cfg(feature = "artifact-graph")]
1551 artifact_graph,
1552 #[cfg(feature = "artifact-graph")]
1553 scene_objects,
1554 #[cfg(feature = "artifact-graph")]
1555 source_range_to_object,
1556 #[cfg(feature = "artifact-graph")]
1557 var_solutions,
1558 filenames,
1559 default_planes,
1560 ..
1561 } = err;
1562
1563 if let Some(source_range) = error.source_ranges().first() {
1564 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1565 } else {
1566 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1567 }
1568
1569 Ok(ExecOutcome {
1570 variables,
1571 filenames,
1572 #[cfg(feature = "artifact-graph")]
1573 operations,
1574 #[cfg(feature = "artifact-graph")]
1575 artifact_graph,
1576 #[cfg(feature = "artifact-graph")]
1577 scene_objects,
1578 #[cfg(feature = "artifact-graph")]
1579 source_range_to_object,
1580 #[cfg(feature = "artifact-graph")]
1581 var_solutions,
1582 issues: non_fatal,
1583 default_planes,
1584 })
1585 }
1586
1587 async fn add_point(
1588 &mut self,
1589 ctx: &ExecutorContext,
1590 sketch: ObjectId,
1591 ctor: PointCtor,
1592 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1593 let at_ast = to_ast_point2d(&ctor.position)
1595 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1596 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1597 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1598 unlabeled: None,
1599 arguments: vec![ast::LabeledArg {
1600 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1601 arg: at_ast,
1602 }],
1603 digest: None,
1604 non_code_meta: Default::default(),
1605 })));
1606
1607 let sketch_id = sketch;
1609 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1610 #[cfg(target_arch = "wasm32")]
1611 web_sys::console::error_1(
1612 &format!(
1613 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1614 &self.scene_graph.objects
1615 )
1616 .into(),
1617 );
1618 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1619 })?;
1620 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1621 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1622 "Object is not a sketch, it is {}",
1623 sketch_object.kind.human_friendly_kind_with_article(),
1624 ))));
1625 };
1626 let mut new_ast = self.program.ast.clone();
1628 let (sketch_block_ref, _) = self
1629 .mutate_ast(
1630 &mut new_ast,
1631 sketch_id,
1632 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1633 )
1634 .map_err(KclErrorWithOutputs::no_outputs)?;
1635 let new_source = source_from_ast(&new_ast);
1637 let (new_program, errors) = Program::parse(&new_source)
1639 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1640 if !errors.is_empty() {
1641 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1642 "Error parsing KCL source after adding point: {errors:?}"
1643 ))));
1644 }
1645 let Some(new_program) = new_program else {
1646 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1647 "No AST produced after adding point".to_string(),
1648 )));
1649 };
1650
1651 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1652 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1653 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1654 )))
1655 })?;
1656 #[cfg(not(feature = "artifact-graph"))]
1657 let _ = point_node_ref;
1658
1659 self.program = new_program.clone();
1661
1662 let mut truncated_program = new_program;
1664 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1665 .map_err(KclErrorWithOutputs::no_outputs)?;
1666
1667 let outcome = ctx
1669 .run_mock(
1670 &truncated_program,
1671 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1672 )
1673 .await?;
1674
1675 #[cfg(not(feature = "artifact-graph"))]
1676 let new_object_ids = Vec::new();
1677 #[cfg(feature = "artifact-graph")]
1678 let new_object_ids = {
1679 let make_err =
1680 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1681 let segment_id = outcome
1682 .source_range_to_object
1683 .get(&point_node_ref.range)
1684 .copied()
1685 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1686 let segment_object = outcome
1687 .scene_objects
1688 .get(segment_id.0)
1689 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1690 let ObjectKind::Segment { segment } = &segment_object.kind else {
1691 return Err(make_err(format!(
1692 "Object is not a segment, it is {}",
1693 segment_object.kind.human_friendly_kind_with_article()
1694 )));
1695 };
1696 let Segment::Point(_) = segment else {
1697 return Err(make_err(format!(
1698 "Segment is not a point, it is {}",
1699 segment.human_friendly_kind_with_article()
1700 )));
1701 };
1702 vec![segment_id]
1703 };
1704 let src_delta = SourceDelta { text: new_source };
1705 let outcome = self.update_state_after_exec(outcome, false);
1707 let scene_graph_delta = SceneGraphDelta {
1708 new_graph: self.scene_graph.clone(),
1709 invalidates_ids: false,
1710 new_objects: new_object_ids,
1711 exec_outcome: outcome,
1712 };
1713 Ok((src_delta, scene_graph_delta))
1714 }
1715
1716 async fn add_line(
1717 &mut self,
1718 ctx: &ExecutorContext,
1719 sketch: ObjectId,
1720 ctor: LineCtor,
1721 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1722 let start_ast = to_ast_point2d(&ctor.start)
1724 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1725 let end_ast = to_ast_point2d(&ctor.end)
1726 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1727 let mut arguments = vec![
1728 ast::LabeledArg {
1729 label: Some(ast::Identifier::new(LINE_START_PARAM)),
1730 arg: start_ast,
1731 },
1732 ast::LabeledArg {
1733 label: Some(ast::Identifier::new(LINE_END_PARAM)),
1734 arg: end_ast,
1735 },
1736 ];
1737 if ctor.construction == Some(true) {
1739 arguments.push(ast::LabeledArg {
1740 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1741 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1742 value: ast::LiteralValue::Bool(true),
1743 raw: "true".to_string(),
1744 digest: None,
1745 }))),
1746 });
1747 }
1748 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1749 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1750 unlabeled: None,
1751 arguments,
1752 digest: None,
1753 non_code_meta: Default::default(),
1754 })));
1755
1756 let sketch_id = sketch;
1758 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1759 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1760 })?;
1761 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1762 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1763 "Object is not a sketch, it is {}",
1764 sketch_object.kind.human_friendly_kind_with_article(),
1765 ))));
1766 };
1767 let mut new_ast = self.program.ast.clone();
1769 let (sketch_block_ref, _) = self
1770 .mutate_ast(
1771 &mut new_ast,
1772 sketch_id,
1773 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1774 )
1775 .map_err(KclErrorWithOutputs::no_outputs)?;
1776 let new_source = source_from_ast(&new_ast);
1778 let (new_program, errors) = Program::parse(&new_source)
1780 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1781 if !errors.is_empty() {
1782 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1783 "Error parsing KCL source after adding line: {errors:?}"
1784 ))));
1785 }
1786 let Some(new_program) = new_program else {
1787 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1788 "No AST produced after adding line".to_string(),
1789 )));
1790 };
1791
1792 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1793 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1794 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1795 )))
1796 })?;
1797 #[cfg(not(feature = "artifact-graph"))]
1798 let _ = line_node_ref;
1799
1800 self.program = new_program.clone();
1802
1803 let mut truncated_program = new_program;
1805 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1806 .map_err(KclErrorWithOutputs::no_outputs)?;
1807
1808 let outcome = ctx
1810 .run_mock(
1811 &truncated_program,
1812 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1813 )
1814 .await?;
1815
1816 #[cfg(not(feature = "artifact-graph"))]
1817 let new_object_ids = Vec::new();
1818 #[cfg(feature = "artifact-graph")]
1819 let new_object_ids = {
1820 let make_err =
1821 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1822 let segment_id = outcome
1823 .source_range_to_object
1824 .get(&line_node_ref.range)
1825 .copied()
1826 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1827 let segment_object = outcome
1828 .scene_object_by_id(segment_id)
1829 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1830 let ObjectKind::Segment { segment } = &segment_object.kind else {
1831 return Err(make_err(format!(
1832 "Object is not a segment, it is {}",
1833 segment_object.kind.human_friendly_kind_with_article()
1834 )));
1835 };
1836 let Segment::Line(line) = segment else {
1837 return Err(make_err(format!(
1838 "Segment is not a line, it is {}",
1839 segment.human_friendly_kind_with_article()
1840 )));
1841 };
1842 vec![line.start, line.end, segment_id]
1843 };
1844 let src_delta = SourceDelta { text: new_source };
1845 let outcome = self.update_state_after_exec(outcome, false);
1847 let scene_graph_delta = SceneGraphDelta {
1848 new_graph: self.scene_graph.clone(),
1849 invalidates_ids: false,
1850 new_objects: new_object_ids,
1851 exec_outcome: outcome,
1852 };
1853 Ok((src_delta, scene_graph_delta))
1854 }
1855
1856 async fn add_arc(
1857 &mut self,
1858 ctx: &ExecutorContext,
1859 sketch: ObjectId,
1860 ctor: ArcCtor,
1861 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1862 let start_ast = to_ast_point2d(&ctor.start)
1864 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1865 let end_ast = to_ast_point2d(&ctor.end)
1866 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1867 let center_ast = to_ast_point2d(&ctor.center)
1868 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1869 let mut arguments = vec![
1870 ast::LabeledArg {
1871 label: Some(ast::Identifier::new(ARC_START_PARAM)),
1872 arg: start_ast,
1873 },
1874 ast::LabeledArg {
1875 label: Some(ast::Identifier::new(ARC_END_PARAM)),
1876 arg: end_ast,
1877 },
1878 ast::LabeledArg {
1879 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1880 arg: center_ast,
1881 },
1882 ];
1883 if ctor.construction == Some(true) {
1885 arguments.push(ast::LabeledArg {
1886 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1887 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1888 value: ast::LiteralValue::Bool(true),
1889 raw: "true".to_string(),
1890 digest: None,
1891 }))),
1892 });
1893 }
1894 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1895 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1896 unlabeled: None,
1897 arguments,
1898 digest: None,
1899 non_code_meta: Default::default(),
1900 })));
1901
1902 let sketch_id = sketch;
1904 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1905 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1906 })?;
1907 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1908 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1909 "Object is not a sketch, it is {}",
1910 sketch_object.kind.human_friendly_kind_with_article(),
1911 ))));
1912 };
1913 let mut new_ast = self.program.ast.clone();
1915 let (sketch_block_ref, _) = self
1916 .mutate_ast(
1917 &mut new_ast,
1918 sketch_id,
1919 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1920 )
1921 .map_err(KclErrorWithOutputs::no_outputs)?;
1922 let new_source = source_from_ast(&new_ast);
1924 let (new_program, errors) = Program::parse(&new_source)
1926 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1927 if !errors.is_empty() {
1928 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1929 "Error parsing KCL source after adding arc: {errors:?}"
1930 ))));
1931 }
1932 let Some(new_program) = new_program else {
1933 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1934 "No AST produced after adding arc".to_string(),
1935 )));
1936 };
1937
1938 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1939 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1940 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
1941 )))
1942 })?;
1943 #[cfg(not(feature = "artifact-graph"))]
1944 let _ = arc_node_ref;
1945
1946 self.program = new_program.clone();
1948
1949 let mut truncated_program = new_program;
1951 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1952 .map_err(KclErrorWithOutputs::no_outputs)?;
1953
1954 let outcome = ctx
1956 .run_mock(
1957 &truncated_program,
1958 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1959 )
1960 .await?;
1961
1962 #[cfg(not(feature = "artifact-graph"))]
1963 let new_object_ids = Vec::new();
1964 #[cfg(feature = "artifact-graph")]
1965 let new_object_ids = {
1966 let make_err =
1967 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1968 let segment_id = outcome
1969 .source_range_to_object
1970 .get(&arc_node_ref.range)
1971 .copied()
1972 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
1973 let segment_object = outcome
1974 .scene_objects
1975 .get(segment_id.0)
1976 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1977 let ObjectKind::Segment { segment } = &segment_object.kind else {
1978 return Err(make_err(format!(
1979 "Object is not a segment, it is {}",
1980 segment_object.kind.human_friendly_kind_with_article()
1981 )));
1982 };
1983 let Segment::Arc(arc) = segment else {
1984 return Err(make_err(format!(
1985 "Segment is not an arc, it is {}",
1986 segment.human_friendly_kind_with_article()
1987 )));
1988 };
1989 vec![arc.start, arc.end, arc.center, segment_id]
1990 };
1991 let src_delta = SourceDelta { text: new_source };
1992 let outcome = self.update_state_after_exec(outcome, false);
1994 let scene_graph_delta = SceneGraphDelta {
1995 new_graph: self.scene_graph.clone(),
1996 invalidates_ids: false,
1997 new_objects: new_object_ids,
1998 exec_outcome: outcome,
1999 };
2000 Ok((src_delta, scene_graph_delta))
2001 }
2002
2003 async fn add_circle(
2004 &mut self,
2005 ctx: &ExecutorContext,
2006 sketch: ObjectId,
2007 ctor: CircleCtor,
2008 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2009 let start_ast = to_ast_point2d(&ctor.start)
2011 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2012 let center_ast = to_ast_point2d(&ctor.center)
2013 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2014 let mut arguments = vec![
2015 ast::LabeledArg {
2016 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2017 arg: start_ast,
2018 },
2019 ast::LabeledArg {
2020 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2021 arg: center_ast,
2022 },
2023 ];
2024 if ctor.construction == Some(true) {
2026 arguments.push(ast::LabeledArg {
2027 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2028 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2029 value: ast::LiteralValue::Bool(true),
2030 raw: "true".to_string(),
2031 digest: None,
2032 }))),
2033 });
2034 }
2035 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2036 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2037 unlabeled: None,
2038 arguments,
2039 digest: None,
2040 non_code_meta: Default::default(),
2041 })));
2042
2043 let sketch_id = sketch;
2045 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2046 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2047 })?;
2048 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2049 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2050 "Object is not a sketch, it is {}",
2051 sketch_object.kind.human_friendly_kind_with_article(),
2052 ))));
2053 };
2054 let mut new_ast = self.program.ast.clone();
2056 let (sketch_block_ref, _) = self
2057 .mutate_ast(
2058 &mut new_ast,
2059 sketch_id,
2060 AstMutateCommand::AddSketchBlockVarDecl {
2061 prefix: CIRCLE_VARIABLE.to_owned(),
2062 expr: circle_ast,
2063 },
2064 )
2065 .map_err(KclErrorWithOutputs::no_outputs)?;
2066 let new_source = source_from_ast(&new_ast);
2068 let (new_program, errors) = Program::parse(&new_source)
2070 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2071 if !errors.is_empty() {
2072 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2073 "Error parsing KCL source after adding circle: {errors:?}"
2074 ))));
2075 }
2076 let Some(new_program) = new_program else {
2077 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2078 "No AST produced after adding circle".to_string(),
2079 )));
2080 };
2081
2082 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2083 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2084 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2085 )))
2086 })?;
2087 #[cfg(not(feature = "artifact-graph"))]
2088 let _ = circle_node_ref;
2089
2090 self.program = new_program.clone();
2092
2093 let mut truncated_program = new_program;
2095 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2096 .map_err(KclErrorWithOutputs::no_outputs)?;
2097
2098 let outcome = ctx
2100 .run_mock(
2101 &truncated_program,
2102 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2103 )
2104 .await?;
2105
2106 #[cfg(not(feature = "artifact-graph"))]
2107 let new_object_ids = Vec::new();
2108 #[cfg(feature = "artifact-graph")]
2109 let new_object_ids = {
2110 let make_err =
2111 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2112 let segment_id = outcome
2113 .source_range_to_object
2114 .get(&circle_node_ref.range)
2115 .copied()
2116 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2117 let segment_object = outcome
2118 .scene_objects
2119 .get(segment_id.0)
2120 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2121 let ObjectKind::Segment { segment } = &segment_object.kind else {
2122 return Err(make_err(format!(
2123 "Object is not a segment, it is {}",
2124 segment_object.kind.human_friendly_kind_with_article()
2125 )));
2126 };
2127 let Segment::Circle(circle) = segment else {
2128 return Err(make_err(format!(
2129 "Segment is not a circle, it is {}",
2130 segment.human_friendly_kind_with_article()
2131 )));
2132 };
2133 vec![circle.start, circle.center, segment_id]
2134 };
2135 let src_delta = SourceDelta { text: new_source };
2136 let outcome = self.update_state_after_exec(outcome, false);
2138 let scene_graph_delta = SceneGraphDelta {
2139 new_graph: self.scene_graph.clone(),
2140 invalidates_ids: false,
2141 new_objects: new_object_ids,
2142 exec_outcome: outcome,
2143 };
2144 Ok((src_delta, scene_graph_delta))
2145 }
2146
2147 fn edit_point(
2148 &mut self,
2149 new_ast: &mut ast::Node<ast::Program>,
2150 sketch: ObjectId,
2151 point: ObjectId,
2152 ctor: PointCtor,
2153 ) -> Result<(), KclError> {
2154 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2156
2157 let sketch_id = sketch;
2159 let sketch_object = self
2160 .scene_graph
2161 .objects
2162 .get(sketch_id.0)
2163 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2164 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2165 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2166 };
2167 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2168 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2169 })?;
2170 let point_id = point;
2172 let point_object = self
2173 .scene_graph
2174 .objects
2175 .get(point_id.0)
2176 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2177 let ObjectKind::Segment {
2178 segment: Segment::Point(point),
2179 } = &point_object.kind
2180 else {
2181 return Err(KclError::refactor(format!(
2182 "Object is not a point segment: {point_object:?}"
2183 )));
2184 };
2185
2186 if let Some(owner_id) = point.owner {
2188 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2189 KclError::refactor(format!(
2190 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2191 ))
2192 })?;
2193 let ObjectKind::Segment { segment } = &owner_object.kind else {
2194 return Err(KclError::refactor(format!(
2195 "Internal: Owner of point is not a segment, but found {}",
2196 owner_object.kind.human_friendly_kind_with_article()
2197 )));
2198 };
2199
2200 if let Segment::Line(line) = segment {
2202 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2203 return Err(KclError::refactor(format!(
2204 "Internal: Owner of point does not have line ctor, but found {}",
2205 line.ctor.human_friendly_kind_with_article()
2206 )));
2207 };
2208 let mut line_ctor = line_ctor.clone();
2209 if line.start == point_id {
2211 line_ctor.start = ctor.position;
2212 } else if line.end == point_id {
2213 line_ctor.end = ctor.position;
2214 } else {
2215 return Err(KclError::refactor(format!(
2216 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2217 )));
2218 }
2219 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2220 }
2221
2222 if let Segment::Arc(arc) = segment {
2224 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2225 return Err(KclError::refactor(format!(
2226 "Internal: Owner of point does not have arc ctor, but found {}",
2227 arc.ctor.human_friendly_kind_with_article()
2228 )));
2229 };
2230 let mut arc_ctor = arc_ctor.clone();
2231 if arc.center == point_id {
2233 arc_ctor.center = ctor.position;
2234 } else if arc.start == point_id {
2235 arc_ctor.start = ctor.position;
2236 } else if arc.end == point_id {
2237 arc_ctor.end = ctor.position;
2238 } else {
2239 return Err(KclError::refactor(format!(
2240 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2241 )));
2242 }
2243 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2244 }
2245
2246 if let Segment::Circle(circle) = segment {
2248 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2249 return Err(KclError::refactor(format!(
2250 "Internal: Owner of point does not have circle ctor, but found {}",
2251 circle.ctor.human_friendly_kind_with_article()
2252 )));
2253 };
2254 let mut circle_ctor = circle_ctor.clone();
2255 if circle.center == point_id {
2256 circle_ctor.center = ctor.position;
2257 } else if circle.start == point_id {
2258 circle_ctor.start = ctor.position;
2259 } else {
2260 return Err(KclError::refactor(format!(
2261 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2262 )));
2263 }
2264 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2265 }
2266
2267 }
2270
2271 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2273 Ok(())
2274 }
2275
2276 fn edit_line(
2277 &mut self,
2278 new_ast: &mut ast::Node<ast::Program>,
2279 sketch: ObjectId,
2280 line: ObjectId,
2281 ctor: LineCtor,
2282 ) -> Result<(), KclError> {
2283 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2285 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2286
2287 let sketch_id = sketch;
2289 let sketch_object = self
2290 .scene_graph
2291 .objects
2292 .get(sketch_id.0)
2293 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2294 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2295 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2296 };
2297 sketch
2298 .segments
2299 .iter()
2300 .find(|o| **o == line)
2301 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2302 let line_id = line;
2304 let line_object = self
2305 .scene_graph
2306 .objects
2307 .get(line_id.0)
2308 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2309 let ObjectKind::Segment { .. } = &line_object.kind else {
2310 let kind = line_object.kind.human_friendly_kind_with_article();
2311 return Err(KclError::refactor(format!(
2312 "This constraint only works on Segments, but you selected {kind}"
2313 )));
2314 };
2315
2316 self.mutate_ast(
2318 new_ast,
2319 line_id,
2320 AstMutateCommand::EditLine {
2321 start: new_start_ast,
2322 end: new_end_ast,
2323 construction: ctor.construction,
2324 },
2325 )?;
2326 Ok(())
2327 }
2328
2329 fn edit_arc(
2330 &mut self,
2331 new_ast: &mut ast::Node<ast::Program>,
2332 sketch: ObjectId,
2333 arc: ObjectId,
2334 ctor: ArcCtor,
2335 ) -> Result<(), KclError> {
2336 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2338 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2339 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2340
2341 let sketch_id = sketch;
2343 let sketch_object = self
2344 .scene_graph
2345 .objects
2346 .get(sketch_id.0)
2347 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2348 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2349 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2350 };
2351 sketch
2352 .segments
2353 .iter()
2354 .find(|o| **o == arc)
2355 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2356 let arc_id = arc;
2358 let arc_object = self
2359 .scene_graph
2360 .objects
2361 .get(arc_id.0)
2362 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2363 let ObjectKind::Segment { .. } = &arc_object.kind else {
2364 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2365 };
2366
2367 self.mutate_ast(
2369 new_ast,
2370 arc_id,
2371 AstMutateCommand::EditArc {
2372 start: new_start_ast,
2373 end: new_end_ast,
2374 center: new_center_ast,
2375 construction: ctor.construction,
2376 },
2377 )?;
2378 Ok(())
2379 }
2380
2381 fn edit_circle(
2382 &mut self,
2383 new_ast: &mut ast::Node<ast::Program>,
2384 sketch: ObjectId,
2385 circle: ObjectId,
2386 ctor: CircleCtor,
2387 ) -> Result<(), KclError> {
2388 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2390 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2391
2392 let sketch_id = sketch;
2394 let sketch_object = self
2395 .scene_graph
2396 .objects
2397 .get(sketch_id.0)
2398 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2399 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2400 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2401 };
2402 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2403 KclError::refactor(format!(
2404 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2405 ))
2406 })?;
2407 let circle_id = circle;
2409 let circle_object = self
2410 .scene_graph
2411 .objects
2412 .get(circle_id.0)
2413 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2414 let ObjectKind::Segment { .. } = &circle_object.kind else {
2415 return Err(KclError::refactor(format!(
2416 "Object is not a segment: {circle_object:?}"
2417 )));
2418 };
2419
2420 self.mutate_ast(
2422 new_ast,
2423 circle_id,
2424 AstMutateCommand::EditCircle {
2425 start: new_start_ast,
2426 center: new_center_ast,
2427 construction: ctor.construction,
2428 },
2429 )?;
2430 Ok(())
2431 }
2432
2433 fn delete_segment(
2434 &mut self,
2435 new_ast: &mut ast::Node<ast::Program>,
2436 sketch: ObjectId,
2437 segment_id: ObjectId,
2438 ) -> Result<(), KclError> {
2439 let sketch_id = sketch;
2441 let sketch_object = self
2442 .scene_graph
2443 .objects
2444 .get(sketch_id.0)
2445 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2446 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2447 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2448 };
2449 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2450 KclError::refactor(format!(
2451 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2452 ))
2453 })?;
2454 let segment_object =
2456 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2457 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2458 })?;
2459 let ObjectKind::Segment { .. } = &segment_object.kind else {
2460 return Err(KclError::refactor(format!(
2461 "Object is not a segment, it is {}",
2462 segment_object.kind.human_friendly_kind_with_article()
2463 )));
2464 };
2465
2466 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2468 Ok(())
2469 }
2470
2471 fn delete_constraint(
2472 &mut self,
2473 new_ast: &mut ast::Node<ast::Program>,
2474 sketch: ObjectId,
2475 constraint_id: ObjectId,
2476 ) -> Result<(), KclError> {
2477 let sketch_id = sketch;
2479 let sketch_object = self
2480 .scene_graph
2481 .objects
2482 .get(sketch_id.0)
2483 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2484 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2485 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2486 };
2487 sketch
2488 .constraints
2489 .iter()
2490 .find(|o| **o == constraint_id)
2491 .ok_or_else(|| {
2492 KclError::refactor(format!(
2493 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2494 ))
2495 })?;
2496 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2498 KclError::refactor(format!(
2499 "Constraint not found in scene graph: constraint={constraint_id:?}"
2500 ))
2501 })?;
2502 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2503 return Err(KclError::refactor(format!(
2504 "Object is not a constraint, it is {}",
2505 constraint_object.kind.human_friendly_kind_with_article()
2506 )));
2507 };
2508
2509 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2511 Ok(())
2512 }
2513
2514 fn edit_coincident_constraint(
2515 &mut self,
2516 new_ast: &mut ast::Node<ast::Program>,
2517 constraint_id: ObjectId,
2518 segments: Vec<ConstraintSegment>,
2519 ) -> Result<(), KclError> {
2520 if segments.len() < 2 {
2521 return Err(KclError::refactor(format!(
2522 "Coincident constraint must have at least 2 inputs, got {}",
2523 segments.len()
2524 )));
2525 }
2526
2527 let segment_asts = segments
2528 .iter()
2529 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2530 .collect::<Result<Vec<_>, _>>()?;
2531
2532 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2533 elements: segment_asts,
2534 digest: None,
2535 non_code_meta: Default::default(),
2536 })));
2537
2538 self.mutate_ast(
2539 new_ast,
2540 constraint_id,
2541 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2542 )?;
2543 Ok(())
2544 }
2545
2546 fn edit_horizontal_points_constraint(
2547 &mut self,
2548 new_ast: &mut ast::Node<ast::Program>,
2549 constraint_id: ObjectId,
2550 points: Vec<ConstraintSegment>,
2551 ) -> Result<(), KclError> {
2552 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2553 }
2554
2555 fn edit_vertical_points_constraint(
2556 &mut self,
2557 new_ast: &mut ast::Node<ast::Program>,
2558 constraint_id: ObjectId,
2559 points: Vec<ConstraintSegment>,
2560 ) -> Result<(), KclError> {
2561 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2562 }
2563
2564 fn edit_axis_points_constraint(
2565 &mut self,
2566 new_ast: &mut ast::Node<ast::Program>,
2567 constraint_id: ObjectId,
2568 points: Vec<ConstraintSegment>,
2569 constraint_name: &str,
2570 ) -> Result<(), KclError> {
2571 if points.len() < 2 {
2572 return Err(KclError::refactor(format!(
2573 "{constraint_name} points constraint must have at least 2 points, got {}",
2574 points.len()
2575 )));
2576 }
2577
2578 let point_asts = points
2579 .iter()
2580 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2581 .collect::<Result<Vec<_>, _>>()?;
2582
2583 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2584 elements: point_asts,
2585 digest: None,
2586 non_code_meta: Default::default(),
2587 })));
2588
2589 self.mutate_ast(
2590 new_ast,
2591 constraint_id,
2592 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2593 )?;
2594 Ok(())
2595 }
2596
2597 fn edit_equal_length_constraint(
2599 &mut self,
2600 new_ast: &mut ast::Node<ast::Program>,
2601 constraint_id: ObjectId,
2602 lines: Vec<ObjectId>,
2603 ) -> Result<(), KclError> {
2604 if lines.len() < 2 {
2605 return Err(KclError::refactor(format!(
2606 "Lines equal length constraint must have at least 2 lines, got {}",
2607 lines.len()
2608 )));
2609 }
2610
2611 let line_asts = lines
2612 .iter()
2613 .map(|line_id| {
2614 let line_object = self
2615 .scene_graph
2616 .objects
2617 .get(line_id.0)
2618 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2619 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2620 let kind = line_object.kind.human_friendly_kind_with_article();
2621 return Err(KclError::refactor(format!(
2622 "This constraint only works on Segments, but you selected {kind}"
2623 )));
2624 };
2625 let Segment::Line(_) = line_segment else {
2626 let kind = line_segment.human_friendly_kind_with_article();
2627 return Err(KclError::refactor(format!(
2628 "Only lines can be made equal length, but you selected {kind}"
2629 )));
2630 };
2631
2632 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2633 })
2634 .collect::<Result<Vec<_>, _>>()?;
2635
2636 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2637 elements: line_asts,
2638 digest: None,
2639 non_code_meta: Default::default(),
2640 })));
2641
2642 self.mutate_ast(
2643 new_ast,
2644 constraint_id,
2645 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2646 )?;
2647 Ok(())
2648 }
2649
2650 fn edit_parallel_constraint(
2652 &mut self,
2653 new_ast: &mut ast::Node<ast::Program>,
2654 constraint_id: ObjectId,
2655 lines: Vec<ObjectId>,
2656 ) -> Result<(), KclError> {
2657 if lines.len() < 2 {
2658 return Err(KclError::refactor(format!(
2659 "Parallel constraint must have at least 2 lines, got {}",
2660 lines.len()
2661 )));
2662 }
2663
2664 let line_asts = lines
2665 .iter()
2666 .map(|line_id| {
2667 let line_object = self
2668 .scene_graph
2669 .objects
2670 .get(line_id.0)
2671 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2672 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2673 let kind = line_object.kind.human_friendly_kind_with_article();
2674 return Err(KclError::refactor(format!(
2675 "This constraint only works on Segments, but you selected {kind}"
2676 )));
2677 };
2678 let Segment::Line(_) = line_segment else {
2679 let kind = line_segment.human_friendly_kind_with_article();
2680 return Err(KclError::refactor(format!(
2681 "Only lines can be made parallel, but you selected {kind}"
2682 )));
2683 };
2684
2685 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2686 })
2687 .collect::<Result<Vec<_>, _>>()?;
2688
2689 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2690 elements: line_asts,
2691 digest: None,
2692 non_code_meta: Default::default(),
2693 })));
2694
2695 self.mutate_ast(
2696 new_ast,
2697 constraint_id,
2698 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2699 )?;
2700 Ok(())
2701 }
2702
2703 fn edit_equal_radius_constraint(
2705 &mut self,
2706 new_ast: &mut ast::Node<ast::Program>,
2707 constraint_id: ObjectId,
2708 input: Vec<ObjectId>,
2709 ) -> Result<(), KclError> {
2710 if input.len() < 2 {
2711 return Err(KclError::refactor(format!(
2712 "equalRadius constraint must have at least 2 segments, got {}",
2713 input.len()
2714 )));
2715 }
2716
2717 let input_asts = input
2718 .iter()
2719 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2720 .collect::<Result<Vec<_>, _>>()?;
2721
2722 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2723 elements: input_asts,
2724 digest: None,
2725 non_code_meta: Default::default(),
2726 })));
2727
2728 self.mutate_ast(
2729 new_ast,
2730 constraint_id,
2731 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2732 )?;
2733 Ok(())
2734 }
2735
2736 async fn execute_after_edit(
2737 &mut self,
2738 ctx: &ExecutorContext,
2739 sketch: ObjectId,
2740 sketch_block_ref: AstNodeRef,
2741 segment_ids_edited: AhashIndexSet<ObjectId>,
2742 edit_kind: EditDeleteKind,
2743 new_ast: &mut ast::Node<ast::Program>,
2744 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2745 let new_source = source_from_ast(new_ast);
2747 let (new_program, errors) = Program::parse(&new_source)
2749 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2750 if !errors.is_empty() {
2751 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2752 "Error parsing KCL source after editing: {errors:?}"
2753 ))));
2754 }
2755 let Some(new_program) = new_program else {
2756 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2757 "No AST produced after editing".to_string(),
2758 )));
2759 };
2760
2761 self.program = new_program.clone();
2763
2764 let is_delete = edit_kind.is_delete();
2766 let truncated_program = {
2767 let mut truncated_program = new_program;
2768 only_sketch_block(
2769 &mut truncated_program.ast,
2770 &sketch_block_ref,
2771 edit_kind.to_change_kind(),
2772 )
2773 .map_err(KclErrorWithOutputs::no_outputs)?;
2774 truncated_program
2775 };
2776
2777 #[cfg(not(feature = "artifact-graph"))]
2778 drop(segment_ids_edited);
2779
2780 let mock_config = MockConfig {
2782 sketch_block_id: Some(sketch),
2783 freedom_analysis: is_delete,
2784 #[cfg(feature = "artifact-graph")]
2785 segment_ids_edited: segment_ids_edited.clone(),
2786 ..Default::default()
2787 };
2788 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2789
2790 let outcome = self.update_state_after_exec(outcome, is_delete);
2792
2793 #[cfg(feature = "artifact-graph")]
2794 let new_source = {
2795 let mut new_ast = self.program.ast.clone();
2800 for (var_range, value) in &outcome.var_solutions {
2801 let rounded = value.round(3);
2802 let source_ref = SourceRef::Simple {
2803 range: *var_range,
2804 node_path: None,
2805 };
2806 mutate_ast_node_by_source_ref(
2807 &mut new_ast,
2808 &source_ref,
2809 AstMutateCommand::EditVarInitialValue { value: rounded },
2810 )
2811 .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2812 }
2813 source_from_ast(&new_ast)
2814 };
2815
2816 let src_delta = SourceDelta { text: new_source };
2817 let scene_graph_delta = SceneGraphDelta {
2818 new_graph: self.scene_graph.clone(),
2819 invalidates_ids: is_delete,
2820 new_objects: Vec::new(),
2821 exec_outcome: outcome,
2822 };
2823 Ok((src_delta, scene_graph_delta))
2824 }
2825
2826 async fn execute_after_delete_sketch(
2827 &mut self,
2828 ctx: &ExecutorContext,
2829 new_ast: &mut ast::Node<ast::Program>,
2830 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2831 let new_source = source_from_ast(new_ast);
2833 let (new_program, errors) = Program::parse(&new_source)
2835 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2836 if !errors.is_empty() {
2837 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2838 "Error parsing KCL source after editing: {errors:?}"
2839 ))));
2840 }
2841 let Some(new_program) = new_program else {
2842 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2843 "No AST produced after editing".to_string(),
2844 )));
2845 };
2846
2847 self.program = new_program.clone();
2849
2850 let outcome = ctx.run_with_caching(new_program).await?;
2856 let freedom_analysis_ran = true;
2857
2858 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2859
2860 let src_delta = SourceDelta { text: new_source };
2861 let scene_graph_delta = SceneGraphDelta {
2862 new_graph: self.scene_graph.clone(),
2863 invalidates_ids: true,
2864 new_objects: Vec::new(),
2865 exec_outcome: outcome,
2866 };
2867 Ok((src_delta, scene_graph_delta))
2868 }
2869
2870 fn point_id_to_ast_reference(
2875 &self,
2876 point_id: ObjectId,
2877 new_ast: &mut ast::Node<ast::Program>,
2878 ) -> Result<ast::Expr, KclError> {
2879 let point_object = self
2880 .scene_graph
2881 .objects
2882 .get(point_id.0)
2883 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2884 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2885 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2886 };
2887 let Segment::Point(point) = point_segment else {
2888 return Err(KclError::refactor(format!(
2889 "Only points are currently supported: {point_object:?}"
2890 )));
2891 };
2892
2893 if let Some(owner_id) = point.owner {
2894 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2895 KclError::refactor(format!(
2896 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2897 ))
2898 })?;
2899 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2900 return Err(KclError::refactor(format!(
2901 "Owner of point is not a segment, but found {}",
2902 owner_object.kind.human_friendly_kind_with_article()
2903 )));
2904 };
2905
2906 match owner_segment {
2907 Segment::Line(line) => {
2908 let property = if line.start == point_id {
2909 LINE_PROPERTY_START
2910 } else if line.end == point_id {
2911 LINE_PROPERTY_END
2912 } else {
2913 return Err(KclError::refactor(format!(
2914 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2915 )));
2916 };
2917 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
2918 }
2919 Segment::Arc(arc) => {
2920 let property = if arc.start == point_id {
2921 ARC_PROPERTY_START
2922 } else if arc.end == point_id {
2923 ARC_PROPERTY_END
2924 } else if arc.center == point_id {
2925 ARC_PROPERTY_CENTER
2926 } else {
2927 return Err(KclError::refactor(format!(
2928 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2929 )));
2930 };
2931 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
2932 }
2933 Segment::Circle(circle) => {
2934 let property = if circle.start == point_id {
2935 CIRCLE_PROPERTY_START
2936 } else if circle.center == point_id {
2937 CIRCLE_PROPERTY_CENTER
2938 } else {
2939 return Err(KclError::refactor(format!(
2940 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2941 )));
2942 };
2943 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
2944 }
2945 _ => Err(KclError::refactor(format!(
2946 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
2947 ))),
2948 }
2949 } else {
2950 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
2952 }
2953 }
2954
2955 fn coincident_segment_to_ast(
2956 &self,
2957 segment: &ConstraintSegment,
2958 new_ast: &mut ast::Node<ast::Program>,
2959 ) -> Result<ast::Expr, KclError> {
2960 match segment {
2961 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
2962 ConstraintSegment::Segment(segment_id) => {
2963 let segment_object = self
2964 .scene_graph
2965 .objects
2966 .get(segment_id.0)
2967 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
2968 let ObjectKind::Segment { segment } = &segment_object.kind else {
2969 return Err(KclError::refactor(format!(
2970 "Object is not a segment, it is {}",
2971 segment_object.kind.human_friendly_kind_with_article()
2972 )));
2973 };
2974
2975 match segment {
2976 Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
2977 Segment::Line(_) => {
2978 get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None)
2979 }
2980 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
2981 Segment::Circle(_) => {
2982 get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
2983 }
2984 }
2985 }
2986 }
2987 }
2988
2989 fn axis_constraint_segment_to_ast(
2990 &self,
2991 segment: &ConstraintSegment,
2992 new_ast: &mut ast::Node<ast::Program>,
2993 ) -> Result<ast::Expr, KclError> {
2994 match segment {
2995 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
2996 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
2997 }
2998 }
2999
3000 async fn add_coincident(
3001 &mut self,
3002 sketch: ObjectId,
3003 coincident: Coincident,
3004 new_ast: &mut ast::Node<ast::Program>,
3005 ) -> Result<AstNodeRef, KclError> {
3006 let sketch_id = sketch;
3007 let segment_asts = coincident
3008 .segments
3009 .iter()
3010 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3011 .collect::<Result<Vec<_>, _>>()?;
3012 if segment_asts.len() < 2 {
3013 return Err(KclError::refactor(format!(
3014 "Coincident constraint must have at least 2 inputs, got {}",
3015 segment_asts.len()
3016 )));
3017 }
3018
3019 let coincident_ast = create_coincident_ast(segment_asts);
3021
3022 let (sketch_block_ref, _) = self.mutate_ast(
3024 new_ast,
3025 sketch_id,
3026 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3027 )?;
3028 Ok(sketch_block_ref)
3029 }
3030
3031 async fn add_distance(
3032 &mut self,
3033 sketch: ObjectId,
3034 distance: Distance,
3035 new_ast: &mut ast::Node<ast::Program>,
3036 ) -> Result<AstNodeRef, KclError> {
3037 let sketch_id = sketch;
3038 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3039 [pt0, pt1] => [
3040 self.coincident_segment_to_ast(pt0, new_ast)?,
3041 self.coincident_segment_to_ast(pt1, new_ast)?,
3042 ],
3043 _ => {
3044 return Err(KclError::refactor(format!(
3045 "Distance constraint must have exactly 2 points, got {}",
3046 distance.points.len()
3047 )));
3048 }
3049 };
3050
3051 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3053 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3054 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3055 ast::ArrayExpression {
3056 elements: vec![pt0_ast, pt1_ast],
3057 digest: None,
3058 non_code_meta: Default::default(),
3059 },
3060 )))),
3061 arguments: Default::default(),
3062 digest: None,
3063 non_code_meta: Default::default(),
3064 })));
3065 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3066 left: distance_call_ast,
3067 operator: ast::BinaryOperator::Eq,
3068 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3069 value: ast::LiteralValue::Number {
3070 value: distance.distance.value,
3071 suffix: distance.distance.units,
3072 },
3073 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3074 KclError::refactor(format!(
3075 "Could not format numeric suffix: {:?}",
3076 distance.distance.units
3077 ))
3078 })?,
3079 digest: None,
3080 }))),
3081 digest: None,
3082 })));
3083
3084 let (sketch_block_ref, _) = self.mutate_ast(
3086 new_ast,
3087 sketch_id,
3088 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3089 )?;
3090 Ok(sketch_block_ref)
3091 }
3092
3093 async fn add_angle(
3094 &mut self,
3095 sketch: ObjectId,
3096 angle: Angle,
3097 new_ast: &mut ast::Node<ast::Program>,
3098 ) -> Result<AstNodeRef, KclError> {
3099 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3100 return Err(KclError::refactor(format!(
3101 "Angle constraint must have exactly 2 lines, got {}",
3102 angle.lines.len()
3103 )));
3104 };
3105 let sketch_id = sketch;
3106
3107 let line0_object = self
3109 .scene_graph
3110 .objects
3111 .get(l0_id.0)
3112 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3113 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3114 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3115 };
3116 let Segment::Line(_) = line0_segment else {
3117 return Err(KclError::refactor(format!(
3118 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3119 )));
3120 };
3121 let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3122
3123 let line1_object = self
3124 .scene_graph
3125 .objects
3126 .get(l1_id.0)
3127 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3128 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3129 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3130 };
3131 let Segment::Line(_) = line1_segment else {
3132 return Err(KclError::refactor(format!(
3133 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3134 )));
3135 };
3136 let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3137
3138 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3140 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3141 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3142 ast::ArrayExpression {
3143 elements: vec![l0_ast, l1_ast],
3144 digest: None,
3145 non_code_meta: Default::default(),
3146 },
3147 )))),
3148 arguments: Default::default(),
3149 digest: None,
3150 non_code_meta: Default::default(),
3151 })));
3152 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3153 left: angle_call_ast,
3154 operator: ast::BinaryOperator::Eq,
3155 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3156 value: ast::LiteralValue::Number {
3157 value: angle.angle.value,
3158 suffix: angle.angle.units,
3159 },
3160 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3161 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3162 })?,
3163 digest: None,
3164 }))),
3165 digest: None,
3166 })));
3167
3168 let (sketch_block_ref, _) = self.mutate_ast(
3170 new_ast,
3171 sketch_id,
3172 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3173 )?;
3174 Ok(sketch_block_ref)
3175 }
3176
3177 async fn add_tangent(
3178 &mut self,
3179 sketch: ObjectId,
3180 tangent: Tangent,
3181 new_ast: &mut ast::Node<ast::Program>,
3182 ) -> Result<AstNodeRef, KclError> {
3183 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3184 return Err(KclError::refactor(format!(
3185 "Tangent constraint must have exactly 2 segments, got {}",
3186 tangent.input.len()
3187 )));
3188 };
3189 let sketch_id = sketch;
3190
3191 let seg0_object = self
3192 .scene_graph
3193 .objects
3194 .get(seg0_id.0)
3195 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3196 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3197 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3198 };
3199 let seg0_ast = match seg0_segment {
3200 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, LINE_VARIABLE, None)?,
3201 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, ARC_VARIABLE, None)?,
3202 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3203 _ => {
3204 return Err(KclError::refactor(format!(
3205 "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3206 )));
3207 }
3208 };
3209
3210 let seg1_object = self
3211 .scene_graph
3212 .objects
3213 .get(seg1_id.0)
3214 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3215 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3216 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3217 };
3218 let seg1_ast = match seg1_segment {
3219 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, LINE_VARIABLE, None)?,
3220 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, ARC_VARIABLE, None)?,
3221 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3222 _ => {
3223 return Err(KclError::refactor(format!(
3224 "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3225 )));
3226 }
3227 };
3228
3229 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3230 let (sketch_block_ref, _) = self.mutate_ast(
3231 new_ast,
3232 sketch_id,
3233 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3234 )?;
3235 Ok(sketch_block_ref)
3236 }
3237
3238 async fn add_symmetric(
3239 &mut self,
3240 sketch: ObjectId,
3241 symmetric: Symmetric,
3242 new_ast: &mut ast::Node<ast::Program>,
3243 ) -> Result<AstNodeRef, KclError> {
3244 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3245 return Err(KclError::refactor(format!(
3246 "Symmetric constraint must have exactly 2 inputs, got {}",
3247 symmetric.input.len()
3248 )));
3249 };
3250 let sketch_id = sketch;
3251
3252 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3253 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3254 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3255
3256 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3257 let (sketch_block_ref, _) = self.mutate_ast(
3258 new_ast,
3259 sketch_id,
3260 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3261 )?;
3262 Ok(sketch_block_ref)
3263 }
3264
3265 async fn add_midpoint(
3266 &mut self,
3267 sketch: ObjectId,
3268 midpoint: Midpoint,
3269 new_ast: &mut ast::Node<ast::Program>,
3270 ) -> Result<AstNodeRef, KclError> {
3271 let sketch_id = sketch;
3272 let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3273
3274 let segment_object = self
3275 .scene_graph
3276 .objects
3277 .get(midpoint.segment.0)
3278 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3279 let ObjectKind::Segment {
3280 segment: midpoint_segment,
3281 } = &segment_object.kind
3282 else {
3283 return Err(KclError::refactor(format!(
3284 "Object must be a segment, but it was {}",
3285 segment_object.kind.human_friendly_kind_with_article()
3286 )));
3287 };
3288 let segment_ast = match midpoint_segment {
3289 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3290 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3291 _ => {
3292 return Err(KclError::refactor(format!(
3293 "Midpoint target must be a line or arc segment but it was {}",
3294 midpoint_segment.human_friendly_kind_with_article()
3295 )));
3296 }
3297 };
3298
3299 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3300 let (sketch_block_ref, _) = self.mutate_ast(
3301 new_ast,
3302 sketch_id,
3303 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3304 )?;
3305 Ok(sketch_block_ref)
3306 }
3307
3308 async fn add_equal_radius(
3309 &mut self,
3310 sketch: ObjectId,
3311 equal_radius: EqualRadius,
3312 new_ast: &mut ast::Node<ast::Program>,
3313 ) -> Result<AstNodeRef, KclError> {
3314 if equal_radius.input.len() < 2 {
3315 return Err(KclError::refactor(format!(
3316 "equalRadius constraint must have at least 2 segments, got {}",
3317 equal_radius.input.len()
3318 )));
3319 }
3320
3321 let sketch_id = sketch;
3322 let input_asts = equal_radius
3323 .input
3324 .iter()
3325 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3326 .collect::<Result<Vec<_>, _>>()?;
3327
3328 let equal_radius_ast = create_equal_radius_ast(input_asts);
3329 let (sketch_block_ref, _) = self.mutate_ast(
3330 new_ast,
3331 sketch_id,
3332 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3333 )?;
3334 Ok(sketch_block_ref)
3335 }
3336
3337 async fn add_radius(
3338 &mut self,
3339 sketch: ObjectId,
3340 radius: Radius,
3341 new_ast: &mut ast::Node<ast::Program>,
3342 ) -> Result<AstNodeRef, KclError> {
3343 let params = ArcSizeConstraintParams {
3344 points: vec![radius.arc],
3345 function_name: RADIUS_FN,
3346 value: radius.radius.value,
3347 units: radius.radius.units,
3348 constraint_type_name: "Radius",
3349 };
3350 self.add_arc_size_constraint(sketch, params, new_ast).await
3351 }
3352
3353 async fn add_diameter(
3354 &mut self,
3355 sketch: ObjectId,
3356 diameter: Diameter,
3357 new_ast: &mut ast::Node<ast::Program>,
3358 ) -> Result<AstNodeRef, KclError> {
3359 let params = ArcSizeConstraintParams {
3360 points: vec![diameter.arc],
3361 function_name: DIAMETER_FN,
3362 value: diameter.diameter.value,
3363 units: diameter.diameter.units,
3364 constraint_type_name: "Diameter",
3365 };
3366 self.add_arc_size_constraint(sketch, params, new_ast).await
3367 }
3368
3369 async fn add_fixed_constraints(
3370 &mut self,
3371 sketch: ObjectId,
3372 points: Vec<FixedPoint>,
3373 new_ast: &mut ast::Node<ast::Program>,
3374 ) -> Result<AstNodeRef, KclError> {
3375 let mut sketch_block_ref = None;
3376
3377 for fixed_point in points {
3378 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3379 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3380 .map_err(|err| KclError::refactor(err.to_string()))?;
3381
3382 let (sketch_ref, _) = self.mutate_ast(
3383 new_ast,
3384 sketch,
3385 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3386 )?;
3387 sketch_block_ref = Some(sketch_ref);
3388 }
3389
3390 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3391 }
3392
3393 async fn add_arc_size_constraint(
3394 &mut self,
3395 sketch: ObjectId,
3396 params: ArcSizeConstraintParams,
3397 new_ast: &mut ast::Node<ast::Program>,
3398 ) -> Result<AstNodeRef, KclError> {
3399 let sketch_id = sketch;
3400
3401 if params.points.len() != 1 {
3403 return Err(KclError::refactor(format!(
3404 "{} constraint must have exactly 1 argument (an arc segment), got {}",
3405 params.constraint_type_name,
3406 params.points.len()
3407 )));
3408 }
3409
3410 let arc_id = params.points[0];
3411 let arc_object = self
3412 .scene_graph
3413 .objects
3414 .get(arc_id.0)
3415 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3416 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3417 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3418 };
3419 let ref_type = match arc_segment {
3420 Segment::Arc(_) => ARC_VARIABLE,
3421 Segment::Circle(_) => CIRCLE_VARIABLE,
3422 _ => {
3423 return Err(KclError::refactor(format!(
3424 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3425 params.constraint_type_name
3426 )));
3427 }
3428 };
3429 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3431
3432 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3434 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3435 unlabeled: Some(arc_ast),
3436 arguments: Default::default(),
3437 digest: None,
3438 non_code_meta: Default::default(),
3439 })));
3440 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3441 left: call_ast,
3442 operator: ast::BinaryOperator::Eq,
3443 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3444 value: ast::LiteralValue::Number {
3445 value: params.value,
3446 suffix: params.units,
3447 },
3448 raw: format_number_literal(params.value, params.units, None)
3449 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3450 digest: None,
3451 }))),
3452 digest: None,
3453 })));
3454
3455 let (sketch_block_ref, _) = self.mutate_ast(
3457 new_ast,
3458 sketch_id,
3459 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3460 )?;
3461 Ok(sketch_block_ref)
3462 }
3463
3464 async fn add_horizontal_distance(
3465 &mut self,
3466 sketch: ObjectId,
3467 distance: Distance,
3468 new_ast: &mut ast::Node<ast::Program>,
3469 ) -> Result<AstNodeRef, KclError> {
3470 let sketch_id = sketch;
3471 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3472 [pt0, pt1] => [
3473 self.coincident_segment_to_ast(pt0, new_ast)?,
3474 self.coincident_segment_to_ast(pt1, new_ast)?,
3475 ],
3476 _ => {
3477 return Err(KclError::refactor(format!(
3478 "Horizontal distance constraint must have exactly 2 points, got {}",
3479 distance.points.len()
3480 )));
3481 }
3482 };
3483
3484 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3486 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3487 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3488 ast::ArrayExpression {
3489 elements: vec![pt0_ast, pt1_ast],
3490 digest: None,
3491 non_code_meta: Default::default(),
3492 },
3493 )))),
3494 arguments: Default::default(),
3495 digest: None,
3496 non_code_meta: Default::default(),
3497 })));
3498 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3499 left: distance_call_ast,
3500 operator: ast::BinaryOperator::Eq,
3501 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3502 value: ast::LiteralValue::Number {
3503 value: distance.distance.value,
3504 suffix: distance.distance.units,
3505 },
3506 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3507 KclError::refactor(format!(
3508 "Could not format numeric suffix: {:?}",
3509 distance.distance.units
3510 ))
3511 })?,
3512 digest: None,
3513 }))),
3514 digest: None,
3515 })));
3516
3517 let (sketch_block_ref, _) = self.mutate_ast(
3519 new_ast,
3520 sketch_id,
3521 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3522 )?;
3523 Ok(sketch_block_ref)
3524 }
3525
3526 async fn add_vertical_distance(
3527 &mut self,
3528 sketch: ObjectId,
3529 distance: Distance,
3530 new_ast: &mut ast::Node<ast::Program>,
3531 ) -> Result<AstNodeRef, KclError> {
3532 let sketch_id = sketch;
3533 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3534 [pt0, pt1] => [
3535 self.coincident_segment_to_ast(pt0, new_ast)?,
3536 self.coincident_segment_to_ast(pt1, new_ast)?,
3537 ],
3538 _ => {
3539 return Err(KclError::refactor(format!(
3540 "Vertical distance constraint must have exactly 2 points, got {}",
3541 distance.points.len()
3542 )));
3543 }
3544 };
3545
3546 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3548 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3549 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3550 ast::ArrayExpression {
3551 elements: vec![pt0_ast, pt1_ast],
3552 digest: None,
3553 non_code_meta: Default::default(),
3554 },
3555 )))),
3556 arguments: Default::default(),
3557 digest: None,
3558 non_code_meta: Default::default(),
3559 })));
3560 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3561 left: distance_call_ast,
3562 operator: ast::BinaryOperator::Eq,
3563 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3564 value: ast::LiteralValue::Number {
3565 value: distance.distance.value,
3566 suffix: distance.distance.units,
3567 },
3568 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3569 KclError::refactor(format!(
3570 "Could not format numeric suffix: {:?}",
3571 distance.distance.units
3572 ))
3573 })?,
3574 digest: None,
3575 }))),
3576 digest: None,
3577 })));
3578
3579 let (sketch_block_ref, _) = self.mutate_ast(
3581 new_ast,
3582 sketch_id,
3583 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3584 )?;
3585 Ok(sketch_block_ref)
3586 }
3587
3588 async fn add_horizontal(
3589 &mut self,
3590 sketch: ObjectId,
3591 horizontal: Horizontal,
3592 new_ast: &mut ast::Node<ast::Program>,
3593 ) -> Result<AstNodeRef, KclError> {
3594 let sketch_id = sketch;
3595
3596 let first_arg_ast = match horizontal {
3598 Horizontal::Line { line } => {
3599 let line_object = self
3600 .scene_graph
3601 .objects
3602 .get(line.0)
3603 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3604 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3605 let kind = line_object.kind.human_friendly_kind_with_article();
3606 return Err(KclError::refactor(format!(
3607 "This constraint only works on Segments, but you selected {kind}"
3608 )));
3609 };
3610 let Segment::Line(_) = line_segment else {
3611 return Err(KclError::refactor(format!(
3612 "Only lines can be made horizontal, but you selected {}",
3613 line_segment.human_friendly_kind_with_article(),
3614 )));
3615 };
3616 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3617 }
3618 Horizontal::Points { points } => {
3619 let point_asts = points
3620 .iter()
3621 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3622 .collect::<Result<Vec<_>, _>>()?;
3623 ast::ArrayExpression::new(point_asts).into()
3624 }
3625 };
3626
3627 let horizontal_ast = create_horizontal_ast(first_arg_ast);
3629
3630 let (sketch_block_ref, _) = self.mutate_ast(
3632 new_ast,
3633 sketch_id,
3634 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3635 )?;
3636 Ok(sketch_block_ref)
3637 }
3638
3639 async fn add_lines_equal_length(
3640 &mut self,
3641 sketch: ObjectId,
3642 lines_equal_length: LinesEqualLength,
3643 new_ast: &mut ast::Node<ast::Program>,
3644 ) -> Result<AstNodeRef, KclError> {
3645 if lines_equal_length.lines.len() < 2 {
3646 return Err(KclError::refactor(format!(
3647 "Lines equal length constraint must have at least 2 lines, got {}",
3648 lines_equal_length.lines.len()
3649 )));
3650 };
3651
3652 let sketch_id = sketch;
3653
3654 let line_asts = lines_equal_length
3656 .lines
3657 .iter()
3658 .map(|line_id| {
3659 let line_object = self
3660 .scene_graph
3661 .objects
3662 .get(line_id.0)
3663 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3664 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3665 let kind = line_object.kind.human_friendly_kind_with_article();
3666 return Err(KclError::refactor(format!(
3667 "This constraint only works on Segments, but you selected {kind}"
3668 )));
3669 };
3670 let Segment::Line(_) = line_segment else {
3671 let kind = line_segment.human_friendly_kind_with_article();
3672 return Err(KclError::refactor(format!(
3673 "Only lines can be made equal length, but you selected {kind}"
3674 )));
3675 };
3676
3677 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3678 })
3679 .collect::<Result<Vec<_>, _>>()?;
3680
3681 let equal_length_ast = create_equal_length_ast(line_asts);
3683
3684 let (sketch_block_ref, _) = self.mutate_ast(
3686 new_ast,
3687 sketch_id,
3688 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3689 )?;
3690 Ok(sketch_block_ref)
3691 }
3692
3693 fn equal_radius_segment_id_to_ast_reference(
3694 &mut self,
3695 segment_id: ObjectId,
3696 new_ast: &mut ast::Node<ast::Program>,
3697 ) -> Result<ast::Expr, KclError> {
3698 let segment_object = self
3699 .scene_graph
3700 .objects
3701 .get(segment_id.0)
3702 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3703 let ObjectKind::Segment { segment } = &segment_object.kind else {
3704 return Err(KclError::refactor(format!(
3705 "Object is not a segment, it was {}",
3706 segment_object.kind.human_friendly_kind_with_article()
3707 )));
3708 };
3709
3710 let ref_type = match segment {
3711 Segment::Arc(_) => ARC_VARIABLE,
3712 Segment::Circle(_) => CIRCLE_VARIABLE,
3713 _ => {
3714 return Err(KclError::refactor(format!(
3715 "equalRadius supports only arc/circle segments, got {}",
3716 segment.human_friendly_kind_with_article()
3717 )));
3718 }
3719 };
3720
3721 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3722 }
3723
3724 fn symmetric_input_id_to_ast_reference(
3725 &mut self,
3726 segment_id: ObjectId,
3727 new_ast: &mut ast::Node<ast::Program>,
3728 ) -> Result<ast::Expr, KclError> {
3729 let segment_object = self
3730 .scene_graph
3731 .objects
3732 .get(segment_id.0)
3733 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3734 let ObjectKind::Segment { segment } = &segment_object.kind else {
3735 return Err(KclError::refactor(format!(
3736 "Object is not a segment, it was {}",
3737 segment_object.kind.human_friendly_kind_with_article()
3738 )));
3739 };
3740
3741 match segment {
3742 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3743 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3744 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3745 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3746 }
3747 }
3748
3749 fn symmetric_axis_id_to_ast_reference(
3750 &mut self,
3751 segment_id: ObjectId,
3752 new_ast: &mut ast::Node<ast::Program>,
3753 ) -> Result<ast::Expr, KclError> {
3754 let segment_object = self
3755 .scene_graph
3756 .objects
3757 .get(segment_id.0)
3758 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
3759 let ObjectKind::Segment { segment } = &segment_object.kind else {
3760 return Err(KclError::refactor(format!(
3761 "Object is not a segment, it was {}",
3762 segment_object.kind.human_friendly_kind_with_article()
3763 )));
3764 };
3765 match segment {
3766 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3767 _ => Err(KclError::refactor(format!(
3768 "Symmetric axis must be a line, got {}",
3769 segment.human_friendly_kind_with_article()
3770 ))),
3771 }
3772 }
3773
3774 async fn add_parallel(
3775 &mut self,
3776 sketch: ObjectId,
3777 parallel: Parallel,
3778 new_ast: &mut ast::Node<ast::Program>,
3779 ) -> Result<AstNodeRef, KclError> {
3780 if parallel.lines.len() < 2 {
3781 return Err(KclError::refactor(format!(
3782 "Parallel constraint must have at least 2 lines, got {}",
3783 parallel.lines.len()
3784 )));
3785 };
3786
3787 let sketch_id = sketch;
3788
3789 let line_asts = parallel
3790 .lines
3791 .iter()
3792 .map(|line_id| {
3793 let line_object = self
3794 .scene_graph
3795 .objects
3796 .get(line_id.0)
3797 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3798 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3799 let kind = line_object.kind.human_friendly_kind_with_article();
3800 return Err(KclError::refactor(format!(
3801 "This constraint only works on Segments, but you selected {kind}"
3802 )));
3803 };
3804 let Segment::Line(_) = line_segment else {
3805 let kind = line_segment.human_friendly_kind_with_article();
3806 return Err(KclError::refactor(format!(
3807 "Only lines can be made parallel, but you selected {kind}"
3808 )));
3809 };
3810
3811 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3812 })
3813 .collect::<Result<Vec<_>, _>>()?;
3814
3815 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3816 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3817 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3818 ast::ArrayExpression {
3819 elements: line_asts,
3820 digest: None,
3821 non_code_meta: Default::default(),
3822 },
3823 )))),
3824 arguments: Default::default(),
3825 digest: None,
3826 non_code_meta: Default::default(),
3827 })));
3828
3829 let (sketch_block_ref, _) = self.mutate_ast(
3830 new_ast,
3831 sketch_id,
3832 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3833 )?;
3834 Ok(sketch_block_ref)
3835 }
3836
3837 async fn add_perpendicular(
3838 &mut self,
3839 sketch: ObjectId,
3840 perpendicular: Perpendicular,
3841 new_ast: &mut ast::Node<ast::Program>,
3842 ) -> Result<AstNodeRef, KclError> {
3843 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3844 .await
3845 }
3846
3847 async fn add_lines_at_angle_constraint(
3848 &mut self,
3849 sketch: ObjectId,
3850 angle_kind: LinesAtAngleKind,
3851 lines: Vec<ObjectId>,
3852 new_ast: &mut ast::Node<ast::Program>,
3853 ) -> Result<AstNodeRef, KclError> {
3854 let &[line0_id, line1_id] = lines.as_slice() else {
3855 return Err(KclError::refactor(format!(
3856 "{} constraint must have exactly 2 lines, got {}",
3857 angle_kind.to_function_name(),
3858 lines.len()
3859 )));
3860 };
3861
3862 let sketch_id = sketch;
3863
3864 let line0_object = self
3866 .scene_graph
3867 .objects
3868 .get(line0_id.0)
3869 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3870 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3871 let kind = line0_object.kind.human_friendly_kind_with_article();
3872 return Err(KclError::refactor(format!(
3873 "This constraint only works on Segments, but you selected {kind}"
3874 )));
3875 };
3876 let Segment::Line(_) = line0_segment else {
3877 return Err(KclError::refactor(format!(
3878 "Only lines can be made {}, but you selected {}",
3879 angle_kind.to_function_name(),
3880 line0_segment.human_friendly_kind_with_article(),
3881 )));
3882 };
3883 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3884
3885 let line1_object = self
3886 .scene_graph
3887 .objects
3888 .get(line1_id.0)
3889 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3890 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3891 let kind = line1_object.kind.human_friendly_kind_with_article();
3892 return Err(KclError::refactor(format!(
3893 "This constraint only works on Segments, but you selected {kind}"
3894 )));
3895 };
3896 let Segment::Line(_) = line1_segment else {
3897 return Err(KclError::refactor(format!(
3898 "Only lines can be made {}, but you selected {}",
3899 angle_kind.to_function_name(),
3900 line1_segment.human_friendly_kind_with_article(),
3901 )));
3902 };
3903 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3904
3905 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3907 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3908 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3909 ast::ArrayExpression {
3910 elements: vec![line0_ast, line1_ast],
3911 digest: None,
3912 non_code_meta: Default::default(),
3913 },
3914 )))),
3915 arguments: Default::default(),
3916 digest: None,
3917 non_code_meta: Default::default(),
3918 })));
3919
3920 let (sketch_block_ref, _) = self.mutate_ast(
3922 new_ast,
3923 sketch_id,
3924 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3925 )?;
3926 Ok(sketch_block_ref)
3927 }
3928
3929 async fn add_vertical(
3930 &mut self,
3931 sketch: ObjectId,
3932 vertical: Vertical,
3933 new_ast: &mut ast::Node<ast::Program>,
3934 ) -> Result<AstNodeRef, KclError> {
3935 let sketch_id = sketch;
3936
3937 let first_arg_ast = match vertical {
3938 Vertical::Line { line } => {
3939 let line_object = self
3941 .scene_graph
3942 .objects
3943 .get(line.0)
3944 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3945 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3946 let kind = line_object.kind.human_friendly_kind_with_article();
3947 return Err(KclError::refactor(format!(
3948 "This constraint only works on Segments, but you selected {kind}"
3949 )));
3950 };
3951 let Segment::Line(_) = line_segment else {
3952 return Err(KclError::refactor(format!(
3953 "Only lines can be made vertical, but you selected {}",
3954 line_segment.human_friendly_kind_with_article()
3955 )));
3956 };
3957 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3958 }
3959 Vertical::Points { points } => {
3960 let point_asts = points
3961 .iter()
3962 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3963 .collect::<Result<Vec<_>, _>>()?;
3964 ast::ArrayExpression::new(point_asts).into()
3965 }
3966 };
3967
3968 let vertical_ast = create_vertical_ast(first_arg_ast);
3970
3971 let (sketch_block_ref, _) = self.mutate_ast(
3973 new_ast,
3974 sketch_id,
3975 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
3976 )?;
3977 Ok(sketch_block_ref)
3978 }
3979
3980 async fn execute_after_add_constraint(
3981 &mut self,
3982 ctx: &ExecutorContext,
3983 sketch_id: ObjectId,
3984 #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_ref: AstNodeRef,
3985 new_ast: &mut ast::Node<ast::Program>,
3986 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3987 let new_source = source_from_ast(new_ast);
3989 let (new_program, errors) = Program::parse(&new_source)
3991 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3992 if !errors.is_empty() {
3993 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3994 "Error parsing KCL source after adding constraint: {errors:?}"
3995 ))));
3996 }
3997 let Some(new_program) = new_program else {
3998 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3999 "No AST produced after adding constraint".to_string(),
4000 )));
4001 };
4002 #[cfg(feature = "artifact-graph")]
4003 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4004 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4005 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4006 )))
4007 })?;
4008
4009 let mut truncated_program = new_program.clone();
4012 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4013 .map_err(KclErrorWithOutputs::no_outputs)?;
4014
4015 let outcome = ctx
4017 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4018 .await?;
4019
4020 #[cfg(not(feature = "artifact-graph"))]
4021 let new_object_ids = Vec::new();
4022 #[cfg(feature = "artifact-graph")]
4023 let new_object_ids = {
4024 let constraint_id = outcome
4026 .source_range_to_object
4027 .get(&constraint_node_ref.range)
4028 .copied()
4029 .ok_or_else(|| {
4030 KclErrorWithOutputs::from_error_outcome(
4031 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4032 outcome.clone(),
4033 )
4034 })?;
4035 vec![constraint_id]
4036 };
4037
4038 self.program = new_program;
4041
4042 let outcome = self.update_state_after_exec(outcome, true);
4044
4045 let src_delta = SourceDelta { text: new_source };
4046 let scene_graph_delta = SceneGraphDelta {
4047 new_graph: self.scene_graph.clone(),
4048 invalidates_ids: false,
4049 new_objects: new_object_ids,
4050 exec_outcome: outcome,
4051 };
4052 Ok((src_delta, scene_graph_delta))
4053 }
4054
4055 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4057 if segment_ids_set.contains(&segment_id) {
4058 return true;
4059 }
4060
4061 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4062 return false;
4063 };
4064 let ObjectKind::Segment { segment } = &segment_object.kind else {
4065 return false;
4066 };
4067 let Segment::Point(point) = segment else {
4068 return false;
4069 };
4070
4071 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4072 }
4073
4074 fn remaining_constraint_segments(
4075 &self,
4076 segments: &[ConstraintSegment],
4077 segment_ids_set: &AhashIndexSet<ObjectId>,
4078 ) -> Vec<ConstraintSegment> {
4079 segments
4080 .iter()
4081 .copied()
4082 .filter(|segment| match segment {
4083 ConstraintSegment::Origin(_) => true,
4084 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4085 })
4086 .collect()
4087 }
4088
4089 fn find_referenced_constraints(
4090 &self,
4091 sketch_id: ObjectId,
4092 segment_ids_set: &AhashIndexSet<ObjectId>,
4093 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4094 let sketch_object = self
4096 .scene_graph
4097 .objects
4098 .get(sketch_id.0)
4099 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4100 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4101 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4102 };
4103 let mut constraint_ids_set = AhashIndexSet::default();
4104 for constraint_id in &sketch.constraints {
4105 let constraint_object = self
4106 .scene_graph
4107 .objects
4108 .get(constraint_id.0)
4109 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4110 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4111 return Err(KclError::refactor(format!(
4112 "Object is not a constraint, it is {}",
4113 constraint_object.kind.human_friendly_kind_with_article()
4114 )));
4115 };
4116 let depends_on_segment = match constraint {
4117 Constraint::Coincident(c) => c
4118 .segment_ids()
4119 .any(|seg_id| self.segment_will_be_deleted(seg_id, segment_ids_set)),
4120 Constraint::Distance(d) => d
4121 .point_ids()
4122 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4123 Constraint::Fixed(fixed) => fixed
4124 .points
4125 .iter()
4126 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4127 Constraint::Radius(r) => self.segment_will_be_deleted(r.arc, segment_ids_set),
4128 Constraint::Diameter(d) => self.segment_will_be_deleted(d.arc, segment_ids_set),
4129 Constraint::EqualRadius(equal_radius) => equal_radius
4130 .input
4131 .iter()
4132 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4133 Constraint::HorizontalDistance(d) => d
4134 .point_ids()
4135 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4136 Constraint::VerticalDistance(d) => d
4137 .point_ids()
4138 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4139 Constraint::Horizontal(h) => match h {
4140 Horizontal::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4141 Horizontal::Points { points } => points.iter().any(|point| match point {
4142 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4143 ConstraintSegment::Origin(_) => false,
4144 }),
4145 },
4146 Constraint::Vertical(v) => match v {
4147 Vertical::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4148 Vertical::Points { points } => points.iter().any(|point| match point {
4149 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4150 ConstraintSegment::Origin(_) => false,
4151 }),
4152 },
4153 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4154 .lines
4155 .iter()
4156 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4157 Constraint::Midpoint(midpoint) => {
4158 self.segment_will_be_deleted(midpoint.segment, segment_ids_set)
4159 || self.segment_will_be_deleted(midpoint.point, segment_ids_set)
4160 }
4161 Constraint::Parallel(parallel) => parallel
4162 .lines
4163 .iter()
4164 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4165 Constraint::Perpendicular(perpendicular) => perpendicular
4166 .lines
4167 .iter()
4168 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4169 Constraint::Angle(angle) => angle
4170 .lines
4171 .iter()
4172 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4173 Constraint::Symmetric(symmetric) => {
4174 self.segment_will_be_deleted(symmetric.axis, segment_ids_set)
4175 || symmetric
4176 .input
4177 .iter()
4178 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set))
4179 }
4180 Constraint::Tangent(tangent) => tangent
4181 .input
4182 .iter()
4183 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4184 };
4185 if depends_on_segment {
4186 constraint_ids_set.insert(*constraint_id);
4187 }
4188 }
4189 Ok(constraint_ids_set)
4190 }
4191
4192 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4193 #[cfg(not(feature = "artifact-graph"))]
4194 {
4195 let _ = freedom_analysis_ran; outcome
4197 }
4198 #[cfg(feature = "artifact-graph")]
4199 {
4200 let mut outcome = outcome;
4201 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4202
4203 if freedom_analysis_ran {
4204 self.point_freedom_cache.clear();
4207 for new_obj in &new_objects {
4208 if let ObjectKind::Segment {
4209 segment: crate::front::Segment::Point(point),
4210 } = &new_obj.kind
4211 {
4212 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4213 }
4214 }
4215 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4216 self.scene_graph.objects = new_objects;
4218 } else {
4219 for old_obj in &self.scene_graph.objects {
4222 if let ObjectKind::Segment {
4223 segment: crate::front::Segment::Point(point),
4224 } = &old_obj.kind
4225 {
4226 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4227 }
4228 }
4229
4230 let mut updated_objects = Vec::with_capacity(new_objects.len());
4232 for new_obj in new_objects {
4233 let mut obj = new_obj;
4234 if let ObjectKind::Segment {
4235 segment: crate::front::Segment::Point(point),
4236 } = &mut obj.kind
4237 {
4238 let new_freedom = point.freedom;
4239 match new_freedom {
4245 Freedom::Free => {
4246 match self.point_freedom_cache.get(&obj.id).copied() {
4247 Some(Freedom::Conflict) => {
4248 }
4251 Some(Freedom::Fixed) => {
4252 point.freedom = Freedom::Fixed;
4254 }
4255 Some(Freedom::Free) => {
4256 }
4258 None => {
4259 }
4261 }
4262 }
4263 Freedom::Fixed => {
4264 }
4266 Freedom::Conflict => {
4267 }
4269 }
4270 self.point_freedom_cache.insert(obj.id, point.freedom);
4272 }
4273 updated_objects.push(obj);
4274 }
4275
4276 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4277 self.scene_graph.objects = updated_objects;
4278 }
4279 outcome
4280 }
4281 }
4282
4283 fn mutate_ast(
4284 &mut self,
4285 ast: &mut ast::Node<ast::Program>,
4286 object_id: ObjectId,
4287 command: AstMutateCommand,
4288 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4289 let sketch_object = self
4290 .scene_graph
4291 .objects
4292 .get(object_id.0)
4293 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4294 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4295 }
4296}
4297
4298fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4299 let sketch_object = scene_graph
4301 .objects
4302 .get(sketch_id.0)
4303 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4304 let ObjectKind::Sketch(_) = &sketch_object.kind else {
4305 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4306 };
4307 expect_single_node_ref(sketch_object)
4308}
4309
4310fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4311 match &object.source {
4312 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4313 range: *range,
4314 node_path: node_path.clone(),
4315 }),
4316 SourceRef::BackTrace { ranges } => {
4317 let [range] = ranges.as_slice() else {
4318 return Err(KclError::refactor(format!(
4319 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4320 ranges.len()
4321 )));
4322 };
4323 Ok(AstNodeRef {
4324 range: range.0,
4325 node_path: range.1.clone(),
4326 })
4327 }
4328 }
4329}
4330
4331fn only_sketch_block_from_range(
4334 ast: &mut ast::Node<ast::Program>,
4335 sketch_block_range: SourceRange,
4336 edit_kind: ChangeKind,
4337) -> Result<(), KclError> {
4338 let r1 = sketch_block_range;
4339 let matches_range = |r2: SourceRange| -> bool {
4340 match edit_kind {
4343 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4344 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4346 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4347 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4349 }
4350 };
4351 let mut found = false;
4352 for item in ast.body.iter_mut() {
4353 match item {
4354 ast::BodyItem::ImportStatement(_) => {}
4355 ast::BodyItem::ExpressionStatement(node) => {
4356 if matches_range(SourceRange::from(&*node))
4357 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4358 {
4359 sketch_block.is_being_edited = true;
4360 found = true;
4361 break;
4362 }
4363 }
4364 ast::BodyItem::VariableDeclaration(node) => {
4365 if matches_range(SourceRange::from(&node.declaration.init))
4366 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4367 {
4368 sketch_block.is_being_edited = true;
4369 found = true;
4370 break;
4371 }
4372 }
4373 ast::BodyItem::TypeDeclaration(_) => {}
4374 ast::BodyItem::ReturnStatement(node) => {
4375 if matches_range(SourceRange::from(&node.argument))
4376 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4377 {
4378 sketch_block.is_being_edited = true;
4379 found = true;
4380 break;
4381 }
4382 }
4383 }
4384 }
4385 if !found {
4386 return Err(KclError::refactor(format!(
4387 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4388 )));
4389 }
4390
4391 Ok(())
4392}
4393
4394fn only_sketch_block(
4395 ast: &mut ast::Node<ast::Program>,
4396 sketch_block_ref: &AstNodeRef,
4397 edit_kind: ChangeKind,
4398) -> Result<(), KclError> {
4399 let Some(target_node_path) = &sketch_block_ref.node_path else {
4400 #[cfg(target_arch = "wasm32")]
4401 web_sys::console::warn_1(
4402 &format!(
4403 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4404 &sketch_block_ref
4405 )
4406 .into(),
4407 );
4408 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4409 };
4410 let mut found = false;
4411 for item in ast.body.iter_mut() {
4412 match item {
4413 ast::BodyItem::ImportStatement(_) => {}
4414 ast::BodyItem::ExpressionStatement(node) => {
4415 if let Some(node_path) = &node.node_path
4417 && node_path == target_node_path
4418 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4419 {
4420 sketch_block.is_being_edited = true;
4421 found = true;
4422 break;
4423 }
4424 if let Some(node_path) = node.expression.node_path()
4426 && node_path == target_node_path
4427 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4428 {
4429 sketch_block.is_being_edited = true;
4430 found = true;
4431 break;
4432 }
4433 }
4434 ast::BodyItem::VariableDeclaration(node) => {
4435 if let Some(node_path) = node.declaration.init.node_path()
4436 && node_path == target_node_path
4437 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4438 {
4439 sketch_block.is_being_edited = true;
4440 found = true;
4441 break;
4442 }
4443 }
4444 ast::BodyItem::TypeDeclaration(_) => {}
4445 ast::BodyItem::ReturnStatement(node) => {
4446 if let Some(node_path) = node.argument.node_path()
4447 && node_path == target_node_path
4448 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4449 {
4450 sketch_block.is_being_edited = true;
4451 found = true;
4452 break;
4453 }
4454 }
4455 }
4456 }
4457 if !found {
4458 return Err(KclError::refactor(format!(
4459 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4460 )));
4461 }
4462
4463 Ok(())
4464}
4465
4466fn sketch_on_ast_expr(
4467 ast: &mut ast::Node<ast::Program>,
4468 scene_graph: &SceneGraph,
4469 on: &Plane,
4470) -> Result<ast::Expr, KclError> {
4471 match on {
4472 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4473 Plane::Object(object_id) => {
4474 let on_object = scene_graph
4475 .objects
4476 .get(object_id.0)
4477 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4478 #[cfg(feature = "artifact-graph")]
4479 {
4480 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4481 return Ok(face_expr);
4482 }
4483 }
4484 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4485 }
4486 }
4487}
4488
4489#[cfg(feature = "artifact-graph")]
4490fn sketch_face_of_scene_object_ast_expr(
4491 ast: &mut ast::Node<ast::Program>,
4492 on_object: &crate::front::Object,
4493) -> Result<Option<ast::Expr>, KclError> {
4494 let SourceRef::BackTrace { ranges } = &on_object.source else {
4495 return Ok(None);
4496 };
4497
4498 match &on_object.kind {
4499 ObjectKind::Wall(_) => {
4500 let [sweep_range, segment_range] = ranges.as_slice() else {
4501 return Err(KclError::refactor(format!(
4502 "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4503 ranges.len(),
4504 on_object.artifact_id
4505 )));
4506 };
4507 let sweep_ref = get_or_insert_ast_reference(
4508 ast,
4509 &SourceRef::Simple {
4510 range: sweep_range.0,
4511 node_path: sweep_range.1.clone(),
4512 },
4513 "solid",
4514 None,
4515 )?;
4516 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4517 return Err(KclError::refactor(format!(
4518 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4519 on_object.artifact_id
4520 )));
4521 };
4522 let solid_name = solid_name_expr.name.name.clone();
4523 let solid_expr = ast_name_expr(solid_name.clone());
4524 let segment_ref = get_or_insert_ast_reference(
4525 ast,
4526 &SourceRef::Simple {
4527 range: segment_range.0,
4528 node_path: segment_range.1.clone(),
4529 },
4530 LINE_VARIABLE,
4531 None,
4532 )?;
4533
4534 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4535 let ast::Expr::Name(segment_name_expr) = segment_ref else {
4536 return Err(KclError::refactor(format!(
4537 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4538 on_object.artifact_id
4539 )));
4540 };
4541 create_member_expression(
4542 create_member_expression(ast_name_expr(region_name), "tags"),
4543 &segment_name_expr.name.name,
4544 )
4545 } else {
4546 segment_ref
4547 };
4548
4549 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4550 }
4551 ObjectKind::Cap(cap) => {
4552 let [range] = ranges.as_slice() else {
4553 return Err(KclError::refactor(format!(
4554 "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4555 ranges.len(),
4556 on_object.artifact_id
4557 )));
4558 };
4559 let sweep_ref = get_or_insert_ast_reference(
4560 ast,
4561 &SourceRef::Simple {
4562 range: range.0,
4563 node_path: range.1.clone(),
4564 },
4565 "solid",
4566 None,
4567 )?;
4568 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4569 return Err(KclError::refactor(format!(
4570 "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4571 on_object.artifact_id
4572 )));
4573 };
4574 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4575 let face_expr = match cap.kind {
4577 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4578 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4579 };
4580
4581 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4582 }
4583 _ => Ok(None),
4584 }
4585}
4586
4587#[cfg(feature = "artifact-graph")]
4588fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4589 let mut existing_artifact_ids = scene_objects
4590 .iter()
4591 .map(|object| object.artifact_id)
4592 .collect::<HashSet<_>>();
4593
4594 for artifact in artifact_graph.values() {
4595 match artifact {
4596 Artifact::Wall(wall) => {
4597 if existing_artifact_ids.contains(&wall.id) {
4598 continue;
4599 }
4600
4601 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4602 Artifact::Segment(segment) => Some(segment),
4603 _ => None,
4604 }) else {
4605 continue;
4606 };
4607 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4608 Artifact::Sweep(sweep) => Some(sweep),
4609 _ => None,
4610 }) else {
4611 continue;
4612 };
4613 let source_segment = segment
4614 .original_seg_id
4615 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4616 .and_then(|artifact| match artifact {
4617 Artifact::Segment(segment) => Some(segment),
4618 _ => None,
4619 })
4620 .unwrap_or(segment);
4621 let id = ObjectId(scene_objects.len());
4622 scene_objects.push(crate::front::Object {
4623 id,
4624 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4625 label: Default::default(),
4626 comments: Default::default(),
4627 artifact_id: wall.id,
4628 source: SourceRef::BackTrace {
4629 ranges: vec![
4630 (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4631 (
4632 source_segment.code_ref.range,
4633 Some(source_segment.code_ref.node_path.clone()),
4634 ),
4635 ],
4636 },
4637 });
4638 existing_artifact_ids.insert(wall.id);
4639 }
4640 Artifact::Cap(cap) => {
4641 if existing_artifact_ids.contains(&cap.id) {
4642 continue;
4643 }
4644
4645 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4646 Artifact::Sweep(sweep) => Some(sweep),
4647 _ => None,
4648 }) else {
4649 continue;
4650 };
4651 let id = ObjectId(scene_objects.len());
4652 let kind = match cap.sub_type {
4653 CapSubType::Start => crate::frontend::api::CapKind::Start,
4654 CapSubType::End => crate::frontend::api::CapKind::End,
4655 };
4656 scene_objects.push(crate::front::Object {
4657 id,
4658 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4659 label: Default::default(),
4660 comments: Default::default(),
4661 artifact_id: cap.id,
4662 source: SourceRef::BackTrace {
4663 ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4664 },
4665 });
4666 existing_artifact_ids.insert(cap.id);
4667 }
4668 _ => {}
4669 }
4670 }
4671}
4672
4673fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4674 use crate::engine::PlaneName;
4675
4676 match name {
4677 PlaneName::Xy => ast_name_expr("XY".to_owned()),
4678 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4679 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4680 PlaneName::NegXy => negated_plane_ast_expr("XY"),
4681 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4682 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4683 }
4684}
4685
4686fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4687 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4688 ast::UnaryOperator::Neg,
4689 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4690 )))
4691}
4692
4693#[cfg(feature = "artifact-graph")]
4694fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4695 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4696 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4697 unlabeled: Some(solid_expr),
4698 arguments: vec![ast::LabeledArg {
4699 label: Some(ast::Identifier::new("face")),
4700 arg: face_expr,
4701 }],
4702 digest: None,
4703 non_code_meta: Default::default(),
4704 })))
4705}
4706
4707#[cfg(feature = "artifact-graph")]
4708fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4709 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4710 return None;
4711 };
4712 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4713 return None;
4714 };
4715 if !matches!(
4716 sweep_call.callee.name.name.as_str(),
4717 "extrude" | "revolve" | "sweep" | "loft"
4718 ) {
4719 return None;
4720 }
4721 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4722 return None;
4723 };
4724 let candidate = region_name_expr.name.name.clone();
4725 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4726 return None;
4727 };
4728 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
4729 return None;
4730 };
4731 if region_call.callee.name.name != "region" {
4732 return None;
4733 }
4734 Some(candidate)
4735}
4736
4737fn get_or_insert_ast_reference(
4744 ast: &mut ast::Node<ast::Program>,
4745 source_ref: &SourceRef,
4746 prefix: &str,
4747 property: Option<&str>,
4748) -> Result<ast::Expr, KclError> {
4749 let command = AstMutateCommand::AddVariableDeclaration {
4750 prefix: prefix.to_owned(),
4751 };
4752 let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
4753 let AstMutateCommandReturn::Name(var_name) = ret else {
4754 return Err(KclError::refactor(
4755 "Expected variable name returned from AddVariableDeclaration".to_owned(),
4756 ));
4757 };
4758 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4759 let Some(property) = property else {
4760 return Ok(var_expr);
4762 };
4763
4764 Ok(create_member_expression(var_expr, property))
4765}
4766
4767fn mutate_ast_node_by_source_ref(
4768 ast: &mut ast::Node<ast::Program>,
4769 source_ref: &SourceRef,
4770 command: AstMutateCommand,
4771) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4772 let (source_range, node_path) = match source_ref {
4773 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
4774 SourceRef::BackTrace { ranges } => {
4775 let [range] = ranges.as_slice() else {
4776 return Err(KclError::refactor(format!(
4777 "Expected single source ref, got {}; ranges={ranges:#?}",
4778 ranges.len(),
4779 )));
4780 };
4781 (range.0, range.1.clone())
4782 }
4783 };
4784 let mut context = AstMutateContext {
4785 source_range,
4786 node_path,
4787 command,
4788 defined_names_stack: Default::default(),
4789 };
4790 let control = dfs_mut(ast, &mut context);
4791 match control {
4792 ControlFlow::Continue(_) => Err(KclError::refactor(
4793 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
4794 )),
4795 ControlFlow::Break(break_value) => break_value,
4796 }
4797}
4798
4799#[derive(Debug)]
4800struct AstMutateContext {
4801 source_range: SourceRange,
4802 node_path: Option<ast::NodePath>,
4803 command: AstMutateCommand,
4804 defined_names_stack: Vec<HashSet<String>>,
4805}
4806
4807#[derive(Debug)]
4808#[allow(clippy::large_enum_variant)]
4809enum AstMutateCommand {
4810 AddSketchBlockExprStmt {
4812 expr: ast::Expr,
4813 },
4814 AddSketchBlockVarDecl {
4816 prefix: String,
4817 expr: ast::Expr,
4818 },
4819 AddVariableDeclaration {
4820 prefix: String,
4821 },
4822 EditPoint {
4823 at: ast::Expr,
4824 },
4825 EditLine {
4826 start: ast::Expr,
4827 end: ast::Expr,
4828 construction: Option<bool>,
4829 },
4830 EditArc {
4831 start: ast::Expr,
4832 end: ast::Expr,
4833 center: ast::Expr,
4834 construction: Option<bool>,
4835 },
4836 EditCircle {
4837 start: ast::Expr,
4838 center: ast::Expr,
4839 construction: Option<bool>,
4840 },
4841 EditConstraintValue {
4842 value: ast::BinaryPart,
4843 },
4844 EditCallUnlabeled {
4845 arg: ast::Expr,
4846 },
4847 #[cfg(feature = "artifact-graph")]
4848 EditVarInitialValue {
4849 value: Number,
4850 },
4851 DeleteNode,
4852}
4853
4854impl AstMutateCommand {
4855 fn needs_defined_names_stack(&self) -> bool {
4856 matches!(
4857 self,
4858 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4859 )
4860 }
4861}
4862
4863#[derive(Debug)]
4864enum AstMutateCommandReturn {
4865 None,
4866 Name(String),
4867}
4868
4869#[derive(Debug, Clone)]
4870struct AstNodeRef {
4871 range: SourceRange,
4872 node_path: Option<ast::NodePath>,
4873}
4874
4875impl<T> From<&ast::Node<T>> for AstNodeRef {
4876 fn from(value: &ast::Node<T>) -> Self {
4877 AstNodeRef {
4878 range: value.into(),
4879 node_path: value.node_path.clone(),
4880 }
4881 }
4882}
4883
4884impl From<&ast::BodyItem> for AstNodeRef {
4885 fn from(value: &ast::BodyItem) -> Self {
4886 match value {
4887 ast::BodyItem::ImportStatement(node) => AstNodeRef {
4888 range: node.into(),
4889 node_path: node.node_path.clone(),
4890 },
4891 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4892 range: node.into(),
4893 node_path: node.node_path.clone(),
4894 },
4895 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4896 range: node.into(),
4897 node_path: node.node_path.clone(),
4898 },
4899 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4900 range: node.into(),
4901 node_path: node.node_path.clone(),
4902 },
4903 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
4904 range: node.into(),
4905 node_path: node.node_path.clone(),
4906 },
4907 }
4908 }
4909}
4910
4911impl From<&ast::Expr> for AstNodeRef {
4912 fn from(value: &ast::Expr) -> Self {
4913 AstNodeRef {
4914 range: SourceRange::from(value),
4915 node_path: value.node_path().cloned(),
4916 }
4917 }
4918}
4919
4920impl From<&AstMutateContext> for AstNodeRef {
4921 fn from(value: &AstMutateContext) -> Self {
4922 AstNodeRef {
4923 range: value.source_range,
4924 node_path: value.node_path.clone(),
4925 }
4926 }
4927}
4928
4929impl TryFrom<&NodeMut<'_>> for AstNodeRef {
4930 type Error = crate::walk::AstNodeError;
4931
4932 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
4933 Ok(AstNodeRef {
4934 range: SourceRange::try_from(value)?,
4935 node_path: value.try_into()?,
4936 })
4937 }
4938}
4939
4940impl From<AstNodeRef> for SourceRange {
4941 fn from(value: AstNodeRef) -> Self {
4942 value.range
4943 }
4944}
4945
4946impl Visitor for AstMutateContext {
4947 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
4948 type Continue = ();
4949
4950 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
4951 filter_and_process(self, node)
4952 }
4953
4954 fn finish(&mut self, node: NodeMut<'_>) {
4955 match &node {
4956 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
4957 self.defined_names_stack.pop();
4958 }
4959 _ => {}
4960 }
4961 }
4962}
4963
4964fn filter_and_process(
4965 ctx: &mut AstMutateContext,
4966 node: NodeMut,
4967) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
4968 let Ok(node_range) = SourceRange::try_from(&node) else {
4969 return TraversalReturn::new_continue(());
4971 };
4972 if let NodeMut::VariableDeclaration(var_decl) = &node {
4977 let expr_range = SourceRange::from(&var_decl.declaration.init);
4978 let expr_node_path = var_decl.declaration.init.node_path();
4979 if source_ref_matches(ctx, expr_range, expr_node_path) {
4980 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
4981 return TraversalReturn::new_break(Ok((
4984 AstNodeRef::from(&**var_decl),
4985 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
4986 )));
4987 }
4988 if let AstMutateCommand::DeleteNode = &ctx.command {
4989 return TraversalReturn {
4992 mutate_body_item: MutateBodyItem::Delete,
4993 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
4994 };
4995 }
4996 }
4997 }
4998 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5001 let expr_range = SourceRange::from(&expr_stmt.expression);
5002 let expr_node_path = expr_stmt.expression.node_path();
5003 if source_ref_matches(ctx, expr_range, expr_node_path) {
5004 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5005 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5008 return TraversalReturn::new_continue(());
5009 };
5010 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5011 }
5012 if let AstMutateCommand::DeleteNode = &ctx.command {
5013 return TraversalReturn {
5016 mutate_body_item: MutateBodyItem::Delete,
5017 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5018 };
5019 }
5020 }
5021 }
5022
5023 if ctx.command.needs_defined_names_stack() {
5024 if let NodeMut::Program(program) = &node {
5025 ctx.defined_names_stack.push(find_defined_names(*program));
5026 } else if let NodeMut::SketchBlock(block) = &node {
5027 ctx.defined_names_stack.push(find_defined_names(&block.body));
5028 }
5029 }
5030
5031 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5033 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5034 return TraversalReturn::new_continue(());
5035 }
5036 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5037 return TraversalReturn::new_continue(());
5038 };
5039 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5040}
5041
5042fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5043 match &ctx.node_path {
5044 Some(target) => Some(target) == node_path,
5045 None => node_range == ctx.source_range,
5046 }
5047}
5048
5049fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5050 match &ctx.command {
5051 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5052 if let NodeMut::SketchBlock(sketch_block) = node {
5053 sketch_block
5054 .body
5055 .items
5056 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5057 inner: ast::ExpressionStatement {
5058 expression: expr.clone(),
5059 digest: None,
5060 },
5061 start: Default::default(),
5062 end: Default::default(),
5063 module_id: Default::default(),
5064 node_path: None,
5065 outer_attrs: Default::default(),
5066 pre_comments: Default::default(),
5067 comment_start: Default::default(),
5068 }));
5069 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5070 }
5071 }
5072 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5073 if let NodeMut::SketchBlock(sketch_block) = node {
5074 let empty_defined_names = HashSet::new();
5075 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5076 let Ok(name) = next_free_name(prefix, defined_names) else {
5077 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5078 };
5079 sketch_block
5080 .body
5081 .items
5082 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5083 ast::VariableDeclaration::new(
5084 ast::VariableDeclarator::new(&name, expr.clone()),
5085 ast::ItemVisibility::Default,
5086 ast::VariableKind::Const,
5087 ),
5088 ))));
5089 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5090 }
5091 }
5092 AstMutateCommand::AddVariableDeclaration { prefix } => {
5093 if let NodeMut::VariableDeclaration(inner) = node {
5094 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5095 }
5096 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5097 let empty_defined_names = HashSet::new();
5098 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5099 let Ok(name) = next_free_name(prefix, defined_names) else {
5100 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5102 };
5103 let mutate_node =
5104 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5105 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5106 ast::ItemVisibility::Default,
5107 ast::VariableKind::Const,
5108 ))));
5109 return TraversalReturn {
5110 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5111 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5112 };
5113 }
5114 }
5115 AstMutateCommand::EditPoint { at } => {
5116 if let NodeMut::CallExpressionKw(call) = node {
5117 if call.callee.name.name != POINT_FN {
5118 return TraversalReturn::new_continue(());
5119 }
5120 for labeled_arg in &mut call.arguments {
5122 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5123 labeled_arg.arg = at.clone();
5124 }
5125 }
5126 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5127 }
5128 }
5129 AstMutateCommand::EditLine {
5130 start,
5131 end,
5132 construction,
5133 } => {
5134 if let NodeMut::CallExpressionKw(call) = node {
5135 if call.callee.name.name != LINE_FN {
5136 return TraversalReturn::new_continue(());
5137 }
5138 for labeled_arg in &mut call.arguments {
5140 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5141 labeled_arg.arg = start.clone();
5142 }
5143 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5144 labeled_arg.arg = end.clone();
5145 }
5146 }
5147 if let Some(construction_value) = construction {
5149 let construction_exists = call
5150 .arguments
5151 .iter()
5152 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5153 if *construction_value {
5154 if construction_exists {
5156 for labeled_arg in &mut call.arguments {
5158 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5159 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5160 value: ast::LiteralValue::Bool(true),
5161 raw: "true".to_string(),
5162 digest: None,
5163 })));
5164 }
5165 }
5166 } else {
5167 call.arguments.push(ast::LabeledArg {
5169 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5170 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5171 value: ast::LiteralValue::Bool(true),
5172 raw: "true".to_string(),
5173 digest: None,
5174 }))),
5175 });
5176 }
5177 } else {
5178 call.arguments
5180 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5181 }
5182 }
5183 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5184 }
5185 }
5186 AstMutateCommand::EditArc {
5187 start,
5188 end,
5189 center,
5190 construction,
5191 } => {
5192 if let NodeMut::CallExpressionKw(call) = node {
5193 if call.callee.name.name != ARC_FN {
5194 return TraversalReturn::new_continue(());
5195 }
5196 for labeled_arg in &mut call.arguments {
5198 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5199 labeled_arg.arg = start.clone();
5200 }
5201 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5202 labeled_arg.arg = end.clone();
5203 }
5204 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5205 labeled_arg.arg = center.clone();
5206 }
5207 }
5208 if let Some(construction_value) = construction {
5210 let construction_exists = call
5211 .arguments
5212 .iter()
5213 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5214 if *construction_value {
5215 if construction_exists {
5217 for labeled_arg in &mut call.arguments {
5219 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5220 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5221 value: ast::LiteralValue::Bool(true),
5222 raw: "true".to_string(),
5223 digest: None,
5224 })));
5225 }
5226 }
5227 } else {
5228 call.arguments.push(ast::LabeledArg {
5230 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5231 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5232 value: ast::LiteralValue::Bool(true),
5233 raw: "true".to_string(),
5234 digest: None,
5235 }))),
5236 });
5237 }
5238 } else {
5239 call.arguments
5241 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5242 }
5243 }
5244 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5245 }
5246 }
5247 AstMutateCommand::EditCircle {
5248 start,
5249 center,
5250 construction,
5251 } => {
5252 if let NodeMut::CallExpressionKw(call) = node {
5253 if call.callee.name.name != CIRCLE_FN {
5254 return TraversalReturn::new_continue(());
5255 }
5256 for labeled_arg in &mut call.arguments {
5258 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5259 labeled_arg.arg = start.clone();
5260 }
5261 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5262 labeled_arg.arg = center.clone();
5263 }
5264 }
5265 if let Some(construction_value) = construction {
5267 let construction_exists = call
5268 .arguments
5269 .iter()
5270 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5271 if *construction_value {
5272 if construction_exists {
5273 for labeled_arg in &mut call.arguments {
5274 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5275 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5276 value: ast::LiteralValue::Bool(true),
5277 raw: "true".to_string(),
5278 digest: None,
5279 })));
5280 }
5281 }
5282 } else {
5283 call.arguments.push(ast::LabeledArg {
5284 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5285 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5286 value: ast::LiteralValue::Bool(true),
5287 raw: "true".to_string(),
5288 digest: None,
5289 }))),
5290 });
5291 }
5292 } else {
5293 call.arguments
5294 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5295 }
5296 }
5297 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5298 }
5299 }
5300 AstMutateCommand::EditConstraintValue { value } => {
5301 if let NodeMut::BinaryExpression(binary_expr) = node {
5302 let left_is_constraint = matches!(
5303 &binary_expr.left,
5304 ast::BinaryPart::CallExpressionKw(call)
5305 if matches!(
5306 call.callee.name.name.as_str(),
5307 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5308 )
5309 );
5310 if left_is_constraint {
5311 binary_expr.right = value.clone();
5312 } else {
5313 binary_expr.left = value.clone();
5314 }
5315
5316 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5317 }
5318 }
5319 AstMutateCommand::EditCallUnlabeled { arg } => {
5320 if let NodeMut::CallExpressionKw(call) = node {
5321 call.unlabeled = Some(arg.clone());
5322 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5323 }
5324 }
5325 #[cfg(feature = "artifact-graph")]
5326 AstMutateCommand::EditVarInitialValue { value } => {
5327 if let NodeMut::NumericLiteral(numeric_literal) = node {
5328 let Ok(literal) = to_source_number(*value) else {
5330 return TraversalReturn::new_break(Err(KclError::refactor(format!(
5331 "Could not convert number to AST literal: {:?}",
5332 *value
5333 ))));
5334 };
5335 *numeric_literal = ast::Node::no_src(literal);
5336 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5337 }
5338 }
5339 AstMutateCommand::DeleteNode => {
5340 return TraversalReturn {
5341 mutate_body_item: MutateBodyItem::Delete,
5342 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5343 };
5344 }
5345 }
5346 TraversalReturn::new_continue(())
5347}
5348
5349struct FindSketchBlockSourceRange {
5350 target_before_mutation: SourceRange,
5352 found: Cell<Option<AstNodeRef>>,
5356}
5357
5358impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5359 type Error = crate::front::Error;
5360
5361 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5362 let Ok(node_range) = SourceRange::try_from(&node) else {
5363 return Ok(true);
5364 };
5365
5366 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5367 if node_range.module_id() == self.target_before_mutation.module_id()
5368 && node_range.start() == self.target_before_mutation.start()
5369 && node_range.end() >= self.target_before_mutation.end()
5371 {
5372 self.found.set(sketch_block.body.items.last().map(|item| match item {
5373 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5377 _ => AstNodeRef::from(item),
5378 }));
5379 return Ok(false);
5380 } else {
5381 return Ok(true);
5384 }
5385 }
5386
5387 for child in node.children().iter() {
5388 if !child.visit(*self)? {
5389 return Ok(false);
5390 }
5391 }
5392
5393 Ok(true)
5394 }
5395}
5396
5397struct FindSketchBlockByNodePath {
5398 target_node_path: ast::NodePath,
5400 found: Cell<Option<AstNodeRef>>,
5404}
5405
5406impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5407 type Error = crate::front::Error;
5408
5409 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5410 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5411 return Ok(true);
5412 };
5413
5414 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5415 if let Some(node_path) = node_path
5416 && node_path == self.target_node_path
5417 {
5418 self.found.set(sketch_block.body.items.last().map(|item| match item {
5419 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5423 _ => AstNodeRef::from(item),
5424 }));
5425
5426 return Ok(false);
5427 } else {
5428 return Ok(true);
5431 }
5432 }
5433
5434 for child in node.children().iter() {
5435 if !child.visit(*self)? {
5436 return Ok(false);
5437 }
5438 }
5439
5440 Ok(true)
5441 }
5442}
5443
5444fn find_sketch_block_added_item(
5452 ast: &ast::Node<ast::Program>,
5453 sketch_block_before_mutation: &AstNodeRef,
5454) -> Result<AstNodeRef, KclError> {
5455 if let Some(node_path) = &sketch_block_before_mutation.node_path {
5456 let find = FindSketchBlockByNodePath {
5457 target_node_path: node_path.clone(),
5458 found: Cell::new(None),
5459 };
5460 let node = crate::walk::Node::from(ast);
5461 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5462 find.found.into_inner().ok_or_else(|| {
5463 KclError::refactor(format!(
5464 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5465 ))
5466 })
5467 } else {
5468 let find = FindSketchBlockSourceRange {
5470 target_before_mutation: sketch_block_before_mutation.range,
5471 found: Cell::new(None),
5472 };
5473 let node = crate::walk::Node::from(ast);
5474 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5475 find.found.into_inner().ok_or_else(|| KclError::refactor(
5476 format!("Source range after mutation not found for range before mutation: {sketch_block_before_mutation:?}; Did you try formatting (i.e. call recast) before calling this?"),
5477 ))
5478 }
5479}
5480
5481fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5482 ast.recast_top(&Default::default(), 0)
5484}
5485
5486pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5487 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5488 inner: ast::ArrayExpression {
5489 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5490 non_code_meta: Default::default(),
5491 digest: None,
5492 },
5493 start: Default::default(),
5494 end: Default::default(),
5495 module_id: Default::default(),
5496 node_path: None,
5497 outer_attrs: Default::default(),
5498 pre_comments: Default::default(),
5499 comment_start: Default::default(),
5500 })))
5501}
5502
5503fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5504 match expr {
5505 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5506 inner: ast::Literal::from(to_source_number(*number)?),
5507 start: Default::default(),
5508 end: Default::default(),
5509 module_id: Default::default(),
5510 node_path: None,
5511 outer_attrs: Default::default(),
5512 pre_comments: Default::default(),
5513 comment_start: Default::default(),
5514 }))),
5515 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5516 inner: ast::SketchVar {
5517 initial: Some(Box::new(ast::Node {
5518 inner: to_source_number(*number)?,
5519 start: Default::default(),
5520 end: Default::default(),
5521 module_id: Default::default(),
5522 node_path: None,
5523 outer_attrs: Default::default(),
5524 pre_comments: Default::default(),
5525 comment_start: Default::default(),
5526 })),
5527 digest: None,
5528 },
5529 start: Default::default(),
5530 end: Default::default(),
5531 module_id: Default::default(),
5532 node_path: None,
5533 outer_attrs: Default::default(),
5534 pre_comments: Default::default(),
5535 comment_start: Default::default(),
5536 }))),
5537 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5538 }
5539}
5540
5541fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5542 Ok(ast::NumericLiteral {
5543 value: number.value,
5544 suffix: number.units,
5545 raw: format_number_literal(number.value, number.units, None)?,
5546 digest: None,
5547 })
5548}
5549
5550pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5551 ast::Expr::Name(Box::new(ast_name(name)))
5552}
5553
5554fn ast_name(name: String) -> ast::Node<ast::Name> {
5555 ast::Node {
5556 inner: ast::Name {
5557 name: ast::Node {
5558 inner: ast::Identifier { name, digest: None },
5559 start: Default::default(),
5560 end: Default::default(),
5561 module_id: Default::default(),
5562 node_path: None,
5563 outer_attrs: Default::default(),
5564 pre_comments: Default::default(),
5565 comment_start: Default::default(),
5566 },
5567 path: Vec::new(),
5568 abs_path: false,
5569 digest: None,
5570 },
5571 start: Default::default(),
5572 end: Default::default(),
5573 module_id: Default::default(),
5574 node_path: None,
5575 outer_attrs: Default::default(),
5576 pre_comments: Default::default(),
5577 comment_start: Default::default(),
5578 }
5579}
5580
5581pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5582 ast::Name {
5583 name: ast::Node {
5584 inner: ast::Identifier {
5585 name: name.to_owned(),
5586 digest: None,
5587 },
5588 start: Default::default(),
5589 end: Default::default(),
5590 module_id: Default::default(),
5591 node_path: None,
5592 outer_attrs: Default::default(),
5593 pre_comments: Default::default(),
5594 comment_start: Default::default(),
5595 },
5596 path: Default::default(),
5597 abs_path: false,
5598 digest: None,
5599 }
5600}
5601
5602pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
5606 let elements = exprs.into_iter().collect::<Vec<_>>();
5607 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
5608
5609 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5611 elements,
5612 digest: None,
5613 non_code_meta: Default::default(),
5614 })));
5615
5616 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5618 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5619 unlabeled: Some(array_expr),
5620 arguments: Default::default(),
5621 digest: None,
5622 non_code_meta: Default::default(),
5623 })))
5624}
5625
5626pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5628 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5629 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5630 unlabeled: None,
5631 arguments: vec![
5632 ast::LabeledArg {
5633 label: Some(ast::Identifier::new(LINE_START_PARAM)),
5634 arg: start_ast,
5635 },
5636 ast::LabeledArg {
5637 label: Some(ast::Identifier::new(LINE_END_PARAM)),
5638 arg: end_ast,
5639 },
5640 ],
5641 digest: None,
5642 non_code_meta: Default::default(),
5643 })))
5644}
5645
5646pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5648 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5649 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5650 unlabeled: None,
5651 arguments: vec![
5652 ast::LabeledArg {
5653 label: Some(ast::Identifier::new(ARC_START_PARAM)),
5654 arg: start_ast,
5655 },
5656 ast::LabeledArg {
5657 label: Some(ast::Identifier::new(ARC_END_PARAM)),
5658 arg: end_ast,
5659 },
5660 ast::LabeledArg {
5661 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5662 arg: center_ast,
5663 },
5664 ],
5665 digest: None,
5666 non_code_meta: Default::default(),
5667 })))
5668}
5669
5670pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5672 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5673 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
5674 unlabeled: None,
5675 arguments: vec![
5676 ast::LabeledArg {
5677 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5678 arg: start_ast,
5679 },
5680 ast::LabeledArg {
5681 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5682 arg: center_ast,
5683 },
5684 ],
5685 digest: None,
5686 non_code_meta: Default::default(),
5687 })))
5688}
5689
5690pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
5692 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5693 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
5694 unlabeled: Some(line_expr),
5695 arguments: Default::default(),
5696 digest: None,
5697 non_code_meta: Default::default(),
5698 })))
5699}
5700
5701pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5703 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5704 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5705 unlabeled: Some(line_expr),
5706 arguments: Default::default(),
5707 digest: None,
5708 non_code_meta: Default::default(),
5709 })))
5710}
5711
5712pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5714 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5715 object: object_expr,
5716 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5717 name: ast::Node::no_src(ast::Identifier {
5718 name: property.to_string(),
5719 digest: None,
5720 }),
5721 path: Vec::new(),
5722 abs_path: false,
5723 digest: None,
5724 }))),
5725 computed: false,
5726 digest: None,
5727 })))
5728}
5729
5730fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5732 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5734 position.x,
5735 )?))));
5736 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5737 position.y,
5738 )?))));
5739 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5740 elements: vec![x_literal, y_literal],
5741 digest: None,
5742 non_code_meta: Default::default(),
5743 })));
5744
5745 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5747 elements: vec![point_expr, point_array],
5748 digest: None,
5749 non_code_meta: Default::default(),
5750 })));
5751
5752 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5754 ast::CallExpressionKw {
5755 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5756 unlabeled: Some(array_expr),
5757 arguments: Default::default(),
5758 digest: None,
5759 non_code_meta: Default::default(),
5760 },
5761 ))))
5762}
5763
5764pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5766 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5767 elements: line_exprs,
5768 digest: None,
5769 non_code_meta: Default::default(),
5770 })));
5771
5772 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5774 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5775 unlabeled: Some(array_expr),
5776 arguments: Default::default(),
5777 digest: None,
5778 non_code_meta: Default::default(),
5779 })))
5780}
5781
5782pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5784 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5785 elements: segment_exprs,
5786 digest: None,
5787 non_code_meta: Default::default(),
5788 })));
5789
5790 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5791 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5792 unlabeled: Some(array_expr),
5793 arguments: Default::default(),
5794 digest: None,
5795 non_code_meta: Default::default(),
5796 })))
5797}
5798
5799pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5801 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5802 elements: vec![seg1_expr, seg2_expr],
5803 digest: None,
5804 non_code_meta: Default::default(),
5805 })));
5806
5807 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5808 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5809 unlabeled: Some(array_expr),
5810 arguments: Default::default(),
5811 digest: None,
5812 non_code_meta: Default::default(),
5813 })))
5814}
5815
5816pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
5818 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5819 elements: input_exprs,
5820 digest: None,
5821 non_code_meta: Default::default(),
5822 })));
5823 let arguments = vec![ast::LabeledArg {
5824 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
5825 arg: axis_expr,
5826 }];
5827
5828 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5829 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
5830 unlabeled: Some(array_expr),
5831 arguments,
5832 digest: None,
5833 non_code_meta: Default::default(),
5834 })))
5835}
5836
5837pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
5839 let arguments = vec![ast::LabeledArg {
5840 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
5841 arg: point_expr,
5842 }];
5843
5844 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5845 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
5846 unlabeled: Some(segment_expr),
5847 arguments,
5848 digest: None,
5849 non_code_meta: Default::default(),
5850 })))
5851}
5852
5853#[cfg(all(feature = "artifact-graph", test))]
5854mod tests {
5855 use super::*;
5856 use crate::engine::PlaneName;
5857 use crate::execution::cache::SketchModeState;
5858 use crate::execution::cache::clear_mem_cache;
5859 use crate::execution::cache::read_old_memory;
5860 use crate::execution::cache::write_old_memory;
5861 use crate::front::Distance;
5862 use crate::front::Fixed;
5863 use crate::front::FixedPoint;
5864 use crate::front::Midpoint;
5865 use crate::front::Object;
5866 use crate::front::Plane;
5867 use crate::front::Sketch;
5868 use crate::front::Tangent;
5869 use crate::frontend::sketch::Vertical;
5870 use crate::pretty::NumericSuffix;
5871
5872 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
5873 for object in &scene_graph.objects {
5874 if let ObjectKind::Sketch(_) = &object.kind {
5875 return Some(object);
5876 }
5877 }
5878 None
5879 }
5880
5881 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
5882 for object in &scene_graph.objects {
5883 if let ObjectKind::Face(_) = &object.kind {
5884 return Some(object);
5885 }
5886 }
5887 None
5888 }
5889
5890 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
5891 for object in &scene_graph.objects {
5892 if matches!(&object.kind, ObjectKind::Wall(_)) {
5893 return Some(object.id);
5894 }
5895 }
5896 None
5897 }
5898
5899 #[test]
5900 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
5901 let source = "\
5902region001 = region(point = [0.1, 0.1], sketch = s)
5903extrude001 = extrude(region001, length = 5)
5904revolve001 = revolve(region001, axis = Y)
5905sweep001 = sweep(region001, path = path001)
5906loft001 = loft(region001)
5907not_sweep001 = shell(extrude001, faces = [], thickness = 1)
5908";
5909
5910 let program = Program::parse(source).unwrap().0.unwrap();
5911
5912 assert_eq!(
5913 region_name_from_sweep_variable(&program.ast, "extrude001"),
5914 Some("region001".to_owned())
5915 );
5916 assert_eq!(
5917 region_name_from_sweep_variable(&program.ast, "revolve001"),
5918 Some("region001".to_owned())
5919 );
5920 assert_eq!(
5921 region_name_from_sweep_variable(&program.ast, "sweep001"),
5922 Some("region001".to_owned())
5923 );
5924 assert_eq!(
5925 region_name_from_sweep_variable(&program.ast, "loft001"),
5926 Some("region001".to_owned())
5927 );
5928 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
5929 }
5930
5931 #[track_caller]
5932 fn expect_sketch(object: &Object) -> &Sketch {
5933 if let ObjectKind::Sketch(sketch) = &object.kind {
5934 sketch
5935 } else {
5936 panic!("Object is not a sketch: {:?}", object);
5937 }
5938 }
5939
5940 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
5941 LineCtor {
5942 start: Point2d {
5943 x: Expr::Number(Number { value: start_x, units }),
5944 y: Expr::Number(Number { value: start_y, units }),
5945 },
5946 end: Point2d {
5947 x: Expr::Number(Number { value: end_x, units }),
5948 y: Expr::Number(Number { value: end_y, units }),
5949 },
5950 construction: None,
5951 }
5952 }
5953
5954 async fn create_sketch_with_single_line(
5955 frontend: &mut FrontendState,
5956 ctx: &ExecutorContext,
5957 mock_ctx: &ExecutorContext,
5958 version: Version,
5959 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
5960 frontend.program = Program::empty();
5961
5962 let sketch_args = SketchCtor {
5963 on: Plane::Default(PlaneName::Xy),
5964 };
5965 let (_src_delta, _scene_delta, sketch_id) = frontend
5966 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
5967 .await
5968 .unwrap();
5969
5970 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
5971 let (source_delta, scene_graph_delta) = frontend
5972 .add_segment(mock_ctx, version, sketch_id, segment, None)
5973 .await
5974 .unwrap();
5975 let line_id = *scene_graph_delta
5976 .new_objects
5977 .last()
5978 .expect("Expected line object id to be created");
5979
5980 (sketch_id, line_id, source_delta, scene_graph_delta)
5981 }
5982
5983 #[tokio::test(flavor = "multi_thread")]
5984 async fn test_sketch_checkpoint_round_trip_restores_state() {
5985 let mut frontend = FrontendState::new();
5986 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5987 let mock_ctx = ExecutorContext::new_mock(None).await;
5988 let version = Version(0);
5989
5990 let (sketch_id, line_id, source_delta, scene_graph_delta) =
5991 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5992
5993 let expected_source = source_delta.text.clone();
5994 let expected_scene_graph = frontend.scene_graph.clone();
5995 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
5996 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
5997
5998 let checkpoint_id = frontend
5999 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6000 .await
6001 .unwrap();
6002
6003 let edited_segments = vec![ExistingSegmentCtor {
6004 id: line_id,
6005 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6006 }];
6007 let (edited_source, _edited_scene) = frontend
6008 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6009 .await
6010 .unwrap();
6011 assert_ne!(edited_source.text, expected_source);
6012
6013 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6014
6015 assert_eq!(restored.source_delta.text, expected_source);
6016 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6017 assert!(restored.scene_graph_delta.invalidates_ids);
6018 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6019 assert_eq!(frontend.scene_graph, expected_scene_graph);
6020 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6021
6022 ctx.close().await;
6023 mock_ctx.close().await;
6024 }
6025
6026 #[tokio::test(flavor = "multi_thread")]
6027 async fn test_sketch_checkpoints_prune_oldest_entries() {
6028 let mut frontend = FrontendState::new();
6029 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6030 let mock_ctx = ExecutorContext::new_mock(None).await;
6031 let version = Version(0);
6032
6033 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6034 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6035
6036 let mut checkpoint_ids = Vec::new();
6037 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6038 checkpoint_ids.push(
6039 frontend
6040 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6041 .await
6042 .unwrap(),
6043 );
6044 }
6045
6046 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6047 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6048
6049 let oldest_retained = checkpoint_ids[3];
6050 assert_eq!(
6051 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6052 Some(oldest_retained)
6053 );
6054
6055 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6056 assert!(evicted_restore.is_err());
6057 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6058
6059 frontend
6060 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6061 .await
6062 .unwrap();
6063
6064 ctx.close().await;
6065 mock_ctx.close().await;
6066 }
6067
6068 #[tokio::test(flavor = "multi_thread")]
6069 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6070 let mut frontend = FrontendState::new();
6071 let missing_checkpoint = SketchCheckpointId::new(999);
6072
6073 let err = frontend
6074 .restore_sketch_checkpoint(missing_checkpoint)
6075 .await
6076 .expect_err("Expected restore to fail for missing checkpoint");
6077
6078 assert!(err.msg.contains("Sketch checkpoint not found"));
6079 }
6080
6081 #[tokio::test(flavor = "multi_thread")]
6082 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6083 let mut frontend = FrontendState::new();
6084 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6085 let mock_ctx = ExecutorContext::new_mock(None).await;
6086 let version = Version(0);
6087
6088 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6089 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6090
6091 let checkpoint_a = frontend
6092 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6093 .await
6094 .unwrap();
6095 let checkpoint_b = frontend
6096 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6097 .await
6098 .unwrap();
6099 assert_eq!(frontend.sketch_checkpoints.len(), 2);
6100
6101 frontend.clear_sketch_checkpoints();
6102 assert!(frontend.sketch_checkpoints.is_empty());
6103 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6104 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6105
6106 ctx.close().await;
6107 mock_ctx.close().await;
6108 }
6109
6110 #[tokio::test(flavor = "multi_thread")]
6111 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6112 let mut frontend = FrontendState::new();
6113 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6114 let mock_ctx = ExecutorContext::new_mock(None).await;
6115 let version = Version(0);
6116
6117 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6118 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6119 let old_source = source_delta.text.clone();
6120 let old_checkpoint = frontend
6121 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6122 .await
6123 .unwrap();
6124 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6125
6126 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
6127 .unwrap()
6128 .0
6129 .unwrap();
6130
6131 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6132 let SetProgramOutcome::Success {
6133 checkpoint_id: Some(new_checkpoint),
6134 ..
6135 } = result
6136 else {
6137 panic!("Expected Success with a fresh checkpoint baseline");
6138 };
6139
6140 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6141
6142 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6143 assert_eq!(old_restore.source_delta.text, old_source);
6144
6145 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6146 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6147
6148 ctx.close().await;
6149 mock_ctx.close().await;
6150 }
6151
6152 #[tokio::test(flavor = "multi_thread")]
6153 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6154 let mut frontend = FrontendState::new();
6155 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6156 let mock_ctx = ExecutorContext::new_mock(None).await;
6157 let version = Version(0);
6158
6159 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6160 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6161 let old_checkpoint = frontend
6162 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6163 .await
6164 .unwrap();
6165 let checkpoint_count_before = frontend.sketch_checkpoints.len();
6166
6167 let failing_program = Program::parse(
6168 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6169 )
6170 .unwrap()
6171 .0
6172 .unwrap();
6173
6174 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6175 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6176 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6177 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6178
6179 ctx.close().await;
6180 mock_ctx.close().await;
6181 }
6182
6183 #[tokio::test(flavor = "multi_thread")]
6184 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6185 let mut frontend = FrontendState::new();
6186 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6187
6188 let program = Program::parse(
6189 "width = 2mm\nsketch001 = sketch(on = offsetPlane(XY, offset = width)) {\n line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])\n distance([line1.start, line1.end]) == width\n}\n",
6190 )
6191 .unwrap()
6192 .0
6193 .unwrap();
6194 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6195 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6196 panic!("Expected successful baseline program execution");
6197 };
6198
6199 clear_mem_cache().await;
6200 assert!(read_old_memory().await.is_none());
6201
6202 let checkpoint_without_mock_memory = frontend
6203 .create_sketch_checkpoint((*exec_outcome).clone())
6204 .await
6205 .unwrap();
6206
6207 write_old_memory(SketchModeState::new_for_tests()).await;
6208 assert!(read_old_memory().await.is_some());
6209
6210 let checkpoint_with_mock_memory = frontend
6211 .create_sketch_checkpoint((*exec_outcome).clone())
6212 .await
6213 .unwrap();
6214
6215 clear_mem_cache().await;
6216 assert!(read_old_memory().await.is_none());
6217
6218 frontend
6219 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6220 .await
6221 .unwrap();
6222 assert!(read_old_memory().await.is_some());
6223
6224 frontend
6225 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6226 .await
6227 .unwrap();
6228 assert!(read_old_memory().await.is_none());
6229
6230 ctx.close().await;
6231 }
6232
6233 #[tokio::test(flavor = "multi_thread")]
6234 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6235 let source = "\
6236sketch(on = XY) {
6237 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6238}
6239
6240bad = missing_name
6241";
6242 let program = Program::parse(source).unwrap().0.unwrap();
6243
6244 let mut frontend = FrontendState::new();
6245
6246 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6247 let mock_ctx = ExecutorContext::new_mock(None).await;
6248 let version = Version(0);
6249 let project_id = ProjectId(0);
6250 let file_id = FileId(0);
6251
6252 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6253 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6254 };
6255
6256 let sketch_id = frontend
6257 .scene_graph
6258 .objects
6259 .iter()
6260 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6261 .expect("Expected sketch object from errored hack_set_program");
6262
6263 frontend
6264 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6265 .await
6266 .unwrap();
6267
6268 ctx.close().await;
6269 mock_ctx.close().await;
6270 }
6271
6272 #[tokio::test(flavor = "multi_thread")]
6273 async fn test_new_sketch_add_point_edit_point() {
6274 let program = Program::empty();
6275
6276 let mut frontend = FrontendState::new();
6277 frontend.program = program;
6278
6279 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6280 let mock_ctx = ExecutorContext::new_mock(None).await;
6281 let version = Version(0);
6282
6283 let sketch_args = SketchCtor {
6284 on: Plane::Default(PlaneName::Xy),
6285 };
6286 let (_src_delta, scene_delta, sketch_id) = frontend
6287 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6288 .await
6289 .unwrap();
6290 assert_eq!(sketch_id, ObjectId(1));
6291 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6292 let sketch_object = &scene_delta.new_graph.objects[1];
6293 assert_eq!(sketch_object.id, ObjectId(1));
6294 assert_eq!(
6295 sketch_object.kind,
6296 ObjectKind::Sketch(Sketch {
6297 args: SketchCtor {
6298 on: Plane::Default(PlaneName::Xy)
6299 },
6300 plane: ObjectId(0),
6301 segments: vec![],
6302 constraints: vec![],
6303 })
6304 );
6305 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6306
6307 let point_ctor = PointCtor {
6308 position: Point2d {
6309 x: Expr::Number(Number {
6310 value: 1.0,
6311 units: NumericSuffix::Inch,
6312 }),
6313 y: Expr::Number(Number {
6314 value: 2.0,
6315 units: NumericSuffix::Inch,
6316 }),
6317 },
6318 };
6319 let segment = SegmentCtor::Point(point_ctor);
6320 let (src_delta, scene_delta) = frontend
6321 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6322 .await
6323 .unwrap();
6324 assert_eq!(
6325 src_delta.text.as_str(),
6326 "sketch001 = sketch(on = XY) {
6327 point(at = [1in, 2in])
6328}
6329"
6330 );
6331 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6332 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6333 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6334 assert_eq!(scene_object.id.0, i);
6335 }
6336
6337 let point_id = *scene_delta.new_objects.last().unwrap();
6338
6339 let point_ctor = PointCtor {
6340 position: Point2d {
6341 x: Expr::Number(Number {
6342 value: 3.0,
6343 units: NumericSuffix::Inch,
6344 }),
6345 y: Expr::Number(Number {
6346 value: 4.0,
6347 units: NumericSuffix::Inch,
6348 }),
6349 },
6350 };
6351 let segments = vec![ExistingSegmentCtor {
6352 id: point_id,
6353 ctor: SegmentCtor::Point(point_ctor),
6354 }];
6355 let (src_delta, scene_delta) = frontend
6356 .edit_segments(&mock_ctx, version, sketch_id, segments)
6357 .await
6358 .unwrap();
6359 assert_eq!(
6360 src_delta.text.as_str(),
6361 "sketch001 = sketch(on = XY) {
6362 point(at = [3in, 4in])
6363}
6364"
6365 );
6366 assert_eq!(scene_delta.new_objects, vec![]);
6367 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6368
6369 ctx.close().await;
6370 mock_ctx.close().await;
6371 }
6372
6373 #[tokio::test(flavor = "multi_thread")]
6374 async fn test_new_sketch_add_line_edit_line() {
6375 let program = Program::empty();
6376
6377 let mut frontend = FrontendState::new();
6378 frontend.program = program;
6379
6380 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6381 let mock_ctx = ExecutorContext::new_mock(None).await;
6382 let version = Version(0);
6383
6384 let sketch_args = SketchCtor {
6385 on: Plane::Default(PlaneName::Xy),
6386 };
6387 let (_src_delta, scene_delta, sketch_id) = frontend
6388 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6389 .await
6390 .unwrap();
6391 assert_eq!(sketch_id, ObjectId(1));
6392 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6393 let sketch_object = &scene_delta.new_graph.objects[1];
6394 assert_eq!(sketch_object.id, ObjectId(1));
6395 assert_eq!(
6396 sketch_object.kind,
6397 ObjectKind::Sketch(Sketch {
6398 args: SketchCtor {
6399 on: Plane::Default(PlaneName::Xy)
6400 },
6401 plane: ObjectId(0),
6402 segments: vec![],
6403 constraints: vec![],
6404 })
6405 );
6406 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6407
6408 let line_ctor = LineCtor {
6409 start: Point2d {
6410 x: Expr::Number(Number {
6411 value: 0.0,
6412 units: NumericSuffix::Mm,
6413 }),
6414 y: Expr::Number(Number {
6415 value: 0.0,
6416 units: NumericSuffix::Mm,
6417 }),
6418 },
6419 end: Point2d {
6420 x: Expr::Number(Number {
6421 value: 10.0,
6422 units: NumericSuffix::Mm,
6423 }),
6424 y: Expr::Number(Number {
6425 value: 10.0,
6426 units: NumericSuffix::Mm,
6427 }),
6428 },
6429 construction: None,
6430 };
6431 let segment = SegmentCtor::Line(line_ctor);
6432 let (src_delta, scene_delta) = frontend
6433 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6434 .await
6435 .unwrap();
6436 assert_eq!(
6437 src_delta.text.as_str(),
6438 "sketch001 = sketch(on = XY) {
6439 line(start = [0mm, 0mm], end = [10mm, 10mm])
6440}
6441"
6442 );
6443 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6444 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6445 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6446 assert_eq!(scene_object.id.0, i);
6447 }
6448
6449 let line = *scene_delta.new_objects.last().unwrap();
6451
6452 let line_ctor = LineCtor {
6453 start: Point2d {
6454 x: Expr::Number(Number {
6455 value: 1.0,
6456 units: NumericSuffix::Mm,
6457 }),
6458 y: Expr::Number(Number {
6459 value: 2.0,
6460 units: NumericSuffix::Mm,
6461 }),
6462 },
6463 end: Point2d {
6464 x: Expr::Number(Number {
6465 value: 13.0,
6466 units: NumericSuffix::Mm,
6467 }),
6468 y: Expr::Number(Number {
6469 value: 14.0,
6470 units: NumericSuffix::Mm,
6471 }),
6472 },
6473 construction: None,
6474 };
6475 let segments = vec![ExistingSegmentCtor {
6476 id: line,
6477 ctor: SegmentCtor::Line(line_ctor),
6478 }];
6479 let (src_delta, scene_delta) = frontend
6480 .edit_segments(&mock_ctx, version, sketch_id, segments)
6481 .await
6482 .unwrap();
6483 assert_eq!(
6484 src_delta.text.as_str(),
6485 "sketch001 = sketch(on = XY) {
6486 line(start = [1mm, 2mm], end = [13mm, 14mm])
6487}
6488"
6489 );
6490 assert_eq!(scene_delta.new_objects, vec![]);
6491 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6492
6493 ctx.close().await;
6494 mock_ctx.close().await;
6495 }
6496
6497 #[tokio::test(flavor = "multi_thread")]
6498 async fn test_new_sketch_add_arc_edit_arc() {
6499 let program = Program::empty();
6500
6501 let mut frontend = FrontendState::new();
6502 frontend.program = program;
6503
6504 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6505 let mock_ctx = ExecutorContext::new_mock(None).await;
6506 let version = Version(0);
6507
6508 let sketch_args = SketchCtor {
6509 on: Plane::Default(PlaneName::Xy),
6510 };
6511 let (_src_delta, scene_delta, sketch_id) = frontend
6512 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6513 .await
6514 .unwrap();
6515 assert_eq!(sketch_id, ObjectId(1));
6516 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6517 let sketch_object = &scene_delta.new_graph.objects[1];
6518 assert_eq!(sketch_object.id, ObjectId(1));
6519 assert_eq!(
6520 sketch_object.kind,
6521 ObjectKind::Sketch(Sketch {
6522 args: SketchCtor {
6523 on: Plane::Default(PlaneName::Xy),
6524 },
6525 plane: ObjectId(0),
6526 segments: vec![],
6527 constraints: vec![],
6528 })
6529 );
6530 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6531
6532 let arc_ctor = ArcCtor {
6533 start: Point2d {
6534 x: Expr::Var(Number {
6535 value: 0.0,
6536 units: NumericSuffix::Mm,
6537 }),
6538 y: Expr::Var(Number {
6539 value: 0.0,
6540 units: NumericSuffix::Mm,
6541 }),
6542 },
6543 end: Point2d {
6544 x: Expr::Var(Number {
6545 value: 10.0,
6546 units: NumericSuffix::Mm,
6547 }),
6548 y: Expr::Var(Number {
6549 value: 10.0,
6550 units: NumericSuffix::Mm,
6551 }),
6552 },
6553 center: Point2d {
6554 x: Expr::Var(Number {
6555 value: 10.0,
6556 units: NumericSuffix::Mm,
6557 }),
6558 y: Expr::Var(Number {
6559 value: 0.0,
6560 units: NumericSuffix::Mm,
6561 }),
6562 },
6563 construction: None,
6564 };
6565 let segment = SegmentCtor::Arc(arc_ctor);
6566 let (src_delta, scene_delta) = frontend
6567 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6568 .await
6569 .unwrap();
6570 assert_eq!(
6571 src_delta.text.as_str(),
6572 "sketch001 = sketch(on = XY) {
6573 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6574}
6575"
6576 );
6577 assert_eq!(
6578 scene_delta.new_objects,
6579 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6580 );
6581 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6582 assert_eq!(scene_object.id.0, i);
6583 }
6584 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6585
6586 let arc = *scene_delta.new_objects.last().unwrap();
6588
6589 let arc_ctor = ArcCtor {
6590 start: Point2d {
6591 x: Expr::Var(Number {
6592 value: 1.0,
6593 units: NumericSuffix::Mm,
6594 }),
6595 y: Expr::Var(Number {
6596 value: 2.0,
6597 units: NumericSuffix::Mm,
6598 }),
6599 },
6600 end: Point2d {
6601 x: Expr::Var(Number {
6602 value: 13.0,
6603 units: NumericSuffix::Mm,
6604 }),
6605 y: Expr::Var(Number {
6606 value: 14.0,
6607 units: NumericSuffix::Mm,
6608 }),
6609 },
6610 center: Point2d {
6611 x: Expr::Var(Number {
6612 value: 13.0,
6613 units: NumericSuffix::Mm,
6614 }),
6615 y: Expr::Var(Number {
6616 value: 2.0,
6617 units: NumericSuffix::Mm,
6618 }),
6619 },
6620 construction: None,
6621 };
6622 let segments = vec![ExistingSegmentCtor {
6623 id: arc,
6624 ctor: SegmentCtor::Arc(arc_ctor),
6625 }];
6626 let (src_delta, scene_delta) = frontend
6627 .edit_segments(&mock_ctx, version, sketch_id, segments)
6628 .await
6629 .unwrap();
6630 assert_eq!(
6631 src_delta.text.as_str(),
6632 "sketch001 = sketch(on = XY) {
6633 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6634}
6635"
6636 );
6637 assert_eq!(scene_delta.new_objects, vec![]);
6638 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6639
6640 ctx.close().await;
6641 mock_ctx.close().await;
6642 }
6643
6644 #[tokio::test(flavor = "multi_thread")]
6645 async fn test_new_sketch_add_circle_edit_circle() {
6646 let program = Program::empty();
6647
6648 let mut frontend = FrontendState::new();
6649 frontend.program = program;
6650
6651 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6652 let mock_ctx = ExecutorContext::new_mock(None).await;
6653 let version = Version(0);
6654
6655 let sketch_args = SketchCtor {
6656 on: Plane::Default(PlaneName::Xy),
6657 };
6658 let (_src_delta, _scene_delta, sketch_id) = frontend
6659 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6660 .await
6661 .unwrap();
6662
6663 let circle_ctor = CircleCtor {
6665 start: Point2d {
6666 x: Expr::Var(Number {
6667 value: 5.0,
6668 units: NumericSuffix::Mm,
6669 }),
6670 y: Expr::Var(Number {
6671 value: 0.0,
6672 units: NumericSuffix::Mm,
6673 }),
6674 },
6675 center: Point2d {
6676 x: Expr::Var(Number {
6677 value: 0.0,
6678 units: NumericSuffix::Mm,
6679 }),
6680 y: Expr::Var(Number {
6681 value: 0.0,
6682 units: NumericSuffix::Mm,
6683 }),
6684 },
6685 construction: None,
6686 };
6687 let segment = SegmentCtor::Circle(circle_ctor);
6688 let (src_delta, scene_delta) = frontend
6689 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6690 .await
6691 .unwrap();
6692 assert_eq!(
6693 src_delta.text.as_str(),
6694 "sketch001 = sketch(on = XY) {
6695 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6696}
6697"
6698 );
6699 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6701 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6702
6703 let circle = *scene_delta.new_objects.last().unwrap();
6704
6705 let circle_ctor = CircleCtor {
6707 start: Point2d {
6708 x: Expr::Var(Number {
6709 value: 10.0,
6710 units: NumericSuffix::Mm,
6711 }),
6712 y: Expr::Var(Number {
6713 value: 0.0,
6714 units: NumericSuffix::Mm,
6715 }),
6716 },
6717 center: Point2d {
6718 x: Expr::Var(Number {
6719 value: 3.0,
6720 units: NumericSuffix::Mm,
6721 }),
6722 y: Expr::Var(Number {
6723 value: 4.0,
6724 units: NumericSuffix::Mm,
6725 }),
6726 },
6727 construction: None,
6728 };
6729 let segments = vec![ExistingSegmentCtor {
6730 id: circle,
6731 ctor: SegmentCtor::Circle(circle_ctor),
6732 }];
6733 let (src_delta, scene_delta) = frontend
6734 .edit_segments(&mock_ctx, version, sketch_id, segments)
6735 .await
6736 .unwrap();
6737 assert_eq!(
6738 src_delta.text.as_str(),
6739 "sketch001 = sketch(on = XY) {
6740 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6741}
6742"
6743 );
6744 assert_eq!(scene_delta.new_objects, vec![]);
6745 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6746
6747 ctx.close().await;
6748 mock_ctx.close().await;
6749 }
6750
6751 #[tokio::test(flavor = "multi_thread")]
6752 async fn test_delete_circle() {
6753 let initial_source = "sketch001 = sketch(on = XY) {
6754 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6755}
6756";
6757
6758 let program = Program::parse(initial_source).unwrap().0.unwrap();
6759 let mut frontend = FrontendState::new();
6760
6761 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6762 let mock_ctx = ExecutorContext::new_mock(None).await;
6763 let version = Version(0);
6764
6765 frontend.hack_set_program(&ctx, program).await.unwrap();
6766 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6767 let sketch_id = sketch_object.id;
6768 let sketch = expect_sketch(sketch_object);
6769
6770 assert_eq!(sketch.segments.len(), 3);
6772 let circle_id = sketch.segments[2];
6773
6774 let (src_delta, scene_delta) = frontend
6776 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6777 .await
6778 .unwrap();
6779 assert_eq!(
6780 src_delta.text.as_str(),
6781 "sketch001 = sketch(on = XY) {
6782}
6783"
6784 );
6785 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6786 let new_sketch = expect_sketch(new_sketch_object);
6787 assert_eq!(new_sketch.segments.len(), 0);
6788
6789 ctx.close().await;
6790 mock_ctx.close().await;
6791 }
6792
6793 #[tokio::test(flavor = "multi_thread")]
6794 async fn test_edit_circle_via_point() {
6795 let initial_source = "sketch001 = sketch(on = XY) {
6796 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6797}
6798";
6799
6800 let program = Program::parse(initial_source).unwrap().0.unwrap();
6801 let mut frontend = FrontendState::new();
6802
6803 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6804 let mock_ctx = ExecutorContext::new_mock(None).await;
6805 let version = Version(0);
6806
6807 frontend.hack_set_program(&ctx, program).await.unwrap();
6808 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6809 let sketch_id = sketch_object.id;
6810 let sketch = expect_sketch(sketch_object);
6811
6812 let circle_id = sketch
6814 .segments
6815 .iter()
6816 .copied()
6817 .find(|seg_id| {
6818 matches!(
6819 &frontend.scene_graph.objects[seg_id.0].kind,
6820 ObjectKind::Segment {
6821 segment: Segment::Circle(_)
6822 }
6823 )
6824 })
6825 .expect("Expected a circle segment in sketch");
6826 let circle_object = &frontend.scene_graph.objects[circle_id.0];
6827 let ObjectKind::Segment {
6828 segment: Segment::Circle(circle),
6829 } = &circle_object.kind
6830 else {
6831 panic!("Expected circle segment, got: {:?}", circle_object.kind);
6832 };
6833 let start_point_id = circle.start;
6834
6835 let segments = vec![ExistingSegmentCtor {
6837 id: start_point_id,
6838 ctor: SegmentCtor::Point(PointCtor {
6839 position: Point2d {
6840 x: Expr::Var(Number {
6841 value: 7.0,
6842 units: NumericSuffix::Mm,
6843 }),
6844 y: Expr::Var(Number {
6845 value: 1.0,
6846 units: NumericSuffix::Mm,
6847 }),
6848 },
6849 }),
6850 }];
6851 let (src_delta, _scene_delta) = frontend
6852 .edit_segments(&mock_ctx, version, sketch_id, segments)
6853 .await
6854 .unwrap();
6855 assert_eq!(
6856 src_delta.text.as_str(),
6857 "sketch001 = sketch(on = XY) {
6858 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
6859}
6860"
6861 );
6862
6863 ctx.close().await;
6864 mock_ctx.close().await;
6865 }
6866
6867 #[tokio::test(flavor = "multi_thread")]
6868 async fn test_add_line_when_sketch_block_uses_variable() {
6869 let initial_source = "s = sketch(on = XY) {}
6870";
6871
6872 let program = Program::parse(initial_source).unwrap().0.unwrap();
6873
6874 let mut frontend = FrontendState::new();
6875
6876 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6877 let mock_ctx = ExecutorContext::new_mock(None).await;
6878 let version = Version(0);
6879
6880 frontend.hack_set_program(&ctx, program).await.unwrap();
6881 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6882 let sketch_id = sketch_object.id;
6883
6884 let line_ctor = LineCtor {
6885 start: Point2d {
6886 x: Expr::Number(Number {
6887 value: 0.0,
6888 units: NumericSuffix::Mm,
6889 }),
6890 y: Expr::Number(Number {
6891 value: 0.0,
6892 units: NumericSuffix::Mm,
6893 }),
6894 },
6895 end: Point2d {
6896 x: Expr::Number(Number {
6897 value: 10.0,
6898 units: NumericSuffix::Mm,
6899 }),
6900 y: Expr::Number(Number {
6901 value: 10.0,
6902 units: NumericSuffix::Mm,
6903 }),
6904 },
6905 construction: None,
6906 };
6907 let segment = SegmentCtor::Line(line_ctor);
6908 let (src_delta, scene_delta) = frontend
6909 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6910 .await
6911 .unwrap();
6912 assert_eq!(
6913 src_delta.text.as_str(),
6914 "s = sketch(on = XY) {
6915 line(start = [0mm, 0mm], end = [10mm, 10mm])
6916}
6917"
6918 );
6919 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6920 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6921
6922 ctx.close().await;
6923 mock_ctx.close().await;
6924 }
6925
6926 #[tokio::test(flavor = "multi_thread")]
6927 async fn test_new_sketch_add_line_delete_sketch() {
6928 let program = Program::empty();
6929
6930 let mut frontend = FrontendState::new();
6931 frontend.program = program;
6932
6933 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6934 let mock_ctx = ExecutorContext::new_mock(None).await;
6935 let version = Version(0);
6936
6937 let sketch_args = SketchCtor {
6938 on: Plane::Default(PlaneName::Xy),
6939 };
6940 let (_src_delta, scene_delta, sketch_id) = frontend
6941 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6942 .await
6943 .unwrap();
6944 assert_eq!(sketch_id, ObjectId(1));
6945 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6946 let sketch_object = &scene_delta.new_graph.objects[1];
6947 assert_eq!(sketch_object.id, ObjectId(1));
6948 assert_eq!(
6949 sketch_object.kind,
6950 ObjectKind::Sketch(Sketch {
6951 args: SketchCtor {
6952 on: Plane::Default(PlaneName::Xy)
6953 },
6954 plane: ObjectId(0),
6955 segments: vec![],
6956 constraints: vec![],
6957 })
6958 );
6959 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6960
6961 let line_ctor = LineCtor {
6962 start: Point2d {
6963 x: Expr::Number(Number {
6964 value: 0.0,
6965 units: NumericSuffix::Mm,
6966 }),
6967 y: Expr::Number(Number {
6968 value: 0.0,
6969 units: NumericSuffix::Mm,
6970 }),
6971 },
6972 end: Point2d {
6973 x: Expr::Number(Number {
6974 value: 10.0,
6975 units: NumericSuffix::Mm,
6976 }),
6977 y: Expr::Number(Number {
6978 value: 10.0,
6979 units: NumericSuffix::Mm,
6980 }),
6981 },
6982 construction: None,
6983 };
6984 let segment = SegmentCtor::Line(line_ctor);
6985 let (src_delta, scene_delta) = frontend
6986 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6987 .await
6988 .unwrap();
6989 assert_eq!(
6990 src_delta.text.as_str(),
6991 "sketch001 = sketch(on = XY) {
6992 line(start = [0mm, 0mm], end = [10mm, 10mm])
6993}
6994"
6995 );
6996 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6997
6998 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
6999 assert_eq!(src_delta.text.as_str(), "");
7000 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7001
7002 ctx.close().await;
7003 mock_ctx.close().await;
7004 }
7005
7006 #[tokio::test(flavor = "multi_thread")]
7007 async fn test_delete_sketch_when_sketch_block_uses_variable() {
7008 let initial_source = "s = sketch(on = XY) {}
7009";
7010
7011 let program = Program::parse(initial_source).unwrap().0.unwrap();
7012
7013 let mut frontend = FrontendState::new();
7014
7015 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7016 let mock_ctx = ExecutorContext::new_mock(None).await;
7017 let version = Version(0);
7018
7019 frontend.hack_set_program(&ctx, program).await.unwrap();
7020 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7021 let sketch_id = sketch_object.id;
7022
7023 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7024 assert_eq!(src_delta.text.as_str(), "");
7025 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7026
7027 ctx.close().await;
7028 mock_ctx.close().await;
7029 }
7030
7031 #[tokio::test(flavor = "multi_thread")]
7032 async fn test_delete_sketch_after_comment() {
7033 let initial_source = "sketch001 = sketch(on = XZ) {
7034}
7035";
7036
7037 let program = Program::parse(initial_source).unwrap().0.unwrap();
7038 let mut frontend = FrontendState::new();
7039
7040 let ctx = ExecutorContext::new_with_engine(
7041 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7042 Default::default(),
7043 );
7044 let version = Version(0);
7045
7046 frontend.hack_set_program(&ctx, program).await.unwrap();
7047 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7048 let sketch_id = sketch_object.id;
7049 let original_source = sketch_object.source.clone();
7050
7051 let commented_source = "// test 1
7052sketch001 = sketch(on = XZ) {
7053}
7054";
7055 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7056 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7057
7058 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7059 assert_eq!(cached_sketch_object.source, original_source);
7060
7061 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7062 assert!(
7063 !src_delta.text.contains("sketch001"),
7064 "sketch was not deleted: {}",
7065 src_delta.text
7066 );
7067 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7068
7069 ctx.close().await;
7070 }
7071
7072 #[tokio::test(flavor = "multi_thread")]
7073 async fn test_edit_line_when_editing_its_start_point() {
7074 let initial_source = "\
7075sketch(on = XY) {
7076 line(start = [var 1, var 2], end = [var 3, var 4])
7077}
7078";
7079
7080 let program = Program::parse(initial_source).unwrap().0.unwrap();
7081
7082 let mut frontend = FrontendState::new();
7083
7084 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7085 let mock_ctx = ExecutorContext::new_mock(None).await;
7086 let version = Version(0);
7087
7088 frontend.hack_set_program(&ctx, program).await.unwrap();
7089 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7090 let sketch_id = sketch_object.id;
7091 let sketch = expect_sketch(sketch_object);
7092
7093 let point_id = *sketch.segments.first().unwrap();
7094
7095 let point_ctor = PointCtor {
7096 position: Point2d {
7097 x: Expr::Var(Number {
7098 value: 5.0,
7099 units: NumericSuffix::Inch,
7100 }),
7101 y: Expr::Var(Number {
7102 value: 6.0,
7103 units: NumericSuffix::Inch,
7104 }),
7105 },
7106 };
7107 let segments = vec![ExistingSegmentCtor {
7108 id: point_id,
7109 ctor: SegmentCtor::Point(point_ctor),
7110 }];
7111 let (src_delta, scene_delta) = frontend
7112 .edit_segments(&mock_ctx, version, sketch_id, segments)
7113 .await
7114 .unwrap();
7115 assert_eq!(
7116 src_delta.text.as_str(),
7117 "\
7118sketch(on = XY) {
7119 line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7120}
7121"
7122 );
7123 assert_eq!(scene_delta.new_objects, vec![]);
7124 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7125
7126 ctx.close().await;
7127 mock_ctx.close().await;
7128 }
7129
7130 #[tokio::test(flavor = "multi_thread")]
7131 async fn test_edit_line_when_editing_its_end_point() {
7132 let initial_source = "\
7133sketch(on = XY) {
7134 line(start = [var 1, var 2], end = [var 3, var 4])
7135}
7136";
7137
7138 let program = Program::parse(initial_source).unwrap().0.unwrap();
7139
7140 let mut frontend = FrontendState::new();
7141
7142 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7143 let mock_ctx = ExecutorContext::new_mock(None).await;
7144 let version = Version(0);
7145
7146 frontend.hack_set_program(&ctx, program).await.unwrap();
7147 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7148 let sketch_id = sketch_object.id;
7149 let sketch = expect_sketch(sketch_object);
7150 let point_id = *sketch.segments.get(1).unwrap();
7151
7152 let point_ctor = PointCtor {
7153 position: Point2d {
7154 x: Expr::Var(Number {
7155 value: 5.0,
7156 units: NumericSuffix::Inch,
7157 }),
7158 y: Expr::Var(Number {
7159 value: 6.0,
7160 units: NumericSuffix::Inch,
7161 }),
7162 },
7163 };
7164 let segments = vec![ExistingSegmentCtor {
7165 id: point_id,
7166 ctor: SegmentCtor::Point(point_ctor),
7167 }];
7168 let (src_delta, scene_delta) = frontend
7169 .edit_segments(&mock_ctx, version, sketch_id, segments)
7170 .await
7171 .unwrap();
7172 assert_eq!(
7173 src_delta.text.as_str(),
7174 "\
7175sketch(on = XY) {
7176 line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
7177}
7178"
7179 );
7180 assert_eq!(scene_delta.new_objects, vec![]);
7181 assert_eq!(
7182 scene_delta.new_graph.objects.len(),
7183 5,
7184 "{:#?}",
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_edit_line_with_coincident_feedback() {
7194 let initial_source = "\
7195sketch(on = XY) {
7196 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
7197 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7198 fixed([line1.start, [0, 0]])
7199 coincident([line1.end, line2.start])
7200 equalLength([line1, line2])
7201}
7202";
7203
7204 let program = Program::parse(initial_source).unwrap().0.unwrap();
7205
7206 let mut frontend = FrontendState::new();
7207
7208 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7209 let mock_ctx = ExecutorContext::new_mock(None).await;
7210 let version = Version(0);
7211
7212 frontend.hack_set_program(&ctx, program).await.unwrap();
7213 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7214 let sketch_id = sketch_object.id;
7215 let sketch = expect_sketch(sketch_object);
7216 let line2_end_id = *sketch.segments.get(4).unwrap();
7217
7218 let segments = vec![ExistingSegmentCtor {
7219 id: line2_end_id,
7220 ctor: SegmentCtor::Point(PointCtor {
7221 position: Point2d {
7222 x: Expr::Var(Number {
7223 value: 9.0,
7224 units: NumericSuffix::None,
7225 }),
7226 y: Expr::Var(Number {
7227 value: 10.0,
7228 units: NumericSuffix::None,
7229 }),
7230 },
7231 }),
7232 }];
7233 let (src_delta, scene_delta) = frontend
7234 .edit_segments(&mock_ctx, version, sketch_id, segments)
7235 .await
7236 .unwrap();
7237 assert_eq!(
7238 src_delta.text.as_str(),
7239 "\
7240sketch(on = XY) {
7241 line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7242 line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7243 fixed([line1.start, [0, 0]])
7244 coincident([line1.end, line2.start])
7245 equalLength([line1, line2])
7246}
7247"
7248 );
7249 assert_eq!(
7250 scene_delta.new_graph.objects.len(),
7251 11,
7252 "{:#?}",
7253 scene_delta.new_graph.objects
7254 );
7255
7256 ctx.close().await;
7257 mock_ctx.close().await;
7258 }
7259
7260 #[tokio::test(flavor = "multi_thread")]
7261 async fn test_delete_point_without_var() {
7262 let initial_source = "\
7263sketch(on = XY) {
7264 point(at = [var 1, var 2])
7265 point(at = [var 3, var 4])
7266 point(at = [var 5, var 6])
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
7283 let point_id = *sketch.segments.get(1).unwrap();
7284
7285 let (src_delta, scene_delta) = frontend
7286 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7287 .await
7288 .unwrap();
7289 assert_eq!(
7290 src_delta.text.as_str(),
7291 "\
7292sketch(on = XY) {
7293 point(at = [var 1mm, var 2mm])
7294 point(at = [var 5mm, var 6mm])
7295}
7296"
7297 );
7298 assert_eq!(scene_delta.new_objects, vec![]);
7299 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7300
7301 ctx.close().await;
7302 mock_ctx.close().await;
7303 }
7304
7305 #[tokio::test(flavor = "multi_thread")]
7306 async fn test_delete_point_with_var() {
7307 let initial_source = "\
7308sketch(on = XY) {
7309 point(at = [var 1, var 2])
7310 point1 = point(at = [var 3, var 4])
7311 point(at = [var 5, var 6])
7312}
7313";
7314
7315 let program = Program::parse(initial_source).unwrap().0.unwrap();
7316
7317 let mut frontend = FrontendState::new();
7318
7319 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7320 let mock_ctx = ExecutorContext::new_mock(None).await;
7321 let version = Version(0);
7322
7323 frontend.hack_set_program(&ctx, program).await.unwrap();
7324 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7325 let sketch_id = sketch_object.id;
7326 let sketch = expect_sketch(sketch_object);
7327
7328 let point_id = *sketch.segments.get(1).unwrap();
7329
7330 let (src_delta, scene_delta) = frontend
7331 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7332 .await
7333 .unwrap();
7334 assert_eq!(
7335 src_delta.text.as_str(),
7336 "\
7337sketch(on = XY) {
7338 point(at = [var 1mm, var 2mm])
7339 point(at = [var 5mm, var 6mm])
7340}
7341"
7342 );
7343 assert_eq!(scene_delta.new_objects, vec![]);
7344 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7345
7346 ctx.close().await;
7347 mock_ctx.close().await;
7348 }
7349
7350 #[tokio::test(flavor = "multi_thread")]
7351 async fn test_delete_multiple_points() {
7352 let initial_source = "\
7353sketch(on = XY) {
7354 point(at = [var 1, var 2])
7355 point1 = point(at = [var 3, var 4])
7356 point(at = [var 5, var 6])
7357}
7358";
7359
7360 let program = Program::parse(initial_source).unwrap().0.unwrap();
7361
7362 let mut frontend = FrontendState::new();
7363
7364 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7365 let mock_ctx = ExecutorContext::new_mock(None).await;
7366 let version = Version(0);
7367
7368 frontend.hack_set_program(&ctx, program).await.unwrap();
7369 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7370 let sketch_id = sketch_object.id;
7371
7372 let sketch = expect_sketch(sketch_object);
7373
7374 let point1_id = *sketch.segments.first().unwrap();
7375 let point2_id = *sketch.segments.get(1).unwrap();
7376
7377 let (src_delta, scene_delta) = frontend
7378 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
7379 .await
7380 .unwrap();
7381 assert_eq!(
7382 src_delta.text.as_str(),
7383 "\
7384sketch(on = XY) {
7385 point(at = [var 5mm, var 6mm])
7386}
7387"
7388 );
7389 assert_eq!(scene_delta.new_objects, vec![]);
7390 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7391
7392 ctx.close().await;
7393 mock_ctx.close().await;
7394 }
7395
7396 #[tokio::test(flavor = "multi_thread")]
7397 async fn test_delete_coincident_constraint() {
7398 let initial_source = "\
7399sketch(on = XY) {
7400 point1 = point(at = [var 1, var 2])
7401 point2 = point(at = [var 3, var 4])
7402 coincident([point1, point2])
7403 point(at = [var 5, var 6])
7404}
7405";
7406
7407 let program = Program::parse(initial_source).unwrap().0.unwrap();
7408
7409 let mut frontend = FrontendState::new();
7410
7411 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7412 let mock_ctx = ExecutorContext::new_mock(None).await;
7413 let version = Version(0);
7414
7415 frontend.hack_set_program(&ctx, program).await.unwrap();
7416 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7417 let sketch_id = sketch_object.id;
7418 let sketch = expect_sketch(sketch_object);
7419
7420 let coincident_id = *sketch.constraints.first().unwrap();
7421
7422 let (src_delta, scene_delta) = frontend
7423 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7424 .await
7425 .unwrap();
7426 assert_eq!(
7427 src_delta.text.as_str(),
7428 "\
7429sketch(on = XY) {
7430 point1 = point(at = [var 1mm, var 2mm])
7431 point2 = point(at = [var 3mm, var 4mm])
7432 point(at = [var 5mm, var 6mm])
7433}
7434"
7435 );
7436 assert_eq!(scene_delta.new_objects, vec![]);
7437 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7438
7439 ctx.close().await;
7440 mock_ctx.close().await;
7441 }
7442
7443 #[tokio::test(flavor = "multi_thread")]
7444 async fn test_delete_line_cascades_to_coincident_constraint() {
7445 let initial_source = "\
7446sketch(on = XY) {
7447 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7448 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7449 coincident([line1.end, line2.start])
7450}
7451";
7452
7453 let program = Program::parse(initial_source).unwrap().0.unwrap();
7454
7455 let mut frontend = FrontendState::new();
7456
7457 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7458 let mock_ctx = ExecutorContext::new_mock(None).await;
7459 let version = Version(0);
7460
7461 frontend.hack_set_program(&ctx, program).await.unwrap();
7462 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7463 let sketch_id = sketch_object.id;
7464 let sketch = expect_sketch(sketch_object);
7465 let line_id = *sketch.segments.get(5).unwrap();
7466
7467 let (src_delta, scene_delta) = frontend
7468 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7469 .await
7470 .unwrap();
7471 assert_eq!(
7472 src_delta.text.as_str(),
7473 "\
7474sketch(on = XY) {
7475 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7476}
7477"
7478 );
7479 assert_eq!(
7480 scene_delta.new_graph.objects.len(),
7481 5,
7482 "{:#?}",
7483 scene_delta.new_graph.objects
7484 );
7485
7486 ctx.close().await;
7487 mock_ctx.close().await;
7488 }
7489
7490 #[tokio::test(flavor = "multi_thread")]
7491 async fn test_delete_line_cascades_to_distance_constraint() {
7492 let initial_source = "\
7493sketch(on = XY) {
7494 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7495 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7496 distance([line1.end, line2.start]) == 10mm
7497}
7498";
7499
7500 let program = Program::parse(initial_source).unwrap().0.unwrap();
7501
7502 let mut frontend = FrontendState::new();
7503
7504 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7505 let mock_ctx = ExecutorContext::new_mock(None).await;
7506 let version = Version(0);
7507
7508 frontend.hack_set_program(&ctx, program).await.unwrap();
7509 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7510 let sketch_id = sketch_object.id;
7511 let sketch = expect_sketch(sketch_object);
7512 let line_id = *sketch.segments.get(5).unwrap();
7513
7514 let (src_delta, scene_delta) = frontend
7515 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7516 .await
7517 .unwrap();
7518 assert_eq!(
7519 src_delta.text.as_str(),
7520 "\
7521sketch(on = XY) {
7522 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7523}
7524"
7525 );
7526 assert_eq!(
7527 scene_delta.new_graph.objects.len(),
7528 5,
7529 "{:#?}",
7530 scene_delta.new_graph.objects
7531 );
7532
7533 ctx.close().await;
7534 mock_ctx.close().await;
7535 }
7536
7537 #[tokio::test(flavor = "multi_thread")]
7538 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
7539 let initial_source = "\
7540sketch(on = XY) {
7541 point1 = point(at = [var 1, var 2])
7542 point2 = point(at = [var 3, var 4])
7543 horizontalDistance([point1, point2]) == 10mm
7544}
7545";
7546
7547 let program = Program::parse(initial_source).unwrap().0.unwrap();
7548
7549 let mut frontend = FrontendState::new();
7550
7551 let mock_ctx = ExecutorContext::new_mock(None).await;
7552 let version = Version(0);
7553
7554 frontend.program = program.clone();
7555 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7556 frontend.update_state_after_exec(outcome, true);
7557 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7558 let sketch_id = sketch_object.id;
7559 let sketch = expect_sketch(sketch_object);
7560 let point2_id = *sketch.segments.get(1).unwrap();
7561
7562 let (src_delta, scene_delta) = frontend
7563 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
7564 .await
7565 .unwrap();
7566 assert_eq!(
7567 src_delta.text.as_str(),
7568 "\
7569sketch(on = XY) {
7570 point1 = point(at = [var 1mm, var 2mm])
7571}
7572"
7573 );
7574 assert_eq!(
7575 scene_delta.new_graph.objects.len(),
7576 3,
7577 "{:#?}",
7578 scene_delta.new_graph.objects
7579 );
7580
7581 mock_ctx.close().await;
7582 }
7583
7584 #[tokio::test(flavor = "multi_thread")]
7585 async fn test_delete_line_cascades_to_fixed_constraint() {
7586 let initial_source = "\
7587sketch(on = XY) {
7588 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7589 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7590 fixed([line1.start, [0, 0]])
7591}
7592";
7593
7594 let program = Program::parse(initial_source).unwrap().0.unwrap();
7595
7596 let mut frontend = FrontendState::new();
7597
7598 let mock_ctx = ExecutorContext::new_mock(None).await;
7599 let version = Version(0);
7600
7601 frontend.program = program.clone();
7602 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7603 frontend.update_state_after_exec(outcome, true);
7604 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7605 let sketch_id = sketch_object.id;
7606 let sketch = expect_sketch(sketch_object);
7607 let line1_id = *sketch.segments.get(2).unwrap();
7608
7609 let (src_delta, scene_delta) = frontend
7610 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7611 .await
7612 .unwrap();
7613 assert_eq!(
7614 src_delta.text.as_str(),
7615 "\
7616sketch(on = XY) {
7617 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7618}
7619"
7620 );
7621 assert_eq!(
7622 scene_delta.new_graph.objects.len(),
7623 5,
7624 "{:#?}",
7625 scene_delta.new_graph.objects
7626 );
7627
7628 mock_ctx.close().await;
7629 }
7630
7631 #[tokio::test(flavor = "multi_thread")]
7632 async fn test_delete_line_cascades_to_midpoint_constraint() {
7633 let initial_source = "\
7634sketch(on = XY) {
7635 point1 = point(at = [var 1, var 2])
7636 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
7637 midpoint(line1, point = point1)
7638}
7639";
7640
7641 let program = Program::parse(initial_source).unwrap().0.unwrap();
7642
7643 let mut frontend = FrontendState::new();
7644
7645 let mock_ctx = ExecutorContext::new_mock(None).await;
7646 let version = Version(0);
7647
7648 frontend.program = program.clone();
7649 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7650 frontend.update_state_after_exec(outcome, true);
7651 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7652 let sketch_id = sketch_object.id;
7653 let sketch = expect_sketch(sketch_object);
7654 let line1_id = *sketch.segments.get(3).unwrap();
7655
7656 let (src_delta, scene_delta) = frontend
7657 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7658 .await
7659 .unwrap();
7660 assert_eq!(
7661 src_delta.text.as_str(),
7662 "\
7663sketch(on = XY) {
7664 point1 = point(at = [var 1mm, var 2mm])
7665}
7666"
7667 );
7668 assert_eq!(
7669 scene_delta.new_graph.objects.len(),
7670 3,
7671 "{:#?}",
7672 scene_delta.new_graph.objects
7673 );
7674
7675 mock_ctx.close().await;
7676 }
7677
7678 #[tokio::test(flavor = "multi_thread")]
7679 async fn test_delete_point_preserves_multiline_coincident_constraint() {
7680 let initial_source = "\
7681sketch(on = XY) {
7682 point1 = point(at = [var 1, var 2])
7683 point2 = point(at = [var 3, var 4])
7684 point3 = point(at = [var 5, var 6])
7685 coincident([point1, point2, point3])
7686}
7687";
7688
7689 let program = Program::parse(initial_source).unwrap().0.unwrap();
7690
7691 let mut frontend = FrontendState::new();
7692
7693 let mock_ctx = ExecutorContext::new_mock(None).await;
7694 let version = Version(0);
7695
7696 frontend.program = program.clone();
7697 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7698 frontend.update_state_after_exec(outcome, true);
7699 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7700 let sketch_id = sketch_object.id;
7701 let sketch = expect_sketch(sketch_object);
7702 let point3_id = *sketch.segments.get(2).unwrap();
7703
7704 let (src_delta, scene_delta) = frontend
7705 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
7706 .await
7707 .unwrap();
7708 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
7709 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
7710 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
7711 assert!(
7712 src_delta.text.contains("coincident([point1, point2])"),
7713 "{}",
7714 src_delta.text
7715 );
7716
7717 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7718 let sketch = expect_sketch(sketch_object);
7719 assert_eq!(sketch.segments.len(), 2);
7720 assert_eq!(sketch.constraints.len(), 1);
7721
7722 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7723 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7724 panic!("Expected constraint object");
7725 };
7726 let Constraint::Coincident(coincident) = constraint else {
7727 panic!("Expected coincident constraint");
7728 };
7729 assert_eq!(
7730 coincident.segments,
7731 sketch
7732 .segments
7733 .iter()
7734 .copied()
7735 .map(Into::into)
7736 .collect::<Vec<ConstraintSegment>>()
7737 );
7738
7739 mock_ctx.close().await;
7740 }
7741
7742 #[tokio::test(flavor = "multi_thread")]
7743 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
7744 let initial_source = "\
7745sketch(on = XY) {
7746 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7747 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7748 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7749 equalLength([line1, line2, line3])
7750}
7751";
7752
7753 let program = Program::parse(initial_source).unwrap().0.unwrap();
7754
7755 let mut frontend = FrontendState::new();
7756
7757 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7758 let mock_ctx = ExecutorContext::new_mock(None).await;
7759 let version = Version(0);
7760
7761 frontend.hack_set_program(&ctx, program).await.unwrap();
7762 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7763 let sketch_id = sketch_object.id;
7764 let sketch = expect_sketch(sketch_object);
7765 let line3_id = *sketch.segments.get(8).unwrap();
7766
7767 let (src_delta, scene_delta) = frontend
7768 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
7769 .await
7770 .unwrap();
7771 assert_eq!(
7772 src_delta.text.as_str(),
7773 "\
7774sketch(on = XY) {
7775 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7776 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7777 equalLength([line1, line2])
7778}
7779"
7780 );
7781
7782 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7783 let sketch = expect_sketch(sketch_object);
7784 assert_eq!(sketch.constraints.len(), 1);
7785
7786 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7787 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7788 panic!("Expected constraint object");
7789 };
7790 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
7791 panic!("Expected lines equal length constraint");
7792 };
7793 assert_eq!(lines_equal_length.lines.len(), 2);
7794
7795 ctx.close().await;
7796 mock_ctx.close().await;
7797 }
7798
7799 #[tokio::test(flavor = "multi_thread")]
7800 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
7801 let initial_source = "\
7802sketch(on = XY) {
7803 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7804 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7805 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7806 horizontal([line1.end, line2.start, line3.start])
7807}
7808";
7809
7810 let program = Program::parse(initial_source).unwrap().0.unwrap();
7811
7812 let mut frontend = FrontendState::new();
7813
7814 let mock_ctx = ExecutorContext::new_mock(None).await;
7815 let version = Version(0);
7816
7817 frontend.program = program.clone();
7818 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7819 frontend.update_state_after_exec(outcome, true);
7820 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7821 let sketch_id = sketch_object.id;
7822 let sketch = expect_sketch(sketch_object);
7823 let line1_id = *sketch.segments.get(2).unwrap();
7824
7825 let (src_delta, scene_delta) = frontend
7826 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7827 .await
7828 .unwrap();
7829 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
7830 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
7831 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
7832 assert!(
7833 src_delta.text.contains("horizontal([line2.start, line3.start])"),
7834 "{}",
7835 src_delta.text
7836 );
7837
7838 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7839 let sketch = expect_sketch(sketch_object);
7840 assert_eq!(sketch.constraints.len(), 1);
7841
7842 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7843 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7844 panic!("Expected constraint object");
7845 };
7846 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
7847 panic!("Expected horizontal points constraint");
7848 };
7849 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
7850 assert_eq!(*points, remaining_points);
7851
7852 mock_ctx.close().await;
7853 }
7854
7855 #[tokio::test(flavor = "multi_thread")]
7856 async fn test_delete_line_preserves_multiline_vertical_constraint() {
7857 let initial_source = "\
7858sketch(on = XY) {
7859 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7860 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7861 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7862 vertical([line1.end, line2.start, line3.start])
7863}
7864";
7865
7866 let program = Program::parse(initial_source).unwrap().0.unwrap();
7867
7868 let mut frontend = FrontendState::new();
7869
7870 let mock_ctx = ExecutorContext::new_mock(None).await;
7871 let version = Version(0);
7872
7873 frontend.program = program.clone();
7874 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7875 frontend.update_state_after_exec(outcome, true);
7876 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7877 let sketch_id = sketch_object.id;
7878 let sketch = expect_sketch(sketch_object);
7879 let line1_id = *sketch.segments.get(2).unwrap();
7880
7881 let (src_delta, scene_delta) = frontend
7882 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7883 .await
7884 .unwrap();
7885 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
7886 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
7887 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
7888 assert!(
7889 src_delta.text.contains("vertical([line2.start, line3.start])"),
7890 "{}",
7891 src_delta.text
7892 );
7893
7894 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7895 let sketch = expect_sketch(sketch_object);
7896 assert_eq!(sketch.constraints.len(), 1);
7897
7898 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7899 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7900 panic!("Expected constraint object");
7901 };
7902 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
7903 panic!("Expected vertical points constraint");
7904 };
7905 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
7906 assert_eq!(*points, remaining_points);
7907
7908 mock_ctx.close().await;
7909 }
7910
7911 #[tokio::test(flavor = "multi_thread")]
7912 async fn test_delete_line_preserves_multiline_coincident_constraint() {
7913 let initial_source = "\
7914sketch(on = XY) {
7915 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7916 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7917 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7918 coincident([line1.end, line2.start, line3.start])
7919}
7920";
7921
7922 let program = Program::parse(initial_source).unwrap().0.unwrap();
7923
7924 let mut frontend = FrontendState::new();
7925
7926 let mock_ctx = ExecutorContext::new_mock(None).await;
7927 let version = Version(0);
7928
7929 frontend.program = program.clone();
7930 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7931 frontend.update_state_after_exec(outcome, true);
7932 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7933 let sketch_id = sketch_object.id;
7934 let sketch = expect_sketch(sketch_object);
7935 let line1_id = *sketch.segments.get(2).unwrap();
7936
7937 let (src_delta, scene_delta) = frontend
7938 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7939 .await
7940 .unwrap();
7941 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
7942 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
7943 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
7944 assert!(
7945 src_delta.text.contains("coincident([line2.start, line3.start])"),
7946 "{}",
7947 src_delta.text
7948 );
7949
7950 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7951 let sketch = expect_sketch(sketch_object);
7952 assert_eq!(sketch.constraints.len(), 1);
7953
7954 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7955 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7956 panic!("Expected constraint object");
7957 };
7958 let Constraint::Coincident(coincident) = constraint else {
7959 panic!("Expected coincident constraint");
7960 };
7961 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
7962 assert_eq!(coincident.segments, remaining_segments);
7963
7964 mock_ctx.close().await;
7965 }
7966
7967 #[tokio::test(flavor = "multi_thread")]
7968 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
7969 let initial_source = "\
7970sketch(on = XY) {
7971 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7972 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7973 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7974 equalLength([line1, line2, line3])
7975}
7976";
7977
7978 let program = Program::parse(initial_source).unwrap().0.unwrap();
7979
7980 let mut frontend = FrontendState::new();
7981
7982 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7983 let mock_ctx = ExecutorContext::new_mock(None).await;
7984 let version = Version(0);
7985
7986 frontend.hack_set_program(&ctx, program).await.unwrap();
7987 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7988 let sketch_id = sketch_object.id;
7989 let sketch = expect_sketch(sketch_object);
7990 let line2_id = *sketch.segments.get(5).unwrap();
7991 let line3_id = *sketch.segments.get(8).unwrap();
7992
7993 let (src_delta, scene_delta) = frontend
7994 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
7995 .await
7996 .unwrap();
7997 assert_eq!(
7998 src_delta.text.as_str(),
7999 "\
8000sketch(on = XY) {
8001 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8002}
8003"
8004 );
8005
8006 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8007 let sketch = expect_sketch(sketch_object);
8008 assert!(sketch.constraints.is_empty());
8009
8010 ctx.close().await;
8011 mock_ctx.close().await;
8012 }
8013
8014 #[tokio::test(flavor = "multi_thread")]
8015 async fn test_delete_line_preserves_multiline_parallel_constraint() {
8016 let initial_source = "\
8017sketch(on = XY) {
8018 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8019 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8020 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8021 parallel([line1, line2, line3])
8022}
8023";
8024
8025 let program = Program::parse(initial_source).unwrap().0.unwrap();
8026
8027 let mut frontend = FrontendState::new();
8028
8029 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8030 let mock_ctx = ExecutorContext::new_mock(None).await;
8031 let version = Version(0);
8032
8033 frontend.hack_set_program(&ctx, program).await.unwrap();
8034 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8035 let sketch_id = sketch_object.id;
8036 let sketch = expect_sketch(sketch_object);
8037 let line3_id = *sketch.segments.get(8).unwrap();
8038
8039 let (src_delta, scene_delta) = frontend
8040 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8041 .await
8042 .unwrap();
8043 assert_eq!(
8044 src_delta.text.as_str(),
8045 "\
8046sketch(on = XY) {
8047 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8048 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8049 parallel([line1, line2])
8050}
8051"
8052 );
8053
8054 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8055 let sketch = expect_sketch(sketch_object);
8056 assert_eq!(sketch.constraints.len(), 1);
8057
8058 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8059 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8060 panic!("Expected constraint object");
8061 };
8062 let Constraint::Parallel(parallel) = constraint else {
8063 panic!("Expected parallel constraint");
8064 };
8065 assert_eq!(parallel.lines.len(), 2);
8066
8067 ctx.close().await;
8068 mock_ctx.close().await;
8069 }
8070
8071 #[tokio::test(flavor = "multi_thread")]
8072 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8073 let initial_source = "\
8074sketch(on = XY) {
8075 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8076 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8077 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8078 parallel([line1, line2, line3])
8079}
8080";
8081
8082 let program = Program::parse(initial_source).unwrap().0.unwrap();
8083
8084 let mut frontend = FrontendState::new();
8085
8086 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8087 let mock_ctx = ExecutorContext::new_mock(None).await;
8088 let version = Version(0);
8089
8090 frontend.hack_set_program(&ctx, program).await.unwrap();
8091 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8092 let sketch_id = sketch_object.id;
8093 let sketch = expect_sketch(sketch_object);
8094 let line2_id = *sketch.segments.get(5).unwrap();
8095 let line3_id = *sketch.segments.get(8).unwrap();
8096
8097 let (src_delta, scene_delta) = frontend
8098 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8099 .await
8100 .unwrap();
8101 assert_eq!(
8102 src_delta.text.as_str(),
8103 "\
8104sketch(on = XY) {
8105 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8106}
8107"
8108 );
8109
8110 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8111 let sketch = expect_sketch(sketch_object);
8112 assert!(sketch.constraints.is_empty());
8113
8114 ctx.close().await;
8115 mock_ctx.close().await;
8116 }
8117
8118 #[tokio::test(flavor = "multi_thread")]
8119 async fn test_delete_line_line_coincident_constraint() {
8120 let initial_source = "\
8121sketch(on = XY) {
8122 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8123 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8124 coincident([line1, line2])
8125}
8126";
8127
8128 let program = Program::parse(initial_source).unwrap().0.unwrap();
8129
8130 let mut frontend = FrontendState::new();
8131
8132 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8133 let mock_ctx = ExecutorContext::new_mock(None).await;
8134 let version = Version(0);
8135
8136 frontend.hack_set_program(&ctx, program).await.unwrap();
8137 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8138 let sketch_id = sketch_object.id;
8139 let sketch = expect_sketch(sketch_object);
8140
8141 let coincident_id = *sketch.constraints.first().unwrap();
8142
8143 let (src_delta, scene_delta) = frontend
8144 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8145 .await
8146 .unwrap();
8147 assert_eq!(
8148 src_delta.text.as_str(),
8149 "\
8150sketch(on = XY) {
8151 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8152 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8153}
8154"
8155 );
8156 assert_eq!(scene_delta.new_objects, vec![]);
8157 assert_eq!(scene_delta.new_graph.objects.len(), 8);
8158
8159 ctx.close().await;
8160 mock_ctx.close().await;
8161 }
8162
8163 #[tokio::test(flavor = "multi_thread")]
8164 async fn test_two_points_coincident() {
8165 let initial_source = "\
8166sketch(on = XY) {
8167 point1 = point(at = [var 1, var 2])
8168 point(at = [3, 4])
8169}
8170";
8171
8172 let program = Program::parse(initial_source).unwrap().0.unwrap();
8173
8174 let mut frontend = FrontendState::new();
8175
8176 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8177 let mock_ctx = ExecutorContext::new_mock(None).await;
8178 let version = Version(0);
8179
8180 frontend.hack_set_program(&ctx, program).await.unwrap();
8181 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8182 let sketch_id = sketch_object.id;
8183 let sketch = expect_sketch(sketch_object);
8184 let point0_id = *sketch.segments.first().unwrap();
8185 let point1_id = *sketch.segments.get(1).unwrap();
8186
8187 let constraint = Constraint::Coincident(Coincident {
8188 segments: vec![point0_id.into(), point1_id.into()],
8189 });
8190 let (src_delta, scene_delta) = frontend
8191 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8192 .await
8193 .unwrap();
8194 assert_eq!(
8195 src_delta.text.as_str(),
8196 "\
8197sketch(on = XY) {
8198 point1 = point(at = [var 1, var 2])
8199 point2 = point(at = [3, 4])
8200 coincident([point1, point2])
8201}
8202"
8203 );
8204 assert_eq!(
8205 scene_delta.new_graph.objects.len(),
8206 5,
8207 "{:#?}",
8208 scene_delta.new_graph.objects
8209 );
8210
8211 ctx.close().await;
8212 mock_ctx.close().await;
8213 }
8214
8215 #[tokio::test(flavor = "multi_thread")]
8216 async fn test_three_points_coincident() {
8217 let initial_source = "\
8218sketch(on = XY) {
8219 point1 = point(at = [var 1, var 2])
8220 point(at = [var 3, var 4])
8221 point(at = [var 5, var 6])
8222}
8223";
8224
8225 let program = Program::parse(initial_source).unwrap().0.unwrap();
8226
8227 let mut frontend = FrontendState::new();
8228
8229 let mock_ctx = ExecutorContext::new_mock(None).await;
8230 let version = Version(0);
8231
8232 frontend.program = program.clone();
8233 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8234 frontend.update_state_after_exec(outcome, true);
8235 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8236 let sketch_id = sketch_object.id;
8237 let sketch = expect_sketch(sketch_object);
8238 let segments = sketch
8239 .segments
8240 .iter()
8241 .take(3)
8242 .copied()
8243 .map(Into::into)
8244 .collect::<Vec<ConstraintSegment>>();
8245
8246 let constraint = Constraint::Coincident(Coincident {
8247 segments: segments.clone(),
8248 });
8249 let (src_delta, scene_delta) = frontend
8250 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8251 .await
8252 .unwrap();
8253 assert_eq!(
8254 src_delta.text.as_str(),
8255 "\
8256sketch(on = XY) {
8257 point1 = point(at = [var 1, var 2])
8258 point2 = point(at = [var 3, var 4])
8259 point3 = point(at = [var 5, var 6])
8260 coincident([point1, point2, point3])
8261}
8262"
8263 );
8264
8265 let constraint_object = scene_delta
8266 .new_graph
8267 .objects
8268 .iter()
8269 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8270 .unwrap();
8271
8272 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8273 panic!("expected a constraint object");
8274 };
8275
8276 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8277
8278 mock_ctx.close().await;
8279 }
8280
8281 #[tokio::test(flavor = "multi_thread")]
8282 async fn test_source_with_three_point_coincident_tracks_all_segments() {
8283 let initial_source = "\
8284sketch(on = XY) {
8285 point1 = point(at = [var 1, var 2])
8286 point2 = point(at = [var 3, var 4])
8287 point3 = point(at = [var 5, var 6])
8288 coincident([point1, point2, point3])
8289}
8290";
8291
8292 let program = Program::parse(initial_source).unwrap().0.unwrap();
8293
8294 let mut frontend = FrontendState::new();
8295
8296 let ctx = ExecutorContext::new_mock(None).await;
8297 frontend.program = program.clone();
8298 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8299 frontend.update_state_after_exec(outcome, true);
8300
8301 let constraint_object = frontend
8302 .scene_graph
8303 .objects
8304 .iter()
8305 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8306 .unwrap();
8307 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8308 panic!("expected a constraint object");
8309 };
8310
8311 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8312 let sketch = expect_sketch(sketch_object);
8313 let expected_segments = sketch
8314 .segments
8315 .iter()
8316 .take(3)
8317 .copied()
8318 .map(Into::into)
8319 .collect::<Vec<ConstraintSegment>>();
8320
8321 assert_eq!(
8322 constraint,
8323 &Constraint::Coincident(Coincident {
8324 segments: expected_segments,
8325 })
8326 );
8327
8328 ctx.close().await;
8329 }
8330
8331 #[tokio::test(flavor = "multi_thread")]
8332 async fn test_point_origin_coincident_preserves_order() {
8333 let initial_source = "\
8334sketch(on = XY) {
8335 point(at = [var 1, var 2])
8336}
8337";
8338
8339 for (origin_first, expected_source) in [
8340 (
8341 true,
8342 "\
8343sketch(on = XY) {
8344 point1 = point(at = [var 1, var 2])
8345 coincident([ORIGIN, point1])
8346}
8347",
8348 ),
8349 (
8350 false,
8351 "\
8352sketch(on = XY) {
8353 point1 = point(at = [var 1, var 2])
8354 coincident([point1, ORIGIN])
8355}
8356",
8357 ),
8358 ] {
8359 let program = Program::parse(initial_source).unwrap().0.unwrap();
8360
8361 let mut frontend = FrontendState::new();
8362
8363 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8364 let mock_ctx = ExecutorContext::new_mock(None).await;
8365 let version = Version(0);
8366
8367 frontend.hack_set_program(&ctx, program).await.unwrap();
8368 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8369 let sketch_id = sketch_object.id;
8370 let sketch = expect_sketch(sketch_object);
8371 let point_id = *sketch.segments.first().unwrap();
8372
8373 let segments = if origin_first {
8374 vec![ConstraintSegment::ORIGIN, point_id.into()]
8375 } else {
8376 vec![point_id.into(), ConstraintSegment::ORIGIN]
8377 };
8378 let constraint = Constraint::Coincident(Coincident {
8379 segments: segments.clone(),
8380 });
8381 let (src_delta, scene_delta) = frontend
8382 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8383 .await
8384 .unwrap();
8385 assert_eq!(src_delta.text.as_str(), expected_source);
8386
8387 let constraint_object = scene_delta
8388 .new_graph
8389 .objects
8390 .iter()
8391 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8392 .unwrap();
8393
8394 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8395 panic!("expected a constraint object");
8396 };
8397
8398 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8399
8400 ctx.close().await;
8401 mock_ctx.close().await;
8402 }
8403 }
8404
8405 #[tokio::test(flavor = "multi_thread")]
8406 async fn test_coincident_of_line_end_points() {
8407 let initial_source = "\
8408sketch(on = XY) {
8409 line(start = [var 1, var 2], end = [var 3, var 4])
8410 line(start = [var 5, var 6], end = [var 7, var 8])
8411}
8412";
8413
8414 let program = Program::parse(initial_source).unwrap().0.unwrap();
8415
8416 let mut frontend = FrontendState::new();
8417
8418 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8419 let mock_ctx = ExecutorContext::new_mock(None).await;
8420 let version = Version(0);
8421
8422 frontend.hack_set_program(&ctx, program).await.unwrap();
8423 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8424 let sketch_id = sketch_object.id;
8425 let sketch = expect_sketch(sketch_object);
8426 let point0_id = *sketch.segments.get(1).unwrap();
8427 let point1_id = *sketch.segments.get(3).unwrap();
8428
8429 let constraint = Constraint::Coincident(Coincident {
8430 segments: vec![point0_id.into(), point1_id.into()],
8431 });
8432 let (src_delta, scene_delta) = frontend
8433 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8434 .await
8435 .unwrap();
8436 assert_eq!(
8437 src_delta.text.as_str(),
8438 "\
8439sketch(on = XY) {
8440 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8441 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8442 coincident([line1.end, line2.start])
8443}
8444"
8445 );
8446 assert_eq!(
8447 scene_delta.new_graph.objects.len(),
8448 9,
8449 "{:#?}",
8450 scene_delta.new_graph.objects
8451 );
8452
8453 ctx.close().await;
8454 mock_ctx.close().await;
8455 }
8456
8457 #[tokio::test(flavor = "multi_thread")]
8458 async fn test_coincident_of_line_point_and_circle_segment() {
8459 let initial_source = "\
8460sketch(on = XY) {
8461 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8462 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8463}
8464";
8465 let program = Program::parse(initial_source).unwrap().0.unwrap();
8466 let mut frontend = FrontendState::new();
8467
8468 let mock_ctx = ExecutorContext::new_mock(None).await;
8469 let version = Version(0);
8470
8471 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8472 frontend.program = program;
8473 frontend.update_state_after_exec(outcome, true);
8474 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
8475 let sketch_id = sketch_object.id;
8476 let sketch = expect_sketch(sketch_object);
8477
8478 let circle_id = sketch
8479 .segments
8480 .iter()
8481 .copied()
8482 .find(|seg_id| {
8483 matches!(
8484 &frontend.scene_graph.objects[seg_id.0].kind,
8485 ObjectKind::Segment {
8486 segment: Segment::Circle(_)
8487 }
8488 )
8489 })
8490 .expect("Expected a circle segment in sketch");
8491 let line_id = sketch
8492 .segments
8493 .iter()
8494 .copied()
8495 .find(|seg_id| {
8496 matches!(
8497 &frontend.scene_graph.objects[seg_id.0].kind,
8498 ObjectKind::Segment {
8499 segment: Segment::Line(_)
8500 }
8501 )
8502 })
8503 .expect("Expected a line segment in sketch");
8504
8505 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
8506 ObjectKind::Segment {
8507 segment: Segment::Line(line),
8508 } => line.start,
8509 _ => panic!("Expected line segment object"),
8510 };
8511
8512 let constraint = Constraint::Coincident(Coincident {
8513 segments: vec![line_start_point_id.into(), circle_id.into()],
8514 });
8515 let (src_delta, _scene_delta) = frontend
8516 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8517 .await
8518 .unwrap();
8519 assert_eq!(
8520 src_delta.text.as_str(),
8521 "\
8522sketch(on = XY) {
8523 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8524 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8525 coincident([line1.start, circle1])
8526}
8527"
8528 );
8529
8530 mock_ctx.close().await;
8531 }
8532
8533 #[tokio::test(flavor = "multi_thread")]
8534 async fn test_invalid_coincident_arc_and_line_preserves_state() {
8535 let program = Program::empty();
8543
8544 let mut frontend = FrontendState::new();
8545 frontend.program = program;
8546
8547 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8548 let mock_ctx = ExecutorContext::new_mock(None).await;
8549 let version = Version(0);
8550
8551 let sketch_args = SketchCtor {
8552 on: Plane::Default(PlaneName::Xy),
8553 };
8554 let (_src_delta, _scene_delta, sketch_id) = frontend
8555 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8556 .await
8557 .unwrap();
8558
8559 let arc_ctor = ArcCtor {
8561 start: Point2d {
8562 x: Expr::Var(Number {
8563 value: 0.0,
8564 units: NumericSuffix::Mm,
8565 }),
8566 y: Expr::Var(Number {
8567 value: 0.0,
8568 units: NumericSuffix::Mm,
8569 }),
8570 },
8571 end: Point2d {
8572 x: Expr::Var(Number {
8573 value: 10.0,
8574 units: NumericSuffix::Mm,
8575 }),
8576 y: Expr::Var(Number {
8577 value: 10.0,
8578 units: NumericSuffix::Mm,
8579 }),
8580 },
8581 center: Point2d {
8582 x: Expr::Var(Number {
8583 value: 10.0,
8584 units: NumericSuffix::Mm,
8585 }),
8586 y: Expr::Var(Number {
8587 value: 0.0,
8588 units: NumericSuffix::Mm,
8589 }),
8590 },
8591 construction: None,
8592 };
8593 let (_src_delta, scene_delta) = frontend
8594 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
8595 .await
8596 .unwrap();
8597 let arc_id = *scene_delta.new_objects.last().unwrap();
8599
8600 let line_ctor = LineCtor {
8602 start: Point2d {
8603 x: Expr::Var(Number {
8604 value: 20.0,
8605 units: NumericSuffix::Mm,
8606 }),
8607 y: Expr::Var(Number {
8608 value: 0.0,
8609 units: NumericSuffix::Mm,
8610 }),
8611 },
8612 end: Point2d {
8613 x: Expr::Var(Number {
8614 value: 30.0,
8615 units: NumericSuffix::Mm,
8616 }),
8617 y: Expr::Var(Number {
8618 value: 10.0,
8619 units: NumericSuffix::Mm,
8620 }),
8621 },
8622 construction: None,
8623 };
8624 let (_src_delta, scene_delta) = frontend
8625 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
8626 .await
8627 .unwrap();
8628 let line_id = *scene_delta.new_objects.last().unwrap();
8630
8631 let constraint = Constraint::Coincident(Coincident {
8634 segments: vec![arc_id.into(), line_id.into()],
8635 });
8636 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
8637
8638 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
8640
8641 let sketch_object_after =
8644 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
8645 let sketch_after = expect_sketch(sketch_object_after);
8646
8647 assert!(
8649 sketch_after.segments.contains(&arc_id),
8650 "Arc segment should still exist after failed constraint"
8651 );
8652 assert!(
8653 sketch_after.segments.contains(&line_id),
8654 "Line segment should still exist after failed constraint"
8655 );
8656
8657 let arc_obj = frontend
8659 .scene_graph
8660 .objects
8661 .get(arc_id.0)
8662 .expect("Arc object should still be accessible");
8663 let line_obj = frontend
8664 .scene_graph
8665 .objects
8666 .get(line_id.0)
8667 .expect("Line object should still be accessible");
8668
8669 match &arc_obj.kind {
8672 ObjectKind::Segment {
8673 segment: Segment::Arc(_),
8674 } => {}
8675 _ => panic!("Arc object should still be an arc segment"),
8676 }
8677 match &line_obj.kind {
8678 ObjectKind::Segment {
8679 segment: Segment::Line(_),
8680 } => {}
8681 _ => panic!("Line object should still be a line segment"),
8682 }
8683
8684 ctx.close().await;
8685 mock_ctx.close().await;
8686 }
8687
8688 #[tokio::test(flavor = "multi_thread")]
8689 async fn test_distance_two_points() {
8690 let initial_source = "\
8691sketch(on = XY) {
8692 point(at = [var 1, var 2])
8693 point(at = [var 3, var 4])
8694}
8695";
8696
8697 let program = Program::parse(initial_source).unwrap().0.unwrap();
8698
8699 let mut frontend = FrontendState::new();
8700
8701 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8702 let mock_ctx = ExecutorContext::new_mock(None).await;
8703 let version = Version(0);
8704
8705 frontend.hack_set_program(&ctx, program).await.unwrap();
8706 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8707 let sketch_id = sketch_object.id;
8708 let sketch = expect_sketch(sketch_object);
8709 let point0_id = *sketch.segments.first().unwrap();
8710 let point1_id = *sketch.segments.get(1).unwrap();
8711
8712 let constraint = Constraint::Distance(Distance {
8713 points: vec![point0_id.into(), point1_id.into()],
8714 distance: Number {
8715 value: 2.0,
8716 units: NumericSuffix::Mm,
8717 },
8718 source: Default::default(),
8719 });
8720 let (src_delta, scene_delta) = frontend
8721 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8722 .await
8723 .unwrap();
8724 assert_eq!(
8725 src_delta.text.as_str(),
8726 "\
8728sketch(on = XY) {
8729 point1 = point(at = [var 1, var 2])
8730 point2 = point(at = [var 3, var 4])
8731 distance([point1, point2]) == 2mm
8732}
8733"
8734 );
8735 assert_eq!(
8736 scene_delta.new_graph.objects.len(),
8737 5,
8738 "{:#?}",
8739 scene_delta.new_graph.objects
8740 );
8741
8742 ctx.close().await;
8743 mock_ctx.close().await;
8744 }
8745
8746 #[tokio::test(flavor = "multi_thread")]
8747 async fn test_horizontal_distance_two_points() {
8748 let initial_source = "\
8749sketch(on = XY) {
8750 point(at = [var 1, var 2])
8751 point(at = [var 3, var 4])
8752}
8753";
8754
8755 let program = Program::parse(initial_source).unwrap().0.unwrap();
8756
8757 let mut frontend = FrontendState::new();
8758
8759 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8760 let mock_ctx = ExecutorContext::new_mock(None).await;
8761 let version = Version(0);
8762
8763 frontend.hack_set_program(&ctx, program).await.unwrap();
8764 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8765 let sketch_id = sketch_object.id;
8766 let sketch = expect_sketch(sketch_object);
8767 let point0_id = *sketch.segments.first().unwrap();
8768 let point1_id = *sketch.segments.get(1).unwrap();
8769
8770 let constraint = Constraint::HorizontalDistance(Distance {
8771 points: vec![point0_id.into(), point1_id.into()],
8772 distance: Number {
8773 value: 2.0,
8774 units: NumericSuffix::Mm,
8775 },
8776 source: Default::default(),
8777 });
8778 let (src_delta, scene_delta) = frontend
8779 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8780 .await
8781 .unwrap();
8782 assert_eq!(
8783 src_delta.text.as_str(),
8784 "\
8786sketch(on = XY) {
8787 point1 = point(at = [var 1, var 2])
8788 point2 = point(at = [var 3, var 4])
8789 horizontalDistance([point1, point2]) == 2mm
8790}
8791"
8792 );
8793 assert_eq!(
8794 scene_delta.new_graph.objects.len(),
8795 5,
8796 "{:#?}",
8797 scene_delta.new_graph.objects
8798 );
8799
8800 ctx.close().await;
8801 mock_ctx.close().await;
8802 }
8803
8804 #[tokio::test(flavor = "multi_thread")]
8805 async fn test_radius_single_arc_segment() {
8806 let initial_source = "\
8807sketch(on = XY) {
8808 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8809}
8810";
8811
8812 let program = Program::parse(initial_source).unwrap().0.unwrap();
8813
8814 let mut frontend = FrontendState::new();
8815
8816 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8817 let mock_ctx = ExecutorContext::new_mock(None).await;
8818 let version = Version(0);
8819
8820 frontend.hack_set_program(&ctx, program).await.unwrap();
8821 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8822 let sketch_id = sketch_object.id;
8823 let sketch = expect_sketch(sketch_object);
8824 let arc_id = sketch
8826 .segments
8827 .iter()
8828 .find(|&seg_id| {
8829 let obj = frontend.scene_graph.objects.get(seg_id.0);
8830 matches!(
8831 obj.map(|o| &o.kind),
8832 Some(ObjectKind::Segment {
8833 segment: Segment::Arc(_)
8834 })
8835 )
8836 })
8837 .unwrap();
8838
8839 let constraint = Constraint::Radius(Radius {
8840 arc: *arc_id,
8841 radius: Number {
8842 value: 5.0,
8843 units: NumericSuffix::Mm,
8844 },
8845 source: Default::default(),
8846 });
8847 let (src_delta, scene_delta) = frontend
8848 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8849 .await
8850 .unwrap();
8851 assert_eq!(
8852 src_delta.text.as_str(),
8853 "\
8855sketch(on = XY) {
8856 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8857 radius(arc1) == 5mm
8858}
8859"
8860 );
8861 assert_eq!(
8862 scene_delta.new_graph.objects.len(),
8863 7, "{:#?}",
8865 scene_delta.new_graph.objects
8866 );
8867
8868 ctx.close().await;
8869 mock_ctx.close().await;
8870 }
8871
8872 #[tokio::test(flavor = "multi_thread")]
8873 async fn test_vertical_distance_two_points() {
8874 let initial_source = "\
8875sketch(on = XY) {
8876 point(at = [var 1, var 2])
8877 point(at = [var 3, var 4])
8878}
8879";
8880
8881 let program = Program::parse(initial_source).unwrap().0.unwrap();
8882
8883 let mut frontend = FrontendState::new();
8884
8885 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8886 let mock_ctx = ExecutorContext::new_mock(None).await;
8887 let version = Version(0);
8888
8889 frontend.hack_set_program(&ctx, program).await.unwrap();
8890 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8891 let sketch_id = sketch_object.id;
8892 let sketch = expect_sketch(sketch_object);
8893 let point0_id = *sketch.segments.first().unwrap();
8894 let point1_id = *sketch.segments.get(1).unwrap();
8895
8896 let constraint = Constraint::VerticalDistance(Distance {
8897 points: vec![point0_id.into(), point1_id.into()],
8898 distance: Number {
8899 value: 2.0,
8900 units: NumericSuffix::Mm,
8901 },
8902 source: Default::default(),
8903 });
8904 let (src_delta, scene_delta) = frontend
8905 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8906 .await
8907 .unwrap();
8908 assert_eq!(
8909 src_delta.text.as_str(),
8910 "\
8912sketch(on = XY) {
8913 point1 = point(at = [var 1, var 2])
8914 point2 = point(at = [var 3, var 4])
8915 verticalDistance([point1, point2]) == 2mm
8916}
8917"
8918 );
8919 assert_eq!(
8920 scene_delta.new_graph.objects.len(),
8921 5,
8922 "{:#?}",
8923 scene_delta.new_graph.objects
8924 );
8925
8926 ctx.close().await;
8927 mock_ctx.close().await;
8928 }
8929
8930 #[tokio::test(flavor = "multi_thread")]
8931 async fn test_add_fixed_standalone_point() {
8932 let initial_source = "\
8933sketch(on = XY) {
8934 point(at = [var 1, var 2])
8935}
8936";
8937
8938 let program = Program::parse(initial_source).unwrap().0.unwrap();
8939
8940 let mut frontend = FrontendState::new();
8941
8942 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8943 let mock_ctx = ExecutorContext::new_mock(None).await;
8944 let version = Version(0);
8945
8946 frontend.hack_set_program(&ctx, program).await.unwrap();
8947 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8948 let sketch_id = sketch_object.id;
8949 let sketch = expect_sketch(sketch_object);
8950 let point_id = *sketch.segments.first().unwrap();
8951
8952 let (src_delta, scene_delta) = frontend
8953 .add_constraint(
8954 &mock_ctx,
8955 version,
8956 sketch_id,
8957 Constraint::Fixed(Fixed {
8958 points: vec![FixedPoint {
8959 point: point_id,
8960 position: Point2d {
8961 x: Number {
8962 value: 2.0,
8963 units: NumericSuffix::Mm,
8964 },
8965 y: Number {
8966 value: 3.0,
8967 units: NumericSuffix::Mm,
8968 },
8969 },
8970 }],
8971 }),
8972 )
8973 .await
8974 .unwrap();
8975 assert_eq!(
8976 src_delta.text.as_str(),
8977 "\
8978sketch(on = XY) {
8979 point1 = point(at = [var 1, var 2])
8980 fixed([point1, [2mm, 3mm]])
8981}
8982"
8983 );
8984 assert_eq!(
8985 scene_delta.new_graph.objects.len(),
8986 4,
8987 "{:#?}",
8988 scene_delta.new_graph.objects
8989 );
8990
8991 ctx.close().await;
8992 mock_ctx.close().await;
8993 }
8994
8995 #[tokio::test(flavor = "multi_thread")]
8996 async fn test_add_fixed_multiple_points() {
8997 let initial_source = "\
8998sketch(on = XY) {
8999 point(at = [var 1, var 2])
9000 point(at = [var 3, var 4])
9001}
9002";
9003
9004 let program = Program::parse(initial_source).unwrap().0.unwrap();
9005
9006 let mut frontend = FrontendState::new();
9007
9008 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9009 let mock_ctx = ExecutorContext::new_mock(None).await;
9010 let version = Version(0);
9011
9012 frontend.hack_set_program(&ctx, program).await.unwrap();
9013 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9014 let sketch_id = sketch_object.id;
9015 let sketch = expect_sketch(sketch_object);
9016 let point0_id = *sketch.segments.first().unwrap();
9017 let point1_id = *sketch.segments.get(1).unwrap();
9018
9019 let (src_delta, scene_delta) = frontend
9020 .add_constraint(
9021 &mock_ctx,
9022 version,
9023 sketch_id,
9024 Constraint::Fixed(Fixed {
9025 points: vec![
9026 FixedPoint {
9027 point: point0_id,
9028 position: Point2d {
9029 x: Number {
9030 value: 2.0,
9031 units: NumericSuffix::Mm,
9032 },
9033 y: Number {
9034 value: 3.0,
9035 units: NumericSuffix::Mm,
9036 },
9037 },
9038 },
9039 FixedPoint {
9040 point: point1_id,
9041 position: Point2d {
9042 x: Number {
9043 value: 4.0,
9044 units: NumericSuffix::Mm,
9045 },
9046 y: Number {
9047 value: 5.0,
9048 units: NumericSuffix::Mm,
9049 },
9050 },
9051 },
9052 ],
9053 }),
9054 )
9055 .await
9056 .unwrap();
9057 assert_eq!(
9058 src_delta.text.as_str(),
9059 "\
9060sketch(on = XY) {
9061 point1 = point(at = [var 1, var 2])
9062 point2 = point(at = [var 3, var 4])
9063 fixed([point1, [2mm, 3mm]])
9064 fixed([point2, [4mm, 5mm]])
9065}
9066"
9067 );
9068 assert_eq!(
9069 scene_delta.new_graph.objects.len(),
9070 6,
9071 "{:#?}",
9072 scene_delta.new_graph.objects
9073 );
9074
9075 ctx.close().await;
9076 mock_ctx.close().await;
9077 }
9078
9079 #[tokio::test(flavor = "multi_thread")]
9080 async fn test_add_fixed_owned_point() {
9081 let initial_source = "\
9082sketch(on = XY) {
9083 line(start = [var 1, var 2], end = [var 3, var 4])
9084}
9085";
9086
9087 let program = Program::parse(initial_source).unwrap().0.unwrap();
9088
9089 let mut frontend = FrontendState::new();
9090
9091 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9092 let mock_ctx = ExecutorContext::new_mock(None).await;
9093 let version = Version(0);
9094
9095 frontend.hack_set_program(&ctx, program).await.unwrap();
9096 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9097 let sketch_id = sketch_object.id;
9098 let sketch = expect_sketch(sketch_object);
9099 let line_start_id = *sketch.segments.first().unwrap();
9100
9101 let (src_delta, scene_delta) = frontend
9102 .add_constraint(
9103 &mock_ctx,
9104 version,
9105 sketch_id,
9106 Constraint::Fixed(Fixed {
9107 points: vec![FixedPoint {
9108 point: line_start_id,
9109 position: Point2d {
9110 x: Number {
9111 value: 2.0,
9112 units: NumericSuffix::Mm,
9113 },
9114 y: Number {
9115 value: 3.0,
9116 units: NumericSuffix::Mm,
9117 },
9118 },
9119 }],
9120 }),
9121 )
9122 .await
9123 .unwrap();
9124 assert_eq!(
9125 src_delta.text.as_str(),
9126 "\
9127sketch(on = XY) {
9128 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9129 fixed([line1.start, [2mm, 3mm]])
9130}
9131"
9132 );
9133 assert_eq!(
9134 scene_delta.new_graph.objects.len(),
9135 6,
9136 "{:#?}",
9137 scene_delta.new_graph.objects
9138 );
9139
9140 ctx.close().await;
9141 mock_ctx.close().await;
9142 }
9143
9144 #[tokio::test(flavor = "multi_thread")]
9145 async fn test_radius_error_cases() {
9146 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9147 let mock_ctx = ExecutorContext::new_mock(None).await;
9148 let version = Version(0);
9149
9150 let initial_source_point = "\
9152sketch(on = XY) {
9153 point(at = [var 1, var 2])
9154}
9155";
9156 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
9157 let mut frontend_point = FrontendState::new();
9158 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
9159 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
9160 let sketch_id_point = sketch_object_point.id;
9161 let sketch_point = expect_sketch(sketch_object_point);
9162 let point_id = *sketch_point.segments.first().unwrap();
9163
9164 let constraint_point = Constraint::Radius(Radius {
9165 arc: point_id,
9166 radius: Number {
9167 value: 5.0,
9168 units: NumericSuffix::Mm,
9169 },
9170 source: Default::default(),
9171 });
9172 let result_point = frontend_point
9173 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
9174 .await;
9175 assert!(result_point.is_err(), "Single point should error for radius");
9176
9177 let initial_source_line = "\
9179sketch(on = XY) {
9180 line(start = [var 1, var 2], end = [var 3, var 4])
9181}
9182";
9183 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
9184 let mut frontend_line = FrontendState::new();
9185 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
9186 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
9187 let sketch_id_line = sketch_object_line.id;
9188 let sketch_line = expect_sketch(sketch_object_line);
9189 let line_id = *sketch_line.segments.first().unwrap();
9190
9191 let constraint_line = Constraint::Radius(Radius {
9192 arc: line_id,
9193 radius: Number {
9194 value: 5.0,
9195 units: NumericSuffix::Mm,
9196 },
9197 source: Default::default(),
9198 });
9199 let result_line = frontend_line
9200 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
9201 .await;
9202 assert!(result_line.is_err(), "Single line segment should error for radius");
9203
9204 ctx.close().await;
9205 mock_ctx.close().await;
9206 }
9207
9208 #[tokio::test(flavor = "multi_thread")]
9209 async fn test_diameter_single_arc_segment() {
9210 let initial_source = "\
9211sketch(on = XY) {
9212 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9213}
9214";
9215
9216 let program = Program::parse(initial_source).unwrap().0.unwrap();
9217
9218 let mut frontend = FrontendState::new();
9219
9220 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9221 let mock_ctx = ExecutorContext::new_mock(None).await;
9222 let version = Version(0);
9223
9224 frontend.hack_set_program(&ctx, program).await.unwrap();
9225 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9226 let sketch_id = sketch_object.id;
9227 let sketch = expect_sketch(sketch_object);
9228 let arc_id = sketch
9230 .segments
9231 .iter()
9232 .find(|&seg_id| {
9233 let obj = frontend.scene_graph.objects.get(seg_id.0);
9234 matches!(
9235 obj.map(|o| &o.kind),
9236 Some(ObjectKind::Segment {
9237 segment: Segment::Arc(_)
9238 })
9239 )
9240 })
9241 .unwrap();
9242
9243 let constraint = Constraint::Diameter(Diameter {
9244 arc: *arc_id,
9245 diameter: Number {
9246 value: 10.0,
9247 units: NumericSuffix::Mm,
9248 },
9249 source: Default::default(),
9250 });
9251 let (src_delta, scene_delta) = frontend
9252 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9253 .await
9254 .unwrap();
9255 assert_eq!(
9256 src_delta.text.as_str(),
9257 "\
9259sketch(on = XY) {
9260 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9261 diameter(arc1) == 10mm
9262}
9263"
9264 );
9265 assert_eq!(
9266 scene_delta.new_graph.objects.len(),
9267 7, "{:#?}",
9269 scene_delta.new_graph.objects
9270 );
9271
9272 ctx.close().await;
9273 mock_ctx.close().await;
9274 }
9275
9276 #[tokio::test(flavor = "multi_thread")]
9277 async fn test_diameter_error_cases() {
9278 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9279 let mock_ctx = ExecutorContext::new_mock(None).await;
9280 let version = Version(0);
9281
9282 let initial_source_point = "\
9284sketch(on = XY) {
9285 point(at = [var 1, var 2])
9286}
9287";
9288 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
9289 let mut frontend_point = FrontendState::new();
9290 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
9291 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
9292 let sketch_id_point = sketch_object_point.id;
9293 let sketch_point = expect_sketch(sketch_object_point);
9294 let point_id = *sketch_point.segments.first().unwrap();
9295
9296 let constraint_point = Constraint::Diameter(Diameter {
9297 arc: point_id,
9298 diameter: Number {
9299 value: 10.0,
9300 units: NumericSuffix::Mm,
9301 },
9302 source: Default::default(),
9303 });
9304 let result_point = frontend_point
9305 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
9306 .await;
9307 assert!(result_point.is_err(), "Single point should error for diameter");
9308
9309 let initial_source_line = "\
9311sketch(on = XY) {
9312 line(start = [var 1, var 2], end = [var 3, var 4])
9313}
9314";
9315 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
9316 let mut frontend_line = FrontendState::new();
9317 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
9318 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
9319 let sketch_id_line = sketch_object_line.id;
9320 let sketch_line = expect_sketch(sketch_object_line);
9321 let line_id = *sketch_line.segments.first().unwrap();
9322
9323 let constraint_line = Constraint::Diameter(Diameter {
9324 arc: line_id,
9325 diameter: Number {
9326 value: 10.0,
9327 units: NumericSuffix::Mm,
9328 },
9329 source: Default::default(),
9330 });
9331 let result_line = frontend_line
9332 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
9333 .await;
9334 assert!(result_line.is_err(), "Single line segment should error for diameter");
9335
9336 ctx.close().await;
9337 mock_ctx.close().await;
9338 }
9339
9340 #[tokio::test(flavor = "multi_thread")]
9341 async fn test_line_horizontal() {
9342 let initial_source = "\
9343sketch(on = XY) {
9344 line(start = [var 1, var 2], end = [var 3, var 4])
9345}
9346";
9347
9348 let program = Program::parse(initial_source).unwrap().0.unwrap();
9349
9350 let mut frontend = FrontendState::new();
9351
9352 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9353 let mock_ctx = ExecutorContext::new_mock(None).await;
9354 let version = Version(0);
9355
9356 frontend.hack_set_program(&ctx, program).await.unwrap();
9357 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9358 let sketch_id = sketch_object.id;
9359 let sketch = expect_sketch(sketch_object);
9360 let line1_id = *sketch.segments.get(2).unwrap();
9361
9362 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
9363 let (src_delta, scene_delta) = frontend
9364 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9365 .await
9366 .unwrap();
9367 assert_eq!(
9368 src_delta.text.as_str(),
9369 "\
9370sketch(on = XY) {
9371 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9372 horizontal(line1)
9373}
9374"
9375 );
9376 assert_eq!(
9377 scene_delta.new_graph.objects.len(),
9378 6,
9379 "{:#?}",
9380 scene_delta.new_graph.objects
9381 );
9382
9383 ctx.close().await;
9384 mock_ctx.close().await;
9385 }
9386
9387 #[tokio::test(flavor = "multi_thread")]
9388 async fn test_line_vertical() {
9389 let initial_source = "\
9390sketch(on = XY) {
9391 line(start = [var 1, var 2], end = [var 3, var 4])
9392}
9393";
9394
9395 let program = Program::parse(initial_source).unwrap().0.unwrap();
9396
9397 let mut frontend = FrontendState::new();
9398
9399 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9400 let mock_ctx = ExecutorContext::new_mock(None).await;
9401 let version = Version(0);
9402
9403 frontend.hack_set_program(&ctx, program).await.unwrap();
9404 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9405 let sketch_id = sketch_object.id;
9406 let sketch = expect_sketch(sketch_object);
9407 let line1_id = *sketch.segments.get(2).unwrap();
9408
9409 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
9410 let (src_delta, scene_delta) = frontend
9411 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9412 .await
9413 .unwrap();
9414 assert_eq!(
9415 src_delta.text.as_str(),
9416 "\
9417sketch(on = XY) {
9418 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9419 vertical(line1)
9420}
9421"
9422 );
9423 assert_eq!(
9424 scene_delta.new_graph.objects.len(),
9425 6,
9426 "{:#?}",
9427 scene_delta.new_graph.objects
9428 );
9429
9430 ctx.close().await;
9431 mock_ctx.close().await;
9432 }
9433
9434 #[tokio::test(flavor = "multi_thread")]
9435 async fn test_points_vertical() {
9436 let initial_source = "\
9437sketch001 = sketch(on = XY) {
9438 p0 = point(at = [var -2.23mm, var 3.1mm])
9439 pf = point(at = [4, 4])
9440}
9441";
9442
9443 let program = Program::parse(initial_source).unwrap().0.unwrap();
9444
9445 let mut frontend = FrontendState::new();
9446
9447 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9448 let mock_ctx = ExecutorContext::new_mock(None).await;
9449 let version = Version(0);
9450
9451 frontend.hack_set_program(&ctx, program).await.unwrap();
9452 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9453 let sketch_id = sketch_object.id;
9454 let sketch = expect_sketch(sketch_object);
9455 let point_ids = vec![
9456 sketch.segments.first().unwrap().to_owned(),
9457 sketch.segments.get(1).unwrap().to_owned(),
9458 ];
9459
9460 let constraint = Constraint::Vertical(Vertical::Points {
9461 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
9462 });
9463 let (src_delta, scene_delta) = frontend
9464 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9465 .await
9466 .unwrap();
9467 assert_eq!(
9468 src_delta.text.as_str(),
9469 "\
9470sketch001 = sketch(on = XY) {
9471 p0 = point(at = [var -2.23mm, var 3.1mm])
9472 pf = point(at = [4, 4])
9473 vertical([p0, pf])
9474}
9475"
9476 );
9477 assert_eq!(
9478 scene_delta.new_graph.objects.len(),
9479 5,
9480 "{:#?}",
9481 scene_delta.new_graph.objects
9482 );
9483
9484 ctx.close().await;
9485 mock_ctx.close().await;
9486 }
9487
9488 #[tokio::test(flavor = "multi_thread")]
9489 async fn test_points_horizontal() {
9490 let initial_source = "\
9491sketch001 = sketch(on = XY) {
9492 p0 = point(at = [var -2.23mm, var 3.1mm])
9493 pf = point(at = [4, 4])
9494}
9495";
9496
9497 let program = Program::parse(initial_source).unwrap().0.unwrap();
9498
9499 let mut frontend = FrontendState::new();
9500
9501 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9502 let mock_ctx = ExecutorContext::new_mock(None).await;
9503 let version = Version(0);
9504
9505 frontend.hack_set_program(&ctx, program).await.unwrap();
9506 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9507 let sketch_id = sketch_object.id;
9508 let sketch = expect_sketch(sketch_object);
9509 let point_ids = vec![
9510 sketch.segments.first().unwrap().to_owned(),
9511 sketch.segments.get(1).unwrap().to_owned(),
9512 ];
9513
9514 let constraint = Constraint::Horizontal(Horizontal::Points {
9515 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
9516 });
9517 let (src_delta, scene_delta) = frontend
9518 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9519 .await
9520 .unwrap();
9521 assert_eq!(
9522 src_delta.text.as_str(),
9523 "\
9524sketch001 = sketch(on = XY) {
9525 p0 = point(at = [var -2.23mm, var 3.1mm])
9526 pf = point(at = [4, 4])
9527 horizontal([p0, pf])
9528}
9529"
9530 );
9531 assert_eq!(
9532 scene_delta.new_graph.objects.len(),
9533 5,
9534 "{:#?}",
9535 scene_delta.new_graph.objects
9536 );
9537
9538 ctx.close().await;
9539 mock_ctx.close().await;
9540 }
9541
9542 #[tokio::test(flavor = "multi_thread")]
9543 async fn test_point_horizontal_with_origin() {
9544 let initial_source = "\
9545sketch001 = sketch(on = XY) {
9546 p0 = point(at = [var -2.23mm, var 3.1mm])
9547}
9548";
9549
9550 let program = Program::parse(initial_source).unwrap().0.unwrap();
9551
9552 let mut frontend = FrontendState::new();
9553
9554 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9555 let mock_ctx = ExecutorContext::new_mock(None).await;
9556 let version = Version(0);
9557
9558 frontend.hack_set_program(&ctx, program).await.unwrap();
9559 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9560 let sketch_id = sketch_object.id;
9561 let sketch = expect_sketch(sketch_object);
9562 let point_id = *sketch.segments.first().unwrap();
9563
9564 let constraint = Constraint::Horizontal(Horizontal::Points {
9565 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
9566 });
9567 let (src_delta, scene_delta) = frontend
9568 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9569 .await
9570 .unwrap();
9571 assert_eq!(
9572 src_delta.text.as_str(),
9573 "\
9574sketch001 = sketch(on = XY) {
9575 p0 = point(at = [var -2.23mm, var 3.1mm])
9576 horizontal([p0, ORIGIN])
9577}
9578"
9579 );
9580 assert_eq!(
9581 scene_delta.new_graph.objects.len(),
9582 4,
9583 "{:#?}",
9584 scene_delta.new_graph.objects
9585 );
9586
9587 ctx.close().await;
9588 mock_ctx.close().await;
9589 }
9590
9591 #[tokio::test(flavor = "multi_thread")]
9592 async fn test_lines_equal_length() {
9593 let initial_source = "\
9594sketch(on = XY) {
9595 line(start = [var 1, var 2], end = [var 3, var 4])
9596 line(start = [var 5, var 6], end = [var 7, var 8])
9597}
9598";
9599
9600 let program = Program::parse(initial_source).unwrap().0.unwrap();
9601
9602 let mut frontend = FrontendState::new();
9603
9604 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9605 let mock_ctx = ExecutorContext::new_mock(None).await;
9606 let version = Version(0);
9607
9608 frontend.hack_set_program(&ctx, program).await.unwrap();
9609 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9610 let sketch_id = sketch_object.id;
9611 let sketch = expect_sketch(sketch_object);
9612 let line1_id = *sketch.segments.get(2).unwrap();
9613 let line2_id = *sketch.segments.get(5).unwrap();
9614
9615 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
9616 lines: vec![line1_id, line2_id],
9617 });
9618 let (src_delta, scene_delta) = frontend
9619 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9620 .await
9621 .unwrap();
9622 assert_eq!(
9623 src_delta.text.as_str(),
9624 "\
9625sketch(on = XY) {
9626 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9627 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9628 equalLength([line1, line2])
9629}
9630"
9631 );
9632 assert_eq!(
9633 scene_delta.new_graph.objects.len(),
9634 9,
9635 "{:#?}",
9636 scene_delta.new_graph.objects
9637 );
9638
9639 ctx.close().await;
9640 mock_ctx.close().await;
9641 }
9642
9643 #[tokio::test(flavor = "multi_thread")]
9644 async fn test_add_constraint_multi_line_equal_length() {
9645 let initial_source = "\
9646sketch(on = XY) {
9647 line(start = [var 1, var 2], end = [var 3, var 4])
9648 line(start = [var 5, var 6], end = [var 7, var 8])
9649 line(start = [var 9, var 10], end = [var 11, var 12])
9650}
9651";
9652
9653 let program = Program::parse(initial_source).unwrap().0.unwrap();
9654
9655 let mut frontend = FrontendState::new();
9656 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9657 let mock_ctx = ExecutorContext::new_mock(None).await;
9658 let version = Version(0);
9659
9660 frontend.hack_set_program(&ctx, program).await.unwrap();
9661 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9662 let sketch_id = sketch_object.id;
9663 let sketch = expect_sketch(sketch_object);
9664 let line1_id = *sketch.segments.get(2).unwrap();
9665 let line2_id = *sketch.segments.get(5).unwrap();
9666 let line3_id = *sketch.segments.get(8).unwrap();
9667
9668 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
9669 lines: vec![line1_id, line2_id, line3_id],
9670 });
9671 let (src_delta, scene_delta) = frontend
9672 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9673 .await
9674 .unwrap();
9675 assert_eq!(
9676 src_delta.text.as_str(),
9677 "\
9678sketch(on = XY) {
9679 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9680 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9681 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9682 equalLength([line1, line2, line3])
9683}
9684"
9685 );
9686 let constraints = scene_delta
9687 .new_graph
9688 .objects
9689 .iter()
9690 .filter_map(|obj| {
9691 let ObjectKind::Constraint { constraint } = &obj.kind else {
9692 return None;
9693 };
9694 Some(constraint)
9695 })
9696 .collect::<Vec<_>>();
9697
9698 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
9699 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
9700 panic!("expected equal length constraint, got {:?}", constraints[0]);
9701 };
9702 assert_eq!(lines_equal_length.lines.len(), 3);
9703
9704 ctx.close().await;
9705 mock_ctx.close().await;
9706 }
9707
9708 #[tokio::test(flavor = "multi_thread")]
9709 async fn test_lines_parallel() {
9710 let initial_source = "\
9711sketch(on = XY) {
9712 line(start = [var 1, var 2], end = [var 3, var 4])
9713 line(start = [var 5, var 6], end = [var 7, var 8])
9714}
9715";
9716
9717 let program = Program::parse(initial_source).unwrap().0.unwrap();
9718
9719 let mut frontend = FrontendState::new();
9720
9721 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9722 let mock_ctx = ExecutorContext::new_mock(None).await;
9723 let version = Version(0);
9724
9725 frontend.hack_set_program(&ctx, program).await.unwrap();
9726 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9727 let sketch_id = sketch_object.id;
9728 let sketch = expect_sketch(sketch_object);
9729 let line1_id = *sketch.segments.get(2).unwrap();
9730 let line2_id = *sketch.segments.get(5).unwrap();
9731
9732 let constraint = Constraint::Parallel(Parallel {
9733 lines: vec![line1_id, line2_id],
9734 });
9735 let (src_delta, scene_delta) = frontend
9736 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9737 .await
9738 .unwrap();
9739 assert_eq!(
9740 src_delta.text.as_str(),
9741 "\
9742sketch(on = XY) {
9743 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9744 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9745 parallel([line1, line2])
9746}
9747"
9748 );
9749 assert_eq!(
9750 scene_delta.new_graph.objects.len(),
9751 9,
9752 "{:#?}",
9753 scene_delta.new_graph.objects
9754 );
9755
9756 ctx.close().await;
9757 mock_ctx.close().await;
9758 }
9759
9760 #[tokio::test(flavor = "multi_thread")]
9761 async fn test_lines_parallel_multiline() {
9762 let initial_source = "\
9763sketch(on = XY) {
9764 line(start = [var 1, var 2], end = [var 3, var 4])
9765 line(start = [var 5, var 6], end = [var 7, var 8])
9766 line(start = [var 9, var 10], end = [var 11, var 12])
9767}
9768";
9769
9770 let program = Program::parse(initial_source).unwrap().0.unwrap();
9771
9772 let mut frontend = FrontendState::new();
9773
9774 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9775 let mock_ctx = ExecutorContext::new_mock(None).await;
9776 let version = Version(0);
9777
9778 frontend.hack_set_program(&ctx, program).await.unwrap();
9779 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9780 let sketch_id = sketch_object.id;
9781 let sketch = expect_sketch(sketch_object);
9782 let line1_id = *sketch.segments.get(2).unwrap();
9783 let line2_id = *sketch.segments.get(5).unwrap();
9784 let line3_id = *sketch.segments.get(8).unwrap();
9785
9786 let constraint = Constraint::Parallel(Parallel {
9787 lines: vec![line1_id, line2_id, line3_id],
9788 });
9789 let (src_delta, scene_delta) = frontend
9790 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9791 .await
9792 .unwrap();
9793 assert_eq!(
9794 src_delta.text.as_str(),
9795 "\
9796sketch(on = XY) {
9797 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9798 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9799 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9800 parallel([line1, line2, line3])
9801}
9802"
9803 );
9804
9805 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9806 let sketch = expect_sketch(sketch_object);
9807 assert_eq!(sketch.constraints.len(), 1);
9808
9809 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9810 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9811 panic!("Expected constraint object");
9812 };
9813 let Constraint::Parallel(parallel) = constraint else {
9814 panic!("Expected parallel constraint");
9815 };
9816 assert_eq!(parallel.lines.len(), 3);
9817
9818 ctx.close().await;
9819 mock_ctx.close().await;
9820 }
9821
9822 #[tokio::test(flavor = "multi_thread")]
9823 async fn test_lines_perpendicular() {
9824 let initial_source = "\
9825sketch(on = XY) {
9826 line(start = [var 1, var 2], end = [var 3, var 4])
9827 line(start = [var 5, var 6], end = [var 7, var 8])
9828}
9829";
9830
9831 let program = Program::parse(initial_source).unwrap().0.unwrap();
9832
9833 let mut frontend = FrontendState::new();
9834
9835 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9836 let mock_ctx = ExecutorContext::new_mock(None).await;
9837 let version = Version(0);
9838
9839 frontend.hack_set_program(&ctx, program).await.unwrap();
9840 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9841 let sketch_id = sketch_object.id;
9842 let sketch = expect_sketch(sketch_object);
9843 let line1_id = *sketch.segments.get(2).unwrap();
9844 let line2_id = *sketch.segments.get(5).unwrap();
9845
9846 let constraint = Constraint::Perpendicular(Perpendicular {
9847 lines: vec![line1_id, line2_id],
9848 });
9849 let (src_delta, scene_delta) = frontend
9850 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9851 .await
9852 .unwrap();
9853 assert_eq!(
9854 src_delta.text.as_str(),
9855 "\
9856sketch(on = XY) {
9857 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9858 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9859 perpendicular([line1, line2])
9860}
9861"
9862 );
9863 assert_eq!(
9864 scene_delta.new_graph.objects.len(),
9865 9,
9866 "{:#?}",
9867 scene_delta.new_graph.objects
9868 );
9869
9870 ctx.close().await;
9871 mock_ctx.close().await;
9872 }
9873
9874 #[tokio::test(flavor = "multi_thread")]
9875 async fn test_lines_angle() {
9876 let initial_source = "\
9877sketch(on = XY) {
9878 line(start = [var 1, var 2], end = [var 3, var 4])
9879 line(start = [var 5, var 6], end = [var 7, var 8])
9880}
9881";
9882
9883 let program = Program::parse(initial_source).unwrap().0.unwrap();
9884
9885 let mut frontend = FrontendState::new();
9886
9887 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9888 let mock_ctx = ExecutorContext::new_mock(None).await;
9889 let version = Version(0);
9890
9891 frontend.hack_set_program(&ctx, program).await.unwrap();
9892 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9893 let sketch_id = sketch_object.id;
9894 let sketch = expect_sketch(sketch_object);
9895 let line1_id = *sketch.segments.get(2).unwrap();
9896 let line2_id = *sketch.segments.get(5).unwrap();
9897
9898 let constraint = Constraint::Angle(Angle {
9899 lines: vec![line1_id, line2_id],
9900 angle: Number {
9901 value: 30.0,
9902 units: NumericSuffix::Deg,
9903 },
9904 source: Default::default(),
9905 });
9906 let (src_delta, scene_delta) = frontend
9907 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9908 .await
9909 .unwrap();
9910 assert_eq!(
9911 src_delta.text.as_str(),
9912 "\
9914sketch(on = XY) {
9915 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9916 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9917 angle([line1, line2]) == 30deg
9918}
9919"
9920 );
9921 assert_eq!(
9922 scene_delta.new_graph.objects.len(),
9923 9,
9924 "{:#?}",
9925 scene_delta.new_graph.objects
9926 );
9927
9928 ctx.close().await;
9929 mock_ctx.close().await;
9930 }
9931
9932 #[tokio::test(flavor = "multi_thread")]
9933 async fn test_segments_tangent() {
9934 let initial_source = "\
9935sketch(on = XY) {
9936 line(start = [var 1, var 2], end = [var 3, var 4])
9937 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
9938}
9939";
9940
9941 let program = Program::parse(initial_source).unwrap().0.unwrap();
9942
9943 let mut frontend = FrontendState::new();
9944
9945 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9946 let mock_ctx = ExecutorContext::new_mock(None).await;
9947 let version = Version(0);
9948
9949 frontend.hack_set_program(&ctx, program).await.unwrap();
9950 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9951 let sketch_id = sketch_object.id;
9952 let sketch = expect_sketch(sketch_object);
9953 let line1_id = *sketch.segments.get(2).unwrap();
9954 let arc1_id = *sketch.segments.get(6).unwrap();
9955
9956 let constraint = Constraint::Tangent(Tangent {
9957 input: vec![line1_id, arc1_id],
9958 });
9959 let (src_delta, scene_delta) = frontend
9960 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9961 .await
9962 .unwrap();
9963 assert_eq!(
9964 src_delta.text.as_str(),
9965 "\
9966sketch(on = XY) {
9967 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9968 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
9969 tangent([line1, arc1])
9970}
9971"
9972 );
9973 assert_eq!(
9974 scene_delta.new_graph.objects.len(),
9975 10,
9976 "{:#?}",
9977 scene_delta.new_graph.objects
9978 );
9979
9980 ctx.close().await;
9981 mock_ctx.close().await;
9982 }
9983
9984 #[tokio::test(flavor = "multi_thread")]
9985 async fn test_point_midpoint() {
9986 let initial_source = "\
9987sketch(on = XY) {
9988 point(at = [var 1, var 1])
9989 line(start = [var 0, var 0], end = [var 6, var 4])
9990}
9991";
9992
9993 let program = Program::parse(initial_source).unwrap().0.unwrap();
9994
9995 let mut frontend = FrontendState::new();
9996
9997 let ctx = ExecutorContext::new_mock(None).await;
9998 let version = Version(0);
9999
10000 frontend.program = program.clone();
10001 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10002 frontend.update_state_after_exec(outcome, true);
10003 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10004 let sketch_id = sketch_object.id;
10005 let sketch = expect_sketch(sketch_object);
10006 let point_id = *sketch.segments.first().unwrap();
10007 let line_id = *sketch.segments.get(3).unwrap();
10008
10009 let constraint = Constraint::Midpoint(Midpoint {
10010 point: point_id,
10011 segment: line_id,
10012 });
10013 let (src_delta, scene_delta) = frontend
10014 .add_constraint(&ctx, version, sketch_id, constraint)
10015 .await
10016 .unwrap();
10017 assert_eq!(
10018 src_delta.text.as_str(),
10019 "\
10020sketch(on = XY) {
10021 point1 = point(at = [var 1, var 1])
10022 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10023 midpoint(line1, point = point1)
10024}
10025"
10026 );
10027 assert_eq!(
10028 scene_delta.new_graph.objects.len(),
10029 7,
10030 "{:#?}",
10031 scene_delta.new_graph.objects
10032 );
10033
10034 ctx.close().await;
10035 }
10036
10037 #[tokio::test(flavor = "multi_thread")]
10038 async fn test_segments_symmetric() {
10039 let initial_source = "\
10040sketch(on = XY) {
10041 line(start = [var 0, var 0], end = [var 0, var 4])
10042 line(start = [var 4, var 0], end = [var 4, var 4])
10043 line(start = [var 2, var -1], end = [var 2, var 5])
10044}
10045";
10046
10047 let program = Program::parse(initial_source).unwrap().0.unwrap();
10048
10049 let mut frontend = FrontendState::new();
10050
10051 let ctx = ExecutorContext::new_mock(None).await;
10052 let version = Version(0);
10053
10054 frontend.program = program.clone();
10055 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10056 frontend.update_state_after_exec(outcome, true);
10057 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10058 let sketch_id = sketch_object.id;
10059 let sketch = expect_sketch(sketch_object);
10060 let line1_id = *sketch.segments.get(2).unwrap();
10061 let line2_id = *sketch.segments.get(5).unwrap();
10062 let axis_id = *sketch.segments.get(8).unwrap();
10063
10064 let constraint = Constraint::Symmetric(Symmetric {
10065 input: vec![line1_id, line2_id],
10066 axis: axis_id,
10067 });
10068 let (src_delta, scene_delta) = frontend
10069 .add_constraint(&ctx, version, sketch_id, constraint)
10070 .await
10071 .unwrap();
10072 assert_eq!(
10073 src_delta.text.as_str(),
10074 "\
10075sketch(on = XY) {
10076 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
10077 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
10078 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
10079 symmetric([line1, line2], axis = line3)
10080}
10081"
10082 );
10083 assert_eq!(
10084 scene_delta.new_graph.objects.len(),
10085 12,
10086 "{:#?}",
10087 scene_delta.new_graph.objects
10088 );
10089
10090 ctx.close().await;
10091 }
10092
10093 #[tokio::test(flavor = "multi_thread")]
10094 async fn test_point_arc_midpoint() {
10095 let initial_source = "\
10096sketch(on = XY) {
10097 point(at = [var 6, var 3])
10098 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10099}
10100";
10101
10102 let program = Program::parse(initial_source).unwrap().0.unwrap();
10103
10104 let mut frontend = FrontendState::new();
10105
10106 let ctx = ExecutorContext::new_mock(None).await;
10107 let version = Version(0);
10108
10109 frontend.program = program.clone();
10110 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10111 frontend.update_state_after_exec(outcome, true);
10112 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10113 let sketch_id = sketch_object.id;
10114 let sketch = expect_sketch(sketch_object);
10115 let point_id = *sketch.segments.first().unwrap();
10116 let arc_id = *sketch.segments.get(4).unwrap();
10117
10118 let constraint = Constraint::Midpoint(Midpoint {
10119 point: point_id,
10120 segment: arc_id,
10121 });
10122 let (src_delta, scene_delta) = frontend
10123 .add_constraint(&ctx, version, sketch_id, constraint)
10124 .await
10125 .unwrap();
10126 assert_eq!(
10127 src_delta.text.as_str(),
10128 "\
10129sketch(on = XY) {
10130 point1 = point(at = [var 6, var 3])
10131 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10132 midpoint(arc1, point = point1)
10133}
10134"
10135 );
10136 assert_eq!(
10137 scene_delta.new_graph.objects.len(),
10138 8,
10139 "{:#?}",
10140 scene_delta.new_graph.objects
10141 );
10142
10143 ctx.close().await;
10144 }
10145
10146 #[tokio::test(flavor = "multi_thread")]
10147 async fn test_segments_symmetric_arcs() {
10148 let initial_source = "\
10149sketch(on = XY) {
10150 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
10151 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
10152 line(start = [var 0, var -10], end = [var 0, var 10])
10153}
10154";
10155
10156 let program = Program::parse(initial_source).unwrap().0.unwrap();
10157
10158 let mut frontend = FrontendState::new();
10159
10160 let ctx = ExecutorContext::new_mock(None).await;
10161 let version = Version(0);
10162
10163 frontend.program = program.clone();
10164 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10165 frontend.update_state_after_exec(outcome, true);
10166 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10167 let sketch_id = sketch_object.id;
10168 let sketch = expect_sketch(sketch_object);
10169 let arc1_id = *sketch.segments.get(3).unwrap();
10170 let arc2_id = *sketch.segments.get(7).unwrap();
10171 let axis_id = *sketch.segments.get(10).unwrap();
10172
10173 let constraint = Constraint::Symmetric(Symmetric {
10174 input: vec![arc1_id, arc2_id],
10175 axis: axis_id,
10176 });
10177 let (src_delta, scene_delta) = frontend
10178 .add_constraint(&ctx, version, sketch_id, constraint)
10179 .await
10180 .unwrap();
10181 assert_eq!(
10182 src_delta.text.as_str(),
10183 "\
10184sketch(on = XY) {
10185 arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
10186 arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
10187 line1 = line(start = [var 0, var -10], end = [var 0, var 10])
10188 symmetric([arc1, arc2], axis = line1)
10189}
10190"
10191 );
10192 assert_eq!(
10193 scene_delta.new_graph.objects.len(),
10194 14,
10195 "{:#?}",
10196 scene_delta.new_graph.objects
10197 );
10198
10199 ctx.close().await;
10200 }
10201
10202 #[tokio::test(flavor = "multi_thread")]
10203 async fn test_sketch_on_face_simple() {
10204 let initial_source = "\
10205len = 2mm
10206cube = startSketchOn(XY)
10207 |> startProfile(at = [0, 0])
10208 |> line(end = [len, 0], tag = $side)
10209 |> line(end = [0, len])
10210 |> line(end = [-len, 0])
10211 |> line(end = [0, -len])
10212 |> close()
10213 |> extrude(length = len)
10214
10215face = faceOf(cube, face = side)
10216";
10217
10218 let program = Program::parse(initial_source).unwrap().0.unwrap();
10219
10220 let mut frontend = FrontendState::new();
10221
10222 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10223 let mock_ctx = ExecutorContext::new_mock(None).await;
10224 let version = Version(0);
10225
10226 frontend.hack_set_program(&ctx, program).await.unwrap();
10227 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
10228 let face_id = face_object.id;
10229
10230 let sketch_args = SketchCtor {
10231 on: Plane::Object(face_id),
10232 };
10233 let (_src_delta, scene_delta, sketch_id) = frontend
10234 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10235 .await
10236 .unwrap();
10237 assert_eq!(sketch_id, ObjectId(2));
10238 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
10239 let sketch_object = &scene_delta.new_graph.objects[2];
10240 assert_eq!(sketch_object.id, ObjectId(2));
10241 assert_eq!(
10242 sketch_object.kind,
10243 ObjectKind::Sketch(Sketch {
10244 args: SketchCtor {
10245 on: Plane::Object(face_id),
10246 },
10247 plane: face_id,
10248 segments: vec![],
10249 constraints: vec![],
10250 })
10251 );
10252 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10253
10254 ctx.close().await;
10255 mock_ctx.close().await;
10256 }
10257
10258 #[tokio::test(flavor = "multi_thread")]
10259 async fn test_sketch_on_wall_artifact_from_region_extrude() {
10260 let initial_source = "\
10261s = sketch(on = YZ) {
10262 line1 = line(start = [0, 0], end = [0, 1])
10263 line2 = line(start = [0, 1], end = [1, 1])
10264 line3 = line(start = [1, 1], end = [0, 0])
10265}
10266region001 = region(point = [0.1, 0.1], sketch = s)
10267extrude001 = extrude(region001, length = 5)
10268";
10269
10270 let program = Program::parse(initial_source).unwrap().0.unwrap();
10271
10272 let mut frontend = FrontendState::new();
10273 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10274 let version = Version(0);
10275
10276 frontend.hack_set_program(&ctx, program).await.unwrap();
10277 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
10278
10279 let sketch_args = SketchCtor {
10280 on: Plane::Object(wall_object_id),
10281 };
10282 let (src_delta, _scene_delta, _sketch_id) = frontend
10283 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10284 .await
10285 .unwrap();
10286 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
10287
10288 ctx.close().await;
10289 }
10290
10291 #[tokio::test(flavor = "multi_thread")]
10292 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
10293 let initial_source = "\
10294sketch001 = sketch(on = YZ) {
10295 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
10296 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
10297 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
10298 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
10299 coincident([line1.end, line2.start])
10300 coincident([line2.end, line3.start])
10301 coincident([line3.end, line4.start])
10302 coincident([line4.end, line1.start])
10303 parallel([line2, line4])
10304 parallel([line3, line1])
10305 perpendicular([line1, line2])
10306 horizontal(line3)
10307 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
10308}
10309region001 = region(point = [3.1, 3.74], sketch = sketch001)
10310extrude001 = extrude(region001, length = 5)
10311";
10312
10313 let program = Program::parse(initial_source).unwrap().0.unwrap();
10314
10315 let mut frontend = FrontendState::new();
10316 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10317 let version = Version(0);
10318
10319 frontend.hack_set_program(&ctx, program).await.unwrap();
10320 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
10321
10322 let sketch_args = SketchCtor {
10323 on: Plane::Object(wall_object_id),
10324 };
10325 let (src_delta, _scene_delta, _sketch_id) = frontend
10326 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10327 .await
10328 .unwrap();
10329 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
10330
10331 ctx.close().await;
10332 }
10333
10334 #[tokio::test(flavor = "multi_thread")]
10335 async fn test_sketch_on_plane_incremental() {
10336 let initial_source = "\
10337len = 2mm
10338cube = startSketchOn(XY)
10339 |> startProfile(at = [0, 0])
10340 |> line(end = [len, 0], tag = $side)
10341 |> line(end = [0, len])
10342 |> line(end = [-len, 0])
10343 |> line(end = [0, -len])
10344 |> close()
10345 |> extrude(length = len)
10346
10347plane = planeOf(cube, face = side)
10348";
10349
10350 let program = Program::parse(initial_source).unwrap().0.unwrap();
10351
10352 let mut frontend = FrontendState::new();
10353
10354 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10355 let mock_ctx = ExecutorContext::new_mock(None).await;
10356 let version = Version(0);
10357
10358 frontend.hack_set_program(&ctx, program).await.unwrap();
10359 let plane_object = frontend
10361 .scene_graph
10362 .objects
10363 .iter()
10364 .rev()
10365 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
10366 .unwrap();
10367 let plane_id = plane_object.id;
10368
10369 let sketch_args = SketchCtor {
10370 on: Plane::Object(plane_id),
10371 };
10372 let (src_delta, scene_delta, sketch_id) = frontend
10373 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10374 .await
10375 .unwrap();
10376 assert_eq!(
10377 src_delta.text.as_str(),
10378 "\
10379len = 2mm
10380cube = startSketchOn(XY)
10381 |> startProfile(at = [0, 0])
10382 |> line(end = [len, 0], tag = $side)
10383 |> line(end = [0, len])
10384 |> line(end = [-len, 0])
10385 |> line(end = [0, -len])
10386 |> close()
10387 |> extrude(length = len)
10388
10389plane = planeOf(cube, face = side)
10390sketch001 = sketch(on = plane) {
10391}
10392"
10393 );
10394 assert_eq!(sketch_id, ObjectId(2));
10395 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
10396 let sketch_object = &scene_delta.new_graph.objects[2];
10397 assert_eq!(sketch_object.id, ObjectId(2));
10398 assert_eq!(
10399 sketch_object.kind,
10400 ObjectKind::Sketch(Sketch {
10401 args: SketchCtor {
10402 on: Plane::Object(plane_id),
10403 },
10404 plane: plane_id,
10405 segments: vec![],
10406 constraints: vec![],
10407 })
10408 );
10409 assert_eq!(scene_delta.new_graph.objects.len(), 9);
10410
10411 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
10412 assert_eq!(plane_object.id, plane_id);
10413 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
10414
10415 ctx.close().await;
10416 mock_ctx.close().await;
10417 }
10418
10419 #[tokio::test(flavor = "multi_thread")]
10420 async fn test_new_sketch_uses_unique_variable_name() {
10421 let initial_source = "\
10422sketch1 = sketch(on = XY) {
10423}
10424";
10425
10426 let program = Program::parse(initial_source).unwrap().0.unwrap();
10427
10428 let mut frontend = FrontendState::new();
10429 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10430 let version = Version(0);
10431
10432 frontend.hack_set_program(&ctx, program).await.unwrap();
10433
10434 let sketch_args = SketchCtor {
10435 on: Plane::Default(PlaneName::Yz),
10436 };
10437 let (src_delta, _, _) = frontend
10438 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10439 .await
10440 .unwrap();
10441
10442 assert_eq!(
10443 src_delta.text.as_str(),
10444 "\
10445sketch1 = sketch(on = XY) {
10446}
10447sketch001 = sketch(on = YZ) {
10448}
10449"
10450 );
10451
10452 ctx.close().await;
10453 }
10454
10455 #[tokio::test(flavor = "multi_thread")]
10456 async fn test_new_sketch_twice_using_same_plane() {
10457 let initial_source = "\
10458sketch1 = sketch(on = XY) {
10459}
10460";
10461
10462 let program = Program::parse(initial_source).unwrap().0.unwrap();
10463
10464 let mut frontend = FrontendState::new();
10465 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10466 let version = Version(0);
10467
10468 frontend.hack_set_program(&ctx, program).await.unwrap();
10469
10470 let sketch_args = SketchCtor {
10471 on: Plane::Default(PlaneName::Xy),
10472 };
10473 let (src_delta, _, _) = frontend
10474 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10475 .await
10476 .unwrap();
10477
10478 assert_eq!(
10479 src_delta.text.as_str(),
10480 "\
10481sketch1 = sketch(on = XY) {
10482}
10483sketch001 = sketch(on = XY) {
10484}
10485"
10486 );
10487
10488 ctx.close().await;
10489 }
10490
10491 #[tokio::test(flavor = "multi_thread")]
10492 async fn test_sketch_mode_reuses_cached_on_expression() {
10493 let initial_source = "\
10494width = 2mm
10495sketch(on = offsetPlane(XY, offset = width)) {
10496 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
10497 distance([line1.start, line1.end]) == width
10498}
10499";
10500 let program = Program::parse(initial_source).unwrap().0.unwrap();
10501
10502 let mut frontend = FrontendState::new();
10503 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10504 let mock_ctx = ExecutorContext::new_mock(None).await;
10505 let version = Version(0);
10506 let project_id = ProjectId(0);
10507 let file_id = FileId(0);
10508
10509 frontend.hack_set_program(&ctx, program).await.unwrap();
10510 let initial_object_count = frontend.scene_graph.objects.len();
10511 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
10512 .expect("Expected sketch object to exist")
10513 .id;
10514
10515 let scene_delta = frontend
10518 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
10519 .await
10520 .unwrap();
10521 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
10522
10523 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
10526 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
10527
10528 ctx.close().await;
10529 mock_ctx.close().await;
10530 }
10531
10532 #[tokio::test(flavor = "multi_thread")]
10533 async fn test_multiple_sketch_blocks() {
10534 let initial_source = "\
10535// Cube that requires the engine.
10536width = 2
10537sketch001 = startSketchOn(XY)
10538profile001 = startProfile(sketch001, at = [0, 0])
10539 |> yLine(length = width, tag = $seg1)
10540 |> xLine(length = width)
10541 |> yLine(length = -width)
10542 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
10543 |> close()
10544extrude001 = extrude(profile001, length = width)
10545
10546// Get a value that requires the engine.
10547x = segLen(seg1)
10548
10549// Triangle with side length 2*x.
10550sketch(on = XY) {
10551 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
10552 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
10553 coincident([line1.end, line2.start])
10554 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
10555 coincident([line2.end, line3.start])
10556 coincident([line3.end, line1.start])
10557 equalLength([line3, line1])
10558 equalLength([line1, line2])
10559 distance([line1.start, line1.end]) == 2*x
10560}
10561
10562// Line segment with length x.
10563sketch2 = sketch(on = XY) {
10564 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
10565 distance([line1.start, line1.end]) == x
10566}
10567";
10568
10569 let program = Program::parse(initial_source).unwrap().0.unwrap();
10570
10571 let mut frontend = FrontendState::new();
10572
10573 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10574 let mock_ctx = ExecutorContext::new_mock(None).await;
10575 let version = Version(0);
10576 let project_id = ProjectId(0);
10577 let file_id = FileId(0);
10578
10579 frontend.hack_set_program(&ctx, program).await.unwrap();
10580 let sketch_objects = frontend
10581 .scene_graph
10582 .objects
10583 .iter()
10584 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
10585 .collect::<Vec<_>>();
10586 let sketch1_id = sketch_objects.first().unwrap().id;
10587 let sketch2_id = sketch_objects.get(1).unwrap().id;
10588 let point1_id = ObjectId(sketch1_id.0 + 1);
10590 let point2_id = ObjectId(sketch2_id.0 + 1);
10592
10593 let scene_delta = frontend
10602 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
10603 .await
10604 .unwrap();
10605 assert_eq!(
10606 scene_delta.new_graph.objects.len(),
10607 18,
10608 "{:#?}",
10609 scene_delta.new_graph.objects
10610 );
10611
10612 let point_ctor = PointCtor {
10614 position: Point2d {
10615 x: Expr::Var(Number {
10616 value: 1.0,
10617 units: NumericSuffix::Mm,
10618 }),
10619 y: Expr::Var(Number {
10620 value: 2.0,
10621 units: NumericSuffix::Mm,
10622 }),
10623 },
10624 };
10625 let segments = vec![ExistingSegmentCtor {
10626 id: point1_id,
10627 ctor: SegmentCtor::Point(point_ctor),
10628 }];
10629 let (src_delta, _) = frontend
10630 .edit_segments(&mock_ctx, version, sketch1_id, segments)
10631 .await
10632 .unwrap();
10633 assert_eq!(
10635 src_delta.text.as_str(),
10636 "\
10637// Cube that requires the engine.
10638width = 2
10639sketch001 = startSketchOn(XY)
10640profile001 = startProfile(sketch001, at = [0, 0])
10641 |> yLine(length = width, tag = $seg1)
10642 |> xLine(length = width)
10643 |> yLine(length = -width)
10644 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
10645 |> close()
10646extrude001 = extrude(profile001, length = width)
10647
10648// Get a value that requires the engine.
10649x = segLen(seg1)
10650
10651// Triangle with side length 2*x.
10652sketch(on = XY) {
10653 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
10654 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
10655 coincident([line1.end, line2.start])
10656 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
10657 coincident([line2.end, line3.start])
10658 coincident([line3.end, line1.start])
10659 equalLength([line3, line1])
10660 equalLength([line1, line2])
10661 distance([line1.start, line1.end]) == 2 * x
10662}
10663
10664// Line segment with length x.
10665sketch2 = sketch(on = XY) {
10666 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
10667 distance([line1.start, line1.end]) == x
10668}
10669"
10670 );
10671
10672 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
10674 assert_eq!(
10676 src_delta.text.as_str(),
10677 "\
10678// Cube that requires the engine.
10679width = 2
10680sketch001 = startSketchOn(XY)
10681profile001 = startProfile(sketch001, at = [0, 0])
10682 |> yLine(length = width, tag = $seg1)
10683 |> xLine(length = width)
10684 |> yLine(length = -width)
10685 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
10686 |> close()
10687extrude001 = extrude(profile001, length = width)
10688
10689// Get a value that requires the engine.
10690x = segLen(seg1)
10691
10692// Triangle with side length 2*x.
10693sketch(on = XY) {
10694 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
10695 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
10696 coincident([line1.end, line2.start])
10697 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
10698 coincident([line2.end, line3.start])
10699 coincident([line3.end, line1.start])
10700 equalLength([line3, line1])
10701 equalLength([line1, line2])
10702 distance([line1.start, line1.end]) == 2 * x
10703}
10704
10705// Line segment with length x.
10706sketch2 = sketch(on = XY) {
10707 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
10708 distance([line1.start, line1.end]) == x
10709}
10710"
10711 );
10712 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
10720 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
10721
10722 let scene_delta = frontend
10730 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
10731 .await
10732 .unwrap();
10733 assert_eq!(
10734 scene_delta.new_graph.objects.len(),
10735 24,
10736 "{:#?}",
10737 scene_delta.new_graph.objects
10738 );
10739
10740 let point_ctor = PointCtor {
10742 position: Point2d {
10743 x: Expr::Var(Number {
10744 value: 3.0,
10745 units: NumericSuffix::Mm,
10746 }),
10747 y: Expr::Var(Number {
10748 value: 4.0,
10749 units: NumericSuffix::Mm,
10750 }),
10751 },
10752 };
10753 let segments = vec![ExistingSegmentCtor {
10754 id: point2_id,
10755 ctor: SegmentCtor::Point(point_ctor),
10756 }];
10757 let (src_delta, _) = frontend
10758 .edit_segments(&mock_ctx, version, sketch2_id, segments)
10759 .await
10760 .unwrap();
10761 assert_eq!(
10763 src_delta.text.as_str(),
10764 "\
10765// Cube that requires the engine.
10766width = 2
10767sketch001 = startSketchOn(XY)
10768profile001 = startProfile(sketch001, at = [0, 0])
10769 |> yLine(length = width, tag = $seg1)
10770 |> xLine(length = width)
10771 |> yLine(length = -width)
10772 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
10773 |> close()
10774extrude001 = extrude(profile001, length = width)
10775
10776// Get a value that requires the engine.
10777x = segLen(seg1)
10778
10779// Triangle with side length 2*x.
10780sketch(on = XY) {
10781 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
10782 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
10783 coincident([line1.end, line2.start])
10784 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
10785 coincident([line2.end, line3.start])
10786 coincident([line3.end, line1.start])
10787 equalLength([line3, line1])
10788 equalLength([line1, line2])
10789 distance([line1.start, line1.end]) == 2 * x
10790}
10791
10792// Line segment with length x.
10793sketch2 = sketch(on = XY) {
10794 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
10795 distance([line1.start, line1.end]) == x
10796}
10797"
10798 );
10799
10800 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
10802 assert_eq!(
10804 src_delta.text.as_str(),
10805 "\
10806// Cube that requires the engine.
10807width = 2
10808sketch001 = startSketchOn(XY)
10809profile001 = startProfile(sketch001, at = [0, 0])
10810 |> yLine(length = width, tag = $seg1)
10811 |> xLine(length = width)
10812 |> yLine(length = -width)
10813 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
10814 |> close()
10815extrude001 = extrude(profile001, length = width)
10816
10817// Get a value that requires the engine.
10818x = segLen(seg1)
10819
10820// Triangle with side length 2*x.
10821sketch(on = XY) {
10822 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
10823 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
10824 coincident([line1.end, line2.start])
10825 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
10826 coincident([line2.end, line3.start])
10827 coincident([line3.end, line1.start])
10828 equalLength([line3, line1])
10829 equalLength([line1, line2])
10830 distance([line1.start, line1.end]) == 2 * x
10831}
10832
10833// Line segment with length x.
10834sketch2 = sketch(on = XY) {
10835 line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
10836 distance([line1.start, line1.end]) == x
10837}
10838"
10839 );
10840
10841 ctx.close().await;
10842 mock_ctx.close().await;
10843 }
10844
10845 #[tokio::test(flavor = "multi_thread")]
10846 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
10847 clear_mem_cache().await;
10848
10849 let source = r#"sketch001 = sketch(on = XZ) {
10850 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
10851}
10852sketch002 = sketch(on = XY) {
10853 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
10854 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
10855 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
10856 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
10857 coincident([line1.end, line2.start])
10858 coincident([line2.end, line3.start])
10859 coincident([line3.end, line4.start])
10860 coincident([line4.end, line1.start])
10861 parallel([line2, line4])
10862 parallel([line3, line1])
10863 perpendicular([line1, line2])
10864 horizontal(line3)
10865 coincident([line1.start, ORIGIN])
10866}
10867"#;
10868
10869 let program = Program::parse(source).unwrap().0.unwrap();
10870 let mut frontend = FrontendState::new();
10871 let ctx = ExecutorContext::new_with_engine(
10872 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
10873 Default::default(),
10874 );
10875 let mock_ctx = ExecutorContext::new_mock(None).await;
10876 let version = Version(0);
10877 let project_id = ProjectId(0);
10878 let file_id = FileId(0);
10879
10880 frontend.hack_set_program(&ctx, program).await.unwrap();
10881 let sketch_objects = frontend
10882 .scene_graph
10883 .objects
10884 .iter()
10885 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
10886 .collect::<Vec<_>>();
10887 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
10888
10889 let sketch1_id = sketch_objects[0].id;
10890 let sketch2_id = sketch_objects[1].id;
10891
10892 frontend
10893 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
10894 .await
10895 .unwrap();
10896 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
10897
10898 let scene_delta = frontend
10899 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
10900 .await
10901 .unwrap();
10902 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
10903
10904 clear_mem_cache().await;
10905 ctx.close().await;
10906 mock_ctx.close().await;
10907 }
10908
10909 #[tokio::test(flavor = "multi_thread")]
10914 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
10915 let initial_source = "@settings(defaultLengthUnit = mm)
10917
10918
10919
10920sketch001 = sketch(on = XY) {
10921 point(at = [1in, 2in])
10922}
10923";
10924
10925 let program = Program::parse(initial_source).unwrap().0.unwrap();
10926 let mut frontend = FrontendState::new();
10927
10928 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10929 let mock_ctx = ExecutorContext::new_mock(None).await;
10930 let version = Version(0);
10931 let project_id = ProjectId(0);
10932 let file_id = FileId(0);
10933
10934 frontend.hack_set_program(&ctx, program).await.unwrap();
10935 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10936 let sketch_id = sketch_object.id;
10937
10938 frontend
10940 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
10941 .await
10942 .unwrap();
10943
10944 let point_ctor = PointCtor {
10946 position: Point2d {
10947 x: Expr::Number(Number {
10948 value: 5.0,
10949 units: NumericSuffix::Mm,
10950 }),
10951 y: Expr::Number(Number {
10952 value: 6.0,
10953 units: NumericSuffix::Mm,
10954 }),
10955 },
10956 };
10957 let segment = SegmentCtor::Point(point_ctor);
10958 let (src_delta, scene_delta) = frontend
10959 .add_segment(&mock_ctx, version, sketch_id, segment, None)
10960 .await
10961 .unwrap();
10962 assert!(
10964 src_delta.text.contains("point(at = [5mm, 6mm])"),
10965 "Expected new point in source, got: {}",
10966 src_delta.text
10967 );
10968 assert!(!scene_delta.new_objects.is_empty());
10969
10970 ctx.close().await;
10971 mock_ctx.close().await;
10972 }
10973
10974 #[tokio::test(flavor = "multi_thread")]
10975 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
10976 let initial_source = "@settings(defaultLengthUnit = mm)
10978
10979
10980
10981s = sketch(on = XY) {}
10982";
10983
10984 let program = Program::parse(initial_source).unwrap().0.unwrap();
10985 let mut frontend = FrontendState::new();
10986
10987 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10988 let mock_ctx = ExecutorContext::new_mock(None).await;
10989 let version = Version(0);
10990
10991 frontend.hack_set_program(&ctx, program).await.unwrap();
10992 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10993 let sketch_id = sketch_object.id;
10994
10995 let line_ctor = LineCtor {
10996 start: Point2d {
10997 x: Expr::Number(Number {
10998 value: 0.0,
10999 units: NumericSuffix::Mm,
11000 }),
11001 y: Expr::Number(Number {
11002 value: 0.0,
11003 units: NumericSuffix::Mm,
11004 }),
11005 },
11006 end: Point2d {
11007 x: Expr::Number(Number {
11008 value: 10.0,
11009 units: NumericSuffix::Mm,
11010 }),
11011 y: Expr::Number(Number {
11012 value: 10.0,
11013 units: NumericSuffix::Mm,
11014 }),
11015 },
11016 construction: None,
11017 };
11018 let segment = SegmentCtor::Line(line_ctor);
11019 let (src_delta, scene_delta) = frontend
11020 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11021 .await
11022 .unwrap();
11023 assert!(
11024 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
11025 "Expected line in source, got: {}",
11026 src_delta.text
11027 );
11028 assert_eq!(scene_delta.new_objects.len(), 3);
11030
11031 ctx.close().await;
11032 mock_ctx.close().await;
11033 }
11034
11035 #[tokio::test(flavor = "multi_thread")]
11036 async fn test_extra_newlines_between_operations_edit_line() {
11037 let initial_source = "@settings(defaultLengthUnit = mm)
11039
11040
11041sketch001 = sketch(on = XY) {
11042
11043 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
11044
11045}
11046";
11047
11048 let program = Program::parse(initial_source).unwrap().0.unwrap();
11049 let mut frontend = FrontendState::new();
11050
11051 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11052 let mock_ctx = ExecutorContext::new_mock(None).await;
11053 let version = Version(0);
11054 let project_id = ProjectId(0);
11055 let file_id = FileId(0);
11056
11057 frontend.hack_set_program(&ctx, program).await.unwrap();
11058 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11059 let sketch_id = sketch_object.id;
11060 let sketch = expect_sketch(sketch_object);
11061
11062 let line_id = sketch
11064 .segments
11065 .iter()
11066 .copied()
11067 .find(|seg_id| {
11068 matches!(
11069 &frontend.scene_graph.objects[seg_id.0].kind,
11070 ObjectKind::Segment {
11071 segment: Segment::Line(_)
11072 }
11073 )
11074 })
11075 .expect("Expected a line segment in sketch");
11076
11077 frontend
11079 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11080 .await
11081 .unwrap();
11082
11083 let line_ctor = LineCtor {
11085 start: Point2d {
11086 x: Expr::Var(Number {
11087 value: 1.0,
11088 units: NumericSuffix::Mm,
11089 }),
11090 y: Expr::Var(Number {
11091 value: 2.0,
11092 units: NumericSuffix::Mm,
11093 }),
11094 },
11095 end: Point2d {
11096 x: Expr::Var(Number {
11097 value: 13.0,
11098 units: NumericSuffix::Mm,
11099 }),
11100 y: Expr::Var(Number {
11101 value: 14.0,
11102 units: NumericSuffix::Mm,
11103 }),
11104 },
11105 construction: None,
11106 };
11107 let segments = vec![ExistingSegmentCtor {
11108 id: line_id,
11109 ctor: SegmentCtor::Line(line_ctor),
11110 }];
11111 let (src_delta, _scene_delta) = frontend
11112 .edit_segments(&mock_ctx, version, sketch_id, segments)
11113 .await
11114 .unwrap();
11115 assert!(
11116 src_delta
11117 .text
11118 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
11119 "Expected edited line in source, got: {}",
11120 src_delta.text
11121 );
11122
11123 ctx.close().await;
11124 mock_ctx.close().await;
11125 }
11126
11127 #[tokio::test(flavor = "multi_thread")]
11128 async fn test_extra_newlines_delete_segment() {
11129 let initial_source = "@settings(defaultLengthUnit = mm)
11131
11132
11133
11134sketch001 = sketch(on = XY) {
11135 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
11136}
11137";
11138
11139 let program = Program::parse(initial_source).unwrap().0.unwrap();
11140 let mut frontend = FrontendState::new();
11141
11142 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11143 let mock_ctx = ExecutorContext::new_mock(None).await;
11144 let version = Version(0);
11145
11146 frontend.hack_set_program(&ctx, program).await.unwrap();
11147 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11148 let sketch_id = sketch_object.id;
11149 let sketch = expect_sketch(sketch_object);
11150
11151 assert_eq!(sketch.segments.len(), 3);
11153 let circle_id = sketch.segments[2];
11154
11155 let (src_delta, scene_delta) = frontend
11157 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
11158 .await
11159 .unwrap();
11160 assert!(
11161 src_delta.text.contains("sketch(on = XY) {"),
11162 "Expected sketch block in source, got: {}",
11163 src_delta.text
11164 );
11165 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11166 let new_sketch = expect_sketch(new_sketch_object);
11167 assert_eq!(new_sketch.segments.len(), 0);
11168
11169 ctx.close().await;
11170 mock_ctx.close().await;
11171 }
11172
11173 #[tokio::test(flavor = "multi_thread")]
11174 async fn test_unformatted_source_add_arc() {
11175 let initial_source = "@settings(defaultLengthUnit = mm)
11177
11178
11179
11180
11181sketch001 = sketch(on = XY) {
11182}
11183";
11184
11185 let program = Program::parse(initial_source).unwrap().0.unwrap();
11186 let mut frontend = FrontendState::new();
11187
11188 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11189 let mock_ctx = ExecutorContext::new_mock(None).await;
11190 let version = Version(0);
11191
11192 frontend.hack_set_program(&ctx, program).await.unwrap();
11193 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11194 let sketch_id = sketch_object.id;
11195
11196 let arc_ctor = ArcCtor {
11197 start: Point2d {
11198 x: Expr::Var(Number {
11199 value: 5.0,
11200 units: NumericSuffix::Mm,
11201 }),
11202 y: Expr::Var(Number {
11203 value: 0.0,
11204 units: NumericSuffix::Mm,
11205 }),
11206 },
11207 end: Point2d {
11208 x: Expr::Var(Number {
11209 value: 0.0,
11210 units: NumericSuffix::Mm,
11211 }),
11212 y: Expr::Var(Number {
11213 value: 5.0,
11214 units: NumericSuffix::Mm,
11215 }),
11216 },
11217 center: Point2d {
11218 x: Expr::Var(Number {
11219 value: 0.0,
11220 units: NumericSuffix::Mm,
11221 }),
11222 y: Expr::Var(Number {
11223 value: 0.0,
11224 units: NumericSuffix::Mm,
11225 }),
11226 },
11227 construction: None,
11228 };
11229 let segment = SegmentCtor::Arc(arc_ctor);
11230 let (src_delta, scene_delta) = frontend
11231 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11232 .await
11233 .unwrap();
11234 assert!(
11235 src_delta
11236 .text
11237 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
11238 "Expected arc in source, got: {}",
11239 src_delta.text
11240 );
11241 assert!(!scene_delta.new_objects.is_empty());
11242
11243 ctx.close().await;
11244 mock_ctx.close().await;
11245 }
11246
11247 #[tokio::test(flavor = "multi_thread")]
11248 async fn test_extra_newlines_add_circle() {
11249 let initial_source = "@settings(defaultLengthUnit = mm)
11251
11252
11253
11254sketch001 = sketch(on = XY) {
11255}
11256";
11257
11258 let program = Program::parse(initial_source).unwrap().0.unwrap();
11259 let mut frontend = FrontendState::new();
11260
11261 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11262 let mock_ctx = ExecutorContext::new_mock(None).await;
11263 let version = Version(0);
11264
11265 frontend.hack_set_program(&ctx, program).await.unwrap();
11266 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11267 let sketch_id = sketch_object.id;
11268
11269 let circle_ctor = CircleCtor {
11270 start: Point2d {
11271 x: Expr::Var(Number {
11272 value: 5.0,
11273 units: NumericSuffix::Mm,
11274 }),
11275 y: Expr::Var(Number {
11276 value: 0.0,
11277 units: NumericSuffix::Mm,
11278 }),
11279 },
11280 center: Point2d {
11281 x: Expr::Var(Number {
11282 value: 0.0,
11283 units: NumericSuffix::Mm,
11284 }),
11285 y: Expr::Var(Number {
11286 value: 0.0,
11287 units: NumericSuffix::Mm,
11288 }),
11289 },
11290 construction: None,
11291 };
11292 let segment = SegmentCtor::Circle(circle_ctor);
11293 let (src_delta, scene_delta) = frontend
11294 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11295 .await
11296 .unwrap();
11297 assert!(
11298 src_delta
11299 .text
11300 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
11301 "Expected circle in source, got: {}",
11302 src_delta.text
11303 );
11304 assert!(!scene_delta.new_objects.is_empty());
11305
11306 ctx.close().await;
11307 mock_ctx.close().await;
11308 }
11309
11310 #[tokio::test(flavor = "multi_thread")]
11311 async fn test_extra_newlines_add_constraint() {
11312 let initial_source = "@settings(defaultLengthUnit = mm)
11314
11315
11316
11317sketch001 = sketch(on = XY) {
11318 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
11319 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
11320}
11321";
11322
11323 let program = Program::parse(initial_source).unwrap().0.unwrap();
11324 let mut frontend = FrontendState::new();
11325
11326 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11327 let mock_ctx = ExecutorContext::new_mock(None).await;
11328 let version = Version(0);
11329 let project_id = ProjectId(0);
11330 let file_id = FileId(0);
11331
11332 frontend.hack_set_program(&ctx, program).await.unwrap();
11333 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11334 let sketch_id = sketch_object.id;
11335 let sketch = expect_sketch(sketch_object);
11336
11337 let line_ids: Vec<ObjectId> = sketch
11339 .segments
11340 .iter()
11341 .copied()
11342 .filter(|seg_id| {
11343 matches!(
11344 &frontend.scene_graph.objects[seg_id.0].kind,
11345 ObjectKind::Segment {
11346 segment: Segment::Line(_)
11347 }
11348 )
11349 })
11350 .collect();
11351 assert_eq!(line_ids.len(), 2, "Expected two line segments");
11352
11353 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
11354 let ObjectKind::Segment {
11355 segment: Segment::Line(line1_data),
11356 } = &line1.kind
11357 else {
11358 panic!("Expected line");
11359 };
11360 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
11361 let ObjectKind::Segment {
11362 segment: Segment::Line(line2_data),
11363 } = &line2.kind
11364 else {
11365 panic!("Expected line");
11366 };
11367
11368 let constraint = Constraint::Coincident(Coincident {
11370 segments: vec![line1_data.end.into(), line2_data.start.into()],
11371 });
11372
11373 frontend
11375 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11376 .await
11377 .unwrap();
11378 let (src_delta, _scene_delta) = frontend
11379 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11380 .await
11381 .unwrap();
11382 assert!(
11383 src_delta.text.contains("coincident("),
11384 "Expected coincident constraint in source, got: {}",
11385 src_delta.text
11386 );
11387
11388 ctx.close().await;
11389 mock_ctx.close().await;
11390 }
11391
11392 #[tokio::test(flavor = "multi_thread")]
11393 async fn test_extra_newlines_add_line_then_edit_line() {
11394 let initial_source = "@settings(defaultLengthUnit = mm)
11396
11397
11398
11399sketch001 = sketch(on = XY) {
11400}
11401";
11402
11403 let program = Program::parse(initial_source).unwrap().0.unwrap();
11404 let mut frontend = FrontendState::new();
11405
11406 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11407 let mock_ctx = ExecutorContext::new_mock(None).await;
11408 let version = Version(0);
11409
11410 frontend.hack_set_program(&ctx, program).await.unwrap();
11411 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11412 let sketch_id = sketch_object.id;
11413
11414 let line_ctor = LineCtor {
11416 start: Point2d {
11417 x: Expr::Number(Number {
11418 value: 0.0,
11419 units: NumericSuffix::Mm,
11420 }),
11421 y: Expr::Number(Number {
11422 value: 0.0,
11423 units: NumericSuffix::Mm,
11424 }),
11425 },
11426 end: Point2d {
11427 x: Expr::Number(Number {
11428 value: 10.0,
11429 units: NumericSuffix::Mm,
11430 }),
11431 y: Expr::Number(Number {
11432 value: 10.0,
11433 units: NumericSuffix::Mm,
11434 }),
11435 },
11436 construction: None,
11437 };
11438 let segment = SegmentCtor::Line(line_ctor);
11439 let (src_delta, scene_delta) = frontend
11440 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11441 .await
11442 .unwrap();
11443 assert!(
11444 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
11445 "Expected line in source after add, got: {}",
11446 src_delta.text
11447 );
11448 let line_id = *scene_delta.new_objects.last().unwrap();
11450
11451 let line_ctor = LineCtor {
11453 start: Point2d {
11454 x: Expr::Number(Number {
11455 value: 1.0,
11456 units: NumericSuffix::Mm,
11457 }),
11458 y: Expr::Number(Number {
11459 value: 2.0,
11460 units: NumericSuffix::Mm,
11461 }),
11462 },
11463 end: Point2d {
11464 x: Expr::Number(Number {
11465 value: 13.0,
11466 units: NumericSuffix::Mm,
11467 }),
11468 y: Expr::Number(Number {
11469 value: 14.0,
11470 units: NumericSuffix::Mm,
11471 }),
11472 },
11473 construction: None,
11474 };
11475 let segments = vec![ExistingSegmentCtor {
11476 id: line_id,
11477 ctor: SegmentCtor::Line(line_ctor),
11478 }];
11479 let (src_delta, scene_delta) = frontend
11480 .edit_segments(&mock_ctx, version, sketch_id, segments)
11481 .await
11482 .unwrap();
11483 assert!(
11484 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
11485 "Expected edited line in source, got: {}",
11486 src_delta.text
11487 );
11488 assert_eq!(scene_delta.new_objects, vec![]);
11489
11490 ctx.close().await;
11491 mock_ctx.close().await;
11492 }
11493}