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