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