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;
19use crate::execution::Artifact;
20use crate::execution::ArtifactGraph;
21use crate::execution::CapSubType;
22use crate::execution::MockConfig;
23use crate::execution::SKETCH_BLOCK_PARAM_ON;
24use crate::execution::cache::SketchModeState;
25use crate::execution::cache::clear_mem_cache;
26use crate::execution::cache::read_old_memory;
27use crate::execution::cache::write_old_memory;
28use crate::fmt::format_number_literal;
29use crate::front::Angle;
30use crate::front::ArcCtor;
31use crate::front::CircleCtor;
32use crate::front::Distance;
33use crate::front::EqualRadius;
34use crate::front::Error;
35use crate::front::ExecResult;
36use crate::front::FixedPoint;
37use crate::front::Freedom;
38use crate::front::LinesEqualLength;
39use crate::front::Midpoint;
40use crate::front::Object;
41use crate::front::Parallel;
42use crate::front::Perpendicular;
43use crate::front::PointCtor;
44use crate::front::Symmetric;
45use crate::front::Tangent;
46use crate::frontend::api::Expr;
47use crate::frontend::api::FileId;
48use crate::frontend::api::Number;
49use crate::frontend::api::ObjectId;
50use crate::frontend::api::ObjectKind;
51use crate::frontend::api::Plane;
52use crate::frontend::api::ProjectId;
53use crate::frontend::api::RestoreSketchCheckpointOutcome;
54use crate::frontend::api::SceneGraph;
55use crate::frontend::api::SceneGraphDelta;
56use crate::frontend::api::SketchCheckpointId;
57use crate::frontend::api::SourceDelta;
58use crate::frontend::api::SourceRef;
59use crate::frontend::api::Version;
60use crate::frontend::modify::find_defined_names;
61use crate::frontend::modify::next_free_name;
62use crate::frontend::modify::next_free_name_with_padding;
63use crate::frontend::sketch::Coincident;
64use crate::frontend::sketch::Constraint;
65use crate::frontend::sketch::ConstraintSegment;
66use crate::frontend::sketch::Diameter;
67use crate::frontend::sketch::ExistingSegmentCtor;
68use crate::frontend::sketch::Horizontal;
69use crate::frontend::sketch::LineCtor;
70use crate::frontend::sketch::Point2d;
71use crate::frontend::sketch::Radius;
72use crate::frontend::sketch::Segment;
73use crate::frontend::sketch::SegmentCtor;
74use crate::frontend::sketch::SketchApi;
75use crate::frontend::sketch::SketchCtor;
76use crate::frontend::sketch::Vertical;
77use crate::frontend::traverse::MutateBodyItem;
78use crate::frontend::traverse::TraversalReturn;
79use crate::frontend::traverse::Visitor;
80use crate::frontend::traverse::dfs_mut;
81use crate::id::IncIdGenerator;
82use crate::parsing::ast::types as ast;
83use crate::pretty::NumericSuffix;
84use crate::std::constraints::LinesAtAngleKind;
85use crate::walk::NodeMut;
86use crate::walk::Visitable;
87
88pub(crate) mod api;
89pub(crate) mod modify;
90pub(crate) mod sketch;
91
92pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
93
94#[derive(Debug, Clone)]
95struct SketchCheckpoint {
96 id: SketchCheckpointId,
97 source: SourceDelta,
98 program: Program,
99 scene_graph: SceneGraph,
100 exec_outcome: ExecOutcome,
101 point_freedom_cache: HashMap<ObjectId, Freedom>,
102 mock_memory: Option<SketchModeState>,
103}
104mod traverse;
105pub(crate) mod trim;
106
107struct ArcSizeConstraintParams {
108 points: Vec<ObjectId>,
109 function_name: &'static str,
110 value: f64,
111 units: NumericSuffix,
112 label_position: Option<Point2d<Number>>,
113 constraint_type_name: &'static str,
114}
115
116const POINT_FN: &str = "point";
117const POINT_AT_PARAM: &str = "at";
118const LINE_FN: &str = "line";
119const LINE_VARIABLE: &str = "line";
120const LINE_START_PARAM: &str = "start";
121const LINE_END_PARAM: &str = "end";
122const ARC_FN: &str = "arc";
123const ARC_VARIABLE: &str = "arc";
124const ARC_START_PARAM: &str = "start";
125const ARC_END_PARAM: &str = "end";
126const ARC_CENTER_PARAM: &str = "center";
127const CIRCLE_FN: &str = "circle";
128const CIRCLE_VARIABLE: &str = "circle";
129const CIRCLE_START_PARAM: &str = "start";
130const CIRCLE_CENTER_PARAM: &str = "center";
131const LABEL_POSITION_PARAM: &str = "labelPosition";
132
133const COINCIDENT_FN: &str = "coincident";
134const DIAMETER_FN: &str = "diameter";
135const DISTANCE_FN: &str = "distance";
136const FIXED_FN: &str = "fixed";
137const ANGLE_FN: &str = "angle";
138const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
139const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
140const EQUAL_LENGTH_FN: &str = "equalLength";
141const EQUAL_RADIUS_FN: &str = "equalRadius";
142const HORIZONTAL_FN: &str = "horizontal";
143const MIDPOINT_FN: &str = "midpoint";
144const MIDPOINT_POINT_PARAM: &str = "point";
145const RADIUS_FN: &str = "radius";
146const SYMMETRIC_FN: &str = "symmetric";
147const SYMMETRIC_AXIS_PARAM: &str = "axis";
148const TANGENT_FN: &str = "tangent";
149const VERTICAL_FN: &str = "vertical";
150
151const LINE_PROPERTY_START: &str = "start";
152const LINE_PROPERTY_END: &str = "end";
153
154const ARC_PROPERTY_START: &str = "start";
155const ARC_PROPERTY_END: &str = "end";
156const ARC_PROPERTY_CENTER: &str = "center";
157const CIRCLE_PROPERTY_START: &str = "start";
158const CIRCLE_PROPERTY_CENTER: &str = "center";
159
160const CONSTRUCTION_PARAM: &str = "construction";
161
162#[derive(Debug, Clone, Copy)]
163enum EditDeleteKind {
164 Edit,
165 DeleteNonSketch,
166}
167
168impl EditDeleteKind {
169 fn is_delete(&self) -> bool {
171 match self {
172 EditDeleteKind::Edit => false,
173 EditDeleteKind::DeleteNonSketch => true,
174 }
175 }
176
177 fn to_change_kind(self) -> ChangeKind {
178 match self {
179 EditDeleteKind::Edit => ChangeKind::Edit,
180 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
181 }
182 }
183}
184
185#[derive(Debug, Clone, Copy)]
186enum ChangeKind {
187 Add,
188 Edit,
189 Delete,
190 None,
191}
192
193#[derive(Debug, Clone, Serialize, ts_rs::TS)]
194#[ts(export, export_to = "FrontendApi.ts")]
195#[serde(tag = "type")]
196pub enum SetProgramOutcome {
197 #[serde(rename_all = "camelCase")]
198 Success {
199 scene_graph: Box<SceneGraph>,
200 exec_outcome: Box<ExecOutcome>,
201 checkpoint_id: Option<SketchCheckpointId>,
202 },
203 #[serde(rename_all = "camelCase")]
204 ExecFailure { error: Box<KclErrorWithOutputs> },
205}
206
207#[derive(Debug, Clone)]
208pub struct FrontendState {
209 program: Program,
210 scene_graph: SceneGraph,
211 point_freedom_cache: HashMap<ObjectId, Freedom>,
214 sketch_checkpoints: VecDeque<SketchCheckpoint>,
215 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
216}
217
218impl Default for FrontendState {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224impl FrontendState {
225 pub fn new() -> Self {
226 Self {
227 program: Program::empty(),
228 scene_graph: SceneGraph {
229 project: ProjectId(0),
230 file: FileId(0),
231 version: Version(0),
232 objects: Default::default(),
233 settings: Default::default(),
234 sketch_mode: Default::default(),
235 },
236 point_freedom_cache: HashMap::new(),
237 sketch_checkpoints: VecDeque::new(),
238 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
239 }
240 }
241
242 pub fn scene_graph(&self) -> &SceneGraph {
244 &self.scene_graph
245 }
246
247 pub fn default_length_unit(&self) -> UnitLength {
248 self.program
249 .meta_settings()
250 .ok()
251 .flatten()
252 .map(|settings| settings.default_length_units)
253 .unwrap_or(UnitLength::Millimeters)
254 }
255
256 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
257 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
258
259 let checkpoint = SketchCheckpoint {
260 id: checkpoint_id,
261 source: SourceDelta {
262 text: source_from_ast(&self.program.ast),
263 },
264 program: self.program.clone(),
265 scene_graph: self.scene_graph.clone(),
266 exec_outcome,
267 point_freedom_cache: self.point_freedom_cache.clone(),
268 mock_memory: read_old_memory().await,
269 };
270
271 self.sketch_checkpoints.push_back(checkpoint);
272 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
273 self.sketch_checkpoints.pop_front();
274 }
275
276 Ok(checkpoint_id)
277 }
278
279 pub async fn restore_sketch_checkpoint(
280 &mut self,
281 checkpoint_id: SketchCheckpointId,
282 ) -> api::Result<RestoreSketchCheckpointOutcome> {
283 let checkpoint = self
284 .sketch_checkpoints
285 .iter()
286 .find(|checkpoint| checkpoint.id == checkpoint_id)
287 .cloned()
288 .ok_or_else(|| Error {
289 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
290 })?;
291
292 self.program = checkpoint.program;
293 self.scene_graph = checkpoint.scene_graph.clone();
294 self.point_freedom_cache = checkpoint.point_freedom_cache;
295
296 if let Some(mock_memory) = checkpoint.mock_memory {
297 write_old_memory(mock_memory).await;
298 } else {
299 clear_mem_cache().await;
300 }
301
302 Ok(RestoreSketchCheckpointOutcome {
303 source_delta: checkpoint.source,
304 scene_graph_delta: SceneGraphDelta {
305 new_graph: checkpoint.scene_graph,
306 new_objects: Vec::new(),
307 invalidates_ids: true,
308 exec_outcome: checkpoint.exec_outcome,
309 },
310 })
311 }
312
313 pub fn clear_sketch_checkpoints(&mut self) {
314 self.sketch_checkpoints.clear();
315 }
316}
317
318impl SketchApi for FrontendState {
319 async fn execute_mock(
320 &mut self,
321 ctx: &ExecutorContext,
322 _version: Version,
323 sketch: ObjectId,
324 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
325 let sketch_block_ref =
326 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
327
328 let mut truncated_program = self.program.clone();
329 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
330 .map_err(KclErrorWithOutputs::no_outputs)?;
331
332 let outcome = ctx
334 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
335 .await?;
336 let new_source = source_from_ast(&self.program.ast);
337 let src_delta = SourceDelta { text: new_source };
338 let outcome = self.update_state_after_exec(outcome, true);
340 let scene_graph_delta = SceneGraphDelta {
341 new_graph: self.scene_graph.clone(),
342 new_objects: Default::default(),
343 invalidates_ids: false,
344 exec_outcome: outcome,
345 };
346 Ok((src_delta, scene_graph_delta))
347 }
348
349 async fn new_sketch(
350 &mut self,
351 ctx: &ExecutorContext,
352 _project: ProjectId,
353 _file: FileId,
354 _version: Version,
355 args: SketchCtor,
356 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
357 let mut new_ast = self.program.ast.clone();
360 let mut plane_ast =
362 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
363 let mut defined_names = find_defined_names(&new_ast);
364 let is_face_of_expr = matches!(
365 &plane_ast,
366 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
367 );
368 if is_face_of_expr {
369 let face_name = next_free_name_with_padding("face", &defined_names)
370 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
371 let face_decl = ast::VariableDeclaration::new(
372 ast::VariableDeclarator::new(&face_name, plane_ast),
373 ast::ItemVisibility::Default,
374 ast::VariableKind::Const,
375 );
376 new_ast
377 .body
378 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
379 face_decl,
380 ))));
381 defined_names.insert(face_name.clone());
382 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
383 }
384 let sketch_ast = ast::SketchBlock {
385 arguments: vec![ast::LabeledArg {
386 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
387 arg: plane_ast,
388 }],
389 body: Default::default(),
390 is_being_edited: false,
391 non_code_meta: Default::default(),
392 digest: None,
393 };
394 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
397 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
398 let sketch_decl = ast::VariableDeclaration::new(
399 ast::VariableDeclarator::new(
400 &sketch_name,
401 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
402 ),
403 ast::ItemVisibility::Default,
404 ast::VariableKind::Const,
405 );
406 new_ast
407 .body
408 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
409 sketch_decl,
410 ))));
411 let new_source = source_from_ast(&new_ast);
413 let (new_program, errors) = Program::parse(&new_source)
415 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
416 if !errors.is_empty() {
417 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
418 "Error parsing KCL source after adding sketch: {errors:?}"
419 ))));
420 }
421 let Some(new_program) = new_program else {
422 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
423 "No AST produced after adding sketch".to_owned(),
424 )));
425 };
426
427 self.program = new_program.clone();
429
430 let outcome = ctx.run_with_caching(new_program.clone()).await?;
433 let freedom_analysis_ran = true;
434
435 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
436
437 let Some(sketch_id) = self
438 .scene_graph
439 .objects
440 .iter()
441 .filter_map(|object| match object.kind {
442 ObjectKind::Sketch(_) => Some(object.id),
443 _ => None,
444 })
445 .max_by_key(|id| id.0)
446 else {
447 return Err(KclErrorWithOutputs::from_error_outcome(
448 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
449 outcome,
450 ));
451 };
452 self.scene_graph.sketch_mode = Some(sketch_id);
454
455 let src_delta = SourceDelta { text: new_source };
456 let scene_graph_delta = SceneGraphDelta {
457 new_graph: self.scene_graph.clone(),
458 invalidates_ids: false,
459 new_objects: vec![sketch_id],
460 exec_outcome: outcome,
461 };
462 Ok((src_delta, scene_graph_delta, sketch_id))
463 }
464
465 async fn edit_sketch(
466 &mut self,
467 ctx: &ExecutorContext,
468 _project: ProjectId,
469 _file: FileId,
470 _version: Version,
471 sketch: ObjectId,
472 ) -> ExecResult<SceneGraphDelta> {
473 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
477 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
478 })?;
479 let ObjectKind::Sketch(_) = &sketch_object.kind else {
480 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
481 "Object is not a sketch, it is {}",
482 sketch_object.kind.human_friendly_kind_with_article()
483 ))));
484 };
485 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
486
487 self.scene_graph.sketch_mode = Some(sketch);
489
490 let mut truncated_program = self.program.clone();
492 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
493 .map_err(KclErrorWithOutputs::no_outputs)?;
494
495 let outcome = ctx
498 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
499 .await?;
500
501 let outcome = self.update_state_after_exec(outcome, true);
503 let scene_graph_delta = SceneGraphDelta {
504 new_graph: self.scene_graph.clone(),
505 invalidates_ids: false,
506 new_objects: Vec::new(),
507 exec_outcome: outcome,
508 };
509 Ok(scene_graph_delta)
510 }
511
512 async fn exit_sketch(
513 &mut self,
514 ctx: &ExecutorContext,
515 _version: Version,
516 sketch: ObjectId,
517 ) -> ExecResult<SceneGraph> {
518 #[cfg(not(target_arch = "wasm32"))]
520 let _ = sketch;
521 #[cfg(target_arch = "wasm32")]
522 if self.scene_graph.sketch_mode != Some(sketch) {
523 web_sys::console::warn_1(
524 &format!(
525 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
526 &self.scene_graph.sketch_mode
527 )
528 .into(),
529 );
530 }
531 self.scene_graph.sketch_mode = None;
532
533 let outcome = ctx.run_with_caching(self.program.clone()).await?;
535
536 self.update_state_after_exec(outcome, false);
538
539 Ok(self.scene_graph.clone())
540 }
541
542 async fn delete_sketch(
543 &mut self,
544 ctx: &ExecutorContext,
545 _version: Version,
546 sketch: ObjectId,
547 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
548 let mut new_ast = self.program.ast.clone();
551
552 let sketch_id = sketch;
554 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
555 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
556 })?;
557 let ObjectKind::Sketch(_) = &sketch_object.kind else {
558 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
559 "Object is not a sketch, it is {}",
560 sketch_object.kind.human_friendly_kind_with_article(),
561 ))));
562 };
563
564 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
566 .map_err(KclErrorWithOutputs::no_outputs)?;
567
568 self.execute_after_delete_sketch(ctx, &mut new_ast).await
569 }
570
571 async fn add_segment(
572 &mut self,
573 ctx: &ExecutorContext,
574 _version: Version,
575 sketch: ObjectId,
576 segment: SegmentCtor,
577 _label: Option<String>,
578 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
579 match segment {
581 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
582 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
583 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
584 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
585 }
586 }
587
588 async fn edit_segments(
589 &mut self,
590 ctx: &ExecutorContext,
591 _version: Version,
592 sketch: ObjectId,
593 segments: Vec<ExistingSegmentCtor>,
594 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
595 let sketch_block_ref =
597 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
598
599 let mut new_ast = self.program.ast.clone();
600 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
601
602 for segment in &segments {
605 segment_ids_edited.insert(segment.id);
606 }
607
608 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
623
624 for segment in segments {
625 let segment_id = segment.id;
626 match segment.ctor {
627 SegmentCtor::Point(ctor) => {
628 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
630 && let ObjectKind::Segment { segment } = &segment_object.kind
631 && let Segment::Point(point) = segment
632 && let Some(owner_id) = point.owner
633 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
634 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
635 {
636 match owner_segment {
637 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
638 if let Some(existing) = final_edits.get_mut(&owner_id) {
639 let SegmentCtor::Line(line_ctor) = existing else {
640 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
641 "Internal: Expected line ctor for owner, but found {}",
642 existing.human_friendly_kind_with_article()
643 ))));
644 };
645 if line.start == segment_id {
647 line_ctor.start = ctor.position;
648 } else {
649 line_ctor.end = ctor.position;
650 }
651 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
652 let mut line_ctor = line_ctor.clone();
654 if line.start == segment_id {
655 line_ctor.start = ctor.position;
656 } else {
657 line_ctor.end = ctor.position;
658 }
659 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
660 } else {
661 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
663 "Internal: Line does not have line ctor, but found {}",
664 line.ctor.human_friendly_kind_with_article()
665 ))));
666 }
667 continue;
668 }
669 Segment::Arc(arc)
670 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
671 {
672 if let Some(existing) = final_edits.get_mut(&owner_id) {
673 let SegmentCtor::Arc(arc_ctor) = existing else {
674 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
675 "Internal: Expected arc ctor for owner, but found {}",
676 existing.human_friendly_kind_with_article()
677 ))));
678 };
679 if arc.start == segment_id {
680 arc_ctor.start = ctor.position;
681 } else if arc.end == segment_id {
682 arc_ctor.end = ctor.position;
683 } else {
684 arc_ctor.center = ctor.position;
685 }
686 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
687 let mut arc_ctor = arc_ctor.clone();
688 if arc.start == segment_id {
689 arc_ctor.start = ctor.position;
690 } else if arc.end == segment_id {
691 arc_ctor.end = ctor.position;
692 } else {
693 arc_ctor.center = ctor.position;
694 }
695 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
696 } else {
697 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
698 "Internal: Arc does not have arc ctor, but found {}",
699 arc.ctor.human_friendly_kind_with_article()
700 ))));
701 }
702 continue;
703 }
704 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
705 if let Some(existing) = final_edits.get_mut(&owner_id) {
706 let SegmentCtor::Circle(circle_ctor) = existing else {
707 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
708 "Internal: Expected circle ctor for owner, but found {}",
709 existing.human_friendly_kind_with_article()
710 ))));
711 };
712 if circle.start == segment_id {
713 circle_ctor.start = ctor.position;
714 } else {
715 circle_ctor.center = ctor.position;
716 }
717 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
718 let mut circle_ctor = circle_ctor.clone();
719 if circle.start == segment_id {
720 circle_ctor.start = ctor.position;
721 } else {
722 circle_ctor.center = ctor.position;
723 }
724 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
725 } else {
726 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
727 "Internal: Circle does not have circle ctor, but found {}",
728 circle.ctor.human_friendly_kind_with_article()
729 ))));
730 }
731 continue;
732 }
733 _ => {}
734 }
735 }
736
737 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
739 }
740 SegmentCtor::Line(ctor) => {
741 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
742 }
743 SegmentCtor::Arc(ctor) => {
744 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
745 }
746 SegmentCtor::Circle(ctor) => {
747 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
748 }
749 }
750 }
751
752 for (segment_id, ctor) in final_edits {
753 match ctor {
754 SegmentCtor::Point(ctor) => self
755 .edit_point(&mut new_ast, sketch, segment_id, ctor)
756 .map_err(KclErrorWithOutputs::no_outputs)?,
757 SegmentCtor::Line(ctor) => self
758 .edit_line(&mut new_ast, sketch, segment_id, ctor)
759 .map_err(KclErrorWithOutputs::no_outputs)?,
760 SegmentCtor::Arc(ctor) => self
761 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
762 .map_err(KclErrorWithOutputs::no_outputs)?,
763 SegmentCtor::Circle(ctor) => self
764 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
765 .map_err(KclErrorWithOutputs::no_outputs)?,
766 }
767 }
768 self.execute_after_edit(
769 ctx,
770 sketch,
771 sketch_block_ref,
772 segment_ids_edited,
773 EditDeleteKind::Edit,
774 &mut new_ast,
775 )
776 .await
777 }
778
779 async fn delete_objects(
780 &mut self,
781 ctx: &ExecutorContext,
782 _version: Version,
783 sketch: ObjectId,
784 constraint_ids: Vec<ObjectId>,
785 segment_ids: Vec<ObjectId>,
786 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
787 let sketch_block_ref =
789 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
790
791 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
793 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
794
795 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
798
799 for segment_id in segment_ids_set.iter().copied() {
800 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
801 && let ObjectKind::Segment { segment } = &segment_object.kind
802 && let Segment::Point(point) = segment
803 && let Some(owner_id) = point.owner
804 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
805 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
806 && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
807 {
808 resolved_segment_ids_to_delete.insert(owner_id);
810 } else {
811 resolved_segment_ids_to_delete.insert(segment_id);
813 }
814 }
815 let referenced_constraint_ids = self
816 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
817 .map_err(KclErrorWithOutputs::no_outputs)?;
818
819 let mut new_ast = self.program.ast.clone();
820
821 for constraint_id in referenced_constraint_ids {
822 if constraint_ids_set.contains(&constraint_id) {
823 continue;
824 }
825
826 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
827 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
828 })?;
829 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
830 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
831 "Object is not a constraint, it is {}",
832 constraint_object.kind.human_friendly_kind_with_article()
833 ))));
834 };
835
836 match constraint {
837 Constraint::Coincident(coincident) => {
838 let remaining_segments =
839 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
840
841 if remaining_segments.len() >= 2 {
843 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
844 .map_err(KclErrorWithOutputs::no_outputs)?;
845 } else {
846 constraint_ids_set.insert(constraint_id);
847 }
848 }
849 Constraint::EqualRadius(equal_radius) => {
850 let remaining_input = equal_radius
851 .input
852 .iter()
853 .copied()
854 .filter(|segment_id| {
855 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
856 })
857 .collect::<Vec<_>>();
858
859 if remaining_input.len() >= 2 {
860 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
861 .map_err(KclErrorWithOutputs::no_outputs)?;
862 } else {
863 constraint_ids_set.insert(constraint_id);
864 }
865 }
866 Constraint::LinesEqualLength(lines_equal_length) => {
867 let remaining_lines = lines_equal_length
868 .lines
869 .iter()
870 .copied()
871 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
872 .collect::<Vec<_>>();
873
874 if remaining_lines.len() >= 2 {
876 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
877 .map_err(KclErrorWithOutputs::no_outputs)?;
878 } else {
879 constraint_ids_set.insert(constraint_id);
880 }
881 }
882 Constraint::Parallel(parallel) => {
883 let remaining_lines = parallel
884 .lines
885 .iter()
886 .copied()
887 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
888 .collect::<Vec<_>>();
889
890 if remaining_lines.len() >= 2 {
891 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
892 .map_err(KclErrorWithOutputs::no_outputs)?;
893 } else {
894 constraint_ids_set.insert(constraint_id);
895 }
896 }
897 Constraint::Horizontal(Horizontal::Points { points }) => {
898 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
899
900 if remaining_points.len() >= 2 {
901 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
902 .map_err(KclErrorWithOutputs::no_outputs)?;
903 } else {
904 constraint_ids_set.insert(constraint_id);
905 }
906 }
907 Constraint::Vertical(Vertical::Points { points }) => {
908 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
909
910 if remaining_points.len() >= 2 {
911 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
912 .map_err(KclErrorWithOutputs::no_outputs)?;
913 } else {
914 constraint_ids_set.insert(constraint_id);
915 }
916 }
917 Constraint::Fixed(fixed) => {
918 if fixed.points.iter().any(|fixed_point| {
919 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
920 }) {
921 constraint_ids_set.insert(constraint_id);
922 }
923 }
924 _ => {
925 constraint_ids_set.insert(constraint_id);
927 }
928 }
929 }
930
931 for constraint_id in constraint_ids_set {
932 self.delete_constraint(&mut new_ast, sketch, constraint_id)
933 .map_err(KclErrorWithOutputs::no_outputs)?;
934 }
935 for segment_id in resolved_segment_ids_to_delete {
936 self.delete_segment(&mut new_ast, sketch, segment_id)
937 .map_err(KclErrorWithOutputs::no_outputs)?;
938 }
939
940 self.execute_after_edit(
941 ctx,
942 sketch,
943 sketch_block_ref,
944 Default::default(),
945 EditDeleteKind::DeleteNonSketch,
946 &mut new_ast,
947 )
948 .await
949 }
950
951 async fn add_constraint(
952 &mut self,
953 ctx: &ExecutorContext,
954 _version: Version,
955 sketch: ObjectId,
956 constraint: Constraint,
957 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
958 let original_program = self.program.clone();
962 let original_scene_graph = self.scene_graph.clone();
963
964 let mut new_ast = self.program.ast.clone();
965 let sketch_block_ref = match constraint {
966 Constraint::Coincident(coincident) => self
967 .add_coincident(sketch, coincident, &mut new_ast)
968 .await
969 .map_err(KclErrorWithOutputs::no_outputs)?,
970 Constraint::Distance(distance) => self
971 .add_distance(sketch, distance, &mut new_ast)
972 .await
973 .map_err(KclErrorWithOutputs::no_outputs)?,
974 Constraint::EqualRadius(equal_radius) => self
975 .add_equal_radius(sketch, equal_radius, &mut new_ast)
976 .await
977 .map_err(KclErrorWithOutputs::no_outputs)?,
978 Constraint::Fixed(fixed) => self
979 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
980 .await
981 .map_err(KclErrorWithOutputs::no_outputs)?,
982 Constraint::HorizontalDistance(distance) => self
983 .add_horizontal_distance(sketch, distance, &mut new_ast)
984 .await
985 .map_err(KclErrorWithOutputs::no_outputs)?,
986 Constraint::VerticalDistance(distance) => self
987 .add_vertical_distance(sketch, distance, &mut new_ast)
988 .await
989 .map_err(KclErrorWithOutputs::no_outputs)?,
990 Constraint::Horizontal(horizontal) => self
991 .add_horizontal(sketch, horizontal, &mut new_ast)
992 .await
993 .map_err(KclErrorWithOutputs::no_outputs)?,
994 Constraint::LinesEqualLength(lines_equal_length) => self
995 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
996 .await
997 .map_err(KclErrorWithOutputs::no_outputs)?,
998 Constraint::Midpoint(midpoint) => self
999 .add_midpoint(sketch, midpoint, &mut new_ast)
1000 .await
1001 .map_err(KclErrorWithOutputs::no_outputs)?,
1002 Constraint::Parallel(parallel) => self
1003 .add_parallel(sketch, parallel, &mut new_ast)
1004 .await
1005 .map_err(KclErrorWithOutputs::no_outputs)?,
1006 Constraint::Perpendicular(perpendicular) => self
1007 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1008 .await
1009 .map_err(KclErrorWithOutputs::no_outputs)?,
1010 Constraint::Radius(radius) => self
1011 .add_radius(sketch, radius, &mut new_ast)
1012 .await
1013 .map_err(KclErrorWithOutputs::no_outputs)?,
1014 Constraint::Diameter(diameter) => self
1015 .add_diameter(sketch, diameter, &mut new_ast)
1016 .await
1017 .map_err(KclErrorWithOutputs::no_outputs)?,
1018 Constraint::Symmetric(symmetric) => self
1019 .add_symmetric(sketch, symmetric, &mut new_ast)
1020 .await
1021 .map_err(KclErrorWithOutputs::no_outputs)?,
1022 Constraint::Vertical(vertical) => self
1023 .add_vertical(sketch, vertical, &mut new_ast)
1024 .await
1025 .map_err(KclErrorWithOutputs::no_outputs)?,
1026 Constraint::Angle(lines_at_angle) => self
1027 .add_angle(sketch, lines_at_angle, &mut new_ast)
1028 .await
1029 .map_err(KclErrorWithOutputs::no_outputs)?,
1030 Constraint::Tangent(tangent) => self
1031 .add_tangent(sketch, tangent, &mut new_ast)
1032 .await
1033 .map_err(KclErrorWithOutputs::no_outputs)?,
1034 };
1035
1036 let result = self
1037 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1038 .await;
1039
1040 if result.is_err() {
1042 self.program = original_program;
1043 self.scene_graph = original_scene_graph;
1044 }
1045
1046 result
1047 }
1048
1049 async fn chain_segment(
1050 &mut self,
1051 ctx: &ExecutorContext,
1052 version: Version,
1053 sketch: ObjectId,
1054 previous_segment_end_point_id: ObjectId,
1055 segment: SegmentCtor,
1056 _label: Option<String>,
1057 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1058 let SegmentCtor::Line(line_ctor) = segment else {
1062 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1063 "chain_segment currently only supports Line segments, got {}",
1064 segment.human_friendly_kind_with_article(),
1065 ))));
1066 };
1067
1068 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1070
1071 let new_line_id = first_scene_delta
1074 .new_objects
1075 .iter()
1076 .find(|&obj_id| {
1077 let obj = self.scene_graph.objects.get(obj_id.0);
1078 if let Some(obj) = obj {
1079 matches!(
1080 &obj.kind,
1081 ObjectKind::Segment {
1082 segment: Segment::Line(_)
1083 }
1084 )
1085 } else {
1086 false
1087 }
1088 })
1089 .ok_or_else(|| {
1090 KclErrorWithOutputs::no_outputs(KclError::refactor(
1091 "Failed to find new line segment in scene graph".to_string(),
1092 ))
1093 })?;
1094
1095 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1096 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1097 "New line object not found: {new_line_id:?}"
1098 )))
1099 })?;
1100
1101 let ObjectKind::Segment {
1102 segment: new_line_segment,
1103 } = &new_line_obj.kind
1104 else {
1105 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1106 "Object is not a segment: {new_line_obj:?}"
1107 ))));
1108 };
1109
1110 let Segment::Line(new_line) = new_line_segment else {
1111 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1112 "Segment is not a line: {new_line_segment:?}"
1113 ))));
1114 };
1115
1116 let new_line_start_point_id = new_line.start;
1117
1118 let coincident = Coincident {
1120 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1121 };
1122
1123 let (final_src_delta, final_scene_delta) = self
1124 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1125 .await?;
1126
1127 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1130 combined_new_objects.extend(final_scene_delta.new_objects);
1131
1132 let scene_graph_delta = SceneGraphDelta {
1133 new_graph: self.scene_graph.clone(),
1134 invalidates_ids: false,
1135 new_objects: combined_new_objects,
1136 exec_outcome: final_scene_delta.exec_outcome,
1137 };
1138
1139 Ok((final_src_delta, scene_graph_delta))
1140 }
1141
1142 async fn edit_constraint(
1143 &mut self,
1144 ctx: &ExecutorContext,
1145 _version: Version,
1146 sketch: ObjectId,
1147 constraint_id: ObjectId,
1148 value_expression: String,
1149 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1150 let sketch_block_ref =
1152 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1153
1154 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1155 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1156 })?;
1157 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1158 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1159 "Object is not a constraint: {constraint_id:?}"
1160 ))));
1161 }
1162
1163 let mut new_ast = self.program.ast.clone();
1164
1165 let (parsed, errors) = Program::parse(&value_expression)
1167 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1168 if !errors.is_empty() {
1169 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1170 "Error parsing value expression: {errors:?}"
1171 ))));
1172 }
1173 let mut parsed = parsed.ok_or_else(|| {
1174 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1175 })?;
1176 if parsed.ast.body.is_empty() {
1177 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1178 "Empty value expression".to_string(),
1179 )));
1180 }
1181 let first = parsed.ast.body.remove(0);
1182 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1183 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1184 "Value expression must be a simple expression".to_string(),
1185 )));
1186 };
1187
1188 let new_value: ast::BinaryPart = expr_stmt
1189 .inner
1190 .expression
1191 .try_into()
1192 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1193
1194 self.mutate_ast(
1195 &mut new_ast,
1196 constraint_id,
1197 AstMutateCommand::EditConstraintValue { value: new_value },
1198 )
1199 .map_err(KclErrorWithOutputs::no_outputs)?;
1200
1201 self.execute_after_edit(
1202 ctx,
1203 sketch,
1204 sketch_block_ref,
1205 Default::default(),
1206 EditDeleteKind::Edit,
1207 &mut new_ast,
1208 )
1209 .await
1210 }
1211
1212 async fn edit_distance_constraint_label_position(
1213 &mut self,
1214 ctx: &ExecutorContext,
1215 _version: Version,
1216 sketch: ObjectId,
1217 constraint_id: ObjectId,
1218 label_position: Point2d<Number>,
1219 anchor_segment_ids: Vec<ObjectId>,
1220 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1221 let sketch_block_ref =
1223 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1224
1225 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1226 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1227 })?;
1228 if !matches!(
1229 &object.kind,
1230 ObjectKind::Constraint {
1231 constraint: Constraint::Distance(_)
1232 | Constraint::HorizontalDistance(_)
1233 | Constraint::VerticalDistance(_)
1234 | Constraint::Radius(_)
1235 | Constraint::Diameter(_),
1236 }
1237 ) {
1238 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1239 "Object does not support labelPosition: {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 additional_edited_segment_ids: Vec<ObjectId>,
1442 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1443 let sketch_block_ref =
1444 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1445
1446 let mut new_ast = self.program.ast.clone();
1447 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1448
1449 for segment in edit_segments {
1451 segment_ids_edited.insert(segment.id);
1452 match segment.ctor {
1453 SegmentCtor::Point(ctor) => self
1454 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1455 .map_err(KclErrorWithOutputs::no_outputs)?,
1456 SegmentCtor::Line(ctor) => self
1457 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1458 .map_err(KclErrorWithOutputs::no_outputs)?,
1459 SegmentCtor::Arc(ctor) => self
1460 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1461 .map_err(KclErrorWithOutputs::no_outputs)?,
1462 SegmentCtor::Circle(ctor) => self
1463 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1464 .map_err(KclErrorWithOutputs::no_outputs)?,
1465 }
1466 }
1467
1468 segment_ids_edited.extend(additional_edited_segment_ids);
1469
1470 for constraint in add_constraints {
1472 match constraint {
1473 Constraint::Coincident(coincident) => {
1474 self.add_coincident(sketch, coincident, &mut new_ast)
1475 .await
1476 .map_err(KclErrorWithOutputs::no_outputs)?;
1477 }
1478 other => {
1479 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1480 "unsupported constraint in tail cut batch: {other:?}"
1481 ))));
1482 }
1483 }
1484 }
1485
1486 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1488
1489 let has_constraint_deletions = !constraint_ids_set.is_empty();
1490 for constraint_id in constraint_ids_set {
1491 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1492 .map_err(KclErrorWithOutputs::no_outputs)?;
1493 }
1494
1495 let (source_delta, mut scene_graph_delta) = self
1499 .execute_after_edit(
1500 ctx,
1501 sketch,
1502 sketch_block_ref,
1503 segment_ids_edited,
1504 EditDeleteKind::Edit,
1505 &mut new_ast,
1506 )
1507 .await?;
1508
1509 if has_constraint_deletions {
1512 scene_graph_delta.invalidates_ids = true;
1513 }
1514
1515 Ok((source_delta, scene_graph_delta))
1516 }
1517}
1518
1519impl FrontendState {
1520 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1521 self.program = program.clone();
1522
1523 self.point_freedom_cache.clear();
1534 match ctx.run_with_caching(program).await {
1535 Ok(outcome) => {
1536 let outcome = self.update_state_after_exec(outcome, true);
1537 let checkpoint_id = self
1538 .create_sketch_checkpoint(outcome.clone())
1539 .await
1540 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1541 Ok(SetProgramOutcome::Success {
1542 scene_graph: Box::new(self.scene_graph.clone()),
1543 exec_outcome: Box::new(outcome),
1544 checkpoint_id: Some(checkpoint_id),
1545 })
1546 }
1547 Err(mut err) => {
1548 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1551 self.update_state_after_exec(outcome, true);
1552 err.scene_graph = Some(self.scene_graph.clone());
1553 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1554 }
1555 }
1556 }
1557
1558 pub async fn engine_execute(
1561 &mut self,
1562 ctx: &ExecutorContext,
1563 program: Program,
1564 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1565 self.program = program.clone();
1566
1567 self.point_freedom_cache.clear();
1571 match ctx.run_with_caching(program).await {
1572 Ok(outcome) => {
1573 let outcome = self.update_state_after_exec(outcome, true);
1574 Ok(SceneGraphDelta {
1575 new_graph: self.scene_graph.clone(),
1576 exec_outcome: outcome,
1577 new_objects: Default::default(),
1579 invalidates_ids: Default::default(),
1581 })
1582 }
1583 Err(mut err) => {
1584 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1586 self.update_state_after_exec(outcome, true);
1587 err.scene_graph = Some(self.scene_graph.clone());
1588 Err(err)
1589 }
1590 }
1591 }
1592
1593 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1594 if matches!(err.error, KclError::EngineHangup { .. }) {
1595 return Err(err);
1599 }
1600
1601 let KclErrorWithOutputs {
1602 error,
1603 mut non_fatal,
1604 variables,
1605 operations,
1606 artifact_graph,
1607 scene_objects,
1608 source_range_to_object,
1609 var_solutions,
1610 filenames,
1611 default_planes,
1612 ..
1613 } = err;
1614
1615 if let Some(source_range) = error.source_ranges().first() {
1616 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1617 } else {
1618 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1619 }
1620
1621 Ok(ExecOutcome {
1622 variables,
1623 filenames,
1624 operations,
1625 artifact_graph,
1626 scene_objects,
1627 source_range_to_object,
1628 var_solutions,
1629 issues: non_fatal,
1630 default_planes,
1631 })
1632 }
1633
1634 async fn add_point(
1635 &mut self,
1636 ctx: &ExecutorContext,
1637 sketch: ObjectId,
1638 ctor: PointCtor,
1639 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1640 let at_ast = to_ast_point2d(&ctor.position)
1642 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1643 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1644 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1645 unlabeled: None,
1646 arguments: vec![ast::LabeledArg {
1647 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1648 arg: at_ast,
1649 }],
1650 digest: None,
1651 non_code_meta: Default::default(),
1652 })));
1653
1654 let sketch_id = sketch;
1656 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1657 #[cfg(target_arch = "wasm32")]
1658 web_sys::console::error_1(
1659 &format!(
1660 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1661 &self.scene_graph.objects
1662 )
1663 .into(),
1664 );
1665 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1666 })?;
1667 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1668 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1669 "Object is not a sketch, it is {}",
1670 sketch_object.kind.human_friendly_kind_with_article(),
1671 ))));
1672 };
1673 let mut new_ast = self.program.ast.clone();
1675 let (sketch_block_ref, _) = self
1676 .mutate_ast(
1677 &mut new_ast,
1678 sketch_id,
1679 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1680 )
1681 .map_err(KclErrorWithOutputs::no_outputs)?;
1682 let new_source = source_from_ast(&new_ast);
1684 let (new_program, errors) = Program::parse(&new_source)
1686 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1687 if !errors.is_empty() {
1688 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1689 "Error parsing KCL source after adding point: {errors:?}"
1690 ))));
1691 }
1692 let Some(new_program) = new_program else {
1693 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1694 "No AST produced after adding point".to_string(),
1695 )));
1696 };
1697
1698 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1699 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1700 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1701 )))
1702 })?;
1703
1704 self.program = new_program.clone();
1706
1707 let mut truncated_program = new_program;
1709 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1710 .map_err(KclErrorWithOutputs::no_outputs)?;
1711
1712 let outcome = ctx
1714 .run_mock(
1715 &truncated_program,
1716 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1717 )
1718 .await?;
1719
1720 let new_object_ids = {
1721 let make_err =
1722 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1723 let segment_id = outcome
1724 .source_range_to_object
1725 .get(&point_node_ref.range)
1726 .copied()
1727 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1728 let segment_object = outcome
1729 .scene_objects
1730 .get(segment_id.0)
1731 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1732 let ObjectKind::Segment { segment } = &segment_object.kind else {
1733 return Err(make_err(format!(
1734 "Object is not a segment, it is {}",
1735 segment_object.kind.human_friendly_kind_with_article()
1736 )));
1737 };
1738 let Segment::Point(_) = segment else {
1739 return Err(make_err(format!(
1740 "Segment is not a point, it is {}",
1741 segment.human_friendly_kind_with_article()
1742 )));
1743 };
1744 vec![segment_id]
1745 };
1746 let src_delta = SourceDelta { text: new_source };
1747 let outcome = self.update_state_after_exec(outcome, false);
1749 let scene_graph_delta = SceneGraphDelta {
1750 new_graph: self.scene_graph.clone(),
1751 invalidates_ids: false,
1752 new_objects: new_object_ids,
1753 exec_outcome: outcome,
1754 };
1755 Ok((src_delta, scene_graph_delta))
1756 }
1757
1758 async fn add_line(
1759 &mut self,
1760 ctx: &ExecutorContext,
1761 sketch: ObjectId,
1762 ctor: LineCtor,
1763 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1764 let start_ast = to_ast_point2d(&ctor.start)
1766 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1767 let end_ast = to_ast_point2d(&ctor.end)
1768 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1769 let mut arguments = vec![
1770 ast::LabeledArg {
1771 label: Some(ast::Identifier::new(LINE_START_PARAM)),
1772 arg: start_ast,
1773 },
1774 ast::LabeledArg {
1775 label: Some(ast::Identifier::new(LINE_END_PARAM)),
1776 arg: end_ast,
1777 },
1778 ];
1779 if ctor.construction == Some(true) {
1781 arguments.push(ast::LabeledArg {
1782 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1783 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1784 value: ast::LiteralValue::Bool(true),
1785 raw: "true".to_string(),
1786 digest: None,
1787 }))),
1788 });
1789 }
1790 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1791 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1792 unlabeled: None,
1793 arguments,
1794 digest: None,
1795 non_code_meta: Default::default(),
1796 })));
1797
1798 let sketch_id = sketch;
1800 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1801 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1802 })?;
1803 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1804 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1805 "Object is not a sketch, it is {}",
1806 sketch_object.kind.human_friendly_kind_with_article(),
1807 ))));
1808 };
1809 let mut new_ast = self.program.ast.clone();
1811 let (sketch_block_ref, _) = self
1812 .mutate_ast(
1813 &mut new_ast,
1814 sketch_id,
1815 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1816 )
1817 .map_err(KclErrorWithOutputs::no_outputs)?;
1818 let new_source = source_from_ast(&new_ast);
1820 let (new_program, errors) = Program::parse(&new_source)
1822 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1823 if !errors.is_empty() {
1824 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1825 "Error parsing KCL source after adding line: {errors:?}"
1826 ))));
1827 }
1828 let Some(new_program) = new_program else {
1829 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1830 "No AST produced after adding line".to_string(),
1831 )));
1832 };
1833
1834 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1835 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1836 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1837 )))
1838 })?;
1839
1840 self.program = new_program.clone();
1842
1843 let mut truncated_program = new_program;
1845 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1846 .map_err(KclErrorWithOutputs::no_outputs)?;
1847
1848 let outcome = ctx
1850 .run_mock(
1851 &truncated_program,
1852 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1853 )
1854 .await?;
1855
1856 let new_object_ids = {
1857 let make_err =
1858 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1859 let segment_id = outcome
1860 .source_range_to_object
1861 .get(&line_node_ref.range)
1862 .copied()
1863 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1864 let segment_object = outcome
1865 .scene_object_by_id(segment_id)
1866 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1867 let ObjectKind::Segment { segment } = &segment_object.kind else {
1868 return Err(make_err(format!(
1869 "Object is not a segment, it is {}",
1870 segment_object.kind.human_friendly_kind_with_article()
1871 )));
1872 };
1873 let Segment::Line(line) = segment else {
1874 return Err(make_err(format!(
1875 "Segment is not a line, it is {}",
1876 segment.human_friendly_kind_with_article()
1877 )));
1878 };
1879 vec![line.start, line.end, segment_id]
1880 };
1881 let src_delta = SourceDelta { text: new_source };
1882 let outcome = self.update_state_after_exec(outcome, false);
1884 let scene_graph_delta = SceneGraphDelta {
1885 new_graph: self.scene_graph.clone(),
1886 invalidates_ids: false,
1887 new_objects: new_object_ids,
1888 exec_outcome: outcome,
1889 };
1890 Ok((src_delta, scene_graph_delta))
1891 }
1892
1893 async fn add_arc(
1894 &mut self,
1895 ctx: &ExecutorContext,
1896 sketch: ObjectId,
1897 ctor: ArcCtor,
1898 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1899 let start_ast = to_ast_point2d(&ctor.start)
1901 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1902 let end_ast = to_ast_point2d(&ctor.end)
1903 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1904 let center_ast = to_ast_point2d(&ctor.center)
1905 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1906 let mut arguments = vec![
1907 ast::LabeledArg {
1908 label: Some(ast::Identifier::new(ARC_START_PARAM)),
1909 arg: start_ast,
1910 },
1911 ast::LabeledArg {
1912 label: Some(ast::Identifier::new(ARC_END_PARAM)),
1913 arg: end_ast,
1914 },
1915 ast::LabeledArg {
1916 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1917 arg: center_ast,
1918 },
1919 ];
1920 if ctor.construction == Some(true) {
1922 arguments.push(ast::LabeledArg {
1923 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1924 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1925 value: ast::LiteralValue::Bool(true),
1926 raw: "true".to_string(),
1927 digest: None,
1928 }))),
1929 });
1930 }
1931 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1932 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1933 unlabeled: None,
1934 arguments,
1935 digest: None,
1936 non_code_meta: Default::default(),
1937 })));
1938
1939 let sketch_id = sketch;
1941 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1942 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1943 })?;
1944 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1945 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1946 "Object is not a sketch, it is {}",
1947 sketch_object.kind.human_friendly_kind_with_article(),
1948 ))));
1949 };
1950 let mut new_ast = self.program.ast.clone();
1952 let (sketch_block_ref, _) = self
1953 .mutate_ast(
1954 &mut new_ast,
1955 sketch_id,
1956 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1957 )
1958 .map_err(KclErrorWithOutputs::no_outputs)?;
1959 let new_source = source_from_ast(&new_ast);
1961 let (new_program, errors) = Program::parse(&new_source)
1963 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1964 if !errors.is_empty() {
1965 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1966 "Error parsing KCL source after adding arc: {errors:?}"
1967 ))));
1968 }
1969 let Some(new_program) = new_program else {
1970 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1971 "No AST produced after adding arc".to_string(),
1972 )));
1973 };
1974
1975 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1976 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1977 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
1978 )))
1979 })?;
1980
1981 self.program = new_program.clone();
1983
1984 let mut truncated_program = new_program;
1986 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1987 .map_err(KclErrorWithOutputs::no_outputs)?;
1988
1989 let outcome = ctx
1991 .run_mock(
1992 &truncated_program,
1993 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1994 )
1995 .await?;
1996
1997 let new_object_ids = {
1998 let make_err =
1999 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2000 let segment_id = outcome
2001 .source_range_to_object
2002 .get(&arc_node_ref.range)
2003 .copied()
2004 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2005 let segment_object = outcome
2006 .scene_objects
2007 .get(segment_id.0)
2008 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2009 let ObjectKind::Segment { segment } = &segment_object.kind else {
2010 return Err(make_err(format!(
2011 "Object is not a segment, it is {}",
2012 segment_object.kind.human_friendly_kind_with_article()
2013 )));
2014 };
2015 let Segment::Arc(arc) = segment else {
2016 return Err(make_err(format!(
2017 "Segment is not an arc, it is {}",
2018 segment.human_friendly_kind_with_article()
2019 )));
2020 };
2021 vec![arc.start, arc.end, arc.center, segment_id]
2022 };
2023 let src_delta = SourceDelta { text: new_source };
2024 let outcome = self.update_state_after_exec(outcome, false);
2026 let scene_graph_delta = SceneGraphDelta {
2027 new_graph: self.scene_graph.clone(),
2028 invalidates_ids: false,
2029 new_objects: new_object_ids,
2030 exec_outcome: outcome,
2031 };
2032 Ok((src_delta, scene_graph_delta))
2033 }
2034
2035 async fn add_circle(
2036 &mut self,
2037 ctx: &ExecutorContext,
2038 sketch: ObjectId,
2039 ctor: CircleCtor,
2040 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2041 let start_ast = to_ast_point2d(&ctor.start)
2043 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2044 let center_ast = to_ast_point2d(&ctor.center)
2045 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2046 let mut arguments = vec![
2047 ast::LabeledArg {
2048 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2049 arg: start_ast,
2050 },
2051 ast::LabeledArg {
2052 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2053 arg: center_ast,
2054 },
2055 ];
2056 if ctor.construction == Some(true) {
2058 arguments.push(ast::LabeledArg {
2059 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2060 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2061 value: ast::LiteralValue::Bool(true),
2062 raw: "true".to_string(),
2063 digest: None,
2064 }))),
2065 });
2066 }
2067 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2068 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2069 unlabeled: None,
2070 arguments,
2071 digest: None,
2072 non_code_meta: Default::default(),
2073 })));
2074
2075 let sketch_id = sketch;
2077 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2078 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2079 })?;
2080 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2081 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2082 "Object is not a sketch, it is {}",
2083 sketch_object.kind.human_friendly_kind_with_article(),
2084 ))));
2085 };
2086 let mut new_ast = self.program.ast.clone();
2088 let (sketch_block_ref, _) = self
2089 .mutate_ast(
2090 &mut new_ast,
2091 sketch_id,
2092 AstMutateCommand::AddSketchBlockVarDecl {
2093 prefix: CIRCLE_VARIABLE.to_owned(),
2094 expr: circle_ast,
2095 },
2096 )
2097 .map_err(KclErrorWithOutputs::no_outputs)?;
2098 let new_source = source_from_ast(&new_ast);
2100 let (new_program, errors) = Program::parse(&new_source)
2102 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2103 if !errors.is_empty() {
2104 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2105 "Error parsing KCL source after adding circle: {errors:?}"
2106 ))));
2107 }
2108 let Some(new_program) = new_program else {
2109 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2110 "No AST produced after adding circle".to_string(),
2111 )));
2112 };
2113
2114 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2115 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2116 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2117 )))
2118 })?;
2119
2120 self.program = new_program.clone();
2122
2123 let mut truncated_program = new_program;
2125 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2126 .map_err(KclErrorWithOutputs::no_outputs)?;
2127
2128 let outcome = ctx
2130 .run_mock(
2131 &truncated_program,
2132 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2133 )
2134 .await?;
2135
2136 let new_object_ids = {
2137 let make_err =
2138 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2139 let segment_id = outcome
2140 .source_range_to_object
2141 .get(&circle_node_ref.range)
2142 .copied()
2143 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2144 let segment_object = outcome
2145 .scene_objects
2146 .get(segment_id.0)
2147 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2148 let ObjectKind::Segment { segment } = &segment_object.kind else {
2149 return Err(make_err(format!(
2150 "Object is not a segment, it is {}",
2151 segment_object.kind.human_friendly_kind_with_article()
2152 )));
2153 };
2154 let Segment::Circle(circle) = segment else {
2155 return Err(make_err(format!(
2156 "Segment is not a circle, it is {}",
2157 segment.human_friendly_kind_with_article()
2158 )));
2159 };
2160 vec![circle.start, circle.center, segment_id]
2161 };
2162 let src_delta = SourceDelta { text: new_source };
2163 let outcome = self.update_state_after_exec(outcome, false);
2165 let scene_graph_delta = SceneGraphDelta {
2166 new_graph: self.scene_graph.clone(),
2167 invalidates_ids: false,
2168 new_objects: new_object_ids,
2169 exec_outcome: outcome,
2170 };
2171 Ok((src_delta, scene_graph_delta))
2172 }
2173
2174 fn edit_point(
2175 &mut self,
2176 new_ast: &mut ast::Node<ast::Program>,
2177 sketch: ObjectId,
2178 point: ObjectId,
2179 ctor: PointCtor,
2180 ) -> Result<(), KclError> {
2181 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2183
2184 let sketch_id = sketch;
2186 let sketch_object = self
2187 .scene_graph
2188 .objects
2189 .get(sketch_id.0)
2190 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2191 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2192 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2193 };
2194 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2195 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2196 })?;
2197 let point_id = point;
2199 let point_object = self
2200 .scene_graph
2201 .objects
2202 .get(point_id.0)
2203 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2204 let ObjectKind::Segment {
2205 segment: Segment::Point(point),
2206 } = &point_object.kind
2207 else {
2208 return Err(KclError::refactor(format!(
2209 "Object is not a point segment: {point_object:?}"
2210 )));
2211 };
2212
2213 if let Some(owner_id) = point.owner {
2215 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2216 KclError::refactor(format!(
2217 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2218 ))
2219 })?;
2220 let ObjectKind::Segment { segment } = &owner_object.kind else {
2221 return Err(KclError::refactor(format!(
2222 "Internal: Owner of point is not a segment, but found {}",
2223 owner_object.kind.human_friendly_kind_with_article()
2224 )));
2225 };
2226
2227 if let Segment::Line(line) = segment {
2229 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2230 return Err(KclError::refactor(format!(
2231 "Internal: Owner of point does not have line ctor, but found {}",
2232 line.ctor.human_friendly_kind_with_article()
2233 )));
2234 };
2235 let mut line_ctor = line_ctor.clone();
2236 if line.start == point_id {
2238 line_ctor.start = ctor.position;
2239 } else if line.end == point_id {
2240 line_ctor.end = ctor.position;
2241 } else {
2242 return Err(KclError::refactor(format!(
2243 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2244 )));
2245 }
2246 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2247 }
2248
2249 if let Segment::Arc(arc) = segment {
2251 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2252 return Err(KclError::refactor(format!(
2253 "Internal: Owner of point does not have arc ctor, but found {}",
2254 arc.ctor.human_friendly_kind_with_article()
2255 )));
2256 };
2257 let mut arc_ctor = arc_ctor.clone();
2258 if arc.center == point_id {
2260 arc_ctor.center = ctor.position;
2261 } else if arc.start == point_id {
2262 arc_ctor.start = ctor.position;
2263 } else if arc.end == point_id {
2264 arc_ctor.end = ctor.position;
2265 } else {
2266 return Err(KclError::refactor(format!(
2267 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2268 )));
2269 }
2270 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2271 }
2272
2273 if let Segment::Circle(circle) = segment {
2275 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2276 return Err(KclError::refactor(format!(
2277 "Internal: Owner of point does not have circle ctor, but found {}",
2278 circle.ctor.human_friendly_kind_with_article()
2279 )));
2280 };
2281 let mut circle_ctor = circle_ctor.clone();
2282 if circle.center == point_id {
2283 circle_ctor.center = ctor.position;
2284 } else if circle.start == point_id {
2285 circle_ctor.start = ctor.position;
2286 } else {
2287 return Err(KclError::refactor(format!(
2288 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2289 )));
2290 }
2291 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2292 }
2293
2294 }
2297
2298 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2300 Ok(())
2301 }
2302
2303 fn edit_line(
2304 &mut self,
2305 new_ast: &mut ast::Node<ast::Program>,
2306 sketch: ObjectId,
2307 line: ObjectId,
2308 ctor: LineCtor,
2309 ) -> Result<(), KclError> {
2310 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2312 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2313
2314 let sketch_id = sketch;
2316 let sketch_object = self
2317 .scene_graph
2318 .objects
2319 .get(sketch_id.0)
2320 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2321 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2322 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2323 };
2324 sketch
2325 .segments
2326 .iter()
2327 .find(|o| **o == line)
2328 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2329 let line_id = line;
2331 let line_object = self
2332 .scene_graph
2333 .objects
2334 .get(line_id.0)
2335 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2336 let ObjectKind::Segment { .. } = &line_object.kind else {
2337 let kind = line_object.kind.human_friendly_kind_with_article();
2338 return Err(KclError::refactor(format!(
2339 "This constraint only works on Segments, but you selected {kind}"
2340 )));
2341 };
2342
2343 self.mutate_ast(
2345 new_ast,
2346 line_id,
2347 AstMutateCommand::EditLine {
2348 start: new_start_ast,
2349 end: new_end_ast,
2350 construction: ctor.construction,
2351 },
2352 )?;
2353 Ok(())
2354 }
2355
2356 fn edit_arc(
2357 &mut self,
2358 new_ast: &mut ast::Node<ast::Program>,
2359 sketch: ObjectId,
2360 arc: ObjectId,
2361 ctor: ArcCtor,
2362 ) -> Result<(), KclError> {
2363 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2365 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2366 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2367
2368 let sketch_id = sketch;
2370 let sketch_object = self
2371 .scene_graph
2372 .objects
2373 .get(sketch_id.0)
2374 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2375 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2376 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2377 };
2378 sketch
2379 .segments
2380 .iter()
2381 .find(|o| **o == arc)
2382 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2383 let arc_id = arc;
2385 let arc_object = self
2386 .scene_graph
2387 .objects
2388 .get(arc_id.0)
2389 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2390 let ObjectKind::Segment { .. } = &arc_object.kind else {
2391 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2392 };
2393
2394 self.mutate_ast(
2396 new_ast,
2397 arc_id,
2398 AstMutateCommand::EditArc {
2399 start: new_start_ast,
2400 end: new_end_ast,
2401 center: new_center_ast,
2402 construction: ctor.construction,
2403 },
2404 )?;
2405 Ok(())
2406 }
2407
2408 fn edit_circle(
2409 &mut self,
2410 new_ast: &mut ast::Node<ast::Program>,
2411 sketch: ObjectId,
2412 circle: ObjectId,
2413 ctor: CircleCtor,
2414 ) -> Result<(), KclError> {
2415 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2417 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2418
2419 let sketch_id = sketch;
2421 let sketch_object = self
2422 .scene_graph
2423 .objects
2424 .get(sketch_id.0)
2425 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2426 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2427 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2428 };
2429 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2430 KclError::refactor(format!(
2431 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2432 ))
2433 })?;
2434 let circle_id = circle;
2436 let circle_object = self
2437 .scene_graph
2438 .objects
2439 .get(circle_id.0)
2440 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2441 let ObjectKind::Segment { .. } = &circle_object.kind else {
2442 return Err(KclError::refactor(format!(
2443 "Object is not a segment: {circle_object:?}"
2444 )));
2445 };
2446
2447 self.mutate_ast(
2449 new_ast,
2450 circle_id,
2451 AstMutateCommand::EditCircle {
2452 start: new_start_ast,
2453 center: new_center_ast,
2454 construction: ctor.construction,
2455 },
2456 )?;
2457 Ok(())
2458 }
2459
2460 fn delete_segment(
2461 &mut self,
2462 new_ast: &mut ast::Node<ast::Program>,
2463 sketch: ObjectId,
2464 segment_id: ObjectId,
2465 ) -> Result<(), KclError> {
2466 let sketch_id = sketch;
2468 let sketch_object = self
2469 .scene_graph
2470 .objects
2471 .get(sketch_id.0)
2472 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2473 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2474 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2475 };
2476 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2477 KclError::refactor(format!(
2478 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2479 ))
2480 })?;
2481 let segment_object =
2483 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2484 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2485 })?;
2486 let ObjectKind::Segment { .. } = &segment_object.kind else {
2487 return Err(KclError::refactor(format!(
2488 "Object is not a segment, it is {}",
2489 segment_object.kind.human_friendly_kind_with_article()
2490 )));
2491 };
2492
2493 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2495 Ok(())
2496 }
2497
2498 fn delete_constraint(
2499 &mut self,
2500 new_ast: &mut ast::Node<ast::Program>,
2501 sketch: ObjectId,
2502 constraint_id: ObjectId,
2503 ) -> Result<(), KclError> {
2504 let sketch_id = sketch;
2506 let sketch_object = self
2507 .scene_graph
2508 .objects
2509 .get(sketch_id.0)
2510 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2511 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2512 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2513 };
2514 sketch
2515 .constraints
2516 .iter()
2517 .find(|o| **o == constraint_id)
2518 .ok_or_else(|| {
2519 KclError::refactor(format!(
2520 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2521 ))
2522 })?;
2523 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2525 KclError::refactor(format!(
2526 "Constraint not found in scene graph: constraint={constraint_id:?}"
2527 ))
2528 })?;
2529 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2530 return Err(KclError::refactor(format!(
2531 "Object is not a constraint, it is {}",
2532 constraint_object.kind.human_friendly_kind_with_article()
2533 )));
2534 };
2535
2536 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2538 Ok(())
2539 }
2540
2541 fn edit_coincident_constraint(
2542 &mut self,
2543 new_ast: &mut ast::Node<ast::Program>,
2544 constraint_id: ObjectId,
2545 segments: Vec<ConstraintSegment>,
2546 ) -> Result<(), KclError> {
2547 if segments.len() < 2 {
2548 return Err(KclError::refactor(format!(
2549 "Coincident constraint must have at least 2 inputs, got {}",
2550 segments.len()
2551 )));
2552 }
2553
2554 let segment_asts = segments
2555 .iter()
2556 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2557 .collect::<Result<Vec<_>, _>>()?;
2558
2559 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2560 elements: segment_asts,
2561 digest: None,
2562 non_code_meta: Default::default(),
2563 })));
2564
2565 self.mutate_ast(
2566 new_ast,
2567 constraint_id,
2568 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2569 )?;
2570 Ok(())
2571 }
2572
2573 fn edit_horizontal_points_constraint(
2574 &mut self,
2575 new_ast: &mut ast::Node<ast::Program>,
2576 constraint_id: ObjectId,
2577 points: Vec<ConstraintSegment>,
2578 ) -> Result<(), KclError> {
2579 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2580 }
2581
2582 fn edit_vertical_points_constraint(
2583 &mut self,
2584 new_ast: &mut ast::Node<ast::Program>,
2585 constraint_id: ObjectId,
2586 points: Vec<ConstraintSegment>,
2587 ) -> Result<(), KclError> {
2588 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2589 }
2590
2591 fn edit_axis_points_constraint(
2592 &mut self,
2593 new_ast: &mut ast::Node<ast::Program>,
2594 constraint_id: ObjectId,
2595 points: Vec<ConstraintSegment>,
2596 constraint_name: &str,
2597 ) -> Result<(), KclError> {
2598 if points.len() < 2 {
2599 return Err(KclError::refactor(format!(
2600 "{constraint_name} points constraint must have at least 2 points, got {}",
2601 points.len()
2602 )));
2603 }
2604
2605 let point_asts = points
2606 .iter()
2607 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2608 .collect::<Result<Vec<_>, _>>()?;
2609
2610 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2611 elements: point_asts,
2612 digest: None,
2613 non_code_meta: Default::default(),
2614 })));
2615
2616 self.mutate_ast(
2617 new_ast,
2618 constraint_id,
2619 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2620 )?;
2621 Ok(())
2622 }
2623
2624 fn edit_equal_length_constraint(
2626 &mut self,
2627 new_ast: &mut ast::Node<ast::Program>,
2628 constraint_id: ObjectId,
2629 lines: Vec<ObjectId>,
2630 ) -> Result<(), KclError> {
2631 if lines.len() < 2 {
2632 return Err(KclError::refactor(format!(
2633 "Lines equal length constraint must have at least 2 lines, got {}",
2634 lines.len()
2635 )));
2636 }
2637
2638 let line_asts = lines
2639 .iter()
2640 .map(|line_id| {
2641 let line_object = self
2642 .scene_graph
2643 .objects
2644 .get(line_id.0)
2645 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2646 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2647 let kind = line_object.kind.human_friendly_kind_with_article();
2648 return Err(KclError::refactor(format!(
2649 "This constraint only works on Segments, but you selected {kind}"
2650 )));
2651 };
2652 let Segment::Line(_) = line_segment else {
2653 let kind = line_segment.human_friendly_kind_with_article();
2654 return Err(KclError::refactor(format!(
2655 "Only lines can be made equal length, but you selected {kind}"
2656 )));
2657 };
2658
2659 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2660 })
2661 .collect::<Result<Vec<_>, _>>()?;
2662
2663 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2664 elements: line_asts,
2665 digest: None,
2666 non_code_meta: Default::default(),
2667 })));
2668
2669 self.mutate_ast(
2670 new_ast,
2671 constraint_id,
2672 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2673 )?;
2674 Ok(())
2675 }
2676
2677 fn edit_parallel_constraint(
2679 &mut self,
2680 new_ast: &mut ast::Node<ast::Program>,
2681 constraint_id: ObjectId,
2682 lines: Vec<ObjectId>,
2683 ) -> Result<(), KclError> {
2684 if lines.len() < 2 {
2685 return Err(KclError::refactor(format!(
2686 "Parallel constraint must have at least 2 lines, got {}",
2687 lines.len()
2688 )));
2689 }
2690
2691 let line_asts = lines
2692 .iter()
2693 .map(|line_id| {
2694 let line_object = self
2695 .scene_graph
2696 .objects
2697 .get(line_id.0)
2698 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2699 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2700 let kind = line_object.kind.human_friendly_kind_with_article();
2701 return Err(KclError::refactor(format!(
2702 "This constraint only works on Segments, but you selected {kind}"
2703 )));
2704 };
2705 let Segment::Line(_) = line_segment else {
2706 let kind = line_segment.human_friendly_kind_with_article();
2707 return Err(KclError::refactor(format!(
2708 "Only lines can be made parallel, but you selected {kind}"
2709 )));
2710 };
2711
2712 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2713 })
2714 .collect::<Result<Vec<_>, _>>()?;
2715
2716 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2717 elements: line_asts,
2718 digest: None,
2719 non_code_meta: Default::default(),
2720 })));
2721
2722 self.mutate_ast(
2723 new_ast,
2724 constraint_id,
2725 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2726 )?;
2727 Ok(())
2728 }
2729
2730 fn edit_equal_radius_constraint(
2732 &mut self,
2733 new_ast: &mut ast::Node<ast::Program>,
2734 constraint_id: ObjectId,
2735 input: Vec<ObjectId>,
2736 ) -> Result<(), KclError> {
2737 if input.len() < 2 {
2738 return Err(KclError::refactor(format!(
2739 "equalRadius constraint must have at least 2 segments, got {}",
2740 input.len()
2741 )));
2742 }
2743
2744 let input_asts = input
2745 .iter()
2746 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2747 .collect::<Result<Vec<_>, _>>()?;
2748
2749 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2750 elements: input_asts,
2751 digest: None,
2752 non_code_meta: Default::default(),
2753 })));
2754
2755 self.mutate_ast(
2756 new_ast,
2757 constraint_id,
2758 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2759 )?;
2760 Ok(())
2761 }
2762
2763 async fn execute_after_edit(
2764 &mut self,
2765 ctx: &ExecutorContext,
2766 sketch: ObjectId,
2767 sketch_block_ref: AstNodeRef,
2768 segment_ids_edited: AhashIndexSet<ObjectId>,
2769 edit_kind: EditDeleteKind,
2770 new_ast: &mut ast::Node<ast::Program>,
2771 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2772 let new_source = source_from_ast(new_ast);
2774 let (new_program, errors) = Program::parse(&new_source)
2776 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2777 if !errors.is_empty() {
2778 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2779 "Error parsing KCL source after editing: {errors:?}"
2780 ))));
2781 }
2782 let Some(new_program) = new_program else {
2783 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2784 "No AST produced after editing".to_string(),
2785 )));
2786 };
2787
2788 self.program = new_program.clone();
2790
2791 let is_delete = edit_kind.is_delete();
2793 let truncated_program = {
2794 let mut truncated_program = new_program;
2795 only_sketch_block(
2796 &mut truncated_program.ast,
2797 &sketch_block_ref,
2798 edit_kind.to_change_kind(),
2799 )
2800 .map_err(KclErrorWithOutputs::no_outputs)?;
2801 truncated_program
2802 };
2803
2804 let mock_config = MockConfig {
2806 sketch_block_id: Some(sketch),
2807 freedom_analysis: is_delete,
2808 segment_ids_edited: segment_ids_edited.clone(),
2809 ..Default::default()
2810 };
2811 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2812
2813 let outcome = self.update_state_after_exec(outcome, is_delete);
2815
2816 let new_source = {
2817 let mut new_ast = self.program.ast.clone();
2822 for (var_range, value) in &outcome.var_solutions {
2823 let rounded = value.round(3);
2824 let source_ref = SourceRef::Simple {
2825 range: *var_range,
2826 node_path: None,
2827 };
2828 mutate_ast_node_by_source_ref(
2829 &mut new_ast,
2830 &source_ref,
2831 AstMutateCommand::EditVarInitialValue { value: rounded },
2832 )
2833 .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2834 }
2835 source_from_ast(&new_ast)
2836 };
2837
2838 let src_delta = SourceDelta { text: new_source };
2839 let scene_graph_delta = SceneGraphDelta {
2840 new_graph: self.scene_graph.clone(),
2841 invalidates_ids: is_delete,
2842 new_objects: Vec::new(),
2843 exec_outcome: outcome,
2844 };
2845 Ok((src_delta, scene_graph_delta))
2846 }
2847
2848 async fn execute_after_delete_sketch(
2849 &mut self,
2850 ctx: &ExecutorContext,
2851 new_ast: &mut ast::Node<ast::Program>,
2852 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2853 let new_source = source_from_ast(new_ast);
2855 let (new_program, errors) = Program::parse(&new_source)
2857 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2858 if !errors.is_empty() {
2859 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2860 "Error parsing KCL source after editing: {errors:?}"
2861 ))));
2862 }
2863 let Some(new_program) = new_program else {
2864 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2865 "No AST produced after editing".to_string(),
2866 )));
2867 };
2868
2869 self.program = new_program.clone();
2871
2872 let outcome = ctx.run_with_caching(new_program).await?;
2878 let freedom_analysis_ran = true;
2879
2880 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2881
2882 let src_delta = SourceDelta { text: new_source };
2883 let scene_graph_delta = SceneGraphDelta {
2884 new_graph: self.scene_graph.clone(),
2885 invalidates_ids: true,
2886 new_objects: Vec::new(),
2887 exec_outcome: outcome,
2888 };
2889 Ok((src_delta, scene_graph_delta))
2890 }
2891
2892 fn point_id_to_ast_reference(
2897 &self,
2898 point_id: ObjectId,
2899 new_ast: &mut ast::Node<ast::Program>,
2900 ) -> Result<ast::Expr, KclError> {
2901 let point_object = self
2902 .scene_graph
2903 .objects
2904 .get(point_id.0)
2905 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2906 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2907 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2908 };
2909 let Segment::Point(point) = point_segment else {
2910 return Err(KclError::refactor(format!(
2911 "Only points are currently supported: {point_object:?}"
2912 )));
2913 };
2914
2915 if let Some(owner_id) = point.owner {
2916 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2917 KclError::refactor(format!(
2918 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2919 ))
2920 })?;
2921 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2922 return Err(KclError::refactor(format!(
2923 "Owner of point is not a segment, but found {}",
2924 owner_object.kind.human_friendly_kind_with_article()
2925 )));
2926 };
2927
2928 match owner_segment {
2929 Segment::Line(line) => {
2930 let property = if line.start == point_id {
2931 LINE_PROPERTY_START
2932 } else if line.end == point_id {
2933 LINE_PROPERTY_END
2934 } else {
2935 return Err(KclError::refactor(format!(
2936 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2937 )));
2938 };
2939 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
2940 }
2941 Segment::Arc(arc) => {
2942 let property = if arc.start == point_id {
2943 ARC_PROPERTY_START
2944 } else if arc.end == point_id {
2945 ARC_PROPERTY_END
2946 } else if arc.center == point_id {
2947 ARC_PROPERTY_CENTER
2948 } else {
2949 return Err(KclError::refactor(format!(
2950 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2951 )));
2952 };
2953 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
2954 }
2955 Segment::Circle(circle) => {
2956 let property = if circle.start == point_id {
2957 CIRCLE_PROPERTY_START
2958 } else if circle.center == point_id {
2959 CIRCLE_PROPERTY_CENTER
2960 } else {
2961 return Err(KclError::refactor(format!(
2962 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2963 )));
2964 };
2965 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
2966 }
2967 _ => Err(KclError::refactor(format!(
2968 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
2969 ))),
2970 }
2971 } else {
2972 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
2974 }
2975 }
2976
2977 fn coincident_segment_to_ast(
2978 &self,
2979 segment: &ConstraintSegment,
2980 new_ast: &mut ast::Node<ast::Program>,
2981 ) -> Result<ast::Expr, KclError> {
2982 match segment {
2983 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
2984 ConstraintSegment::Segment(segment_id) => {
2985 let segment_object = self
2986 .scene_graph
2987 .objects
2988 .get(segment_id.0)
2989 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
2990 let ObjectKind::Segment { segment } = &segment_object.kind else {
2991 return Err(KclError::refactor(format!(
2992 "Object is not a segment, it is {}",
2993 segment_object.kind.human_friendly_kind_with_article()
2994 )));
2995 };
2996
2997 match segment {
2998 Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
2999 Segment::Line(_) => {
3000 get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None)
3001 }
3002 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3003 Segment::Circle(_) => {
3004 get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
3005 }
3006 }
3007 }
3008 }
3009 }
3010
3011 fn axis_constraint_segment_to_ast(
3012 &self,
3013 segment: &ConstraintSegment,
3014 new_ast: &mut ast::Node<ast::Program>,
3015 ) -> Result<ast::Expr, KclError> {
3016 match segment {
3017 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3018 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3019 }
3020 }
3021
3022 async fn add_coincident(
3023 &mut self,
3024 sketch: ObjectId,
3025 coincident: Coincident,
3026 new_ast: &mut ast::Node<ast::Program>,
3027 ) -> Result<AstNodeRef, KclError> {
3028 let sketch_id = sketch;
3029 let segment_asts = coincident
3030 .segments
3031 .iter()
3032 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3033 .collect::<Result<Vec<_>, _>>()?;
3034 if segment_asts.len() < 2 {
3035 return Err(KclError::refactor(format!(
3036 "Coincident constraint must have at least 2 inputs, got {}",
3037 segment_asts.len()
3038 )));
3039 }
3040
3041 let coincident_ast = create_coincident_ast(segment_asts);
3043
3044 let (sketch_block_ref, _) = self.mutate_ast(
3046 new_ast,
3047 sketch_id,
3048 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3049 )?;
3050 Ok(sketch_block_ref)
3051 }
3052
3053 async fn add_distance(
3054 &mut self,
3055 sketch: ObjectId,
3056 distance: Distance,
3057 new_ast: &mut ast::Node<ast::Program>,
3058 ) -> Result<AstNodeRef, KclError> {
3059 let sketch_id = sketch;
3060 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3061 [pt0, pt1] => [
3062 self.coincident_segment_to_ast(pt0, new_ast)?,
3063 self.coincident_segment_to_ast(pt1, new_ast)?,
3064 ],
3065 _ => {
3066 return Err(KclError::refactor(format!(
3067 "Distance constraint must have exactly 2 points, got {}",
3068 distance.points.len()
3069 )));
3070 }
3071 };
3072
3073 let arguments = match &distance.label_position {
3074 Some(label_position) => vec![ast::LabeledArg {
3075 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3076 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3077 }],
3078 None => Default::default(),
3079 };
3080
3081 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3083 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3084 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3085 ast::ArrayExpression {
3086 elements: vec![pt0_ast, pt1_ast],
3087 digest: None,
3088 non_code_meta: Default::default(),
3089 },
3090 )))),
3091 arguments,
3092 digest: None,
3093 non_code_meta: Default::default(),
3094 })));
3095 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3096 left: distance_call_ast,
3097 operator: ast::BinaryOperator::Eq,
3098 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3099 value: ast::LiteralValue::Number {
3100 value: distance.distance.value,
3101 suffix: distance.distance.units,
3102 },
3103 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3104 KclError::refactor(format!(
3105 "Could not format numeric suffix: {:?}",
3106 distance.distance.units
3107 ))
3108 })?,
3109 digest: None,
3110 }))),
3111 digest: None,
3112 })));
3113
3114 let (sketch_block_ref, _) = self.mutate_ast(
3116 new_ast,
3117 sketch_id,
3118 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3119 )?;
3120 Ok(sketch_block_ref)
3121 }
3122
3123 async fn add_angle(
3124 &mut self,
3125 sketch: ObjectId,
3126 angle: Angle,
3127 new_ast: &mut ast::Node<ast::Program>,
3128 ) -> Result<AstNodeRef, KclError> {
3129 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3130 return Err(KclError::refactor(format!(
3131 "Angle constraint must have exactly 2 lines, got {}",
3132 angle.lines.len()
3133 )));
3134 };
3135 let sketch_id = sketch;
3136
3137 let line0_object = self
3139 .scene_graph
3140 .objects
3141 .get(l0_id.0)
3142 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3143 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3144 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3145 };
3146 let Segment::Line(_) = line0_segment else {
3147 return Err(KclError::refactor(format!(
3148 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3149 )));
3150 };
3151 let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3152
3153 let line1_object = self
3154 .scene_graph
3155 .objects
3156 .get(l1_id.0)
3157 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3158 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3159 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3160 };
3161 let Segment::Line(_) = line1_segment else {
3162 return Err(KclError::refactor(format!(
3163 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3164 )));
3165 };
3166 let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3167
3168 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3170 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3171 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3172 ast::ArrayExpression {
3173 elements: vec![l0_ast, l1_ast],
3174 digest: None,
3175 non_code_meta: Default::default(),
3176 },
3177 )))),
3178 arguments: Default::default(),
3179 digest: None,
3180 non_code_meta: Default::default(),
3181 })));
3182 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3183 left: angle_call_ast,
3184 operator: ast::BinaryOperator::Eq,
3185 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3186 value: ast::LiteralValue::Number {
3187 value: angle.angle.value,
3188 suffix: angle.angle.units,
3189 },
3190 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3191 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3192 })?,
3193 digest: None,
3194 }))),
3195 digest: None,
3196 })));
3197
3198 let (sketch_block_ref, _) = self.mutate_ast(
3200 new_ast,
3201 sketch_id,
3202 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3203 )?;
3204 Ok(sketch_block_ref)
3205 }
3206
3207 async fn add_tangent(
3208 &mut self,
3209 sketch: ObjectId,
3210 tangent: Tangent,
3211 new_ast: &mut ast::Node<ast::Program>,
3212 ) -> Result<AstNodeRef, KclError> {
3213 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3214 return Err(KclError::refactor(format!(
3215 "Tangent constraint must have exactly 2 segments, got {}",
3216 tangent.input.len()
3217 )));
3218 };
3219 let sketch_id = sketch;
3220
3221 let seg0_object = self
3222 .scene_graph
3223 .objects
3224 .get(seg0_id.0)
3225 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3226 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3227 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3228 };
3229 let seg0_ast = match seg0_segment {
3230 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, LINE_VARIABLE, None)?,
3231 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, ARC_VARIABLE, None)?,
3232 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3233 _ => {
3234 return Err(KclError::refactor(format!(
3235 "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3236 )));
3237 }
3238 };
3239
3240 let seg1_object = self
3241 .scene_graph
3242 .objects
3243 .get(seg1_id.0)
3244 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3245 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3246 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3247 };
3248 let seg1_ast = match seg1_segment {
3249 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, LINE_VARIABLE, None)?,
3250 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, ARC_VARIABLE, None)?,
3251 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3252 _ => {
3253 return Err(KclError::refactor(format!(
3254 "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3255 )));
3256 }
3257 };
3258
3259 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3260 let (sketch_block_ref, _) = self.mutate_ast(
3261 new_ast,
3262 sketch_id,
3263 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3264 )?;
3265 Ok(sketch_block_ref)
3266 }
3267
3268 async fn add_symmetric(
3269 &mut self,
3270 sketch: ObjectId,
3271 symmetric: Symmetric,
3272 new_ast: &mut ast::Node<ast::Program>,
3273 ) -> Result<AstNodeRef, KclError> {
3274 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3275 return Err(KclError::refactor(format!(
3276 "Symmetric constraint must have exactly 2 inputs, got {}",
3277 symmetric.input.len()
3278 )));
3279 };
3280 let sketch_id = sketch;
3281
3282 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3283 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3284 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3285
3286 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3287 let (sketch_block_ref, _) = self.mutate_ast(
3288 new_ast,
3289 sketch_id,
3290 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3291 )?;
3292 Ok(sketch_block_ref)
3293 }
3294
3295 async fn add_midpoint(
3296 &mut self,
3297 sketch: ObjectId,
3298 midpoint: Midpoint,
3299 new_ast: &mut ast::Node<ast::Program>,
3300 ) -> Result<AstNodeRef, KclError> {
3301 let sketch_id = sketch;
3302 let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3303
3304 let segment_object = self
3305 .scene_graph
3306 .objects
3307 .get(midpoint.segment.0)
3308 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3309 let ObjectKind::Segment {
3310 segment: midpoint_segment,
3311 } = &segment_object.kind
3312 else {
3313 return Err(KclError::refactor(format!(
3314 "Object must be a segment, but it was {}",
3315 segment_object.kind.human_friendly_kind_with_article()
3316 )));
3317 };
3318 let segment_ast = match midpoint_segment {
3319 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3320 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3321 _ => {
3322 return Err(KclError::refactor(format!(
3323 "Midpoint target must be a line or arc segment but it was {}",
3324 midpoint_segment.human_friendly_kind_with_article()
3325 )));
3326 }
3327 };
3328
3329 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3330 let (sketch_block_ref, _) = self.mutate_ast(
3331 new_ast,
3332 sketch_id,
3333 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3334 )?;
3335 Ok(sketch_block_ref)
3336 }
3337
3338 async fn add_equal_radius(
3339 &mut self,
3340 sketch: ObjectId,
3341 equal_radius: EqualRadius,
3342 new_ast: &mut ast::Node<ast::Program>,
3343 ) -> Result<AstNodeRef, KclError> {
3344 if equal_radius.input.len() < 2 {
3345 return Err(KclError::refactor(format!(
3346 "equalRadius constraint must have at least 2 segments, got {}",
3347 equal_radius.input.len()
3348 )));
3349 }
3350
3351 let sketch_id = sketch;
3352 let input_asts = equal_radius
3353 .input
3354 .iter()
3355 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3356 .collect::<Result<Vec<_>, _>>()?;
3357
3358 let equal_radius_ast = create_equal_radius_ast(input_asts);
3359 let (sketch_block_ref, _) = self.mutate_ast(
3360 new_ast,
3361 sketch_id,
3362 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3363 )?;
3364 Ok(sketch_block_ref)
3365 }
3366
3367 async fn add_radius(
3368 &mut self,
3369 sketch: ObjectId,
3370 radius: Radius,
3371 new_ast: &mut ast::Node<ast::Program>,
3372 ) -> Result<AstNodeRef, KclError> {
3373 let params = ArcSizeConstraintParams {
3374 points: vec![radius.arc],
3375 function_name: RADIUS_FN,
3376 value: radius.radius.value,
3377 units: radius.radius.units,
3378 label_position: radius.label_position,
3379 constraint_type_name: "Radius",
3380 };
3381 self.add_arc_size_constraint(sketch, params, new_ast).await
3382 }
3383
3384 async fn add_diameter(
3385 &mut self,
3386 sketch: ObjectId,
3387 diameter: Diameter,
3388 new_ast: &mut ast::Node<ast::Program>,
3389 ) -> Result<AstNodeRef, KclError> {
3390 let params = ArcSizeConstraintParams {
3391 points: vec![diameter.arc],
3392 function_name: DIAMETER_FN,
3393 value: diameter.diameter.value,
3394 units: diameter.diameter.units,
3395 label_position: diameter.label_position,
3396 constraint_type_name: "Diameter",
3397 };
3398 self.add_arc_size_constraint(sketch, params, new_ast).await
3399 }
3400
3401 async fn add_fixed_constraints(
3402 &mut self,
3403 sketch: ObjectId,
3404 points: Vec<FixedPoint>,
3405 new_ast: &mut ast::Node<ast::Program>,
3406 ) -> Result<AstNodeRef, KclError> {
3407 let mut sketch_block_ref = None;
3408
3409 for fixed_point in points {
3410 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3411 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3412 .map_err(|err| KclError::refactor(err.to_string()))?;
3413
3414 let (sketch_ref, _) = self.mutate_ast(
3415 new_ast,
3416 sketch,
3417 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3418 )?;
3419 sketch_block_ref = Some(sketch_ref);
3420 }
3421
3422 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3423 }
3424
3425 async fn add_arc_size_constraint(
3426 &mut self,
3427 sketch: ObjectId,
3428 params: ArcSizeConstraintParams,
3429 new_ast: &mut ast::Node<ast::Program>,
3430 ) -> Result<AstNodeRef, KclError> {
3431 let sketch_id = sketch;
3432
3433 if params.points.len() != 1 {
3435 return Err(KclError::refactor(format!(
3436 "{} constraint must have exactly 1 argument (an arc segment), got {}",
3437 params.constraint_type_name,
3438 params.points.len()
3439 )));
3440 }
3441
3442 let arc_id = params.points[0];
3443 let arc_object = self
3444 .scene_graph
3445 .objects
3446 .get(arc_id.0)
3447 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3448 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3449 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3450 };
3451 let ref_type = match arc_segment {
3452 Segment::Arc(_) => ARC_VARIABLE,
3453 Segment::Circle(_) => CIRCLE_VARIABLE,
3454 _ => {
3455 return Err(KclError::refactor(format!(
3456 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3457 params.constraint_type_name
3458 )));
3459 }
3460 };
3461 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3463 let arguments = match ¶ms.label_position {
3464 Some(label_position) => vec![ast::LabeledArg {
3465 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3466 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3467 }],
3468 None => Default::default(),
3469 };
3470
3471 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3473 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3474 unlabeled: Some(arc_ast),
3475 arguments,
3476 digest: None,
3477 non_code_meta: Default::default(),
3478 })));
3479 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3480 left: call_ast,
3481 operator: ast::BinaryOperator::Eq,
3482 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3483 value: ast::LiteralValue::Number {
3484 value: params.value,
3485 suffix: params.units,
3486 },
3487 raw: format_number_literal(params.value, params.units, None)
3488 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3489 digest: None,
3490 }))),
3491 digest: None,
3492 })));
3493
3494 let (sketch_block_ref, _) = self.mutate_ast(
3496 new_ast,
3497 sketch_id,
3498 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3499 )?;
3500 Ok(sketch_block_ref)
3501 }
3502
3503 async fn add_horizontal_distance(
3504 &mut self,
3505 sketch: ObjectId,
3506 distance: Distance,
3507 new_ast: &mut ast::Node<ast::Program>,
3508 ) -> Result<AstNodeRef, KclError> {
3509 let sketch_id = sketch;
3510 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3511 [pt0, pt1] => [
3512 self.coincident_segment_to_ast(pt0, new_ast)?,
3513 self.coincident_segment_to_ast(pt1, new_ast)?,
3514 ],
3515 _ => {
3516 return Err(KclError::refactor(format!(
3517 "Horizontal distance constraint must have exactly 2 points, got {}",
3518 distance.points.len()
3519 )));
3520 }
3521 };
3522
3523 let arguments = match &distance.label_position {
3524 Some(label_position) => vec![ast::LabeledArg {
3525 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3526 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3527 }],
3528 None => Default::default(),
3529 };
3530
3531 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3533 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3534 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3535 ast::ArrayExpression {
3536 elements: vec![pt0_ast, pt1_ast],
3537 digest: None,
3538 non_code_meta: Default::default(),
3539 },
3540 )))),
3541 arguments,
3542 digest: None,
3543 non_code_meta: Default::default(),
3544 })));
3545 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3546 left: distance_call_ast,
3547 operator: ast::BinaryOperator::Eq,
3548 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3549 value: ast::LiteralValue::Number {
3550 value: distance.distance.value,
3551 suffix: distance.distance.units,
3552 },
3553 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3554 KclError::refactor(format!(
3555 "Could not format numeric suffix: {:?}",
3556 distance.distance.units
3557 ))
3558 })?,
3559 digest: None,
3560 }))),
3561 digest: None,
3562 })));
3563
3564 let (sketch_block_ref, _) = self.mutate_ast(
3566 new_ast,
3567 sketch_id,
3568 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3569 )?;
3570 Ok(sketch_block_ref)
3571 }
3572
3573 async fn add_vertical_distance(
3574 &mut self,
3575 sketch: ObjectId,
3576 distance: Distance,
3577 new_ast: &mut ast::Node<ast::Program>,
3578 ) -> Result<AstNodeRef, KclError> {
3579 let sketch_id = sketch;
3580 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3581 [pt0, pt1] => [
3582 self.coincident_segment_to_ast(pt0, new_ast)?,
3583 self.coincident_segment_to_ast(pt1, new_ast)?,
3584 ],
3585 _ => {
3586 return Err(KclError::refactor(format!(
3587 "Vertical distance constraint must have exactly 2 points, got {}",
3588 distance.points.len()
3589 )));
3590 }
3591 };
3592
3593 let arguments = match &distance.label_position {
3594 Some(label_position) => vec![ast::LabeledArg {
3595 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3596 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3597 }],
3598 None => Default::default(),
3599 };
3600
3601 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3603 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3604 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3605 ast::ArrayExpression {
3606 elements: vec![pt0_ast, pt1_ast],
3607 digest: None,
3608 non_code_meta: Default::default(),
3609 },
3610 )))),
3611 arguments,
3612 digest: None,
3613 non_code_meta: Default::default(),
3614 })));
3615 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3616 left: distance_call_ast,
3617 operator: ast::BinaryOperator::Eq,
3618 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3619 value: ast::LiteralValue::Number {
3620 value: distance.distance.value,
3621 suffix: distance.distance.units,
3622 },
3623 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3624 KclError::refactor(format!(
3625 "Could not format numeric suffix: {:?}",
3626 distance.distance.units
3627 ))
3628 })?,
3629 digest: None,
3630 }))),
3631 digest: None,
3632 })));
3633
3634 let (sketch_block_ref, _) = self.mutate_ast(
3636 new_ast,
3637 sketch_id,
3638 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3639 )?;
3640 Ok(sketch_block_ref)
3641 }
3642
3643 async fn add_horizontal(
3644 &mut self,
3645 sketch: ObjectId,
3646 horizontal: Horizontal,
3647 new_ast: &mut ast::Node<ast::Program>,
3648 ) -> Result<AstNodeRef, KclError> {
3649 let sketch_id = sketch;
3650
3651 let first_arg_ast = match horizontal {
3653 Horizontal::Line { line } => {
3654 let line_object = self
3655 .scene_graph
3656 .objects
3657 .get(line.0)
3658 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3659 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3660 let kind = line_object.kind.human_friendly_kind_with_article();
3661 return Err(KclError::refactor(format!(
3662 "This constraint only works on Segments, but you selected {kind}"
3663 )));
3664 };
3665 let Segment::Line(_) = line_segment else {
3666 return Err(KclError::refactor(format!(
3667 "Only lines can be made horizontal, but you selected {}",
3668 line_segment.human_friendly_kind_with_article(),
3669 )));
3670 };
3671 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3672 }
3673 Horizontal::Points { points } => {
3674 let point_asts = points
3675 .iter()
3676 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3677 .collect::<Result<Vec<_>, _>>()?;
3678 ast::ArrayExpression::new(point_asts).into()
3679 }
3680 };
3681
3682 let horizontal_ast = create_horizontal_ast(first_arg_ast);
3684
3685 let (sketch_block_ref, _) = self.mutate_ast(
3687 new_ast,
3688 sketch_id,
3689 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3690 )?;
3691 Ok(sketch_block_ref)
3692 }
3693
3694 async fn add_lines_equal_length(
3695 &mut self,
3696 sketch: ObjectId,
3697 lines_equal_length: LinesEqualLength,
3698 new_ast: &mut ast::Node<ast::Program>,
3699 ) -> Result<AstNodeRef, KclError> {
3700 if lines_equal_length.lines.len() < 2 {
3701 return Err(KclError::refactor(format!(
3702 "Lines equal length constraint must have at least 2 lines, got {}",
3703 lines_equal_length.lines.len()
3704 )));
3705 };
3706
3707 let sketch_id = sketch;
3708
3709 let line_asts = lines_equal_length
3711 .lines
3712 .iter()
3713 .map(|line_id| {
3714 let line_object = self
3715 .scene_graph
3716 .objects
3717 .get(line_id.0)
3718 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3719 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3720 let kind = line_object.kind.human_friendly_kind_with_article();
3721 return Err(KclError::refactor(format!(
3722 "This constraint only works on Segments, but you selected {kind}"
3723 )));
3724 };
3725 let Segment::Line(_) = line_segment else {
3726 let kind = line_segment.human_friendly_kind_with_article();
3727 return Err(KclError::refactor(format!(
3728 "Only lines can be made equal length, but you selected {kind}"
3729 )));
3730 };
3731
3732 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3733 })
3734 .collect::<Result<Vec<_>, _>>()?;
3735
3736 let equal_length_ast = create_equal_length_ast(line_asts);
3738
3739 let (sketch_block_ref, _) = self.mutate_ast(
3741 new_ast,
3742 sketch_id,
3743 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3744 )?;
3745 Ok(sketch_block_ref)
3746 }
3747
3748 fn equal_radius_segment_id_to_ast_reference(
3749 &mut self,
3750 segment_id: ObjectId,
3751 new_ast: &mut ast::Node<ast::Program>,
3752 ) -> Result<ast::Expr, KclError> {
3753 let segment_object = self
3754 .scene_graph
3755 .objects
3756 .get(segment_id.0)
3757 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3758 let ObjectKind::Segment { segment } = &segment_object.kind else {
3759 return Err(KclError::refactor(format!(
3760 "Object is not a segment, it was {}",
3761 segment_object.kind.human_friendly_kind_with_article()
3762 )));
3763 };
3764
3765 let ref_type = match segment {
3766 Segment::Arc(_) => ARC_VARIABLE,
3767 Segment::Circle(_) => CIRCLE_VARIABLE,
3768 _ => {
3769 return Err(KclError::refactor(format!(
3770 "equalRadius supports only arc/circle segments, got {}",
3771 segment.human_friendly_kind_with_article()
3772 )));
3773 }
3774 };
3775
3776 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3777 }
3778
3779 fn symmetric_input_id_to_ast_reference(
3780 &mut self,
3781 segment_id: ObjectId,
3782 new_ast: &mut ast::Node<ast::Program>,
3783 ) -> Result<ast::Expr, KclError> {
3784 let segment_object = self
3785 .scene_graph
3786 .objects
3787 .get(segment_id.0)
3788 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3789 let ObjectKind::Segment { segment } = &segment_object.kind else {
3790 return Err(KclError::refactor(format!(
3791 "Object is not a segment, it was {}",
3792 segment_object.kind.human_friendly_kind_with_article()
3793 )));
3794 };
3795
3796 match segment {
3797 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3798 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3799 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3800 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3801 }
3802 }
3803
3804 fn symmetric_axis_id_to_ast_reference(
3805 &mut self,
3806 segment_id: ObjectId,
3807 new_ast: &mut ast::Node<ast::Program>,
3808 ) -> Result<ast::Expr, KclError> {
3809 let segment_object = self
3810 .scene_graph
3811 .objects
3812 .get(segment_id.0)
3813 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
3814 let ObjectKind::Segment { segment } = &segment_object.kind else {
3815 return Err(KclError::refactor(format!(
3816 "Object is not a segment, it was {}",
3817 segment_object.kind.human_friendly_kind_with_article()
3818 )));
3819 };
3820 match segment {
3821 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3822 _ => Err(KclError::refactor(format!(
3823 "Symmetric axis must be a line, got {}",
3824 segment.human_friendly_kind_with_article()
3825 ))),
3826 }
3827 }
3828
3829 async fn add_parallel(
3830 &mut self,
3831 sketch: ObjectId,
3832 parallel: Parallel,
3833 new_ast: &mut ast::Node<ast::Program>,
3834 ) -> Result<AstNodeRef, KclError> {
3835 if parallel.lines.len() < 2 {
3836 return Err(KclError::refactor(format!(
3837 "Parallel constraint must have at least 2 lines, got {}",
3838 parallel.lines.len()
3839 )));
3840 };
3841
3842 let sketch_id = sketch;
3843
3844 let line_asts = parallel
3845 .lines
3846 .iter()
3847 .map(|line_id| {
3848 let line_object = self
3849 .scene_graph
3850 .objects
3851 .get(line_id.0)
3852 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3853 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3854 let kind = line_object.kind.human_friendly_kind_with_article();
3855 return Err(KclError::refactor(format!(
3856 "This constraint only works on Segments, but you selected {kind}"
3857 )));
3858 };
3859 let Segment::Line(_) = line_segment else {
3860 let kind = line_segment.human_friendly_kind_with_article();
3861 return Err(KclError::refactor(format!(
3862 "Only lines can be made parallel, but you selected {kind}"
3863 )));
3864 };
3865
3866 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3867 })
3868 .collect::<Result<Vec<_>, _>>()?;
3869
3870 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3871 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3872 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3873 ast::ArrayExpression {
3874 elements: line_asts,
3875 digest: None,
3876 non_code_meta: Default::default(),
3877 },
3878 )))),
3879 arguments: Default::default(),
3880 digest: None,
3881 non_code_meta: Default::default(),
3882 })));
3883
3884 let (sketch_block_ref, _) = self.mutate_ast(
3885 new_ast,
3886 sketch_id,
3887 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3888 )?;
3889 Ok(sketch_block_ref)
3890 }
3891
3892 async fn add_perpendicular(
3893 &mut self,
3894 sketch: ObjectId,
3895 perpendicular: Perpendicular,
3896 new_ast: &mut ast::Node<ast::Program>,
3897 ) -> Result<AstNodeRef, KclError> {
3898 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3899 .await
3900 }
3901
3902 async fn add_lines_at_angle_constraint(
3903 &mut self,
3904 sketch: ObjectId,
3905 angle_kind: LinesAtAngleKind,
3906 lines: Vec<ObjectId>,
3907 new_ast: &mut ast::Node<ast::Program>,
3908 ) -> Result<AstNodeRef, KclError> {
3909 let &[line0_id, line1_id] = lines.as_slice() else {
3910 return Err(KclError::refactor(format!(
3911 "{} constraint must have exactly 2 lines, got {}",
3912 angle_kind.to_function_name(),
3913 lines.len()
3914 )));
3915 };
3916
3917 let sketch_id = sketch;
3918
3919 let line0_object = self
3921 .scene_graph
3922 .objects
3923 .get(line0_id.0)
3924 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3925 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3926 let kind = line0_object.kind.human_friendly_kind_with_article();
3927 return Err(KclError::refactor(format!(
3928 "This constraint only works on Segments, but you selected {kind}"
3929 )));
3930 };
3931 let Segment::Line(_) = line0_segment else {
3932 return Err(KclError::refactor(format!(
3933 "Only lines can be made {}, but you selected {}",
3934 angle_kind.to_function_name(),
3935 line0_segment.human_friendly_kind_with_article(),
3936 )));
3937 };
3938 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3939
3940 let line1_object = self
3941 .scene_graph
3942 .objects
3943 .get(line1_id.0)
3944 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3945 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3946 let kind = line1_object.kind.human_friendly_kind_with_article();
3947 return Err(KclError::refactor(format!(
3948 "This constraint only works on Segments, but you selected {kind}"
3949 )));
3950 };
3951 let Segment::Line(_) = line1_segment else {
3952 return Err(KclError::refactor(format!(
3953 "Only lines can be made {}, but you selected {}",
3954 angle_kind.to_function_name(),
3955 line1_segment.human_friendly_kind_with_article(),
3956 )));
3957 };
3958 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3959
3960 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3962 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3963 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3964 ast::ArrayExpression {
3965 elements: vec![line0_ast, line1_ast],
3966 digest: None,
3967 non_code_meta: Default::default(),
3968 },
3969 )))),
3970 arguments: Default::default(),
3971 digest: None,
3972 non_code_meta: Default::default(),
3973 })));
3974
3975 let (sketch_block_ref, _) = self.mutate_ast(
3977 new_ast,
3978 sketch_id,
3979 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3980 )?;
3981 Ok(sketch_block_ref)
3982 }
3983
3984 async fn add_vertical(
3985 &mut self,
3986 sketch: ObjectId,
3987 vertical: Vertical,
3988 new_ast: &mut ast::Node<ast::Program>,
3989 ) -> Result<AstNodeRef, KclError> {
3990 let sketch_id = sketch;
3991
3992 let first_arg_ast = match vertical {
3993 Vertical::Line { line } => {
3994 let line_object = self
3996 .scene_graph
3997 .objects
3998 .get(line.0)
3999 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4000 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4001 let kind = line_object.kind.human_friendly_kind_with_article();
4002 return Err(KclError::refactor(format!(
4003 "This constraint only works on Segments, but you selected {kind}"
4004 )));
4005 };
4006 let Segment::Line(_) = line_segment else {
4007 return Err(KclError::refactor(format!(
4008 "Only lines can be made vertical, but you selected {}",
4009 line_segment.human_friendly_kind_with_article()
4010 )));
4011 };
4012 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
4013 }
4014 Vertical::Points { points } => {
4015 let point_asts = points
4016 .iter()
4017 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4018 .collect::<Result<Vec<_>, _>>()?;
4019 ast::ArrayExpression::new(point_asts).into()
4020 }
4021 };
4022
4023 let vertical_ast = create_vertical_ast(first_arg_ast);
4025
4026 let (sketch_block_ref, _) = self.mutate_ast(
4028 new_ast,
4029 sketch_id,
4030 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4031 )?;
4032 Ok(sketch_block_ref)
4033 }
4034
4035 async fn execute_after_add_constraint(
4036 &mut self,
4037 ctx: &ExecutorContext,
4038 sketch_id: ObjectId,
4039 sketch_block_ref: AstNodeRef,
4040 new_ast: &mut ast::Node<ast::Program>,
4041 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4042 let new_source = source_from_ast(new_ast);
4044 let (new_program, errors) = Program::parse(&new_source)
4046 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4047 if !errors.is_empty() {
4048 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4049 "Error parsing KCL source after adding constraint: {errors:?}"
4050 ))));
4051 }
4052 let Some(new_program) = new_program else {
4053 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4054 "No AST produced after adding constraint".to_string(),
4055 )));
4056 };
4057 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4058 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4059 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4060 )))
4061 })?;
4062
4063 let mut truncated_program = new_program.clone();
4066 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4067 .map_err(KclErrorWithOutputs::no_outputs)?;
4068
4069 let outcome = ctx
4071 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4072 .await?;
4073
4074 let new_object_ids = {
4075 let constraint_id = outcome
4077 .source_range_to_object
4078 .get(&constraint_node_ref.range)
4079 .copied()
4080 .ok_or_else(|| {
4081 KclErrorWithOutputs::from_error_outcome(
4082 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4083 outcome.clone(),
4084 )
4085 })?;
4086 vec![constraint_id]
4087 };
4088
4089 self.program = new_program;
4092
4093 let outcome = self.update_state_after_exec(outcome, true);
4095
4096 let src_delta = SourceDelta { text: new_source };
4097 let scene_graph_delta = SceneGraphDelta {
4098 new_graph: self.scene_graph.clone(),
4099 invalidates_ids: false,
4100 new_objects: new_object_ids,
4101 exec_outcome: outcome,
4102 };
4103 Ok((src_delta, scene_graph_delta))
4104 }
4105
4106 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4108 if segment_ids_set.contains(&segment_id) {
4109 return true;
4110 }
4111
4112 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4113 return false;
4114 };
4115 let ObjectKind::Segment { segment } = &segment_object.kind else {
4116 return false;
4117 };
4118 let Segment::Point(point) = segment else {
4119 return false;
4120 };
4121
4122 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4123 }
4124
4125 fn remaining_constraint_segments(
4126 &self,
4127 segments: &[ConstraintSegment],
4128 segment_ids_set: &AhashIndexSet<ObjectId>,
4129 ) -> Vec<ConstraintSegment> {
4130 segments
4131 .iter()
4132 .copied()
4133 .filter(|segment| match segment {
4134 ConstraintSegment::Origin(_) => true,
4135 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4136 })
4137 .collect()
4138 }
4139
4140 fn find_referenced_constraints(
4141 &self,
4142 sketch_id: ObjectId,
4143 segment_ids_set: &AhashIndexSet<ObjectId>,
4144 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4145 let sketch_object = self
4147 .scene_graph
4148 .objects
4149 .get(sketch_id.0)
4150 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4151 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4152 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4153 };
4154 let mut constraint_ids_set = AhashIndexSet::default();
4155 for constraint_id in &sketch.constraints {
4156 let constraint_object = self
4157 .scene_graph
4158 .objects
4159 .get(constraint_id.0)
4160 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4161 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4162 return Err(KclError::refactor(format!(
4163 "Object is not a constraint, it is {}",
4164 constraint_object.kind.human_friendly_kind_with_article()
4165 )));
4166 };
4167 let depends_on_segment = match constraint {
4168 Constraint::Coincident(c) => c
4169 .segment_ids()
4170 .any(|seg_id| self.segment_will_be_deleted(seg_id, segment_ids_set)),
4171 Constraint::Distance(d) => d
4172 .point_ids()
4173 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4174 Constraint::Fixed(fixed) => fixed
4175 .points
4176 .iter()
4177 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4178 Constraint::Radius(r) => self.segment_will_be_deleted(r.arc, segment_ids_set),
4179 Constraint::Diameter(d) => self.segment_will_be_deleted(d.arc, segment_ids_set),
4180 Constraint::EqualRadius(equal_radius) => equal_radius
4181 .input
4182 .iter()
4183 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4184 Constraint::HorizontalDistance(d) => d
4185 .point_ids()
4186 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4187 Constraint::VerticalDistance(d) => d
4188 .point_ids()
4189 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4190 Constraint::Horizontal(h) => match h {
4191 Horizontal::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4192 Horizontal::Points { points } => points.iter().any(|point| match point {
4193 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4194 ConstraintSegment::Origin(_) => false,
4195 }),
4196 },
4197 Constraint::Vertical(v) => match v {
4198 Vertical::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4199 Vertical::Points { points } => points.iter().any(|point| match point {
4200 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4201 ConstraintSegment::Origin(_) => false,
4202 }),
4203 },
4204 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4205 .lines
4206 .iter()
4207 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4208 Constraint::Midpoint(midpoint) => {
4209 self.segment_will_be_deleted(midpoint.segment, segment_ids_set)
4210 || self.segment_will_be_deleted(midpoint.point, segment_ids_set)
4211 }
4212 Constraint::Parallel(parallel) => parallel
4213 .lines
4214 .iter()
4215 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4216 Constraint::Perpendicular(perpendicular) => perpendicular
4217 .lines
4218 .iter()
4219 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4220 Constraint::Angle(angle) => angle
4221 .lines
4222 .iter()
4223 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4224 Constraint::Symmetric(symmetric) => {
4225 self.segment_will_be_deleted(symmetric.axis, segment_ids_set)
4226 || symmetric
4227 .input
4228 .iter()
4229 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set))
4230 }
4231 Constraint::Tangent(tangent) => tangent
4232 .input
4233 .iter()
4234 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4235 };
4236 if depends_on_segment {
4237 constraint_ids_set.insert(*constraint_id);
4238 }
4239 }
4240 Ok(constraint_ids_set)
4241 }
4242
4243 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4244 let mut outcome = outcome;
4245 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4246
4247 if freedom_analysis_ran {
4248 self.point_freedom_cache.clear();
4251 for new_obj in &new_objects {
4252 if let ObjectKind::Segment {
4253 segment: crate::front::Segment::Point(point),
4254 } = &new_obj.kind
4255 {
4256 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4257 }
4258 }
4259 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4260 self.scene_graph.objects = new_objects;
4262 } else {
4263 for old_obj in &self.scene_graph.objects {
4266 if let ObjectKind::Segment {
4267 segment: crate::front::Segment::Point(point),
4268 } = &old_obj.kind
4269 {
4270 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4271 }
4272 }
4273
4274 let mut updated_objects = Vec::with_capacity(new_objects.len());
4276 for new_obj in new_objects {
4277 let mut obj = new_obj;
4278 if let ObjectKind::Segment {
4279 segment: crate::front::Segment::Point(point),
4280 } = &mut obj.kind
4281 {
4282 let new_freedom = point.freedom;
4283 match new_freedom {
4289 Freedom::Free => {
4290 match self.point_freedom_cache.get(&obj.id).copied() {
4291 Some(Freedom::Conflict) => {
4292 }
4295 Some(Freedom::Fixed) => {
4296 point.freedom = Freedom::Fixed;
4298 }
4299 Some(Freedom::Free) => {
4300 }
4302 None => {
4303 }
4305 }
4306 }
4307 Freedom::Fixed => {
4308 }
4310 Freedom::Conflict => {
4311 }
4313 }
4314 self.point_freedom_cache.insert(obj.id, point.freedom);
4316 }
4317 updated_objects.push(obj);
4318 }
4319
4320 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4321 self.scene_graph.objects = updated_objects;
4322 }
4323 outcome
4324 }
4325
4326 fn mutate_ast(
4327 &mut self,
4328 ast: &mut ast::Node<ast::Program>,
4329 object_id: ObjectId,
4330 command: AstMutateCommand,
4331 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4332 let sketch_object = self
4333 .scene_graph
4334 .objects
4335 .get(object_id.0)
4336 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4337 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4338 }
4339}
4340
4341fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4342 let sketch_object = scene_graph
4344 .objects
4345 .get(sketch_id.0)
4346 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4347 let ObjectKind::Sketch(_) = &sketch_object.kind else {
4348 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4349 };
4350 expect_single_node_ref(sketch_object)
4351}
4352
4353fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4354 match &object.source {
4355 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4356 range: *range,
4357 node_path: node_path.clone(),
4358 }),
4359 SourceRef::BackTrace { ranges } => {
4360 let [range] = ranges.as_slice() else {
4361 return Err(KclError::refactor(format!(
4362 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4363 ranges.len()
4364 )));
4365 };
4366 Ok(AstNodeRef {
4367 range: range.0,
4368 node_path: range.1.clone(),
4369 })
4370 }
4371 }
4372}
4373
4374fn only_sketch_block_from_range(
4377 ast: &mut ast::Node<ast::Program>,
4378 sketch_block_range: SourceRange,
4379 edit_kind: ChangeKind,
4380) -> Result<(), KclError> {
4381 let r1 = sketch_block_range;
4382 let matches_range = |r2: SourceRange| -> bool {
4383 match edit_kind {
4386 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4387 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4389 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4390 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4392 }
4393 };
4394 let mut found = false;
4395 for item in ast.body.iter_mut() {
4396 match item {
4397 ast::BodyItem::ImportStatement(_) => {}
4398 ast::BodyItem::ExpressionStatement(node) => {
4399 if matches_range(SourceRange::from(&*node))
4400 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4401 {
4402 sketch_block.is_being_edited = true;
4403 found = true;
4404 break;
4405 }
4406 }
4407 ast::BodyItem::VariableDeclaration(node) => {
4408 if matches_range(SourceRange::from(&node.declaration.init))
4409 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4410 {
4411 sketch_block.is_being_edited = true;
4412 found = true;
4413 break;
4414 }
4415 }
4416 ast::BodyItem::TypeDeclaration(_) => {}
4417 ast::BodyItem::ReturnStatement(node) => {
4418 if matches_range(SourceRange::from(&node.argument))
4419 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4420 {
4421 sketch_block.is_being_edited = true;
4422 found = true;
4423 break;
4424 }
4425 }
4426 }
4427 }
4428 if !found {
4429 return Err(KclError::refactor(format!(
4430 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4431 )));
4432 }
4433
4434 Ok(())
4435}
4436
4437fn only_sketch_block(
4438 ast: &mut ast::Node<ast::Program>,
4439 sketch_block_ref: &AstNodeRef,
4440 edit_kind: ChangeKind,
4441) -> Result<(), KclError> {
4442 let Some(target_node_path) = &sketch_block_ref.node_path else {
4443 #[cfg(target_arch = "wasm32")]
4444 web_sys::console::warn_1(
4445 &format!(
4446 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4447 &sketch_block_ref
4448 )
4449 .into(),
4450 );
4451 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4452 };
4453 let mut found = false;
4454 for item in ast.body.iter_mut() {
4455 match item {
4456 ast::BodyItem::ImportStatement(_) => {}
4457 ast::BodyItem::ExpressionStatement(node) => {
4458 if let Some(node_path) = &node.node_path
4460 && node_path == target_node_path
4461 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4462 {
4463 sketch_block.is_being_edited = true;
4464 found = true;
4465 break;
4466 }
4467 if let Some(node_path) = node.expression.node_path()
4469 && node_path == target_node_path
4470 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4471 {
4472 sketch_block.is_being_edited = true;
4473 found = true;
4474 break;
4475 }
4476 }
4477 ast::BodyItem::VariableDeclaration(node) => {
4478 if let Some(node_path) = node.declaration.init.node_path()
4479 && node_path == target_node_path
4480 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4481 {
4482 sketch_block.is_being_edited = true;
4483 found = true;
4484 break;
4485 }
4486 }
4487 ast::BodyItem::TypeDeclaration(_) => {}
4488 ast::BodyItem::ReturnStatement(node) => {
4489 if let Some(node_path) = node.argument.node_path()
4490 && node_path == target_node_path
4491 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4492 {
4493 sketch_block.is_being_edited = true;
4494 found = true;
4495 break;
4496 }
4497 }
4498 }
4499 }
4500 if !found {
4501 return Err(KclError::refactor(format!(
4502 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4503 )));
4504 }
4505
4506 Ok(())
4507}
4508
4509fn sketch_on_ast_expr(
4510 ast: &mut ast::Node<ast::Program>,
4511 scene_graph: &SceneGraph,
4512 on: &Plane,
4513) -> Result<ast::Expr, KclError> {
4514 match on {
4515 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4516 Plane::Object(object_id) => {
4517 let on_object = scene_graph
4518 .objects
4519 .get(object_id.0)
4520 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4521 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4522 return Ok(face_expr);
4523 }
4524 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4525 }
4526 }
4527}
4528
4529fn sketch_face_of_scene_object_ast_expr(
4530 ast: &mut ast::Node<ast::Program>,
4531 on_object: &crate::front::Object,
4532) -> Result<Option<ast::Expr>, KclError> {
4533 let SourceRef::BackTrace { ranges } = &on_object.source else {
4534 return Ok(None);
4535 };
4536
4537 match &on_object.kind {
4538 ObjectKind::Wall(_) => {
4539 let [sweep_range, segment_range] = ranges.as_slice() else {
4540 return Err(KclError::refactor(format!(
4541 "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4542 ranges.len(),
4543 on_object.artifact_id
4544 )));
4545 };
4546 let sweep_ref = get_or_insert_ast_reference(
4547 ast,
4548 &SourceRef::Simple {
4549 range: sweep_range.0,
4550 node_path: sweep_range.1.clone(),
4551 },
4552 "solid",
4553 None,
4554 )?;
4555 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4556 return Err(KclError::refactor(format!(
4557 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4558 on_object.artifact_id
4559 )));
4560 };
4561 let solid_name = solid_name_expr.name.name.clone();
4562 let solid_expr = ast_name_expr(solid_name.clone());
4563 let segment_ref = get_or_insert_ast_reference(
4564 ast,
4565 &SourceRef::Simple {
4566 range: segment_range.0,
4567 node_path: segment_range.1.clone(),
4568 },
4569 LINE_VARIABLE,
4570 None,
4571 )?;
4572
4573 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4574 let ast::Expr::Name(segment_name_expr) = segment_ref else {
4575 return Err(KclError::refactor(format!(
4576 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4577 on_object.artifact_id
4578 )));
4579 };
4580 create_member_expression(
4581 create_member_expression(ast_name_expr(region_name), "tags"),
4582 &segment_name_expr.name.name,
4583 )
4584 } else {
4585 segment_ref
4586 };
4587
4588 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4589 }
4590 ObjectKind::Cap(cap) => {
4591 let [range] = ranges.as_slice() else {
4592 return Err(KclError::refactor(format!(
4593 "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4594 ranges.len(),
4595 on_object.artifact_id
4596 )));
4597 };
4598 let sweep_ref = get_or_insert_ast_reference(
4599 ast,
4600 &SourceRef::Simple {
4601 range: range.0,
4602 node_path: range.1.clone(),
4603 },
4604 "solid",
4605 None,
4606 )?;
4607 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4608 return Err(KclError::refactor(format!(
4609 "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4610 on_object.artifact_id
4611 )));
4612 };
4613 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4614 let face_expr = match cap.kind {
4616 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4617 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4618 };
4619
4620 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4621 }
4622 _ => Ok(None),
4623 }
4624}
4625
4626fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4627 let mut existing_artifact_ids = scene_objects
4628 .iter()
4629 .map(|object| object.artifact_id)
4630 .collect::<HashSet<_>>();
4631
4632 for artifact in artifact_graph.values() {
4633 match artifact {
4634 Artifact::Wall(wall) => {
4635 if existing_artifact_ids.contains(&wall.id) {
4636 continue;
4637 }
4638
4639 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4640 Artifact::Segment(segment) => Some(segment),
4641 _ => None,
4642 }) else {
4643 continue;
4644 };
4645 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4646 Artifact::Sweep(sweep) => Some(sweep),
4647 _ => None,
4648 }) else {
4649 continue;
4650 };
4651 let source_segment = segment
4652 .original_seg_id
4653 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4654 .and_then(|artifact| match artifact {
4655 Artifact::Segment(segment) => Some(segment),
4656 _ => None,
4657 })
4658 .unwrap_or(segment);
4659 let id = ObjectId(scene_objects.len());
4660 scene_objects.push(crate::front::Object {
4661 id,
4662 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4663 label: Default::default(),
4664 comments: Default::default(),
4665 artifact_id: wall.id,
4666 source: SourceRef::BackTrace {
4667 ranges: vec![
4668 (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4669 (
4670 source_segment.code_ref.range,
4671 Some(source_segment.code_ref.node_path.clone()),
4672 ),
4673 ],
4674 },
4675 });
4676 existing_artifact_ids.insert(wall.id);
4677 }
4678 Artifact::Cap(cap) => {
4679 if existing_artifact_ids.contains(&cap.id) {
4680 continue;
4681 }
4682
4683 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4684 Artifact::Sweep(sweep) => Some(sweep),
4685 _ => None,
4686 }) else {
4687 continue;
4688 };
4689 let id = ObjectId(scene_objects.len());
4690 let kind = match cap.sub_type {
4691 CapSubType::Start => crate::frontend::api::CapKind::Start,
4692 CapSubType::End => crate::frontend::api::CapKind::End,
4693 };
4694 scene_objects.push(crate::front::Object {
4695 id,
4696 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4697 label: Default::default(),
4698 comments: Default::default(),
4699 artifact_id: cap.id,
4700 source: SourceRef::BackTrace {
4701 ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4702 },
4703 });
4704 existing_artifact_ids.insert(cap.id);
4705 }
4706 _ => {}
4707 }
4708 }
4709}
4710
4711fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4712 use crate::engine::PlaneName;
4713
4714 match name {
4715 PlaneName::Xy => ast_name_expr("XY".to_owned()),
4716 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4717 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4718 PlaneName::NegXy => negated_plane_ast_expr("XY"),
4719 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4720 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4721 }
4722}
4723
4724fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4725 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4726 ast::UnaryOperator::Neg,
4727 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4728 )))
4729}
4730
4731fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4732 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4733 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4734 unlabeled: Some(solid_expr),
4735 arguments: vec![ast::LabeledArg {
4736 label: Some(ast::Identifier::new("face")),
4737 arg: face_expr,
4738 }],
4739 digest: None,
4740 non_code_meta: Default::default(),
4741 })))
4742}
4743
4744fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4745 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4746 return None;
4747 };
4748 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4749 return None;
4750 };
4751 if !matches!(
4752 sweep_call.callee.name.name.as_str(),
4753 "extrude" | "revolve" | "sweep" | "loft"
4754 ) {
4755 return None;
4756 }
4757 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4758 return None;
4759 };
4760 let candidate = region_name_expr.name.name.clone();
4761 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4762 return None;
4763 };
4764 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
4765 return None;
4766 };
4767 if region_call.callee.name.name != "region" {
4768 return None;
4769 }
4770 Some(candidate)
4771}
4772
4773fn get_or_insert_ast_reference(
4780 ast: &mut ast::Node<ast::Program>,
4781 source_ref: &SourceRef,
4782 prefix: &str,
4783 property: Option<&str>,
4784) -> Result<ast::Expr, KclError> {
4785 let command = AstMutateCommand::AddVariableDeclaration {
4786 prefix: prefix.to_owned(),
4787 };
4788 let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
4789 let AstMutateCommandReturn::Name(var_name) = ret else {
4790 return Err(KclError::refactor(
4791 "Expected variable name returned from AddVariableDeclaration".to_owned(),
4792 ));
4793 };
4794 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4795 let Some(property) = property else {
4796 return Ok(var_expr);
4798 };
4799
4800 Ok(create_member_expression(var_expr, property))
4801}
4802
4803fn mutate_ast_node_by_source_ref(
4804 ast: &mut ast::Node<ast::Program>,
4805 source_ref: &SourceRef,
4806 command: AstMutateCommand,
4807) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4808 let (source_range, node_path) = match source_ref {
4809 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
4810 SourceRef::BackTrace { ranges } => {
4811 let [range] = ranges.as_slice() else {
4812 return Err(KclError::refactor(format!(
4813 "Expected single source ref, got {}; ranges={ranges:#?}",
4814 ranges.len(),
4815 )));
4816 };
4817 (range.0, range.1.clone())
4818 }
4819 };
4820 let mut context = AstMutateContext {
4821 source_range,
4822 node_path,
4823 command,
4824 defined_names_stack: Default::default(),
4825 };
4826 let control = dfs_mut(ast, &mut context);
4827 match control {
4828 ControlFlow::Continue(_) => Err(KclError::refactor(
4829 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
4830 )),
4831 ControlFlow::Break(break_value) => break_value,
4832 }
4833}
4834
4835#[derive(Debug)]
4836struct AstMutateContext {
4837 source_range: SourceRange,
4838 node_path: Option<ast::NodePath>,
4839 command: AstMutateCommand,
4840 defined_names_stack: Vec<HashSet<String>>,
4841}
4842
4843#[derive(Debug)]
4844#[allow(clippy::large_enum_variant)]
4845enum AstMutateCommand {
4846 AddSketchBlockExprStmt {
4848 expr: ast::Expr,
4849 },
4850 AddSketchBlockVarDecl {
4852 prefix: String,
4853 expr: ast::Expr,
4854 },
4855 AddVariableDeclaration {
4856 prefix: String,
4857 },
4858 EditPoint {
4859 at: ast::Expr,
4860 },
4861 EditLine {
4862 start: ast::Expr,
4863 end: ast::Expr,
4864 construction: Option<bool>,
4865 },
4866 EditArc {
4867 start: ast::Expr,
4868 end: ast::Expr,
4869 center: ast::Expr,
4870 construction: Option<bool>,
4871 },
4872 EditCircle {
4873 start: ast::Expr,
4874 center: ast::Expr,
4875 construction: Option<bool>,
4876 },
4877 EditConstraintValue {
4878 value: ast::BinaryPart,
4879 },
4880 EditDistanceConstraintLabelPosition {
4881 label_position: ast::Expr,
4882 },
4883 EditCallUnlabeled {
4884 arg: ast::Expr,
4885 },
4886 EditVarInitialValue {
4887 value: Number,
4888 },
4889 DeleteNode,
4890}
4891
4892impl AstMutateCommand {
4893 fn needs_defined_names_stack(&self) -> bool {
4894 matches!(
4895 self,
4896 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4897 )
4898 }
4899}
4900
4901#[derive(Debug)]
4902enum AstMutateCommandReturn {
4903 None,
4904 Name(String),
4905}
4906
4907#[derive(Debug, Clone)]
4908struct AstNodeRef {
4909 range: SourceRange,
4910 node_path: Option<ast::NodePath>,
4911}
4912
4913impl<T> From<&ast::Node<T>> for AstNodeRef {
4914 fn from(value: &ast::Node<T>) -> Self {
4915 AstNodeRef {
4916 range: value.into(),
4917 node_path: value.node_path.clone(),
4918 }
4919 }
4920}
4921
4922impl From<&ast::BodyItem> for AstNodeRef {
4923 fn from(value: &ast::BodyItem) -> Self {
4924 match value {
4925 ast::BodyItem::ImportStatement(node) => AstNodeRef {
4926 range: node.into(),
4927 node_path: node.node_path.clone(),
4928 },
4929 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4930 range: node.into(),
4931 node_path: node.node_path.clone(),
4932 },
4933 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4934 range: node.into(),
4935 node_path: node.node_path.clone(),
4936 },
4937 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4938 range: node.into(),
4939 node_path: node.node_path.clone(),
4940 },
4941 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
4942 range: node.into(),
4943 node_path: node.node_path.clone(),
4944 },
4945 }
4946 }
4947}
4948
4949impl From<&ast::Expr> for AstNodeRef {
4950 fn from(value: &ast::Expr) -> Self {
4951 AstNodeRef {
4952 range: SourceRange::from(value),
4953 node_path: value.node_path().cloned(),
4954 }
4955 }
4956}
4957
4958impl From<&AstMutateContext> for AstNodeRef {
4959 fn from(value: &AstMutateContext) -> Self {
4960 AstNodeRef {
4961 range: value.source_range,
4962 node_path: value.node_path.clone(),
4963 }
4964 }
4965}
4966
4967impl TryFrom<&NodeMut<'_>> for AstNodeRef {
4968 type Error = crate::walk::AstNodeError;
4969
4970 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
4971 Ok(AstNodeRef {
4972 range: SourceRange::try_from(value)?,
4973 node_path: value.try_into()?,
4974 })
4975 }
4976}
4977
4978impl From<AstNodeRef> for SourceRange {
4979 fn from(value: AstNodeRef) -> Self {
4980 value.range
4981 }
4982}
4983
4984impl Visitor for AstMutateContext {
4985 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
4986 type Continue = ();
4987
4988 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
4989 filter_and_process(self, node)
4990 }
4991
4992 fn finish(&mut self, node: NodeMut<'_>) {
4993 match &node {
4994 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
4995 self.defined_names_stack.pop();
4996 }
4997 _ => {}
4998 }
4999 }
5000}
5001
5002fn filter_and_process(
5003 ctx: &mut AstMutateContext,
5004 node: NodeMut,
5005) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5006 let Ok(node_range) = SourceRange::try_from(&node) else {
5007 return TraversalReturn::new_continue(());
5009 };
5010 if let NodeMut::VariableDeclaration(var_decl) = &node {
5015 let expr_range = SourceRange::from(&var_decl.declaration.init);
5016 let expr_node_path = var_decl.declaration.init.node_path();
5017 if source_ref_matches(ctx, expr_range, expr_node_path) {
5018 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5019 return TraversalReturn::new_break(Ok((
5022 AstNodeRef::from(&**var_decl),
5023 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5024 )));
5025 }
5026 if let AstMutateCommand::DeleteNode = &ctx.command {
5027 return TraversalReturn {
5030 mutate_body_item: MutateBodyItem::Delete,
5031 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5032 };
5033 }
5034 }
5035 }
5036 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5039 let expr_range = SourceRange::from(&expr_stmt.expression);
5040 let expr_node_path = expr_stmt.expression.node_path();
5041 if source_ref_matches(ctx, expr_range, expr_node_path) {
5042 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5043 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5046 return TraversalReturn::new_continue(());
5047 };
5048 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5049 }
5050 if let AstMutateCommand::DeleteNode = &ctx.command {
5051 return TraversalReturn {
5054 mutate_body_item: MutateBodyItem::Delete,
5055 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5056 };
5057 }
5058 }
5059 }
5060
5061 if ctx.command.needs_defined_names_stack() {
5062 if let NodeMut::Program(program) = &node {
5063 ctx.defined_names_stack.push(find_defined_names(*program));
5064 } else if let NodeMut::SketchBlock(block) = &node {
5065 ctx.defined_names_stack.push(find_defined_names(&block.body));
5066 }
5067 }
5068
5069 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5071 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5072 return TraversalReturn::new_continue(());
5073 }
5074 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5075 return TraversalReturn::new_continue(());
5076 };
5077 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5078}
5079
5080fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5081 match &ctx.node_path {
5082 Some(target) => Some(target) == node_path,
5083 None => node_range == ctx.source_range,
5084 }
5085}
5086
5087fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5088 match &ctx.command {
5089 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5090 if let NodeMut::SketchBlock(sketch_block) = node {
5091 sketch_block
5092 .body
5093 .items
5094 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5095 inner: ast::ExpressionStatement {
5096 expression: expr.clone(),
5097 digest: None,
5098 },
5099 start: Default::default(),
5100 end: Default::default(),
5101 module_id: Default::default(),
5102 node_path: None,
5103 outer_attrs: Default::default(),
5104 pre_comments: Default::default(),
5105 comment_start: Default::default(),
5106 }));
5107 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5108 }
5109 }
5110 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5111 if let NodeMut::SketchBlock(sketch_block) = node {
5112 let empty_defined_names = HashSet::new();
5113 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5114 let Ok(name) = next_free_name(prefix, defined_names) else {
5115 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5116 };
5117 sketch_block
5118 .body
5119 .items
5120 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5121 ast::VariableDeclaration::new(
5122 ast::VariableDeclarator::new(&name, expr.clone()),
5123 ast::ItemVisibility::Default,
5124 ast::VariableKind::Const,
5125 ),
5126 ))));
5127 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5128 }
5129 }
5130 AstMutateCommand::AddVariableDeclaration { prefix } => {
5131 if let NodeMut::VariableDeclaration(inner) = node {
5132 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5133 }
5134 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5135 let empty_defined_names = HashSet::new();
5136 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5137 let Ok(name) = next_free_name(prefix, defined_names) else {
5138 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5140 };
5141 let mutate_node =
5142 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5143 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5144 ast::ItemVisibility::Default,
5145 ast::VariableKind::Const,
5146 ))));
5147 return TraversalReturn {
5148 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5149 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5150 };
5151 }
5152 }
5153 AstMutateCommand::EditPoint { at } => {
5154 if let NodeMut::CallExpressionKw(call) = node {
5155 if call.callee.name.name != POINT_FN {
5156 return TraversalReturn::new_continue(());
5157 }
5158 for labeled_arg in &mut call.arguments {
5160 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5161 labeled_arg.arg = at.clone();
5162 }
5163 }
5164 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5165 }
5166 }
5167 AstMutateCommand::EditLine {
5168 start,
5169 end,
5170 construction,
5171 } => {
5172 if let NodeMut::CallExpressionKw(call) = node {
5173 if call.callee.name.name != LINE_FN {
5174 return TraversalReturn::new_continue(());
5175 }
5176 for labeled_arg in &mut call.arguments {
5178 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5179 labeled_arg.arg = start.clone();
5180 }
5181 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5182 labeled_arg.arg = end.clone();
5183 }
5184 }
5185 if let Some(construction_value) = construction {
5187 let construction_exists = call
5188 .arguments
5189 .iter()
5190 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5191 if *construction_value {
5192 if construction_exists {
5194 for labeled_arg in &mut call.arguments {
5196 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5197 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5198 value: ast::LiteralValue::Bool(true),
5199 raw: "true".to_string(),
5200 digest: None,
5201 })));
5202 }
5203 }
5204 } else {
5205 call.arguments.push(ast::LabeledArg {
5207 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5208 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5209 value: ast::LiteralValue::Bool(true),
5210 raw: "true".to_string(),
5211 digest: None,
5212 }))),
5213 });
5214 }
5215 } else {
5216 call.arguments
5218 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5219 }
5220 }
5221 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5222 }
5223 }
5224 AstMutateCommand::EditArc {
5225 start,
5226 end,
5227 center,
5228 construction,
5229 } => {
5230 if let NodeMut::CallExpressionKw(call) = node {
5231 if call.callee.name.name != ARC_FN {
5232 return TraversalReturn::new_continue(());
5233 }
5234 for labeled_arg in &mut call.arguments {
5236 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5237 labeled_arg.arg = start.clone();
5238 }
5239 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5240 labeled_arg.arg = end.clone();
5241 }
5242 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5243 labeled_arg.arg = center.clone();
5244 }
5245 }
5246 if let Some(construction_value) = construction {
5248 let construction_exists = call
5249 .arguments
5250 .iter()
5251 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5252 if *construction_value {
5253 if construction_exists {
5255 for labeled_arg in &mut call.arguments {
5257 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5258 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5259 value: ast::LiteralValue::Bool(true),
5260 raw: "true".to_string(),
5261 digest: None,
5262 })));
5263 }
5264 }
5265 } else {
5266 call.arguments.push(ast::LabeledArg {
5268 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5269 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5270 value: ast::LiteralValue::Bool(true),
5271 raw: "true".to_string(),
5272 digest: None,
5273 }))),
5274 });
5275 }
5276 } else {
5277 call.arguments
5279 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5280 }
5281 }
5282 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5283 }
5284 }
5285 AstMutateCommand::EditCircle {
5286 start,
5287 center,
5288 construction,
5289 } => {
5290 if let NodeMut::CallExpressionKw(call) = node {
5291 if call.callee.name.name != CIRCLE_FN {
5292 return TraversalReturn::new_continue(());
5293 }
5294 for labeled_arg in &mut call.arguments {
5296 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5297 labeled_arg.arg = start.clone();
5298 }
5299 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5300 labeled_arg.arg = center.clone();
5301 }
5302 }
5303 if let Some(construction_value) = construction {
5305 let construction_exists = call
5306 .arguments
5307 .iter()
5308 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5309 if *construction_value {
5310 if construction_exists {
5311 for labeled_arg in &mut call.arguments {
5312 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5313 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5314 value: ast::LiteralValue::Bool(true),
5315 raw: "true".to_string(),
5316 digest: None,
5317 })));
5318 }
5319 }
5320 } else {
5321 call.arguments.push(ast::LabeledArg {
5322 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5323 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5324 value: ast::LiteralValue::Bool(true),
5325 raw: "true".to_string(),
5326 digest: None,
5327 }))),
5328 });
5329 }
5330 } else {
5331 call.arguments
5332 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5333 }
5334 }
5335 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5336 }
5337 }
5338 AstMutateCommand::EditConstraintValue { value } => {
5339 if let NodeMut::BinaryExpression(binary_expr) = node {
5340 let left_is_constraint = matches!(
5341 &binary_expr.left,
5342 ast::BinaryPart::CallExpressionKw(call)
5343 if matches!(
5344 call.callee.name.name.as_str(),
5345 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5346 )
5347 );
5348 if left_is_constraint {
5349 binary_expr.right = value.clone();
5350 } else {
5351 binary_expr.left = value.clone();
5352 }
5353
5354 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5355 }
5356 }
5357 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5358 if let NodeMut::BinaryExpression(binary_expr) = node {
5359 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5360 return TraversalReturn::new_continue(());
5361 };
5362 if !matches!(
5363 call.callee.name.name.as_str(),
5364 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
5365 ) {
5366 return TraversalReturn::new_continue(());
5367 }
5368
5369 if let Some(label_arg) = call
5370 .arguments
5371 .iter_mut()
5372 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5373 {
5374 label_arg.arg = label_position.clone();
5375 } else {
5376 call.arguments.push(ast::LabeledArg {
5377 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5378 arg: label_position.clone(),
5379 });
5380 }
5381
5382 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5383 }
5384 }
5385 AstMutateCommand::EditCallUnlabeled { arg } => {
5386 if let NodeMut::CallExpressionKw(call) = node {
5387 call.unlabeled = Some(arg.clone());
5388 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5389 }
5390 }
5391 AstMutateCommand::EditVarInitialValue { value } => {
5392 if let NodeMut::NumericLiteral(numeric_literal) = node {
5393 let Ok(literal) = to_source_number(*value) else {
5395 return TraversalReturn::new_break(Err(KclError::refactor(format!(
5396 "Could not convert number to AST literal: {:?}",
5397 *value
5398 ))));
5399 };
5400 *numeric_literal = ast::Node::no_src(literal);
5401 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5402 }
5403 }
5404 AstMutateCommand::DeleteNode => {
5405 return TraversalReturn {
5406 mutate_body_item: MutateBodyItem::Delete,
5407 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5408 };
5409 }
5410 }
5411 TraversalReturn::new_continue(())
5412}
5413
5414struct FindSketchBlockSourceRange {
5415 target_before_mutation: SourceRange,
5417 found: Cell<Option<AstNodeRef>>,
5421}
5422
5423impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5424 type Error = crate::front::Error;
5425
5426 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5427 let Ok(node_range) = SourceRange::try_from(&node) else {
5428 return Ok(true);
5429 };
5430
5431 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5432 if node_range.module_id() == self.target_before_mutation.module_id()
5433 && node_range.start() == self.target_before_mutation.start()
5434 && node_range.end() >= self.target_before_mutation.end()
5436 {
5437 self.found.set(sketch_block.body.items.last().map(|item| match item {
5438 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5442 _ => AstNodeRef::from(item),
5443 }));
5444 return Ok(false);
5445 } else {
5446 return Ok(true);
5449 }
5450 }
5451
5452 for child in node.children().iter() {
5453 if !child.visit(*self)? {
5454 return Ok(false);
5455 }
5456 }
5457
5458 Ok(true)
5459 }
5460}
5461
5462struct FindSketchBlockByNodePath {
5463 target_node_path: ast::NodePath,
5465 found: Cell<Option<AstNodeRef>>,
5469}
5470
5471impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5472 type Error = crate::front::Error;
5473
5474 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5475 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5476 return Ok(true);
5477 };
5478
5479 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5480 if let Some(node_path) = node_path
5481 && node_path == self.target_node_path
5482 {
5483 self.found.set(sketch_block.body.items.last().map(|item| match item {
5484 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5488 _ => AstNodeRef::from(item),
5489 }));
5490
5491 return Ok(false);
5492 } else {
5493 return Ok(true);
5496 }
5497 }
5498
5499 for child in node.children().iter() {
5500 if !child.visit(*self)? {
5501 return Ok(false);
5502 }
5503 }
5504
5505 Ok(true)
5506 }
5507}
5508
5509fn find_sketch_block_added_item(
5517 ast: &ast::Node<ast::Program>,
5518 sketch_block_before_mutation: &AstNodeRef,
5519) -> Result<AstNodeRef, KclError> {
5520 if let Some(node_path) = &sketch_block_before_mutation.node_path {
5521 let find = FindSketchBlockByNodePath {
5522 target_node_path: node_path.clone(),
5523 found: Cell::new(None),
5524 };
5525 let node = crate::walk::Node::from(ast);
5526 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5527 find.found.into_inner().ok_or_else(|| {
5528 KclError::refactor(format!(
5529 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5530 ))
5531 })
5532 } else {
5533 let find = FindSketchBlockSourceRange {
5535 target_before_mutation: sketch_block_before_mutation.range,
5536 found: Cell::new(None),
5537 };
5538 let node = crate::walk::Node::from(ast);
5539 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5540 find.found.into_inner().ok_or_else(|| KclError::refactor(
5541 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?"),
5542 ))
5543 }
5544}
5545
5546fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5547 ast.recast_top(&Default::default(), 0)
5549}
5550
5551pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5552 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5553 inner: ast::ArrayExpression {
5554 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5555 non_code_meta: Default::default(),
5556 digest: None,
5557 },
5558 start: Default::default(),
5559 end: Default::default(),
5560 module_id: Default::default(),
5561 node_path: None,
5562 outer_attrs: Default::default(),
5563 pre_comments: Default::default(),
5564 comment_start: Default::default(),
5565 })))
5566}
5567
5568fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
5569 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
5570 ast::ArrayExpression {
5571 elements: vec![
5572 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5573 point.x,
5574 )?)))),
5575 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5576 point.y,
5577 )?)))),
5578 ],
5579 non_code_meta: Default::default(),
5580 digest: None,
5581 },
5582 ))))
5583}
5584
5585fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5586 match expr {
5587 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5588 inner: ast::Literal::from(to_source_number(*number)?),
5589 start: Default::default(),
5590 end: Default::default(),
5591 module_id: Default::default(),
5592 node_path: None,
5593 outer_attrs: Default::default(),
5594 pre_comments: Default::default(),
5595 comment_start: Default::default(),
5596 }))),
5597 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5598 inner: ast::SketchVar {
5599 initial: Some(Box::new(ast::Node {
5600 inner: to_source_number(*number)?,
5601 start: Default::default(),
5602 end: Default::default(),
5603 module_id: Default::default(),
5604 node_path: None,
5605 outer_attrs: Default::default(),
5606 pre_comments: Default::default(),
5607 comment_start: Default::default(),
5608 })),
5609 digest: None,
5610 },
5611 start: Default::default(),
5612 end: Default::default(),
5613 module_id: Default::default(),
5614 node_path: None,
5615 outer_attrs: Default::default(),
5616 pre_comments: Default::default(),
5617 comment_start: Default::default(),
5618 }))),
5619 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5620 }
5621}
5622
5623fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5624 Ok(ast::NumericLiteral {
5625 value: number.value,
5626 suffix: number.units,
5627 raw: format_number_literal(number.value, number.units, None)?,
5628 digest: None,
5629 })
5630}
5631
5632pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5633 ast::Expr::Name(Box::new(ast_name(name)))
5634}
5635
5636fn ast_name(name: String) -> ast::Node<ast::Name> {
5637 ast::Node {
5638 inner: ast::Name {
5639 name: ast::Node {
5640 inner: ast::Identifier { name, digest: None },
5641 start: Default::default(),
5642 end: Default::default(),
5643 module_id: Default::default(),
5644 node_path: None,
5645 outer_attrs: Default::default(),
5646 pre_comments: Default::default(),
5647 comment_start: Default::default(),
5648 },
5649 path: Vec::new(),
5650 abs_path: false,
5651 digest: None,
5652 },
5653 start: Default::default(),
5654 end: Default::default(),
5655 module_id: Default::default(),
5656 node_path: None,
5657 outer_attrs: Default::default(),
5658 pre_comments: Default::default(),
5659 comment_start: Default::default(),
5660 }
5661}
5662
5663pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5664 ast::Name {
5665 name: ast::Node {
5666 inner: ast::Identifier {
5667 name: name.to_owned(),
5668 digest: None,
5669 },
5670 start: Default::default(),
5671 end: Default::default(),
5672 module_id: Default::default(),
5673 node_path: None,
5674 outer_attrs: Default::default(),
5675 pre_comments: Default::default(),
5676 comment_start: Default::default(),
5677 },
5678 path: Default::default(),
5679 abs_path: false,
5680 digest: None,
5681 }
5682}
5683
5684pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
5688 let elements = exprs.into_iter().collect::<Vec<_>>();
5689 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
5690
5691 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5693 elements,
5694 digest: None,
5695 non_code_meta: Default::default(),
5696 })));
5697
5698 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5700 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5701 unlabeled: Some(array_expr),
5702 arguments: Default::default(),
5703 digest: None,
5704 non_code_meta: Default::default(),
5705 })))
5706}
5707
5708pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5710 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5711 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5712 unlabeled: None,
5713 arguments: vec![
5714 ast::LabeledArg {
5715 label: Some(ast::Identifier::new(LINE_START_PARAM)),
5716 arg: start_ast,
5717 },
5718 ast::LabeledArg {
5719 label: Some(ast::Identifier::new(LINE_END_PARAM)),
5720 arg: end_ast,
5721 },
5722 ],
5723 digest: None,
5724 non_code_meta: Default::default(),
5725 })))
5726}
5727
5728pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5730 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5731 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5732 unlabeled: None,
5733 arguments: vec![
5734 ast::LabeledArg {
5735 label: Some(ast::Identifier::new(ARC_START_PARAM)),
5736 arg: start_ast,
5737 },
5738 ast::LabeledArg {
5739 label: Some(ast::Identifier::new(ARC_END_PARAM)),
5740 arg: end_ast,
5741 },
5742 ast::LabeledArg {
5743 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5744 arg: center_ast,
5745 },
5746 ],
5747 digest: None,
5748 non_code_meta: Default::default(),
5749 })))
5750}
5751
5752pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_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(CIRCLE_FN)),
5756 unlabeled: None,
5757 arguments: vec![
5758 ast::LabeledArg {
5759 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5760 arg: start_ast,
5761 },
5762 ast::LabeledArg {
5763 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5764 arg: center_ast,
5765 },
5766 ],
5767 digest: None,
5768 non_code_meta: Default::default(),
5769 })))
5770}
5771
5772pub(crate) fn create_horizontal_ast(line_expr: 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(HORIZONTAL_FN)),
5776 unlabeled: Some(line_expr),
5777 arguments: Default::default(),
5778 digest: None,
5779 non_code_meta: Default::default(),
5780 })))
5781}
5782
5783pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5785 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5786 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5787 unlabeled: Some(line_expr),
5788 arguments: Default::default(),
5789 digest: None,
5790 non_code_meta: Default::default(),
5791 })))
5792}
5793
5794pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5796 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5797 object: object_expr,
5798 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5799 name: ast::Node::no_src(ast::Identifier {
5800 name: property.to_string(),
5801 digest: None,
5802 }),
5803 path: Vec::new(),
5804 abs_path: false,
5805 digest: None,
5806 }))),
5807 computed: false,
5808 digest: None,
5809 })))
5810}
5811
5812fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5814 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5816 position.x,
5817 )?))));
5818 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5819 position.y,
5820 )?))));
5821 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5822 elements: vec![x_literal, y_literal],
5823 digest: None,
5824 non_code_meta: Default::default(),
5825 })));
5826
5827 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5829 elements: vec![point_expr, point_array],
5830 digest: None,
5831 non_code_meta: Default::default(),
5832 })));
5833
5834 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5836 ast::CallExpressionKw {
5837 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5838 unlabeled: Some(array_expr),
5839 arguments: Default::default(),
5840 digest: None,
5841 non_code_meta: Default::default(),
5842 },
5843 ))))
5844}
5845
5846pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5848 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5849 elements: line_exprs,
5850 digest: None,
5851 non_code_meta: Default::default(),
5852 })));
5853
5854 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5856 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5857 unlabeled: Some(array_expr),
5858 arguments: Default::default(),
5859 digest: None,
5860 non_code_meta: Default::default(),
5861 })))
5862}
5863
5864pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5866 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5867 elements: segment_exprs,
5868 digest: None,
5869 non_code_meta: Default::default(),
5870 })));
5871
5872 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5873 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5874 unlabeled: Some(array_expr),
5875 arguments: Default::default(),
5876 digest: None,
5877 non_code_meta: Default::default(),
5878 })))
5879}
5880
5881pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5883 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5884 elements: vec![seg1_expr, seg2_expr],
5885 digest: None,
5886 non_code_meta: Default::default(),
5887 })));
5888
5889 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5890 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5891 unlabeled: Some(array_expr),
5892 arguments: Default::default(),
5893 digest: None,
5894 non_code_meta: Default::default(),
5895 })))
5896}
5897
5898pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
5900 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5901 elements: input_exprs,
5902 digest: None,
5903 non_code_meta: Default::default(),
5904 })));
5905 let arguments = vec![ast::LabeledArg {
5906 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
5907 arg: axis_expr,
5908 }];
5909
5910 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5911 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
5912 unlabeled: Some(array_expr),
5913 arguments,
5914 digest: None,
5915 non_code_meta: Default::default(),
5916 })))
5917}
5918
5919pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
5921 let arguments = vec![ast::LabeledArg {
5922 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
5923 arg: point_expr,
5924 }];
5925
5926 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5927 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
5928 unlabeled: Some(segment_expr),
5929 arguments,
5930 digest: None,
5931 non_code_meta: Default::default(),
5932 })))
5933}
5934
5935#[cfg(test)]
5936mod tests {
5937 use super::*;
5938 use crate::engine::PlaneName;
5939 use crate::execution::cache::SketchModeState;
5940 use crate::execution::cache::clear_mem_cache;
5941 use crate::execution::cache::read_old_memory;
5942 use crate::execution::cache::write_old_memory;
5943 use crate::front::Distance;
5944 use crate::front::Fixed;
5945 use crate::front::FixedPoint;
5946 use crate::front::Midpoint;
5947 use crate::front::Object;
5948 use crate::front::Plane;
5949 use crate::front::Sketch;
5950 use crate::front::Tangent;
5951 use crate::frontend::sketch::Vertical;
5952 use crate::pretty::NumericSuffix;
5953
5954 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
5955 for object in &scene_graph.objects {
5956 if let ObjectKind::Sketch(_) = &object.kind {
5957 return Some(object);
5958 }
5959 }
5960 None
5961 }
5962
5963 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
5964 for object in &scene_graph.objects {
5965 if let ObjectKind::Face(_) = &object.kind {
5966 return Some(object);
5967 }
5968 }
5969 None
5970 }
5971
5972 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
5973 for object in &scene_graph.objects {
5974 if matches!(&object.kind, ObjectKind::Wall(_)) {
5975 return Some(object.id);
5976 }
5977 }
5978 None
5979 }
5980
5981 #[test]
5982 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
5983 let source = "\
5984region001 = region(point = [0.1, 0.1], sketch = s)
5985extrude001 = extrude(region001, length = 5)
5986revolve001 = revolve(region001, axis = Y)
5987sweep001 = sweep(region001, path = path001)
5988loft001 = loft(region001)
5989not_sweep001 = shell(extrude001, faces = [], thickness = 1)
5990";
5991
5992 let program = Program::parse(source).unwrap().0.unwrap();
5993
5994 assert_eq!(
5995 region_name_from_sweep_variable(&program.ast, "extrude001"),
5996 Some("region001".to_owned())
5997 );
5998 assert_eq!(
5999 region_name_from_sweep_variable(&program.ast, "revolve001"),
6000 Some("region001".to_owned())
6001 );
6002 assert_eq!(
6003 region_name_from_sweep_variable(&program.ast, "sweep001"),
6004 Some("region001".to_owned())
6005 );
6006 assert_eq!(
6007 region_name_from_sweep_variable(&program.ast, "loft001"),
6008 Some("region001".to_owned())
6009 );
6010 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6011 }
6012
6013 #[track_caller]
6014 fn expect_sketch(object: &Object) -> &Sketch {
6015 if let ObjectKind::Sketch(sketch) = &object.kind {
6016 sketch
6017 } else {
6018 panic!("Object is not a sketch: {:?}", object);
6019 }
6020 }
6021
6022 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6023 let point_object = scene_graph.objects.get(point_id.0).unwrap();
6024 let ObjectKind::Segment {
6025 segment: Segment::Point(point),
6026 } = &point_object.kind
6027 else {
6028 panic!("Object is not a point segment: {point_object:?}");
6029 };
6030 point.position.clone()
6031 }
6032
6033 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6034 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6035 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6036 }
6037
6038 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6039 LineCtor {
6040 start: Point2d {
6041 x: Expr::Number(Number { value: start_x, units }),
6042 y: Expr::Number(Number { value: start_y, units }),
6043 },
6044 end: Point2d {
6045 x: Expr::Number(Number { value: end_x, units }),
6046 y: Expr::Number(Number { value: end_y, units }),
6047 },
6048 construction: None,
6049 }
6050 }
6051
6052 async fn create_sketch_with_single_line(
6053 frontend: &mut FrontendState,
6054 ctx: &ExecutorContext,
6055 mock_ctx: &ExecutorContext,
6056 version: Version,
6057 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6058 frontend.program = Program::empty();
6059
6060 let sketch_args = SketchCtor {
6061 on: Plane::Default(PlaneName::Xy),
6062 };
6063 let (_src_delta, _scene_delta, sketch_id) = frontend
6064 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6065 .await
6066 .unwrap();
6067
6068 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6069 let (source_delta, scene_graph_delta) = frontend
6070 .add_segment(mock_ctx, version, sketch_id, segment, None)
6071 .await
6072 .unwrap();
6073 let line_id = *scene_graph_delta
6074 .new_objects
6075 .last()
6076 .expect("Expected line object id to be created");
6077
6078 (sketch_id, line_id, source_delta, scene_graph_delta)
6079 }
6080
6081 #[tokio::test(flavor = "multi_thread")]
6082 async fn test_sketch_checkpoint_round_trip_restores_state() {
6083 let mut frontend = FrontendState::new();
6084 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6085 let mock_ctx = ExecutorContext::new_mock(None).await;
6086 let version = Version(0);
6087
6088 let (sketch_id, line_id, source_delta, scene_graph_delta) =
6089 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6090
6091 let expected_source = source_delta.text.clone();
6092 let expected_scene_graph = frontend.scene_graph.clone();
6093 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6094 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6095
6096 let checkpoint_id = frontend
6097 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6098 .await
6099 .unwrap();
6100
6101 let edited_segments = vec![ExistingSegmentCtor {
6102 id: line_id,
6103 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6104 }];
6105 let (edited_source, _edited_scene) = frontend
6106 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6107 .await
6108 .unwrap();
6109 assert_ne!(edited_source.text, expected_source);
6110
6111 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6112
6113 assert_eq!(restored.source_delta.text, expected_source);
6114 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6115 assert!(restored.scene_graph_delta.invalidates_ids);
6116 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6117 assert_eq!(frontend.scene_graph, expected_scene_graph);
6118 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6119
6120 ctx.close().await;
6121 mock_ctx.close().await;
6122 }
6123
6124 #[tokio::test(flavor = "multi_thread")]
6125 async fn test_sketch_checkpoints_prune_oldest_entries() {
6126 let mut frontend = FrontendState::new();
6127 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6128 let mock_ctx = ExecutorContext::new_mock(None).await;
6129 let version = Version(0);
6130
6131 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6132 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6133
6134 let mut checkpoint_ids = Vec::new();
6135 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6136 checkpoint_ids.push(
6137 frontend
6138 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6139 .await
6140 .unwrap(),
6141 );
6142 }
6143
6144 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6145 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6146
6147 let oldest_retained = checkpoint_ids[3];
6148 assert_eq!(
6149 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6150 Some(oldest_retained)
6151 );
6152
6153 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6154 assert!(evicted_restore.is_err());
6155 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6156
6157 frontend
6158 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6159 .await
6160 .unwrap();
6161
6162 ctx.close().await;
6163 mock_ctx.close().await;
6164 }
6165
6166 #[tokio::test(flavor = "multi_thread")]
6167 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6168 let mut frontend = FrontendState::new();
6169 let missing_checkpoint = SketchCheckpointId::new(999);
6170
6171 let err = frontend
6172 .restore_sketch_checkpoint(missing_checkpoint)
6173 .await
6174 .expect_err("Expected restore to fail for missing checkpoint");
6175
6176 assert!(err.msg.contains("Sketch checkpoint not found"));
6177 }
6178
6179 #[tokio::test(flavor = "multi_thread")]
6180 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6181 let mut frontend = FrontendState::new();
6182 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6183 let mock_ctx = ExecutorContext::new_mock(None).await;
6184 let version = Version(0);
6185
6186 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6187 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6188
6189 let checkpoint_a = frontend
6190 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6191 .await
6192 .unwrap();
6193 let checkpoint_b = frontend
6194 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6195 .await
6196 .unwrap();
6197 assert_eq!(frontend.sketch_checkpoints.len(), 2);
6198
6199 frontend.clear_sketch_checkpoints();
6200 assert!(frontend.sketch_checkpoints.is_empty());
6201 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6202 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6203
6204 ctx.close().await;
6205 mock_ctx.close().await;
6206 }
6207
6208 #[tokio::test(flavor = "multi_thread")]
6209 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6210 let mut frontend = FrontendState::new();
6211 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6212 let mock_ctx = ExecutorContext::new_mock(None).await;
6213 let version = Version(0);
6214
6215 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6216 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6217 let old_source = source_delta.text.clone();
6218 let old_checkpoint = frontend
6219 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6220 .await
6221 .unwrap();
6222 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6223
6224 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
6225 .unwrap()
6226 .0
6227 .unwrap();
6228
6229 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6230 let SetProgramOutcome::Success {
6231 checkpoint_id: Some(new_checkpoint),
6232 ..
6233 } = result
6234 else {
6235 panic!("Expected Success with a fresh checkpoint baseline");
6236 };
6237
6238 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6239
6240 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6241 assert_eq!(old_restore.source_delta.text, old_source);
6242
6243 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6244 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6245
6246 ctx.close().await;
6247 mock_ctx.close().await;
6248 }
6249
6250 #[tokio::test(flavor = "multi_thread")]
6251 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6252 let mut frontend = FrontendState::new();
6253 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6254 let mock_ctx = ExecutorContext::new_mock(None).await;
6255 let version = Version(0);
6256
6257 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6258 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6259 let old_checkpoint = frontend
6260 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6261 .await
6262 .unwrap();
6263 let checkpoint_count_before = frontend.sketch_checkpoints.len();
6264
6265 let failing_program = Program::parse(
6266 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6267 )
6268 .unwrap()
6269 .0
6270 .unwrap();
6271
6272 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6273 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6274 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6275 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6276
6277 ctx.close().await;
6278 mock_ctx.close().await;
6279 }
6280
6281 #[tokio::test(flavor = "multi_thread")]
6282 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6283 let mut frontend = FrontendState::new();
6284 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6285
6286 let program = Program::parse(
6287 "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",
6288 )
6289 .unwrap()
6290 .0
6291 .unwrap();
6292 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6293 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6294 panic!("Expected successful baseline program execution");
6295 };
6296
6297 clear_mem_cache().await;
6298 assert!(read_old_memory().await.is_none());
6299
6300 let checkpoint_without_mock_memory = frontend
6301 .create_sketch_checkpoint((*exec_outcome).clone())
6302 .await
6303 .unwrap();
6304
6305 write_old_memory(SketchModeState::new_for_tests()).await;
6306 assert!(read_old_memory().await.is_some());
6307
6308 let checkpoint_with_mock_memory = frontend
6309 .create_sketch_checkpoint((*exec_outcome).clone())
6310 .await
6311 .unwrap();
6312
6313 clear_mem_cache().await;
6314 assert!(read_old_memory().await.is_none());
6315
6316 frontend
6317 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6318 .await
6319 .unwrap();
6320 assert!(read_old_memory().await.is_some());
6321
6322 frontend
6323 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6324 .await
6325 .unwrap();
6326 assert!(read_old_memory().await.is_none());
6327
6328 ctx.close().await;
6329 }
6330
6331 #[tokio::test(flavor = "multi_thread")]
6332 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6333 let source = "\
6334sketch(on = XY) {
6335 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6336}
6337
6338bad = missing_name
6339";
6340 let program = Program::parse(source).unwrap().0.unwrap();
6341
6342 let mut frontend = FrontendState::new();
6343
6344 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6345 let mock_ctx = ExecutorContext::new_mock(None).await;
6346 let version = Version(0);
6347 let project_id = ProjectId(0);
6348 let file_id = FileId(0);
6349
6350 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6351 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6352 };
6353
6354 let sketch_id = frontend
6355 .scene_graph
6356 .objects
6357 .iter()
6358 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6359 .expect("Expected sketch object from errored hack_set_program");
6360
6361 frontend
6362 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6363 .await
6364 .unwrap();
6365
6366 ctx.close().await;
6367 mock_ctx.close().await;
6368 }
6369
6370 #[tokio::test(flavor = "multi_thread")]
6371 async fn test_new_sketch_add_point_edit_point() {
6372 let program = Program::empty();
6373
6374 let mut frontend = FrontendState::new();
6375 frontend.program = program;
6376
6377 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6378 let mock_ctx = ExecutorContext::new_mock(None).await;
6379 let version = Version(0);
6380
6381 let sketch_args = SketchCtor {
6382 on: Plane::Default(PlaneName::Xy),
6383 };
6384 let (_src_delta, scene_delta, sketch_id) = frontend
6385 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6386 .await
6387 .unwrap();
6388 assert_eq!(sketch_id, ObjectId(1));
6389 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6390 let sketch_object = &scene_delta.new_graph.objects[1];
6391 assert_eq!(sketch_object.id, ObjectId(1));
6392 assert_eq!(
6393 sketch_object.kind,
6394 ObjectKind::Sketch(Sketch {
6395 args: SketchCtor {
6396 on: Plane::Default(PlaneName::Xy)
6397 },
6398 plane: ObjectId(0),
6399 segments: vec![],
6400 constraints: vec![],
6401 })
6402 );
6403 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6404
6405 let point_ctor = PointCtor {
6406 position: Point2d {
6407 x: Expr::Number(Number {
6408 value: 1.0,
6409 units: NumericSuffix::Inch,
6410 }),
6411 y: Expr::Number(Number {
6412 value: 2.0,
6413 units: NumericSuffix::Inch,
6414 }),
6415 },
6416 };
6417 let segment = SegmentCtor::Point(point_ctor);
6418 let (src_delta, scene_delta) = frontend
6419 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6420 .await
6421 .unwrap();
6422 assert_eq!(
6423 src_delta.text.as_str(),
6424 "sketch001 = sketch(on = XY) {
6425 point(at = [1in, 2in])
6426}
6427"
6428 );
6429 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6430 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6431 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6432 assert_eq!(scene_object.id.0, i);
6433 }
6434
6435 let point_id = *scene_delta.new_objects.last().unwrap();
6436
6437 let point_ctor = PointCtor {
6438 position: Point2d {
6439 x: Expr::Number(Number {
6440 value: 3.0,
6441 units: NumericSuffix::Inch,
6442 }),
6443 y: Expr::Number(Number {
6444 value: 4.0,
6445 units: NumericSuffix::Inch,
6446 }),
6447 },
6448 };
6449 let segments = vec![ExistingSegmentCtor {
6450 id: point_id,
6451 ctor: SegmentCtor::Point(point_ctor),
6452 }];
6453 let (src_delta, scene_delta) = frontend
6454 .edit_segments(&mock_ctx, version, sketch_id, segments)
6455 .await
6456 .unwrap();
6457 assert_eq!(
6458 src_delta.text.as_str(),
6459 "sketch001 = sketch(on = XY) {
6460 point(at = [3in, 4in])
6461}
6462"
6463 );
6464 assert_eq!(scene_delta.new_objects, vec![]);
6465 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6466
6467 ctx.close().await;
6468 mock_ctx.close().await;
6469 }
6470
6471 #[tokio::test(flavor = "multi_thread")]
6472 async fn test_new_sketch_add_line_edit_line() {
6473 let program = Program::empty();
6474
6475 let mut frontend = FrontendState::new();
6476 frontend.program = program;
6477
6478 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6479 let mock_ctx = ExecutorContext::new_mock(None).await;
6480 let version = Version(0);
6481
6482 let sketch_args = SketchCtor {
6483 on: Plane::Default(PlaneName::Xy),
6484 };
6485 let (_src_delta, scene_delta, sketch_id) = frontend
6486 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6487 .await
6488 .unwrap();
6489 assert_eq!(sketch_id, ObjectId(1));
6490 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6491 let sketch_object = &scene_delta.new_graph.objects[1];
6492 assert_eq!(sketch_object.id, ObjectId(1));
6493 assert_eq!(
6494 sketch_object.kind,
6495 ObjectKind::Sketch(Sketch {
6496 args: SketchCtor {
6497 on: Plane::Default(PlaneName::Xy)
6498 },
6499 plane: ObjectId(0),
6500 segments: vec![],
6501 constraints: vec![],
6502 })
6503 );
6504 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6505
6506 let line_ctor = LineCtor {
6507 start: Point2d {
6508 x: Expr::Number(Number {
6509 value: 0.0,
6510 units: NumericSuffix::Mm,
6511 }),
6512 y: Expr::Number(Number {
6513 value: 0.0,
6514 units: NumericSuffix::Mm,
6515 }),
6516 },
6517 end: Point2d {
6518 x: Expr::Number(Number {
6519 value: 10.0,
6520 units: NumericSuffix::Mm,
6521 }),
6522 y: Expr::Number(Number {
6523 value: 10.0,
6524 units: NumericSuffix::Mm,
6525 }),
6526 },
6527 construction: None,
6528 };
6529 let segment = SegmentCtor::Line(line_ctor);
6530 let (src_delta, scene_delta) = frontend
6531 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6532 .await
6533 .unwrap();
6534 assert_eq!(
6535 src_delta.text.as_str(),
6536 "sketch001 = sketch(on = XY) {
6537 line(start = [0mm, 0mm], end = [10mm, 10mm])
6538}
6539"
6540 );
6541 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6542 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6543 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6544 assert_eq!(scene_object.id.0, i);
6545 }
6546
6547 let line = *scene_delta.new_objects.last().unwrap();
6549
6550 let line_ctor = LineCtor {
6551 start: Point2d {
6552 x: Expr::Number(Number {
6553 value: 1.0,
6554 units: NumericSuffix::Mm,
6555 }),
6556 y: Expr::Number(Number {
6557 value: 2.0,
6558 units: NumericSuffix::Mm,
6559 }),
6560 },
6561 end: Point2d {
6562 x: Expr::Number(Number {
6563 value: 13.0,
6564 units: NumericSuffix::Mm,
6565 }),
6566 y: Expr::Number(Number {
6567 value: 14.0,
6568 units: NumericSuffix::Mm,
6569 }),
6570 },
6571 construction: None,
6572 };
6573 let segments = vec![ExistingSegmentCtor {
6574 id: line,
6575 ctor: SegmentCtor::Line(line_ctor),
6576 }];
6577 let (src_delta, scene_delta) = frontend
6578 .edit_segments(&mock_ctx, version, sketch_id, segments)
6579 .await
6580 .unwrap();
6581 assert_eq!(
6582 src_delta.text.as_str(),
6583 "sketch001 = sketch(on = XY) {
6584 line(start = [1mm, 2mm], end = [13mm, 14mm])
6585}
6586"
6587 );
6588 assert_eq!(scene_delta.new_objects, vec![]);
6589 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6590
6591 ctx.close().await;
6592 mock_ctx.close().await;
6593 }
6594
6595 #[tokio::test(flavor = "multi_thread")]
6596 async fn test_new_sketch_add_arc_edit_arc() {
6597 let program = Program::empty();
6598
6599 let mut frontend = FrontendState::new();
6600 frontend.program = program;
6601
6602 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6603 let mock_ctx = ExecutorContext::new_mock(None).await;
6604 let version = Version(0);
6605
6606 let sketch_args = SketchCtor {
6607 on: Plane::Default(PlaneName::Xy),
6608 };
6609 let (_src_delta, scene_delta, sketch_id) = frontend
6610 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6611 .await
6612 .unwrap();
6613 assert_eq!(sketch_id, ObjectId(1));
6614 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6615 let sketch_object = &scene_delta.new_graph.objects[1];
6616 assert_eq!(sketch_object.id, ObjectId(1));
6617 assert_eq!(
6618 sketch_object.kind,
6619 ObjectKind::Sketch(Sketch {
6620 args: SketchCtor {
6621 on: Plane::Default(PlaneName::Xy),
6622 },
6623 plane: ObjectId(0),
6624 segments: vec![],
6625 constraints: vec![],
6626 })
6627 );
6628 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6629
6630 let arc_ctor = ArcCtor {
6631 start: Point2d {
6632 x: Expr::Var(Number {
6633 value: 0.0,
6634 units: NumericSuffix::Mm,
6635 }),
6636 y: Expr::Var(Number {
6637 value: 0.0,
6638 units: NumericSuffix::Mm,
6639 }),
6640 },
6641 end: Point2d {
6642 x: Expr::Var(Number {
6643 value: 10.0,
6644 units: NumericSuffix::Mm,
6645 }),
6646 y: Expr::Var(Number {
6647 value: 10.0,
6648 units: NumericSuffix::Mm,
6649 }),
6650 },
6651 center: Point2d {
6652 x: Expr::Var(Number {
6653 value: 10.0,
6654 units: NumericSuffix::Mm,
6655 }),
6656 y: Expr::Var(Number {
6657 value: 0.0,
6658 units: NumericSuffix::Mm,
6659 }),
6660 },
6661 construction: None,
6662 };
6663 let segment = SegmentCtor::Arc(arc_ctor);
6664 let (src_delta, scene_delta) = frontend
6665 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6666 .await
6667 .unwrap();
6668 assert_eq!(
6669 src_delta.text.as_str(),
6670 "sketch001 = sketch(on = XY) {
6671 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6672}
6673"
6674 );
6675 assert_eq!(
6676 scene_delta.new_objects,
6677 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6678 );
6679 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6680 assert_eq!(scene_object.id.0, i);
6681 }
6682 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6683
6684 let arc = *scene_delta.new_objects.last().unwrap();
6686
6687 let arc_ctor = ArcCtor {
6688 start: Point2d {
6689 x: Expr::Var(Number {
6690 value: 1.0,
6691 units: NumericSuffix::Mm,
6692 }),
6693 y: Expr::Var(Number {
6694 value: 2.0,
6695 units: NumericSuffix::Mm,
6696 }),
6697 },
6698 end: Point2d {
6699 x: Expr::Var(Number {
6700 value: 13.0,
6701 units: NumericSuffix::Mm,
6702 }),
6703 y: Expr::Var(Number {
6704 value: 14.0,
6705 units: NumericSuffix::Mm,
6706 }),
6707 },
6708 center: Point2d {
6709 x: Expr::Var(Number {
6710 value: 13.0,
6711 units: NumericSuffix::Mm,
6712 }),
6713 y: Expr::Var(Number {
6714 value: 2.0,
6715 units: NumericSuffix::Mm,
6716 }),
6717 },
6718 construction: None,
6719 };
6720 let segments = vec![ExistingSegmentCtor {
6721 id: arc,
6722 ctor: SegmentCtor::Arc(arc_ctor),
6723 }];
6724 let (src_delta, scene_delta) = frontend
6725 .edit_segments(&mock_ctx, version, sketch_id, segments)
6726 .await
6727 .unwrap();
6728 assert_eq!(
6729 src_delta.text.as_str(),
6730 "sketch001 = sketch(on = XY) {
6731 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6732}
6733"
6734 );
6735 assert_eq!(scene_delta.new_objects, vec![]);
6736 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6737
6738 ctx.close().await;
6739 mock_ctx.close().await;
6740 }
6741
6742 #[tokio::test(flavor = "multi_thread")]
6743 async fn test_new_sketch_add_circle_edit_circle() {
6744 let program = Program::empty();
6745
6746 let mut frontend = FrontendState::new();
6747 frontend.program = program;
6748
6749 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6750 let mock_ctx = ExecutorContext::new_mock(None).await;
6751 let version = Version(0);
6752
6753 let sketch_args = SketchCtor {
6754 on: Plane::Default(PlaneName::Xy),
6755 };
6756 let (_src_delta, _scene_delta, sketch_id) = frontend
6757 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6758 .await
6759 .unwrap();
6760
6761 let circle_ctor = CircleCtor {
6763 start: Point2d {
6764 x: Expr::Var(Number {
6765 value: 5.0,
6766 units: NumericSuffix::Mm,
6767 }),
6768 y: Expr::Var(Number {
6769 value: 0.0,
6770 units: NumericSuffix::Mm,
6771 }),
6772 },
6773 center: Point2d {
6774 x: Expr::Var(Number {
6775 value: 0.0,
6776 units: NumericSuffix::Mm,
6777 }),
6778 y: Expr::Var(Number {
6779 value: 0.0,
6780 units: NumericSuffix::Mm,
6781 }),
6782 },
6783 construction: None,
6784 };
6785 let segment = SegmentCtor::Circle(circle_ctor);
6786 let (src_delta, scene_delta) = frontend
6787 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6788 .await
6789 .unwrap();
6790 assert_eq!(
6791 src_delta.text.as_str(),
6792 "sketch001 = sketch(on = XY) {
6793 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6794}
6795"
6796 );
6797 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6799 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6800
6801 let circle = *scene_delta.new_objects.last().unwrap();
6802
6803 let circle_ctor = CircleCtor {
6805 start: Point2d {
6806 x: Expr::Var(Number {
6807 value: 10.0,
6808 units: NumericSuffix::Mm,
6809 }),
6810 y: Expr::Var(Number {
6811 value: 0.0,
6812 units: NumericSuffix::Mm,
6813 }),
6814 },
6815 center: Point2d {
6816 x: Expr::Var(Number {
6817 value: 3.0,
6818 units: NumericSuffix::Mm,
6819 }),
6820 y: Expr::Var(Number {
6821 value: 4.0,
6822 units: NumericSuffix::Mm,
6823 }),
6824 },
6825 construction: None,
6826 };
6827 let segments = vec![ExistingSegmentCtor {
6828 id: circle,
6829 ctor: SegmentCtor::Circle(circle_ctor),
6830 }];
6831 let (src_delta, scene_delta) = frontend
6832 .edit_segments(&mock_ctx, version, sketch_id, segments)
6833 .await
6834 .unwrap();
6835 assert_eq!(
6836 src_delta.text.as_str(),
6837 "sketch001 = sketch(on = XY) {
6838 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6839}
6840"
6841 );
6842 assert_eq!(scene_delta.new_objects, vec![]);
6843 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6844
6845 ctx.close().await;
6846 mock_ctx.close().await;
6847 }
6848
6849 #[tokio::test(flavor = "multi_thread")]
6850 async fn test_delete_circle() {
6851 let initial_source = "sketch001 = sketch(on = XY) {
6852 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6853}
6854";
6855
6856 let program = Program::parse(initial_source).unwrap().0.unwrap();
6857 let mut frontend = FrontendState::new();
6858
6859 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6860 let mock_ctx = ExecutorContext::new_mock(None).await;
6861 let version = Version(0);
6862
6863 frontend.hack_set_program(&ctx, program).await.unwrap();
6864 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6865 let sketch_id = sketch_object.id;
6866 let sketch = expect_sketch(sketch_object);
6867
6868 assert_eq!(sketch.segments.len(), 3);
6870 let circle_id = sketch.segments[2];
6871
6872 let (src_delta, scene_delta) = frontend
6874 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6875 .await
6876 .unwrap();
6877 assert_eq!(
6878 src_delta.text.as_str(),
6879 "sketch001 = sketch(on = XY) {
6880}
6881"
6882 );
6883 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6884 let new_sketch = expect_sketch(new_sketch_object);
6885 assert_eq!(new_sketch.segments.len(), 0);
6886
6887 ctx.close().await;
6888 mock_ctx.close().await;
6889 }
6890
6891 #[tokio::test(flavor = "multi_thread")]
6892 async fn test_edit_circle_via_point() {
6893 let initial_source = "sketch001 = sketch(on = XY) {
6894 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6895}
6896";
6897
6898 let program = Program::parse(initial_source).unwrap().0.unwrap();
6899 let mut frontend = FrontendState::new();
6900
6901 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6902 let mock_ctx = ExecutorContext::new_mock(None).await;
6903 let version = Version(0);
6904
6905 frontend.hack_set_program(&ctx, program).await.unwrap();
6906 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6907 let sketch_id = sketch_object.id;
6908 let sketch = expect_sketch(sketch_object);
6909
6910 let circle_id = sketch
6912 .segments
6913 .iter()
6914 .copied()
6915 .find(|seg_id| {
6916 matches!(
6917 &frontend.scene_graph.objects[seg_id.0].kind,
6918 ObjectKind::Segment {
6919 segment: Segment::Circle(_)
6920 }
6921 )
6922 })
6923 .expect("Expected a circle segment in sketch");
6924 let circle_object = &frontend.scene_graph.objects[circle_id.0];
6925 let ObjectKind::Segment {
6926 segment: Segment::Circle(circle),
6927 } = &circle_object.kind
6928 else {
6929 panic!("Expected circle segment, got: {:?}", circle_object.kind);
6930 };
6931 let start_point_id = circle.start;
6932
6933 let segments = vec![ExistingSegmentCtor {
6935 id: start_point_id,
6936 ctor: SegmentCtor::Point(PointCtor {
6937 position: Point2d {
6938 x: Expr::Var(Number {
6939 value: 7.0,
6940 units: NumericSuffix::Mm,
6941 }),
6942 y: Expr::Var(Number {
6943 value: 1.0,
6944 units: NumericSuffix::Mm,
6945 }),
6946 },
6947 }),
6948 }];
6949 let (src_delta, _scene_delta) = frontend
6950 .edit_segments(&mock_ctx, version, sketch_id, segments)
6951 .await
6952 .unwrap();
6953 assert_eq!(
6954 src_delta.text.as_str(),
6955 "sketch001 = sketch(on = XY) {
6956 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
6957}
6958"
6959 );
6960
6961 ctx.close().await;
6962 mock_ctx.close().await;
6963 }
6964
6965 #[tokio::test(flavor = "multi_thread")]
6966 async fn test_add_line_when_sketch_block_uses_variable() {
6967 let initial_source = "s = sketch(on = XY) {}
6968";
6969
6970 let program = Program::parse(initial_source).unwrap().0.unwrap();
6971
6972 let mut frontend = FrontendState::new();
6973
6974 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6975 let mock_ctx = ExecutorContext::new_mock(None).await;
6976 let version = Version(0);
6977
6978 frontend.hack_set_program(&ctx, program).await.unwrap();
6979 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6980 let sketch_id = sketch_object.id;
6981
6982 let line_ctor = LineCtor {
6983 start: Point2d {
6984 x: Expr::Number(Number {
6985 value: 0.0,
6986 units: NumericSuffix::Mm,
6987 }),
6988 y: Expr::Number(Number {
6989 value: 0.0,
6990 units: NumericSuffix::Mm,
6991 }),
6992 },
6993 end: Point2d {
6994 x: Expr::Number(Number {
6995 value: 10.0,
6996 units: NumericSuffix::Mm,
6997 }),
6998 y: Expr::Number(Number {
6999 value: 10.0,
7000 units: NumericSuffix::Mm,
7001 }),
7002 },
7003 construction: None,
7004 };
7005 let segment = SegmentCtor::Line(line_ctor);
7006 let (src_delta, scene_delta) = frontend
7007 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7008 .await
7009 .unwrap();
7010 assert_eq!(
7011 src_delta.text.as_str(),
7012 "s = sketch(on = XY) {
7013 line(start = [0mm, 0mm], end = [10mm, 10mm])
7014}
7015"
7016 );
7017 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7018 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7019
7020 ctx.close().await;
7021 mock_ctx.close().await;
7022 }
7023
7024 #[tokio::test(flavor = "multi_thread")]
7025 async fn test_new_sketch_add_line_delete_sketch() {
7026 let program = Program::empty();
7027
7028 let mut frontend = FrontendState::new();
7029 frontend.program = program;
7030
7031 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7032 let mock_ctx = ExecutorContext::new_mock(None).await;
7033 let version = Version(0);
7034
7035 let sketch_args = SketchCtor {
7036 on: Plane::Default(PlaneName::Xy),
7037 };
7038 let (_src_delta, scene_delta, sketch_id) = frontend
7039 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7040 .await
7041 .unwrap();
7042 assert_eq!(sketch_id, ObjectId(1));
7043 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7044 let sketch_object = &scene_delta.new_graph.objects[1];
7045 assert_eq!(sketch_object.id, ObjectId(1));
7046 assert_eq!(
7047 sketch_object.kind,
7048 ObjectKind::Sketch(Sketch {
7049 args: SketchCtor {
7050 on: Plane::Default(PlaneName::Xy)
7051 },
7052 plane: ObjectId(0),
7053 segments: vec![],
7054 constraints: vec![],
7055 })
7056 );
7057 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7058
7059 let line_ctor = LineCtor {
7060 start: Point2d {
7061 x: Expr::Number(Number {
7062 value: 0.0,
7063 units: NumericSuffix::Mm,
7064 }),
7065 y: Expr::Number(Number {
7066 value: 0.0,
7067 units: NumericSuffix::Mm,
7068 }),
7069 },
7070 end: Point2d {
7071 x: Expr::Number(Number {
7072 value: 10.0,
7073 units: NumericSuffix::Mm,
7074 }),
7075 y: Expr::Number(Number {
7076 value: 10.0,
7077 units: NumericSuffix::Mm,
7078 }),
7079 },
7080 construction: None,
7081 };
7082 let segment = SegmentCtor::Line(line_ctor);
7083 let (src_delta, scene_delta) = frontend
7084 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7085 .await
7086 .unwrap();
7087 assert_eq!(
7088 src_delta.text.as_str(),
7089 "sketch001 = sketch(on = XY) {
7090 line(start = [0mm, 0mm], end = [10mm, 10mm])
7091}
7092"
7093 );
7094 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7095
7096 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7097 assert_eq!(src_delta.text.as_str(), "");
7098 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7099
7100 ctx.close().await;
7101 mock_ctx.close().await;
7102 }
7103
7104 #[tokio::test(flavor = "multi_thread")]
7105 async fn test_delete_sketch_when_sketch_block_uses_variable() {
7106 let initial_source = "s = sketch(on = XY) {}
7107";
7108
7109 let program = Program::parse(initial_source).unwrap().0.unwrap();
7110
7111 let mut frontend = FrontendState::new();
7112
7113 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7114 let mock_ctx = ExecutorContext::new_mock(None).await;
7115 let version = Version(0);
7116
7117 frontend.hack_set_program(&ctx, program).await.unwrap();
7118 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7119 let sketch_id = sketch_object.id;
7120
7121 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7122 assert_eq!(src_delta.text.as_str(), "");
7123 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7124
7125 ctx.close().await;
7126 mock_ctx.close().await;
7127 }
7128
7129 #[tokio::test(flavor = "multi_thread")]
7130 async fn test_delete_sketch_after_comment() {
7131 let initial_source = "sketch001 = sketch(on = XZ) {
7132}
7133";
7134
7135 let program = Program::parse(initial_source).unwrap().0.unwrap();
7136 let mut frontend = FrontendState::new();
7137
7138 let ctx = ExecutorContext::new_with_engine(
7139 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7140 Default::default(),
7141 );
7142 let version = Version(0);
7143
7144 frontend.hack_set_program(&ctx, program).await.unwrap();
7145 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7146 let sketch_id = sketch_object.id;
7147 let original_source = sketch_object.source.clone();
7148
7149 let commented_source = "// test 1
7150sketch001 = sketch(on = XZ) {
7151}
7152";
7153 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7154 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7155
7156 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7157 assert_eq!(cached_sketch_object.source, original_source);
7158
7159 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7160 assert!(
7161 !src_delta.text.contains("sketch001"),
7162 "sketch was not deleted: {}",
7163 src_delta.text
7164 );
7165 assert_eq!(src_delta.text.as_str(), "// test 1\n");
7167 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7168
7169 ctx.close().await;
7170 }
7171
7172 #[tokio::test(flavor = "multi_thread")]
7173 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7174 let initial_source = "sketch001 = sketch(on = XZ) {
7175}
7176foo = 1
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
7192 let commented_source = "// keep me
7193sketch001 = sketch(on = XZ) {
7194}
7195foo = 1
7196";
7197 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7198 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7199
7200 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7201 assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7203
7204 ctx.close().await;
7205 }
7206
7207 #[tokio::test(flavor = "multi_thread")]
7208 async fn test_delete_segment_preserves_pre_comment() {
7209 let initial_source = "\
7210sketch(on = XY) {
7211 point(at = [var 1, var 2])
7212 // describe the middle point
7213 point(at = [var 3, var 4])
7214 point(at = [var 5, var 6])
7215}
7216";
7217
7218 let program = Program::parse(initial_source).unwrap().0.unwrap();
7219 let mut frontend = FrontendState::new();
7220
7221 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7222 let mock_ctx = ExecutorContext::new_mock(None).await;
7223 let version = Version(0);
7224
7225 frontend.hack_set_program(&ctx, program).await.unwrap();
7226 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7227 let sketch_id = sketch_object.id;
7228 let sketch = expect_sketch(sketch_object);
7229
7230 let middle_point_id = *sketch.segments.get(1).unwrap();
7231
7232 let (src_delta, _scene_delta) = frontend
7233 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7234 .await
7235 .unwrap();
7236 assert_eq!(
7239 src_delta.text.as_str(),
7240 "\
7241sketch(on = XY) {
7242 point(at = [var 1mm, var 2mm])
7243 // describe the middle point
7244 point(at = [var 5mm, var 6mm])
7245}
7246"
7247 );
7248
7249 ctx.close().await;
7250 mock_ctx.close().await;
7251 }
7252
7253 #[tokio::test(flavor = "multi_thread")]
7254 async fn test_delete_last_segment_preserves_pre_comment() {
7255 let initial_source = "\
7256sketch(on = XY) {
7257 point(at = [var 1, var 2])
7258 // describe the trailing point
7259 point(at = [var 3, var 4])
7260}
7261";
7262
7263 let program = Program::parse(initial_source).unwrap().0.unwrap();
7264 let mut frontend = FrontendState::new();
7265
7266 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7267 let mock_ctx = ExecutorContext::new_mock(None).await;
7268 let version = Version(0);
7269
7270 frontend.hack_set_program(&ctx, program).await.unwrap();
7271 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7272 let sketch_id = sketch_object.id;
7273 let sketch = expect_sketch(sketch_object);
7274
7275 let last_point_id = *sketch.segments.last().unwrap();
7276
7277 let (src_delta, _scene_delta) = frontend
7278 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7279 .await
7280 .unwrap();
7281 assert_eq!(
7284 src_delta.text.as_str(),
7285 "\
7286sketch(on = XY) {
7287 point(at = [var 1mm, var 2mm])
7288 // describe the trailing point
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_segment_drops_inline_trailing_comment() {
7299 let initial_source = "\
7300sketch(on = XY) {
7301 point(at = [var 1, var 2])
7302 point(at = [var 3, var 4]) // same-line note that gets dropped
7303 point(at = [var 5, var 6])
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 middle_point_id = *sketch.segments.get(1).unwrap();
7320
7321 let (src_delta, _scene_delta) = frontend
7322 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7323 .await
7324 .unwrap();
7325 assert!(
7327 !src_delta.text.contains("same-line note"),
7328 "inline comment should have been removed: {}",
7329 src_delta.text
7330 );
7331
7332 ctx.close().await;
7333 mock_ctx.close().await;
7334 }
7335
7336 #[tokio::test(flavor = "multi_thread")]
7337 async fn test_delete_segments_preserves_block_comments_across_positions() {
7338 let initial_source = "\
7346sketch(on = XY) {
7347 /* above first - moves to middle */
7348 point(at = [var 1, var 2]) /* same-line on first - dropped */
7349 /* above middle - stays */
7350 point(at = [var 3, var 4])
7351 /* above last - moves to trailing meta */
7352 point(at = [var 5, var 6])
7353}
7354";
7355
7356 let program = Program::parse(initial_source).unwrap().0.unwrap();
7357 let mut frontend = FrontendState::new();
7358
7359 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7360 let mock_ctx = ExecutorContext::new_mock(None).await;
7361 let version = Version(0);
7362
7363 frontend.hack_set_program(&ctx, program).await.unwrap();
7364 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7365 let sketch_id = sketch_object.id;
7366 let sketch = expect_sketch(sketch_object);
7367
7368 let first_point_id = *sketch.segments.first().unwrap();
7369 let last_point_id = *sketch.segments.last().unwrap();
7370
7371 let (src_delta, _scene_delta) = frontend
7372 .delete_objects(
7373 &mock_ctx,
7374 version,
7375 sketch_id,
7376 Vec::new(),
7377 vec![first_point_id, last_point_id],
7378 )
7379 .await
7380 .unwrap();
7381 assert_eq!(
7382 src_delta.text.as_str(),
7383 "\
7384sketch(on = XY) {
7385 /* above first - moves to middle */
7386 /* above middle - stays */
7387 point(at = [var 3mm, var 4mm])
7388 /* above last - moves to trailing meta */
7389}
7390"
7391 );
7392
7393 ctx.close().await;
7394 mock_ctx.close().await;
7395 }
7396
7397 #[tokio::test(flavor = "multi_thread")]
7398 async fn test_edit_line_when_editing_its_start_point() {
7399 let initial_source = "\
7400sketch(on = XY) {
7401 line(start = [var 1, var 2], end = [var 3, var 4])
7402}
7403";
7404
7405 let program = Program::parse(initial_source).unwrap().0.unwrap();
7406
7407 let mut frontend = FrontendState::new();
7408
7409 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7410 let mock_ctx = ExecutorContext::new_mock(None).await;
7411 let version = Version(0);
7412
7413 frontend.hack_set_program(&ctx, program).await.unwrap();
7414 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7415 let sketch_id = sketch_object.id;
7416 let sketch = expect_sketch(sketch_object);
7417
7418 let point_id = *sketch.segments.first().unwrap();
7419
7420 let point_ctor = PointCtor {
7421 position: Point2d {
7422 x: Expr::Var(Number {
7423 value: 5.0,
7424 units: NumericSuffix::Inch,
7425 }),
7426 y: Expr::Var(Number {
7427 value: 6.0,
7428 units: NumericSuffix::Inch,
7429 }),
7430 },
7431 };
7432 let segments = vec![ExistingSegmentCtor {
7433 id: point_id,
7434 ctor: SegmentCtor::Point(point_ctor),
7435 }];
7436 let (src_delta, scene_delta) = frontend
7437 .edit_segments(&mock_ctx, version, sketch_id, segments)
7438 .await
7439 .unwrap();
7440 assert_eq!(
7441 src_delta.text.as_str(),
7442 "\
7443sketch(on = XY) {
7444 line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7445}
7446"
7447 );
7448 assert_eq!(scene_delta.new_objects, vec![]);
7449 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7450
7451 ctx.close().await;
7452 mock_ctx.close().await;
7453 }
7454
7455 #[tokio::test(flavor = "multi_thread")]
7456 async fn test_edit_line_when_editing_its_end_point() {
7457 let initial_source = "\
7458sketch(on = XY) {
7459 line(start = [var 1, var 2], end = [var 3, var 4])
7460}
7461";
7462
7463 let program = Program::parse(initial_source).unwrap().0.unwrap();
7464
7465 let mut frontend = FrontendState::new();
7466
7467 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7468 let mock_ctx = ExecutorContext::new_mock(None).await;
7469 let version = Version(0);
7470
7471 frontend.hack_set_program(&ctx, program).await.unwrap();
7472 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7473 let sketch_id = sketch_object.id;
7474 let sketch = expect_sketch(sketch_object);
7475 let point_id = *sketch.segments.get(1).unwrap();
7476
7477 let point_ctor = PointCtor {
7478 position: Point2d {
7479 x: Expr::Var(Number {
7480 value: 5.0,
7481 units: NumericSuffix::Inch,
7482 }),
7483 y: Expr::Var(Number {
7484 value: 6.0,
7485 units: NumericSuffix::Inch,
7486 }),
7487 },
7488 };
7489 let segments = vec![ExistingSegmentCtor {
7490 id: point_id,
7491 ctor: SegmentCtor::Point(point_ctor),
7492 }];
7493 let (src_delta, scene_delta) = frontend
7494 .edit_segments(&mock_ctx, version, sketch_id, segments)
7495 .await
7496 .unwrap();
7497 assert_eq!(
7498 src_delta.text.as_str(),
7499 "\
7500sketch(on = XY) {
7501 line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
7502}
7503"
7504 );
7505 assert_eq!(scene_delta.new_objects, vec![]);
7506 assert_eq!(
7507 scene_delta.new_graph.objects.len(),
7508 5,
7509 "{:#?}",
7510 scene_delta.new_graph.objects
7511 );
7512
7513 ctx.close().await;
7514 mock_ctx.close().await;
7515 }
7516
7517 #[tokio::test(flavor = "multi_thread")]
7518 async fn test_edit_line_with_coincident_feedback() {
7519 let initial_source = "\
7520sketch(on = XY) {
7521 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
7522 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7523 fixed([line1.start, [0, 0]])
7524 coincident([line1.end, line2.start])
7525 equalLength([line1, line2])
7526}
7527";
7528
7529 let program = Program::parse(initial_source).unwrap().0.unwrap();
7530
7531 let mut frontend = FrontendState::new();
7532
7533 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7534 let mock_ctx = ExecutorContext::new_mock(None).await;
7535 let version = Version(0);
7536
7537 frontend.hack_set_program(&ctx, program).await.unwrap();
7538 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7539 let sketch_id = sketch_object.id;
7540 let sketch = expect_sketch(sketch_object);
7541 let line2_end_id = *sketch.segments.get(4).unwrap();
7542
7543 let segments = vec![ExistingSegmentCtor {
7544 id: line2_end_id,
7545 ctor: SegmentCtor::Point(PointCtor {
7546 position: Point2d {
7547 x: Expr::Var(Number {
7548 value: 9.0,
7549 units: NumericSuffix::None,
7550 }),
7551 y: Expr::Var(Number {
7552 value: 10.0,
7553 units: NumericSuffix::None,
7554 }),
7555 },
7556 }),
7557 }];
7558 let (src_delta, scene_delta) = frontend
7559 .edit_segments(&mock_ctx, version, sketch_id, segments)
7560 .await
7561 .unwrap();
7562 assert_eq!(
7563 src_delta.text.as_str(),
7564 "\
7565sketch(on = XY) {
7566 line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7567 line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7568 fixed([line1.start, [0, 0]])
7569 coincident([line1.end, line2.start])
7570 equalLength([line1, line2])
7571}
7572"
7573 );
7574 assert_eq!(
7575 scene_delta.new_graph.objects.len(),
7576 11,
7577 "{:#?}",
7578 scene_delta.new_graph.objects
7579 );
7580
7581 ctx.close().await;
7582 mock_ctx.close().await;
7583 }
7584
7585 #[tokio::test(flavor = "multi_thread")]
7586 async fn test_delete_point_without_var() {
7587 let initial_source = "\
7588sketch(on = XY) {
7589 point(at = [var 1, var 2])
7590 point(at = [var 3, var 4])
7591 point(at = [var 5, var 6])
7592}
7593";
7594
7595 let program = Program::parse(initial_source).unwrap().0.unwrap();
7596
7597 let mut frontend = FrontendState::new();
7598
7599 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7600 let mock_ctx = ExecutorContext::new_mock(None).await;
7601 let version = Version(0);
7602
7603 frontend.hack_set_program(&ctx, program).await.unwrap();
7604 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7605 let sketch_id = sketch_object.id;
7606 let sketch = expect_sketch(sketch_object);
7607
7608 let point_id = *sketch.segments.get(1).unwrap();
7609
7610 let (src_delta, scene_delta) = frontend
7611 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7612 .await
7613 .unwrap();
7614 assert_eq!(
7615 src_delta.text.as_str(),
7616 "\
7617sketch(on = XY) {
7618 point(at = [var 1mm, var 2mm])
7619 point(at = [var 5mm, var 6mm])
7620}
7621"
7622 );
7623 assert_eq!(scene_delta.new_objects, vec![]);
7624 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7625
7626 ctx.close().await;
7627 mock_ctx.close().await;
7628 }
7629
7630 #[tokio::test(flavor = "multi_thread")]
7631 async fn test_delete_point_with_var() {
7632 let initial_source = "\
7633sketch(on = XY) {
7634 point(at = [var 1, var 2])
7635 point1 = point(at = [var 3, var 4])
7636 point(at = [var 5, var 6])
7637}
7638";
7639
7640 let program = Program::parse(initial_source).unwrap().0.unwrap();
7641
7642 let mut frontend = FrontendState::new();
7643
7644 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7645 let mock_ctx = ExecutorContext::new_mock(None).await;
7646 let version = Version(0);
7647
7648 frontend.hack_set_program(&ctx, program).await.unwrap();
7649 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7650 let sketch_id = sketch_object.id;
7651 let sketch = expect_sketch(sketch_object);
7652
7653 let point_id = *sketch.segments.get(1).unwrap();
7654
7655 let (src_delta, scene_delta) = frontend
7656 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7657 .await
7658 .unwrap();
7659 assert_eq!(
7660 src_delta.text.as_str(),
7661 "\
7662sketch(on = XY) {
7663 point(at = [var 1mm, var 2mm])
7664 point(at = [var 5mm, var 6mm])
7665}
7666"
7667 );
7668 assert_eq!(scene_delta.new_objects, vec![]);
7669 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7670
7671 ctx.close().await;
7672 mock_ctx.close().await;
7673 }
7674
7675 #[tokio::test(flavor = "multi_thread")]
7676 async fn test_delete_multiple_points() {
7677 let initial_source = "\
7678sketch(on = XY) {
7679 point(at = [var 1, var 2])
7680 point1 = point(at = [var 3, var 4])
7681 point(at = [var 5, var 6])
7682}
7683";
7684
7685 let program = Program::parse(initial_source).unwrap().0.unwrap();
7686
7687 let mut frontend = FrontendState::new();
7688
7689 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7690 let mock_ctx = ExecutorContext::new_mock(None).await;
7691 let version = Version(0);
7692
7693 frontend.hack_set_program(&ctx, program).await.unwrap();
7694 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7695 let sketch_id = sketch_object.id;
7696
7697 let sketch = expect_sketch(sketch_object);
7698
7699 let point1_id = *sketch.segments.first().unwrap();
7700 let point2_id = *sketch.segments.get(1).unwrap();
7701
7702 let (src_delta, scene_delta) = frontend
7703 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
7704 .await
7705 .unwrap();
7706 assert_eq!(
7707 src_delta.text.as_str(),
7708 "\
7709sketch(on = XY) {
7710 point(at = [var 5mm, var 6mm])
7711}
7712"
7713 );
7714 assert_eq!(scene_delta.new_objects, vec![]);
7715 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7716
7717 ctx.close().await;
7718 mock_ctx.close().await;
7719 }
7720
7721 #[tokio::test(flavor = "multi_thread")]
7722 async fn test_delete_coincident_constraint() {
7723 let initial_source = "\
7724sketch(on = XY) {
7725 point1 = point(at = [var 1, var 2])
7726 point2 = point(at = [var 3, var 4])
7727 coincident([point1, point2])
7728 point(at = [var 5, var 6])
7729}
7730";
7731
7732 let program = Program::parse(initial_source).unwrap().0.unwrap();
7733
7734 let mut frontend = FrontendState::new();
7735
7736 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7737 let mock_ctx = ExecutorContext::new_mock(None).await;
7738 let version = Version(0);
7739
7740 frontend.hack_set_program(&ctx, program).await.unwrap();
7741 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7742 let sketch_id = sketch_object.id;
7743 let sketch = expect_sketch(sketch_object);
7744
7745 let coincident_id = *sketch.constraints.first().unwrap();
7746
7747 let (src_delta, scene_delta) = frontend
7748 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7749 .await
7750 .unwrap();
7751 assert_eq!(
7752 src_delta.text.as_str(),
7753 "\
7754sketch(on = XY) {
7755 point1 = point(at = [var 1mm, var 2mm])
7756 point2 = point(at = [var 3mm, var 4mm])
7757 point(at = [var 5mm, var 6mm])
7758}
7759"
7760 );
7761 assert_eq!(scene_delta.new_objects, vec![]);
7762 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7763
7764 ctx.close().await;
7765 mock_ctx.close().await;
7766 }
7767
7768 #[tokio::test(flavor = "multi_thread")]
7769 async fn test_delete_line_cascades_to_coincident_constraint() {
7770 let initial_source = "\
7771sketch(on = XY) {
7772 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7773 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7774 coincident([line1.end, line2.start])
7775}
7776";
7777
7778 let program = Program::parse(initial_source).unwrap().0.unwrap();
7779
7780 let mut frontend = FrontendState::new();
7781
7782 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7783 let mock_ctx = ExecutorContext::new_mock(None).await;
7784 let version = Version(0);
7785
7786 frontend.hack_set_program(&ctx, program).await.unwrap();
7787 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7788 let sketch_id = sketch_object.id;
7789 let sketch = expect_sketch(sketch_object);
7790 let line_id = *sketch.segments.get(5).unwrap();
7791
7792 let (src_delta, scene_delta) = frontend
7793 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7794 .await
7795 .unwrap();
7796 assert_eq!(
7797 src_delta.text.as_str(),
7798 "\
7799sketch(on = XY) {
7800 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7801}
7802"
7803 );
7804 assert_eq!(
7805 scene_delta.new_graph.objects.len(),
7806 5,
7807 "{:#?}",
7808 scene_delta.new_graph.objects
7809 );
7810
7811 ctx.close().await;
7812 mock_ctx.close().await;
7813 }
7814
7815 #[tokio::test(flavor = "multi_thread")]
7816 async fn test_delete_line_cascades_to_distance_constraint() {
7817 let initial_source = "\
7818sketch(on = XY) {
7819 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7820 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7821 distance([line1.end, line2.start]) == 10mm
7822}
7823";
7824
7825 let program = Program::parse(initial_source).unwrap().0.unwrap();
7826
7827 let mut frontend = FrontendState::new();
7828
7829 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7830 let mock_ctx = ExecutorContext::new_mock(None).await;
7831 let version = Version(0);
7832
7833 frontend.hack_set_program(&ctx, program).await.unwrap();
7834 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7835 let sketch_id = sketch_object.id;
7836 let sketch = expect_sketch(sketch_object);
7837 let line_id = *sketch.segments.get(5).unwrap();
7838
7839 let (src_delta, scene_delta) = frontend
7840 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7841 .await
7842 .unwrap();
7843 assert_eq!(
7844 src_delta.text.as_str(),
7845 "\
7846sketch(on = XY) {
7847 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7848}
7849"
7850 );
7851 assert_eq!(
7852 scene_delta.new_graph.objects.len(),
7853 5,
7854 "{:#?}",
7855 scene_delta.new_graph.objects
7856 );
7857
7858 ctx.close().await;
7859 mock_ctx.close().await;
7860 }
7861
7862 #[tokio::test(flavor = "multi_thread")]
7863 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
7864 let initial_source = "\
7865sketch(on = XY) {
7866 point1 = point(at = [var 1, var 2])
7867 point2 = point(at = [var 3, var 4])
7868 horizontalDistance([point1, point2]) == 10mm
7869}
7870";
7871
7872 let program = Program::parse(initial_source).unwrap().0.unwrap();
7873
7874 let mut frontend = FrontendState::new();
7875
7876 let mock_ctx = ExecutorContext::new_mock(None).await;
7877 let version = Version(0);
7878
7879 frontend.program = program.clone();
7880 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7881 frontend.update_state_after_exec(outcome, true);
7882 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7883 let sketch_id = sketch_object.id;
7884 let sketch = expect_sketch(sketch_object);
7885 let point2_id = *sketch.segments.get(1).unwrap();
7886
7887 let (src_delta, scene_delta) = frontend
7888 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
7889 .await
7890 .unwrap();
7891 assert_eq!(
7892 src_delta.text.as_str(),
7893 "\
7894sketch(on = XY) {
7895 point1 = point(at = [var 1mm, var 2mm])
7896}
7897"
7898 );
7899 assert_eq!(
7900 scene_delta.new_graph.objects.len(),
7901 3,
7902 "{:#?}",
7903 scene_delta.new_graph.objects
7904 );
7905
7906 mock_ctx.close().await;
7907 }
7908
7909 #[tokio::test(flavor = "multi_thread")]
7910 async fn test_delete_line_cascades_to_fixed_constraint() {
7911 let initial_source = "\
7912sketch(on = XY) {
7913 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7914 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7915 fixed([line1.start, [0, 0]])
7916}
7917";
7918
7919 let program = Program::parse(initial_source).unwrap().0.unwrap();
7920
7921 let mut frontend = FrontendState::new();
7922
7923 let mock_ctx = ExecutorContext::new_mock(None).await;
7924 let version = Version(0);
7925
7926 frontend.program = program.clone();
7927 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7928 frontend.update_state_after_exec(outcome, true);
7929 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7930 let sketch_id = sketch_object.id;
7931 let sketch = expect_sketch(sketch_object);
7932 let line1_id = *sketch.segments.get(2).unwrap();
7933
7934 let (src_delta, scene_delta) = frontend
7935 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7936 .await
7937 .unwrap();
7938 assert_eq!(
7939 src_delta.text.as_str(),
7940 "\
7941sketch(on = XY) {
7942 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7943}
7944"
7945 );
7946 assert_eq!(
7947 scene_delta.new_graph.objects.len(),
7948 5,
7949 "{:#?}",
7950 scene_delta.new_graph.objects
7951 );
7952
7953 mock_ctx.close().await;
7954 }
7955
7956 #[tokio::test(flavor = "multi_thread")]
7957 async fn test_delete_line_cascades_to_midpoint_constraint() {
7958 let initial_source = "\
7959sketch(on = XY) {
7960 point1 = point(at = [var 1, var 2])
7961 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
7962 midpoint(line1, point = point1)
7963}
7964";
7965
7966 let program = Program::parse(initial_source).unwrap().0.unwrap();
7967
7968 let mut frontend = FrontendState::new();
7969
7970 let mock_ctx = ExecutorContext::new_mock(None).await;
7971 let version = Version(0);
7972
7973 frontend.program = program.clone();
7974 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7975 frontend.update_state_after_exec(outcome, true);
7976 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7977 let sketch_id = sketch_object.id;
7978 let sketch = expect_sketch(sketch_object);
7979 let line1_id = *sketch.segments.get(3).unwrap();
7980
7981 let (src_delta, scene_delta) = frontend
7982 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7983 .await
7984 .unwrap();
7985 assert_eq!(
7986 src_delta.text.as_str(),
7987 "\
7988sketch(on = XY) {
7989 point1 = point(at = [var 1mm, var 2mm])
7990}
7991"
7992 );
7993 assert_eq!(
7994 scene_delta.new_graph.objects.len(),
7995 3,
7996 "{:#?}",
7997 scene_delta.new_graph.objects
7998 );
7999
8000 mock_ctx.close().await;
8001 }
8002
8003 #[tokio::test(flavor = "multi_thread")]
8004 async fn test_delete_point_preserves_multiline_coincident_constraint() {
8005 let initial_source = "\
8006sketch(on = XY) {
8007 point1 = point(at = [var 1, var 2])
8008 point2 = point(at = [var 3, var 4])
8009 point3 = point(at = [var 5, var 6])
8010 coincident([point1, point2, point3])
8011}
8012";
8013
8014 let program = Program::parse(initial_source).unwrap().0.unwrap();
8015
8016 let mut frontend = FrontendState::new();
8017
8018 let mock_ctx = ExecutorContext::new_mock(None).await;
8019 let version = Version(0);
8020
8021 frontend.program = program.clone();
8022 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8023 frontend.update_state_after_exec(outcome, true);
8024 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8025 let sketch_id = sketch_object.id;
8026 let sketch = expect_sketch(sketch_object);
8027 let point3_id = *sketch.segments.get(2).unwrap();
8028
8029 let (src_delta, scene_delta) = frontend
8030 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8031 .await
8032 .unwrap();
8033 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8034 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8035 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8036 assert!(
8037 src_delta.text.contains("coincident([point1, point2])"),
8038 "{}",
8039 src_delta.text
8040 );
8041
8042 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8043 let sketch = expect_sketch(sketch_object);
8044 assert_eq!(sketch.segments.len(), 2);
8045 assert_eq!(sketch.constraints.len(), 1);
8046
8047 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8048 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8049 panic!("Expected constraint object");
8050 };
8051 let Constraint::Coincident(coincident) = constraint else {
8052 panic!("Expected coincident constraint");
8053 };
8054 assert_eq!(
8055 coincident.segments,
8056 sketch
8057 .segments
8058 .iter()
8059 .copied()
8060 .map(Into::into)
8061 .collect::<Vec<ConstraintSegment>>()
8062 );
8063
8064 mock_ctx.close().await;
8065 }
8066
8067 #[tokio::test(flavor = "multi_thread")]
8068 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8069 let initial_source = "\
8070sketch(on = XY) {
8071 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8072 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8073 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8074 equalLength([line1, line2, line3])
8075}
8076";
8077
8078 let program = Program::parse(initial_source).unwrap().0.unwrap();
8079
8080 let mut frontend = FrontendState::new();
8081
8082 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8083 let mock_ctx = ExecutorContext::new_mock(None).await;
8084 let version = Version(0);
8085
8086 frontend.hack_set_program(&ctx, program).await.unwrap();
8087 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8088 let sketch_id = sketch_object.id;
8089 let sketch = expect_sketch(sketch_object);
8090 let line3_id = *sketch.segments.get(8).unwrap();
8091
8092 let (src_delta, scene_delta) = frontend
8093 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8094 .await
8095 .unwrap();
8096 assert_eq!(
8097 src_delta.text.as_str(),
8098 "\
8099sketch(on = XY) {
8100 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8101 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8102 equalLength([line1, line2])
8103}
8104"
8105 );
8106
8107 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8108 let sketch = expect_sketch(sketch_object);
8109 assert_eq!(sketch.constraints.len(), 1);
8110
8111 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8112 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8113 panic!("Expected constraint object");
8114 };
8115 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8116 panic!("Expected lines equal length constraint");
8117 };
8118 assert_eq!(lines_equal_length.lines.len(), 2);
8119
8120 ctx.close().await;
8121 mock_ctx.close().await;
8122 }
8123
8124 #[tokio::test(flavor = "multi_thread")]
8125 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8126 let initial_source = "\
8127sketch(on = XY) {
8128 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8129 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8130 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8131 horizontal([line1.end, line2.start, line3.start])
8132}
8133";
8134
8135 let program = Program::parse(initial_source).unwrap().0.unwrap();
8136
8137 let mut frontend = FrontendState::new();
8138
8139 let mock_ctx = ExecutorContext::new_mock(None).await;
8140 let version = Version(0);
8141
8142 frontend.program = program.clone();
8143 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8144 frontend.update_state_after_exec(outcome, true);
8145 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8146 let sketch_id = sketch_object.id;
8147 let sketch = expect_sketch(sketch_object);
8148 let line1_id = *sketch.segments.get(2).unwrap();
8149
8150 let (src_delta, scene_delta) = frontend
8151 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8152 .await
8153 .unwrap();
8154 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8155 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8156 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8157 assert!(
8158 src_delta.text.contains("horizontal([line2.start, line3.start])"),
8159 "{}",
8160 src_delta.text
8161 );
8162
8163 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8164 let sketch = expect_sketch(sketch_object);
8165 assert_eq!(sketch.constraints.len(), 1);
8166
8167 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8168 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8169 panic!("Expected constraint object");
8170 };
8171 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8172 panic!("Expected horizontal points constraint");
8173 };
8174 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8175 assert_eq!(*points, remaining_points);
8176
8177 mock_ctx.close().await;
8178 }
8179
8180 #[tokio::test(flavor = "multi_thread")]
8181 async fn test_delete_line_preserves_multiline_vertical_constraint() {
8182 let initial_source = "\
8183sketch(on = XY) {
8184 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8185 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8186 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8187 vertical([line1.end, line2.start, line3.start])
8188}
8189";
8190
8191 let program = Program::parse(initial_source).unwrap().0.unwrap();
8192
8193 let mut frontend = FrontendState::new();
8194
8195 let mock_ctx = ExecutorContext::new_mock(None).await;
8196 let version = Version(0);
8197
8198 frontend.program = program.clone();
8199 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8200 frontend.update_state_after_exec(outcome, true);
8201 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8202 let sketch_id = sketch_object.id;
8203 let sketch = expect_sketch(sketch_object);
8204 let line1_id = *sketch.segments.get(2).unwrap();
8205
8206 let (src_delta, scene_delta) = frontend
8207 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8208 .await
8209 .unwrap();
8210 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8211 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8212 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8213 assert!(
8214 src_delta.text.contains("vertical([line2.start, line3.start])"),
8215 "{}",
8216 src_delta.text
8217 );
8218
8219 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8220 let sketch = expect_sketch(sketch_object);
8221 assert_eq!(sketch.constraints.len(), 1);
8222
8223 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8224 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8225 panic!("Expected constraint object");
8226 };
8227 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8228 panic!("Expected vertical points constraint");
8229 };
8230 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8231 assert_eq!(*points, remaining_points);
8232
8233 mock_ctx.close().await;
8234 }
8235
8236 #[tokio::test(flavor = "multi_thread")]
8237 async fn test_delete_line_preserves_multiline_coincident_constraint() {
8238 let initial_source = "\
8239sketch(on = XY) {
8240 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8241 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8242 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8243 coincident([line1.end, line2.start, line3.start])
8244}
8245";
8246
8247 let program = Program::parse(initial_source).unwrap().0.unwrap();
8248
8249 let mut frontend = FrontendState::new();
8250
8251 let mock_ctx = ExecutorContext::new_mock(None).await;
8252 let version = Version(0);
8253
8254 frontend.program = program.clone();
8255 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8256 frontend.update_state_after_exec(outcome, true);
8257 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8258 let sketch_id = sketch_object.id;
8259 let sketch = expect_sketch(sketch_object);
8260 let line1_id = *sketch.segments.get(2).unwrap();
8261
8262 let (src_delta, scene_delta) = frontend
8263 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8264 .await
8265 .unwrap();
8266 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8267 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8268 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8269 assert!(
8270 src_delta.text.contains("coincident([line2.start, line3.start])"),
8271 "{}",
8272 src_delta.text
8273 );
8274
8275 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8276 let sketch = expect_sketch(sketch_object);
8277 assert_eq!(sketch.constraints.len(), 1);
8278
8279 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8280 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8281 panic!("Expected constraint object");
8282 };
8283 let Constraint::Coincident(coincident) = constraint else {
8284 panic!("Expected coincident constraint");
8285 };
8286 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8287 assert_eq!(coincident.segments, remaining_segments);
8288
8289 mock_ctx.close().await;
8290 }
8291
8292 #[tokio::test(flavor = "multi_thread")]
8293 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8294 let initial_source = "\
8295sketch(on = XY) {
8296 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8297 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8298 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8299 equalLength([line1, line2, line3])
8300}
8301";
8302
8303 let program = Program::parse(initial_source).unwrap().0.unwrap();
8304
8305 let mut frontend = FrontendState::new();
8306
8307 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8308 let mock_ctx = ExecutorContext::new_mock(None).await;
8309 let version = Version(0);
8310
8311 frontend.hack_set_program(&ctx, program).await.unwrap();
8312 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8313 let sketch_id = sketch_object.id;
8314 let sketch = expect_sketch(sketch_object);
8315 let line2_id = *sketch.segments.get(5).unwrap();
8316 let line3_id = *sketch.segments.get(8).unwrap();
8317
8318 let (src_delta, scene_delta) = frontend
8319 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8320 .await
8321 .unwrap();
8322 assert_eq!(
8323 src_delta.text.as_str(),
8324 "\
8325sketch(on = XY) {
8326 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8327}
8328"
8329 );
8330
8331 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8332 let sketch = expect_sketch(sketch_object);
8333 assert!(sketch.constraints.is_empty());
8334
8335 ctx.close().await;
8336 mock_ctx.close().await;
8337 }
8338
8339 #[tokio::test(flavor = "multi_thread")]
8340 async fn test_delete_line_preserves_multiline_parallel_constraint() {
8341 let initial_source = "\
8342sketch(on = XY) {
8343 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8344 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8345 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8346 parallel([line1, line2, line3])
8347}
8348";
8349
8350 let program = Program::parse(initial_source).unwrap().0.unwrap();
8351
8352 let mut frontend = FrontendState::new();
8353
8354 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8355 let mock_ctx = ExecutorContext::new_mock(None).await;
8356 let version = Version(0);
8357
8358 frontend.hack_set_program(&ctx, program).await.unwrap();
8359 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8360 let sketch_id = sketch_object.id;
8361 let sketch = expect_sketch(sketch_object);
8362 let line3_id = *sketch.segments.get(8).unwrap();
8363
8364 let (src_delta, scene_delta) = frontend
8365 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8366 .await
8367 .unwrap();
8368 assert_eq!(
8369 src_delta.text.as_str(),
8370 "\
8371sketch(on = XY) {
8372 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8373 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8374 parallel([line1, line2])
8375}
8376"
8377 );
8378
8379 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8380 let sketch = expect_sketch(sketch_object);
8381 assert_eq!(sketch.constraints.len(), 1);
8382
8383 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8384 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8385 panic!("Expected constraint object");
8386 };
8387 let Constraint::Parallel(parallel) = constraint else {
8388 panic!("Expected parallel constraint");
8389 };
8390 assert_eq!(parallel.lines.len(), 2);
8391
8392 ctx.close().await;
8393 mock_ctx.close().await;
8394 }
8395
8396 #[tokio::test(flavor = "multi_thread")]
8397 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8398 let initial_source = "\
8399sketch(on = XY) {
8400 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8401 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8402 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8403 parallel([line1, line2, line3])
8404}
8405";
8406
8407 let program = Program::parse(initial_source).unwrap().0.unwrap();
8408
8409 let mut frontend = FrontendState::new();
8410
8411 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8412 let mock_ctx = ExecutorContext::new_mock(None).await;
8413 let version = Version(0);
8414
8415 frontend.hack_set_program(&ctx, program).await.unwrap();
8416 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8417 let sketch_id = sketch_object.id;
8418 let sketch = expect_sketch(sketch_object);
8419 let line2_id = *sketch.segments.get(5).unwrap();
8420 let line3_id = *sketch.segments.get(8).unwrap();
8421
8422 let (src_delta, scene_delta) = frontend
8423 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8424 .await
8425 .unwrap();
8426 assert_eq!(
8427 src_delta.text.as_str(),
8428 "\
8429sketch(on = XY) {
8430 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8431}
8432"
8433 );
8434
8435 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8436 let sketch = expect_sketch(sketch_object);
8437 assert!(sketch.constraints.is_empty());
8438
8439 ctx.close().await;
8440 mock_ctx.close().await;
8441 }
8442
8443 #[tokio::test(flavor = "multi_thread")]
8444 async fn test_delete_line_line_coincident_constraint() {
8445 let initial_source = "\
8446sketch(on = XY) {
8447 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8448 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8449 coincident([line1, line2])
8450}
8451";
8452
8453 let program = Program::parse(initial_source).unwrap().0.unwrap();
8454
8455 let mut frontend = FrontendState::new();
8456
8457 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8458 let mock_ctx = ExecutorContext::new_mock(None).await;
8459 let version = Version(0);
8460
8461 frontend.hack_set_program(&ctx, program).await.unwrap();
8462 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8463 let sketch_id = sketch_object.id;
8464 let sketch = expect_sketch(sketch_object);
8465
8466 let coincident_id = *sketch.constraints.first().unwrap();
8467
8468 let (src_delta, scene_delta) = frontend
8469 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8470 .await
8471 .unwrap();
8472 assert_eq!(
8473 src_delta.text.as_str(),
8474 "\
8475sketch(on = XY) {
8476 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8477 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8478}
8479"
8480 );
8481 assert_eq!(scene_delta.new_objects, vec![]);
8482 assert_eq!(scene_delta.new_graph.objects.len(), 8);
8483
8484 ctx.close().await;
8485 mock_ctx.close().await;
8486 }
8487
8488 #[tokio::test(flavor = "multi_thread")]
8489 async fn test_two_points_coincident() {
8490 let initial_source = "\
8491sketch(on = XY) {
8492 point1 = point(at = [var 1, var 2])
8493 point(at = [3, 4])
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 let point0_id = *sketch.segments.first().unwrap();
8510 let point1_id = *sketch.segments.get(1).unwrap();
8511
8512 let constraint = Constraint::Coincident(Coincident {
8513 segments: vec![point0_id.into(), point1_id.into()],
8514 });
8515 let (src_delta, scene_delta) = frontend
8516 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8517 .await
8518 .unwrap();
8519 assert_eq!(
8520 src_delta.text.as_str(),
8521 "\
8522sketch(on = XY) {
8523 point1 = point(at = [var 1, var 2])
8524 point2 = point(at = [3, 4])
8525 coincident([point1, point2])
8526}
8527"
8528 );
8529 assert_eq!(
8530 scene_delta.new_graph.objects.len(),
8531 5,
8532 "{:#?}",
8533 scene_delta.new_graph.objects
8534 );
8535
8536 ctx.close().await;
8537 mock_ctx.close().await;
8538 }
8539
8540 #[tokio::test(flavor = "multi_thread")]
8541 async fn test_three_points_coincident() {
8542 let initial_source = "\
8543sketch(on = XY) {
8544 point1 = point(at = [var 1, var 2])
8545 point(at = [var 3, var 4])
8546 point(at = [var 5, var 6])
8547}
8548";
8549
8550 let program = Program::parse(initial_source).unwrap().0.unwrap();
8551
8552 let mut frontend = FrontendState::new();
8553
8554 let mock_ctx = ExecutorContext::new_mock(None).await;
8555 let version = Version(0);
8556
8557 frontend.program = program.clone();
8558 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8559 frontend.update_state_after_exec(outcome, true);
8560 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8561 let sketch_id = sketch_object.id;
8562 let sketch = expect_sketch(sketch_object);
8563 let segments = sketch
8564 .segments
8565 .iter()
8566 .take(3)
8567 .copied()
8568 .map(Into::into)
8569 .collect::<Vec<ConstraintSegment>>();
8570
8571 let constraint = Constraint::Coincident(Coincident {
8572 segments: segments.clone(),
8573 });
8574 let (src_delta, scene_delta) = frontend
8575 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8576 .await
8577 .unwrap();
8578 assert_eq!(
8579 src_delta.text.as_str(),
8580 "\
8581sketch(on = XY) {
8582 point1 = point(at = [var 1, var 2])
8583 point2 = point(at = [var 3, var 4])
8584 point3 = point(at = [var 5, var 6])
8585 coincident([point1, point2, point3])
8586}
8587"
8588 );
8589
8590 let constraint_object = scene_delta
8591 .new_graph
8592 .objects
8593 .iter()
8594 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8595 .unwrap();
8596
8597 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8598 panic!("expected a constraint object");
8599 };
8600
8601 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8602
8603 mock_ctx.close().await;
8604 }
8605
8606 #[tokio::test(flavor = "multi_thread")]
8607 async fn test_source_with_three_point_coincident_tracks_all_segments() {
8608 let initial_source = "\
8609sketch(on = XY) {
8610 point1 = point(at = [var 1, var 2])
8611 point2 = point(at = [var 3, var 4])
8612 point3 = point(at = [var 5, var 6])
8613 coincident([point1, point2, point3])
8614}
8615";
8616
8617 let program = Program::parse(initial_source).unwrap().0.unwrap();
8618
8619 let mut frontend = FrontendState::new();
8620
8621 let ctx = ExecutorContext::new_mock(None).await;
8622 frontend.program = program.clone();
8623 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8624 frontend.update_state_after_exec(outcome, true);
8625
8626 let constraint_object = frontend
8627 .scene_graph
8628 .objects
8629 .iter()
8630 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8631 .unwrap();
8632 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8633 panic!("expected a constraint object");
8634 };
8635
8636 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8637 let sketch = expect_sketch(sketch_object);
8638 let expected_segments = sketch
8639 .segments
8640 .iter()
8641 .take(3)
8642 .copied()
8643 .map(Into::into)
8644 .collect::<Vec<ConstraintSegment>>();
8645
8646 assert_eq!(
8647 constraint,
8648 &Constraint::Coincident(Coincident {
8649 segments: expected_segments,
8650 })
8651 );
8652
8653 ctx.close().await;
8654 }
8655
8656 #[tokio::test(flavor = "multi_thread")]
8657 async fn test_point_origin_coincident_preserves_order() {
8658 let initial_source = "\
8659sketch(on = XY) {
8660 point(at = [var 1, var 2])
8661}
8662";
8663
8664 for (origin_first, expected_source) in [
8665 (
8666 true,
8667 "\
8668sketch(on = XY) {
8669 point1 = point(at = [var 1, var 2])
8670 coincident([ORIGIN, point1])
8671}
8672",
8673 ),
8674 (
8675 false,
8676 "\
8677sketch(on = XY) {
8678 point1 = point(at = [var 1, var 2])
8679 coincident([point1, ORIGIN])
8680}
8681",
8682 ),
8683 ] {
8684 let program = Program::parse(initial_source).unwrap().0.unwrap();
8685
8686 let mut frontend = FrontendState::new();
8687
8688 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8689 let mock_ctx = ExecutorContext::new_mock(None).await;
8690 let version = Version(0);
8691
8692 frontend.hack_set_program(&ctx, program).await.unwrap();
8693 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8694 let sketch_id = sketch_object.id;
8695 let sketch = expect_sketch(sketch_object);
8696 let point_id = *sketch.segments.first().unwrap();
8697
8698 let segments = if origin_first {
8699 vec![ConstraintSegment::ORIGIN, point_id.into()]
8700 } else {
8701 vec![point_id.into(), ConstraintSegment::ORIGIN]
8702 };
8703 let constraint = Constraint::Coincident(Coincident {
8704 segments: segments.clone(),
8705 });
8706 let (src_delta, scene_delta) = frontend
8707 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8708 .await
8709 .unwrap();
8710 assert_eq!(src_delta.text.as_str(), expected_source);
8711
8712 let constraint_object = scene_delta
8713 .new_graph
8714 .objects
8715 .iter()
8716 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8717 .unwrap();
8718
8719 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8720 panic!("expected a constraint object");
8721 };
8722
8723 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8724
8725 ctx.close().await;
8726 mock_ctx.close().await;
8727 }
8728 }
8729
8730 #[tokio::test(flavor = "multi_thread")]
8731 async fn test_coincident_of_line_end_points() {
8732 let initial_source = "\
8733sketch(on = XY) {
8734 line(start = [var 1, var 2], end = [var 3, var 4])
8735 line(start = [var 5, var 6], end = [var 7, var 8])
8736}
8737";
8738
8739 let program = Program::parse(initial_source).unwrap().0.unwrap();
8740
8741 let mut frontend = FrontendState::new();
8742
8743 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8744 let mock_ctx = ExecutorContext::new_mock(None).await;
8745 let version = Version(0);
8746
8747 frontend.hack_set_program(&ctx, program).await.unwrap();
8748 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8749 let sketch_id = sketch_object.id;
8750 let sketch = expect_sketch(sketch_object);
8751 let point0_id = *sketch.segments.get(1).unwrap();
8752 let point1_id = *sketch.segments.get(3).unwrap();
8753
8754 let constraint = Constraint::Coincident(Coincident {
8755 segments: vec![point0_id.into(), point1_id.into()],
8756 });
8757 let (src_delta, scene_delta) = frontend
8758 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8759 .await
8760 .unwrap();
8761 assert_eq!(
8762 src_delta.text.as_str(),
8763 "\
8764sketch(on = XY) {
8765 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8766 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8767 coincident([line1.end, line2.start])
8768}
8769"
8770 );
8771 assert_eq!(
8772 scene_delta.new_graph.objects.len(),
8773 9,
8774 "{:#?}",
8775 scene_delta.new_graph.objects
8776 );
8777
8778 ctx.close().await;
8779 mock_ctx.close().await;
8780 }
8781
8782 #[tokio::test(flavor = "multi_thread")]
8783 async fn test_coincident_of_line_point_and_circle_segment() {
8784 let initial_source = "\
8785sketch(on = XY) {
8786 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8787 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8788}
8789";
8790 let program = Program::parse(initial_source).unwrap().0.unwrap();
8791 let mut frontend = FrontendState::new();
8792
8793 let mock_ctx = ExecutorContext::new_mock(None).await;
8794 let version = Version(0);
8795
8796 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8797 frontend.program = program;
8798 frontend.update_state_after_exec(outcome, true);
8799 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
8800 let sketch_id = sketch_object.id;
8801 let sketch = expect_sketch(sketch_object);
8802
8803 let circle_id = sketch
8804 .segments
8805 .iter()
8806 .copied()
8807 .find(|seg_id| {
8808 matches!(
8809 &frontend.scene_graph.objects[seg_id.0].kind,
8810 ObjectKind::Segment {
8811 segment: Segment::Circle(_)
8812 }
8813 )
8814 })
8815 .expect("Expected a circle segment in sketch");
8816 let line_id = sketch
8817 .segments
8818 .iter()
8819 .copied()
8820 .find(|seg_id| {
8821 matches!(
8822 &frontend.scene_graph.objects[seg_id.0].kind,
8823 ObjectKind::Segment {
8824 segment: Segment::Line(_)
8825 }
8826 )
8827 })
8828 .expect("Expected a line segment in sketch");
8829
8830 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
8831 ObjectKind::Segment {
8832 segment: Segment::Line(line),
8833 } => line.start,
8834 _ => panic!("Expected line segment object"),
8835 };
8836
8837 let constraint = Constraint::Coincident(Coincident {
8838 segments: vec![line_start_point_id.into(), circle_id.into()],
8839 });
8840 let (src_delta, _scene_delta) = frontend
8841 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8842 .await
8843 .unwrap();
8844 assert_eq!(
8845 src_delta.text.as_str(),
8846 "\
8847sketch(on = XY) {
8848 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8849 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8850 coincident([line1.start, circle1])
8851}
8852"
8853 );
8854
8855 mock_ctx.close().await;
8856 }
8857
8858 #[tokio::test(flavor = "multi_thread")]
8859 async fn test_invalid_coincident_arc_and_line_preserves_state() {
8860 let program = Program::empty();
8868
8869 let mut frontend = FrontendState::new();
8870 frontend.program = program;
8871
8872 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8873 let mock_ctx = ExecutorContext::new_mock(None).await;
8874 let version = Version(0);
8875
8876 let sketch_args = SketchCtor {
8877 on: Plane::Default(PlaneName::Xy),
8878 };
8879 let (_src_delta, _scene_delta, sketch_id) = frontend
8880 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8881 .await
8882 .unwrap();
8883
8884 let arc_ctor = ArcCtor {
8886 start: Point2d {
8887 x: Expr::Var(Number {
8888 value: 0.0,
8889 units: NumericSuffix::Mm,
8890 }),
8891 y: Expr::Var(Number {
8892 value: 0.0,
8893 units: NumericSuffix::Mm,
8894 }),
8895 },
8896 end: Point2d {
8897 x: Expr::Var(Number {
8898 value: 10.0,
8899 units: NumericSuffix::Mm,
8900 }),
8901 y: Expr::Var(Number {
8902 value: 10.0,
8903 units: NumericSuffix::Mm,
8904 }),
8905 },
8906 center: Point2d {
8907 x: Expr::Var(Number {
8908 value: 10.0,
8909 units: NumericSuffix::Mm,
8910 }),
8911 y: Expr::Var(Number {
8912 value: 0.0,
8913 units: NumericSuffix::Mm,
8914 }),
8915 },
8916 construction: None,
8917 };
8918 let (_src_delta, scene_delta) = frontend
8919 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
8920 .await
8921 .unwrap();
8922 let arc_id = *scene_delta.new_objects.last().unwrap();
8924
8925 let line_ctor = LineCtor {
8927 start: Point2d {
8928 x: Expr::Var(Number {
8929 value: 20.0,
8930 units: NumericSuffix::Mm,
8931 }),
8932 y: Expr::Var(Number {
8933 value: 0.0,
8934 units: NumericSuffix::Mm,
8935 }),
8936 },
8937 end: Point2d {
8938 x: Expr::Var(Number {
8939 value: 30.0,
8940 units: NumericSuffix::Mm,
8941 }),
8942 y: Expr::Var(Number {
8943 value: 10.0,
8944 units: NumericSuffix::Mm,
8945 }),
8946 },
8947 construction: None,
8948 };
8949 let (_src_delta, scene_delta) = frontend
8950 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
8951 .await
8952 .unwrap();
8953 let line_id = *scene_delta.new_objects.last().unwrap();
8955
8956 let constraint = Constraint::Coincident(Coincident {
8959 segments: vec![arc_id.into(), line_id.into()],
8960 });
8961 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
8962
8963 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
8965
8966 let sketch_object_after =
8969 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
8970 let sketch_after = expect_sketch(sketch_object_after);
8971
8972 assert!(
8974 sketch_after.segments.contains(&arc_id),
8975 "Arc segment should still exist after failed constraint"
8976 );
8977 assert!(
8978 sketch_after.segments.contains(&line_id),
8979 "Line segment should still exist after failed constraint"
8980 );
8981
8982 let arc_obj = frontend
8984 .scene_graph
8985 .objects
8986 .get(arc_id.0)
8987 .expect("Arc object should still be accessible");
8988 let line_obj = frontend
8989 .scene_graph
8990 .objects
8991 .get(line_id.0)
8992 .expect("Line object should still be accessible");
8993
8994 match &arc_obj.kind {
8997 ObjectKind::Segment {
8998 segment: Segment::Arc(_),
8999 } => {}
9000 _ => panic!("Arc object should still be an arc segment"),
9001 }
9002 match &line_obj.kind {
9003 ObjectKind::Segment {
9004 segment: Segment::Line(_),
9005 } => {}
9006 _ => panic!("Line object should still be a line segment"),
9007 }
9008
9009 ctx.close().await;
9010 mock_ctx.close().await;
9011 }
9012
9013 #[tokio::test(flavor = "multi_thread")]
9014 async fn test_distance_two_points() {
9015 let initial_source = "\
9016sketch(on = XY) {
9017 point(at = [var 1, var 2])
9018 point(at = [var 3, var 4])
9019}
9020";
9021
9022 let program = Program::parse(initial_source).unwrap().0.unwrap();
9023
9024 let mut frontend = FrontendState::new();
9025
9026 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9027 let mock_ctx = ExecutorContext::new_mock(None).await;
9028 let version = Version(0);
9029
9030 frontend.hack_set_program(&ctx, program).await.unwrap();
9031 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9032 let sketch_id = sketch_object.id;
9033 let sketch = expect_sketch(sketch_object);
9034 let point0_id = *sketch.segments.first().unwrap();
9035 let point1_id = *sketch.segments.get(1).unwrap();
9036
9037 let constraint = Constraint::Distance(Distance {
9038 points: vec![point0_id.into(), point1_id.into()],
9039 distance: Number {
9040 value: 2.0,
9041 units: NumericSuffix::Mm,
9042 },
9043 label_position: None,
9044 source: Default::default(),
9045 });
9046 let (src_delta, scene_delta) = frontend
9047 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9048 .await
9049 .unwrap();
9050 assert_eq!(
9051 src_delta.text.as_str(),
9052 "\
9054sketch(on = XY) {
9055 point1 = point(at = [var 1, var 2])
9056 point2 = point(at = [var 3, var 4])
9057 distance([point1, point2]) == 2mm
9058}
9059"
9060 );
9061 assert_eq!(
9062 scene_delta.new_graph.objects.len(),
9063 5,
9064 "{:#?}",
9065 scene_delta.new_graph.objects
9066 );
9067
9068 ctx.close().await;
9069 mock_ctx.close().await;
9070 }
9071
9072 #[tokio::test(flavor = "multi_thread")]
9073 async fn test_distance_two_points_with_label() {
9074 let initial_source = "\
9075sketch(on = XY) {
9076 point(at = [var 1, var 2])
9077 point(at = [var 3, var 4])
9078}
9079";
9080
9081 let program = Program::parse(initial_source).unwrap().0.unwrap();
9082
9083 let mut frontend = FrontendState::new();
9084
9085 let mock_ctx = ExecutorContext::new_mock(None).await;
9086 let version = Version(0);
9087
9088 frontend.program = program.clone();
9089 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9090 frontend.update_state_after_exec(outcome, true);
9091 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9092 let sketch_id = sketch_object.id;
9093 let sketch = expect_sketch(sketch_object);
9094 let point0_id = *sketch.segments.first().unwrap();
9095 let point1_id = *sketch.segments.get(1).unwrap();
9096
9097 let label_position = Point2d {
9098 x: Number {
9099 value: 10.0,
9100 units: NumericSuffix::Mm,
9101 },
9102 y: Number {
9103 value: 11.0,
9104 units: NumericSuffix::Mm,
9105 },
9106 };
9107 let constraint = Constraint::Distance(Distance {
9108 points: vec![point0_id.into(), point1_id.into()],
9109 distance: Number {
9110 value: 2.0,
9111 units: NumericSuffix::Mm,
9112 },
9113 label_position: Some(label_position.clone()),
9114 source: Default::default(),
9115 });
9116 let (src_delta, scene_delta) = frontend
9117 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9118 .await
9119 .unwrap();
9120 assert_eq!(
9121 src_delta.text.as_str(),
9122 "\
9123sketch(on = XY) {
9124 point1 = point(at = [var 1, var 2])
9125 point2 = point(at = [var 3, var 4])
9126 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9127}
9128"
9129 );
9130
9131 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9132 let sketch = expect_sketch(sketch_object);
9133 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9134 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9135 panic!("Expected constraint object");
9136 };
9137 let Constraint::Distance(distance) = constraint else {
9138 panic!("Expected distance constraint");
9139 };
9140 assert_eq!(distance.label_position, Some(label_position));
9141
9142 mock_ctx.close().await;
9143 }
9144
9145 #[tokio::test(flavor = "multi_thread")]
9146 async fn test_edit_distance_constraint_label_position() {
9147 let initial_source = "\
9148sketch(on = XY) {
9149 point(at = [var 1, var 2])
9150 point(at = [var 3, var 2])
9151}
9152";
9153
9154 let program = Program::parse(initial_source).unwrap().0.unwrap();
9155
9156 let mut frontend = FrontendState::new();
9157
9158 let mock_ctx = ExecutorContext::new_mock(None).await;
9159 let version = Version(0);
9160
9161 frontend.program = program.clone();
9162 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9163 frontend.update_state_after_exec(outcome, true);
9164 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9165 let sketch_id = sketch_object.id;
9166 let sketch = expect_sketch(sketch_object);
9167 let point0_id = *sketch.segments.first().unwrap();
9168 let point1_id = *sketch.segments.get(1).unwrap();
9169
9170 let constraint = Constraint::Distance(Distance {
9171 points: vec![point0_id.into(), point1_id.into()],
9172 distance: Number {
9173 value: 2.0,
9174 units: NumericSuffix::Mm,
9175 },
9176 label_position: None,
9177 source: Default::default(),
9178 });
9179 let (_, scene_delta) = frontend
9180 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9181 .await
9182 .unwrap();
9183 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9184 let sketch = expect_sketch(sketch_object);
9185 let constraint_id = sketch.constraints[0];
9186 let label_position = Point2d {
9187 x: Number {
9188 value: 10.0,
9189 units: NumericSuffix::Mm,
9190 },
9191 y: Number {
9192 value: 11.0,
9193 units: NumericSuffix::Mm,
9194 },
9195 };
9196
9197 let (src_delta, scene_delta) = frontend
9198 .edit_distance_constraint_label_position(
9199 &mock_ctx,
9200 version,
9201 sketch_id,
9202 constraint_id,
9203 label_position.clone(),
9204 vec![],
9205 )
9206 .await
9207 .unwrap();
9208 assert_eq!(
9209 src_delta.text.as_str(),
9210 "\
9211sketch(on = XY) {
9212 point1 = point(at = [var 1mm, var 2mm])
9213 point2 = point(at = [var 3mm, var 2mm])
9214 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9215}
9216"
9217 );
9218
9219 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9220 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9221 panic!("Expected constraint object");
9222 };
9223 let Constraint::Distance(distance) = constraint else {
9224 panic!("Expected distance constraint");
9225 };
9226 assert_eq!(distance.label_position, Some(label_position));
9227
9228 mock_ctx.close().await;
9229 }
9230
9231 #[tokio::test(flavor = "multi_thread")]
9232 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9233 let initial_source = "\
9234sketch(on = XY) {
9235 point1 = point(at = [var 0mm, var 0mm])
9236 point2 = point(at = [var 10mm, var 0mm])
9237 distance([point1, point2]) == 5mm
9238}
9239";
9240
9241 let program = Program::parse(initial_source).unwrap().0.unwrap();
9242 let mut frontend = FrontendState::new();
9243 let mock_ctx = ExecutorContext::new_mock(None).await;
9244 let version = Version(0);
9245
9246 frontend.program = program.clone();
9247 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9248 frontend.update_state_after_exec(outcome, true);
9249 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9250 let sketch_id = sketch_object.id;
9251 let sketch = expect_sketch(sketch_object);
9252 let point0_id = sketch.segments[0];
9253 let point1_id = sketch.segments[1];
9254 let constraint_id = sketch.constraints[0];
9255
9256 let edited_segments = vec![ExistingSegmentCtor {
9257 id: point0_id,
9258 ctor: SegmentCtor::Point(PointCtor {
9259 position: Point2d {
9260 x: Expr::Var(Number {
9261 value: 2.0,
9262 units: NumericSuffix::Mm,
9263 }),
9264 y: Expr::Var(Number {
9265 value: 1.0,
9266 units: NumericSuffix::Mm,
9267 }),
9268 },
9269 }),
9270 }];
9271 let (_, scene_delta) = frontend
9272 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9273 .await
9274 .unwrap();
9275 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9276 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9277
9278 let label_position = Point2d {
9279 x: Number {
9280 value: 3.0,
9281 units: NumericSuffix::Mm,
9282 },
9283 y: Number {
9284 value: 4.0,
9285 units: NumericSuffix::Mm,
9286 },
9287 };
9288 let (_, scene_delta) = frontend
9289 .edit_distance_constraint_label_position(
9290 &mock_ctx,
9291 version,
9292 sketch_id,
9293 constraint_id,
9294 label_position,
9295 vec![point0_id],
9296 )
9297 .await
9298 .unwrap();
9299
9300 assert_point_position_close(
9301 point_position(&scene_delta.new_graph, point0_id),
9302 point0_after_segment_edit,
9303 );
9304 assert_point_position_close(
9305 point_position(&scene_delta.new_graph, point1_id),
9306 point1_after_segment_edit,
9307 );
9308
9309 mock_ctx.close().await;
9310 }
9311
9312 #[tokio::test(flavor = "multi_thread")]
9313 async fn test_distance_point_line() {
9314 let initial_source = "\
9315sketch(on = XY) {
9316 point(at = [var 0, var 5])
9317 line(start = [var 0, var 0], end = [var 10, var 0])
9318}
9319";
9320
9321 let program = Program::parse(initial_source).unwrap().0.unwrap();
9322
9323 let mut frontend = FrontendState::new();
9324
9325 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9326 let mock_ctx = ExecutorContext::new_mock(None).await;
9327 let version = Version(0);
9328
9329 frontend.hack_set_program(&ctx, program).await.unwrap();
9330 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9331 let sketch_id = sketch_object.id;
9332 let sketch = expect_sketch(sketch_object);
9333 let point_id = *sketch.segments.first().unwrap();
9334 let line_id = *sketch
9335 .segments
9336 .iter()
9337 .find(|segment_id| {
9338 matches!(
9339 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9340 Some(ObjectKind::Segment {
9341 segment: Segment::Line(_)
9342 })
9343 )
9344 })
9345 .unwrap();
9346
9347 let label_position = Point2d {
9348 x: Number {
9349 value: 10.0,
9350 units: NumericSuffix::Mm,
9351 },
9352 y: Number {
9353 value: 11.0,
9354 units: NumericSuffix::Mm,
9355 },
9356 };
9357 let constraint = Constraint::Distance(Distance {
9358 points: vec![point_id.into(), line_id.into()],
9359 distance: Number {
9360 value: 5.0,
9361 units: NumericSuffix::Mm,
9362 },
9363 label_position: Some(label_position.clone()),
9364 source: Default::default(),
9365 });
9366 let (src_delta, scene_delta) = frontend
9367 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9368 .await
9369 .unwrap();
9370 assert_eq!(
9371 src_delta.text.as_str(),
9372 "\
9373sketch(on = XY) {
9374 point1 = point(at = [var 0, var 5])
9375 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9376 distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
9377}
9378"
9379 );
9380 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9381 let sketch = expect_sketch(sketch_object);
9382 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9383 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9384 panic!("Expected constraint object");
9385 };
9386 let Constraint::Distance(distance) = constraint else {
9387 panic!("Expected distance constraint");
9388 };
9389 assert_eq!(distance.label_position, Some(label_position));
9390
9391 ctx.close().await;
9392 mock_ctx.close().await;
9393 }
9394
9395 #[tokio::test(flavor = "multi_thread")]
9396 async fn test_distance_point_arc() {
9397 let initial_source = "\
9398sketch(on = XY) {
9399 point(at = [var 0, var 8])
9400 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9401}
9402";
9403
9404 let program = Program::parse(initial_source).unwrap().0.unwrap();
9405
9406 let mut frontend = FrontendState::new();
9407
9408 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9409 let mock_ctx = ExecutorContext::new_mock(None).await;
9410 let version = Version(0);
9411
9412 frontend.hack_set_program(&ctx, program).await.unwrap();
9413 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9414 let sketch_id = sketch_object.id;
9415 let sketch = expect_sketch(sketch_object);
9416 let point_id = *sketch.segments.first().unwrap();
9417 let arc_id = *sketch
9418 .segments
9419 .iter()
9420 .find(|segment_id| {
9421 matches!(
9422 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9423 Some(ObjectKind::Segment {
9424 segment: Segment::Arc(_)
9425 })
9426 )
9427 })
9428 .unwrap();
9429
9430 let constraint = Constraint::Distance(Distance {
9431 points: vec![point_id.into(), arc_id.into()],
9432 distance: Number {
9433 value: 3.0,
9434 units: NumericSuffix::Mm,
9435 },
9436 label_position: None,
9437 source: Default::default(),
9438 });
9439 let (src_delta, _scene_delta) = frontend
9440 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9441 .await
9442 .unwrap();
9443 assert_eq!(
9444 src_delta.text.as_str(),
9445 "\
9446sketch(on = XY) {
9447 point1 = point(at = [var 0, var 8])
9448 arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9449 distance([point1, arc1]) == 3mm
9450}
9451"
9452 );
9453
9454 ctx.close().await;
9455 mock_ctx.close().await;
9456 }
9457
9458 #[tokio::test(flavor = "multi_thread")]
9459 async fn test_distance_arc_origin() {
9460 let initial_source = "\
9461sketch001 = sketch(on = XY) {
9462 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
9463}
9464";
9465
9466 let program = Program::parse(initial_source).unwrap().0.unwrap();
9467
9468 let mut frontend = FrontendState::new();
9469
9470 let mock_ctx = ExecutorContext::new_mock(None).await;
9471 let version = Version(0);
9472
9473 frontend.program = program.clone();
9474 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9475 frontend.update_state_after_exec(outcome, true);
9476 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9477 let sketch_id = sketch_object.id;
9478 let sketch = expect_sketch(sketch_object);
9479 let arc_id = *sketch
9480 .segments
9481 .iter()
9482 .find(|segment_id| {
9483 matches!(
9484 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9485 Some(ObjectKind::Segment {
9486 segment: Segment::Arc(_)
9487 })
9488 )
9489 })
9490 .unwrap();
9491
9492 let constraint = Constraint::Distance(Distance {
9493 points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
9494 distance: Number {
9495 value: 3.0,
9496 units: NumericSuffix::Mm,
9497 },
9498 label_position: None,
9499 source: Default::default(),
9500 });
9501 let (src_delta, _scene_delta) = frontend
9502 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9503 .await
9504 .unwrap();
9505 assert_eq!(
9506 src_delta.text.as_str(),
9507 "\
9508sketch001 = sketch(on = XY) {
9509 arc1 = arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
9510 distance([arc1, ORIGIN]) == 3mm
9511}
9512"
9513 );
9514
9515 mock_ctx.close().await;
9516 }
9517
9518 #[tokio::test(flavor = "multi_thread")]
9519 async fn test_distance_line_origin() {
9520 let initial_source = "\
9521sketch(on = XY) {
9522 line(start = [var 5, var 0], end = [var 5, var 10])
9523}
9524";
9525
9526 let program = Program::parse(initial_source).unwrap().0.unwrap();
9527
9528 let mut frontend = FrontendState::new();
9529
9530 let mock_ctx = ExecutorContext::new_mock(None).await;
9531 let version = Version(0);
9532
9533 frontend.program = program.clone();
9534 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9535 frontend.update_state_after_exec(outcome, true);
9536 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9537 let sketch_id = sketch_object.id;
9538 let sketch = expect_sketch(sketch_object);
9539 let line_id = *sketch
9540 .segments
9541 .iter()
9542 .find(|segment_id| {
9543 matches!(
9544 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9545 Some(ObjectKind::Segment {
9546 segment: Segment::Line(_)
9547 })
9548 )
9549 })
9550 .unwrap();
9551
9552 let constraint = Constraint::Distance(Distance {
9553 points: vec![ConstraintSegment::ORIGIN, line_id.into()],
9554 distance: Number {
9555 value: 5.0,
9556 units: NumericSuffix::Mm,
9557 },
9558 label_position: None,
9559 source: Default::default(),
9560 });
9561 let (src_delta, _scene_delta) = frontend
9562 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9563 .await
9564 .unwrap();
9565 assert_eq!(
9566 src_delta.text.as_str(),
9567 "\
9568sketch(on = XY) {
9569 line1 = line(start = [var 5, var 0], end = [var 5, var 10])
9570 distance([ORIGIN, line1]) == 5mm
9571}
9572"
9573 );
9574
9575 mock_ctx.close().await;
9576 }
9577
9578 #[tokio::test(flavor = "multi_thread")]
9579 async fn test_distance_line_circle() {
9580 let initial_source = "\
9581sketch(on = XY) {
9582 line(start = [var -10, var 8], end = [var 10, var 8])
9583 circle(start = [var 5, var 0], center = [var 0, var 0])
9584}
9585";
9586
9587 let program = Program::parse(initial_source).unwrap().0.unwrap();
9588
9589 let mut frontend = FrontendState::new();
9590
9591 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9592 let mock_ctx = ExecutorContext::new_mock(None).await;
9593 let version = Version(0);
9594
9595 frontend.hack_set_program(&ctx, program).await.unwrap();
9596 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9597 let sketch_id = sketch_object.id;
9598 let sketch = expect_sketch(sketch_object);
9599 let line_id = *sketch
9600 .segments
9601 .iter()
9602 .find(|segment_id| {
9603 matches!(
9604 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9605 Some(ObjectKind::Segment {
9606 segment: Segment::Line(_)
9607 })
9608 )
9609 })
9610 .unwrap();
9611 let circle_id = *sketch
9612 .segments
9613 .iter()
9614 .find(|segment_id| {
9615 matches!(
9616 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9617 Some(ObjectKind::Segment {
9618 segment: Segment::Circle(_)
9619 })
9620 )
9621 })
9622 .unwrap();
9623
9624 let constraint = Constraint::Distance(Distance {
9625 points: vec![line_id.into(), circle_id.into()],
9626 distance: Number {
9627 value: 3.0,
9628 units: NumericSuffix::Mm,
9629 },
9630 label_position: None,
9631 source: Default::default(),
9632 });
9633 let (src_delta, _scene_delta) = frontend
9634 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9635 .await
9636 .unwrap();
9637 assert_eq!(
9638 src_delta.text.as_str(),
9639 "\
9640sketch(on = XY) {
9641 line1 = line(start = [var -10, var 8], end = [var 10, var 8])
9642 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
9643 distance([line1, circle1]) == 3mm
9644}
9645"
9646 );
9647
9648 ctx.close().await;
9649 mock_ctx.close().await;
9650 }
9651
9652 #[tokio::test(flavor = "multi_thread")]
9653 async fn test_distance_circle_arc() {
9654 let initial_source = "\
9655sketch(on = XY) {
9656 circle(start = [var 5, var 0], center = [var 0, var 0])
9657 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
9658}
9659";
9660
9661 let program = Program::parse(initial_source).unwrap().0.unwrap();
9662
9663 let mut frontend = FrontendState::new();
9664
9665 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9666 let mock_ctx = ExecutorContext::new_mock(None).await;
9667 let version = Version(0);
9668
9669 frontend.hack_set_program(&ctx, program).await.unwrap();
9670 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9671 let sketch_id = sketch_object.id;
9672 let sketch = expect_sketch(sketch_object);
9673 let circle_id = *sketch
9674 .segments
9675 .iter()
9676 .find(|segment_id| {
9677 matches!(
9678 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9679 Some(ObjectKind::Segment {
9680 segment: Segment::Circle(_)
9681 })
9682 )
9683 })
9684 .unwrap();
9685 let arc_id = *sketch
9686 .segments
9687 .iter()
9688 .find(|segment_id| {
9689 matches!(
9690 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9691 Some(ObjectKind::Segment {
9692 segment: Segment::Arc(_)
9693 })
9694 )
9695 })
9696 .unwrap();
9697
9698 let constraint = Constraint::Distance(Distance {
9699 points: vec![circle_id.into(), arc_id.into()],
9700 distance: Number {
9701 value: 3.0,
9702 units: NumericSuffix::Mm,
9703 },
9704 label_position: None,
9705 source: Default::default(),
9706 });
9707 let (src_delta, _scene_delta) = frontend
9708 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9709 .await
9710 .unwrap();
9711 assert_eq!(
9712 src_delta.text.as_str(),
9713 "\
9714sketch(on = XY) {
9715 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
9716 arc1 = arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
9717 distance([circle1, arc1]) == 3mm
9718}
9719"
9720 );
9721
9722 ctx.close().await;
9723 mock_ctx.close().await;
9724 }
9725
9726 #[tokio::test(flavor = "multi_thread")]
9727 async fn test_distance_parallel_lines() {
9728 let initial_source = "\
9729sketch(on = XY) {
9730 line(start = [var 0, var 0], end = [var 10, var 0])
9731 line(start = [var 0, var 5], end = [var 10, var 5])
9732}
9733";
9734
9735 let program = Program::parse(initial_source).unwrap().0.unwrap();
9736
9737 let mut frontend = FrontendState::new();
9738
9739 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9740 let mock_ctx = ExecutorContext::new_mock(None).await;
9741 let version = Version(0);
9742
9743 frontend.hack_set_program(&ctx, program).await.unwrap();
9744 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9745 let sketch_id = sketch_object.id;
9746 let sketch = expect_sketch(sketch_object);
9747 let line_ids = sketch
9748 .segments
9749 .iter()
9750 .copied()
9751 .filter(|segment_id| {
9752 matches!(
9753 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9754 Some(ObjectKind::Segment {
9755 segment: Segment::Line(_)
9756 })
9757 )
9758 })
9759 .collect::<Vec<_>>();
9760
9761 let constraint = Constraint::Distance(Distance {
9762 points: vec![line_ids[0].into(), line_ids[1].into()],
9763 distance: Number {
9764 value: 5.0,
9765 units: NumericSuffix::Mm,
9766 },
9767 label_position: None,
9768 source: Default::default(),
9769 });
9770 let (src_delta, _scene_delta) = frontend
9771 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9772 .await
9773 .unwrap();
9774 assert_eq!(
9775 src_delta.text.as_str(),
9776 "\
9777sketch(on = XY) {
9778 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9779 line2 = line(start = [var 0, var 5], end = [var 10, var 5])
9780 distance([line1, line2]) == 5mm
9781}
9782"
9783 );
9784
9785 ctx.close().await;
9786 mock_ctx.close().await;
9787 }
9788
9789 #[tokio::test(flavor = "multi_thread")]
9790 async fn test_distance_non_parallel_lines_lowers_to_distance() {
9791 let initial_source = "\
9792sketch(on = XY) {
9793 line(start = [var 0, var 0], end = [var 10, var 0])
9794 line(start = [var 0, var 0], end = [var 0, var 10])
9795}
9796";
9797
9798 let program = Program::parse(initial_source).unwrap().0.unwrap();
9799
9800 let mut frontend = FrontendState::new();
9801
9802 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9803 let mock_ctx = ExecutorContext::new_mock(None).await;
9804 let version = Version(0);
9805
9806 frontend.hack_set_program(&ctx, program).await.unwrap();
9807 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9808 let sketch_id = sketch_object.id;
9809 let sketch = expect_sketch(sketch_object);
9810 let line_ids = sketch
9811 .segments
9812 .iter()
9813 .copied()
9814 .filter(|segment_id| {
9815 matches!(
9816 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9817 Some(ObjectKind::Segment {
9818 segment: Segment::Line(_)
9819 })
9820 )
9821 })
9822 .collect::<Vec<_>>();
9823
9824 let constraint = Constraint::Distance(Distance {
9825 points: vec![line_ids[0].into(), line_ids[1].into()],
9826 distance: Number {
9827 value: 5.0,
9828 units: NumericSuffix::Mm,
9829 },
9830 label_position: None,
9831 source: Default::default(),
9832 });
9833 let (src_delta, _scene_delta) = frontend
9834 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9835 .await
9836 .unwrap();
9837 assert_eq!(
9838 src_delta.text.as_str(),
9839 "\
9840sketch(on = XY) {
9841 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9842 line2 = line(start = [var 0, var 0], end = [var 0, var 10])
9843 distance([line1, line2]) == 5mm
9844}
9845"
9846 );
9847
9848 ctx.close().await;
9849 mock_ctx.close().await;
9850 }
9851
9852 #[tokio::test(flavor = "multi_thread")]
9853 async fn test_horizontal_distance_two_points() {
9854 let initial_source = "\
9855sketch(on = XY) {
9856 point(at = [var 1, var 2])
9857 point(at = [var 3, var 4])
9858}
9859";
9860
9861 let program = Program::parse(initial_source).unwrap().0.unwrap();
9862
9863 let mut frontend = FrontendState::new();
9864
9865 let mock_ctx = ExecutorContext::new_mock(None).await;
9866 let version = Version(0);
9867
9868 frontend.program = program.clone();
9869 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9870 frontend.update_state_after_exec(outcome, true);
9871 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9872 let sketch_id = sketch_object.id;
9873 let sketch = expect_sketch(sketch_object);
9874 let point0_id = *sketch.segments.first().unwrap();
9875 let point1_id = *sketch.segments.get(1).unwrap();
9876 let label_position = Point2d {
9877 x: Number {
9878 value: 10.0,
9879 units: NumericSuffix::Mm,
9880 },
9881 y: Number {
9882 value: 11.0,
9883 units: NumericSuffix::Mm,
9884 },
9885 };
9886
9887 let constraint = Constraint::HorizontalDistance(Distance {
9888 points: vec![point0_id.into(), point1_id.into()],
9889 distance: Number {
9890 value: 2.0,
9891 units: NumericSuffix::Mm,
9892 },
9893 label_position: Some(label_position.clone()),
9894 source: Default::default(),
9895 });
9896 let (src_delta, scene_delta) = frontend
9897 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9898 .await
9899 .unwrap();
9900 assert_eq!(
9901 src_delta.text.as_str(),
9902 "\
9904sketch(on = XY) {
9905 point1 = point(at = [var 1, var 2])
9906 point2 = point(at = [var 3, var 4])
9907 horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9908}
9909"
9910 );
9911 assert_eq!(
9912 scene_delta.new_graph.objects.len(),
9913 5,
9914 "{:#?}",
9915 scene_delta.new_graph.objects
9916 );
9917 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9918 let sketch = expect_sketch(sketch_object);
9919 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9920 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9921 panic!("Expected constraint object");
9922 };
9923 let Constraint::HorizontalDistance(distance) = constraint else {
9924 panic!("Expected horizontal distance constraint");
9925 };
9926 assert_eq!(distance.label_position, Some(label_position));
9927
9928 mock_ctx.close().await;
9929 }
9930
9931 #[tokio::test(flavor = "multi_thread")]
9932 async fn test_radius_single_arc_segment() {
9933 let initial_source = "\
9934sketch(on = XY) {
9935 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9936}
9937";
9938
9939 let program = Program::parse(initial_source).unwrap().0.unwrap();
9940
9941 let mut frontend = FrontendState::new();
9942
9943 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9944 let mock_ctx = ExecutorContext::new_mock(None).await;
9945 let version = Version(0);
9946
9947 frontend.hack_set_program(&ctx, program).await.unwrap();
9948 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9949 let sketch_id = sketch_object.id;
9950 let sketch = expect_sketch(sketch_object);
9951 let arc_id = sketch
9953 .segments
9954 .iter()
9955 .find(|&seg_id| {
9956 let obj = frontend.scene_graph.objects.get(seg_id.0);
9957 matches!(
9958 obj.map(|o| &o.kind),
9959 Some(ObjectKind::Segment {
9960 segment: Segment::Arc(_)
9961 })
9962 )
9963 })
9964 .unwrap();
9965
9966 let constraint = Constraint::Radius(Radius {
9967 arc: *arc_id,
9968 radius: Number {
9969 value: 5.0,
9970 units: NumericSuffix::Mm,
9971 },
9972 label_position: None,
9973 source: Default::default(),
9974 });
9975 let (src_delta, scene_delta) = frontend
9976 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9977 .await
9978 .unwrap();
9979 assert_eq!(
9980 src_delta.text.as_str(),
9981 "\
9983sketch(on = XY) {
9984 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9985 radius(arc1) == 5mm
9986}
9987"
9988 );
9989 assert_eq!(
9990 scene_delta.new_graph.objects.len(),
9991 7, "{:#?}",
9993 scene_delta.new_graph.objects
9994 );
9995
9996 ctx.close().await;
9997 mock_ctx.close().await;
9998 }
9999
10000 #[tokio::test(flavor = "multi_thread")]
10001 async fn test_radius_single_arc_segment_with_label_position() {
10002 let initial_source = "\
10003sketch(on = XY) {
10004 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10005}
10006";
10007
10008 let program = Program::parse(initial_source).unwrap().0.unwrap();
10009 let mut frontend = FrontendState::new();
10010 let mock_ctx = ExecutorContext::new_mock(None).await;
10011 let version = Version(0);
10012
10013 frontend.program = program.clone();
10014 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10015 frontend.update_state_after_exec(outcome, true);
10016 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10017 let sketch_id = sketch_object.id;
10018 let sketch = expect_sketch(sketch_object);
10019 let arc_id = sketch
10020 .segments
10021 .iter()
10022 .find(|&seg_id| {
10023 let obj = frontend.scene_graph.objects.get(seg_id.0);
10024 matches!(
10025 obj.map(|o| &o.kind),
10026 Some(ObjectKind::Segment {
10027 segment: Segment::Arc(_)
10028 })
10029 )
10030 })
10031 .unwrap();
10032
10033 let label_position = Point2d {
10034 x: Number {
10035 value: 10.0,
10036 units: NumericSuffix::Mm,
10037 },
10038 y: Number {
10039 value: 11.0,
10040 units: NumericSuffix::Mm,
10041 },
10042 };
10043 let constraint = Constraint::Radius(Radius {
10044 arc: *arc_id,
10045 radius: Number {
10046 value: 5.0,
10047 units: NumericSuffix::Mm,
10048 },
10049 label_position: Some(label_position.clone()),
10050 source: Default::default(),
10051 });
10052 let (src_delta, scene_delta) = frontend
10053 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10054 .await
10055 .unwrap();
10056 assert_eq!(
10057 src_delta.text.as_str(),
10058 "\
10059sketch(on = XY) {
10060 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10061 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10062}
10063"
10064 );
10065
10066 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10067 let sketch = expect_sketch(sketch_object);
10068 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10069 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10070 panic!("Expected constraint object");
10071 };
10072 let Constraint::Radius(radius) = constraint else {
10073 panic!("Expected radius constraint");
10074 };
10075 assert_eq!(radius.label_position, Some(label_position));
10076
10077 mock_ctx.close().await;
10078 }
10079
10080 #[tokio::test(flavor = "multi_thread")]
10081 async fn test_edit_radius_constraint_label_position() {
10082 let initial_source = "\
10083sketch(on = XY) {
10084 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10085 radius(arc1) == 5mm
10086}
10087";
10088
10089 let program = Program::parse(initial_source).unwrap().0.unwrap();
10090 let mut frontend = FrontendState::new();
10091 let mock_ctx = ExecutorContext::new_mock(None).await;
10092 let version = Version(0);
10093
10094 frontend.program = program.clone();
10095 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10096 frontend.update_state_after_exec(outcome, true);
10097 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10098 let sketch_id = sketch_object.id;
10099 let sketch = expect_sketch(sketch_object);
10100 let constraint_id = sketch.constraints[0];
10101 let label_position = Point2d {
10102 x: Number {
10103 value: 10.0,
10104 units: NumericSuffix::Mm,
10105 },
10106 y: Number {
10107 value: 11.0,
10108 units: NumericSuffix::Mm,
10109 },
10110 };
10111
10112 let (src_delta, scene_delta) = frontend
10113 .edit_distance_constraint_label_position(
10114 &mock_ctx,
10115 version,
10116 sketch_id,
10117 constraint_id,
10118 label_position.clone(),
10119 vec![],
10120 )
10121 .await
10122 .unwrap();
10123 assert_eq!(
10124 src_delta.text.as_str(),
10125 "\
10126sketch(on = XY) {
10127 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10128 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10129}
10130"
10131 );
10132
10133 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10134 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10135 panic!("Expected constraint object");
10136 };
10137 let Constraint::Radius(radius) = constraint else {
10138 panic!("Expected radius constraint");
10139 };
10140 assert_eq!(radius.label_position, Some(label_position));
10141
10142 mock_ctx.close().await;
10143 }
10144
10145 #[tokio::test(flavor = "multi_thread")]
10146 async fn test_vertical_distance_two_points() {
10147 let initial_source = "\
10148sketch(on = XY) {
10149 point(at = [var 1, var 2])
10150 point(at = [var 3, var 4])
10151}
10152";
10153
10154 let program = Program::parse(initial_source).unwrap().0.unwrap();
10155
10156 let mut frontend = FrontendState::new();
10157
10158 let mock_ctx = ExecutorContext::new_mock(None).await;
10159 let version = Version(0);
10160
10161 frontend.program = program.clone();
10162 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10163 frontend.update_state_after_exec(outcome, true);
10164 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10165 let sketch_id = sketch_object.id;
10166 let sketch = expect_sketch(sketch_object);
10167 let point0_id = *sketch.segments.first().unwrap();
10168 let point1_id = *sketch.segments.get(1).unwrap();
10169 let label_position = Point2d {
10170 x: Number {
10171 value: 10.0,
10172 units: NumericSuffix::Mm,
10173 },
10174 y: Number {
10175 value: 11.0,
10176 units: NumericSuffix::Mm,
10177 },
10178 };
10179
10180 let constraint = Constraint::VerticalDistance(Distance {
10181 points: vec![point0_id.into(), point1_id.into()],
10182 distance: Number {
10183 value: 2.0,
10184 units: NumericSuffix::Mm,
10185 },
10186 label_position: Some(label_position.clone()),
10187 source: Default::default(),
10188 });
10189 let (src_delta, scene_delta) = frontend
10190 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10191 .await
10192 .unwrap();
10193 assert_eq!(
10194 src_delta.text.as_str(),
10195 "\
10197sketch(on = XY) {
10198 point1 = point(at = [var 1, var 2])
10199 point2 = point(at = [var 3, var 4])
10200 verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10201}
10202"
10203 );
10204 assert_eq!(
10205 scene_delta.new_graph.objects.len(),
10206 5,
10207 "{:#?}",
10208 scene_delta.new_graph.objects
10209 );
10210 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10211 let sketch = expect_sketch(sketch_object);
10212 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10213 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10214 panic!("Expected constraint object");
10215 };
10216 let Constraint::VerticalDistance(distance) = constraint else {
10217 panic!("Expected vertical distance constraint");
10218 };
10219 assert_eq!(distance.label_position, Some(label_position));
10220
10221 mock_ctx.close().await;
10222 }
10223
10224 #[tokio::test(flavor = "multi_thread")]
10225 async fn test_add_fixed_standalone_point() {
10226 let initial_source = "\
10227sketch(on = XY) {
10228 point(at = [var 1, var 2])
10229}
10230";
10231
10232 let program = Program::parse(initial_source).unwrap().0.unwrap();
10233
10234 let mut frontend = FrontendState::new();
10235
10236 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10237 let mock_ctx = ExecutorContext::new_mock(None).await;
10238 let version = Version(0);
10239
10240 frontend.hack_set_program(&ctx, program).await.unwrap();
10241 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10242 let sketch_id = sketch_object.id;
10243 let sketch = expect_sketch(sketch_object);
10244 let point_id = *sketch.segments.first().unwrap();
10245
10246 let (src_delta, scene_delta) = frontend
10247 .add_constraint(
10248 &mock_ctx,
10249 version,
10250 sketch_id,
10251 Constraint::Fixed(Fixed {
10252 points: vec![FixedPoint {
10253 point: point_id,
10254 position: Point2d {
10255 x: Number {
10256 value: 2.0,
10257 units: NumericSuffix::Mm,
10258 },
10259 y: Number {
10260 value: 3.0,
10261 units: NumericSuffix::Mm,
10262 },
10263 },
10264 }],
10265 }),
10266 )
10267 .await
10268 .unwrap();
10269 assert_eq!(
10270 src_delta.text.as_str(),
10271 "\
10272sketch(on = XY) {
10273 point1 = point(at = [var 1, var 2])
10274 fixed([point1, [2mm, 3mm]])
10275}
10276"
10277 );
10278 assert_eq!(
10279 scene_delta.new_graph.objects.len(),
10280 4,
10281 "{:#?}",
10282 scene_delta.new_graph.objects
10283 );
10284
10285 ctx.close().await;
10286 mock_ctx.close().await;
10287 }
10288
10289 #[tokio::test(flavor = "multi_thread")]
10290 async fn test_add_fixed_multiple_points() {
10291 let initial_source = "\
10292sketch(on = XY) {
10293 point(at = [var 1, var 2])
10294 point(at = [var 3, var 4])
10295}
10296";
10297
10298 let program = Program::parse(initial_source).unwrap().0.unwrap();
10299
10300 let mut frontend = FrontendState::new();
10301
10302 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10303 let mock_ctx = ExecutorContext::new_mock(None).await;
10304 let version = Version(0);
10305
10306 frontend.hack_set_program(&ctx, program).await.unwrap();
10307 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10308 let sketch_id = sketch_object.id;
10309 let sketch = expect_sketch(sketch_object);
10310 let point0_id = *sketch.segments.first().unwrap();
10311 let point1_id = *sketch.segments.get(1).unwrap();
10312
10313 let (src_delta, scene_delta) = frontend
10314 .add_constraint(
10315 &mock_ctx,
10316 version,
10317 sketch_id,
10318 Constraint::Fixed(Fixed {
10319 points: vec![
10320 FixedPoint {
10321 point: point0_id,
10322 position: Point2d {
10323 x: Number {
10324 value: 2.0,
10325 units: NumericSuffix::Mm,
10326 },
10327 y: Number {
10328 value: 3.0,
10329 units: NumericSuffix::Mm,
10330 },
10331 },
10332 },
10333 FixedPoint {
10334 point: point1_id,
10335 position: Point2d {
10336 x: Number {
10337 value: 4.0,
10338 units: NumericSuffix::Mm,
10339 },
10340 y: Number {
10341 value: 5.0,
10342 units: NumericSuffix::Mm,
10343 },
10344 },
10345 },
10346 ],
10347 }),
10348 )
10349 .await
10350 .unwrap();
10351 assert_eq!(
10352 src_delta.text.as_str(),
10353 "\
10354sketch(on = XY) {
10355 point1 = point(at = [var 1, var 2])
10356 point2 = point(at = [var 3, var 4])
10357 fixed([point1, [2mm, 3mm]])
10358 fixed([point2, [4mm, 5mm]])
10359}
10360"
10361 );
10362 assert_eq!(
10363 scene_delta.new_graph.objects.len(),
10364 6,
10365 "{:#?}",
10366 scene_delta.new_graph.objects
10367 );
10368
10369 ctx.close().await;
10370 mock_ctx.close().await;
10371 }
10372
10373 #[tokio::test(flavor = "multi_thread")]
10374 async fn test_add_fixed_owned_point() {
10375 let initial_source = "\
10376sketch(on = XY) {
10377 line(start = [var 1, var 2], end = [var 3, var 4])
10378}
10379";
10380
10381 let program = Program::parse(initial_source).unwrap().0.unwrap();
10382
10383 let mut frontend = FrontendState::new();
10384
10385 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10386 let mock_ctx = ExecutorContext::new_mock(None).await;
10387 let version = Version(0);
10388
10389 frontend.hack_set_program(&ctx, program).await.unwrap();
10390 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10391 let sketch_id = sketch_object.id;
10392 let sketch = expect_sketch(sketch_object);
10393 let line_start_id = *sketch.segments.first().unwrap();
10394
10395 let (src_delta, scene_delta) = frontend
10396 .add_constraint(
10397 &mock_ctx,
10398 version,
10399 sketch_id,
10400 Constraint::Fixed(Fixed {
10401 points: vec![FixedPoint {
10402 point: line_start_id,
10403 position: Point2d {
10404 x: Number {
10405 value: 2.0,
10406 units: NumericSuffix::Mm,
10407 },
10408 y: Number {
10409 value: 3.0,
10410 units: NumericSuffix::Mm,
10411 },
10412 },
10413 }],
10414 }),
10415 )
10416 .await
10417 .unwrap();
10418 assert_eq!(
10419 src_delta.text.as_str(),
10420 "\
10421sketch(on = XY) {
10422 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10423 fixed([line1.start, [2mm, 3mm]])
10424}
10425"
10426 );
10427 assert_eq!(
10428 scene_delta.new_graph.objects.len(),
10429 6,
10430 "{:#?}",
10431 scene_delta.new_graph.objects
10432 );
10433
10434 ctx.close().await;
10435 mock_ctx.close().await;
10436 }
10437
10438 #[tokio::test(flavor = "multi_thread")]
10439 async fn test_radius_error_cases() {
10440 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10441 let mock_ctx = ExecutorContext::new_mock(None).await;
10442 let version = Version(0);
10443
10444 let initial_source_point = "\
10446sketch(on = XY) {
10447 point(at = [var 1, var 2])
10448}
10449";
10450 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10451 let mut frontend_point = FrontendState::new();
10452 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
10453 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
10454 let sketch_id_point = sketch_object_point.id;
10455 let sketch_point = expect_sketch(sketch_object_point);
10456 let point_id = *sketch_point.segments.first().unwrap();
10457
10458 let constraint_point = Constraint::Radius(Radius {
10459 arc: point_id,
10460 radius: Number {
10461 value: 5.0,
10462 units: NumericSuffix::Mm,
10463 },
10464 label_position: None,
10465 source: Default::default(),
10466 });
10467 let result_point = frontend_point
10468 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
10469 .await;
10470 assert!(result_point.is_err(), "Single point should error for radius");
10471
10472 let initial_source_line = "\
10474sketch(on = XY) {
10475 line(start = [var 1, var 2], end = [var 3, var 4])
10476}
10477";
10478 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
10479 let mut frontend_line = FrontendState::new();
10480 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10481 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10482 let sketch_id_line = sketch_object_line.id;
10483 let sketch_line = expect_sketch(sketch_object_line);
10484 let line_id = *sketch_line.segments.first().unwrap();
10485
10486 let constraint_line = Constraint::Radius(Radius {
10487 arc: line_id,
10488 radius: Number {
10489 value: 5.0,
10490 units: NumericSuffix::Mm,
10491 },
10492 label_position: None,
10493 source: Default::default(),
10494 });
10495 let result_line = frontend_line
10496 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10497 .await;
10498 assert!(result_line.is_err(), "Single line segment should error for radius");
10499
10500 ctx.close().await;
10501 mock_ctx.close().await;
10502 }
10503
10504 #[tokio::test(flavor = "multi_thread")]
10505 async fn test_diameter_single_arc_segment() {
10506 let initial_source = "\
10507sketch(on = XY) {
10508 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10509}
10510";
10511
10512 let program = Program::parse(initial_source).unwrap().0.unwrap();
10513
10514 let mut frontend = FrontendState::new();
10515
10516 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10517 let mock_ctx = ExecutorContext::new_mock(None).await;
10518 let version = Version(0);
10519
10520 frontend.hack_set_program(&ctx, program).await.unwrap();
10521 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10522 let sketch_id = sketch_object.id;
10523 let sketch = expect_sketch(sketch_object);
10524 let arc_id = sketch
10526 .segments
10527 .iter()
10528 .find(|&seg_id| {
10529 let obj = frontend.scene_graph.objects.get(seg_id.0);
10530 matches!(
10531 obj.map(|o| &o.kind),
10532 Some(ObjectKind::Segment {
10533 segment: Segment::Arc(_)
10534 })
10535 )
10536 })
10537 .unwrap();
10538
10539 let constraint = Constraint::Diameter(Diameter {
10540 arc: *arc_id,
10541 diameter: Number {
10542 value: 10.0,
10543 units: NumericSuffix::Mm,
10544 },
10545 label_position: None,
10546 source: Default::default(),
10547 });
10548 let (src_delta, scene_delta) = frontend
10549 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10550 .await
10551 .unwrap();
10552 assert_eq!(
10553 src_delta.text.as_str(),
10554 "\
10556sketch(on = XY) {
10557 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10558 diameter(arc1) == 10mm
10559}
10560"
10561 );
10562 assert_eq!(
10563 scene_delta.new_graph.objects.len(),
10564 7, "{:#?}",
10566 scene_delta.new_graph.objects
10567 );
10568
10569 ctx.close().await;
10570 mock_ctx.close().await;
10571 }
10572
10573 #[tokio::test(flavor = "multi_thread")]
10574 async fn test_diameter_single_arc_segment_with_label_position() {
10575 let initial_source = "\
10576sketch(on = XY) {
10577 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10578}
10579";
10580
10581 let program = Program::parse(initial_source).unwrap().0.unwrap();
10582 let mut frontend = FrontendState::new();
10583 let mock_ctx = ExecutorContext::new_mock(None).await;
10584 let version = Version(0);
10585
10586 frontend.program = program.clone();
10587 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10588 frontend.update_state_after_exec(outcome, true);
10589 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10590 let sketch_id = sketch_object.id;
10591 let sketch = expect_sketch(sketch_object);
10592 let arc_id = sketch
10593 .segments
10594 .iter()
10595 .find(|&seg_id| {
10596 let obj = frontend.scene_graph.objects.get(seg_id.0);
10597 matches!(
10598 obj.map(|o| &o.kind),
10599 Some(ObjectKind::Segment {
10600 segment: Segment::Arc(_)
10601 })
10602 )
10603 })
10604 .unwrap();
10605
10606 let label_position = Point2d {
10607 x: Number {
10608 value: 10.0,
10609 units: NumericSuffix::Mm,
10610 },
10611 y: Number {
10612 value: 11.0,
10613 units: NumericSuffix::Mm,
10614 },
10615 };
10616 let constraint = Constraint::Diameter(Diameter {
10617 arc: *arc_id,
10618 diameter: Number {
10619 value: 10.0,
10620 units: NumericSuffix::Mm,
10621 },
10622 label_position: Some(label_position.clone()),
10623 source: Default::default(),
10624 });
10625 let (src_delta, scene_delta) = frontend
10626 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10627 .await
10628 .unwrap();
10629 assert_eq!(
10630 src_delta.text.as_str(),
10631 "\
10632sketch(on = XY) {
10633 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10634 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10635}
10636"
10637 );
10638
10639 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10640 let sketch = expect_sketch(sketch_object);
10641 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10642 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10643 panic!("Expected constraint object");
10644 };
10645 let Constraint::Diameter(diameter) = constraint else {
10646 panic!("Expected diameter constraint");
10647 };
10648 assert_eq!(diameter.label_position, Some(label_position));
10649
10650 mock_ctx.close().await;
10651 }
10652
10653 #[tokio::test(flavor = "multi_thread")]
10654 async fn test_edit_diameter_constraint_label_position() {
10655 let initial_source = "\
10656sketch(on = XY) {
10657 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10658 diameter(arc1) == 10mm
10659}
10660";
10661
10662 let program = Program::parse(initial_source).unwrap().0.unwrap();
10663 let mut frontend = FrontendState::new();
10664 let mock_ctx = ExecutorContext::new_mock(None).await;
10665 let version = Version(0);
10666
10667 frontend.program = program.clone();
10668 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10669 frontend.update_state_after_exec(outcome, true);
10670 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10671 let sketch_id = sketch_object.id;
10672 let sketch = expect_sketch(sketch_object);
10673 let constraint_id = sketch.constraints[0];
10674 let label_position = Point2d {
10675 x: Number {
10676 value: 10.0,
10677 units: NumericSuffix::Mm,
10678 },
10679 y: Number {
10680 value: 11.0,
10681 units: NumericSuffix::Mm,
10682 },
10683 };
10684
10685 let (src_delta, scene_delta) = frontend
10686 .edit_distance_constraint_label_position(
10687 &mock_ctx,
10688 version,
10689 sketch_id,
10690 constraint_id,
10691 label_position.clone(),
10692 vec![],
10693 )
10694 .await
10695 .unwrap();
10696 assert_eq!(
10697 src_delta.text.as_str(),
10698 "\
10699sketch(on = XY) {
10700 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10701 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10702}
10703"
10704 );
10705
10706 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10707 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10708 panic!("Expected constraint object");
10709 };
10710 let Constraint::Diameter(diameter) = constraint else {
10711 panic!("Expected diameter constraint");
10712 };
10713 assert_eq!(diameter.label_position, Some(label_position));
10714
10715 mock_ctx.close().await;
10716 }
10717
10718 #[tokio::test(flavor = "multi_thread")]
10719 async fn test_diameter_error_cases() {
10720 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10721 let mock_ctx = ExecutorContext::new_mock(None).await;
10722 let version = Version(0);
10723
10724 let initial_source_point = "\
10726sketch(on = XY) {
10727 point(at = [var 1, var 2])
10728}
10729";
10730 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10731 let mut frontend_point = FrontendState::new();
10732 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
10733 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
10734 let sketch_id_point = sketch_object_point.id;
10735 let sketch_point = expect_sketch(sketch_object_point);
10736 let point_id = *sketch_point.segments.first().unwrap();
10737
10738 let constraint_point = Constraint::Diameter(Diameter {
10739 arc: point_id,
10740 diameter: Number {
10741 value: 10.0,
10742 units: NumericSuffix::Mm,
10743 },
10744 label_position: None,
10745 source: Default::default(),
10746 });
10747 let result_point = frontend_point
10748 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
10749 .await;
10750 assert!(result_point.is_err(), "Single point should error for diameter");
10751
10752 let initial_source_line = "\
10754sketch(on = XY) {
10755 line(start = [var 1, var 2], end = [var 3, var 4])
10756}
10757";
10758 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
10759 let mut frontend_line = FrontendState::new();
10760 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10761 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10762 let sketch_id_line = sketch_object_line.id;
10763 let sketch_line = expect_sketch(sketch_object_line);
10764 let line_id = *sketch_line.segments.first().unwrap();
10765
10766 let constraint_line = Constraint::Diameter(Diameter {
10767 arc: line_id,
10768 diameter: Number {
10769 value: 10.0,
10770 units: NumericSuffix::Mm,
10771 },
10772 label_position: None,
10773 source: Default::default(),
10774 });
10775 let result_line = frontend_line
10776 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10777 .await;
10778 assert!(result_line.is_err(), "Single line segment should error for diameter");
10779
10780 ctx.close().await;
10781 mock_ctx.close().await;
10782 }
10783
10784 #[tokio::test(flavor = "multi_thread")]
10785 async fn test_line_horizontal() {
10786 let initial_source = "\
10787sketch(on = XY) {
10788 line(start = [var 1, var 2], end = [var 3, var 4])
10789}
10790";
10791
10792 let program = Program::parse(initial_source).unwrap().0.unwrap();
10793
10794 let mut frontend = FrontendState::new();
10795
10796 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10797 let mock_ctx = ExecutorContext::new_mock(None).await;
10798 let version = Version(0);
10799
10800 frontend.hack_set_program(&ctx, program).await.unwrap();
10801 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10802 let sketch_id = sketch_object.id;
10803 let sketch = expect_sketch(sketch_object);
10804 let line1_id = *sketch.segments.get(2).unwrap();
10805
10806 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
10807 let (src_delta, scene_delta) = frontend
10808 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10809 .await
10810 .unwrap();
10811 assert_eq!(
10812 src_delta.text.as_str(),
10813 "\
10814sketch(on = XY) {
10815 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10816 horizontal(line1)
10817}
10818"
10819 );
10820 assert_eq!(
10821 scene_delta.new_graph.objects.len(),
10822 6,
10823 "{:#?}",
10824 scene_delta.new_graph.objects
10825 );
10826
10827 ctx.close().await;
10828 mock_ctx.close().await;
10829 }
10830
10831 #[tokio::test(flavor = "multi_thread")]
10832 async fn test_line_vertical() {
10833 let initial_source = "\
10834sketch(on = XY) {
10835 line(start = [var 1, var 2], end = [var 3, var 4])
10836}
10837";
10838
10839 let program = Program::parse(initial_source).unwrap().0.unwrap();
10840
10841 let mut frontend = FrontendState::new();
10842
10843 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10844 let mock_ctx = ExecutorContext::new_mock(None).await;
10845 let version = Version(0);
10846
10847 frontend.hack_set_program(&ctx, program).await.unwrap();
10848 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10849 let sketch_id = sketch_object.id;
10850 let sketch = expect_sketch(sketch_object);
10851 let line1_id = *sketch.segments.get(2).unwrap();
10852
10853 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
10854 let (src_delta, scene_delta) = frontend
10855 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10856 .await
10857 .unwrap();
10858 assert_eq!(
10859 src_delta.text.as_str(),
10860 "\
10861sketch(on = XY) {
10862 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10863 vertical(line1)
10864}
10865"
10866 );
10867 assert_eq!(
10868 scene_delta.new_graph.objects.len(),
10869 6,
10870 "{:#?}",
10871 scene_delta.new_graph.objects
10872 );
10873
10874 ctx.close().await;
10875 mock_ctx.close().await;
10876 }
10877
10878 #[tokio::test(flavor = "multi_thread")]
10879 async fn test_points_vertical() {
10880 let initial_source = "\
10881sketch001 = sketch(on = XY) {
10882 p0 = point(at = [var -2.23mm, var 3.1mm])
10883 pf = point(at = [4, 4])
10884}
10885";
10886
10887 let program = Program::parse(initial_source).unwrap().0.unwrap();
10888
10889 let mut frontend = FrontendState::new();
10890
10891 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10892 let mock_ctx = ExecutorContext::new_mock(None).await;
10893 let version = Version(0);
10894
10895 frontend.hack_set_program(&ctx, program).await.unwrap();
10896 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10897 let sketch_id = sketch_object.id;
10898 let sketch = expect_sketch(sketch_object);
10899 let point_ids = vec![
10900 sketch.segments.first().unwrap().to_owned(),
10901 sketch.segments.get(1).unwrap().to_owned(),
10902 ];
10903
10904 let constraint = Constraint::Vertical(Vertical::Points {
10905 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10906 });
10907 let (src_delta, scene_delta) = frontend
10908 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10909 .await
10910 .unwrap();
10911 assert_eq!(
10912 src_delta.text.as_str(),
10913 "\
10914sketch001 = sketch(on = XY) {
10915 p0 = point(at = [var -2.23mm, var 3.1mm])
10916 pf = point(at = [4, 4])
10917 vertical([p0, pf])
10918}
10919"
10920 );
10921 assert_eq!(
10922 scene_delta.new_graph.objects.len(),
10923 5,
10924 "{:#?}",
10925 scene_delta.new_graph.objects
10926 );
10927
10928 ctx.close().await;
10929 mock_ctx.close().await;
10930 }
10931
10932 #[tokio::test(flavor = "multi_thread")]
10933 async fn test_points_horizontal() {
10934 let initial_source = "\
10935sketch001 = sketch(on = XY) {
10936 p0 = point(at = [var -2.23mm, var 3.1mm])
10937 pf = point(at = [4, 4])
10938}
10939";
10940
10941 let program = Program::parse(initial_source).unwrap().0.unwrap();
10942
10943 let mut frontend = FrontendState::new();
10944
10945 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10946 let mock_ctx = ExecutorContext::new_mock(None).await;
10947 let version = Version(0);
10948
10949 frontend.hack_set_program(&ctx, program).await.unwrap();
10950 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10951 let sketch_id = sketch_object.id;
10952 let sketch = expect_sketch(sketch_object);
10953 let point_ids = vec![
10954 sketch.segments.first().unwrap().to_owned(),
10955 sketch.segments.get(1).unwrap().to_owned(),
10956 ];
10957
10958 let constraint = Constraint::Horizontal(Horizontal::Points {
10959 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10960 });
10961 let (src_delta, scene_delta) = frontend
10962 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10963 .await
10964 .unwrap();
10965 assert_eq!(
10966 src_delta.text.as_str(),
10967 "\
10968sketch001 = sketch(on = XY) {
10969 p0 = point(at = [var -2.23mm, var 3.1mm])
10970 pf = point(at = [4, 4])
10971 horizontal([p0, pf])
10972}
10973"
10974 );
10975 assert_eq!(
10976 scene_delta.new_graph.objects.len(),
10977 5,
10978 "{:#?}",
10979 scene_delta.new_graph.objects
10980 );
10981
10982 ctx.close().await;
10983 mock_ctx.close().await;
10984 }
10985
10986 #[tokio::test(flavor = "multi_thread")]
10987 async fn test_point_horizontal_with_origin() {
10988 let initial_source = "\
10989sketch001 = sketch(on = XY) {
10990 p0 = point(at = [var -2.23mm, var 3.1mm])
10991}
10992";
10993
10994 let program = Program::parse(initial_source).unwrap().0.unwrap();
10995
10996 let mut frontend = FrontendState::new();
10997
10998 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10999 let mock_ctx = ExecutorContext::new_mock(None).await;
11000 let version = Version(0);
11001
11002 frontend.hack_set_program(&ctx, program).await.unwrap();
11003 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11004 let sketch_id = sketch_object.id;
11005 let sketch = expect_sketch(sketch_object);
11006 let point_id = *sketch.segments.first().unwrap();
11007
11008 let constraint = Constraint::Horizontal(Horizontal::Points {
11009 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
11010 });
11011 let (src_delta, scene_delta) = frontend
11012 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11013 .await
11014 .unwrap();
11015 assert_eq!(
11016 src_delta.text.as_str(),
11017 "\
11018sketch001 = sketch(on = XY) {
11019 p0 = point(at = [var -2.23mm, var 3.1mm])
11020 horizontal([p0, ORIGIN])
11021}
11022"
11023 );
11024 assert_eq!(
11025 scene_delta.new_graph.objects.len(),
11026 4,
11027 "{:#?}",
11028 scene_delta.new_graph.objects
11029 );
11030
11031 ctx.close().await;
11032 mock_ctx.close().await;
11033 }
11034
11035 #[tokio::test(flavor = "multi_thread")]
11036 async fn test_lines_equal_length() {
11037 let initial_source = "\
11038sketch(on = XY) {
11039 line(start = [var 1, var 2], end = [var 3, var 4])
11040 line(start = [var 5, var 6], end = [var 7, var 8])
11041}
11042";
11043
11044 let program = Program::parse(initial_source).unwrap().0.unwrap();
11045
11046 let mut frontend = FrontendState::new();
11047
11048 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11049 let mock_ctx = ExecutorContext::new_mock(None).await;
11050 let version = Version(0);
11051
11052 frontend.hack_set_program(&ctx, program).await.unwrap();
11053 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11054 let sketch_id = sketch_object.id;
11055 let sketch = expect_sketch(sketch_object);
11056 let line1_id = *sketch.segments.get(2).unwrap();
11057 let line2_id = *sketch.segments.get(5).unwrap();
11058
11059 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
11060 lines: vec![line1_id, line2_id],
11061 });
11062 let (src_delta, scene_delta) = frontend
11063 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11064 .await
11065 .unwrap();
11066 assert_eq!(
11067 src_delta.text.as_str(),
11068 "\
11069sketch(on = XY) {
11070 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11071 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11072 equalLength([line1, line2])
11073}
11074"
11075 );
11076 assert_eq!(
11077 scene_delta.new_graph.objects.len(),
11078 9,
11079 "{:#?}",
11080 scene_delta.new_graph.objects
11081 );
11082
11083 ctx.close().await;
11084 mock_ctx.close().await;
11085 }
11086
11087 #[tokio::test(flavor = "multi_thread")]
11088 async fn test_add_constraint_multi_line_equal_length() {
11089 let initial_source = "\
11090sketch(on = XY) {
11091 line(start = [var 1, var 2], end = [var 3, var 4])
11092 line(start = [var 5, var 6], end = [var 7, var 8])
11093 line(start = [var 9, var 10], end = [var 11, var 12])
11094}
11095";
11096
11097 let program = Program::parse(initial_source).unwrap().0.unwrap();
11098
11099 let mut frontend = FrontendState::new();
11100 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11101 let mock_ctx = ExecutorContext::new_mock(None).await;
11102 let version = Version(0);
11103
11104 frontend.hack_set_program(&ctx, program).await.unwrap();
11105 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11106 let sketch_id = sketch_object.id;
11107 let sketch = expect_sketch(sketch_object);
11108 let line1_id = *sketch.segments.get(2).unwrap();
11109 let line2_id = *sketch.segments.get(5).unwrap();
11110 let line3_id = *sketch.segments.get(8).unwrap();
11111
11112 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
11113 lines: vec![line1_id, line2_id, line3_id],
11114 });
11115 let (src_delta, scene_delta) = frontend
11116 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11117 .await
11118 .unwrap();
11119 assert_eq!(
11120 src_delta.text.as_str(),
11121 "\
11122sketch(on = XY) {
11123 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11124 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11125 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
11126 equalLength([line1, line2, line3])
11127}
11128"
11129 );
11130 let constraints = scene_delta
11131 .new_graph
11132 .objects
11133 .iter()
11134 .filter_map(|obj| {
11135 let ObjectKind::Constraint { constraint } = &obj.kind else {
11136 return None;
11137 };
11138 Some(constraint)
11139 })
11140 .collect::<Vec<_>>();
11141
11142 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
11143 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
11144 panic!("expected equal length constraint, got {:?}", constraints[0]);
11145 };
11146 assert_eq!(lines_equal_length.lines.len(), 3);
11147
11148 ctx.close().await;
11149 mock_ctx.close().await;
11150 }
11151
11152 #[tokio::test(flavor = "multi_thread")]
11153 async fn test_lines_parallel() {
11154 let initial_source = "\
11155sketch(on = XY) {
11156 line(start = [var 1, var 2], end = [var 3, var 4])
11157 line(start = [var 5, var 6], end = [var 7, var 8])
11158}
11159";
11160
11161 let program = Program::parse(initial_source).unwrap().0.unwrap();
11162
11163 let mut frontend = FrontendState::new();
11164
11165 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11166 let mock_ctx = ExecutorContext::new_mock(None).await;
11167 let version = Version(0);
11168
11169 frontend.hack_set_program(&ctx, program).await.unwrap();
11170 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11171 let sketch_id = sketch_object.id;
11172 let sketch = expect_sketch(sketch_object);
11173 let line1_id = *sketch.segments.get(2).unwrap();
11174 let line2_id = *sketch.segments.get(5).unwrap();
11175
11176 let constraint = Constraint::Parallel(Parallel {
11177 lines: vec![line1_id, line2_id],
11178 });
11179 let (src_delta, scene_delta) = frontend
11180 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11181 .await
11182 .unwrap();
11183 assert_eq!(
11184 src_delta.text.as_str(),
11185 "\
11186sketch(on = XY) {
11187 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11188 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11189 parallel([line1, line2])
11190}
11191"
11192 );
11193 assert_eq!(
11194 scene_delta.new_graph.objects.len(),
11195 9,
11196 "{:#?}",
11197 scene_delta.new_graph.objects
11198 );
11199
11200 ctx.close().await;
11201 mock_ctx.close().await;
11202 }
11203
11204 #[tokio::test(flavor = "multi_thread")]
11205 async fn test_lines_parallel_multiline() {
11206 let initial_source = "\
11207sketch(on = XY) {
11208 line(start = [var 1, var 2], end = [var 3, var 4])
11209 line(start = [var 5, var 6], end = [var 7, var 8])
11210 line(start = [var 9, var 10], end = [var 11, var 12])
11211}
11212";
11213
11214 let program = Program::parse(initial_source).unwrap().0.unwrap();
11215
11216 let mut frontend = FrontendState::new();
11217
11218 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11219 let mock_ctx = ExecutorContext::new_mock(None).await;
11220 let version = Version(0);
11221
11222 frontend.hack_set_program(&ctx, program).await.unwrap();
11223 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11224 let sketch_id = sketch_object.id;
11225 let sketch = expect_sketch(sketch_object);
11226 let line1_id = *sketch.segments.get(2).unwrap();
11227 let line2_id = *sketch.segments.get(5).unwrap();
11228 let line3_id = *sketch.segments.get(8).unwrap();
11229
11230 let constraint = Constraint::Parallel(Parallel {
11231 lines: vec![line1_id, line2_id, line3_id],
11232 });
11233 let (src_delta, scene_delta) = frontend
11234 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11235 .await
11236 .unwrap();
11237 assert_eq!(
11238 src_delta.text.as_str(),
11239 "\
11240sketch(on = XY) {
11241 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11242 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11243 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
11244 parallel([line1, line2, line3])
11245}
11246"
11247 );
11248
11249 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11250 let sketch = expect_sketch(sketch_object);
11251 assert_eq!(sketch.constraints.len(), 1);
11252
11253 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11254 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11255 panic!("Expected constraint object");
11256 };
11257 let Constraint::Parallel(parallel) = constraint else {
11258 panic!("Expected parallel constraint");
11259 };
11260 assert_eq!(parallel.lines.len(), 3);
11261
11262 ctx.close().await;
11263 mock_ctx.close().await;
11264 }
11265
11266 #[tokio::test(flavor = "multi_thread")]
11267 async fn test_lines_perpendicular() {
11268 let initial_source = "\
11269sketch(on = XY) {
11270 line(start = [var 1, var 2], end = [var 3, var 4])
11271 line(start = [var 5, var 6], end = [var 7, var 8])
11272}
11273";
11274
11275 let program = Program::parse(initial_source).unwrap().0.unwrap();
11276
11277 let mut frontend = FrontendState::new();
11278
11279 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11280 let mock_ctx = ExecutorContext::new_mock(None).await;
11281 let version = Version(0);
11282
11283 frontend.hack_set_program(&ctx, program).await.unwrap();
11284 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11285 let sketch_id = sketch_object.id;
11286 let sketch = expect_sketch(sketch_object);
11287 let line1_id = *sketch.segments.get(2).unwrap();
11288 let line2_id = *sketch.segments.get(5).unwrap();
11289
11290 let constraint = Constraint::Perpendicular(Perpendicular {
11291 lines: vec![line1_id, line2_id],
11292 });
11293 let (src_delta, scene_delta) = frontend
11294 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11295 .await
11296 .unwrap();
11297 assert_eq!(
11298 src_delta.text.as_str(),
11299 "\
11300sketch(on = XY) {
11301 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11302 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11303 perpendicular([line1, line2])
11304}
11305"
11306 );
11307 assert_eq!(
11308 scene_delta.new_graph.objects.len(),
11309 9,
11310 "{:#?}",
11311 scene_delta.new_graph.objects
11312 );
11313
11314 ctx.close().await;
11315 mock_ctx.close().await;
11316 }
11317
11318 #[tokio::test(flavor = "multi_thread")]
11319 async fn test_lines_angle() {
11320 let initial_source = "\
11321sketch(on = XY) {
11322 line(start = [var 1, var 2], end = [var 3, var 4])
11323 line(start = [var 5, var 6], end = [var 7, var 8])
11324}
11325";
11326
11327 let program = Program::parse(initial_source).unwrap().0.unwrap();
11328
11329 let mut frontend = FrontendState::new();
11330
11331 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11332 let mock_ctx = ExecutorContext::new_mock(None).await;
11333 let version = Version(0);
11334
11335 frontend.hack_set_program(&ctx, program).await.unwrap();
11336 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11337 let sketch_id = sketch_object.id;
11338 let sketch = expect_sketch(sketch_object);
11339 let line1_id = *sketch.segments.get(2).unwrap();
11340 let line2_id = *sketch.segments.get(5).unwrap();
11341
11342 let constraint = Constraint::Angle(Angle {
11343 lines: vec![line1_id, line2_id],
11344 angle: Number {
11345 value: 30.0,
11346 units: NumericSuffix::Deg,
11347 },
11348 source: Default::default(),
11349 });
11350 let (src_delta, scene_delta) = frontend
11351 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11352 .await
11353 .unwrap();
11354 assert_eq!(
11355 src_delta.text.as_str(),
11356 "\
11358sketch(on = XY) {
11359 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11360 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11361 angle([line1, line2]) == 30deg
11362}
11363"
11364 );
11365 assert_eq!(
11366 scene_delta.new_graph.objects.len(),
11367 9,
11368 "{:#?}",
11369 scene_delta.new_graph.objects
11370 );
11371
11372 ctx.close().await;
11373 mock_ctx.close().await;
11374 }
11375
11376 #[tokio::test(flavor = "multi_thread")]
11377 async fn test_segments_tangent() {
11378 let initial_source = "\
11379sketch(on = XY) {
11380 line(start = [var 1, var 2], end = [var 3, var 4])
11381 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11382}
11383";
11384
11385 let program = Program::parse(initial_source).unwrap().0.unwrap();
11386
11387 let mut frontend = FrontendState::new();
11388
11389 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11390 let mock_ctx = ExecutorContext::new_mock(None).await;
11391 let version = Version(0);
11392
11393 frontend.hack_set_program(&ctx, program).await.unwrap();
11394 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11395 let sketch_id = sketch_object.id;
11396 let sketch = expect_sketch(sketch_object);
11397 let line1_id = *sketch.segments.get(2).unwrap();
11398 let arc1_id = *sketch.segments.get(6).unwrap();
11399
11400 let constraint = Constraint::Tangent(Tangent {
11401 input: vec![line1_id, arc1_id],
11402 });
11403 let (src_delta, scene_delta) = frontend
11404 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11405 .await
11406 .unwrap();
11407 assert_eq!(
11408 src_delta.text.as_str(),
11409 "\
11410sketch(on = XY) {
11411 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11412 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11413 tangent([line1, arc1])
11414}
11415"
11416 );
11417 assert_eq!(
11418 scene_delta.new_graph.objects.len(),
11419 10,
11420 "{:#?}",
11421 scene_delta.new_graph.objects
11422 );
11423
11424 ctx.close().await;
11425 mock_ctx.close().await;
11426 }
11427
11428 #[tokio::test(flavor = "multi_thread")]
11429 async fn test_point_midpoint() {
11430 let initial_source = "\
11431sketch(on = XY) {
11432 point(at = [var 1, var 1])
11433 line(start = [var 0, var 0], end = [var 6, var 4])
11434}
11435";
11436
11437 let program = Program::parse(initial_source).unwrap().0.unwrap();
11438
11439 let mut frontend = FrontendState::new();
11440
11441 let ctx = ExecutorContext::new_mock(None).await;
11442 let version = Version(0);
11443
11444 frontend.program = program.clone();
11445 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11446 frontend.update_state_after_exec(outcome, true);
11447 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11448 let sketch_id = sketch_object.id;
11449 let sketch = expect_sketch(sketch_object);
11450 let point_id = *sketch.segments.first().unwrap();
11451 let line_id = *sketch.segments.get(3).unwrap();
11452
11453 let constraint = Constraint::Midpoint(Midpoint {
11454 point: point_id,
11455 segment: line_id,
11456 });
11457 let (src_delta, scene_delta) = frontend
11458 .add_constraint(&ctx, version, sketch_id, constraint)
11459 .await
11460 .unwrap();
11461 assert_eq!(
11462 src_delta.text.as_str(),
11463 "\
11464sketch(on = XY) {
11465 point1 = point(at = [var 1, var 1])
11466 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
11467 midpoint(line1, point = point1)
11468}
11469"
11470 );
11471 assert_eq!(
11472 scene_delta.new_graph.objects.len(),
11473 7,
11474 "{:#?}",
11475 scene_delta.new_graph.objects
11476 );
11477
11478 ctx.close().await;
11479 }
11480
11481 #[tokio::test(flavor = "multi_thread")]
11482 async fn test_segments_symmetric() {
11483 let initial_source = "\
11484sketch(on = XY) {
11485 line(start = [var 0, var 0], end = [var 0, var 4])
11486 line(start = [var 4, var 0], end = [var 4, var 4])
11487 line(start = [var 2, var -1], end = [var 2, var 5])
11488}
11489";
11490
11491 let program = Program::parse(initial_source).unwrap().0.unwrap();
11492
11493 let mut frontend = FrontendState::new();
11494
11495 let ctx = ExecutorContext::new_mock(None).await;
11496 let version = Version(0);
11497
11498 frontend.program = program.clone();
11499 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11500 frontend.update_state_after_exec(outcome, true);
11501 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11502 let sketch_id = sketch_object.id;
11503 let sketch = expect_sketch(sketch_object);
11504 let line1_id = *sketch.segments.get(2).unwrap();
11505 let line2_id = *sketch.segments.get(5).unwrap();
11506 let axis_id = *sketch.segments.get(8).unwrap();
11507
11508 let constraint = Constraint::Symmetric(Symmetric {
11509 input: vec![line1_id, line2_id],
11510 axis: axis_id,
11511 });
11512 let (src_delta, scene_delta) = frontend
11513 .add_constraint(&ctx, version, sketch_id, constraint)
11514 .await
11515 .unwrap();
11516 assert_eq!(
11517 src_delta.text.as_str(),
11518 "\
11519sketch(on = XY) {
11520 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
11521 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
11522 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
11523 symmetric([line1, line2], axis = line3)
11524}
11525"
11526 );
11527 assert_eq!(
11528 scene_delta.new_graph.objects.len(),
11529 12,
11530 "{:#?}",
11531 scene_delta.new_graph.objects
11532 );
11533
11534 ctx.close().await;
11535 }
11536
11537 #[tokio::test(flavor = "multi_thread")]
11538 async fn test_point_arc_midpoint() {
11539 let initial_source = "\
11540sketch(on = XY) {
11541 point(at = [var 6, var 3])
11542 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11543}
11544";
11545
11546 let program = Program::parse(initial_source).unwrap().0.unwrap();
11547
11548 let mut frontend = FrontendState::new();
11549
11550 let ctx = ExecutorContext::new_mock(None).await;
11551 let version = Version(0);
11552
11553 frontend.program = program.clone();
11554 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11555 frontend.update_state_after_exec(outcome, true);
11556 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11557 let sketch_id = sketch_object.id;
11558 let sketch = expect_sketch(sketch_object);
11559 let point_id = *sketch.segments.first().unwrap();
11560 let arc_id = *sketch.segments.get(4).unwrap();
11561
11562 let constraint = Constraint::Midpoint(Midpoint {
11563 point: point_id,
11564 segment: arc_id,
11565 });
11566 let (src_delta, scene_delta) = frontend
11567 .add_constraint(&ctx, version, sketch_id, constraint)
11568 .await
11569 .unwrap();
11570 assert_eq!(
11571 src_delta.text.as_str(),
11572 "\
11573sketch(on = XY) {
11574 point1 = point(at = [var 6, var 3])
11575 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11576 midpoint(arc1, point = point1)
11577}
11578"
11579 );
11580 assert_eq!(
11581 scene_delta.new_graph.objects.len(),
11582 8,
11583 "{:#?}",
11584 scene_delta.new_graph.objects
11585 );
11586
11587 ctx.close().await;
11588 }
11589
11590 #[tokio::test(flavor = "multi_thread")]
11591 async fn test_segments_symmetric_arcs() {
11592 let initial_source = "\
11593sketch(on = XY) {
11594 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11595 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11596 line(start = [var 0, var -10], end = [var 0, var 10])
11597}
11598";
11599
11600 let program = Program::parse(initial_source).unwrap().0.unwrap();
11601
11602 let mut frontend = FrontendState::new();
11603
11604 let ctx = ExecutorContext::new_mock(None).await;
11605 let version = Version(0);
11606
11607 frontend.program = program.clone();
11608 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11609 frontend.update_state_after_exec(outcome, true);
11610 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11611 let sketch_id = sketch_object.id;
11612 let sketch = expect_sketch(sketch_object);
11613 let arc1_id = *sketch.segments.get(3).unwrap();
11614 let arc2_id = *sketch.segments.get(7).unwrap();
11615 let axis_id = *sketch.segments.get(10).unwrap();
11616
11617 let constraint = Constraint::Symmetric(Symmetric {
11618 input: vec![arc1_id, arc2_id],
11619 axis: axis_id,
11620 });
11621 let (src_delta, scene_delta) = frontend
11622 .add_constraint(&ctx, version, sketch_id, constraint)
11623 .await
11624 .unwrap();
11625 assert_eq!(
11626 src_delta.text.as_str(),
11627 "\
11628sketch(on = XY) {
11629 arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11630 arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11631 line1 = line(start = [var 0, var -10], end = [var 0, var 10])
11632 symmetric([arc1, arc2], axis = line1)
11633}
11634"
11635 );
11636 assert_eq!(
11637 scene_delta.new_graph.objects.len(),
11638 14,
11639 "{:#?}",
11640 scene_delta.new_graph.objects
11641 );
11642
11643 ctx.close().await;
11644 }
11645
11646 #[tokio::test(flavor = "multi_thread")]
11647 async fn test_sketch_on_face_simple() {
11648 let initial_source = "\
11649len = 2mm
11650cube = startSketchOn(XY)
11651 |> startProfile(at = [0, 0])
11652 |> line(end = [len, 0], tag = $side)
11653 |> line(end = [0, len])
11654 |> line(end = [-len, 0])
11655 |> line(end = [0, -len])
11656 |> close()
11657 |> extrude(length = len)
11658
11659face = faceOf(cube, face = side)
11660";
11661
11662 let program = Program::parse(initial_source).unwrap().0.unwrap();
11663
11664 let mut frontend = FrontendState::new();
11665
11666 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11667 let mock_ctx = ExecutorContext::new_mock(None).await;
11668 let version = Version(0);
11669
11670 frontend.hack_set_program(&ctx, program).await.unwrap();
11671 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
11672 let face_id = face_object.id;
11673
11674 let sketch_args = SketchCtor {
11675 on: Plane::Object(face_id),
11676 };
11677 let (_src_delta, scene_delta, sketch_id) = frontend
11678 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11679 .await
11680 .unwrap();
11681 assert_eq!(sketch_id, ObjectId(2));
11682 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11683 let sketch_object = &scene_delta.new_graph.objects[2];
11684 assert_eq!(sketch_object.id, ObjectId(2));
11685 assert_eq!(
11686 sketch_object.kind,
11687 ObjectKind::Sketch(Sketch {
11688 args: SketchCtor {
11689 on: Plane::Object(face_id),
11690 },
11691 plane: face_id,
11692 segments: vec![],
11693 constraints: vec![],
11694 })
11695 );
11696 assert_eq!(scene_delta.new_graph.objects.len(), 8);
11697
11698 ctx.close().await;
11699 mock_ctx.close().await;
11700 }
11701
11702 #[tokio::test(flavor = "multi_thread")]
11703 async fn test_sketch_on_wall_artifact_from_region_extrude() {
11704 let initial_source = "\
11705s = sketch(on = YZ) {
11706 line1 = line(start = [0, 0], end = [0, 1])
11707 line2 = line(start = [0, 1], end = [1, 1])
11708 line3 = line(start = [1, 1], end = [0, 0])
11709}
11710region001 = region(point = [0.1, 0.1], sketch = s)
11711extrude001 = extrude(region001, length = 5)
11712";
11713
11714 let program = Program::parse(initial_source).unwrap().0.unwrap();
11715
11716 let mut frontend = FrontendState::new();
11717 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11718 let version = Version(0);
11719
11720 frontend.hack_set_program(&ctx, program).await.unwrap();
11721 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11722
11723 let sketch_args = SketchCtor {
11724 on: Plane::Object(wall_object_id),
11725 };
11726 let (src_delta, _scene_delta, _sketch_id) = frontend
11727 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11728 .await
11729 .unwrap();
11730 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11731
11732 ctx.close().await;
11733 }
11734
11735 #[tokio::test(flavor = "multi_thread")]
11736 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
11737 let initial_source = "\
11738sketch001 = sketch(on = YZ) {
11739 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
11740 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
11741 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
11742 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
11743 coincident([line1.end, line2.start])
11744 coincident([line2.end, line3.start])
11745 coincident([line3.end, line4.start])
11746 coincident([line4.end, line1.start])
11747 parallel([line2, line4])
11748 parallel([line3, line1])
11749 perpendicular([line1, line2])
11750 horizontal(line3)
11751 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
11752}
11753region001 = region(point = [3.1, 3.74], sketch = sketch001)
11754extrude001 = extrude(region001, length = 5)
11755";
11756
11757 let program = Program::parse(initial_source).unwrap().0.unwrap();
11758
11759 let mut frontend = FrontendState::new();
11760 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11761 let version = Version(0);
11762
11763 frontend.hack_set_program(&ctx, program).await.unwrap();
11764 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11765
11766 let sketch_args = SketchCtor {
11767 on: Plane::Object(wall_object_id),
11768 };
11769 let (src_delta, _scene_delta, _sketch_id) = frontend
11770 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11771 .await
11772 .unwrap();
11773 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11774
11775 ctx.close().await;
11776 }
11777
11778 #[tokio::test(flavor = "multi_thread")]
11779 async fn test_sketch_on_plane_incremental() {
11780 let initial_source = "\
11781len = 2mm
11782cube = startSketchOn(XY)
11783 |> startProfile(at = [0, 0])
11784 |> line(end = [len, 0], tag = $side)
11785 |> line(end = [0, len])
11786 |> line(end = [-len, 0])
11787 |> line(end = [0, -len])
11788 |> close()
11789 |> extrude(length = len)
11790
11791plane = planeOf(cube, face = side)
11792";
11793
11794 let program = Program::parse(initial_source).unwrap().0.unwrap();
11795
11796 let mut frontend = FrontendState::new();
11797
11798 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11799 let mock_ctx = ExecutorContext::new_mock(None).await;
11800 let version = Version(0);
11801
11802 frontend.hack_set_program(&ctx, program).await.unwrap();
11803 let plane_object = frontend
11805 .scene_graph
11806 .objects
11807 .iter()
11808 .rev()
11809 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
11810 .unwrap();
11811 let plane_id = plane_object.id;
11812
11813 let sketch_args = SketchCtor {
11814 on: Plane::Object(plane_id),
11815 };
11816 let (src_delta, scene_delta, sketch_id) = frontend
11817 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11818 .await
11819 .unwrap();
11820 assert_eq!(
11821 src_delta.text.as_str(),
11822 "\
11823len = 2mm
11824cube = startSketchOn(XY)
11825 |> startProfile(at = [0, 0])
11826 |> line(end = [len, 0], tag = $side)
11827 |> line(end = [0, len])
11828 |> line(end = [-len, 0])
11829 |> line(end = [0, -len])
11830 |> close()
11831 |> extrude(length = len)
11832
11833plane = planeOf(cube, face = side)
11834sketch001 = sketch(on = plane) {
11835}
11836"
11837 );
11838 assert_eq!(sketch_id, ObjectId(2));
11839 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11840 let sketch_object = &scene_delta.new_graph.objects[2];
11841 assert_eq!(sketch_object.id, ObjectId(2));
11842 assert_eq!(
11843 sketch_object.kind,
11844 ObjectKind::Sketch(Sketch {
11845 args: SketchCtor {
11846 on: Plane::Object(plane_id),
11847 },
11848 plane: plane_id,
11849 segments: vec![],
11850 constraints: vec![],
11851 })
11852 );
11853 assert_eq!(scene_delta.new_graph.objects.len(), 9);
11854
11855 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
11856 assert_eq!(plane_object.id, plane_id);
11857 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
11858
11859 ctx.close().await;
11860 mock_ctx.close().await;
11861 }
11862
11863 #[tokio::test(flavor = "multi_thread")]
11864 async fn test_new_sketch_uses_unique_variable_name() {
11865 let initial_source = "\
11866sketch1 = sketch(on = XY) {
11867}
11868";
11869
11870 let program = Program::parse(initial_source).unwrap().0.unwrap();
11871
11872 let mut frontend = FrontendState::new();
11873 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11874 let version = Version(0);
11875
11876 frontend.hack_set_program(&ctx, program).await.unwrap();
11877
11878 let sketch_args = SketchCtor {
11879 on: Plane::Default(PlaneName::Yz),
11880 };
11881 let (src_delta, _, _) = frontend
11882 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11883 .await
11884 .unwrap();
11885
11886 assert_eq!(
11887 src_delta.text.as_str(),
11888 "\
11889sketch1 = sketch(on = XY) {
11890}
11891sketch001 = sketch(on = YZ) {
11892}
11893"
11894 );
11895
11896 ctx.close().await;
11897 }
11898
11899 #[tokio::test(flavor = "multi_thread")]
11900 async fn test_new_sketch_twice_using_same_plane() {
11901 let initial_source = "\
11902sketch1 = sketch(on = XY) {
11903}
11904";
11905
11906 let program = Program::parse(initial_source).unwrap().0.unwrap();
11907
11908 let mut frontend = FrontendState::new();
11909 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11910 let version = Version(0);
11911
11912 frontend.hack_set_program(&ctx, program).await.unwrap();
11913
11914 let sketch_args = SketchCtor {
11915 on: Plane::Default(PlaneName::Xy),
11916 };
11917 let (src_delta, _, _) = frontend
11918 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11919 .await
11920 .unwrap();
11921
11922 assert_eq!(
11923 src_delta.text.as_str(),
11924 "\
11925sketch1 = sketch(on = XY) {
11926}
11927sketch001 = sketch(on = XY) {
11928}
11929"
11930 );
11931
11932 ctx.close().await;
11933 }
11934
11935 #[tokio::test(flavor = "multi_thread")]
11936 async fn test_sketch_mode_reuses_cached_on_expression() {
11937 let initial_source = "\
11938width = 2mm
11939sketch(on = offsetPlane(XY, offset = width)) {
11940 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
11941 distance([line1.start, line1.end]) == width
11942}
11943";
11944 let program = Program::parse(initial_source).unwrap().0.unwrap();
11945
11946 let mut frontend = FrontendState::new();
11947 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11948 let mock_ctx = ExecutorContext::new_mock(None).await;
11949 let version = Version(0);
11950 let project_id = ProjectId(0);
11951 let file_id = FileId(0);
11952
11953 frontend.hack_set_program(&ctx, program).await.unwrap();
11954 let initial_object_count = frontend.scene_graph.objects.len();
11955 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
11956 .expect("Expected sketch object to exist")
11957 .id;
11958
11959 let scene_delta = frontend
11962 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11963 .await
11964 .unwrap();
11965 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11966
11967 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
11970 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11971
11972 ctx.close().await;
11973 mock_ctx.close().await;
11974 }
11975
11976 #[tokio::test(flavor = "multi_thread")]
11977 async fn test_multiple_sketch_blocks() {
11978 let initial_source = "\
11979// Cube that requires the engine.
11980width = 2
11981sketch001 = startSketchOn(XY)
11982profile001 = startProfile(sketch001, at = [0, 0])
11983 |> yLine(length = width, tag = $seg1)
11984 |> xLine(length = width)
11985 |> yLine(length = -width)
11986 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11987 |> close()
11988extrude001 = extrude(profile001, length = width)
11989
11990// Get a value that requires the engine.
11991x = segLen(seg1)
11992
11993// Triangle with side length 2*x.
11994sketch(on = XY) {
11995 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11996 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11997 coincident([line1.end, line2.start])
11998 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11999 coincident([line2.end, line3.start])
12000 coincident([line3.end, line1.start])
12001 equalLength([line3, line1])
12002 equalLength([line1, line2])
12003 distance([line1.start, line1.end]) == 2*x
12004}
12005
12006// Line segment with length x.
12007sketch2 = sketch(on = XY) {
12008 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12009 distance([line1.start, line1.end]) == x
12010}
12011";
12012
12013 let program = Program::parse(initial_source).unwrap().0.unwrap();
12014
12015 let mut frontend = FrontendState::new();
12016
12017 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12018 let mock_ctx = ExecutorContext::new_mock(None).await;
12019 let version = Version(0);
12020 let project_id = ProjectId(0);
12021 let file_id = FileId(0);
12022
12023 frontend.hack_set_program(&ctx, program).await.unwrap();
12024 let sketch_objects = frontend
12025 .scene_graph
12026 .objects
12027 .iter()
12028 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
12029 .collect::<Vec<_>>();
12030 let sketch1_id = sketch_objects.first().unwrap().id;
12031 let sketch2_id = sketch_objects.get(1).unwrap().id;
12032 let point1_id = ObjectId(sketch1_id.0 + 1);
12034 let point2_id = ObjectId(sketch2_id.0 + 1);
12036
12037 let scene_delta = frontend
12046 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
12047 .await
12048 .unwrap();
12049 assert_eq!(
12050 scene_delta.new_graph.objects.len(),
12051 18,
12052 "{:#?}",
12053 scene_delta.new_graph.objects
12054 );
12055
12056 let point_ctor = PointCtor {
12058 position: Point2d {
12059 x: Expr::Var(Number {
12060 value: 1.0,
12061 units: NumericSuffix::Mm,
12062 }),
12063 y: Expr::Var(Number {
12064 value: 2.0,
12065 units: NumericSuffix::Mm,
12066 }),
12067 },
12068 };
12069 let segments = vec![ExistingSegmentCtor {
12070 id: point1_id,
12071 ctor: SegmentCtor::Point(point_ctor),
12072 }];
12073 let (src_delta, _) = frontend
12074 .edit_segments(&mock_ctx, version, sketch1_id, segments)
12075 .await
12076 .unwrap();
12077 assert_eq!(
12079 src_delta.text.as_str(),
12080 "\
12081// Cube that requires the engine.
12082width = 2
12083sketch001 = startSketchOn(XY)
12084profile001 = startProfile(sketch001, at = [0, 0])
12085 |> yLine(length = width, tag = $seg1)
12086 |> xLine(length = width)
12087 |> yLine(length = -width)
12088 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12089 |> close()
12090extrude001 = extrude(profile001, length = width)
12091
12092// Get a value that requires the engine.
12093x = segLen(seg1)
12094
12095// Triangle with side length 2*x.
12096sketch(on = XY) {
12097 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
12098 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
12099 coincident([line1.end, line2.start])
12100 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
12101 coincident([line2.end, line3.start])
12102 coincident([line3.end, line1.start])
12103 equalLength([line3, line1])
12104 equalLength([line1, line2])
12105 distance([line1.start, line1.end]) == 2 * x
12106}
12107
12108// Line segment with length x.
12109sketch2 = sketch(on = XY) {
12110 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12111 distance([line1.start, line1.end]) == x
12112}
12113"
12114 );
12115
12116 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
12118 assert_eq!(
12120 src_delta.text.as_str(),
12121 "\
12122// Cube that requires the engine.
12123width = 2
12124sketch001 = startSketchOn(XY)
12125profile001 = startProfile(sketch001, at = [0, 0])
12126 |> yLine(length = width, tag = $seg1)
12127 |> xLine(length = width)
12128 |> yLine(length = -width)
12129 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12130 |> close()
12131extrude001 = extrude(profile001, length = width)
12132
12133// Get a value that requires the engine.
12134x = segLen(seg1)
12135
12136// Triangle with side length 2*x.
12137sketch(on = XY) {
12138 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
12139 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12140 coincident([line1.end, line2.start])
12141 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12142 coincident([line2.end, line3.start])
12143 coincident([line3.end, line1.start])
12144 equalLength([line3, line1])
12145 equalLength([line1, line2])
12146 distance([line1.start, line1.end]) == 2 * x
12147}
12148
12149// Line segment with length x.
12150sketch2 = sketch(on = XY) {
12151 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12152 distance([line1.start, line1.end]) == x
12153}
12154"
12155 );
12156 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
12164 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
12165
12166 let scene_delta = frontend
12174 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
12175 .await
12176 .unwrap();
12177 assert_eq!(
12178 scene_delta.new_graph.objects.len(),
12179 24,
12180 "{:#?}",
12181 scene_delta.new_graph.objects
12182 );
12183
12184 let point_ctor = PointCtor {
12186 position: Point2d {
12187 x: Expr::Var(Number {
12188 value: 3.0,
12189 units: NumericSuffix::Mm,
12190 }),
12191 y: Expr::Var(Number {
12192 value: 4.0,
12193 units: NumericSuffix::Mm,
12194 }),
12195 },
12196 };
12197 let segments = vec![ExistingSegmentCtor {
12198 id: point2_id,
12199 ctor: SegmentCtor::Point(point_ctor),
12200 }];
12201 let (src_delta, _) = frontend
12202 .edit_segments(&mock_ctx, version, sketch2_id, segments)
12203 .await
12204 .unwrap();
12205 assert_eq!(
12207 src_delta.text.as_str(),
12208 "\
12209// Cube that requires the engine.
12210width = 2
12211sketch001 = startSketchOn(XY)
12212profile001 = startProfile(sketch001, at = [0, 0])
12213 |> yLine(length = width, tag = $seg1)
12214 |> xLine(length = width)
12215 |> yLine(length = -width)
12216 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12217 |> close()
12218extrude001 = extrude(profile001, length = width)
12219
12220// Get a value that requires the engine.
12221x = segLen(seg1)
12222
12223// Triangle with side length 2*x.
12224sketch(on = XY) {
12225 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
12226 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12227 coincident([line1.end, line2.start])
12228 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12229 coincident([line2.end, line3.start])
12230 coincident([line3.end, line1.start])
12231 equalLength([line3, line1])
12232 equalLength([line1, line2])
12233 distance([line1.start, line1.end]) == 2 * x
12234}
12235
12236// Line segment with length x.
12237sketch2 = sketch(on = XY) {
12238 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
12239 distance([line1.start, line1.end]) == x
12240}
12241"
12242 );
12243
12244 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
12246 assert_eq!(
12248 src_delta.text.as_str(),
12249 "\
12250// Cube that requires the engine.
12251width = 2
12252sketch001 = startSketchOn(XY)
12253profile001 = startProfile(sketch001, at = [0, 0])
12254 |> yLine(length = width, tag = $seg1)
12255 |> xLine(length = width)
12256 |> yLine(length = -width)
12257 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12258 |> close()
12259extrude001 = extrude(profile001, length = width)
12260
12261// Get a value that requires the engine.
12262x = segLen(seg1)
12263
12264// Triangle with side length 2*x.
12265sketch(on = XY) {
12266 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
12267 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12268 coincident([line1.end, line2.start])
12269 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12270 coincident([line2.end, line3.start])
12271 coincident([line3.end, line1.start])
12272 equalLength([line3, line1])
12273 equalLength([line1, line2])
12274 distance([line1.start, line1.end]) == 2 * x
12275}
12276
12277// Line segment with length x.
12278sketch2 = sketch(on = XY) {
12279 line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
12280 distance([line1.start, line1.end]) == x
12281}
12282"
12283 );
12284
12285 ctx.close().await;
12286 mock_ctx.close().await;
12287 }
12288
12289 #[tokio::test(flavor = "multi_thread")]
12290 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
12291 clear_mem_cache().await;
12292
12293 let source = r#"sketch001 = sketch(on = XZ) {
12294 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
12295}
12296sketch002 = sketch(on = XY) {
12297 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
12298 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
12299 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
12300 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
12301 coincident([line1.end, line2.start])
12302 coincident([line2.end, line3.start])
12303 coincident([line3.end, line4.start])
12304 coincident([line4.end, line1.start])
12305 parallel([line2, line4])
12306 parallel([line3, line1])
12307 perpendicular([line1, line2])
12308 horizontal(line3)
12309 coincident([line1.start, ORIGIN])
12310}
12311"#;
12312
12313 let program = Program::parse(source).unwrap().0.unwrap();
12314 let mut frontend = FrontendState::new();
12315 let ctx = ExecutorContext::new_with_engine(
12316 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
12317 Default::default(),
12318 );
12319 let mock_ctx = ExecutorContext::new_mock(None).await;
12320 let version = Version(0);
12321 let project_id = ProjectId(0);
12322 let file_id = FileId(0);
12323
12324 frontend.hack_set_program(&ctx, program).await.unwrap();
12325 let sketch_objects = frontend
12326 .scene_graph
12327 .objects
12328 .iter()
12329 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
12330 .collect::<Vec<_>>();
12331 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
12332
12333 let sketch1_id = sketch_objects[0].id;
12334 let sketch2_id = sketch_objects[1].id;
12335
12336 frontend
12337 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
12338 .await
12339 .unwrap();
12340 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
12341
12342 let scene_delta = frontend
12343 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
12344 .await
12345 .unwrap();
12346 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
12347
12348 clear_mem_cache().await;
12349 ctx.close().await;
12350 mock_ctx.close().await;
12351 }
12352
12353 #[tokio::test(flavor = "multi_thread")]
12358 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
12359 let initial_source = "@settings(defaultLengthUnit = mm)
12361
12362
12363
12364sketch001 = sketch(on = XY) {
12365 point(at = [1in, 2in])
12366}
12367";
12368
12369 let program = Program::parse(initial_source).unwrap().0.unwrap();
12370 let mut frontend = FrontendState::new();
12371
12372 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12373 let mock_ctx = ExecutorContext::new_mock(None).await;
12374 let version = Version(0);
12375 let project_id = ProjectId(0);
12376 let file_id = FileId(0);
12377
12378 frontend.hack_set_program(&ctx, program).await.unwrap();
12379 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12380 let sketch_id = sketch_object.id;
12381
12382 frontend
12384 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12385 .await
12386 .unwrap();
12387
12388 let point_ctor = PointCtor {
12390 position: Point2d {
12391 x: Expr::Number(Number {
12392 value: 5.0,
12393 units: NumericSuffix::Mm,
12394 }),
12395 y: Expr::Number(Number {
12396 value: 6.0,
12397 units: NumericSuffix::Mm,
12398 }),
12399 },
12400 };
12401 let segment = SegmentCtor::Point(point_ctor);
12402 let (src_delta, scene_delta) = frontend
12403 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12404 .await
12405 .unwrap();
12406 assert!(
12408 src_delta.text.contains("point(at = [5mm, 6mm])"),
12409 "Expected new point in source, got: {}",
12410 src_delta.text
12411 );
12412 assert!(!scene_delta.new_objects.is_empty());
12413
12414 ctx.close().await;
12415 mock_ctx.close().await;
12416 }
12417
12418 #[tokio::test(flavor = "multi_thread")]
12419 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
12420 let initial_source = "@settings(defaultLengthUnit = mm)
12422
12423
12424
12425s = sketch(on = XY) {}
12426";
12427
12428 let program = Program::parse(initial_source).unwrap().0.unwrap();
12429 let mut frontend = FrontendState::new();
12430
12431 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12432 let mock_ctx = ExecutorContext::new_mock(None).await;
12433 let version = Version(0);
12434
12435 frontend.hack_set_program(&ctx, program).await.unwrap();
12436 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12437 let sketch_id = sketch_object.id;
12438
12439 let line_ctor = LineCtor {
12440 start: Point2d {
12441 x: Expr::Number(Number {
12442 value: 0.0,
12443 units: NumericSuffix::Mm,
12444 }),
12445 y: Expr::Number(Number {
12446 value: 0.0,
12447 units: NumericSuffix::Mm,
12448 }),
12449 },
12450 end: Point2d {
12451 x: Expr::Number(Number {
12452 value: 10.0,
12453 units: NumericSuffix::Mm,
12454 }),
12455 y: Expr::Number(Number {
12456 value: 10.0,
12457 units: NumericSuffix::Mm,
12458 }),
12459 },
12460 construction: None,
12461 };
12462 let segment = SegmentCtor::Line(line_ctor);
12463 let (src_delta, scene_delta) = frontend
12464 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12465 .await
12466 .unwrap();
12467 assert!(
12468 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12469 "Expected line in source, got: {}",
12470 src_delta.text
12471 );
12472 assert_eq!(scene_delta.new_objects.len(), 3);
12474
12475 ctx.close().await;
12476 mock_ctx.close().await;
12477 }
12478
12479 #[tokio::test(flavor = "multi_thread")]
12480 async fn test_extra_newlines_between_operations_edit_line() {
12481 let initial_source = "@settings(defaultLengthUnit = mm)
12483
12484
12485sketch001 = sketch(on = XY) {
12486
12487 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12488
12489}
12490";
12491
12492 let program = Program::parse(initial_source).unwrap().0.unwrap();
12493 let mut frontend = FrontendState::new();
12494
12495 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12496 let mock_ctx = ExecutorContext::new_mock(None).await;
12497 let version = Version(0);
12498 let project_id = ProjectId(0);
12499 let file_id = FileId(0);
12500
12501 frontend.hack_set_program(&ctx, program).await.unwrap();
12502 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12503 let sketch_id = sketch_object.id;
12504 let sketch = expect_sketch(sketch_object);
12505
12506 let line_id = sketch
12508 .segments
12509 .iter()
12510 .copied()
12511 .find(|seg_id| {
12512 matches!(
12513 &frontend.scene_graph.objects[seg_id.0].kind,
12514 ObjectKind::Segment {
12515 segment: Segment::Line(_)
12516 }
12517 )
12518 })
12519 .expect("Expected a line segment in sketch");
12520
12521 frontend
12523 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12524 .await
12525 .unwrap();
12526
12527 let line_ctor = LineCtor {
12529 start: Point2d {
12530 x: Expr::Var(Number {
12531 value: 1.0,
12532 units: NumericSuffix::Mm,
12533 }),
12534 y: Expr::Var(Number {
12535 value: 2.0,
12536 units: NumericSuffix::Mm,
12537 }),
12538 },
12539 end: Point2d {
12540 x: Expr::Var(Number {
12541 value: 13.0,
12542 units: NumericSuffix::Mm,
12543 }),
12544 y: Expr::Var(Number {
12545 value: 14.0,
12546 units: NumericSuffix::Mm,
12547 }),
12548 },
12549 construction: None,
12550 };
12551 let segments = vec![ExistingSegmentCtor {
12552 id: line_id,
12553 ctor: SegmentCtor::Line(line_ctor),
12554 }];
12555 let (src_delta, _scene_delta) = frontend
12556 .edit_segments(&mock_ctx, version, sketch_id, segments)
12557 .await
12558 .unwrap();
12559 assert!(
12560 src_delta
12561 .text
12562 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
12563 "Expected edited line in source, got: {}",
12564 src_delta.text
12565 );
12566
12567 ctx.close().await;
12568 mock_ctx.close().await;
12569 }
12570
12571 #[tokio::test(flavor = "multi_thread")]
12572 async fn test_extra_newlines_delete_segment() {
12573 let initial_source = "@settings(defaultLengthUnit = mm)
12575
12576
12577
12578sketch001 = sketch(on = XY) {
12579 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
12580}
12581";
12582
12583 let program = Program::parse(initial_source).unwrap().0.unwrap();
12584 let mut frontend = FrontendState::new();
12585
12586 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12587 let mock_ctx = ExecutorContext::new_mock(None).await;
12588 let version = Version(0);
12589
12590 frontend.hack_set_program(&ctx, program).await.unwrap();
12591 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12592 let sketch_id = sketch_object.id;
12593 let sketch = expect_sketch(sketch_object);
12594
12595 assert_eq!(sketch.segments.len(), 3);
12597 let circle_id = sketch.segments[2];
12598
12599 let (src_delta, scene_delta) = frontend
12601 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
12602 .await
12603 .unwrap();
12604 assert!(
12605 src_delta.text.contains("sketch(on = XY) {"),
12606 "Expected sketch block in source, got: {}",
12607 src_delta.text
12608 );
12609 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12610 let new_sketch = expect_sketch(new_sketch_object);
12611 assert_eq!(new_sketch.segments.len(), 0);
12612
12613 ctx.close().await;
12614 mock_ctx.close().await;
12615 }
12616
12617 #[tokio::test(flavor = "multi_thread")]
12618 async fn test_unformatted_source_add_arc() {
12619 let initial_source = "@settings(defaultLengthUnit = mm)
12621
12622
12623
12624
12625sketch001 = sketch(on = XY) {
12626}
12627";
12628
12629 let program = Program::parse(initial_source).unwrap().0.unwrap();
12630 let mut frontend = FrontendState::new();
12631
12632 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12633 let mock_ctx = ExecutorContext::new_mock(None).await;
12634 let version = Version(0);
12635
12636 frontend.hack_set_program(&ctx, program).await.unwrap();
12637 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12638 let sketch_id = sketch_object.id;
12639
12640 let arc_ctor = ArcCtor {
12641 start: Point2d {
12642 x: Expr::Var(Number {
12643 value: 5.0,
12644 units: NumericSuffix::Mm,
12645 }),
12646 y: Expr::Var(Number {
12647 value: 0.0,
12648 units: NumericSuffix::Mm,
12649 }),
12650 },
12651 end: Point2d {
12652 x: Expr::Var(Number {
12653 value: 0.0,
12654 units: NumericSuffix::Mm,
12655 }),
12656 y: Expr::Var(Number {
12657 value: 5.0,
12658 units: NumericSuffix::Mm,
12659 }),
12660 },
12661 center: Point2d {
12662 x: Expr::Var(Number {
12663 value: 0.0,
12664 units: NumericSuffix::Mm,
12665 }),
12666 y: Expr::Var(Number {
12667 value: 0.0,
12668 units: NumericSuffix::Mm,
12669 }),
12670 },
12671 construction: None,
12672 };
12673 let segment = SegmentCtor::Arc(arc_ctor);
12674 let (src_delta, scene_delta) = frontend
12675 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12676 .await
12677 .unwrap();
12678 assert!(
12679 src_delta
12680 .text
12681 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
12682 "Expected arc in source, got: {}",
12683 src_delta.text
12684 );
12685 assert!(!scene_delta.new_objects.is_empty());
12686
12687 ctx.close().await;
12688 mock_ctx.close().await;
12689 }
12690
12691 #[tokio::test(flavor = "multi_thread")]
12692 async fn test_extra_newlines_add_circle() {
12693 let initial_source = "@settings(defaultLengthUnit = mm)
12695
12696
12697
12698sketch001 = sketch(on = XY) {
12699}
12700";
12701
12702 let program = Program::parse(initial_source).unwrap().0.unwrap();
12703 let mut frontend = FrontendState::new();
12704
12705 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12706 let mock_ctx = ExecutorContext::new_mock(None).await;
12707 let version = Version(0);
12708
12709 frontend.hack_set_program(&ctx, program).await.unwrap();
12710 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12711 let sketch_id = sketch_object.id;
12712
12713 let circle_ctor = CircleCtor {
12714 start: Point2d {
12715 x: Expr::Var(Number {
12716 value: 5.0,
12717 units: NumericSuffix::Mm,
12718 }),
12719 y: Expr::Var(Number {
12720 value: 0.0,
12721 units: NumericSuffix::Mm,
12722 }),
12723 },
12724 center: Point2d {
12725 x: Expr::Var(Number {
12726 value: 0.0,
12727 units: NumericSuffix::Mm,
12728 }),
12729 y: Expr::Var(Number {
12730 value: 0.0,
12731 units: NumericSuffix::Mm,
12732 }),
12733 },
12734 construction: None,
12735 };
12736 let segment = SegmentCtor::Circle(circle_ctor);
12737 let (src_delta, scene_delta) = frontend
12738 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12739 .await
12740 .unwrap();
12741 assert!(
12742 src_delta
12743 .text
12744 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
12745 "Expected circle in source, got: {}",
12746 src_delta.text
12747 );
12748 assert!(!scene_delta.new_objects.is_empty());
12749
12750 ctx.close().await;
12751 mock_ctx.close().await;
12752 }
12753
12754 #[tokio::test(flavor = "multi_thread")]
12755 async fn test_extra_newlines_add_constraint() {
12756 let initial_source = "@settings(defaultLengthUnit = mm)
12758
12759
12760
12761sketch001 = sketch(on = XY) {
12762 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12763 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
12764}
12765";
12766
12767 let program = Program::parse(initial_source).unwrap().0.unwrap();
12768 let mut frontend = FrontendState::new();
12769
12770 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12771 let mock_ctx = ExecutorContext::new_mock(None).await;
12772 let version = Version(0);
12773 let project_id = ProjectId(0);
12774 let file_id = FileId(0);
12775
12776 frontend.hack_set_program(&ctx, program).await.unwrap();
12777 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12778 let sketch_id = sketch_object.id;
12779 let sketch = expect_sketch(sketch_object);
12780
12781 let line_ids: Vec<ObjectId> = sketch
12783 .segments
12784 .iter()
12785 .copied()
12786 .filter(|seg_id| {
12787 matches!(
12788 &frontend.scene_graph.objects[seg_id.0].kind,
12789 ObjectKind::Segment {
12790 segment: Segment::Line(_)
12791 }
12792 )
12793 })
12794 .collect();
12795 assert_eq!(line_ids.len(), 2, "Expected two line segments");
12796
12797 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
12798 let ObjectKind::Segment {
12799 segment: Segment::Line(line1_data),
12800 } = &line1.kind
12801 else {
12802 panic!("Expected line");
12803 };
12804 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
12805 let ObjectKind::Segment {
12806 segment: Segment::Line(line2_data),
12807 } = &line2.kind
12808 else {
12809 panic!("Expected line");
12810 };
12811
12812 let constraint = Constraint::Coincident(Coincident {
12814 segments: vec![line1_data.end.into(), line2_data.start.into()],
12815 });
12816
12817 frontend
12819 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12820 .await
12821 .unwrap();
12822 let (src_delta, _scene_delta) = frontend
12823 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12824 .await
12825 .unwrap();
12826 assert!(
12827 src_delta.text.contains("coincident("),
12828 "Expected coincident constraint in source, got: {}",
12829 src_delta.text
12830 );
12831
12832 ctx.close().await;
12833 mock_ctx.close().await;
12834 }
12835
12836 #[tokio::test(flavor = "multi_thread")]
12837 async fn test_extra_newlines_add_line_then_edit_line() {
12838 let initial_source = "@settings(defaultLengthUnit = mm)
12840
12841
12842
12843sketch001 = sketch(on = XY) {
12844}
12845";
12846
12847 let program = Program::parse(initial_source).unwrap().0.unwrap();
12848 let mut frontend = FrontendState::new();
12849
12850 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12851 let mock_ctx = ExecutorContext::new_mock(None).await;
12852 let version = Version(0);
12853
12854 frontend.hack_set_program(&ctx, program).await.unwrap();
12855 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12856 let sketch_id = sketch_object.id;
12857
12858 let line_ctor = LineCtor {
12860 start: Point2d {
12861 x: Expr::Number(Number {
12862 value: 0.0,
12863 units: NumericSuffix::Mm,
12864 }),
12865 y: Expr::Number(Number {
12866 value: 0.0,
12867 units: NumericSuffix::Mm,
12868 }),
12869 },
12870 end: Point2d {
12871 x: Expr::Number(Number {
12872 value: 10.0,
12873 units: NumericSuffix::Mm,
12874 }),
12875 y: Expr::Number(Number {
12876 value: 10.0,
12877 units: NumericSuffix::Mm,
12878 }),
12879 },
12880 construction: None,
12881 };
12882 let segment = SegmentCtor::Line(line_ctor);
12883 let (src_delta, scene_delta) = frontend
12884 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12885 .await
12886 .unwrap();
12887 assert!(
12888 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12889 "Expected line in source after add, got: {}",
12890 src_delta.text
12891 );
12892 let line_id = *scene_delta.new_objects.last().unwrap();
12894
12895 let line_ctor = LineCtor {
12897 start: Point2d {
12898 x: Expr::Number(Number {
12899 value: 1.0,
12900 units: NumericSuffix::Mm,
12901 }),
12902 y: Expr::Number(Number {
12903 value: 2.0,
12904 units: NumericSuffix::Mm,
12905 }),
12906 },
12907 end: Point2d {
12908 x: Expr::Number(Number {
12909 value: 13.0,
12910 units: NumericSuffix::Mm,
12911 }),
12912 y: Expr::Number(Number {
12913 value: 14.0,
12914 units: NumericSuffix::Mm,
12915 }),
12916 },
12917 construction: None,
12918 };
12919 let segments = vec![ExistingSegmentCtor {
12920 id: line_id,
12921 ctor: SegmentCtor::Line(line_ctor),
12922 }];
12923 let (src_delta, scene_delta) = frontend
12924 .edit_segments(&mock_ctx, version, sketch_id, segments)
12925 .await
12926 .unwrap();
12927 assert!(
12928 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
12929 "Expected edited line in source, got: {}",
12930 src_delta.text
12931 );
12932 assert_eq!(scene_delta.new_objects, vec![]);
12933
12934 ctx.close().await;
12935 mock_ctx.close().await;
12936 }
12937}