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::annotations::WarningLevel;
25use crate::execution::cache::SketchModeState;
26use crate::execution::cache::clear_mem_cache;
27use crate::execution::cache::read_old_memory;
28use crate::execution::cache::write_old_memory;
29use crate::fmt::format_number_literal;
30use crate::front::Angle;
31use crate::front::ArcCtor;
32use crate::front::CircleCtor;
33use crate::front::ControlPointSplineCtor;
34use crate::front::Distance;
35use crate::front::EqualRadius;
36use crate::front::Error;
37use crate::front::ExecResult;
38use crate::front::FixedPoint;
39use crate::front::Freedom;
40use crate::front::LinesEqualLength;
41use crate::front::Midpoint;
42use crate::front::Object;
43use crate::front::Parallel;
44use crate::front::Perpendicular;
45use crate::front::PointCtor;
46use crate::front::Symmetric;
47use crate::front::Tangent;
48use crate::frontend::api::Expr;
49use crate::frontend::api::FileId;
50use crate::frontend::api::Number;
51use crate::frontend::api::ObjectId;
52use crate::frontend::api::ObjectKind;
53use crate::frontend::api::Plane;
54use crate::frontend::api::ProjectId;
55use crate::frontend::api::RestoreSketchCheckpointOutcome;
56use crate::frontend::api::SceneGraph;
57use crate::frontend::api::SceneGraphDelta;
58use crate::frontend::api::SketchCheckpointId;
59use crate::frontend::api::SourceDelta;
60use crate::frontend::api::SourceRef;
61use crate::frontend::api::Version;
62use crate::frontend::modify::find_defined_names;
63use crate::frontend::modify::next_free_name;
64use crate::frontend::modify::next_free_name_with_padding;
65use crate::frontend::sketch::Coincident;
66use crate::frontend::sketch::Constraint;
67use crate::frontend::sketch::ConstraintSegment;
68use crate::frontend::sketch::Diameter;
69use crate::frontend::sketch::ExistingSegmentCtor;
70use crate::frontend::sketch::Horizontal;
71use crate::frontend::sketch::LineCtor;
72use crate::frontend::sketch::Point2d;
73use crate::frontend::sketch::Radius;
74use crate::frontend::sketch::Segment;
75use crate::frontend::sketch::SegmentCtor;
76use crate::frontend::sketch::SketchApi;
77use crate::frontend::sketch::SketchCtor;
78use crate::frontend::sketch::Vertical;
79use crate::frontend::traverse::MutateBodyItem;
80use crate::frontend::traverse::TraversalReturn;
81use crate::frontend::traverse::Visitor;
82use crate::frontend::traverse::dfs_mut;
83use crate::id::IncIdGenerator;
84use crate::parsing::ast::types as ast;
85use crate::pretty::NumericSuffix;
86use crate::std::constraints::LinesAtAngleKind;
87use crate::walk::NodeMut;
88use crate::walk::Visitable;
89
90pub(crate) mod api;
91pub(crate) mod modify;
92pub(crate) mod sketch;
93
94pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
95
96#[derive(Debug, Clone)]
97struct SketchCheckpoint {
98 id: SketchCheckpointId,
99 source: SourceDelta,
100 program: Program,
101 scene_graph: SceneGraph,
102 exec_outcome: ExecOutcome,
103 point_freedom_cache: HashMap<ObjectId, Freedom>,
104 mock_memory: Option<SketchModeState>,
105}
106mod traverse;
107pub(crate) mod trim;
108
109struct ArcSizeConstraintParams {
110 points: Vec<ObjectId>,
111 function_name: &'static str,
112 value: f64,
113 units: NumericSuffix,
114 label_position: Option<Point2d<Number>>,
115 constraint_type_name: &'static str,
116}
117
118const POINT_FN: &str = "point";
119const POINT_AT_PARAM: &str = "at";
120const LINE_FN: &str = "line";
121const LINE_VARIABLE: &str = "line";
122const LINE_START_PARAM: &str = "start";
123const LINE_END_PARAM: &str = "end";
124const ARC_FN: &str = "arc";
125const ARC_VARIABLE: &str = "arc";
126const ARC_START_PARAM: &str = "start";
127const ARC_END_PARAM: &str = "end";
128const ARC_CENTER_PARAM: &str = "center";
129const CIRCLE_FN: &str = "circle";
130const CIRCLE_VARIABLE: &str = "circle";
131const CIRCLE_START_PARAM: &str = "start";
132const CIRCLE_CENTER_PARAM: &str = "center";
133const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
134const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
135const LABEL_POSITION_PARAM: &str = "labelPosition";
136
137const COINCIDENT_FN: &str = "coincident";
138const DIAMETER_FN: &str = "diameter";
139const DISTANCE_FN: &str = "distance";
140const FIXED_FN: &str = "fixed";
141const ANGLE_FN: &str = "angle";
142const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
143const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
144const EQUAL_LENGTH_FN: &str = "equalLength";
145const EQUAL_RADIUS_FN: &str = "equalRadius";
146const HORIZONTAL_FN: &str = "horizontal";
147const MIDPOINT_FN: &str = "midpoint";
148const MIDPOINT_POINT_PARAM: &str = "point";
149const RADIUS_FN: &str = "radius";
150const SYMMETRIC_FN: &str = "symmetric";
151const SYMMETRIC_AXIS_PARAM: &str = "axis";
152const TANGENT_FN: &str = "tangent";
153const VERTICAL_FN: &str = "vertical";
154
155const LINE_PROPERTY_START: &str = "start";
156const LINE_PROPERTY_END: &str = "end";
157
158const ARC_PROPERTY_START: &str = "start";
159const ARC_PROPERTY_END: &str = "end";
160const ARC_PROPERTY_CENTER: &str = "center";
161const CIRCLE_PROPERTY_START: &str = "start";
162const CIRCLE_PROPERTY_CENTER: &str = "center";
163const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
164const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
165
166const CONSTRUCTION_PARAM: &str = "construction";
167
168#[derive(Debug, Clone, Copy)]
169enum EditDeleteKind {
170 Edit,
171 DeleteNonSketch,
172}
173
174impl EditDeleteKind {
175 fn is_delete(&self) -> bool {
177 match self {
178 EditDeleteKind::Edit => false,
179 EditDeleteKind::DeleteNonSketch => true,
180 }
181 }
182
183 fn to_change_kind(self) -> ChangeKind {
184 match self {
185 EditDeleteKind::Edit => ChangeKind::Edit,
186 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
187 }
188 }
189}
190
191#[derive(Debug, Clone, Copy)]
192enum ChangeKind {
193 Add,
194 Edit,
195 Delete,
196 None,
197}
198
199#[derive(Debug, Clone, Serialize, ts_rs::TS)]
200#[ts(export, export_to = "FrontendApi.ts")]
201#[serde(tag = "type")]
202pub enum SetProgramOutcome {
203 #[serde(rename_all = "camelCase")]
204 Success {
205 scene_graph: Box<SceneGraph>,
206 exec_outcome: Box<ExecOutcome>,
207 checkpoint_id: Option<SketchCheckpointId>,
208 },
209 #[serde(rename_all = "camelCase")]
210 ExecFailure { error: Box<KclErrorWithOutputs> },
211}
212
213#[derive(Debug, Clone)]
214pub struct FrontendState {
215 program: Program,
216 scene_graph: SceneGraph,
217 point_freedom_cache: HashMap<ObjectId, Freedom>,
220 sketch_checkpoints: VecDeque<SketchCheckpoint>,
221 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
222}
223
224impl Default for FrontendState {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230impl FrontendState {
231 pub fn new() -> Self {
232 Self {
233 program: Program::empty(),
234 scene_graph: SceneGraph {
235 project: ProjectId(0),
236 file: FileId(0),
237 version: Version(0),
238 objects: Default::default(),
239 settings: Default::default(),
240 sketch_mode: Default::default(),
241 },
242 point_freedom_cache: HashMap::new(),
243 sketch_checkpoints: VecDeque::new(),
244 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
245 }
246 }
247
248 pub fn scene_graph(&self) -> &SceneGraph {
250 &self.scene_graph
251 }
252
253 pub fn default_length_unit(&self) -> UnitLength {
254 self.program
255 .meta_settings()
256 .ok()
257 .flatten()
258 .map(|settings| settings.default_length_units)
259 .unwrap_or(UnitLength::Millimeters)
260 }
261
262 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
263 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
264
265 let checkpoint = SketchCheckpoint {
266 id: checkpoint_id,
267 source: SourceDelta {
268 text: source_from_ast(&self.program.ast),
269 },
270 program: self.program.clone(),
271 scene_graph: self.scene_graph.clone(),
272 exec_outcome,
273 point_freedom_cache: self.point_freedom_cache.clone(),
274 mock_memory: read_old_memory().await,
275 };
276
277 self.sketch_checkpoints.push_back(checkpoint);
278 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
279 self.sketch_checkpoints.pop_front();
280 }
281
282 Ok(checkpoint_id)
283 }
284
285 pub async fn restore_sketch_checkpoint(
286 &mut self,
287 checkpoint_id: SketchCheckpointId,
288 ) -> api::Result<RestoreSketchCheckpointOutcome> {
289 let checkpoint = self
290 .sketch_checkpoints
291 .iter()
292 .find(|checkpoint| checkpoint.id == checkpoint_id)
293 .cloned()
294 .ok_or_else(|| Error {
295 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
296 })?;
297
298 self.program = checkpoint.program;
299 self.scene_graph = checkpoint.scene_graph.clone();
300 self.point_freedom_cache = checkpoint.point_freedom_cache;
301
302 if let Some(mock_memory) = checkpoint.mock_memory {
303 write_old_memory(mock_memory).await;
304 } else {
305 clear_mem_cache().await;
306 }
307
308 Ok(RestoreSketchCheckpointOutcome {
309 source_delta: checkpoint.source,
310 scene_graph_delta: SceneGraphDelta {
311 new_graph: self.scene_graph_for_ui(),
312 new_objects: Vec::new(),
313 invalidates_ids: true,
314 exec_outcome: checkpoint.exec_outcome,
315 },
316 })
317 }
318
319 pub fn clear_sketch_checkpoints(&mut self) {
320 self.sketch_checkpoints.clear();
321 }
322 fn scene_graph_for_ui(&self) -> SceneGraph {
323 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
324 matches!(
325 object.kind,
326 ObjectKind::Segment {
327 segment: Segment::ControlPointSpline(_)
328 }
329 )
330 });
331
332 if !has_control_point_splines {
333 return self.scene_graph.clone();
334 }
335
336 let hidden_constraint_ids = self
337 .scene_graph
338 .objects
339 .iter()
340 .filter_map(|object| match &object.kind {
341 ObjectKind::Constraint {
342 constraint: Constraint::Coincident(coincident),
343 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
344 Some(object.id)
345 }
346 _ => None,
347 })
348 .collect::<HashSet<_>>();
349
350 if hidden_constraint_ids.is_empty() {
351 return self.scene_graph.clone();
352 }
353
354 let mut scene_graph = self.scene_graph.clone();
355 for object in &mut scene_graph.objects {
356 match &mut object.kind {
357 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
358 object.kind = ObjectKind::Nil;
359 }
360 ObjectKind::Sketch(sketch) => {
361 sketch
362 .constraints
363 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
364 }
365 _ => {}
366 }
367 }
368
369 scene_graph
370 }
371}
372
373fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
374 let mut first_owner_id = None;
375 for segment_id in coincident.segment_ids() {
376 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
377 return false;
378 };
379
380 match first_owner_id {
381 Some(first_owner_id) if first_owner_id != owner_id => return false,
382 Some(_) => {}
383 None => first_owner_id = Some(owner_id),
384 }
385 }
386
387 first_owner_id.is_some()
388}
389
390fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
391 let object = scene_graph.objects.get(segment_id.0)?;
392 let ObjectKind::Segment { segment } = &object.kind else {
393 return None;
394 };
395
396 match segment {
397 Segment::ControlPointSpline(_) => Some(segment_id),
398 Segment::Point(point) => point
399 .owner
400 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
401 Segment::Line(line) => line
402 .owner
403 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
404 _ => None,
405 }
406}
407
408fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
409 matches!(
410 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
411 Some(ObjectKind::Segment {
412 segment: Segment::ControlPointSpline(_)
413 })
414 )
415}
416
417fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
418 let experimental_features_allowed = program
419 .meta_settings()
420 .ok()
421 .flatten()
422 .map(|settings| settings.experimental_features == WarningLevel::Allow)
423 .unwrap_or(false);
424 if experimental_features_allowed {
425 return Ok(program.clone());
426 }
427
428 program.change_experimental_features(Some(WarningLevel::Allow))
429}
430
431impl SketchApi for FrontendState {
432 async fn execute_mock(
433 &mut self,
434 ctx: &ExecutorContext,
435 _version: Version,
436 sketch: ObjectId,
437 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
438 let sketch_block_ref =
439 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
440
441 let mut truncated_program = self.program.clone();
442 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
443 .map_err(KclErrorWithOutputs::no_outputs)?;
444
445 let outcome = ctx
447 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
448 .await?;
449 let new_source = source_from_ast(&self.program.ast);
450 let src_delta = SourceDelta { text: new_source };
451 let outcome = self.update_state_after_exec(outcome, true);
453 let scene_graph_delta = SceneGraphDelta {
454 new_graph: self.scene_graph.clone(),
455 new_objects: Default::default(),
456 invalidates_ids: false,
457 exec_outcome: outcome,
458 };
459 Ok((src_delta, scene_graph_delta))
460 }
461
462 async fn new_sketch(
463 &mut self,
464 ctx: &ExecutorContext,
465 _project: ProjectId,
466 _file: FileId,
467 _version: Version,
468 args: SketchCtor,
469 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
470 let mut new_ast = self.program.ast.clone();
473 let mut plane_ast =
475 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
476 let mut defined_names = find_defined_names(&new_ast);
477 let is_face_of_expr = matches!(
478 &plane_ast,
479 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
480 );
481 if is_face_of_expr {
482 let face_name = next_free_name_with_padding("face", &defined_names)
483 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
484 let face_decl = ast::VariableDeclaration::new(
485 ast::VariableDeclarator::new(&face_name, plane_ast),
486 ast::ItemVisibility::Default,
487 ast::VariableKind::Const,
488 );
489 new_ast
490 .body
491 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
492 face_decl,
493 ))));
494 defined_names.insert(face_name.clone());
495 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
496 }
497 let sketch_ast = ast::SketchBlock {
498 arguments: vec![ast::LabeledArg {
499 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
500 arg: plane_ast,
501 }],
502 body: Default::default(),
503 is_being_edited: false,
504 non_code_meta: Default::default(),
505 digest: None,
506 };
507 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
510 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
511 let sketch_decl = ast::VariableDeclaration::new(
512 ast::VariableDeclarator::new(
513 &sketch_name,
514 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
515 ),
516 ast::ItemVisibility::Default,
517 ast::VariableKind::Const,
518 );
519 new_ast
520 .body
521 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
522 sketch_decl,
523 ))));
524 let new_source = source_from_ast(&new_ast);
526 let (new_program, errors) = Program::parse(&new_source)
528 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
529 if !errors.is_empty() {
530 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
531 "Error parsing KCL source after adding sketch: {errors:?}"
532 ))));
533 }
534 let Some(new_program) = new_program else {
535 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
536 "No AST produced after adding sketch".to_owned(),
537 )));
538 };
539
540 self.program = new_program.clone();
542
543 let outcome = ctx.run_with_caching(new_program.clone()).await?;
546 let freedom_analysis_ran = true;
547
548 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
549
550 let Some(sketch_id) = self
551 .scene_graph
552 .objects
553 .iter()
554 .filter_map(|object| match object.kind {
555 ObjectKind::Sketch(_) => Some(object.id),
556 _ => None,
557 })
558 .max_by_key(|id| id.0)
559 else {
560 return Err(KclErrorWithOutputs::from_error_outcome(
561 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
562 outcome,
563 ));
564 };
565 self.scene_graph.sketch_mode = Some(sketch_id);
567
568 let src_delta = SourceDelta { text: new_source };
569 let scene_graph_delta = SceneGraphDelta {
570 new_graph: self.scene_graph_for_ui(),
571 invalidates_ids: false,
572 new_objects: vec![sketch_id],
573 exec_outcome: outcome,
574 };
575 Ok((src_delta, scene_graph_delta, sketch_id))
576 }
577
578 async fn edit_sketch(
579 &mut self,
580 ctx: &ExecutorContext,
581 _project: ProjectId,
582 _file: FileId,
583 _version: Version,
584 sketch: ObjectId,
585 ) -> ExecResult<SceneGraphDelta> {
586 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
590 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
591 })?;
592 let ObjectKind::Sketch(_) = &sketch_object.kind else {
593 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
594 "Object is not a sketch, it is {}",
595 sketch_object.kind.human_friendly_kind_with_article()
596 ))));
597 };
598 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
599
600 self.scene_graph.sketch_mode = Some(sketch);
602
603 let mut truncated_program = self.program.clone();
605 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
606 .map_err(KclErrorWithOutputs::no_outputs)?;
607
608 let outcome = ctx
611 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
612 .await?;
613
614 let outcome = self.update_state_after_exec(outcome, true);
616 let scene_graph_delta = SceneGraphDelta {
617 new_graph: self.scene_graph_for_ui(),
618 invalidates_ids: false,
619 new_objects: Vec::new(),
620 exec_outcome: outcome,
621 };
622 Ok(scene_graph_delta)
623 }
624
625 async fn exit_sketch(
626 &mut self,
627 ctx: &ExecutorContext,
628 _version: Version,
629 sketch: ObjectId,
630 ) -> ExecResult<SceneGraph> {
631 #[cfg(not(target_arch = "wasm32"))]
633 let _ = sketch;
634 #[cfg(target_arch = "wasm32")]
635 if self.scene_graph.sketch_mode != Some(sketch) {
636 web_sys::console::warn_1(
637 &format!(
638 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
639 &self.scene_graph.sketch_mode
640 )
641 .into(),
642 );
643 }
644 self.scene_graph.sketch_mode = None;
645
646 let outcome = ctx.run_with_caching(self.program.clone()).await?;
648
649 self.update_state_after_exec(outcome, false);
651
652 Ok(self.scene_graph_for_ui())
653 }
654
655 async fn delete_sketch(
656 &mut self,
657 ctx: &ExecutorContext,
658 _version: Version,
659 sketch: ObjectId,
660 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
661 let mut new_ast = self.program.ast.clone();
664
665 let sketch_id = sketch;
667 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
668 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
669 })?;
670 let ObjectKind::Sketch(_) = &sketch_object.kind else {
671 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
672 "Object is not a sketch, it is {}",
673 sketch_object.kind.human_friendly_kind_with_article(),
674 ))));
675 };
676
677 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
679 .map_err(KclErrorWithOutputs::no_outputs)?;
680
681 self.execute_after_delete_sketch(ctx, &mut new_ast).await
682 }
683
684 async fn add_segment(
685 &mut self,
686 ctx: &ExecutorContext,
687 _version: Version,
688 sketch: ObjectId,
689 segment: SegmentCtor,
690 _label: Option<String>,
691 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
692 match segment {
694 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
695 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
696 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
697 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
698 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
699 }
700 }
701
702 async fn edit_segments(
703 &mut self,
704 ctx: &ExecutorContext,
705 _version: Version,
706 sketch: ObjectId,
707 segments: Vec<ExistingSegmentCtor>,
708 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
709 let sketch_block_ref =
711 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
712
713 let mut new_ast = self.program.ast.clone();
714 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
715 let mut invalidates_ids = false;
716
717 for segment in &segments {
720 segment_ids_edited.insert(segment.id);
721 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
722 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
723 && let ObjectKind::Segment {
724 segment: Segment::ControlPointSpline(existing_spline),
725 } = &existing_object.kind
726 && existing_spline.controls.len() != new_ctor.points.len()
727 {
728 invalidates_ids = true;
729 }
730 }
731
732 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
747
748 for segment in segments {
749 let segment_id = segment.id;
750 match segment.ctor {
751 SegmentCtor::Point(ctor) => {
752 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
754 && let ObjectKind::Segment { segment } = &segment_object.kind
755 && let Segment::Point(point) = segment
756 && let Some(owner_id) = point.owner
757 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
758 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
759 {
760 match owner_segment {
761 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
762 if let Some(existing) = final_edits.get_mut(&owner_id) {
763 let SegmentCtor::Line(line_ctor) = existing else {
764 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
765 "Internal: Expected line ctor for owner, but found {}",
766 existing.human_friendly_kind_with_article()
767 ))));
768 };
769 if line.start == segment_id {
771 line_ctor.start = ctor.position;
772 } else {
773 line_ctor.end = ctor.position;
774 }
775 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
776 let mut line_ctor = line_ctor.clone();
778 if line.start == segment_id {
779 line_ctor.start = ctor.position;
780 } else {
781 line_ctor.end = ctor.position;
782 }
783 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
784 } else {
785 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
787 "Internal: Line does not have line ctor, but found {}",
788 line.ctor.human_friendly_kind_with_article()
789 ))));
790 }
791 continue;
792 }
793 Segment::Arc(arc)
794 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
795 {
796 if let Some(existing) = final_edits.get_mut(&owner_id) {
797 let SegmentCtor::Arc(arc_ctor) = existing else {
798 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
799 "Internal: Expected arc ctor for owner, but found {}",
800 existing.human_friendly_kind_with_article()
801 ))));
802 };
803 if arc.start == segment_id {
804 arc_ctor.start = ctor.position;
805 } else if arc.end == segment_id {
806 arc_ctor.end = ctor.position;
807 } else {
808 arc_ctor.center = ctor.position;
809 }
810 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
811 let mut arc_ctor = arc_ctor.clone();
812 if arc.start == segment_id {
813 arc_ctor.start = ctor.position;
814 } else if arc.end == segment_id {
815 arc_ctor.end = ctor.position;
816 } else {
817 arc_ctor.center = ctor.position;
818 }
819 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
820 } else {
821 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
822 "Internal: Arc does not have arc ctor, but found {}",
823 arc.ctor.human_friendly_kind_with_article()
824 ))));
825 }
826 continue;
827 }
828 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
829 if let Some(existing) = final_edits.get_mut(&owner_id) {
830 let SegmentCtor::Circle(circle_ctor) = existing else {
831 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
832 "Internal: Expected circle ctor for owner, but found {}",
833 existing.human_friendly_kind_with_article()
834 ))));
835 };
836 if circle.start == segment_id {
837 circle_ctor.start = ctor.position;
838 } else {
839 circle_ctor.center = ctor.position;
840 }
841 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
842 let mut circle_ctor = circle_ctor.clone();
843 if circle.start == segment_id {
844 circle_ctor.start = ctor.position;
845 } else {
846 circle_ctor.center = ctor.position;
847 }
848 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
849 } else {
850 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
851 "Internal: Circle does not have circle ctor, but found {}",
852 circle.ctor.human_friendly_kind_with_article()
853 ))));
854 }
855 continue;
856 }
857 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
858 let Some(control_index) =
859 spline.controls.iter().position(|control_id| *control_id == segment_id)
860 else {
861 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
862 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
863 ))));
864 };
865 if let Some(existing) = final_edits.get_mut(&owner_id) {
866 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
867 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
868 "Internal: Expected controlPointSpline ctor for owner, but found {}",
869 existing.human_friendly_kind_with_article()
870 ))));
871 };
872 spline_ctor.points[control_index] = ctor.position;
873 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
874 let mut spline_ctor = spline_ctor.clone();
875 spline_ctor.points[control_index] = ctor.position;
876 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
877 } else {
878 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
879 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
880 spline.ctor.human_friendly_kind_with_article()
881 ))));
882 }
883 continue;
884 }
885 _ => {}
886 }
887 }
888
889 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
891 }
892 SegmentCtor::Line(ctor) => {
893 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
894 }
895 SegmentCtor::Arc(ctor) => {
896 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
897 }
898 SegmentCtor::Circle(ctor) => {
899 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
900 }
901 SegmentCtor::ControlPointSpline(ctor) => {
902 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
903 }
904 }
905 }
906
907 for (segment_id, ctor) in final_edits {
908 match ctor {
909 SegmentCtor::Point(ctor) => self
910 .edit_point(&mut new_ast, sketch, segment_id, ctor)
911 .map_err(KclErrorWithOutputs::no_outputs)?,
912 SegmentCtor::Line(ctor) => self
913 .edit_line(&mut new_ast, sketch, segment_id, ctor)
914 .map_err(KclErrorWithOutputs::no_outputs)?,
915 SegmentCtor::Arc(ctor) => self
916 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
917 .map_err(KclErrorWithOutputs::no_outputs)?,
918 SegmentCtor::Circle(ctor) => self
919 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
920 .map_err(KclErrorWithOutputs::no_outputs)?,
921 SegmentCtor::ControlPointSpline(ctor) => self
922 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
923 .map_err(KclErrorWithOutputs::no_outputs)?,
924 }
925 }
926 let (source_delta, mut scene_graph_delta) = self
927 .execute_after_edit(
928 ctx,
929 sketch,
930 sketch_block_ref,
931 segment_ids_edited,
932 EditDeleteKind::Edit,
933 &mut new_ast,
934 )
935 .await?;
936 if invalidates_ids {
937 scene_graph_delta.invalidates_ids = true;
938 }
939 Ok((source_delta, scene_graph_delta))
940 }
941
942 async fn delete_objects(
943 &mut self,
944 ctx: &ExecutorContext,
945 _version: Version,
946 sketch: ObjectId,
947 constraint_ids: Vec<ObjectId>,
948 segment_ids: Vec<ObjectId>,
949 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
950 let sketch_block_ref =
952 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
953
954 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
956 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
957
958 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
961
962 for segment_id in segment_ids_set.iter().copied() {
963 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
964 let ObjectKind::Segment { segment } = &segment_object.kind else {
965 return None;
966 };
967 match segment {
968 Segment::Point(point) => point.owner,
969 Segment::Line(line) => line.owner,
970 _ => None,
971 }
972 });
973
974 if let Some(owner_id) = owner_id
975 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
976 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
977 && matches!(
978 owner_segment,
979 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
980 )
981 {
982 resolved_segment_ids_to_delete.insert(owner_id);
984 } else {
985 resolved_segment_ids_to_delete.insert(segment_id);
987 }
988 }
989 let referenced_constraint_ids = self
990 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
991 .map_err(KclErrorWithOutputs::no_outputs)?;
992
993 let mut new_ast = self.program.ast.clone();
994
995 for constraint_id in referenced_constraint_ids {
996 if constraint_ids_set.contains(&constraint_id) {
997 continue;
998 }
999
1000 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1001 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1002 })?;
1003 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1004 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1005 "Object is not a constraint, it is {}",
1006 constraint_object.kind.human_friendly_kind_with_article()
1007 ))));
1008 };
1009
1010 match constraint {
1011 Constraint::Coincident(coincident) => {
1012 let remaining_segments =
1013 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1014
1015 if remaining_segments.len() >= 2 {
1017 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1018 .map_err(KclErrorWithOutputs::no_outputs)?;
1019 } else {
1020 constraint_ids_set.insert(constraint_id);
1021 }
1022 }
1023 Constraint::EqualRadius(equal_radius) => {
1024 let remaining_input = equal_radius
1025 .input
1026 .iter()
1027 .copied()
1028 .filter(|segment_id| {
1029 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1030 })
1031 .collect::<Vec<_>>();
1032
1033 if remaining_input.len() >= 2 {
1034 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1035 .map_err(KclErrorWithOutputs::no_outputs)?;
1036 } else {
1037 constraint_ids_set.insert(constraint_id);
1038 }
1039 }
1040 Constraint::LinesEqualLength(lines_equal_length) => {
1041 let remaining_lines = lines_equal_length
1042 .lines
1043 .iter()
1044 .copied()
1045 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1046 .collect::<Vec<_>>();
1047
1048 if remaining_lines.len() >= 2 {
1050 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1051 .map_err(KclErrorWithOutputs::no_outputs)?;
1052 } else {
1053 constraint_ids_set.insert(constraint_id);
1054 }
1055 }
1056 Constraint::Parallel(parallel) => {
1057 let remaining_lines = parallel
1058 .lines
1059 .iter()
1060 .copied()
1061 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1062 .collect::<Vec<_>>();
1063
1064 if remaining_lines.len() >= 2 {
1065 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1066 .map_err(KclErrorWithOutputs::no_outputs)?;
1067 } else {
1068 constraint_ids_set.insert(constraint_id);
1069 }
1070 }
1071 Constraint::Horizontal(Horizontal::Points { points }) => {
1072 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1073
1074 if remaining_points.len() >= 2 {
1075 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1076 .map_err(KclErrorWithOutputs::no_outputs)?;
1077 } else {
1078 constraint_ids_set.insert(constraint_id);
1079 }
1080 }
1081 Constraint::Vertical(Vertical::Points { points }) => {
1082 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1083
1084 if remaining_points.len() >= 2 {
1085 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1086 .map_err(KclErrorWithOutputs::no_outputs)?;
1087 } else {
1088 constraint_ids_set.insert(constraint_id);
1089 }
1090 }
1091 Constraint::Fixed(fixed) => {
1092 if fixed.points.iter().any(|fixed_point| {
1093 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1094 }) {
1095 constraint_ids_set.insert(constraint_id);
1096 }
1097 }
1098 _ => {
1099 constraint_ids_set.insert(constraint_id);
1101 }
1102 }
1103 }
1104
1105 for constraint_id in constraint_ids_set {
1106 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1107 .map_err(KclErrorWithOutputs::no_outputs)?;
1108 }
1109 for segment_id in resolved_segment_ids_to_delete {
1110 self.delete_segment(&mut new_ast, sketch, segment_id)
1111 .map_err(KclErrorWithOutputs::no_outputs)?;
1112 }
1113
1114 self.execute_after_edit(
1115 ctx,
1116 sketch,
1117 sketch_block_ref,
1118 Default::default(),
1119 EditDeleteKind::DeleteNonSketch,
1120 &mut new_ast,
1121 )
1122 .await
1123 }
1124
1125 async fn add_constraint(
1126 &mut self,
1127 ctx: &ExecutorContext,
1128 _version: Version,
1129 sketch: ObjectId,
1130 constraint: Constraint,
1131 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1132 let original_program = self.program.clone();
1136 let original_scene_graph = self.scene_graph.clone();
1137
1138 let mut new_ast = self.program.ast.clone();
1139 let sketch_block_ref = match constraint {
1140 Constraint::Coincident(coincident) => self
1141 .add_coincident(sketch, coincident, &mut new_ast)
1142 .await
1143 .map_err(KclErrorWithOutputs::no_outputs)?,
1144 Constraint::Distance(distance) => self
1145 .add_distance(sketch, distance, &mut new_ast)
1146 .await
1147 .map_err(KclErrorWithOutputs::no_outputs)?,
1148 Constraint::EqualRadius(equal_radius) => self
1149 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1150 .await
1151 .map_err(KclErrorWithOutputs::no_outputs)?,
1152 Constraint::Fixed(fixed) => self
1153 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1154 .await
1155 .map_err(KclErrorWithOutputs::no_outputs)?,
1156 Constraint::HorizontalDistance(distance) => self
1157 .add_horizontal_distance(sketch, distance, &mut new_ast)
1158 .await
1159 .map_err(KclErrorWithOutputs::no_outputs)?,
1160 Constraint::VerticalDistance(distance) => self
1161 .add_vertical_distance(sketch, distance, &mut new_ast)
1162 .await
1163 .map_err(KclErrorWithOutputs::no_outputs)?,
1164 Constraint::Horizontal(horizontal) => self
1165 .add_horizontal(sketch, horizontal, &mut new_ast)
1166 .await
1167 .map_err(KclErrorWithOutputs::no_outputs)?,
1168 Constraint::LinesEqualLength(lines_equal_length) => self
1169 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1170 .await
1171 .map_err(KclErrorWithOutputs::no_outputs)?,
1172 Constraint::Midpoint(midpoint) => self
1173 .add_midpoint(sketch, midpoint, &mut new_ast)
1174 .await
1175 .map_err(KclErrorWithOutputs::no_outputs)?,
1176 Constraint::Parallel(parallel) => self
1177 .add_parallel(sketch, parallel, &mut new_ast)
1178 .await
1179 .map_err(KclErrorWithOutputs::no_outputs)?,
1180 Constraint::Perpendicular(perpendicular) => self
1181 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1182 .await
1183 .map_err(KclErrorWithOutputs::no_outputs)?,
1184 Constraint::Radius(radius) => self
1185 .add_radius(sketch, radius, &mut new_ast)
1186 .await
1187 .map_err(KclErrorWithOutputs::no_outputs)?,
1188 Constraint::Diameter(diameter) => self
1189 .add_diameter(sketch, diameter, &mut new_ast)
1190 .await
1191 .map_err(KclErrorWithOutputs::no_outputs)?,
1192 Constraint::Symmetric(symmetric) => self
1193 .add_symmetric(sketch, symmetric, &mut new_ast)
1194 .await
1195 .map_err(KclErrorWithOutputs::no_outputs)?,
1196 Constraint::Vertical(vertical) => self
1197 .add_vertical(sketch, vertical, &mut new_ast)
1198 .await
1199 .map_err(KclErrorWithOutputs::no_outputs)?,
1200 Constraint::Angle(lines_at_angle) => self
1201 .add_angle(sketch, lines_at_angle, &mut new_ast)
1202 .await
1203 .map_err(KclErrorWithOutputs::no_outputs)?,
1204 Constraint::Tangent(tangent) => self
1205 .add_tangent(sketch, tangent, &mut new_ast)
1206 .await
1207 .map_err(KclErrorWithOutputs::no_outputs)?,
1208 };
1209
1210 let result = self
1211 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1212 .await;
1213
1214 if result.is_err() {
1216 self.program = original_program;
1217 self.scene_graph = original_scene_graph;
1218 }
1219
1220 result
1221 }
1222
1223 async fn chain_segment(
1224 &mut self,
1225 ctx: &ExecutorContext,
1226 version: Version,
1227 sketch: ObjectId,
1228 previous_segment_end_point_id: ObjectId,
1229 segment: SegmentCtor,
1230 _label: Option<String>,
1231 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1232 let SegmentCtor::Line(line_ctor) = segment else {
1236 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1237 "chain_segment currently only supports Line segments, got {}",
1238 segment.human_friendly_kind_with_article(),
1239 ))));
1240 };
1241
1242 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1244
1245 let new_line_id = first_scene_delta
1248 .new_objects
1249 .iter()
1250 .find(|&obj_id| {
1251 let obj = self.scene_graph.objects.get(obj_id.0);
1252 if let Some(obj) = obj {
1253 matches!(
1254 &obj.kind,
1255 ObjectKind::Segment {
1256 segment: Segment::Line(_)
1257 }
1258 )
1259 } else {
1260 false
1261 }
1262 })
1263 .ok_or_else(|| {
1264 KclErrorWithOutputs::no_outputs(KclError::refactor(
1265 "Failed to find new line segment in scene graph".to_string(),
1266 ))
1267 })?;
1268
1269 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1270 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1271 "New line object not found: {new_line_id:?}"
1272 )))
1273 })?;
1274
1275 let ObjectKind::Segment {
1276 segment: new_line_segment,
1277 } = &new_line_obj.kind
1278 else {
1279 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1280 "Object is not a segment: {new_line_obj:?}"
1281 ))));
1282 };
1283
1284 let Segment::Line(new_line) = new_line_segment else {
1285 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1286 "Segment is not a line: {new_line_segment:?}"
1287 ))));
1288 };
1289
1290 let new_line_start_point_id = new_line.start;
1291
1292 let coincident = Coincident {
1294 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1295 };
1296
1297 let (final_src_delta, final_scene_delta) = self
1298 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1299 .await?;
1300
1301 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1304 combined_new_objects.extend(final_scene_delta.new_objects);
1305
1306 let scene_graph_delta = SceneGraphDelta {
1307 new_graph: self.scene_graph_for_ui(),
1308 invalidates_ids: false,
1309 new_objects: combined_new_objects,
1310 exec_outcome: final_scene_delta.exec_outcome,
1311 };
1312
1313 Ok((final_src_delta, scene_graph_delta))
1314 }
1315
1316 async fn edit_constraint(
1317 &mut self,
1318 ctx: &ExecutorContext,
1319 _version: Version,
1320 sketch: ObjectId,
1321 constraint_id: ObjectId,
1322 value_expression: String,
1323 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1324 let sketch_block_ref =
1326 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1327
1328 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1329 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1330 })?;
1331 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1332 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1333 "Object is not a constraint: {constraint_id:?}"
1334 ))));
1335 }
1336
1337 let mut new_ast = self.program.ast.clone();
1338
1339 let (parsed, errors) = Program::parse(&value_expression)
1341 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1342 if !errors.is_empty() {
1343 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1344 "Error parsing value expression: {errors:?}"
1345 ))));
1346 }
1347 let mut parsed = parsed.ok_or_else(|| {
1348 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1349 })?;
1350 if parsed.ast.body.is_empty() {
1351 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1352 "Empty value expression".to_string(),
1353 )));
1354 }
1355 let first = parsed.ast.body.remove(0);
1356 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1357 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1358 "Value expression must be a simple expression".to_string(),
1359 )));
1360 };
1361
1362 let new_value: ast::BinaryPart = expr_stmt
1363 .inner
1364 .expression
1365 .try_into()
1366 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1367
1368 self.mutate_ast(
1369 &mut new_ast,
1370 constraint_id,
1371 AstMutateCommand::EditConstraintValue { value: new_value },
1372 )
1373 .map_err(KclErrorWithOutputs::no_outputs)?;
1374
1375 self.execute_after_edit(
1376 ctx,
1377 sketch,
1378 sketch_block_ref,
1379 Default::default(),
1380 EditDeleteKind::Edit,
1381 &mut new_ast,
1382 )
1383 .await
1384 }
1385
1386 async fn edit_distance_constraint_label_position(
1387 &mut self,
1388 ctx: &ExecutorContext,
1389 _version: Version,
1390 sketch: ObjectId,
1391 constraint_id: ObjectId,
1392 label_position: Point2d<Number>,
1393 anchor_segment_ids: Vec<ObjectId>,
1394 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1395 let sketch_block_ref =
1397 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1398
1399 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1400 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1401 })?;
1402 if !matches!(
1403 &object.kind,
1404 ObjectKind::Constraint {
1405 constraint: Constraint::Distance(_)
1406 | Constraint::HorizontalDistance(_)
1407 | Constraint::VerticalDistance(_)
1408 | Constraint::Radius(_)
1409 | Constraint::Diameter(_),
1410 }
1411 ) {
1412 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1413 "Object does not support labelPosition: {constraint_id:?}"
1414 ))));
1415 }
1416
1417 let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1418 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1419 "Could not convert label position to AST: {err}"
1420 )))
1421 })?;
1422 let mut new_ast = self.program.ast.clone();
1423 self.mutate_ast(
1424 &mut new_ast,
1425 constraint_id,
1426 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1427 )
1428 .map_err(KclErrorWithOutputs::no_outputs)?;
1429
1430 self.execute_after_edit(
1431 ctx,
1432 sketch,
1433 sketch_block_ref,
1434 anchor_segment_ids.into_iter().collect(),
1435 EditDeleteKind::Edit,
1436 &mut new_ast,
1437 )
1438 .await
1439 }
1440
1441 async fn batch_split_segment_operations(
1449 &mut self,
1450 ctx: &ExecutorContext,
1451 _version: Version,
1452 sketch: ObjectId,
1453 edit_segments: Vec<ExistingSegmentCtor>,
1454 add_constraints: Vec<Constraint>,
1455 delete_constraint_ids: Vec<ObjectId>,
1456 _new_segment_info: sketch::NewSegmentInfo,
1457 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1458 let sketch_block_ref =
1460 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1461
1462 let mut new_ast = self.program.ast.clone();
1463 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1464
1465 for segment in edit_segments {
1467 segment_ids_edited.insert(segment.id);
1468 match segment.ctor {
1469 SegmentCtor::Point(ctor) => self
1470 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1471 .map_err(KclErrorWithOutputs::no_outputs)?,
1472 SegmentCtor::Line(ctor) => self
1473 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1474 .map_err(KclErrorWithOutputs::no_outputs)?,
1475 SegmentCtor::Arc(ctor) => self
1476 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1477 .map_err(KclErrorWithOutputs::no_outputs)?,
1478 SegmentCtor::Circle(ctor) => self
1479 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1480 .map_err(KclErrorWithOutputs::no_outputs)?,
1481 SegmentCtor::ControlPointSpline(ctor) => self
1482 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1483 .map_err(KclErrorWithOutputs::no_outputs)?,
1484 }
1485 }
1486
1487 for constraint in add_constraints {
1489 match constraint {
1490 Constraint::Coincident(coincident) => {
1491 self.add_coincident(sketch, coincident, &mut new_ast)
1492 .await
1493 .map_err(KclErrorWithOutputs::no_outputs)?;
1494 }
1495 Constraint::Distance(distance) => {
1496 self.add_distance(sketch, distance, &mut new_ast)
1497 .await
1498 .map_err(KclErrorWithOutputs::no_outputs)?;
1499 }
1500 Constraint::EqualRadius(equal_radius) => {
1501 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1502 .await
1503 .map_err(KclErrorWithOutputs::no_outputs)?;
1504 }
1505 Constraint::Fixed(fixed) => {
1506 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1507 .await
1508 .map_err(KclErrorWithOutputs::no_outputs)?;
1509 }
1510 Constraint::HorizontalDistance(distance) => {
1511 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1512 .await
1513 .map_err(KclErrorWithOutputs::no_outputs)?;
1514 }
1515 Constraint::VerticalDistance(distance) => {
1516 self.add_vertical_distance(sketch, distance, &mut new_ast)
1517 .await
1518 .map_err(KclErrorWithOutputs::no_outputs)?;
1519 }
1520 Constraint::Horizontal(horizontal) => {
1521 self.add_horizontal(sketch, horizontal, &mut new_ast)
1522 .await
1523 .map_err(KclErrorWithOutputs::no_outputs)?;
1524 }
1525 Constraint::LinesEqualLength(lines_equal_length) => {
1526 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1527 .await
1528 .map_err(KclErrorWithOutputs::no_outputs)?;
1529 }
1530 Constraint::Midpoint(midpoint) => {
1531 self.add_midpoint(sketch, midpoint, &mut new_ast)
1532 .await
1533 .map_err(KclErrorWithOutputs::no_outputs)?;
1534 }
1535 Constraint::Parallel(parallel) => {
1536 self.add_parallel(sketch, parallel, &mut new_ast)
1537 .await
1538 .map_err(KclErrorWithOutputs::no_outputs)?;
1539 }
1540 Constraint::Perpendicular(perpendicular) => {
1541 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1542 .await
1543 .map_err(KclErrorWithOutputs::no_outputs)?;
1544 }
1545 Constraint::Vertical(vertical) => {
1546 self.add_vertical(sketch, vertical, &mut new_ast)
1547 .await
1548 .map_err(KclErrorWithOutputs::no_outputs)?;
1549 }
1550 Constraint::Diameter(diameter) => {
1551 self.add_diameter(sketch, diameter, &mut new_ast)
1552 .await
1553 .map_err(KclErrorWithOutputs::no_outputs)?;
1554 }
1555 Constraint::Radius(radius) => {
1556 self.add_radius(sketch, radius, &mut new_ast)
1557 .await
1558 .map_err(KclErrorWithOutputs::no_outputs)?;
1559 }
1560 Constraint::Symmetric(symmetric) => {
1561 self.add_symmetric(sketch, symmetric, &mut new_ast)
1562 .await
1563 .map_err(KclErrorWithOutputs::no_outputs)?;
1564 }
1565 Constraint::Angle(angle) => {
1566 self.add_angle(sketch, angle, &mut new_ast)
1567 .await
1568 .map_err(KclErrorWithOutputs::no_outputs)?;
1569 }
1570 Constraint::Tangent(tangent) => {
1571 self.add_tangent(sketch, tangent, &mut new_ast)
1572 .await
1573 .map_err(KclErrorWithOutputs::no_outputs)?;
1574 }
1575 }
1576 }
1577
1578 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1580
1581 let has_constraint_deletions = !constraint_ids_set.is_empty();
1582 for constraint_id in constraint_ids_set {
1583 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1584 .map_err(KclErrorWithOutputs::no_outputs)?;
1585 }
1586
1587 let (source_delta, mut scene_graph_delta) = self
1591 .execute_after_edit(
1592 ctx,
1593 sketch,
1594 sketch_block_ref,
1595 segment_ids_edited,
1596 EditDeleteKind::Edit,
1597 &mut new_ast,
1598 )
1599 .await?;
1600
1601 if has_constraint_deletions {
1604 scene_graph_delta.invalidates_ids = true;
1605 }
1606
1607 Ok((source_delta, scene_graph_delta))
1608 }
1609
1610 async fn batch_tail_cut_operations(
1611 &mut self,
1612 ctx: &ExecutorContext,
1613 _version: Version,
1614 sketch: ObjectId,
1615 edit_segments: Vec<ExistingSegmentCtor>,
1616 add_constraints: Vec<Constraint>,
1617 delete_constraint_ids: Vec<ObjectId>,
1618 additional_edited_segment_ids: Vec<ObjectId>,
1619 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1620 let sketch_block_ref =
1621 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1622
1623 let mut new_ast = self.program.ast.clone();
1624 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1625
1626 for segment in edit_segments {
1628 segment_ids_edited.insert(segment.id);
1629 match segment.ctor {
1630 SegmentCtor::Point(ctor) => self
1631 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1632 .map_err(KclErrorWithOutputs::no_outputs)?,
1633 SegmentCtor::Line(ctor) => self
1634 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1635 .map_err(KclErrorWithOutputs::no_outputs)?,
1636 SegmentCtor::Arc(ctor) => self
1637 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1638 .map_err(KclErrorWithOutputs::no_outputs)?,
1639 SegmentCtor::Circle(ctor) => self
1640 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1641 .map_err(KclErrorWithOutputs::no_outputs)?,
1642 SegmentCtor::ControlPointSpline(ctor) => self
1643 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1644 .map_err(KclErrorWithOutputs::no_outputs)?,
1645 }
1646 }
1647
1648 segment_ids_edited.extend(additional_edited_segment_ids);
1649
1650 for constraint in add_constraints {
1652 match constraint {
1653 Constraint::Coincident(coincident) => {
1654 self.add_coincident(sketch, coincident, &mut new_ast)
1655 .await
1656 .map_err(KclErrorWithOutputs::no_outputs)?;
1657 }
1658 other => {
1659 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1660 "unsupported constraint in tail cut batch: {other:?}"
1661 ))));
1662 }
1663 }
1664 }
1665
1666 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1668
1669 let has_constraint_deletions = !constraint_ids_set.is_empty();
1670 for constraint_id in constraint_ids_set {
1671 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1672 .map_err(KclErrorWithOutputs::no_outputs)?;
1673 }
1674
1675 let (source_delta, mut scene_graph_delta) = self
1679 .execute_after_edit(
1680 ctx,
1681 sketch,
1682 sketch_block_ref,
1683 segment_ids_edited,
1684 EditDeleteKind::Edit,
1685 &mut new_ast,
1686 )
1687 .await?;
1688
1689 if has_constraint_deletions {
1692 scene_graph_delta.invalidates_ids = true;
1693 }
1694
1695 Ok((source_delta, scene_graph_delta))
1696 }
1697}
1698
1699impl FrontendState {
1700 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1701 self.program = program.clone();
1702
1703 self.point_freedom_cache.clear();
1714 match ctx.run_with_caching(program).await {
1715 Ok(outcome) => {
1716 let outcome = self.update_state_after_exec(outcome, true);
1717 let checkpoint_id = self
1718 .create_sketch_checkpoint(outcome.clone())
1719 .await
1720 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1721 Ok(SetProgramOutcome::Success {
1722 scene_graph: Box::new(self.scene_graph_for_ui()),
1723 exec_outcome: Box::new(outcome),
1724 checkpoint_id: Some(checkpoint_id),
1725 })
1726 }
1727 Err(mut err) => {
1728 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1731 self.update_state_after_exec(outcome, true);
1732 err.scene_graph = Some(self.scene_graph_for_ui());
1733 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1734 }
1735 }
1736 }
1737
1738 pub async fn engine_execute(
1741 &mut self,
1742 ctx: &ExecutorContext,
1743 program: Program,
1744 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1745 self.program = program.clone();
1746
1747 self.point_freedom_cache.clear();
1751 match ctx.run_with_caching(program).await {
1752 Ok(outcome) => {
1753 let outcome = self.update_state_after_exec(outcome, true);
1754 Ok(SceneGraphDelta {
1755 new_graph: self.scene_graph_for_ui(),
1756 exec_outcome: outcome,
1757 new_objects: Default::default(),
1759 invalidates_ids: Default::default(),
1761 })
1762 }
1763 Err(mut err) => {
1764 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1766 self.update_state_after_exec(outcome, true);
1767 err.scene_graph = Some(self.scene_graph_for_ui());
1768 Err(err)
1769 }
1770 }
1771 }
1772
1773 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1774 if matches!(err.error, KclError::EngineHangup { .. }) {
1775 return Err(err);
1779 }
1780
1781 let KclErrorWithOutputs {
1782 error,
1783 mut non_fatal,
1784 variables,
1785 operations,
1786 artifact_graph,
1787 scene_objects,
1788 source_range_to_object,
1789 var_solutions,
1790 filenames,
1791 default_planes,
1792 ..
1793 } = err;
1794
1795 if let Some(source_range) = error.source_ranges().first() {
1796 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1797 } else {
1798 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1799 }
1800
1801 Ok(ExecOutcome {
1802 variables,
1803 filenames,
1804 operations,
1805 artifact_graph,
1806 scene_objects,
1807 source_range_to_object,
1808 var_solutions,
1809 issues: non_fatal,
1810 default_planes,
1811 })
1812 }
1813
1814 async fn add_point(
1815 &mut self,
1816 ctx: &ExecutorContext,
1817 sketch: ObjectId,
1818 ctor: PointCtor,
1819 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1820 let at_ast = to_ast_point2d(&ctor.position)
1822 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1823 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1824 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1825 unlabeled: None,
1826 arguments: vec![ast::LabeledArg {
1827 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1828 arg: at_ast,
1829 }],
1830 digest: None,
1831 non_code_meta: Default::default(),
1832 })));
1833
1834 let sketch_id = sketch;
1836 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1837 #[cfg(target_arch = "wasm32")]
1838 web_sys::console::error_1(
1839 &format!(
1840 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1841 &self.scene_graph.objects
1842 )
1843 .into(),
1844 );
1845 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1846 })?;
1847 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1848 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1849 "Object is not a sketch, it is {}",
1850 sketch_object.kind.human_friendly_kind_with_article(),
1851 ))));
1852 };
1853 let mut new_ast = self.program.ast.clone();
1855 let (sketch_block_ref, _) = self
1856 .mutate_ast(
1857 &mut new_ast,
1858 sketch_id,
1859 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1860 )
1861 .map_err(KclErrorWithOutputs::no_outputs)?;
1862 let new_source = source_from_ast(&new_ast);
1864 let (new_program, errors) = Program::parse(&new_source)
1866 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1867 if !errors.is_empty() {
1868 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1869 "Error parsing KCL source after adding point: {errors:?}"
1870 ))));
1871 }
1872 let Some(new_program) = new_program else {
1873 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1874 "No AST produced after adding point".to_string(),
1875 )));
1876 };
1877
1878 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1879 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1880 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1881 )))
1882 })?;
1883
1884 self.program = new_program.clone();
1886
1887 let mut truncated_program = new_program;
1889 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1890 .map_err(KclErrorWithOutputs::no_outputs)?;
1891
1892 let outcome = ctx
1894 .run_mock(
1895 &truncated_program,
1896 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1897 )
1898 .await?;
1899
1900 let new_object_ids = {
1901 let make_err =
1902 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1903 let segment_id = outcome
1904 .source_range_to_object
1905 .get(&point_node_ref.range)
1906 .copied()
1907 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1908 let segment_object = outcome
1909 .scene_objects
1910 .get(segment_id.0)
1911 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1912 let ObjectKind::Segment { segment } = &segment_object.kind else {
1913 return Err(make_err(format!(
1914 "Object is not a segment, it is {}",
1915 segment_object.kind.human_friendly_kind_with_article()
1916 )));
1917 };
1918 let Segment::Point(_) = segment else {
1919 return Err(make_err(format!(
1920 "Segment is not a point, it is {}",
1921 segment.human_friendly_kind_with_article()
1922 )));
1923 };
1924 vec![segment_id]
1925 };
1926 let src_delta = SourceDelta { text: new_source };
1927 let outcome = self.update_state_after_exec(outcome, false);
1929 let scene_graph_delta = SceneGraphDelta {
1930 new_graph: self.scene_graph_for_ui(),
1931 invalidates_ids: false,
1932 new_objects: new_object_ids,
1933 exec_outcome: outcome,
1934 };
1935 Ok((src_delta, scene_graph_delta))
1936 }
1937
1938 async fn add_line(
1939 &mut self,
1940 ctx: &ExecutorContext,
1941 sketch: ObjectId,
1942 ctor: LineCtor,
1943 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1944 let start_ast = to_ast_point2d(&ctor.start)
1946 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1947 let end_ast = to_ast_point2d(&ctor.end)
1948 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1949 let mut arguments = vec![
1950 ast::LabeledArg {
1951 label: Some(ast::Identifier::new(LINE_START_PARAM)),
1952 arg: start_ast,
1953 },
1954 ast::LabeledArg {
1955 label: Some(ast::Identifier::new(LINE_END_PARAM)),
1956 arg: end_ast,
1957 },
1958 ];
1959 if ctor.construction == Some(true) {
1961 arguments.push(ast::LabeledArg {
1962 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1963 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1964 value: ast::LiteralValue::Bool(true),
1965 raw: "true".to_string(),
1966 digest: None,
1967 }))),
1968 });
1969 }
1970 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1971 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1972 unlabeled: None,
1973 arguments,
1974 digest: None,
1975 non_code_meta: Default::default(),
1976 })));
1977
1978 let sketch_id = sketch;
1980 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1981 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1982 })?;
1983 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1984 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1985 "Object is not a sketch, it is {}",
1986 sketch_object.kind.human_friendly_kind_with_article(),
1987 ))));
1988 };
1989 let mut new_ast = self.program.ast.clone();
1991 let (sketch_block_ref, _) = self
1992 .mutate_ast(
1993 &mut new_ast,
1994 sketch_id,
1995 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1996 )
1997 .map_err(KclErrorWithOutputs::no_outputs)?;
1998 let new_source = source_from_ast(&new_ast);
2000 let (new_program, errors) = Program::parse(&new_source)
2002 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2003 if !errors.is_empty() {
2004 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2005 "Error parsing KCL source after adding line: {errors:?}"
2006 ))));
2007 }
2008 let Some(new_program) = new_program else {
2009 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2010 "No AST produced after adding line".to_string(),
2011 )));
2012 };
2013
2014 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2015 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2016 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2017 )))
2018 })?;
2019
2020 self.program = new_program.clone();
2022
2023 let mut truncated_program = new_program;
2025 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2026 .map_err(KclErrorWithOutputs::no_outputs)?;
2027
2028 let outcome = ctx
2030 .run_mock(
2031 &truncated_program,
2032 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2033 )
2034 .await?;
2035
2036 let new_object_ids = {
2037 let make_err =
2038 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2039 let segment_id = outcome
2040 .source_range_to_object
2041 .get(&line_node_ref.range)
2042 .copied()
2043 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2044 let segment_object = outcome
2045 .scene_object_by_id(segment_id)
2046 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2047 let ObjectKind::Segment { segment } = &segment_object.kind else {
2048 return Err(make_err(format!(
2049 "Object is not a segment, it is {}",
2050 segment_object.kind.human_friendly_kind_with_article()
2051 )));
2052 };
2053 let Segment::Line(line) = segment else {
2054 return Err(make_err(format!(
2055 "Segment is not a line, it is {}",
2056 segment.human_friendly_kind_with_article()
2057 )));
2058 };
2059 vec![line.start, line.end, segment_id]
2060 };
2061 let src_delta = SourceDelta { text: new_source };
2062 let outcome = self.update_state_after_exec(outcome, false);
2064 let scene_graph_delta = SceneGraphDelta {
2065 new_graph: self.scene_graph_for_ui(),
2066 invalidates_ids: false,
2067 new_objects: new_object_ids,
2068 exec_outcome: outcome,
2069 };
2070 Ok((src_delta, scene_graph_delta))
2071 }
2072
2073 async fn add_arc(
2074 &mut self,
2075 ctx: &ExecutorContext,
2076 sketch: ObjectId,
2077 ctor: ArcCtor,
2078 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2079 let start_ast = to_ast_point2d(&ctor.start)
2081 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2082 let end_ast = to_ast_point2d(&ctor.end)
2083 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2084 let center_ast = to_ast_point2d(&ctor.center)
2085 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2086 let mut arguments = vec![
2087 ast::LabeledArg {
2088 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2089 arg: start_ast,
2090 },
2091 ast::LabeledArg {
2092 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2093 arg: end_ast,
2094 },
2095 ast::LabeledArg {
2096 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2097 arg: center_ast,
2098 },
2099 ];
2100 if ctor.construction == Some(true) {
2102 arguments.push(ast::LabeledArg {
2103 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2104 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2105 value: ast::LiteralValue::Bool(true),
2106 raw: "true".to_string(),
2107 digest: None,
2108 }))),
2109 });
2110 }
2111 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2112 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2113 unlabeled: None,
2114 arguments,
2115 digest: None,
2116 non_code_meta: Default::default(),
2117 })));
2118
2119 let sketch_id = sketch;
2121 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2122 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2123 })?;
2124 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2125 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2126 "Object is not a sketch, it is {}",
2127 sketch_object.kind.human_friendly_kind_with_article(),
2128 ))));
2129 };
2130 let mut new_ast = self.program.ast.clone();
2132 let (sketch_block_ref, _) = self
2133 .mutate_ast(
2134 &mut new_ast,
2135 sketch_id,
2136 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2137 )
2138 .map_err(KclErrorWithOutputs::no_outputs)?;
2139 let new_source = source_from_ast(&new_ast);
2141 let (new_program, errors) = Program::parse(&new_source)
2143 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2144 if !errors.is_empty() {
2145 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2146 "Error parsing KCL source after adding arc: {errors:?}"
2147 ))));
2148 }
2149 let Some(new_program) = new_program else {
2150 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2151 "No AST produced after adding arc".to_string(),
2152 )));
2153 };
2154
2155 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2156 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2157 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2158 )))
2159 })?;
2160
2161 self.program = new_program.clone();
2163
2164 let mut truncated_program = new_program;
2166 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2167 .map_err(KclErrorWithOutputs::no_outputs)?;
2168
2169 let outcome = ctx
2171 .run_mock(
2172 &truncated_program,
2173 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2174 )
2175 .await?;
2176
2177 let new_object_ids = {
2178 let make_err =
2179 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2180 let segment_id = outcome
2181 .source_range_to_object
2182 .get(&arc_node_ref.range)
2183 .copied()
2184 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2185 let segment_object = outcome
2186 .scene_objects
2187 .get(segment_id.0)
2188 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2189 let ObjectKind::Segment { segment } = &segment_object.kind else {
2190 return Err(make_err(format!(
2191 "Object is not a segment, it is {}",
2192 segment_object.kind.human_friendly_kind_with_article()
2193 )));
2194 };
2195 let Segment::Arc(arc) = segment else {
2196 return Err(make_err(format!(
2197 "Segment is not an arc, it is {}",
2198 segment.human_friendly_kind_with_article()
2199 )));
2200 };
2201 vec![arc.start, arc.end, arc.center, segment_id]
2202 };
2203 let src_delta = SourceDelta { text: new_source };
2204 let outcome = self.update_state_after_exec(outcome, false);
2206 let scene_graph_delta = SceneGraphDelta {
2207 new_graph: self.scene_graph_for_ui(),
2208 invalidates_ids: false,
2209 new_objects: new_object_ids,
2210 exec_outcome: outcome,
2211 };
2212 Ok((src_delta, scene_graph_delta))
2213 }
2214
2215 async fn add_circle(
2216 &mut self,
2217 ctx: &ExecutorContext,
2218 sketch: ObjectId,
2219 ctor: CircleCtor,
2220 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2221 let start_ast = to_ast_point2d(&ctor.start)
2223 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2224 let center_ast = to_ast_point2d(&ctor.center)
2225 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2226 let mut arguments = vec![
2227 ast::LabeledArg {
2228 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2229 arg: start_ast,
2230 },
2231 ast::LabeledArg {
2232 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2233 arg: center_ast,
2234 },
2235 ];
2236 if ctor.construction == Some(true) {
2238 arguments.push(ast::LabeledArg {
2239 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2240 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2241 value: ast::LiteralValue::Bool(true),
2242 raw: "true".to_string(),
2243 digest: None,
2244 }))),
2245 });
2246 }
2247 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2248 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2249 unlabeled: None,
2250 arguments,
2251 digest: None,
2252 non_code_meta: Default::default(),
2253 })));
2254
2255 let sketch_id = sketch;
2257 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2258 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2259 })?;
2260 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2261 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2262 "Object is not a sketch, it is {}",
2263 sketch_object.kind.human_friendly_kind_with_article(),
2264 ))));
2265 };
2266 let mut new_ast = self.program.ast.clone();
2268 let (sketch_block_ref, _) = self
2269 .mutate_ast(
2270 &mut new_ast,
2271 sketch_id,
2272 AstMutateCommand::AddSketchBlockVarDecl {
2273 prefix: CIRCLE_VARIABLE.to_owned(),
2274 expr: circle_ast,
2275 },
2276 )
2277 .map_err(KclErrorWithOutputs::no_outputs)?;
2278 let new_source = source_from_ast(&new_ast);
2280 let (new_program, errors) = Program::parse(&new_source)
2282 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2283 if !errors.is_empty() {
2284 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2285 "Error parsing KCL source after adding circle: {errors:?}"
2286 ))));
2287 }
2288 let Some(new_program) = new_program else {
2289 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2290 "No AST produced after adding circle".to_string(),
2291 )));
2292 };
2293
2294 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2295 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2296 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2297 )))
2298 })?;
2299
2300 self.program = new_program.clone();
2302
2303 let mut truncated_program = new_program;
2305 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2306 .map_err(KclErrorWithOutputs::no_outputs)?;
2307
2308 let outcome = ctx
2310 .run_mock(
2311 &truncated_program,
2312 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2313 )
2314 .await?;
2315
2316 let new_object_ids = {
2317 let make_err =
2318 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2319 let segment_id = outcome
2320 .source_range_to_object
2321 .get(&circle_node_ref.range)
2322 .copied()
2323 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2324 let segment_object = outcome
2325 .scene_objects
2326 .get(segment_id.0)
2327 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2328 let ObjectKind::Segment { segment } = &segment_object.kind else {
2329 return Err(make_err(format!(
2330 "Object is not a segment, it is {}",
2331 segment_object.kind.human_friendly_kind_with_article()
2332 )));
2333 };
2334 let Segment::Circle(circle) = segment else {
2335 return Err(make_err(format!(
2336 "Segment is not a circle, it is {}",
2337 segment.human_friendly_kind_with_article()
2338 )));
2339 };
2340 vec![circle.start, circle.center, segment_id]
2341 };
2342 let src_delta = SourceDelta { text: new_source };
2343 let outcome = self.update_state_after_exec(outcome, false);
2345 let scene_graph_delta = SceneGraphDelta {
2346 new_graph: self.scene_graph_for_ui(),
2347 invalidates_ids: false,
2348 new_objects: new_object_ids,
2349 exec_outcome: outcome,
2350 };
2351 Ok((src_delta, scene_graph_delta))
2352 }
2353
2354 async fn add_control_point_spline(
2355 &mut self,
2356 ctx: &ExecutorContext,
2357 sketch: ObjectId,
2358 ctor: ControlPointSplineCtor,
2359 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2360 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2361 .map_err(KclErrorWithOutputs::no_outputs)?;
2362
2363 let points_ast = to_ast_point2d_array(&ctor.points)
2364 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2365 let mut arguments = vec![ast::LabeledArg {
2366 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2367 arg: points_ast,
2368 }];
2369 if ctor.construction == Some(true) {
2370 arguments.push(ast::LabeledArg {
2371 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2372 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2373 value: ast::LiteralValue::Bool(true),
2374 raw: "true".to_string(),
2375 digest: None,
2376 }))),
2377 });
2378 }
2379 let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2380 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2381 unlabeled: None,
2382 arguments,
2383 digest: None,
2384 non_code_meta: Default::default(),
2385 })));
2386
2387 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2388 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2389 })?;
2390 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2391 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2392 "Object is not a sketch, it is {}",
2393 sketch_object.kind.human_friendly_kind_with_article(),
2394 ))));
2395 };
2396
2397 let mut new_ast = new_program.ast.clone();
2398 let (sketch_block_ref, _) = self
2399 .mutate_ast(
2400 &mut new_ast,
2401 sketch,
2402 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2403 )
2404 .map_err(KclErrorWithOutputs::no_outputs)?;
2405 let new_source = source_from_ast(&new_ast);
2406 let (new_program, errors) = Program::parse(&new_source)
2407 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2408 if !errors.is_empty() {
2409 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2410 "Error parsing KCL source after adding controlPointSpline: {errors:?}"
2411 ))));
2412 }
2413 let Some(new_program) = new_program else {
2414 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2415 "No AST produced after adding controlPointSpline".to_string(),
2416 )));
2417 };
2418
2419 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2420 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2421 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2422 )))
2423 })?;
2424 #[cfg(not(feature = "artifact-graph"))]
2425 let _ = spline_node_ref;
2426
2427 self.program = new_program.clone();
2428
2429 let mut truncated_program = new_program;
2430 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2431 .map_err(KclErrorWithOutputs::no_outputs)?;
2432
2433 let outcome = ctx
2434 .run_mock(
2435 &truncated_program,
2436 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2437 )
2438 .await?;
2439
2440 #[cfg(not(feature = "artifact-graph"))]
2441 let new_object_ids = Vec::new();
2442 #[cfg(feature = "artifact-graph")]
2443 let new_object_ids = {
2444 let make_err =
2445 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2446 let segment_id = outcome
2447 .source_range_to_object
2448 .get(&spline_node_ref.range)
2449 .copied()
2450 .ok_or_else(|| {
2451 make_err(format!(
2452 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2453 ))
2454 })?;
2455 let segment_object = outcome
2456 .scene_objects
2457 .get(segment_id.0)
2458 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2459 let ObjectKind::Segment { segment } = &segment_object.kind else {
2460 return Err(make_err(format!(
2461 "Object is not a segment, it is {}",
2462 segment_object.kind.human_friendly_kind_with_article()
2463 )));
2464 };
2465 let Segment::ControlPointSpline(spline) = segment else {
2466 return Err(make_err(format!(
2467 "Segment is not a control point spline, it is {}",
2468 segment.human_friendly_kind_with_article()
2469 )));
2470 };
2471
2472 let mut ids = outcome
2473 .scene_objects
2474 .iter()
2475 .filter_map(|obj| match &obj.kind {
2476 ObjectKind::Segment {
2477 segment: Segment::Line(line),
2478 } if line.owner == Some(segment_id) => Some(obj.id),
2479 _ => None,
2480 })
2481 .collect::<Vec<_>>();
2482 ids.extend(spline.controls.clone());
2483 ids.push(segment_id);
2484 ids
2485 };
2486 let src_delta = SourceDelta { text: new_source };
2487 let outcome = self.update_state_after_exec(outcome, false);
2488 let scene_graph_delta = SceneGraphDelta {
2489 new_graph: self.scene_graph_for_ui(),
2490 invalidates_ids: false,
2491 new_objects: new_object_ids,
2492 exec_outcome: outcome,
2493 };
2494 Ok((src_delta, scene_graph_delta))
2495 }
2496
2497 fn edit_point(
2498 &mut self,
2499 new_ast: &mut ast::Node<ast::Program>,
2500 sketch: ObjectId,
2501 point: ObjectId,
2502 ctor: PointCtor,
2503 ) -> Result<(), KclError> {
2504 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2506
2507 let sketch_id = sketch;
2509 let sketch_object = self
2510 .scene_graph
2511 .objects
2512 .get(sketch_id.0)
2513 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2514 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2515 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2516 };
2517 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2518 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2519 })?;
2520 let point_id = point;
2522 let point_object = self
2523 .scene_graph
2524 .objects
2525 .get(point_id.0)
2526 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2527 let ObjectKind::Segment {
2528 segment: Segment::Point(point),
2529 } = &point_object.kind
2530 else {
2531 return Err(KclError::refactor(format!(
2532 "Object is not a point segment: {point_object:?}"
2533 )));
2534 };
2535
2536 if let Some(owner_id) = point.owner {
2538 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2539 KclError::refactor(format!(
2540 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2541 ))
2542 })?;
2543 let ObjectKind::Segment { segment } = &owner_object.kind else {
2544 return Err(KclError::refactor(format!(
2545 "Internal: Owner of point is not a segment, but found {}",
2546 owner_object.kind.human_friendly_kind_with_article()
2547 )));
2548 };
2549
2550 if let Segment::Line(line) = segment {
2552 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2553 return Err(KclError::refactor(format!(
2554 "Internal: Owner of point does not have line ctor, but found {}",
2555 line.ctor.human_friendly_kind_with_article()
2556 )));
2557 };
2558 let mut line_ctor = line_ctor.clone();
2559 if line.start == point_id {
2561 line_ctor.start = ctor.position;
2562 } else if line.end == point_id {
2563 line_ctor.end = ctor.position;
2564 } else {
2565 return Err(KclError::refactor(format!(
2566 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2567 )));
2568 }
2569 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2570 }
2571
2572 if let Segment::Arc(arc) = segment {
2574 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2575 return Err(KclError::refactor(format!(
2576 "Internal: Owner of point does not have arc ctor, but found {}",
2577 arc.ctor.human_friendly_kind_with_article()
2578 )));
2579 };
2580 let mut arc_ctor = arc_ctor.clone();
2581 if arc.center == point_id {
2583 arc_ctor.center = ctor.position;
2584 } else if arc.start == point_id {
2585 arc_ctor.start = ctor.position;
2586 } else if arc.end == point_id {
2587 arc_ctor.end = ctor.position;
2588 } else {
2589 return Err(KclError::refactor(format!(
2590 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2591 )));
2592 }
2593 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2594 }
2595
2596 if let Segment::Circle(circle) = segment {
2598 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2599 return Err(KclError::refactor(format!(
2600 "Internal: Owner of point does not have circle ctor, but found {}",
2601 circle.ctor.human_friendly_kind_with_article()
2602 )));
2603 };
2604 let mut circle_ctor = circle_ctor.clone();
2605 if circle.center == point_id {
2606 circle_ctor.center = ctor.position;
2607 } else if circle.start == point_id {
2608 circle_ctor.start = ctor.position;
2609 } else {
2610 return Err(KclError::refactor(format!(
2611 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2612 )));
2613 }
2614 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2615 }
2616
2617 if let Segment::ControlPointSpline(spline) = segment {
2618 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2619 return Err(KclError::refactor(format!(
2620 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2621 spline.ctor.human_friendly_kind_with_article()
2622 )));
2623 };
2624 let mut spline_ctor = spline_ctor.clone();
2625 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2626 return Err(KclError::refactor(format!(
2627 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2628 )));
2629 };
2630 spline_ctor.points[control_index] = ctor.position;
2631 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2632 }
2633
2634 }
2637
2638 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2640 Ok(())
2641 }
2642
2643 fn edit_line(
2644 &mut self,
2645 new_ast: &mut ast::Node<ast::Program>,
2646 sketch: ObjectId,
2647 line: ObjectId,
2648 ctor: LineCtor,
2649 ) -> Result<(), KclError> {
2650 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2652 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2653
2654 let sketch_id = sketch;
2656 let sketch_object = self
2657 .scene_graph
2658 .objects
2659 .get(sketch_id.0)
2660 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2661 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2662 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2663 };
2664 sketch
2665 .segments
2666 .iter()
2667 .find(|o| **o == line)
2668 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2669 let line_id = line;
2671 let line_object = self
2672 .scene_graph
2673 .objects
2674 .get(line_id.0)
2675 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2676 let ObjectKind::Segment { .. } = &line_object.kind else {
2677 let kind = line_object.kind.human_friendly_kind_with_article();
2678 return Err(KclError::refactor(format!(
2679 "This constraint only works on Segments, but you selected {kind}"
2680 )));
2681 };
2682
2683 self.mutate_ast(
2685 new_ast,
2686 line_id,
2687 AstMutateCommand::EditLine {
2688 start: new_start_ast,
2689 end: new_end_ast,
2690 construction: ctor.construction,
2691 },
2692 )?;
2693 Ok(())
2694 }
2695
2696 fn edit_arc(
2697 &mut self,
2698 new_ast: &mut ast::Node<ast::Program>,
2699 sketch: ObjectId,
2700 arc: ObjectId,
2701 ctor: ArcCtor,
2702 ) -> Result<(), KclError> {
2703 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2705 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2706 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2707
2708 let sketch_id = sketch;
2710 let sketch_object = self
2711 .scene_graph
2712 .objects
2713 .get(sketch_id.0)
2714 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2715 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2716 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2717 };
2718 sketch
2719 .segments
2720 .iter()
2721 .find(|o| **o == arc)
2722 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2723 let arc_id = arc;
2725 let arc_object = self
2726 .scene_graph
2727 .objects
2728 .get(arc_id.0)
2729 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2730 let ObjectKind::Segment { .. } = &arc_object.kind else {
2731 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2732 };
2733
2734 self.mutate_ast(
2736 new_ast,
2737 arc_id,
2738 AstMutateCommand::EditArc {
2739 start: new_start_ast,
2740 end: new_end_ast,
2741 center: new_center_ast,
2742 construction: ctor.construction,
2743 },
2744 )?;
2745 Ok(())
2746 }
2747
2748 fn edit_circle(
2749 &mut self,
2750 new_ast: &mut ast::Node<ast::Program>,
2751 sketch: ObjectId,
2752 circle: ObjectId,
2753 ctor: CircleCtor,
2754 ) -> Result<(), KclError> {
2755 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2757 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2758
2759 let sketch_id = sketch;
2761 let sketch_object = self
2762 .scene_graph
2763 .objects
2764 .get(sketch_id.0)
2765 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2766 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2767 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2768 };
2769 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2770 KclError::refactor(format!(
2771 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2772 ))
2773 })?;
2774 let circle_id = circle;
2776 let circle_object = self
2777 .scene_graph
2778 .objects
2779 .get(circle_id.0)
2780 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2781 let ObjectKind::Segment { .. } = &circle_object.kind else {
2782 return Err(KclError::refactor(format!(
2783 "Object is not a segment: {circle_object:?}"
2784 )));
2785 };
2786
2787 self.mutate_ast(
2789 new_ast,
2790 circle_id,
2791 AstMutateCommand::EditCircle {
2792 start: new_start_ast,
2793 center: new_center_ast,
2794 construction: ctor.construction,
2795 },
2796 )?;
2797 Ok(())
2798 }
2799
2800 fn edit_control_point_spline(
2801 &mut self,
2802 new_ast: &mut ast::Node<ast::Program>,
2803 sketch: ObjectId,
2804 spline: ObjectId,
2805 ctor: ControlPointSplineCtor,
2806 ) -> Result<(), KclError> {
2807 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2808
2809 let sketch_object = self
2810 .scene_graph
2811 .objects
2812 .get(sketch.0)
2813 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2814 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2815 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2816 };
2817 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2818 KclError::refactor(format!(
2819 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2820 ))
2821 })?;
2822
2823 let spline_object =
2824 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2825 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2826 })?;
2827 let ObjectKind::Segment { .. } = &spline_object.kind else {
2828 return Err(KclError::refactor(format!(
2829 "Object is not a segment: {spline_object:?}"
2830 )));
2831 };
2832
2833 self.mutate_ast(
2834 new_ast,
2835 spline,
2836 AstMutateCommand::EditControlPointSpline {
2837 points: points_ast,
2838 construction: ctor.construction,
2839 },
2840 )?;
2841 Ok(())
2842 }
2843
2844 fn delete_segment(
2845 &mut self,
2846 new_ast: &mut ast::Node<ast::Program>,
2847 sketch: ObjectId,
2848 segment_id: ObjectId,
2849 ) -> Result<(), KclError> {
2850 let sketch_id = sketch;
2852 let sketch_object = self
2853 .scene_graph
2854 .objects
2855 .get(sketch_id.0)
2856 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2857 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2858 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2859 };
2860 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2861 KclError::refactor(format!(
2862 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2863 ))
2864 })?;
2865 let segment_object =
2867 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2868 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2869 })?;
2870 let ObjectKind::Segment { .. } = &segment_object.kind else {
2871 return Err(KclError::refactor(format!(
2872 "Object is not a segment, it is {}",
2873 segment_object.kind.human_friendly_kind_with_article()
2874 )));
2875 };
2876
2877 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2879 Ok(())
2880 }
2881
2882 fn delete_constraint(
2883 &mut self,
2884 new_ast: &mut ast::Node<ast::Program>,
2885 sketch: ObjectId,
2886 constraint_id: ObjectId,
2887 ) -> Result<(), KclError> {
2888 let sketch_id = sketch;
2890 let sketch_object = self
2891 .scene_graph
2892 .objects
2893 .get(sketch_id.0)
2894 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2895 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2896 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2897 };
2898 sketch
2899 .constraints
2900 .iter()
2901 .find(|o| **o == constraint_id)
2902 .ok_or_else(|| {
2903 KclError::refactor(format!(
2904 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2905 ))
2906 })?;
2907 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2909 KclError::refactor(format!(
2910 "Constraint not found in scene graph: constraint={constraint_id:?}"
2911 ))
2912 })?;
2913 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2914 return Err(KclError::refactor(format!(
2915 "Object is not a constraint, it is {}",
2916 constraint_object.kind.human_friendly_kind_with_article()
2917 )));
2918 };
2919
2920 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2922 Ok(())
2923 }
2924
2925 fn edit_coincident_constraint(
2926 &mut self,
2927 new_ast: &mut ast::Node<ast::Program>,
2928 constraint_id: ObjectId,
2929 segments: Vec<ConstraintSegment>,
2930 ) -> Result<(), KclError> {
2931 if segments.len() < 2 {
2932 return Err(KclError::refactor(format!(
2933 "Coincident constraint must have at least 2 inputs, got {}",
2934 segments.len()
2935 )));
2936 }
2937
2938 let segment_asts = segments
2939 .iter()
2940 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2941 .collect::<Result<Vec<_>, _>>()?;
2942
2943 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2944 elements: segment_asts,
2945 digest: None,
2946 non_code_meta: Default::default(),
2947 })));
2948
2949 self.mutate_ast(
2950 new_ast,
2951 constraint_id,
2952 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2953 )?;
2954 Ok(())
2955 }
2956
2957 fn edit_horizontal_points_constraint(
2958 &mut self,
2959 new_ast: &mut ast::Node<ast::Program>,
2960 constraint_id: ObjectId,
2961 points: Vec<ConstraintSegment>,
2962 ) -> Result<(), KclError> {
2963 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2964 }
2965
2966 fn edit_vertical_points_constraint(
2967 &mut self,
2968 new_ast: &mut ast::Node<ast::Program>,
2969 constraint_id: ObjectId,
2970 points: Vec<ConstraintSegment>,
2971 ) -> Result<(), KclError> {
2972 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2973 }
2974
2975 fn edit_axis_points_constraint(
2976 &mut self,
2977 new_ast: &mut ast::Node<ast::Program>,
2978 constraint_id: ObjectId,
2979 points: Vec<ConstraintSegment>,
2980 constraint_name: &str,
2981 ) -> Result<(), KclError> {
2982 if points.len() < 2 {
2983 return Err(KclError::refactor(format!(
2984 "{constraint_name} points constraint must have at least 2 points, got {}",
2985 points.len()
2986 )));
2987 }
2988
2989 let point_asts = points
2990 .iter()
2991 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2992 .collect::<Result<Vec<_>, _>>()?;
2993
2994 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2995 elements: point_asts,
2996 digest: None,
2997 non_code_meta: Default::default(),
2998 })));
2999
3000 self.mutate_ast(
3001 new_ast,
3002 constraint_id,
3003 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3004 )?;
3005 Ok(())
3006 }
3007
3008 fn edit_equal_length_constraint(
3010 &mut self,
3011 new_ast: &mut ast::Node<ast::Program>,
3012 constraint_id: ObjectId,
3013 lines: Vec<ObjectId>,
3014 ) -> Result<(), KclError> {
3015 if lines.len() < 2 {
3016 return Err(KclError::refactor(format!(
3017 "Lines equal length constraint must have at least 2 lines, got {}",
3018 lines.len()
3019 )));
3020 }
3021
3022 let line_asts = lines
3023 .iter()
3024 .map(|line_id| {
3025 let line_object = self
3026 .scene_graph
3027 .objects
3028 .get(line_id.0)
3029 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3030 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3031 let kind = line_object.kind.human_friendly_kind_with_article();
3032 return Err(KclError::refactor(format!(
3033 "This constraint only works on Segments, but you selected {kind}"
3034 )));
3035 };
3036 let Segment::Line(_) = line_segment else {
3037 let kind = line_segment.human_friendly_kind_with_article();
3038 return Err(KclError::refactor(format!(
3039 "Only lines can be made equal length, but you selected {kind}"
3040 )));
3041 };
3042
3043 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3044 })
3045 .collect::<Result<Vec<_>, _>>()?;
3046
3047 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3048 elements: line_asts,
3049 digest: None,
3050 non_code_meta: Default::default(),
3051 })));
3052
3053 self.mutate_ast(
3054 new_ast,
3055 constraint_id,
3056 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3057 )?;
3058 Ok(())
3059 }
3060
3061 fn edit_parallel_constraint(
3063 &mut self,
3064 new_ast: &mut ast::Node<ast::Program>,
3065 constraint_id: ObjectId,
3066 lines: Vec<ObjectId>,
3067 ) -> Result<(), KclError> {
3068 if lines.len() < 2 {
3069 return Err(KclError::refactor(format!(
3070 "Parallel constraint must have at least 2 lines, got {}",
3071 lines.len()
3072 )));
3073 }
3074
3075 let line_asts = lines
3076 .iter()
3077 .map(|line_id| {
3078 let line_object = self
3079 .scene_graph
3080 .objects
3081 .get(line_id.0)
3082 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3083 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3084 let kind = line_object.kind.human_friendly_kind_with_article();
3085 return Err(KclError::refactor(format!(
3086 "This constraint only works on Segments, but you selected {kind}"
3087 )));
3088 };
3089 let Segment::Line(_) = line_segment else {
3090 let kind = line_segment.human_friendly_kind_with_article();
3091 return Err(KclError::refactor(format!(
3092 "Only lines can be made parallel, but you selected {kind}"
3093 )));
3094 };
3095
3096 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3097 })
3098 .collect::<Result<Vec<_>, _>>()?;
3099
3100 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3101 elements: line_asts,
3102 digest: None,
3103 non_code_meta: Default::default(),
3104 })));
3105
3106 self.mutate_ast(
3107 new_ast,
3108 constraint_id,
3109 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3110 )?;
3111 Ok(())
3112 }
3113
3114 fn edit_equal_radius_constraint(
3116 &mut self,
3117 new_ast: &mut ast::Node<ast::Program>,
3118 constraint_id: ObjectId,
3119 input: Vec<ObjectId>,
3120 ) -> Result<(), KclError> {
3121 if input.len() < 2 {
3122 return Err(KclError::refactor(format!(
3123 "equalRadius constraint must have at least 2 segments, got {}",
3124 input.len()
3125 )));
3126 }
3127
3128 let input_asts = input
3129 .iter()
3130 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3131 .collect::<Result<Vec<_>, _>>()?;
3132
3133 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3134 elements: input_asts,
3135 digest: None,
3136 non_code_meta: Default::default(),
3137 })));
3138
3139 self.mutate_ast(
3140 new_ast,
3141 constraint_id,
3142 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3143 )?;
3144 Ok(())
3145 }
3146
3147 async fn execute_after_edit(
3148 &mut self,
3149 ctx: &ExecutorContext,
3150 sketch: ObjectId,
3151 sketch_block_ref: AstNodeRef,
3152 segment_ids_edited: AhashIndexSet<ObjectId>,
3153 edit_kind: EditDeleteKind,
3154 new_ast: &mut ast::Node<ast::Program>,
3155 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3156 let new_source = source_from_ast(new_ast);
3158 let (new_program, errors) = Program::parse(&new_source)
3160 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3161 if !errors.is_empty() {
3162 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3163 "Error parsing KCL source after editing: {errors:?}"
3164 ))));
3165 }
3166 let Some(new_program) = new_program else {
3167 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3168 "No AST produced after editing".to_string(),
3169 )));
3170 };
3171
3172 self.program = new_program.clone();
3174
3175 let is_delete = edit_kind.is_delete();
3177 let truncated_program = {
3178 let mut truncated_program = new_program;
3179 only_sketch_block(
3180 &mut truncated_program.ast,
3181 &sketch_block_ref,
3182 edit_kind.to_change_kind(),
3183 )
3184 .map_err(KclErrorWithOutputs::no_outputs)?;
3185 truncated_program
3186 };
3187
3188 let mock_config = MockConfig {
3190 sketch_block_id: Some(sketch),
3191 freedom_analysis: is_delete,
3192 segment_ids_edited: segment_ids_edited.clone(),
3193 ..Default::default()
3194 };
3195 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3196
3197 let outcome = self.update_state_after_exec(outcome, is_delete);
3199
3200 let new_source = {
3201 let mut new_ast = self.program.ast.clone();
3206 for (var_range, value) in &outcome.var_solutions {
3207 let rounded = value.round(3);
3208 let source_ref = SourceRef::Simple {
3209 range: *var_range,
3210 node_path: None,
3211 };
3212 mutate_ast_node_by_source_ref(
3213 &mut new_ast,
3214 &source_ref,
3215 AstMutateCommand::EditVarInitialValue { value: rounded },
3216 )
3217 .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
3218 }
3219 source_from_ast(&new_ast)
3220 };
3221
3222 let src_delta = SourceDelta { text: new_source };
3223 let scene_graph_delta = SceneGraphDelta {
3224 new_graph: self.scene_graph_for_ui(),
3225 invalidates_ids: is_delete,
3226 new_objects: Vec::new(),
3227 exec_outcome: outcome,
3228 };
3229 Ok((src_delta, scene_graph_delta))
3230 }
3231
3232 async fn execute_after_delete_sketch(
3233 &mut self,
3234 ctx: &ExecutorContext,
3235 new_ast: &mut ast::Node<ast::Program>,
3236 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3237 let new_source = source_from_ast(new_ast);
3239 let (new_program, errors) = Program::parse(&new_source)
3241 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3242 if !errors.is_empty() {
3243 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3244 "Error parsing KCL source after editing: {errors:?}"
3245 ))));
3246 }
3247 let Some(new_program) = new_program else {
3248 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3249 "No AST produced after editing".to_string(),
3250 )));
3251 };
3252
3253 self.program = new_program.clone();
3255
3256 let outcome = ctx.run_with_caching(new_program).await?;
3262 let freedom_analysis_ran = true;
3263
3264 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3265
3266 let src_delta = SourceDelta { text: new_source };
3267 let scene_graph_delta = SceneGraphDelta {
3268 new_graph: self.scene_graph_for_ui(),
3269 invalidates_ids: true,
3270 new_objects: Vec::new(),
3271 exec_outcome: outcome,
3272 };
3273 Ok((src_delta, scene_graph_delta))
3274 }
3275
3276 fn point_id_to_ast_reference(
3281 &self,
3282 point_id: ObjectId,
3283 new_ast: &mut ast::Node<ast::Program>,
3284 ) -> Result<ast::Expr, KclError> {
3285 let point_object = self
3286 .scene_graph
3287 .objects
3288 .get(point_id.0)
3289 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3290 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3291 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3292 };
3293 let Segment::Point(point) = point_segment else {
3294 return Err(KclError::refactor(format!(
3295 "Only points are currently supported: {point_object:?}"
3296 )));
3297 };
3298
3299 if let Some(owner_id) = point.owner {
3300 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3301 KclError::refactor(format!(
3302 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3303 ))
3304 })?;
3305 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3306 return Err(KclError::refactor(format!(
3307 "Owner of point is not a segment, but found {}",
3308 owner_object.kind.human_friendly_kind_with_article()
3309 )));
3310 };
3311
3312 match owner_segment {
3313 Segment::Line(line) => {
3314 let property = if line.start == point_id {
3315 LINE_PROPERTY_START
3316 } else if line.end == point_id {
3317 LINE_PROPERTY_END
3318 } else {
3319 return Err(KclError::refactor(format!(
3320 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3321 )));
3322 };
3323 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3324 }
3325 Segment::Arc(arc) => {
3326 let property = if arc.start == point_id {
3327 ARC_PROPERTY_START
3328 } else if arc.end == point_id {
3329 ARC_PROPERTY_END
3330 } else if arc.center == point_id {
3331 ARC_PROPERTY_CENTER
3332 } else {
3333 return Err(KclError::refactor(format!(
3334 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3335 )));
3336 };
3337 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3338 }
3339 Segment::Circle(circle) => {
3340 let property = if circle.start == point_id {
3341 CIRCLE_PROPERTY_START
3342 } else if circle.center == point_id {
3343 CIRCLE_PROPERTY_CENTER
3344 } else {
3345 return Err(KclError::refactor(format!(
3346 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3347 )));
3348 };
3349 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3350 }
3351 Segment::ControlPointSpline(spline) => {
3352 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3353 return Err(KclError::refactor(format!(
3354 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3355 )));
3356 };
3357 let owner_expr =
3358 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3359 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3360 Ok(create_index_expression(controls_expr, index))
3361 }
3362 _ => Err(KclError::refactor(format!(
3363 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3364 ))),
3365 }
3366 } else {
3367 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3369 }
3370 }
3371
3372 fn line_id_to_ast_reference(
3373 &self,
3374 line_id: ObjectId,
3375 new_ast: &mut ast::Node<ast::Program>,
3376 ) -> Result<ast::Expr, KclError> {
3377 let line_object = self
3378 .scene_graph
3379 .objects
3380 .get(line_id.0)
3381 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3382 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3383 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3384 };
3385 let Segment::Line(line) = line_segment else {
3386 return Err(KclError::refactor(format!(
3387 "Only lines are currently supported: {line_object:?}"
3388 )));
3389 };
3390
3391 if let Some(owner_id) = line.owner {
3392 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3393 KclError::refactor(format!(
3394 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3395 ))
3396 })?;
3397 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3398 return Err(KclError::refactor(format!(
3399 "Owner of line is not a segment, but found {}",
3400 owner_object.kind.human_friendly_kind_with_article()
3401 )));
3402 };
3403
3404 match owner_segment {
3405 Segment::ControlPointSpline(spline) => {
3406 let Some(index) = spline
3407 .controls
3408 .windows(2)
3409 .position(|window| window[0] == line.start && window[1] == line.end)
3410 else {
3411 return Err(KclError::refactor(format!(
3412 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3413 )));
3414 };
3415 let owner_expr =
3416 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3417 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3418 Ok(create_index_expression(edges_expr, index))
3419 }
3420 _ => Err(KclError::refactor(format!(
3421 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3422 ))),
3423 }
3424 } else {
3425 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3426 }
3427 }
3428
3429 fn coincident_segment_to_ast(
3430 &self,
3431 segment: &ConstraintSegment,
3432 new_ast: &mut ast::Node<ast::Program>,
3433 ) -> Result<ast::Expr, KclError> {
3434 match segment {
3435 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3436 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3437 }
3438 }
3439
3440 fn segment_id_to_constraint_ast_reference(
3441 &self,
3442 segment_id: ObjectId,
3443 new_ast: &mut ast::Node<ast::Program>,
3444 ) -> Result<ast::Expr, KclError> {
3445 let segment_object = self
3446 .scene_graph
3447 .objects
3448 .get(segment_id.0)
3449 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3450 let ObjectKind::Segment { segment } = &segment_object.kind else {
3451 return Err(KclError::refactor(format!(
3452 "Object is not a segment, it is {}",
3453 segment_object.kind.human_friendly_kind_with_article()
3454 )));
3455 };
3456
3457 match segment {
3458 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3459 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3460 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3461 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3462 Segment::ControlPointSpline(_) => {
3463 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3464 }
3465 }
3466 }
3467
3468 fn axis_constraint_segment_to_ast(
3469 &self,
3470 segment: &ConstraintSegment,
3471 new_ast: &mut ast::Node<ast::Program>,
3472 ) -> Result<ast::Expr, KclError> {
3473 match segment {
3474 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3475 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3476 }
3477 }
3478
3479 async fn add_coincident(
3480 &mut self,
3481 sketch: ObjectId,
3482 coincident: Coincident,
3483 new_ast: &mut ast::Node<ast::Program>,
3484 ) -> Result<AstNodeRef, KclError> {
3485 let sketch_id = sketch;
3486 for segment in &coincident.segments {
3487 let ConstraintSegment::Segment(segment_id) = segment else {
3488 continue;
3489 };
3490 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3491 continue;
3492 };
3493 if matches!(
3494 segment_object.kind,
3495 ObjectKind::Segment {
3496 segment: Segment::ControlPointSpline(_)
3497 }
3498 ) {
3499 return Err(KclError::refactor(
3500 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3501 .to_owned(),
3502 ));
3503 }
3504 }
3505 let segment_asts = coincident
3506 .segments
3507 .iter()
3508 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3509 .collect::<Result<Vec<_>, _>>()?;
3510 if segment_asts.len() < 2 {
3511 return Err(KclError::refactor(format!(
3512 "Coincident constraint must have at least 2 inputs, got {}",
3513 segment_asts.len()
3514 )));
3515 }
3516
3517 let coincident_ast = create_coincident_ast(segment_asts);
3519
3520 let (sketch_block_ref, _) = self.mutate_ast(
3522 new_ast,
3523 sketch_id,
3524 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3525 )?;
3526 Ok(sketch_block_ref)
3527 }
3528
3529 async fn add_distance(
3530 &mut self,
3531 sketch: ObjectId,
3532 distance: Distance,
3533 new_ast: &mut ast::Node<ast::Program>,
3534 ) -> Result<AstNodeRef, KclError> {
3535 let sketch_id = sketch;
3536 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3537 [pt0, pt1] => [
3538 self.coincident_segment_to_ast(pt0, new_ast)?,
3539 self.coincident_segment_to_ast(pt1, new_ast)?,
3540 ],
3541 _ => {
3542 return Err(KclError::refactor(format!(
3543 "Distance constraint must have exactly 2 points, got {}",
3544 distance.points.len()
3545 )));
3546 }
3547 };
3548
3549 let arguments = match &distance.label_position {
3550 Some(label_position) => vec![ast::LabeledArg {
3551 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3552 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3553 }],
3554 None => Default::default(),
3555 };
3556
3557 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3559 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3560 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3561 ast::ArrayExpression {
3562 elements: vec![pt0_ast, pt1_ast],
3563 digest: None,
3564 non_code_meta: Default::default(),
3565 },
3566 )))),
3567 arguments,
3568 digest: None,
3569 non_code_meta: Default::default(),
3570 })));
3571 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3572 left: distance_call_ast,
3573 operator: ast::BinaryOperator::Eq,
3574 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3575 value: ast::LiteralValue::Number {
3576 value: distance.distance.value,
3577 suffix: distance.distance.units,
3578 },
3579 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3580 KclError::refactor(format!(
3581 "Could not format numeric suffix: {:?}",
3582 distance.distance.units
3583 ))
3584 })?,
3585 digest: None,
3586 }))),
3587 digest: None,
3588 })));
3589
3590 let (sketch_block_ref, _) = self.mutate_ast(
3592 new_ast,
3593 sketch_id,
3594 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3595 )?;
3596 Ok(sketch_block_ref)
3597 }
3598
3599 async fn add_angle(
3600 &mut self,
3601 sketch: ObjectId,
3602 angle: Angle,
3603 new_ast: &mut ast::Node<ast::Program>,
3604 ) -> Result<AstNodeRef, KclError> {
3605 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3606 return Err(KclError::refactor(format!(
3607 "Angle constraint must have exactly 2 lines, got {}",
3608 angle.lines.len()
3609 )));
3610 };
3611 let sketch_id = sketch;
3612
3613 let line0_object = self
3615 .scene_graph
3616 .objects
3617 .get(l0_id.0)
3618 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3619 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3620 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3621 };
3622 let Segment::Line(_) = line0_segment else {
3623 return Err(KclError::refactor(format!(
3624 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3625 )));
3626 };
3627 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3628
3629 let line1_object = self
3630 .scene_graph
3631 .objects
3632 .get(l1_id.0)
3633 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3634 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3635 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3636 };
3637 let Segment::Line(_) = line1_segment else {
3638 return Err(KclError::refactor(format!(
3639 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3640 )));
3641 };
3642 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3643
3644 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3646 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3647 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3648 ast::ArrayExpression {
3649 elements: vec![l0_ast, l1_ast],
3650 digest: None,
3651 non_code_meta: Default::default(),
3652 },
3653 )))),
3654 arguments: Default::default(),
3655 digest: None,
3656 non_code_meta: Default::default(),
3657 })));
3658 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3659 left: angle_call_ast,
3660 operator: ast::BinaryOperator::Eq,
3661 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3662 value: ast::LiteralValue::Number {
3663 value: angle.angle.value,
3664 suffix: angle.angle.units,
3665 },
3666 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3667 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3668 })?,
3669 digest: None,
3670 }))),
3671 digest: None,
3672 })));
3673
3674 let (sketch_block_ref, _) = self.mutate_ast(
3676 new_ast,
3677 sketch_id,
3678 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3679 )?;
3680 Ok(sketch_block_ref)
3681 }
3682
3683 async fn add_tangent(
3684 &mut self,
3685 sketch: ObjectId,
3686 tangent: Tangent,
3687 new_ast: &mut ast::Node<ast::Program>,
3688 ) -> Result<AstNodeRef, KclError> {
3689 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3690 return Err(KclError::refactor(format!(
3691 "Tangent constraint must have exactly 2 segments, got {}",
3692 tangent.input.len()
3693 )));
3694 };
3695 let sketch_id = sketch;
3696
3697 let seg0_object = self
3698 .scene_graph
3699 .objects
3700 .get(seg0_id.0)
3701 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3702 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3703 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3704 };
3705 let seg0_ast = match seg0_segment {
3706 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3707 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3708 }
3709 _ => {
3710 return Err(KclError::refactor(format!(
3711 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3712 )));
3713 }
3714 };
3715
3716 let seg1_object = self
3717 .scene_graph
3718 .objects
3719 .get(seg1_id.0)
3720 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3721 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3722 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3723 };
3724 let seg1_ast = match seg1_segment {
3725 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3726 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3727 }
3728 _ => {
3729 return Err(KclError::refactor(format!(
3730 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3731 )));
3732 }
3733 };
3734
3735 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3736 let (sketch_block_ref, _) = self.mutate_ast(
3737 new_ast,
3738 sketch_id,
3739 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3740 )?;
3741 Ok(sketch_block_ref)
3742 }
3743
3744 async fn add_symmetric(
3745 &mut self,
3746 sketch: ObjectId,
3747 symmetric: Symmetric,
3748 new_ast: &mut ast::Node<ast::Program>,
3749 ) -> Result<AstNodeRef, KclError> {
3750 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3751 return Err(KclError::refactor(format!(
3752 "Symmetric constraint must have exactly 2 inputs, got {}",
3753 symmetric.input.len()
3754 )));
3755 };
3756 let sketch_id = sketch;
3757
3758 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3759 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3760 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3761
3762 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3763 let (sketch_block_ref, _) = self.mutate_ast(
3764 new_ast,
3765 sketch_id,
3766 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3767 )?;
3768 Ok(sketch_block_ref)
3769 }
3770
3771 async fn add_midpoint(
3772 &mut self,
3773 sketch: ObjectId,
3774 midpoint: Midpoint,
3775 new_ast: &mut ast::Node<ast::Program>,
3776 ) -> Result<AstNodeRef, KclError> {
3777 let sketch_id = sketch;
3778 let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3779
3780 let segment_object = self
3781 .scene_graph
3782 .objects
3783 .get(midpoint.segment.0)
3784 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3785 let ObjectKind::Segment {
3786 segment: midpoint_segment,
3787 } = &segment_object.kind
3788 else {
3789 return Err(KclError::refactor(format!(
3790 "Object must be a segment, but it was {}",
3791 segment_object.kind.human_friendly_kind_with_article()
3792 )));
3793 };
3794 let segment_ast = match midpoint_segment {
3795 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3796 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3797 _ => {
3798 return Err(KclError::refactor(format!(
3799 "Midpoint target must be a line or arc segment but it was {}",
3800 midpoint_segment.human_friendly_kind_with_article()
3801 )));
3802 }
3803 };
3804
3805 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3806 let (sketch_block_ref, _) = self.mutate_ast(
3807 new_ast,
3808 sketch_id,
3809 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3810 )?;
3811 Ok(sketch_block_ref)
3812 }
3813
3814 async fn add_equal_radius(
3815 &mut self,
3816 sketch: ObjectId,
3817 equal_radius: EqualRadius,
3818 new_ast: &mut ast::Node<ast::Program>,
3819 ) -> Result<AstNodeRef, KclError> {
3820 if equal_radius.input.len() < 2 {
3821 return Err(KclError::refactor(format!(
3822 "equalRadius constraint must have at least 2 segments, got {}",
3823 equal_radius.input.len()
3824 )));
3825 }
3826
3827 let sketch_id = sketch;
3828 let input_asts = equal_radius
3829 .input
3830 .iter()
3831 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3832 .collect::<Result<Vec<_>, _>>()?;
3833
3834 let equal_radius_ast = create_equal_radius_ast(input_asts);
3835 let (sketch_block_ref, _) = self.mutate_ast(
3836 new_ast,
3837 sketch_id,
3838 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3839 )?;
3840 Ok(sketch_block_ref)
3841 }
3842
3843 async fn add_radius(
3844 &mut self,
3845 sketch: ObjectId,
3846 radius: Radius,
3847 new_ast: &mut ast::Node<ast::Program>,
3848 ) -> Result<AstNodeRef, KclError> {
3849 let params = ArcSizeConstraintParams {
3850 points: vec![radius.arc],
3851 function_name: RADIUS_FN,
3852 value: radius.radius.value,
3853 units: radius.radius.units,
3854 label_position: radius.label_position,
3855 constraint_type_name: "Radius",
3856 };
3857 self.add_arc_size_constraint(sketch, params, new_ast).await
3858 }
3859
3860 async fn add_diameter(
3861 &mut self,
3862 sketch: ObjectId,
3863 diameter: Diameter,
3864 new_ast: &mut ast::Node<ast::Program>,
3865 ) -> Result<AstNodeRef, KclError> {
3866 let params = ArcSizeConstraintParams {
3867 points: vec![diameter.arc],
3868 function_name: DIAMETER_FN,
3869 value: diameter.diameter.value,
3870 units: diameter.diameter.units,
3871 label_position: diameter.label_position,
3872 constraint_type_name: "Diameter",
3873 };
3874 self.add_arc_size_constraint(sketch, params, new_ast).await
3875 }
3876
3877 async fn add_fixed_constraints(
3878 &mut self,
3879 sketch: ObjectId,
3880 points: Vec<FixedPoint>,
3881 new_ast: &mut ast::Node<ast::Program>,
3882 ) -> Result<AstNodeRef, KclError> {
3883 let mut sketch_block_ref = None;
3884
3885 for fixed_point in points {
3886 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3887 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3888 .map_err(|err| KclError::refactor(err.to_string()))?;
3889
3890 let (sketch_ref, _) = self.mutate_ast(
3891 new_ast,
3892 sketch,
3893 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3894 )?;
3895 sketch_block_ref = Some(sketch_ref);
3896 }
3897
3898 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3899 }
3900
3901 async fn add_arc_size_constraint(
3902 &mut self,
3903 sketch: ObjectId,
3904 params: ArcSizeConstraintParams,
3905 new_ast: &mut ast::Node<ast::Program>,
3906 ) -> Result<AstNodeRef, KclError> {
3907 let sketch_id = sketch;
3908
3909 if params.points.len() != 1 {
3911 return Err(KclError::refactor(format!(
3912 "{} constraint must have exactly 1 argument (an arc segment), got {}",
3913 params.constraint_type_name,
3914 params.points.len()
3915 )));
3916 }
3917
3918 let arc_id = params.points[0];
3919 let arc_object = self
3920 .scene_graph
3921 .objects
3922 .get(arc_id.0)
3923 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3924 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3925 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3926 };
3927 let ref_type = match arc_segment {
3928 Segment::Arc(_) => ARC_VARIABLE,
3929 Segment::Circle(_) => CIRCLE_VARIABLE,
3930 _ => {
3931 return Err(KclError::refactor(format!(
3932 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3933 params.constraint_type_name
3934 )));
3935 }
3936 };
3937 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3939 let arguments = match ¶ms.label_position {
3940 Some(label_position) => vec![ast::LabeledArg {
3941 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3942 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3943 }],
3944 None => Default::default(),
3945 };
3946
3947 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3949 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3950 unlabeled: Some(arc_ast),
3951 arguments,
3952 digest: None,
3953 non_code_meta: Default::default(),
3954 })));
3955 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3956 left: call_ast,
3957 operator: ast::BinaryOperator::Eq,
3958 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3959 value: ast::LiteralValue::Number {
3960 value: params.value,
3961 suffix: params.units,
3962 },
3963 raw: format_number_literal(params.value, params.units, None)
3964 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3965 digest: None,
3966 }))),
3967 digest: None,
3968 })));
3969
3970 let (sketch_block_ref, _) = self.mutate_ast(
3972 new_ast,
3973 sketch_id,
3974 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3975 )?;
3976 Ok(sketch_block_ref)
3977 }
3978
3979 async fn add_horizontal_distance(
3980 &mut self,
3981 sketch: ObjectId,
3982 distance: Distance,
3983 new_ast: &mut ast::Node<ast::Program>,
3984 ) -> Result<AstNodeRef, KclError> {
3985 let sketch_id = sketch;
3986 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3987 [pt0, pt1] => [
3988 self.coincident_segment_to_ast(pt0, new_ast)?,
3989 self.coincident_segment_to_ast(pt1, new_ast)?,
3990 ],
3991 _ => {
3992 return Err(KclError::refactor(format!(
3993 "Horizontal distance constraint must have exactly 2 points, got {}",
3994 distance.points.len()
3995 )));
3996 }
3997 };
3998
3999 let arguments = match &distance.label_position {
4000 Some(label_position) => vec![ast::LabeledArg {
4001 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4002 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4003 }],
4004 None => Default::default(),
4005 };
4006
4007 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4009 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4010 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4011 ast::ArrayExpression {
4012 elements: vec![pt0_ast, pt1_ast],
4013 digest: None,
4014 non_code_meta: Default::default(),
4015 },
4016 )))),
4017 arguments,
4018 digest: None,
4019 non_code_meta: Default::default(),
4020 })));
4021 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4022 left: distance_call_ast,
4023 operator: ast::BinaryOperator::Eq,
4024 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4025 value: ast::LiteralValue::Number {
4026 value: distance.distance.value,
4027 suffix: distance.distance.units,
4028 },
4029 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4030 KclError::refactor(format!(
4031 "Could not format numeric suffix: {:?}",
4032 distance.distance.units
4033 ))
4034 })?,
4035 digest: None,
4036 }))),
4037 digest: None,
4038 })));
4039
4040 let (sketch_block_ref, _) = self.mutate_ast(
4042 new_ast,
4043 sketch_id,
4044 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4045 )?;
4046 Ok(sketch_block_ref)
4047 }
4048
4049 async fn add_vertical_distance(
4050 &mut self,
4051 sketch: ObjectId,
4052 distance: Distance,
4053 new_ast: &mut ast::Node<ast::Program>,
4054 ) -> Result<AstNodeRef, KclError> {
4055 let sketch_id = sketch;
4056 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4057 [pt0, pt1] => [
4058 self.coincident_segment_to_ast(pt0, new_ast)?,
4059 self.coincident_segment_to_ast(pt1, new_ast)?,
4060 ],
4061 _ => {
4062 return Err(KclError::refactor(format!(
4063 "Vertical distance constraint must have exactly 2 points, got {}",
4064 distance.points.len()
4065 )));
4066 }
4067 };
4068
4069 let arguments = match &distance.label_position {
4070 Some(label_position) => vec![ast::LabeledArg {
4071 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4072 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4073 }],
4074 None => Default::default(),
4075 };
4076
4077 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4079 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4080 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4081 ast::ArrayExpression {
4082 elements: vec![pt0_ast, pt1_ast],
4083 digest: None,
4084 non_code_meta: Default::default(),
4085 },
4086 )))),
4087 arguments,
4088 digest: None,
4089 non_code_meta: Default::default(),
4090 })));
4091 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4092 left: distance_call_ast,
4093 operator: ast::BinaryOperator::Eq,
4094 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4095 value: ast::LiteralValue::Number {
4096 value: distance.distance.value,
4097 suffix: distance.distance.units,
4098 },
4099 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4100 KclError::refactor(format!(
4101 "Could not format numeric suffix: {:?}",
4102 distance.distance.units
4103 ))
4104 })?,
4105 digest: None,
4106 }))),
4107 digest: None,
4108 })));
4109
4110 let (sketch_block_ref, _) = self.mutate_ast(
4112 new_ast,
4113 sketch_id,
4114 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4115 )?;
4116 Ok(sketch_block_ref)
4117 }
4118
4119 async fn add_horizontal(
4120 &mut self,
4121 sketch: ObjectId,
4122 horizontal: Horizontal,
4123 new_ast: &mut ast::Node<ast::Program>,
4124 ) -> Result<AstNodeRef, KclError> {
4125 let sketch_id = sketch;
4126
4127 let first_arg_ast = match horizontal {
4129 Horizontal::Line { line } => {
4130 let line_object = self
4131 .scene_graph
4132 .objects
4133 .get(line.0)
4134 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4135 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4136 let kind = line_object.kind.human_friendly_kind_with_article();
4137 return Err(KclError::refactor(format!(
4138 "This constraint only works on Segments, but you selected {kind}"
4139 )));
4140 };
4141 let Segment::Line(_) = line_segment else {
4142 return Err(KclError::refactor(format!(
4143 "Only lines can be made horizontal, but you selected {}",
4144 line_segment.human_friendly_kind_with_article(),
4145 )));
4146 };
4147 self.line_id_to_ast_reference(line, new_ast)?
4148 }
4149 Horizontal::Points { points } => {
4150 let point_asts = points
4151 .iter()
4152 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4153 .collect::<Result<Vec<_>, _>>()?;
4154 ast::ArrayExpression::new(point_asts).into()
4155 }
4156 };
4157 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4159
4160 let (sketch_block_ref, _) = self.mutate_ast(
4162 new_ast,
4163 sketch_id,
4164 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4165 )?;
4166 Ok(sketch_block_ref)
4167 }
4168
4169 async fn add_lines_equal_length(
4170 &mut self,
4171 sketch: ObjectId,
4172 lines_equal_length: LinesEqualLength,
4173 new_ast: &mut ast::Node<ast::Program>,
4174 ) -> Result<AstNodeRef, KclError> {
4175 if lines_equal_length.lines.len() < 2 {
4176 return Err(KclError::refactor(format!(
4177 "Lines equal length constraint must have at least 2 lines, got {}",
4178 lines_equal_length.lines.len()
4179 )));
4180 };
4181
4182 let sketch_id = sketch;
4183
4184 let line_asts = lines_equal_length
4186 .lines
4187 .iter()
4188 .map(|line_id| {
4189 let line_object = self
4190 .scene_graph
4191 .objects
4192 .get(line_id.0)
4193 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4194 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4195 let kind = line_object.kind.human_friendly_kind_with_article();
4196 return Err(KclError::refactor(format!(
4197 "This constraint only works on Segments, but you selected {kind}"
4198 )));
4199 };
4200 let Segment::Line(_) = line_segment else {
4201 let kind = line_segment.human_friendly_kind_with_article();
4202 return Err(KclError::refactor(format!(
4203 "Only lines can be made equal length, but you selected {kind}"
4204 )));
4205 };
4206
4207 self.line_id_to_ast_reference(*line_id, new_ast)
4208 })
4209 .collect::<Result<Vec<_>, _>>()?;
4210
4211 let equal_length_ast = create_equal_length_ast(line_asts);
4213
4214 let (sketch_block_ref, _) = self.mutate_ast(
4216 new_ast,
4217 sketch_id,
4218 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4219 )?;
4220 Ok(sketch_block_ref)
4221 }
4222
4223 fn equal_radius_segment_id_to_ast_reference(
4224 &mut self,
4225 segment_id: ObjectId,
4226 new_ast: &mut ast::Node<ast::Program>,
4227 ) -> Result<ast::Expr, KclError> {
4228 let segment_object = self
4229 .scene_graph
4230 .objects
4231 .get(segment_id.0)
4232 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4233 let ObjectKind::Segment { segment } = &segment_object.kind else {
4234 return Err(KclError::refactor(format!(
4235 "Object is not a segment, it was {}",
4236 segment_object.kind.human_friendly_kind_with_article()
4237 )));
4238 };
4239
4240 let ref_type = match segment {
4241 Segment::Arc(_) => ARC_VARIABLE,
4242 Segment::Circle(_) => CIRCLE_VARIABLE,
4243 _ => {
4244 return Err(KclError::refactor(format!(
4245 "equalRadius supports only arc/circle segments, got {}",
4246 segment.human_friendly_kind_with_article()
4247 )));
4248 }
4249 };
4250
4251 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4252 }
4253
4254 fn symmetric_input_id_to_ast_reference(
4255 &mut self,
4256 segment_id: ObjectId,
4257 new_ast: &mut ast::Node<ast::Program>,
4258 ) -> Result<ast::Expr, KclError> {
4259 let segment_object = self
4260 .scene_graph
4261 .objects
4262 .get(segment_id.0)
4263 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4264 let ObjectKind::Segment { segment } = &segment_object.kind else {
4265 return Err(KclError::refactor(format!(
4266 "Object is not a segment, it was {}",
4267 segment_object.kind.human_friendly_kind_with_article()
4268 )));
4269 };
4270
4271 match segment {
4272 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4273 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4274 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4275 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4276 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4277 "Symmetric does not yet support control point splines".to_owned(),
4278 )),
4279 }
4280 }
4281
4282 fn symmetric_axis_id_to_ast_reference(
4283 &mut self,
4284 segment_id: ObjectId,
4285 new_ast: &mut ast::Node<ast::Program>,
4286 ) -> Result<ast::Expr, KclError> {
4287 let segment_object = self
4288 .scene_graph
4289 .objects
4290 .get(segment_id.0)
4291 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4292 let ObjectKind::Segment { segment } = &segment_object.kind else {
4293 return Err(KclError::refactor(format!(
4294 "Object is not a segment, it was {}",
4295 segment_object.kind.human_friendly_kind_with_article()
4296 )));
4297 };
4298 match segment {
4299 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4300 _ => Err(KclError::refactor(format!(
4301 "Symmetric axis must be a line, got {}",
4302 segment.human_friendly_kind_with_article()
4303 ))),
4304 }
4305 }
4306
4307 async fn add_parallel(
4308 &mut self,
4309 sketch: ObjectId,
4310 parallel: Parallel,
4311 new_ast: &mut ast::Node<ast::Program>,
4312 ) -> Result<AstNodeRef, KclError> {
4313 if parallel.lines.len() < 2 {
4314 return Err(KclError::refactor(format!(
4315 "Parallel constraint must have at least 2 lines, got {}",
4316 parallel.lines.len()
4317 )));
4318 };
4319
4320 let sketch_id = sketch;
4321
4322 let line_asts = parallel
4323 .lines
4324 .iter()
4325 .map(|line_id| {
4326 let line_object = self
4327 .scene_graph
4328 .objects
4329 .get(line_id.0)
4330 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4331 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4332 let kind = line_object.kind.human_friendly_kind_with_article();
4333 return Err(KclError::refactor(format!(
4334 "This constraint only works on Segments, but you selected {kind}"
4335 )));
4336 };
4337 let Segment::Line(_) = line_segment else {
4338 let kind = line_segment.human_friendly_kind_with_article();
4339 return Err(KclError::refactor(format!(
4340 "Only lines can be made parallel, but you selected {kind}"
4341 )));
4342 };
4343
4344 self.line_id_to_ast_reference(*line_id, new_ast)
4345 })
4346 .collect::<Result<Vec<_>, _>>()?;
4347
4348 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4349 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4350 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4351 ast::ArrayExpression {
4352 elements: line_asts,
4353 digest: None,
4354 non_code_meta: Default::default(),
4355 },
4356 )))),
4357 arguments: Default::default(),
4358 digest: None,
4359 non_code_meta: Default::default(),
4360 })));
4361
4362 let (sketch_block_ref, _) = self.mutate_ast(
4363 new_ast,
4364 sketch_id,
4365 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4366 )?;
4367 Ok(sketch_block_ref)
4368 }
4369
4370 async fn add_perpendicular(
4371 &mut self,
4372 sketch: ObjectId,
4373 perpendicular: Perpendicular,
4374 new_ast: &mut ast::Node<ast::Program>,
4375 ) -> Result<AstNodeRef, KclError> {
4376 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4377 .await
4378 }
4379
4380 async fn add_lines_at_angle_constraint(
4381 &mut self,
4382 sketch: ObjectId,
4383 angle_kind: LinesAtAngleKind,
4384 lines: Vec<ObjectId>,
4385 new_ast: &mut ast::Node<ast::Program>,
4386 ) -> Result<AstNodeRef, KclError> {
4387 let &[line0_id, line1_id] = lines.as_slice() else {
4388 return Err(KclError::refactor(format!(
4389 "{} constraint must have exactly 2 lines, got {}",
4390 angle_kind.to_function_name(),
4391 lines.len()
4392 )));
4393 };
4394
4395 let sketch_id = sketch;
4396
4397 let line0_object = self
4399 .scene_graph
4400 .objects
4401 .get(line0_id.0)
4402 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4403 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4404 let kind = line0_object.kind.human_friendly_kind_with_article();
4405 return Err(KclError::refactor(format!(
4406 "This constraint only works on Segments, but you selected {kind}"
4407 )));
4408 };
4409 let Segment::Line(_) = line0_segment else {
4410 return Err(KclError::refactor(format!(
4411 "Only lines can be made {}, but you selected {}",
4412 angle_kind.to_function_name(),
4413 line0_segment.human_friendly_kind_with_article(),
4414 )));
4415 };
4416 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4417
4418 let line1_object = self
4419 .scene_graph
4420 .objects
4421 .get(line1_id.0)
4422 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4423 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4424 let kind = line1_object.kind.human_friendly_kind_with_article();
4425 return Err(KclError::refactor(format!(
4426 "This constraint only works on Segments, but you selected {kind}"
4427 )));
4428 };
4429 let Segment::Line(_) = line1_segment else {
4430 return Err(KclError::refactor(format!(
4431 "Only lines can be made {}, but you selected {}",
4432 angle_kind.to_function_name(),
4433 line1_segment.human_friendly_kind_with_article(),
4434 )));
4435 };
4436 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4437
4438 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4440 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4441 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4442 ast::ArrayExpression {
4443 elements: vec![line0_ast, line1_ast],
4444 digest: None,
4445 non_code_meta: Default::default(),
4446 },
4447 )))),
4448 arguments: Default::default(),
4449 digest: None,
4450 non_code_meta: Default::default(),
4451 })));
4452
4453 let (sketch_block_ref, _) = self.mutate_ast(
4455 new_ast,
4456 sketch_id,
4457 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4458 )?;
4459 Ok(sketch_block_ref)
4460 }
4461
4462 async fn add_vertical(
4463 &mut self,
4464 sketch: ObjectId,
4465 vertical: Vertical,
4466 new_ast: &mut ast::Node<ast::Program>,
4467 ) -> Result<AstNodeRef, KclError> {
4468 let sketch_id = sketch;
4469
4470 let first_arg_ast = match vertical {
4471 Vertical::Line { line } => {
4472 let line_object = self
4474 .scene_graph
4475 .objects
4476 .get(line.0)
4477 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4478 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4479 let kind = line_object.kind.human_friendly_kind_with_article();
4480 return Err(KclError::refactor(format!(
4481 "This constraint only works on Segments, but you selected {kind}"
4482 )));
4483 };
4484 let Segment::Line(_) = line_segment else {
4485 return Err(KclError::refactor(format!(
4486 "Only lines can be made vertical, but you selected {}",
4487 line_segment.human_friendly_kind_with_article()
4488 )));
4489 };
4490 self.line_id_to_ast_reference(line, new_ast)?
4491 }
4492 Vertical::Points { points } => {
4493 let point_asts = points
4494 .iter()
4495 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4496 .collect::<Result<Vec<_>, _>>()?;
4497 ast::ArrayExpression::new(point_asts).into()
4498 }
4499 };
4500 let vertical_ast = create_vertical_ast(first_arg_ast);
4502
4503 let (sketch_block_ref, _) = self.mutate_ast(
4505 new_ast,
4506 sketch_id,
4507 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4508 )?;
4509 Ok(sketch_block_ref)
4510 }
4511
4512 async fn execute_after_add_constraint(
4513 &mut self,
4514 ctx: &ExecutorContext,
4515 sketch_id: ObjectId,
4516 sketch_block_ref: AstNodeRef,
4517 new_ast: &mut ast::Node<ast::Program>,
4518 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4519 let new_source = source_from_ast(new_ast);
4521 let (new_program, errors) = Program::parse(&new_source)
4523 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4524 if !errors.is_empty() {
4525 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4526 "Error parsing KCL source after adding constraint: {errors:?}"
4527 ))));
4528 }
4529 let Some(new_program) = new_program else {
4530 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4531 "No AST produced after adding constraint".to_string(),
4532 )));
4533 };
4534 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4535 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4536 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4537 )))
4538 })?;
4539
4540 let mut truncated_program = new_program.clone();
4543 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4544 .map_err(KclErrorWithOutputs::no_outputs)?;
4545
4546 let outcome = ctx
4548 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4549 .await?;
4550
4551 let new_object_ids = {
4552 let constraint_id = outcome
4554 .source_range_to_object
4555 .get(&constraint_node_ref.range)
4556 .copied()
4557 .ok_or_else(|| {
4558 KclErrorWithOutputs::from_error_outcome(
4559 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4560 outcome.clone(),
4561 )
4562 })?;
4563 vec![constraint_id]
4564 };
4565
4566 self.program = new_program;
4569
4570 let outcome = self.update_state_after_exec(outcome, true);
4572
4573 let src_delta = SourceDelta { text: new_source };
4574 let scene_graph_delta = SceneGraphDelta {
4575 new_graph: self.scene_graph_for_ui(),
4576 invalidates_ids: false,
4577 new_objects: new_object_ids,
4578 exec_outcome: outcome,
4579 };
4580 Ok((src_delta, scene_graph_delta))
4581 }
4582
4583 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4585 if segment_ids_set.contains(&segment_id) {
4586 return true;
4587 }
4588
4589 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4590 return false;
4591 };
4592 let ObjectKind::Segment { segment } = &segment_object.kind else {
4593 return false;
4594 };
4595 let Segment::Point(point) = segment else {
4596 return false;
4597 };
4598
4599 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4600 }
4601
4602 fn remaining_constraint_segments(
4603 &self,
4604 segments: &[ConstraintSegment],
4605 segment_ids_set: &AhashIndexSet<ObjectId>,
4606 ) -> Vec<ConstraintSegment> {
4607 segments
4608 .iter()
4609 .copied()
4610 .filter(|segment| match segment {
4611 ConstraintSegment::Origin(_) => true,
4612 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4613 })
4614 .collect()
4615 }
4616
4617 fn find_referenced_constraints(
4618 &self,
4619 sketch_id: ObjectId,
4620 segment_ids_set: &AhashIndexSet<ObjectId>,
4621 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4622 let sketch_object = self
4624 .scene_graph
4625 .objects
4626 .get(sketch_id.0)
4627 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4628 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4629 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4630 };
4631 let segment_or_owner_matches = |segment_id: ObjectId| {
4632 if segment_ids_set.contains(&segment_id) {
4633 return true;
4634 }
4635 let segment_object = self.scene_graph.objects.get(segment_id.0);
4636 if let Some(obj) = segment_object
4637 && let ObjectKind::Segment { segment } = &obj.kind
4638 {
4639 match segment {
4640 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4641 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4642 _ => false,
4643 }
4644 } else {
4645 false
4646 }
4647 };
4648 let mut constraint_ids_set = AhashIndexSet::default();
4649 for constraint_id in &sketch.constraints {
4650 let constraint_object = self
4651 .scene_graph
4652 .objects
4653 .get(constraint_id.0)
4654 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4655 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4656 return Err(KclError::refactor(format!(
4657 "Object is not a constraint, it is {}",
4658 constraint_object.kind.human_friendly_kind_with_article()
4659 )));
4660 };
4661 let depends_on_segment = match constraint {
4662 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4663 Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4664 Constraint::Fixed(fixed) => fixed
4665 .points
4666 .iter()
4667 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4668 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4669 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4670 Constraint::EqualRadius(equal_radius) => {
4671 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4672 }
4673 Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4674 Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4675 Constraint::Horizontal(h) => match h {
4676 Horizontal::Line { line } => segment_or_owner_matches(*line),
4677 Horizontal::Points { points } => points.iter().any(|point| match point {
4678 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4679 ConstraintSegment::Origin(_) => false,
4680 }),
4681 },
4682 Constraint::Vertical(v) => match v {
4683 Vertical::Line { line } => segment_or_owner_matches(*line),
4684 Vertical::Points { points } => points.iter().any(|point| match point {
4685 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4686 ConstraintSegment::Origin(_) => false,
4687 }),
4688 },
4689 Constraint::LinesEqualLength(lines_equal_length) => {
4690 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4691 }
4692 Constraint::Midpoint(midpoint) => {
4693 segment_or_owner_matches(midpoint.segment) || segment_or_owner_matches(midpoint.point)
4694 }
4695 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4696 Constraint::Perpendicular(perpendicular) => {
4697 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4698 }
4699 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4700 Constraint::Symmetric(symmetric) => {
4701 segment_or_owner_matches(symmetric.axis)
4702 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4703 }
4704 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4705 };
4706 if depends_on_segment {
4707 constraint_ids_set.insert(*constraint_id);
4708 }
4709 }
4710 Ok(constraint_ids_set)
4711 }
4712
4713 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4714 let mut outcome = outcome;
4715 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4716
4717 if freedom_analysis_ran {
4718 self.point_freedom_cache.clear();
4721 for new_obj in &new_objects {
4722 if let ObjectKind::Segment {
4723 segment: crate::front::Segment::Point(point),
4724 } = &new_obj.kind
4725 {
4726 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4727 }
4728 }
4729 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4730 self.scene_graph.objects = new_objects;
4732 } else {
4733 for old_obj in &self.scene_graph.objects {
4736 if let ObjectKind::Segment {
4737 segment: crate::front::Segment::Point(point),
4738 } = &old_obj.kind
4739 {
4740 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4741 }
4742 }
4743
4744 let mut updated_objects = Vec::with_capacity(new_objects.len());
4746 for new_obj in new_objects {
4747 let mut obj = new_obj;
4748 if let ObjectKind::Segment {
4749 segment: crate::front::Segment::Point(point),
4750 } = &mut obj.kind
4751 {
4752 let new_freedom = point.freedom;
4753 match new_freedom {
4759 Freedom::Free => {
4760 match self.point_freedom_cache.get(&obj.id).copied() {
4761 Some(Freedom::Conflict) => {
4762 }
4765 Some(Freedom::Fixed) => {
4766 point.freedom = Freedom::Fixed;
4768 }
4769 Some(Freedom::Free) => {
4770 }
4772 None => {
4773 }
4775 }
4776 }
4777 Freedom::Fixed => {
4778 }
4780 Freedom::Conflict => {
4781 }
4783 }
4784 self.point_freedom_cache.insert(obj.id, point.freedom);
4786 }
4787 updated_objects.push(obj);
4788 }
4789
4790 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4791 self.scene_graph.objects = updated_objects;
4792 }
4793 outcome
4794 }
4795
4796 fn mutate_ast(
4797 &mut self,
4798 ast: &mut ast::Node<ast::Program>,
4799 object_id: ObjectId,
4800 command: AstMutateCommand,
4801 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4802 let sketch_object = self
4803 .scene_graph
4804 .objects
4805 .get(object_id.0)
4806 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4807 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4808 }
4809}
4810
4811fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4812 let sketch_object = scene_graph
4814 .objects
4815 .get(sketch_id.0)
4816 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4817 let ObjectKind::Sketch(_) = &sketch_object.kind else {
4818 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4819 };
4820 expect_single_node_ref(sketch_object)
4821}
4822
4823fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4824 match &object.source {
4825 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4826 range: *range,
4827 node_path: node_path.clone(),
4828 }),
4829 SourceRef::BackTrace { ranges } => {
4830 let [range] = ranges.as_slice() else {
4831 return Err(KclError::refactor(format!(
4832 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4833 ranges.len()
4834 )));
4835 };
4836 Ok(AstNodeRef {
4837 range: range.0,
4838 node_path: range.1.clone(),
4839 })
4840 }
4841 }
4842}
4843
4844fn only_sketch_block_from_range(
4847 ast: &mut ast::Node<ast::Program>,
4848 sketch_block_range: SourceRange,
4849 edit_kind: ChangeKind,
4850) -> Result<(), KclError> {
4851 let r1 = sketch_block_range;
4852 let matches_range = |r2: SourceRange| -> bool {
4853 match edit_kind {
4856 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4857 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4859 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4860 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4862 }
4863 };
4864 let mut found = false;
4865 for item in ast.body.iter_mut() {
4866 match item {
4867 ast::BodyItem::ImportStatement(_) => {}
4868 ast::BodyItem::ExpressionStatement(node) => {
4869 if matches_range(SourceRange::from(&*node))
4870 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4871 {
4872 sketch_block.is_being_edited = true;
4873 found = true;
4874 break;
4875 }
4876 }
4877 ast::BodyItem::VariableDeclaration(node) => {
4878 if matches_range(SourceRange::from(&node.declaration.init))
4879 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4880 {
4881 sketch_block.is_being_edited = true;
4882 found = true;
4883 break;
4884 }
4885 }
4886 ast::BodyItem::TypeDeclaration(_) => {}
4887 ast::BodyItem::ReturnStatement(node) => {
4888 if matches_range(SourceRange::from(&node.argument))
4889 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4890 {
4891 sketch_block.is_being_edited = true;
4892 found = true;
4893 break;
4894 }
4895 }
4896 }
4897 }
4898 if !found {
4899 return Err(KclError::refactor(format!(
4900 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4901 )));
4902 }
4903
4904 Ok(())
4905}
4906
4907fn only_sketch_block(
4908 ast: &mut ast::Node<ast::Program>,
4909 sketch_block_ref: &AstNodeRef,
4910 edit_kind: ChangeKind,
4911) -> Result<(), KclError> {
4912 let Some(target_node_path) = &sketch_block_ref.node_path else {
4913 #[cfg(target_arch = "wasm32")]
4914 web_sys::console::warn_1(
4915 &format!(
4916 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4917 &sketch_block_ref
4918 )
4919 .into(),
4920 );
4921 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4922 };
4923 let mut found = false;
4924 for item in ast.body.iter_mut() {
4925 match item {
4926 ast::BodyItem::ImportStatement(_) => {}
4927 ast::BodyItem::ExpressionStatement(node) => {
4928 if let Some(node_path) = &node.node_path
4930 && node_path == target_node_path
4931 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4932 {
4933 sketch_block.is_being_edited = true;
4934 found = true;
4935 break;
4936 }
4937 if let Some(node_path) = node.expression.node_path()
4939 && node_path == target_node_path
4940 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4941 {
4942 sketch_block.is_being_edited = true;
4943 found = true;
4944 break;
4945 }
4946 }
4947 ast::BodyItem::VariableDeclaration(node) => {
4948 if let Some(node_path) = node.declaration.init.node_path()
4949 && node_path == target_node_path
4950 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4951 {
4952 sketch_block.is_being_edited = true;
4953 found = true;
4954 break;
4955 }
4956 }
4957 ast::BodyItem::TypeDeclaration(_) => {}
4958 ast::BodyItem::ReturnStatement(node) => {
4959 if let Some(node_path) = node.argument.node_path()
4960 && node_path == target_node_path
4961 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4962 {
4963 sketch_block.is_being_edited = true;
4964 found = true;
4965 break;
4966 }
4967 }
4968 }
4969 }
4970 if !found {
4971 return Err(KclError::refactor(format!(
4972 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4973 )));
4974 }
4975
4976 Ok(())
4977}
4978
4979fn sketch_on_ast_expr(
4980 ast: &mut ast::Node<ast::Program>,
4981 scene_graph: &SceneGraph,
4982 on: &Plane,
4983) -> Result<ast::Expr, KclError> {
4984 match on {
4985 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4986 Plane::Object(object_id) => {
4987 let on_object = scene_graph
4988 .objects
4989 .get(object_id.0)
4990 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4991 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4992 return Ok(face_expr);
4993 }
4994 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4995 }
4996 }
4997}
4998
4999fn sketch_face_of_scene_object_ast_expr(
5000 ast: &mut ast::Node<ast::Program>,
5001 on_object: &crate::front::Object,
5002) -> Result<Option<ast::Expr>, KclError> {
5003 let SourceRef::BackTrace { ranges } = &on_object.source else {
5004 return Ok(None);
5005 };
5006
5007 match &on_object.kind {
5008 ObjectKind::Wall(_) => {
5009 let [sweep_range, segment_range] = ranges.as_slice() else {
5010 return Err(KclError::refactor(format!(
5011 "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
5012 ranges.len(),
5013 on_object.artifact_id
5014 )));
5015 };
5016 let sweep_ref = get_or_insert_ast_reference(
5017 ast,
5018 &SourceRef::Simple {
5019 range: sweep_range.0,
5020 node_path: sweep_range.1.clone(),
5021 },
5022 "solid",
5023 None,
5024 )?;
5025 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5026 return Err(KclError::refactor(format!(
5027 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5028 on_object.artifact_id
5029 )));
5030 };
5031 let solid_name = solid_name_expr.name.name.clone();
5032 let solid_expr = ast_name_expr(solid_name.clone());
5033 let segment_ref = get_or_insert_ast_reference(
5034 ast,
5035 &SourceRef::Simple {
5036 range: segment_range.0,
5037 node_path: segment_range.1.clone(),
5038 },
5039 LINE_VARIABLE,
5040 None,
5041 )?;
5042
5043 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
5044 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5045 return Err(KclError::refactor(format!(
5046 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5047 on_object.artifact_id
5048 )));
5049 };
5050 create_member_expression(
5051 create_member_expression(ast_name_expr(region_name), "tags"),
5052 &segment_name_expr.name.name,
5053 )
5054 } else {
5055 segment_ref
5056 };
5057
5058 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5059 }
5060 ObjectKind::Cap(cap) => {
5061 let [range] = ranges.as_slice() else {
5062 return Err(KclError::refactor(format!(
5063 "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
5064 ranges.len(),
5065 on_object.artifact_id
5066 )));
5067 };
5068 let sweep_ref = get_or_insert_ast_reference(
5069 ast,
5070 &SourceRef::Simple {
5071 range: range.0,
5072 node_path: range.1.clone(),
5073 },
5074 "solid",
5075 None,
5076 )?;
5077 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5078 return Err(KclError::refactor(format!(
5079 "Could not resolve sweep reference for selected cap: artifact_id={:?}",
5080 on_object.artifact_id
5081 )));
5082 };
5083 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5084 let face_expr = match cap.kind {
5086 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5087 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5088 };
5089
5090 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5091 }
5092 _ => Ok(None),
5093 }
5094}
5095
5096fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5097 let mut existing_artifact_ids = scene_objects
5098 .iter()
5099 .map(|object| object.artifact_id)
5100 .collect::<HashSet<_>>();
5101
5102 for artifact in artifact_graph.values() {
5103 match artifact {
5104 Artifact::Wall(wall) => {
5105 if existing_artifact_ids.contains(&wall.id) {
5106 continue;
5107 }
5108
5109 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5110 Artifact::Segment(segment) => Some(segment),
5111 _ => None,
5112 }) else {
5113 continue;
5114 };
5115 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5116 Artifact::Sweep(sweep) => Some(sweep),
5117 _ => None,
5118 }) else {
5119 continue;
5120 };
5121 let source_segment = segment
5122 .original_seg_id
5123 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5124 .and_then(|artifact| match artifact {
5125 Artifact::Segment(segment) => Some(segment),
5126 _ => None,
5127 })
5128 .unwrap_or(segment);
5129 let id = ObjectId(scene_objects.len());
5130 scene_objects.push(crate::front::Object {
5131 id,
5132 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
5133 label: Default::default(),
5134 comments: Default::default(),
5135 artifact_id: wall.id,
5136 source: SourceRef::BackTrace {
5137 ranges: vec![
5138 (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
5139 (
5140 source_segment.code_ref.range,
5141 Some(source_segment.code_ref.node_path.clone()),
5142 ),
5143 ],
5144 },
5145 });
5146 existing_artifact_ids.insert(wall.id);
5147 }
5148 Artifact::Cap(cap) => {
5149 if existing_artifact_ids.contains(&cap.id) {
5150 continue;
5151 }
5152
5153 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5154 Artifact::Sweep(sweep) => Some(sweep),
5155 _ => None,
5156 }) else {
5157 continue;
5158 };
5159 let id = ObjectId(scene_objects.len());
5160 let kind = match cap.sub_type {
5161 CapSubType::Start => crate::frontend::api::CapKind::Start,
5162 CapSubType::End => crate::frontend::api::CapKind::End,
5163 };
5164 scene_objects.push(crate::front::Object {
5165 id,
5166 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
5167 label: Default::default(),
5168 comments: Default::default(),
5169 artifact_id: cap.id,
5170 source: SourceRef::BackTrace {
5171 ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
5172 },
5173 });
5174 existing_artifact_ids.insert(cap.id);
5175 }
5176 _ => {}
5177 }
5178 }
5179}
5180
5181fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5182 use crate::engine::PlaneName;
5183
5184 match name {
5185 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5186 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5187 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5188 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5189 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5190 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5191 }
5192}
5193
5194fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5195 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5196 ast::UnaryOperator::Neg,
5197 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5198 )))
5199}
5200
5201fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5202 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5203 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5204 unlabeled: Some(solid_expr),
5205 arguments: vec![ast::LabeledArg {
5206 label: Some(ast::Identifier::new("face")),
5207 arg: face_expr,
5208 }],
5209 digest: None,
5210 non_code_meta: Default::default(),
5211 })))
5212}
5213
5214fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5215 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5216 return None;
5217 };
5218 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5219 return None;
5220 };
5221 if !matches!(
5222 sweep_call.callee.name.name.as_str(),
5223 "extrude" | "revolve" | "sweep" | "loft"
5224 ) {
5225 return None;
5226 }
5227 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5228 return None;
5229 };
5230 let candidate = region_name_expr.name.name.clone();
5231 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5232 return None;
5233 };
5234 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5235 return None;
5236 };
5237 if region_call.callee.name.name != "region" {
5238 return None;
5239 }
5240 Some(candidate)
5241}
5242
5243fn get_or_insert_ast_reference(
5250 ast: &mut ast::Node<ast::Program>,
5251 source_ref: &SourceRef,
5252 prefix: &str,
5253 property: Option<&str>,
5254) -> Result<ast::Expr, KclError> {
5255 let command = AstMutateCommand::AddVariableDeclaration {
5256 prefix: prefix.to_owned(),
5257 };
5258 let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
5259 let AstMutateCommandReturn::Name(var_name) = ret else {
5260 return Err(KclError::refactor(
5261 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5262 ));
5263 };
5264 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5265 let Some(property) = property else {
5266 return Ok(var_expr);
5268 };
5269
5270 Ok(create_member_expression(var_expr, property))
5271}
5272
5273fn mutate_ast_node_by_source_ref(
5274 ast: &mut ast::Node<ast::Program>,
5275 source_ref: &SourceRef,
5276 command: AstMutateCommand,
5277) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5278 let (source_range, node_path) = match source_ref {
5279 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5280 SourceRef::BackTrace { ranges } => {
5281 let [range] = ranges.as_slice() else {
5282 return Err(KclError::refactor(format!(
5283 "Expected single source ref, got {}; ranges={ranges:#?}",
5284 ranges.len(),
5285 )));
5286 };
5287 (range.0, range.1.clone())
5288 }
5289 };
5290 let mut context = AstMutateContext {
5291 source_range,
5292 node_path,
5293 command,
5294 defined_names_stack: Default::default(),
5295 };
5296 let control = dfs_mut(ast, &mut context);
5297 match control {
5298 ControlFlow::Continue(_) => Err(KclError::refactor(
5299 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5300 )),
5301 ControlFlow::Break(break_value) => break_value,
5302 }
5303}
5304
5305#[derive(Debug)]
5306struct AstMutateContext {
5307 source_range: SourceRange,
5308 node_path: Option<ast::NodePath>,
5309 command: AstMutateCommand,
5310 defined_names_stack: Vec<HashSet<String>>,
5311}
5312
5313#[derive(Debug)]
5314#[allow(clippy::large_enum_variant)]
5315enum AstMutateCommand {
5316 AddSketchBlockExprStmt {
5318 expr: ast::Expr,
5319 },
5320 AddSketchBlockVarDecl {
5322 prefix: String,
5323 expr: ast::Expr,
5324 },
5325 AddVariableDeclaration {
5326 prefix: String,
5327 },
5328 EditPoint {
5329 at: ast::Expr,
5330 },
5331 EditLine {
5332 start: ast::Expr,
5333 end: ast::Expr,
5334 construction: Option<bool>,
5335 },
5336 EditArc {
5337 start: ast::Expr,
5338 end: ast::Expr,
5339 center: ast::Expr,
5340 construction: Option<bool>,
5341 },
5342 EditCircle {
5343 start: ast::Expr,
5344 center: ast::Expr,
5345 construction: Option<bool>,
5346 },
5347 EditControlPointSpline {
5348 points: ast::Expr,
5349 construction: Option<bool>,
5350 },
5351 EditConstraintValue {
5352 value: ast::BinaryPart,
5353 },
5354 EditDistanceConstraintLabelPosition {
5355 label_position: ast::Expr,
5356 },
5357 EditCallUnlabeled {
5358 arg: ast::Expr,
5359 },
5360 EditVarInitialValue {
5361 value: Number,
5362 },
5363 DeleteNode,
5364}
5365
5366impl AstMutateCommand {
5367 fn needs_defined_names_stack(&self) -> bool {
5368 matches!(
5369 self,
5370 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5371 )
5372 }
5373}
5374
5375#[derive(Debug)]
5376enum AstMutateCommandReturn {
5377 None,
5378 Name(String),
5379}
5380
5381#[derive(Debug, Clone)]
5382struct AstNodeRef {
5383 range: SourceRange,
5384 node_path: Option<ast::NodePath>,
5385}
5386
5387impl<T> From<&ast::Node<T>> for AstNodeRef {
5388 fn from(value: &ast::Node<T>) -> Self {
5389 AstNodeRef {
5390 range: value.into(),
5391 node_path: value.node_path.clone(),
5392 }
5393 }
5394}
5395
5396impl From<&ast::BodyItem> for AstNodeRef {
5397 fn from(value: &ast::BodyItem) -> Self {
5398 match value {
5399 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5400 range: node.into(),
5401 node_path: node.node_path.clone(),
5402 },
5403 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5404 range: node.into(),
5405 node_path: node.node_path.clone(),
5406 },
5407 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5408 range: node.into(),
5409 node_path: node.node_path.clone(),
5410 },
5411 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5412 range: node.into(),
5413 node_path: node.node_path.clone(),
5414 },
5415 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5416 range: node.into(),
5417 node_path: node.node_path.clone(),
5418 },
5419 }
5420 }
5421}
5422
5423impl From<&ast::Expr> for AstNodeRef {
5424 fn from(value: &ast::Expr) -> Self {
5425 AstNodeRef {
5426 range: SourceRange::from(value),
5427 node_path: value.node_path().cloned(),
5428 }
5429 }
5430}
5431
5432impl From<&AstMutateContext> for AstNodeRef {
5433 fn from(value: &AstMutateContext) -> Self {
5434 AstNodeRef {
5435 range: value.source_range,
5436 node_path: value.node_path.clone(),
5437 }
5438 }
5439}
5440
5441impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5442 type Error = crate::walk::AstNodeError;
5443
5444 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5445 Ok(AstNodeRef {
5446 range: SourceRange::try_from(value)?,
5447 node_path: value.try_into()?,
5448 })
5449 }
5450}
5451
5452impl From<AstNodeRef> for SourceRange {
5453 fn from(value: AstNodeRef) -> Self {
5454 value.range
5455 }
5456}
5457
5458impl Visitor for AstMutateContext {
5459 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5460 type Continue = ();
5461
5462 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5463 filter_and_process(self, node)
5464 }
5465
5466 fn finish(&mut self, node: NodeMut<'_>) {
5467 match &node {
5468 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5469 self.defined_names_stack.pop();
5470 }
5471 _ => {}
5472 }
5473 }
5474}
5475
5476fn filter_and_process(
5477 ctx: &mut AstMutateContext,
5478 node: NodeMut,
5479) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5480 let Ok(node_range) = SourceRange::try_from(&node) else {
5481 return TraversalReturn::new_continue(());
5483 };
5484 if let NodeMut::VariableDeclaration(var_decl) = &node {
5489 let expr_range = SourceRange::from(&var_decl.declaration.init);
5490 let expr_node_path = var_decl.declaration.init.node_path();
5491 if source_ref_matches(ctx, expr_range, expr_node_path) {
5492 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5493 return TraversalReturn::new_break(Ok((
5496 AstNodeRef::from(&**var_decl),
5497 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5498 )));
5499 }
5500 if let AstMutateCommand::DeleteNode = &ctx.command {
5501 return TraversalReturn {
5504 mutate_body_item: MutateBodyItem::Delete,
5505 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5506 };
5507 }
5508 }
5509 }
5510 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5513 let expr_range = SourceRange::from(&expr_stmt.expression);
5514 let expr_node_path = expr_stmt.expression.node_path();
5515 if source_ref_matches(ctx, expr_range, expr_node_path) {
5516 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5517 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5520 return TraversalReturn::new_continue(());
5521 };
5522 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5523 }
5524 if let AstMutateCommand::DeleteNode = &ctx.command {
5525 return TraversalReturn {
5528 mutate_body_item: MutateBodyItem::Delete,
5529 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5530 };
5531 }
5532 }
5533 }
5534
5535 if ctx.command.needs_defined_names_stack() {
5536 if let NodeMut::Program(program) = &node {
5537 ctx.defined_names_stack.push(find_defined_names(*program));
5538 } else if let NodeMut::SketchBlock(block) = &node {
5539 ctx.defined_names_stack.push(find_defined_names(&block.body));
5540 }
5541 }
5542
5543 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5545 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5546 return TraversalReturn::new_continue(());
5547 }
5548 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5549 return TraversalReturn::new_continue(());
5550 };
5551 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5552}
5553
5554fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5555 match &ctx.node_path {
5556 Some(target) => Some(target) == node_path,
5557 None => node_range == ctx.source_range,
5558 }
5559}
5560
5561fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5562 match &ctx.command {
5563 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5564 if let NodeMut::SketchBlock(sketch_block) = node {
5565 sketch_block
5566 .body
5567 .items
5568 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5569 inner: ast::ExpressionStatement {
5570 expression: expr.clone(),
5571 digest: None,
5572 },
5573 start: Default::default(),
5574 end: Default::default(),
5575 module_id: Default::default(),
5576 node_path: None,
5577 outer_attrs: Default::default(),
5578 pre_comments: Default::default(),
5579 comment_start: Default::default(),
5580 }));
5581 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5582 }
5583 }
5584 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5585 if let NodeMut::SketchBlock(sketch_block) = node {
5586 let empty_defined_names = HashSet::new();
5587 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5588 let Ok(name) = next_free_name(prefix, defined_names) else {
5589 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5590 };
5591 sketch_block
5592 .body
5593 .items
5594 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5595 ast::VariableDeclaration::new(
5596 ast::VariableDeclarator::new(&name, expr.clone()),
5597 ast::ItemVisibility::Default,
5598 ast::VariableKind::Const,
5599 ),
5600 ))));
5601 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5602 }
5603 }
5604 AstMutateCommand::AddVariableDeclaration { prefix } => {
5605 if let NodeMut::VariableDeclaration(inner) = node {
5606 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5607 }
5608 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5609 let empty_defined_names = HashSet::new();
5610 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5611 let Ok(name) = next_free_name(prefix, defined_names) else {
5612 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5614 };
5615 let mutate_node =
5616 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5617 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5618 ast::ItemVisibility::Default,
5619 ast::VariableKind::Const,
5620 ))));
5621 return TraversalReturn {
5622 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5623 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5624 };
5625 }
5626 }
5627 AstMutateCommand::EditPoint { at } => {
5628 if let NodeMut::CallExpressionKw(call) = node {
5629 if call.callee.name.name != POINT_FN {
5630 return TraversalReturn::new_continue(());
5631 }
5632 for labeled_arg in &mut call.arguments {
5634 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5635 labeled_arg.arg = at.clone();
5636 }
5637 }
5638 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5639 }
5640 }
5641 AstMutateCommand::EditLine {
5642 start,
5643 end,
5644 construction,
5645 } => {
5646 if let NodeMut::CallExpressionKw(call) = node {
5647 if call.callee.name.name != LINE_FN {
5648 return TraversalReturn::new_continue(());
5649 }
5650 for labeled_arg in &mut call.arguments {
5652 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5653 labeled_arg.arg = start.clone();
5654 }
5655 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5656 labeled_arg.arg = end.clone();
5657 }
5658 }
5659 if let Some(construction_value) = construction {
5661 let construction_exists = call
5662 .arguments
5663 .iter()
5664 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5665 if *construction_value {
5666 if construction_exists {
5668 for labeled_arg in &mut call.arguments {
5670 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5671 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5672 value: ast::LiteralValue::Bool(true),
5673 raw: "true".to_string(),
5674 digest: None,
5675 })));
5676 }
5677 }
5678 } else {
5679 call.arguments.push(ast::LabeledArg {
5681 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5682 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5683 value: ast::LiteralValue::Bool(true),
5684 raw: "true".to_string(),
5685 digest: None,
5686 }))),
5687 });
5688 }
5689 } else {
5690 call.arguments
5692 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5693 }
5694 }
5695 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5696 }
5697 }
5698 AstMutateCommand::EditArc {
5699 start,
5700 end,
5701 center,
5702 construction,
5703 } => {
5704 if let NodeMut::CallExpressionKw(call) = node {
5705 if call.callee.name.name != ARC_FN {
5706 return TraversalReturn::new_continue(());
5707 }
5708 for labeled_arg in &mut call.arguments {
5710 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5711 labeled_arg.arg = start.clone();
5712 }
5713 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5714 labeled_arg.arg = end.clone();
5715 }
5716 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5717 labeled_arg.arg = center.clone();
5718 }
5719 }
5720 if let Some(construction_value) = construction {
5722 let construction_exists = call
5723 .arguments
5724 .iter()
5725 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5726 if *construction_value {
5727 if construction_exists {
5729 for labeled_arg in &mut call.arguments {
5731 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5732 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5733 value: ast::LiteralValue::Bool(true),
5734 raw: "true".to_string(),
5735 digest: None,
5736 })));
5737 }
5738 }
5739 } else {
5740 call.arguments.push(ast::LabeledArg {
5742 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5743 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5744 value: ast::LiteralValue::Bool(true),
5745 raw: "true".to_string(),
5746 digest: None,
5747 }))),
5748 });
5749 }
5750 } else {
5751 call.arguments
5753 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5754 }
5755 }
5756 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5757 }
5758 }
5759 AstMutateCommand::EditCircle {
5760 start,
5761 center,
5762 construction,
5763 } => {
5764 if let NodeMut::CallExpressionKw(call) = node {
5765 if call.callee.name.name != CIRCLE_FN {
5766 return TraversalReturn::new_continue(());
5767 }
5768 for labeled_arg in &mut call.arguments {
5770 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5771 labeled_arg.arg = start.clone();
5772 }
5773 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5774 labeled_arg.arg = center.clone();
5775 }
5776 }
5777 if let Some(construction_value) = construction {
5779 let construction_exists = call
5780 .arguments
5781 .iter()
5782 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5783 if *construction_value {
5784 if construction_exists {
5785 for labeled_arg in &mut call.arguments {
5787 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5788 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5789 value: ast::LiteralValue::Bool(true),
5790 raw: "true".to_string(),
5791 digest: None,
5792 })));
5793 }
5794 }
5795 } else {
5796 call.arguments.push(ast::LabeledArg {
5798 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5799 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5800 value: ast::LiteralValue::Bool(true),
5801 raw: "true".to_string(),
5802 digest: None,
5803 }))),
5804 });
5805 }
5806 } else {
5807 call.arguments
5809 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5810 }
5811 }
5812 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5813 }
5814 }
5815 AstMutateCommand::EditControlPointSpline { points, construction } => {
5816 if let NodeMut::CallExpressionKw(call) = node {
5817 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
5818 return TraversalReturn::new_continue(());
5819 }
5820 for labeled_arg in &mut call.arguments {
5821 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
5822 {
5823 labeled_arg.arg = points.clone();
5824 }
5825 }
5826 if let Some(construction_value) = construction {
5828 let construction_exists = call
5829 .arguments
5830 .iter()
5831 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5832 if *construction_value {
5833 if construction_exists {
5834 for labeled_arg in &mut call.arguments {
5835 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5836 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5837 value: ast::LiteralValue::Bool(true),
5838 raw: "true".to_string(),
5839 digest: None,
5840 })));
5841 }
5842 }
5843 } else {
5844 call.arguments.push(ast::LabeledArg {
5845 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5846 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5847 value: ast::LiteralValue::Bool(true),
5848 raw: "true".to_string(),
5849 digest: None,
5850 }))),
5851 });
5852 }
5853 } else {
5854 call.arguments
5855 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5856 }
5857 }
5858 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5859 }
5860 }
5861 AstMutateCommand::EditConstraintValue { value } => {
5862 if let NodeMut::BinaryExpression(binary_expr) = node {
5863 let left_is_constraint = matches!(
5864 &binary_expr.left,
5865 ast::BinaryPart::CallExpressionKw(call)
5866 if matches!(
5867 call.callee.name.name.as_str(),
5868 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5869 )
5870 );
5871 if left_is_constraint {
5872 binary_expr.right = value.clone();
5873 } else {
5874 binary_expr.left = value.clone();
5875 }
5876
5877 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5878 }
5879 }
5880 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5881 if let NodeMut::BinaryExpression(binary_expr) = node {
5882 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5883 return TraversalReturn::new_continue(());
5884 };
5885 if !matches!(
5886 call.callee.name.name.as_str(),
5887 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
5888 ) {
5889 return TraversalReturn::new_continue(());
5890 }
5891
5892 if let Some(label_arg) = call
5893 .arguments
5894 .iter_mut()
5895 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5896 {
5897 label_arg.arg = label_position.clone();
5898 } else {
5899 call.arguments.push(ast::LabeledArg {
5900 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5901 arg: label_position.clone(),
5902 });
5903 }
5904
5905 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5906 }
5907 }
5908 AstMutateCommand::EditCallUnlabeled { arg } => {
5909 if let NodeMut::CallExpressionKw(call) = node {
5910 call.unlabeled = Some(arg.clone());
5911 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5912 }
5913 }
5914 AstMutateCommand::EditVarInitialValue { value } => {
5915 if let NodeMut::NumericLiteral(numeric_literal) = node {
5916 let Ok(literal) = to_source_number(*value) else {
5918 return TraversalReturn::new_break(Err(KclError::refactor(format!(
5919 "Could not convert number to AST literal: {:?}",
5920 *value
5921 ))));
5922 };
5923 *numeric_literal = ast::Node::no_src(literal);
5924 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5925 }
5926 }
5927 AstMutateCommand::DeleteNode => {
5928 return TraversalReturn {
5929 mutate_body_item: MutateBodyItem::Delete,
5930 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5931 };
5932 }
5933 }
5934 TraversalReturn::new_continue(())
5935}
5936
5937struct FindSketchBlockSourceRange {
5938 target_before_mutation: SourceRange,
5940 found: Cell<Option<AstNodeRef>>,
5944}
5945
5946impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5947 type Error = crate::front::Error;
5948
5949 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5950 let Ok(node_range) = SourceRange::try_from(&node) else {
5951 return Ok(true);
5952 };
5953
5954 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5955 if node_range.module_id() == self.target_before_mutation.module_id()
5956 && node_range.start() == self.target_before_mutation.start()
5957 && node_range.end() >= self.target_before_mutation.end()
5959 {
5960 self.found.set(sketch_block.body.items.last().map(|item| match item {
5961 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5965 _ => AstNodeRef::from(item),
5966 }));
5967 return Ok(false);
5968 } else {
5969 return Ok(true);
5972 }
5973 }
5974
5975 for child in node.children().iter() {
5976 if !child.visit(*self)? {
5977 return Ok(false);
5978 }
5979 }
5980
5981 Ok(true)
5982 }
5983}
5984
5985struct FindSketchBlockByNodePath {
5986 target_node_path: ast::NodePath,
5988 found: Cell<Option<AstNodeRef>>,
5992}
5993
5994impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5995 type Error = crate::front::Error;
5996
5997 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5998 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5999 return Ok(true);
6000 };
6001
6002 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6003 if let Some(node_path) = node_path
6004 && node_path == self.target_node_path
6005 {
6006 self.found.set(sketch_block.body.items.last().map(|item| match item {
6007 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6011 _ => AstNodeRef::from(item),
6012 }));
6013
6014 return Ok(false);
6015 } else {
6016 return Ok(true);
6019 }
6020 }
6021
6022 for child in node.children().iter() {
6023 if !child.visit(*self)? {
6024 return Ok(false);
6025 }
6026 }
6027
6028 Ok(true)
6029 }
6030}
6031
6032fn find_sketch_block_added_item(
6040 ast: &ast::Node<ast::Program>,
6041 sketch_block_before_mutation: &AstNodeRef,
6042) -> Result<AstNodeRef, KclError> {
6043 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6044 let find = FindSketchBlockByNodePath {
6045 target_node_path: node_path.clone(),
6046 found: Cell::new(None),
6047 };
6048 let node = crate::walk::Node::from(ast);
6049 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6050 find.found.into_inner().ok_or_else(|| {
6051 KclError::refactor(format!(
6052 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6053 ))
6054 })
6055 } else {
6056 let find = FindSketchBlockSourceRange {
6058 target_before_mutation: sketch_block_before_mutation.range,
6059 found: Cell::new(None),
6060 };
6061 let node = crate::walk::Node::from(ast);
6062 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6063 find.found.into_inner().ok_or_else(|| KclError::refactor(
6064 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?"),
6065 ))
6066 }
6067}
6068
6069fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6070 ast.recast_top(&Default::default(), 0)
6072}
6073
6074pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6075 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6076 inner: ast::ArrayExpression {
6077 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6078 non_code_meta: Default::default(),
6079 digest: None,
6080 },
6081 start: Default::default(),
6082 end: Default::default(),
6083 module_id: Default::default(),
6084 node_path: None,
6085 outer_attrs: Default::default(),
6086 pre_comments: Default::default(),
6087 comment_start: Default::default(),
6088 })))
6089}
6090
6091pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6092 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6093 ast::ArrayExpression {
6094 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6095 digest: None,
6096 non_code_meta: Default::default(),
6097 },
6098 ))))
6099}
6100
6101fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6102 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6103 ast::ArrayExpression {
6104 elements: vec![
6105 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6106 point.x,
6107 )?)))),
6108 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6109 point.y,
6110 )?)))),
6111 ],
6112 non_code_meta: Default::default(),
6113 digest: None,
6114 },
6115 ))))
6116}
6117
6118fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6119 match expr {
6120 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6121 inner: ast::Literal::from(to_source_number(*number)?),
6122 start: Default::default(),
6123 end: Default::default(),
6124 module_id: Default::default(),
6125 node_path: None,
6126 outer_attrs: Default::default(),
6127 pre_comments: Default::default(),
6128 comment_start: Default::default(),
6129 }))),
6130 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6131 inner: ast::SketchVar {
6132 initial: Some(Box::new(ast::Node {
6133 inner: to_source_number(*number)?,
6134 start: Default::default(),
6135 end: Default::default(),
6136 module_id: Default::default(),
6137 node_path: None,
6138 outer_attrs: Default::default(),
6139 pre_comments: Default::default(),
6140 comment_start: Default::default(),
6141 })),
6142 digest: None,
6143 },
6144 start: Default::default(),
6145 end: Default::default(),
6146 module_id: Default::default(),
6147 node_path: None,
6148 outer_attrs: Default::default(),
6149 pre_comments: Default::default(),
6150 comment_start: Default::default(),
6151 }))),
6152 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6153 }
6154}
6155
6156fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6157 Ok(ast::NumericLiteral {
6158 value: number.value,
6159 suffix: number.units,
6160 raw: format_number_literal(number.value, number.units, None)?,
6161 digest: None,
6162 })
6163}
6164
6165pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6166 ast::Expr::Name(Box::new(ast_name(name)))
6167}
6168
6169fn ast_name(name: String) -> ast::Node<ast::Name> {
6170 ast::Node {
6171 inner: ast::Name {
6172 name: ast::Node {
6173 inner: ast::Identifier { name, digest: None },
6174 start: Default::default(),
6175 end: Default::default(),
6176 module_id: Default::default(),
6177 node_path: None,
6178 outer_attrs: Default::default(),
6179 pre_comments: Default::default(),
6180 comment_start: Default::default(),
6181 },
6182 path: Vec::new(),
6183 abs_path: false,
6184 digest: None,
6185 },
6186 start: Default::default(),
6187 end: Default::default(),
6188 module_id: Default::default(),
6189 node_path: None,
6190 outer_attrs: Default::default(),
6191 pre_comments: Default::default(),
6192 comment_start: Default::default(),
6193 }
6194}
6195
6196pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6197 ast::Name {
6198 name: ast::Node {
6199 inner: ast::Identifier {
6200 name: name.to_owned(),
6201 digest: None,
6202 },
6203 start: Default::default(),
6204 end: Default::default(),
6205 module_id: Default::default(),
6206 node_path: None,
6207 outer_attrs: Default::default(),
6208 pre_comments: Default::default(),
6209 comment_start: Default::default(),
6210 },
6211 path: Default::default(),
6212 abs_path: false,
6213 digest: None,
6214 }
6215}
6216
6217pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6221 let elements = exprs.into_iter().collect::<Vec<_>>();
6222 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6223
6224 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6226 elements,
6227 digest: None,
6228 non_code_meta: Default::default(),
6229 })));
6230
6231 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6233 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6234 unlabeled: Some(array_expr),
6235 arguments: Default::default(),
6236 digest: None,
6237 non_code_meta: Default::default(),
6238 })))
6239}
6240
6241pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6243 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6244 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6245 unlabeled: None,
6246 arguments: vec![
6247 ast::LabeledArg {
6248 label: Some(ast::Identifier::new(LINE_START_PARAM)),
6249 arg: start_ast,
6250 },
6251 ast::LabeledArg {
6252 label: Some(ast::Identifier::new(LINE_END_PARAM)),
6253 arg: end_ast,
6254 },
6255 ],
6256 digest: None,
6257 non_code_meta: Default::default(),
6258 })))
6259}
6260
6261pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6263 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6264 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6265 unlabeled: None,
6266 arguments: vec![
6267 ast::LabeledArg {
6268 label: Some(ast::Identifier::new(ARC_START_PARAM)),
6269 arg: start_ast,
6270 },
6271 ast::LabeledArg {
6272 label: Some(ast::Identifier::new(ARC_END_PARAM)),
6273 arg: end_ast,
6274 },
6275 ast::LabeledArg {
6276 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6277 arg: center_ast,
6278 },
6279 ],
6280 digest: None,
6281 non_code_meta: Default::default(),
6282 })))
6283}
6284
6285pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6287 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6288 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6289 unlabeled: None,
6290 arguments: vec![
6291 ast::LabeledArg {
6292 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6293 arg: start_ast,
6294 },
6295 ast::LabeledArg {
6296 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6297 arg: center_ast,
6298 },
6299 ],
6300 digest: None,
6301 non_code_meta: Default::default(),
6302 })))
6303}
6304
6305pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6307 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6308 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6309 unlabeled: Some(line_expr),
6310 arguments: Default::default(),
6311 digest: None,
6312 non_code_meta: Default::default(),
6313 })))
6314}
6315
6316pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6318 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6319 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6320 unlabeled: Some(line_expr),
6321 arguments: Default::default(),
6322 digest: None,
6323 non_code_meta: Default::default(),
6324 })))
6325}
6326
6327pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6329 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6330 object: object_expr,
6331 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6332 name: ast::Node::no_src(ast::Identifier {
6333 name: property.to_string(),
6334 digest: None,
6335 }),
6336 path: Vec::new(),
6337 abs_path: false,
6338 digest: None,
6339 }))),
6340 computed: false,
6341 digest: None,
6342 })))
6343}
6344
6345pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6346 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6347 object: object_expr,
6348 property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6349 value: index as f64,
6350 suffix: NumericSuffix::None,
6351 raw: index.to_string(),
6352 digest: None,
6353 })))),
6354 computed: true,
6355 digest: None,
6356 })))
6357}
6358
6359fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6361 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6363 position.x,
6364 )?))));
6365 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6366 position.y,
6367 )?))));
6368 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6369 elements: vec![x_literal, y_literal],
6370 digest: None,
6371 non_code_meta: Default::default(),
6372 })));
6373
6374 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6376 elements: vec![point_expr, point_array],
6377 digest: None,
6378 non_code_meta: Default::default(),
6379 })));
6380
6381 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6383 ast::CallExpressionKw {
6384 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6385 unlabeled: Some(array_expr),
6386 arguments: Default::default(),
6387 digest: None,
6388 non_code_meta: Default::default(),
6389 },
6390 ))))
6391}
6392
6393pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6395 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6396 elements: line_exprs,
6397 digest: None,
6398 non_code_meta: Default::default(),
6399 })));
6400
6401 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6403 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6404 unlabeled: Some(array_expr),
6405 arguments: Default::default(),
6406 digest: None,
6407 non_code_meta: Default::default(),
6408 })))
6409}
6410
6411pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6413 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6414 elements: segment_exprs,
6415 digest: None,
6416 non_code_meta: Default::default(),
6417 })));
6418
6419 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6420 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6421 unlabeled: Some(array_expr),
6422 arguments: Default::default(),
6423 digest: None,
6424 non_code_meta: Default::default(),
6425 })))
6426}
6427
6428pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6430 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6431 elements: vec![seg1_expr, seg2_expr],
6432 digest: None,
6433 non_code_meta: Default::default(),
6434 })));
6435
6436 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6437 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6438 unlabeled: Some(array_expr),
6439 arguments: Default::default(),
6440 digest: None,
6441 non_code_meta: Default::default(),
6442 })))
6443}
6444
6445pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6447 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6448 elements: input_exprs,
6449 digest: None,
6450 non_code_meta: Default::default(),
6451 })));
6452 let arguments = vec![ast::LabeledArg {
6453 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6454 arg: axis_expr,
6455 }];
6456
6457 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6458 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6459 unlabeled: Some(array_expr),
6460 arguments,
6461 digest: None,
6462 non_code_meta: Default::default(),
6463 })))
6464}
6465
6466pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6468 let arguments = vec![ast::LabeledArg {
6469 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6470 arg: point_expr,
6471 }];
6472
6473 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6474 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6475 unlabeled: Some(segment_expr),
6476 arguments,
6477 digest: None,
6478 non_code_meta: Default::default(),
6479 })))
6480}
6481
6482#[cfg(test)]
6483mod tests {
6484 use super::*;
6485 use crate::engine::PlaneName;
6486 use crate::execution::cache::SketchModeState;
6487 use crate::execution::cache::clear_mem_cache;
6488 use crate::execution::cache::read_old_memory;
6489 use crate::execution::cache::write_old_memory;
6490 use crate::front::Distance;
6491 use crate::front::Fixed;
6492 use crate::front::FixedPoint;
6493 use crate::front::Midpoint;
6494 use crate::front::Object;
6495 use crate::front::Plane;
6496 use crate::front::Sketch;
6497 use crate::front::Tangent;
6498 use crate::frontend::sketch::Vertical;
6499 use crate::pretty::NumericSuffix;
6500
6501 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6502 for object in &scene_graph.objects {
6503 if let ObjectKind::Sketch(_) = &object.kind {
6504 return Some(object);
6505 }
6506 }
6507 None
6508 }
6509
6510 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6511 for object in &scene_graph.objects {
6512 if let ObjectKind::Face(_) = &object.kind {
6513 return Some(object);
6514 }
6515 }
6516 None
6517 }
6518
6519 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6520 for object in &scene_graph.objects {
6521 if matches!(&object.kind, ObjectKind::Wall(_)) {
6522 return Some(object.id);
6523 }
6524 }
6525 None
6526 }
6527
6528 #[test]
6529 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6530 let source = "\
6531region001 = region(point = [0.1, 0.1], sketch = s)
6532extrude001 = extrude(region001, length = 5)
6533revolve001 = revolve(region001, axis = Y)
6534sweep001 = sweep(region001, path = path001)
6535loft001 = loft(region001)
6536not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6537";
6538
6539 let program = Program::parse(source).unwrap().0.unwrap();
6540
6541 assert_eq!(
6542 region_name_from_sweep_variable(&program.ast, "extrude001"),
6543 Some("region001".to_owned())
6544 );
6545 assert_eq!(
6546 region_name_from_sweep_variable(&program.ast, "revolve001"),
6547 Some("region001".to_owned())
6548 );
6549 assert_eq!(
6550 region_name_from_sweep_variable(&program.ast, "sweep001"),
6551 Some("region001".to_owned())
6552 );
6553 assert_eq!(
6554 region_name_from_sweep_variable(&program.ast, "loft001"),
6555 Some("region001".to_owned())
6556 );
6557 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6558 }
6559
6560 #[track_caller]
6561 fn expect_sketch(object: &Object) -> &Sketch {
6562 if let ObjectKind::Sketch(sketch) = &object.kind {
6563 sketch
6564 } else {
6565 panic!("Object is not a sketch: {:?}", object);
6566 }
6567 }
6568
6569 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6570 let point_object = scene_graph.objects.get(point_id.0).unwrap();
6571 let ObjectKind::Segment {
6572 segment: Segment::Point(point),
6573 } = &point_object.kind
6574 else {
6575 panic!("Object is not a point segment: {point_object:?}");
6576 };
6577 point.position.clone()
6578 }
6579
6580 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6581 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6582 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6583 }
6584
6585 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6586 LineCtor {
6587 start: Point2d {
6588 x: Expr::Number(Number { value: start_x, units }),
6589 y: Expr::Number(Number { value: start_y, units }),
6590 },
6591 end: Point2d {
6592 x: Expr::Number(Number { value: end_x, units }),
6593 y: Expr::Number(Number { value: end_y, units }),
6594 },
6595 construction: None,
6596 }
6597 }
6598
6599 async fn create_sketch_with_single_line(
6600 frontend: &mut FrontendState,
6601 ctx: &ExecutorContext,
6602 mock_ctx: &ExecutorContext,
6603 version: Version,
6604 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6605 frontend.program = Program::empty();
6606
6607 let sketch_args = SketchCtor {
6608 on: Plane::Default(PlaneName::Xy),
6609 };
6610 let (_src_delta, _scene_delta, sketch_id) = frontend
6611 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6612 .await
6613 .unwrap();
6614
6615 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6616 let (source_delta, scene_graph_delta) = frontend
6617 .add_segment(mock_ctx, version, sketch_id, segment, None)
6618 .await
6619 .unwrap();
6620 let line_id = *scene_graph_delta
6621 .new_objects
6622 .last()
6623 .expect("Expected line object id to be created");
6624
6625 (sketch_id, line_id, source_delta, scene_graph_delta)
6626 }
6627
6628 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
6629 frontend.program = program.clone();
6630 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
6631 frontend.update_state_after_exec(outcome, true);
6632 }
6633
6634 #[tokio::test(flavor = "multi_thread")]
6635 async fn test_sketch_checkpoint_round_trip_restores_state() {
6636 let mut frontend = FrontendState::new();
6637 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6638 let mock_ctx = ExecutorContext::new_mock(None).await;
6639 let version = Version(0);
6640
6641 let (sketch_id, line_id, source_delta, scene_graph_delta) =
6642 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6643
6644 let expected_source = source_delta.text.clone();
6645 let expected_scene_graph = frontend.scene_graph.clone();
6646 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6647 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6648
6649 let checkpoint_id = frontend
6650 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6651 .await
6652 .unwrap();
6653
6654 let edited_segments = vec![ExistingSegmentCtor {
6655 id: line_id,
6656 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6657 }];
6658 let (edited_source, _edited_scene) = frontend
6659 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6660 .await
6661 .unwrap();
6662 assert_ne!(edited_source.text, expected_source);
6663
6664 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6665
6666 assert_eq!(restored.source_delta.text, expected_source);
6667 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6668 assert!(restored.scene_graph_delta.invalidates_ids);
6669 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6670 assert_eq!(frontend.scene_graph, expected_scene_graph);
6671 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6672
6673 ctx.close().await;
6674 }
6675
6676 #[tokio::test(flavor = "multi_thread")]
6677 async fn test_sketch_checkpoints_prune_oldest_entries() {
6678 let mut frontend = FrontendState::new();
6679 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6680 let mock_ctx = ExecutorContext::new_mock(None).await;
6681 let version = Version(0);
6682
6683 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6684 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6685
6686 let mut checkpoint_ids = Vec::new();
6687 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6688 checkpoint_ids.push(
6689 frontend
6690 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6691 .await
6692 .unwrap(),
6693 );
6694 }
6695
6696 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6697 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6698
6699 let oldest_retained = checkpoint_ids[3];
6700 assert_eq!(
6701 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6702 Some(oldest_retained)
6703 );
6704
6705 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6706 assert!(evicted_restore.is_err());
6707 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6708
6709 frontend
6710 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6711 .await
6712 .unwrap();
6713
6714 ctx.close().await;
6715 }
6716
6717 #[tokio::test(flavor = "multi_thread")]
6718 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6719 let mut frontend = FrontendState::new();
6720 let missing_checkpoint = SketchCheckpointId::new(999);
6721
6722 let err = frontend
6723 .restore_sketch_checkpoint(missing_checkpoint)
6724 .await
6725 .expect_err("Expected restore to fail for missing checkpoint");
6726
6727 assert!(err.msg.contains("Sketch checkpoint not found"));
6728 }
6729
6730 #[tokio::test(flavor = "multi_thread")]
6731 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6732 let mut frontend = FrontendState::new();
6733 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6734 let mock_ctx = ExecutorContext::new_mock(None).await;
6735 let version = Version(0);
6736
6737 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6738 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6739
6740 let checkpoint_a = frontend
6741 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6742 .await
6743 .unwrap();
6744 let checkpoint_b = frontend
6745 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6746 .await
6747 .unwrap();
6748 assert_eq!(frontend.sketch_checkpoints.len(), 2);
6749
6750 frontend.clear_sketch_checkpoints();
6751 assert!(frontend.sketch_checkpoints.is_empty());
6752 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6753 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6754
6755 ctx.close().await;
6756 }
6757
6758 #[tokio::test(flavor = "multi_thread")]
6759 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6760 let mut frontend = FrontendState::new();
6761 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6762 let mock_ctx = ExecutorContext::new_mock(None).await;
6763 let version = Version(0);
6764
6765 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6766 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6767 let old_source = source_delta.text.clone();
6768 let old_checkpoint = frontend
6769 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6770 .await
6771 .unwrap();
6772 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6773
6774 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
6775 .unwrap()
6776 .0
6777 .unwrap();
6778
6779 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6780 let SetProgramOutcome::Success {
6781 checkpoint_id: Some(new_checkpoint),
6782 ..
6783 } = result
6784 else {
6785 panic!("Expected Success with a fresh checkpoint baseline");
6786 };
6787
6788 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6789
6790 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6791 assert_eq!(old_restore.source_delta.text, old_source);
6792
6793 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6794 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6795
6796 ctx.close().await;
6797 }
6798
6799 #[tokio::test(flavor = "multi_thread")]
6800 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6801 let mut frontend = FrontendState::new();
6802 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6803 let mock_ctx = ExecutorContext::new_mock(None).await;
6804 let version = Version(0);
6805
6806 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6807 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6808 let old_checkpoint = frontend
6809 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6810 .await
6811 .unwrap();
6812 let checkpoint_count_before = frontend.sketch_checkpoints.len();
6813
6814 let failing_program = Program::parse(
6815 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6816 )
6817 .unwrap()
6818 .0
6819 .unwrap();
6820
6821 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6822 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6823 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6824 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6825
6826 ctx.close().await;
6827 }
6828
6829 #[tokio::test(flavor = "multi_thread")]
6830 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6831 let mut frontend = FrontendState::new();
6832 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6833
6834 let program = Program::parse(
6835 "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",
6836 )
6837 .unwrap()
6838 .0
6839 .unwrap();
6840 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6841 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6842 panic!("Expected successful baseline program execution");
6843 };
6844
6845 clear_mem_cache().await;
6846 assert!(read_old_memory().await.is_none());
6847
6848 let checkpoint_without_mock_memory = frontend
6849 .create_sketch_checkpoint((*exec_outcome).clone())
6850 .await
6851 .unwrap();
6852
6853 write_old_memory(SketchModeState::new_for_tests()).await;
6854 assert!(read_old_memory().await.is_some());
6855
6856 let checkpoint_with_mock_memory = frontend
6857 .create_sketch_checkpoint((*exec_outcome).clone())
6858 .await
6859 .unwrap();
6860
6861 clear_mem_cache().await;
6862 assert!(read_old_memory().await.is_none());
6863
6864 frontend
6865 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6866 .await
6867 .unwrap();
6868 assert!(read_old_memory().await.is_some());
6869
6870 frontend
6871 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6872 .await
6873 .unwrap();
6874 assert!(read_old_memory().await.is_none());
6875
6876 ctx.close().await;
6877 }
6878
6879 #[tokio::test(flavor = "multi_thread")]
6880 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6881 let source = "\
6882sketch(on = XY) {
6883 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6884}
6885
6886bad = missing_name
6887";
6888 let program = Program::parse(source).unwrap().0.unwrap();
6889
6890 let mut frontend = FrontendState::new();
6891
6892 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6893 let mock_ctx = ExecutorContext::new_mock(None).await;
6894 let version = Version(0);
6895 let project_id = ProjectId(0);
6896 let file_id = FileId(0);
6897
6898 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6899 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6900 };
6901
6902 let sketch_id = frontend
6903 .scene_graph
6904 .objects
6905 .iter()
6906 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6907 .expect("Expected sketch object from errored hack_set_program");
6908
6909 frontend
6910 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6911 .await
6912 .unwrap();
6913
6914 ctx.close().await;
6915 mock_ctx.close().await;
6916 }
6917
6918 #[tokio::test(flavor = "multi_thread")]
6919 async fn test_new_sketch_add_point_edit_point() {
6920 let program = Program::empty();
6921
6922 let mut frontend = FrontendState::new();
6923 frontend.program = program;
6924
6925 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6926 let mock_ctx = ExecutorContext::new_mock(None).await;
6927 let version = Version(0);
6928
6929 let sketch_args = SketchCtor {
6930 on: Plane::Default(PlaneName::Xy),
6931 };
6932 let (_src_delta, scene_delta, sketch_id) = frontend
6933 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6934 .await
6935 .unwrap();
6936 assert_eq!(sketch_id, ObjectId(1));
6937 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6938 let sketch_object = &scene_delta.new_graph.objects[1];
6939 assert_eq!(sketch_object.id, ObjectId(1));
6940 assert_eq!(
6941 sketch_object.kind,
6942 ObjectKind::Sketch(Sketch {
6943 args: SketchCtor {
6944 on: Plane::Default(PlaneName::Xy)
6945 },
6946 plane: ObjectId(0),
6947 segments: vec![],
6948 constraints: vec![],
6949 })
6950 );
6951 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6952
6953 let point_ctor = PointCtor {
6954 position: Point2d {
6955 x: Expr::Number(Number {
6956 value: 1.0,
6957 units: NumericSuffix::Inch,
6958 }),
6959 y: Expr::Number(Number {
6960 value: 2.0,
6961 units: NumericSuffix::Inch,
6962 }),
6963 },
6964 };
6965 let segment = SegmentCtor::Point(point_ctor);
6966 let (src_delta, scene_delta) = frontend
6967 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6968 .await
6969 .unwrap();
6970 assert_eq!(
6971 src_delta.text.as_str(),
6972 "sketch001 = sketch(on = XY) {
6973 point(at = [1in, 2in])
6974}
6975"
6976 );
6977 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6978 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6979 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6980 assert_eq!(scene_object.id.0, i);
6981 }
6982
6983 let point_id = *scene_delta.new_objects.last().unwrap();
6984
6985 let point_ctor = PointCtor {
6986 position: Point2d {
6987 x: Expr::Number(Number {
6988 value: 3.0,
6989 units: NumericSuffix::Inch,
6990 }),
6991 y: Expr::Number(Number {
6992 value: 4.0,
6993 units: NumericSuffix::Inch,
6994 }),
6995 },
6996 };
6997 let segments = vec![ExistingSegmentCtor {
6998 id: point_id,
6999 ctor: SegmentCtor::Point(point_ctor),
7000 }];
7001 let (src_delta, scene_delta) = frontend
7002 .edit_segments(&mock_ctx, version, sketch_id, segments)
7003 .await
7004 .unwrap();
7005 assert_eq!(
7006 src_delta.text.as_str(),
7007 "sketch001 = sketch(on = XY) {
7008 point(at = [3in, 4in])
7009}
7010"
7011 );
7012 assert_eq!(scene_delta.new_objects, vec![]);
7013 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7014
7015 ctx.close().await;
7016 mock_ctx.close().await;
7017 }
7018
7019 #[tokio::test(flavor = "multi_thread")]
7020 async fn test_new_sketch_add_line_edit_line() {
7021 let program = Program::empty();
7022
7023 let mut frontend = FrontendState::new();
7024 frontend.program = program;
7025
7026 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7027 let mock_ctx = ExecutorContext::new_mock(None).await;
7028 let version = Version(0);
7029
7030 let sketch_args = SketchCtor {
7031 on: Plane::Default(PlaneName::Xy),
7032 };
7033 let (_src_delta, scene_delta, sketch_id) = frontend
7034 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7035 .await
7036 .unwrap();
7037 assert_eq!(sketch_id, ObjectId(1));
7038 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7039 let sketch_object = &scene_delta.new_graph.objects[1];
7040 assert_eq!(sketch_object.id, ObjectId(1));
7041 assert_eq!(
7042 sketch_object.kind,
7043 ObjectKind::Sketch(Sketch {
7044 args: SketchCtor {
7045 on: Plane::Default(PlaneName::Xy)
7046 },
7047 plane: ObjectId(0),
7048 segments: vec![],
7049 constraints: vec![],
7050 })
7051 );
7052 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7053
7054 let line_ctor = LineCtor {
7055 start: Point2d {
7056 x: Expr::Number(Number {
7057 value: 0.0,
7058 units: NumericSuffix::Mm,
7059 }),
7060 y: Expr::Number(Number {
7061 value: 0.0,
7062 units: NumericSuffix::Mm,
7063 }),
7064 },
7065 end: Point2d {
7066 x: Expr::Number(Number {
7067 value: 10.0,
7068 units: NumericSuffix::Mm,
7069 }),
7070 y: Expr::Number(Number {
7071 value: 10.0,
7072 units: NumericSuffix::Mm,
7073 }),
7074 },
7075 construction: None,
7076 };
7077 let segment = SegmentCtor::Line(line_ctor);
7078 let (src_delta, scene_delta) = frontend
7079 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7080 .await
7081 .unwrap();
7082 assert_eq!(
7083 src_delta.text.as_str(),
7084 "sketch001 = sketch(on = XY) {
7085 line(start = [0mm, 0mm], end = [10mm, 10mm])
7086}
7087"
7088 );
7089 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7090 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7091 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7092 assert_eq!(scene_object.id.0, i);
7093 }
7094
7095 let line = *scene_delta.new_objects.last().unwrap();
7097
7098 let line_ctor = LineCtor {
7099 start: Point2d {
7100 x: Expr::Number(Number {
7101 value: 1.0,
7102 units: NumericSuffix::Mm,
7103 }),
7104 y: Expr::Number(Number {
7105 value: 2.0,
7106 units: NumericSuffix::Mm,
7107 }),
7108 },
7109 end: Point2d {
7110 x: Expr::Number(Number {
7111 value: 13.0,
7112 units: NumericSuffix::Mm,
7113 }),
7114 y: Expr::Number(Number {
7115 value: 14.0,
7116 units: NumericSuffix::Mm,
7117 }),
7118 },
7119 construction: None,
7120 };
7121 let segments = vec![ExistingSegmentCtor {
7122 id: line,
7123 ctor: SegmentCtor::Line(line_ctor),
7124 }];
7125 let (src_delta, scene_delta) = frontend
7126 .edit_segments(&mock_ctx, version, sketch_id, segments)
7127 .await
7128 .unwrap();
7129 assert_eq!(
7130 src_delta.text.as_str(),
7131 "sketch001 = sketch(on = XY) {
7132 line(start = [1mm, 2mm], end = [13mm, 14mm])
7133}
7134"
7135 );
7136 assert_eq!(scene_delta.new_objects, vec![]);
7137 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7138
7139 ctx.close().await;
7140 mock_ctx.close().await;
7141 }
7142
7143 #[tokio::test(flavor = "multi_thread")]
7144 async fn test_new_sketch_add_arc_edit_arc() {
7145 let program = Program::empty();
7146
7147 let mut frontend = FrontendState::new();
7148 frontend.program = program;
7149
7150 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7151 let mock_ctx = ExecutorContext::new_mock(None).await;
7152 let version = Version(0);
7153
7154 let sketch_args = SketchCtor {
7155 on: Plane::Default(PlaneName::Xy),
7156 };
7157 let (_src_delta, scene_delta, sketch_id) = frontend
7158 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7159 .await
7160 .unwrap();
7161 assert_eq!(sketch_id, ObjectId(1));
7162 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7163 let sketch_object = &scene_delta.new_graph.objects[1];
7164 assert_eq!(sketch_object.id, ObjectId(1));
7165 assert_eq!(
7166 sketch_object.kind,
7167 ObjectKind::Sketch(Sketch {
7168 args: SketchCtor {
7169 on: Plane::Default(PlaneName::Xy),
7170 },
7171 plane: ObjectId(0),
7172 segments: vec![],
7173 constraints: vec![],
7174 })
7175 );
7176 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7177
7178 let arc_ctor = ArcCtor {
7179 start: Point2d {
7180 x: Expr::Var(Number {
7181 value: 0.0,
7182 units: NumericSuffix::Mm,
7183 }),
7184 y: Expr::Var(Number {
7185 value: 0.0,
7186 units: NumericSuffix::Mm,
7187 }),
7188 },
7189 end: Point2d {
7190 x: Expr::Var(Number {
7191 value: 10.0,
7192 units: NumericSuffix::Mm,
7193 }),
7194 y: Expr::Var(Number {
7195 value: 10.0,
7196 units: NumericSuffix::Mm,
7197 }),
7198 },
7199 center: Point2d {
7200 x: Expr::Var(Number {
7201 value: 10.0,
7202 units: NumericSuffix::Mm,
7203 }),
7204 y: Expr::Var(Number {
7205 value: 0.0,
7206 units: NumericSuffix::Mm,
7207 }),
7208 },
7209 construction: None,
7210 };
7211 let segment = SegmentCtor::Arc(arc_ctor);
7212 let (src_delta, scene_delta) = frontend
7213 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7214 .await
7215 .unwrap();
7216 assert_eq!(
7217 src_delta.text.as_str(),
7218 "sketch001 = sketch(on = XY) {
7219 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7220}
7221"
7222 );
7223 assert_eq!(
7224 scene_delta.new_objects,
7225 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7226 );
7227 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7228 assert_eq!(scene_object.id.0, i);
7229 }
7230 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7231
7232 let arc = *scene_delta.new_objects.last().unwrap();
7234
7235 let arc_ctor = ArcCtor {
7236 start: Point2d {
7237 x: Expr::Var(Number {
7238 value: 1.0,
7239 units: NumericSuffix::Mm,
7240 }),
7241 y: Expr::Var(Number {
7242 value: 2.0,
7243 units: NumericSuffix::Mm,
7244 }),
7245 },
7246 end: Point2d {
7247 x: Expr::Var(Number {
7248 value: 13.0,
7249 units: NumericSuffix::Mm,
7250 }),
7251 y: Expr::Var(Number {
7252 value: 14.0,
7253 units: NumericSuffix::Mm,
7254 }),
7255 },
7256 center: Point2d {
7257 x: Expr::Var(Number {
7258 value: 13.0,
7259 units: NumericSuffix::Mm,
7260 }),
7261 y: Expr::Var(Number {
7262 value: 2.0,
7263 units: NumericSuffix::Mm,
7264 }),
7265 },
7266 construction: None,
7267 };
7268 let segments = vec![ExistingSegmentCtor {
7269 id: arc,
7270 ctor: SegmentCtor::Arc(arc_ctor),
7271 }];
7272 let (src_delta, scene_delta) = frontend
7273 .edit_segments(&mock_ctx, version, sketch_id, segments)
7274 .await
7275 .unwrap();
7276 assert_eq!(
7277 src_delta.text.as_str(),
7278 "sketch001 = sketch(on = XY) {
7279 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7280}
7281"
7282 );
7283 assert_eq!(scene_delta.new_objects, vec![]);
7284 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7285
7286 ctx.close().await;
7287 mock_ctx.close().await;
7288 }
7289
7290 #[tokio::test(flavor = "multi_thread")]
7291 async fn test_new_sketch_add_circle_edit_circle() {
7292 let program = Program::empty();
7293
7294 let mut frontend = FrontendState::new();
7295 frontend.program = program;
7296
7297 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7298 let mock_ctx = ExecutorContext::new_mock(None).await;
7299 let version = Version(0);
7300
7301 let sketch_args = SketchCtor {
7302 on: Plane::Default(PlaneName::Xy),
7303 };
7304 let (_src_delta, _scene_delta, sketch_id) = frontend
7305 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7306 .await
7307 .unwrap();
7308
7309 let circle_ctor = CircleCtor {
7311 start: Point2d {
7312 x: Expr::Var(Number {
7313 value: 5.0,
7314 units: NumericSuffix::Mm,
7315 }),
7316 y: Expr::Var(Number {
7317 value: 0.0,
7318 units: NumericSuffix::Mm,
7319 }),
7320 },
7321 center: Point2d {
7322 x: Expr::Var(Number {
7323 value: 0.0,
7324 units: NumericSuffix::Mm,
7325 }),
7326 y: Expr::Var(Number {
7327 value: 0.0,
7328 units: NumericSuffix::Mm,
7329 }),
7330 },
7331 construction: None,
7332 };
7333 let segment = SegmentCtor::Circle(circle_ctor);
7334 let (src_delta, scene_delta) = frontend
7335 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7336 .await
7337 .unwrap();
7338 assert_eq!(
7339 src_delta.text.as_str(),
7340 "sketch001 = sketch(on = XY) {
7341 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7342}
7343"
7344 );
7345 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7347 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7348
7349 let circle = *scene_delta.new_objects.last().unwrap();
7350
7351 let circle_ctor = CircleCtor {
7353 start: Point2d {
7354 x: Expr::Var(Number {
7355 value: 10.0,
7356 units: NumericSuffix::Mm,
7357 }),
7358 y: Expr::Var(Number {
7359 value: 0.0,
7360 units: NumericSuffix::Mm,
7361 }),
7362 },
7363 center: Point2d {
7364 x: Expr::Var(Number {
7365 value: 3.0,
7366 units: NumericSuffix::Mm,
7367 }),
7368 y: Expr::Var(Number {
7369 value: 4.0,
7370 units: NumericSuffix::Mm,
7371 }),
7372 },
7373 construction: None,
7374 };
7375 let segments = vec![ExistingSegmentCtor {
7376 id: circle,
7377 ctor: SegmentCtor::Circle(circle_ctor),
7378 }];
7379 let (src_delta, scene_delta) = frontend
7380 .edit_segments(&mock_ctx, version, sketch_id, segments)
7381 .await
7382 .unwrap();
7383 assert_eq!(
7384 src_delta.text.as_str(),
7385 "sketch001 = sketch(on = XY) {
7386 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7387}
7388"
7389 );
7390 assert_eq!(scene_delta.new_objects, vec![]);
7391 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7392
7393 ctx.close().await;
7394 mock_ctx.close().await;
7395 }
7396
7397 #[tokio::test(flavor = "multi_thread")]
7398 async fn test_delete_circle() {
7399 let initial_source = "sketch001 = sketch(on = XY) {
7400 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7401}
7402";
7403
7404 let program = Program::parse(initial_source).unwrap().0.unwrap();
7405 let mut frontend = FrontendState::new();
7406
7407 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7408 let mock_ctx = ExecutorContext::new_mock(None).await;
7409 let version = Version(0);
7410
7411 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7412 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7413 let sketch_id = sketch_object.id;
7414 let sketch = expect_sketch(sketch_object);
7415
7416 assert_eq!(sketch.segments.len(), 3);
7418 let circle_id = sketch.segments[2];
7419
7420 let (src_delta, scene_delta) = frontend
7422 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7423 .await
7424 .unwrap();
7425 assert_eq!(
7426 src_delta.text.as_str(),
7427 "sketch001 = sketch(on = XY) {
7428}
7429"
7430 );
7431 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7432 let new_sketch = expect_sketch(new_sketch_object);
7433 assert_eq!(new_sketch.segments.len(), 0);
7434
7435 ctx.close().await;
7436 mock_ctx.close().await;
7437 }
7438
7439 #[tokio::test(flavor = "multi_thread")]
7440 async fn test_edit_circle_via_point() {
7441 let initial_source = "sketch001 = sketch(on = XY) {
7442 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7443}
7444";
7445
7446 let program = Program::parse(initial_source).unwrap().0.unwrap();
7447 let mut frontend = FrontendState::new();
7448
7449 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7450 let mock_ctx = ExecutorContext::new_mock(None).await;
7451 let version = Version(0);
7452
7453 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7454 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7455 let sketch_id = sketch_object.id;
7456 let sketch = expect_sketch(sketch_object);
7457
7458 let circle_id = sketch
7460 .segments
7461 .iter()
7462 .copied()
7463 .find(|seg_id| {
7464 matches!(
7465 &frontend.scene_graph.objects[seg_id.0].kind,
7466 ObjectKind::Segment {
7467 segment: Segment::Circle(_)
7468 }
7469 )
7470 })
7471 .expect("Expected a circle segment in sketch");
7472 let circle_object = &frontend.scene_graph.objects[circle_id.0];
7473 let ObjectKind::Segment {
7474 segment: Segment::Circle(circle),
7475 } = &circle_object.kind
7476 else {
7477 panic!("Expected circle segment, got: {:?}", circle_object.kind);
7478 };
7479 let start_point_id = circle.start;
7480
7481 let segments = vec![ExistingSegmentCtor {
7483 id: start_point_id,
7484 ctor: SegmentCtor::Point(PointCtor {
7485 position: Point2d {
7486 x: Expr::Var(Number {
7487 value: 7.0,
7488 units: NumericSuffix::Mm,
7489 }),
7490 y: Expr::Var(Number {
7491 value: 1.0,
7492 units: NumericSuffix::Mm,
7493 }),
7494 },
7495 }),
7496 }];
7497 let (src_delta, _scene_delta) = frontend
7498 .edit_segments(&mock_ctx, version, sketch_id, segments)
7499 .await
7500 .unwrap();
7501 assert_eq!(
7502 src_delta.text.as_str(),
7503 "sketch001 = sketch(on = XY) {
7504 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7505}
7506"
7507 );
7508
7509 ctx.close().await;
7510 mock_ctx.close().await;
7511 }
7512
7513 #[tokio::test(flavor = "multi_thread")]
7514 async fn test_add_line_when_sketch_block_uses_variable() {
7515 let initial_source = "s = sketch(on = XY) {}
7516";
7517
7518 let program = Program::parse(initial_source).unwrap().0.unwrap();
7519
7520 let mut frontend = FrontendState::new();
7521
7522 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7523 let mock_ctx = ExecutorContext::new_mock(None).await;
7524 let version = Version(0);
7525
7526 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7527 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7528 let sketch_id = sketch_object.id;
7529
7530 let line_ctor = LineCtor {
7531 start: Point2d {
7532 x: Expr::Number(Number {
7533 value: 0.0,
7534 units: NumericSuffix::Mm,
7535 }),
7536 y: Expr::Number(Number {
7537 value: 0.0,
7538 units: NumericSuffix::Mm,
7539 }),
7540 },
7541 end: Point2d {
7542 x: Expr::Number(Number {
7543 value: 10.0,
7544 units: NumericSuffix::Mm,
7545 }),
7546 y: Expr::Number(Number {
7547 value: 10.0,
7548 units: NumericSuffix::Mm,
7549 }),
7550 },
7551 construction: None,
7552 };
7553 let segment = SegmentCtor::Line(line_ctor);
7554 let (src_delta, scene_delta) = frontend
7555 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7556 .await
7557 .unwrap();
7558 assert_eq!(
7559 src_delta.text.as_str(),
7560 "s = sketch(on = XY) {
7561 line(start = [0mm, 0mm], end = [10mm, 10mm])
7562}
7563"
7564 );
7565 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7566 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7567
7568 ctx.close().await;
7569 mock_ctx.close().await;
7570 }
7571
7572 #[tokio::test(flavor = "multi_thread")]
7573 async fn test_new_sketch_add_line_delete_sketch() {
7574 let program = Program::empty();
7575
7576 let mut frontend = FrontendState::new();
7577 frontend.program = program;
7578
7579 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7580 let mock_ctx = ExecutorContext::new_mock(None).await;
7581 let version = Version(0);
7582
7583 let sketch_args = SketchCtor {
7584 on: Plane::Default(PlaneName::Xy),
7585 };
7586 let (_src_delta, scene_delta, sketch_id) = frontend
7587 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7588 .await
7589 .unwrap();
7590 assert_eq!(sketch_id, ObjectId(1));
7591 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7592 let sketch_object = &scene_delta.new_graph.objects[1];
7593 assert_eq!(sketch_object.id, ObjectId(1));
7594 assert_eq!(
7595 sketch_object.kind,
7596 ObjectKind::Sketch(Sketch {
7597 args: SketchCtor {
7598 on: Plane::Default(PlaneName::Xy)
7599 },
7600 plane: ObjectId(0),
7601 segments: vec![],
7602 constraints: vec![],
7603 })
7604 );
7605 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7606
7607 let line_ctor = LineCtor {
7608 start: Point2d {
7609 x: Expr::Number(Number {
7610 value: 0.0,
7611 units: NumericSuffix::Mm,
7612 }),
7613 y: Expr::Number(Number {
7614 value: 0.0,
7615 units: NumericSuffix::Mm,
7616 }),
7617 },
7618 end: Point2d {
7619 x: Expr::Number(Number {
7620 value: 10.0,
7621 units: NumericSuffix::Mm,
7622 }),
7623 y: Expr::Number(Number {
7624 value: 10.0,
7625 units: NumericSuffix::Mm,
7626 }),
7627 },
7628 construction: None,
7629 };
7630 let segment = SegmentCtor::Line(line_ctor);
7631 let (src_delta, scene_delta) = frontend
7632 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7633 .await
7634 .unwrap();
7635 assert_eq!(
7636 src_delta.text.as_str(),
7637 "sketch001 = sketch(on = XY) {
7638 line(start = [0mm, 0mm], end = [10mm, 10mm])
7639}
7640"
7641 );
7642 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7643
7644 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7645 assert_eq!(src_delta.text.as_str(), "");
7646 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7647
7648 ctx.close().await;
7649 mock_ctx.close().await;
7650 }
7651
7652 #[tokio::test(flavor = "multi_thread")]
7653 async fn test_delete_sketch_when_sketch_block_uses_variable() {
7654 let initial_source = "s = sketch(on = XY) {}
7655";
7656
7657 let program = Program::parse(initial_source).unwrap().0.unwrap();
7658
7659 let mut frontend = FrontendState::new();
7660
7661 let ctx = ExecutorContext::new_with_engine(
7662 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7663 Default::default(),
7664 );
7665 let version = Version(0);
7666
7667 frontend.hack_set_program(&ctx, program).await.unwrap();
7668 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7669 let sketch_id = sketch_object.id;
7670
7671 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7672 assert_eq!(src_delta.text.as_str(), "");
7673 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7674
7675 ctx.close().await;
7676 }
7677
7678 #[tokio::test(flavor = "multi_thread")]
7679 async fn test_delete_sketch_after_comment() {
7680 let initial_source = "sketch001 = sketch(on = XZ) {
7681}
7682";
7683
7684 let program = Program::parse(initial_source).unwrap().0.unwrap();
7685 let mut frontend = FrontendState::new();
7686
7687 let ctx = ExecutorContext::new_with_engine(
7688 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7689 Default::default(),
7690 );
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 let original_source = sketch_object.source.clone();
7697
7698 let commented_source = "// test 1
7699sketch001 = sketch(on = XZ) {
7700}
7701";
7702 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7703 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7704
7705 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7706 assert_eq!(cached_sketch_object.source, original_source);
7707
7708 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7709 assert!(
7710 !src_delta.text.contains("sketch001"),
7711 "sketch was not deleted: {}",
7712 src_delta.text
7713 );
7714 assert_eq!(src_delta.text.as_str(), "// test 1\n");
7716 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7717
7718 ctx.close().await;
7719 }
7720
7721 #[tokio::test(flavor = "multi_thread")]
7722 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7723 let initial_source = "sketch001 = sketch(on = XZ) {
7724}
7725foo = 1
7726";
7727
7728 let program = Program::parse(initial_source).unwrap().0.unwrap();
7729 let mut frontend = FrontendState::new();
7730
7731 let ctx = ExecutorContext::new_with_engine(
7732 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7733 Default::default(),
7734 );
7735 let version = Version(0);
7736
7737 frontend.hack_set_program(&ctx, program).await.unwrap();
7738 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7739 let sketch_id = sketch_object.id;
7740
7741 let commented_source = "// keep me
7742sketch001 = sketch(on = XZ) {
7743}
7744foo = 1
7745";
7746 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7747 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7748
7749 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7750 assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7752
7753 ctx.close().await;
7754 }
7755
7756 #[tokio::test(flavor = "multi_thread")]
7757 async fn test_delete_segment_preserves_pre_comment() {
7758 let initial_source = "\
7759sketch(on = XY) {
7760 point(at = [var 1, var 2])
7761 // describe the middle point
7762 point(at = [var 3, var 4])
7763 point(at = [var 5, var 6])
7764}
7765";
7766
7767 let program = Program::parse(initial_source).unwrap().0.unwrap();
7768 let mut frontend = FrontendState::new();
7769
7770 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7771 let mock_ctx = ExecutorContext::new_mock(None).await;
7772 let version = Version(0);
7773
7774 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7775 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7776 let sketch_id = sketch_object.id;
7777 let sketch = expect_sketch(sketch_object);
7778
7779 let middle_point_id = *sketch.segments.get(1).unwrap();
7780
7781 let (src_delta, _scene_delta) = frontend
7782 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7783 .await
7784 .unwrap();
7785 assert_eq!(
7788 src_delta.text.as_str(),
7789 "\
7790sketch(on = XY) {
7791 point(at = [var 1mm, var 2mm])
7792 // describe the middle point
7793 point(at = [var 5mm, var 6mm])
7794}
7795"
7796 );
7797
7798 ctx.close().await;
7799 mock_ctx.close().await;
7800 }
7801
7802 #[tokio::test(flavor = "multi_thread")]
7803 async fn test_delete_last_segment_preserves_pre_comment() {
7804 let initial_source = "\
7805sketch(on = XY) {
7806 point(at = [var 1, var 2])
7807 // describe the trailing point
7808 point(at = [var 3, var 4])
7809}
7810";
7811
7812 let program = Program::parse(initial_source).unwrap().0.unwrap();
7813 let mut frontend = FrontendState::new();
7814
7815 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7816 let mock_ctx = ExecutorContext::new_mock(None).await;
7817 let version = Version(0);
7818
7819 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7820 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7821 let sketch_id = sketch_object.id;
7822 let sketch = expect_sketch(sketch_object);
7823
7824 let last_point_id = *sketch.segments.last().unwrap();
7825
7826 let (src_delta, _scene_delta) = frontend
7827 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7828 .await
7829 .unwrap();
7830 assert_eq!(
7833 src_delta.text.as_str(),
7834 "\
7835sketch(on = XY) {
7836 point(at = [var 1mm, var 2mm])
7837 // describe the trailing point
7838}
7839"
7840 );
7841
7842 ctx.close().await;
7843 mock_ctx.close().await;
7844 }
7845
7846 #[tokio::test(flavor = "multi_thread")]
7847 async fn test_delete_segment_drops_inline_trailing_comment() {
7848 let initial_source = "\
7849sketch(on = XY) {
7850 point(at = [var 1, var 2])
7851 point(at = [var 3, var 4]) // same-line note that gets dropped
7852 point(at = [var 5, var 6])
7853}
7854";
7855
7856 let program = Program::parse(initial_source).unwrap().0.unwrap();
7857 let mut frontend = FrontendState::new();
7858
7859 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7860 let mock_ctx = ExecutorContext::new_mock(None).await;
7861 let version = Version(0);
7862
7863 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7864 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7865 let sketch_id = sketch_object.id;
7866 let sketch = expect_sketch(sketch_object);
7867
7868 let middle_point_id = *sketch.segments.get(1).unwrap();
7869
7870 let (src_delta, _scene_delta) = frontend
7871 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7872 .await
7873 .unwrap();
7874 assert!(
7876 !src_delta.text.contains("same-line note"),
7877 "inline comment should have been removed: {}",
7878 src_delta.text
7879 );
7880
7881 ctx.close().await;
7882 mock_ctx.close().await;
7883 }
7884
7885 #[tokio::test(flavor = "multi_thread")]
7886 async fn test_delete_segments_preserves_block_comments_across_positions() {
7887 let initial_source = "\
7895sketch(on = XY) {
7896 /* above first - moves to middle */
7897 point(at = [var 1, var 2]) /* same-line on first - dropped */
7898 /* above middle - stays */
7899 point(at = [var 3, var 4])
7900 /* above last - moves to trailing meta */
7901 point(at = [var 5, var 6])
7902}
7903";
7904
7905 let program = Program::parse(initial_source).unwrap().0.unwrap();
7906 let mut frontend = FrontendState::new();
7907
7908 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7909 let mock_ctx = ExecutorContext::new_mock(None).await;
7910 let version = Version(0);
7911
7912 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7913 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7914 let sketch_id = sketch_object.id;
7915 let sketch = expect_sketch(sketch_object);
7916
7917 let first_point_id = *sketch.segments.first().unwrap();
7918 let last_point_id = *sketch.segments.last().unwrap();
7919
7920 let (src_delta, _scene_delta) = frontend
7921 .delete_objects(
7922 &mock_ctx,
7923 version,
7924 sketch_id,
7925 Vec::new(),
7926 vec![first_point_id, last_point_id],
7927 )
7928 .await
7929 .unwrap();
7930 assert_eq!(
7931 src_delta.text.as_str(),
7932 "\
7933sketch(on = XY) {
7934 /* above first - moves to middle */
7935 /* above middle - stays */
7936 point(at = [var 3mm, var 4mm])
7937 /* above last - moves to trailing meta */
7938}
7939"
7940 );
7941
7942 ctx.close().await;
7943 mock_ctx.close().await;
7944 }
7945
7946 #[tokio::test(flavor = "multi_thread")]
7947 async fn test_edit_line_when_editing_its_start_point() {
7948 let initial_source = "\
7949sketch(on = XY) {
7950 line(start = [var 1, var 2], end = [var 3, var 4])
7951}
7952";
7953
7954 let program = Program::parse(initial_source).unwrap().0.unwrap();
7955
7956 let mut frontend = FrontendState::new();
7957
7958 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7959 let mock_ctx = ExecutorContext::new_mock(None).await;
7960 let version = Version(0);
7961
7962 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7963 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7964 let sketch_id = sketch_object.id;
7965 let sketch = expect_sketch(sketch_object);
7966
7967 let point_id = *sketch.segments.first().unwrap();
7968
7969 let point_ctor = PointCtor {
7970 position: Point2d {
7971 x: Expr::Var(Number {
7972 value: 5.0,
7973 units: NumericSuffix::Inch,
7974 }),
7975 y: Expr::Var(Number {
7976 value: 6.0,
7977 units: NumericSuffix::Inch,
7978 }),
7979 },
7980 };
7981 let segments = vec![ExistingSegmentCtor {
7982 id: point_id,
7983 ctor: SegmentCtor::Point(point_ctor),
7984 }];
7985 let (src_delta, scene_delta) = frontend
7986 .edit_segments(&mock_ctx, version, sketch_id, segments)
7987 .await
7988 .unwrap();
7989 assert_eq!(
7990 src_delta.text.as_str(),
7991 "\
7992sketch(on = XY) {
7993 line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7994}
7995"
7996 );
7997 assert_eq!(scene_delta.new_objects, vec![]);
7998 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7999
8000 ctx.close().await;
8001 mock_ctx.close().await;
8002 }
8003
8004 #[tokio::test(flavor = "multi_thread")]
8005 async fn test_edit_line_when_editing_its_end_point() {
8006 let initial_source = "\
8007sketch(on = XY) {
8008 line(start = [var 1, var 2], end = [var 3, var 4])
8009}
8010";
8011
8012 let program = Program::parse(initial_source).unwrap().0.unwrap();
8013
8014 let mut frontend = FrontendState::new();
8015
8016 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8017 let mock_ctx = ExecutorContext::new_mock(None).await;
8018 let version = Version(0);
8019
8020 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8021 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8022 let sketch_id = sketch_object.id;
8023 let sketch = expect_sketch(sketch_object);
8024 let point_id = *sketch.segments.get(1).unwrap();
8025
8026 let point_ctor = PointCtor {
8027 position: Point2d {
8028 x: Expr::Var(Number {
8029 value: 5.0,
8030 units: NumericSuffix::Inch,
8031 }),
8032 y: Expr::Var(Number {
8033 value: 6.0,
8034 units: NumericSuffix::Inch,
8035 }),
8036 },
8037 };
8038 let segments = vec![ExistingSegmentCtor {
8039 id: point_id,
8040 ctor: SegmentCtor::Point(point_ctor),
8041 }];
8042 let (src_delta, scene_delta) = frontend
8043 .edit_segments(&mock_ctx, version, sketch_id, segments)
8044 .await
8045 .unwrap();
8046 assert_eq!(
8047 src_delta.text.as_str(),
8048 "\
8049sketch(on = XY) {
8050 line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
8051}
8052"
8053 );
8054 assert_eq!(scene_delta.new_objects, vec![]);
8055 assert_eq!(
8056 scene_delta.new_graph.objects.len(),
8057 5,
8058 "{:#?}",
8059 scene_delta.new_graph.objects
8060 );
8061
8062 ctx.close().await;
8063 mock_ctx.close().await;
8064 }
8065
8066 #[tokio::test(flavor = "multi_thread")]
8067 async fn test_edit_line_with_coincident_feedback() {
8068 let initial_source = "\
8069sketch(on = XY) {
8070 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8071 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8072 fixed([line1.start, [0, 0]])
8073 coincident([line1.end, line2.start])
8074 equalLength([line1, line2])
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 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
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 line2_end_id = *sketch.segments.get(4).unwrap();
8091
8092 let segments = vec![ExistingSegmentCtor {
8093 id: line2_end_id,
8094 ctor: SegmentCtor::Point(PointCtor {
8095 position: Point2d {
8096 x: Expr::Var(Number {
8097 value: 9.0,
8098 units: NumericSuffix::None,
8099 }),
8100 y: Expr::Var(Number {
8101 value: 10.0,
8102 units: NumericSuffix::None,
8103 }),
8104 },
8105 }),
8106 }];
8107 let (src_delta, scene_delta) = frontend
8108 .edit_segments(&mock_ctx, version, sketch_id, segments)
8109 .await
8110 .unwrap();
8111 assert_eq!(
8112 src_delta.text.as_str(),
8113 "\
8114sketch(on = XY) {
8115 line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
8116 line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
8117 fixed([line1.start, [0, 0]])
8118 coincident([line1.end, line2.start])
8119 equalLength([line1, line2])
8120}
8121"
8122 );
8123 assert_eq!(
8124 scene_delta.new_graph.objects.len(),
8125 11,
8126 "{:#?}",
8127 scene_delta.new_graph.objects
8128 );
8129
8130 ctx.close().await;
8131 mock_ctx.close().await;
8132 }
8133
8134 #[tokio::test(flavor = "multi_thread")]
8135 async fn test_delete_point_without_var() {
8136 let initial_source = "\
8137sketch(on = XY) {
8138 point(at = [var 1, var 2])
8139 point(at = [var 3, var 4])
8140 point(at = [var 5, var 6])
8141}
8142";
8143
8144 let program = Program::parse(initial_source).unwrap().0.unwrap();
8145
8146 let mut frontend = FrontendState::new();
8147
8148 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8149 let mock_ctx = ExecutorContext::new_mock(None).await;
8150 let version = Version(0);
8151
8152 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8153 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8154 let sketch_id = sketch_object.id;
8155 let sketch = expect_sketch(sketch_object);
8156
8157 let point_id = *sketch.segments.get(1).unwrap();
8158
8159 let (src_delta, scene_delta) = frontend
8160 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
8161 .await
8162 .unwrap();
8163 assert_eq!(
8164 src_delta.text.as_str(),
8165 "\
8166sketch(on = XY) {
8167 point(at = [var 1mm, var 2mm])
8168 point(at = [var 5mm, var 6mm])
8169}
8170"
8171 );
8172 assert_eq!(scene_delta.new_objects, vec![]);
8173 assert_eq!(scene_delta.new_graph.objects.len(), 4);
8174
8175 ctx.close().await;
8176 mock_ctx.close().await;
8177 }
8178
8179 #[tokio::test(flavor = "multi_thread")]
8180 async fn test_delete_point_with_var() {
8181 let initial_source = "\
8182sketch(on = XY) {
8183 point(at = [var 1, var 2])
8184 point1 = point(at = [var 3, var 4])
8185 point(at = [var 5, var 6])
8186}
8187";
8188
8189 let program = Program::parse(initial_source).unwrap().0.unwrap();
8190
8191 let mut frontend = FrontendState::new();
8192
8193 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8194 let mock_ctx = ExecutorContext::new_mock(None).await;
8195 let version = Version(0);
8196
8197 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8198 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8199 let sketch_id = sketch_object.id;
8200 let sketch = expect_sketch(sketch_object);
8201
8202 let point_id = *sketch.segments.get(1).unwrap();
8203
8204 let (src_delta, scene_delta) = frontend
8205 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
8206 .await
8207 .unwrap();
8208 assert_eq!(
8209 src_delta.text.as_str(),
8210 "\
8211sketch(on = XY) {
8212 point(at = [var 1mm, var 2mm])
8213 point(at = [var 5mm, var 6mm])
8214}
8215"
8216 );
8217 assert_eq!(scene_delta.new_objects, vec![]);
8218 assert_eq!(scene_delta.new_graph.objects.len(), 4);
8219
8220 ctx.close().await;
8221 mock_ctx.close().await;
8222 }
8223
8224 #[tokio::test(flavor = "multi_thread")]
8225 async fn test_delete_multiple_points() {
8226 let initial_source = "\
8227sketch(on = XY) {
8228 point(at = [var 1, var 2])
8229 point1 = point(at = [var 3, var 4])
8230 point(at = [var 5, var 6])
8231}
8232";
8233
8234 let program = Program::parse(initial_source).unwrap().0.unwrap();
8235
8236 let mut frontend = FrontendState::new();
8237
8238 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8239 let mock_ctx = ExecutorContext::new_mock(None).await;
8240 let version = Version(0);
8241
8242 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8243 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8244 let sketch_id = sketch_object.id;
8245
8246 let sketch = expect_sketch(sketch_object);
8247
8248 let point1_id = *sketch.segments.first().unwrap();
8249 let point2_id = *sketch.segments.get(1).unwrap();
8250
8251 let (src_delta, scene_delta) = frontend
8252 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
8253 .await
8254 .unwrap();
8255 assert_eq!(
8256 src_delta.text.as_str(),
8257 "\
8258sketch(on = XY) {
8259 point(at = [var 5mm, var 6mm])
8260}
8261"
8262 );
8263 assert_eq!(scene_delta.new_objects, vec![]);
8264 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8265
8266 ctx.close().await;
8267 mock_ctx.close().await;
8268 }
8269
8270 #[tokio::test(flavor = "multi_thread")]
8271 async fn test_delete_coincident_constraint() {
8272 let initial_source = "\
8273sketch(on = XY) {
8274 point1 = point(at = [var 1, var 2])
8275 point2 = point(at = [var 3, var 4])
8276 coincident([point1, point2])
8277 point(at = [var 5, var 6])
8278}
8279";
8280
8281 let program = Program::parse(initial_source).unwrap().0.unwrap();
8282
8283 let mut frontend = FrontendState::new();
8284
8285 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8286 let mock_ctx = ExecutorContext::new_mock(None).await;
8287 let version = Version(0);
8288
8289 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8290 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8291 let sketch_id = sketch_object.id;
8292 let sketch = expect_sketch(sketch_object);
8293
8294 let coincident_id = *sketch.constraints.first().unwrap();
8295
8296 let (src_delta, scene_delta) = frontend
8297 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8298 .await
8299 .unwrap();
8300 assert_eq!(
8301 src_delta.text.as_str(),
8302 "\
8303sketch(on = XY) {
8304 point1 = point(at = [var 1mm, var 2mm])
8305 point2 = point(at = [var 3mm, var 4mm])
8306 point(at = [var 5mm, var 6mm])
8307}
8308"
8309 );
8310 assert_eq!(scene_delta.new_objects, vec![]);
8311 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8312
8313 ctx.close().await;
8314 mock_ctx.close().await;
8315 }
8316
8317 #[tokio::test(flavor = "multi_thread")]
8318 async fn test_delete_line_cascades_to_coincident_constraint() {
8319 let initial_source = "\
8320sketch(on = XY) {
8321 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8322 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8323 coincident([line1.end, line2.start])
8324}
8325";
8326
8327 let program = Program::parse(initial_source).unwrap().0.unwrap();
8328
8329 let mut frontend = FrontendState::new();
8330
8331 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8332 let mock_ctx = ExecutorContext::new_mock(None).await;
8333 let version = Version(0);
8334
8335 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8336 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8337 let sketch_id = sketch_object.id;
8338 let sketch = expect_sketch(sketch_object);
8339 let line_id = *sketch.segments.get(5).unwrap();
8340
8341 let (src_delta, scene_delta) = frontend
8342 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
8343 .await
8344 .unwrap();
8345 assert_eq!(
8346 src_delta.text.as_str(),
8347 "\
8348sketch(on = XY) {
8349 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8350}
8351"
8352 );
8353 assert_eq!(
8354 scene_delta.new_graph.objects.len(),
8355 5,
8356 "{:#?}",
8357 scene_delta.new_graph.objects
8358 );
8359
8360 ctx.close().await;
8361 mock_ctx.close().await;
8362 }
8363
8364 #[tokio::test(flavor = "multi_thread")]
8365 async fn test_delete_line_cascades_to_distance_constraint() {
8366 let initial_source = "\
8367sketch(on = XY) {
8368 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8369 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8370 distance([line1.end, line2.start]) == 10mm
8371}
8372";
8373
8374 let program = Program::parse(initial_source).unwrap().0.unwrap();
8375
8376 let mut frontend = FrontendState::new();
8377
8378 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8379 let mock_ctx = ExecutorContext::new_mock(None).await;
8380 let version = Version(0);
8381
8382 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8383 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8384 let sketch_id = sketch_object.id;
8385 let sketch = expect_sketch(sketch_object);
8386 let line_id = *sketch.segments.get(5).unwrap();
8387
8388 let (src_delta, scene_delta) = frontend
8389 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
8390 .await
8391 .unwrap();
8392 assert_eq!(
8393 src_delta.text.as_str(),
8394 "\
8395sketch(on = XY) {
8396 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8397}
8398"
8399 );
8400 assert_eq!(
8401 scene_delta.new_graph.objects.len(),
8402 5,
8403 "{:#?}",
8404 scene_delta.new_graph.objects
8405 );
8406
8407 ctx.close().await;
8408 mock_ctx.close().await;
8409 }
8410
8411 #[tokio::test(flavor = "multi_thread")]
8412 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
8413 let initial_source = "\
8414sketch(on = XY) {
8415 point1 = point(at = [var 1, var 2])
8416 point2 = point(at = [var 3, var 4])
8417 horizontalDistance([point1, point2]) == 10mm
8418}
8419";
8420
8421 let program = Program::parse(initial_source).unwrap().0.unwrap();
8422
8423 let mut frontend = FrontendState::new();
8424
8425 let mock_ctx = ExecutorContext::new_mock(None).await;
8426 let version = Version(0);
8427
8428 frontend.program = program.clone();
8429 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8430 frontend.update_state_after_exec(outcome, true);
8431 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8432 let sketch_id = sketch_object.id;
8433 let sketch = expect_sketch(sketch_object);
8434 let point2_id = *sketch.segments.get(1).unwrap();
8435
8436 let (src_delta, scene_delta) = frontend
8437 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
8438 .await
8439 .unwrap();
8440 assert_eq!(
8441 src_delta.text.as_str(),
8442 "\
8443sketch(on = XY) {
8444 point1 = point(at = [var 1mm, var 2mm])
8445}
8446"
8447 );
8448 assert_eq!(
8449 scene_delta.new_graph.objects.len(),
8450 3,
8451 "{:#?}",
8452 scene_delta.new_graph.objects
8453 );
8454
8455 mock_ctx.close().await;
8456 }
8457
8458 #[tokio::test(flavor = "multi_thread")]
8459 async fn test_delete_line_cascades_to_fixed_constraint() {
8460 let initial_source = "\
8461sketch(on = XY) {
8462 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8463 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8464 fixed([line1.start, [0, 0]])
8465}
8466";
8467
8468 let program = Program::parse(initial_source).unwrap().0.unwrap();
8469
8470 let mut frontend = FrontendState::new();
8471
8472 let mock_ctx = ExecutorContext::new_mock(None).await;
8473 let version = Version(0);
8474
8475 frontend.program = program.clone();
8476 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8477 frontend.update_state_after_exec(outcome, true);
8478 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8479 let sketch_id = sketch_object.id;
8480 let sketch = expect_sketch(sketch_object);
8481 let line1_id = *sketch.segments.get(2).unwrap();
8482
8483 let (src_delta, scene_delta) = frontend
8484 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8485 .await
8486 .unwrap();
8487 assert_eq!(
8488 src_delta.text.as_str(),
8489 "\
8490sketch(on = XY) {
8491 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8492}
8493"
8494 );
8495 assert_eq!(
8496 scene_delta.new_graph.objects.len(),
8497 5,
8498 "{:#?}",
8499 scene_delta.new_graph.objects
8500 );
8501
8502 mock_ctx.close().await;
8503 }
8504
8505 #[tokio::test(flavor = "multi_thread")]
8506 async fn test_delete_line_cascades_to_midpoint_constraint() {
8507 let initial_source = "\
8508sketch(on = XY) {
8509 point1 = point(at = [var 1, var 2])
8510 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
8511 midpoint(line1, point = point1)
8512}
8513";
8514
8515 let program = Program::parse(initial_source).unwrap().0.unwrap();
8516
8517 let mut frontend = FrontendState::new();
8518
8519 let mock_ctx = ExecutorContext::new_mock(None).await;
8520 let version = Version(0);
8521
8522 frontend.program = program.clone();
8523 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8524 frontend.update_state_after_exec(outcome, true);
8525 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8526 let sketch_id = sketch_object.id;
8527 let sketch = expect_sketch(sketch_object);
8528 let line1_id = *sketch.segments.get(3).unwrap();
8529
8530 let (src_delta, scene_delta) = frontend
8531 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8532 .await
8533 .unwrap();
8534 assert_eq!(
8535 src_delta.text.as_str(),
8536 "\
8537sketch(on = XY) {
8538 point1 = point(at = [var 1mm, var 2mm])
8539}
8540"
8541 );
8542 assert_eq!(
8543 scene_delta.new_graph.objects.len(),
8544 3,
8545 "{:#?}",
8546 scene_delta.new_graph.objects
8547 );
8548
8549 mock_ctx.close().await;
8550 }
8551
8552 #[tokio::test(flavor = "multi_thread")]
8553 async fn test_delete_point_preserves_multiline_coincident_constraint() {
8554 let initial_source = "\
8555sketch(on = XY) {
8556 point1 = point(at = [var 1, var 2])
8557 point2 = point(at = [var 3, var 4])
8558 point3 = point(at = [var 5, var 6])
8559 coincident([point1, point2, point3])
8560}
8561";
8562
8563 let program = Program::parse(initial_source).unwrap().0.unwrap();
8564
8565 let mut frontend = FrontendState::new();
8566
8567 let mock_ctx = ExecutorContext::new_mock(None).await;
8568 let version = Version(0);
8569
8570 frontend.program = program.clone();
8571 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8572 frontend.update_state_after_exec(outcome, true);
8573 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8574 let sketch_id = sketch_object.id;
8575 let sketch = expect_sketch(sketch_object);
8576 let point3_id = *sketch.segments.get(2).unwrap();
8577
8578 let (src_delta, scene_delta) = frontend
8579 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8580 .await
8581 .unwrap();
8582 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8583 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8584 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8585 assert!(
8586 src_delta.text.contains("coincident([point1, point2])"),
8587 "{}",
8588 src_delta.text
8589 );
8590
8591 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8592 let sketch = expect_sketch(sketch_object);
8593 assert_eq!(sketch.segments.len(), 2);
8594 assert_eq!(sketch.constraints.len(), 1);
8595
8596 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8597 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8598 panic!("Expected constraint object");
8599 };
8600 let Constraint::Coincident(coincident) = constraint else {
8601 panic!("Expected coincident constraint");
8602 };
8603 assert_eq!(
8604 coincident.segments,
8605 sketch
8606 .segments
8607 .iter()
8608 .copied()
8609 .map(Into::into)
8610 .collect::<Vec<ConstraintSegment>>()
8611 );
8612
8613 mock_ctx.close().await;
8614 }
8615
8616 #[tokio::test(flavor = "multi_thread")]
8617 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8618 let initial_source = "\
8619sketch(on = XY) {
8620 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8621 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8622 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8623 equalLength([line1, line2, line3])
8624}
8625";
8626
8627 let program = Program::parse(initial_source).unwrap().0.unwrap();
8628
8629 let mut frontend = FrontendState::new();
8630
8631 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8632 let mock_ctx = ExecutorContext::new_mock(None).await;
8633 let version = Version(0);
8634
8635 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8636 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8637 let sketch_id = sketch_object.id;
8638 let sketch = expect_sketch(sketch_object);
8639 let line3_id = *sketch.segments.get(8).unwrap();
8640
8641 let (src_delta, scene_delta) = frontend
8642 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8643 .await
8644 .unwrap();
8645 assert_eq!(
8646 src_delta.text.as_str(),
8647 "\
8648sketch(on = XY) {
8649 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8650 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8651 equalLength([line1, line2])
8652}
8653"
8654 );
8655
8656 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8657 let sketch = expect_sketch(sketch_object);
8658 assert_eq!(sketch.constraints.len(), 1);
8659
8660 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8661 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8662 panic!("Expected constraint object");
8663 };
8664 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8665 panic!("Expected lines equal length constraint");
8666 };
8667 assert_eq!(lines_equal_length.lines.len(), 2);
8668
8669 ctx.close().await;
8670 mock_ctx.close().await;
8671 }
8672
8673 #[tokio::test(flavor = "multi_thread")]
8674 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8675 let initial_source = "\
8676sketch(on = XY) {
8677 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8678 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8679 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8680 horizontal([line1.end, line2.start, line3.start])
8681}
8682";
8683
8684 let program = Program::parse(initial_source).unwrap().0.unwrap();
8685
8686 let mut frontend = FrontendState::new();
8687
8688 let mock_ctx = ExecutorContext::new_mock(None).await;
8689 let version = Version(0);
8690
8691 frontend.program = program.clone();
8692 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8693 frontend.update_state_after_exec(outcome, true);
8694 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8695 let sketch_id = sketch_object.id;
8696 let sketch = expect_sketch(sketch_object);
8697 let line1_id = *sketch.segments.get(2).unwrap();
8698
8699 let (src_delta, scene_delta) = frontend
8700 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8701 .await
8702 .unwrap();
8703 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8704 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8705 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8706 assert!(
8707 src_delta.text.contains("horizontal([line2.start, line3.start])"),
8708 "{}",
8709 src_delta.text
8710 );
8711
8712 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8713 let sketch = expect_sketch(sketch_object);
8714 assert_eq!(sketch.constraints.len(), 1);
8715
8716 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8717 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8718 panic!("Expected constraint object");
8719 };
8720 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8721 panic!("Expected horizontal points constraint");
8722 };
8723 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8724 assert_eq!(*points, remaining_points);
8725
8726 mock_ctx.close().await;
8727 }
8728
8729 #[tokio::test(flavor = "multi_thread")]
8730 async fn test_delete_line_preserves_multiline_vertical_constraint() {
8731 let initial_source = "\
8732sketch(on = XY) {
8733 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8734 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8735 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8736 vertical([line1.end, line2.start, line3.start])
8737}
8738";
8739
8740 let program = Program::parse(initial_source).unwrap().0.unwrap();
8741
8742 let mut frontend = FrontendState::new();
8743
8744 let mock_ctx = ExecutorContext::new_mock(None).await;
8745 let version = Version(0);
8746
8747 frontend.program = program.clone();
8748 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8749 frontend.update_state_after_exec(outcome, true);
8750 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8751 let sketch_id = sketch_object.id;
8752 let sketch = expect_sketch(sketch_object);
8753 let line1_id = *sketch.segments.get(2).unwrap();
8754
8755 let (src_delta, scene_delta) = frontend
8756 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8757 .await
8758 .unwrap();
8759 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8760 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8761 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8762 assert!(
8763 src_delta.text.contains("vertical([line2.start, line3.start])"),
8764 "{}",
8765 src_delta.text
8766 );
8767
8768 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8769 let sketch = expect_sketch(sketch_object);
8770 assert_eq!(sketch.constraints.len(), 1);
8771
8772 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8773 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8774 panic!("Expected constraint object");
8775 };
8776 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8777 panic!("Expected vertical points constraint");
8778 };
8779 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8780 assert_eq!(*points, remaining_points);
8781
8782 mock_ctx.close().await;
8783 }
8784
8785 #[tokio::test(flavor = "multi_thread")]
8786 async fn test_delete_line_preserves_multiline_coincident_constraint() {
8787 let initial_source = "\
8788sketch(on = XY) {
8789 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8790 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8791 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8792 coincident([line1.end, line2.start, line3.start])
8793}
8794";
8795
8796 let program = Program::parse(initial_source).unwrap().0.unwrap();
8797
8798 let mut frontend = FrontendState::new();
8799
8800 let mock_ctx = ExecutorContext::new_mock(None).await;
8801 let version = Version(0);
8802
8803 frontend.program = program.clone();
8804 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8805 frontend.update_state_after_exec(outcome, true);
8806 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8807 let sketch_id = sketch_object.id;
8808 let sketch = expect_sketch(sketch_object);
8809 let line1_id = *sketch.segments.get(2).unwrap();
8810
8811 let (src_delta, scene_delta) = frontend
8812 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8813 .await
8814 .unwrap();
8815 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8816 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8817 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8818 assert!(
8819 src_delta.text.contains("coincident([line2.start, line3.start])"),
8820 "{}",
8821 src_delta.text
8822 );
8823
8824 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8825 let sketch = expect_sketch(sketch_object);
8826 assert_eq!(sketch.constraints.len(), 1);
8827
8828 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8829 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8830 panic!("Expected constraint object");
8831 };
8832 let Constraint::Coincident(coincident) = constraint else {
8833 panic!("Expected coincident constraint");
8834 };
8835 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8836 assert_eq!(coincident.segments, remaining_segments);
8837
8838 mock_ctx.close().await;
8839 }
8840
8841 #[tokio::test(flavor = "multi_thread")]
8842 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8843 let initial_source = "\
8844sketch(on = XY) {
8845 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8846 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8847 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8848 equalLength([line1, line2, line3])
8849}
8850";
8851
8852 let program = Program::parse(initial_source).unwrap().0.unwrap();
8853
8854 let mut frontend = FrontendState::new();
8855
8856 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8857 let mock_ctx = ExecutorContext::new_mock(None).await;
8858 let version = Version(0);
8859
8860 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8861 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8862 let sketch_id = sketch_object.id;
8863 let sketch = expect_sketch(sketch_object);
8864 let line2_id = *sketch.segments.get(5).unwrap();
8865 let line3_id = *sketch.segments.get(8).unwrap();
8866
8867 let (src_delta, scene_delta) = frontend
8868 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8869 .await
8870 .unwrap();
8871 assert_eq!(
8872 src_delta.text.as_str(),
8873 "\
8874sketch(on = XY) {
8875 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8876}
8877"
8878 );
8879
8880 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8881 let sketch = expect_sketch(sketch_object);
8882 assert!(sketch.constraints.is_empty());
8883
8884 ctx.close().await;
8885 mock_ctx.close().await;
8886 }
8887
8888 #[tokio::test(flavor = "multi_thread")]
8889 async fn test_delete_line_preserves_multiline_parallel_constraint() {
8890 let initial_source = "\
8891sketch(on = XY) {
8892 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8893 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8894 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8895 parallel([line1, line2, line3])
8896}
8897";
8898
8899 let program = Program::parse(initial_source).unwrap().0.unwrap();
8900
8901 let mut frontend = FrontendState::new();
8902
8903 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8904 let mock_ctx = ExecutorContext::new_mock(None).await;
8905 let version = Version(0);
8906
8907 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8908 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8909 let sketch_id = sketch_object.id;
8910 let sketch = expect_sketch(sketch_object);
8911 let line3_id = *sketch.segments.get(8).unwrap();
8912
8913 let (src_delta, scene_delta) = frontend
8914 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8915 .await
8916 .unwrap();
8917 assert_eq!(
8918 src_delta.text.as_str(),
8919 "\
8920sketch(on = XY) {
8921 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8922 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8923 parallel([line1, line2])
8924}
8925"
8926 );
8927
8928 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8929 let sketch = expect_sketch(sketch_object);
8930 assert_eq!(sketch.constraints.len(), 1);
8931
8932 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8933 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8934 panic!("Expected constraint object");
8935 };
8936 let Constraint::Parallel(parallel) = constraint else {
8937 panic!("Expected parallel constraint");
8938 };
8939 assert_eq!(parallel.lines.len(), 2);
8940
8941 ctx.close().await;
8942 mock_ctx.close().await;
8943 }
8944
8945 #[tokio::test(flavor = "multi_thread")]
8946 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8947 let initial_source = "\
8948sketch(on = XY) {
8949 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8950 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8951 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8952 parallel([line1, line2, line3])
8953}
8954";
8955
8956 let program = Program::parse(initial_source).unwrap().0.unwrap();
8957
8958 let mut frontend = FrontendState::new();
8959
8960 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8961 let mock_ctx = ExecutorContext::new_mock(None).await;
8962 let version = Version(0);
8963
8964 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8965 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8966 let sketch_id = sketch_object.id;
8967 let sketch = expect_sketch(sketch_object);
8968 let line2_id = *sketch.segments.get(5).unwrap();
8969 let line3_id = *sketch.segments.get(8).unwrap();
8970
8971 let (src_delta, scene_delta) = frontend
8972 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8973 .await
8974 .unwrap();
8975 assert_eq!(
8976 src_delta.text.as_str(),
8977 "\
8978sketch(on = XY) {
8979 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8980}
8981"
8982 );
8983
8984 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8985 let sketch = expect_sketch(sketch_object);
8986 assert!(sketch.constraints.is_empty());
8987
8988 ctx.close().await;
8989 mock_ctx.close().await;
8990 }
8991
8992 #[tokio::test(flavor = "multi_thread")]
8993 async fn test_delete_line_line_coincident_constraint() {
8994 let initial_source = "\
8995sketch(on = XY) {
8996 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8997 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8998 coincident([line1, line2])
8999}
9000";
9001
9002 let program = Program::parse(initial_source).unwrap().0.unwrap();
9003
9004 let mut frontend = FrontendState::new();
9005
9006 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9007 let mock_ctx = ExecutorContext::new_mock(None).await;
9008 let version = Version(0);
9009
9010 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9011 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9012 let sketch_id = sketch_object.id;
9013 let sketch = expect_sketch(sketch_object);
9014
9015 let coincident_id = *sketch.constraints.first().unwrap();
9016
9017 let (src_delta, scene_delta) = frontend
9018 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9019 .await
9020 .unwrap();
9021 assert_eq!(
9022 src_delta.text.as_str(),
9023 "\
9024sketch(on = XY) {
9025 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
9026 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
9027}
9028"
9029 );
9030 assert_eq!(scene_delta.new_objects, vec![]);
9031 assert_eq!(scene_delta.new_graph.objects.len(), 8);
9032
9033 ctx.close().await;
9034 mock_ctx.close().await;
9035 }
9036
9037 #[tokio::test(flavor = "multi_thread")]
9038 async fn test_two_points_coincident() {
9039 let initial_source = "\
9040sketch(on = XY) {
9041 point1 = point(at = [var 1, var 2])
9042 point(at = [3, 4])
9043}
9044";
9045
9046 let program = Program::parse(initial_source).unwrap().0.unwrap();
9047
9048 let mut frontend = FrontendState::new();
9049
9050 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9051 let mock_ctx = ExecutorContext::new_mock(None).await;
9052 let version = Version(0);
9053
9054 frontend.hack_set_program(&ctx, program).await.unwrap();
9055 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9056 let sketch_id = sketch_object.id;
9057 let sketch = expect_sketch(sketch_object);
9058 let point0_id = *sketch.segments.first().unwrap();
9059 let point1_id = *sketch.segments.get(1).unwrap();
9060
9061 let constraint = Constraint::Coincident(Coincident {
9062 segments: vec![point0_id.into(), point1_id.into()],
9063 });
9064 let (src_delta, scene_delta) = frontend
9065 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9066 .await
9067 .unwrap();
9068 assert_eq!(
9069 src_delta.text.as_str(),
9070 "\
9071sketch(on = XY) {
9072 point1 = point(at = [var 1, var 2])
9073 point2 = point(at = [3, 4])
9074 coincident([point1, point2])
9075}
9076"
9077 );
9078 assert_eq!(
9079 scene_delta.new_graph.objects.len(),
9080 5,
9081 "{:#?}",
9082 scene_delta.new_graph.objects
9083 );
9084
9085 ctx.close().await;
9086 mock_ctx.close().await;
9087 }
9088
9089 #[tokio::test(flavor = "multi_thread")]
9090 async fn test_three_points_coincident() {
9091 let initial_source = "\
9092sketch(on = XY) {
9093 point1 = point(at = [var 1, var 2])
9094 point(at = [var 3, var 4])
9095 point(at = [var 5, var 6])
9096}
9097";
9098
9099 let program = Program::parse(initial_source).unwrap().0.unwrap();
9100
9101 let mut frontend = FrontendState::new();
9102
9103 let mock_ctx = ExecutorContext::new_mock(None).await;
9104 let version = Version(0);
9105
9106 frontend.program = program.clone();
9107 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9108 frontend.update_state_after_exec(outcome, true);
9109 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9110 let sketch_id = sketch_object.id;
9111 let sketch = expect_sketch(sketch_object);
9112 let segments = sketch
9113 .segments
9114 .iter()
9115 .take(3)
9116 .copied()
9117 .map(Into::into)
9118 .collect::<Vec<ConstraintSegment>>();
9119
9120 let constraint = Constraint::Coincident(Coincident {
9121 segments: segments.clone(),
9122 });
9123 let (src_delta, scene_delta) = frontend
9124 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9125 .await
9126 .unwrap();
9127 assert_eq!(
9128 src_delta.text.as_str(),
9129 "\
9130sketch(on = XY) {
9131 point1 = point(at = [var 1, var 2])
9132 point2 = point(at = [var 3, var 4])
9133 point3 = point(at = [var 5, var 6])
9134 coincident([point1, point2, point3])
9135}
9136"
9137 );
9138
9139 let constraint_object = scene_delta
9140 .new_graph
9141 .objects
9142 .iter()
9143 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
9144 .unwrap();
9145
9146 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9147 panic!("expected a constraint object");
9148 };
9149
9150 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
9151
9152 mock_ctx.close().await;
9153 }
9154
9155 #[tokio::test(flavor = "multi_thread")]
9156 async fn test_source_with_three_point_coincident_tracks_all_segments() {
9157 let initial_source = "\
9158sketch(on = XY) {
9159 point1 = point(at = [var 1, var 2])
9160 point2 = point(at = [var 3, var 4])
9161 point3 = point(at = [var 5, var 6])
9162 coincident([point1, point2, point3])
9163}
9164";
9165
9166 let program = Program::parse(initial_source).unwrap().0.unwrap();
9167
9168 let mut frontend = FrontendState::new();
9169
9170 let ctx = ExecutorContext::new_mock(None).await;
9171 frontend.program = program.clone();
9172 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9173 frontend.update_state_after_exec(outcome, true);
9174
9175 let constraint_object = frontend
9176 .scene_graph
9177 .objects
9178 .iter()
9179 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
9180 .unwrap();
9181 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9182 panic!("expected a constraint object");
9183 };
9184
9185 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9186 let sketch = expect_sketch(sketch_object);
9187 let expected_segments = sketch
9188 .segments
9189 .iter()
9190 .take(3)
9191 .copied()
9192 .map(Into::into)
9193 .collect::<Vec<ConstraintSegment>>();
9194
9195 assert_eq!(
9196 constraint,
9197 &Constraint::Coincident(Coincident {
9198 segments: expected_segments,
9199 })
9200 );
9201
9202 ctx.close().await;
9203 }
9204
9205 #[tokio::test(flavor = "multi_thread")]
9206 async fn test_point_origin_coincident_preserves_order() {
9207 let initial_source = "\
9208sketch(on = XY) {
9209 point(at = [var 1, var 2])
9210}
9211";
9212
9213 for (origin_first, expected_source) in [
9214 (
9215 true,
9216 "\
9217sketch(on = XY) {
9218 point1 = point(at = [var 1, var 2])
9219 coincident([ORIGIN, point1])
9220}
9221",
9222 ),
9223 (
9224 false,
9225 "\
9226sketch(on = XY) {
9227 point1 = point(at = [var 1, var 2])
9228 coincident([point1, ORIGIN])
9229}
9230",
9231 ),
9232 ] {
9233 let program = Program::parse(initial_source).unwrap().0.unwrap();
9234
9235 let mut frontend = FrontendState::new();
9236
9237 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9238 let mock_ctx = ExecutorContext::new_mock(None).await;
9239 let version = Version(0);
9240
9241 frontend.hack_set_program(&ctx, program).await.unwrap();
9242 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9243 let sketch_id = sketch_object.id;
9244 let sketch = expect_sketch(sketch_object);
9245 let point_id = *sketch.segments.first().unwrap();
9246
9247 let segments = if origin_first {
9248 vec![ConstraintSegment::ORIGIN, point_id.into()]
9249 } else {
9250 vec![point_id.into(), ConstraintSegment::ORIGIN]
9251 };
9252 let constraint = Constraint::Coincident(Coincident {
9253 segments: segments.clone(),
9254 });
9255 let (src_delta, scene_delta) = frontend
9256 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9257 .await
9258 .unwrap();
9259 assert_eq!(src_delta.text.as_str(), expected_source);
9260
9261 let constraint_object = scene_delta
9262 .new_graph
9263 .objects
9264 .iter()
9265 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
9266 .unwrap();
9267
9268 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9269 panic!("expected a constraint object");
9270 };
9271
9272 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
9273
9274 ctx.close().await;
9275 mock_ctx.close().await;
9276 }
9277 }
9278
9279 #[tokio::test(flavor = "multi_thread")]
9280 async fn test_coincident_of_line_end_points() {
9281 let initial_source = "\
9282sketch(on = XY) {
9283 line(start = [var 1, var 2], end = [var 3, var 4])
9284 line(start = [var 5, var 6], end = [var 7, var 8])
9285}
9286";
9287
9288 let program = Program::parse(initial_source).unwrap().0.unwrap();
9289
9290 let mut frontend = FrontendState::new();
9291
9292 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9293 let mock_ctx = ExecutorContext::new_mock(None).await;
9294 let version = Version(0);
9295
9296 frontend.hack_set_program(&ctx, program).await.unwrap();
9297 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9298 let sketch_id = sketch_object.id;
9299 let sketch = expect_sketch(sketch_object);
9300 let point0_id = *sketch.segments.get(1).unwrap();
9301 let point1_id = *sketch.segments.get(3).unwrap();
9302
9303 let constraint = Constraint::Coincident(Coincident {
9304 segments: vec![point0_id.into(), point1_id.into()],
9305 });
9306 let (src_delta, scene_delta) = frontend
9307 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9308 .await
9309 .unwrap();
9310 assert_eq!(
9311 src_delta.text.as_str(),
9312 "\
9313sketch(on = XY) {
9314 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9315 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9316 coincident([line1.end, line2.start])
9317}
9318"
9319 );
9320 assert_eq!(
9321 scene_delta.new_graph.objects.len(),
9322 9,
9323 "{:#?}",
9324 scene_delta.new_graph.objects
9325 );
9326
9327 ctx.close().await;
9328 mock_ctx.close().await;
9329 }
9330
9331 #[tokio::test(flavor = "multi_thread")]
9332 async fn test_coincident_of_line_point_and_circle_segment() {
9333 let initial_source = "\
9334sketch(on = XY) {
9335 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
9336 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
9337}
9338";
9339 let program = Program::parse(initial_source).unwrap().0.unwrap();
9340 let mut frontend = FrontendState::new();
9341
9342 let mock_ctx = ExecutorContext::new_mock(None).await;
9343 let version = Version(0);
9344
9345 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9346 frontend.program = program;
9347 frontend.update_state_after_exec(outcome, true);
9348 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
9349 let sketch_id = sketch_object.id;
9350 let sketch = expect_sketch(sketch_object);
9351
9352 let circle_id = sketch
9353 .segments
9354 .iter()
9355 .copied()
9356 .find(|seg_id| {
9357 matches!(
9358 &frontend.scene_graph.objects[seg_id.0].kind,
9359 ObjectKind::Segment {
9360 segment: Segment::Circle(_)
9361 }
9362 )
9363 })
9364 .expect("Expected a circle segment in sketch");
9365 let line_id = frontend
9366 .scene_graph
9367 .objects
9368 .iter()
9369 .find_map(|obj| match &obj.kind {
9370 ObjectKind::Segment {
9371 segment: Segment::Line(line),
9372 } if line.owner.is_none() => Some(obj.id),
9373 _ => None,
9374 })
9375 .expect("Expected a standalone line segment in scene graph");
9376
9377 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
9378 ObjectKind::Segment {
9379 segment: Segment::Line(line),
9380 } => line.start,
9381 _ => panic!("Expected line segment object"),
9382 };
9383
9384 let constraint = Constraint::Coincident(Coincident {
9385 segments: vec![line_start_point_id.into(), circle_id.into()],
9386 });
9387 let (src_delta, _scene_delta) = frontend
9388 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9389 .await
9390 .unwrap();
9391 assert_eq!(
9392 src_delta.text.as_str(),
9393 "\
9394sketch(on = XY) {
9395 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
9396 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
9397 coincident([line1.start, circle1])
9398}
9399"
9400 );
9401
9402 mock_ctx.close().await;
9403 }
9404
9405 #[tokio::test(flavor = "multi_thread")]
9406 async fn test_invalid_coincident_arc_and_line_preserves_state() {
9407 let program = Program::empty();
9415
9416 let mut frontend = FrontendState::new();
9417 frontend.program = program;
9418
9419 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9420 let mock_ctx = ExecutorContext::new_mock(None).await;
9421 let version = Version(0);
9422
9423 let sketch_args = SketchCtor {
9424 on: Plane::Default(PlaneName::Xy),
9425 };
9426 let (_src_delta, _scene_delta, sketch_id) = frontend
9427 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9428 .await
9429 .unwrap();
9430
9431 let arc_ctor = ArcCtor {
9433 start: Point2d {
9434 x: Expr::Var(Number {
9435 value: 0.0,
9436 units: NumericSuffix::Mm,
9437 }),
9438 y: Expr::Var(Number {
9439 value: 0.0,
9440 units: NumericSuffix::Mm,
9441 }),
9442 },
9443 end: Point2d {
9444 x: Expr::Var(Number {
9445 value: 10.0,
9446 units: NumericSuffix::Mm,
9447 }),
9448 y: Expr::Var(Number {
9449 value: 10.0,
9450 units: NumericSuffix::Mm,
9451 }),
9452 },
9453 center: Point2d {
9454 x: Expr::Var(Number {
9455 value: 10.0,
9456 units: NumericSuffix::Mm,
9457 }),
9458 y: Expr::Var(Number {
9459 value: 0.0,
9460 units: NumericSuffix::Mm,
9461 }),
9462 },
9463 construction: None,
9464 };
9465 let (_src_delta, scene_delta) = frontend
9466 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
9467 .await
9468 .unwrap();
9469 let arc_id = *scene_delta.new_objects.last().unwrap();
9471
9472 let line_ctor = LineCtor {
9474 start: Point2d {
9475 x: Expr::Var(Number {
9476 value: 20.0,
9477 units: NumericSuffix::Mm,
9478 }),
9479 y: Expr::Var(Number {
9480 value: 0.0,
9481 units: NumericSuffix::Mm,
9482 }),
9483 },
9484 end: Point2d {
9485 x: Expr::Var(Number {
9486 value: 30.0,
9487 units: NumericSuffix::Mm,
9488 }),
9489 y: Expr::Var(Number {
9490 value: 10.0,
9491 units: NumericSuffix::Mm,
9492 }),
9493 },
9494 construction: None,
9495 };
9496 let (_src_delta, scene_delta) = frontend
9497 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
9498 .await
9499 .unwrap();
9500 let line_id = *scene_delta.new_objects.last().unwrap();
9502
9503 let constraint = Constraint::Coincident(Coincident {
9506 segments: vec![arc_id.into(), line_id.into()],
9507 });
9508 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
9509
9510 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
9512
9513 let sketch_object_after =
9516 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
9517 let sketch_after = expect_sketch(sketch_object_after);
9518
9519 assert!(
9521 sketch_after.segments.contains(&arc_id),
9522 "Arc segment should still exist after failed constraint"
9523 );
9524 assert!(
9525 sketch_after.segments.contains(&line_id),
9526 "Line segment should still exist after failed constraint"
9527 );
9528
9529 let arc_obj = frontend
9531 .scene_graph
9532 .objects
9533 .get(arc_id.0)
9534 .expect("Arc object should still be accessible");
9535 let line_obj = frontend
9536 .scene_graph
9537 .objects
9538 .get(line_id.0)
9539 .expect("Line object should still be accessible");
9540
9541 match &arc_obj.kind {
9544 ObjectKind::Segment {
9545 segment: Segment::Arc(_),
9546 } => {}
9547 _ => panic!("Arc object should still be an arc segment"),
9548 }
9549 match &line_obj.kind {
9550 ObjectKind::Segment {
9551 segment: Segment::Line(_),
9552 } => {}
9553 _ => panic!("Line object should still be a line segment"),
9554 }
9555
9556 ctx.close().await;
9557 mock_ctx.close().await;
9558 }
9559
9560 #[tokio::test(flavor = "multi_thread")]
9561 async fn test_distance_two_points() {
9562 let initial_source = "\
9563sketch(on = XY) {
9564 point(at = [var 1, var 2])
9565 point(at = [var 3, var 4])
9566}
9567";
9568
9569 let program = Program::parse(initial_source).unwrap().0.unwrap();
9570
9571 let mut frontend = FrontendState::new();
9572
9573 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9574 let mock_ctx = ExecutorContext::new_mock(None).await;
9575 let version = Version(0);
9576
9577 frontend.hack_set_program(&ctx, program).await.unwrap();
9578 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9579 let sketch_id = sketch_object.id;
9580 let sketch = expect_sketch(sketch_object);
9581 let point0_id = *sketch.segments.first().unwrap();
9582 let point1_id = *sketch.segments.get(1).unwrap();
9583
9584 let constraint = Constraint::Distance(Distance {
9585 points: vec![point0_id.into(), point1_id.into()],
9586 distance: Number {
9587 value: 2.0,
9588 units: NumericSuffix::Mm,
9589 },
9590 label_position: None,
9591 source: Default::default(),
9592 });
9593 let (src_delta, scene_delta) = frontend
9594 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9595 .await
9596 .unwrap();
9597 assert_eq!(
9598 src_delta.text.as_str(),
9599 "\
9601sketch(on = XY) {
9602 point1 = point(at = [var 1, var 2])
9603 point2 = point(at = [var 3, var 4])
9604 distance([point1, point2]) == 2mm
9605}
9606"
9607 );
9608 assert_eq!(
9609 scene_delta.new_graph.objects.len(),
9610 5,
9611 "{:#?}",
9612 scene_delta.new_graph.objects
9613 );
9614
9615 ctx.close().await;
9616 mock_ctx.close().await;
9617 }
9618
9619 #[tokio::test(flavor = "multi_thread")]
9620 async fn test_distance_two_points_with_label() {
9621 let initial_source = "\
9622sketch(on = XY) {
9623 point(at = [var 1, var 2])
9624 point(at = [var 3, var 4])
9625}
9626";
9627
9628 let program = Program::parse(initial_source).unwrap().0.unwrap();
9629
9630 let mut frontend = FrontendState::new();
9631
9632 let mock_ctx = ExecutorContext::new_mock(None).await;
9633 let version = Version(0);
9634
9635 frontend.program = program.clone();
9636 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9637 frontend.update_state_after_exec(outcome, true);
9638 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9639 let sketch_id = sketch_object.id;
9640 let sketch = expect_sketch(sketch_object);
9641 let point0_id = *sketch.segments.first().unwrap();
9642 let point1_id = *sketch.segments.get(1).unwrap();
9643
9644 let label_position = Point2d {
9645 x: Number {
9646 value: 10.0,
9647 units: NumericSuffix::Mm,
9648 },
9649 y: Number {
9650 value: 11.0,
9651 units: NumericSuffix::Mm,
9652 },
9653 };
9654 let constraint = Constraint::Distance(Distance {
9655 points: vec![point0_id.into(), point1_id.into()],
9656 distance: Number {
9657 value: 2.0,
9658 units: NumericSuffix::Mm,
9659 },
9660 label_position: Some(label_position.clone()),
9661 source: Default::default(),
9662 });
9663 let (src_delta, scene_delta) = frontend
9664 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9665 .await
9666 .unwrap();
9667 assert_eq!(
9668 src_delta.text.as_str(),
9669 "\
9670sketch(on = XY) {
9671 point1 = point(at = [var 1, var 2])
9672 point2 = point(at = [var 3, var 4])
9673 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9674}
9675"
9676 );
9677
9678 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9679 let sketch = expect_sketch(sketch_object);
9680 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9681 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9682 panic!("Expected constraint object");
9683 };
9684 let Constraint::Distance(distance) = constraint else {
9685 panic!("Expected distance constraint");
9686 };
9687 assert_eq!(distance.label_position, Some(label_position));
9688
9689 mock_ctx.close().await;
9690 }
9691
9692 #[tokio::test(flavor = "multi_thread")]
9693 async fn test_edit_distance_constraint_label_position() {
9694 let initial_source = "\
9695sketch(on = XY) {
9696 point(at = [var 1, var 2])
9697 point(at = [var 3, var 2])
9698}
9699";
9700
9701 let program = Program::parse(initial_source).unwrap().0.unwrap();
9702
9703 let mut frontend = FrontendState::new();
9704
9705 let mock_ctx = ExecutorContext::new_mock(None).await;
9706 let version = Version(0);
9707
9708 frontend.program = program.clone();
9709 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9710 frontend.update_state_after_exec(outcome, true);
9711 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9712 let sketch_id = sketch_object.id;
9713 let sketch = expect_sketch(sketch_object);
9714 let point0_id = *sketch.segments.first().unwrap();
9715 let point1_id = *sketch.segments.get(1).unwrap();
9716
9717 let constraint = Constraint::Distance(Distance {
9718 points: vec![point0_id.into(), point1_id.into()],
9719 distance: Number {
9720 value: 2.0,
9721 units: NumericSuffix::Mm,
9722 },
9723 label_position: None,
9724 source: Default::default(),
9725 });
9726 let (_, scene_delta) = frontend
9727 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9728 .await
9729 .unwrap();
9730 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9731 let sketch = expect_sketch(sketch_object);
9732 let constraint_id = sketch.constraints[0];
9733 let label_position = Point2d {
9734 x: Number {
9735 value: 10.0,
9736 units: NumericSuffix::Mm,
9737 },
9738 y: Number {
9739 value: 11.0,
9740 units: NumericSuffix::Mm,
9741 },
9742 };
9743
9744 let (src_delta, scene_delta) = frontend
9745 .edit_distance_constraint_label_position(
9746 &mock_ctx,
9747 version,
9748 sketch_id,
9749 constraint_id,
9750 label_position.clone(),
9751 vec![],
9752 )
9753 .await
9754 .unwrap();
9755 assert_eq!(
9756 src_delta.text.as_str(),
9757 "\
9758sketch(on = XY) {
9759 point1 = point(at = [var 1mm, var 2mm])
9760 point2 = point(at = [var 3mm, var 2mm])
9761 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9762}
9763"
9764 );
9765
9766 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9767 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9768 panic!("Expected constraint object");
9769 };
9770 let Constraint::Distance(distance) = constraint else {
9771 panic!("Expected distance constraint");
9772 };
9773 assert_eq!(distance.label_position, Some(label_position));
9774
9775 mock_ctx.close().await;
9776 }
9777
9778 #[tokio::test(flavor = "multi_thread")]
9779 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9780 let initial_source = "\
9781sketch(on = XY) {
9782 point1 = point(at = [var 0mm, var 0mm])
9783 point2 = point(at = [var 10mm, var 0mm])
9784 distance([point1, point2]) == 5mm
9785}
9786";
9787
9788 let program = Program::parse(initial_source).unwrap().0.unwrap();
9789 let mut frontend = FrontendState::new();
9790 let mock_ctx = ExecutorContext::new_mock(None).await;
9791 let version = Version(0);
9792
9793 frontend.program = program.clone();
9794 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9795 frontend.update_state_after_exec(outcome, true);
9796 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9797 let sketch_id = sketch_object.id;
9798 let sketch = expect_sketch(sketch_object);
9799 let point0_id = sketch.segments[0];
9800 let point1_id = sketch.segments[1];
9801 let constraint_id = sketch.constraints[0];
9802
9803 let edited_segments = vec![ExistingSegmentCtor {
9804 id: point0_id,
9805 ctor: SegmentCtor::Point(PointCtor {
9806 position: Point2d {
9807 x: Expr::Var(Number {
9808 value: 2.0,
9809 units: NumericSuffix::Mm,
9810 }),
9811 y: Expr::Var(Number {
9812 value: 1.0,
9813 units: NumericSuffix::Mm,
9814 }),
9815 },
9816 }),
9817 }];
9818 let (_, scene_delta) = frontend
9819 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9820 .await
9821 .unwrap();
9822 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9823 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9824
9825 let label_position = Point2d {
9826 x: Number {
9827 value: 3.0,
9828 units: NumericSuffix::Mm,
9829 },
9830 y: Number {
9831 value: 4.0,
9832 units: NumericSuffix::Mm,
9833 },
9834 };
9835 let (_, scene_delta) = frontend
9836 .edit_distance_constraint_label_position(
9837 &mock_ctx,
9838 version,
9839 sketch_id,
9840 constraint_id,
9841 label_position,
9842 vec![point0_id],
9843 )
9844 .await
9845 .unwrap();
9846
9847 assert_point_position_close(
9848 point_position(&scene_delta.new_graph, point0_id),
9849 point0_after_segment_edit,
9850 );
9851 assert_point_position_close(
9852 point_position(&scene_delta.new_graph, point1_id),
9853 point1_after_segment_edit,
9854 );
9855
9856 mock_ctx.close().await;
9857 }
9858
9859 #[tokio::test(flavor = "multi_thread")]
9860 async fn test_distance_point_line() {
9861 let initial_source = "\
9862sketch(on = XY) {
9863 point(at = [var 0, var 5])
9864 line(start = [var 0, var 0], end = [var 10, var 0])
9865}
9866";
9867
9868 let program = Program::parse(initial_source).unwrap().0.unwrap();
9869
9870 let mut frontend = FrontendState::new();
9871
9872 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9873 let mock_ctx = ExecutorContext::new_mock(None).await;
9874 let version = Version(0);
9875
9876 frontend.hack_set_program(&ctx, program).await.unwrap();
9877 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9878 let sketch_id = sketch_object.id;
9879 let sketch = expect_sketch(sketch_object);
9880 let point_id = *sketch.segments.first().unwrap();
9881 let line_id = *sketch
9882 .segments
9883 .iter()
9884 .find(|segment_id| {
9885 matches!(
9886 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9887 Some(ObjectKind::Segment {
9888 segment: Segment::Line(_)
9889 })
9890 )
9891 })
9892 .unwrap();
9893
9894 let label_position = Point2d {
9895 x: Number {
9896 value: 10.0,
9897 units: NumericSuffix::Mm,
9898 },
9899 y: Number {
9900 value: 11.0,
9901 units: NumericSuffix::Mm,
9902 },
9903 };
9904 let constraint = Constraint::Distance(Distance {
9905 points: vec![point_id.into(), line_id.into()],
9906 distance: Number {
9907 value: 5.0,
9908 units: NumericSuffix::Mm,
9909 },
9910 label_position: Some(label_position.clone()),
9911 source: Default::default(),
9912 });
9913 let (src_delta, scene_delta) = frontend
9914 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9915 .await
9916 .unwrap();
9917 assert_eq!(
9918 src_delta.text.as_str(),
9919 "\
9920sketch(on = XY) {
9921 point1 = point(at = [var 0, var 5])
9922 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9923 distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
9924}
9925"
9926 );
9927 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9928 let sketch = expect_sketch(sketch_object);
9929 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9930 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9931 panic!("Expected constraint object");
9932 };
9933 let Constraint::Distance(distance) = constraint else {
9934 panic!("Expected distance constraint");
9935 };
9936 assert_eq!(distance.label_position, Some(label_position));
9937
9938 ctx.close().await;
9939 mock_ctx.close().await;
9940 }
9941
9942 #[tokio::test(flavor = "multi_thread")]
9943 async fn test_distance_point_arc() {
9944 let initial_source = "\
9945sketch(on = XY) {
9946 point(at = [var 0, var 8])
9947 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9948}
9949";
9950
9951 let program = Program::parse(initial_source).unwrap().0.unwrap();
9952
9953 let mut frontend = FrontendState::new();
9954
9955 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9956 let mock_ctx = ExecutorContext::new_mock(None).await;
9957 let version = Version(0);
9958
9959 frontend.hack_set_program(&ctx, program).await.unwrap();
9960 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9961 let sketch_id = sketch_object.id;
9962 let sketch = expect_sketch(sketch_object);
9963 let point_id = *sketch.segments.first().unwrap();
9964 let arc_id = *sketch
9965 .segments
9966 .iter()
9967 .find(|segment_id| {
9968 matches!(
9969 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9970 Some(ObjectKind::Segment {
9971 segment: Segment::Arc(_)
9972 })
9973 )
9974 })
9975 .unwrap();
9976
9977 let constraint = Constraint::Distance(Distance {
9978 points: vec![point_id.into(), arc_id.into()],
9979 distance: Number {
9980 value: 3.0,
9981 units: NumericSuffix::Mm,
9982 },
9983 label_position: None,
9984 source: Default::default(),
9985 });
9986 let (src_delta, _scene_delta) = frontend
9987 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9988 .await
9989 .unwrap();
9990 assert_eq!(
9991 src_delta.text.as_str(),
9992 "\
9993sketch(on = XY) {
9994 point1 = point(at = [var 0, var 8])
9995 arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9996 distance([point1, arc1]) == 3mm
9997}
9998"
9999 );
10000
10001 ctx.close().await;
10002 mock_ctx.close().await;
10003 }
10004
10005 #[tokio::test(flavor = "multi_thread")]
10006 async fn test_distance_arc_origin() {
10007 let initial_source = "\
10008sketch001 = sketch(on = XY) {
10009 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
10010}
10011";
10012
10013 let program = Program::parse(initial_source).unwrap().0.unwrap();
10014
10015 let mut frontend = FrontendState::new();
10016
10017 let mock_ctx = ExecutorContext::new_mock(None).await;
10018 let version = Version(0);
10019
10020 frontend.program = program.clone();
10021 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10022 frontend.update_state_after_exec(outcome, true);
10023 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10024 let sketch_id = sketch_object.id;
10025 let sketch = expect_sketch(sketch_object);
10026 let arc_id = *sketch
10027 .segments
10028 .iter()
10029 .find(|segment_id| {
10030 matches!(
10031 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10032 Some(ObjectKind::Segment {
10033 segment: Segment::Arc(_)
10034 })
10035 )
10036 })
10037 .unwrap();
10038
10039 let constraint = Constraint::Distance(Distance {
10040 points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
10041 distance: Number {
10042 value: 3.0,
10043 units: NumericSuffix::Mm,
10044 },
10045 label_position: None,
10046 source: Default::default(),
10047 });
10048 let (src_delta, _scene_delta) = frontend
10049 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10050 .await
10051 .unwrap();
10052 assert_eq!(
10053 src_delta.text.as_str(),
10054 "\
10055sketch001 = sketch(on = XY) {
10056 arc1 = arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
10057 distance([arc1, ORIGIN]) == 3mm
10058}
10059"
10060 );
10061
10062 mock_ctx.close().await;
10063 }
10064
10065 #[tokio::test(flavor = "multi_thread")]
10066 async fn test_distance_line_origin() {
10067 let initial_source = "\
10068sketch(on = XY) {
10069 line(start = [var 5, var 0], end = [var 5, var 10])
10070}
10071";
10072
10073 let program = Program::parse(initial_source).unwrap().0.unwrap();
10074
10075 let mut frontend = FrontendState::new();
10076
10077 let mock_ctx = ExecutorContext::new_mock(None).await;
10078 let version = Version(0);
10079
10080 frontend.program = program.clone();
10081 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10082 frontend.update_state_after_exec(outcome, true);
10083 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10084 let sketch_id = sketch_object.id;
10085 let sketch = expect_sketch(sketch_object);
10086 let line_id = *sketch
10087 .segments
10088 .iter()
10089 .find(|segment_id| {
10090 matches!(
10091 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10092 Some(ObjectKind::Segment {
10093 segment: Segment::Line(_)
10094 })
10095 )
10096 })
10097 .unwrap();
10098
10099 let constraint = Constraint::Distance(Distance {
10100 points: vec![ConstraintSegment::ORIGIN, line_id.into()],
10101 distance: Number {
10102 value: 5.0,
10103 units: NumericSuffix::Mm,
10104 },
10105 label_position: None,
10106 source: Default::default(),
10107 });
10108 let (src_delta, _scene_delta) = frontend
10109 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10110 .await
10111 .unwrap();
10112 assert_eq!(
10113 src_delta.text.as_str(),
10114 "\
10115sketch(on = XY) {
10116 line1 = line(start = [var 5, var 0], end = [var 5, var 10])
10117 distance([ORIGIN, line1]) == 5mm
10118}
10119"
10120 );
10121
10122 mock_ctx.close().await;
10123 }
10124
10125 #[tokio::test(flavor = "multi_thread")]
10126 async fn test_distance_line_circle() {
10127 let initial_source = "\
10128sketch(on = XY) {
10129 line(start = [var -10, var 8], end = [var 10, var 8])
10130 circle(start = [var 5, var 0], center = [var 0, var 0])
10131}
10132";
10133
10134 let program = Program::parse(initial_source).unwrap().0.unwrap();
10135
10136 let mut frontend = FrontendState::new();
10137
10138 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10139 let mock_ctx = ExecutorContext::new_mock(None).await;
10140 let version = Version(0);
10141
10142 frontend.hack_set_program(&ctx, program).await.unwrap();
10143 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10144 let sketch_id = sketch_object.id;
10145 let sketch = expect_sketch(sketch_object);
10146 let line_id = *sketch
10147 .segments
10148 .iter()
10149 .find(|segment_id| {
10150 matches!(
10151 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10152 Some(ObjectKind::Segment {
10153 segment: Segment::Line(_)
10154 })
10155 )
10156 })
10157 .unwrap();
10158 let circle_id = *sketch
10159 .segments
10160 .iter()
10161 .find(|segment_id| {
10162 matches!(
10163 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10164 Some(ObjectKind::Segment {
10165 segment: Segment::Circle(_)
10166 })
10167 )
10168 })
10169 .unwrap();
10170
10171 let constraint = Constraint::Distance(Distance {
10172 points: vec![line_id.into(), circle_id.into()],
10173 distance: Number {
10174 value: 3.0,
10175 units: NumericSuffix::Mm,
10176 },
10177 label_position: None,
10178 source: Default::default(),
10179 });
10180 let (src_delta, _scene_delta) = frontend
10181 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10182 .await
10183 .unwrap();
10184 assert_eq!(
10185 src_delta.text.as_str(),
10186 "\
10187sketch(on = XY) {
10188 line1 = line(start = [var -10, var 8], end = [var 10, var 8])
10189 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
10190 distance([line1, circle1]) == 3mm
10191}
10192"
10193 );
10194
10195 ctx.close().await;
10196 mock_ctx.close().await;
10197 }
10198
10199 #[tokio::test(flavor = "multi_thread")]
10200 async fn test_distance_circle_arc() {
10201 let initial_source = "\
10202sketch(on = XY) {
10203 circle(start = [var 5, var 0], center = [var 0, var 0])
10204 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
10205}
10206";
10207
10208 let program = Program::parse(initial_source).unwrap().0.unwrap();
10209
10210 let mut frontend = FrontendState::new();
10211
10212 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10213 let mock_ctx = ExecutorContext::new_mock(None).await;
10214 let version = Version(0);
10215
10216 frontend.hack_set_program(&ctx, program).await.unwrap();
10217 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10218 let sketch_id = sketch_object.id;
10219 let sketch = expect_sketch(sketch_object);
10220 let circle_id = *sketch
10221 .segments
10222 .iter()
10223 .find(|segment_id| {
10224 matches!(
10225 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10226 Some(ObjectKind::Segment {
10227 segment: Segment::Circle(_)
10228 })
10229 )
10230 })
10231 .unwrap();
10232 let arc_id = *sketch
10233 .segments
10234 .iter()
10235 .find(|segment_id| {
10236 matches!(
10237 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10238 Some(ObjectKind::Segment {
10239 segment: Segment::Arc(_)
10240 })
10241 )
10242 })
10243 .unwrap();
10244
10245 let constraint = Constraint::Distance(Distance {
10246 points: vec![circle_id.into(), arc_id.into()],
10247 distance: Number {
10248 value: 3.0,
10249 units: NumericSuffix::Mm,
10250 },
10251 label_position: None,
10252 source: Default::default(),
10253 });
10254 let (src_delta, _scene_delta) = frontend
10255 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10256 .await
10257 .unwrap();
10258 assert_eq!(
10259 src_delta.text.as_str(),
10260 "\
10261sketch(on = XY) {
10262 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
10263 arc1 = arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
10264 distance([circle1, arc1]) == 3mm
10265}
10266"
10267 );
10268
10269 ctx.close().await;
10270 mock_ctx.close().await;
10271 }
10272
10273 #[tokio::test(flavor = "multi_thread")]
10274 async fn test_distance_parallel_lines() {
10275 let initial_source = "\
10276sketch(on = XY) {
10277 line(start = [var 0, var 0], end = [var 10, var 0])
10278 line(start = [var 0, var 5], end = [var 10, var 5])
10279}
10280";
10281
10282 let program = Program::parse(initial_source).unwrap().0.unwrap();
10283
10284 let mut frontend = FrontendState::new();
10285
10286 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10287 let mock_ctx = ExecutorContext::new_mock(None).await;
10288 let version = Version(0);
10289
10290 frontend.hack_set_program(&ctx, program).await.unwrap();
10291 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10292 let sketch_id = sketch_object.id;
10293 let sketch = expect_sketch(sketch_object);
10294 let line_ids = sketch
10295 .segments
10296 .iter()
10297 .copied()
10298 .filter(|segment_id| {
10299 matches!(
10300 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10301 Some(ObjectKind::Segment {
10302 segment: Segment::Line(_)
10303 })
10304 )
10305 })
10306 .collect::<Vec<_>>();
10307
10308 let constraint = Constraint::Distance(Distance {
10309 points: vec![line_ids[0].into(), line_ids[1].into()],
10310 distance: Number {
10311 value: 5.0,
10312 units: NumericSuffix::Mm,
10313 },
10314 label_position: None,
10315 source: Default::default(),
10316 });
10317 let (src_delta, _scene_delta) = frontend
10318 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10319 .await
10320 .unwrap();
10321 assert_eq!(
10322 src_delta.text.as_str(),
10323 "\
10324sketch(on = XY) {
10325 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10326 line2 = line(start = [var 0, var 5], end = [var 10, var 5])
10327 distance([line1, line2]) == 5mm
10328}
10329"
10330 );
10331
10332 ctx.close().await;
10333 mock_ctx.close().await;
10334 }
10335
10336 #[tokio::test(flavor = "multi_thread")]
10337 async fn test_distance_non_parallel_lines_lowers_to_distance() {
10338 let initial_source = "\
10339sketch(on = XY) {
10340 line(start = [var 0, var 0], end = [var 10, var 0])
10341 line(start = [var 0, var 0], end = [var 0, var 10])
10342}
10343";
10344
10345 let program = Program::parse(initial_source).unwrap().0.unwrap();
10346
10347 let mut frontend = FrontendState::new();
10348
10349 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10350 let mock_ctx = ExecutorContext::new_mock(None).await;
10351 let version = Version(0);
10352
10353 frontend.hack_set_program(&ctx, program).await.unwrap();
10354 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10355 let sketch_id = sketch_object.id;
10356 let sketch = expect_sketch(sketch_object);
10357 let line_ids = sketch
10358 .segments
10359 .iter()
10360 .copied()
10361 .filter(|segment_id| {
10362 matches!(
10363 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10364 Some(ObjectKind::Segment {
10365 segment: Segment::Line(_)
10366 })
10367 )
10368 })
10369 .collect::<Vec<_>>();
10370
10371 let constraint = Constraint::Distance(Distance {
10372 points: vec![line_ids[0].into(), line_ids[1].into()],
10373 distance: Number {
10374 value: 5.0,
10375 units: NumericSuffix::Mm,
10376 },
10377 label_position: None,
10378 source: Default::default(),
10379 });
10380 let (src_delta, _scene_delta) = frontend
10381 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10382 .await
10383 .unwrap();
10384 assert_eq!(
10385 src_delta.text.as_str(),
10386 "\
10387sketch(on = XY) {
10388 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10389 line2 = line(start = [var 0, var 0], end = [var 0, var 10])
10390 distance([line1, line2]) == 5mm
10391}
10392"
10393 );
10394
10395 ctx.close().await;
10396 mock_ctx.close().await;
10397 }
10398
10399 #[tokio::test(flavor = "multi_thread")]
10400 async fn test_horizontal_distance_two_points() {
10401 let initial_source = "\
10402sketch(on = XY) {
10403 point(at = [var 1, var 2])
10404 point(at = [var 3, var 4])
10405}
10406";
10407
10408 let program = Program::parse(initial_source).unwrap().0.unwrap();
10409
10410 let mut frontend = FrontendState::new();
10411
10412 let mock_ctx = ExecutorContext::new_mock(None).await;
10413 let version = Version(0);
10414
10415 frontend.program = program.clone();
10416 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10417 frontend.update_state_after_exec(outcome, true);
10418 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10419 let sketch_id = sketch_object.id;
10420 let sketch = expect_sketch(sketch_object);
10421 let point0_id = *sketch.segments.first().unwrap();
10422 let point1_id = *sketch.segments.get(1).unwrap();
10423 let label_position = Point2d {
10424 x: Number {
10425 value: 10.0,
10426 units: NumericSuffix::Mm,
10427 },
10428 y: Number {
10429 value: 11.0,
10430 units: NumericSuffix::Mm,
10431 },
10432 };
10433
10434 let constraint = Constraint::HorizontalDistance(Distance {
10435 points: vec![point0_id.into(), point1_id.into()],
10436 distance: Number {
10437 value: 2.0,
10438 units: NumericSuffix::Mm,
10439 },
10440 label_position: Some(label_position.clone()),
10441 source: Default::default(),
10442 });
10443 let (src_delta, scene_delta) = frontend
10444 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10445 .await
10446 .unwrap();
10447 assert_eq!(
10448 src_delta.text.as_str(),
10449 "\
10451sketch(on = XY) {
10452 point1 = point(at = [var 1, var 2])
10453 point2 = point(at = [var 3, var 4])
10454 horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10455}
10456"
10457 );
10458 assert_eq!(
10459 scene_delta.new_graph.objects.len(),
10460 5,
10461 "{:#?}",
10462 scene_delta.new_graph.objects
10463 );
10464 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10465 let sketch = expect_sketch(sketch_object);
10466 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10467 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10468 panic!("Expected constraint object");
10469 };
10470 let Constraint::HorizontalDistance(distance) = constraint else {
10471 panic!("Expected horizontal distance constraint");
10472 };
10473 assert_eq!(distance.label_position, Some(label_position));
10474
10475 mock_ctx.close().await;
10476 }
10477
10478 #[tokio::test(flavor = "multi_thread")]
10479 async fn test_radius_single_arc_segment() {
10480 let initial_source = "\
10481sketch(on = XY) {
10482 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10483}
10484";
10485
10486 let program = Program::parse(initial_source).unwrap().0.unwrap();
10487
10488 let mut frontend = FrontendState::new();
10489
10490 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10491 let mock_ctx = ExecutorContext::new_mock(None).await;
10492 let version = Version(0);
10493
10494 frontend.hack_set_program(&ctx, program).await.unwrap();
10495 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10496 let sketch_id = sketch_object.id;
10497 let sketch = expect_sketch(sketch_object);
10498 let arc_id = sketch
10500 .segments
10501 .iter()
10502 .find(|&seg_id| {
10503 let obj = frontend.scene_graph.objects.get(seg_id.0);
10504 matches!(
10505 obj.map(|o| &o.kind),
10506 Some(ObjectKind::Segment {
10507 segment: Segment::Arc(_)
10508 })
10509 )
10510 })
10511 .unwrap();
10512
10513 let constraint = Constraint::Radius(Radius {
10514 arc: *arc_id,
10515 radius: Number {
10516 value: 5.0,
10517 units: NumericSuffix::Mm,
10518 },
10519 label_position: None,
10520 source: Default::default(),
10521 });
10522 let (src_delta, scene_delta) = frontend
10523 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10524 .await
10525 .unwrap();
10526 assert_eq!(
10527 src_delta.text.as_str(),
10528 "\
10530sketch(on = XY) {
10531 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10532 radius(arc1) == 5mm
10533}
10534"
10535 );
10536 assert_eq!(
10537 scene_delta.new_graph.objects.len(),
10538 7, "{:#?}",
10540 scene_delta.new_graph.objects
10541 );
10542
10543 ctx.close().await;
10544 mock_ctx.close().await;
10545 }
10546
10547 #[tokio::test(flavor = "multi_thread")]
10548 async fn test_radius_single_arc_segment_with_label_position() {
10549 let initial_source = "\
10550sketch(on = XY) {
10551 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10552}
10553";
10554
10555 let program = Program::parse(initial_source).unwrap().0.unwrap();
10556 let mut frontend = FrontendState::new();
10557 let mock_ctx = ExecutorContext::new_mock(None).await;
10558 let version = Version(0);
10559
10560 frontend.program = program.clone();
10561 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10562 frontend.update_state_after_exec(outcome, true);
10563 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10564 let sketch_id = sketch_object.id;
10565 let sketch = expect_sketch(sketch_object);
10566 let arc_id = sketch
10567 .segments
10568 .iter()
10569 .find(|&seg_id| {
10570 let obj = frontend.scene_graph.objects.get(seg_id.0);
10571 matches!(
10572 obj.map(|o| &o.kind),
10573 Some(ObjectKind::Segment {
10574 segment: Segment::Arc(_)
10575 })
10576 )
10577 })
10578 .unwrap();
10579
10580 let label_position = Point2d {
10581 x: Number {
10582 value: 10.0,
10583 units: NumericSuffix::Mm,
10584 },
10585 y: Number {
10586 value: 11.0,
10587 units: NumericSuffix::Mm,
10588 },
10589 };
10590 let constraint = Constraint::Radius(Radius {
10591 arc: *arc_id,
10592 radius: Number {
10593 value: 5.0,
10594 units: NumericSuffix::Mm,
10595 },
10596 label_position: Some(label_position.clone()),
10597 source: Default::default(),
10598 });
10599 let (src_delta, scene_delta) = frontend
10600 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10601 .await
10602 .unwrap();
10603 assert_eq!(
10604 src_delta.text.as_str(),
10605 "\
10606sketch(on = XY) {
10607 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10608 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10609}
10610"
10611 );
10612
10613 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10614 let sketch = expect_sketch(sketch_object);
10615 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10616 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10617 panic!("Expected constraint object");
10618 };
10619 let Constraint::Radius(radius) = constraint else {
10620 panic!("Expected radius constraint");
10621 };
10622 assert_eq!(radius.label_position, Some(label_position));
10623
10624 mock_ctx.close().await;
10625 }
10626
10627 #[tokio::test(flavor = "multi_thread")]
10628 async fn test_edit_radius_constraint_label_position() {
10629 let initial_source = "\
10630sketch(on = XY) {
10631 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10632 radius(arc1) == 5mm
10633}
10634";
10635
10636 let program = Program::parse(initial_source).unwrap().0.unwrap();
10637 let mut frontend = FrontendState::new();
10638 let mock_ctx = ExecutorContext::new_mock(None).await;
10639 let version = Version(0);
10640
10641 frontend.program = program.clone();
10642 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10643 frontend.update_state_after_exec(outcome, true);
10644 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10645 let sketch_id = sketch_object.id;
10646 let sketch = expect_sketch(sketch_object);
10647 let constraint_id = sketch.constraints[0];
10648 let label_position = Point2d {
10649 x: Number {
10650 value: 10.0,
10651 units: NumericSuffix::Mm,
10652 },
10653 y: Number {
10654 value: 11.0,
10655 units: NumericSuffix::Mm,
10656 },
10657 };
10658
10659 let (src_delta, scene_delta) = frontend
10660 .edit_distance_constraint_label_position(
10661 &mock_ctx,
10662 version,
10663 sketch_id,
10664 constraint_id,
10665 label_position.clone(),
10666 vec![],
10667 )
10668 .await
10669 .unwrap();
10670 assert_eq!(
10671 src_delta.text.as_str(),
10672 "\
10673sketch(on = XY) {
10674 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10675 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10676}
10677"
10678 );
10679
10680 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10681 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10682 panic!("Expected constraint object");
10683 };
10684 let Constraint::Radius(radius) = constraint else {
10685 panic!("Expected radius constraint");
10686 };
10687 assert_eq!(radius.label_position, Some(label_position));
10688
10689 mock_ctx.close().await;
10690 }
10691
10692 #[tokio::test(flavor = "multi_thread")]
10693 async fn test_vertical_distance_two_points() {
10694 let initial_source = "\
10695sketch(on = XY) {
10696 point(at = [var 1, var 2])
10697 point(at = [var 3, var 4])
10698}
10699";
10700
10701 let program = Program::parse(initial_source).unwrap().0.unwrap();
10702
10703 let mut frontend = FrontendState::new();
10704
10705 let mock_ctx = ExecutorContext::new_mock(None).await;
10706 let version = Version(0);
10707
10708 frontend.program = program.clone();
10709 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10710 frontend.update_state_after_exec(outcome, true);
10711 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10712 let sketch_id = sketch_object.id;
10713 let sketch = expect_sketch(sketch_object);
10714 let point0_id = *sketch.segments.first().unwrap();
10715 let point1_id = *sketch.segments.get(1).unwrap();
10716 let label_position = Point2d {
10717 x: Number {
10718 value: 10.0,
10719 units: NumericSuffix::Mm,
10720 },
10721 y: Number {
10722 value: 11.0,
10723 units: NumericSuffix::Mm,
10724 },
10725 };
10726
10727 let constraint = Constraint::VerticalDistance(Distance {
10728 points: vec![point0_id.into(), point1_id.into()],
10729 distance: Number {
10730 value: 2.0,
10731 units: NumericSuffix::Mm,
10732 },
10733 label_position: Some(label_position.clone()),
10734 source: Default::default(),
10735 });
10736 let (src_delta, scene_delta) = frontend
10737 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10738 .await
10739 .unwrap();
10740 assert_eq!(
10741 src_delta.text.as_str(),
10742 "\
10744sketch(on = XY) {
10745 point1 = point(at = [var 1, var 2])
10746 point2 = point(at = [var 3, var 4])
10747 verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10748}
10749"
10750 );
10751 assert_eq!(
10752 scene_delta.new_graph.objects.len(),
10753 5,
10754 "{:#?}",
10755 scene_delta.new_graph.objects
10756 );
10757 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10758 let sketch = expect_sketch(sketch_object);
10759 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10760 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10761 panic!("Expected constraint object");
10762 };
10763 let Constraint::VerticalDistance(distance) = constraint else {
10764 panic!("Expected vertical distance constraint");
10765 };
10766 assert_eq!(distance.label_position, Some(label_position));
10767
10768 mock_ctx.close().await;
10769 }
10770
10771 #[tokio::test(flavor = "multi_thread")]
10772 async fn test_add_fixed_standalone_point() {
10773 let initial_source = "\
10774sketch(on = XY) {
10775 point(at = [var 1, var 2])
10776}
10777";
10778
10779 let program = Program::parse(initial_source).unwrap().0.unwrap();
10780
10781 let mut frontend = FrontendState::new();
10782
10783 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10784 let mock_ctx = ExecutorContext::new_mock(None).await;
10785 let version = Version(0);
10786
10787 frontend.hack_set_program(&ctx, program).await.unwrap();
10788 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10789 let sketch_id = sketch_object.id;
10790 let sketch = expect_sketch(sketch_object);
10791 let point_id = *sketch.segments.first().unwrap();
10792
10793 let (src_delta, scene_delta) = frontend
10794 .add_constraint(
10795 &mock_ctx,
10796 version,
10797 sketch_id,
10798 Constraint::Fixed(Fixed {
10799 points: vec![FixedPoint {
10800 point: point_id,
10801 position: Point2d {
10802 x: Number {
10803 value: 2.0,
10804 units: NumericSuffix::Mm,
10805 },
10806 y: Number {
10807 value: 3.0,
10808 units: NumericSuffix::Mm,
10809 },
10810 },
10811 }],
10812 }),
10813 )
10814 .await
10815 .unwrap();
10816 assert_eq!(
10817 src_delta.text.as_str(),
10818 "\
10819sketch(on = XY) {
10820 point1 = point(at = [var 1, var 2])
10821 fixed([point1, [2mm, 3mm]])
10822}
10823"
10824 );
10825 assert_eq!(
10826 scene_delta.new_graph.objects.len(),
10827 4,
10828 "{:#?}",
10829 scene_delta.new_graph.objects
10830 );
10831
10832 ctx.close().await;
10833 mock_ctx.close().await;
10834 }
10835
10836 #[tokio::test(flavor = "multi_thread")]
10837 async fn test_add_fixed_multiple_points() {
10838 let initial_source = "\
10839sketch(on = XY) {
10840 point(at = [var 1, var 2])
10841 point(at = [var 3, var 4])
10842}
10843";
10844
10845 let program = Program::parse(initial_source).unwrap().0.unwrap();
10846
10847 let mut frontend = FrontendState::new();
10848
10849 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10850 let mock_ctx = ExecutorContext::new_mock(None).await;
10851 let version = Version(0);
10852
10853 frontend.hack_set_program(&ctx, program).await.unwrap();
10854 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10855 let sketch_id = sketch_object.id;
10856 let sketch = expect_sketch(sketch_object);
10857 let point0_id = *sketch.segments.first().unwrap();
10858 let point1_id = *sketch.segments.get(1).unwrap();
10859
10860 let (src_delta, scene_delta) = frontend
10861 .add_constraint(
10862 &mock_ctx,
10863 version,
10864 sketch_id,
10865 Constraint::Fixed(Fixed {
10866 points: vec![
10867 FixedPoint {
10868 point: point0_id,
10869 position: Point2d {
10870 x: Number {
10871 value: 2.0,
10872 units: NumericSuffix::Mm,
10873 },
10874 y: Number {
10875 value: 3.0,
10876 units: NumericSuffix::Mm,
10877 },
10878 },
10879 },
10880 FixedPoint {
10881 point: point1_id,
10882 position: Point2d {
10883 x: Number {
10884 value: 4.0,
10885 units: NumericSuffix::Mm,
10886 },
10887 y: Number {
10888 value: 5.0,
10889 units: NumericSuffix::Mm,
10890 },
10891 },
10892 },
10893 ],
10894 }),
10895 )
10896 .await
10897 .unwrap();
10898 assert_eq!(
10899 src_delta.text.as_str(),
10900 "\
10901sketch(on = XY) {
10902 point1 = point(at = [var 1, var 2])
10903 point2 = point(at = [var 3, var 4])
10904 fixed([point1, [2mm, 3mm]])
10905 fixed([point2, [4mm, 5mm]])
10906}
10907"
10908 );
10909 assert_eq!(
10910 scene_delta.new_graph.objects.len(),
10911 6,
10912 "{:#?}",
10913 scene_delta.new_graph.objects
10914 );
10915
10916 ctx.close().await;
10917 mock_ctx.close().await;
10918 }
10919
10920 #[tokio::test(flavor = "multi_thread")]
10921 async fn test_add_fixed_owned_point() {
10922 let initial_source = "\
10923sketch(on = XY) {
10924 line(start = [var 1, var 2], end = [var 3, var 4])
10925}
10926";
10927
10928 let program = Program::parse(initial_source).unwrap().0.unwrap();
10929
10930 let mut frontend = FrontendState::new();
10931
10932 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10933 let mock_ctx = ExecutorContext::new_mock(None).await;
10934 let version = Version(0);
10935
10936 frontend.hack_set_program(&ctx, program).await.unwrap();
10937 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10938 let sketch_id = sketch_object.id;
10939 let sketch = expect_sketch(sketch_object);
10940 let line_start_id = *sketch.segments.first().unwrap();
10941
10942 let (src_delta, scene_delta) = frontend
10943 .add_constraint(
10944 &mock_ctx,
10945 version,
10946 sketch_id,
10947 Constraint::Fixed(Fixed {
10948 points: vec![FixedPoint {
10949 point: line_start_id,
10950 position: Point2d {
10951 x: Number {
10952 value: 2.0,
10953 units: NumericSuffix::Mm,
10954 },
10955 y: Number {
10956 value: 3.0,
10957 units: NumericSuffix::Mm,
10958 },
10959 },
10960 }],
10961 }),
10962 )
10963 .await
10964 .unwrap();
10965 assert_eq!(
10966 src_delta.text.as_str(),
10967 "\
10968sketch(on = XY) {
10969 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10970 fixed([line1.start, [2mm, 3mm]])
10971}
10972"
10973 );
10974 assert_eq!(
10975 scene_delta.new_graph.objects.len(),
10976 6,
10977 "{:#?}",
10978 scene_delta.new_graph.objects
10979 );
10980
10981 ctx.close().await;
10982 mock_ctx.close().await;
10983 }
10984
10985 #[tokio::test(flavor = "multi_thread")]
10986 async fn test_radius_error_cases() {
10987 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10988 let mock_ctx = ExecutorContext::new_mock(None).await;
10989 let version = Version(0);
10990
10991 let initial_source_point = "\
10993sketch(on = XY) {
10994 point(at = [var 1, var 2])
10995}
10996";
10997 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10998 let mut frontend_point = FrontendState::new();
10999 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11000 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11001 let sketch_id_point = sketch_object_point.id;
11002 let sketch_point = expect_sketch(sketch_object_point);
11003 let point_id = *sketch_point.segments.first().unwrap();
11004
11005 let constraint_point = Constraint::Radius(Radius {
11006 arc: point_id,
11007 radius: Number {
11008 value: 5.0,
11009 units: NumericSuffix::Mm,
11010 },
11011 label_position: None,
11012 source: Default::default(),
11013 });
11014 let result_point = frontend_point
11015 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11016 .await;
11017 assert!(result_point.is_err(), "Single point should error for radius");
11018
11019 let initial_source_line = "\
11021sketch(on = XY) {
11022 line(start = [var 1, var 2], end = [var 3, var 4])
11023}
11024";
11025 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11026 let mut frontend_line = FrontendState::new();
11027 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11028 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11029 let sketch_id_line = sketch_object_line.id;
11030 let sketch_line = expect_sketch(sketch_object_line);
11031 let line_id = *sketch_line.segments.first().unwrap();
11032
11033 let constraint_line = Constraint::Radius(Radius {
11034 arc: line_id,
11035 radius: Number {
11036 value: 5.0,
11037 units: NumericSuffix::Mm,
11038 },
11039 label_position: None,
11040 source: Default::default(),
11041 });
11042 let result_line = frontend_line
11043 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11044 .await;
11045 assert!(result_line.is_err(), "Single line segment should error for radius");
11046
11047 ctx.close().await;
11048 mock_ctx.close().await;
11049 }
11050
11051 #[tokio::test(flavor = "multi_thread")]
11052 async fn test_diameter_single_arc_segment() {
11053 let initial_source = "\
11054sketch(on = XY) {
11055 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11056}
11057";
11058
11059 let program = Program::parse(initial_source).unwrap().0.unwrap();
11060
11061 let mut frontend = FrontendState::new();
11062
11063 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11064 let mock_ctx = ExecutorContext::new_mock(None).await;
11065 let version = Version(0);
11066
11067 frontend.hack_set_program(&ctx, program).await.unwrap();
11068 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11069 let sketch_id = sketch_object.id;
11070 let sketch = expect_sketch(sketch_object);
11071 let arc_id = sketch
11073 .segments
11074 .iter()
11075 .find(|&seg_id| {
11076 let obj = frontend.scene_graph.objects.get(seg_id.0);
11077 matches!(
11078 obj.map(|o| &o.kind),
11079 Some(ObjectKind::Segment {
11080 segment: Segment::Arc(_)
11081 })
11082 )
11083 })
11084 .unwrap();
11085
11086 let constraint = Constraint::Diameter(Diameter {
11087 arc: *arc_id,
11088 diameter: Number {
11089 value: 10.0,
11090 units: NumericSuffix::Mm,
11091 },
11092 label_position: None,
11093 source: Default::default(),
11094 });
11095 let (src_delta, scene_delta) = frontend
11096 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11097 .await
11098 .unwrap();
11099 assert_eq!(
11100 src_delta.text.as_str(),
11101 "\
11103sketch(on = XY) {
11104 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11105 diameter(arc1) == 10mm
11106}
11107"
11108 );
11109 assert_eq!(
11110 scene_delta.new_graph.objects.len(),
11111 7, "{:#?}",
11113 scene_delta.new_graph.objects
11114 );
11115
11116 ctx.close().await;
11117 mock_ctx.close().await;
11118 }
11119
11120 #[tokio::test(flavor = "multi_thread")]
11121 async fn test_diameter_single_arc_segment_with_label_position() {
11122 let initial_source = "\
11123sketch(on = XY) {
11124 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11125}
11126";
11127
11128 let program = Program::parse(initial_source).unwrap().0.unwrap();
11129 let mut frontend = FrontendState::new();
11130 let mock_ctx = ExecutorContext::new_mock(None).await;
11131 let version = Version(0);
11132
11133 frontend.program = program.clone();
11134 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11135 frontend.update_state_after_exec(outcome, true);
11136 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11137 let sketch_id = sketch_object.id;
11138 let sketch = expect_sketch(sketch_object);
11139 let arc_id = sketch
11140 .segments
11141 .iter()
11142 .find(|&seg_id| {
11143 let obj = frontend.scene_graph.objects.get(seg_id.0);
11144 matches!(
11145 obj.map(|o| &o.kind),
11146 Some(ObjectKind::Segment {
11147 segment: Segment::Arc(_)
11148 })
11149 )
11150 })
11151 .unwrap();
11152
11153 let label_position = Point2d {
11154 x: Number {
11155 value: 10.0,
11156 units: NumericSuffix::Mm,
11157 },
11158 y: Number {
11159 value: 11.0,
11160 units: NumericSuffix::Mm,
11161 },
11162 };
11163 let constraint = Constraint::Diameter(Diameter {
11164 arc: *arc_id,
11165 diameter: Number {
11166 value: 10.0,
11167 units: NumericSuffix::Mm,
11168 },
11169 label_position: Some(label_position.clone()),
11170 source: Default::default(),
11171 });
11172 let (src_delta, scene_delta) = frontend
11173 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11174 .await
11175 .unwrap();
11176 assert_eq!(
11177 src_delta.text.as_str(),
11178 "\
11179sketch(on = XY) {
11180 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11181 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
11182}
11183"
11184 );
11185
11186 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11187 let sketch = expect_sketch(sketch_object);
11188 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11189 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11190 panic!("Expected constraint object");
11191 };
11192 let Constraint::Diameter(diameter) = constraint else {
11193 panic!("Expected diameter constraint");
11194 };
11195 assert_eq!(diameter.label_position, Some(label_position));
11196
11197 mock_ctx.close().await;
11198 }
11199
11200 #[tokio::test(flavor = "multi_thread")]
11201 async fn test_edit_diameter_constraint_label_position() {
11202 let initial_source = "\
11203sketch(on = XY) {
11204 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11205 diameter(arc1) == 10mm
11206}
11207";
11208
11209 let program = Program::parse(initial_source).unwrap().0.unwrap();
11210 let mut frontend = FrontendState::new();
11211 let mock_ctx = ExecutorContext::new_mock(None).await;
11212 let version = Version(0);
11213
11214 frontend.program = program.clone();
11215 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11216 frontend.update_state_after_exec(outcome, true);
11217 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11218 let sketch_id = sketch_object.id;
11219 let sketch = expect_sketch(sketch_object);
11220 let constraint_id = sketch.constraints[0];
11221 let label_position = Point2d {
11222 x: Number {
11223 value: 10.0,
11224 units: NumericSuffix::Mm,
11225 },
11226 y: Number {
11227 value: 11.0,
11228 units: NumericSuffix::Mm,
11229 },
11230 };
11231
11232 let (src_delta, scene_delta) = frontend
11233 .edit_distance_constraint_label_position(
11234 &mock_ctx,
11235 version,
11236 sketch_id,
11237 constraint_id,
11238 label_position.clone(),
11239 vec![],
11240 )
11241 .await
11242 .unwrap();
11243 assert_eq!(
11244 src_delta.text.as_str(),
11245 "\
11246sketch(on = XY) {
11247 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11248 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
11249}
11250"
11251 );
11252
11253 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11254 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11255 panic!("Expected constraint object");
11256 };
11257 let Constraint::Diameter(diameter) = constraint else {
11258 panic!("Expected diameter constraint");
11259 };
11260 assert_eq!(diameter.label_position, Some(label_position));
11261
11262 mock_ctx.close().await;
11263 }
11264
11265 #[tokio::test(flavor = "multi_thread")]
11266 async fn test_diameter_error_cases() {
11267 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11268 let mock_ctx = ExecutorContext::new_mock(None).await;
11269 let version = Version(0);
11270
11271 let initial_source_point = "\
11273sketch(on = XY) {
11274 point(at = [var 1, var 2])
11275}
11276";
11277 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
11278 let mut frontend_point = FrontendState::new();
11279 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11280 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11281 let sketch_id_point = sketch_object_point.id;
11282 let sketch_point = expect_sketch(sketch_object_point);
11283 let point_id = *sketch_point.segments.first().unwrap();
11284
11285 let constraint_point = Constraint::Diameter(Diameter {
11286 arc: point_id,
11287 diameter: Number {
11288 value: 10.0,
11289 units: NumericSuffix::Mm,
11290 },
11291 label_position: None,
11292 source: Default::default(),
11293 });
11294 let result_point = frontend_point
11295 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11296 .await;
11297 assert!(result_point.is_err(), "Single point should error for diameter");
11298
11299 let initial_source_line = "\
11301sketch(on = XY) {
11302 line(start = [var 1, var 2], end = [var 3, var 4])
11303}
11304";
11305 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11306 let mut frontend_line = FrontendState::new();
11307 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11308 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11309 let sketch_id_line = sketch_object_line.id;
11310 let sketch_line = expect_sketch(sketch_object_line);
11311 let line_id = *sketch_line.segments.first().unwrap();
11312
11313 let constraint_line = Constraint::Diameter(Diameter {
11314 arc: line_id,
11315 diameter: Number {
11316 value: 10.0,
11317 units: NumericSuffix::Mm,
11318 },
11319 label_position: None,
11320 source: Default::default(),
11321 });
11322 let result_line = frontend_line
11323 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11324 .await;
11325 assert!(result_line.is_err(), "Single line segment should error for diameter");
11326
11327 ctx.close().await;
11328 mock_ctx.close().await;
11329 }
11330
11331 #[tokio::test(flavor = "multi_thread")]
11332 async fn test_line_horizontal() {
11333 let initial_source = "\
11334sketch(on = XY) {
11335 line(start = [var 1, var 2], end = [var 3, var 4])
11336}
11337";
11338
11339 let program = Program::parse(initial_source).unwrap().0.unwrap();
11340
11341 let mut frontend = FrontendState::new();
11342
11343 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11344 let mock_ctx = ExecutorContext::new_mock(None).await;
11345 let version = Version(0);
11346
11347 frontend.hack_set_program(&ctx, program).await.unwrap();
11348 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11349 let sketch_id = sketch_object.id;
11350 let sketch = expect_sketch(sketch_object);
11351 let line1_id = *sketch.segments.get(2).unwrap();
11352
11353 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
11354 let (src_delta, scene_delta) = frontend
11355 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11356 .await
11357 .unwrap();
11358 assert_eq!(
11359 src_delta.text.as_str(),
11360 "\
11361sketch(on = XY) {
11362 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11363 horizontal(line1)
11364}
11365"
11366 );
11367 assert_eq!(
11368 scene_delta.new_graph.objects.len(),
11369 6,
11370 "{:#?}",
11371 scene_delta.new_graph.objects
11372 );
11373
11374 ctx.close().await;
11375 mock_ctx.close().await;
11376 }
11377
11378 #[tokio::test(flavor = "multi_thread")]
11379 async fn test_control_point_spline_edge_horizontal() {
11380 let initial_source = "\
11381@settings(experimentalFeatures = allow)
11382splineSketch = sketch(on = XY) {
11383 controlPointSpline1 = controlPointSpline(points = [
11384 [var 0mm, var 0mm],
11385 [var 10mm, var 20mm],
11386 [var 20mm, var 0mm],
11387 ])
11388}
11389";
11390
11391 let program = Program::parse(initial_source).unwrap().0.unwrap();
11392
11393 let mut frontend = FrontendState::new();
11394
11395 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11396 let mock_ctx = ExecutorContext::new_mock(None).await;
11397 let version = Version(0);
11398
11399 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11400 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11401 let sketch_id = sketch_object.id;
11402 let sketch = expect_sketch(sketch_object);
11403 let spline_id = sketch
11404 .segments
11405 .iter()
11406 .copied()
11407 .find(|seg_id| {
11408 matches!(
11409 &frontend.scene_graph.objects[seg_id.0].kind,
11410 ObjectKind::Segment {
11411 segment: Segment::ControlPointSpline(_)
11412 }
11413 )
11414 })
11415 .expect("Expected a control point spline segment in sketch");
11416 let edge_id = frontend
11417 .scene_graph
11418 .objects
11419 .iter()
11420 .find_map(|obj| match &obj.kind {
11421 ObjectKind::Segment {
11422 segment: Segment::Line(line),
11423 } if line.owner == Some(spline_id) => Some(obj.id),
11424 _ => None,
11425 })
11426 .expect("Expected an owned control-polygon edge");
11427
11428 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
11429 let (src_delta, _) = frontend
11430 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11431 .await
11432 .unwrap();
11433 assert!(
11434 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
11435 "Expected horizontal constraint on spline edge, got: {}",
11436 src_delta.text
11437 );
11438
11439 ctx.close().await;
11440 mock_ctx.close().await;
11441 }
11442
11443 #[tokio::test(flavor = "multi_thread")]
11444 async fn test_control_point_spline_edge_angle() {
11445 let initial_source = "\
11446@settings(experimentalFeatures = allow)
11447splineSketch = sketch(on = XY) {
11448 controlPointSpline1 = controlPointSpline(points = [
11449 [var 0mm, var 0mm],
11450 [var 10mm, var 20mm],
11451 [var 20mm, var 0mm],
11452 ])
11453
11454 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
11455}
11456";
11457
11458 let program = Program::parse(initial_source).unwrap().0.unwrap();
11459
11460 let mut frontend = FrontendState::new();
11461
11462 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11463 let mock_ctx = ExecutorContext::new_mock(None).await;
11464 let version = Version(0);
11465
11466 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11467 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11468 let sketch_id = sketch_object.id;
11469 let sketch = expect_sketch(sketch_object);
11470 let spline_id = sketch
11471 .segments
11472 .iter()
11473 .copied()
11474 .find(|seg_id| {
11475 matches!(
11476 &frontend.scene_graph.objects[seg_id.0].kind,
11477 ObjectKind::Segment {
11478 segment: Segment::ControlPointSpline(_)
11479 }
11480 )
11481 })
11482 .expect("Expected a control point spline segment in sketch");
11483 let edge_id = frontend
11484 .scene_graph
11485 .objects
11486 .iter()
11487 .find_map(|obj| match &obj.kind {
11488 ObjectKind::Segment {
11489 segment: Segment::Line(line),
11490 } if line.owner == Some(spline_id) => Some(obj.id),
11491 _ => None,
11492 })
11493 .expect("Expected an owned control-polygon edge");
11494 let line1_id = frontend
11495 .scene_graph
11496 .objects
11497 .iter()
11498 .find_map(|obj| match &obj.kind {
11499 ObjectKind::Segment {
11500 segment: Segment::Line(line),
11501 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
11502 _ => None,
11503 })
11504 .or_else(|| {
11505 sketch.segments.iter().copied().find(|seg_id| {
11506 matches!(
11507 &frontend.scene_graph.objects[seg_id.0].kind,
11508 ObjectKind::Segment {
11509 segment: Segment::Line(line),
11510 } if line.owner.is_none()
11511 )
11512 })
11513 })
11514 .expect("Expected a standalone line segment in sketch");
11515
11516 let constraint = Constraint::Angle(Angle {
11517 lines: vec![line1_id, edge_id],
11518 angle: Number {
11519 value: 30.0,
11520 units: NumericSuffix::Deg,
11521 },
11522 source: Default::default(),
11523 });
11524 let (src_delta, _) = frontend
11525 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11526 .await
11527 .unwrap();
11528 assert!(
11529 src_delta
11530 .text
11531 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
11532 "Expected angle constraint on spline edge, got: {}",
11533 src_delta.text
11534 );
11535
11536 ctx.close().await;
11537 mock_ctx.close().await;
11538 }
11539
11540 #[tokio::test(flavor = "multi_thread")]
11541 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
11542 let initial_source = "\
11543@settings(experimentalFeatures = allow)
11544splineSketch = sketch(on = XY) {
11545 spline1 = controlPointSpline(points = [
11546 [var 0mm, var 0mm],
11547 [var 10mm, var 20mm],
11548 [var 20mm, var 0mm],
11549 ])
11550 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
11551 coincident([spline1.controls[1], spline1.edges[0]])
11552 coincident([spline1.controls[0], line1])
11553}
11554";
11555
11556 let program = Program::parse(initial_source).unwrap().0.unwrap();
11557
11558 let mut frontend = FrontendState::new();
11559
11560 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11561 let mock_ctx = ExecutorContext::new_mock(None).await;
11562
11563 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11564
11565 let ui_scene_graph = frontend.scene_graph_for_ui();
11566 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
11567 let sketch = expect_sketch(sketch_object);
11568
11569 assert_eq!(
11570 sketch.constraints.len(),
11571 1,
11572 "Expected only the external coincident constraint to remain visible in the UI scene graph"
11573 );
11574
11575 let visible_constraints = ui_scene_graph
11576 .objects
11577 .iter()
11578 .filter_map(|object| match &object.kind {
11579 ObjectKind::Constraint {
11580 constraint: Constraint::Coincident(coincident),
11581 } => Some(coincident.clone()),
11582 _ => None,
11583 })
11584 .collect::<Vec<_>>();
11585
11586 assert_eq!(
11587 visible_constraints.len(),
11588 1,
11589 "Expected only one coincident constraint object in the UI scene graph"
11590 );
11591 assert_eq!(
11592 visible_constraints[0].get_segments().len(),
11593 2,
11594 "Expected the remaining visible coincident constraint to reference two segments"
11595 );
11596
11597 ctx.close().await;
11598 mock_ctx.close().await;
11599 }
11600
11601 #[tokio::test(flavor = "multi_thread")]
11602 async fn test_edit_control_point_spline_can_append_control_point() {
11603 let initial_source = "\
11604@settings(experimentalFeatures = allow)
11605splineSketch = sketch(on = XY) {
11606 controlPointSpline(points = [
11607 [var 0mm, var 0mm],
11608 [var 10mm, var 20mm],
11609 [var 20mm, var 0mm],
11610 ])
11611}
11612";
11613
11614 let program = Program::parse(initial_source).unwrap().0.unwrap();
11615
11616 let mut frontend = FrontendState::new();
11617
11618 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11619 let mock_ctx = ExecutorContext::new_mock(None).await;
11620 let version = Version(0);
11621
11622 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11623 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11624 let sketch_id = sketch_object.id;
11625 let sketch = expect_sketch(sketch_object);
11626 let spline_id = sketch
11627 .segments
11628 .iter()
11629 .copied()
11630 .find(|seg_id| {
11631 matches!(
11632 &frontend.scene_graph.objects[seg_id.0].kind,
11633 ObjectKind::Segment {
11634 segment: Segment::ControlPointSpline(_)
11635 }
11636 )
11637 })
11638 .expect("Expected a control point spline segment in sketch");
11639
11640 let ctor = ControlPointSplineCtor {
11641 points: vec![
11642 Point2d {
11643 x: Expr::Var(Number {
11644 value: 0.0,
11645 units: NumericSuffix::Mm,
11646 }),
11647 y: Expr::Var(Number {
11648 value: 0.0,
11649 units: NumericSuffix::Mm,
11650 }),
11651 },
11652 Point2d {
11653 x: Expr::Var(Number {
11654 value: 10.0,
11655 units: NumericSuffix::Mm,
11656 }),
11657 y: Expr::Var(Number {
11658 value: 20.0,
11659 units: NumericSuffix::Mm,
11660 }),
11661 },
11662 Point2d {
11663 x: Expr::Var(Number {
11664 value: 20.0,
11665 units: NumericSuffix::Mm,
11666 }),
11667 y: Expr::Var(Number {
11668 value: 0.0,
11669 units: NumericSuffix::Mm,
11670 }),
11671 },
11672 Point2d {
11673 x: Expr::Var(Number {
11674 value: 30.0,
11675 units: NumericSuffix::Mm,
11676 }),
11677 y: Expr::Var(Number {
11678 value: 10.0,
11679 units: NumericSuffix::Mm,
11680 }),
11681 },
11682 ],
11683 construction: None,
11684 };
11685
11686 let segments = vec![ExistingSegmentCtor {
11687 id: spline_id,
11688 ctor: SegmentCtor::ControlPointSpline(ctor),
11689 }];
11690 let (src_delta, scene_delta) = frontend
11691 .edit_segments(&mock_ctx, version, sketch_id, segments)
11692 .await
11693 .unwrap();
11694
11695 assert!(
11696 src_delta.text.contains("[var 30mm, var 10mm]"),
11697 "Expected appended spline control point in source, got: {}",
11698 src_delta.text
11699 );
11700
11701 assert!(
11702 scene_delta.invalidates_ids,
11703 "Expected appending a spline control point to invalidate ids"
11704 );
11705 let updated_spline = scene_delta
11706 .new_graph
11707 .objects
11708 .iter()
11709 .find_map(|obj| match &obj.kind {
11710 ObjectKind::Segment {
11711 segment: Segment::ControlPointSpline(updated_spline),
11712 } if updated_spline.controls.len() == 4 => Some(updated_spline),
11713 _ => None,
11714 })
11715 .expect("Expected edited scene graph to contain a four-point control point spline");
11716 assert_eq!(
11717 updated_spline.controls.len(),
11718 4,
11719 "Expected edited spline to expose four control points"
11720 );
11721
11722 ctx.close().await;
11723 mock_ctx.close().await;
11724 }
11725
11726 #[tokio::test(flavor = "multi_thread")]
11727 async fn test_line_vertical() {
11728 let initial_source = "\
11729sketch(on = XY) {
11730 line(start = [var 1, var 2], end = [var 3, var 4])
11731}
11732";
11733
11734 let program = Program::parse(initial_source).unwrap().0.unwrap();
11735
11736 let mut frontend = FrontendState::new();
11737
11738 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11739 let mock_ctx = ExecutorContext::new_mock(None).await;
11740 let version = Version(0);
11741
11742 frontend.hack_set_program(&ctx, program).await.unwrap();
11743 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11744 let sketch_id = sketch_object.id;
11745 let sketch = expect_sketch(sketch_object);
11746 let line1_id = *sketch.segments.get(2).unwrap();
11747
11748 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
11749 let (src_delta, scene_delta) = frontend
11750 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11751 .await
11752 .unwrap();
11753 assert_eq!(
11754 src_delta.text.as_str(),
11755 "\
11756sketch(on = XY) {
11757 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11758 vertical(line1)
11759}
11760"
11761 );
11762 assert_eq!(
11763 scene_delta.new_graph.objects.len(),
11764 6,
11765 "{:#?}",
11766 scene_delta.new_graph.objects
11767 );
11768
11769 ctx.close().await;
11770 mock_ctx.close().await;
11771 }
11772
11773 #[tokio::test(flavor = "multi_thread")]
11774 async fn test_points_vertical() {
11775 let initial_source = "\
11776sketch001 = sketch(on = XY) {
11777 p0 = point(at = [var -2.23mm, var 3.1mm])
11778 pf = point(at = [4, 4])
11779}
11780";
11781
11782 let program = Program::parse(initial_source).unwrap().0.unwrap();
11783
11784 let mut frontend = FrontendState::new();
11785
11786 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11787 let mock_ctx = ExecutorContext::new_mock(None).await;
11788 let version = Version(0);
11789
11790 frontend.hack_set_program(&ctx, program).await.unwrap();
11791 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11792 let sketch_id = sketch_object.id;
11793 let sketch = expect_sketch(sketch_object);
11794 let point_ids = vec![
11795 sketch.segments.first().unwrap().to_owned(),
11796 sketch.segments.get(1).unwrap().to_owned(),
11797 ];
11798
11799 let constraint = Constraint::Vertical(Vertical::Points {
11800 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
11801 });
11802 let (src_delta, scene_delta) = frontend
11803 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11804 .await
11805 .unwrap();
11806 assert_eq!(
11807 src_delta.text.as_str(),
11808 "\
11809sketch001 = sketch(on = XY) {
11810 p0 = point(at = [var -2.23mm, var 3.1mm])
11811 pf = point(at = [4, 4])
11812 vertical([p0, pf])
11813}
11814"
11815 );
11816 assert_eq!(
11817 scene_delta.new_graph.objects.len(),
11818 5,
11819 "{:#?}",
11820 scene_delta.new_graph.objects
11821 );
11822
11823 ctx.close().await;
11824 mock_ctx.close().await;
11825 }
11826
11827 #[tokio::test(flavor = "multi_thread")]
11828 async fn test_points_horizontal() {
11829 let initial_source = "\
11830sketch001 = sketch(on = XY) {
11831 p0 = point(at = [var -2.23mm, var 3.1mm])
11832 pf = point(at = [4, 4])
11833}
11834";
11835
11836 let program = Program::parse(initial_source).unwrap().0.unwrap();
11837
11838 let mut frontend = FrontendState::new();
11839
11840 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11841 let mock_ctx = ExecutorContext::new_mock(None).await;
11842 let version = Version(0);
11843
11844 frontend.hack_set_program(&ctx, program).await.unwrap();
11845 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11846 let sketch_id = sketch_object.id;
11847 let sketch = expect_sketch(sketch_object);
11848 let point_ids = vec![
11849 sketch.segments.first().unwrap().to_owned(),
11850 sketch.segments.get(1).unwrap().to_owned(),
11851 ];
11852
11853 let constraint = Constraint::Horizontal(Horizontal::Points {
11854 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
11855 });
11856 let (src_delta, scene_delta) = frontend
11857 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11858 .await
11859 .unwrap();
11860 assert_eq!(
11861 src_delta.text.as_str(),
11862 "\
11863sketch001 = sketch(on = XY) {
11864 p0 = point(at = [var -2.23mm, var 3.1mm])
11865 pf = point(at = [4, 4])
11866 horizontal([p0, pf])
11867}
11868"
11869 );
11870 assert_eq!(
11871 scene_delta.new_graph.objects.len(),
11872 5,
11873 "{:#?}",
11874 scene_delta.new_graph.objects
11875 );
11876
11877 ctx.close().await;
11878 mock_ctx.close().await;
11879 }
11880
11881 #[tokio::test(flavor = "multi_thread")]
11882 async fn test_point_horizontal_with_origin() {
11883 let initial_source = "\
11884sketch001 = sketch(on = XY) {
11885 p0 = point(at = [var -2.23mm, var 3.1mm])
11886}
11887";
11888
11889 let program = Program::parse(initial_source).unwrap().0.unwrap();
11890
11891 let mut frontend = FrontendState::new();
11892
11893 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11894 let mock_ctx = ExecutorContext::new_mock(None).await;
11895 let version = Version(0);
11896
11897 frontend.hack_set_program(&ctx, program).await.unwrap();
11898 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11899 let sketch_id = sketch_object.id;
11900 let sketch = expect_sketch(sketch_object);
11901 let point_id = *sketch.segments.first().unwrap();
11902
11903 let constraint = Constraint::Horizontal(Horizontal::Points {
11904 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
11905 });
11906 let (src_delta, scene_delta) = frontend
11907 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11908 .await
11909 .unwrap();
11910 assert_eq!(
11911 src_delta.text.as_str(),
11912 "\
11913sketch001 = sketch(on = XY) {
11914 p0 = point(at = [var -2.23mm, var 3.1mm])
11915 horizontal([p0, ORIGIN])
11916}
11917"
11918 );
11919 assert_eq!(
11920 scene_delta.new_graph.objects.len(),
11921 4,
11922 "{:#?}",
11923 scene_delta.new_graph.objects
11924 );
11925
11926 ctx.close().await;
11927 mock_ctx.close().await;
11928 }
11929
11930 #[tokio::test(flavor = "multi_thread")]
11931 async fn test_lines_equal_length() {
11932 let initial_source = "\
11933sketch(on = XY) {
11934 line(start = [var 1, var 2], end = [var 3, var 4])
11935 line(start = [var 5, var 6], end = [var 7, var 8])
11936}
11937";
11938
11939 let program = Program::parse(initial_source).unwrap().0.unwrap();
11940
11941 let mut frontend = FrontendState::new();
11942
11943 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11944 let mock_ctx = ExecutorContext::new_mock(None).await;
11945 let version = Version(0);
11946
11947 frontend.hack_set_program(&ctx, program).await.unwrap();
11948 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11949 let sketch_id = sketch_object.id;
11950 let sketch = expect_sketch(sketch_object);
11951 let line1_id = *sketch.segments.get(2).unwrap();
11952 let line2_id = *sketch.segments.get(5).unwrap();
11953
11954 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
11955 lines: vec![line1_id, line2_id],
11956 });
11957 let (src_delta, scene_delta) = frontend
11958 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11959 .await
11960 .unwrap();
11961 assert_eq!(
11962 src_delta.text.as_str(),
11963 "\
11964sketch(on = XY) {
11965 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11966 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11967 equalLength([line1, line2])
11968}
11969"
11970 );
11971 assert_eq!(
11972 scene_delta.new_graph.objects.len(),
11973 9,
11974 "{:#?}",
11975 scene_delta.new_graph.objects
11976 );
11977
11978 ctx.close().await;
11979 mock_ctx.close().await;
11980 }
11981
11982 #[tokio::test(flavor = "multi_thread")]
11983 async fn test_add_constraint_multi_line_equal_length() {
11984 let initial_source = "\
11985sketch(on = XY) {
11986 line(start = [var 1, var 2], end = [var 3, var 4])
11987 line(start = [var 5, var 6], end = [var 7, var 8])
11988 line(start = [var 9, var 10], end = [var 11, var 12])
11989}
11990";
11991
11992 let program = Program::parse(initial_source).unwrap().0.unwrap();
11993
11994 let mut frontend = FrontendState::new();
11995 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11996 let mock_ctx = ExecutorContext::new_mock(None).await;
11997 let version = Version(0);
11998
11999 frontend.hack_set_program(&ctx, program).await.unwrap();
12000 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12001 let sketch_id = sketch_object.id;
12002 let sketch = expect_sketch(sketch_object);
12003 let line1_id = *sketch.segments.get(2).unwrap();
12004 let line2_id = *sketch.segments.get(5).unwrap();
12005 let line3_id = *sketch.segments.get(8).unwrap();
12006
12007 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12008 lines: vec![line1_id, line2_id, line3_id],
12009 });
12010 let (src_delta, scene_delta) = frontend
12011 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12012 .await
12013 .unwrap();
12014 assert_eq!(
12015 src_delta.text.as_str(),
12016 "\
12017sketch(on = XY) {
12018 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12019 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12020 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
12021 equalLength([line1, line2, line3])
12022}
12023"
12024 );
12025 let constraints = scene_delta
12026 .new_graph
12027 .objects
12028 .iter()
12029 .filter_map(|obj| {
12030 let ObjectKind::Constraint { constraint } = &obj.kind else {
12031 return None;
12032 };
12033 Some(constraint)
12034 })
12035 .collect::<Vec<_>>();
12036
12037 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
12038 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
12039 panic!("expected equal length constraint, got {:?}", constraints[0]);
12040 };
12041 assert_eq!(lines_equal_length.lines.len(), 3);
12042
12043 ctx.close().await;
12044 mock_ctx.close().await;
12045 }
12046
12047 #[tokio::test(flavor = "multi_thread")]
12048 async fn test_lines_parallel() {
12049 let initial_source = "\
12050sketch(on = XY) {
12051 line(start = [var 1, var 2], end = [var 3, var 4])
12052 line(start = [var 5, var 6], end = [var 7, var 8])
12053}
12054";
12055
12056 let program = Program::parse(initial_source).unwrap().0.unwrap();
12057
12058 let mut frontend = FrontendState::new();
12059
12060 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12061 let mock_ctx = ExecutorContext::new_mock(None).await;
12062 let version = Version(0);
12063
12064 frontend.hack_set_program(&ctx, program).await.unwrap();
12065 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12066 let sketch_id = sketch_object.id;
12067 let sketch = expect_sketch(sketch_object);
12068 let line1_id = *sketch.segments.get(2).unwrap();
12069 let line2_id = *sketch.segments.get(5).unwrap();
12070
12071 let constraint = Constraint::Parallel(Parallel {
12072 lines: vec![line1_id, line2_id],
12073 });
12074 let (src_delta, scene_delta) = frontend
12075 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12076 .await
12077 .unwrap();
12078 assert_eq!(
12079 src_delta.text.as_str(),
12080 "\
12081sketch(on = XY) {
12082 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12083 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12084 parallel([line1, line2])
12085}
12086"
12087 );
12088 assert_eq!(
12089 scene_delta.new_graph.objects.len(),
12090 9,
12091 "{:#?}",
12092 scene_delta.new_graph.objects
12093 );
12094
12095 ctx.close().await;
12096 mock_ctx.close().await;
12097 }
12098
12099 #[tokio::test(flavor = "multi_thread")]
12100 async fn test_lines_parallel_multiline() {
12101 let initial_source = "\
12102sketch(on = XY) {
12103 line(start = [var 1, var 2], end = [var 3, var 4])
12104 line(start = [var 5, var 6], end = [var 7, var 8])
12105 line(start = [var 9, var 10], end = [var 11, var 12])
12106}
12107";
12108
12109 let program = Program::parse(initial_source).unwrap().0.unwrap();
12110
12111 let mut frontend = FrontendState::new();
12112
12113 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12114 let mock_ctx = ExecutorContext::new_mock(None).await;
12115 let version = Version(0);
12116
12117 frontend.hack_set_program(&ctx, program).await.unwrap();
12118 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12119 let sketch_id = sketch_object.id;
12120 let sketch = expect_sketch(sketch_object);
12121 let line1_id = *sketch.segments.get(2).unwrap();
12122 let line2_id = *sketch.segments.get(5).unwrap();
12123 let line3_id = *sketch.segments.get(8).unwrap();
12124
12125 let constraint = Constraint::Parallel(Parallel {
12126 lines: vec![line1_id, line2_id, line3_id],
12127 });
12128 let (src_delta, scene_delta) = frontend
12129 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12130 .await
12131 .unwrap();
12132 assert_eq!(
12133 src_delta.text.as_str(),
12134 "\
12135sketch(on = XY) {
12136 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12137 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12138 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
12139 parallel([line1, line2, line3])
12140}
12141"
12142 );
12143
12144 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12145 let sketch = expect_sketch(sketch_object);
12146 assert_eq!(sketch.constraints.len(), 1);
12147
12148 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12149 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12150 panic!("Expected constraint object");
12151 };
12152 let Constraint::Parallel(parallel) = constraint else {
12153 panic!("Expected parallel constraint");
12154 };
12155 assert_eq!(parallel.lines.len(), 3);
12156
12157 ctx.close().await;
12158 mock_ctx.close().await;
12159 }
12160
12161 #[tokio::test(flavor = "multi_thread")]
12162 async fn test_lines_perpendicular() {
12163 let initial_source = "\
12164sketch(on = XY) {
12165 line(start = [var 1, var 2], end = [var 3, var 4])
12166 line(start = [var 5, var 6], end = [var 7, var 8])
12167}
12168";
12169
12170 let program = Program::parse(initial_source).unwrap().0.unwrap();
12171
12172 let mut frontend = FrontendState::new();
12173
12174 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12175 let mock_ctx = ExecutorContext::new_mock(None).await;
12176 let version = Version(0);
12177
12178 frontend.hack_set_program(&ctx, program).await.unwrap();
12179 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12180 let sketch_id = sketch_object.id;
12181 let sketch = expect_sketch(sketch_object);
12182 let line1_id = *sketch.segments.get(2).unwrap();
12183 let line2_id = *sketch.segments.get(5).unwrap();
12184
12185 let constraint = Constraint::Perpendicular(Perpendicular {
12186 lines: vec![line1_id, line2_id],
12187 });
12188 let (src_delta, scene_delta) = frontend
12189 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12190 .await
12191 .unwrap();
12192 assert_eq!(
12193 src_delta.text.as_str(),
12194 "\
12195sketch(on = XY) {
12196 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12197 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12198 perpendicular([line1, line2])
12199}
12200"
12201 );
12202 assert_eq!(
12203 scene_delta.new_graph.objects.len(),
12204 9,
12205 "{:#?}",
12206 scene_delta.new_graph.objects
12207 );
12208
12209 ctx.close().await;
12210 mock_ctx.close().await;
12211 }
12212
12213 #[tokio::test(flavor = "multi_thread")]
12214 async fn test_lines_angle() {
12215 let initial_source = "\
12216sketch(on = XY) {
12217 line(start = [var 1, var 2], end = [var 3, var 4])
12218 line(start = [var 5, var 6], end = [var 7, var 8])
12219}
12220";
12221
12222 let program = Program::parse(initial_source).unwrap().0.unwrap();
12223
12224 let mut frontend = FrontendState::new();
12225
12226 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12227 let mock_ctx = ExecutorContext::new_mock(None).await;
12228 let version = Version(0);
12229
12230 frontend.hack_set_program(&ctx, program).await.unwrap();
12231 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12232 let sketch_id = sketch_object.id;
12233 let sketch = expect_sketch(sketch_object);
12234 let line1_id = *sketch.segments.get(2).unwrap();
12235 let line2_id = *sketch.segments.get(5).unwrap();
12236
12237 let constraint = Constraint::Angle(Angle {
12238 lines: vec![line1_id, line2_id],
12239 angle: Number {
12240 value: 30.0,
12241 units: NumericSuffix::Deg,
12242 },
12243 source: Default::default(),
12244 });
12245 let (src_delta, scene_delta) = frontend
12246 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12247 .await
12248 .unwrap();
12249 assert_eq!(
12250 src_delta.text.as_str(),
12251 "\
12253sketch(on = XY) {
12254 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12255 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12256 angle([line1, line2]) == 30deg
12257}
12258"
12259 );
12260 assert_eq!(
12261 scene_delta.new_graph.objects.len(),
12262 9,
12263 "{:#?}",
12264 scene_delta.new_graph.objects
12265 );
12266
12267 ctx.close().await;
12268 mock_ctx.close().await;
12269 }
12270
12271 #[tokio::test(flavor = "multi_thread")]
12272 async fn test_segments_tangent() {
12273 let initial_source = "\
12274sketch(on = XY) {
12275 line(start = [var 1, var 2], end = [var 3, var 4])
12276 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
12277}
12278";
12279
12280 let program = Program::parse(initial_source).unwrap().0.unwrap();
12281
12282 let mut frontend = FrontendState::new();
12283
12284 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12285 let mock_ctx = ExecutorContext::new_mock(None).await;
12286 let version = Version(0);
12287
12288 frontend.hack_set_program(&ctx, program).await.unwrap();
12289 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12290 let sketch_id = sketch_object.id;
12291 let sketch = expect_sketch(sketch_object);
12292 let line1_id = *sketch.segments.get(2).unwrap();
12293 let arc1_id = *sketch.segments.get(6).unwrap();
12294
12295 let constraint = Constraint::Tangent(Tangent {
12296 input: vec![line1_id, arc1_id],
12297 });
12298 let (src_delta, scene_delta) = frontend
12299 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12300 .await
12301 .unwrap();
12302 assert_eq!(
12303 src_delta.text.as_str(),
12304 "\
12305sketch(on = XY) {
12306 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12307 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
12308 tangent([line1, arc1])
12309}
12310"
12311 );
12312 assert_eq!(
12313 scene_delta.new_graph.objects.len(),
12314 10,
12315 "{:#?}",
12316 scene_delta.new_graph.objects
12317 );
12318
12319 ctx.close().await;
12320 mock_ctx.close().await;
12321 }
12322
12323 #[tokio::test(flavor = "multi_thread")]
12324 async fn test_point_midpoint() {
12325 let initial_source = "\
12326sketch(on = XY) {
12327 point(at = [var 1, var 1])
12328 line(start = [var 0, var 0], end = [var 6, var 4])
12329}
12330";
12331
12332 let program = Program::parse(initial_source).unwrap().0.unwrap();
12333
12334 let mut frontend = FrontendState::new();
12335
12336 let ctx = ExecutorContext::new_mock(None).await;
12337 let version = Version(0);
12338
12339 frontend.program = program.clone();
12340 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12341 frontend.update_state_after_exec(outcome, true);
12342 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12343 let sketch_id = sketch_object.id;
12344 let sketch = expect_sketch(sketch_object);
12345 let point_id = *sketch.segments.first().unwrap();
12346 let line_id = *sketch.segments.get(3).unwrap();
12347
12348 let constraint = Constraint::Midpoint(Midpoint {
12349 point: point_id,
12350 segment: line_id,
12351 });
12352 let (src_delta, scene_delta) = frontend
12353 .add_constraint(&ctx, version, sketch_id, constraint)
12354 .await
12355 .unwrap();
12356 assert_eq!(
12357 src_delta.text.as_str(),
12358 "\
12359sketch(on = XY) {
12360 point1 = point(at = [var 1, var 1])
12361 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
12362 midpoint(line1, point = point1)
12363}
12364"
12365 );
12366 assert_eq!(
12367 scene_delta.new_graph.objects.len(),
12368 7,
12369 "{:#?}",
12370 scene_delta.new_graph.objects
12371 );
12372
12373 ctx.close().await;
12374 }
12375
12376 #[tokio::test(flavor = "multi_thread")]
12377 async fn test_segments_symmetric() {
12378 let initial_source = "\
12379sketch(on = XY) {
12380 line(start = [var 0, var 0], end = [var 0, var 4])
12381 line(start = [var 4, var 0], end = [var 4, var 4])
12382 line(start = [var 2, var -1], end = [var 2, var 5])
12383}
12384";
12385
12386 let program = Program::parse(initial_source).unwrap().0.unwrap();
12387
12388 let mut frontend = FrontendState::new();
12389
12390 let ctx = ExecutorContext::new_mock(None).await;
12391 let version = Version(0);
12392
12393 frontend.program = program.clone();
12394 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12395 frontend.update_state_after_exec(outcome, true);
12396 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12397 let sketch_id = sketch_object.id;
12398 let sketch = expect_sketch(sketch_object);
12399 let line1_id = *sketch.segments.get(2).unwrap();
12400 let line2_id = *sketch.segments.get(5).unwrap();
12401 let axis_id = *sketch.segments.get(8).unwrap();
12402
12403 let constraint = Constraint::Symmetric(Symmetric {
12404 input: vec![line1_id, line2_id],
12405 axis: axis_id,
12406 });
12407 let (src_delta, scene_delta) = frontend
12408 .add_constraint(&ctx, version, sketch_id, constraint)
12409 .await
12410 .unwrap();
12411 assert_eq!(
12412 src_delta.text.as_str(),
12413 "\
12414sketch(on = XY) {
12415 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
12416 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
12417 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
12418 symmetric([line1, line2], axis = line3)
12419}
12420"
12421 );
12422 assert_eq!(
12423 scene_delta.new_graph.objects.len(),
12424 12,
12425 "{:#?}",
12426 scene_delta.new_graph.objects
12427 );
12428
12429 ctx.close().await;
12430 }
12431
12432 #[tokio::test(flavor = "multi_thread")]
12433 async fn test_point_arc_midpoint() {
12434 let initial_source = "\
12435sketch(on = XY) {
12436 point(at = [var 6, var 3])
12437 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
12438}
12439";
12440
12441 let program = Program::parse(initial_source).unwrap().0.unwrap();
12442
12443 let mut frontend = FrontendState::new();
12444
12445 let ctx = ExecutorContext::new_mock(None).await;
12446 let version = Version(0);
12447
12448 frontend.program = program.clone();
12449 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12450 frontend.update_state_after_exec(outcome, true);
12451 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12452 let sketch_id = sketch_object.id;
12453 let sketch = expect_sketch(sketch_object);
12454 let point_id = *sketch.segments.first().unwrap();
12455 let arc_id = *sketch.segments.get(4).unwrap();
12456
12457 let constraint = Constraint::Midpoint(Midpoint {
12458 point: point_id,
12459 segment: arc_id,
12460 });
12461 let (src_delta, scene_delta) = frontend
12462 .add_constraint(&ctx, version, sketch_id, constraint)
12463 .await
12464 .unwrap();
12465 assert_eq!(
12466 src_delta.text.as_str(),
12467 "\
12468sketch(on = XY) {
12469 point1 = point(at = [var 6, var 3])
12470 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
12471 midpoint(arc1, point = point1)
12472}
12473"
12474 );
12475 assert_eq!(
12476 scene_delta.new_graph.objects.len(),
12477 8,
12478 "{:#?}",
12479 scene_delta.new_graph.objects
12480 );
12481
12482 ctx.close().await;
12483 }
12484
12485 #[tokio::test(flavor = "multi_thread")]
12486 async fn test_segments_symmetric_arcs() {
12487 let initial_source = "\
12488sketch(on = XY) {
12489 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
12490 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
12491 line(start = [var 0, var -10], end = [var 0, var 10])
12492}
12493";
12494
12495 let program = Program::parse(initial_source).unwrap().0.unwrap();
12496
12497 let mut frontend = FrontendState::new();
12498
12499 let ctx = ExecutorContext::new_mock(None).await;
12500 let version = Version(0);
12501
12502 frontend.program = program.clone();
12503 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12504 frontend.update_state_after_exec(outcome, true);
12505 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12506 let sketch_id = sketch_object.id;
12507 let sketch = expect_sketch(sketch_object);
12508 let arc1_id = *sketch.segments.get(3).unwrap();
12509 let arc2_id = *sketch.segments.get(7).unwrap();
12510 let axis_id = *sketch.segments.get(10).unwrap();
12511
12512 let constraint = Constraint::Symmetric(Symmetric {
12513 input: vec![arc1_id, arc2_id],
12514 axis: axis_id,
12515 });
12516 let (src_delta, scene_delta) = frontend
12517 .add_constraint(&ctx, version, sketch_id, constraint)
12518 .await
12519 .unwrap();
12520 assert_eq!(
12521 src_delta.text.as_str(),
12522 "\
12523sketch(on = XY) {
12524 arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
12525 arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
12526 line1 = line(start = [var 0, var -10], end = [var 0, var 10])
12527 symmetric([arc1, arc2], axis = line1)
12528}
12529"
12530 );
12531 assert_eq!(
12532 scene_delta.new_graph.objects.len(),
12533 14,
12534 "{:#?}",
12535 scene_delta.new_graph.objects
12536 );
12537
12538 ctx.close().await;
12539 }
12540
12541 #[tokio::test(flavor = "multi_thread")]
12542 async fn test_sketch_on_face_simple() {
12543 let initial_source = "\
12544len = 2mm
12545cube = startSketchOn(XY)
12546 |> startProfile(at = [0, 0])
12547 |> line(end = [len, 0], tag = $side)
12548 |> line(end = [0, len])
12549 |> line(end = [-len, 0])
12550 |> line(end = [0, -len])
12551 |> close()
12552 |> extrude(length = len)
12553
12554face = faceOf(cube, face = side)
12555";
12556
12557 let program = Program::parse(initial_source).unwrap().0.unwrap();
12558
12559 let mut frontend = FrontendState::new();
12560
12561 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12562 let mock_ctx = ExecutorContext::new_mock(None).await;
12563 let version = Version(0);
12564
12565 frontend.hack_set_program(&ctx, program).await.unwrap();
12566 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
12567 let face_id = face_object.id;
12568
12569 let sketch_args = SketchCtor {
12570 on: Plane::Object(face_id),
12571 };
12572 let (_src_delta, scene_delta, sketch_id) = frontend
12573 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12574 .await
12575 .unwrap();
12576 assert_eq!(sketch_id, ObjectId(2));
12577 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
12578 let sketch_object = &scene_delta.new_graph.objects[2];
12579 assert_eq!(sketch_object.id, ObjectId(2));
12580 assert_eq!(
12581 sketch_object.kind,
12582 ObjectKind::Sketch(Sketch {
12583 args: SketchCtor {
12584 on: Plane::Object(face_id),
12585 },
12586 plane: face_id,
12587 segments: vec![],
12588 constraints: vec![],
12589 })
12590 );
12591 assert_eq!(scene_delta.new_graph.objects.len(), 8);
12592
12593 ctx.close().await;
12594 mock_ctx.close().await;
12595 }
12596
12597 #[tokio::test(flavor = "multi_thread")]
12598 async fn test_sketch_on_wall_artifact_from_region_extrude() {
12599 let initial_source = "\
12600s = sketch(on = YZ) {
12601 line1 = line(start = [0, 0], end = [0, 1])
12602 line2 = line(start = [0, 1], end = [1, 1])
12603 line3 = line(start = [1, 1], end = [0, 0])
12604}
12605region001 = region(point = [0.1, 0.1], sketch = s)
12606extrude001 = extrude(region001, length = 5)
12607";
12608
12609 let program = Program::parse(initial_source).unwrap().0.unwrap();
12610
12611 let mut frontend = FrontendState::new();
12612 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12613 let version = Version(0);
12614
12615 frontend.hack_set_program(&ctx, program).await.unwrap();
12616 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
12617
12618 let sketch_args = SketchCtor {
12619 on: Plane::Object(wall_object_id),
12620 };
12621 let (src_delta, _scene_delta, _sketch_id) = frontend
12622 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12623 .await
12624 .unwrap();
12625 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
12626
12627 ctx.close().await;
12628 }
12629
12630 #[tokio::test(flavor = "multi_thread")]
12631 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
12632 let initial_source = "\
12633sketch001 = sketch(on = YZ) {
12634 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
12635 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
12636 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
12637 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
12638 coincident([line1.end, line2.start])
12639 coincident([line2.end, line3.start])
12640 coincident([line3.end, line4.start])
12641 coincident([line4.end, line1.start])
12642 parallel([line2, line4])
12643 parallel([line3, line1])
12644 perpendicular([line1, line2])
12645 horizontal(line3)
12646 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
12647}
12648region001 = region(point = [3.1, 3.74], sketch = sketch001)
12649extrude001 = extrude(region001, length = 5)
12650";
12651
12652 let program = Program::parse(initial_source).unwrap().0.unwrap();
12653
12654 let mut frontend = FrontendState::new();
12655 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12656 let version = Version(0);
12657
12658 frontend.hack_set_program(&ctx, program).await.unwrap();
12659 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
12660
12661 let sketch_args = SketchCtor {
12662 on: Plane::Object(wall_object_id),
12663 };
12664 let (src_delta, _scene_delta, _sketch_id) = frontend
12665 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12666 .await
12667 .unwrap();
12668 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
12669
12670 ctx.close().await;
12671 }
12672
12673 #[tokio::test(flavor = "multi_thread")]
12674 async fn test_sketch_on_plane_incremental() {
12675 let initial_source = "\
12676len = 2mm
12677cube = startSketchOn(XY)
12678 |> startProfile(at = [0, 0])
12679 |> line(end = [len, 0], tag = $side)
12680 |> line(end = [0, len])
12681 |> line(end = [-len, 0])
12682 |> line(end = [0, -len])
12683 |> close()
12684 |> extrude(length = len)
12685
12686plane = planeOf(cube, face = side)
12687";
12688
12689 let program = Program::parse(initial_source).unwrap().0.unwrap();
12690
12691 let mut frontend = FrontendState::new();
12692
12693 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12694 let mock_ctx = ExecutorContext::new_mock(None).await;
12695 let version = Version(0);
12696
12697 frontend.hack_set_program(&ctx, program).await.unwrap();
12698 let plane_object = frontend
12700 .scene_graph
12701 .objects
12702 .iter()
12703 .rev()
12704 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
12705 .unwrap();
12706 let plane_id = plane_object.id;
12707
12708 let sketch_args = SketchCtor {
12709 on: Plane::Object(plane_id),
12710 };
12711 let (src_delta, scene_delta, sketch_id) = frontend
12712 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12713 .await
12714 .unwrap();
12715 assert_eq!(
12716 src_delta.text.as_str(),
12717 "\
12718len = 2mm
12719cube = startSketchOn(XY)
12720 |> startProfile(at = [0, 0])
12721 |> line(end = [len, 0], tag = $side)
12722 |> line(end = [0, len])
12723 |> line(end = [-len, 0])
12724 |> line(end = [0, -len])
12725 |> close()
12726 |> extrude(length = len)
12727
12728plane = planeOf(cube, face = side)
12729sketch001 = sketch(on = plane) {
12730}
12731"
12732 );
12733 assert_eq!(sketch_id, ObjectId(2));
12734 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
12735 let sketch_object = &scene_delta.new_graph.objects[2];
12736 assert_eq!(sketch_object.id, ObjectId(2));
12737 assert_eq!(
12738 sketch_object.kind,
12739 ObjectKind::Sketch(Sketch {
12740 args: SketchCtor {
12741 on: Plane::Object(plane_id),
12742 },
12743 plane: plane_id,
12744 segments: vec![],
12745 constraints: vec![],
12746 })
12747 );
12748 assert_eq!(scene_delta.new_graph.objects.len(), 9);
12749
12750 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
12751 assert_eq!(plane_object.id, plane_id);
12752 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
12753
12754 ctx.close().await;
12755 mock_ctx.close().await;
12756 }
12757
12758 #[tokio::test(flavor = "multi_thread")]
12759 async fn test_new_sketch_uses_unique_variable_name() {
12760 let initial_source = "\
12761sketch1 = sketch(on = XY) {
12762}
12763";
12764
12765 let program = Program::parse(initial_source).unwrap().0.unwrap();
12766
12767 let mut frontend = FrontendState::new();
12768 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12769 let version = Version(0);
12770
12771 frontend.hack_set_program(&ctx, program).await.unwrap();
12772
12773 let sketch_args = SketchCtor {
12774 on: Plane::Default(PlaneName::Yz),
12775 };
12776 let (src_delta, _, _) = frontend
12777 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12778 .await
12779 .unwrap();
12780
12781 assert_eq!(
12782 src_delta.text.as_str(),
12783 "\
12784sketch1 = sketch(on = XY) {
12785}
12786sketch001 = sketch(on = YZ) {
12787}
12788"
12789 );
12790
12791 ctx.close().await;
12792 }
12793
12794 #[tokio::test(flavor = "multi_thread")]
12795 async fn test_new_sketch_twice_using_same_plane() {
12796 let initial_source = "\
12797sketch1 = sketch(on = XY) {
12798}
12799";
12800
12801 let program = Program::parse(initial_source).unwrap().0.unwrap();
12802
12803 let mut frontend = FrontendState::new();
12804 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12805 let version = Version(0);
12806
12807 frontend.hack_set_program(&ctx, program).await.unwrap();
12808
12809 let sketch_args = SketchCtor {
12810 on: Plane::Default(PlaneName::Xy),
12811 };
12812 let (src_delta, _, _) = frontend
12813 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12814 .await
12815 .unwrap();
12816
12817 assert_eq!(
12818 src_delta.text.as_str(),
12819 "\
12820sketch1 = sketch(on = XY) {
12821}
12822sketch001 = sketch(on = XY) {
12823}
12824"
12825 );
12826
12827 ctx.close().await;
12828 }
12829
12830 #[tokio::test(flavor = "multi_thread")]
12831 async fn test_sketch_mode_reuses_cached_on_expression() {
12832 let initial_source = "\
12833width = 2mm
12834sketch(on = offsetPlane(XY, offset = width)) {
12835 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
12836 distance([line1.start, line1.end]) == width
12837}
12838";
12839 let program = Program::parse(initial_source).unwrap().0.unwrap();
12840
12841 let mut frontend = FrontendState::new();
12842 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12843 let mock_ctx = ExecutorContext::new_mock(None).await;
12844 let version = Version(0);
12845 let project_id = ProjectId(0);
12846 let file_id = FileId(0);
12847
12848 frontend.hack_set_program(&ctx, program).await.unwrap();
12849 let initial_object_count = frontend.scene_graph.objects.len();
12850 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
12851 .expect("Expected sketch object to exist")
12852 .id;
12853
12854 let scene_delta = frontend
12857 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12858 .await
12859 .unwrap();
12860 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
12861
12862 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
12865 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
12866
12867 ctx.close().await;
12868 mock_ctx.close().await;
12869 }
12870
12871 #[tokio::test(flavor = "multi_thread")]
12872 async fn test_multiple_sketch_blocks() {
12873 let initial_source = "\
12874// Cube that requires the engine.
12875width = 2
12876sketch001 = startSketchOn(XY)
12877profile001 = startProfile(sketch001, at = [0, 0])
12878 |> yLine(length = width, tag = $seg1)
12879 |> xLine(length = width)
12880 |> yLine(length = -width)
12881 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12882 |> close()
12883extrude001 = extrude(profile001, length = width)
12884
12885// Get a value that requires the engine.
12886x = segLen(seg1)
12887
12888// Triangle with side length 2*x.
12889sketch(on = XY) {
12890 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12891 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12892 coincident([line1.end, line2.start])
12893 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12894 coincident([line2.end, line3.start])
12895 coincident([line3.end, line1.start])
12896 equalLength([line3, line1])
12897 equalLength([line1, line2])
12898 distance([line1.start, line1.end]) == 2*x
12899}
12900
12901// Line segment with length x.
12902sketch2 = sketch(on = XY) {
12903 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12904 distance([line1.start, line1.end]) == x
12905}
12906";
12907
12908 let program = Program::parse(initial_source).unwrap().0.unwrap();
12909
12910 let mut frontend = FrontendState::new();
12911
12912 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12913 let mock_ctx = ExecutorContext::new_mock(None).await;
12914 let version = Version(0);
12915 let project_id = ProjectId(0);
12916 let file_id = FileId(0);
12917
12918 frontend.hack_set_program(&ctx, program).await.unwrap();
12919 let sketch_objects = frontend
12920 .scene_graph
12921 .objects
12922 .iter()
12923 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
12924 .collect::<Vec<_>>();
12925 let sketch1_id = sketch_objects.first().unwrap().id;
12926 let sketch2_id = sketch_objects.get(1).unwrap().id;
12927 let point1_id = ObjectId(sketch1_id.0 + 1);
12929 let point2_id = ObjectId(sketch2_id.0 + 1);
12931
12932 let scene_delta = frontend
12941 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
12942 .await
12943 .unwrap();
12944 assert_eq!(
12945 scene_delta.new_graph.objects.len(),
12946 18,
12947 "{:#?}",
12948 scene_delta.new_graph.objects
12949 );
12950
12951 let point_ctor = PointCtor {
12953 position: Point2d {
12954 x: Expr::Var(Number {
12955 value: 1.0,
12956 units: NumericSuffix::Mm,
12957 }),
12958 y: Expr::Var(Number {
12959 value: 2.0,
12960 units: NumericSuffix::Mm,
12961 }),
12962 },
12963 };
12964 let segments = vec![ExistingSegmentCtor {
12965 id: point1_id,
12966 ctor: SegmentCtor::Point(point_ctor),
12967 }];
12968 let (src_delta, _) = frontend
12969 .edit_segments(&mock_ctx, version, sketch1_id, segments)
12970 .await
12971 .unwrap();
12972 assert_eq!(
12974 src_delta.text.as_str(),
12975 "\
12976// Cube that requires the engine.
12977width = 2
12978sketch001 = startSketchOn(XY)
12979profile001 = startProfile(sketch001, at = [0, 0])
12980 |> yLine(length = width, tag = $seg1)
12981 |> xLine(length = width)
12982 |> yLine(length = -width)
12983 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12984 |> close()
12985extrude001 = extrude(profile001, length = width)
12986
12987// Get a value that requires the engine.
12988x = segLen(seg1)
12989
12990// Triangle with side length 2*x.
12991sketch(on = XY) {
12992 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
12993 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
12994 coincident([line1.end, line2.start])
12995 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
12996 coincident([line2.end, line3.start])
12997 coincident([line3.end, line1.start])
12998 equalLength([line3, line1])
12999 equalLength([line1, line2])
13000 distance([line1.start, line1.end]) == 2 * x
13001}
13002
13003// Line segment with length x.
13004sketch2 = sketch(on = XY) {
13005 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13006 distance([line1.start, line1.end]) == x
13007}
13008"
13009 );
13010
13011 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
13013 assert_eq!(
13015 src_delta.text.as_str(),
13016 "\
13017// Cube that requires the engine.
13018width = 2
13019sketch001 = startSketchOn(XY)
13020profile001 = startProfile(sketch001, at = [0, 0])
13021 |> yLine(length = width, tag = $seg1)
13022 |> xLine(length = width)
13023 |> yLine(length = -width)
13024 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13025 |> close()
13026extrude001 = extrude(profile001, length = width)
13027
13028// Get a value that requires the engine.
13029x = segLen(seg1)
13030
13031// Triangle with side length 2*x.
13032sketch(on = XY) {
13033 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
13034 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13035 coincident([line1.end, line2.start])
13036 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13037 coincident([line2.end, line3.start])
13038 coincident([line3.end, line1.start])
13039 equalLength([line3, line1])
13040 equalLength([line1, line2])
13041 distance([line1.start, line1.end]) == 2 * x
13042}
13043
13044// Line segment with length x.
13045sketch2 = sketch(on = XY) {
13046 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13047 distance([line1.start, line1.end]) == x
13048}
13049"
13050 );
13051 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
13059 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
13060
13061 let scene_delta = frontend
13069 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
13070 .await
13071 .unwrap();
13072 assert_eq!(
13073 scene_delta.new_graph.objects.len(),
13074 24,
13075 "{:#?}",
13076 scene_delta.new_graph.objects
13077 );
13078
13079 let point_ctor = PointCtor {
13081 position: Point2d {
13082 x: Expr::Var(Number {
13083 value: 3.0,
13084 units: NumericSuffix::Mm,
13085 }),
13086 y: Expr::Var(Number {
13087 value: 4.0,
13088 units: NumericSuffix::Mm,
13089 }),
13090 },
13091 };
13092 let segments = vec![ExistingSegmentCtor {
13093 id: point2_id,
13094 ctor: SegmentCtor::Point(point_ctor),
13095 }];
13096 let (src_delta, _) = frontend
13097 .edit_segments(&mock_ctx, version, sketch2_id, segments)
13098 .await
13099 .unwrap();
13100 assert_eq!(
13102 src_delta.text.as_str(),
13103 "\
13104// Cube that requires the engine.
13105width = 2
13106sketch001 = startSketchOn(XY)
13107profile001 = startProfile(sketch001, at = [0, 0])
13108 |> yLine(length = width, tag = $seg1)
13109 |> xLine(length = width)
13110 |> yLine(length = -width)
13111 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13112 |> close()
13113extrude001 = extrude(profile001, length = width)
13114
13115// Get a value that requires the engine.
13116x = segLen(seg1)
13117
13118// Triangle with side length 2*x.
13119sketch(on = XY) {
13120 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
13121 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13122 coincident([line1.end, line2.start])
13123 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13124 coincident([line2.end, line3.start])
13125 coincident([line3.end, line1.start])
13126 equalLength([line3, line1])
13127 equalLength([line1, line2])
13128 distance([line1.start, line1.end]) == 2 * x
13129}
13130
13131// Line segment with length x.
13132sketch2 = sketch(on = XY) {
13133 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
13134 distance([line1.start, line1.end]) == x
13135}
13136"
13137 );
13138
13139 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
13141 assert_eq!(
13143 src_delta.text.as_str(),
13144 "\
13145// Cube that requires the engine.
13146width = 2
13147sketch001 = startSketchOn(XY)
13148profile001 = startProfile(sketch001, at = [0, 0])
13149 |> yLine(length = width, tag = $seg1)
13150 |> xLine(length = width)
13151 |> yLine(length = -width)
13152 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13153 |> close()
13154extrude001 = extrude(profile001, length = width)
13155
13156// Get a value that requires the engine.
13157x = segLen(seg1)
13158
13159// Triangle with side length 2*x.
13160sketch(on = XY) {
13161 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
13162 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13163 coincident([line1.end, line2.start])
13164 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13165 coincident([line2.end, line3.start])
13166 coincident([line3.end, line1.start])
13167 equalLength([line3, line1])
13168 equalLength([line1, line2])
13169 distance([line1.start, line1.end]) == 2 * x
13170}
13171
13172// Line segment with length x.
13173sketch2 = sketch(on = XY) {
13174 line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
13175 distance([line1.start, line1.end]) == x
13176}
13177"
13178 );
13179
13180 ctx.close().await;
13181 mock_ctx.close().await;
13182 }
13183
13184 #[tokio::test(flavor = "multi_thread")]
13185 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
13186 clear_mem_cache().await;
13187
13188 let source = r#"sketch001 = sketch(on = XZ) {
13189 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
13190}
13191sketch002 = sketch(on = XY) {
13192 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
13193 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
13194 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
13195 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
13196 coincident([line1.end, line2.start])
13197 coincident([line2.end, line3.start])
13198 coincident([line3.end, line4.start])
13199 coincident([line4.end, line1.start])
13200 parallel([line2, line4])
13201 parallel([line3, line1])
13202 perpendicular([line1, line2])
13203 horizontal(line3)
13204 coincident([line1.start, ORIGIN])
13205}
13206"#;
13207
13208 let program = Program::parse(source).unwrap().0.unwrap();
13209 let mut frontend = FrontendState::new();
13210 let ctx = ExecutorContext::new_with_engine(
13211 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
13212 Default::default(),
13213 );
13214 let mock_ctx = ExecutorContext::new_mock(None).await;
13215 let version = Version(0);
13216 let project_id = ProjectId(0);
13217 let file_id = FileId(0);
13218
13219 frontend.hack_set_program(&ctx, program).await.unwrap();
13220 let sketch_objects = frontend
13221 .scene_graph
13222 .objects
13223 .iter()
13224 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
13225 .collect::<Vec<_>>();
13226 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
13227
13228 let sketch1_id = sketch_objects[0].id;
13229 let sketch2_id = sketch_objects[1].id;
13230
13231 frontend
13232 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
13233 .await
13234 .unwrap();
13235 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
13236
13237 let scene_delta = frontend
13238 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
13239 .await
13240 .unwrap();
13241 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
13242
13243 clear_mem_cache().await;
13244 ctx.close().await;
13245 mock_ctx.close().await;
13246 }
13247
13248 #[tokio::test(flavor = "multi_thread")]
13253 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
13254 let initial_source = "@settings(defaultLengthUnit = mm)
13256
13257
13258
13259sketch001 = sketch(on = XY) {
13260 point(at = [1in, 2in])
13261}
13262";
13263
13264 let program = Program::parse(initial_source).unwrap().0.unwrap();
13265 let mut frontend = FrontendState::new();
13266
13267 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13268 let mock_ctx = ExecutorContext::new_mock(None).await;
13269 let version = Version(0);
13270 let project_id = ProjectId(0);
13271 let file_id = FileId(0);
13272
13273 frontend.hack_set_program(&ctx, program).await.unwrap();
13274 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13275 let sketch_id = sketch_object.id;
13276
13277 frontend
13279 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13280 .await
13281 .unwrap();
13282
13283 let point_ctor = PointCtor {
13285 position: Point2d {
13286 x: Expr::Number(Number {
13287 value: 5.0,
13288 units: NumericSuffix::Mm,
13289 }),
13290 y: Expr::Number(Number {
13291 value: 6.0,
13292 units: NumericSuffix::Mm,
13293 }),
13294 },
13295 };
13296 let segment = SegmentCtor::Point(point_ctor);
13297 let (src_delta, scene_delta) = frontend
13298 .add_segment(&mock_ctx, version, sketch_id, segment, None)
13299 .await
13300 .unwrap();
13301 assert!(
13303 src_delta.text.contains("point(at = [5mm, 6mm])"),
13304 "Expected new point in source, got: {}",
13305 src_delta.text
13306 );
13307 assert!(!scene_delta.new_objects.is_empty());
13308
13309 ctx.close().await;
13310 mock_ctx.close().await;
13311 }
13312
13313 #[tokio::test(flavor = "multi_thread")]
13314 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
13315 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
13316
13317 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
13318 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
13319
13320 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
13321 assert!(
13322 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
13323 "Expected experimental settings to be added to source"
13324 );
13325 }
13326
13327 #[tokio::test(flavor = "multi_thread")]
13328 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
13329 let initial_source = "@settings(defaultLengthUnit = mm)
13331
13332
13333
13334s = sketch(on = XY) {}
13335";
13336
13337 let program = Program::parse(initial_source).unwrap().0.unwrap();
13338 let mut frontend = FrontendState::new();
13339
13340 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13341 let mock_ctx = ExecutorContext::new_mock(None).await;
13342 let version = Version(0);
13343
13344 frontend.hack_set_program(&ctx, program).await.unwrap();
13345 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13346 let sketch_id = sketch_object.id;
13347
13348 let line_ctor = LineCtor {
13349 start: Point2d {
13350 x: Expr::Number(Number {
13351 value: 0.0,
13352 units: NumericSuffix::Mm,
13353 }),
13354 y: Expr::Number(Number {
13355 value: 0.0,
13356 units: NumericSuffix::Mm,
13357 }),
13358 },
13359 end: Point2d {
13360 x: Expr::Number(Number {
13361 value: 10.0,
13362 units: NumericSuffix::Mm,
13363 }),
13364 y: Expr::Number(Number {
13365 value: 10.0,
13366 units: NumericSuffix::Mm,
13367 }),
13368 },
13369 construction: None,
13370 };
13371 let segment = SegmentCtor::Line(line_ctor);
13372 let (src_delta, scene_delta) = frontend
13373 .add_segment(&mock_ctx, version, sketch_id, segment, None)
13374 .await
13375 .unwrap();
13376 assert!(
13377 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
13378 "Expected line in source, got: {}",
13379 src_delta.text
13380 );
13381 assert_eq!(scene_delta.new_objects.len(), 3);
13383
13384 ctx.close().await;
13385 mock_ctx.close().await;
13386 }
13387
13388 #[tokio::test(flavor = "multi_thread")]
13389 async fn test_extra_newlines_between_operations_edit_line() {
13390 let initial_source = "@settings(defaultLengthUnit = mm)
13392
13393
13394sketch001 = sketch(on = XY) {
13395
13396 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
13397
13398}
13399";
13400
13401 let program = Program::parse(initial_source).unwrap().0.unwrap();
13402 let mut frontend = FrontendState::new();
13403
13404 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13405 let mock_ctx = ExecutorContext::new_mock(None).await;
13406 let version = Version(0);
13407 let project_id = ProjectId(0);
13408 let file_id = FileId(0);
13409
13410 frontend.hack_set_program(&ctx, program).await.unwrap();
13411 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13412 let sketch_id = sketch_object.id;
13413 let sketch = expect_sketch(sketch_object);
13414
13415 let line_id = sketch
13417 .segments
13418 .iter()
13419 .copied()
13420 .find(|seg_id| {
13421 matches!(
13422 &frontend.scene_graph.objects[seg_id.0].kind,
13423 ObjectKind::Segment {
13424 segment: Segment::Line(_)
13425 }
13426 )
13427 })
13428 .expect("Expected a line segment in sketch");
13429
13430 frontend
13432 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13433 .await
13434 .unwrap();
13435
13436 let line_ctor = LineCtor {
13438 start: Point2d {
13439 x: Expr::Var(Number {
13440 value: 1.0,
13441 units: NumericSuffix::Mm,
13442 }),
13443 y: Expr::Var(Number {
13444 value: 2.0,
13445 units: NumericSuffix::Mm,
13446 }),
13447 },
13448 end: Point2d {
13449 x: Expr::Var(Number {
13450 value: 13.0,
13451 units: NumericSuffix::Mm,
13452 }),
13453 y: Expr::Var(Number {
13454 value: 14.0,
13455 units: NumericSuffix::Mm,
13456 }),
13457 },
13458 construction: None,
13459 };
13460 let segments = vec![ExistingSegmentCtor {
13461 id: line_id,
13462 ctor: SegmentCtor::Line(line_ctor),
13463 }];
13464 let (src_delta, _scene_delta) = frontend
13465 .edit_segments(&mock_ctx, version, sketch_id, segments)
13466 .await
13467 .unwrap();
13468 assert!(
13469 src_delta
13470 .text
13471 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
13472 "Expected edited line in source, got: {}",
13473 src_delta.text
13474 );
13475
13476 ctx.close().await;
13477 mock_ctx.close().await;
13478 }
13479
13480 #[tokio::test(flavor = "multi_thread")]
13481 async fn test_extra_newlines_delete_segment() {
13482 let initial_source = "@settings(defaultLengthUnit = mm)
13484
13485
13486
13487sketch001 = sketch(on = XY) {
13488 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
13489}
13490";
13491
13492 let program = Program::parse(initial_source).unwrap().0.unwrap();
13493 let mut frontend = FrontendState::new();
13494
13495 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13496 let mock_ctx = ExecutorContext::new_mock(None).await;
13497 let version = Version(0);
13498
13499 frontend.hack_set_program(&ctx, program).await.unwrap();
13500 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13501 let sketch_id = sketch_object.id;
13502 let sketch = expect_sketch(sketch_object);
13503
13504 assert_eq!(sketch.segments.len(), 3);
13506 let circle_id = sketch.segments[2];
13507
13508 let (src_delta, scene_delta) = frontend
13510 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
13511 .await
13512 .unwrap();
13513 assert!(
13514 src_delta.text.contains("sketch(on = XY) {"),
13515 "Expected sketch block in source, got: {}",
13516 src_delta.text
13517 );
13518 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13519 let new_sketch = expect_sketch(new_sketch_object);
13520 assert_eq!(new_sketch.segments.len(), 0);
13521
13522 ctx.close().await;
13523 mock_ctx.close().await;
13524 }
13525
13526 #[tokio::test(flavor = "multi_thread")]
13527 async fn test_unformatted_source_add_arc() {
13528 let initial_source = "@settings(defaultLengthUnit = mm)
13530
13531
13532
13533
13534sketch001 = sketch(on = XY) {
13535}
13536";
13537
13538 let program = Program::parse(initial_source).unwrap().0.unwrap();
13539 let mut frontend = FrontendState::new();
13540
13541 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13542 let mock_ctx = ExecutorContext::new_mock(None).await;
13543 let version = Version(0);
13544
13545 frontend.hack_set_program(&ctx, program).await.unwrap();
13546 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13547 let sketch_id = sketch_object.id;
13548
13549 let arc_ctor = ArcCtor {
13550 start: Point2d {
13551 x: Expr::Var(Number {
13552 value: 5.0,
13553 units: NumericSuffix::Mm,
13554 }),
13555 y: Expr::Var(Number {
13556 value: 0.0,
13557 units: NumericSuffix::Mm,
13558 }),
13559 },
13560 end: Point2d {
13561 x: Expr::Var(Number {
13562 value: 0.0,
13563 units: NumericSuffix::Mm,
13564 }),
13565 y: Expr::Var(Number {
13566 value: 5.0,
13567 units: NumericSuffix::Mm,
13568 }),
13569 },
13570 center: Point2d {
13571 x: Expr::Var(Number {
13572 value: 0.0,
13573 units: NumericSuffix::Mm,
13574 }),
13575 y: Expr::Var(Number {
13576 value: 0.0,
13577 units: NumericSuffix::Mm,
13578 }),
13579 },
13580 construction: None,
13581 };
13582 let segment = SegmentCtor::Arc(arc_ctor);
13583 let (src_delta, scene_delta) = frontend
13584 .add_segment(&mock_ctx, version, sketch_id, segment, None)
13585 .await
13586 .unwrap();
13587 assert!(
13588 src_delta
13589 .text
13590 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
13591 "Expected arc in source, got: {}",
13592 src_delta.text
13593 );
13594 assert!(!scene_delta.new_objects.is_empty());
13595
13596 ctx.close().await;
13597 mock_ctx.close().await;
13598 }
13599
13600 #[tokio::test(flavor = "multi_thread")]
13601 async fn test_extra_newlines_add_circle() {
13602 let initial_source = "@settings(defaultLengthUnit = mm)
13604
13605
13606
13607sketch001 = sketch(on = XY) {
13608}
13609";
13610
13611 let program = Program::parse(initial_source).unwrap().0.unwrap();
13612 let mut frontend = FrontendState::new();
13613
13614 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13615 let mock_ctx = ExecutorContext::new_mock(None).await;
13616 let version = Version(0);
13617
13618 frontend.hack_set_program(&ctx, program).await.unwrap();
13619 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13620 let sketch_id = sketch_object.id;
13621
13622 let circle_ctor = CircleCtor {
13623 start: Point2d {
13624 x: Expr::Var(Number {
13625 value: 5.0,
13626 units: NumericSuffix::Mm,
13627 }),
13628 y: Expr::Var(Number {
13629 value: 0.0,
13630 units: NumericSuffix::Mm,
13631 }),
13632 },
13633 center: Point2d {
13634 x: Expr::Var(Number {
13635 value: 0.0,
13636 units: NumericSuffix::Mm,
13637 }),
13638 y: Expr::Var(Number {
13639 value: 0.0,
13640 units: NumericSuffix::Mm,
13641 }),
13642 },
13643 construction: None,
13644 };
13645 let segment = SegmentCtor::Circle(circle_ctor);
13646 let (src_delta, scene_delta) = frontend
13647 .add_segment(&mock_ctx, version, sketch_id, segment, None)
13648 .await
13649 .unwrap();
13650 assert!(
13651 src_delta
13652 .text
13653 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
13654 "Expected circle in source, got: {}",
13655 src_delta.text
13656 );
13657 assert!(!scene_delta.new_objects.is_empty());
13658
13659 ctx.close().await;
13660 mock_ctx.close().await;
13661 }
13662
13663 #[tokio::test(flavor = "multi_thread")]
13664 async fn test_extra_newlines_add_constraint() {
13665 let initial_source = "@settings(defaultLengthUnit = mm)
13667
13668
13669
13670sketch001 = sketch(on = XY) {
13671 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
13672 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
13673}
13674";
13675
13676 let program = Program::parse(initial_source).unwrap().0.unwrap();
13677 let mut frontend = FrontendState::new();
13678
13679 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13680 let mock_ctx = ExecutorContext::new_mock(None).await;
13681 let version = Version(0);
13682 let project_id = ProjectId(0);
13683 let file_id = FileId(0);
13684
13685 frontend.hack_set_program(&ctx, program).await.unwrap();
13686 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13687 let sketch_id = sketch_object.id;
13688 let sketch = expect_sketch(sketch_object);
13689
13690 let line_ids: Vec<ObjectId> = sketch
13692 .segments
13693 .iter()
13694 .copied()
13695 .filter(|seg_id| {
13696 matches!(
13697 &frontend.scene_graph.objects[seg_id.0].kind,
13698 ObjectKind::Segment {
13699 segment: Segment::Line(_)
13700 }
13701 )
13702 })
13703 .collect();
13704 assert_eq!(line_ids.len(), 2, "Expected two line segments");
13705
13706 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
13707 let ObjectKind::Segment {
13708 segment: Segment::Line(line1_data),
13709 } = &line1.kind
13710 else {
13711 panic!("Expected line");
13712 };
13713 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
13714 let ObjectKind::Segment {
13715 segment: Segment::Line(line2_data),
13716 } = &line2.kind
13717 else {
13718 panic!("Expected line");
13719 };
13720
13721 let constraint = Constraint::Coincident(Coincident {
13723 segments: vec![line1_data.end.into(), line2_data.start.into()],
13724 });
13725
13726 frontend
13728 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13729 .await
13730 .unwrap();
13731 let (src_delta, _scene_delta) = frontend
13732 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13733 .await
13734 .unwrap();
13735 assert!(
13736 src_delta.text.contains("coincident("),
13737 "Expected coincident constraint in source, got: {}",
13738 src_delta.text
13739 );
13740
13741 ctx.close().await;
13742 mock_ctx.close().await;
13743 }
13744
13745 #[tokio::test(flavor = "multi_thread")]
13746 async fn test_extra_newlines_add_line_then_edit_line() {
13747 let initial_source = "@settings(defaultLengthUnit = mm)
13749
13750
13751
13752sketch001 = sketch(on = XY) {
13753}
13754";
13755
13756 let program = Program::parse(initial_source).unwrap().0.unwrap();
13757 let mut frontend = FrontendState::new();
13758
13759 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13760 let mock_ctx = ExecutorContext::new_mock(None).await;
13761 let version = Version(0);
13762
13763 frontend.hack_set_program(&ctx, program).await.unwrap();
13764 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13765 let sketch_id = sketch_object.id;
13766
13767 let line_ctor = LineCtor {
13769 start: Point2d {
13770 x: Expr::Number(Number {
13771 value: 0.0,
13772 units: NumericSuffix::Mm,
13773 }),
13774 y: Expr::Number(Number {
13775 value: 0.0,
13776 units: NumericSuffix::Mm,
13777 }),
13778 },
13779 end: Point2d {
13780 x: Expr::Number(Number {
13781 value: 10.0,
13782 units: NumericSuffix::Mm,
13783 }),
13784 y: Expr::Number(Number {
13785 value: 10.0,
13786 units: NumericSuffix::Mm,
13787 }),
13788 },
13789 construction: None,
13790 };
13791 let segment = SegmentCtor::Line(line_ctor);
13792 let (src_delta, scene_delta) = frontend
13793 .add_segment(&mock_ctx, version, sketch_id, segment, None)
13794 .await
13795 .unwrap();
13796 assert!(
13797 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
13798 "Expected line in source after add, got: {}",
13799 src_delta.text
13800 );
13801 let line_id = *scene_delta.new_objects.last().unwrap();
13803
13804 let line_ctor = LineCtor {
13806 start: Point2d {
13807 x: Expr::Number(Number {
13808 value: 1.0,
13809 units: NumericSuffix::Mm,
13810 }),
13811 y: Expr::Number(Number {
13812 value: 2.0,
13813 units: NumericSuffix::Mm,
13814 }),
13815 },
13816 end: Point2d {
13817 x: Expr::Number(Number {
13818 value: 13.0,
13819 units: NumericSuffix::Mm,
13820 }),
13821 y: Expr::Number(Number {
13822 value: 14.0,
13823 units: NumericSuffix::Mm,
13824 }),
13825 },
13826 construction: None,
13827 };
13828 let segments = vec![ExistingSegmentCtor {
13829 id: line_id,
13830 ctor: SegmentCtor::Line(line_ctor),
13831 }];
13832 let (src_delta, scene_delta) = frontend
13833 .edit_segments(&mock_ctx, version, sketch_id, segments)
13834 .await
13835 .unwrap();
13836 assert!(
13837 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
13838 "Expected edited line in source, got: {}",
13839 src_delta.text
13840 );
13841 assert_eq!(scene_delta.new_objects, vec![]);
13842
13843 ctx.close().await;
13844 mock_ctx.close().await;
13845 }
13846}