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