1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_error::CompilationIssue;
9use kcl_error::SourceRange;
10use kittycad_modeling_cmds::units::UnitLength;
11use serde::Serialize;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::collections::AhashIndexSet;
19#[cfg(feature = "artifact-graph")]
20use crate::execution::Artifact;
21#[cfg(feature = "artifact-graph")]
22use crate::execution::ArtifactGraph;
23#[cfg(feature = "artifact-graph")]
24use crate::execution::CapSubType;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::cache::SketchModeState;
28use crate::execution::cache::clear_mem_cache;
29use crate::execution::cache::read_old_memory;
30use crate::execution::cache::write_old_memory;
31use crate::fmt::format_number_literal;
32use crate::front::Angle;
33use crate::front::ArcCtor;
34use crate::front::CircleCtor;
35use crate::front::Distance;
36use crate::front::EqualRadius;
37use crate::front::Error;
38use crate::front::ExecResult;
39use crate::front::FixedPoint;
40use crate::front::Freedom;
41use crate::front::LinesEqualLength;
42use crate::front::Midpoint;
43use crate::front::Object;
44use crate::front::Parallel;
45use crate::front::Perpendicular;
46use crate::front::PointCtor;
47use crate::front::Symmetric;
48use crate::front::Tangent;
49use crate::frontend::api::Expr;
50use crate::frontend::api::FileId;
51use crate::frontend::api::Number;
52use crate::frontend::api::ObjectId;
53use crate::frontend::api::ObjectKind;
54use crate::frontend::api::Plane;
55use crate::frontend::api::ProjectId;
56use crate::frontend::api::RestoreSketchCheckpointOutcome;
57use crate::frontend::api::SceneGraph;
58use crate::frontend::api::SceneGraphDelta;
59use crate::frontend::api::SketchCheckpointId;
60use crate::frontend::api::SourceDelta;
61use crate::frontend::api::SourceRef;
62use crate::frontend::api::Version;
63use crate::frontend::modify::find_defined_names;
64use crate::frontend::modify::next_free_name;
65use crate::frontend::modify::next_free_name_with_padding;
66use crate::frontend::sketch::Coincident;
67use crate::frontend::sketch::Constraint;
68use crate::frontend::sketch::ConstraintSegment;
69use crate::frontend::sketch::Diameter;
70use crate::frontend::sketch::ExistingSegmentCtor;
71use crate::frontend::sketch::Horizontal;
72use crate::frontend::sketch::LineCtor;
73use crate::frontend::sketch::Point2d;
74use crate::frontend::sketch::Radius;
75use crate::frontend::sketch::Segment;
76use crate::frontend::sketch::SegmentCtor;
77use crate::frontend::sketch::SketchApi;
78use crate::frontend::sketch::SketchCtor;
79use crate::frontend::sketch::Vertical;
80use crate::frontend::traverse::MutateBodyItem;
81use crate::frontend::traverse::TraversalReturn;
82use crate::frontend::traverse::Visitor;
83use crate::frontend::traverse::dfs_mut;
84use crate::id::IncIdGenerator;
85use crate::parsing::ast::types as ast;
86use crate::pretty::NumericSuffix;
87use crate::std::constraints::LinesAtAngleKind;
88use crate::walk::NodeMut;
89use crate::walk::Visitable;
90
91pub(crate) mod api;
92pub(crate) mod modify;
93pub(crate) mod sketch;
94
95pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
96
97#[derive(Debug, Clone)]
98struct SketchCheckpoint {
99 id: SketchCheckpointId,
100 source: SourceDelta,
101 program: Program,
102 scene_graph: SceneGraph,
103 exec_outcome: ExecOutcome,
104 point_freedom_cache: HashMap<ObjectId, Freedom>,
105 mock_memory: Option<SketchModeState>,
106}
107mod traverse;
108pub(crate) mod trim;
109
110struct ArcSizeConstraintParams {
111 points: Vec<ObjectId>,
112 function_name: &'static str,
113 value: f64,
114 units: NumericSuffix,
115 constraint_type_name: &'static str,
116}
117
118const POINT_FN: &str = "point";
119const POINT_AT_PARAM: &str = "at";
120const LINE_FN: &str = "line";
121const LINE_VARIABLE: &str = "line";
122const LINE_START_PARAM: &str = "start";
123const LINE_END_PARAM: &str = "end";
124const ARC_FN: &str = "arc";
125const ARC_VARIABLE: &str = "arc";
126const ARC_START_PARAM: &str = "start";
127const ARC_END_PARAM: &str = "end";
128const ARC_CENTER_PARAM: &str = "center";
129const CIRCLE_FN: &str = "circle";
130const CIRCLE_VARIABLE: &str = "circle";
131const CIRCLE_START_PARAM: &str = "start";
132const CIRCLE_CENTER_PARAM: &str = "center";
133const LABEL_POSITION_PARAM: &str = "labelPosition";
134
135const COINCIDENT_FN: &str = "coincident";
136const DIAMETER_FN: &str = "diameter";
137const DISTANCE_FN: &str = "distance";
138const FIXED_FN: &str = "fixed";
139const ANGLE_FN: &str = "angle";
140const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
141const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
142const EQUAL_LENGTH_FN: &str = "equalLength";
143const EQUAL_RADIUS_FN: &str = "equalRadius";
144const HORIZONTAL_FN: &str = "horizontal";
145const MIDPOINT_FN: &str = "midpoint";
146const MIDPOINT_POINT_PARAM: &str = "point";
147const RADIUS_FN: &str = "radius";
148const SYMMETRIC_FN: &str = "symmetric";
149const SYMMETRIC_AXIS_PARAM: &str = "axis";
150const TANGENT_FN: &str = "tangent";
151const VERTICAL_FN: &str = "vertical";
152
153const LINE_PROPERTY_START: &str = "start";
154const LINE_PROPERTY_END: &str = "end";
155
156const ARC_PROPERTY_START: &str = "start";
157const ARC_PROPERTY_END: &str = "end";
158const ARC_PROPERTY_CENTER: &str = "center";
159const CIRCLE_PROPERTY_START: &str = "start";
160const CIRCLE_PROPERTY_CENTER: &str = "center";
161
162const CONSTRUCTION_PARAM: &str = "construction";
163
164#[derive(Debug, Clone, Copy)]
165enum EditDeleteKind {
166 Edit,
167 DeleteNonSketch,
168}
169
170impl EditDeleteKind {
171 fn is_delete(&self) -> bool {
173 match self {
174 EditDeleteKind::Edit => false,
175 EditDeleteKind::DeleteNonSketch => true,
176 }
177 }
178
179 fn to_change_kind(self) -> ChangeKind {
180 match self {
181 EditDeleteKind::Edit => ChangeKind::Edit,
182 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Copy)]
188enum ChangeKind {
189 Add,
190 Edit,
191 Delete,
192 None,
193}
194
195#[derive(Debug, Clone, Serialize, ts_rs::TS)]
196#[ts(export, export_to = "FrontendApi.ts")]
197#[serde(tag = "type")]
198pub enum SetProgramOutcome {
199 #[serde(rename_all = "camelCase")]
200 Success {
201 scene_graph: Box<SceneGraph>,
202 exec_outcome: Box<ExecOutcome>,
203 checkpoint_id: Option<SketchCheckpointId>,
204 },
205 #[serde(rename_all = "camelCase")]
206 ExecFailure { error: Box<KclErrorWithOutputs> },
207}
208
209#[derive(Debug, Clone)]
210pub struct FrontendState {
211 program: Program,
212 scene_graph: SceneGraph,
213 point_freedom_cache: HashMap<ObjectId, Freedom>,
216 sketch_checkpoints: VecDeque<SketchCheckpoint>,
217 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
218}
219
220impl Default for FrontendState {
221 fn default() -> Self {
222 Self::new()
223 }
224}
225
226impl FrontendState {
227 pub fn new() -> Self {
228 Self {
229 program: Program::empty(),
230 scene_graph: SceneGraph {
231 project: ProjectId(0),
232 file: FileId(0),
233 version: Version(0),
234 objects: Default::default(),
235 settings: Default::default(),
236 sketch_mode: Default::default(),
237 },
238 point_freedom_cache: HashMap::new(),
239 sketch_checkpoints: VecDeque::new(),
240 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
241 }
242 }
243
244 pub fn scene_graph(&self) -> &SceneGraph {
246 &self.scene_graph
247 }
248
249 pub fn default_length_unit(&self) -> UnitLength {
250 self.program
251 .meta_settings()
252 .ok()
253 .flatten()
254 .map(|settings| settings.default_length_units)
255 .unwrap_or(UnitLength::Millimeters)
256 }
257
258 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
259 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
260
261 let checkpoint = SketchCheckpoint {
262 id: checkpoint_id,
263 source: SourceDelta {
264 text: source_from_ast(&self.program.ast),
265 },
266 program: self.program.clone(),
267 scene_graph: self.scene_graph.clone(),
268 exec_outcome,
269 point_freedom_cache: self.point_freedom_cache.clone(),
270 mock_memory: read_old_memory().await,
271 };
272
273 self.sketch_checkpoints.push_back(checkpoint);
274 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
275 self.sketch_checkpoints.pop_front();
276 }
277
278 Ok(checkpoint_id)
279 }
280
281 pub async fn restore_sketch_checkpoint(
282 &mut self,
283 checkpoint_id: SketchCheckpointId,
284 ) -> api::Result<RestoreSketchCheckpointOutcome> {
285 let checkpoint = self
286 .sketch_checkpoints
287 .iter()
288 .find(|checkpoint| checkpoint.id == checkpoint_id)
289 .cloned()
290 .ok_or_else(|| Error {
291 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
292 })?;
293
294 self.program = checkpoint.program;
295 self.scene_graph = checkpoint.scene_graph.clone();
296 self.point_freedom_cache = checkpoint.point_freedom_cache;
297
298 if let Some(mock_memory) = checkpoint.mock_memory {
299 write_old_memory(mock_memory).await;
300 } else {
301 clear_mem_cache().await;
302 }
303
304 Ok(RestoreSketchCheckpointOutcome {
305 source_delta: checkpoint.source,
306 scene_graph_delta: SceneGraphDelta {
307 new_graph: checkpoint.scene_graph,
308 new_objects: Vec::new(),
309 invalidates_ids: true,
310 exec_outcome: checkpoint.exec_outcome,
311 },
312 })
313 }
314
315 pub fn clear_sketch_checkpoints(&mut self) {
316 self.sketch_checkpoints.clear();
317 }
318}
319
320impl SketchApi for FrontendState {
321 async fn execute_mock(
322 &mut self,
323 ctx: &ExecutorContext,
324 _version: Version,
325 sketch: ObjectId,
326 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
327 let sketch_block_ref =
328 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
329
330 let mut truncated_program = self.program.clone();
331 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
332 .map_err(KclErrorWithOutputs::no_outputs)?;
333
334 let outcome = ctx
336 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
337 .await?;
338 let new_source = source_from_ast(&self.program.ast);
339 let src_delta = SourceDelta { text: new_source };
340 let outcome = self.update_state_after_exec(outcome, true);
342 let scene_graph_delta = SceneGraphDelta {
343 new_graph: self.scene_graph.clone(),
344 new_objects: Default::default(),
345 invalidates_ids: false,
346 exec_outcome: outcome,
347 };
348 Ok((src_delta, scene_graph_delta))
349 }
350
351 async fn new_sketch(
352 &mut self,
353 ctx: &ExecutorContext,
354 _project: ProjectId,
355 _file: FileId,
356 _version: Version,
357 args: SketchCtor,
358 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
359 let mut new_ast = self.program.ast.clone();
362 let mut plane_ast =
364 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
365 let mut defined_names = find_defined_names(&new_ast);
366 let is_face_of_expr = matches!(
367 &plane_ast,
368 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
369 );
370 if is_face_of_expr {
371 let face_name = next_free_name_with_padding("face", &defined_names)
372 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
373 let face_decl = ast::VariableDeclaration::new(
374 ast::VariableDeclarator::new(&face_name, plane_ast),
375 ast::ItemVisibility::Default,
376 ast::VariableKind::Const,
377 );
378 new_ast
379 .body
380 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
381 face_decl,
382 ))));
383 defined_names.insert(face_name.clone());
384 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
385 }
386 let sketch_ast = ast::SketchBlock {
387 arguments: vec![ast::LabeledArg {
388 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
389 arg: plane_ast,
390 }],
391 body: Default::default(),
392 is_being_edited: false,
393 non_code_meta: Default::default(),
394 digest: None,
395 };
396 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
399 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
400 let sketch_decl = ast::VariableDeclaration::new(
401 ast::VariableDeclarator::new(
402 &sketch_name,
403 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
404 ),
405 ast::ItemVisibility::Default,
406 ast::VariableKind::Const,
407 );
408 new_ast
409 .body
410 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
411 sketch_decl,
412 ))));
413 let new_source = source_from_ast(&new_ast);
415 let (new_program, errors) = Program::parse(&new_source)
417 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
418 if !errors.is_empty() {
419 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
420 "Error parsing KCL source after adding sketch: {errors:?}"
421 ))));
422 }
423 let Some(new_program) = new_program else {
424 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
425 "No AST produced after adding sketch".to_owned(),
426 )));
427 };
428
429 self.program = new_program.clone();
431
432 let outcome = ctx.run_with_caching(new_program.clone()).await?;
435 let freedom_analysis_ran = true;
436
437 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
438
439 let Some(sketch_id) = self
440 .scene_graph
441 .objects
442 .iter()
443 .filter_map(|object| match object.kind {
444 ObjectKind::Sketch(_) => Some(object.id),
445 _ => None,
446 })
447 .max_by_key(|id| id.0)
448 else {
449 return Err(KclErrorWithOutputs::from_error_outcome(
450 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
451 outcome,
452 ));
453 };
454 self.scene_graph.sketch_mode = Some(sketch_id);
456
457 let src_delta = SourceDelta { text: new_source };
458 let scene_graph_delta = SceneGraphDelta {
459 new_graph: self.scene_graph.clone(),
460 invalidates_ids: false,
461 new_objects: vec![sketch_id],
462 exec_outcome: outcome,
463 };
464 Ok((src_delta, scene_graph_delta, sketch_id))
465 }
466
467 async fn edit_sketch(
468 &mut self,
469 ctx: &ExecutorContext,
470 _project: ProjectId,
471 _file: FileId,
472 _version: Version,
473 sketch: ObjectId,
474 ) -> ExecResult<SceneGraphDelta> {
475 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
479 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
480 })?;
481 let ObjectKind::Sketch(_) = &sketch_object.kind else {
482 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
483 "Object is not a sketch, it is {}",
484 sketch_object.kind.human_friendly_kind_with_article()
485 ))));
486 };
487 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
488
489 self.scene_graph.sketch_mode = Some(sketch);
491
492 let mut truncated_program = self.program.clone();
494 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
495 .map_err(KclErrorWithOutputs::no_outputs)?;
496
497 let outcome = ctx
500 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
501 .await?;
502
503 let outcome = self.update_state_after_exec(outcome, true);
505 let scene_graph_delta = SceneGraphDelta {
506 new_graph: self.scene_graph.clone(),
507 invalidates_ids: false,
508 new_objects: Vec::new(),
509 exec_outcome: outcome,
510 };
511 Ok(scene_graph_delta)
512 }
513
514 async fn exit_sketch(
515 &mut self,
516 ctx: &ExecutorContext,
517 _version: Version,
518 sketch: ObjectId,
519 ) -> ExecResult<SceneGraph> {
520 #[cfg(not(target_arch = "wasm32"))]
522 let _ = sketch;
523 #[cfg(target_arch = "wasm32")]
524 if self.scene_graph.sketch_mode != Some(sketch) {
525 web_sys::console::warn_1(
526 &format!(
527 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
528 &self.scene_graph.sketch_mode
529 )
530 .into(),
531 );
532 }
533 self.scene_graph.sketch_mode = None;
534
535 let outcome = ctx.run_with_caching(self.program.clone()).await?;
537
538 self.update_state_after_exec(outcome, false);
540
541 Ok(self.scene_graph.clone())
542 }
543
544 async fn delete_sketch(
545 &mut self,
546 ctx: &ExecutorContext,
547 _version: Version,
548 sketch: ObjectId,
549 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
550 let mut new_ast = self.program.ast.clone();
553
554 let sketch_id = sketch;
556 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
557 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
558 })?;
559 let ObjectKind::Sketch(_) = &sketch_object.kind else {
560 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
561 "Object is not a sketch, it is {}",
562 sketch_object.kind.human_friendly_kind_with_article(),
563 ))));
564 };
565
566 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
568 .map_err(KclErrorWithOutputs::no_outputs)?;
569
570 self.execute_after_delete_sketch(ctx, &mut new_ast).await
571 }
572
573 async fn add_segment(
574 &mut self,
575 ctx: &ExecutorContext,
576 _version: Version,
577 sketch: ObjectId,
578 segment: SegmentCtor,
579 _label: Option<String>,
580 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
581 match segment {
583 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
584 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
585 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
586 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
587 }
588 }
589
590 async fn edit_segments(
591 &mut self,
592 ctx: &ExecutorContext,
593 _version: Version,
594 sketch: ObjectId,
595 segments: Vec<ExistingSegmentCtor>,
596 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
597 let sketch_block_ref =
599 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
600
601 let mut new_ast = self.program.ast.clone();
602 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
603
604 for segment in &segments {
607 segment_ids_edited.insert(segment.id);
608 }
609
610 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
625
626 for segment in segments {
627 let segment_id = segment.id;
628 match segment.ctor {
629 SegmentCtor::Point(ctor) => {
630 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
632 && let ObjectKind::Segment { segment } = &segment_object.kind
633 && let Segment::Point(point) = segment
634 && let Some(owner_id) = point.owner
635 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
636 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
637 {
638 match owner_segment {
639 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
640 if let Some(existing) = final_edits.get_mut(&owner_id) {
641 let SegmentCtor::Line(line_ctor) = existing else {
642 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
643 "Internal: Expected line ctor for owner, but found {}",
644 existing.human_friendly_kind_with_article()
645 ))));
646 };
647 if line.start == segment_id {
649 line_ctor.start = ctor.position;
650 } else {
651 line_ctor.end = ctor.position;
652 }
653 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
654 let mut line_ctor = line_ctor.clone();
656 if line.start == segment_id {
657 line_ctor.start = ctor.position;
658 } else {
659 line_ctor.end = ctor.position;
660 }
661 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
662 } else {
663 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
665 "Internal: Line does not have line ctor, but found {}",
666 line.ctor.human_friendly_kind_with_article()
667 ))));
668 }
669 continue;
670 }
671 Segment::Arc(arc)
672 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
673 {
674 if let Some(existing) = final_edits.get_mut(&owner_id) {
675 let SegmentCtor::Arc(arc_ctor) = existing else {
676 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
677 "Internal: Expected arc ctor for owner, but found {}",
678 existing.human_friendly_kind_with_article()
679 ))));
680 };
681 if arc.start == segment_id {
682 arc_ctor.start = ctor.position;
683 } else if arc.end == segment_id {
684 arc_ctor.end = ctor.position;
685 } else {
686 arc_ctor.center = ctor.position;
687 }
688 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
689 let mut arc_ctor = arc_ctor.clone();
690 if arc.start == segment_id {
691 arc_ctor.start = ctor.position;
692 } else if arc.end == segment_id {
693 arc_ctor.end = ctor.position;
694 } else {
695 arc_ctor.center = ctor.position;
696 }
697 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
698 } else {
699 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
700 "Internal: Arc does not have arc ctor, but found {}",
701 arc.ctor.human_friendly_kind_with_article()
702 ))));
703 }
704 continue;
705 }
706 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
707 if let Some(existing) = final_edits.get_mut(&owner_id) {
708 let SegmentCtor::Circle(circle_ctor) = existing else {
709 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
710 "Internal: Expected circle ctor for owner, but found {}",
711 existing.human_friendly_kind_with_article()
712 ))));
713 };
714 if circle.start == segment_id {
715 circle_ctor.start = ctor.position;
716 } else {
717 circle_ctor.center = ctor.position;
718 }
719 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
720 let mut circle_ctor = circle_ctor.clone();
721 if circle.start == segment_id {
722 circle_ctor.start = ctor.position;
723 } else {
724 circle_ctor.center = ctor.position;
725 }
726 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
727 } else {
728 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
729 "Internal: Circle does not have circle ctor, but found {}",
730 circle.ctor.human_friendly_kind_with_article()
731 ))));
732 }
733 continue;
734 }
735 _ => {}
736 }
737 }
738
739 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
741 }
742 SegmentCtor::Line(ctor) => {
743 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
744 }
745 SegmentCtor::Arc(ctor) => {
746 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
747 }
748 SegmentCtor::Circle(ctor) => {
749 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
750 }
751 }
752 }
753
754 for (segment_id, ctor) in final_edits {
755 match ctor {
756 SegmentCtor::Point(ctor) => self
757 .edit_point(&mut new_ast, sketch, segment_id, ctor)
758 .map_err(KclErrorWithOutputs::no_outputs)?,
759 SegmentCtor::Line(ctor) => self
760 .edit_line(&mut new_ast, sketch, segment_id, ctor)
761 .map_err(KclErrorWithOutputs::no_outputs)?,
762 SegmentCtor::Arc(ctor) => self
763 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
764 .map_err(KclErrorWithOutputs::no_outputs)?,
765 SegmentCtor::Circle(ctor) => self
766 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
767 .map_err(KclErrorWithOutputs::no_outputs)?,
768 }
769 }
770 self.execute_after_edit(
771 ctx,
772 sketch,
773 sketch_block_ref,
774 segment_ids_edited,
775 EditDeleteKind::Edit,
776 &mut new_ast,
777 )
778 .await
779 }
780
781 async fn delete_objects(
782 &mut self,
783 ctx: &ExecutorContext,
784 _version: Version,
785 sketch: ObjectId,
786 constraint_ids: Vec<ObjectId>,
787 segment_ids: Vec<ObjectId>,
788 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
789 let sketch_block_ref =
791 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
792
793 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
795 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
796
797 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
800
801 for segment_id in segment_ids_set.iter().copied() {
802 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
803 && let ObjectKind::Segment { segment } = &segment_object.kind
804 && let Segment::Point(point) = segment
805 && let Some(owner_id) = point.owner
806 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
807 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
808 && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
809 {
810 resolved_segment_ids_to_delete.insert(owner_id);
812 } else {
813 resolved_segment_ids_to_delete.insert(segment_id);
815 }
816 }
817 let referenced_constraint_ids = self
818 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
819 .map_err(KclErrorWithOutputs::no_outputs)?;
820
821 let mut new_ast = self.program.ast.clone();
822
823 for constraint_id in referenced_constraint_ids {
824 if constraint_ids_set.contains(&constraint_id) {
825 continue;
826 }
827
828 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
829 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
830 })?;
831 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
832 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
833 "Object is not a constraint, it is {}",
834 constraint_object.kind.human_friendly_kind_with_article()
835 ))));
836 };
837
838 match constraint {
839 Constraint::Coincident(coincident) => {
840 let remaining_segments =
841 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
842
843 if remaining_segments.len() >= 2 {
845 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
846 .map_err(KclErrorWithOutputs::no_outputs)?;
847 } else {
848 constraint_ids_set.insert(constraint_id);
849 }
850 }
851 Constraint::EqualRadius(equal_radius) => {
852 let remaining_input = equal_radius
853 .input
854 .iter()
855 .copied()
856 .filter(|segment_id| {
857 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
858 })
859 .collect::<Vec<_>>();
860
861 if remaining_input.len() >= 2 {
862 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
863 .map_err(KclErrorWithOutputs::no_outputs)?;
864 } else {
865 constraint_ids_set.insert(constraint_id);
866 }
867 }
868 Constraint::LinesEqualLength(lines_equal_length) => {
869 let remaining_lines = lines_equal_length
870 .lines
871 .iter()
872 .copied()
873 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
874 .collect::<Vec<_>>();
875
876 if remaining_lines.len() >= 2 {
878 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
879 .map_err(KclErrorWithOutputs::no_outputs)?;
880 } else {
881 constraint_ids_set.insert(constraint_id);
882 }
883 }
884 Constraint::Parallel(parallel) => {
885 let remaining_lines = parallel
886 .lines
887 .iter()
888 .copied()
889 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
890 .collect::<Vec<_>>();
891
892 if remaining_lines.len() >= 2 {
893 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
894 .map_err(KclErrorWithOutputs::no_outputs)?;
895 } else {
896 constraint_ids_set.insert(constraint_id);
897 }
898 }
899 Constraint::Horizontal(Horizontal::Points { points }) => {
900 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
901
902 if remaining_points.len() >= 2 {
903 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
904 .map_err(KclErrorWithOutputs::no_outputs)?;
905 } else {
906 constraint_ids_set.insert(constraint_id);
907 }
908 }
909 Constraint::Vertical(Vertical::Points { points }) => {
910 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
911
912 if remaining_points.len() >= 2 {
913 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
914 .map_err(KclErrorWithOutputs::no_outputs)?;
915 } else {
916 constraint_ids_set.insert(constraint_id);
917 }
918 }
919 Constraint::Fixed(fixed) => {
920 if fixed.points.iter().any(|fixed_point| {
921 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
922 }) {
923 constraint_ids_set.insert(constraint_id);
924 }
925 }
926 _ => {
927 constraint_ids_set.insert(constraint_id);
929 }
930 }
931 }
932
933 for constraint_id in constraint_ids_set {
934 self.delete_constraint(&mut new_ast, sketch, constraint_id)
935 .map_err(KclErrorWithOutputs::no_outputs)?;
936 }
937 for segment_id in resolved_segment_ids_to_delete {
938 self.delete_segment(&mut new_ast, sketch, segment_id)
939 .map_err(KclErrorWithOutputs::no_outputs)?;
940 }
941
942 self.execute_after_edit(
943 ctx,
944 sketch,
945 sketch_block_ref,
946 Default::default(),
947 EditDeleteKind::DeleteNonSketch,
948 &mut new_ast,
949 )
950 .await
951 }
952
953 async fn add_constraint(
954 &mut self,
955 ctx: &ExecutorContext,
956 _version: Version,
957 sketch: ObjectId,
958 constraint: Constraint,
959 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
960 let original_program = self.program.clone();
964 let original_scene_graph = self.scene_graph.clone();
965
966 let mut new_ast = self.program.ast.clone();
967 let sketch_block_ref = match constraint {
968 Constraint::Coincident(coincident) => self
969 .add_coincident(sketch, coincident, &mut new_ast)
970 .await
971 .map_err(KclErrorWithOutputs::no_outputs)?,
972 Constraint::Distance(distance) => self
973 .add_distance(sketch, distance, &mut new_ast)
974 .await
975 .map_err(KclErrorWithOutputs::no_outputs)?,
976 Constraint::EqualRadius(equal_radius) => self
977 .add_equal_radius(sketch, equal_radius, &mut new_ast)
978 .await
979 .map_err(KclErrorWithOutputs::no_outputs)?,
980 Constraint::Fixed(fixed) => self
981 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
982 .await
983 .map_err(KclErrorWithOutputs::no_outputs)?,
984 Constraint::HorizontalDistance(distance) => self
985 .add_horizontal_distance(sketch, distance, &mut new_ast)
986 .await
987 .map_err(KclErrorWithOutputs::no_outputs)?,
988 Constraint::VerticalDistance(distance) => self
989 .add_vertical_distance(sketch, distance, &mut new_ast)
990 .await
991 .map_err(KclErrorWithOutputs::no_outputs)?,
992 Constraint::Horizontal(horizontal) => self
993 .add_horizontal(sketch, horizontal, &mut new_ast)
994 .await
995 .map_err(KclErrorWithOutputs::no_outputs)?,
996 Constraint::LinesEqualLength(lines_equal_length) => self
997 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
998 .await
999 .map_err(KclErrorWithOutputs::no_outputs)?,
1000 Constraint::Midpoint(midpoint) => self
1001 .add_midpoint(sketch, midpoint, &mut new_ast)
1002 .await
1003 .map_err(KclErrorWithOutputs::no_outputs)?,
1004 Constraint::Parallel(parallel) => self
1005 .add_parallel(sketch, parallel, &mut new_ast)
1006 .await
1007 .map_err(KclErrorWithOutputs::no_outputs)?,
1008 Constraint::Perpendicular(perpendicular) => self
1009 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1010 .await
1011 .map_err(KclErrorWithOutputs::no_outputs)?,
1012 Constraint::Radius(radius) => self
1013 .add_radius(sketch, radius, &mut new_ast)
1014 .await
1015 .map_err(KclErrorWithOutputs::no_outputs)?,
1016 Constraint::Diameter(diameter) => self
1017 .add_diameter(sketch, diameter, &mut new_ast)
1018 .await
1019 .map_err(KclErrorWithOutputs::no_outputs)?,
1020 Constraint::Symmetric(symmetric) => self
1021 .add_symmetric(sketch, symmetric, &mut new_ast)
1022 .await
1023 .map_err(KclErrorWithOutputs::no_outputs)?,
1024 Constraint::Vertical(vertical) => self
1025 .add_vertical(sketch, vertical, &mut new_ast)
1026 .await
1027 .map_err(KclErrorWithOutputs::no_outputs)?,
1028 Constraint::Angle(lines_at_angle) => self
1029 .add_angle(sketch, lines_at_angle, &mut new_ast)
1030 .await
1031 .map_err(KclErrorWithOutputs::no_outputs)?,
1032 Constraint::Tangent(tangent) => self
1033 .add_tangent(sketch, tangent, &mut new_ast)
1034 .await
1035 .map_err(KclErrorWithOutputs::no_outputs)?,
1036 };
1037
1038 let result = self
1039 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1040 .await;
1041
1042 if result.is_err() {
1044 self.program = original_program;
1045 self.scene_graph = original_scene_graph;
1046 }
1047
1048 result
1049 }
1050
1051 async fn chain_segment(
1052 &mut self,
1053 ctx: &ExecutorContext,
1054 version: Version,
1055 sketch: ObjectId,
1056 previous_segment_end_point_id: ObjectId,
1057 segment: SegmentCtor,
1058 _label: Option<String>,
1059 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1060 let SegmentCtor::Line(line_ctor) = segment else {
1064 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1065 "chain_segment currently only supports Line segments, got {}",
1066 segment.human_friendly_kind_with_article(),
1067 ))));
1068 };
1069
1070 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1072
1073 let new_line_id = first_scene_delta
1076 .new_objects
1077 .iter()
1078 .find(|&obj_id| {
1079 let obj = self.scene_graph.objects.get(obj_id.0);
1080 if let Some(obj) = obj {
1081 matches!(
1082 &obj.kind,
1083 ObjectKind::Segment {
1084 segment: Segment::Line(_)
1085 }
1086 )
1087 } else {
1088 false
1089 }
1090 })
1091 .ok_or_else(|| {
1092 KclErrorWithOutputs::no_outputs(KclError::refactor(
1093 "Failed to find new line segment in scene graph".to_string(),
1094 ))
1095 })?;
1096
1097 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1098 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1099 "New line object not found: {new_line_id:?}"
1100 )))
1101 })?;
1102
1103 let ObjectKind::Segment {
1104 segment: new_line_segment,
1105 } = &new_line_obj.kind
1106 else {
1107 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1108 "Object is not a segment: {new_line_obj:?}"
1109 ))));
1110 };
1111
1112 let Segment::Line(new_line) = new_line_segment else {
1113 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114 "Segment is not a line: {new_line_segment:?}"
1115 ))));
1116 };
1117
1118 let new_line_start_point_id = new_line.start;
1119
1120 let coincident = Coincident {
1122 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1123 };
1124
1125 let (final_src_delta, final_scene_delta) = self
1126 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1127 .await?;
1128
1129 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1132 combined_new_objects.extend(final_scene_delta.new_objects);
1133
1134 let scene_graph_delta = SceneGraphDelta {
1135 new_graph: self.scene_graph.clone(),
1136 invalidates_ids: false,
1137 new_objects: combined_new_objects,
1138 exec_outcome: final_scene_delta.exec_outcome,
1139 };
1140
1141 Ok((final_src_delta, scene_graph_delta))
1142 }
1143
1144 async fn edit_constraint(
1145 &mut self,
1146 ctx: &ExecutorContext,
1147 _version: Version,
1148 sketch: ObjectId,
1149 constraint_id: ObjectId,
1150 value_expression: String,
1151 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1152 let sketch_block_ref =
1154 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1155
1156 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1157 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1158 })?;
1159 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1160 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1161 "Object is not a constraint: {constraint_id:?}"
1162 ))));
1163 }
1164
1165 let mut new_ast = self.program.ast.clone();
1166
1167 let (parsed, errors) = Program::parse(&value_expression)
1169 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1170 if !errors.is_empty() {
1171 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1172 "Error parsing value expression: {errors:?}"
1173 ))));
1174 }
1175 let mut parsed = parsed.ok_or_else(|| {
1176 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1177 })?;
1178 if parsed.ast.body.is_empty() {
1179 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1180 "Empty value expression".to_string(),
1181 )));
1182 }
1183 let first = parsed.ast.body.remove(0);
1184 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1185 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1186 "Value expression must be a simple expression".to_string(),
1187 )));
1188 };
1189
1190 let new_value: ast::BinaryPart = expr_stmt
1191 .inner
1192 .expression
1193 .try_into()
1194 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1195
1196 self.mutate_ast(
1197 &mut new_ast,
1198 constraint_id,
1199 AstMutateCommand::EditConstraintValue { value: new_value },
1200 )
1201 .map_err(KclErrorWithOutputs::no_outputs)?;
1202
1203 self.execute_after_edit(
1204 ctx,
1205 sketch,
1206 sketch_block_ref,
1207 Default::default(),
1208 EditDeleteKind::Edit,
1209 &mut new_ast,
1210 )
1211 .await
1212 }
1213
1214 async fn edit_distance_constraint_label_position(
1215 &mut self,
1216 ctx: &ExecutorContext,
1217 _version: Version,
1218 sketch: ObjectId,
1219 constraint_id: ObjectId,
1220 label_position: Point2d<Number>,
1221 anchor_segment_ids: Vec<ObjectId>,
1222 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1223 let sketch_block_ref =
1225 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1226
1227 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1228 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1229 })?;
1230 if !matches!(
1231 &object.kind,
1232 ObjectKind::Constraint {
1233 constraint: Constraint::Distance(_)
1234 | Constraint::HorizontalDistance(_)
1235 | Constraint::VerticalDistance(_),
1236 }
1237 ) {
1238 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1239 "Object is not a distance constraint: {constraint_id:?}"
1240 ))));
1241 }
1242
1243 let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1244 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1245 "Could not convert label position to AST: {err}"
1246 )))
1247 })?;
1248 let mut new_ast = self.program.ast.clone();
1249 self.mutate_ast(
1250 &mut new_ast,
1251 constraint_id,
1252 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1253 )
1254 .map_err(KclErrorWithOutputs::no_outputs)?;
1255
1256 self.execute_after_edit(
1257 ctx,
1258 sketch,
1259 sketch_block_ref,
1260 anchor_segment_ids.into_iter().collect(),
1261 EditDeleteKind::Edit,
1262 &mut new_ast,
1263 )
1264 .await
1265 }
1266
1267 async fn batch_split_segment_operations(
1275 &mut self,
1276 ctx: &ExecutorContext,
1277 _version: Version,
1278 sketch: ObjectId,
1279 edit_segments: Vec<ExistingSegmentCtor>,
1280 add_constraints: Vec<Constraint>,
1281 delete_constraint_ids: Vec<ObjectId>,
1282 _new_segment_info: sketch::NewSegmentInfo,
1283 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1284 let sketch_block_ref =
1286 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1287
1288 let mut new_ast = self.program.ast.clone();
1289 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1290
1291 for segment in edit_segments {
1293 segment_ids_edited.insert(segment.id);
1294 match segment.ctor {
1295 SegmentCtor::Point(ctor) => self
1296 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1297 .map_err(KclErrorWithOutputs::no_outputs)?,
1298 SegmentCtor::Line(ctor) => self
1299 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1300 .map_err(KclErrorWithOutputs::no_outputs)?,
1301 SegmentCtor::Arc(ctor) => self
1302 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1303 .map_err(KclErrorWithOutputs::no_outputs)?,
1304 SegmentCtor::Circle(ctor) => self
1305 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1306 .map_err(KclErrorWithOutputs::no_outputs)?,
1307 }
1308 }
1309
1310 for constraint in add_constraints {
1312 match constraint {
1313 Constraint::Coincident(coincident) => {
1314 self.add_coincident(sketch, coincident, &mut new_ast)
1315 .await
1316 .map_err(KclErrorWithOutputs::no_outputs)?;
1317 }
1318 Constraint::Distance(distance) => {
1319 self.add_distance(sketch, distance, &mut new_ast)
1320 .await
1321 .map_err(KclErrorWithOutputs::no_outputs)?;
1322 }
1323 Constraint::EqualRadius(equal_radius) => {
1324 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1325 .await
1326 .map_err(KclErrorWithOutputs::no_outputs)?;
1327 }
1328 Constraint::Fixed(fixed) => {
1329 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1330 .await
1331 .map_err(KclErrorWithOutputs::no_outputs)?;
1332 }
1333 Constraint::HorizontalDistance(distance) => {
1334 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1335 .await
1336 .map_err(KclErrorWithOutputs::no_outputs)?;
1337 }
1338 Constraint::VerticalDistance(distance) => {
1339 self.add_vertical_distance(sketch, distance, &mut new_ast)
1340 .await
1341 .map_err(KclErrorWithOutputs::no_outputs)?;
1342 }
1343 Constraint::Horizontal(horizontal) => {
1344 self.add_horizontal(sketch, horizontal, &mut new_ast)
1345 .await
1346 .map_err(KclErrorWithOutputs::no_outputs)?;
1347 }
1348 Constraint::LinesEqualLength(lines_equal_length) => {
1349 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1350 .await
1351 .map_err(KclErrorWithOutputs::no_outputs)?;
1352 }
1353 Constraint::Midpoint(midpoint) => {
1354 self.add_midpoint(sketch, midpoint, &mut new_ast)
1355 .await
1356 .map_err(KclErrorWithOutputs::no_outputs)?;
1357 }
1358 Constraint::Parallel(parallel) => {
1359 self.add_parallel(sketch, parallel, &mut new_ast)
1360 .await
1361 .map_err(KclErrorWithOutputs::no_outputs)?;
1362 }
1363 Constraint::Perpendicular(perpendicular) => {
1364 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1365 .await
1366 .map_err(KclErrorWithOutputs::no_outputs)?;
1367 }
1368 Constraint::Vertical(vertical) => {
1369 self.add_vertical(sketch, vertical, &mut new_ast)
1370 .await
1371 .map_err(KclErrorWithOutputs::no_outputs)?;
1372 }
1373 Constraint::Diameter(diameter) => {
1374 self.add_diameter(sketch, diameter, &mut new_ast)
1375 .await
1376 .map_err(KclErrorWithOutputs::no_outputs)?;
1377 }
1378 Constraint::Radius(radius) => {
1379 self.add_radius(sketch, radius, &mut new_ast)
1380 .await
1381 .map_err(KclErrorWithOutputs::no_outputs)?;
1382 }
1383 Constraint::Symmetric(symmetric) => {
1384 self.add_symmetric(sketch, symmetric, &mut new_ast)
1385 .await
1386 .map_err(KclErrorWithOutputs::no_outputs)?;
1387 }
1388 Constraint::Angle(angle) => {
1389 self.add_angle(sketch, angle, &mut new_ast)
1390 .await
1391 .map_err(KclErrorWithOutputs::no_outputs)?;
1392 }
1393 Constraint::Tangent(tangent) => {
1394 self.add_tangent(sketch, tangent, &mut new_ast)
1395 .await
1396 .map_err(KclErrorWithOutputs::no_outputs)?;
1397 }
1398 }
1399 }
1400
1401 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1403
1404 let has_constraint_deletions = !constraint_ids_set.is_empty();
1405 for constraint_id in constraint_ids_set {
1406 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1407 .map_err(KclErrorWithOutputs::no_outputs)?;
1408 }
1409
1410 let (source_delta, mut scene_graph_delta) = self
1414 .execute_after_edit(
1415 ctx,
1416 sketch,
1417 sketch_block_ref,
1418 segment_ids_edited,
1419 EditDeleteKind::Edit,
1420 &mut new_ast,
1421 )
1422 .await?;
1423
1424 if has_constraint_deletions {
1427 scene_graph_delta.invalidates_ids = true;
1428 }
1429
1430 Ok((source_delta, scene_graph_delta))
1431 }
1432
1433 async fn batch_tail_cut_operations(
1434 &mut self,
1435 ctx: &ExecutorContext,
1436 _version: Version,
1437 sketch: ObjectId,
1438 edit_segments: Vec<ExistingSegmentCtor>,
1439 add_constraints: Vec<Constraint>,
1440 delete_constraint_ids: Vec<ObjectId>,
1441 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1442 let sketch_block_ref =
1443 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1444
1445 let mut new_ast = self.program.ast.clone();
1446 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1447
1448 for segment in edit_segments {
1450 segment_ids_edited.insert(segment.id);
1451 match segment.ctor {
1452 SegmentCtor::Point(ctor) => self
1453 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1454 .map_err(KclErrorWithOutputs::no_outputs)?,
1455 SegmentCtor::Line(ctor) => self
1456 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1457 .map_err(KclErrorWithOutputs::no_outputs)?,
1458 SegmentCtor::Arc(ctor) => self
1459 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1460 .map_err(KclErrorWithOutputs::no_outputs)?,
1461 SegmentCtor::Circle(ctor) => self
1462 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1463 .map_err(KclErrorWithOutputs::no_outputs)?,
1464 }
1465 }
1466
1467 for constraint in add_constraints {
1469 match constraint {
1470 Constraint::Coincident(coincident) => {
1471 self.add_coincident(sketch, coincident, &mut new_ast)
1472 .await
1473 .map_err(KclErrorWithOutputs::no_outputs)?;
1474 }
1475 other => {
1476 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1477 "unsupported constraint in tail cut batch: {other:?}"
1478 ))));
1479 }
1480 }
1481 }
1482
1483 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1485
1486 let has_constraint_deletions = !constraint_ids_set.is_empty();
1487 for constraint_id in constraint_ids_set {
1488 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1489 .map_err(KclErrorWithOutputs::no_outputs)?;
1490 }
1491
1492 let (source_delta, mut scene_graph_delta) = self
1496 .execute_after_edit(
1497 ctx,
1498 sketch,
1499 sketch_block_ref,
1500 segment_ids_edited,
1501 EditDeleteKind::Edit,
1502 &mut new_ast,
1503 )
1504 .await?;
1505
1506 if has_constraint_deletions {
1509 scene_graph_delta.invalidates_ids = true;
1510 }
1511
1512 Ok((source_delta, scene_graph_delta))
1513 }
1514}
1515
1516impl FrontendState {
1517 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1518 self.program = program.clone();
1519
1520 self.point_freedom_cache.clear();
1531 match ctx.run_with_caching(program).await {
1532 Ok(outcome) => {
1533 let outcome = self.update_state_after_exec(outcome, true);
1534 let checkpoint_id = self
1535 .create_sketch_checkpoint(outcome.clone())
1536 .await
1537 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1538 Ok(SetProgramOutcome::Success {
1539 scene_graph: Box::new(self.scene_graph.clone()),
1540 exec_outcome: Box::new(outcome),
1541 checkpoint_id: Some(checkpoint_id),
1542 })
1543 }
1544 Err(mut err) => {
1545 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1548 self.update_state_after_exec(outcome, true);
1549 err.scene_graph = Some(self.scene_graph.clone());
1550 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1551 }
1552 }
1553 }
1554
1555 pub async fn engine_execute(
1558 &mut self,
1559 ctx: &ExecutorContext,
1560 program: Program,
1561 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1562 self.program = program.clone();
1563
1564 self.point_freedom_cache.clear();
1568 match ctx.run_with_caching(program).await {
1569 Ok(outcome) => {
1570 let outcome = self.update_state_after_exec(outcome, true);
1571 Ok(SceneGraphDelta {
1572 new_graph: self.scene_graph.clone(),
1573 exec_outcome: outcome,
1574 new_objects: Default::default(),
1576 invalidates_ids: Default::default(),
1578 })
1579 }
1580 Err(mut err) => {
1581 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1583 self.update_state_after_exec(outcome, true);
1584 err.scene_graph = Some(self.scene_graph.clone());
1585 Err(err)
1586 }
1587 }
1588 }
1589
1590 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1591 if matches!(err.error, KclError::EngineHangup { .. }) {
1592 return Err(err);
1596 }
1597
1598 let KclErrorWithOutputs {
1599 error,
1600 mut non_fatal,
1601 variables,
1602 #[cfg(feature = "artifact-graph")]
1603 operations,
1604 #[cfg(feature = "artifact-graph")]
1605 artifact_graph,
1606 #[cfg(feature = "artifact-graph")]
1607 scene_objects,
1608 #[cfg(feature = "artifact-graph")]
1609 source_range_to_object,
1610 #[cfg(feature = "artifact-graph")]
1611 var_solutions,
1612 filenames,
1613 default_planes,
1614 ..
1615 } = err;
1616
1617 if let Some(source_range) = error.source_ranges().first() {
1618 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1619 } else {
1620 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1621 }
1622
1623 Ok(ExecOutcome {
1624 variables,
1625 filenames,
1626 #[cfg(feature = "artifact-graph")]
1627 operations,
1628 #[cfg(feature = "artifact-graph")]
1629 artifact_graph,
1630 #[cfg(feature = "artifact-graph")]
1631 scene_objects,
1632 #[cfg(feature = "artifact-graph")]
1633 source_range_to_object,
1634 #[cfg(feature = "artifact-graph")]
1635 var_solutions,
1636 issues: non_fatal,
1637 default_planes,
1638 })
1639 }
1640
1641 async fn add_point(
1642 &mut self,
1643 ctx: &ExecutorContext,
1644 sketch: ObjectId,
1645 ctor: PointCtor,
1646 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1647 let at_ast = to_ast_point2d(&ctor.position)
1649 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1650 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1651 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1652 unlabeled: None,
1653 arguments: vec![ast::LabeledArg {
1654 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1655 arg: at_ast,
1656 }],
1657 digest: None,
1658 non_code_meta: Default::default(),
1659 })));
1660
1661 let sketch_id = sketch;
1663 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1664 #[cfg(target_arch = "wasm32")]
1665 web_sys::console::error_1(
1666 &format!(
1667 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1668 &self.scene_graph.objects
1669 )
1670 .into(),
1671 );
1672 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1673 })?;
1674 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1675 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1676 "Object is not a sketch, it is {}",
1677 sketch_object.kind.human_friendly_kind_with_article(),
1678 ))));
1679 };
1680 let mut new_ast = self.program.ast.clone();
1682 let (sketch_block_ref, _) = self
1683 .mutate_ast(
1684 &mut new_ast,
1685 sketch_id,
1686 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1687 )
1688 .map_err(KclErrorWithOutputs::no_outputs)?;
1689 let new_source = source_from_ast(&new_ast);
1691 let (new_program, errors) = Program::parse(&new_source)
1693 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1694 if !errors.is_empty() {
1695 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1696 "Error parsing KCL source after adding point: {errors:?}"
1697 ))));
1698 }
1699 let Some(new_program) = new_program else {
1700 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1701 "No AST produced after adding point".to_string(),
1702 )));
1703 };
1704
1705 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1706 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1707 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1708 )))
1709 })?;
1710 #[cfg(not(feature = "artifact-graph"))]
1711 let _ = point_node_ref;
1712
1713 self.program = new_program.clone();
1715
1716 let mut truncated_program = new_program;
1718 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1719 .map_err(KclErrorWithOutputs::no_outputs)?;
1720
1721 let outcome = ctx
1723 .run_mock(
1724 &truncated_program,
1725 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1726 )
1727 .await?;
1728
1729 #[cfg(not(feature = "artifact-graph"))]
1730 let new_object_ids = Vec::new();
1731 #[cfg(feature = "artifact-graph")]
1732 let new_object_ids = {
1733 let make_err =
1734 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1735 let segment_id = outcome
1736 .source_range_to_object
1737 .get(&point_node_ref.range)
1738 .copied()
1739 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1740 let segment_object = outcome
1741 .scene_objects
1742 .get(segment_id.0)
1743 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1744 let ObjectKind::Segment { segment } = &segment_object.kind else {
1745 return Err(make_err(format!(
1746 "Object is not a segment, it is {}",
1747 segment_object.kind.human_friendly_kind_with_article()
1748 )));
1749 };
1750 let Segment::Point(_) = segment else {
1751 return Err(make_err(format!(
1752 "Segment is not a point, it is {}",
1753 segment.human_friendly_kind_with_article()
1754 )));
1755 };
1756 vec![segment_id]
1757 };
1758 let src_delta = SourceDelta { text: new_source };
1759 let outcome = self.update_state_after_exec(outcome, false);
1761 let scene_graph_delta = SceneGraphDelta {
1762 new_graph: self.scene_graph.clone(),
1763 invalidates_ids: false,
1764 new_objects: new_object_ids,
1765 exec_outcome: outcome,
1766 };
1767 Ok((src_delta, scene_graph_delta))
1768 }
1769
1770 async fn add_line(
1771 &mut self,
1772 ctx: &ExecutorContext,
1773 sketch: ObjectId,
1774 ctor: LineCtor,
1775 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1776 let start_ast = to_ast_point2d(&ctor.start)
1778 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1779 let end_ast = to_ast_point2d(&ctor.end)
1780 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1781 let mut arguments = vec![
1782 ast::LabeledArg {
1783 label: Some(ast::Identifier::new(LINE_START_PARAM)),
1784 arg: start_ast,
1785 },
1786 ast::LabeledArg {
1787 label: Some(ast::Identifier::new(LINE_END_PARAM)),
1788 arg: end_ast,
1789 },
1790 ];
1791 if ctor.construction == Some(true) {
1793 arguments.push(ast::LabeledArg {
1794 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1795 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1796 value: ast::LiteralValue::Bool(true),
1797 raw: "true".to_string(),
1798 digest: None,
1799 }))),
1800 });
1801 }
1802 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1803 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1804 unlabeled: None,
1805 arguments,
1806 digest: None,
1807 non_code_meta: Default::default(),
1808 })));
1809
1810 let sketch_id = sketch;
1812 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1813 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1814 })?;
1815 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1816 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1817 "Object is not a sketch, it is {}",
1818 sketch_object.kind.human_friendly_kind_with_article(),
1819 ))));
1820 };
1821 let mut new_ast = self.program.ast.clone();
1823 let (sketch_block_ref, _) = self
1824 .mutate_ast(
1825 &mut new_ast,
1826 sketch_id,
1827 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1828 )
1829 .map_err(KclErrorWithOutputs::no_outputs)?;
1830 let new_source = source_from_ast(&new_ast);
1832 let (new_program, errors) = Program::parse(&new_source)
1834 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1835 if !errors.is_empty() {
1836 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1837 "Error parsing KCL source after adding line: {errors:?}"
1838 ))));
1839 }
1840 let Some(new_program) = new_program else {
1841 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1842 "No AST produced after adding line".to_string(),
1843 )));
1844 };
1845
1846 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1847 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1848 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1849 )))
1850 })?;
1851 #[cfg(not(feature = "artifact-graph"))]
1852 let _ = line_node_ref;
1853
1854 self.program = new_program.clone();
1856
1857 let mut truncated_program = new_program;
1859 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1860 .map_err(KclErrorWithOutputs::no_outputs)?;
1861
1862 let outcome = ctx
1864 .run_mock(
1865 &truncated_program,
1866 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1867 )
1868 .await?;
1869
1870 #[cfg(not(feature = "artifact-graph"))]
1871 let new_object_ids = Vec::new();
1872 #[cfg(feature = "artifact-graph")]
1873 let new_object_ids = {
1874 let make_err =
1875 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1876 let segment_id = outcome
1877 .source_range_to_object
1878 .get(&line_node_ref.range)
1879 .copied()
1880 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1881 let segment_object = outcome
1882 .scene_object_by_id(segment_id)
1883 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1884 let ObjectKind::Segment { segment } = &segment_object.kind else {
1885 return Err(make_err(format!(
1886 "Object is not a segment, it is {}",
1887 segment_object.kind.human_friendly_kind_with_article()
1888 )));
1889 };
1890 let Segment::Line(line) = segment else {
1891 return Err(make_err(format!(
1892 "Segment is not a line, it is {}",
1893 segment.human_friendly_kind_with_article()
1894 )));
1895 };
1896 vec![line.start, line.end, segment_id]
1897 };
1898 let src_delta = SourceDelta { text: new_source };
1899 let outcome = self.update_state_after_exec(outcome, false);
1901 let scene_graph_delta = SceneGraphDelta {
1902 new_graph: self.scene_graph.clone(),
1903 invalidates_ids: false,
1904 new_objects: new_object_ids,
1905 exec_outcome: outcome,
1906 };
1907 Ok((src_delta, scene_graph_delta))
1908 }
1909
1910 async fn add_arc(
1911 &mut self,
1912 ctx: &ExecutorContext,
1913 sketch: ObjectId,
1914 ctor: ArcCtor,
1915 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1916 let start_ast = to_ast_point2d(&ctor.start)
1918 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1919 let end_ast = to_ast_point2d(&ctor.end)
1920 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1921 let center_ast = to_ast_point2d(&ctor.center)
1922 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1923 let mut arguments = vec![
1924 ast::LabeledArg {
1925 label: Some(ast::Identifier::new(ARC_START_PARAM)),
1926 arg: start_ast,
1927 },
1928 ast::LabeledArg {
1929 label: Some(ast::Identifier::new(ARC_END_PARAM)),
1930 arg: end_ast,
1931 },
1932 ast::LabeledArg {
1933 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1934 arg: center_ast,
1935 },
1936 ];
1937 if ctor.construction == Some(true) {
1939 arguments.push(ast::LabeledArg {
1940 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1941 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1942 value: ast::LiteralValue::Bool(true),
1943 raw: "true".to_string(),
1944 digest: None,
1945 }))),
1946 });
1947 }
1948 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1949 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1950 unlabeled: None,
1951 arguments,
1952 digest: None,
1953 non_code_meta: Default::default(),
1954 })));
1955
1956 let sketch_id = sketch;
1958 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1959 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1960 })?;
1961 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1962 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1963 "Object is not a sketch, it is {}",
1964 sketch_object.kind.human_friendly_kind_with_article(),
1965 ))));
1966 };
1967 let mut new_ast = self.program.ast.clone();
1969 let (sketch_block_ref, _) = self
1970 .mutate_ast(
1971 &mut new_ast,
1972 sketch_id,
1973 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1974 )
1975 .map_err(KclErrorWithOutputs::no_outputs)?;
1976 let new_source = source_from_ast(&new_ast);
1978 let (new_program, errors) = Program::parse(&new_source)
1980 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1981 if !errors.is_empty() {
1982 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1983 "Error parsing KCL source after adding arc: {errors:?}"
1984 ))));
1985 }
1986 let Some(new_program) = new_program else {
1987 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1988 "No AST produced after adding arc".to_string(),
1989 )));
1990 };
1991
1992 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1993 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1994 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
1995 )))
1996 })?;
1997 #[cfg(not(feature = "artifact-graph"))]
1998 let _ = arc_node_ref;
1999
2000 self.program = new_program.clone();
2002
2003 let mut truncated_program = new_program;
2005 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2006 .map_err(KclErrorWithOutputs::no_outputs)?;
2007
2008 let outcome = ctx
2010 .run_mock(
2011 &truncated_program,
2012 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2013 )
2014 .await?;
2015
2016 #[cfg(not(feature = "artifact-graph"))]
2017 let new_object_ids = Vec::new();
2018 #[cfg(feature = "artifact-graph")]
2019 let new_object_ids = {
2020 let make_err =
2021 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2022 let segment_id = outcome
2023 .source_range_to_object
2024 .get(&arc_node_ref.range)
2025 .copied()
2026 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2027 let segment_object = outcome
2028 .scene_objects
2029 .get(segment_id.0)
2030 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2031 let ObjectKind::Segment { segment } = &segment_object.kind else {
2032 return Err(make_err(format!(
2033 "Object is not a segment, it is {}",
2034 segment_object.kind.human_friendly_kind_with_article()
2035 )));
2036 };
2037 let Segment::Arc(arc) = segment else {
2038 return Err(make_err(format!(
2039 "Segment is not an arc, it is {}",
2040 segment.human_friendly_kind_with_article()
2041 )));
2042 };
2043 vec![arc.start, arc.end, arc.center, segment_id]
2044 };
2045 let src_delta = SourceDelta { text: new_source };
2046 let outcome = self.update_state_after_exec(outcome, false);
2048 let scene_graph_delta = SceneGraphDelta {
2049 new_graph: self.scene_graph.clone(),
2050 invalidates_ids: false,
2051 new_objects: new_object_ids,
2052 exec_outcome: outcome,
2053 };
2054 Ok((src_delta, scene_graph_delta))
2055 }
2056
2057 async fn add_circle(
2058 &mut self,
2059 ctx: &ExecutorContext,
2060 sketch: ObjectId,
2061 ctor: CircleCtor,
2062 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2063 let start_ast = to_ast_point2d(&ctor.start)
2065 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2066 let center_ast = to_ast_point2d(&ctor.center)
2067 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2068 let mut arguments = vec![
2069 ast::LabeledArg {
2070 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2071 arg: start_ast,
2072 },
2073 ast::LabeledArg {
2074 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2075 arg: center_ast,
2076 },
2077 ];
2078 if ctor.construction == Some(true) {
2080 arguments.push(ast::LabeledArg {
2081 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2082 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2083 value: ast::LiteralValue::Bool(true),
2084 raw: "true".to_string(),
2085 digest: None,
2086 }))),
2087 });
2088 }
2089 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2090 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2091 unlabeled: None,
2092 arguments,
2093 digest: None,
2094 non_code_meta: Default::default(),
2095 })));
2096
2097 let sketch_id = sketch;
2099 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2100 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2101 })?;
2102 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2103 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2104 "Object is not a sketch, it is {}",
2105 sketch_object.kind.human_friendly_kind_with_article(),
2106 ))));
2107 };
2108 let mut new_ast = self.program.ast.clone();
2110 let (sketch_block_ref, _) = self
2111 .mutate_ast(
2112 &mut new_ast,
2113 sketch_id,
2114 AstMutateCommand::AddSketchBlockVarDecl {
2115 prefix: CIRCLE_VARIABLE.to_owned(),
2116 expr: circle_ast,
2117 },
2118 )
2119 .map_err(KclErrorWithOutputs::no_outputs)?;
2120 let new_source = source_from_ast(&new_ast);
2122 let (new_program, errors) = Program::parse(&new_source)
2124 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2125 if !errors.is_empty() {
2126 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2127 "Error parsing KCL source after adding circle: {errors:?}"
2128 ))));
2129 }
2130 let Some(new_program) = new_program else {
2131 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2132 "No AST produced after adding circle".to_string(),
2133 )));
2134 };
2135
2136 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2137 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2138 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2139 )))
2140 })?;
2141 #[cfg(not(feature = "artifact-graph"))]
2142 let _ = circle_node_ref;
2143
2144 self.program = new_program.clone();
2146
2147 let mut truncated_program = new_program;
2149 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2150 .map_err(KclErrorWithOutputs::no_outputs)?;
2151
2152 let outcome = ctx
2154 .run_mock(
2155 &truncated_program,
2156 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2157 )
2158 .await?;
2159
2160 #[cfg(not(feature = "artifact-graph"))]
2161 let new_object_ids = Vec::new();
2162 #[cfg(feature = "artifact-graph")]
2163 let new_object_ids = {
2164 let make_err =
2165 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2166 let segment_id = outcome
2167 .source_range_to_object
2168 .get(&circle_node_ref.range)
2169 .copied()
2170 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2171 let segment_object = outcome
2172 .scene_objects
2173 .get(segment_id.0)
2174 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2175 let ObjectKind::Segment { segment } = &segment_object.kind else {
2176 return Err(make_err(format!(
2177 "Object is not a segment, it is {}",
2178 segment_object.kind.human_friendly_kind_with_article()
2179 )));
2180 };
2181 let Segment::Circle(circle) = segment else {
2182 return Err(make_err(format!(
2183 "Segment is not a circle, it is {}",
2184 segment.human_friendly_kind_with_article()
2185 )));
2186 };
2187 vec![circle.start, circle.center, segment_id]
2188 };
2189 let src_delta = SourceDelta { text: new_source };
2190 let outcome = self.update_state_after_exec(outcome, false);
2192 let scene_graph_delta = SceneGraphDelta {
2193 new_graph: self.scene_graph.clone(),
2194 invalidates_ids: false,
2195 new_objects: new_object_ids,
2196 exec_outcome: outcome,
2197 };
2198 Ok((src_delta, scene_graph_delta))
2199 }
2200
2201 fn edit_point(
2202 &mut self,
2203 new_ast: &mut ast::Node<ast::Program>,
2204 sketch: ObjectId,
2205 point: ObjectId,
2206 ctor: PointCtor,
2207 ) -> Result<(), KclError> {
2208 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2210
2211 let sketch_id = sketch;
2213 let sketch_object = self
2214 .scene_graph
2215 .objects
2216 .get(sketch_id.0)
2217 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2218 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2219 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2220 };
2221 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2222 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2223 })?;
2224 let point_id = point;
2226 let point_object = self
2227 .scene_graph
2228 .objects
2229 .get(point_id.0)
2230 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2231 let ObjectKind::Segment {
2232 segment: Segment::Point(point),
2233 } = &point_object.kind
2234 else {
2235 return Err(KclError::refactor(format!(
2236 "Object is not a point segment: {point_object:?}"
2237 )));
2238 };
2239
2240 if let Some(owner_id) = point.owner {
2242 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2243 KclError::refactor(format!(
2244 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2245 ))
2246 })?;
2247 let ObjectKind::Segment { segment } = &owner_object.kind else {
2248 return Err(KclError::refactor(format!(
2249 "Internal: Owner of point is not a segment, but found {}",
2250 owner_object.kind.human_friendly_kind_with_article()
2251 )));
2252 };
2253
2254 if let Segment::Line(line) = segment {
2256 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2257 return Err(KclError::refactor(format!(
2258 "Internal: Owner of point does not have line ctor, but found {}",
2259 line.ctor.human_friendly_kind_with_article()
2260 )));
2261 };
2262 let mut line_ctor = line_ctor.clone();
2263 if line.start == point_id {
2265 line_ctor.start = ctor.position;
2266 } else if line.end == point_id {
2267 line_ctor.end = ctor.position;
2268 } else {
2269 return Err(KclError::refactor(format!(
2270 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2271 )));
2272 }
2273 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2274 }
2275
2276 if let Segment::Arc(arc) = segment {
2278 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2279 return Err(KclError::refactor(format!(
2280 "Internal: Owner of point does not have arc ctor, but found {}",
2281 arc.ctor.human_friendly_kind_with_article()
2282 )));
2283 };
2284 let mut arc_ctor = arc_ctor.clone();
2285 if arc.center == point_id {
2287 arc_ctor.center = ctor.position;
2288 } else if arc.start == point_id {
2289 arc_ctor.start = ctor.position;
2290 } else if arc.end == point_id {
2291 arc_ctor.end = ctor.position;
2292 } else {
2293 return Err(KclError::refactor(format!(
2294 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2295 )));
2296 }
2297 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2298 }
2299
2300 if let Segment::Circle(circle) = segment {
2302 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2303 return Err(KclError::refactor(format!(
2304 "Internal: Owner of point does not have circle ctor, but found {}",
2305 circle.ctor.human_friendly_kind_with_article()
2306 )));
2307 };
2308 let mut circle_ctor = circle_ctor.clone();
2309 if circle.center == point_id {
2310 circle_ctor.center = ctor.position;
2311 } else if circle.start == point_id {
2312 circle_ctor.start = ctor.position;
2313 } else {
2314 return Err(KclError::refactor(format!(
2315 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2316 )));
2317 }
2318 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2319 }
2320
2321 }
2324
2325 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2327 Ok(())
2328 }
2329
2330 fn edit_line(
2331 &mut self,
2332 new_ast: &mut ast::Node<ast::Program>,
2333 sketch: ObjectId,
2334 line: ObjectId,
2335 ctor: LineCtor,
2336 ) -> Result<(), KclError> {
2337 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2339 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2340
2341 let sketch_id = sketch;
2343 let sketch_object = self
2344 .scene_graph
2345 .objects
2346 .get(sketch_id.0)
2347 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2348 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2349 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2350 };
2351 sketch
2352 .segments
2353 .iter()
2354 .find(|o| **o == line)
2355 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2356 let line_id = line;
2358 let line_object = self
2359 .scene_graph
2360 .objects
2361 .get(line_id.0)
2362 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2363 let ObjectKind::Segment { .. } = &line_object.kind else {
2364 let kind = line_object.kind.human_friendly_kind_with_article();
2365 return Err(KclError::refactor(format!(
2366 "This constraint only works on Segments, but you selected {kind}"
2367 )));
2368 };
2369
2370 self.mutate_ast(
2372 new_ast,
2373 line_id,
2374 AstMutateCommand::EditLine {
2375 start: new_start_ast,
2376 end: new_end_ast,
2377 construction: ctor.construction,
2378 },
2379 )?;
2380 Ok(())
2381 }
2382
2383 fn edit_arc(
2384 &mut self,
2385 new_ast: &mut ast::Node<ast::Program>,
2386 sketch: ObjectId,
2387 arc: ObjectId,
2388 ctor: ArcCtor,
2389 ) -> Result<(), KclError> {
2390 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2392 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2393 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2394
2395 let sketch_id = sketch;
2397 let sketch_object = self
2398 .scene_graph
2399 .objects
2400 .get(sketch_id.0)
2401 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2402 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2403 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2404 };
2405 sketch
2406 .segments
2407 .iter()
2408 .find(|o| **o == arc)
2409 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2410 let arc_id = arc;
2412 let arc_object = self
2413 .scene_graph
2414 .objects
2415 .get(arc_id.0)
2416 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2417 let ObjectKind::Segment { .. } = &arc_object.kind else {
2418 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2419 };
2420
2421 self.mutate_ast(
2423 new_ast,
2424 arc_id,
2425 AstMutateCommand::EditArc {
2426 start: new_start_ast,
2427 end: new_end_ast,
2428 center: new_center_ast,
2429 construction: ctor.construction,
2430 },
2431 )?;
2432 Ok(())
2433 }
2434
2435 fn edit_circle(
2436 &mut self,
2437 new_ast: &mut ast::Node<ast::Program>,
2438 sketch: ObjectId,
2439 circle: ObjectId,
2440 ctor: CircleCtor,
2441 ) -> Result<(), KclError> {
2442 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2444 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2445
2446 let sketch_id = sketch;
2448 let sketch_object = self
2449 .scene_graph
2450 .objects
2451 .get(sketch_id.0)
2452 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2453 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2454 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2455 };
2456 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2457 KclError::refactor(format!(
2458 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2459 ))
2460 })?;
2461 let circle_id = circle;
2463 let circle_object = self
2464 .scene_graph
2465 .objects
2466 .get(circle_id.0)
2467 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2468 let ObjectKind::Segment { .. } = &circle_object.kind else {
2469 return Err(KclError::refactor(format!(
2470 "Object is not a segment: {circle_object:?}"
2471 )));
2472 };
2473
2474 self.mutate_ast(
2476 new_ast,
2477 circle_id,
2478 AstMutateCommand::EditCircle {
2479 start: new_start_ast,
2480 center: new_center_ast,
2481 construction: ctor.construction,
2482 },
2483 )?;
2484 Ok(())
2485 }
2486
2487 fn delete_segment(
2488 &mut self,
2489 new_ast: &mut ast::Node<ast::Program>,
2490 sketch: ObjectId,
2491 segment_id: ObjectId,
2492 ) -> Result<(), KclError> {
2493 let sketch_id = sketch;
2495 let sketch_object = self
2496 .scene_graph
2497 .objects
2498 .get(sketch_id.0)
2499 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2500 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2501 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2502 };
2503 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2504 KclError::refactor(format!(
2505 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2506 ))
2507 })?;
2508 let segment_object =
2510 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2511 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2512 })?;
2513 let ObjectKind::Segment { .. } = &segment_object.kind else {
2514 return Err(KclError::refactor(format!(
2515 "Object is not a segment, it is {}",
2516 segment_object.kind.human_friendly_kind_with_article()
2517 )));
2518 };
2519
2520 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2522 Ok(())
2523 }
2524
2525 fn delete_constraint(
2526 &mut self,
2527 new_ast: &mut ast::Node<ast::Program>,
2528 sketch: ObjectId,
2529 constraint_id: ObjectId,
2530 ) -> Result<(), KclError> {
2531 let sketch_id = sketch;
2533 let sketch_object = self
2534 .scene_graph
2535 .objects
2536 .get(sketch_id.0)
2537 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2538 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2539 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2540 };
2541 sketch
2542 .constraints
2543 .iter()
2544 .find(|o| **o == constraint_id)
2545 .ok_or_else(|| {
2546 KclError::refactor(format!(
2547 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2548 ))
2549 })?;
2550 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2552 KclError::refactor(format!(
2553 "Constraint not found in scene graph: constraint={constraint_id:?}"
2554 ))
2555 })?;
2556 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2557 return Err(KclError::refactor(format!(
2558 "Object is not a constraint, it is {}",
2559 constraint_object.kind.human_friendly_kind_with_article()
2560 )));
2561 };
2562
2563 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2565 Ok(())
2566 }
2567
2568 fn edit_coincident_constraint(
2569 &mut self,
2570 new_ast: &mut ast::Node<ast::Program>,
2571 constraint_id: ObjectId,
2572 segments: Vec<ConstraintSegment>,
2573 ) -> Result<(), KclError> {
2574 if segments.len() < 2 {
2575 return Err(KclError::refactor(format!(
2576 "Coincident constraint must have at least 2 inputs, got {}",
2577 segments.len()
2578 )));
2579 }
2580
2581 let segment_asts = segments
2582 .iter()
2583 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2584 .collect::<Result<Vec<_>, _>>()?;
2585
2586 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2587 elements: segment_asts,
2588 digest: None,
2589 non_code_meta: Default::default(),
2590 })));
2591
2592 self.mutate_ast(
2593 new_ast,
2594 constraint_id,
2595 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2596 )?;
2597 Ok(())
2598 }
2599
2600 fn edit_horizontal_points_constraint(
2601 &mut self,
2602 new_ast: &mut ast::Node<ast::Program>,
2603 constraint_id: ObjectId,
2604 points: Vec<ConstraintSegment>,
2605 ) -> Result<(), KclError> {
2606 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2607 }
2608
2609 fn edit_vertical_points_constraint(
2610 &mut self,
2611 new_ast: &mut ast::Node<ast::Program>,
2612 constraint_id: ObjectId,
2613 points: Vec<ConstraintSegment>,
2614 ) -> Result<(), KclError> {
2615 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2616 }
2617
2618 fn edit_axis_points_constraint(
2619 &mut self,
2620 new_ast: &mut ast::Node<ast::Program>,
2621 constraint_id: ObjectId,
2622 points: Vec<ConstraintSegment>,
2623 constraint_name: &str,
2624 ) -> Result<(), KclError> {
2625 if points.len() < 2 {
2626 return Err(KclError::refactor(format!(
2627 "{constraint_name} points constraint must have at least 2 points, got {}",
2628 points.len()
2629 )));
2630 }
2631
2632 let point_asts = points
2633 .iter()
2634 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2635 .collect::<Result<Vec<_>, _>>()?;
2636
2637 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2638 elements: point_asts,
2639 digest: None,
2640 non_code_meta: Default::default(),
2641 })));
2642
2643 self.mutate_ast(
2644 new_ast,
2645 constraint_id,
2646 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2647 )?;
2648 Ok(())
2649 }
2650
2651 fn edit_equal_length_constraint(
2653 &mut self,
2654 new_ast: &mut ast::Node<ast::Program>,
2655 constraint_id: ObjectId,
2656 lines: Vec<ObjectId>,
2657 ) -> Result<(), KclError> {
2658 if lines.len() < 2 {
2659 return Err(KclError::refactor(format!(
2660 "Lines equal length constraint must have at least 2 lines, got {}",
2661 lines.len()
2662 )));
2663 }
2664
2665 let line_asts = lines
2666 .iter()
2667 .map(|line_id| {
2668 let line_object = self
2669 .scene_graph
2670 .objects
2671 .get(line_id.0)
2672 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2673 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2674 let kind = line_object.kind.human_friendly_kind_with_article();
2675 return Err(KclError::refactor(format!(
2676 "This constraint only works on Segments, but you selected {kind}"
2677 )));
2678 };
2679 let Segment::Line(_) = line_segment else {
2680 let kind = line_segment.human_friendly_kind_with_article();
2681 return Err(KclError::refactor(format!(
2682 "Only lines can be made equal length, but you selected {kind}"
2683 )));
2684 };
2685
2686 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2687 })
2688 .collect::<Result<Vec<_>, _>>()?;
2689
2690 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2691 elements: line_asts,
2692 digest: None,
2693 non_code_meta: Default::default(),
2694 })));
2695
2696 self.mutate_ast(
2697 new_ast,
2698 constraint_id,
2699 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2700 )?;
2701 Ok(())
2702 }
2703
2704 fn edit_parallel_constraint(
2706 &mut self,
2707 new_ast: &mut ast::Node<ast::Program>,
2708 constraint_id: ObjectId,
2709 lines: Vec<ObjectId>,
2710 ) -> Result<(), KclError> {
2711 if lines.len() < 2 {
2712 return Err(KclError::refactor(format!(
2713 "Parallel constraint must have at least 2 lines, got {}",
2714 lines.len()
2715 )));
2716 }
2717
2718 let line_asts = lines
2719 .iter()
2720 .map(|line_id| {
2721 let line_object = self
2722 .scene_graph
2723 .objects
2724 .get(line_id.0)
2725 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2726 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2727 let kind = line_object.kind.human_friendly_kind_with_article();
2728 return Err(KclError::refactor(format!(
2729 "This constraint only works on Segments, but you selected {kind}"
2730 )));
2731 };
2732 let Segment::Line(_) = line_segment else {
2733 let kind = line_segment.human_friendly_kind_with_article();
2734 return Err(KclError::refactor(format!(
2735 "Only lines can be made parallel, but you selected {kind}"
2736 )));
2737 };
2738
2739 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2740 })
2741 .collect::<Result<Vec<_>, _>>()?;
2742
2743 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2744 elements: line_asts,
2745 digest: None,
2746 non_code_meta: Default::default(),
2747 })));
2748
2749 self.mutate_ast(
2750 new_ast,
2751 constraint_id,
2752 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2753 )?;
2754 Ok(())
2755 }
2756
2757 fn edit_equal_radius_constraint(
2759 &mut self,
2760 new_ast: &mut ast::Node<ast::Program>,
2761 constraint_id: ObjectId,
2762 input: Vec<ObjectId>,
2763 ) -> Result<(), KclError> {
2764 if input.len() < 2 {
2765 return Err(KclError::refactor(format!(
2766 "equalRadius constraint must have at least 2 segments, got {}",
2767 input.len()
2768 )));
2769 }
2770
2771 let input_asts = input
2772 .iter()
2773 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2774 .collect::<Result<Vec<_>, _>>()?;
2775
2776 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2777 elements: input_asts,
2778 digest: None,
2779 non_code_meta: Default::default(),
2780 })));
2781
2782 self.mutate_ast(
2783 new_ast,
2784 constraint_id,
2785 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2786 )?;
2787 Ok(())
2788 }
2789
2790 async fn execute_after_edit(
2791 &mut self,
2792 ctx: &ExecutorContext,
2793 sketch: ObjectId,
2794 sketch_block_ref: AstNodeRef,
2795 segment_ids_edited: AhashIndexSet<ObjectId>,
2796 edit_kind: EditDeleteKind,
2797 new_ast: &mut ast::Node<ast::Program>,
2798 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2799 let new_source = source_from_ast(new_ast);
2801 let (new_program, errors) = Program::parse(&new_source)
2803 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2804 if !errors.is_empty() {
2805 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2806 "Error parsing KCL source after editing: {errors:?}"
2807 ))));
2808 }
2809 let Some(new_program) = new_program else {
2810 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2811 "No AST produced after editing".to_string(),
2812 )));
2813 };
2814
2815 self.program = new_program.clone();
2817
2818 let is_delete = edit_kind.is_delete();
2820 let truncated_program = {
2821 let mut truncated_program = new_program;
2822 only_sketch_block(
2823 &mut truncated_program.ast,
2824 &sketch_block_ref,
2825 edit_kind.to_change_kind(),
2826 )
2827 .map_err(KclErrorWithOutputs::no_outputs)?;
2828 truncated_program
2829 };
2830
2831 #[cfg(not(feature = "artifact-graph"))]
2832 drop(segment_ids_edited);
2833
2834 let mock_config = MockConfig {
2836 sketch_block_id: Some(sketch),
2837 freedom_analysis: is_delete,
2838 #[cfg(feature = "artifact-graph")]
2839 segment_ids_edited: segment_ids_edited.clone(),
2840 ..Default::default()
2841 };
2842 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2843
2844 let outcome = self.update_state_after_exec(outcome, is_delete);
2846
2847 #[cfg(feature = "artifact-graph")]
2848 let new_source = {
2849 let mut new_ast = self.program.ast.clone();
2854 for (var_range, value) in &outcome.var_solutions {
2855 let rounded = value.round(3);
2856 let source_ref = SourceRef::Simple {
2857 range: *var_range,
2858 node_path: None,
2859 };
2860 mutate_ast_node_by_source_ref(
2861 &mut new_ast,
2862 &source_ref,
2863 AstMutateCommand::EditVarInitialValue { value: rounded },
2864 )
2865 .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2866 }
2867 source_from_ast(&new_ast)
2868 };
2869
2870 let src_delta = SourceDelta { text: new_source };
2871 let scene_graph_delta = SceneGraphDelta {
2872 new_graph: self.scene_graph.clone(),
2873 invalidates_ids: is_delete,
2874 new_objects: Vec::new(),
2875 exec_outcome: outcome,
2876 };
2877 Ok((src_delta, scene_graph_delta))
2878 }
2879
2880 async fn execute_after_delete_sketch(
2881 &mut self,
2882 ctx: &ExecutorContext,
2883 new_ast: &mut ast::Node<ast::Program>,
2884 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2885 let new_source = source_from_ast(new_ast);
2887 let (new_program, errors) = Program::parse(&new_source)
2889 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2890 if !errors.is_empty() {
2891 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2892 "Error parsing KCL source after editing: {errors:?}"
2893 ))));
2894 }
2895 let Some(new_program) = new_program else {
2896 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2897 "No AST produced after editing".to_string(),
2898 )));
2899 };
2900
2901 self.program = new_program.clone();
2903
2904 let outcome = ctx.run_with_caching(new_program).await?;
2910 let freedom_analysis_ran = true;
2911
2912 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2913
2914 let src_delta = SourceDelta { text: new_source };
2915 let scene_graph_delta = SceneGraphDelta {
2916 new_graph: self.scene_graph.clone(),
2917 invalidates_ids: true,
2918 new_objects: Vec::new(),
2919 exec_outcome: outcome,
2920 };
2921 Ok((src_delta, scene_graph_delta))
2922 }
2923
2924 fn point_id_to_ast_reference(
2929 &self,
2930 point_id: ObjectId,
2931 new_ast: &mut ast::Node<ast::Program>,
2932 ) -> Result<ast::Expr, KclError> {
2933 let point_object = self
2934 .scene_graph
2935 .objects
2936 .get(point_id.0)
2937 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2938 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2939 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2940 };
2941 let Segment::Point(point) = point_segment else {
2942 return Err(KclError::refactor(format!(
2943 "Only points are currently supported: {point_object:?}"
2944 )));
2945 };
2946
2947 if let Some(owner_id) = point.owner {
2948 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2949 KclError::refactor(format!(
2950 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2951 ))
2952 })?;
2953 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2954 return Err(KclError::refactor(format!(
2955 "Owner of point is not a segment, but found {}",
2956 owner_object.kind.human_friendly_kind_with_article()
2957 )));
2958 };
2959
2960 match owner_segment {
2961 Segment::Line(line) => {
2962 let property = if line.start == point_id {
2963 LINE_PROPERTY_START
2964 } else if line.end == point_id {
2965 LINE_PROPERTY_END
2966 } else {
2967 return Err(KclError::refactor(format!(
2968 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2969 )));
2970 };
2971 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
2972 }
2973 Segment::Arc(arc) => {
2974 let property = if arc.start == point_id {
2975 ARC_PROPERTY_START
2976 } else if arc.end == point_id {
2977 ARC_PROPERTY_END
2978 } else if arc.center == point_id {
2979 ARC_PROPERTY_CENTER
2980 } else {
2981 return Err(KclError::refactor(format!(
2982 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2983 )));
2984 };
2985 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
2986 }
2987 Segment::Circle(circle) => {
2988 let property = if circle.start == point_id {
2989 CIRCLE_PROPERTY_START
2990 } else if circle.center == point_id {
2991 CIRCLE_PROPERTY_CENTER
2992 } else {
2993 return Err(KclError::refactor(format!(
2994 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2995 )));
2996 };
2997 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
2998 }
2999 _ => Err(KclError::refactor(format!(
3000 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3001 ))),
3002 }
3003 } else {
3004 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3006 }
3007 }
3008
3009 fn coincident_segment_to_ast(
3010 &self,
3011 segment: &ConstraintSegment,
3012 new_ast: &mut ast::Node<ast::Program>,
3013 ) -> Result<ast::Expr, KclError> {
3014 match segment {
3015 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3016 ConstraintSegment::Segment(segment_id) => {
3017 let segment_object = self
3018 .scene_graph
3019 .objects
3020 .get(segment_id.0)
3021 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3022 let ObjectKind::Segment { segment } = &segment_object.kind else {
3023 return Err(KclError::refactor(format!(
3024 "Object is not a segment, it is {}",
3025 segment_object.kind.human_friendly_kind_with_article()
3026 )));
3027 };
3028
3029 match segment {
3030 Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
3031 Segment::Line(_) => {
3032 get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None)
3033 }
3034 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3035 Segment::Circle(_) => {
3036 get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
3037 }
3038 }
3039 }
3040 }
3041 }
3042
3043 fn axis_constraint_segment_to_ast(
3044 &self,
3045 segment: &ConstraintSegment,
3046 new_ast: &mut ast::Node<ast::Program>,
3047 ) -> Result<ast::Expr, KclError> {
3048 match segment {
3049 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3050 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3051 }
3052 }
3053
3054 async fn add_coincident(
3055 &mut self,
3056 sketch: ObjectId,
3057 coincident: Coincident,
3058 new_ast: &mut ast::Node<ast::Program>,
3059 ) -> Result<AstNodeRef, KclError> {
3060 let sketch_id = sketch;
3061 let segment_asts = coincident
3062 .segments
3063 .iter()
3064 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3065 .collect::<Result<Vec<_>, _>>()?;
3066 if segment_asts.len() < 2 {
3067 return Err(KclError::refactor(format!(
3068 "Coincident constraint must have at least 2 inputs, got {}",
3069 segment_asts.len()
3070 )));
3071 }
3072
3073 let coincident_ast = create_coincident_ast(segment_asts);
3075
3076 let (sketch_block_ref, _) = self.mutate_ast(
3078 new_ast,
3079 sketch_id,
3080 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3081 )?;
3082 Ok(sketch_block_ref)
3083 }
3084
3085 async fn add_distance(
3086 &mut self,
3087 sketch: ObjectId,
3088 distance: Distance,
3089 new_ast: &mut ast::Node<ast::Program>,
3090 ) -> Result<AstNodeRef, KclError> {
3091 let sketch_id = sketch;
3092 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3093 [pt0, pt1] => [
3094 self.coincident_segment_to_ast(pt0, new_ast)?,
3095 self.coincident_segment_to_ast(pt1, new_ast)?,
3096 ],
3097 _ => {
3098 return Err(KclError::refactor(format!(
3099 "Distance constraint must have exactly 2 points, got {}",
3100 distance.points.len()
3101 )));
3102 }
3103 };
3104
3105 let arguments = match &distance.label_position {
3106 Some(label_position) => vec![ast::LabeledArg {
3107 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3108 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3109 }],
3110 None => Default::default(),
3111 };
3112
3113 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3115 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3116 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3117 ast::ArrayExpression {
3118 elements: vec![pt0_ast, pt1_ast],
3119 digest: None,
3120 non_code_meta: Default::default(),
3121 },
3122 )))),
3123 arguments,
3124 digest: None,
3125 non_code_meta: Default::default(),
3126 })));
3127 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3128 left: distance_call_ast,
3129 operator: ast::BinaryOperator::Eq,
3130 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3131 value: ast::LiteralValue::Number {
3132 value: distance.distance.value,
3133 suffix: distance.distance.units,
3134 },
3135 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3136 KclError::refactor(format!(
3137 "Could not format numeric suffix: {:?}",
3138 distance.distance.units
3139 ))
3140 })?,
3141 digest: None,
3142 }))),
3143 digest: None,
3144 })));
3145
3146 let (sketch_block_ref, _) = self.mutate_ast(
3148 new_ast,
3149 sketch_id,
3150 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3151 )?;
3152 Ok(sketch_block_ref)
3153 }
3154
3155 async fn add_angle(
3156 &mut self,
3157 sketch: ObjectId,
3158 angle: Angle,
3159 new_ast: &mut ast::Node<ast::Program>,
3160 ) -> Result<AstNodeRef, KclError> {
3161 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3162 return Err(KclError::refactor(format!(
3163 "Angle constraint must have exactly 2 lines, got {}",
3164 angle.lines.len()
3165 )));
3166 };
3167 let sketch_id = sketch;
3168
3169 let line0_object = self
3171 .scene_graph
3172 .objects
3173 .get(l0_id.0)
3174 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3175 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3176 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3177 };
3178 let Segment::Line(_) = line0_segment else {
3179 return Err(KclError::refactor(format!(
3180 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3181 )));
3182 };
3183 let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3184
3185 let line1_object = self
3186 .scene_graph
3187 .objects
3188 .get(l1_id.0)
3189 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3190 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3191 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3192 };
3193 let Segment::Line(_) = line1_segment else {
3194 return Err(KclError::refactor(format!(
3195 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3196 )));
3197 };
3198 let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3199
3200 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3202 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3203 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3204 ast::ArrayExpression {
3205 elements: vec![l0_ast, l1_ast],
3206 digest: None,
3207 non_code_meta: Default::default(),
3208 },
3209 )))),
3210 arguments: Default::default(),
3211 digest: None,
3212 non_code_meta: Default::default(),
3213 })));
3214 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3215 left: angle_call_ast,
3216 operator: ast::BinaryOperator::Eq,
3217 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3218 value: ast::LiteralValue::Number {
3219 value: angle.angle.value,
3220 suffix: angle.angle.units,
3221 },
3222 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3223 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3224 })?,
3225 digest: None,
3226 }))),
3227 digest: None,
3228 })));
3229
3230 let (sketch_block_ref, _) = self.mutate_ast(
3232 new_ast,
3233 sketch_id,
3234 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3235 )?;
3236 Ok(sketch_block_ref)
3237 }
3238
3239 async fn add_tangent(
3240 &mut self,
3241 sketch: ObjectId,
3242 tangent: Tangent,
3243 new_ast: &mut ast::Node<ast::Program>,
3244 ) -> Result<AstNodeRef, KclError> {
3245 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3246 return Err(KclError::refactor(format!(
3247 "Tangent constraint must have exactly 2 segments, got {}",
3248 tangent.input.len()
3249 )));
3250 };
3251 let sketch_id = sketch;
3252
3253 let seg0_object = self
3254 .scene_graph
3255 .objects
3256 .get(seg0_id.0)
3257 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3258 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3259 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3260 };
3261 let seg0_ast = match seg0_segment {
3262 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, LINE_VARIABLE, None)?,
3263 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, ARC_VARIABLE, None)?,
3264 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3265 _ => {
3266 return Err(KclError::refactor(format!(
3267 "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3268 )));
3269 }
3270 };
3271
3272 let seg1_object = self
3273 .scene_graph
3274 .objects
3275 .get(seg1_id.0)
3276 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3277 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3278 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3279 };
3280 let seg1_ast = match seg1_segment {
3281 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, LINE_VARIABLE, None)?,
3282 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, ARC_VARIABLE, None)?,
3283 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3284 _ => {
3285 return Err(KclError::refactor(format!(
3286 "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3287 )));
3288 }
3289 };
3290
3291 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3292 let (sketch_block_ref, _) = self.mutate_ast(
3293 new_ast,
3294 sketch_id,
3295 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3296 )?;
3297 Ok(sketch_block_ref)
3298 }
3299
3300 async fn add_symmetric(
3301 &mut self,
3302 sketch: ObjectId,
3303 symmetric: Symmetric,
3304 new_ast: &mut ast::Node<ast::Program>,
3305 ) -> Result<AstNodeRef, KclError> {
3306 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3307 return Err(KclError::refactor(format!(
3308 "Symmetric constraint must have exactly 2 inputs, got {}",
3309 symmetric.input.len()
3310 )));
3311 };
3312 let sketch_id = sketch;
3313
3314 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3315 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3316 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3317
3318 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3319 let (sketch_block_ref, _) = self.mutate_ast(
3320 new_ast,
3321 sketch_id,
3322 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3323 )?;
3324 Ok(sketch_block_ref)
3325 }
3326
3327 async fn add_midpoint(
3328 &mut self,
3329 sketch: ObjectId,
3330 midpoint: Midpoint,
3331 new_ast: &mut ast::Node<ast::Program>,
3332 ) -> Result<AstNodeRef, KclError> {
3333 let sketch_id = sketch;
3334 let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3335
3336 let segment_object = self
3337 .scene_graph
3338 .objects
3339 .get(midpoint.segment.0)
3340 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3341 let ObjectKind::Segment {
3342 segment: midpoint_segment,
3343 } = &segment_object.kind
3344 else {
3345 return Err(KclError::refactor(format!(
3346 "Object must be a segment, but it was {}",
3347 segment_object.kind.human_friendly_kind_with_article()
3348 )));
3349 };
3350 let segment_ast = match midpoint_segment {
3351 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3352 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3353 _ => {
3354 return Err(KclError::refactor(format!(
3355 "Midpoint target must be a line or arc segment but it was {}",
3356 midpoint_segment.human_friendly_kind_with_article()
3357 )));
3358 }
3359 };
3360
3361 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3362 let (sketch_block_ref, _) = self.mutate_ast(
3363 new_ast,
3364 sketch_id,
3365 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3366 )?;
3367 Ok(sketch_block_ref)
3368 }
3369
3370 async fn add_equal_radius(
3371 &mut self,
3372 sketch: ObjectId,
3373 equal_radius: EqualRadius,
3374 new_ast: &mut ast::Node<ast::Program>,
3375 ) -> Result<AstNodeRef, KclError> {
3376 if equal_radius.input.len() < 2 {
3377 return Err(KclError::refactor(format!(
3378 "equalRadius constraint must have at least 2 segments, got {}",
3379 equal_radius.input.len()
3380 )));
3381 }
3382
3383 let sketch_id = sketch;
3384 let input_asts = equal_radius
3385 .input
3386 .iter()
3387 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3388 .collect::<Result<Vec<_>, _>>()?;
3389
3390 let equal_radius_ast = create_equal_radius_ast(input_asts);
3391 let (sketch_block_ref, _) = self.mutate_ast(
3392 new_ast,
3393 sketch_id,
3394 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3395 )?;
3396 Ok(sketch_block_ref)
3397 }
3398
3399 async fn add_radius(
3400 &mut self,
3401 sketch: ObjectId,
3402 radius: Radius,
3403 new_ast: &mut ast::Node<ast::Program>,
3404 ) -> Result<AstNodeRef, KclError> {
3405 let params = ArcSizeConstraintParams {
3406 points: vec![radius.arc],
3407 function_name: RADIUS_FN,
3408 value: radius.radius.value,
3409 units: radius.radius.units,
3410 constraint_type_name: "Radius",
3411 };
3412 self.add_arc_size_constraint(sketch, params, new_ast).await
3413 }
3414
3415 async fn add_diameter(
3416 &mut self,
3417 sketch: ObjectId,
3418 diameter: Diameter,
3419 new_ast: &mut ast::Node<ast::Program>,
3420 ) -> Result<AstNodeRef, KclError> {
3421 let params = ArcSizeConstraintParams {
3422 points: vec![diameter.arc],
3423 function_name: DIAMETER_FN,
3424 value: diameter.diameter.value,
3425 units: diameter.diameter.units,
3426 constraint_type_name: "Diameter",
3427 };
3428 self.add_arc_size_constraint(sketch, params, new_ast).await
3429 }
3430
3431 async fn add_fixed_constraints(
3432 &mut self,
3433 sketch: ObjectId,
3434 points: Vec<FixedPoint>,
3435 new_ast: &mut ast::Node<ast::Program>,
3436 ) -> Result<AstNodeRef, KclError> {
3437 let mut sketch_block_ref = None;
3438
3439 for fixed_point in points {
3440 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3441 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3442 .map_err(|err| KclError::refactor(err.to_string()))?;
3443
3444 let (sketch_ref, _) = self.mutate_ast(
3445 new_ast,
3446 sketch,
3447 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3448 )?;
3449 sketch_block_ref = Some(sketch_ref);
3450 }
3451
3452 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3453 }
3454
3455 async fn add_arc_size_constraint(
3456 &mut self,
3457 sketch: ObjectId,
3458 params: ArcSizeConstraintParams,
3459 new_ast: &mut ast::Node<ast::Program>,
3460 ) -> Result<AstNodeRef, KclError> {
3461 let sketch_id = sketch;
3462
3463 if params.points.len() != 1 {
3465 return Err(KclError::refactor(format!(
3466 "{} constraint must have exactly 1 argument (an arc segment), got {}",
3467 params.constraint_type_name,
3468 params.points.len()
3469 )));
3470 }
3471
3472 let arc_id = params.points[0];
3473 let arc_object = self
3474 .scene_graph
3475 .objects
3476 .get(arc_id.0)
3477 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3478 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3479 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3480 };
3481 let ref_type = match arc_segment {
3482 Segment::Arc(_) => ARC_VARIABLE,
3483 Segment::Circle(_) => CIRCLE_VARIABLE,
3484 _ => {
3485 return Err(KclError::refactor(format!(
3486 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3487 params.constraint_type_name
3488 )));
3489 }
3490 };
3491 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3493
3494 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3496 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3497 unlabeled: Some(arc_ast),
3498 arguments: Default::default(),
3499 digest: None,
3500 non_code_meta: Default::default(),
3501 })));
3502 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3503 left: call_ast,
3504 operator: ast::BinaryOperator::Eq,
3505 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3506 value: ast::LiteralValue::Number {
3507 value: params.value,
3508 suffix: params.units,
3509 },
3510 raw: format_number_literal(params.value, params.units, None)
3511 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3512 digest: None,
3513 }))),
3514 digest: None,
3515 })));
3516
3517 let (sketch_block_ref, _) = self.mutate_ast(
3519 new_ast,
3520 sketch_id,
3521 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3522 )?;
3523 Ok(sketch_block_ref)
3524 }
3525
3526 async fn add_horizontal_distance(
3527 &mut self,
3528 sketch: ObjectId,
3529 distance: Distance,
3530 new_ast: &mut ast::Node<ast::Program>,
3531 ) -> Result<AstNodeRef, KclError> {
3532 let sketch_id = sketch;
3533 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3534 [pt0, pt1] => [
3535 self.coincident_segment_to_ast(pt0, new_ast)?,
3536 self.coincident_segment_to_ast(pt1, new_ast)?,
3537 ],
3538 _ => {
3539 return Err(KclError::refactor(format!(
3540 "Horizontal distance constraint must have exactly 2 points, got {}",
3541 distance.points.len()
3542 )));
3543 }
3544 };
3545
3546 let arguments = match &distance.label_position {
3547 Some(label_position) => vec![ast::LabeledArg {
3548 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3549 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3550 }],
3551 None => Default::default(),
3552 };
3553
3554 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3556 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3557 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3558 ast::ArrayExpression {
3559 elements: vec![pt0_ast, pt1_ast],
3560 digest: None,
3561 non_code_meta: Default::default(),
3562 },
3563 )))),
3564 arguments,
3565 digest: None,
3566 non_code_meta: Default::default(),
3567 })));
3568 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3569 left: distance_call_ast,
3570 operator: ast::BinaryOperator::Eq,
3571 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3572 value: ast::LiteralValue::Number {
3573 value: distance.distance.value,
3574 suffix: distance.distance.units,
3575 },
3576 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3577 KclError::refactor(format!(
3578 "Could not format numeric suffix: {:?}",
3579 distance.distance.units
3580 ))
3581 })?,
3582 digest: None,
3583 }))),
3584 digest: None,
3585 })));
3586
3587 let (sketch_block_ref, _) = self.mutate_ast(
3589 new_ast,
3590 sketch_id,
3591 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3592 )?;
3593 Ok(sketch_block_ref)
3594 }
3595
3596 async fn add_vertical_distance(
3597 &mut self,
3598 sketch: ObjectId,
3599 distance: Distance,
3600 new_ast: &mut ast::Node<ast::Program>,
3601 ) -> Result<AstNodeRef, KclError> {
3602 let sketch_id = sketch;
3603 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3604 [pt0, pt1] => [
3605 self.coincident_segment_to_ast(pt0, new_ast)?,
3606 self.coincident_segment_to_ast(pt1, new_ast)?,
3607 ],
3608 _ => {
3609 return Err(KclError::refactor(format!(
3610 "Vertical distance constraint must have exactly 2 points, got {}",
3611 distance.points.len()
3612 )));
3613 }
3614 };
3615
3616 let arguments = match &distance.label_position {
3617 Some(label_position) => vec![ast::LabeledArg {
3618 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3619 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3620 }],
3621 None => Default::default(),
3622 };
3623
3624 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3626 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3627 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3628 ast::ArrayExpression {
3629 elements: vec![pt0_ast, pt1_ast],
3630 digest: None,
3631 non_code_meta: Default::default(),
3632 },
3633 )))),
3634 arguments,
3635 digest: None,
3636 non_code_meta: Default::default(),
3637 })));
3638 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3639 left: distance_call_ast,
3640 operator: ast::BinaryOperator::Eq,
3641 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3642 value: ast::LiteralValue::Number {
3643 value: distance.distance.value,
3644 suffix: distance.distance.units,
3645 },
3646 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3647 KclError::refactor(format!(
3648 "Could not format numeric suffix: {:?}",
3649 distance.distance.units
3650 ))
3651 })?,
3652 digest: None,
3653 }))),
3654 digest: None,
3655 })));
3656
3657 let (sketch_block_ref, _) = self.mutate_ast(
3659 new_ast,
3660 sketch_id,
3661 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3662 )?;
3663 Ok(sketch_block_ref)
3664 }
3665
3666 async fn add_horizontal(
3667 &mut self,
3668 sketch: ObjectId,
3669 horizontal: Horizontal,
3670 new_ast: &mut ast::Node<ast::Program>,
3671 ) -> Result<AstNodeRef, KclError> {
3672 let sketch_id = sketch;
3673
3674 let first_arg_ast = match horizontal {
3676 Horizontal::Line { line } => {
3677 let line_object = self
3678 .scene_graph
3679 .objects
3680 .get(line.0)
3681 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3682 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3683 let kind = line_object.kind.human_friendly_kind_with_article();
3684 return Err(KclError::refactor(format!(
3685 "This constraint only works on Segments, but you selected {kind}"
3686 )));
3687 };
3688 let Segment::Line(_) = line_segment else {
3689 return Err(KclError::refactor(format!(
3690 "Only lines can be made horizontal, but you selected {}",
3691 line_segment.human_friendly_kind_with_article(),
3692 )));
3693 };
3694 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3695 }
3696 Horizontal::Points { points } => {
3697 let point_asts = points
3698 .iter()
3699 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3700 .collect::<Result<Vec<_>, _>>()?;
3701 ast::ArrayExpression::new(point_asts).into()
3702 }
3703 };
3704
3705 let horizontal_ast = create_horizontal_ast(first_arg_ast);
3707
3708 let (sketch_block_ref, _) = self.mutate_ast(
3710 new_ast,
3711 sketch_id,
3712 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3713 )?;
3714 Ok(sketch_block_ref)
3715 }
3716
3717 async fn add_lines_equal_length(
3718 &mut self,
3719 sketch: ObjectId,
3720 lines_equal_length: LinesEqualLength,
3721 new_ast: &mut ast::Node<ast::Program>,
3722 ) -> Result<AstNodeRef, KclError> {
3723 if lines_equal_length.lines.len() < 2 {
3724 return Err(KclError::refactor(format!(
3725 "Lines equal length constraint must have at least 2 lines, got {}",
3726 lines_equal_length.lines.len()
3727 )));
3728 };
3729
3730 let sketch_id = sketch;
3731
3732 let line_asts = lines_equal_length
3734 .lines
3735 .iter()
3736 .map(|line_id| {
3737 let line_object = self
3738 .scene_graph
3739 .objects
3740 .get(line_id.0)
3741 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3742 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3743 let kind = line_object.kind.human_friendly_kind_with_article();
3744 return Err(KclError::refactor(format!(
3745 "This constraint only works on Segments, but you selected {kind}"
3746 )));
3747 };
3748 let Segment::Line(_) = line_segment else {
3749 let kind = line_segment.human_friendly_kind_with_article();
3750 return Err(KclError::refactor(format!(
3751 "Only lines can be made equal length, but you selected {kind}"
3752 )));
3753 };
3754
3755 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3756 })
3757 .collect::<Result<Vec<_>, _>>()?;
3758
3759 let equal_length_ast = create_equal_length_ast(line_asts);
3761
3762 let (sketch_block_ref, _) = self.mutate_ast(
3764 new_ast,
3765 sketch_id,
3766 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3767 )?;
3768 Ok(sketch_block_ref)
3769 }
3770
3771 fn equal_radius_segment_id_to_ast_reference(
3772 &mut self,
3773 segment_id: ObjectId,
3774 new_ast: &mut ast::Node<ast::Program>,
3775 ) -> Result<ast::Expr, KclError> {
3776 let segment_object = self
3777 .scene_graph
3778 .objects
3779 .get(segment_id.0)
3780 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3781 let ObjectKind::Segment { segment } = &segment_object.kind else {
3782 return Err(KclError::refactor(format!(
3783 "Object is not a segment, it was {}",
3784 segment_object.kind.human_friendly_kind_with_article()
3785 )));
3786 };
3787
3788 let ref_type = match segment {
3789 Segment::Arc(_) => ARC_VARIABLE,
3790 Segment::Circle(_) => CIRCLE_VARIABLE,
3791 _ => {
3792 return Err(KclError::refactor(format!(
3793 "equalRadius supports only arc/circle segments, got {}",
3794 segment.human_friendly_kind_with_article()
3795 )));
3796 }
3797 };
3798
3799 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3800 }
3801
3802 fn symmetric_input_id_to_ast_reference(
3803 &mut self,
3804 segment_id: ObjectId,
3805 new_ast: &mut ast::Node<ast::Program>,
3806 ) -> Result<ast::Expr, KclError> {
3807 let segment_object = self
3808 .scene_graph
3809 .objects
3810 .get(segment_id.0)
3811 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3812 let ObjectKind::Segment { segment } = &segment_object.kind else {
3813 return Err(KclError::refactor(format!(
3814 "Object is not a segment, it was {}",
3815 segment_object.kind.human_friendly_kind_with_article()
3816 )));
3817 };
3818
3819 match segment {
3820 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3821 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3822 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3823 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3824 }
3825 }
3826
3827 fn symmetric_axis_id_to_ast_reference(
3828 &mut self,
3829 segment_id: ObjectId,
3830 new_ast: &mut ast::Node<ast::Program>,
3831 ) -> Result<ast::Expr, KclError> {
3832 let segment_object = self
3833 .scene_graph
3834 .objects
3835 .get(segment_id.0)
3836 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
3837 let ObjectKind::Segment { segment } = &segment_object.kind else {
3838 return Err(KclError::refactor(format!(
3839 "Object is not a segment, it was {}",
3840 segment_object.kind.human_friendly_kind_with_article()
3841 )));
3842 };
3843 match segment {
3844 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3845 _ => Err(KclError::refactor(format!(
3846 "Symmetric axis must be a line, got {}",
3847 segment.human_friendly_kind_with_article()
3848 ))),
3849 }
3850 }
3851
3852 async fn add_parallel(
3853 &mut self,
3854 sketch: ObjectId,
3855 parallel: Parallel,
3856 new_ast: &mut ast::Node<ast::Program>,
3857 ) -> Result<AstNodeRef, KclError> {
3858 if parallel.lines.len() < 2 {
3859 return Err(KclError::refactor(format!(
3860 "Parallel constraint must have at least 2 lines, got {}",
3861 parallel.lines.len()
3862 )));
3863 };
3864
3865 let sketch_id = sketch;
3866
3867 let line_asts = parallel
3868 .lines
3869 .iter()
3870 .map(|line_id| {
3871 let line_object = self
3872 .scene_graph
3873 .objects
3874 .get(line_id.0)
3875 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3876 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3877 let kind = line_object.kind.human_friendly_kind_with_article();
3878 return Err(KclError::refactor(format!(
3879 "This constraint only works on Segments, but you selected {kind}"
3880 )));
3881 };
3882 let Segment::Line(_) = line_segment else {
3883 let kind = line_segment.human_friendly_kind_with_article();
3884 return Err(KclError::refactor(format!(
3885 "Only lines can be made parallel, but you selected {kind}"
3886 )));
3887 };
3888
3889 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3890 })
3891 .collect::<Result<Vec<_>, _>>()?;
3892
3893 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3894 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3895 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3896 ast::ArrayExpression {
3897 elements: line_asts,
3898 digest: None,
3899 non_code_meta: Default::default(),
3900 },
3901 )))),
3902 arguments: Default::default(),
3903 digest: None,
3904 non_code_meta: Default::default(),
3905 })));
3906
3907 let (sketch_block_ref, _) = self.mutate_ast(
3908 new_ast,
3909 sketch_id,
3910 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3911 )?;
3912 Ok(sketch_block_ref)
3913 }
3914
3915 async fn add_perpendicular(
3916 &mut self,
3917 sketch: ObjectId,
3918 perpendicular: Perpendicular,
3919 new_ast: &mut ast::Node<ast::Program>,
3920 ) -> Result<AstNodeRef, KclError> {
3921 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3922 .await
3923 }
3924
3925 async fn add_lines_at_angle_constraint(
3926 &mut self,
3927 sketch: ObjectId,
3928 angle_kind: LinesAtAngleKind,
3929 lines: Vec<ObjectId>,
3930 new_ast: &mut ast::Node<ast::Program>,
3931 ) -> Result<AstNodeRef, KclError> {
3932 let &[line0_id, line1_id] = lines.as_slice() else {
3933 return Err(KclError::refactor(format!(
3934 "{} constraint must have exactly 2 lines, got {}",
3935 angle_kind.to_function_name(),
3936 lines.len()
3937 )));
3938 };
3939
3940 let sketch_id = sketch;
3941
3942 let line0_object = self
3944 .scene_graph
3945 .objects
3946 .get(line0_id.0)
3947 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3948 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3949 let kind = line0_object.kind.human_friendly_kind_with_article();
3950 return Err(KclError::refactor(format!(
3951 "This constraint only works on Segments, but you selected {kind}"
3952 )));
3953 };
3954 let Segment::Line(_) = line0_segment else {
3955 return Err(KclError::refactor(format!(
3956 "Only lines can be made {}, but you selected {}",
3957 angle_kind.to_function_name(),
3958 line0_segment.human_friendly_kind_with_article(),
3959 )));
3960 };
3961 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3962
3963 let line1_object = self
3964 .scene_graph
3965 .objects
3966 .get(line1_id.0)
3967 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3968 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3969 let kind = line1_object.kind.human_friendly_kind_with_article();
3970 return Err(KclError::refactor(format!(
3971 "This constraint only works on Segments, but you selected {kind}"
3972 )));
3973 };
3974 let Segment::Line(_) = line1_segment else {
3975 return Err(KclError::refactor(format!(
3976 "Only lines can be made {}, but you selected {}",
3977 angle_kind.to_function_name(),
3978 line1_segment.human_friendly_kind_with_article(),
3979 )));
3980 };
3981 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3982
3983 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3985 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3986 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3987 ast::ArrayExpression {
3988 elements: vec![line0_ast, line1_ast],
3989 digest: None,
3990 non_code_meta: Default::default(),
3991 },
3992 )))),
3993 arguments: Default::default(),
3994 digest: None,
3995 non_code_meta: Default::default(),
3996 })));
3997
3998 let (sketch_block_ref, _) = self.mutate_ast(
4000 new_ast,
4001 sketch_id,
4002 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4003 )?;
4004 Ok(sketch_block_ref)
4005 }
4006
4007 async fn add_vertical(
4008 &mut self,
4009 sketch: ObjectId,
4010 vertical: Vertical,
4011 new_ast: &mut ast::Node<ast::Program>,
4012 ) -> Result<AstNodeRef, KclError> {
4013 let sketch_id = sketch;
4014
4015 let first_arg_ast = match vertical {
4016 Vertical::Line { line } => {
4017 let line_object = self
4019 .scene_graph
4020 .objects
4021 .get(line.0)
4022 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4023 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4024 let kind = line_object.kind.human_friendly_kind_with_article();
4025 return Err(KclError::refactor(format!(
4026 "This constraint only works on Segments, but you selected {kind}"
4027 )));
4028 };
4029 let Segment::Line(_) = line_segment else {
4030 return Err(KclError::refactor(format!(
4031 "Only lines can be made vertical, but you selected {}",
4032 line_segment.human_friendly_kind_with_article()
4033 )));
4034 };
4035 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
4036 }
4037 Vertical::Points { points } => {
4038 let point_asts = points
4039 .iter()
4040 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4041 .collect::<Result<Vec<_>, _>>()?;
4042 ast::ArrayExpression::new(point_asts).into()
4043 }
4044 };
4045
4046 let vertical_ast = create_vertical_ast(first_arg_ast);
4048
4049 let (sketch_block_ref, _) = self.mutate_ast(
4051 new_ast,
4052 sketch_id,
4053 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4054 )?;
4055 Ok(sketch_block_ref)
4056 }
4057
4058 async fn execute_after_add_constraint(
4059 &mut self,
4060 ctx: &ExecutorContext,
4061 sketch_id: ObjectId,
4062 #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_ref: AstNodeRef,
4063 new_ast: &mut ast::Node<ast::Program>,
4064 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4065 let new_source = source_from_ast(new_ast);
4067 let (new_program, errors) = Program::parse(&new_source)
4069 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4070 if !errors.is_empty() {
4071 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4072 "Error parsing KCL source after adding constraint: {errors:?}"
4073 ))));
4074 }
4075 let Some(new_program) = new_program else {
4076 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4077 "No AST produced after adding constraint".to_string(),
4078 )));
4079 };
4080 #[cfg(feature = "artifact-graph")]
4081 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4082 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4083 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4084 )))
4085 })?;
4086
4087 let mut truncated_program = new_program.clone();
4090 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4091 .map_err(KclErrorWithOutputs::no_outputs)?;
4092
4093 let outcome = ctx
4095 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4096 .await?;
4097
4098 #[cfg(not(feature = "artifact-graph"))]
4099 let new_object_ids = Vec::new();
4100 #[cfg(feature = "artifact-graph")]
4101 let new_object_ids = {
4102 let constraint_id = outcome
4104 .source_range_to_object
4105 .get(&constraint_node_ref.range)
4106 .copied()
4107 .ok_or_else(|| {
4108 KclErrorWithOutputs::from_error_outcome(
4109 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4110 outcome.clone(),
4111 )
4112 })?;
4113 vec![constraint_id]
4114 };
4115
4116 self.program = new_program;
4119
4120 let outcome = self.update_state_after_exec(outcome, true);
4122
4123 let src_delta = SourceDelta { text: new_source };
4124 let scene_graph_delta = SceneGraphDelta {
4125 new_graph: self.scene_graph.clone(),
4126 invalidates_ids: false,
4127 new_objects: new_object_ids,
4128 exec_outcome: outcome,
4129 };
4130 Ok((src_delta, scene_graph_delta))
4131 }
4132
4133 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4135 if segment_ids_set.contains(&segment_id) {
4136 return true;
4137 }
4138
4139 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4140 return false;
4141 };
4142 let ObjectKind::Segment { segment } = &segment_object.kind else {
4143 return false;
4144 };
4145 let Segment::Point(point) = segment else {
4146 return false;
4147 };
4148
4149 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4150 }
4151
4152 fn remaining_constraint_segments(
4153 &self,
4154 segments: &[ConstraintSegment],
4155 segment_ids_set: &AhashIndexSet<ObjectId>,
4156 ) -> Vec<ConstraintSegment> {
4157 segments
4158 .iter()
4159 .copied()
4160 .filter(|segment| match segment {
4161 ConstraintSegment::Origin(_) => true,
4162 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4163 })
4164 .collect()
4165 }
4166
4167 fn find_referenced_constraints(
4168 &self,
4169 sketch_id: ObjectId,
4170 segment_ids_set: &AhashIndexSet<ObjectId>,
4171 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4172 let sketch_object = self
4174 .scene_graph
4175 .objects
4176 .get(sketch_id.0)
4177 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4178 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4179 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4180 };
4181 let mut constraint_ids_set = AhashIndexSet::default();
4182 for constraint_id in &sketch.constraints {
4183 let constraint_object = self
4184 .scene_graph
4185 .objects
4186 .get(constraint_id.0)
4187 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4188 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4189 return Err(KclError::refactor(format!(
4190 "Object is not a constraint, it is {}",
4191 constraint_object.kind.human_friendly_kind_with_article()
4192 )));
4193 };
4194 let depends_on_segment = match constraint {
4195 Constraint::Coincident(c) => c
4196 .segment_ids()
4197 .any(|seg_id| self.segment_will_be_deleted(seg_id, segment_ids_set)),
4198 Constraint::Distance(d) => d
4199 .point_ids()
4200 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4201 Constraint::Fixed(fixed) => fixed
4202 .points
4203 .iter()
4204 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4205 Constraint::Radius(r) => self.segment_will_be_deleted(r.arc, segment_ids_set),
4206 Constraint::Diameter(d) => self.segment_will_be_deleted(d.arc, segment_ids_set),
4207 Constraint::EqualRadius(equal_radius) => equal_radius
4208 .input
4209 .iter()
4210 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4211 Constraint::HorizontalDistance(d) => d
4212 .point_ids()
4213 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4214 Constraint::VerticalDistance(d) => d
4215 .point_ids()
4216 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4217 Constraint::Horizontal(h) => match h {
4218 Horizontal::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4219 Horizontal::Points { points } => points.iter().any(|point| match point {
4220 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4221 ConstraintSegment::Origin(_) => false,
4222 }),
4223 },
4224 Constraint::Vertical(v) => match v {
4225 Vertical::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4226 Vertical::Points { points } => points.iter().any(|point| match point {
4227 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4228 ConstraintSegment::Origin(_) => false,
4229 }),
4230 },
4231 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4232 .lines
4233 .iter()
4234 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4235 Constraint::Midpoint(midpoint) => {
4236 self.segment_will_be_deleted(midpoint.segment, segment_ids_set)
4237 || self.segment_will_be_deleted(midpoint.point, segment_ids_set)
4238 }
4239 Constraint::Parallel(parallel) => parallel
4240 .lines
4241 .iter()
4242 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4243 Constraint::Perpendicular(perpendicular) => perpendicular
4244 .lines
4245 .iter()
4246 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4247 Constraint::Angle(angle) => angle
4248 .lines
4249 .iter()
4250 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4251 Constraint::Symmetric(symmetric) => {
4252 self.segment_will_be_deleted(symmetric.axis, segment_ids_set)
4253 || symmetric
4254 .input
4255 .iter()
4256 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set))
4257 }
4258 Constraint::Tangent(tangent) => tangent
4259 .input
4260 .iter()
4261 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4262 };
4263 if depends_on_segment {
4264 constraint_ids_set.insert(*constraint_id);
4265 }
4266 }
4267 Ok(constraint_ids_set)
4268 }
4269
4270 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4271 #[cfg(not(feature = "artifact-graph"))]
4272 {
4273 let _ = freedom_analysis_ran; outcome
4275 }
4276 #[cfg(feature = "artifact-graph")]
4277 {
4278 let mut outcome = outcome;
4279 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4280
4281 if freedom_analysis_ran {
4282 self.point_freedom_cache.clear();
4285 for new_obj in &new_objects {
4286 if let ObjectKind::Segment {
4287 segment: crate::front::Segment::Point(point),
4288 } = &new_obj.kind
4289 {
4290 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4291 }
4292 }
4293 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4294 self.scene_graph.objects = new_objects;
4296 } else {
4297 for old_obj in &self.scene_graph.objects {
4300 if let ObjectKind::Segment {
4301 segment: crate::front::Segment::Point(point),
4302 } = &old_obj.kind
4303 {
4304 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4305 }
4306 }
4307
4308 let mut updated_objects = Vec::with_capacity(new_objects.len());
4310 for new_obj in new_objects {
4311 let mut obj = new_obj;
4312 if let ObjectKind::Segment {
4313 segment: crate::front::Segment::Point(point),
4314 } = &mut obj.kind
4315 {
4316 let new_freedom = point.freedom;
4317 match new_freedom {
4323 Freedom::Free => {
4324 match self.point_freedom_cache.get(&obj.id).copied() {
4325 Some(Freedom::Conflict) => {
4326 }
4329 Some(Freedom::Fixed) => {
4330 point.freedom = Freedom::Fixed;
4332 }
4333 Some(Freedom::Free) => {
4334 }
4336 None => {
4337 }
4339 }
4340 }
4341 Freedom::Fixed => {
4342 }
4344 Freedom::Conflict => {
4345 }
4347 }
4348 self.point_freedom_cache.insert(obj.id, point.freedom);
4350 }
4351 updated_objects.push(obj);
4352 }
4353
4354 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4355 self.scene_graph.objects = updated_objects;
4356 }
4357 outcome
4358 }
4359 }
4360
4361 fn mutate_ast(
4362 &mut self,
4363 ast: &mut ast::Node<ast::Program>,
4364 object_id: ObjectId,
4365 command: AstMutateCommand,
4366 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4367 let sketch_object = self
4368 .scene_graph
4369 .objects
4370 .get(object_id.0)
4371 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4372 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4373 }
4374}
4375
4376fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4377 let sketch_object = scene_graph
4379 .objects
4380 .get(sketch_id.0)
4381 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4382 let ObjectKind::Sketch(_) = &sketch_object.kind else {
4383 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4384 };
4385 expect_single_node_ref(sketch_object)
4386}
4387
4388fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4389 match &object.source {
4390 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4391 range: *range,
4392 node_path: node_path.clone(),
4393 }),
4394 SourceRef::BackTrace { ranges } => {
4395 let [range] = ranges.as_slice() else {
4396 return Err(KclError::refactor(format!(
4397 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4398 ranges.len()
4399 )));
4400 };
4401 Ok(AstNodeRef {
4402 range: range.0,
4403 node_path: range.1.clone(),
4404 })
4405 }
4406 }
4407}
4408
4409fn only_sketch_block_from_range(
4412 ast: &mut ast::Node<ast::Program>,
4413 sketch_block_range: SourceRange,
4414 edit_kind: ChangeKind,
4415) -> Result<(), KclError> {
4416 let r1 = sketch_block_range;
4417 let matches_range = |r2: SourceRange| -> bool {
4418 match edit_kind {
4421 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4422 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4424 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4425 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4427 }
4428 };
4429 let mut found = false;
4430 for item in ast.body.iter_mut() {
4431 match item {
4432 ast::BodyItem::ImportStatement(_) => {}
4433 ast::BodyItem::ExpressionStatement(node) => {
4434 if matches_range(SourceRange::from(&*node))
4435 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4436 {
4437 sketch_block.is_being_edited = true;
4438 found = true;
4439 break;
4440 }
4441 }
4442 ast::BodyItem::VariableDeclaration(node) => {
4443 if matches_range(SourceRange::from(&node.declaration.init))
4444 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4445 {
4446 sketch_block.is_being_edited = true;
4447 found = true;
4448 break;
4449 }
4450 }
4451 ast::BodyItem::TypeDeclaration(_) => {}
4452 ast::BodyItem::ReturnStatement(node) => {
4453 if matches_range(SourceRange::from(&node.argument))
4454 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4455 {
4456 sketch_block.is_being_edited = true;
4457 found = true;
4458 break;
4459 }
4460 }
4461 }
4462 }
4463 if !found {
4464 return Err(KclError::refactor(format!(
4465 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4466 )));
4467 }
4468
4469 Ok(())
4470}
4471
4472fn only_sketch_block(
4473 ast: &mut ast::Node<ast::Program>,
4474 sketch_block_ref: &AstNodeRef,
4475 edit_kind: ChangeKind,
4476) -> Result<(), KclError> {
4477 let Some(target_node_path) = &sketch_block_ref.node_path else {
4478 #[cfg(target_arch = "wasm32")]
4479 web_sys::console::warn_1(
4480 &format!(
4481 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4482 &sketch_block_ref
4483 )
4484 .into(),
4485 );
4486 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4487 };
4488 let mut found = false;
4489 for item in ast.body.iter_mut() {
4490 match item {
4491 ast::BodyItem::ImportStatement(_) => {}
4492 ast::BodyItem::ExpressionStatement(node) => {
4493 if let Some(node_path) = &node.node_path
4495 && node_path == target_node_path
4496 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4497 {
4498 sketch_block.is_being_edited = true;
4499 found = true;
4500 break;
4501 }
4502 if let Some(node_path) = node.expression.node_path()
4504 && node_path == target_node_path
4505 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4506 {
4507 sketch_block.is_being_edited = true;
4508 found = true;
4509 break;
4510 }
4511 }
4512 ast::BodyItem::VariableDeclaration(node) => {
4513 if let Some(node_path) = node.declaration.init.node_path()
4514 && node_path == target_node_path
4515 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4516 {
4517 sketch_block.is_being_edited = true;
4518 found = true;
4519 break;
4520 }
4521 }
4522 ast::BodyItem::TypeDeclaration(_) => {}
4523 ast::BodyItem::ReturnStatement(node) => {
4524 if let Some(node_path) = node.argument.node_path()
4525 && node_path == target_node_path
4526 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4527 {
4528 sketch_block.is_being_edited = true;
4529 found = true;
4530 break;
4531 }
4532 }
4533 }
4534 }
4535 if !found {
4536 return Err(KclError::refactor(format!(
4537 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4538 )));
4539 }
4540
4541 Ok(())
4542}
4543
4544fn sketch_on_ast_expr(
4545 ast: &mut ast::Node<ast::Program>,
4546 scene_graph: &SceneGraph,
4547 on: &Plane,
4548) -> Result<ast::Expr, KclError> {
4549 match on {
4550 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4551 Plane::Object(object_id) => {
4552 let on_object = scene_graph
4553 .objects
4554 .get(object_id.0)
4555 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4556 #[cfg(feature = "artifact-graph")]
4557 {
4558 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4559 return Ok(face_expr);
4560 }
4561 }
4562 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4563 }
4564 }
4565}
4566
4567#[cfg(feature = "artifact-graph")]
4568fn sketch_face_of_scene_object_ast_expr(
4569 ast: &mut ast::Node<ast::Program>,
4570 on_object: &crate::front::Object,
4571) -> Result<Option<ast::Expr>, KclError> {
4572 let SourceRef::BackTrace { ranges } = &on_object.source else {
4573 return Ok(None);
4574 };
4575
4576 match &on_object.kind {
4577 ObjectKind::Wall(_) => {
4578 let [sweep_range, segment_range] = ranges.as_slice() else {
4579 return Err(KclError::refactor(format!(
4580 "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4581 ranges.len(),
4582 on_object.artifact_id
4583 )));
4584 };
4585 let sweep_ref = get_or_insert_ast_reference(
4586 ast,
4587 &SourceRef::Simple {
4588 range: sweep_range.0,
4589 node_path: sweep_range.1.clone(),
4590 },
4591 "solid",
4592 None,
4593 )?;
4594 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4595 return Err(KclError::refactor(format!(
4596 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4597 on_object.artifact_id
4598 )));
4599 };
4600 let solid_name = solid_name_expr.name.name.clone();
4601 let solid_expr = ast_name_expr(solid_name.clone());
4602 let segment_ref = get_or_insert_ast_reference(
4603 ast,
4604 &SourceRef::Simple {
4605 range: segment_range.0,
4606 node_path: segment_range.1.clone(),
4607 },
4608 LINE_VARIABLE,
4609 None,
4610 )?;
4611
4612 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4613 let ast::Expr::Name(segment_name_expr) = segment_ref else {
4614 return Err(KclError::refactor(format!(
4615 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4616 on_object.artifact_id
4617 )));
4618 };
4619 create_member_expression(
4620 create_member_expression(ast_name_expr(region_name), "tags"),
4621 &segment_name_expr.name.name,
4622 )
4623 } else {
4624 segment_ref
4625 };
4626
4627 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4628 }
4629 ObjectKind::Cap(cap) => {
4630 let [range] = ranges.as_slice() else {
4631 return Err(KclError::refactor(format!(
4632 "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4633 ranges.len(),
4634 on_object.artifact_id
4635 )));
4636 };
4637 let sweep_ref = get_or_insert_ast_reference(
4638 ast,
4639 &SourceRef::Simple {
4640 range: range.0,
4641 node_path: range.1.clone(),
4642 },
4643 "solid",
4644 None,
4645 )?;
4646 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4647 return Err(KclError::refactor(format!(
4648 "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4649 on_object.artifact_id
4650 )));
4651 };
4652 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4653 let face_expr = match cap.kind {
4655 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4656 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4657 };
4658
4659 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4660 }
4661 _ => Ok(None),
4662 }
4663}
4664
4665#[cfg(feature = "artifact-graph")]
4666fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4667 let mut existing_artifact_ids = scene_objects
4668 .iter()
4669 .map(|object| object.artifact_id)
4670 .collect::<HashSet<_>>();
4671
4672 for artifact in artifact_graph.values() {
4673 match artifact {
4674 Artifact::Wall(wall) => {
4675 if existing_artifact_ids.contains(&wall.id) {
4676 continue;
4677 }
4678
4679 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4680 Artifact::Segment(segment) => Some(segment),
4681 _ => None,
4682 }) else {
4683 continue;
4684 };
4685 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4686 Artifact::Sweep(sweep) => Some(sweep),
4687 _ => None,
4688 }) else {
4689 continue;
4690 };
4691 let source_segment = segment
4692 .original_seg_id
4693 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4694 .and_then(|artifact| match artifact {
4695 Artifact::Segment(segment) => Some(segment),
4696 _ => None,
4697 })
4698 .unwrap_or(segment);
4699 let id = ObjectId(scene_objects.len());
4700 scene_objects.push(crate::front::Object {
4701 id,
4702 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4703 label: Default::default(),
4704 comments: Default::default(),
4705 artifact_id: wall.id,
4706 source: SourceRef::BackTrace {
4707 ranges: vec![
4708 (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4709 (
4710 source_segment.code_ref.range,
4711 Some(source_segment.code_ref.node_path.clone()),
4712 ),
4713 ],
4714 },
4715 });
4716 existing_artifact_ids.insert(wall.id);
4717 }
4718 Artifact::Cap(cap) => {
4719 if existing_artifact_ids.contains(&cap.id) {
4720 continue;
4721 }
4722
4723 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4724 Artifact::Sweep(sweep) => Some(sweep),
4725 _ => None,
4726 }) else {
4727 continue;
4728 };
4729 let id = ObjectId(scene_objects.len());
4730 let kind = match cap.sub_type {
4731 CapSubType::Start => crate::frontend::api::CapKind::Start,
4732 CapSubType::End => crate::frontend::api::CapKind::End,
4733 };
4734 scene_objects.push(crate::front::Object {
4735 id,
4736 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4737 label: Default::default(),
4738 comments: Default::default(),
4739 artifact_id: cap.id,
4740 source: SourceRef::BackTrace {
4741 ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4742 },
4743 });
4744 existing_artifact_ids.insert(cap.id);
4745 }
4746 _ => {}
4747 }
4748 }
4749}
4750
4751fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4752 use crate::engine::PlaneName;
4753
4754 match name {
4755 PlaneName::Xy => ast_name_expr("XY".to_owned()),
4756 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4757 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4758 PlaneName::NegXy => negated_plane_ast_expr("XY"),
4759 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4760 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4761 }
4762}
4763
4764fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4765 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4766 ast::UnaryOperator::Neg,
4767 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4768 )))
4769}
4770
4771#[cfg(feature = "artifact-graph")]
4772fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4773 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4774 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4775 unlabeled: Some(solid_expr),
4776 arguments: vec![ast::LabeledArg {
4777 label: Some(ast::Identifier::new("face")),
4778 arg: face_expr,
4779 }],
4780 digest: None,
4781 non_code_meta: Default::default(),
4782 })))
4783}
4784
4785#[cfg(feature = "artifact-graph")]
4786fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4787 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4788 return None;
4789 };
4790 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4791 return None;
4792 };
4793 if !matches!(
4794 sweep_call.callee.name.name.as_str(),
4795 "extrude" | "revolve" | "sweep" | "loft"
4796 ) {
4797 return None;
4798 }
4799 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4800 return None;
4801 };
4802 let candidate = region_name_expr.name.name.clone();
4803 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4804 return None;
4805 };
4806 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
4807 return None;
4808 };
4809 if region_call.callee.name.name != "region" {
4810 return None;
4811 }
4812 Some(candidate)
4813}
4814
4815fn get_or_insert_ast_reference(
4822 ast: &mut ast::Node<ast::Program>,
4823 source_ref: &SourceRef,
4824 prefix: &str,
4825 property: Option<&str>,
4826) -> Result<ast::Expr, KclError> {
4827 let command = AstMutateCommand::AddVariableDeclaration {
4828 prefix: prefix.to_owned(),
4829 };
4830 let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
4831 let AstMutateCommandReturn::Name(var_name) = ret else {
4832 return Err(KclError::refactor(
4833 "Expected variable name returned from AddVariableDeclaration".to_owned(),
4834 ));
4835 };
4836 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4837 let Some(property) = property else {
4838 return Ok(var_expr);
4840 };
4841
4842 Ok(create_member_expression(var_expr, property))
4843}
4844
4845fn mutate_ast_node_by_source_ref(
4846 ast: &mut ast::Node<ast::Program>,
4847 source_ref: &SourceRef,
4848 command: AstMutateCommand,
4849) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4850 let (source_range, node_path) = match source_ref {
4851 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
4852 SourceRef::BackTrace { ranges } => {
4853 let [range] = ranges.as_slice() else {
4854 return Err(KclError::refactor(format!(
4855 "Expected single source ref, got {}; ranges={ranges:#?}",
4856 ranges.len(),
4857 )));
4858 };
4859 (range.0, range.1.clone())
4860 }
4861 };
4862 let mut context = AstMutateContext {
4863 source_range,
4864 node_path,
4865 command,
4866 defined_names_stack: Default::default(),
4867 };
4868 let control = dfs_mut(ast, &mut context);
4869 match control {
4870 ControlFlow::Continue(_) => Err(KclError::refactor(
4871 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
4872 )),
4873 ControlFlow::Break(break_value) => break_value,
4874 }
4875}
4876
4877#[derive(Debug)]
4878struct AstMutateContext {
4879 source_range: SourceRange,
4880 node_path: Option<ast::NodePath>,
4881 command: AstMutateCommand,
4882 defined_names_stack: Vec<HashSet<String>>,
4883}
4884
4885#[derive(Debug)]
4886#[allow(clippy::large_enum_variant)]
4887enum AstMutateCommand {
4888 AddSketchBlockExprStmt {
4890 expr: ast::Expr,
4891 },
4892 AddSketchBlockVarDecl {
4894 prefix: String,
4895 expr: ast::Expr,
4896 },
4897 AddVariableDeclaration {
4898 prefix: String,
4899 },
4900 EditPoint {
4901 at: ast::Expr,
4902 },
4903 EditLine {
4904 start: ast::Expr,
4905 end: ast::Expr,
4906 construction: Option<bool>,
4907 },
4908 EditArc {
4909 start: ast::Expr,
4910 end: ast::Expr,
4911 center: ast::Expr,
4912 construction: Option<bool>,
4913 },
4914 EditCircle {
4915 start: ast::Expr,
4916 center: ast::Expr,
4917 construction: Option<bool>,
4918 },
4919 EditConstraintValue {
4920 value: ast::BinaryPart,
4921 },
4922 EditDistanceConstraintLabelPosition {
4923 label_position: ast::Expr,
4924 },
4925 EditCallUnlabeled {
4926 arg: ast::Expr,
4927 },
4928 #[cfg(feature = "artifact-graph")]
4929 EditVarInitialValue {
4930 value: Number,
4931 },
4932 DeleteNode,
4933}
4934
4935impl AstMutateCommand {
4936 fn needs_defined_names_stack(&self) -> bool {
4937 matches!(
4938 self,
4939 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4940 )
4941 }
4942}
4943
4944#[derive(Debug)]
4945enum AstMutateCommandReturn {
4946 None,
4947 Name(String),
4948}
4949
4950#[derive(Debug, Clone)]
4951struct AstNodeRef {
4952 range: SourceRange,
4953 node_path: Option<ast::NodePath>,
4954}
4955
4956impl<T> From<&ast::Node<T>> for AstNodeRef {
4957 fn from(value: &ast::Node<T>) -> Self {
4958 AstNodeRef {
4959 range: value.into(),
4960 node_path: value.node_path.clone(),
4961 }
4962 }
4963}
4964
4965impl From<&ast::BodyItem> for AstNodeRef {
4966 fn from(value: &ast::BodyItem) -> Self {
4967 match value {
4968 ast::BodyItem::ImportStatement(node) => AstNodeRef {
4969 range: node.into(),
4970 node_path: node.node_path.clone(),
4971 },
4972 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4973 range: node.into(),
4974 node_path: node.node_path.clone(),
4975 },
4976 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4977 range: node.into(),
4978 node_path: node.node_path.clone(),
4979 },
4980 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4981 range: node.into(),
4982 node_path: node.node_path.clone(),
4983 },
4984 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
4985 range: node.into(),
4986 node_path: node.node_path.clone(),
4987 },
4988 }
4989 }
4990}
4991
4992impl From<&ast::Expr> for AstNodeRef {
4993 fn from(value: &ast::Expr) -> Self {
4994 AstNodeRef {
4995 range: SourceRange::from(value),
4996 node_path: value.node_path().cloned(),
4997 }
4998 }
4999}
5000
5001impl From<&AstMutateContext> for AstNodeRef {
5002 fn from(value: &AstMutateContext) -> Self {
5003 AstNodeRef {
5004 range: value.source_range,
5005 node_path: value.node_path.clone(),
5006 }
5007 }
5008}
5009
5010impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5011 type Error = crate::walk::AstNodeError;
5012
5013 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5014 Ok(AstNodeRef {
5015 range: SourceRange::try_from(value)?,
5016 node_path: value.try_into()?,
5017 })
5018 }
5019}
5020
5021impl From<AstNodeRef> for SourceRange {
5022 fn from(value: AstNodeRef) -> Self {
5023 value.range
5024 }
5025}
5026
5027impl Visitor for AstMutateContext {
5028 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5029 type Continue = ();
5030
5031 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5032 filter_and_process(self, node)
5033 }
5034
5035 fn finish(&mut self, node: NodeMut<'_>) {
5036 match &node {
5037 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5038 self.defined_names_stack.pop();
5039 }
5040 _ => {}
5041 }
5042 }
5043}
5044
5045fn filter_and_process(
5046 ctx: &mut AstMutateContext,
5047 node: NodeMut,
5048) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5049 let Ok(node_range) = SourceRange::try_from(&node) else {
5050 return TraversalReturn::new_continue(());
5052 };
5053 if let NodeMut::VariableDeclaration(var_decl) = &node {
5058 let expr_range = SourceRange::from(&var_decl.declaration.init);
5059 let expr_node_path = var_decl.declaration.init.node_path();
5060 if source_ref_matches(ctx, expr_range, expr_node_path) {
5061 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5062 return TraversalReturn::new_break(Ok((
5065 AstNodeRef::from(&**var_decl),
5066 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5067 )));
5068 }
5069 if let AstMutateCommand::DeleteNode = &ctx.command {
5070 return TraversalReturn {
5073 mutate_body_item: MutateBodyItem::Delete,
5074 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5075 };
5076 }
5077 }
5078 }
5079 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5082 let expr_range = SourceRange::from(&expr_stmt.expression);
5083 let expr_node_path = expr_stmt.expression.node_path();
5084 if source_ref_matches(ctx, expr_range, expr_node_path) {
5085 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5086 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5089 return TraversalReturn::new_continue(());
5090 };
5091 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5092 }
5093 if let AstMutateCommand::DeleteNode = &ctx.command {
5094 return TraversalReturn {
5097 mutate_body_item: MutateBodyItem::Delete,
5098 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5099 };
5100 }
5101 }
5102 }
5103
5104 if ctx.command.needs_defined_names_stack() {
5105 if let NodeMut::Program(program) = &node {
5106 ctx.defined_names_stack.push(find_defined_names(*program));
5107 } else if let NodeMut::SketchBlock(block) = &node {
5108 ctx.defined_names_stack.push(find_defined_names(&block.body));
5109 }
5110 }
5111
5112 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5114 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5115 return TraversalReturn::new_continue(());
5116 }
5117 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5118 return TraversalReturn::new_continue(());
5119 };
5120 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5121}
5122
5123fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5124 match &ctx.node_path {
5125 Some(target) => Some(target) == node_path,
5126 None => node_range == ctx.source_range,
5127 }
5128}
5129
5130fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5131 match &ctx.command {
5132 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5133 if let NodeMut::SketchBlock(sketch_block) = node {
5134 sketch_block
5135 .body
5136 .items
5137 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5138 inner: ast::ExpressionStatement {
5139 expression: expr.clone(),
5140 digest: None,
5141 },
5142 start: Default::default(),
5143 end: Default::default(),
5144 module_id: Default::default(),
5145 node_path: None,
5146 outer_attrs: Default::default(),
5147 pre_comments: Default::default(),
5148 comment_start: Default::default(),
5149 }));
5150 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5151 }
5152 }
5153 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5154 if let NodeMut::SketchBlock(sketch_block) = node {
5155 let empty_defined_names = HashSet::new();
5156 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5157 let Ok(name) = next_free_name(prefix, defined_names) else {
5158 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5159 };
5160 sketch_block
5161 .body
5162 .items
5163 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5164 ast::VariableDeclaration::new(
5165 ast::VariableDeclarator::new(&name, expr.clone()),
5166 ast::ItemVisibility::Default,
5167 ast::VariableKind::Const,
5168 ),
5169 ))));
5170 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5171 }
5172 }
5173 AstMutateCommand::AddVariableDeclaration { prefix } => {
5174 if let NodeMut::VariableDeclaration(inner) = node {
5175 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5176 }
5177 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5178 let empty_defined_names = HashSet::new();
5179 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5180 let Ok(name) = next_free_name(prefix, defined_names) else {
5181 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5183 };
5184 let mutate_node =
5185 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5186 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5187 ast::ItemVisibility::Default,
5188 ast::VariableKind::Const,
5189 ))));
5190 return TraversalReturn {
5191 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5192 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5193 };
5194 }
5195 }
5196 AstMutateCommand::EditPoint { at } => {
5197 if let NodeMut::CallExpressionKw(call) = node {
5198 if call.callee.name.name != POINT_FN {
5199 return TraversalReturn::new_continue(());
5200 }
5201 for labeled_arg in &mut call.arguments {
5203 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5204 labeled_arg.arg = at.clone();
5205 }
5206 }
5207 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5208 }
5209 }
5210 AstMutateCommand::EditLine {
5211 start,
5212 end,
5213 construction,
5214 } => {
5215 if let NodeMut::CallExpressionKw(call) = node {
5216 if call.callee.name.name != LINE_FN {
5217 return TraversalReturn::new_continue(());
5218 }
5219 for labeled_arg in &mut call.arguments {
5221 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5222 labeled_arg.arg = start.clone();
5223 }
5224 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5225 labeled_arg.arg = end.clone();
5226 }
5227 }
5228 if let Some(construction_value) = construction {
5230 let construction_exists = call
5231 .arguments
5232 .iter()
5233 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5234 if *construction_value {
5235 if construction_exists {
5237 for labeled_arg in &mut call.arguments {
5239 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5240 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5241 value: ast::LiteralValue::Bool(true),
5242 raw: "true".to_string(),
5243 digest: None,
5244 })));
5245 }
5246 }
5247 } else {
5248 call.arguments.push(ast::LabeledArg {
5250 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5251 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5252 value: ast::LiteralValue::Bool(true),
5253 raw: "true".to_string(),
5254 digest: None,
5255 }))),
5256 });
5257 }
5258 } else {
5259 call.arguments
5261 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5262 }
5263 }
5264 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5265 }
5266 }
5267 AstMutateCommand::EditArc {
5268 start,
5269 end,
5270 center,
5271 construction,
5272 } => {
5273 if let NodeMut::CallExpressionKw(call) = node {
5274 if call.callee.name.name != ARC_FN {
5275 return TraversalReturn::new_continue(());
5276 }
5277 for labeled_arg in &mut call.arguments {
5279 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5280 labeled_arg.arg = start.clone();
5281 }
5282 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5283 labeled_arg.arg = end.clone();
5284 }
5285 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5286 labeled_arg.arg = center.clone();
5287 }
5288 }
5289 if let Some(construction_value) = construction {
5291 let construction_exists = call
5292 .arguments
5293 .iter()
5294 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5295 if *construction_value {
5296 if construction_exists {
5298 for labeled_arg in &mut call.arguments {
5300 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5301 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5302 value: ast::LiteralValue::Bool(true),
5303 raw: "true".to_string(),
5304 digest: None,
5305 })));
5306 }
5307 }
5308 } else {
5309 call.arguments.push(ast::LabeledArg {
5311 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5312 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5313 value: ast::LiteralValue::Bool(true),
5314 raw: "true".to_string(),
5315 digest: None,
5316 }))),
5317 });
5318 }
5319 } else {
5320 call.arguments
5322 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5323 }
5324 }
5325 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5326 }
5327 }
5328 AstMutateCommand::EditCircle {
5329 start,
5330 center,
5331 construction,
5332 } => {
5333 if let NodeMut::CallExpressionKw(call) = node {
5334 if call.callee.name.name != CIRCLE_FN {
5335 return TraversalReturn::new_continue(());
5336 }
5337 for labeled_arg in &mut call.arguments {
5339 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5340 labeled_arg.arg = start.clone();
5341 }
5342 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5343 labeled_arg.arg = center.clone();
5344 }
5345 }
5346 if let Some(construction_value) = construction {
5348 let construction_exists = call
5349 .arguments
5350 .iter()
5351 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5352 if *construction_value {
5353 if construction_exists {
5354 for labeled_arg in &mut call.arguments {
5355 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5356 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5357 value: ast::LiteralValue::Bool(true),
5358 raw: "true".to_string(),
5359 digest: None,
5360 })));
5361 }
5362 }
5363 } else {
5364 call.arguments.push(ast::LabeledArg {
5365 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5366 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5367 value: ast::LiteralValue::Bool(true),
5368 raw: "true".to_string(),
5369 digest: None,
5370 }))),
5371 });
5372 }
5373 } else {
5374 call.arguments
5375 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5376 }
5377 }
5378 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5379 }
5380 }
5381 AstMutateCommand::EditConstraintValue { value } => {
5382 if let NodeMut::BinaryExpression(binary_expr) = node {
5383 let left_is_constraint = matches!(
5384 &binary_expr.left,
5385 ast::BinaryPart::CallExpressionKw(call)
5386 if matches!(
5387 call.callee.name.name.as_str(),
5388 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5389 )
5390 );
5391 if left_is_constraint {
5392 binary_expr.right = value.clone();
5393 } else {
5394 binary_expr.left = value.clone();
5395 }
5396
5397 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5398 }
5399 }
5400 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5401 if let NodeMut::BinaryExpression(binary_expr) = node {
5402 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5403 return TraversalReturn::new_continue(());
5404 };
5405 if !matches!(
5406 call.callee.name.name.as_str(),
5407 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN
5408 ) {
5409 return TraversalReturn::new_continue(());
5410 }
5411
5412 if let Some(label_arg) = call
5413 .arguments
5414 .iter_mut()
5415 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5416 {
5417 label_arg.arg = label_position.clone();
5418 } else {
5419 call.arguments.push(ast::LabeledArg {
5420 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5421 arg: label_position.clone(),
5422 });
5423 }
5424
5425 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5426 }
5427 }
5428 AstMutateCommand::EditCallUnlabeled { arg } => {
5429 if let NodeMut::CallExpressionKw(call) = node {
5430 call.unlabeled = Some(arg.clone());
5431 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5432 }
5433 }
5434 #[cfg(feature = "artifact-graph")]
5435 AstMutateCommand::EditVarInitialValue { value } => {
5436 if let NodeMut::NumericLiteral(numeric_literal) = node {
5437 let Ok(literal) = to_source_number(*value) else {
5439 return TraversalReturn::new_break(Err(KclError::refactor(format!(
5440 "Could not convert number to AST literal: {:?}",
5441 *value
5442 ))));
5443 };
5444 *numeric_literal = ast::Node::no_src(literal);
5445 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5446 }
5447 }
5448 AstMutateCommand::DeleteNode => {
5449 return TraversalReturn {
5450 mutate_body_item: MutateBodyItem::Delete,
5451 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5452 };
5453 }
5454 }
5455 TraversalReturn::new_continue(())
5456}
5457
5458struct FindSketchBlockSourceRange {
5459 target_before_mutation: SourceRange,
5461 found: Cell<Option<AstNodeRef>>,
5465}
5466
5467impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5468 type Error = crate::front::Error;
5469
5470 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5471 let Ok(node_range) = SourceRange::try_from(&node) else {
5472 return Ok(true);
5473 };
5474
5475 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5476 if node_range.module_id() == self.target_before_mutation.module_id()
5477 && node_range.start() == self.target_before_mutation.start()
5478 && node_range.end() >= self.target_before_mutation.end()
5480 {
5481 self.found.set(sketch_block.body.items.last().map(|item| match item {
5482 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5486 _ => AstNodeRef::from(item),
5487 }));
5488 return Ok(false);
5489 } else {
5490 return Ok(true);
5493 }
5494 }
5495
5496 for child in node.children().iter() {
5497 if !child.visit(*self)? {
5498 return Ok(false);
5499 }
5500 }
5501
5502 Ok(true)
5503 }
5504}
5505
5506struct FindSketchBlockByNodePath {
5507 target_node_path: ast::NodePath,
5509 found: Cell<Option<AstNodeRef>>,
5513}
5514
5515impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5516 type Error = crate::front::Error;
5517
5518 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5519 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5520 return Ok(true);
5521 };
5522
5523 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5524 if let Some(node_path) = node_path
5525 && node_path == self.target_node_path
5526 {
5527 self.found.set(sketch_block.body.items.last().map(|item| match item {
5528 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5532 _ => AstNodeRef::from(item),
5533 }));
5534
5535 return Ok(false);
5536 } else {
5537 return Ok(true);
5540 }
5541 }
5542
5543 for child in node.children().iter() {
5544 if !child.visit(*self)? {
5545 return Ok(false);
5546 }
5547 }
5548
5549 Ok(true)
5550 }
5551}
5552
5553fn find_sketch_block_added_item(
5561 ast: &ast::Node<ast::Program>,
5562 sketch_block_before_mutation: &AstNodeRef,
5563) -> Result<AstNodeRef, KclError> {
5564 if let Some(node_path) = &sketch_block_before_mutation.node_path {
5565 let find = FindSketchBlockByNodePath {
5566 target_node_path: node_path.clone(),
5567 found: Cell::new(None),
5568 };
5569 let node = crate::walk::Node::from(ast);
5570 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5571 find.found.into_inner().ok_or_else(|| {
5572 KclError::refactor(format!(
5573 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5574 ))
5575 })
5576 } else {
5577 let find = FindSketchBlockSourceRange {
5579 target_before_mutation: sketch_block_before_mutation.range,
5580 found: Cell::new(None),
5581 };
5582 let node = crate::walk::Node::from(ast);
5583 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5584 find.found.into_inner().ok_or_else(|| KclError::refactor(
5585 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?"),
5586 ))
5587 }
5588}
5589
5590fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5591 ast.recast_top(&Default::default(), 0)
5593}
5594
5595pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5596 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5597 inner: ast::ArrayExpression {
5598 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5599 non_code_meta: Default::default(),
5600 digest: None,
5601 },
5602 start: Default::default(),
5603 end: Default::default(),
5604 module_id: Default::default(),
5605 node_path: None,
5606 outer_attrs: Default::default(),
5607 pre_comments: Default::default(),
5608 comment_start: Default::default(),
5609 })))
5610}
5611
5612fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
5613 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
5614 ast::ArrayExpression {
5615 elements: vec![
5616 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5617 point.x,
5618 )?)))),
5619 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5620 point.y,
5621 )?)))),
5622 ],
5623 non_code_meta: Default::default(),
5624 digest: None,
5625 },
5626 ))))
5627}
5628
5629fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5630 match expr {
5631 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5632 inner: ast::Literal::from(to_source_number(*number)?),
5633 start: Default::default(),
5634 end: Default::default(),
5635 module_id: Default::default(),
5636 node_path: None,
5637 outer_attrs: Default::default(),
5638 pre_comments: Default::default(),
5639 comment_start: Default::default(),
5640 }))),
5641 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5642 inner: ast::SketchVar {
5643 initial: Some(Box::new(ast::Node {
5644 inner: to_source_number(*number)?,
5645 start: Default::default(),
5646 end: Default::default(),
5647 module_id: Default::default(),
5648 node_path: None,
5649 outer_attrs: Default::default(),
5650 pre_comments: Default::default(),
5651 comment_start: Default::default(),
5652 })),
5653 digest: None,
5654 },
5655 start: Default::default(),
5656 end: Default::default(),
5657 module_id: Default::default(),
5658 node_path: None,
5659 outer_attrs: Default::default(),
5660 pre_comments: Default::default(),
5661 comment_start: Default::default(),
5662 }))),
5663 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5664 }
5665}
5666
5667fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5668 Ok(ast::NumericLiteral {
5669 value: number.value,
5670 suffix: number.units,
5671 raw: format_number_literal(number.value, number.units, None)?,
5672 digest: None,
5673 })
5674}
5675
5676pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5677 ast::Expr::Name(Box::new(ast_name(name)))
5678}
5679
5680fn ast_name(name: String) -> ast::Node<ast::Name> {
5681 ast::Node {
5682 inner: ast::Name {
5683 name: ast::Node {
5684 inner: ast::Identifier { name, digest: None },
5685 start: Default::default(),
5686 end: Default::default(),
5687 module_id: Default::default(),
5688 node_path: None,
5689 outer_attrs: Default::default(),
5690 pre_comments: Default::default(),
5691 comment_start: Default::default(),
5692 },
5693 path: Vec::new(),
5694 abs_path: false,
5695 digest: None,
5696 },
5697 start: Default::default(),
5698 end: Default::default(),
5699 module_id: Default::default(),
5700 node_path: None,
5701 outer_attrs: Default::default(),
5702 pre_comments: Default::default(),
5703 comment_start: Default::default(),
5704 }
5705}
5706
5707pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5708 ast::Name {
5709 name: ast::Node {
5710 inner: ast::Identifier {
5711 name: name.to_owned(),
5712 digest: None,
5713 },
5714 start: Default::default(),
5715 end: Default::default(),
5716 module_id: Default::default(),
5717 node_path: None,
5718 outer_attrs: Default::default(),
5719 pre_comments: Default::default(),
5720 comment_start: Default::default(),
5721 },
5722 path: Default::default(),
5723 abs_path: false,
5724 digest: None,
5725 }
5726}
5727
5728pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
5732 let elements = exprs.into_iter().collect::<Vec<_>>();
5733 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
5734
5735 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5737 elements,
5738 digest: None,
5739 non_code_meta: Default::default(),
5740 })));
5741
5742 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5744 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5745 unlabeled: Some(array_expr),
5746 arguments: Default::default(),
5747 digest: None,
5748 non_code_meta: Default::default(),
5749 })))
5750}
5751
5752pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5754 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5755 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5756 unlabeled: None,
5757 arguments: vec![
5758 ast::LabeledArg {
5759 label: Some(ast::Identifier::new(LINE_START_PARAM)),
5760 arg: start_ast,
5761 },
5762 ast::LabeledArg {
5763 label: Some(ast::Identifier::new(LINE_END_PARAM)),
5764 arg: end_ast,
5765 },
5766 ],
5767 digest: None,
5768 non_code_meta: Default::default(),
5769 })))
5770}
5771
5772pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5774 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5775 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5776 unlabeled: None,
5777 arguments: vec![
5778 ast::LabeledArg {
5779 label: Some(ast::Identifier::new(ARC_START_PARAM)),
5780 arg: start_ast,
5781 },
5782 ast::LabeledArg {
5783 label: Some(ast::Identifier::new(ARC_END_PARAM)),
5784 arg: end_ast,
5785 },
5786 ast::LabeledArg {
5787 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5788 arg: center_ast,
5789 },
5790 ],
5791 digest: None,
5792 non_code_meta: Default::default(),
5793 })))
5794}
5795
5796pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5798 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5799 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
5800 unlabeled: None,
5801 arguments: vec![
5802 ast::LabeledArg {
5803 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5804 arg: start_ast,
5805 },
5806 ast::LabeledArg {
5807 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5808 arg: center_ast,
5809 },
5810 ],
5811 digest: None,
5812 non_code_meta: Default::default(),
5813 })))
5814}
5815
5816pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
5818 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5819 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
5820 unlabeled: Some(line_expr),
5821 arguments: Default::default(),
5822 digest: None,
5823 non_code_meta: Default::default(),
5824 })))
5825}
5826
5827pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5829 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5830 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5831 unlabeled: Some(line_expr),
5832 arguments: Default::default(),
5833 digest: None,
5834 non_code_meta: Default::default(),
5835 })))
5836}
5837
5838pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5840 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5841 object: object_expr,
5842 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5843 name: ast::Node::no_src(ast::Identifier {
5844 name: property.to_string(),
5845 digest: None,
5846 }),
5847 path: Vec::new(),
5848 abs_path: false,
5849 digest: None,
5850 }))),
5851 computed: false,
5852 digest: None,
5853 })))
5854}
5855
5856fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5858 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5860 position.x,
5861 )?))));
5862 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5863 position.y,
5864 )?))));
5865 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5866 elements: vec![x_literal, y_literal],
5867 digest: None,
5868 non_code_meta: Default::default(),
5869 })));
5870
5871 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5873 elements: vec![point_expr, point_array],
5874 digest: None,
5875 non_code_meta: Default::default(),
5876 })));
5877
5878 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5880 ast::CallExpressionKw {
5881 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5882 unlabeled: Some(array_expr),
5883 arguments: Default::default(),
5884 digest: None,
5885 non_code_meta: Default::default(),
5886 },
5887 ))))
5888}
5889
5890pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5892 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5893 elements: line_exprs,
5894 digest: None,
5895 non_code_meta: Default::default(),
5896 })));
5897
5898 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5900 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5901 unlabeled: Some(array_expr),
5902 arguments: Default::default(),
5903 digest: None,
5904 non_code_meta: Default::default(),
5905 })))
5906}
5907
5908pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5910 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5911 elements: segment_exprs,
5912 digest: None,
5913 non_code_meta: Default::default(),
5914 })));
5915
5916 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5917 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5918 unlabeled: Some(array_expr),
5919 arguments: Default::default(),
5920 digest: None,
5921 non_code_meta: Default::default(),
5922 })))
5923}
5924
5925pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5927 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5928 elements: vec![seg1_expr, seg2_expr],
5929 digest: None,
5930 non_code_meta: Default::default(),
5931 })));
5932
5933 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5934 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5935 unlabeled: Some(array_expr),
5936 arguments: Default::default(),
5937 digest: None,
5938 non_code_meta: Default::default(),
5939 })))
5940}
5941
5942pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
5944 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5945 elements: input_exprs,
5946 digest: None,
5947 non_code_meta: Default::default(),
5948 })));
5949 let arguments = vec![ast::LabeledArg {
5950 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
5951 arg: axis_expr,
5952 }];
5953
5954 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5955 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
5956 unlabeled: Some(array_expr),
5957 arguments,
5958 digest: None,
5959 non_code_meta: Default::default(),
5960 })))
5961}
5962
5963pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
5965 let arguments = vec![ast::LabeledArg {
5966 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
5967 arg: point_expr,
5968 }];
5969
5970 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5971 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
5972 unlabeled: Some(segment_expr),
5973 arguments,
5974 digest: None,
5975 non_code_meta: Default::default(),
5976 })))
5977}
5978
5979#[cfg(all(feature = "artifact-graph", test))]
5980mod tests {
5981 use super::*;
5982 use crate::engine::PlaneName;
5983 use crate::execution::cache::SketchModeState;
5984 use crate::execution::cache::clear_mem_cache;
5985 use crate::execution::cache::read_old_memory;
5986 use crate::execution::cache::write_old_memory;
5987 use crate::front::Distance;
5988 use crate::front::Fixed;
5989 use crate::front::FixedPoint;
5990 use crate::front::Midpoint;
5991 use crate::front::Object;
5992 use crate::front::Plane;
5993 use crate::front::Sketch;
5994 use crate::front::Tangent;
5995 use crate::frontend::sketch::Vertical;
5996 use crate::pretty::NumericSuffix;
5997
5998 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
5999 for object in &scene_graph.objects {
6000 if let ObjectKind::Sketch(_) = &object.kind {
6001 return Some(object);
6002 }
6003 }
6004 None
6005 }
6006
6007 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6008 for object in &scene_graph.objects {
6009 if let ObjectKind::Face(_) = &object.kind {
6010 return Some(object);
6011 }
6012 }
6013 None
6014 }
6015
6016 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6017 for object in &scene_graph.objects {
6018 if matches!(&object.kind, ObjectKind::Wall(_)) {
6019 return Some(object.id);
6020 }
6021 }
6022 None
6023 }
6024
6025 #[test]
6026 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6027 let source = "\
6028region001 = region(point = [0.1, 0.1], sketch = s)
6029extrude001 = extrude(region001, length = 5)
6030revolve001 = revolve(region001, axis = Y)
6031sweep001 = sweep(region001, path = path001)
6032loft001 = loft(region001)
6033not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6034";
6035
6036 let program = Program::parse(source).unwrap().0.unwrap();
6037
6038 assert_eq!(
6039 region_name_from_sweep_variable(&program.ast, "extrude001"),
6040 Some("region001".to_owned())
6041 );
6042 assert_eq!(
6043 region_name_from_sweep_variable(&program.ast, "revolve001"),
6044 Some("region001".to_owned())
6045 );
6046 assert_eq!(
6047 region_name_from_sweep_variable(&program.ast, "sweep001"),
6048 Some("region001".to_owned())
6049 );
6050 assert_eq!(
6051 region_name_from_sweep_variable(&program.ast, "loft001"),
6052 Some("region001".to_owned())
6053 );
6054 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6055 }
6056
6057 #[track_caller]
6058 fn expect_sketch(object: &Object) -> &Sketch {
6059 if let ObjectKind::Sketch(sketch) = &object.kind {
6060 sketch
6061 } else {
6062 panic!("Object is not a sketch: {:?}", object);
6063 }
6064 }
6065
6066 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6067 let point_object = scene_graph.objects.get(point_id.0).unwrap();
6068 let ObjectKind::Segment {
6069 segment: Segment::Point(point),
6070 } = &point_object.kind
6071 else {
6072 panic!("Object is not a point segment: {point_object:?}");
6073 };
6074 point.position.clone()
6075 }
6076
6077 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6078 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6079 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6080 }
6081
6082 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6083 LineCtor {
6084 start: Point2d {
6085 x: Expr::Number(Number { value: start_x, units }),
6086 y: Expr::Number(Number { value: start_y, units }),
6087 },
6088 end: Point2d {
6089 x: Expr::Number(Number { value: end_x, units }),
6090 y: Expr::Number(Number { value: end_y, units }),
6091 },
6092 construction: None,
6093 }
6094 }
6095
6096 async fn create_sketch_with_single_line(
6097 frontend: &mut FrontendState,
6098 ctx: &ExecutorContext,
6099 mock_ctx: &ExecutorContext,
6100 version: Version,
6101 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6102 frontend.program = Program::empty();
6103
6104 let sketch_args = SketchCtor {
6105 on: Plane::Default(PlaneName::Xy),
6106 };
6107 let (_src_delta, _scene_delta, sketch_id) = frontend
6108 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6109 .await
6110 .unwrap();
6111
6112 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6113 let (source_delta, scene_graph_delta) = frontend
6114 .add_segment(mock_ctx, version, sketch_id, segment, None)
6115 .await
6116 .unwrap();
6117 let line_id = *scene_graph_delta
6118 .new_objects
6119 .last()
6120 .expect("Expected line object id to be created");
6121
6122 (sketch_id, line_id, source_delta, scene_graph_delta)
6123 }
6124
6125 #[tokio::test(flavor = "multi_thread")]
6126 async fn test_sketch_checkpoint_round_trip_restores_state() {
6127 let mut frontend = FrontendState::new();
6128 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6129 let mock_ctx = ExecutorContext::new_mock(None).await;
6130 let version = Version(0);
6131
6132 let (sketch_id, line_id, source_delta, scene_graph_delta) =
6133 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6134
6135 let expected_source = source_delta.text.clone();
6136 let expected_scene_graph = frontend.scene_graph.clone();
6137 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6138 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6139
6140 let checkpoint_id = frontend
6141 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6142 .await
6143 .unwrap();
6144
6145 let edited_segments = vec![ExistingSegmentCtor {
6146 id: line_id,
6147 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6148 }];
6149 let (edited_source, _edited_scene) = frontend
6150 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6151 .await
6152 .unwrap();
6153 assert_ne!(edited_source.text, expected_source);
6154
6155 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6156
6157 assert_eq!(restored.source_delta.text, expected_source);
6158 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6159 assert!(restored.scene_graph_delta.invalidates_ids);
6160 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6161 assert_eq!(frontend.scene_graph, expected_scene_graph);
6162 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6163
6164 ctx.close().await;
6165 mock_ctx.close().await;
6166 }
6167
6168 #[tokio::test(flavor = "multi_thread")]
6169 async fn test_sketch_checkpoints_prune_oldest_entries() {
6170 let mut frontend = FrontendState::new();
6171 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6172 let mock_ctx = ExecutorContext::new_mock(None).await;
6173 let version = Version(0);
6174
6175 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6176 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6177
6178 let mut checkpoint_ids = Vec::new();
6179 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6180 checkpoint_ids.push(
6181 frontend
6182 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6183 .await
6184 .unwrap(),
6185 );
6186 }
6187
6188 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6189 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6190
6191 let oldest_retained = checkpoint_ids[3];
6192 assert_eq!(
6193 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6194 Some(oldest_retained)
6195 );
6196
6197 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6198 assert!(evicted_restore.is_err());
6199 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6200
6201 frontend
6202 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6203 .await
6204 .unwrap();
6205
6206 ctx.close().await;
6207 mock_ctx.close().await;
6208 }
6209
6210 #[tokio::test(flavor = "multi_thread")]
6211 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6212 let mut frontend = FrontendState::new();
6213 let missing_checkpoint = SketchCheckpointId::new(999);
6214
6215 let err = frontend
6216 .restore_sketch_checkpoint(missing_checkpoint)
6217 .await
6218 .expect_err("Expected restore to fail for missing checkpoint");
6219
6220 assert!(err.msg.contains("Sketch checkpoint not found"));
6221 }
6222
6223 #[tokio::test(flavor = "multi_thread")]
6224 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6225 let mut frontend = FrontendState::new();
6226 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6227 let mock_ctx = ExecutorContext::new_mock(None).await;
6228 let version = Version(0);
6229
6230 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6231 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6232
6233 let checkpoint_a = frontend
6234 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6235 .await
6236 .unwrap();
6237 let checkpoint_b = frontend
6238 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6239 .await
6240 .unwrap();
6241 assert_eq!(frontend.sketch_checkpoints.len(), 2);
6242
6243 frontend.clear_sketch_checkpoints();
6244 assert!(frontend.sketch_checkpoints.is_empty());
6245 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6246 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6247
6248 ctx.close().await;
6249 mock_ctx.close().await;
6250 }
6251
6252 #[tokio::test(flavor = "multi_thread")]
6253 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6254 let mut frontend = FrontendState::new();
6255 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6256 let mock_ctx = ExecutorContext::new_mock(None).await;
6257 let version = Version(0);
6258
6259 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6260 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6261 let old_source = source_delta.text.clone();
6262 let old_checkpoint = frontend
6263 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6264 .await
6265 .unwrap();
6266 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6267
6268 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
6269 .unwrap()
6270 .0
6271 .unwrap();
6272
6273 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6274 let SetProgramOutcome::Success {
6275 checkpoint_id: Some(new_checkpoint),
6276 ..
6277 } = result
6278 else {
6279 panic!("Expected Success with a fresh checkpoint baseline");
6280 };
6281
6282 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6283
6284 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6285 assert_eq!(old_restore.source_delta.text, old_source);
6286
6287 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6288 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6289
6290 ctx.close().await;
6291 mock_ctx.close().await;
6292 }
6293
6294 #[tokio::test(flavor = "multi_thread")]
6295 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6296 let mut frontend = FrontendState::new();
6297 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6298 let mock_ctx = ExecutorContext::new_mock(None).await;
6299 let version = Version(0);
6300
6301 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6302 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6303 let old_checkpoint = frontend
6304 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6305 .await
6306 .unwrap();
6307 let checkpoint_count_before = frontend.sketch_checkpoints.len();
6308
6309 let failing_program = Program::parse(
6310 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6311 )
6312 .unwrap()
6313 .0
6314 .unwrap();
6315
6316 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6317 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6318 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6319 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6320
6321 ctx.close().await;
6322 mock_ctx.close().await;
6323 }
6324
6325 #[tokio::test(flavor = "multi_thread")]
6326 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6327 let mut frontend = FrontendState::new();
6328 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6329
6330 let program = Program::parse(
6331 "width = 2mm\nsketch001 = sketch(on = offsetPlane(XY, offset = width)) {\n line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])\n distance([line1.start, line1.end]) == width\n}\n",
6332 )
6333 .unwrap()
6334 .0
6335 .unwrap();
6336 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6337 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6338 panic!("Expected successful baseline program execution");
6339 };
6340
6341 clear_mem_cache().await;
6342 assert!(read_old_memory().await.is_none());
6343
6344 let checkpoint_without_mock_memory = frontend
6345 .create_sketch_checkpoint((*exec_outcome).clone())
6346 .await
6347 .unwrap();
6348
6349 write_old_memory(SketchModeState::new_for_tests()).await;
6350 assert!(read_old_memory().await.is_some());
6351
6352 let checkpoint_with_mock_memory = frontend
6353 .create_sketch_checkpoint((*exec_outcome).clone())
6354 .await
6355 .unwrap();
6356
6357 clear_mem_cache().await;
6358 assert!(read_old_memory().await.is_none());
6359
6360 frontend
6361 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6362 .await
6363 .unwrap();
6364 assert!(read_old_memory().await.is_some());
6365
6366 frontend
6367 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6368 .await
6369 .unwrap();
6370 assert!(read_old_memory().await.is_none());
6371
6372 ctx.close().await;
6373 }
6374
6375 #[tokio::test(flavor = "multi_thread")]
6376 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6377 let source = "\
6378sketch(on = XY) {
6379 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6380}
6381
6382bad = missing_name
6383";
6384 let program = Program::parse(source).unwrap().0.unwrap();
6385
6386 let mut frontend = FrontendState::new();
6387
6388 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6389 let mock_ctx = ExecutorContext::new_mock(None).await;
6390 let version = Version(0);
6391 let project_id = ProjectId(0);
6392 let file_id = FileId(0);
6393
6394 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6395 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6396 };
6397
6398 let sketch_id = frontend
6399 .scene_graph
6400 .objects
6401 .iter()
6402 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6403 .expect("Expected sketch object from errored hack_set_program");
6404
6405 frontend
6406 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6407 .await
6408 .unwrap();
6409
6410 ctx.close().await;
6411 mock_ctx.close().await;
6412 }
6413
6414 #[tokio::test(flavor = "multi_thread")]
6415 async fn test_new_sketch_add_point_edit_point() {
6416 let program = Program::empty();
6417
6418 let mut frontend = FrontendState::new();
6419 frontend.program = program;
6420
6421 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6422 let mock_ctx = ExecutorContext::new_mock(None).await;
6423 let version = Version(0);
6424
6425 let sketch_args = SketchCtor {
6426 on: Plane::Default(PlaneName::Xy),
6427 };
6428 let (_src_delta, scene_delta, sketch_id) = frontend
6429 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6430 .await
6431 .unwrap();
6432 assert_eq!(sketch_id, ObjectId(1));
6433 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6434 let sketch_object = &scene_delta.new_graph.objects[1];
6435 assert_eq!(sketch_object.id, ObjectId(1));
6436 assert_eq!(
6437 sketch_object.kind,
6438 ObjectKind::Sketch(Sketch {
6439 args: SketchCtor {
6440 on: Plane::Default(PlaneName::Xy)
6441 },
6442 plane: ObjectId(0),
6443 segments: vec![],
6444 constraints: vec![],
6445 })
6446 );
6447 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6448
6449 let point_ctor = PointCtor {
6450 position: Point2d {
6451 x: Expr::Number(Number {
6452 value: 1.0,
6453 units: NumericSuffix::Inch,
6454 }),
6455 y: Expr::Number(Number {
6456 value: 2.0,
6457 units: NumericSuffix::Inch,
6458 }),
6459 },
6460 };
6461 let segment = SegmentCtor::Point(point_ctor);
6462 let (src_delta, scene_delta) = frontend
6463 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6464 .await
6465 .unwrap();
6466 assert_eq!(
6467 src_delta.text.as_str(),
6468 "sketch001 = sketch(on = XY) {
6469 point(at = [1in, 2in])
6470}
6471"
6472 );
6473 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6474 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6475 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6476 assert_eq!(scene_object.id.0, i);
6477 }
6478
6479 let point_id = *scene_delta.new_objects.last().unwrap();
6480
6481 let point_ctor = PointCtor {
6482 position: Point2d {
6483 x: Expr::Number(Number {
6484 value: 3.0,
6485 units: NumericSuffix::Inch,
6486 }),
6487 y: Expr::Number(Number {
6488 value: 4.0,
6489 units: NumericSuffix::Inch,
6490 }),
6491 },
6492 };
6493 let segments = vec![ExistingSegmentCtor {
6494 id: point_id,
6495 ctor: SegmentCtor::Point(point_ctor),
6496 }];
6497 let (src_delta, scene_delta) = frontend
6498 .edit_segments(&mock_ctx, version, sketch_id, segments)
6499 .await
6500 .unwrap();
6501 assert_eq!(
6502 src_delta.text.as_str(),
6503 "sketch001 = sketch(on = XY) {
6504 point(at = [3in, 4in])
6505}
6506"
6507 );
6508 assert_eq!(scene_delta.new_objects, vec![]);
6509 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6510
6511 ctx.close().await;
6512 mock_ctx.close().await;
6513 }
6514
6515 #[tokio::test(flavor = "multi_thread")]
6516 async fn test_new_sketch_add_line_edit_line() {
6517 let program = Program::empty();
6518
6519 let mut frontend = FrontendState::new();
6520 frontend.program = program;
6521
6522 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6523 let mock_ctx = ExecutorContext::new_mock(None).await;
6524 let version = Version(0);
6525
6526 let sketch_args = SketchCtor {
6527 on: Plane::Default(PlaneName::Xy),
6528 };
6529 let (_src_delta, scene_delta, sketch_id) = frontend
6530 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6531 .await
6532 .unwrap();
6533 assert_eq!(sketch_id, ObjectId(1));
6534 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6535 let sketch_object = &scene_delta.new_graph.objects[1];
6536 assert_eq!(sketch_object.id, ObjectId(1));
6537 assert_eq!(
6538 sketch_object.kind,
6539 ObjectKind::Sketch(Sketch {
6540 args: SketchCtor {
6541 on: Plane::Default(PlaneName::Xy)
6542 },
6543 plane: ObjectId(0),
6544 segments: vec![],
6545 constraints: vec![],
6546 })
6547 );
6548 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6549
6550 let line_ctor = LineCtor {
6551 start: Point2d {
6552 x: Expr::Number(Number {
6553 value: 0.0,
6554 units: NumericSuffix::Mm,
6555 }),
6556 y: Expr::Number(Number {
6557 value: 0.0,
6558 units: NumericSuffix::Mm,
6559 }),
6560 },
6561 end: Point2d {
6562 x: Expr::Number(Number {
6563 value: 10.0,
6564 units: NumericSuffix::Mm,
6565 }),
6566 y: Expr::Number(Number {
6567 value: 10.0,
6568 units: NumericSuffix::Mm,
6569 }),
6570 },
6571 construction: None,
6572 };
6573 let segment = SegmentCtor::Line(line_ctor);
6574 let (src_delta, scene_delta) = frontend
6575 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6576 .await
6577 .unwrap();
6578 assert_eq!(
6579 src_delta.text.as_str(),
6580 "sketch001 = sketch(on = XY) {
6581 line(start = [0mm, 0mm], end = [10mm, 10mm])
6582}
6583"
6584 );
6585 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6586 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6587 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6588 assert_eq!(scene_object.id.0, i);
6589 }
6590
6591 let line = *scene_delta.new_objects.last().unwrap();
6593
6594 let line_ctor = LineCtor {
6595 start: Point2d {
6596 x: Expr::Number(Number {
6597 value: 1.0,
6598 units: NumericSuffix::Mm,
6599 }),
6600 y: Expr::Number(Number {
6601 value: 2.0,
6602 units: NumericSuffix::Mm,
6603 }),
6604 },
6605 end: Point2d {
6606 x: Expr::Number(Number {
6607 value: 13.0,
6608 units: NumericSuffix::Mm,
6609 }),
6610 y: Expr::Number(Number {
6611 value: 14.0,
6612 units: NumericSuffix::Mm,
6613 }),
6614 },
6615 construction: None,
6616 };
6617 let segments = vec![ExistingSegmentCtor {
6618 id: line,
6619 ctor: SegmentCtor::Line(line_ctor),
6620 }];
6621 let (src_delta, scene_delta) = frontend
6622 .edit_segments(&mock_ctx, version, sketch_id, segments)
6623 .await
6624 .unwrap();
6625 assert_eq!(
6626 src_delta.text.as_str(),
6627 "sketch001 = sketch(on = XY) {
6628 line(start = [1mm, 2mm], end = [13mm, 14mm])
6629}
6630"
6631 );
6632 assert_eq!(scene_delta.new_objects, vec![]);
6633 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6634
6635 ctx.close().await;
6636 mock_ctx.close().await;
6637 }
6638
6639 #[tokio::test(flavor = "multi_thread")]
6640 async fn test_new_sketch_add_arc_edit_arc() {
6641 let program = Program::empty();
6642
6643 let mut frontend = FrontendState::new();
6644 frontend.program = program;
6645
6646 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6647 let mock_ctx = ExecutorContext::new_mock(None).await;
6648 let version = Version(0);
6649
6650 let sketch_args = SketchCtor {
6651 on: Plane::Default(PlaneName::Xy),
6652 };
6653 let (_src_delta, scene_delta, sketch_id) = frontend
6654 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6655 .await
6656 .unwrap();
6657 assert_eq!(sketch_id, ObjectId(1));
6658 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6659 let sketch_object = &scene_delta.new_graph.objects[1];
6660 assert_eq!(sketch_object.id, ObjectId(1));
6661 assert_eq!(
6662 sketch_object.kind,
6663 ObjectKind::Sketch(Sketch {
6664 args: SketchCtor {
6665 on: Plane::Default(PlaneName::Xy),
6666 },
6667 plane: ObjectId(0),
6668 segments: vec![],
6669 constraints: vec![],
6670 })
6671 );
6672 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6673
6674 let arc_ctor = ArcCtor {
6675 start: Point2d {
6676 x: Expr::Var(Number {
6677 value: 0.0,
6678 units: NumericSuffix::Mm,
6679 }),
6680 y: Expr::Var(Number {
6681 value: 0.0,
6682 units: NumericSuffix::Mm,
6683 }),
6684 },
6685 end: Point2d {
6686 x: Expr::Var(Number {
6687 value: 10.0,
6688 units: NumericSuffix::Mm,
6689 }),
6690 y: Expr::Var(Number {
6691 value: 10.0,
6692 units: NumericSuffix::Mm,
6693 }),
6694 },
6695 center: Point2d {
6696 x: Expr::Var(Number {
6697 value: 10.0,
6698 units: NumericSuffix::Mm,
6699 }),
6700 y: Expr::Var(Number {
6701 value: 0.0,
6702 units: NumericSuffix::Mm,
6703 }),
6704 },
6705 construction: None,
6706 };
6707 let segment = SegmentCtor::Arc(arc_ctor);
6708 let (src_delta, scene_delta) = frontend
6709 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6710 .await
6711 .unwrap();
6712 assert_eq!(
6713 src_delta.text.as_str(),
6714 "sketch001 = sketch(on = XY) {
6715 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6716}
6717"
6718 );
6719 assert_eq!(
6720 scene_delta.new_objects,
6721 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6722 );
6723 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6724 assert_eq!(scene_object.id.0, i);
6725 }
6726 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6727
6728 let arc = *scene_delta.new_objects.last().unwrap();
6730
6731 let arc_ctor = ArcCtor {
6732 start: Point2d {
6733 x: Expr::Var(Number {
6734 value: 1.0,
6735 units: NumericSuffix::Mm,
6736 }),
6737 y: Expr::Var(Number {
6738 value: 2.0,
6739 units: NumericSuffix::Mm,
6740 }),
6741 },
6742 end: Point2d {
6743 x: Expr::Var(Number {
6744 value: 13.0,
6745 units: NumericSuffix::Mm,
6746 }),
6747 y: Expr::Var(Number {
6748 value: 14.0,
6749 units: NumericSuffix::Mm,
6750 }),
6751 },
6752 center: Point2d {
6753 x: Expr::Var(Number {
6754 value: 13.0,
6755 units: NumericSuffix::Mm,
6756 }),
6757 y: Expr::Var(Number {
6758 value: 2.0,
6759 units: NumericSuffix::Mm,
6760 }),
6761 },
6762 construction: None,
6763 };
6764 let segments = vec![ExistingSegmentCtor {
6765 id: arc,
6766 ctor: SegmentCtor::Arc(arc_ctor),
6767 }];
6768 let (src_delta, scene_delta) = frontend
6769 .edit_segments(&mock_ctx, version, sketch_id, segments)
6770 .await
6771 .unwrap();
6772 assert_eq!(
6773 src_delta.text.as_str(),
6774 "sketch001 = sketch(on = XY) {
6775 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6776}
6777"
6778 );
6779 assert_eq!(scene_delta.new_objects, vec![]);
6780 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6781
6782 ctx.close().await;
6783 mock_ctx.close().await;
6784 }
6785
6786 #[tokio::test(flavor = "multi_thread")]
6787 async fn test_new_sketch_add_circle_edit_circle() {
6788 let program = Program::empty();
6789
6790 let mut frontend = FrontendState::new();
6791 frontend.program = program;
6792
6793 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6794 let mock_ctx = ExecutorContext::new_mock(None).await;
6795 let version = Version(0);
6796
6797 let sketch_args = SketchCtor {
6798 on: Plane::Default(PlaneName::Xy),
6799 };
6800 let (_src_delta, _scene_delta, sketch_id) = frontend
6801 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6802 .await
6803 .unwrap();
6804
6805 let circle_ctor = CircleCtor {
6807 start: Point2d {
6808 x: Expr::Var(Number {
6809 value: 5.0,
6810 units: NumericSuffix::Mm,
6811 }),
6812 y: Expr::Var(Number {
6813 value: 0.0,
6814 units: NumericSuffix::Mm,
6815 }),
6816 },
6817 center: Point2d {
6818 x: Expr::Var(Number {
6819 value: 0.0,
6820 units: NumericSuffix::Mm,
6821 }),
6822 y: Expr::Var(Number {
6823 value: 0.0,
6824 units: NumericSuffix::Mm,
6825 }),
6826 },
6827 construction: None,
6828 };
6829 let segment = SegmentCtor::Circle(circle_ctor);
6830 let (src_delta, scene_delta) = frontend
6831 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6832 .await
6833 .unwrap();
6834 assert_eq!(
6835 src_delta.text.as_str(),
6836 "sketch001 = sketch(on = XY) {
6837 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6838}
6839"
6840 );
6841 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6843 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6844
6845 let circle = *scene_delta.new_objects.last().unwrap();
6846
6847 let circle_ctor = CircleCtor {
6849 start: Point2d {
6850 x: Expr::Var(Number {
6851 value: 10.0,
6852 units: NumericSuffix::Mm,
6853 }),
6854 y: Expr::Var(Number {
6855 value: 0.0,
6856 units: NumericSuffix::Mm,
6857 }),
6858 },
6859 center: Point2d {
6860 x: Expr::Var(Number {
6861 value: 3.0,
6862 units: NumericSuffix::Mm,
6863 }),
6864 y: Expr::Var(Number {
6865 value: 4.0,
6866 units: NumericSuffix::Mm,
6867 }),
6868 },
6869 construction: None,
6870 };
6871 let segments = vec![ExistingSegmentCtor {
6872 id: circle,
6873 ctor: SegmentCtor::Circle(circle_ctor),
6874 }];
6875 let (src_delta, scene_delta) = frontend
6876 .edit_segments(&mock_ctx, version, sketch_id, segments)
6877 .await
6878 .unwrap();
6879 assert_eq!(
6880 src_delta.text.as_str(),
6881 "sketch001 = sketch(on = XY) {
6882 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6883}
6884"
6885 );
6886 assert_eq!(scene_delta.new_objects, vec![]);
6887 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6888
6889 ctx.close().await;
6890 mock_ctx.close().await;
6891 }
6892
6893 #[tokio::test(flavor = "multi_thread")]
6894 async fn test_delete_circle() {
6895 let initial_source = "sketch001 = sketch(on = XY) {
6896 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6897}
6898";
6899
6900 let program = Program::parse(initial_source).unwrap().0.unwrap();
6901 let mut frontend = FrontendState::new();
6902
6903 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6904 let mock_ctx = ExecutorContext::new_mock(None).await;
6905 let version = Version(0);
6906
6907 frontend.hack_set_program(&ctx, program).await.unwrap();
6908 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6909 let sketch_id = sketch_object.id;
6910 let sketch = expect_sketch(sketch_object);
6911
6912 assert_eq!(sketch.segments.len(), 3);
6914 let circle_id = sketch.segments[2];
6915
6916 let (src_delta, scene_delta) = frontend
6918 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6919 .await
6920 .unwrap();
6921 assert_eq!(
6922 src_delta.text.as_str(),
6923 "sketch001 = sketch(on = XY) {
6924}
6925"
6926 );
6927 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6928 let new_sketch = expect_sketch(new_sketch_object);
6929 assert_eq!(new_sketch.segments.len(), 0);
6930
6931 ctx.close().await;
6932 mock_ctx.close().await;
6933 }
6934
6935 #[tokio::test(flavor = "multi_thread")]
6936 async fn test_edit_circle_via_point() {
6937 let initial_source = "sketch001 = sketch(on = XY) {
6938 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6939}
6940";
6941
6942 let program = Program::parse(initial_source).unwrap().0.unwrap();
6943 let mut frontend = FrontendState::new();
6944
6945 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6946 let mock_ctx = ExecutorContext::new_mock(None).await;
6947 let version = Version(0);
6948
6949 frontend.hack_set_program(&ctx, program).await.unwrap();
6950 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6951 let sketch_id = sketch_object.id;
6952 let sketch = expect_sketch(sketch_object);
6953
6954 let circle_id = sketch
6956 .segments
6957 .iter()
6958 .copied()
6959 .find(|seg_id| {
6960 matches!(
6961 &frontend.scene_graph.objects[seg_id.0].kind,
6962 ObjectKind::Segment {
6963 segment: Segment::Circle(_)
6964 }
6965 )
6966 })
6967 .expect("Expected a circle segment in sketch");
6968 let circle_object = &frontend.scene_graph.objects[circle_id.0];
6969 let ObjectKind::Segment {
6970 segment: Segment::Circle(circle),
6971 } = &circle_object.kind
6972 else {
6973 panic!("Expected circle segment, got: {:?}", circle_object.kind);
6974 };
6975 let start_point_id = circle.start;
6976
6977 let segments = vec![ExistingSegmentCtor {
6979 id: start_point_id,
6980 ctor: SegmentCtor::Point(PointCtor {
6981 position: Point2d {
6982 x: Expr::Var(Number {
6983 value: 7.0,
6984 units: NumericSuffix::Mm,
6985 }),
6986 y: Expr::Var(Number {
6987 value: 1.0,
6988 units: NumericSuffix::Mm,
6989 }),
6990 },
6991 }),
6992 }];
6993 let (src_delta, _scene_delta) = frontend
6994 .edit_segments(&mock_ctx, version, sketch_id, segments)
6995 .await
6996 .unwrap();
6997 assert_eq!(
6998 src_delta.text.as_str(),
6999 "sketch001 = sketch(on = XY) {
7000 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7001}
7002"
7003 );
7004
7005 ctx.close().await;
7006 mock_ctx.close().await;
7007 }
7008
7009 #[tokio::test(flavor = "multi_thread")]
7010 async fn test_add_line_when_sketch_block_uses_variable() {
7011 let initial_source = "s = sketch(on = XY) {}
7012";
7013
7014 let program = Program::parse(initial_source).unwrap().0.unwrap();
7015
7016 let mut frontend = FrontendState::new();
7017
7018 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7019 let mock_ctx = ExecutorContext::new_mock(None).await;
7020 let version = Version(0);
7021
7022 frontend.hack_set_program(&ctx, program).await.unwrap();
7023 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7024 let sketch_id = sketch_object.id;
7025
7026 let line_ctor = LineCtor {
7027 start: Point2d {
7028 x: Expr::Number(Number {
7029 value: 0.0,
7030 units: NumericSuffix::Mm,
7031 }),
7032 y: Expr::Number(Number {
7033 value: 0.0,
7034 units: NumericSuffix::Mm,
7035 }),
7036 },
7037 end: Point2d {
7038 x: Expr::Number(Number {
7039 value: 10.0,
7040 units: NumericSuffix::Mm,
7041 }),
7042 y: Expr::Number(Number {
7043 value: 10.0,
7044 units: NumericSuffix::Mm,
7045 }),
7046 },
7047 construction: None,
7048 };
7049 let segment = SegmentCtor::Line(line_ctor);
7050 let (src_delta, scene_delta) = frontend
7051 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7052 .await
7053 .unwrap();
7054 assert_eq!(
7055 src_delta.text.as_str(),
7056 "s = sketch(on = XY) {
7057 line(start = [0mm, 0mm], end = [10mm, 10mm])
7058}
7059"
7060 );
7061 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7062 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7063
7064 ctx.close().await;
7065 mock_ctx.close().await;
7066 }
7067
7068 #[tokio::test(flavor = "multi_thread")]
7069 async fn test_new_sketch_add_line_delete_sketch() {
7070 let program = Program::empty();
7071
7072 let mut frontend = FrontendState::new();
7073 frontend.program = program;
7074
7075 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7076 let mock_ctx = ExecutorContext::new_mock(None).await;
7077 let version = Version(0);
7078
7079 let sketch_args = SketchCtor {
7080 on: Plane::Default(PlaneName::Xy),
7081 };
7082 let (_src_delta, scene_delta, sketch_id) = frontend
7083 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7084 .await
7085 .unwrap();
7086 assert_eq!(sketch_id, ObjectId(1));
7087 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7088 let sketch_object = &scene_delta.new_graph.objects[1];
7089 assert_eq!(sketch_object.id, ObjectId(1));
7090 assert_eq!(
7091 sketch_object.kind,
7092 ObjectKind::Sketch(Sketch {
7093 args: SketchCtor {
7094 on: Plane::Default(PlaneName::Xy)
7095 },
7096 plane: ObjectId(0),
7097 segments: vec![],
7098 constraints: vec![],
7099 })
7100 );
7101 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7102
7103 let line_ctor = LineCtor {
7104 start: Point2d {
7105 x: Expr::Number(Number {
7106 value: 0.0,
7107 units: NumericSuffix::Mm,
7108 }),
7109 y: Expr::Number(Number {
7110 value: 0.0,
7111 units: NumericSuffix::Mm,
7112 }),
7113 },
7114 end: Point2d {
7115 x: Expr::Number(Number {
7116 value: 10.0,
7117 units: NumericSuffix::Mm,
7118 }),
7119 y: Expr::Number(Number {
7120 value: 10.0,
7121 units: NumericSuffix::Mm,
7122 }),
7123 },
7124 construction: None,
7125 };
7126 let segment = SegmentCtor::Line(line_ctor);
7127 let (src_delta, scene_delta) = frontend
7128 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7129 .await
7130 .unwrap();
7131 assert_eq!(
7132 src_delta.text.as_str(),
7133 "sketch001 = sketch(on = XY) {
7134 line(start = [0mm, 0mm], end = [10mm, 10mm])
7135}
7136"
7137 );
7138 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7139
7140 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7141 assert_eq!(src_delta.text.as_str(), "");
7142 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7143
7144 ctx.close().await;
7145 mock_ctx.close().await;
7146 }
7147
7148 #[tokio::test(flavor = "multi_thread")]
7149 async fn test_delete_sketch_when_sketch_block_uses_variable() {
7150 let initial_source = "s = sketch(on = XY) {}
7151";
7152
7153 let program = Program::parse(initial_source).unwrap().0.unwrap();
7154
7155 let mut frontend = FrontendState::new();
7156
7157 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7158 let mock_ctx = ExecutorContext::new_mock(None).await;
7159 let version = Version(0);
7160
7161 frontend.hack_set_program(&ctx, program).await.unwrap();
7162 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7163 let sketch_id = sketch_object.id;
7164
7165 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7166 assert_eq!(src_delta.text.as_str(), "");
7167 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7168
7169 ctx.close().await;
7170 mock_ctx.close().await;
7171 }
7172
7173 #[tokio::test(flavor = "multi_thread")]
7174 async fn test_delete_sketch_after_comment() {
7175 let initial_source = "sketch001 = sketch(on = XZ) {
7176}
7177";
7178
7179 let program = Program::parse(initial_source).unwrap().0.unwrap();
7180 let mut frontend = FrontendState::new();
7181
7182 let ctx = ExecutorContext::new_with_engine(
7183 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7184 Default::default(),
7185 );
7186 let version = Version(0);
7187
7188 frontend.hack_set_program(&ctx, program).await.unwrap();
7189 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7190 let sketch_id = sketch_object.id;
7191 let original_source = sketch_object.source.clone();
7192
7193 let commented_source = "// test 1
7194sketch001 = sketch(on = XZ) {
7195}
7196";
7197 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7198 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7199
7200 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7201 assert_eq!(cached_sketch_object.source, original_source);
7202
7203 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7204 assert!(
7205 !src_delta.text.contains("sketch001"),
7206 "sketch was not deleted: {}",
7207 src_delta.text
7208 );
7209 assert_eq!(src_delta.text.as_str(), "// test 1\n");
7211 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7212
7213 ctx.close().await;
7214 }
7215
7216 #[tokio::test(flavor = "multi_thread")]
7217 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7218 let initial_source = "sketch001 = sketch(on = XZ) {
7219}
7220foo = 1
7221";
7222
7223 let program = Program::parse(initial_source).unwrap().0.unwrap();
7224 let mut frontend = FrontendState::new();
7225
7226 let ctx = ExecutorContext::new_with_engine(
7227 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7228 Default::default(),
7229 );
7230 let version = Version(0);
7231
7232 frontend.hack_set_program(&ctx, program).await.unwrap();
7233 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7234 let sketch_id = sketch_object.id;
7235
7236 let commented_source = "// keep me
7237sketch001 = sketch(on = XZ) {
7238}
7239foo = 1
7240";
7241 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7242 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7243
7244 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7245 assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7247
7248 ctx.close().await;
7249 }
7250
7251 #[tokio::test(flavor = "multi_thread")]
7252 async fn test_delete_segment_preserves_pre_comment() {
7253 let initial_source = "\
7254sketch(on = XY) {
7255 point(at = [var 1, var 2])
7256 // describe the middle point
7257 point(at = [var 3, var 4])
7258 point(at = [var 5, var 6])
7259}
7260";
7261
7262 let program = Program::parse(initial_source).unwrap().0.unwrap();
7263 let mut frontend = FrontendState::new();
7264
7265 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7266 let mock_ctx = ExecutorContext::new_mock(None).await;
7267 let version = Version(0);
7268
7269 frontend.hack_set_program(&ctx, program).await.unwrap();
7270 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7271 let sketch_id = sketch_object.id;
7272 let sketch = expect_sketch(sketch_object);
7273
7274 let middle_point_id = *sketch.segments.get(1).unwrap();
7275
7276 let (src_delta, _scene_delta) = frontend
7277 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7278 .await
7279 .unwrap();
7280 assert_eq!(
7283 src_delta.text.as_str(),
7284 "\
7285sketch(on = XY) {
7286 point(at = [var 1mm, var 2mm])
7287 // describe the middle point
7288 point(at = [var 5mm, var 6mm])
7289}
7290"
7291 );
7292
7293 ctx.close().await;
7294 mock_ctx.close().await;
7295 }
7296
7297 #[tokio::test(flavor = "multi_thread")]
7298 async fn test_delete_last_segment_preserves_pre_comment() {
7299 let initial_source = "\
7300sketch(on = XY) {
7301 point(at = [var 1, var 2])
7302 // describe the trailing point
7303 point(at = [var 3, var 4])
7304}
7305";
7306
7307 let program = Program::parse(initial_source).unwrap().0.unwrap();
7308 let mut frontend = FrontendState::new();
7309
7310 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7311 let mock_ctx = ExecutorContext::new_mock(None).await;
7312 let version = Version(0);
7313
7314 frontend.hack_set_program(&ctx, program).await.unwrap();
7315 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7316 let sketch_id = sketch_object.id;
7317 let sketch = expect_sketch(sketch_object);
7318
7319 let last_point_id = *sketch.segments.last().unwrap();
7320
7321 let (src_delta, _scene_delta) = frontend
7322 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7323 .await
7324 .unwrap();
7325 assert_eq!(
7328 src_delta.text.as_str(),
7329 "\
7330sketch(on = XY) {
7331 point(at = [var 1mm, var 2mm])
7332 // describe the trailing point
7333}
7334"
7335 );
7336
7337 ctx.close().await;
7338 mock_ctx.close().await;
7339 }
7340
7341 #[tokio::test(flavor = "multi_thread")]
7342 async fn test_delete_segment_drops_inline_trailing_comment() {
7343 let initial_source = "\
7344sketch(on = XY) {
7345 point(at = [var 1, var 2])
7346 point(at = [var 3, var 4]) // same-line note that gets dropped
7347 point(at = [var 5, var 6])
7348}
7349";
7350
7351 let program = Program::parse(initial_source).unwrap().0.unwrap();
7352 let mut frontend = FrontendState::new();
7353
7354 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7355 let mock_ctx = ExecutorContext::new_mock(None).await;
7356 let version = Version(0);
7357
7358 frontend.hack_set_program(&ctx, program).await.unwrap();
7359 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7360 let sketch_id = sketch_object.id;
7361 let sketch = expect_sketch(sketch_object);
7362
7363 let middle_point_id = *sketch.segments.get(1).unwrap();
7364
7365 let (src_delta, _scene_delta) = frontend
7366 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7367 .await
7368 .unwrap();
7369 assert!(
7371 !src_delta.text.contains("same-line note"),
7372 "inline comment should have been removed: {}",
7373 src_delta.text
7374 );
7375
7376 ctx.close().await;
7377 mock_ctx.close().await;
7378 }
7379
7380 #[tokio::test(flavor = "multi_thread")]
7381 async fn test_delete_segments_preserves_block_comments_across_positions() {
7382 let initial_source = "\
7390sketch(on = XY) {
7391 /* above first - moves to middle */
7392 point(at = [var 1, var 2]) /* same-line on first - dropped */
7393 /* above middle - stays */
7394 point(at = [var 3, var 4])
7395 /* above last - moves to trailing meta */
7396 point(at = [var 5, var 6])
7397}
7398";
7399
7400 let program = Program::parse(initial_source).unwrap().0.unwrap();
7401 let mut frontend = FrontendState::new();
7402
7403 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7404 let mock_ctx = ExecutorContext::new_mock(None).await;
7405 let version = Version(0);
7406
7407 frontend.hack_set_program(&ctx, program).await.unwrap();
7408 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7409 let sketch_id = sketch_object.id;
7410 let sketch = expect_sketch(sketch_object);
7411
7412 let first_point_id = *sketch.segments.first().unwrap();
7413 let last_point_id = *sketch.segments.last().unwrap();
7414
7415 let (src_delta, _scene_delta) = frontend
7416 .delete_objects(
7417 &mock_ctx,
7418 version,
7419 sketch_id,
7420 Vec::new(),
7421 vec![first_point_id, last_point_id],
7422 )
7423 .await
7424 .unwrap();
7425 assert_eq!(
7426 src_delta.text.as_str(),
7427 "\
7428sketch(on = XY) {
7429 /* above first - moves to middle */
7430 /* above middle - stays */
7431 point(at = [var 3mm, var 4mm])
7432 /* above last - moves to trailing meta */
7433}
7434"
7435 );
7436
7437 ctx.close().await;
7438 mock_ctx.close().await;
7439 }
7440
7441 #[tokio::test(flavor = "multi_thread")]
7442 async fn test_edit_line_when_editing_its_start_point() {
7443 let initial_source = "\
7444sketch(on = XY) {
7445 line(start = [var 1, var 2], end = [var 3, var 4])
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
7462 let point_id = *sketch.segments.first().unwrap();
7463
7464 let point_ctor = PointCtor {
7465 position: Point2d {
7466 x: Expr::Var(Number {
7467 value: 5.0,
7468 units: NumericSuffix::Inch,
7469 }),
7470 y: Expr::Var(Number {
7471 value: 6.0,
7472 units: NumericSuffix::Inch,
7473 }),
7474 },
7475 };
7476 let segments = vec![ExistingSegmentCtor {
7477 id: point_id,
7478 ctor: SegmentCtor::Point(point_ctor),
7479 }];
7480 let (src_delta, scene_delta) = frontend
7481 .edit_segments(&mock_ctx, version, sketch_id, segments)
7482 .await
7483 .unwrap();
7484 assert_eq!(
7485 src_delta.text.as_str(),
7486 "\
7487sketch(on = XY) {
7488 line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7489}
7490"
7491 );
7492 assert_eq!(scene_delta.new_objects, vec![]);
7493 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7494
7495 ctx.close().await;
7496 mock_ctx.close().await;
7497 }
7498
7499 #[tokio::test(flavor = "multi_thread")]
7500 async fn test_edit_line_when_editing_its_end_point() {
7501 let initial_source = "\
7502sketch(on = XY) {
7503 line(start = [var 1, var 2], end = [var 3, var 4])
7504}
7505";
7506
7507 let program = Program::parse(initial_source).unwrap().0.unwrap();
7508
7509 let mut frontend = FrontendState::new();
7510
7511 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7512 let mock_ctx = ExecutorContext::new_mock(None).await;
7513 let version = Version(0);
7514
7515 frontend.hack_set_program(&ctx, program).await.unwrap();
7516 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7517 let sketch_id = sketch_object.id;
7518 let sketch = expect_sketch(sketch_object);
7519 let point_id = *sketch.segments.get(1).unwrap();
7520
7521 let point_ctor = PointCtor {
7522 position: Point2d {
7523 x: Expr::Var(Number {
7524 value: 5.0,
7525 units: NumericSuffix::Inch,
7526 }),
7527 y: Expr::Var(Number {
7528 value: 6.0,
7529 units: NumericSuffix::Inch,
7530 }),
7531 },
7532 };
7533 let segments = vec![ExistingSegmentCtor {
7534 id: point_id,
7535 ctor: SegmentCtor::Point(point_ctor),
7536 }];
7537 let (src_delta, scene_delta) = frontend
7538 .edit_segments(&mock_ctx, version, sketch_id, segments)
7539 .await
7540 .unwrap();
7541 assert_eq!(
7542 src_delta.text.as_str(),
7543 "\
7544sketch(on = XY) {
7545 line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
7546}
7547"
7548 );
7549 assert_eq!(scene_delta.new_objects, vec![]);
7550 assert_eq!(
7551 scene_delta.new_graph.objects.len(),
7552 5,
7553 "{:#?}",
7554 scene_delta.new_graph.objects
7555 );
7556
7557 ctx.close().await;
7558 mock_ctx.close().await;
7559 }
7560
7561 #[tokio::test(flavor = "multi_thread")]
7562 async fn test_edit_line_with_coincident_feedback() {
7563 let initial_source = "\
7564sketch(on = XY) {
7565 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
7566 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7567 fixed([line1.start, [0, 0]])
7568 coincident([line1.end, line2.start])
7569 equalLength([line1, line2])
7570}
7571";
7572
7573 let program = Program::parse(initial_source).unwrap().0.unwrap();
7574
7575 let mut frontend = FrontendState::new();
7576
7577 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7578 let mock_ctx = ExecutorContext::new_mock(None).await;
7579 let version = Version(0);
7580
7581 frontend.hack_set_program(&ctx, program).await.unwrap();
7582 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7583 let sketch_id = sketch_object.id;
7584 let sketch = expect_sketch(sketch_object);
7585 let line2_end_id = *sketch.segments.get(4).unwrap();
7586
7587 let segments = vec![ExistingSegmentCtor {
7588 id: line2_end_id,
7589 ctor: SegmentCtor::Point(PointCtor {
7590 position: Point2d {
7591 x: Expr::Var(Number {
7592 value: 9.0,
7593 units: NumericSuffix::None,
7594 }),
7595 y: Expr::Var(Number {
7596 value: 10.0,
7597 units: NumericSuffix::None,
7598 }),
7599 },
7600 }),
7601 }];
7602 let (src_delta, scene_delta) = frontend
7603 .edit_segments(&mock_ctx, version, sketch_id, segments)
7604 .await
7605 .unwrap();
7606 assert_eq!(
7607 src_delta.text.as_str(),
7608 "\
7609sketch(on = XY) {
7610 line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7611 line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7612 fixed([line1.start, [0, 0]])
7613 coincident([line1.end, line2.start])
7614 equalLength([line1, line2])
7615}
7616"
7617 );
7618 assert_eq!(
7619 scene_delta.new_graph.objects.len(),
7620 11,
7621 "{:#?}",
7622 scene_delta.new_graph.objects
7623 );
7624
7625 ctx.close().await;
7626 mock_ctx.close().await;
7627 }
7628
7629 #[tokio::test(flavor = "multi_thread")]
7630 async fn test_delete_point_without_var() {
7631 let initial_source = "\
7632sketch(on = XY) {
7633 point(at = [var 1, var 2])
7634 point(at = [var 3, var 4])
7635 point(at = [var 5, var 6])
7636}
7637";
7638
7639 let program = Program::parse(initial_source).unwrap().0.unwrap();
7640
7641 let mut frontend = FrontendState::new();
7642
7643 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7644 let mock_ctx = ExecutorContext::new_mock(None).await;
7645 let version = Version(0);
7646
7647 frontend.hack_set_program(&ctx, program).await.unwrap();
7648 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7649 let sketch_id = sketch_object.id;
7650 let sketch = expect_sketch(sketch_object);
7651
7652 let point_id = *sketch.segments.get(1).unwrap();
7653
7654 let (src_delta, scene_delta) = frontend
7655 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7656 .await
7657 .unwrap();
7658 assert_eq!(
7659 src_delta.text.as_str(),
7660 "\
7661sketch(on = XY) {
7662 point(at = [var 1mm, var 2mm])
7663 point(at = [var 5mm, var 6mm])
7664}
7665"
7666 );
7667 assert_eq!(scene_delta.new_objects, vec![]);
7668 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7669
7670 ctx.close().await;
7671 mock_ctx.close().await;
7672 }
7673
7674 #[tokio::test(flavor = "multi_thread")]
7675 async fn test_delete_point_with_var() {
7676 let initial_source = "\
7677sketch(on = XY) {
7678 point(at = [var 1, var 2])
7679 point1 = point(at = [var 3, var 4])
7680 point(at = [var 5, var 6])
7681}
7682";
7683
7684 let program = Program::parse(initial_source).unwrap().0.unwrap();
7685
7686 let mut frontend = FrontendState::new();
7687
7688 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7689 let mock_ctx = ExecutorContext::new_mock(None).await;
7690 let version = Version(0);
7691
7692 frontend.hack_set_program(&ctx, program).await.unwrap();
7693 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7694 let sketch_id = sketch_object.id;
7695 let sketch = expect_sketch(sketch_object);
7696
7697 let point_id = *sketch.segments.get(1).unwrap();
7698
7699 let (src_delta, scene_delta) = frontend
7700 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7701 .await
7702 .unwrap();
7703 assert_eq!(
7704 src_delta.text.as_str(),
7705 "\
7706sketch(on = XY) {
7707 point(at = [var 1mm, var 2mm])
7708 point(at = [var 5mm, var 6mm])
7709}
7710"
7711 );
7712 assert_eq!(scene_delta.new_objects, vec![]);
7713 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7714
7715 ctx.close().await;
7716 mock_ctx.close().await;
7717 }
7718
7719 #[tokio::test(flavor = "multi_thread")]
7720 async fn test_delete_multiple_points() {
7721 let initial_source = "\
7722sketch(on = XY) {
7723 point(at = [var 1, var 2])
7724 point1 = point(at = [var 3, var 4])
7725 point(at = [var 5, var 6])
7726}
7727";
7728
7729 let program = Program::parse(initial_source).unwrap().0.unwrap();
7730
7731 let mut frontend = FrontendState::new();
7732
7733 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7734 let mock_ctx = ExecutorContext::new_mock(None).await;
7735 let version = Version(0);
7736
7737 frontend.hack_set_program(&ctx, program).await.unwrap();
7738 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7739 let sketch_id = sketch_object.id;
7740
7741 let sketch = expect_sketch(sketch_object);
7742
7743 let point1_id = *sketch.segments.first().unwrap();
7744 let point2_id = *sketch.segments.get(1).unwrap();
7745
7746 let (src_delta, scene_delta) = frontend
7747 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
7748 .await
7749 .unwrap();
7750 assert_eq!(
7751 src_delta.text.as_str(),
7752 "\
7753sketch(on = XY) {
7754 point(at = [var 5mm, var 6mm])
7755}
7756"
7757 );
7758 assert_eq!(scene_delta.new_objects, vec![]);
7759 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7760
7761 ctx.close().await;
7762 mock_ctx.close().await;
7763 }
7764
7765 #[tokio::test(flavor = "multi_thread")]
7766 async fn test_delete_coincident_constraint() {
7767 let initial_source = "\
7768sketch(on = XY) {
7769 point1 = point(at = [var 1, var 2])
7770 point2 = point(at = [var 3, var 4])
7771 coincident([point1, point2])
7772 point(at = [var 5, var 6])
7773}
7774";
7775
7776 let program = Program::parse(initial_source).unwrap().0.unwrap();
7777
7778 let mut frontend = FrontendState::new();
7779
7780 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7781 let mock_ctx = ExecutorContext::new_mock(None).await;
7782 let version = Version(0);
7783
7784 frontend.hack_set_program(&ctx, program).await.unwrap();
7785 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7786 let sketch_id = sketch_object.id;
7787 let sketch = expect_sketch(sketch_object);
7788
7789 let coincident_id = *sketch.constraints.first().unwrap();
7790
7791 let (src_delta, scene_delta) = frontend
7792 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7793 .await
7794 .unwrap();
7795 assert_eq!(
7796 src_delta.text.as_str(),
7797 "\
7798sketch(on = XY) {
7799 point1 = point(at = [var 1mm, var 2mm])
7800 point2 = point(at = [var 3mm, var 4mm])
7801 point(at = [var 5mm, var 6mm])
7802}
7803"
7804 );
7805 assert_eq!(scene_delta.new_objects, vec![]);
7806 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7807
7808 ctx.close().await;
7809 mock_ctx.close().await;
7810 }
7811
7812 #[tokio::test(flavor = "multi_thread")]
7813 async fn test_delete_line_cascades_to_coincident_constraint() {
7814 let initial_source = "\
7815sketch(on = XY) {
7816 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7817 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7818 coincident([line1.end, line2.start])
7819}
7820";
7821
7822 let program = Program::parse(initial_source).unwrap().0.unwrap();
7823
7824 let mut frontend = FrontendState::new();
7825
7826 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7827 let mock_ctx = ExecutorContext::new_mock(None).await;
7828 let version = Version(0);
7829
7830 frontend.hack_set_program(&ctx, program).await.unwrap();
7831 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7832 let sketch_id = sketch_object.id;
7833 let sketch = expect_sketch(sketch_object);
7834 let line_id = *sketch.segments.get(5).unwrap();
7835
7836 let (src_delta, scene_delta) = frontend
7837 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7838 .await
7839 .unwrap();
7840 assert_eq!(
7841 src_delta.text.as_str(),
7842 "\
7843sketch(on = XY) {
7844 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7845}
7846"
7847 );
7848 assert_eq!(
7849 scene_delta.new_graph.objects.len(),
7850 5,
7851 "{:#?}",
7852 scene_delta.new_graph.objects
7853 );
7854
7855 ctx.close().await;
7856 mock_ctx.close().await;
7857 }
7858
7859 #[tokio::test(flavor = "multi_thread")]
7860 async fn test_delete_line_cascades_to_distance_constraint() {
7861 let initial_source = "\
7862sketch(on = XY) {
7863 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7864 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7865 distance([line1.end, line2.start]) == 10mm
7866}
7867";
7868
7869 let program = Program::parse(initial_source).unwrap().0.unwrap();
7870
7871 let mut frontend = FrontendState::new();
7872
7873 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7874 let mock_ctx = ExecutorContext::new_mock(None).await;
7875 let version = Version(0);
7876
7877 frontend.hack_set_program(&ctx, program).await.unwrap();
7878 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7879 let sketch_id = sketch_object.id;
7880 let sketch = expect_sketch(sketch_object);
7881 let line_id = *sketch.segments.get(5).unwrap();
7882
7883 let (src_delta, scene_delta) = frontend
7884 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7885 .await
7886 .unwrap();
7887 assert_eq!(
7888 src_delta.text.as_str(),
7889 "\
7890sketch(on = XY) {
7891 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7892}
7893"
7894 );
7895 assert_eq!(
7896 scene_delta.new_graph.objects.len(),
7897 5,
7898 "{:#?}",
7899 scene_delta.new_graph.objects
7900 );
7901
7902 ctx.close().await;
7903 mock_ctx.close().await;
7904 }
7905
7906 #[tokio::test(flavor = "multi_thread")]
7907 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
7908 let initial_source = "\
7909sketch(on = XY) {
7910 point1 = point(at = [var 1, var 2])
7911 point2 = point(at = [var 3, var 4])
7912 horizontalDistance([point1, point2]) == 10mm
7913}
7914";
7915
7916 let program = Program::parse(initial_source).unwrap().0.unwrap();
7917
7918 let mut frontend = FrontendState::new();
7919
7920 let mock_ctx = ExecutorContext::new_mock(None).await;
7921 let version = Version(0);
7922
7923 frontend.program = program.clone();
7924 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7925 frontend.update_state_after_exec(outcome, true);
7926 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7927 let sketch_id = sketch_object.id;
7928 let sketch = expect_sketch(sketch_object);
7929 let point2_id = *sketch.segments.get(1).unwrap();
7930
7931 let (src_delta, scene_delta) = frontend
7932 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
7933 .await
7934 .unwrap();
7935 assert_eq!(
7936 src_delta.text.as_str(),
7937 "\
7938sketch(on = XY) {
7939 point1 = point(at = [var 1mm, var 2mm])
7940}
7941"
7942 );
7943 assert_eq!(
7944 scene_delta.new_graph.objects.len(),
7945 3,
7946 "{:#?}",
7947 scene_delta.new_graph.objects
7948 );
7949
7950 mock_ctx.close().await;
7951 }
7952
7953 #[tokio::test(flavor = "multi_thread")]
7954 async fn test_delete_line_cascades_to_fixed_constraint() {
7955 let initial_source = "\
7956sketch(on = XY) {
7957 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7958 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7959 fixed([line1.start, [0, 0]])
7960}
7961";
7962
7963 let program = Program::parse(initial_source).unwrap().0.unwrap();
7964
7965 let mut frontend = FrontendState::new();
7966
7967 let mock_ctx = ExecutorContext::new_mock(None).await;
7968 let version = Version(0);
7969
7970 frontend.program = program.clone();
7971 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7972 frontend.update_state_after_exec(outcome, true);
7973 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7974 let sketch_id = sketch_object.id;
7975 let sketch = expect_sketch(sketch_object);
7976 let line1_id = *sketch.segments.get(2).unwrap();
7977
7978 let (src_delta, scene_delta) = frontend
7979 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7980 .await
7981 .unwrap();
7982 assert_eq!(
7983 src_delta.text.as_str(),
7984 "\
7985sketch(on = XY) {
7986 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7987}
7988"
7989 );
7990 assert_eq!(
7991 scene_delta.new_graph.objects.len(),
7992 5,
7993 "{:#?}",
7994 scene_delta.new_graph.objects
7995 );
7996
7997 mock_ctx.close().await;
7998 }
7999
8000 #[tokio::test(flavor = "multi_thread")]
8001 async fn test_delete_line_cascades_to_midpoint_constraint() {
8002 let initial_source = "\
8003sketch(on = XY) {
8004 point1 = point(at = [var 1, var 2])
8005 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
8006 midpoint(line1, point = point1)
8007}
8008";
8009
8010 let program = Program::parse(initial_source).unwrap().0.unwrap();
8011
8012 let mut frontend = FrontendState::new();
8013
8014 let mock_ctx = ExecutorContext::new_mock(None).await;
8015 let version = Version(0);
8016
8017 frontend.program = program.clone();
8018 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8019 frontend.update_state_after_exec(outcome, true);
8020 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8021 let sketch_id = sketch_object.id;
8022 let sketch = expect_sketch(sketch_object);
8023 let line1_id = *sketch.segments.get(3).unwrap();
8024
8025 let (src_delta, scene_delta) = frontend
8026 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8027 .await
8028 .unwrap();
8029 assert_eq!(
8030 src_delta.text.as_str(),
8031 "\
8032sketch(on = XY) {
8033 point1 = point(at = [var 1mm, var 2mm])
8034}
8035"
8036 );
8037 assert_eq!(
8038 scene_delta.new_graph.objects.len(),
8039 3,
8040 "{:#?}",
8041 scene_delta.new_graph.objects
8042 );
8043
8044 mock_ctx.close().await;
8045 }
8046
8047 #[tokio::test(flavor = "multi_thread")]
8048 async fn test_delete_point_preserves_multiline_coincident_constraint() {
8049 let initial_source = "\
8050sketch(on = XY) {
8051 point1 = point(at = [var 1, var 2])
8052 point2 = point(at = [var 3, var 4])
8053 point3 = point(at = [var 5, var 6])
8054 coincident([point1, point2, point3])
8055}
8056";
8057
8058 let program = Program::parse(initial_source).unwrap().0.unwrap();
8059
8060 let mut frontend = FrontendState::new();
8061
8062 let mock_ctx = ExecutorContext::new_mock(None).await;
8063 let version = Version(0);
8064
8065 frontend.program = program.clone();
8066 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8067 frontend.update_state_after_exec(outcome, true);
8068 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8069 let sketch_id = sketch_object.id;
8070 let sketch = expect_sketch(sketch_object);
8071 let point3_id = *sketch.segments.get(2).unwrap();
8072
8073 let (src_delta, scene_delta) = frontend
8074 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8075 .await
8076 .unwrap();
8077 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8078 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8079 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8080 assert!(
8081 src_delta.text.contains("coincident([point1, point2])"),
8082 "{}",
8083 src_delta.text
8084 );
8085
8086 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8087 let sketch = expect_sketch(sketch_object);
8088 assert_eq!(sketch.segments.len(), 2);
8089 assert_eq!(sketch.constraints.len(), 1);
8090
8091 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8092 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8093 panic!("Expected constraint object");
8094 };
8095 let Constraint::Coincident(coincident) = constraint else {
8096 panic!("Expected coincident constraint");
8097 };
8098 assert_eq!(
8099 coincident.segments,
8100 sketch
8101 .segments
8102 .iter()
8103 .copied()
8104 .map(Into::into)
8105 .collect::<Vec<ConstraintSegment>>()
8106 );
8107
8108 mock_ctx.close().await;
8109 }
8110
8111 #[tokio::test(flavor = "multi_thread")]
8112 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8113 let initial_source = "\
8114sketch(on = XY) {
8115 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8116 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8117 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8118 equalLength([line1, line2, line3])
8119}
8120";
8121
8122 let program = Program::parse(initial_source).unwrap().0.unwrap();
8123
8124 let mut frontend = FrontendState::new();
8125
8126 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8127 let mock_ctx = ExecutorContext::new_mock(None).await;
8128 let version = Version(0);
8129
8130 frontend.hack_set_program(&ctx, program).await.unwrap();
8131 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8132 let sketch_id = sketch_object.id;
8133 let sketch = expect_sketch(sketch_object);
8134 let line3_id = *sketch.segments.get(8).unwrap();
8135
8136 let (src_delta, scene_delta) = frontend
8137 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8138 .await
8139 .unwrap();
8140 assert_eq!(
8141 src_delta.text.as_str(),
8142 "\
8143sketch(on = XY) {
8144 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8145 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8146 equalLength([line1, line2])
8147}
8148"
8149 );
8150
8151 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8152 let sketch = expect_sketch(sketch_object);
8153 assert_eq!(sketch.constraints.len(), 1);
8154
8155 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8156 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8157 panic!("Expected constraint object");
8158 };
8159 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8160 panic!("Expected lines equal length constraint");
8161 };
8162 assert_eq!(lines_equal_length.lines.len(), 2);
8163
8164 ctx.close().await;
8165 mock_ctx.close().await;
8166 }
8167
8168 #[tokio::test(flavor = "multi_thread")]
8169 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8170 let initial_source = "\
8171sketch(on = XY) {
8172 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8173 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8174 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8175 horizontal([line1.end, line2.start, line3.start])
8176}
8177";
8178
8179 let program = Program::parse(initial_source).unwrap().0.unwrap();
8180
8181 let mut frontend = FrontendState::new();
8182
8183 let mock_ctx = ExecutorContext::new_mock(None).await;
8184 let version = Version(0);
8185
8186 frontend.program = program.clone();
8187 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8188 frontend.update_state_after_exec(outcome, true);
8189 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8190 let sketch_id = sketch_object.id;
8191 let sketch = expect_sketch(sketch_object);
8192 let line1_id = *sketch.segments.get(2).unwrap();
8193
8194 let (src_delta, scene_delta) = frontend
8195 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8196 .await
8197 .unwrap();
8198 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8199 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8200 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8201 assert!(
8202 src_delta.text.contains("horizontal([line2.start, line3.start])"),
8203 "{}",
8204 src_delta.text
8205 );
8206
8207 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8208 let sketch = expect_sketch(sketch_object);
8209 assert_eq!(sketch.constraints.len(), 1);
8210
8211 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8212 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8213 panic!("Expected constraint object");
8214 };
8215 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8216 panic!("Expected horizontal points constraint");
8217 };
8218 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8219 assert_eq!(*points, remaining_points);
8220
8221 mock_ctx.close().await;
8222 }
8223
8224 #[tokio::test(flavor = "multi_thread")]
8225 async fn test_delete_line_preserves_multiline_vertical_constraint() {
8226 let initial_source = "\
8227sketch(on = XY) {
8228 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8229 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8230 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8231 vertical([line1.end, line2.start, line3.start])
8232}
8233";
8234
8235 let program = Program::parse(initial_source).unwrap().0.unwrap();
8236
8237 let mut frontend = FrontendState::new();
8238
8239 let mock_ctx = ExecutorContext::new_mock(None).await;
8240 let version = Version(0);
8241
8242 frontend.program = program.clone();
8243 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8244 frontend.update_state_after_exec(outcome, true);
8245 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8246 let sketch_id = sketch_object.id;
8247 let sketch = expect_sketch(sketch_object);
8248 let line1_id = *sketch.segments.get(2).unwrap();
8249
8250 let (src_delta, scene_delta) = frontend
8251 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8252 .await
8253 .unwrap();
8254 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8255 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8256 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8257 assert!(
8258 src_delta.text.contains("vertical([line2.start, line3.start])"),
8259 "{}",
8260 src_delta.text
8261 );
8262
8263 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8264 let sketch = expect_sketch(sketch_object);
8265 assert_eq!(sketch.constraints.len(), 1);
8266
8267 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8268 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8269 panic!("Expected constraint object");
8270 };
8271 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8272 panic!("Expected vertical points constraint");
8273 };
8274 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8275 assert_eq!(*points, remaining_points);
8276
8277 mock_ctx.close().await;
8278 }
8279
8280 #[tokio::test(flavor = "multi_thread")]
8281 async fn test_delete_line_preserves_multiline_coincident_constraint() {
8282 let initial_source = "\
8283sketch(on = XY) {
8284 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8285 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8286 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8287 coincident([line1.end, line2.start, line3.start])
8288}
8289";
8290
8291 let program = Program::parse(initial_source).unwrap().0.unwrap();
8292
8293 let mut frontend = FrontendState::new();
8294
8295 let mock_ctx = ExecutorContext::new_mock(None).await;
8296 let version = Version(0);
8297
8298 frontend.program = program.clone();
8299 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8300 frontend.update_state_after_exec(outcome, true);
8301 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8302 let sketch_id = sketch_object.id;
8303 let sketch = expect_sketch(sketch_object);
8304 let line1_id = *sketch.segments.get(2).unwrap();
8305
8306 let (src_delta, scene_delta) = frontend
8307 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8308 .await
8309 .unwrap();
8310 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8311 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8312 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8313 assert!(
8314 src_delta.text.contains("coincident([line2.start, line3.start])"),
8315 "{}",
8316 src_delta.text
8317 );
8318
8319 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8320 let sketch = expect_sketch(sketch_object);
8321 assert_eq!(sketch.constraints.len(), 1);
8322
8323 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8324 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8325 panic!("Expected constraint object");
8326 };
8327 let Constraint::Coincident(coincident) = constraint else {
8328 panic!("Expected coincident constraint");
8329 };
8330 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8331 assert_eq!(coincident.segments, remaining_segments);
8332
8333 mock_ctx.close().await;
8334 }
8335
8336 #[tokio::test(flavor = "multi_thread")]
8337 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8338 let initial_source = "\
8339sketch(on = XY) {
8340 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8341 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8342 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8343 equalLength([line1, line2, line3])
8344}
8345";
8346
8347 let program = Program::parse(initial_source).unwrap().0.unwrap();
8348
8349 let mut frontend = FrontendState::new();
8350
8351 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8352 let mock_ctx = ExecutorContext::new_mock(None).await;
8353 let version = Version(0);
8354
8355 frontend.hack_set_program(&ctx, program).await.unwrap();
8356 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8357 let sketch_id = sketch_object.id;
8358 let sketch = expect_sketch(sketch_object);
8359 let line2_id = *sketch.segments.get(5).unwrap();
8360 let line3_id = *sketch.segments.get(8).unwrap();
8361
8362 let (src_delta, scene_delta) = frontend
8363 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8364 .await
8365 .unwrap();
8366 assert_eq!(
8367 src_delta.text.as_str(),
8368 "\
8369sketch(on = XY) {
8370 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8371}
8372"
8373 );
8374
8375 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8376 let sketch = expect_sketch(sketch_object);
8377 assert!(sketch.constraints.is_empty());
8378
8379 ctx.close().await;
8380 mock_ctx.close().await;
8381 }
8382
8383 #[tokio::test(flavor = "multi_thread")]
8384 async fn test_delete_line_preserves_multiline_parallel_constraint() {
8385 let initial_source = "\
8386sketch(on = XY) {
8387 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8388 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8389 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8390 parallel([line1, line2, line3])
8391}
8392";
8393
8394 let program = Program::parse(initial_source).unwrap().0.unwrap();
8395
8396 let mut frontend = FrontendState::new();
8397
8398 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8399 let mock_ctx = ExecutorContext::new_mock(None).await;
8400 let version = Version(0);
8401
8402 frontend.hack_set_program(&ctx, program).await.unwrap();
8403 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8404 let sketch_id = sketch_object.id;
8405 let sketch = expect_sketch(sketch_object);
8406 let line3_id = *sketch.segments.get(8).unwrap();
8407
8408 let (src_delta, scene_delta) = frontend
8409 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8410 .await
8411 .unwrap();
8412 assert_eq!(
8413 src_delta.text.as_str(),
8414 "\
8415sketch(on = XY) {
8416 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8417 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8418 parallel([line1, line2])
8419}
8420"
8421 );
8422
8423 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8424 let sketch = expect_sketch(sketch_object);
8425 assert_eq!(sketch.constraints.len(), 1);
8426
8427 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8428 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8429 panic!("Expected constraint object");
8430 };
8431 let Constraint::Parallel(parallel) = constraint else {
8432 panic!("Expected parallel constraint");
8433 };
8434 assert_eq!(parallel.lines.len(), 2);
8435
8436 ctx.close().await;
8437 mock_ctx.close().await;
8438 }
8439
8440 #[tokio::test(flavor = "multi_thread")]
8441 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8442 let initial_source = "\
8443sketch(on = XY) {
8444 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8445 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8446 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8447 parallel([line1, line2, line3])
8448}
8449";
8450
8451 let program = Program::parse(initial_source).unwrap().0.unwrap();
8452
8453 let mut frontend = FrontendState::new();
8454
8455 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8456 let mock_ctx = ExecutorContext::new_mock(None).await;
8457 let version = Version(0);
8458
8459 frontend.hack_set_program(&ctx, program).await.unwrap();
8460 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8461 let sketch_id = sketch_object.id;
8462 let sketch = expect_sketch(sketch_object);
8463 let line2_id = *sketch.segments.get(5).unwrap();
8464 let line3_id = *sketch.segments.get(8).unwrap();
8465
8466 let (src_delta, scene_delta) = frontend
8467 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8468 .await
8469 .unwrap();
8470 assert_eq!(
8471 src_delta.text.as_str(),
8472 "\
8473sketch(on = XY) {
8474 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8475}
8476"
8477 );
8478
8479 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8480 let sketch = expect_sketch(sketch_object);
8481 assert!(sketch.constraints.is_empty());
8482
8483 ctx.close().await;
8484 mock_ctx.close().await;
8485 }
8486
8487 #[tokio::test(flavor = "multi_thread")]
8488 async fn test_delete_line_line_coincident_constraint() {
8489 let initial_source = "\
8490sketch(on = XY) {
8491 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8492 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8493 coincident([line1, line2])
8494}
8495";
8496
8497 let program = Program::parse(initial_source).unwrap().0.unwrap();
8498
8499 let mut frontend = FrontendState::new();
8500
8501 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8502 let mock_ctx = ExecutorContext::new_mock(None).await;
8503 let version = Version(0);
8504
8505 frontend.hack_set_program(&ctx, program).await.unwrap();
8506 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8507 let sketch_id = sketch_object.id;
8508 let sketch = expect_sketch(sketch_object);
8509
8510 let coincident_id = *sketch.constraints.first().unwrap();
8511
8512 let (src_delta, scene_delta) = frontend
8513 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8514 .await
8515 .unwrap();
8516 assert_eq!(
8517 src_delta.text.as_str(),
8518 "\
8519sketch(on = XY) {
8520 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8521 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8522}
8523"
8524 );
8525 assert_eq!(scene_delta.new_objects, vec![]);
8526 assert_eq!(scene_delta.new_graph.objects.len(), 8);
8527
8528 ctx.close().await;
8529 mock_ctx.close().await;
8530 }
8531
8532 #[tokio::test(flavor = "multi_thread")]
8533 async fn test_two_points_coincident() {
8534 let initial_source = "\
8535sketch(on = XY) {
8536 point1 = point(at = [var 1, var 2])
8537 point(at = [3, 4])
8538}
8539";
8540
8541 let program = Program::parse(initial_source).unwrap().0.unwrap();
8542
8543 let mut frontend = FrontendState::new();
8544
8545 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8546 let mock_ctx = ExecutorContext::new_mock(None).await;
8547 let version = Version(0);
8548
8549 frontend.hack_set_program(&ctx, program).await.unwrap();
8550 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8551 let sketch_id = sketch_object.id;
8552 let sketch = expect_sketch(sketch_object);
8553 let point0_id = *sketch.segments.first().unwrap();
8554 let point1_id = *sketch.segments.get(1).unwrap();
8555
8556 let constraint = Constraint::Coincident(Coincident {
8557 segments: vec![point0_id.into(), point1_id.into()],
8558 });
8559 let (src_delta, scene_delta) = frontend
8560 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8561 .await
8562 .unwrap();
8563 assert_eq!(
8564 src_delta.text.as_str(),
8565 "\
8566sketch(on = XY) {
8567 point1 = point(at = [var 1, var 2])
8568 point2 = point(at = [3, 4])
8569 coincident([point1, point2])
8570}
8571"
8572 );
8573 assert_eq!(
8574 scene_delta.new_graph.objects.len(),
8575 5,
8576 "{:#?}",
8577 scene_delta.new_graph.objects
8578 );
8579
8580 ctx.close().await;
8581 mock_ctx.close().await;
8582 }
8583
8584 #[tokio::test(flavor = "multi_thread")]
8585 async fn test_three_points_coincident() {
8586 let initial_source = "\
8587sketch(on = XY) {
8588 point1 = point(at = [var 1, var 2])
8589 point(at = [var 3, var 4])
8590 point(at = [var 5, var 6])
8591}
8592";
8593
8594 let program = Program::parse(initial_source).unwrap().0.unwrap();
8595
8596 let mut frontend = FrontendState::new();
8597
8598 let mock_ctx = ExecutorContext::new_mock(None).await;
8599 let version = Version(0);
8600
8601 frontend.program = program.clone();
8602 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8603 frontend.update_state_after_exec(outcome, true);
8604 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8605 let sketch_id = sketch_object.id;
8606 let sketch = expect_sketch(sketch_object);
8607 let segments = sketch
8608 .segments
8609 .iter()
8610 .take(3)
8611 .copied()
8612 .map(Into::into)
8613 .collect::<Vec<ConstraintSegment>>();
8614
8615 let constraint = Constraint::Coincident(Coincident {
8616 segments: segments.clone(),
8617 });
8618 let (src_delta, scene_delta) = frontend
8619 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8620 .await
8621 .unwrap();
8622 assert_eq!(
8623 src_delta.text.as_str(),
8624 "\
8625sketch(on = XY) {
8626 point1 = point(at = [var 1, var 2])
8627 point2 = point(at = [var 3, var 4])
8628 point3 = point(at = [var 5, var 6])
8629 coincident([point1, point2, point3])
8630}
8631"
8632 );
8633
8634 let constraint_object = scene_delta
8635 .new_graph
8636 .objects
8637 .iter()
8638 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8639 .unwrap();
8640
8641 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8642 panic!("expected a constraint object");
8643 };
8644
8645 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8646
8647 mock_ctx.close().await;
8648 }
8649
8650 #[tokio::test(flavor = "multi_thread")]
8651 async fn test_source_with_three_point_coincident_tracks_all_segments() {
8652 let initial_source = "\
8653sketch(on = XY) {
8654 point1 = point(at = [var 1, var 2])
8655 point2 = point(at = [var 3, var 4])
8656 point3 = point(at = [var 5, var 6])
8657 coincident([point1, point2, point3])
8658}
8659";
8660
8661 let program = Program::parse(initial_source).unwrap().0.unwrap();
8662
8663 let mut frontend = FrontendState::new();
8664
8665 let ctx = ExecutorContext::new_mock(None).await;
8666 frontend.program = program.clone();
8667 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8668 frontend.update_state_after_exec(outcome, true);
8669
8670 let constraint_object = frontend
8671 .scene_graph
8672 .objects
8673 .iter()
8674 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8675 .unwrap();
8676 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8677 panic!("expected a constraint object");
8678 };
8679
8680 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8681 let sketch = expect_sketch(sketch_object);
8682 let expected_segments = sketch
8683 .segments
8684 .iter()
8685 .take(3)
8686 .copied()
8687 .map(Into::into)
8688 .collect::<Vec<ConstraintSegment>>();
8689
8690 assert_eq!(
8691 constraint,
8692 &Constraint::Coincident(Coincident {
8693 segments: expected_segments,
8694 })
8695 );
8696
8697 ctx.close().await;
8698 }
8699
8700 #[tokio::test(flavor = "multi_thread")]
8701 async fn test_point_origin_coincident_preserves_order() {
8702 let initial_source = "\
8703sketch(on = XY) {
8704 point(at = [var 1, var 2])
8705}
8706";
8707
8708 for (origin_first, expected_source) in [
8709 (
8710 true,
8711 "\
8712sketch(on = XY) {
8713 point1 = point(at = [var 1, var 2])
8714 coincident([ORIGIN, point1])
8715}
8716",
8717 ),
8718 (
8719 false,
8720 "\
8721sketch(on = XY) {
8722 point1 = point(at = [var 1, var 2])
8723 coincident([point1, ORIGIN])
8724}
8725",
8726 ),
8727 ] {
8728 let program = Program::parse(initial_source).unwrap().0.unwrap();
8729
8730 let mut frontend = FrontendState::new();
8731
8732 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8733 let mock_ctx = ExecutorContext::new_mock(None).await;
8734 let version = Version(0);
8735
8736 frontend.hack_set_program(&ctx, program).await.unwrap();
8737 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8738 let sketch_id = sketch_object.id;
8739 let sketch = expect_sketch(sketch_object);
8740 let point_id = *sketch.segments.first().unwrap();
8741
8742 let segments = if origin_first {
8743 vec![ConstraintSegment::ORIGIN, point_id.into()]
8744 } else {
8745 vec![point_id.into(), ConstraintSegment::ORIGIN]
8746 };
8747 let constraint = Constraint::Coincident(Coincident {
8748 segments: segments.clone(),
8749 });
8750 let (src_delta, scene_delta) = frontend
8751 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8752 .await
8753 .unwrap();
8754 assert_eq!(src_delta.text.as_str(), expected_source);
8755
8756 let constraint_object = scene_delta
8757 .new_graph
8758 .objects
8759 .iter()
8760 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8761 .unwrap();
8762
8763 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8764 panic!("expected a constraint object");
8765 };
8766
8767 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8768
8769 ctx.close().await;
8770 mock_ctx.close().await;
8771 }
8772 }
8773
8774 #[tokio::test(flavor = "multi_thread")]
8775 async fn test_coincident_of_line_end_points() {
8776 let initial_source = "\
8777sketch(on = XY) {
8778 line(start = [var 1, var 2], end = [var 3, var 4])
8779 line(start = [var 5, var 6], end = [var 7, var 8])
8780}
8781";
8782
8783 let program = Program::parse(initial_source).unwrap().0.unwrap();
8784
8785 let mut frontend = FrontendState::new();
8786
8787 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8788 let mock_ctx = ExecutorContext::new_mock(None).await;
8789 let version = Version(0);
8790
8791 frontend.hack_set_program(&ctx, program).await.unwrap();
8792 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8793 let sketch_id = sketch_object.id;
8794 let sketch = expect_sketch(sketch_object);
8795 let point0_id = *sketch.segments.get(1).unwrap();
8796 let point1_id = *sketch.segments.get(3).unwrap();
8797
8798 let constraint = Constraint::Coincident(Coincident {
8799 segments: vec![point0_id.into(), point1_id.into()],
8800 });
8801 let (src_delta, scene_delta) = frontend
8802 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8803 .await
8804 .unwrap();
8805 assert_eq!(
8806 src_delta.text.as_str(),
8807 "\
8808sketch(on = XY) {
8809 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8810 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8811 coincident([line1.end, line2.start])
8812}
8813"
8814 );
8815 assert_eq!(
8816 scene_delta.new_graph.objects.len(),
8817 9,
8818 "{:#?}",
8819 scene_delta.new_graph.objects
8820 );
8821
8822 ctx.close().await;
8823 mock_ctx.close().await;
8824 }
8825
8826 #[tokio::test(flavor = "multi_thread")]
8827 async fn test_coincident_of_line_point_and_circle_segment() {
8828 let initial_source = "\
8829sketch(on = XY) {
8830 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8831 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8832}
8833";
8834 let program = Program::parse(initial_source).unwrap().0.unwrap();
8835 let mut frontend = FrontendState::new();
8836
8837 let mock_ctx = ExecutorContext::new_mock(None).await;
8838 let version = Version(0);
8839
8840 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8841 frontend.program = program;
8842 frontend.update_state_after_exec(outcome, true);
8843 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
8844 let sketch_id = sketch_object.id;
8845 let sketch = expect_sketch(sketch_object);
8846
8847 let circle_id = sketch
8848 .segments
8849 .iter()
8850 .copied()
8851 .find(|seg_id| {
8852 matches!(
8853 &frontend.scene_graph.objects[seg_id.0].kind,
8854 ObjectKind::Segment {
8855 segment: Segment::Circle(_)
8856 }
8857 )
8858 })
8859 .expect("Expected a circle segment in sketch");
8860 let line_id = sketch
8861 .segments
8862 .iter()
8863 .copied()
8864 .find(|seg_id| {
8865 matches!(
8866 &frontend.scene_graph.objects[seg_id.0].kind,
8867 ObjectKind::Segment {
8868 segment: Segment::Line(_)
8869 }
8870 )
8871 })
8872 .expect("Expected a line segment in sketch");
8873
8874 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
8875 ObjectKind::Segment {
8876 segment: Segment::Line(line),
8877 } => line.start,
8878 _ => panic!("Expected line segment object"),
8879 };
8880
8881 let constraint = Constraint::Coincident(Coincident {
8882 segments: vec![line_start_point_id.into(), circle_id.into()],
8883 });
8884 let (src_delta, _scene_delta) = frontend
8885 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8886 .await
8887 .unwrap();
8888 assert_eq!(
8889 src_delta.text.as_str(),
8890 "\
8891sketch(on = XY) {
8892 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8893 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8894 coincident([line1.start, circle1])
8895}
8896"
8897 );
8898
8899 mock_ctx.close().await;
8900 }
8901
8902 #[tokio::test(flavor = "multi_thread")]
8903 async fn test_invalid_coincident_arc_and_line_preserves_state() {
8904 let program = Program::empty();
8912
8913 let mut frontend = FrontendState::new();
8914 frontend.program = program;
8915
8916 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8917 let mock_ctx = ExecutorContext::new_mock(None).await;
8918 let version = Version(0);
8919
8920 let sketch_args = SketchCtor {
8921 on: Plane::Default(PlaneName::Xy),
8922 };
8923 let (_src_delta, _scene_delta, sketch_id) = frontend
8924 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8925 .await
8926 .unwrap();
8927
8928 let arc_ctor = ArcCtor {
8930 start: Point2d {
8931 x: Expr::Var(Number {
8932 value: 0.0,
8933 units: NumericSuffix::Mm,
8934 }),
8935 y: Expr::Var(Number {
8936 value: 0.0,
8937 units: NumericSuffix::Mm,
8938 }),
8939 },
8940 end: Point2d {
8941 x: Expr::Var(Number {
8942 value: 10.0,
8943 units: NumericSuffix::Mm,
8944 }),
8945 y: Expr::Var(Number {
8946 value: 10.0,
8947 units: NumericSuffix::Mm,
8948 }),
8949 },
8950 center: Point2d {
8951 x: Expr::Var(Number {
8952 value: 10.0,
8953 units: NumericSuffix::Mm,
8954 }),
8955 y: Expr::Var(Number {
8956 value: 0.0,
8957 units: NumericSuffix::Mm,
8958 }),
8959 },
8960 construction: None,
8961 };
8962 let (_src_delta, scene_delta) = frontend
8963 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
8964 .await
8965 .unwrap();
8966 let arc_id = *scene_delta.new_objects.last().unwrap();
8968
8969 let line_ctor = LineCtor {
8971 start: Point2d {
8972 x: Expr::Var(Number {
8973 value: 20.0,
8974 units: NumericSuffix::Mm,
8975 }),
8976 y: Expr::Var(Number {
8977 value: 0.0,
8978 units: NumericSuffix::Mm,
8979 }),
8980 },
8981 end: Point2d {
8982 x: Expr::Var(Number {
8983 value: 30.0,
8984 units: NumericSuffix::Mm,
8985 }),
8986 y: Expr::Var(Number {
8987 value: 10.0,
8988 units: NumericSuffix::Mm,
8989 }),
8990 },
8991 construction: None,
8992 };
8993 let (_src_delta, scene_delta) = frontend
8994 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
8995 .await
8996 .unwrap();
8997 let line_id = *scene_delta.new_objects.last().unwrap();
8999
9000 let constraint = Constraint::Coincident(Coincident {
9003 segments: vec![arc_id.into(), line_id.into()],
9004 });
9005 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
9006
9007 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
9009
9010 let sketch_object_after =
9013 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
9014 let sketch_after = expect_sketch(sketch_object_after);
9015
9016 assert!(
9018 sketch_after.segments.contains(&arc_id),
9019 "Arc segment should still exist after failed constraint"
9020 );
9021 assert!(
9022 sketch_after.segments.contains(&line_id),
9023 "Line segment should still exist after failed constraint"
9024 );
9025
9026 let arc_obj = frontend
9028 .scene_graph
9029 .objects
9030 .get(arc_id.0)
9031 .expect("Arc object should still be accessible");
9032 let line_obj = frontend
9033 .scene_graph
9034 .objects
9035 .get(line_id.0)
9036 .expect("Line object should still be accessible");
9037
9038 match &arc_obj.kind {
9041 ObjectKind::Segment {
9042 segment: Segment::Arc(_),
9043 } => {}
9044 _ => panic!("Arc object should still be an arc segment"),
9045 }
9046 match &line_obj.kind {
9047 ObjectKind::Segment {
9048 segment: Segment::Line(_),
9049 } => {}
9050 _ => panic!("Line object should still be a line segment"),
9051 }
9052
9053 ctx.close().await;
9054 mock_ctx.close().await;
9055 }
9056
9057 #[tokio::test(flavor = "multi_thread")]
9058 async fn test_distance_two_points() {
9059 let initial_source = "\
9060sketch(on = XY) {
9061 point(at = [var 1, var 2])
9062 point(at = [var 3, var 4])
9063}
9064";
9065
9066 let program = Program::parse(initial_source).unwrap().0.unwrap();
9067
9068 let mut frontend = FrontendState::new();
9069
9070 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9071 let mock_ctx = ExecutorContext::new_mock(None).await;
9072 let version = Version(0);
9073
9074 frontend.hack_set_program(&ctx, program).await.unwrap();
9075 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9076 let sketch_id = sketch_object.id;
9077 let sketch = expect_sketch(sketch_object);
9078 let point0_id = *sketch.segments.first().unwrap();
9079 let point1_id = *sketch.segments.get(1).unwrap();
9080
9081 let constraint = Constraint::Distance(Distance {
9082 points: vec![point0_id.into(), point1_id.into()],
9083 distance: Number {
9084 value: 2.0,
9085 units: NumericSuffix::Mm,
9086 },
9087 label_position: None,
9088 source: Default::default(),
9089 });
9090 let (src_delta, scene_delta) = frontend
9091 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9092 .await
9093 .unwrap();
9094 assert_eq!(
9095 src_delta.text.as_str(),
9096 "\
9098sketch(on = XY) {
9099 point1 = point(at = [var 1, var 2])
9100 point2 = point(at = [var 3, var 4])
9101 distance([point1, point2]) == 2mm
9102}
9103"
9104 );
9105 assert_eq!(
9106 scene_delta.new_graph.objects.len(),
9107 5,
9108 "{:#?}",
9109 scene_delta.new_graph.objects
9110 );
9111
9112 ctx.close().await;
9113 mock_ctx.close().await;
9114 }
9115
9116 #[tokio::test(flavor = "multi_thread")]
9117 async fn test_distance_two_points_with_label() {
9118 let initial_source = "\
9119sketch(on = XY) {
9120 point(at = [var 1, var 2])
9121 point(at = [var 3, var 4])
9122}
9123";
9124
9125 let program = Program::parse(initial_source).unwrap().0.unwrap();
9126
9127 let mut frontend = FrontendState::new();
9128
9129 let mock_ctx = ExecutorContext::new_mock(None).await;
9130 let version = Version(0);
9131
9132 frontend.program = program.clone();
9133 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9134 frontend.update_state_after_exec(outcome, true);
9135 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9136 let sketch_id = sketch_object.id;
9137 let sketch = expect_sketch(sketch_object);
9138 let point0_id = *sketch.segments.first().unwrap();
9139 let point1_id = *sketch.segments.get(1).unwrap();
9140
9141 let label_position = Point2d {
9142 x: Number {
9143 value: 10.0,
9144 units: NumericSuffix::Mm,
9145 },
9146 y: Number {
9147 value: 11.0,
9148 units: NumericSuffix::Mm,
9149 },
9150 };
9151 let constraint = Constraint::Distance(Distance {
9152 points: vec![point0_id.into(), point1_id.into()],
9153 distance: Number {
9154 value: 2.0,
9155 units: NumericSuffix::Mm,
9156 },
9157 label_position: Some(label_position.clone()),
9158 source: Default::default(),
9159 });
9160 let (src_delta, scene_delta) = frontend
9161 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9162 .await
9163 .unwrap();
9164 assert_eq!(
9165 src_delta.text.as_str(),
9166 "\
9167sketch(on = XY) {
9168 point1 = point(at = [var 1, var 2])
9169 point2 = point(at = [var 3, var 4])
9170 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9171}
9172"
9173 );
9174
9175 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9176 let sketch = expect_sketch(sketch_object);
9177 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9178 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9179 panic!("Expected constraint object");
9180 };
9181 let Constraint::Distance(distance) = constraint else {
9182 panic!("Expected distance constraint");
9183 };
9184 assert_eq!(distance.label_position, Some(label_position));
9185
9186 mock_ctx.close().await;
9187 }
9188
9189 #[tokio::test(flavor = "multi_thread")]
9190 async fn test_edit_distance_constraint_label_position() {
9191 let initial_source = "\
9192sketch(on = XY) {
9193 point(at = [var 1, var 2])
9194 point(at = [var 3, var 2])
9195}
9196";
9197
9198 let program = Program::parse(initial_source).unwrap().0.unwrap();
9199
9200 let mut frontend = FrontendState::new();
9201
9202 let mock_ctx = ExecutorContext::new_mock(None).await;
9203 let version = Version(0);
9204
9205 frontend.program = program.clone();
9206 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9207 frontend.update_state_after_exec(outcome, true);
9208 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9209 let sketch_id = sketch_object.id;
9210 let sketch = expect_sketch(sketch_object);
9211 let point0_id = *sketch.segments.first().unwrap();
9212 let point1_id = *sketch.segments.get(1).unwrap();
9213
9214 let constraint = Constraint::Distance(Distance {
9215 points: vec![point0_id.into(), point1_id.into()],
9216 distance: Number {
9217 value: 2.0,
9218 units: NumericSuffix::Mm,
9219 },
9220 label_position: None,
9221 source: Default::default(),
9222 });
9223 let (_, scene_delta) = frontend
9224 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9225 .await
9226 .unwrap();
9227 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9228 let sketch = expect_sketch(sketch_object);
9229 let constraint_id = sketch.constraints[0];
9230 let label_position = Point2d {
9231 x: Number {
9232 value: 10.0,
9233 units: NumericSuffix::Mm,
9234 },
9235 y: Number {
9236 value: 11.0,
9237 units: NumericSuffix::Mm,
9238 },
9239 };
9240
9241 let (src_delta, scene_delta) = frontend
9242 .edit_distance_constraint_label_position(
9243 &mock_ctx,
9244 version,
9245 sketch_id,
9246 constraint_id,
9247 label_position.clone(),
9248 vec![],
9249 )
9250 .await
9251 .unwrap();
9252 assert_eq!(
9253 src_delta.text.as_str(),
9254 "\
9255sketch(on = XY) {
9256 point1 = point(at = [var 1mm, var 2mm])
9257 point2 = point(at = [var 3mm, var 2mm])
9258 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9259}
9260"
9261 );
9262
9263 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9264 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9265 panic!("Expected constraint object");
9266 };
9267 let Constraint::Distance(distance) = constraint else {
9268 panic!("Expected distance constraint");
9269 };
9270 assert_eq!(distance.label_position, Some(label_position));
9271
9272 mock_ctx.close().await;
9273 }
9274
9275 #[tokio::test(flavor = "multi_thread")]
9276 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9277 let initial_source = "\
9278sketch(on = XY) {
9279 point1 = point(at = [var 0mm, var 0mm])
9280 point2 = point(at = [var 10mm, var 0mm])
9281 distance([point1, point2]) == 5mm
9282}
9283";
9284
9285 let program = Program::parse(initial_source).unwrap().0.unwrap();
9286 let mut frontend = FrontendState::new();
9287 let mock_ctx = ExecutorContext::new_mock(None).await;
9288 let version = Version(0);
9289
9290 frontend.program = program.clone();
9291 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9292 frontend.update_state_after_exec(outcome, true);
9293 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9294 let sketch_id = sketch_object.id;
9295 let sketch = expect_sketch(sketch_object);
9296 let point0_id = sketch.segments[0];
9297 let point1_id = sketch.segments[1];
9298 let constraint_id = sketch.constraints[0];
9299
9300 let edited_segments = vec![ExistingSegmentCtor {
9301 id: point0_id,
9302 ctor: SegmentCtor::Point(PointCtor {
9303 position: Point2d {
9304 x: Expr::Var(Number {
9305 value: 2.0,
9306 units: NumericSuffix::Mm,
9307 }),
9308 y: Expr::Var(Number {
9309 value: 1.0,
9310 units: NumericSuffix::Mm,
9311 }),
9312 },
9313 }),
9314 }];
9315 let (_, scene_delta) = frontend
9316 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9317 .await
9318 .unwrap();
9319 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9320 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9321
9322 let label_position = Point2d {
9323 x: Number {
9324 value: 3.0,
9325 units: NumericSuffix::Mm,
9326 },
9327 y: Number {
9328 value: 4.0,
9329 units: NumericSuffix::Mm,
9330 },
9331 };
9332 let (_, scene_delta) = frontend
9333 .edit_distance_constraint_label_position(
9334 &mock_ctx,
9335 version,
9336 sketch_id,
9337 constraint_id,
9338 label_position,
9339 vec![point0_id],
9340 )
9341 .await
9342 .unwrap();
9343
9344 assert_point_position_close(
9345 point_position(&scene_delta.new_graph, point0_id),
9346 point0_after_segment_edit,
9347 );
9348 assert_point_position_close(
9349 point_position(&scene_delta.new_graph, point1_id),
9350 point1_after_segment_edit,
9351 );
9352
9353 mock_ctx.close().await;
9354 }
9355
9356 #[tokio::test(flavor = "multi_thread")]
9357 async fn test_horizontal_distance_two_points() {
9358 let initial_source = "\
9359sketch(on = XY) {
9360 point(at = [var 1, var 2])
9361 point(at = [var 3, var 4])
9362}
9363";
9364
9365 let program = Program::parse(initial_source).unwrap().0.unwrap();
9366
9367 let mut frontend = FrontendState::new();
9368
9369 let mock_ctx = ExecutorContext::new_mock(None).await;
9370 let version = Version(0);
9371
9372 frontend.program = program.clone();
9373 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9374 frontend.update_state_after_exec(outcome, true);
9375 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9376 let sketch_id = sketch_object.id;
9377 let sketch = expect_sketch(sketch_object);
9378 let point0_id = *sketch.segments.first().unwrap();
9379 let point1_id = *sketch.segments.get(1).unwrap();
9380 let label_position = Point2d {
9381 x: Number {
9382 value: 10.0,
9383 units: NumericSuffix::Mm,
9384 },
9385 y: Number {
9386 value: 11.0,
9387 units: NumericSuffix::Mm,
9388 },
9389 };
9390
9391 let constraint = Constraint::HorizontalDistance(Distance {
9392 points: vec![point0_id.into(), point1_id.into()],
9393 distance: Number {
9394 value: 2.0,
9395 units: NumericSuffix::Mm,
9396 },
9397 label_position: Some(label_position.clone()),
9398 source: Default::default(),
9399 });
9400 let (src_delta, scene_delta) = frontend
9401 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9402 .await
9403 .unwrap();
9404 assert_eq!(
9405 src_delta.text.as_str(),
9406 "\
9408sketch(on = XY) {
9409 point1 = point(at = [var 1, var 2])
9410 point2 = point(at = [var 3, var 4])
9411 horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9412}
9413"
9414 );
9415 assert_eq!(
9416 scene_delta.new_graph.objects.len(),
9417 5,
9418 "{:#?}",
9419 scene_delta.new_graph.objects
9420 );
9421 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9422 let sketch = expect_sketch(sketch_object);
9423 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9424 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9425 panic!("Expected constraint object");
9426 };
9427 let Constraint::HorizontalDistance(distance) = constraint else {
9428 panic!("Expected horizontal distance constraint");
9429 };
9430 assert_eq!(distance.label_position, Some(label_position));
9431
9432 mock_ctx.close().await;
9433 }
9434
9435 #[tokio::test(flavor = "multi_thread")]
9436 async fn test_radius_single_arc_segment() {
9437 let initial_source = "\
9438sketch(on = XY) {
9439 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9440}
9441";
9442
9443 let program = Program::parse(initial_source).unwrap().0.unwrap();
9444
9445 let mut frontend = FrontendState::new();
9446
9447 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9448 let mock_ctx = ExecutorContext::new_mock(None).await;
9449 let version = Version(0);
9450
9451 frontend.hack_set_program(&ctx, program).await.unwrap();
9452 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9453 let sketch_id = sketch_object.id;
9454 let sketch = expect_sketch(sketch_object);
9455 let arc_id = sketch
9457 .segments
9458 .iter()
9459 .find(|&seg_id| {
9460 let obj = frontend.scene_graph.objects.get(seg_id.0);
9461 matches!(
9462 obj.map(|o| &o.kind),
9463 Some(ObjectKind::Segment {
9464 segment: Segment::Arc(_)
9465 })
9466 )
9467 })
9468 .unwrap();
9469
9470 let constraint = Constraint::Radius(Radius {
9471 arc: *arc_id,
9472 radius: Number {
9473 value: 5.0,
9474 units: NumericSuffix::Mm,
9475 },
9476 source: Default::default(),
9477 });
9478 let (src_delta, scene_delta) = frontend
9479 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9480 .await
9481 .unwrap();
9482 assert_eq!(
9483 src_delta.text.as_str(),
9484 "\
9486sketch(on = XY) {
9487 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9488 radius(arc1) == 5mm
9489}
9490"
9491 );
9492 assert_eq!(
9493 scene_delta.new_graph.objects.len(),
9494 7, "{:#?}",
9496 scene_delta.new_graph.objects
9497 );
9498
9499 ctx.close().await;
9500 mock_ctx.close().await;
9501 }
9502
9503 #[tokio::test(flavor = "multi_thread")]
9504 async fn test_vertical_distance_two_points() {
9505 let initial_source = "\
9506sketch(on = XY) {
9507 point(at = [var 1, var 2])
9508 point(at = [var 3, var 4])
9509}
9510";
9511
9512 let program = Program::parse(initial_source).unwrap().0.unwrap();
9513
9514 let mut frontend = FrontendState::new();
9515
9516 let mock_ctx = ExecutorContext::new_mock(None).await;
9517 let version = Version(0);
9518
9519 frontend.program = program.clone();
9520 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9521 frontend.update_state_after_exec(outcome, true);
9522 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9523 let sketch_id = sketch_object.id;
9524 let sketch = expect_sketch(sketch_object);
9525 let point0_id = *sketch.segments.first().unwrap();
9526 let point1_id = *sketch.segments.get(1).unwrap();
9527 let label_position = Point2d {
9528 x: Number {
9529 value: 10.0,
9530 units: NumericSuffix::Mm,
9531 },
9532 y: Number {
9533 value: 11.0,
9534 units: NumericSuffix::Mm,
9535 },
9536 };
9537
9538 let constraint = Constraint::VerticalDistance(Distance {
9539 points: vec![point0_id.into(), point1_id.into()],
9540 distance: Number {
9541 value: 2.0,
9542 units: NumericSuffix::Mm,
9543 },
9544 label_position: Some(label_position.clone()),
9545 source: Default::default(),
9546 });
9547 let (src_delta, scene_delta) = frontend
9548 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9549 .await
9550 .unwrap();
9551 assert_eq!(
9552 src_delta.text.as_str(),
9553 "\
9555sketch(on = XY) {
9556 point1 = point(at = [var 1, var 2])
9557 point2 = point(at = [var 3, var 4])
9558 verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9559}
9560"
9561 );
9562 assert_eq!(
9563 scene_delta.new_graph.objects.len(),
9564 5,
9565 "{:#?}",
9566 scene_delta.new_graph.objects
9567 );
9568 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9569 let sketch = expect_sketch(sketch_object);
9570 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9571 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9572 panic!("Expected constraint object");
9573 };
9574 let Constraint::VerticalDistance(distance) = constraint else {
9575 panic!("Expected vertical distance constraint");
9576 };
9577 assert_eq!(distance.label_position, Some(label_position));
9578
9579 mock_ctx.close().await;
9580 }
9581
9582 #[tokio::test(flavor = "multi_thread")]
9583 async fn test_add_fixed_standalone_point() {
9584 let initial_source = "\
9585sketch(on = XY) {
9586 point(at = [var 1, var 2])
9587}
9588";
9589
9590 let program = Program::parse(initial_source).unwrap().0.unwrap();
9591
9592 let mut frontend = FrontendState::new();
9593
9594 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9595 let mock_ctx = ExecutorContext::new_mock(None).await;
9596 let version = Version(0);
9597
9598 frontend.hack_set_program(&ctx, program).await.unwrap();
9599 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9600 let sketch_id = sketch_object.id;
9601 let sketch = expect_sketch(sketch_object);
9602 let point_id = *sketch.segments.first().unwrap();
9603
9604 let (src_delta, scene_delta) = frontend
9605 .add_constraint(
9606 &mock_ctx,
9607 version,
9608 sketch_id,
9609 Constraint::Fixed(Fixed {
9610 points: vec![FixedPoint {
9611 point: point_id,
9612 position: Point2d {
9613 x: Number {
9614 value: 2.0,
9615 units: NumericSuffix::Mm,
9616 },
9617 y: Number {
9618 value: 3.0,
9619 units: NumericSuffix::Mm,
9620 },
9621 },
9622 }],
9623 }),
9624 )
9625 .await
9626 .unwrap();
9627 assert_eq!(
9628 src_delta.text.as_str(),
9629 "\
9630sketch(on = XY) {
9631 point1 = point(at = [var 1, var 2])
9632 fixed([point1, [2mm, 3mm]])
9633}
9634"
9635 );
9636 assert_eq!(
9637 scene_delta.new_graph.objects.len(),
9638 4,
9639 "{:#?}",
9640 scene_delta.new_graph.objects
9641 );
9642
9643 ctx.close().await;
9644 mock_ctx.close().await;
9645 }
9646
9647 #[tokio::test(flavor = "multi_thread")]
9648 async fn test_add_fixed_multiple_points() {
9649 let initial_source = "\
9650sketch(on = XY) {
9651 point(at = [var 1, var 2])
9652 point(at = [var 3, var 4])
9653}
9654";
9655
9656 let program = Program::parse(initial_source).unwrap().0.unwrap();
9657
9658 let mut frontend = FrontendState::new();
9659
9660 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9661 let mock_ctx = ExecutorContext::new_mock(None).await;
9662 let version = Version(0);
9663
9664 frontend.hack_set_program(&ctx, program).await.unwrap();
9665 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9666 let sketch_id = sketch_object.id;
9667 let sketch = expect_sketch(sketch_object);
9668 let point0_id = *sketch.segments.first().unwrap();
9669 let point1_id = *sketch.segments.get(1).unwrap();
9670
9671 let (src_delta, scene_delta) = frontend
9672 .add_constraint(
9673 &mock_ctx,
9674 version,
9675 sketch_id,
9676 Constraint::Fixed(Fixed {
9677 points: vec![
9678 FixedPoint {
9679 point: point0_id,
9680 position: Point2d {
9681 x: Number {
9682 value: 2.0,
9683 units: NumericSuffix::Mm,
9684 },
9685 y: Number {
9686 value: 3.0,
9687 units: NumericSuffix::Mm,
9688 },
9689 },
9690 },
9691 FixedPoint {
9692 point: point1_id,
9693 position: Point2d {
9694 x: Number {
9695 value: 4.0,
9696 units: NumericSuffix::Mm,
9697 },
9698 y: Number {
9699 value: 5.0,
9700 units: NumericSuffix::Mm,
9701 },
9702 },
9703 },
9704 ],
9705 }),
9706 )
9707 .await
9708 .unwrap();
9709 assert_eq!(
9710 src_delta.text.as_str(),
9711 "\
9712sketch(on = XY) {
9713 point1 = point(at = [var 1, var 2])
9714 point2 = point(at = [var 3, var 4])
9715 fixed([point1, [2mm, 3mm]])
9716 fixed([point2, [4mm, 5mm]])
9717}
9718"
9719 );
9720 assert_eq!(
9721 scene_delta.new_graph.objects.len(),
9722 6,
9723 "{:#?}",
9724 scene_delta.new_graph.objects
9725 );
9726
9727 ctx.close().await;
9728 mock_ctx.close().await;
9729 }
9730
9731 #[tokio::test(flavor = "multi_thread")]
9732 async fn test_add_fixed_owned_point() {
9733 let initial_source = "\
9734sketch(on = XY) {
9735 line(start = [var 1, var 2], end = [var 3, var 4])
9736}
9737";
9738
9739 let program = Program::parse(initial_source).unwrap().0.unwrap();
9740
9741 let mut frontend = FrontendState::new();
9742
9743 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9744 let mock_ctx = ExecutorContext::new_mock(None).await;
9745 let version = Version(0);
9746
9747 frontend.hack_set_program(&ctx, program).await.unwrap();
9748 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9749 let sketch_id = sketch_object.id;
9750 let sketch = expect_sketch(sketch_object);
9751 let line_start_id = *sketch.segments.first().unwrap();
9752
9753 let (src_delta, scene_delta) = frontend
9754 .add_constraint(
9755 &mock_ctx,
9756 version,
9757 sketch_id,
9758 Constraint::Fixed(Fixed {
9759 points: vec![FixedPoint {
9760 point: line_start_id,
9761 position: Point2d {
9762 x: Number {
9763 value: 2.0,
9764 units: NumericSuffix::Mm,
9765 },
9766 y: Number {
9767 value: 3.0,
9768 units: NumericSuffix::Mm,
9769 },
9770 },
9771 }],
9772 }),
9773 )
9774 .await
9775 .unwrap();
9776 assert_eq!(
9777 src_delta.text.as_str(),
9778 "\
9779sketch(on = XY) {
9780 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9781 fixed([line1.start, [2mm, 3mm]])
9782}
9783"
9784 );
9785 assert_eq!(
9786 scene_delta.new_graph.objects.len(),
9787 6,
9788 "{:#?}",
9789 scene_delta.new_graph.objects
9790 );
9791
9792 ctx.close().await;
9793 mock_ctx.close().await;
9794 }
9795
9796 #[tokio::test(flavor = "multi_thread")]
9797 async fn test_radius_error_cases() {
9798 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9799 let mock_ctx = ExecutorContext::new_mock(None).await;
9800 let version = Version(0);
9801
9802 let initial_source_point = "\
9804sketch(on = XY) {
9805 point(at = [var 1, var 2])
9806}
9807";
9808 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
9809 let mut frontend_point = FrontendState::new();
9810 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
9811 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
9812 let sketch_id_point = sketch_object_point.id;
9813 let sketch_point = expect_sketch(sketch_object_point);
9814 let point_id = *sketch_point.segments.first().unwrap();
9815
9816 let constraint_point = Constraint::Radius(Radius {
9817 arc: point_id,
9818 radius: Number {
9819 value: 5.0,
9820 units: NumericSuffix::Mm,
9821 },
9822 source: Default::default(),
9823 });
9824 let result_point = frontend_point
9825 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
9826 .await;
9827 assert!(result_point.is_err(), "Single point should error for radius");
9828
9829 let initial_source_line = "\
9831sketch(on = XY) {
9832 line(start = [var 1, var 2], end = [var 3, var 4])
9833}
9834";
9835 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
9836 let mut frontend_line = FrontendState::new();
9837 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
9838 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
9839 let sketch_id_line = sketch_object_line.id;
9840 let sketch_line = expect_sketch(sketch_object_line);
9841 let line_id = *sketch_line.segments.first().unwrap();
9842
9843 let constraint_line = Constraint::Radius(Radius {
9844 arc: line_id,
9845 radius: Number {
9846 value: 5.0,
9847 units: NumericSuffix::Mm,
9848 },
9849 source: Default::default(),
9850 });
9851 let result_line = frontend_line
9852 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
9853 .await;
9854 assert!(result_line.is_err(), "Single line segment should error for radius");
9855
9856 ctx.close().await;
9857 mock_ctx.close().await;
9858 }
9859
9860 #[tokio::test(flavor = "multi_thread")]
9861 async fn test_diameter_single_arc_segment() {
9862 let initial_source = "\
9863sketch(on = XY) {
9864 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9865}
9866";
9867
9868 let program = Program::parse(initial_source).unwrap().0.unwrap();
9869
9870 let mut frontend = FrontendState::new();
9871
9872 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9873 let mock_ctx = ExecutorContext::new_mock(None).await;
9874 let version = Version(0);
9875
9876 frontend.hack_set_program(&ctx, program).await.unwrap();
9877 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9878 let sketch_id = sketch_object.id;
9879 let sketch = expect_sketch(sketch_object);
9880 let arc_id = sketch
9882 .segments
9883 .iter()
9884 .find(|&seg_id| {
9885 let obj = frontend.scene_graph.objects.get(seg_id.0);
9886 matches!(
9887 obj.map(|o| &o.kind),
9888 Some(ObjectKind::Segment {
9889 segment: Segment::Arc(_)
9890 })
9891 )
9892 })
9893 .unwrap();
9894
9895 let constraint = Constraint::Diameter(Diameter {
9896 arc: *arc_id,
9897 diameter: Number {
9898 value: 10.0,
9899 units: NumericSuffix::Mm,
9900 },
9901 source: Default::default(),
9902 });
9903 let (src_delta, scene_delta) = frontend
9904 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9905 .await
9906 .unwrap();
9907 assert_eq!(
9908 src_delta.text.as_str(),
9909 "\
9911sketch(on = XY) {
9912 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9913 diameter(arc1) == 10mm
9914}
9915"
9916 );
9917 assert_eq!(
9918 scene_delta.new_graph.objects.len(),
9919 7, "{:#?}",
9921 scene_delta.new_graph.objects
9922 );
9923
9924 ctx.close().await;
9925 mock_ctx.close().await;
9926 }
9927
9928 #[tokio::test(flavor = "multi_thread")]
9929 async fn test_diameter_error_cases() {
9930 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9931 let mock_ctx = ExecutorContext::new_mock(None).await;
9932 let version = Version(0);
9933
9934 let initial_source_point = "\
9936sketch(on = XY) {
9937 point(at = [var 1, var 2])
9938}
9939";
9940 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
9941 let mut frontend_point = FrontendState::new();
9942 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
9943 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
9944 let sketch_id_point = sketch_object_point.id;
9945 let sketch_point = expect_sketch(sketch_object_point);
9946 let point_id = *sketch_point.segments.first().unwrap();
9947
9948 let constraint_point = Constraint::Diameter(Diameter {
9949 arc: point_id,
9950 diameter: Number {
9951 value: 10.0,
9952 units: NumericSuffix::Mm,
9953 },
9954 source: Default::default(),
9955 });
9956 let result_point = frontend_point
9957 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
9958 .await;
9959 assert!(result_point.is_err(), "Single point should error for diameter");
9960
9961 let initial_source_line = "\
9963sketch(on = XY) {
9964 line(start = [var 1, var 2], end = [var 3, var 4])
9965}
9966";
9967 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
9968 let mut frontend_line = FrontendState::new();
9969 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
9970 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
9971 let sketch_id_line = sketch_object_line.id;
9972 let sketch_line = expect_sketch(sketch_object_line);
9973 let line_id = *sketch_line.segments.first().unwrap();
9974
9975 let constraint_line = Constraint::Diameter(Diameter {
9976 arc: line_id,
9977 diameter: Number {
9978 value: 10.0,
9979 units: NumericSuffix::Mm,
9980 },
9981 source: Default::default(),
9982 });
9983 let result_line = frontend_line
9984 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
9985 .await;
9986 assert!(result_line.is_err(), "Single line segment should error for diameter");
9987
9988 ctx.close().await;
9989 mock_ctx.close().await;
9990 }
9991
9992 #[tokio::test(flavor = "multi_thread")]
9993 async fn test_line_horizontal() {
9994 let initial_source = "\
9995sketch(on = XY) {
9996 line(start = [var 1, var 2], end = [var 3, var 4])
9997}
9998";
9999
10000 let program = Program::parse(initial_source).unwrap().0.unwrap();
10001
10002 let mut frontend = FrontendState::new();
10003
10004 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10005 let mock_ctx = ExecutorContext::new_mock(None).await;
10006 let version = Version(0);
10007
10008 frontend.hack_set_program(&ctx, program).await.unwrap();
10009 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10010 let sketch_id = sketch_object.id;
10011 let sketch = expect_sketch(sketch_object);
10012 let line1_id = *sketch.segments.get(2).unwrap();
10013
10014 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
10015 let (src_delta, scene_delta) = frontend
10016 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10017 .await
10018 .unwrap();
10019 assert_eq!(
10020 src_delta.text.as_str(),
10021 "\
10022sketch(on = XY) {
10023 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10024 horizontal(line1)
10025}
10026"
10027 );
10028 assert_eq!(
10029 scene_delta.new_graph.objects.len(),
10030 6,
10031 "{:#?}",
10032 scene_delta.new_graph.objects
10033 );
10034
10035 ctx.close().await;
10036 mock_ctx.close().await;
10037 }
10038
10039 #[tokio::test(flavor = "multi_thread")]
10040 async fn test_line_vertical() {
10041 let initial_source = "\
10042sketch(on = XY) {
10043 line(start = [var 1, var 2], end = [var 3, var 4])
10044}
10045";
10046
10047 let program = Program::parse(initial_source).unwrap().0.unwrap();
10048
10049 let mut frontend = FrontendState::new();
10050
10051 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10052 let mock_ctx = ExecutorContext::new_mock(None).await;
10053 let version = Version(0);
10054
10055 frontend.hack_set_program(&ctx, program).await.unwrap();
10056 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10057 let sketch_id = sketch_object.id;
10058 let sketch = expect_sketch(sketch_object);
10059 let line1_id = *sketch.segments.get(2).unwrap();
10060
10061 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
10062 let (src_delta, scene_delta) = frontend
10063 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10064 .await
10065 .unwrap();
10066 assert_eq!(
10067 src_delta.text.as_str(),
10068 "\
10069sketch(on = XY) {
10070 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10071 vertical(line1)
10072}
10073"
10074 );
10075 assert_eq!(
10076 scene_delta.new_graph.objects.len(),
10077 6,
10078 "{:#?}",
10079 scene_delta.new_graph.objects
10080 );
10081
10082 ctx.close().await;
10083 mock_ctx.close().await;
10084 }
10085
10086 #[tokio::test(flavor = "multi_thread")]
10087 async fn test_points_vertical() {
10088 let initial_source = "\
10089sketch001 = sketch(on = XY) {
10090 p0 = point(at = [var -2.23mm, var 3.1mm])
10091 pf = point(at = [4, 4])
10092}
10093";
10094
10095 let program = Program::parse(initial_source).unwrap().0.unwrap();
10096
10097 let mut frontend = FrontendState::new();
10098
10099 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10100 let mock_ctx = ExecutorContext::new_mock(None).await;
10101 let version = Version(0);
10102
10103 frontend.hack_set_program(&ctx, program).await.unwrap();
10104 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10105 let sketch_id = sketch_object.id;
10106 let sketch = expect_sketch(sketch_object);
10107 let point_ids = vec![
10108 sketch.segments.first().unwrap().to_owned(),
10109 sketch.segments.get(1).unwrap().to_owned(),
10110 ];
10111
10112 let constraint = Constraint::Vertical(Vertical::Points {
10113 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10114 });
10115 let (src_delta, scene_delta) = frontend
10116 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10117 .await
10118 .unwrap();
10119 assert_eq!(
10120 src_delta.text.as_str(),
10121 "\
10122sketch001 = sketch(on = XY) {
10123 p0 = point(at = [var -2.23mm, var 3.1mm])
10124 pf = point(at = [4, 4])
10125 vertical([p0, pf])
10126}
10127"
10128 );
10129 assert_eq!(
10130 scene_delta.new_graph.objects.len(),
10131 5,
10132 "{:#?}",
10133 scene_delta.new_graph.objects
10134 );
10135
10136 ctx.close().await;
10137 mock_ctx.close().await;
10138 }
10139
10140 #[tokio::test(flavor = "multi_thread")]
10141 async fn test_points_horizontal() {
10142 let initial_source = "\
10143sketch001 = sketch(on = XY) {
10144 p0 = point(at = [var -2.23mm, var 3.1mm])
10145 pf = point(at = [4, 4])
10146}
10147";
10148
10149 let program = Program::parse(initial_source).unwrap().0.unwrap();
10150
10151 let mut frontend = FrontendState::new();
10152
10153 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10154 let mock_ctx = ExecutorContext::new_mock(None).await;
10155 let version = Version(0);
10156
10157 frontend.hack_set_program(&ctx, program).await.unwrap();
10158 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10159 let sketch_id = sketch_object.id;
10160 let sketch = expect_sketch(sketch_object);
10161 let point_ids = vec![
10162 sketch.segments.first().unwrap().to_owned(),
10163 sketch.segments.get(1).unwrap().to_owned(),
10164 ];
10165
10166 let constraint = Constraint::Horizontal(Horizontal::Points {
10167 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10168 });
10169 let (src_delta, scene_delta) = frontend
10170 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10171 .await
10172 .unwrap();
10173 assert_eq!(
10174 src_delta.text.as_str(),
10175 "\
10176sketch001 = sketch(on = XY) {
10177 p0 = point(at = [var -2.23mm, var 3.1mm])
10178 pf = point(at = [4, 4])
10179 horizontal([p0, pf])
10180}
10181"
10182 );
10183 assert_eq!(
10184 scene_delta.new_graph.objects.len(),
10185 5,
10186 "{:#?}",
10187 scene_delta.new_graph.objects
10188 );
10189
10190 ctx.close().await;
10191 mock_ctx.close().await;
10192 }
10193
10194 #[tokio::test(flavor = "multi_thread")]
10195 async fn test_point_horizontal_with_origin() {
10196 let initial_source = "\
10197sketch001 = sketch(on = XY) {
10198 p0 = point(at = [var -2.23mm, var 3.1mm])
10199}
10200";
10201
10202 let program = Program::parse(initial_source).unwrap().0.unwrap();
10203
10204 let mut frontend = FrontendState::new();
10205
10206 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10207 let mock_ctx = ExecutorContext::new_mock(None).await;
10208 let version = Version(0);
10209
10210 frontend.hack_set_program(&ctx, program).await.unwrap();
10211 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10212 let sketch_id = sketch_object.id;
10213 let sketch = expect_sketch(sketch_object);
10214 let point_id = *sketch.segments.first().unwrap();
10215
10216 let constraint = Constraint::Horizontal(Horizontal::Points {
10217 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
10218 });
10219 let (src_delta, scene_delta) = frontend
10220 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10221 .await
10222 .unwrap();
10223 assert_eq!(
10224 src_delta.text.as_str(),
10225 "\
10226sketch001 = sketch(on = XY) {
10227 p0 = point(at = [var -2.23mm, var 3.1mm])
10228 horizontal([p0, ORIGIN])
10229}
10230"
10231 );
10232 assert_eq!(
10233 scene_delta.new_graph.objects.len(),
10234 4,
10235 "{:#?}",
10236 scene_delta.new_graph.objects
10237 );
10238
10239 ctx.close().await;
10240 mock_ctx.close().await;
10241 }
10242
10243 #[tokio::test(flavor = "multi_thread")]
10244 async fn test_lines_equal_length() {
10245 let initial_source = "\
10246sketch(on = XY) {
10247 line(start = [var 1, var 2], end = [var 3, var 4])
10248 line(start = [var 5, var 6], end = [var 7, var 8])
10249}
10250";
10251
10252 let program = Program::parse(initial_source).unwrap().0.unwrap();
10253
10254 let mut frontend = FrontendState::new();
10255
10256 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10257 let mock_ctx = ExecutorContext::new_mock(None).await;
10258 let version = Version(0);
10259
10260 frontend.hack_set_program(&ctx, program).await.unwrap();
10261 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10262 let sketch_id = sketch_object.id;
10263 let sketch = expect_sketch(sketch_object);
10264 let line1_id = *sketch.segments.get(2).unwrap();
10265 let line2_id = *sketch.segments.get(5).unwrap();
10266
10267 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10268 lines: vec![line1_id, line2_id],
10269 });
10270 let (src_delta, scene_delta) = frontend
10271 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10272 .await
10273 .unwrap();
10274 assert_eq!(
10275 src_delta.text.as_str(),
10276 "\
10277sketch(on = XY) {
10278 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10279 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10280 equalLength([line1, line2])
10281}
10282"
10283 );
10284 assert_eq!(
10285 scene_delta.new_graph.objects.len(),
10286 9,
10287 "{:#?}",
10288 scene_delta.new_graph.objects
10289 );
10290
10291 ctx.close().await;
10292 mock_ctx.close().await;
10293 }
10294
10295 #[tokio::test(flavor = "multi_thread")]
10296 async fn test_add_constraint_multi_line_equal_length() {
10297 let initial_source = "\
10298sketch(on = XY) {
10299 line(start = [var 1, var 2], end = [var 3, var 4])
10300 line(start = [var 5, var 6], end = [var 7, var 8])
10301 line(start = [var 9, var 10], end = [var 11, var 12])
10302}
10303";
10304
10305 let program = Program::parse(initial_source).unwrap().0.unwrap();
10306
10307 let mut frontend = FrontendState::new();
10308 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10309 let mock_ctx = ExecutorContext::new_mock(None).await;
10310 let version = Version(0);
10311
10312 frontend.hack_set_program(&ctx, program).await.unwrap();
10313 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10314 let sketch_id = sketch_object.id;
10315 let sketch = expect_sketch(sketch_object);
10316 let line1_id = *sketch.segments.get(2).unwrap();
10317 let line2_id = *sketch.segments.get(5).unwrap();
10318 let line3_id = *sketch.segments.get(8).unwrap();
10319
10320 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10321 lines: vec![line1_id, line2_id, line3_id],
10322 });
10323 let (src_delta, scene_delta) = frontend
10324 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10325 .await
10326 .unwrap();
10327 assert_eq!(
10328 src_delta.text.as_str(),
10329 "\
10330sketch(on = XY) {
10331 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10332 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10333 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10334 equalLength([line1, line2, line3])
10335}
10336"
10337 );
10338 let constraints = scene_delta
10339 .new_graph
10340 .objects
10341 .iter()
10342 .filter_map(|obj| {
10343 let ObjectKind::Constraint { constraint } = &obj.kind else {
10344 return None;
10345 };
10346 Some(constraint)
10347 })
10348 .collect::<Vec<_>>();
10349
10350 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
10351 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
10352 panic!("expected equal length constraint, got {:?}", constraints[0]);
10353 };
10354 assert_eq!(lines_equal_length.lines.len(), 3);
10355
10356 ctx.close().await;
10357 mock_ctx.close().await;
10358 }
10359
10360 #[tokio::test(flavor = "multi_thread")]
10361 async fn test_lines_parallel() {
10362 let initial_source = "\
10363sketch(on = XY) {
10364 line(start = [var 1, var 2], end = [var 3, var 4])
10365 line(start = [var 5, var 6], end = [var 7, var 8])
10366}
10367";
10368
10369 let program = Program::parse(initial_source).unwrap().0.unwrap();
10370
10371 let mut frontend = FrontendState::new();
10372
10373 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10374 let mock_ctx = ExecutorContext::new_mock(None).await;
10375 let version = Version(0);
10376
10377 frontend.hack_set_program(&ctx, program).await.unwrap();
10378 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10379 let sketch_id = sketch_object.id;
10380 let sketch = expect_sketch(sketch_object);
10381 let line1_id = *sketch.segments.get(2).unwrap();
10382 let line2_id = *sketch.segments.get(5).unwrap();
10383
10384 let constraint = Constraint::Parallel(Parallel {
10385 lines: vec![line1_id, line2_id],
10386 });
10387 let (src_delta, scene_delta) = frontend
10388 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10389 .await
10390 .unwrap();
10391 assert_eq!(
10392 src_delta.text.as_str(),
10393 "\
10394sketch(on = XY) {
10395 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10396 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10397 parallel([line1, line2])
10398}
10399"
10400 );
10401 assert_eq!(
10402 scene_delta.new_graph.objects.len(),
10403 9,
10404 "{:#?}",
10405 scene_delta.new_graph.objects
10406 );
10407
10408 ctx.close().await;
10409 mock_ctx.close().await;
10410 }
10411
10412 #[tokio::test(flavor = "multi_thread")]
10413 async fn test_lines_parallel_multiline() {
10414 let initial_source = "\
10415sketch(on = XY) {
10416 line(start = [var 1, var 2], end = [var 3, var 4])
10417 line(start = [var 5, var 6], end = [var 7, var 8])
10418 line(start = [var 9, var 10], end = [var 11, var 12])
10419}
10420";
10421
10422 let program = Program::parse(initial_source).unwrap().0.unwrap();
10423
10424 let mut frontend = FrontendState::new();
10425
10426 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10427 let mock_ctx = ExecutorContext::new_mock(None).await;
10428 let version = Version(0);
10429
10430 frontend.hack_set_program(&ctx, program).await.unwrap();
10431 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10432 let sketch_id = sketch_object.id;
10433 let sketch = expect_sketch(sketch_object);
10434 let line1_id = *sketch.segments.get(2).unwrap();
10435 let line2_id = *sketch.segments.get(5).unwrap();
10436 let line3_id = *sketch.segments.get(8).unwrap();
10437
10438 let constraint = Constraint::Parallel(Parallel {
10439 lines: vec![line1_id, line2_id, line3_id],
10440 });
10441 let (src_delta, scene_delta) = frontend
10442 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10443 .await
10444 .unwrap();
10445 assert_eq!(
10446 src_delta.text.as_str(),
10447 "\
10448sketch(on = XY) {
10449 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10450 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10451 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10452 parallel([line1, line2, line3])
10453}
10454"
10455 );
10456
10457 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10458 let sketch = expect_sketch(sketch_object);
10459 assert_eq!(sketch.constraints.len(), 1);
10460
10461 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10462 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10463 panic!("Expected constraint object");
10464 };
10465 let Constraint::Parallel(parallel) = constraint else {
10466 panic!("Expected parallel constraint");
10467 };
10468 assert_eq!(parallel.lines.len(), 3);
10469
10470 ctx.close().await;
10471 mock_ctx.close().await;
10472 }
10473
10474 #[tokio::test(flavor = "multi_thread")]
10475 async fn test_lines_perpendicular() {
10476 let initial_source = "\
10477sketch(on = XY) {
10478 line(start = [var 1, var 2], end = [var 3, var 4])
10479 line(start = [var 5, var 6], end = [var 7, var 8])
10480}
10481";
10482
10483 let program = Program::parse(initial_source).unwrap().0.unwrap();
10484
10485 let mut frontend = FrontendState::new();
10486
10487 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10488 let mock_ctx = ExecutorContext::new_mock(None).await;
10489 let version = Version(0);
10490
10491 frontend.hack_set_program(&ctx, program).await.unwrap();
10492 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10493 let sketch_id = sketch_object.id;
10494 let sketch = expect_sketch(sketch_object);
10495 let line1_id = *sketch.segments.get(2).unwrap();
10496 let line2_id = *sketch.segments.get(5).unwrap();
10497
10498 let constraint = Constraint::Perpendicular(Perpendicular {
10499 lines: vec![line1_id, line2_id],
10500 });
10501 let (src_delta, scene_delta) = frontend
10502 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10503 .await
10504 .unwrap();
10505 assert_eq!(
10506 src_delta.text.as_str(),
10507 "\
10508sketch(on = XY) {
10509 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10510 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10511 perpendicular([line1, line2])
10512}
10513"
10514 );
10515 assert_eq!(
10516 scene_delta.new_graph.objects.len(),
10517 9,
10518 "{:#?}",
10519 scene_delta.new_graph.objects
10520 );
10521
10522 ctx.close().await;
10523 mock_ctx.close().await;
10524 }
10525
10526 #[tokio::test(flavor = "multi_thread")]
10527 async fn test_lines_angle() {
10528 let initial_source = "\
10529sketch(on = XY) {
10530 line(start = [var 1, var 2], end = [var 3, var 4])
10531 line(start = [var 5, var 6], end = [var 7, var 8])
10532}
10533";
10534
10535 let program = Program::parse(initial_source).unwrap().0.unwrap();
10536
10537 let mut frontend = FrontendState::new();
10538
10539 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10540 let mock_ctx = ExecutorContext::new_mock(None).await;
10541 let version = Version(0);
10542
10543 frontend.hack_set_program(&ctx, program).await.unwrap();
10544 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10545 let sketch_id = sketch_object.id;
10546 let sketch = expect_sketch(sketch_object);
10547 let line1_id = *sketch.segments.get(2).unwrap();
10548 let line2_id = *sketch.segments.get(5).unwrap();
10549
10550 let constraint = Constraint::Angle(Angle {
10551 lines: vec![line1_id, line2_id],
10552 angle: Number {
10553 value: 30.0,
10554 units: NumericSuffix::Deg,
10555 },
10556 source: Default::default(),
10557 });
10558 let (src_delta, scene_delta) = frontend
10559 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10560 .await
10561 .unwrap();
10562 assert_eq!(
10563 src_delta.text.as_str(),
10564 "\
10566sketch(on = XY) {
10567 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10568 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10569 angle([line1, line2]) == 30deg
10570}
10571"
10572 );
10573 assert_eq!(
10574 scene_delta.new_graph.objects.len(),
10575 9,
10576 "{:#?}",
10577 scene_delta.new_graph.objects
10578 );
10579
10580 ctx.close().await;
10581 mock_ctx.close().await;
10582 }
10583
10584 #[tokio::test(flavor = "multi_thread")]
10585 async fn test_segments_tangent() {
10586 let initial_source = "\
10587sketch(on = XY) {
10588 line(start = [var 1, var 2], end = [var 3, var 4])
10589 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10590}
10591";
10592
10593 let program = Program::parse(initial_source).unwrap().0.unwrap();
10594
10595 let mut frontend = FrontendState::new();
10596
10597 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10598 let mock_ctx = ExecutorContext::new_mock(None).await;
10599 let version = Version(0);
10600
10601 frontend.hack_set_program(&ctx, program).await.unwrap();
10602 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10603 let sketch_id = sketch_object.id;
10604 let sketch = expect_sketch(sketch_object);
10605 let line1_id = *sketch.segments.get(2).unwrap();
10606 let arc1_id = *sketch.segments.get(6).unwrap();
10607
10608 let constraint = Constraint::Tangent(Tangent {
10609 input: vec![line1_id, arc1_id],
10610 });
10611 let (src_delta, scene_delta) = frontend
10612 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10613 .await
10614 .unwrap();
10615 assert_eq!(
10616 src_delta.text.as_str(),
10617 "\
10618sketch(on = XY) {
10619 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10620 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10621 tangent([line1, arc1])
10622}
10623"
10624 );
10625 assert_eq!(
10626 scene_delta.new_graph.objects.len(),
10627 10,
10628 "{:#?}",
10629 scene_delta.new_graph.objects
10630 );
10631
10632 ctx.close().await;
10633 mock_ctx.close().await;
10634 }
10635
10636 #[tokio::test(flavor = "multi_thread")]
10637 async fn test_point_midpoint() {
10638 let initial_source = "\
10639sketch(on = XY) {
10640 point(at = [var 1, var 1])
10641 line(start = [var 0, var 0], end = [var 6, var 4])
10642}
10643";
10644
10645 let program = Program::parse(initial_source).unwrap().0.unwrap();
10646
10647 let mut frontend = FrontendState::new();
10648
10649 let ctx = ExecutorContext::new_mock(None).await;
10650 let version = Version(0);
10651
10652 frontend.program = program.clone();
10653 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10654 frontend.update_state_after_exec(outcome, true);
10655 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10656 let sketch_id = sketch_object.id;
10657 let sketch = expect_sketch(sketch_object);
10658 let point_id = *sketch.segments.first().unwrap();
10659 let line_id = *sketch.segments.get(3).unwrap();
10660
10661 let constraint = Constraint::Midpoint(Midpoint {
10662 point: point_id,
10663 segment: line_id,
10664 });
10665 let (src_delta, scene_delta) = frontend
10666 .add_constraint(&ctx, version, sketch_id, constraint)
10667 .await
10668 .unwrap();
10669 assert_eq!(
10670 src_delta.text.as_str(),
10671 "\
10672sketch(on = XY) {
10673 point1 = point(at = [var 1, var 1])
10674 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10675 midpoint(line1, point = point1)
10676}
10677"
10678 );
10679 assert_eq!(
10680 scene_delta.new_graph.objects.len(),
10681 7,
10682 "{:#?}",
10683 scene_delta.new_graph.objects
10684 );
10685
10686 ctx.close().await;
10687 }
10688
10689 #[tokio::test(flavor = "multi_thread")]
10690 async fn test_segments_symmetric() {
10691 let initial_source = "\
10692sketch(on = XY) {
10693 line(start = [var 0, var 0], end = [var 0, var 4])
10694 line(start = [var 4, var 0], end = [var 4, var 4])
10695 line(start = [var 2, var -1], end = [var 2, var 5])
10696}
10697";
10698
10699 let program = Program::parse(initial_source).unwrap().0.unwrap();
10700
10701 let mut frontend = FrontendState::new();
10702
10703 let ctx = ExecutorContext::new_mock(None).await;
10704 let version = Version(0);
10705
10706 frontend.program = program.clone();
10707 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10708 frontend.update_state_after_exec(outcome, true);
10709 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10710 let sketch_id = sketch_object.id;
10711 let sketch = expect_sketch(sketch_object);
10712 let line1_id = *sketch.segments.get(2).unwrap();
10713 let line2_id = *sketch.segments.get(5).unwrap();
10714 let axis_id = *sketch.segments.get(8).unwrap();
10715
10716 let constraint = Constraint::Symmetric(Symmetric {
10717 input: vec![line1_id, line2_id],
10718 axis: axis_id,
10719 });
10720 let (src_delta, scene_delta) = frontend
10721 .add_constraint(&ctx, version, sketch_id, constraint)
10722 .await
10723 .unwrap();
10724 assert_eq!(
10725 src_delta.text.as_str(),
10726 "\
10727sketch(on = XY) {
10728 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
10729 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
10730 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
10731 symmetric([line1, line2], axis = line3)
10732}
10733"
10734 );
10735 assert_eq!(
10736 scene_delta.new_graph.objects.len(),
10737 12,
10738 "{:#?}",
10739 scene_delta.new_graph.objects
10740 );
10741
10742 ctx.close().await;
10743 }
10744
10745 #[tokio::test(flavor = "multi_thread")]
10746 async fn test_point_arc_midpoint() {
10747 let initial_source = "\
10748sketch(on = XY) {
10749 point(at = [var 6, var 3])
10750 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10751}
10752";
10753
10754 let program = Program::parse(initial_source).unwrap().0.unwrap();
10755
10756 let mut frontend = FrontendState::new();
10757
10758 let ctx = ExecutorContext::new_mock(None).await;
10759 let version = Version(0);
10760
10761 frontend.program = program.clone();
10762 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10763 frontend.update_state_after_exec(outcome, true);
10764 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10765 let sketch_id = sketch_object.id;
10766 let sketch = expect_sketch(sketch_object);
10767 let point_id = *sketch.segments.first().unwrap();
10768 let arc_id = *sketch.segments.get(4).unwrap();
10769
10770 let constraint = Constraint::Midpoint(Midpoint {
10771 point: point_id,
10772 segment: arc_id,
10773 });
10774 let (src_delta, scene_delta) = frontend
10775 .add_constraint(&ctx, version, sketch_id, constraint)
10776 .await
10777 .unwrap();
10778 assert_eq!(
10779 src_delta.text.as_str(),
10780 "\
10781sketch(on = XY) {
10782 point1 = point(at = [var 6, var 3])
10783 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10784 midpoint(arc1, point = point1)
10785}
10786"
10787 );
10788 assert_eq!(
10789 scene_delta.new_graph.objects.len(),
10790 8,
10791 "{:#?}",
10792 scene_delta.new_graph.objects
10793 );
10794
10795 ctx.close().await;
10796 }
10797
10798 #[tokio::test(flavor = "multi_thread")]
10799 async fn test_segments_symmetric_arcs() {
10800 let initial_source = "\
10801sketch(on = XY) {
10802 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
10803 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
10804 line(start = [var 0, var -10], end = [var 0, var 10])
10805}
10806";
10807
10808 let program = Program::parse(initial_source).unwrap().0.unwrap();
10809
10810 let mut frontend = FrontendState::new();
10811
10812 let ctx = ExecutorContext::new_mock(None).await;
10813 let version = Version(0);
10814
10815 frontend.program = program.clone();
10816 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10817 frontend.update_state_after_exec(outcome, true);
10818 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10819 let sketch_id = sketch_object.id;
10820 let sketch = expect_sketch(sketch_object);
10821 let arc1_id = *sketch.segments.get(3).unwrap();
10822 let arc2_id = *sketch.segments.get(7).unwrap();
10823 let axis_id = *sketch.segments.get(10).unwrap();
10824
10825 let constraint = Constraint::Symmetric(Symmetric {
10826 input: vec![arc1_id, arc2_id],
10827 axis: axis_id,
10828 });
10829 let (src_delta, scene_delta) = frontend
10830 .add_constraint(&ctx, version, sketch_id, constraint)
10831 .await
10832 .unwrap();
10833 assert_eq!(
10834 src_delta.text.as_str(),
10835 "\
10836sketch(on = XY) {
10837 arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
10838 arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
10839 line1 = line(start = [var 0, var -10], end = [var 0, var 10])
10840 symmetric([arc1, arc2], axis = line1)
10841}
10842"
10843 );
10844 assert_eq!(
10845 scene_delta.new_graph.objects.len(),
10846 14,
10847 "{:#?}",
10848 scene_delta.new_graph.objects
10849 );
10850
10851 ctx.close().await;
10852 }
10853
10854 #[tokio::test(flavor = "multi_thread")]
10855 async fn test_sketch_on_face_simple() {
10856 let initial_source = "\
10857len = 2mm
10858cube = startSketchOn(XY)
10859 |> startProfile(at = [0, 0])
10860 |> line(end = [len, 0], tag = $side)
10861 |> line(end = [0, len])
10862 |> line(end = [-len, 0])
10863 |> line(end = [0, -len])
10864 |> close()
10865 |> extrude(length = len)
10866
10867face = faceOf(cube, face = side)
10868";
10869
10870 let program = Program::parse(initial_source).unwrap().0.unwrap();
10871
10872 let mut frontend = FrontendState::new();
10873
10874 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10875 let mock_ctx = ExecutorContext::new_mock(None).await;
10876 let version = Version(0);
10877
10878 frontend.hack_set_program(&ctx, program).await.unwrap();
10879 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
10880 let face_id = face_object.id;
10881
10882 let sketch_args = SketchCtor {
10883 on: Plane::Object(face_id),
10884 };
10885 let (_src_delta, scene_delta, sketch_id) = frontend
10886 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10887 .await
10888 .unwrap();
10889 assert_eq!(sketch_id, ObjectId(2));
10890 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
10891 let sketch_object = &scene_delta.new_graph.objects[2];
10892 assert_eq!(sketch_object.id, ObjectId(2));
10893 assert_eq!(
10894 sketch_object.kind,
10895 ObjectKind::Sketch(Sketch {
10896 args: SketchCtor {
10897 on: Plane::Object(face_id),
10898 },
10899 plane: face_id,
10900 segments: vec![],
10901 constraints: vec![],
10902 })
10903 );
10904 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10905
10906 ctx.close().await;
10907 mock_ctx.close().await;
10908 }
10909
10910 #[tokio::test(flavor = "multi_thread")]
10911 async fn test_sketch_on_wall_artifact_from_region_extrude() {
10912 let initial_source = "\
10913s = sketch(on = YZ) {
10914 line1 = line(start = [0, 0], end = [0, 1])
10915 line2 = line(start = [0, 1], end = [1, 1])
10916 line3 = line(start = [1, 1], end = [0, 0])
10917}
10918region001 = region(point = [0.1, 0.1], sketch = s)
10919extrude001 = extrude(region001, length = 5)
10920";
10921
10922 let program = Program::parse(initial_source).unwrap().0.unwrap();
10923
10924 let mut frontend = FrontendState::new();
10925 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10926 let version = Version(0);
10927
10928 frontend.hack_set_program(&ctx, program).await.unwrap();
10929 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
10930
10931 let sketch_args = SketchCtor {
10932 on: Plane::Object(wall_object_id),
10933 };
10934 let (src_delta, _scene_delta, _sketch_id) = frontend
10935 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10936 .await
10937 .unwrap();
10938 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
10939
10940 ctx.close().await;
10941 }
10942
10943 #[tokio::test(flavor = "multi_thread")]
10944 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
10945 let initial_source = "\
10946sketch001 = sketch(on = YZ) {
10947 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
10948 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
10949 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
10950 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
10951 coincident([line1.end, line2.start])
10952 coincident([line2.end, line3.start])
10953 coincident([line3.end, line4.start])
10954 coincident([line4.end, line1.start])
10955 parallel([line2, line4])
10956 parallel([line3, line1])
10957 perpendicular([line1, line2])
10958 horizontal(line3)
10959 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
10960}
10961region001 = region(point = [3.1, 3.74], sketch = sketch001)
10962extrude001 = extrude(region001, length = 5)
10963";
10964
10965 let program = Program::parse(initial_source).unwrap().0.unwrap();
10966
10967 let mut frontend = FrontendState::new();
10968 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10969 let version = Version(0);
10970
10971 frontend.hack_set_program(&ctx, program).await.unwrap();
10972 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
10973
10974 let sketch_args = SketchCtor {
10975 on: Plane::Object(wall_object_id),
10976 };
10977 let (src_delta, _scene_delta, _sketch_id) = frontend
10978 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10979 .await
10980 .unwrap();
10981 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
10982
10983 ctx.close().await;
10984 }
10985
10986 #[tokio::test(flavor = "multi_thread")]
10987 async fn test_sketch_on_plane_incremental() {
10988 let initial_source = "\
10989len = 2mm
10990cube = startSketchOn(XY)
10991 |> startProfile(at = [0, 0])
10992 |> line(end = [len, 0], tag = $side)
10993 |> line(end = [0, len])
10994 |> line(end = [-len, 0])
10995 |> line(end = [0, -len])
10996 |> close()
10997 |> extrude(length = len)
10998
10999plane = planeOf(cube, face = side)
11000";
11001
11002 let program = Program::parse(initial_source).unwrap().0.unwrap();
11003
11004 let mut frontend = FrontendState::new();
11005
11006 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11007 let mock_ctx = ExecutorContext::new_mock(None).await;
11008 let version = Version(0);
11009
11010 frontend.hack_set_program(&ctx, program).await.unwrap();
11011 let plane_object = frontend
11013 .scene_graph
11014 .objects
11015 .iter()
11016 .rev()
11017 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
11018 .unwrap();
11019 let plane_id = plane_object.id;
11020
11021 let sketch_args = SketchCtor {
11022 on: Plane::Object(plane_id),
11023 };
11024 let (src_delta, scene_delta, sketch_id) = frontend
11025 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11026 .await
11027 .unwrap();
11028 assert_eq!(
11029 src_delta.text.as_str(),
11030 "\
11031len = 2mm
11032cube = startSketchOn(XY)
11033 |> startProfile(at = [0, 0])
11034 |> line(end = [len, 0], tag = $side)
11035 |> line(end = [0, len])
11036 |> line(end = [-len, 0])
11037 |> line(end = [0, -len])
11038 |> close()
11039 |> extrude(length = len)
11040
11041plane = planeOf(cube, face = side)
11042sketch001 = sketch(on = plane) {
11043}
11044"
11045 );
11046 assert_eq!(sketch_id, ObjectId(2));
11047 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11048 let sketch_object = &scene_delta.new_graph.objects[2];
11049 assert_eq!(sketch_object.id, ObjectId(2));
11050 assert_eq!(
11051 sketch_object.kind,
11052 ObjectKind::Sketch(Sketch {
11053 args: SketchCtor {
11054 on: Plane::Object(plane_id),
11055 },
11056 plane: plane_id,
11057 segments: vec![],
11058 constraints: vec![],
11059 })
11060 );
11061 assert_eq!(scene_delta.new_graph.objects.len(), 9);
11062
11063 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
11064 assert_eq!(plane_object.id, plane_id);
11065 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
11066
11067 ctx.close().await;
11068 mock_ctx.close().await;
11069 }
11070
11071 #[tokio::test(flavor = "multi_thread")]
11072 async fn test_new_sketch_uses_unique_variable_name() {
11073 let initial_source = "\
11074sketch1 = sketch(on = XY) {
11075}
11076";
11077
11078 let program = Program::parse(initial_source).unwrap().0.unwrap();
11079
11080 let mut frontend = FrontendState::new();
11081 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11082 let version = Version(0);
11083
11084 frontend.hack_set_program(&ctx, program).await.unwrap();
11085
11086 let sketch_args = SketchCtor {
11087 on: Plane::Default(PlaneName::Yz),
11088 };
11089 let (src_delta, _, _) = frontend
11090 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11091 .await
11092 .unwrap();
11093
11094 assert_eq!(
11095 src_delta.text.as_str(),
11096 "\
11097sketch1 = sketch(on = XY) {
11098}
11099sketch001 = sketch(on = YZ) {
11100}
11101"
11102 );
11103
11104 ctx.close().await;
11105 }
11106
11107 #[tokio::test(flavor = "multi_thread")]
11108 async fn test_new_sketch_twice_using_same_plane() {
11109 let initial_source = "\
11110sketch1 = sketch(on = XY) {
11111}
11112";
11113
11114 let program = Program::parse(initial_source).unwrap().0.unwrap();
11115
11116 let mut frontend = FrontendState::new();
11117 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11118 let version = Version(0);
11119
11120 frontend.hack_set_program(&ctx, program).await.unwrap();
11121
11122 let sketch_args = SketchCtor {
11123 on: Plane::Default(PlaneName::Xy),
11124 };
11125 let (src_delta, _, _) = frontend
11126 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11127 .await
11128 .unwrap();
11129
11130 assert_eq!(
11131 src_delta.text.as_str(),
11132 "\
11133sketch1 = sketch(on = XY) {
11134}
11135sketch001 = sketch(on = XY) {
11136}
11137"
11138 );
11139
11140 ctx.close().await;
11141 }
11142
11143 #[tokio::test(flavor = "multi_thread")]
11144 async fn test_sketch_mode_reuses_cached_on_expression() {
11145 let initial_source = "\
11146width = 2mm
11147sketch(on = offsetPlane(XY, offset = width)) {
11148 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
11149 distance([line1.start, line1.end]) == width
11150}
11151";
11152 let program = Program::parse(initial_source).unwrap().0.unwrap();
11153
11154 let mut frontend = FrontendState::new();
11155 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11156 let mock_ctx = ExecutorContext::new_mock(None).await;
11157 let version = Version(0);
11158 let project_id = ProjectId(0);
11159 let file_id = FileId(0);
11160
11161 frontend.hack_set_program(&ctx, program).await.unwrap();
11162 let initial_object_count = frontend.scene_graph.objects.len();
11163 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
11164 .expect("Expected sketch object to exist")
11165 .id;
11166
11167 let scene_delta = frontend
11170 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11171 .await
11172 .unwrap();
11173 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11174
11175 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
11178 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11179
11180 ctx.close().await;
11181 mock_ctx.close().await;
11182 }
11183
11184 #[tokio::test(flavor = "multi_thread")]
11185 async fn test_multiple_sketch_blocks() {
11186 let initial_source = "\
11187// Cube that requires the engine.
11188width = 2
11189sketch001 = startSketchOn(XY)
11190profile001 = startProfile(sketch001, at = [0, 0])
11191 |> yLine(length = width, tag = $seg1)
11192 |> xLine(length = width)
11193 |> yLine(length = -width)
11194 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11195 |> close()
11196extrude001 = extrude(profile001, length = width)
11197
11198// Get a value that requires the engine.
11199x = segLen(seg1)
11200
11201// Triangle with side length 2*x.
11202sketch(on = XY) {
11203 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11204 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11205 coincident([line1.end, line2.start])
11206 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11207 coincident([line2.end, line3.start])
11208 coincident([line3.end, line1.start])
11209 equalLength([line3, line1])
11210 equalLength([line1, line2])
11211 distance([line1.start, line1.end]) == 2*x
11212}
11213
11214// Line segment with length x.
11215sketch2 = sketch(on = XY) {
11216 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11217 distance([line1.start, line1.end]) == x
11218}
11219";
11220
11221 let program = Program::parse(initial_source).unwrap().0.unwrap();
11222
11223 let mut frontend = FrontendState::new();
11224
11225 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11226 let mock_ctx = ExecutorContext::new_mock(None).await;
11227 let version = Version(0);
11228 let project_id = ProjectId(0);
11229 let file_id = FileId(0);
11230
11231 frontend.hack_set_program(&ctx, program).await.unwrap();
11232 let sketch_objects = frontend
11233 .scene_graph
11234 .objects
11235 .iter()
11236 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
11237 .collect::<Vec<_>>();
11238 let sketch1_id = sketch_objects.first().unwrap().id;
11239 let sketch2_id = sketch_objects.get(1).unwrap().id;
11240 let point1_id = ObjectId(sketch1_id.0 + 1);
11242 let point2_id = ObjectId(sketch2_id.0 + 1);
11244
11245 let scene_delta = frontend
11254 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11255 .await
11256 .unwrap();
11257 assert_eq!(
11258 scene_delta.new_graph.objects.len(),
11259 18,
11260 "{:#?}",
11261 scene_delta.new_graph.objects
11262 );
11263
11264 let point_ctor = PointCtor {
11266 position: Point2d {
11267 x: Expr::Var(Number {
11268 value: 1.0,
11269 units: NumericSuffix::Mm,
11270 }),
11271 y: Expr::Var(Number {
11272 value: 2.0,
11273 units: NumericSuffix::Mm,
11274 }),
11275 },
11276 };
11277 let segments = vec![ExistingSegmentCtor {
11278 id: point1_id,
11279 ctor: SegmentCtor::Point(point_ctor),
11280 }];
11281 let (src_delta, _) = frontend
11282 .edit_segments(&mock_ctx, version, sketch1_id, segments)
11283 .await
11284 .unwrap();
11285 assert_eq!(
11287 src_delta.text.as_str(),
11288 "\
11289// Cube that requires the engine.
11290width = 2
11291sketch001 = startSketchOn(XY)
11292profile001 = startProfile(sketch001, at = [0, 0])
11293 |> yLine(length = width, tag = $seg1)
11294 |> xLine(length = width)
11295 |> yLine(length = -width)
11296 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11297 |> close()
11298extrude001 = extrude(profile001, length = width)
11299
11300// Get a value that requires the engine.
11301x = segLen(seg1)
11302
11303// Triangle with side length 2*x.
11304sketch(on = XY) {
11305 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
11306 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
11307 coincident([line1.end, line2.start])
11308 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
11309 coincident([line2.end, line3.start])
11310 coincident([line3.end, line1.start])
11311 equalLength([line3, line1])
11312 equalLength([line1, line2])
11313 distance([line1.start, line1.end]) == 2 * x
11314}
11315
11316// Line segment with length x.
11317sketch2 = sketch(on = XY) {
11318 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11319 distance([line1.start, line1.end]) == x
11320}
11321"
11322 );
11323
11324 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
11326 assert_eq!(
11328 src_delta.text.as_str(),
11329 "\
11330// Cube that requires the engine.
11331width = 2
11332sketch001 = startSketchOn(XY)
11333profile001 = startProfile(sketch001, at = [0, 0])
11334 |> yLine(length = width, tag = $seg1)
11335 |> xLine(length = width)
11336 |> yLine(length = -width)
11337 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11338 |> close()
11339extrude001 = extrude(profile001, length = width)
11340
11341// Get a value that requires the engine.
11342x = segLen(seg1)
11343
11344// Triangle with side length 2*x.
11345sketch(on = XY) {
11346 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11347 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11348 coincident([line1.end, line2.start])
11349 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11350 coincident([line2.end, line3.start])
11351 coincident([line3.end, line1.start])
11352 equalLength([line3, line1])
11353 equalLength([line1, line2])
11354 distance([line1.start, line1.end]) == 2 * x
11355}
11356
11357// Line segment with length x.
11358sketch2 = sketch(on = XY) {
11359 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11360 distance([line1.start, line1.end]) == x
11361}
11362"
11363 );
11364 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11372 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
11373
11374 let scene_delta = frontend
11382 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11383 .await
11384 .unwrap();
11385 assert_eq!(
11386 scene_delta.new_graph.objects.len(),
11387 24,
11388 "{:#?}",
11389 scene_delta.new_graph.objects
11390 );
11391
11392 let point_ctor = PointCtor {
11394 position: Point2d {
11395 x: Expr::Var(Number {
11396 value: 3.0,
11397 units: NumericSuffix::Mm,
11398 }),
11399 y: Expr::Var(Number {
11400 value: 4.0,
11401 units: NumericSuffix::Mm,
11402 }),
11403 },
11404 };
11405 let segments = vec![ExistingSegmentCtor {
11406 id: point2_id,
11407 ctor: SegmentCtor::Point(point_ctor),
11408 }];
11409 let (src_delta, _) = frontend
11410 .edit_segments(&mock_ctx, version, sketch2_id, segments)
11411 .await
11412 .unwrap();
11413 assert_eq!(
11415 src_delta.text.as_str(),
11416 "\
11417// Cube that requires the engine.
11418width = 2
11419sketch001 = startSketchOn(XY)
11420profile001 = startProfile(sketch001, at = [0, 0])
11421 |> yLine(length = width, tag = $seg1)
11422 |> xLine(length = width)
11423 |> yLine(length = -width)
11424 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11425 |> close()
11426extrude001 = extrude(profile001, length = width)
11427
11428// Get a value that requires the engine.
11429x = segLen(seg1)
11430
11431// Triangle with side length 2*x.
11432sketch(on = XY) {
11433 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11434 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11435 coincident([line1.end, line2.start])
11436 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11437 coincident([line2.end, line3.start])
11438 coincident([line3.end, line1.start])
11439 equalLength([line3, line1])
11440 equalLength([line1, line2])
11441 distance([line1.start, line1.end]) == 2 * x
11442}
11443
11444// Line segment with length x.
11445sketch2 = sketch(on = XY) {
11446 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
11447 distance([line1.start, line1.end]) == x
11448}
11449"
11450 );
11451
11452 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
11454 assert_eq!(
11456 src_delta.text.as_str(),
11457 "\
11458// Cube that requires the engine.
11459width = 2
11460sketch001 = startSketchOn(XY)
11461profile001 = startProfile(sketch001, at = [0, 0])
11462 |> yLine(length = width, tag = $seg1)
11463 |> xLine(length = width)
11464 |> yLine(length = -width)
11465 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11466 |> close()
11467extrude001 = extrude(profile001, length = width)
11468
11469// Get a value that requires the engine.
11470x = segLen(seg1)
11471
11472// Triangle with side length 2*x.
11473sketch(on = XY) {
11474 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11475 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11476 coincident([line1.end, line2.start])
11477 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11478 coincident([line2.end, line3.start])
11479 coincident([line3.end, line1.start])
11480 equalLength([line3, line1])
11481 equalLength([line1, line2])
11482 distance([line1.start, line1.end]) == 2 * x
11483}
11484
11485// Line segment with length x.
11486sketch2 = sketch(on = XY) {
11487 line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
11488 distance([line1.start, line1.end]) == x
11489}
11490"
11491 );
11492
11493 ctx.close().await;
11494 mock_ctx.close().await;
11495 }
11496
11497 #[tokio::test(flavor = "multi_thread")]
11498 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
11499 clear_mem_cache().await;
11500
11501 let source = r#"sketch001 = sketch(on = XZ) {
11502 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
11503}
11504sketch002 = sketch(on = XY) {
11505 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
11506 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
11507 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
11508 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
11509 coincident([line1.end, line2.start])
11510 coincident([line2.end, line3.start])
11511 coincident([line3.end, line4.start])
11512 coincident([line4.end, line1.start])
11513 parallel([line2, line4])
11514 parallel([line3, line1])
11515 perpendicular([line1, line2])
11516 horizontal(line3)
11517 coincident([line1.start, ORIGIN])
11518}
11519"#;
11520
11521 let program = Program::parse(source).unwrap().0.unwrap();
11522 let mut frontend = FrontendState::new();
11523 let ctx = ExecutorContext::new_with_engine(
11524 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
11525 Default::default(),
11526 );
11527 let mock_ctx = ExecutorContext::new_mock(None).await;
11528 let version = Version(0);
11529 let project_id = ProjectId(0);
11530 let file_id = FileId(0);
11531
11532 frontend.hack_set_program(&ctx, program).await.unwrap();
11533 let sketch_objects = frontend
11534 .scene_graph
11535 .objects
11536 .iter()
11537 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
11538 .collect::<Vec<_>>();
11539 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
11540
11541 let sketch1_id = sketch_objects[0].id;
11542 let sketch2_id = sketch_objects[1].id;
11543
11544 frontend
11545 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11546 .await
11547 .unwrap();
11548 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11549
11550 let scene_delta = frontend
11551 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11552 .await
11553 .unwrap();
11554 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
11555
11556 clear_mem_cache().await;
11557 ctx.close().await;
11558 mock_ctx.close().await;
11559 }
11560
11561 #[tokio::test(flavor = "multi_thread")]
11566 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
11567 let initial_source = "@settings(defaultLengthUnit = mm)
11569
11570
11571
11572sketch001 = sketch(on = XY) {
11573 point(at = [1in, 2in])
11574}
11575";
11576
11577 let program = Program::parse(initial_source).unwrap().0.unwrap();
11578 let mut frontend = FrontendState::new();
11579
11580 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11581 let mock_ctx = ExecutorContext::new_mock(None).await;
11582 let version = Version(0);
11583 let project_id = ProjectId(0);
11584 let file_id = FileId(0);
11585
11586 frontend.hack_set_program(&ctx, program).await.unwrap();
11587 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11588 let sketch_id = sketch_object.id;
11589
11590 frontend
11592 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11593 .await
11594 .unwrap();
11595
11596 let point_ctor = PointCtor {
11598 position: Point2d {
11599 x: Expr::Number(Number {
11600 value: 5.0,
11601 units: NumericSuffix::Mm,
11602 }),
11603 y: Expr::Number(Number {
11604 value: 6.0,
11605 units: NumericSuffix::Mm,
11606 }),
11607 },
11608 };
11609 let segment = SegmentCtor::Point(point_ctor);
11610 let (src_delta, scene_delta) = frontend
11611 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11612 .await
11613 .unwrap();
11614 assert!(
11616 src_delta.text.contains("point(at = [5mm, 6mm])"),
11617 "Expected new point in source, got: {}",
11618 src_delta.text
11619 );
11620 assert!(!scene_delta.new_objects.is_empty());
11621
11622 ctx.close().await;
11623 mock_ctx.close().await;
11624 }
11625
11626 #[tokio::test(flavor = "multi_thread")]
11627 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
11628 let initial_source = "@settings(defaultLengthUnit = mm)
11630
11631
11632
11633s = sketch(on = XY) {}
11634";
11635
11636 let program = Program::parse(initial_source).unwrap().0.unwrap();
11637 let mut frontend = FrontendState::new();
11638
11639 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11640 let mock_ctx = ExecutorContext::new_mock(None).await;
11641 let version = Version(0);
11642
11643 frontend.hack_set_program(&ctx, program).await.unwrap();
11644 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11645 let sketch_id = sketch_object.id;
11646
11647 let line_ctor = LineCtor {
11648 start: Point2d {
11649 x: Expr::Number(Number {
11650 value: 0.0,
11651 units: NumericSuffix::Mm,
11652 }),
11653 y: Expr::Number(Number {
11654 value: 0.0,
11655 units: NumericSuffix::Mm,
11656 }),
11657 },
11658 end: Point2d {
11659 x: Expr::Number(Number {
11660 value: 10.0,
11661 units: NumericSuffix::Mm,
11662 }),
11663 y: Expr::Number(Number {
11664 value: 10.0,
11665 units: NumericSuffix::Mm,
11666 }),
11667 },
11668 construction: None,
11669 };
11670 let segment = SegmentCtor::Line(line_ctor);
11671 let (src_delta, scene_delta) = frontend
11672 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11673 .await
11674 .unwrap();
11675 assert!(
11676 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
11677 "Expected line in source, got: {}",
11678 src_delta.text
11679 );
11680 assert_eq!(scene_delta.new_objects.len(), 3);
11682
11683 ctx.close().await;
11684 mock_ctx.close().await;
11685 }
11686
11687 #[tokio::test(flavor = "multi_thread")]
11688 async fn test_extra_newlines_between_operations_edit_line() {
11689 let initial_source = "@settings(defaultLengthUnit = mm)
11691
11692
11693sketch001 = sketch(on = XY) {
11694
11695 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
11696
11697}
11698";
11699
11700 let program = Program::parse(initial_source).unwrap().0.unwrap();
11701 let mut frontend = FrontendState::new();
11702
11703 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11704 let mock_ctx = ExecutorContext::new_mock(None).await;
11705 let version = Version(0);
11706 let project_id = ProjectId(0);
11707 let file_id = FileId(0);
11708
11709 frontend.hack_set_program(&ctx, program).await.unwrap();
11710 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11711 let sketch_id = sketch_object.id;
11712 let sketch = expect_sketch(sketch_object);
11713
11714 let line_id = sketch
11716 .segments
11717 .iter()
11718 .copied()
11719 .find(|seg_id| {
11720 matches!(
11721 &frontend.scene_graph.objects[seg_id.0].kind,
11722 ObjectKind::Segment {
11723 segment: Segment::Line(_)
11724 }
11725 )
11726 })
11727 .expect("Expected a line segment in sketch");
11728
11729 frontend
11731 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11732 .await
11733 .unwrap();
11734
11735 let line_ctor = LineCtor {
11737 start: Point2d {
11738 x: Expr::Var(Number {
11739 value: 1.0,
11740 units: NumericSuffix::Mm,
11741 }),
11742 y: Expr::Var(Number {
11743 value: 2.0,
11744 units: NumericSuffix::Mm,
11745 }),
11746 },
11747 end: Point2d {
11748 x: Expr::Var(Number {
11749 value: 13.0,
11750 units: NumericSuffix::Mm,
11751 }),
11752 y: Expr::Var(Number {
11753 value: 14.0,
11754 units: NumericSuffix::Mm,
11755 }),
11756 },
11757 construction: None,
11758 };
11759 let segments = vec![ExistingSegmentCtor {
11760 id: line_id,
11761 ctor: SegmentCtor::Line(line_ctor),
11762 }];
11763 let (src_delta, _scene_delta) = frontend
11764 .edit_segments(&mock_ctx, version, sketch_id, segments)
11765 .await
11766 .unwrap();
11767 assert!(
11768 src_delta
11769 .text
11770 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
11771 "Expected edited line in source, got: {}",
11772 src_delta.text
11773 );
11774
11775 ctx.close().await;
11776 mock_ctx.close().await;
11777 }
11778
11779 #[tokio::test(flavor = "multi_thread")]
11780 async fn test_extra_newlines_delete_segment() {
11781 let initial_source = "@settings(defaultLengthUnit = mm)
11783
11784
11785
11786sketch001 = sketch(on = XY) {
11787 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
11788}
11789";
11790
11791 let program = Program::parse(initial_source).unwrap().0.unwrap();
11792 let mut frontend = FrontendState::new();
11793
11794 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11795 let mock_ctx = ExecutorContext::new_mock(None).await;
11796 let version = Version(0);
11797
11798 frontend.hack_set_program(&ctx, program).await.unwrap();
11799 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11800 let sketch_id = sketch_object.id;
11801 let sketch = expect_sketch(sketch_object);
11802
11803 assert_eq!(sketch.segments.len(), 3);
11805 let circle_id = sketch.segments[2];
11806
11807 let (src_delta, scene_delta) = frontend
11809 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
11810 .await
11811 .unwrap();
11812 assert!(
11813 src_delta.text.contains("sketch(on = XY) {"),
11814 "Expected sketch block in source, got: {}",
11815 src_delta.text
11816 );
11817 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11818 let new_sketch = expect_sketch(new_sketch_object);
11819 assert_eq!(new_sketch.segments.len(), 0);
11820
11821 ctx.close().await;
11822 mock_ctx.close().await;
11823 }
11824
11825 #[tokio::test(flavor = "multi_thread")]
11826 async fn test_unformatted_source_add_arc() {
11827 let initial_source = "@settings(defaultLengthUnit = mm)
11829
11830
11831
11832
11833sketch001 = sketch(on = XY) {
11834}
11835";
11836
11837 let program = Program::parse(initial_source).unwrap().0.unwrap();
11838 let mut frontend = FrontendState::new();
11839
11840 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11841 let mock_ctx = ExecutorContext::new_mock(None).await;
11842 let version = Version(0);
11843
11844 frontend.hack_set_program(&ctx, program).await.unwrap();
11845 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11846 let sketch_id = sketch_object.id;
11847
11848 let arc_ctor = ArcCtor {
11849 start: Point2d {
11850 x: Expr::Var(Number {
11851 value: 5.0,
11852 units: NumericSuffix::Mm,
11853 }),
11854 y: Expr::Var(Number {
11855 value: 0.0,
11856 units: NumericSuffix::Mm,
11857 }),
11858 },
11859 end: Point2d {
11860 x: Expr::Var(Number {
11861 value: 0.0,
11862 units: NumericSuffix::Mm,
11863 }),
11864 y: Expr::Var(Number {
11865 value: 5.0,
11866 units: NumericSuffix::Mm,
11867 }),
11868 },
11869 center: Point2d {
11870 x: Expr::Var(Number {
11871 value: 0.0,
11872 units: NumericSuffix::Mm,
11873 }),
11874 y: Expr::Var(Number {
11875 value: 0.0,
11876 units: NumericSuffix::Mm,
11877 }),
11878 },
11879 construction: None,
11880 };
11881 let segment = SegmentCtor::Arc(arc_ctor);
11882 let (src_delta, scene_delta) = frontend
11883 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11884 .await
11885 .unwrap();
11886 assert!(
11887 src_delta
11888 .text
11889 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
11890 "Expected arc in source, got: {}",
11891 src_delta.text
11892 );
11893 assert!(!scene_delta.new_objects.is_empty());
11894
11895 ctx.close().await;
11896 mock_ctx.close().await;
11897 }
11898
11899 #[tokio::test(flavor = "multi_thread")]
11900 async fn test_extra_newlines_add_circle() {
11901 let initial_source = "@settings(defaultLengthUnit = mm)
11903
11904
11905
11906sketch001 = sketch(on = XY) {
11907}
11908";
11909
11910 let program = Program::parse(initial_source).unwrap().0.unwrap();
11911 let mut frontend = FrontendState::new();
11912
11913 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11914 let mock_ctx = ExecutorContext::new_mock(None).await;
11915 let version = Version(0);
11916
11917 frontend.hack_set_program(&ctx, program).await.unwrap();
11918 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11919 let sketch_id = sketch_object.id;
11920
11921 let circle_ctor = CircleCtor {
11922 start: Point2d {
11923 x: Expr::Var(Number {
11924 value: 5.0,
11925 units: NumericSuffix::Mm,
11926 }),
11927 y: Expr::Var(Number {
11928 value: 0.0,
11929 units: NumericSuffix::Mm,
11930 }),
11931 },
11932 center: Point2d {
11933 x: Expr::Var(Number {
11934 value: 0.0,
11935 units: NumericSuffix::Mm,
11936 }),
11937 y: Expr::Var(Number {
11938 value: 0.0,
11939 units: NumericSuffix::Mm,
11940 }),
11941 },
11942 construction: None,
11943 };
11944 let segment = SegmentCtor::Circle(circle_ctor);
11945 let (src_delta, scene_delta) = frontend
11946 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11947 .await
11948 .unwrap();
11949 assert!(
11950 src_delta
11951 .text
11952 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
11953 "Expected circle in source, got: {}",
11954 src_delta.text
11955 );
11956 assert!(!scene_delta.new_objects.is_empty());
11957
11958 ctx.close().await;
11959 mock_ctx.close().await;
11960 }
11961
11962 #[tokio::test(flavor = "multi_thread")]
11963 async fn test_extra_newlines_add_constraint() {
11964 let initial_source = "@settings(defaultLengthUnit = mm)
11966
11967
11968
11969sketch001 = sketch(on = XY) {
11970 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
11971 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
11972}
11973";
11974
11975 let program = Program::parse(initial_source).unwrap().0.unwrap();
11976 let mut frontend = FrontendState::new();
11977
11978 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11979 let mock_ctx = ExecutorContext::new_mock(None).await;
11980 let version = Version(0);
11981 let project_id = ProjectId(0);
11982 let file_id = FileId(0);
11983
11984 frontend.hack_set_program(&ctx, program).await.unwrap();
11985 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11986 let sketch_id = sketch_object.id;
11987 let sketch = expect_sketch(sketch_object);
11988
11989 let line_ids: Vec<ObjectId> = sketch
11991 .segments
11992 .iter()
11993 .copied()
11994 .filter(|seg_id| {
11995 matches!(
11996 &frontend.scene_graph.objects[seg_id.0].kind,
11997 ObjectKind::Segment {
11998 segment: Segment::Line(_)
11999 }
12000 )
12001 })
12002 .collect();
12003 assert_eq!(line_ids.len(), 2, "Expected two line segments");
12004
12005 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
12006 let ObjectKind::Segment {
12007 segment: Segment::Line(line1_data),
12008 } = &line1.kind
12009 else {
12010 panic!("Expected line");
12011 };
12012 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
12013 let ObjectKind::Segment {
12014 segment: Segment::Line(line2_data),
12015 } = &line2.kind
12016 else {
12017 panic!("Expected line");
12018 };
12019
12020 let constraint = Constraint::Coincident(Coincident {
12022 segments: vec![line1_data.end.into(), line2_data.start.into()],
12023 });
12024
12025 frontend
12027 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12028 .await
12029 .unwrap();
12030 let (src_delta, _scene_delta) = frontend
12031 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12032 .await
12033 .unwrap();
12034 assert!(
12035 src_delta.text.contains("coincident("),
12036 "Expected coincident constraint in source, got: {}",
12037 src_delta.text
12038 );
12039
12040 ctx.close().await;
12041 mock_ctx.close().await;
12042 }
12043
12044 #[tokio::test(flavor = "multi_thread")]
12045 async fn test_extra_newlines_add_line_then_edit_line() {
12046 let initial_source = "@settings(defaultLengthUnit = mm)
12048
12049
12050
12051sketch001 = sketch(on = XY) {
12052}
12053";
12054
12055 let program = Program::parse(initial_source).unwrap().0.unwrap();
12056 let mut frontend = FrontendState::new();
12057
12058 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12059 let mock_ctx = ExecutorContext::new_mock(None).await;
12060 let version = Version(0);
12061
12062 frontend.hack_set_program(&ctx, program).await.unwrap();
12063 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12064 let sketch_id = sketch_object.id;
12065
12066 let line_ctor = LineCtor {
12068 start: Point2d {
12069 x: Expr::Number(Number {
12070 value: 0.0,
12071 units: NumericSuffix::Mm,
12072 }),
12073 y: Expr::Number(Number {
12074 value: 0.0,
12075 units: NumericSuffix::Mm,
12076 }),
12077 },
12078 end: Point2d {
12079 x: Expr::Number(Number {
12080 value: 10.0,
12081 units: NumericSuffix::Mm,
12082 }),
12083 y: Expr::Number(Number {
12084 value: 10.0,
12085 units: NumericSuffix::Mm,
12086 }),
12087 },
12088 construction: None,
12089 };
12090 let segment = SegmentCtor::Line(line_ctor);
12091 let (src_delta, scene_delta) = frontend
12092 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12093 .await
12094 .unwrap();
12095 assert!(
12096 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12097 "Expected line in source after add, got: {}",
12098 src_delta.text
12099 );
12100 let line_id = *scene_delta.new_objects.last().unwrap();
12102
12103 let line_ctor = LineCtor {
12105 start: Point2d {
12106 x: Expr::Number(Number {
12107 value: 1.0,
12108 units: NumericSuffix::Mm,
12109 }),
12110 y: Expr::Number(Number {
12111 value: 2.0,
12112 units: NumericSuffix::Mm,
12113 }),
12114 },
12115 end: Point2d {
12116 x: Expr::Number(Number {
12117 value: 13.0,
12118 units: NumericSuffix::Mm,
12119 }),
12120 y: Expr::Number(Number {
12121 value: 14.0,
12122 units: NumericSuffix::Mm,
12123 }),
12124 },
12125 construction: None,
12126 };
12127 let segments = vec![ExistingSegmentCtor {
12128 id: line_id,
12129 ctor: SegmentCtor::Line(line_ctor),
12130 }];
12131 let (src_delta, scene_delta) = frontend
12132 .edit_segments(&mock_ctx, version, sketch_id, segments)
12133 .await
12134 .unwrap();
12135 assert!(
12136 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
12137 "Expected edited line in source, got: {}",
12138 src_delta.text
12139 );
12140 assert_eq!(scene_delta.new_objects, vec![]);
12141
12142 ctx.close().await;
12143 mock_ctx.close().await;
12144 }
12145}