1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_error::CompilationIssue;
9use kcl_error::SourceRange;
10use kittycad_modeling_cmds::units::UnitLength;
11use serde::Serialize;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::collections::AhashIndexSet;
19#[cfg(feature = "artifact-graph")]
20use crate::execution::Artifact;
21#[cfg(feature = "artifact-graph")]
22use crate::execution::ArtifactGraph;
23#[cfg(feature = "artifact-graph")]
24use crate::execution::CapSubType;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::cache::SketchModeState;
28use crate::execution::cache::clear_mem_cache;
29use crate::execution::cache::read_old_memory;
30use crate::execution::cache::write_old_memory;
31use crate::fmt::format_number_literal;
32use crate::front::Angle;
33use crate::front::ArcCtor;
34use crate::front::CircleCtor;
35use crate::front::Distance;
36use crate::front::EqualRadius;
37use crate::front::Error;
38use crate::front::ExecResult;
39use crate::front::FixedPoint;
40use crate::front::Freedom;
41use crate::front::LinesEqualLength;
42use crate::front::Midpoint;
43use crate::front::Object;
44use crate::front::Parallel;
45use crate::front::Perpendicular;
46use crate::front::PointCtor;
47use crate::front::Symmetric;
48use crate::front::Tangent;
49use crate::frontend::api::Expr;
50use crate::frontend::api::FileId;
51use crate::frontend::api::Number;
52use crate::frontend::api::ObjectId;
53use crate::frontend::api::ObjectKind;
54use crate::frontend::api::Plane;
55use crate::frontend::api::ProjectId;
56use crate::frontend::api::RestoreSketchCheckpointOutcome;
57use crate::frontend::api::SceneGraph;
58use crate::frontend::api::SceneGraphDelta;
59use crate::frontend::api::SketchCheckpointId;
60use crate::frontend::api::SourceDelta;
61use crate::frontend::api::SourceRef;
62use crate::frontend::api::Version;
63use crate::frontend::modify::find_defined_names;
64use crate::frontend::modify::next_free_name;
65use crate::frontend::modify::next_free_name_with_padding;
66use crate::frontend::sketch::Coincident;
67use crate::frontend::sketch::Constraint;
68use crate::frontend::sketch::ConstraintSegment;
69use crate::frontend::sketch::Diameter;
70use crate::frontend::sketch::ExistingSegmentCtor;
71use crate::frontend::sketch::Horizontal;
72use crate::frontend::sketch::LineCtor;
73use crate::frontend::sketch::Point2d;
74use crate::frontend::sketch::Radius;
75use crate::frontend::sketch::Segment;
76use crate::frontend::sketch::SegmentCtor;
77use crate::frontend::sketch::SketchApi;
78use crate::frontend::sketch::SketchCtor;
79use crate::frontend::sketch::Vertical;
80use crate::frontend::traverse::MutateBodyItem;
81use crate::frontend::traverse::TraversalReturn;
82use crate::frontend::traverse::Visitor;
83use crate::frontend::traverse::dfs_mut;
84use crate::id::IncIdGenerator;
85use crate::parsing::ast::types as ast;
86use crate::pretty::NumericSuffix;
87use crate::std::constraints::LinesAtAngleKind;
88use crate::walk::NodeMut;
89use crate::walk::Visitable;
90
91pub(crate) mod api;
92pub(crate) mod modify;
93pub(crate) mod sketch;
94
95pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
96
97#[derive(Debug, Clone)]
98struct SketchCheckpoint {
99 id: SketchCheckpointId,
100 source: SourceDelta,
101 program: Program,
102 scene_graph: SceneGraph,
103 exec_outcome: ExecOutcome,
104 point_freedom_cache: HashMap<ObjectId, Freedom>,
105 mock_memory: Option<SketchModeState>,
106}
107mod traverse;
108pub(crate) mod trim;
109
110struct ArcSizeConstraintParams {
111 points: Vec<ObjectId>,
112 function_name: &'static str,
113 value: f64,
114 units: NumericSuffix,
115 label_position: Option<Point2d<Number>>,
116 constraint_type_name: &'static str,
117}
118
119const POINT_FN: &str = "point";
120const POINT_AT_PARAM: &str = "at";
121const LINE_FN: &str = "line";
122const LINE_VARIABLE: &str = "line";
123const LINE_START_PARAM: &str = "start";
124const LINE_END_PARAM: &str = "end";
125const ARC_FN: &str = "arc";
126const ARC_VARIABLE: &str = "arc";
127const ARC_START_PARAM: &str = "start";
128const ARC_END_PARAM: &str = "end";
129const ARC_CENTER_PARAM: &str = "center";
130const CIRCLE_FN: &str = "circle";
131const CIRCLE_VARIABLE: &str = "circle";
132const CIRCLE_START_PARAM: &str = "start";
133const CIRCLE_CENTER_PARAM: &str = "center";
134const LABEL_POSITION_PARAM: &str = "labelPosition";
135
136const COINCIDENT_FN: &str = "coincident";
137const DIAMETER_FN: &str = "diameter";
138const DISTANCE_FN: &str = "distance";
139const FIXED_FN: &str = "fixed";
140const ANGLE_FN: &str = "angle";
141const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
142const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
143const EQUAL_LENGTH_FN: &str = "equalLength";
144const EQUAL_RADIUS_FN: &str = "equalRadius";
145const HORIZONTAL_FN: &str = "horizontal";
146const MIDPOINT_FN: &str = "midpoint";
147const MIDPOINT_POINT_PARAM: &str = "point";
148const RADIUS_FN: &str = "radius";
149const SYMMETRIC_FN: &str = "symmetric";
150const SYMMETRIC_AXIS_PARAM: &str = "axis";
151const TANGENT_FN: &str = "tangent";
152const VERTICAL_FN: &str = "vertical";
153
154const LINE_PROPERTY_START: &str = "start";
155const LINE_PROPERTY_END: &str = "end";
156
157const ARC_PROPERTY_START: &str = "start";
158const ARC_PROPERTY_END: &str = "end";
159const ARC_PROPERTY_CENTER: &str = "center";
160const CIRCLE_PROPERTY_START: &str = "start";
161const CIRCLE_PROPERTY_CENTER: &str = "center";
162
163const CONSTRUCTION_PARAM: &str = "construction";
164
165#[derive(Debug, Clone, Copy)]
166enum EditDeleteKind {
167 Edit,
168 DeleteNonSketch,
169}
170
171impl EditDeleteKind {
172 fn is_delete(&self) -> bool {
174 match self {
175 EditDeleteKind::Edit => false,
176 EditDeleteKind::DeleteNonSketch => true,
177 }
178 }
179
180 fn to_change_kind(self) -> ChangeKind {
181 match self {
182 EditDeleteKind::Edit => ChangeKind::Edit,
183 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
184 }
185 }
186}
187
188#[derive(Debug, Clone, Copy)]
189enum ChangeKind {
190 Add,
191 Edit,
192 Delete,
193 None,
194}
195
196#[derive(Debug, Clone, Serialize, ts_rs::TS)]
197#[ts(export, export_to = "FrontendApi.ts")]
198#[serde(tag = "type")]
199pub enum SetProgramOutcome {
200 #[serde(rename_all = "camelCase")]
201 Success {
202 scene_graph: Box<SceneGraph>,
203 exec_outcome: Box<ExecOutcome>,
204 checkpoint_id: Option<SketchCheckpointId>,
205 },
206 #[serde(rename_all = "camelCase")]
207 ExecFailure { error: Box<KclErrorWithOutputs> },
208}
209
210#[derive(Debug, Clone)]
211pub struct FrontendState {
212 program: Program,
213 scene_graph: SceneGraph,
214 point_freedom_cache: HashMap<ObjectId, Freedom>,
217 sketch_checkpoints: VecDeque<SketchCheckpoint>,
218 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
219}
220
221impl Default for FrontendState {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227impl FrontendState {
228 pub fn new() -> Self {
229 Self {
230 program: Program::empty(),
231 scene_graph: SceneGraph {
232 project: ProjectId(0),
233 file: FileId(0),
234 version: Version(0),
235 objects: Default::default(),
236 settings: Default::default(),
237 sketch_mode: Default::default(),
238 },
239 point_freedom_cache: HashMap::new(),
240 sketch_checkpoints: VecDeque::new(),
241 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
242 }
243 }
244
245 pub fn scene_graph(&self) -> &SceneGraph {
247 &self.scene_graph
248 }
249
250 pub fn default_length_unit(&self) -> UnitLength {
251 self.program
252 .meta_settings()
253 .ok()
254 .flatten()
255 .map(|settings| settings.default_length_units)
256 .unwrap_or(UnitLength::Millimeters)
257 }
258
259 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
260 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
261
262 let checkpoint = SketchCheckpoint {
263 id: checkpoint_id,
264 source: SourceDelta {
265 text: source_from_ast(&self.program.ast),
266 },
267 program: self.program.clone(),
268 scene_graph: self.scene_graph.clone(),
269 exec_outcome,
270 point_freedom_cache: self.point_freedom_cache.clone(),
271 mock_memory: read_old_memory().await,
272 };
273
274 self.sketch_checkpoints.push_back(checkpoint);
275 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
276 self.sketch_checkpoints.pop_front();
277 }
278
279 Ok(checkpoint_id)
280 }
281
282 pub async fn restore_sketch_checkpoint(
283 &mut self,
284 checkpoint_id: SketchCheckpointId,
285 ) -> api::Result<RestoreSketchCheckpointOutcome> {
286 let checkpoint = self
287 .sketch_checkpoints
288 .iter()
289 .find(|checkpoint| checkpoint.id == checkpoint_id)
290 .cloned()
291 .ok_or_else(|| Error {
292 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
293 })?;
294
295 self.program = checkpoint.program;
296 self.scene_graph = checkpoint.scene_graph.clone();
297 self.point_freedom_cache = checkpoint.point_freedom_cache;
298
299 if let Some(mock_memory) = checkpoint.mock_memory {
300 write_old_memory(mock_memory).await;
301 } else {
302 clear_mem_cache().await;
303 }
304
305 Ok(RestoreSketchCheckpointOutcome {
306 source_delta: checkpoint.source,
307 scene_graph_delta: SceneGraphDelta {
308 new_graph: checkpoint.scene_graph,
309 new_objects: Vec::new(),
310 invalidates_ids: true,
311 exec_outcome: checkpoint.exec_outcome,
312 },
313 })
314 }
315
316 pub fn clear_sketch_checkpoints(&mut self) {
317 self.sketch_checkpoints.clear();
318 }
319}
320
321impl SketchApi for FrontendState {
322 async fn execute_mock(
323 &mut self,
324 ctx: &ExecutorContext,
325 _version: Version,
326 sketch: ObjectId,
327 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
328 let sketch_block_ref =
329 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
330
331 let mut truncated_program = self.program.clone();
332 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
333 .map_err(KclErrorWithOutputs::no_outputs)?;
334
335 let outcome = ctx
337 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
338 .await?;
339 let new_source = source_from_ast(&self.program.ast);
340 let src_delta = SourceDelta { text: new_source };
341 let outcome = self.update_state_after_exec(outcome, true);
343 let scene_graph_delta = SceneGraphDelta {
344 new_graph: self.scene_graph.clone(),
345 new_objects: Default::default(),
346 invalidates_ids: false,
347 exec_outcome: outcome,
348 };
349 Ok((src_delta, scene_graph_delta))
350 }
351
352 async fn new_sketch(
353 &mut self,
354 ctx: &ExecutorContext,
355 _project: ProjectId,
356 _file: FileId,
357 _version: Version,
358 args: SketchCtor,
359 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
360 let mut new_ast = self.program.ast.clone();
363 let mut plane_ast =
365 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
366 let mut defined_names = find_defined_names(&new_ast);
367 let is_face_of_expr = matches!(
368 &plane_ast,
369 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
370 );
371 if is_face_of_expr {
372 let face_name = next_free_name_with_padding("face", &defined_names)
373 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
374 let face_decl = ast::VariableDeclaration::new(
375 ast::VariableDeclarator::new(&face_name, plane_ast),
376 ast::ItemVisibility::Default,
377 ast::VariableKind::Const,
378 );
379 new_ast
380 .body
381 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
382 face_decl,
383 ))));
384 defined_names.insert(face_name.clone());
385 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
386 }
387 let sketch_ast = ast::SketchBlock {
388 arguments: vec![ast::LabeledArg {
389 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
390 arg: plane_ast,
391 }],
392 body: Default::default(),
393 is_being_edited: false,
394 non_code_meta: Default::default(),
395 digest: None,
396 };
397 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
400 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
401 let sketch_decl = ast::VariableDeclaration::new(
402 ast::VariableDeclarator::new(
403 &sketch_name,
404 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
405 ),
406 ast::ItemVisibility::Default,
407 ast::VariableKind::Const,
408 );
409 new_ast
410 .body
411 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
412 sketch_decl,
413 ))));
414 let new_source = source_from_ast(&new_ast);
416 let (new_program, errors) = Program::parse(&new_source)
418 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
419 if !errors.is_empty() {
420 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
421 "Error parsing KCL source after adding sketch: {errors:?}"
422 ))));
423 }
424 let Some(new_program) = new_program else {
425 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
426 "No AST produced after adding sketch".to_owned(),
427 )));
428 };
429
430 self.program = new_program.clone();
432
433 let outcome = ctx.run_with_caching(new_program.clone()).await?;
436 let freedom_analysis_ran = true;
437
438 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
439
440 let Some(sketch_id) = self
441 .scene_graph
442 .objects
443 .iter()
444 .filter_map(|object| match object.kind {
445 ObjectKind::Sketch(_) => Some(object.id),
446 _ => None,
447 })
448 .max_by_key(|id| id.0)
449 else {
450 return Err(KclErrorWithOutputs::from_error_outcome(
451 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
452 outcome,
453 ));
454 };
455 self.scene_graph.sketch_mode = Some(sketch_id);
457
458 let src_delta = SourceDelta { text: new_source };
459 let scene_graph_delta = SceneGraphDelta {
460 new_graph: self.scene_graph.clone(),
461 invalidates_ids: false,
462 new_objects: vec![sketch_id],
463 exec_outcome: outcome,
464 };
465 Ok((src_delta, scene_graph_delta, sketch_id))
466 }
467
468 async fn edit_sketch(
469 &mut self,
470 ctx: &ExecutorContext,
471 _project: ProjectId,
472 _file: FileId,
473 _version: Version,
474 sketch: ObjectId,
475 ) -> ExecResult<SceneGraphDelta> {
476 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
480 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
481 })?;
482 let ObjectKind::Sketch(_) = &sketch_object.kind else {
483 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
484 "Object is not a sketch, it is {}",
485 sketch_object.kind.human_friendly_kind_with_article()
486 ))));
487 };
488 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
489
490 self.scene_graph.sketch_mode = Some(sketch);
492
493 let mut truncated_program = self.program.clone();
495 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
496 .map_err(KclErrorWithOutputs::no_outputs)?;
497
498 let outcome = ctx
501 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
502 .await?;
503
504 let outcome = self.update_state_after_exec(outcome, true);
506 let scene_graph_delta = SceneGraphDelta {
507 new_graph: self.scene_graph.clone(),
508 invalidates_ids: false,
509 new_objects: Vec::new(),
510 exec_outcome: outcome,
511 };
512 Ok(scene_graph_delta)
513 }
514
515 async fn exit_sketch(
516 &mut self,
517 ctx: &ExecutorContext,
518 _version: Version,
519 sketch: ObjectId,
520 ) -> ExecResult<SceneGraph> {
521 #[cfg(not(target_arch = "wasm32"))]
523 let _ = sketch;
524 #[cfg(target_arch = "wasm32")]
525 if self.scene_graph.sketch_mode != Some(sketch) {
526 web_sys::console::warn_1(
527 &format!(
528 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
529 &self.scene_graph.sketch_mode
530 )
531 .into(),
532 );
533 }
534 self.scene_graph.sketch_mode = None;
535
536 let outcome = ctx.run_with_caching(self.program.clone()).await?;
538
539 self.update_state_after_exec(outcome, false);
541
542 Ok(self.scene_graph.clone())
543 }
544
545 async fn delete_sketch(
546 &mut self,
547 ctx: &ExecutorContext,
548 _version: Version,
549 sketch: ObjectId,
550 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
551 let mut new_ast = self.program.ast.clone();
554
555 let sketch_id = sketch;
557 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
558 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
559 })?;
560 let ObjectKind::Sketch(_) = &sketch_object.kind else {
561 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
562 "Object is not a sketch, it is {}",
563 sketch_object.kind.human_friendly_kind_with_article(),
564 ))));
565 };
566
567 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
569 .map_err(KclErrorWithOutputs::no_outputs)?;
570
571 self.execute_after_delete_sketch(ctx, &mut new_ast).await
572 }
573
574 async fn add_segment(
575 &mut self,
576 ctx: &ExecutorContext,
577 _version: Version,
578 sketch: ObjectId,
579 segment: SegmentCtor,
580 _label: Option<String>,
581 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
582 match segment {
584 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
585 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
586 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
587 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
588 }
589 }
590
591 async fn edit_segments(
592 &mut self,
593 ctx: &ExecutorContext,
594 _version: Version,
595 sketch: ObjectId,
596 segments: Vec<ExistingSegmentCtor>,
597 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
598 let sketch_block_ref =
600 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
601
602 let mut new_ast = self.program.ast.clone();
603 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
604
605 for segment in &segments {
608 segment_ids_edited.insert(segment.id);
609 }
610
611 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
626
627 for segment in segments {
628 let segment_id = segment.id;
629 match segment.ctor {
630 SegmentCtor::Point(ctor) => {
631 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
633 && let ObjectKind::Segment { segment } = &segment_object.kind
634 && let Segment::Point(point) = segment
635 && let Some(owner_id) = point.owner
636 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
637 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
638 {
639 match owner_segment {
640 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
641 if let Some(existing) = final_edits.get_mut(&owner_id) {
642 let SegmentCtor::Line(line_ctor) = existing else {
643 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
644 "Internal: Expected line ctor for owner, but found {}",
645 existing.human_friendly_kind_with_article()
646 ))));
647 };
648 if line.start == segment_id {
650 line_ctor.start = ctor.position;
651 } else {
652 line_ctor.end = ctor.position;
653 }
654 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
655 let mut line_ctor = line_ctor.clone();
657 if line.start == segment_id {
658 line_ctor.start = ctor.position;
659 } else {
660 line_ctor.end = ctor.position;
661 }
662 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
663 } else {
664 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
666 "Internal: Line does not have line ctor, but found {}",
667 line.ctor.human_friendly_kind_with_article()
668 ))));
669 }
670 continue;
671 }
672 Segment::Arc(arc)
673 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
674 {
675 if let Some(existing) = final_edits.get_mut(&owner_id) {
676 let SegmentCtor::Arc(arc_ctor) = existing else {
677 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
678 "Internal: Expected arc ctor for owner, but found {}",
679 existing.human_friendly_kind_with_article()
680 ))));
681 };
682 if arc.start == segment_id {
683 arc_ctor.start = ctor.position;
684 } else if arc.end == segment_id {
685 arc_ctor.end = ctor.position;
686 } else {
687 arc_ctor.center = ctor.position;
688 }
689 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
690 let mut arc_ctor = arc_ctor.clone();
691 if arc.start == segment_id {
692 arc_ctor.start = ctor.position;
693 } else if arc.end == segment_id {
694 arc_ctor.end = ctor.position;
695 } else {
696 arc_ctor.center = ctor.position;
697 }
698 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
699 } else {
700 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
701 "Internal: Arc does not have arc ctor, but found {}",
702 arc.ctor.human_friendly_kind_with_article()
703 ))));
704 }
705 continue;
706 }
707 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
708 if let Some(existing) = final_edits.get_mut(&owner_id) {
709 let SegmentCtor::Circle(circle_ctor) = existing else {
710 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
711 "Internal: Expected circle ctor for owner, but found {}",
712 existing.human_friendly_kind_with_article()
713 ))));
714 };
715 if circle.start == segment_id {
716 circle_ctor.start = ctor.position;
717 } else {
718 circle_ctor.center = ctor.position;
719 }
720 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
721 let mut circle_ctor = circle_ctor.clone();
722 if circle.start == segment_id {
723 circle_ctor.start = ctor.position;
724 } else {
725 circle_ctor.center = ctor.position;
726 }
727 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
728 } else {
729 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
730 "Internal: Circle does not have circle ctor, but found {}",
731 circle.ctor.human_friendly_kind_with_article()
732 ))));
733 }
734 continue;
735 }
736 _ => {}
737 }
738 }
739
740 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
742 }
743 SegmentCtor::Line(ctor) => {
744 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
745 }
746 SegmentCtor::Arc(ctor) => {
747 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
748 }
749 SegmentCtor::Circle(ctor) => {
750 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
751 }
752 }
753 }
754
755 for (segment_id, ctor) in final_edits {
756 match ctor {
757 SegmentCtor::Point(ctor) => self
758 .edit_point(&mut new_ast, sketch, segment_id, ctor)
759 .map_err(KclErrorWithOutputs::no_outputs)?,
760 SegmentCtor::Line(ctor) => self
761 .edit_line(&mut new_ast, sketch, segment_id, ctor)
762 .map_err(KclErrorWithOutputs::no_outputs)?,
763 SegmentCtor::Arc(ctor) => self
764 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
765 .map_err(KclErrorWithOutputs::no_outputs)?,
766 SegmentCtor::Circle(ctor) => self
767 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
768 .map_err(KclErrorWithOutputs::no_outputs)?,
769 }
770 }
771 self.execute_after_edit(
772 ctx,
773 sketch,
774 sketch_block_ref,
775 segment_ids_edited,
776 EditDeleteKind::Edit,
777 &mut new_ast,
778 )
779 .await
780 }
781
782 async fn delete_objects(
783 &mut self,
784 ctx: &ExecutorContext,
785 _version: Version,
786 sketch: ObjectId,
787 constraint_ids: Vec<ObjectId>,
788 segment_ids: Vec<ObjectId>,
789 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
790 let sketch_block_ref =
792 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
793
794 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
796 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
797
798 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
801
802 for segment_id in segment_ids_set.iter().copied() {
803 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
804 && let ObjectKind::Segment { segment } = &segment_object.kind
805 && let Segment::Point(point) = segment
806 && let Some(owner_id) = point.owner
807 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
808 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
809 && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
810 {
811 resolved_segment_ids_to_delete.insert(owner_id);
813 } else {
814 resolved_segment_ids_to_delete.insert(segment_id);
816 }
817 }
818 let referenced_constraint_ids = self
819 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
820 .map_err(KclErrorWithOutputs::no_outputs)?;
821
822 let mut new_ast = self.program.ast.clone();
823
824 for constraint_id in referenced_constraint_ids {
825 if constraint_ids_set.contains(&constraint_id) {
826 continue;
827 }
828
829 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
830 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
831 })?;
832 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
833 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
834 "Object is not a constraint, it is {}",
835 constraint_object.kind.human_friendly_kind_with_article()
836 ))));
837 };
838
839 match constraint {
840 Constraint::Coincident(coincident) => {
841 let remaining_segments =
842 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
843
844 if remaining_segments.len() >= 2 {
846 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
847 .map_err(KclErrorWithOutputs::no_outputs)?;
848 } else {
849 constraint_ids_set.insert(constraint_id);
850 }
851 }
852 Constraint::EqualRadius(equal_radius) => {
853 let remaining_input = equal_radius
854 .input
855 .iter()
856 .copied()
857 .filter(|segment_id| {
858 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
859 })
860 .collect::<Vec<_>>();
861
862 if remaining_input.len() >= 2 {
863 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
864 .map_err(KclErrorWithOutputs::no_outputs)?;
865 } else {
866 constraint_ids_set.insert(constraint_id);
867 }
868 }
869 Constraint::LinesEqualLength(lines_equal_length) => {
870 let remaining_lines = lines_equal_length
871 .lines
872 .iter()
873 .copied()
874 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
875 .collect::<Vec<_>>();
876
877 if remaining_lines.len() >= 2 {
879 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
880 .map_err(KclErrorWithOutputs::no_outputs)?;
881 } else {
882 constraint_ids_set.insert(constraint_id);
883 }
884 }
885 Constraint::Parallel(parallel) => {
886 let remaining_lines = parallel
887 .lines
888 .iter()
889 .copied()
890 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
891 .collect::<Vec<_>>();
892
893 if remaining_lines.len() >= 2 {
894 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
895 .map_err(KclErrorWithOutputs::no_outputs)?;
896 } else {
897 constraint_ids_set.insert(constraint_id);
898 }
899 }
900 Constraint::Horizontal(Horizontal::Points { points }) => {
901 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
902
903 if remaining_points.len() >= 2 {
904 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
905 .map_err(KclErrorWithOutputs::no_outputs)?;
906 } else {
907 constraint_ids_set.insert(constraint_id);
908 }
909 }
910 Constraint::Vertical(Vertical::Points { points }) => {
911 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
912
913 if remaining_points.len() >= 2 {
914 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
915 .map_err(KclErrorWithOutputs::no_outputs)?;
916 } else {
917 constraint_ids_set.insert(constraint_id);
918 }
919 }
920 Constraint::Fixed(fixed) => {
921 if fixed.points.iter().any(|fixed_point| {
922 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
923 }) {
924 constraint_ids_set.insert(constraint_id);
925 }
926 }
927 _ => {
928 constraint_ids_set.insert(constraint_id);
930 }
931 }
932 }
933
934 for constraint_id in constraint_ids_set {
935 self.delete_constraint(&mut new_ast, sketch, constraint_id)
936 .map_err(KclErrorWithOutputs::no_outputs)?;
937 }
938 for segment_id in resolved_segment_ids_to_delete {
939 self.delete_segment(&mut new_ast, sketch, segment_id)
940 .map_err(KclErrorWithOutputs::no_outputs)?;
941 }
942
943 self.execute_after_edit(
944 ctx,
945 sketch,
946 sketch_block_ref,
947 Default::default(),
948 EditDeleteKind::DeleteNonSketch,
949 &mut new_ast,
950 )
951 .await
952 }
953
954 async fn add_constraint(
955 &mut self,
956 ctx: &ExecutorContext,
957 _version: Version,
958 sketch: ObjectId,
959 constraint: Constraint,
960 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
961 let original_program = self.program.clone();
965 let original_scene_graph = self.scene_graph.clone();
966
967 let mut new_ast = self.program.ast.clone();
968 let sketch_block_ref = match constraint {
969 Constraint::Coincident(coincident) => self
970 .add_coincident(sketch, coincident, &mut new_ast)
971 .await
972 .map_err(KclErrorWithOutputs::no_outputs)?,
973 Constraint::Distance(distance) => self
974 .add_distance(sketch, distance, &mut new_ast)
975 .await
976 .map_err(KclErrorWithOutputs::no_outputs)?,
977 Constraint::EqualRadius(equal_radius) => self
978 .add_equal_radius(sketch, equal_radius, &mut new_ast)
979 .await
980 .map_err(KclErrorWithOutputs::no_outputs)?,
981 Constraint::Fixed(fixed) => self
982 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
983 .await
984 .map_err(KclErrorWithOutputs::no_outputs)?,
985 Constraint::HorizontalDistance(distance) => self
986 .add_horizontal_distance(sketch, distance, &mut new_ast)
987 .await
988 .map_err(KclErrorWithOutputs::no_outputs)?,
989 Constraint::VerticalDistance(distance) => self
990 .add_vertical_distance(sketch, distance, &mut new_ast)
991 .await
992 .map_err(KclErrorWithOutputs::no_outputs)?,
993 Constraint::Horizontal(horizontal) => self
994 .add_horizontal(sketch, horizontal, &mut new_ast)
995 .await
996 .map_err(KclErrorWithOutputs::no_outputs)?,
997 Constraint::LinesEqualLength(lines_equal_length) => self
998 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
999 .await
1000 .map_err(KclErrorWithOutputs::no_outputs)?,
1001 Constraint::Midpoint(midpoint) => self
1002 .add_midpoint(sketch, midpoint, &mut new_ast)
1003 .await
1004 .map_err(KclErrorWithOutputs::no_outputs)?,
1005 Constraint::Parallel(parallel) => self
1006 .add_parallel(sketch, parallel, &mut new_ast)
1007 .await
1008 .map_err(KclErrorWithOutputs::no_outputs)?,
1009 Constraint::Perpendicular(perpendicular) => self
1010 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1011 .await
1012 .map_err(KclErrorWithOutputs::no_outputs)?,
1013 Constraint::Radius(radius) => self
1014 .add_radius(sketch, radius, &mut new_ast)
1015 .await
1016 .map_err(KclErrorWithOutputs::no_outputs)?,
1017 Constraint::Diameter(diameter) => self
1018 .add_diameter(sketch, diameter, &mut new_ast)
1019 .await
1020 .map_err(KclErrorWithOutputs::no_outputs)?,
1021 Constraint::Symmetric(symmetric) => self
1022 .add_symmetric(sketch, symmetric, &mut new_ast)
1023 .await
1024 .map_err(KclErrorWithOutputs::no_outputs)?,
1025 Constraint::Vertical(vertical) => self
1026 .add_vertical(sketch, vertical, &mut new_ast)
1027 .await
1028 .map_err(KclErrorWithOutputs::no_outputs)?,
1029 Constraint::Angle(lines_at_angle) => self
1030 .add_angle(sketch, lines_at_angle, &mut new_ast)
1031 .await
1032 .map_err(KclErrorWithOutputs::no_outputs)?,
1033 Constraint::Tangent(tangent) => self
1034 .add_tangent(sketch, tangent, &mut new_ast)
1035 .await
1036 .map_err(KclErrorWithOutputs::no_outputs)?,
1037 };
1038
1039 let result = self
1040 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1041 .await;
1042
1043 if result.is_err() {
1045 self.program = original_program;
1046 self.scene_graph = original_scene_graph;
1047 }
1048
1049 result
1050 }
1051
1052 async fn chain_segment(
1053 &mut self,
1054 ctx: &ExecutorContext,
1055 version: Version,
1056 sketch: ObjectId,
1057 previous_segment_end_point_id: ObjectId,
1058 segment: SegmentCtor,
1059 _label: Option<String>,
1060 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1061 let SegmentCtor::Line(line_ctor) = segment else {
1065 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1066 "chain_segment currently only supports Line segments, got {}",
1067 segment.human_friendly_kind_with_article(),
1068 ))));
1069 };
1070
1071 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1073
1074 let new_line_id = first_scene_delta
1077 .new_objects
1078 .iter()
1079 .find(|&obj_id| {
1080 let obj = self.scene_graph.objects.get(obj_id.0);
1081 if let Some(obj) = obj {
1082 matches!(
1083 &obj.kind,
1084 ObjectKind::Segment {
1085 segment: Segment::Line(_)
1086 }
1087 )
1088 } else {
1089 false
1090 }
1091 })
1092 .ok_or_else(|| {
1093 KclErrorWithOutputs::no_outputs(KclError::refactor(
1094 "Failed to find new line segment in scene graph".to_string(),
1095 ))
1096 })?;
1097
1098 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1099 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1100 "New line object not found: {new_line_id:?}"
1101 )))
1102 })?;
1103
1104 let ObjectKind::Segment {
1105 segment: new_line_segment,
1106 } = &new_line_obj.kind
1107 else {
1108 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1109 "Object is not a segment: {new_line_obj:?}"
1110 ))));
1111 };
1112
1113 let Segment::Line(new_line) = new_line_segment else {
1114 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1115 "Segment is not a line: {new_line_segment:?}"
1116 ))));
1117 };
1118
1119 let new_line_start_point_id = new_line.start;
1120
1121 let coincident = Coincident {
1123 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1124 };
1125
1126 let (final_src_delta, final_scene_delta) = self
1127 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1128 .await?;
1129
1130 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1133 combined_new_objects.extend(final_scene_delta.new_objects);
1134
1135 let scene_graph_delta = SceneGraphDelta {
1136 new_graph: self.scene_graph.clone(),
1137 invalidates_ids: false,
1138 new_objects: combined_new_objects,
1139 exec_outcome: final_scene_delta.exec_outcome,
1140 };
1141
1142 Ok((final_src_delta, scene_graph_delta))
1143 }
1144
1145 async fn edit_constraint(
1146 &mut self,
1147 ctx: &ExecutorContext,
1148 _version: Version,
1149 sketch: ObjectId,
1150 constraint_id: ObjectId,
1151 value_expression: String,
1152 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1153 let sketch_block_ref =
1155 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1156
1157 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1158 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1159 })?;
1160 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1161 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1162 "Object is not a constraint: {constraint_id:?}"
1163 ))));
1164 }
1165
1166 let mut new_ast = self.program.ast.clone();
1167
1168 let (parsed, errors) = Program::parse(&value_expression)
1170 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1171 if !errors.is_empty() {
1172 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1173 "Error parsing value expression: {errors:?}"
1174 ))));
1175 }
1176 let mut parsed = parsed.ok_or_else(|| {
1177 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1178 })?;
1179 if parsed.ast.body.is_empty() {
1180 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1181 "Empty value expression".to_string(),
1182 )));
1183 }
1184 let first = parsed.ast.body.remove(0);
1185 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1186 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1187 "Value expression must be a simple expression".to_string(),
1188 )));
1189 };
1190
1191 let new_value: ast::BinaryPart = expr_stmt
1192 .inner
1193 .expression
1194 .try_into()
1195 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1196
1197 self.mutate_ast(
1198 &mut new_ast,
1199 constraint_id,
1200 AstMutateCommand::EditConstraintValue { value: new_value },
1201 )
1202 .map_err(KclErrorWithOutputs::no_outputs)?;
1203
1204 self.execute_after_edit(
1205 ctx,
1206 sketch,
1207 sketch_block_ref,
1208 Default::default(),
1209 EditDeleteKind::Edit,
1210 &mut new_ast,
1211 )
1212 .await
1213 }
1214
1215 async fn edit_distance_constraint_label_position(
1216 &mut self,
1217 ctx: &ExecutorContext,
1218 _version: Version,
1219 sketch: ObjectId,
1220 constraint_id: ObjectId,
1221 label_position: Point2d<Number>,
1222 anchor_segment_ids: Vec<ObjectId>,
1223 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1224 let sketch_block_ref =
1226 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1227
1228 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1229 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1230 })?;
1231 if !matches!(
1232 &object.kind,
1233 ObjectKind::Constraint {
1234 constraint: Constraint::Distance(_)
1235 | Constraint::HorizontalDistance(_)
1236 | Constraint::VerticalDistance(_)
1237 | Constraint::Radius(_)
1238 | Constraint::Diameter(_),
1239 }
1240 ) {
1241 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1242 "Object does not support labelPosition: {constraint_id:?}"
1243 ))));
1244 }
1245
1246 let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1247 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1248 "Could not convert label position to AST: {err}"
1249 )))
1250 })?;
1251 let mut new_ast = self.program.ast.clone();
1252 self.mutate_ast(
1253 &mut new_ast,
1254 constraint_id,
1255 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1256 )
1257 .map_err(KclErrorWithOutputs::no_outputs)?;
1258
1259 self.execute_after_edit(
1260 ctx,
1261 sketch,
1262 sketch_block_ref,
1263 anchor_segment_ids.into_iter().collect(),
1264 EditDeleteKind::Edit,
1265 &mut new_ast,
1266 )
1267 .await
1268 }
1269
1270 async fn batch_split_segment_operations(
1278 &mut self,
1279 ctx: &ExecutorContext,
1280 _version: Version,
1281 sketch: ObjectId,
1282 edit_segments: Vec<ExistingSegmentCtor>,
1283 add_constraints: Vec<Constraint>,
1284 delete_constraint_ids: Vec<ObjectId>,
1285 _new_segment_info: sketch::NewSegmentInfo,
1286 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1287 let sketch_block_ref =
1289 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1290
1291 let mut new_ast = self.program.ast.clone();
1292 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1293
1294 for segment in edit_segments {
1296 segment_ids_edited.insert(segment.id);
1297 match segment.ctor {
1298 SegmentCtor::Point(ctor) => self
1299 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1300 .map_err(KclErrorWithOutputs::no_outputs)?,
1301 SegmentCtor::Line(ctor) => self
1302 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1303 .map_err(KclErrorWithOutputs::no_outputs)?,
1304 SegmentCtor::Arc(ctor) => self
1305 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1306 .map_err(KclErrorWithOutputs::no_outputs)?,
1307 SegmentCtor::Circle(ctor) => self
1308 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1309 .map_err(KclErrorWithOutputs::no_outputs)?,
1310 }
1311 }
1312
1313 for constraint in add_constraints {
1315 match constraint {
1316 Constraint::Coincident(coincident) => {
1317 self.add_coincident(sketch, coincident, &mut new_ast)
1318 .await
1319 .map_err(KclErrorWithOutputs::no_outputs)?;
1320 }
1321 Constraint::Distance(distance) => {
1322 self.add_distance(sketch, distance, &mut new_ast)
1323 .await
1324 .map_err(KclErrorWithOutputs::no_outputs)?;
1325 }
1326 Constraint::EqualRadius(equal_radius) => {
1327 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1328 .await
1329 .map_err(KclErrorWithOutputs::no_outputs)?;
1330 }
1331 Constraint::Fixed(fixed) => {
1332 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1333 .await
1334 .map_err(KclErrorWithOutputs::no_outputs)?;
1335 }
1336 Constraint::HorizontalDistance(distance) => {
1337 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1338 .await
1339 .map_err(KclErrorWithOutputs::no_outputs)?;
1340 }
1341 Constraint::VerticalDistance(distance) => {
1342 self.add_vertical_distance(sketch, distance, &mut new_ast)
1343 .await
1344 .map_err(KclErrorWithOutputs::no_outputs)?;
1345 }
1346 Constraint::Horizontal(horizontal) => {
1347 self.add_horizontal(sketch, horizontal, &mut new_ast)
1348 .await
1349 .map_err(KclErrorWithOutputs::no_outputs)?;
1350 }
1351 Constraint::LinesEqualLength(lines_equal_length) => {
1352 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1353 .await
1354 .map_err(KclErrorWithOutputs::no_outputs)?;
1355 }
1356 Constraint::Midpoint(midpoint) => {
1357 self.add_midpoint(sketch, midpoint, &mut new_ast)
1358 .await
1359 .map_err(KclErrorWithOutputs::no_outputs)?;
1360 }
1361 Constraint::Parallel(parallel) => {
1362 self.add_parallel(sketch, parallel, &mut new_ast)
1363 .await
1364 .map_err(KclErrorWithOutputs::no_outputs)?;
1365 }
1366 Constraint::Perpendicular(perpendicular) => {
1367 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1368 .await
1369 .map_err(KclErrorWithOutputs::no_outputs)?;
1370 }
1371 Constraint::Vertical(vertical) => {
1372 self.add_vertical(sketch, vertical, &mut new_ast)
1373 .await
1374 .map_err(KclErrorWithOutputs::no_outputs)?;
1375 }
1376 Constraint::Diameter(diameter) => {
1377 self.add_diameter(sketch, diameter, &mut new_ast)
1378 .await
1379 .map_err(KclErrorWithOutputs::no_outputs)?;
1380 }
1381 Constraint::Radius(radius) => {
1382 self.add_radius(sketch, radius, &mut new_ast)
1383 .await
1384 .map_err(KclErrorWithOutputs::no_outputs)?;
1385 }
1386 Constraint::Symmetric(symmetric) => {
1387 self.add_symmetric(sketch, symmetric, &mut new_ast)
1388 .await
1389 .map_err(KclErrorWithOutputs::no_outputs)?;
1390 }
1391 Constraint::Angle(angle) => {
1392 self.add_angle(sketch, angle, &mut new_ast)
1393 .await
1394 .map_err(KclErrorWithOutputs::no_outputs)?;
1395 }
1396 Constraint::Tangent(tangent) => {
1397 self.add_tangent(sketch, tangent, &mut new_ast)
1398 .await
1399 .map_err(KclErrorWithOutputs::no_outputs)?;
1400 }
1401 }
1402 }
1403
1404 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1406
1407 let has_constraint_deletions = !constraint_ids_set.is_empty();
1408 for constraint_id in constraint_ids_set {
1409 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1410 .map_err(KclErrorWithOutputs::no_outputs)?;
1411 }
1412
1413 let (source_delta, mut scene_graph_delta) = self
1417 .execute_after_edit(
1418 ctx,
1419 sketch,
1420 sketch_block_ref,
1421 segment_ids_edited,
1422 EditDeleteKind::Edit,
1423 &mut new_ast,
1424 )
1425 .await?;
1426
1427 if has_constraint_deletions {
1430 scene_graph_delta.invalidates_ids = true;
1431 }
1432
1433 Ok((source_delta, scene_graph_delta))
1434 }
1435
1436 async fn batch_tail_cut_operations(
1437 &mut self,
1438 ctx: &ExecutorContext,
1439 _version: Version,
1440 sketch: ObjectId,
1441 edit_segments: Vec<ExistingSegmentCtor>,
1442 add_constraints: Vec<Constraint>,
1443 delete_constraint_ids: Vec<ObjectId>,
1444 additional_edited_segment_ids: Vec<ObjectId>,
1445 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1446 let sketch_block_ref =
1447 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1448
1449 let mut new_ast = self.program.ast.clone();
1450 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1451
1452 for segment in edit_segments {
1454 segment_ids_edited.insert(segment.id);
1455 match segment.ctor {
1456 SegmentCtor::Point(ctor) => self
1457 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1458 .map_err(KclErrorWithOutputs::no_outputs)?,
1459 SegmentCtor::Line(ctor) => self
1460 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1461 .map_err(KclErrorWithOutputs::no_outputs)?,
1462 SegmentCtor::Arc(ctor) => self
1463 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1464 .map_err(KclErrorWithOutputs::no_outputs)?,
1465 SegmentCtor::Circle(ctor) => self
1466 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1467 .map_err(KclErrorWithOutputs::no_outputs)?,
1468 }
1469 }
1470
1471 segment_ids_edited.extend(additional_edited_segment_ids);
1472
1473 for constraint in add_constraints {
1475 match constraint {
1476 Constraint::Coincident(coincident) => {
1477 self.add_coincident(sketch, coincident, &mut new_ast)
1478 .await
1479 .map_err(KclErrorWithOutputs::no_outputs)?;
1480 }
1481 other => {
1482 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1483 "unsupported constraint in tail cut batch: {other:?}"
1484 ))));
1485 }
1486 }
1487 }
1488
1489 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1491
1492 let has_constraint_deletions = !constraint_ids_set.is_empty();
1493 for constraint_id in constraint_ids_set {
1494 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1495 .map_err(KclErrorWithOutputs::no_outputs)?;
1496 }
1497
1498 let (source_delta, mut scene_graph_delta) = self
1502 .execute_after_edit(
1503 ctx,
1504 sketch,
1505 sketch_block_ref,
1506 segment_ids_edited,
1507 EditDeleteKind::Edit,
1508 &mut new_ast,
1509 )
1510 .await?;
1511
1512 if has_constraint_deletions {
1515 scene_graph_delta.invalidates_ids = true;
1516 }
1517
1518 Ok((source_delta, scene_graph_delta))
1519 }
1520}
1521
1522impl FrontendState {
1523 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1524 self.program = program.clone();
1525
1526 self.point_freedom_cache.clear();
1537 match ctx.run_with_caching(program).await {
1538 Ok(outcome) => {
1539 let outcome = self.update_state_after_exec(outcome, true);
1540 let checkpoint_id = self
1541 .create_sketch_checkpoint(outcome.clone())
1542 .await
1543 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1544 Ok(SetProgramOutcome::Success {
1545 scene_graph: Box::new(self.scene_graph.clone()),
1546 exec_outcome: Box::new(outcome),
1547 checkpoint_id: Some(checkpoint_id),
1548 })
1549 }
1550 Err(mut err) => {
1551 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1554 self.update_state_after_exec(outcome, true);
1555 err.scene_graph = Some(self.scene_graph.clone());
1556 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1557 }
1558 }
1559 }
1560
1561 pub async fn engine_execute(
1564 &mut self,
1565 ctx: &ExecutorContext,
1566 program: Program,
1567 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1568 self.program = program.clone();
1569
1570 self.point_freedom_cache.clear();
1574 match ctx.run_with_caching(program).await {
1575 Ok(outcome) => {
1576 let outcome = self.update_state_after_exec(outcome, true);
1577 Ok(SceneGraphDelta {
1578 new_graph: self.scene_graph.clone(),
1579 exec_outcome: outcome,
1580 new_objects: Default::default(),
1582 invalidates_ids: Default::default(),
1584 })
1585 }
1586 Err(mut err) => {
1587 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1589 self.update_state_after_exec(outcome, true);
1590 err.scene_graph = Some(self.scene_graph.clone());
1591 Err(err)
1592 }
1593 }
1594 }
1595
1596 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1597 if matches!(err.error, KclError::EngineHangup { .. }) {
1598 return Err(err);
1602 }
1603
1604 let KclErrorWithOutputs {
1605 error,
1606 mut non_fatal,
1607 variables,
1608 #[cfg(feature = "artifact-graph")]
1609 operations,
1610 #[cfg(feature = "artifact-graph")]
1611 artifact_graph,
1612 #[cfg(feature = "artifact-graph")]
1613 scene_objects,
1614 #[cfg(feature = "artifact-graph")]
1615 source_range_to_object,
1616 #[cfg(feature = "artifact-graph")]
1617 var_solutions,
1618 filenames,
1619 default_planes,
1620 ..
1621 } = err;
1622
1623 if let Some(source_range) = error.source_ranges().first() {
1624 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1625 } else {
1626 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1627 }
1628
1629 Ok(ExecOutcome {
1630 variables,
1631 filenames,
1632 #[cfg(feature = "artifact-graph")]
1633 operations,
1634 #[cfg(feature = "artifact-graph")]
1635 artifact_graph,
1636 #[cfg(feature = "artifact-graph")]
1637 scene_objects,
1638 #[cfg(feature = "artifact-graph")]
1639 source_range_to_object,
1640 #[cfg(feature = "artifact-graph")]
1641 var_solutions,
1642 issues: non_fatal,
1643 default_planes,
1644 })
1645 }
1646
1647 async fn add_point(
1648 &mut self,
1649 ctx: &ExecutorContext,
1650 sketch: ObjectId,
1651 ctor: PointCtor,
1652 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1653 let at_ast = to_ast_point2d(&ctor.position)
1655 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1656 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1657 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1658 unlabeled: None,
1659 arguments: vec![ast::LabeledArg {
1660 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1661 arg: at_ast,
1662 }],
1663 digest: None,
1664 non_code_meta: Default::default(),
1665 })));
1666
1667 let sketch_id = sketch;
1669 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1670 #[cfg(target_arch = "wasm32")]
1671 web_sys::console::error_1(
1672 &format!(
1673 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1674 &self.scene_graph.objects
1675 )
1676 .into(),
1677 );
1678 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1679 })?;
1680 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1681 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1682 "Object is not a sketch, it is {}",
1683 sketch_object.kind.human_friendly_kind_with_article(),
1684 ))));
1685 };
1686 let mut new_ast = self.program.ast.clone();
1688 let (sketch_block_ref, _) = self
1689 .mutate_ast(
1690 &mut new_ast,
1691 sketch_id,
1692 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1693 )
1694 .map_err(KclErrorWithOutputs::no_outputs)?;
1695 let new_source = source_from_ast(&new_ast);
1697 let (new_program, errors) = Program::parse(&new_source)
1699 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1700 if !errors.is_empty() {
1701 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1702 "Error parsing KCL source after adding point: {errors:?}"
1703 ))));
1704 }
1705 let Some(new_program) = new_program else {
1706 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1707 "No AST produced after adding point".to_string(),
1708 )));
1709 };
1710
1711 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1712 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1713 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1714 )))
1715 })?;
1716 #[cfg(not(feature = "artifact-graph"))]
1717 let _ = point_node_ref;
1718
1719 self.program = new_program.clone();
1721
1722 let mut truncated_program = new_program;
1724 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1725 .map_err(KclErrorWithOutputs::no_outputs)?;
1726
1727 let outcome = ctx
1729 .run_mock(
1730 &truncated_program,
1731 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1732 )
1733 .await?;
1734
1735 #[cfg(not(feature = "artifact-graph"))]
1736 let new_object_ids = Vec::new();
1737 #[cfg(feature = "artifact-graph")]
1738 let new_object_ids = {
1739 let make_err =
1740 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1741 let segment_id = outcome
1742 .source_range_to_object
1743 .get(&point_node_ref.range)
1744 .copied()
1745 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1746 let segment_object = outcome
1747 .scene_objects
1748 .get(segment_id.0)
1749 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1750 let ObjectKind::Segment { segment } = &segment_object.kind else {
1751 return Err(make_err(format!(
1752 "Object is not a segment, it is {}",
1753 segment_object.kind.human_friendly_kind_with_article()
1754 )));
1755 };
1756 let Segment::Point(_) = segment else {
1757 return Err(make_err(format!(
1758 "Segment is not a point, it is {}",
1759 segment.human_friendly_kind_with_article()
1760 )));
1761 };
1762 vec![segment_id]
1763 };
1764 let src_delta = SourceDelta { text: new_source };
1765 let outcome = self.update_state_after_exec(outcome, false);
1767 let scene_graph_delta = SceneGraphDelta {
1768 new_graph: self.scene_graph.clone(),
1769 invalidates_ids: false,
1770 new_objects: new_object_ids,
1771 exec_outcome: outcome,
1772 };
1773 Ok((src_delta, scene_graph_delta))
1774 }
1775
1776 async fn add_line(
1777 &mut self,
1778 ctx: &ExecutorContext,
1779 sketch: ObjectId,
1780 ctor: LineCtor,
1781 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1782 let start_ast = to_ast_point2d(&ctor.start)
1784 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1785 let end_ast = to_ast_point2d(&ctor.end)
1786 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1787 let mut arguments = vec![
1788 ast::LabeledArg {
1789 label: Some(ast::Identifier::new(LINE_START_PARAM)),
1790 arg: start_ast,
1791 },
1792 ast::LabeledArg {
1793 label: Some(ast::Identifier::new(LINE_END_PARAM)),
1794 arg: end_ast,
1795 },
1796 ];
1797 if ctor.construction == Some(true) {
1799 arguments.push(ast::LabeledArg {
1800 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1801 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1802 value: ast::LiteralValue::Bool(true),
1803 raw: "true".to_string(),
1804 digest: None,
1805 }))),
1806 });
1807 }
1808 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1809 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1810 unlabeled: None,
1811 arguments,
1812 digest: None,
1813 non_code_meta: Default::default(),
1814 })));
1815
1816 let sketch_id = sketch;
1818 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1819 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1820 })?;
1821 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1822 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1823 "Object is not a sketch, it is {}",
1824 sketch_object.kind.human_friendly_kind_with_article(),
1825 ))));
1826 };
1827 let mut new_ast = self.program.ast.clone();
1829 let (sketch_block_ref, _) = self
1830 .mutate_ast(
1831 &mut new_ast,
1832 sketch_id,
1833 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1834 )
1835 .map_err(KclErrorWithOutputs::no_outputs)?;
1836 let new_source = source_from_ast(&new_ast);
1838 let (new_program, errors) = Program::parse(&new_source)
1840 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1841 if !errors.is_empty() {
1842 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1843 "Error parsing KCL source after adding line: {errors:?}"
1844 ))));
1845 }
1846 let Some(new_program) = new_program else {
1847 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1848 "No AST produced after adding line".to_string(),
1849 )));
1850 };
1851
1852 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1853 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1854 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1855 )))
1856 })?;
1857 #[cfg(not(feature = "artifact-graph"))]
1858 let _ = line_node_ref;
1859
1860 self.program = new_program.clone();
1862
1863 let mut truncated_program = new_program;
1865 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1866 .map_err(KclErrorWithOutputs::no_outputs)?;
1867
1868 let outcome = ctx
1870 .run_mock(
1871 &truncated_program,
1872 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1873 )
1874 .await?;
1875
1876 #[cfg(not(feature = "artifact-graph"))]
1877 let new_object_ids = Vec::new();
1878 #[cfg(feature = "artifact-graph")]
1879 let new_object_ids = {
1880 let make_err =
1881 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1882 let segment_id = outcome
1883 .source_range_to_object
1884 .get(&line_node_ref.range)
1885 .copied()
1886 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1887 let segment_object = outcome
1888 .scene_object_by_id(segment_id)
1889 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1890 let ObjectKind::Segment { segment } = &segment_object.kind else {
1891 return Err(make_err(format!(
1892 "Object is not a segment, it is {}",
1893 segment_object.kind.human_friendly_kind_with_article()
1894 )));
1895 };
1896 let Segment::Line(line) = segment else {
1897 return Err(make_err(format!(
1898 "Segment is not a line, it is {}",
1899 segment.human_friendly_kind_with_article()
1900 )));
1901 };
1902 vec![line.start, line.end, segment_id]
1903 };
1904 let src_delta = SourceDelta { text: new_source };
1905 let outcome = self.update_state_after_exec(outcome, false);
1907 let scene_graph_delta = SceneGraphDelta {
1908 new_graph: self.scene_graph.clone(),
1909 invalidates_ids: false,
1910 new_objects: new_object_ids,
1911 exec_outcome: outcome,
1912 };
1913 Ok((src_delta, scene_graph_delta))
1914 }
1915
1916 async fn add_arc(
1917 &mut self,
1918 ctx: &ExecutorContext,
1919 sketch: ObjectId,
1920 ctor: ArcCtor,
1921 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1922 let start_ast = to_ast_point2d(&ctor.start)
1924 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1925 let end_ast = to_ast_point2d(&ctor.end)
1926 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1927 let center_ast = to_ast_point2d(&ctor.center)
1928 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1929 let mut arguments = vec![
1930 ast::LabeledArg {
1931 label: Some(ast::Identifier::new(ARC_START_PARAM)),
1932 arg: start_ast,
1933 },
1934 ast::LabeledArg {
1935 label: Some(ast::Identifier::new(ARC_END_PARAM)),
1936 arg: end_ast,
1937 },
1938 ast::LabeledArg {
1939 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1940 arg: center_ast,
1941 },
1942 ];
1943 if ctor.construction == Some(true) {
1945 arguments.push(ast::LabeledArg {
1946 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1947 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1948 value: ast::LiteralValue::Bool(true),
1949 raw: "true".to_string(),
1950 digest: None,
1951 }))),
1952 });
1953 }
1954 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1955 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1956 unlabeled: None,
1957 arguments,
1958 digest: None,
1959 non_code_meta: Default::default(),
1960 })));
1961
1962 let sketch_id = sketch;
1964 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1965 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1966 })?;
1967 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1968 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1969 "Object is not a sketch, it is {}",
1970 sketch_object.kind.human_friendly_kind_with_article(),
1971 ))));
1972 };
1973 let mut new_ast = self.program.ast.clone();
1975 let (sketch_block_ref, _) = self
1976 .mutate_ast(
1977 &mut new_ast,
1978 sketch_id,
1979 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1980 )
1981 .map_err(KclErrorWithOutputs::no_outputs)?;
1982 let new_source = source_from_ast(&new_ast);
1984 let (new_program, errors) = Program::parse(&new_source)
1986 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1987 if !errors.is_empty() {
1988 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1989 "Error parsing KCL source after adding arc: {errors:?}"
1990 ))));
1991 }
1992 let Some(new_program) = new_program else {
1993 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1994 "No AST produced after adding arc".to_string(),
1995 )));
1996 };
1997
1998 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1999 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2000 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2001 )))
2002 })?;
2003 #[cfg(not(feature = "artifact-graph"))]
2004 let _ = arc_node_ref;
2005
2006 self.program = new_program.clone();
2008
2009 let mut truncated_program = new_program;
2011 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2012 .map_err(KclErrorWithOutputs::no_outputs)?;
2013
2014 let outcome = ctx
2016 .run_mock(
2017 &truncated_program,
2018 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2019 )
2020 .await?;
2021
2022 #[cfg(not(feature = "artifact-graph"))]
2023 let new_object_ids = Vec::new();
2024 #[cfg(feature = "artifact-graph")]
2025 let new_object_ids = {
2026 let make_err =
2027 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2028 let segment_id = outcome
2029 .source_range_to_object
2030 .get(&arc_node_ref.range)
2031 .copied()
2032 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2033 let segment_object = outcome
2034 .scene_objects
2035 .get(segment_id.0)
2036 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2037 let ObjectKind::Segment { segment } = &segment_object.kind else {
2038 return Err(make_err(format!(
2039 "Object is not a segment, it is {}",
2040 segment_object.kind.human_friendly_kind_with_article()
2041 )));
2042 };
2043 let Segment::Arc(arc) = segment else {
2044 return Err(make_err(format!(
2045 "Segment is not an arc, it is {}",
2046 segment.human_friendly_kind_with_article()
2047 )));
2048 };
2049 vec![arc.start, arc.end, arc.center, segment_id]
2050 };
2051 let src_delta = SourceDelta { text: new_source };
2052 let outcome = self.update_state_after_exec(outcome, false);
2054 let scene_graph_delta = SceneGraphDelta {
2055 new_graph: self.scene_graph.clone(),
2056 invalidates_ids: false,
2057 new_objects: new_object_ids,
2058 exec_outcome: outcome,
2059 };
2060 Ok((src_delta, scene_graph_delta))
2061 }
2062
2063 async fn add_circle(
2064 &mut self,
2065 ctx: &ExecutorContext,
2066 sketch: ObjectId,
2067 ctor: CircleCtor,
2068 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2069 let start_ast = to_ast_point2d(&ctor.start)
2071 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2072 let center_ast = to_ast_point2d(&ctor.center)
2073 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2074 let mut arguments = vec![
2075 ast::LabeledArg {
2076 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2077 arg: start_ast,
2078 },
2079 ast::LabeledArg {
2080 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2081 arg: center_ast,
2082 },
2083 ];
2084 if ctor.construction == Some(true) {
2086 arguments.push(ast::LabeledArg {
2087 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2088 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2089 value: ast::LiteralValue::Bool(true),
2090 raw: "true".to_string(),
2091 digest: None,
2092 }))),
2093 });
2094 }
2095 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2096 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2097 unlabeled: None,
2098 arguments,
2099 digest: None,
2100 non_code_meta: Default::default(),
2101 })));
2102
2103 let sketch_id = sketch;
2105 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2106 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2107 })?;
2108 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2109 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2110 "Object is not a sketch, it is {}",
2111 sketch_object.kind.human_friendly_kind_with_article(),
2112 ))));
2113 };
2114 let mut new_ast = self.program.ast.clone();
2116 let (sketch_block_ref, _) = self
2117 .mutate_ast(
2118 &mut new_ast,
2119 sketch_id,
2120 AstMutateCommand::AddSketchBlockVarDecl {
2121 prefix: CIRCLE_VARIABLE.to_owned(),
2122 expr: circle_ast,
2123 },
2124 )
2125 .map_err(KclErrorWithOutputs::no_outputs)?;
2126 let new_source = source_from_ast(&new_ast);
2128 let (new_program, errors) = Program::parse(&new_source)
2130 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2131 if !errors.is_empty() {
2132 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2133 "Error parsing KCL source after adding circle: {errors:?}"
2134 ))));
2135 }
2136 let Some(new_program) = new_program else {
2137 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2138 "No AST produced after adding circle".to_string(),
2139 )));
2140 };
2141
2142 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2143 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2144 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2145 )))
2146 })?;
2147 #[cfg(not(feature = "artifact-graph"))]
2148 let _ = circle_node_ref;
2149
2150 self.program = new_program.clone();
2152
2153 let mut truncated_program = new_program;
2155 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2156 .map_err(KclErrorWithOutputs::no_outputs)?;
2157
2158 let outcome = ctx
2160 .run_mock(
2161 &truncated_program,
2162 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2163 )
2164 .await?;
2165
2166 #[cfg(not(feature = "artifact-graph"))]
2167 let new_object_ids = Vec::new();
2168 #[cfg(feature = "artifact-graph")]
2169 let new_object_ids = {
2170 let make_err =
2171 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2172 let segment_id = outcome
2173 .source_range_to_object
2174 .get(&circle_node_ref.range)
2175 .copied()
2176 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2177 let segment_object = outcome
2178 .scene_objects
2179 .get(segment_id.0)
2180 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2181 let ObjectKind::Segment { segment } = &segment_object.kind else {
2182 return Err(make_err(format!(
2183 "Object is not a segment, it is {}",
2184 segment_object.kind.human_friendly_kind_with_article()
2185 )));
2186 };
2187 let Segment::Circle(circle) = segment else {
2188 return Err(make_err(format!(
2189 "Segment is not a circle, it is {}",
2190 segment.human_friendly_kind_with_article()
2191 )));
2192 };
2193 vec![circle.start, circle.center, segment_id]
2194 };
2195 let src_delta = SourceDelta { text: new_source };
2196 let outcome = self.update_state_after_exec(outcome, false);
2198 let scene_graph_delta = SceneGraphDelta {
2199 new_graph: self.scene_graph.clone(),
2200 invalidates_ids: false,
2201 new_objects: new_object_ids,
2202 exec_outcome: outcome,
2203 };
2204 Ok((src_delta, scene_graph_delta))
2205 }
2206
2207 fn edit_point(
2208 &mut self,
2209 new_ast: &mut ast::Node<ast::Program>,
2210 sketch: ObjectId,
2211 point: ObjectId,
2212 ctor: PointCtor,
2213 ) -> Result<(), KclError> {
2214 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2216
2217 let sketch_id = sketch;
2219 let sketch_object = self
2220 .scene_graph
2221 .objects
2222 .get(sketch_id.0)
2223 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2224 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2225 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2226 };
2227 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2228 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2229 })?;
2230 let point_id = point;
2232 let point_object = self
2233 .scene_graph
2234 .objects
2235 .get(point_id.0)
2236 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2237 let ObjectKind::Segment {
2238 segment: Segment::Point(point),
2239 } = &point_object.kind
2240 else {
2241 return Err(KclError::refactor(format!(
2242 "Object is not a point segment: {point_object:?}"
2243 )));
2244 };
2245
2246 if let Some(owner_id) = point.owner {
2248 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2249 KclError::refactor(format!(
2250 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2251 ))
2252 })?;
2253 let ObjectKind::Segment { segment } = &owner_object.kind else {
2254 return Err(KclError::refactor(format!(
2255 "Internal: Owner of point is not a segment, but found {}",
2256 owner_object.kind.human_friendly_kind_with_article()
2257 )));
2258 };
2259
2260 if let Segment::Line(line) = segment {
2262 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2263 return Err(KclError::refactor(format!(
2264 "Internal: Owner of point does not have line ctor, but found {}",
2265 line.ctor.human_friendly_kind_with_article()
2266 )));
2267 };
2268 let mut line_ctor = line_ctor.clone();
2269 if line.start == point_id {
2271 line_ctor.start = ctor.position;
2272 } else if line.end == point_id {
2273 line_ctor.end = ctor.position;
2274 } else {
2275 return Err(KclError::refactor(format!(
2276 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2277 )));
2278 }
2279 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2280 }
2281
2282 if let Segment::Arc(arc) = segment {
2284 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2285 return Err(KclError::refactor(format!(
2286 "Internal: Owner of point does not have arc ctor, but found {}",
2287 arc.ctor.human_friendly_kind_with_article()
2288 )));
2289 };
2290 let mut arc_ctor = arc_ctor.clone();
2291 if arc.center == point_id {
2293 arc_ctor.center = ctor.position;
2294 } else if arc.start == point_id {
2295 arc_ctor.start = ctor.position;
2296 } else if arc.end == point_id {
2297 arc_ctor.end = ctor.position;
2298 } else {
2299 return Err(KclError::refactor(format!(
2300 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2301 )));
2302 }
2303 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2304 }
2305
2306 if let Segment::Circle(circle) = segment {
2308 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2309 return Err(KclError::refactor(format!(
2310 "Internal: Owner of point does not have circle ctor, but found {}",
2311 circle.ctor.human_friendly_kind_with_article()
2312 )));
2313 };
2314 let mut circle_ctor = circle_ctor.clone();
2315 if circle.center == point_id {
2316 circle_ctor.center = ctor.position;
2317 } else if circle.start == point_id {
2318 circle_ctor.start = ctor.position;
2319 } else {
2320 return Err(KclError::refactor(format!(
2321 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2322 )));
2323 }
2324 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2325 }
2326
2327 }
2330
2331 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2333 Ok(())
2334 }
2335
2336 fn edit_line(
2337 &mut self,
2338 new_ast: &mut ast::Node<ast::Program>,
2339 sketch: ObjectId,
2340 line: ObjectId,
2341 ctor: LineCtor,
2342 ) -> Result<(), KclError> {
2343 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2345 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2346
2347 let sketch_id = sketch;
2349 let sketch_object = self
2350 .scene_graph
2351 .objects
2352 .get(sketch_id.0)
2353 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2354 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2355 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2356 };
2357 sketch
2358 .segments
2359 .iter()
2360 .find(|o| **o == line)
2361 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2362 let line_id = line;
2364 let line_object = self
2365 .scene_graph
2366 .objects
2367 .get(line_id.0)
2368 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2369 let ObjectKind::Segment { .. } = &line_object.kind else {
2370 let kind = line_object.kind.human_friendly_kind_with_article();
2371 return Err(KclError::refactor(format!(
2372 "This constraint only works on Segments, but you selected {kind}"
2373 )));
2374 };
2375
2376 self.mutate_ast(
2378 new_ast,
2379 line_id,
2380 AstMutateCommand::EditLine {
2381 start: new_start_ast,
2382 end: new_end_ast,
2383 construction: ctor.construction,
2384 },
2385 )?;
2386 Ok(())
2387 }
2388
2389 fn edit_arc(
2390 &mut self,
2391 new_ast: &mut ast::Node<ast::Program>,
2392 sketch: ObjectId,
2393 arc: ObjectId,
2394 ctor: ArcCtor,
2395 ) -> Result<(), KclError> {
2396 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2398 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2399 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2400
2401 let sketch_id = sketch;
2403 let sketch_object = self
2404 .scene_graph
2405 .objects
2406 .get(sketch_id.0)
2407 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2408 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2409 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2410 };
2411 sketch
2412 .segments
2413 .iter()
2414 .find(|o| **o == arc)
2415 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2416 let arc_id = arc;
2418 let arc_object = self
2419 .scene_graph
2420 .objects
2421 .get(arc_id.0)
2422 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2423 let ObjectKind::Segment { .. } = &arc_object.kind else {
2424 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2425 };
2426
2427 self.mutate_ast(
2429 new_ast,
2430 arc_id,
2431 AstMutateCommand::EditArc {
2432 start: new_start_ast,
2433 end: new_end_ast,
2434 center: new_center_ast,
2435 construction: ctor.construction,
2436 },
2437 )?;
2438 Ok(())
2439 }
2440
2441 fn edit_circle(
2442 &mut self,
2443 new_ast: &mut ast::Node<ast::Program>,
2444 sketch: ObjectId,
2445 circle: ObjectId,
2446 ctor: CircleCtor,
2447 ) -> Result<(), KclError> {
2448 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2450 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2451
2452 let sketch_id = sketch;
2454 let sketch_object = self
2455 .scene_graph
2456 .objects
2457 .get(sketch_id.0)
2458 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2459 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2460 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2461 };
2462 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2463 KclError::refactor(format!(
2464 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2465 ))
2466 })?;
2467 let circle_id = circle;
2469 let circle_object = self
2470 .scene_graph
2471 .objects
2472 .get(circle_id.0)
2473 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2474 let ObjectKind::Segment { .. } = &circle_object.kind else {
2475 return Err(KclError::refactor(format!(
2476 "Object is not a segment: {circle_object:?}"
2477 )));
2478 };
2479
2480 self.mutate_ast(
2482 new_ast,
2483 circle_id,
2484 AstMutateCommand::EditCircle {
2485 start: new_start_ast,
2486 center: new_center_ast,
2487 construction: ctor.construction,
2488 },
2489 )?;
2490 Ok(())
2491 }
2492
2493 fn delete_segment(
2494 &mut self,
2495 new_ast: &mut ast::Node<ast::Program>,
2496 sketch: ObjectId,
2497 segment_id: ObjectId,
2498 ) -> Result<(), KclError> {
2499 let sketch_id = sketch;
2501 let sketch_object = self
2502 .scene_graph
2503 .objects
2504 .get(sketch_id.0)
2505 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2506 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2507 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2508 };
2509 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2510 KclError::refactor(format!(
2511 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2512 ))
2513 })?;
2514 let segment_object =
2516 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2517 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2518 })?;
2519 let ObjectKind::Segment { .. } = &segment_object.kind else {
2520 return Err(KclError::refactor(format!(
2521 "Object is not a segment, it is {}",
2522 segment_object.kind.human_friendly_kind_with_article()
2523 )));
2524 };
2525
2526 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2528 Ok(())
2529 }
2530
2531 fn delete_constraint(
2532 &mut self,
2533 new_ast: &mut ast::Node<ast::Program>,
2534 sketch: ObjectId,
2535 constraint_id: ObjectId,
2536 ) -> Result<(), KclError> {
2537 let sketch_id = sketch;
2539 let sketch_object = self
2540 .scene_graph
2541 .objects
2542 .get(sketch_id.0)
2543 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2544 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2545 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2546 };
2547 sketch
2548 .constraints
2549 .iter()
2550 .find(|o| **o == constraint_id)
2551 .ok_or_else(|| {
2552 KclError::refactor(format!(
2553 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2554 ))
2555 })?;
2556 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2558 KclError::refactor(format!(
2559 "Constraint not found in scene graph: constraint={constraint_id:?}"
2560 ))
2561 })?;
2562 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2563 return Err(KclError::refactor(format!(
2564 "Object is not a constraint, it is {}",
2565 constraint_object.kind.human_friendly_kind_with_article()
2566 )));
2567 };
2568
2569 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2571 Ok(())
2572 }
2573
2574 fn edit_coincident_constraint(
2575 &mut self,
2576 new_ast: &mut ast::Node<ast::Program>,
2577 constraint_id: ObjectId,
2578 segments: Vec<ConstraintSegment>,
2579 ) -> Result<(), KclError> {
2580 if segments.len() < 2 {
2581 return Err(KclError::refactor(format!(
2582 "Coincident constraint must have at least 2 inputs, got {}",
2583 segments.len()
2584 )));
2585 }
2586
2587 let segment_asts = segments
2588 .iter()
2589 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2590 .collect::<Result<Vec<_>, _>>()?;
2591
2592 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2593 elements: segment_asts,
2594 digest: None,
2595 non_code_meta: Default::default(),
2596 })));
2597
2598 self.mutate_ast(
2599 new_ast,
2600 constraint_id,
2601 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2602 )?;
2603 Ok(())
2604 }
2605
2606 fn edit_horizontal_points_constraint(
2607 &mut self,
2608 new_ast: &mut ast::Node<ast::Program>,
2609 constraint_id: ObjectId,
2610 points: Vec<ConstraintSegment>,
2611 ) -> Result<(), KclError> {
2612 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2613 }
2614
2615 fn edit_vertical_points_constraint(
2616 &mut self,
2617 new_ast: &mut ast::Node<ast::Program>,
2618 constraint_id: ObjectId,
2619 points: Vec<ConstraintSegment>,
2620 ) -> Result<(), KclError> {
2621 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2622 }
2623
2624 fn edit_axis_points_constraint(
2625 &mut self,
2626 new_ast: &mut ast::Node<ast::Program>,
2627 constraint_id: ObjectId,
2628 points: Vec<ConstraintSegment>,
2629 constraint_name: &str,
2630 ) -> Result<(), KclError> {
2631 if points.len() < 2 {
2632 return Err(KclError::refactor(format!(
2633 "{constraint_name} points constraint must have at least 2 points, got {}",
2634 points.len()
2635 )));
2636 }
2637
2638 let point_asts = points
2639 .iter()
2640 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2641 .collect::<Result<Vec<_>, _>>()?;
2642
2643 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2644 elements: point_asts,
2645 digest: None,
2646 non_code_meta: Default::default(),
2647 })));
2648
2649 self.mutate_ast(
2650 new_ast,
2651 constraint_id,
2652 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2653 )?;
2654 Ok(())
2655 }
2656
2657 fn edit_equal_length_constraint(
2659 &mut self,
2660 new_ast: &mut ast::Node<ast::Program>,
2661 constraint_id: ObjectId,
2662 lines: Vec<ObjectId>,
2663 ) -> Result<(), KclError> {
2664 if lines.len() < 2 {
2665 return Err(KclError::refactor(format!(
2666 "Lines equal length constraint must have at least 2 lines, got {}",
2667 lines.len()
2668 )));
2669 }
2670
2671 let line_asts = lines
2672 .iter()
2673 .map(|line_id| {
2674 let line_object = self
2675 .scene_graph
2676 .objects
2677 .get(line_id.0)
2678 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2679 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2680 let kind = line_object.kind.human_friendly_kind_with_article();
2681 return Err(KclError::refactor(format!(
2682 "This constraint only works on Segments, but you selected {kind}"
2683 )));
2684 };
2685 let Segment::Line(_) = line_segment else {
2686 let kind = line_segment.human_friendly_kind_with_article();
2687 return Err(KclError::refactor(format!(
2688 "Only lines can be made equal length, but you selected {kind}"
2689 )));
2690 };
2691
2692 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2693 })
2694 .collect::<Result<Vec<_>, _>>()?;
2695
2696 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2697 elements: line_asts,
2698 digest: None,
2699 non_code_meta: Default::default(),
2700 })));
2701
2702 self.mutate_ast(
2703 new_ast,
2704 constraint_id,
2705 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2706 )?;
2707 Ok(())
2708 }
2709
2710 fn edit_parallel_constraint(
2712 &mut self,
2713 new_ast: &mut ast::Node<ast::Program>,
2714 constraint_id: ObjectId,
2715 lines: Vec<ObjectId>,
2716 ) -> Result<(), KclError> {
2717 if lines.len() < 2 {
2718 return Err(KclError::refactor(format!(
2719 "Parallel constraint must have at least 2 lines, got {}",
2720 lines.len()
2721 )));
2722 }
2723
2724 let line_asts = lines
2725 .iter()
2726 .map(|line_id| {
2727 let line_object = self
2728 .scene_graph
2729 .objects
2730 .get(line_id.0)
2731 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2732 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2733 let kind = line_object.kind.human_friendly_kind_with_article();
2734 return Err(KclError::refactor(format!(
2735 "This constraint only works on Segments, but you selected {kind}"
2736 )));
2737 };
2738 let Segment::Line(_) = line_segment else {
2739 let kind = line_segment.human_friendly_kind_with_article();
2740 return Err(KclError::refactor(format!(
2741 "Only lines can be made parallel, but you selected {kind}"
2742 )));
2743 };
2744
2745 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2746 })
2747 .collect::<Result<Vec<_>, _>>()?;
2748
2749 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2750 elements: line_asts,
2751 digest: None,
2752 non_code_meta: Default::default(),
2753 })));
2754
2755 self.mutate_ast(
2756 new_ast,
2757 constraint_id,
2758 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2759 )?;
2760 Ok(())
2761 }
2762
2763 fn edit_equal_radius_constraint(
2765 &mut self,
2766 new_ast: &mut ast::Node<ast::Program>,
2767 constraint_id: ObjectId,
2768 input: Vec<ObjectId>,
2769 ) -> Result<(), KclError> {
2770 if input.len() < 2 {
2771 return Err(KclError::refactor(format!(
2772 "equalRadius constraint must have at least 2 segments, got {}",
2773 input.len()
2774 )));
2775 }
2776
2777 let input_asts = input
2778 .iter()
2779 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2780 .collect::<Result<Vec<_>, _>>()?;
2781
2782 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2783 elements: input_asts,
2784 digest: None,
2785 non_code_meta: Default::default(),
2786 })));
2787
2788 self.mutate_ast(
2789 new_ast,
2790 constraint_id,
2791 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2792 )?;
2793 Ok(())
2794 }
2795
2796 async fn execute_after_edit(
2797 &mut self,
2798 ctx: &ExecutorContext,
2799 sketch: ObjectId,
2800 sketch_block_ref: AstNodeRef,
2801 segment_ids_edited: AhashIndexSet<ObjectId>,
2802 edit_kind: EditDeleteKind,
2803 new_ast: &mut ast::Node<ast::Program>,
2804 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2805 let new_source = source_from_ast(new_ast);
2807 let (new_program, errors) = Program::parse(&new_source)
2809 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2810 if !errors.is_empty() {
2811 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2812 "Error parsing KCL source after editing: {errors:?}"
2813 ))));
2814 }
2815 let Some(new_program) = new_program else {
2816 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2817 "No AST produced after editing".to_string(),
2818 )));
2819 };
2820
2821 self.program = new_program.clone();
2823
2824 let is_delete = edit_kind.is_delete();
2826 let truncated_program = {
2827 let mut truncated_program = new_program;
2828 only_sketch_block(
2829 &mut truncated_program.ast,
2830 &sketch_block_ref,
2831 edit_kind.to_change_kind(),
2832 )
2833 .map_err(KclErrorWithOutputs::no_outputs)?;
2834 truncated_program
2835 };
2836
2837 #[cfg(not(feature = "artifact-graph"))]
2838 drop(segment_ids_edited);
2839
2840 let mock_config = MockConfig {
2842 sketch_block_id: Some(sketch),
2843 freedom_analysis: is_delete,
2844 #[cfg(feature = "artifact-graph")]
2845 segment_ids_edited: segment_ids_edited.clone(),
2846 ..Default::default()
2847 };
2848 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2849
2850 let outcome = self.update_state_after_exec(outcome, is_delete);
2852
2853 #[cfg(feature = "artifact-graph")]
2854 let new_source = {
2855 let mut new_ast = self.program.ast.clone();
2860 for (var_range, value) in &outcome.var_solutions {
2861 let rounded = value.round(3);
2862 let source_ref = SourceRef::Simple {
2863 range: *var_range,
2864 node_path: None,
2865 };
2866 mutate_ast_node_by_source_ref(
2867 &mut new_ast,
2868 &source_ref,
2869 AstMutateCommand::EditVarInitialValue { value: rounded },
2870 )
2871 .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2872 }
2873 source_from_ast(&new_ast)
2874 };
2875
2876 let src_delta = SourceDelta { text: new_source };
2877 let scene_graph_delta = SceneGraphDelta {
2878 new_graph: self.scene_graph.clone(),
2879 invalidates_ids: is_delete,
2880 new_objects: Vec::new(),
2881 exec_outcome: outcome,
2882 };
2883 Ok((src_delta, scene_graph_delta))
2884 }
2885
2886 async fn execute_after_delete_sketch(
2887 &mut self,
2888 ctx: &ExecutorContext,
2889 new_ast: &mut ast::Node<ast::Program>,
2890 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2891 let new_source = source_from_ast(new_ast);
2893 let (new_program, errors) = Program::parse(&new_source)
2895 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2896 if !errors.is_empty() {
2897 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2898 "Error parsing KCL source after editing: {errors:?}"
2899 ))));
2900 }
2901 let Some(new_program) = new_program else {
2902 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2903 "No AST produced after editing".to_string(),
2904 )));
2905 };
2906
2907 self.program = new_program.clone();
2909
2910 let outcome = ctx.run_with_caching(new_program).await?;
2916 let freedom_analysis_ran = true;
2917
2918 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2919
2920 let src_delta = SourceDelta { text: new_source };
2921 let scene_graph_delta = SceneGraphDelta {
2922 new_graph: self.scene_graph.clone(),
2923 invalidates_ids: true,
2924 new_objects: Vec::new(),
2925 exec_outcome: outcome,
2926 };
2927 Ok((src_delta, scene_graph_delta))
2928 }
2929
2930 fn point_id_to_ast_reference(
2935 &self,
2936 point_id: ObjectId,
2937 new_ast: &mut ast::Node<ast::Program>,
2938 ) -> Result<ast::Expr, KclError> {
2939 let point_object = self
2940 .scene_graph
2941 .objects
2942 .get(point_id.0)
2943 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2944 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2945 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2946 };
2947 let Segment::Point(point) = point_segment else {
2948 return Err(KclError::refactor(format!(
2949 "Only points are currently supported: {point_object:?}"
2950 )));
2951 };
2952
2953 if let Some(owner_id) = point.owner {
2954 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2955 KclError::refactor(format!(
2956 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2957 ))
2958 })?;
2959 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2960 return Err(KclError::refactor(format!(
2961 "Owner of point is not a segment, but found {}",
2962 owner_object.kind.human_friendly_kind_with_article()
2963 )));
2964 };
2965
2966 match owner_segment {
2967 Segment::Line(line) => {
2968 let property = if line.start == point_id {
2969 LINE_PROPERTY_START
2970 } else if line.end == point_id {
2971 LINE_PROPERTY_END
2972 } else {
2973 return Err(KclError::refactor(format!(
2974 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2975 )));
2976 };
2977 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
2978 }
2979 Segment::Arc(arc) => {
2980 let property = if arc.start == point_id {
2981 ARC_PROPERTY_START
2982 } else if arc.end == point_id {
2983 ARC_PROPERTY_END
2984 } else if arc.center == point_id {
2985 ARC_PROPERTY_CENTER
2986 } else {
2987 return Err(KclError::refactor(format!(
2988 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2989 )));
2990 };
2991 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
2992 }
2993 Segment::Circle(circle) => {
2994 let property = if circle.start == point_id {
2995 CIRCLE_PROPERTY_START
2996 } else if circle.center == point_id {
2997 CIRCLE_PROPERTY_CENTER
2998 } else {
2999 return Err(KclError::refactor(format!(
3000 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3001 )));
3002 };
3003 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3004 }
3005 _ => Err(KclError::refactor(format!(
3006 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3007 ))),
3008 }
3009 } else {
3010 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3012 }
3013 }
3014
3015 fn coincident_segment_to_ast(
3016 &self,
3017 segment: &ConstraintSegment,
3018 new_ast: &mut ast::Node<ast::Program>,
3019 ) -> Result<ast::Expr, KclError> {
3020 match segment {
3021 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3022 ConstraintSegment::Segment(segment_id) => {
3023 let segment_object = self
3024 .scene_graph
3025 .objects
3026 .get(segment_id.0)
3027 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3028 let ObjectKind::Segment { segment } = &segment_object.kind else {
3029 return Err(KclError::refactor(format!(
3030 "Object is not a segment, it is {}",
3031 segment_object.kind.human_friendly_kind_with_article()
3032 )));
3033 };
3034
3035 match segment {
3036 Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
3037 Segment::Line(_) => {
3038 get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None)
3039 }
3040 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3041 Segment::Circle(_) => {
3042 get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
3043 }
3044 }
3045 }
3046 }
3047 }
3048
3049 fn axis_constraint_segment_to_ast(
3050 &self,
3051 segment: &ConstraintSegment,
3052 new_ast: &mut ast::Node<ast::Program>,
3053 ) -> Result<ast::Expr, KclError> {
3054 match segment {
3055 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3056 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3057 }
3058 }
3059
3060 async fn add_coincident(
3061 &mut self,
3062 sketch: ObjectId,
3063 coincident: Coincident,
3064 new_ast: &mut ast::Node<ast::Program>,
3065 ) -> Result<AstNodeRef, KclError> {
3066 let sketch_id = sketch;
3067 let segment_asts = coincident
3068 .segments
3069 .iter()
3070 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3071 .collect::<Result<Vec<_>, _>>()?;
3072 if segment_asts.len() < 2 {
3073 return Err(KclError::refactor(format!(
3074 "Coincident constraint must have at least 2 inputs, got {}",
3075 segment_asts.len()
3076 )));
3077 }
3078
3079 let coincident_ast = create_coincident_ast(segment_asts);
3081
3082 let (sketch_block_ref, _) = self.mutate_ast(
3084 new_ast,
3085 sketch_id,
3086 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3087 )?;
3088 Ok(sketch_block_ref)
3089 }
3090
3091 async fn add_distance(
3092 &mut self,
3093 sketch: ObjectId,
3094 distance: Distance,
3095 new_ast: &mut ast::Node<ast::Program>,
3096 ) -> Result<AstNodeRef, KclError> {
3097 let sketch_id = sketch;
3098 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3099 [pt0, pt1] => [
3100 self.coincident_segment_to_ast(pt0, new_ast)?,
3101 self.coincident_segment_to_ast(pt1, new_ast)?,
3102 ],
3103 _ => {
3104 return Err(KclError::refactor(format!(
3105 "Distance constraint must have exactly 2 points, got {}",
3106 distance.points.len()
3107 )));
3108 }
3109 };
3110
3111 let arguments = match &distance.label_position {
3112 Some(label_position) => vec![ast::LabeledArg {
3113 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3114 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3115 }],
3116 None => Default::default(),
3117 };
3118
3119 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3121 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3122 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3123 ast::ArrayExpression {
3124 elements: vec![pt0_ast, pt1_ast],
3125 digest: None,
3126 non_code_meta: Default::default(),
3127 },
3128 )))),
3129 arguments,
3130 digest: None,
3131 non_code_meta: Default::default(),
3132 })));
3133 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3134 left: distance_call_ast,
3135 operator: ast::BinaryOperator::Eq,
3136 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3137 value: ast::LiteralValue::Number {
3138 value: distance.distance.value,
3139 suffix: distance.distance.units,
3140 },
3141 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3142 KclError::refactor(format!(
3143 "Could not format numeric suffix: {:?}",
3144 distance.distance.units
3145 ))
3146 })?,
3147 digest: None,
3148 }))),
3149 digest: None,
3150 })));
3151
3152 let (sketch_block_ref, _) = self.mutate_ast(
3154 new_ast,
3155 sketch_id,
3156 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3157 )?;
3158 Ok(sketch_block_ref)
3159 }
3160
3161 async fn add_angle(
3162 &mut self,
3163 sketch: ObjectId,
3164 angle: Angle,
3165 new_ast: &mut ast::Node<ast::Program>,
3166 ) -> Result<AstNodeRef, KclError> {
3167 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3168 return Err(KclError::refactor(format!(
3169 "Angle constraint must have exactly 2 lines, got {}",
3170 angle.lines.len()
3171 )));
3172 };
3173 let sketch_id = sketch;
3174
3175 let line0_object = self
3177 .scene_graph
3178 .objects
3179 .get(l0_id.0)
3180 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3181 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3182 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3183 };
3184 let Segment::Line(_) = line0_segment else {
3185 return Err(KclError::refactor(format!(
3186 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3187 )));
3188 };
3189 let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3190
3191 let line1_object = self
3192 .scene_graph
3193 .objects
3194 .get(l1_id.0)
3195 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3196 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3197 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3198 };
3199 let Segment::Line(_) = line1_segment else {
3200 return Err(KclError::refactor(format!(
3201 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3202 )));
3203 };
3204 let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3205
3206 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3208 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3209 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3210 ast::ArrayExpression {
3211 elements: vec![l0_ast, l1_ast],
3212 digest: None,
3213 non_code_meta: Default::default(),
3214 },
3215 )))),
3216 arguments: Default::default(),
3217 digest: None,
3218 non_code_meta: Default::default(),
3219 })));
3220 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3221 left: angle_call_ast,
3222 operator: ast::BinaryOperator::Eq,
3223 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3224 value: ast::LiteralValue::Number {
3225 value: angle.angle.value,
3226 suffix: angle.angle.units,
3227 },
3228 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3229 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3230 })?,
3231 digest: None,
3232 }))),
3233 digest: None,
3234 })));
3235
3236 let (sketch_block_ref, _) = self.mutate_ast(
3238 new_ast,
3239 sketch_id,
3240 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3241 )?;
3242 Ok(sketch_block_ref)
3243 }
3244
3245 async fn add_tangent(
3246 &mut self,
3247 sketch: ObjectId,
3248 tangent: Tangent,
3249 new_ast: &mut ast::Node<ast::Program>,
3250 ) -> Result<AstNodeRef, KclError> {
3251 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3252 return Err(KclError::refactor(format!(
3253 "Tangent constraint must have exactly 2 segments, got {}",
3254 tangent.input.len()
3255 )));
3256 };
3257 let sketch_id = sketch;
3258
3259 let seg0_object = self
3260 .scene_graph
3261 .objects
3262 .get(seg0_id.0)
3263 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3264 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3265 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3266 };
3267 let seg0_ast = match seg0_segment {
3268 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, LINE_VARIABLE, None)?,
3269 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, ARC_VARIABLE, None)?,
3270 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3271 _ => {
3272 return Err(KclError::refactor(format!(
3273 "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3274 )));
3275 }
3276 };
3277
3278 let seg1_object = self
3279 .scene_graph
3280 .objects
3281 .get(seg1_id.0)
3282 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3283 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3284 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3285 };
3286 let seg1_ast = match seg1_segment {
3287 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, LINE_VARIABLE, None)?,
3288 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, ARC_VARIABLE, None)?,
3289 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3290 _ => {
3291 return Err(KclError::refactor(format!(
3292 "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3293 )));
3294 }
3295 };
3296
3297 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3298 let (sketch_block_ref, _) = self.mutate_ast(
3299 new_ast,
3300 sketch_id,
3301 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3302 )?;
3303 Ok(sketch_block_ref)
3304 }
3305
3306 async fn add_symmetric(
3307 &mut self,
3308 sketch: ObjectId,
3309 symmetric: Symmetric,
3310 new_ast: &mut ast::Node<ast::Program>,
3311 ) -> Result<AstNodeRef, KclError> {
3312 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3313 return Err(KclError::refactor(format!(
3314 "Symmetric constraint must have exactly 2 inputs, got {}",
3315 symmetric.input.len()
3316 )));
3317 };
3318 let sketch_id = sketch;
3319
3320 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3321 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3322 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3323
3324 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3325 let (sketch_block_ref, _) = self.mutate_ast(
3326 new_ast,
3327 sketch_id,
3328 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3329 )?;
3330 Ok(sketch_block_ref)
3331 }
3332
3333 async fn add_midpoint(
3334 &mut self,
3335 sketch: ObjectId,
3336 midpoint: Midpoint,
3337 new_ast: &mut ast::Node<ast::Program>,
3338 ) -> Result<AstNodeRef, KclError> {
3339 let sketch_id = sketch;
3340 let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3341
3342 let segment_object = self
3343 .scene_graph
3344 .objects
3345 .get(midpoint.segment.0)
3346 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3347 let ObjectKind::Segment {
3348 segment: midpoint_segment,
3349 } = &segment_object.kind
3350 else {
3351 return Err(KclError::refactor(format!(
3352 "Object must be a segment, but it was {}",
3353 segment_object.kind.human_friendly_kind_with_article()
3354 )));
3355 };
3356 let segment_ast = match midpoint_segment {
3357 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3358 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3359 _ => {
3360 return Err(KclError::refactor(format!(
3361 "Midpoint target must be a line or arc segment but it was {}",
3362 midpoint_segment.human_friendly_kind_with_article()
3363 )));
3364 }
3365 };
3366
3367 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3368 let (sketch_block_ref, _) = self.mutate_ast(
3369 new_ast,
3370 sketch_id,
3371 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3372 )?;
3373 Ok(sketch_block_ref)
3374 }
3375
3376 async fn add_equal_radius(
3377 &mut self,
3378 sketch: ObjectId,
3379 equal_radius: EqualRadius,
3380 new_ast: &mut ast::Node<ast::Program>,
3381 ) -> Result<AstNodeRef, KclError> {
3382 if equal_radius.input.len() < 2 {
3383 return Err(KclError::refactor(format!(
3384 "equalRadius constraint must have at least 2 segments, got {}",
3385 equal_radius.input.len()
3386 )));
3387 }
3388
3389 let sketch_id = sketch;
3390 let input_asts = equal_radius
3391 .input
3392 .iter()
3393 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3394 .collect::<Result<Vec<_>, _>>()?;
3395
3396 let equal_radius_ast = create_equal_radius_ast(input_asts);
3397 let (sketch_block_ref, _) = self.mutate_ast(
3398 new_ast,
3399 sketch_id,
3400 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3401 )?;
3402 Ok(sketch_block_ref)
3403 }
3404
3405 async fn add_radius(
3406 &mut self,
3407 sketch: ObjectId,
3408 radius: Radius,
3409 new_ast: &mut ast::Node<ast::Program>,
3410 ) -> Result<AstNodeRef, KclError> {
3411 let params = ArcSizeConstraintParams {
3412 points: vec![radius.arc],
3413 function_name: RADIUS_FN,
3414 value: radius.radius.value,
3415 units: radius.radius.units,
3416 label_position: radius.label_position,
3417 constraint_type_name: "Radius",
3418 };
3419 self.add_arc_size_constraint(sketch, params, new_ast).await
3420 }
3421
3422 async fn add_diameter(
3423 &mut self,
3424 sketch: ObjectId,
3425 diameter: Diameter,
3426 new_ast: &mut ast::Node<ast::Program>,
3427 ) -> Result<AstNodeRef, KclError> {
3428 let params = ArcSizeConstraintParams {
3429 points: vec![diameter.arc],
3430 function_name: DIAMETER_FN,
3431 value: diameter.diameter.value,
3432 units: diameter.diameter.units,
3433 label_position: diameter.label_position,
3434 constraint_type_name: "Diameter",
3435 };
3436 self.add_arc_size_constraint(sketch, params, new_ast).await
3437 }
3438
3439 async fn add_fixed_constraints(
3440 &mut self,
3441 sketch: ObjectId,
3442 points: Vec<FixedPoint>,
3443 new_ast: &mut ast::Node<ast::Program>,
3444 ) -> Result<AstNodeRef, KclError> {
3445 let mut sketch_block_ref = None;
3446
3447 for fixed_point in points {
3448 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3449 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3450 .map_err(|err| KclError::refactor(err.to_string()))?;
3451
3452 let (sketch_ref, _) = self.mutate_ast(
3453 new_ast,
3454 sketch,
3455 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3456 )?;
3457 sketch_block_ref = Some(sketch_ref);
3458 }
3459
3460 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3461 }
3462
3463 async fn add_arc_size_constraint(
3464 &mut self,
3465 sketch: ObjectId,
3466 params: ArcSizeConstraintParams,
3467 new_ast: &mut ast::Node<ast::Program>,
3468 ) -> Result<AstNodeRef, KclError> {
3469 let sketch_id = sketch;
3470
3471 if params.points.len() != 1 {
3473 return Err(KclError::refactor(format!(
3474 "{} constraint must have exactly 1 argument (an arc segment), got {}",
3475 params.constraint_type_name,
3476 params.points.len()
3477 )));
3478 }
3479
3480 let arc_id = params.points[0];
3481 let arc_object = self
3482 .scene_graph
3483 .objects
3484 .get(arc_id.0)
3485 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3486 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3487 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3488 };
3489 let ref_type = match arc_segment {
3490 Segment::Arc(_) => ARC_VARIABLE,
3491 Segment::Circle(_) => CIRCLE_VARIABLE,
3492 _ => {
3493 return Err(KclError::refactor(format!(
3494 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3495 params.constraint_type_name
3496 )));
3497 }
3498 };
3499 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3501 let arguments = match ¶ms.label_position {
3502 Some(label_position) => vec![ast::LabeledArg {
3503 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3504 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3505 }],
3506 None => Default::default(),
3507 };
3508
3509 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3511 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3512 unlabeled: Some(arc_ast),
3513 arguments,
3514 digest: None,
3515 non_code_meta: Default::default(),
3516 })));
3517 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3518 left: call_ast,
3519 operator: ast::BinaryOperator::Eq,
3520 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3521 value: ast::LiteralValue::Number {
3522 value: params.value,
3523 suffix: params.units,
3524 },
3525 raw: format_number_literal(params.value, params.units, None)
3526 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3527 digest: None,
3528 }))),
3529 digest: None,
3530 })));
3531
3532 let (sketch_block_ref, _) = self.mutate_ast(
3534 new_ast,
3535 sketch_id,
3536 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3537 )?;
3538 Ok(sketch_block_ref)
3539 }
3540
3541 async fn add_horizontal_distance(
3542 &mut self,
3543 sketch: ObjectId,
3544 distance: Distance,
3545 new_ast: &mut ast::Node<ast::Program>,
3546 ) -> Result<AstNodeRef, KclError> {
3547 let sketch_id = sketch;
3548 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3549 [pt0, pt1] => [
3550 self.coincident_segment_to_ast(pt0, new_ast)?,
3551 self.coincident_segment_to_ast(pt1, new_ast)?,
3552 ],
3553 _ => {
3554 return Err(KclError::refactor(format!(
3555 "Horizontal distance constraint must have exactly 2 points, got {}",
3556 distance.points.len()
3557 )));
3558 }
3559 };
3560
3561 let arguments = match &distance.label_position {
3562 Some(label_position) => vec![ast::LabeledArg {
3563 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3564 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3565 }],
3566 None => Default::default(),
3567 };
3568
3569 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3571 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3572 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3573 ast::ArrayExpression {
3574 elements: vec![pt0_ast, pt1_ast],
3575 digest: None,
3576 non_code_meta: Default::default(),
3577 },
3578 )))),
3579 arguments,
3580 digest: None,
3581 non_code_meta: Default::default(),
3582 })));
3583 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3584 left: distance_call_ast,
3585 operator: ast::BinaryOperator::Eq,
3586 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3587 value: ast::LiteralValue::Number {
3588 value: distance.distance.value,
3589 suffix: distance.distance.units,
3590 },
3591 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3592 KclError::refactor(format!(
3593 "Could not format numeric suffix: {:?}",
3594 distance.distance.units
3595 ))
3596 })?,
3597 digest: None,
3598 }))),
3599 digest: None,
3600 })));
3601
3602 let (sketch_block_ref, _) = self.mutate_ast(
3604 new_ast,
3605 sketch_id,
3606 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3607 )?;
3608 Ok(sketch_block_ref)
3609 }
3610
3611 async fn add_vertical_distance(
3612 &mut self,
3613 sketch: ObjectId,
3614 distance: Distance,
3615 new_ast: &mut ast::Node<ast::Program>,
3616 ) -> Result<AstNodeRef, KclError> {
3617 let sketch_id = sketch;
3618 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3619 [pt0, pt1] => [
3620 self.coincident_segment_to_ast(pt0, new_ast)?,
3621 self.coincident_segment_to_ast(pt1, new_ast)?,
3622 ],
3623 _ => {
3624 return Err(KclError::refactor(format!(
3625 "Vertical distance constraint must have exactly 2 points, got {}",
3626 distance.points.len()
3627 )));
3628 }
3629 };
3630
3631 let arguments = match &distance.label_position {
3632 Some(label_position) => vec![ast::LabeledArg {
3633 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3634 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3635 }],
3636 None => Default::default(),
3637 };
3638
3639 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3641 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3642 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3643 ast::ArrayExpression {
3644 elements: vec![pt0_ast, pt1_ast],
3645 digest: None,
3646 non_code_meta: Default::default(),
3647 },
3648 )))),
3649 arguments,
3650 digest: None,
3651 non_code_meta: Default::default(),
3652 })));
3653 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3654 left: distance_call_ast,
3655 operator: ast::BinaryOperator::Eq,
3656 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3657 value: ast::LiteralValue::Number {
3658 value: distance.distance.value,
3659 suffix: distance.distance.units,
3660 },
3661 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3662 KclError::refactor(format!(
3663 "Could not format numeric suffix: {:?}",
3664 distance.distance.units
3665 ))
3666 })?,
3667 digest: None,
3668 }))),
3669 digest: None,
3670 })));
3671
3672 let (sketch_block_ref, _) = self.mutate_ast(
3674 new_ast,
3675 sketch_id,
3676 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3677 )?;
3678 Ok(sketch_block_ref)
3679 }
3680
3681 async fn add_horizontal(
3682 &mut self,
3683 sketch: ObjectId,
3684 horizontal: Horizontal,
3685 new_ast: &mut ast::Node<ast::Program>,
3686 ) -> Result<AstNodeRef, KclError> {
3687 let sketch_id = sketch;
3688
3689 let first_arg_ast = match horizontal {
3691 Horizontal::Line { line } => {
3692 let line_object = self
3693 .scene_graph
3694 .objects
3695 .get(line.0)
3696 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3697 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3698 let kind = line_object.kind.human_friendly_kind_with_article();
3699 return Err(KclError::refactor(format!(
3700 "This constraint only works on Segments, but you selected {kind}"
3701 )));
3702 };
3703 let Segment::Line(_) = line_segment else {
3704 return Err(KclError::refactor(format!(
3705 "Only lines can be made horizontal, but you selected {}",
3706 line_segment.human_friendly_kind_with_article(),
3707 )));
3708 };
3709 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3710 }
3711 Horizontal::Points { points } => {
3712 let point_asts = points
3713 .iter()
3714 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3715 .collect::<Result<Vec<_>, _>>()?;
3716 ast::ArrayExpression::new(point_asts).into()
3717 }
3718 };
3719
3720 let horizontal_ast = create_horizontal_ast(first_arg_ast);
3722
3723 let (sketch_block_ref, _) = self.mutate_ast(
3725 new_ast,
3726 sketch_id,
3727 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3728 )?;
3729 Ok(sketch_block_ref)
3730 }
3731
3732 async fn add_lines_equal_length(
3733 &mut self,
3734 sketch: ObjectId,
3735 lines_equal_length: LinesEqualLength,
3736 new_ast: &mut ast::Node<ast::Program>,
3737 ) -> Result<AstNodeRef, KclError> {
3738 if lines_equal_length.lines.len() < 2 {
3739 return Err(KclError::refactor(format!(
3740 "Lines equal length constraint must have at least 2 lines, got {}",
3741 lines_equal_length.lines.len()
3742 )));
3743 };
3744
3745 let sketch_id = sketch;
3746
3747 let line_asts = lines_equal_length
3749 .lines
3750 .iter()
3751 .map(|line_id| {
3752 let line_object = self
3753 .scene_graph
3754 .objects
3755 .get(line_id.0)
3756 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3757 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3758 let kind = line_object.kind.human_friendly_kind_with_article();
3759 return Err(KclError::refactor(format!(
3760 "This constraint only works on Segments, but you selected {kind}"
3761 )));
3762 };
3763 let Segment::Line(_) = line_segment else {
3764 let kind = line_segment.human_friendly_kind_with_article();
3765 return Err(KclError::refactor(format!(
3766 "Only lines can be made equal length, but you selected {kind}"
3767 )));
3768 };
3769
3770 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3771 })
3772 .collect::<Result<Vec<_>, _>>()?;
3773
3774 let equal_length_ast = create_equal_length_ast(line_asts);
3776
3777 let (sketch_block_ref, _) = self.mutate_ast(
3779 new_ast,
3780 sketch_id,
3781 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3782 )?;
3783 Ok(sketch_block_ref)
3784 }
3785
3786 fn equal_radius_segment_id_to_ast_reference(
3787 &mut self,
3788 segment_id: ObjectId,
3789 new_ast: &mut ast::Node<ast::Program>,
3790 ) -> Result<ast::Expr, KclError> {
3791 let segment_object = self
3792 .scene_graph
3793 .objects
3794 .get(segment_id.0)
3795 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3796 let ObjectKind::Segment { segment } = &segment_object.kind else {
3797 return Err(KclError::refactor(format!(
3798 "Object is not a segment, it was {}",
3799 segment_object.kind.human_friendly_kind_with_article()
3800 )));
3801 };
3802
3803 let ref_type = match segment {
3804 Segment::Arc(_) => ARC_VARIABLE,
3805 Segment::Circle(_) => CIRCLE_VARIABLE,
3806 _ => {
3807 return Err(KclError::refactor(format!(
3808 "equalRadius supports only arc/circle segments, got {}",
3809 segment.human_friendly_kind_with_article()
3810 )));
3811 }
3812 };
3813
3814 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3815 }
3816
3817 fn symmetric_input_id_to_ast_reference(
3818 &mut self,
3819 segment_id: ObjectId,
3820 new_ast: &mut ast::Node<ast::Program>,
3821 ) -> Result<ast::Expr, KclError> {
3822 let segment_object = self
3823 .scene_graph
3824 .objects
3825 .get(segment_id.0)
3826 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3827 let ObjectKind::Segment { segment } = &segment_object.kind else {
3828 return Err(KclError::refactor(format!(
3829 "Object is not a segment, it was {}",
3830 segment_object.kind.human_friendly_kind_with_article()
3831 )));
3832 };
3833
3834 match segment {
3835 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3836 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3837 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3838 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3839 }
3840 }
3841
3842 fn symmetric_axis_id_to_ast_reference(
3843 &mut self,
3844 segment_id: ObjectId,
3845 new_ast: &mut ast::Node<ast::Program>,
3846 ) -> Result<ast::Expr, KclError> {
3847 let segment_object = self
3848 .scene_graph
3849 .objects
3850 .get(segment_id.0)
3851 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
3852 let ObjectKind::Segment { segment } = &segment_object.kind else {
3853 return Err(KclError::refactor(format!(
3854 "Object is not a segment, it was {}",
3855 segment_object.kind.human_friendly_kind_with_article()
3856 )));
3857 };
3858 match segment {
3859 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3860 _ => Err(KclError::refactor(format!(
3861 "Symmetric axis must be a line, got {}",
3862 segment.human_friendly_kind_with_article()
3863 ))),
3864 }
3865 }
3866
3867 async fn add_parallel(
3868 &mut self,
3869 sketch: ObjectId,
3870 parallel: Parallel,
3871 new_ast: &mut ast::Node<ast::Program>,
3872 ) -> Result<AstNodeRef, KclError> {
3873 if parallel.lines.len() < 2 {
3874 return Err(KclError::refactor(format!(
3875 "Parallel constraint must have at least 2 lines, got {}",
3876 parallel.lines.len()
3877 )));
3878 };
3879
3880 let sketch_id = sketch;
3881
3882 let line_asts = parallel
3883 .lines
3884 .iter()
3885 .map(|line_id| {
3886 let line_object = self
3887 .scene_graph
3888 .objects
3889 .get(line_id.0)
3890 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3891 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3892 let kind = line_object.kind.human_friendly_kind_with_article();
3893 return Err(KclError::refactor(format!(
3894 "This constraint only works on Segments, but you selected {kind}"
3895 )));
3896 };
3897 let Segment::Line(_) = line_segment else {
3898 let kind = line_segment.human_friendly_kind_with_article();
3899 return Err(KclError::refactor(format!(
3900 "Only lines can be made parallel, but you selected {kind}"
3901 )));
3902 };
3903
3904 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3905 })
3906 .collect::<Result<Vec<_>, _>>()?;
3907
3908 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3909 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3910 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3911 ast::ArrayExpression {
3912 elements: line_asts,
3913 digest: None,
3914 non_code_meta: Default::default(),
3915 },
3916 )))),
3917 arguments: Default::default(),
3918 digest: None,
3919 non_code_meta: Default::default(),
3920 })));
3921
3922 let (sketch_block_ref, _) = self.mutate_ast(
3923 new_ast,
3924 sketch_id,
3925 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3926 )?;
3927 Ok(sketch_block_ref)
3928 }
3929
3930 async fn add_perpendicular(
3931 &mut self,
3932 sketch: ObjectId,
3933 perpendicular: Perpendicular,
3934 new_ast: &mut ast::Node<ast::Program>,
3935 ) -> Result<AstNodeRef, KclError> {
3936 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3937 .await
3938 }
3939
3940 async fn add_lines_at_angle_constraint(
3941 &mut self,
3942 sketch: ObjectId,
3943 angle_kind: LinesAtAngleKind,
3944 lines: Vec<ObjectId>,
3945 new_ast: &mut ast::Node<ast::Program>,
3946 ) -> Result<AstNodeRef, KclError> {
3947 let &[line0_id, line1_id] = lines.as_slice() else {
3948 return Err(KclError::refactor(format!(
3949 "{} constraint must have exactly 2 lines, got {}",
3950 angle_kind.to_function_name(),
3951 lines.len()
3952 )));
3953 };
3954
3955 let sketch_id = sketch;
3956
3957 let line0_object = self
3959 .scene_graph
3960 .objects
3961 .get(line0_id.0)
3962 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3963 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3964 let kind = line0_object.kind.human_friendly_kind_with_article();
3965 return Err(KclError::refactor(format!(
3966 "This constraint only works on Segments, but you selected {kind}"
3967 )));
3968 };
3969 let Segment::Line(_) = line0_segment else {
3970 return Err(KclError::refactor(format!(
3971 "Only lines can be made {}, but you selected {}",
3972 angle_kind.to_function_name(),
3973 line0_segment.human_friendly_kind_with_article(),
3974 )));
3975 };
3976 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3977
3978 let line1_object = self
3979 .scene_graph
3980 .objects
3981 .get(line1_id.0)
3982 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3983 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3984 let kind = line1_object.kind.human_friendly_kind_with_article();
3985 return Err(KclError::refactor(format!(
3986 "This constraint only works on Segments, but you selected {kind}"
3987 )));
3988 };
3989 let Segment::Line(_) = line1_segment else {
3990 return Err(KclError::refactor(format!(
3991 "Only lines can be made {}, but you selected {}",
3992 angle_kind.to_function_name(),
3993 line1_segment.human_friendly_kind_with_article(),
3994 )));
3995 };
3996 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3997
3998 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4000 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4001 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4002 ast::ArrayExpression {
4003 elements: vec![line0_ast, line1_ast],
4004 digest: None,
4005 non_code_meta: Default::default(),
4006 },
4007 )))),
4008 arguments: Default::default(),
4009 digest: None,
4010 non_code_meta: Default::default(),
4011 })));
4012
4013 let (sketch_block_ref, _) = self.mutate_ast(
4015 new_ast,
4016 sketch_id,
4017 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4018 )?;
4019 Ok(sketch_block_ref)
4020 }
4021
4022 async fn add_vertical(
4023 &mut self,
4024 sketch: ObjectId,
4025 vertical: Vertical,
4026 new_ast: &mut ast::Node<ast::Program>,
4027 ) -> Result<AstNodeRef, KclError> {
4028 let sketch_id = sketch;
4029
4030 let first_arg_ast = match vertical {
4031 Vertical::Line { line } => {
4032 let line_object = self
4034 .scene_graph
4035 .objects
4036 .get(line.0)
4037 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4038 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4039 let kind = line_object.kind.human_friendly_kind_with_article();
4040 return Err(KclError::refactor(format!(
4041 "This constraint only works on Segments, but you selected {kind}"
4042 )));
4043 };
4044 let Segment::Line(_) = line_segment else {
4045 return Err(KclError::refactor(format!(
4046 "Only lines can be made vertical, but you selected {}",
4047 line_segment.human_friendly_kind_with_article()
4048 )));
4049 };
4050 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
4051 }
4052 Vertical::Points { points } => {
4053 let point_asts = points
4054 .iter()
4055 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4056 .collect::<Result<Vec<_>, _>>()?;
4057 ast::ArrayExpression::new(point_asts).into()
4058 }
4059 };
4060
4061 let vertical_ast = create_vertical_ast(first_arg_ast);
4063
4064 let (sketch_block_ref, _) = self.mutate_ast(
4066 new_ast,
4067 sketch_id,
4068 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4069 )?;
4070 Ok(sketch_block_ref)
4071 }
4072
4073 async fn execute_after_add_constraint(
4074 &mut self,
4075 ctx: &ExecutorContext,
4076 sketch_id: ObjectId,
4077 #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_ref: AstNodeRef,
4078 new_ast: &mut ast::Node<ast::Program>,
4079 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4080 let new_source = source_from_ast(new_ast);
4082 let (new_program, errors) = Program::parse(&new_source)
4084 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4085 if !errors.is_empty() {
4086 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4087 "Error parsing KCL source after adding constraint: {errors:?}"
4088 ))));
4089 }
4090 let Some(new_program) = new_program else {
4091 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4092 "No AST produced after adding constraint".to_string(),
4093 )));
4094 };
4095 #[cfg(feature = "artifact-graph")]
4096 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4097 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4098 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4099 )))
4100 })?;
4101
4102 let mut truncated_program = new_program.clone();
4105 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4106 .map_err(KclErrorWithOutputs::no_outputs)?;
4107
4108 let outcome = ctx
4110 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4111 .await?;
4112
4113 #[cfg(not(feature = "artifact-graph"))]
4114 let new_object_ids = Vec::new();
4115 #[cfg(feature = "artifact-graph")]
4116 let new_object_ids = {
4117 let constraint_id = outcome
4119 .source_range_to_object
4120 .get(&constraint_node_ref.range)
4121 .copied()
4122 .ok_or_else(|| {
4123 KclErrorWithOutputs::from_error_outcome(
4124 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4125 outcome.clone(),
4126 )
4127 })?;
4128 vec![constraint_id]
4129 };
4130
4131 self.program = new_program;
4134
4135 let outcome = self.update_state_after_exec(outcome, true);
4137
4138 let src_delta = SourceDelta { text: new_source };
4139 let scene_graph_delta = SceneGraphDelta {
4140 new_graph: self.scene_graph.clone(),
4141 invalidates_ids: false,
4142 new_objects: new_object_ids,
4143 exec_outcome: outcome,
4144 };
4145 Ok((src_delta, scene_graph_delta))
4146 }
4147
4148 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4150 if segment_ids_set.contains(&segment_id) {
4151 return true;
4152 }
4153
4154 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4155 return false;
4156 };
4157 let ObjectKind::Segment { segment } = &segment_object.kind else {
4158 return false;
4159 };
4160 let Segment::Point(point) = segment else {
4161 return false;
4162 };
4163
4164 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4165 }
4166
4167 fn remaining_constraint_segments(
4168 &self,
4169 segments: &[ConstraintSegment],
4170 segment_ids_set: &AhashIndexSet<ObjectId>,
4171 ) -> Vec<ConstraintSegment> {
4172 segments
4173 .iter()
4174 .copied()
4175 .filter(|segment| match segment {
4176 ConstraintSegment::Origin(_) => true,
4177 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4178 })
4179 .collect()
4180 }
4181
4182 fn find_referenced_constraints(
4183 &self,
4184 sketch_id: ObjectId,
4185 segment_ids_set: &AhashIndexSet<ObjectId>,
4186 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4187 let sketch_object = self
4189 .scene_graph
4190 .objects
4191 .get(sketch_id.0)
4192 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4193 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4194 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4195 };
4196 let mut constraint_ids_set = AhashIndexSet::default();
4197 for constraint_id in &sketch.constraints {
4198 let constraint_object = self
4199 .scene_graph
4200 .objects
4201 .get(constraint_id.0)
4202 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4203 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4204 return Err(KclError::refactor(format!(
4205 "Object is not a constraint, it is {}",
4206 constraint_object.kind.human_friendly_kind_with_article()
4207 )));
4208 };
4209 let depends_on_segment = match constraint {
4210 Constraint::Coincident(c) => c
4211 .segment_ids()
4212 .any(|seg_id| self.segment_will_be_deleted(seg_id, segment_ids_set)),
4213 Constraint::Distance(d) => d
4214 .point_ids()
4215 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4216 Constraint::Fixed(fixed) => fixed
4217 .points
4218 .iter()
4219 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4220 Constraint::Radius(r) => self.segment_will_be_deleted(r.arc, segment_ids_set),
4221 Constraint::Diameter(d) => self.segment_will_be_deleted(d.arc, segment_ids_set),
4222 Constraint::EqualRadius(equal_radius) => equal_radius
4223 .input
4224 .iter()
4225 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4226 Constraint::HorizontalDistance(d) => d
4227 .point_ids()
4228 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4229 Constraint::VerticalDistance(d) => d
4230 .point_ids()
4231 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4232 Constraint::Horizontal(h) => match h {
4233 Horizontal::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4234 Horizontal::Points { points } => points.iter().any(|point| match point {
4235 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4236 ConstraintSegment::Origin(_) => false,
4237 }),
4238 },
4239 Constraint::Vertical(v) => match v {
4240 Vertical::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4241 Vertical::Points { points } => points.iter().any(|point| match point {
4242 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4243 ConstraintSegment::Origin(_) => false,
4244 }),
4245 },
4246 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4247 .lines
4248 .iter()
4249 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4250 Constraint::Midpoint(midpoint) => {
4251 self.segment_will_be_deleted(midpoint.segment, segment_ids_set)
4252 || self.segment_will_be_deleted(midpoint.point, segment_ids_set)
4253 }
4254 Constraint::Parallel(parallel) => parallel
4255 .lines
4256 .iter()
4257 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4258 Constraint::Perpendicular(perpendicular) => perpendicular
4259 .lines
4260 .iter()
4261 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4262 Constraint::Angle(angle) => angle
4263 .lines
4264 .iter()
4265 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4266 Constraint::Symmetric(symmetric) => {
4267 self.segment_will_be_deleted(symmetric.axis, segment_ids_set)
4268 || symmetric
4269 .input
4270 .iter()
4271 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set))
4272 }
4273 Constraint::Tangent(tangent) => tangent
4274 .input
4275 .iter()
4276 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4277 };
4278 if depends_on_segment {
4279 constraint_ids_set.insert(*constraint_id);
4280 }
4281 }
4282 Ok(constraint_ids_set)
4283 }
4284
4285 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4286 #[cfg(not(feature = "artifact-graph"))]
4287 {
4288 let _ = freedom_analysis_ran; outcome
4290 }
4291 #[cfg(feature = "artifact-graph")]
4292 {
4293 let mut outcome = outcome;
4294 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4295
4296 if freedom_analysis_ran {
4297 self.point_freedom_cache.clear();
4300 for new_obj in &new_objects {
4301 if let ObjectKind::Segment {
4302 segment: crate::front::Segment::Point(point),
4303 } = &new_obj.kind
4304 {
4305 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4306 }
4307 }
4308 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4309 self.scene_graph.objects = new_objects;
4311 } else {
4312 for old_obj in &self.scene_graph.objects {
4315 if let ObjectKind::Segment {
4316 segment: crate::front::Segment::Point(point),
4317 } = &old_obj.kind
4318 {
4319 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4320 }
4321 }
4322
4323 let mut updated_objects = Vec::with_capacity(new_objects.len());
4325 for new_obj in new_objects {
4326 let mut obj = new_obj;
4327 if let ObjectKind::Segment {
4328 segment: crate::front::Segment::Point(point),
4329 } = &mut obj.kind
4330 {
4331 let new_freedom = point.freedom;
4332 match new_freedom {
4338 Freedom::Free => {
4339 match self.point_freedom_cache.get(&obj.id).copied() {
4340 Some(Freedom::Conflict) => {
4341 }
4344 Some(Freedom::Fixed) => {
4345 point.freedom = Freedom::Fixed;
4347 }
4348 Some(Freedom::Free) => {
4349 }
4351 None => {
4352 }
4354 }
4355 }
4356 Freedom::Fixed => {
4357 }
4359 Freedom::Conflict => {
4360 }
4362 }
4363 self.point_freedom_cache.insert(obj.id, point.freedom);
4365 }
4366 updated_objects.push(obj);
4367 }
4368
4369 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4370 self.scene_graph.objects = updated_objects;
4371 }
4372 outcome
4373 }
4374 }
4375
4376 fn mutate_ast(
4377 &mut self,
4378 ast: &mut ast::Node<ast::Program>,
4379 object_id: ObjectId,
4380 command: AstMutateCommand,
4381 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4382 let sketch_object = self
4383 .scene_graph
4384 .objects
4385 .get(object_id.0)
4386 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4387 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4388 }
4389}
4390
4391fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4392 let sketch_object = scene_graph
4394 .objects
4395 .get(sketch_id.0)
4396 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4397 let ObjectKind::Sketch(_) = &sketch_object.kind else {
4398 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4399 };
4400 expect_single_node_ref(sketch_object)
4401}
4402
4403fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4404 match &object.source {
4405 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4406 range: *range,
4407 node_path: node_path.clone(),
4408 }),
4409 SourceRef::BackTrace { ranges } => {
4410 let [range] = ranges.as_slice() else {
4411 return Err(KclError::refactor(format!(
4412 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4413 ranges.len()
4414 )));
4415 };
4416 Ok(AstNodeRef {
4417 range: range.0,
4418 node_path: range.1.clone(),
4419 })
4420 }
4421 }
4422}
4423
4424fn only_sketch_block_from_range(
4427 ast: &mut ast::Node<ast::Program>,
4428 sketch_block_range: SourceRange,
4429 edit_kind: ChangeKind,
4430) -> Result<(), KclError> {
4431 let r1 = sketch_block_range;
4432 let matches_range = |r2: SourceRange| -> bool {
4433 match edit_kind {
4436 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4437 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4439 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4440 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4442 }
4443 };
4444 let mut found = false;
4445 for item in ast.body.iter_mut() {
4446 match item {
4447 ast::BodyItem::ImportStatement(_) => {}
4448 ast::BodyItem::ExpressionStatement(node) => {
4449 if matches_range(SourceRange::from(&*node))
4450 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4451 {
4452 sketch_block.is_being_edited = true;
4453 found = true;
4454 break;
4455 }
4456 }
4457 ast::BodyItem::VariableDeclaration(node) => {
4458 if matches_range(SourceRange::from(&node.declaration.init))
4459 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4460 {
4461 sketch_block.is_being_edited = true;
4462 found = true;
4463 break;
4464 }
4465 }
4466 ast::BodyItem::TypeDeclaration(_) => {}
4467 ast::BodyItem::ReturnStatement(node) => {
4468 if matches_range(SourceRange::from(&node.argument))
4469 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4470 {
4471 sketch_block.is_being_edited = true;
4472 found = true;
4473 break;
4474 }
4475 }
4476 }
4477 }
4478 if !found {
4479 return Err(KclError::refactor(format!(
4480 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4481 )));
4482 }
4483
4484 Ok(())
4485}
4486
4487fn only_sketch_block(
4488 ast: &mut ast::Node<ast::Program>,
4489 sketch_block_ref: &AstNodeRef,
4490 edit_kind: ChangeKind,
4491) -> Result<(), KclError> {
4492 let Some(target_node_path) = &sketch_block_ref.node_path else {
4493 #[cfg(target_arch = "wasm32")]
4494 web_sys::console::warn_1(
4495 &format!(
4496 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4497 &sketch_block_ref
4498 )
4499 .into(),
4500 );
4501 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4502 };
4503 let mut found = false;
4504 for item in ast.body.iter_mut() {
4505 match item {
4506 ast::BodyItem::ImportStatement(_) => {}
4507 ast::BodyItem::ExpressionStatement(node) => {
4508 if let Some(node_path) = &node.node_path
4510 && node_path == target_node_path
4511 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4512 {
4513 sketch_block.is_being_edited = true;
4514 found = true;
4515 break;
4516 }
4517 if let Some(node_path) = node.expression.node_path()
4519 && node_path == target_node_path
4520 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4521 {
4522 sketch_block.is_being_edited = true;
4523 found = true;
4524 break;
4525 }
4526 }
4527 ast::BodyItem::VariableDeclaration(node) => {
4528 if let Some(node_path) = node.declaration.init.node_path()
4529 && node_path == target_node_path
4530 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4531 {
4532 sketch_block.is_being_edited = true;
4533 found = true;
4534 break;
4535 }
4536 }
4537 ast::BodyItem::TypeDeclaration(_) => {}
4538 ast::BodyItem::ReturnStatement(node) => {
4539 if let Some(node_path) = node.argument.node_path()
4540 && node_path == target_node_path
4541 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4542 {
4543 sketch_block.is_being_edited = true;
4544 found = true;
4545 break;
4546 }
4547 }
4548 }
4549 }
4550 if !found {
4551 return Err(KclError::refactor(format!(
4552 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4553 )));
4554 }
4555
4556 Ok(())
4557}
4558
4559fn sketch_on_ast_expr(
4560 ast: &mut ast::Node<ast::Program>,
4561 scene_graph: &SceneGraph,
4562 on: &Plane,
4563) -> Result<ast::Expr, KclError> {
4564 match on {
4565 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4566 Plane::Object(object_id) => {
4567 let on_object = scene_graph
4568 .objects
4569 .get(object_id.0)
4570 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4571 #[cfg(feature = "artifact-graph")]
4572 {
4573 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4574 return Ok(face_expr);
4575 }
4576 }
4577 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4578 }
4579 }
4580}
4581
4582#[cfg(feature = "artifact-graph")]
4583fn sketch_face_of_scene_object_ast_expr(
4584 ast: &mut ast::Node<ast::Program>,
4585 on_object: &crate::front::Object,
4586) -> Result<Option<ast::Expr>, KclError> {
4587 let SourceRef::BackTrace { ranges } = &on_object.source else {
4588 return Ok(None);
4589 };
4590
4591 match &on_object.kind {
4592 ObjectKind::Wall(_) => {
4593 let [sweep_range, segment_range] = ranges.as_slice() else {
4594 return Err(KclError::refactor(format!(
4595 "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4596 ranges.len(),
4597 on_object.artifact_id
4598 )));
4599 };
4600 let sweep_ref = get_or_insert_ast_reference(
4601 ast,
4602 &SourceRef::Simple {
4603 range: sweep_range.0,
4604 node_path: sweep_range.1.clone(),
4605 },
4606 "solid",
4607 None,
4608 )?;
4609 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4610 return Err(KclError::refactor(format!(
4611 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4612 on_object.artifact_id
4613 )));
4614 };
4615 let solid_name = solid_name_expr.name.name.clone();
4616 let solid_expr = ast_name_expr(solid_name.clone());
4617 let segment_ref = get_or_insert_ast_reference(
4618 ast,
4619 &SourceRef::Simple {
4620 range: segment_range.0,
4621 node_path: segment_range.1.clone(),
4622 },
4623 LINE_VARIABLE,
4624 None,
4625 )?;
4626
4627 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4628 let ast::Expr::Name(segment_name_expr) = segment_ref else {
4629 return Err(KclError::refactor(format!(
4630 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4631 on_object.artifact_id
4632 )));
4633 };
4634 create_member_expression(
4635 create_member_expression(ast_name_expr(region_name), "tags"),
4636 &segment_name_expr.name.name,
4637 )
4638 } else {
4639 segment_ref
4640 };
4641
4642 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4643 }
4644 ObjectKind::Cap(cap) => {
4645 let [range] = ranges.as_slice() else {
4646 return Err(KclError::refactor(format!(
4647 "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4648 ranges.len(),
4649 on_object.artifact_id
4650 )));
4651 };
4652 let sweep_ref = get_or_insert_ast_reference(
4653 ast,
4654 &SourceRef::Simple {
4655 range: range.0,
4656 node_path: range.1.clone(),
4657 },
4658 "solid",
4659 None,
4660 )?;
4661 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4662 return Err(KclError::refactor(format!(
4663 "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4664 on_object.artifact_id
4665 )));
4666 };
4667 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4668 let face_expr = match cap.kind {
4670 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4671 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4672 };
4673
4674 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4675 }
4676 _ => Ok(None),
4677 }
4678}
4679
4680#[cfg(feature = "artifact-graph")]
4681fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4682 let mut existing_artifact_ids = scene_objects
4683 .iter()
4684 .map(|object| object.artifact_id)
4685 .collect::<HashSet<_>>();
4686
4687 for artifact in artifact_graph.values() {
4688 match artifact {
4689 Artifact::Wall(wall) => {
4690 if existing_artifact_ids.contains(&wall.id) {
4691 continue;
4692 }
4693
4694 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4695 Artifact::Segment(segment) => Some(segment),
4696 _ => None,
4697 }) else {
4698 continue;
4699 };
4700 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4701 Artifact::Sweep(sweep) => Some(sweep),
4702 _ => None,
4703 }) else {
4704 continue;
4705 };
4706 let source_segment = segment
4707 .original_seg_id
4708 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4709 .and_then(|artifact| match artifact {
4710 Artifact::Segment(segment) => Some(segment),
4711 _ => None,
4712 })
4713 .unwrap_or(segment);
4714 let id = ObjectId(scene_objects.len());
4715 scene_objects.push(crate::front::Object {
4716 id,
4717 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4718 label: Default::default(),
4719 comments: Default::default(),
4720 artifact_id: wall.id,
4721 source: SourceRef::BackTrace {
4722 ranges: vec![
4723 (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4724 (
4725 source_segment.code_ref.range,
4726 Some(source_segment.code_ref.node_path.clone()),
4727 ),
4728 ],
4729 },
4730 });
4731 existing_artifact_ids.insert(wall.id);
4732 }
4733 Artifact::Cap(cap) => {
4734 if existing_artifact_ids.contains(&cap.id) {
4735 continue;
4736 }
4737
4738 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4739 Artifact::Sweep(sweep) => Some(sweep),
4740 _ => None,
4741 }) else {
4742 continue;
4743 };
4744 let id = ObjectId(scene_objects.len());
4745 let kind = match cap.sub_type {
4746 CapSubType::Start => crate::frontend::api::CapKind::Start,
4747 CapSubType::End => crate::frontend::api::CapKind::End,
4748 };
4749 scene_objects.push(crate::front::Object {
4750 id,
4751 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4752 label: Default::default(),
4753 comments: Default::default(),
4754 artifact_id: cap.id,
4755 source: SourceRef::BackTrace {
4756 ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4757 },
4758 });
4759 existing_artifact_ids.insert(cap.id);
4760 }
4761 _ => {}
4762 }
4763 }
4764}
4765
4766fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4767 use crate::engine::PlaneName;
4768
4769 match name {
4770 PlaneName::Xy => ast_name_expr("XY".to_owned()),
4771 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4772 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4773 PlaneName::NegXy => negated_plane_ast_expr("XY"),
4774 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4775 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4776 }
4777}
4778
4779fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4780 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4781 ast::UnaryOperator::Neg,
4782 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4783 )))
4784}
4785
4786#[cfg(feature = "artifact-graph")]
4787fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4788 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4789 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4790 unlabeled: Some(solid_expr),
4791 arguments: vec![ast::LabeledArg {
4792 label: Some(ast::Identifier::new("face")),
4793 arg: face_expr,
4794 }],
4795 digest: None,
4796 non_code_meta: Default::default(),
4797 })))
4798}
4799
4800#[cfg(feature = "artifact-graph")]
4801fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4802 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4803 return None;
4804 };
4805 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4806 return None;
4807 };
4808 if !matches!(
4809 sweep_call.callee.name.name.as_str(),
4810 "extrude" | "revolve" | "sweep" | "loft"
4811 ) {
4812 return None;
4813 }
4814 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4815 return None;
4816 };
4817 let candidate = region_name_expr.name.name.clone();
4818 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4819 return None;
4820 };
4821 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
4822 return None;
4823 };
4824 if region_call.callee.name.name != "region" {
4825 return None;
4826 }
4827 Some(candidate)
4828}
4829
4830fn get_or_insert_ast_reference(
4837 ast: &mut ast::Node<ast::Program>,
4838 source_ref: &SourceRef,
4839 prefix: &str,
4840 property: Option<&str>,
4841) -> Result<ast::Expr, KclError> {
4842 let command = AstMutateCommand::AddVariableDeclaration {
4843 prefix: prefix.to_owned(),
4844 };
4845 let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
4846 let AstMutateCommandReturn::Name(var_name) = ret else {
4847 return Err(KclError::refactor(
4848 "Expected variable name returned from AddVariableDeclaration".to_owned(),
4849 ));
4850 };
4851 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4852 let Some(property) = property else {
4853 return Ok(var_expr);
4855 };
4856
4857 Ok(create_member_expression(var_expr, property))
4858}
4859
4860fn mutate_ast_node_by_source_ref(
4861 ast: &mut ast::Node<ast::Program>,
4862 source_ref: &SourceRef,
4863 command: AstMutateCommand,
4864) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4865 let (source_range, node_path) = match source_ref {
4866 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
4867 SourceRef::BackTrace { ranges } => {
4868 let [range] = ranges.as_slice() else {
4869 return Err(KclError::refactor(format!(
4870 "Expected single source ref, got {}; ranges={ranges:#?}",
4871 ranges.len(),
4872 )));
4873 };
4874 (range.0, range.1.clone())
4875 }
4876 };
4877 let mut context = AstMutateContext {
4878 source_range,
4879 node_path,
4880 command,
4881 defined_names_stack: Default::default(),
4882 };
4883 let control = dfs_mut(ast, &mut context);
4884 match control {
4885 ControlFlow::Continue(_) => Err(KclError::refactor(
4886 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
4887 )),
4888 ControlFlow::Break(break_value) => break_value,
4889 }
4890}
4891
4892#[derive(Debug)]
4893struct AstMutateContext {
4894 source_range: SourceRange,
4895 node_path: Option<ast::NodePath>,
4896 command: AstMutateCommand,
4897 defined_names_stack: Vec<HashSet<String>>,
4898}
4899
4900#[derive(Debug)]
4901#[allow(clippy::large_enum_variant)]
4902enum AstMutateCommand {
4903 AddSketchBlockExprStmt {
4905 expr: ast::Expr,
4906 },
4907 AddSketchBlockVarDecl {
4909 prefix: String,
4910 expr: ast::Expr,
4911 },
4912 AddVariableDeclaration {
4913 prefix: String,
4914 },
4915 EditPoint {
4916 at: ast::Expr,
4917 },
4918 EditLine {
4919 start: ast::Expr,
4920 end: ast::Expr,
4921 construction: Option<bool>,
4922 },
4923 EditArc {
4924 start: ast::Expr,
4925 end: ast::Expr,
4926 center: ast::Expr,
4927 construction: Option<bool>,
4928 },
4929 EditCircle {
4930 start: ast::Expr,
4931 center: ast::Expr,
4932 construction: Option<bool>,
4933 },
4934 EditConstraintValue {
4935 value: ast::BinaryPart,
4936 },
4937 EditDistanceConstraintLabelPosition {
4938 label_position: ast::Expr,
4939 },
4940 EditCallUnlabeled {
4941 arg: ast::Expr,
4942 },
4943 #[cfg(feature = "artifact-graph")]
4944 EditVarInitialValue {
4945 value: Number,
4946 },
4947 DeleteNode,
4948}
4949
4950impl AstMutateCommand {
4951 fn needs_defined_names_stack(&self) -> bool {
4952 matches!(
4953 self,
4954 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4955 )
4956 }
4957}
4958
4959#[derive(Debug)]
4960enum AstMutateCommandReturn {
4961 None,
4962 Name(String),
4963}
4964
4965#[derive(Debug, Clone)]
4966struct AstNodeRef {
4967 range: SourceRange,
4968 node_path: Option<ast::NodePath>,
4969}
4970
4971impl<T> From<&ast::Node<T>> for AstNodeRef {
4972 fn from(value: &ast::Node<T>) -> Self {
4973 AstNodeRef {
4974 range: value.into(),
4975 node_path: value.node_path.clone(),
4976 }
4977 }
4978}
4979
4980impl From<&ast::BodyItem> for AstNodeRef {
4981 fn from(value: &ast::BodyItem) -> Self {
4982 match value {
4983 ast::BodyItem::ImportStatement(node) => AstNodeRef {
4984 range: node.into(),
4985 node_path: node.node_path.clone(),
4986 },
4987 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4988 range: node.into(),
4989 node_path: node.node_path.clone(),
4990 },
4991 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4992 range: node.into(),
4993 node_path: node.node_path.clone(),
4994 },
4995 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4996 range: node.into(),
4997 node_path: node.node_path.clone(),
4998 },
4999 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5000 range: node.into(),
5001 node_path: node.node_path.clone(),
5002 },
5003 }
5004 }
5005}
5006
5007impl From<&ast::Expr> for AstNodeRef {
5008 fn from(value: &ast::Expr) -> Self {
5009 AstNodeRef {
5010 range: SourceRange::from(value),
5011 node_path: value.node_path().cloned(),
5012 }
5013 }
5014}
5015
5016impl From<&AstMutateContext> for AstNodeRef {
5017 fn from(value: &AstMutateContext) -> Self {
5018 AstNodeRef {
5019 range: value.source_range,
5020 node_path: value.node_path.clone(),
5021 }
5022 }
5023}
5024
5025impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5026 type Error = crate::walk::AstNodeError;
5027
5028 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5029 Ok(AstNodeRef {
5030 range: SourceRange::try_from(value)?,
5031 node_path: value.try_into()?,
5032 })
5033 }
5034}
5035
5036impl From<AstNodeRef> for SourceRange {
5037 fn from(value: AstNodeRef) -> Self {
5038 value.range
5039 }
5040}
5041
5042impl Visitor for AstMutateContext {
5043 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5044 type Continue = ();
5045
5046 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5047 filter_and_process(self, node)
5048 }
5049
5050 fn finish(&mut self, node: NodeMut<'_>) {
5051 match &node {
5052 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5053 self.defined_names_stack.pop();
5054 }
5055 _ => {}
5056 }
5057 }
5058}
5059
5060fn filter_and_process(
5061 ctx: &mut AstMutateContext,
5062 node: NodeMut,
5063) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5064 let Ok(node_range) = SourceRange::try_from(&node) else {
5065 return TraversalReturn::new_continue(());
5067 };
5068 if let NodeMut::VariableDeclaration(var_decl) = &node {
5073 let expr_range = SourceRange::from(&var_decl.declaration.init);
5074 let expr_node_path = var_decl.declaration.init.node_path();
5075 if source_ref_matches(ctx, expr_range, expr_node_path) {
5076 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5077 return TraversalReturn::new_break(Ok((
5080 AstNodeRef::from(&**var_decl),
5081 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5082 )));
5083 }
5084 if let AstMutateCommand::DeleteNode = &ctx.command {
5085 return TraversalReturn {
5088 mutate_body_item: MutateBodyItem::Delete,
5089 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5090 };
5091 }
5092 }
5093 }
5094 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5097 let expr_range = SourceRange::from(&expr_stmt.expression);
5098 let expr_node_path = expr_stmt.expression.node_path();
5099 if source_ref_matches(ctx, expr_range, expr_node_path) {
5100 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5101 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5104 return TraversalReturn::new_continue(());
5105 };
5106 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5107 }
5108 if let AstMutateCommand::DeleteNode = &ctx.command {
5109 return TraversalReturn {
5112 mutate_body_item: MutateBodyItem::Delete,
5113 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5114 };
5115 }
5116 }
5117 }
5118
5119 if ctx.command.needs_defined_names_stack() {
5120 if let NodeMut::Program(program) = &node {
5121 ctx.defined_names_stack.push(find_defined_names(*program));
5122 } else if let NodeMut::SketchBlock(block) = &node {
5123 ctx.defined_names_stack.push(find_defined_names(&block.body));
5124 }
5125 }
5126
5127 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5129 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5130 return TraversalReturn::new_continue(());
5131 }
5132 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5133 return TraversalReturn::new_continue(());
5134 };
5135 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5136}
5137
5138fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5139 match &ctx.node_path {
5140 Some(target) => Some(target) == node_path,
5141 None => node_range == ctx.source_range,
5142 }
5143}
5144
5145fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5146 match &ctx.command {
5147 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5148 if let NodeMut::SketchBlock(sketch_block) = node {
5149 sketch_block
5150 .body
5151 .items
5152 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5153 inner: ast::ExpressionStatement {
5154 expression: expr.clone(),
5155 digest: None,
5156 },
5157 start: Default::default(),
5158 end: Default::default(),
5159 module_id: Default::default(),
5160 node_path: None,
5161 outer_attrs: Default::default(),
5162 pre_comments: Default::default(),
5163 comment_start: Default::default(),
5164 }));
5165 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5166 }
5167 }
5168 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5169 if let NodeMut::SketchBlock(sketch_block) = node {
5170 let empty_defined_names = HashSet::new();
5171 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5172 let Ok(name) = next_free_name(prefix, defined_names) else {
5173 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5174 };
5175 sketch_block
5176 .body
5177 .items
5178 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5179 ast::VariableDeclaration::new(
5180 ast::VariableDeclarator::new(&name, expr.clone()),
5181 ast::ItemVisibility::Default,
5182 ast::VariableKind::Const,
5183 ),
5184 ))));
5185 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5186 }
5187 }
5188 AstMutateCommand::AddVariableDeclaration { prefix } => {
5189 if let NodeMut::VariableDeclaration(inner) = node {
5190 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5191 }
5192 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5193 let empty_defined_names = HashSet::new();
5194 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5195 let Ok(name) = next_free_name(prefix, defined_names) else {
5196 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5198 };
5199 let mutate_node =
5200 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5201 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5202 ast::ItemVisibility::Default,
5203 ast::VariableKind::Const,
5204 ))));
5205 return TraversalReturn {
5206 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5207 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5208 };
5209 }
5210 }
5211 AstMutateCommand::EditPoint { at } => {
5212 if let NodeMut::CallExpressionKw(call) = node {
5213 if call.callee.name.name != POINT_FN {
5214 return TraversalReturn::new_continue(());
5215 }
5216 for labeled_arg in &mut call.arguments {
5218 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5219 labeled_arg.arg = at.clone();
5220 }
5221 }
5222 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5223 }
5224 }
5225 AstMutateCommand::EditLine {
5226 start,
5227 end,
5228 construction,
5229 } => {
5230 if let NodeMut::CallExpressionKw(call) = node {
5231 if call.callee.name.name != LINE_FN {
5232 return TraversalReturn::new_continue(());
5233 }
5234 for labeled_arg in &mut call.arguments {
5236 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5237 labeled_arg.arg = start.clone();
5238 }
5239 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5240 labeled_arg.arg = end.clone();
5241 }
5242 }
5243 if let Some(construction_value) = construction {
5245 let construction_exists = call
5246 .arguments
5247 .iter()
5248 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5249 if *construction_value {
5250 if construction_exists {
5252 for labeled_arg in &mut call.arguments {
5254 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5255 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5256 value: ast::LiteralValue::Bool(true),
5257 raw: "true".to_string(),
5258 digest: None,
5259 })));
5260 }
5261 }
5262 } else {
5263 call.arguments.push(ast::LabeledArg {
5265 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5266 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5267 value: ast::LiteralValue::Bool(true),
5268 raw: "true".to_string(),
5269 digest: None,
5270 }))),
5271 });
5272 }
5273 } else {
5274 call.arguments
5276 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5277 }
5278 }
5279 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5280 }
5281 }
5282 AstMutateCommand::EditArc {
5283 start,
5284 end,
5285 center,
5286 construction,
5287 } => {
5288 if let NodeMut::CallExpressionKw(call) = node {
5289 if call.callee.name.name != ARC_FN {
5290 return TraversalReturn::new_continue(());
5291 }
5292 for labeled_arg in &mut call.arguments {
5294 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5295 labeled_arg.arg = start.clone();
5296 }
5297 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5298 labeled_arg.arg = end.clone();
5299 }
5300 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5301 labeled_arg.arg = center.clone();
5302 }
5303 }
5304 if let Some(construction_value) = construction {
5306 let construction_exists = call
5307 .arguments
5308 .iter()
5309 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5310 if *construction_value {
5311 if construction_exists {
5313 for labeled_arg in &mut call.arguments {
5315 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5316 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5317 value: ast::LiteralValue::Bool(true),
5318 raw: "true".to_string(),
5319 digest: None,
5320 })));
5321 }
5322 }
5323 } else {
5324 call.arguments.push(ast::LabeledArg {
5326 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5327 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5328 value: ast::LiteralValue::Bool(true),
5329 raw: "true".to_string(),
5330 digest: None,
5331 }))),
5332 });
5333 }
5334 } else {
5335 call.arguments
5337 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5338 }
5339 }
5340 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5341 }
5342 }
5343 AstMutateCommand::EditCircle {
5344 start,
5345 center,
5346 construction,
5347 } => {
5348 if let NodeMut::CallExpressionKw(call) = node {
5349 if call.callee.name.name != CIRCLE_FN {
5350 return TraversalReturn::new_continue(());
5351 }
5352 for labeled_arg in &mut call.arguments {
5354 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5355 labeled_arg.arg = start.clone();
5356 }
5357 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5358 labeled_arg.arg = center.clone();
5359 }
5360 }
5361 if let Some(construction_value) = construction {
5363 let construction_exists = call
5364 .arguments
5365 .iter()
5366 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5367 if *construction_value {
5368 if construction_exists {
5369 for labeled_arg in &mut call.arguments {
5370 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5371 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5372 value: ast::LiteralValue::Bool(true),
5373 raw: "true".to_string(),
5374 digest: None,
5375 })));
5376 }
5377 }
5378 } else {
5379 call.arguments.push(ast::LabeledArg {
5380 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5381 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5382 value: ast::LiteralValue::Bool(true),
5383 raw: "true".to_string(),
5384 digest: None,
5385 }))),
5386 });
5387 }
5388 } else {
5389 call.arguments
5390 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5391 }
5392 }
5393 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5394 }
5395 }
5396 AstMutateCommand::EditConstraintValue { value } => {
5397 if let NodeMut::BinaryExpression(binary_expr) = node {
5398 let left_is_constraint = matches!(
5399 &binary_expr.left,
5400 ast::BinaryPart::CallExpressionKw(call)
5401 if matches!(
5402 call.callee.name.name.as_str(),
5403 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5404 )
5405 );
5406 if left_is_constraint {
5407 binary_expr.right = value.clone();
5408 } else {
5409 binary_expr.left = value.clone();
5410 }
5411
5412 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5413 }
5414 }
5415 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5416 if let NodeMut::BinaryExpression(binary_expr) = node {
5417 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5418 return TraversalReturn::new_continue(());
5419 };
5420 if !matches!(
5421 call.callee.name.name.as_str(),
5422 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
5423 ) {
5424 return TraversalReturn::new_continue(());
5425 }
5426
5427 if let Some(label_arg) = call
5428 .arguments
5429 .iter_mut()
5430 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5431 {
5432 label_arg.arg = label_position.clone();
5433 } else {
5434 call.arguments.push(ast::LabeledArg {
5435 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5436 arg: label_position.clone(),
5437 });
5438 }
5439
5440 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5441 }
5442 }
5443 AstMutateCommand::EditCallUnlabeled { arg } => {
5444 if let NodeMut::CallExpressionKw(call) = node {
5445 call.unlabeled = Some(arg.clone());
5446 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5447 }
5448 }
5449 #[cfg(feature = "artifact-graph")]
5450 AstMutateCommand::EditVarInitialValue { value } => {
5451 if let NodeMut::NumericLiteral(numeric_literal) = node {
5452 let Ok(literal) = to_source_number(*value) else {
5454 return TraversalReturn::new_break(Err(KclError::refactor(format!(
5455 "Could not convert number to AST literal: {:?}",
5456 *value
5457 ))));
5458 };
5459 *numeric_literal = ast::Node::no_src(literal);
5460 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5461 }
5462 }
5463 AstMutateCommand::DeleteNode => {
5464 return TraversalReturn {
5465 mutate_body_item: MutateBodyItem::Delete,
5466 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5467 };
5468 }
5469 }
5470 TraversalReturn::new_continue(())
5471}
5472
5473struct FindSketchBlockSourceRange {
5474 target_before_mutation: SourceRange,
5476 found: Cell<Option<AstNodeRef>>,
5480}
5481
5482impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5483 type Error = crate::front::Error;
5484
5485 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5486 let Ok(node_range) = SourceRange::try_from(&node) else {
5487 return Ok(true);
5488 };
5489
5490 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5491 if node_range.module_id() == self.target_before_mutation.module_id()
5492 && node_range.start() == self.target_before_mutation.start()
5493 && node_range.end() >= self.target_before_mutation.end()
5495 {
5496 self.found.set(sketch_block.body.items.last().map(|item| match item {
5497 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5501 _ => AstNodeRef::from(item),
5502 }));
5503 return Ok(false);
5504 } else {
5505 return Ok(true);
5508 }
5509 }
5510
5511 for child in node.children().iter() {
5512 if !child.visit(*self)? {
5513 return Ok(false);
5514 }
5515 }
5516
5517 Ok(true)
5518 }
5519}
5520
5521struct FindSketchBlockByNodePath {
5522 target_node_path: ast::NodePath,
5524 found: Cell<Option<AstNodeRef>>,
5528}
5529
5530impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5531 type Error = crate::front::Error;
5532
5533 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5534 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5535 return Ok(true);
5536 };
5537
5538 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5539 if let Some(node_path) = node_path
5540 && node_path == self.target_node_path
5541 {
5542 self.found.set(sketch_block.body.items.last().map(|item| match item {
5543 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5547 _ => AstNodeRef::from(item),
5548 }));
5549
5550 return Ok(false);
5551 } else {
5552 return Ok(true);
5555 }
5556 }
5557
5558 for child in node.children().iter() {
5559 if !child.visit(*self)? {
5560 return Ok(false);
5561 }
5562 }
5563
5564 Ok(true)
5565 }
5566}
5567
5568fn find_sketch_block_added_item(
5576 ast: &ast::Node<ast::Program>,
5577 sketch_block_before_mutation: &AstNodeRef,
5578) -> Result<AstNodeRef, KclError> {
5579 if let Some(node_path) = &sketch_block_before_mutation.node_path {
5580 let find = FindSketchBlockByNodePath {
5581 target_node_path: node_path.clone(),
5582 found: Cell::new(None),
5583 };
5584 let node = crate::walk::Node::from(ast);
5585 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5586 find.found.into_inner().ok_or_else(|| {
5587 KclError::refactor(format!(
5588 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5589 ))
5590 })
5591 } else {
5592 let find = FindSketchBlockSourceRange {
5594 target_before_mutation: sketch_block_before_mutation.range,
5595 found: Cell::new(None),
5596 };
5597 let node = crate::walk::Node::from(ast);
5598 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5599 find.found.into_inner().ok_or_else(|| KclError::refactor(
5600 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?"),
5601 ))
5602 }
5603}
5604
5605fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5606 ast.recast_top(&Default::default(), 0)
5608}
5609
5610pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5611 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5612 inner: ast::ArrayExpression {
5613 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5614 non_code_meta: Default::default(),
5615 digest: None,
5616 },
5617 start: Default::default(),
5618 end: Default::default(),
5619 module_id: Default::default(),
5620 node_path: None,
5621 outer_attrs: Default::default(),
5622 pre_comments: Default::default(),
5623 comment_start: Default::default(),
5624 })))
5625}
5626
5627fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
5628 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
5629 ast::ArrayExpression {
5630 elements: vec![
5631 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5632 point.x,
5633 )?)))),
5634 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5635 point.y,
5636 )?)))),
5637 ],
5638 non_code_meta: Default::default(),
5639 digest: None,
5640 },
5641 ))))
5642}
5643
5644fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5645 match expr {
5646 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5647 inner: ast::Literal::from(to_source_number(*number)?),
5648 start: Default::default(),
5649 end: Default::default(),
5650 module_id: Default::default(),
5651 node_path: None,
5652 outer_attrs: Default::default(),
5653 pre_comments: Default::default(),
5654 comment_start: Default::default(),
5655 }))),
5656 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5657 inner: ast::SketchVar {
5658 initial: Some(Box::new(ast::Node {
5659 inner: to_source_number(*number)?,
5660 start: Default::default(),
5661 end: Default::default(),
5662 module_id: Default::default(),
5663 node_path: None,
5664 outer_attrs: Default::default(),
5665 pre_comments: Default::default(),
5666 comment_start: Default::default(),
5667 })),
5668 digest: None,
5669 },
5670 start: Default::default(),
5671 end: Default::default(),
5672 module_id: Default::default(),
5673 node_path: None,
5674 outer_attrs: Default::default(),
5675 pre_comments: Default::default(),
5676 comment_start: Default::default(),
5677 }))),
5678 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5679 }
5680}
5681
5682fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5683 Ok(ast::NumericLiteral {
5684 value: number.value,
5685 suffix: number.units,
5686 raw: format_number_literal(number.value, number.units, None)?,
5687 digest: None,
5688 })
5689}
5690
5691pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5692 ast::Expr::Name(Box::new(ast_name(name)))
5693}
5694
5695fn ast_name(name: String) -> ast::Node<ast::Name> {
5696 ast::Node {
5697 inner: ast::Name {
5698 name: ast::Node {
5699 inner: ast::Identifier { name, digest: None },
5700 start: Default::default(),
5701 end: Default::default(),
5702 module_id: Default::default(),
5703 node_path: None,
5704 outer_attrs: Default::default(),
5705 pre_comments: Default::default(),
5706 comment_start: Default::default(),
5707 },
5708 path: Vec::new(),
5709 abs_path: false,
5710 digest: None,
5711 },
5712 start: Default::default(),
5713 end: Default::default(),
5714 module_id: Default::default(),
5715 node_path: None,
5716 outer_attrs: Default::default(),
5717 pre_comments: Default::default(),
5718 comment_start: Default::default(),
5719 }
5720}
5721
5722pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5723 ast::Name {
5724 name: ast::Node {
5725 inner: ast::Identifier {
5726 name: name.to_owned(),
5727 digest: None,
5728 },
5729 start: Default::default(),
5730 end: Default::default(),
5731 module_id: Default::default(),
5732 node_path: None,
5733 outer_attrs: Default::default(),
5734 pre_comments: Default::default(),
5735 comment_start: Default::default(),
5736 },
5737 path: Default::default(),
5738 abs_path: false,
5739 digest: None,
5740 }
5741}
5742
5743pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
5747 let elements = exprs.into_iter().collect::<Vec<_>>();
5748 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
5749
5750 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5752 elements,
5753 digest: None,
5754 non_code_meta: Default::default(),
5755 })));
5756
5757 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5759 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5760 unlabeled: Some(array_expr),
5761 arguments: Default::default(),
5762 digest: None,
5763 non_code_meta: Default::default(),
5764 })))
5765}
5766
5767pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5769 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5770 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5771 unlabeled: None,
5772 arguments: vec![
5773 ast::LabeledArg {
5774 label: Some(ast::Identifier::new(LINE_START_PARAM)),
5775 arg: start_ast,
5776 },
5777 ast::LabeledArg {
5778 label: Some(ast::Identifier::new(LINE_END_PARAM)),
5779 arg: end_ast,
5780 },
5781 ],
5782 digest: None,
5783 non_code_meta: Default::default(),
5784 })))
5785}
5786
5787pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5789 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5790 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5791 unlabeled: None,
5792 arguments: vec![
5793 ast::LabeledArg {
5794 label: Some(ast::Identifier::new(ARC_START_PARAM)),
5795 arg: start_ast,
5796 },
5797 ast::LabeledArg {
5798 label: Some(ast::Identifier::new(ARC_END_PARAM)),
5799 arg: end_ast,
5800 },
5801 ast::LabeledArg {
5802 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5803 arg: center_ast,
5804 },
5805 ],
5806 digest: None,
5807 non_code_meta: Default::default(),
5808 })))
5809}
5810
5811pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5813 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5814 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
5815 unlabeled: None,
5816 arguments: vec![
5817 ast::LabeledArg {
5818 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5819 arg: start_ast,
5820 },
5821 ast::LabeledArg {
5822 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5823 arg: center_ast,
5824 },
5825 ],
5826 digest: None,
5827 non_code_meta: Default::default(),
5828 })))
5829}
5830
5831pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
5833 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5834 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
5835 unlabeled: Some(line_expr),
5836 arguments: Default::default(),
5837 digest: None,
5838 non_code_meta: Default::default(),
5839 })))
5840}
5841
5842pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5844 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5845 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5846 unlabeled: Some(line_expr),
5847 arguments: Default::default(),
5848 digest: None,
5849 non_code_meta: Default::default(),
5850 })))
5851}
5852
5853pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5855 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5856 object: object_expr,
5857 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5858 name: ast::Node::no_src(ast::Identifier {
5859 name: property.to_string(),
5860 digest: None,
5861 }),
5862 path: Vec::new(),
5863 abs_path: false,
5864 digest: None,
5865 }))),
5866 computed: false,
5867 digest: None,
5868 })))
5869}
5870
5871fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5873 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5875 position.x,
5876 )?))));
5877 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5878 position.y,
5879 )?))));
5880 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5881 elements: vec![x_literal, y_literal],
5882 digest: None,
5883 non_code_meta: Default::default(),
5884 })));
5885
5886 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5888 elements: vec![point_expr, point_array],
5889 digest: None,
5890 non_code_meta: Default::default(),
5891 })));
5892
5893 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5895 ast::CallExpressionKw {
5896 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5897 unlabeled: Some(array_expr),
5898 arguments: Default::default(),
5899 digest: None,
5900 non_code_meta: Default::default(),
5901 },
5902 ))))
5903}
5904
5905pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5907 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5908 elements: line_exprs,
5909 digest: None,
5910 non_code_meta: Default::default(),
5911 })));
5912
5913 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5915 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5916 unlabeled: Some(array_expr),
5917 arguments: Default::default(),
5918 digest: None,
5919 non_code_meta: Default::default(),
5920 })))
5921}
5922
5923pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5925 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5926 elements: segment_exprs,
5927 digest: None,
5928 non_code_meta: Default::default(),
5929 })));
5930
5931 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5932 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5933 unlabeled: Some(array_expr),
5934 arguments: Default::default(),
5935 digest: None,
5936 non_code_meta: Default::default(),
5937 })))
5938}
5939
5940pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5942 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5943 elements: vec![seg1_expr, seg2_expr],
5944 digest: None,
5945 non_code_meta: Default::default(),
5946 })));
5947
5948 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5949 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5950 unlabeled: Some(array_expr),
5951 arguments: Default::default(),
5952 digest: None,
5953 non_code_meta: Default::default(),
5954 })))
5955}
5956
5957pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
5959 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5960 elements: input_exprs,
5961 digest: None,
5962 non_code_meta: Default::default(),
5963 })));
5964 let arguments = vec![ast::LabeledArg {
5965 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
5966 arg: axis_expr,
5967 }];
5968
5969 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5970 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
5971 unlabeled: Some(array_expr),
5972 arguments,
5973 digest: None,
5974 non_code_meta: Default::default(),
5975 })))
5976}
5977
5978pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
5980 let arguments = vec![ast::LabeledArg {
5981 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
5982 arg: point_expr,
5983 }];
5984
5985 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5986 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
5987 unlabeled: Some(segment_expr),
5988 arguments,
5989 digest: None,
5990 non_code_meta: Default::default(),
5991 })))
5992}
5993
5994#[cfg(all(feature = "artifact-graph", test))]
5995mod tests {
5996 use super::*;
5997 use crate::engine::PlaneName;
5998 use crate::execution::cache::SketchModeState;
5999 use crate::execution::cache::clear_mem_cache;
6000 use crate::execution::cache::read_old_memory;
6001 use crate::execution::cache::write_old_memory;
6002 use crate::front::Distance;
6003 use crate::front::Fixed;
6004 use crate::front::FixedPoint;
6005 use crate::front::Midpoint;
6006 use crate::front::Object;
6007 use crate::front::Plane;
6008 use crate::front::Sketch;
6009 use crate::front::Tangent;
6010 use crate::frontend::sketch::Vertical;
6011 use crate::pretty::NumericSuffix;
6012
6013 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6014 for object in &scene_graph.objects {
6015 if let ObjectKind::Sketch(_) = &object.kind {
6016 return Some(object);
6017 }
6018 }
6019 None
6020 }
6021
6022 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6023 for object in &scene_graph.objects {
6024 if let ObjectKind::Face(_) = &object.kind {
6025 return Some(object);
6026 }
6027 }
6028 None
6029 }
6030
6031 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6032 for object in &scene_graph.objects {
6033 if matches!(&object.kind, ObjectKind::Wall(_)) {
6034 return Some(object.id);
6035 }
6036 }
6037 None
6038 }
6039
6040 #[test]
6041 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6042 let source = "\
6043region001 = region(point = [0.1, 0.1], sketch = s)
6044extrude001 = extrude(region001, length = 5)
6045revolve001 = revolve(region001, axis = Y)
6046sweep001 = sweep(region001, path = path001)
6047loft001 = loft(region001)
6048not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6049";
6050
6051 let program = Program::parse(source).unwrap().0.unwrap();
6052
6053 assert_eq!(
6054 region_name_from_sweep_variable(&program.ast, "extrude001"),
6055 Some("region001".to_owned())
6056 );
6057 assert_eq!(
6058 region_name_from_sweep_variable(&program.ast, "revolve001"),
6059 Some("region001".to_owned())
6060 );
6061 assert_eq!(
6062 region_name_from_sweep_variable(&program.ast, "sweep001"),
6063 Some("region001".to_owned())
6064 );
6065 assert_eq!(
6066 region_name_from_sweep_variable(&program.ast, "loft001"),
6067 Some("region001".to_owned())
6068 );
6069 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6070 }
6071
6072 #[track_caller]
6073 fn expect_sketch(object: &Object) -> &Sketch {
6074 if let ObjectKind::Sketch(sketch) = &object.kind {
6075 sketch
6076 } else {
6077 panic!("Object is not a sketch: {:?}", object);
6078 }
6079 }
6080
6081 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6082 let point_object = scene_graph.objects.get(point_id.0).unwrap();
6083 let ObjectKind::Segment {
6084 segment: Segment::Point(point),
6085 } = &point_object.kind
6086 else {
6087 panic!("Object is not a point segment: {point_object:?}");
6088 };
6089 point.position.clone()
6090 }
6091
6092 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6093 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6094 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6095 }
6096
6097 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6098 LineCtor {
6099 start: Point2d {
6100 x: Expr::Number(Number { value: start_x, units }),
6101 y: Expr::Number(Number { value: start_y, units }),
6102 },
6103 end: Point2d {
6104 x: Expr::Number(Number { value: end_x, units }),
6105 y: Expr::Number(Number { value: end_y, units }),
6106 },
6107 construction: None,
6108 }
6109 }
6110
6111 async fn create_sketch_with_single_line(
6112 frontend: &mut FrontendState,
6113 ctx: &ExecutorContext,
6114 mock_ctx: &ExecutorContext,
6115 version: Version,
6116 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6117 frontend.program = Program::empty();
6118
6119 let sketch_args = SketchCtor {
6120 on: Plane::Default(PlaneName::Xy),
6121 };
6122 let (_src_delta, _scene_delta, sketch_id) = frontend
6123 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6124 .await
6125 .unwrap();
6126
6127 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6128 let (source_delta, scene_graph_delta) = frontend
6129 .add_segment(mock_ctx, version, sketch_id, segment, None)
6130 .await
6131 .unwrap();
6132 let line_id = *scene_graph_delta
6133 .new_objects
6134 .last()
6135 .expect("Expected line object id to be created");
6136
6137 (sketch_id, line_id, source_delta, scene_graph_delta)
6138 }
6139
6140 #[tokio::test(flavor = "multi_thread")]
6141 async fn test_sketch_checkpoint_round_trip_restores_state() {
6142 let mut frontend = FrontendState::new();
6143 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6144 let mock_ctx = ExecutorContext::new_mock(None).await;
6145 let version = Version(0);
6146
6147 let (sketch_id, line_id, source_delta, scene_graph_delta) =
6148 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6149
6150 let expected_source = source_delta.text.clone();
6151 let expected_scene_graph = frontend.scene_graph.clone();
6152 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6153 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6154
6155 let checkpoint_id = frontend
6156 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6157 .await
6158 .unwrap();
6159
6160 let edited_segments = vec![ExistingSegmentCtor {
6161 id: line_id,
6162 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6163 }];
6164 let (edited_source, _edited_scene) = frontend
6165 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6166 .await
6167 .unwrap();
6168 assert_ne!(edited_source.text, expected_source);
6169
6170 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6171
6172 assert_eq!(restored.source_delta.text, expected_source);
6173 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6174 assert!(restored.scene_graph_delta.invalidates_ids);
6175 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6176 assert_eq!(frontend.scene_graph, expected_scene_graph);
6177 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6178
6179 ctx.close().await;
6180 mock_ctx.close().await;
6181 }
6182
6183 #[tokio::test(flavor = "multi_thread")]
6184 async fn test_sketch_checkpoints_prune_oldest_entries() {
6185 let mut frontend = FrontendState::new();
6186 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6187 let mock_ctx = ExecutorContext::new_mock(None).await;
6188 let version = Version(0);
6189
6190 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6191 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6192
6193 let mut checkpoint_ids = Vec::new();
6194 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6195 checkpoint_ids.push(
6196 frontend
6197 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6198 .await
6199 .unwrap(),
6200 );
6201 }
6202
6203 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6204 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6205
6206 let oldest_retained = checkpoint_ids[3];
6207 assert_eq!(
6208 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6209 Some(oldest_retained)
6210 );
6211
6212 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6213 assert!(evicted_restore.is_err());
6214 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6215
6216 frontend
6217 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6218 .await
6219 .unwrap();
6220
6221 ctx.close().await;
6222 mock_ctx.close().await;
6223 }
6224
6225 #[tokio::test(flavor = "multi_thread")]
6226 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6227 let mut frontend = FrontendState::new();
6228 let missing_checkpoint = SketchCheckpointId::new(999);
6229
6230 let err = frontend
6231 .restore_sketch_checkpoint(missing_checkpoint)
6232 .await
6233 .expect_err("Expected restore to fail for missing checkpoint");
6234
6235 assert!(err.msg.contains("Sketch checkpoint not found"));
6236 }
6237
6238 #[tokio::test(flavor = "multi_thread")]
6239 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6240 let mut frontend = FrontendState::new();
6241 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6242 let mock_ctx = ExecutorContext::new_mock(None).await;
6243 let version = Version(0);
6244
6245 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6246 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6247
6248 let checkpoint_a = frontend
6249 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6250 .await
6251 .unwrap();
6252 let checkpoint_b = frontend
6253 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6254 .await
6255 .unwrap();
6256 assert_eq!(frontend.sketch_checkpoints.len(), 2);
6257
6258 frontend.clear_sketch_checkpoints();
6259 assert!(frontend.sketch_checkpoints.is_empty());
6260 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6261 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6262
6263 ctx.close().await;
6264 mock_ctx.close().await;
6265 }
6266
6267 #[tokio::test(flavor = "multi_thread")]
6268 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6269 let mut frontend = FrontendState::new();
6270 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6271 let mock_ctx = ExecutorContext::new_mock(None).await;
6272 let version = Version(0);
6273
6274 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6275 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6276 let old_source = source_delta.text.clone();
6277 let old_checkpoint = frontend
6278 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6279 .await
6280 .unwrap();
6281 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6282
6283 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
6284 .unwrap()
6285 .0
6286 .unwrap();
6287
6288 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6289 let SetProgramOutcome::Success {
6290 checkpoint_id: Some(new_checkpoint),
6291 ..
6292 } = result
6293 else {
6294 panic!("Expected Success with a fresh checkpoint baseline");
6295 };
6296
6297 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6298
6299 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6300 assert_eq!(old_restore.source_delta.text, old_source);
6301
6302 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6303 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6304
6305 ctx.close().await;
6306 mock_ctx.close().await;
6307 }
6308
6309 #[tokio::test(flavor = "multi_thread")]
6310 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6311 let mut frontend = FrontendState::new();
6312 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6313 let mock_ctx = ExecutorContext::new_mock(None).await;
6314 let version = Version(0);
6315
6316 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6317 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6318 let old_checkpoint = frontend
6319 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6320 .await
6321 .unwrap();
6322 let checkpoint_count_before = frontend.sketch_checkpoints.len();
6323
6324 let failing_program = Program::parse(
6325 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6326 )
6327 .unwrap()
6328 .0
6329 .unwrap();
6330
6331 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6332 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6333 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6334 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6335
6336 ctx.close().await;
6337 mock_ctx.close().await;
6338 }
6339
6340 #[tokio::test(flavor = "multi_thread")]
6341 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6342 let mut frontend = FrontendState::new();
6343 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6344
6345 let program = Program::parse(
6346 "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",
6347 )
6348 .unwrap()
6349 .0
6350 .unwrap();
6351 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6352 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6353 panic!("Expected successful baseline program execution");
6354 };
6355
6356 clear_mem_cache().await;
6357 assert!(read_old_memory().await.is_none());
6358
6359 let checkpoint_without_mock_memory = frontend
6360 .create_sketch_checkpoint((*exec_outcome).clone())
6361 .await
6362 .unwrap();
6363
6364 write_old_memory(SketchModeState::new_for_tests()).await;
6365 assert!(read_old_memory().await.is_some());
6366
6367 let checkpoint_with_mock_memory = frontend
6368 .create_sketch_checkpoint((*exec_outcome).clone())
6369 .await
6370 .unwrap();
6371
6372 clear_mem_cache().await;
6373 assert!(read_old_memory().await.is_none());
6374
6375 frontend
6376 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6377 .await
6378 .unwrap();
6379 assert!(read_old_memory().await.is_some());
6380
6381 frontend
6382 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6383 .await
6384 .unwrap();
6385 assert!(read_old_memory().await.is_none());
6386
6387 ctx.close().await;
6388 }
6389
6390 #[tokio::test(flavor = "multi_thread")]
6391 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6392 let source = "\
6393sketch(on = XY) {
6394 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6395}
6396
6397bad = missing_name
6398";
6399 let program = Program::parse(source).unwrap().0.unwrap();
6400
6401 let mut frontend = FrontendState::new();
6402
6403 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6404 let mock_ctx = ExecutorContext::new_mock(None).await;
6405 let version = Version(0);
6406 let project_id = ProjectId(0);
6407 let file_id = FileId(0);
6408
6409 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6410 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6411 };
6412
6413 let sketch_id = frontend
6414 .scene_graph
6415 .objects
6416 .iter()
6417 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6418 .expect("Expected sketch object from errored hack_set_program");
6419
6420 frontend
6421 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6422 .await
6423 .unwrap();
6424
6425 ctx.close().await;
6426 mock_ctx.close().await;
6427 }
6428
6429 #[tokio::test(flavor = "multi_thread")]
6430 async fn test_new_sketch_add_point_edit_point() {
6431 let program = Program::empty();
6432
6433 let mut frontend = FrontendState::new();
6434 frontend.program = program;
6435
6436 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6437 let mock_ctx = ExecutorContext::new_mock(None).await;
6438 let version = Version(0);
6439
6440 let sketch_args = SketchCtor {
6441 on: Plane::Default(PlaneName::Xy),
6442 };
6443 let (_src_delta, scene_delta, sketch_id) = frontend
6444 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6445 .await
6446 .unwrap();
6447 assert_eq!(sketch_id, ObjectId(1));
6448 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6449 let sketch_object = &scene_delta.new_graph.objects[1];
6450 assert_eq!(sketch_object.id, ObjectId(1));
6451 assert_eq!(
6452 sketch_object.kind,
6453 ObjectKind::Sketch(Sketch {
6454 args: SketchCtor {
6455 on: Plane::Default(PlaneName::Xy)
6456 },
6457 plane: ObjectId(0),
6458 segments: vec![],
6459 constraints: vec![],
6460 })
6461 );
6462 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6463
6464 let point_ctor = PointCtor {
6465 position: Point2d {
6466 x: Expr::Number(Number {
6467 value: 1.0,
6468 units: NumericSuffix::Inch,
6469 }),
6470 y: Expr::Number(Number {
6471 value: 2.0,
6472 units: NumericSuffix::Inch,
6473 }),
6474 },
6475 };
6476 let segment = SegmentCtor::Point(point_ctor);
6477 let (src_delta, scene_delta) = frontend
6478 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6479 .await
6480 .unwrap();
6481 assert_eq!(
6482 src_delta.text.as_str(),
6483 "sketch001 = sketch(on = XY) {
6484 point(at = [1in, 2in])
6485}
6486"
6487 );
6488 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6489 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6490 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6491 assert_eq!(scene_object.id.0, i);
6492 }
6493
6494 let point_id = *scene_delta.new_objects.last().unwrap();
6495
6496 let point_ctor = PointCtor {
6497 position: Point2d {
6498 x: Expr::Number(Number {
6499 value: 3.0,
6500 units: NumericSuffix::Inch,
6501 }),
6502 y: Expr::Number(Number {
6503 value: 4.0,
6504 units: NumericSuffix::Inch,
6505 }),
6506 },
6507 };
6508 let segments = vec![ExistingSegmentCtor {
6509 id: point_id,
6510 ctor: SegmentCtor::Point(point_ctor),
6511 }];
6512 let (src_delta, scene_delta) = frontend
6513 .edit_segments(&mock_ctx, version, sketch_id, segments)
6514 .await
6515 .unwrap();
6516 assert_eq!(
6517 src_delta.text.as_str(),
6518 "sketch001 = sketch(on = XY) {
6519 point(at = [3in, 4in])
6520}
6521"
6522 );
6523 assert_eq!(scene_delta.new_objects, vec![]);
6524 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6525
6526 ctx.close().await;
6527 mock_ctx.close().await;
6528 }
6529
6530 #[tokio::test(flavor = "multi_thread")]
6531 async fn test_new_sketch_add_line_edit_line() {
6532 let program = Program::empty();
6533
6534 let mut frontend = FrontendState::new();
6535 frontend.program = program;
6536
6537 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6538 let mock_ctx = ExecutorContext::new_mock(None).await;
6539 let version = Version(0);
6540
6541 let sketch_args = SketchCtor {
6542 on: Plane::Default(PlaneName::Xy),
6543 };
6544 let (_src_delta, scene_delta, sketch_id) = frontend
6545 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6546 .await
6547 .unwrap();
6548 assert_eq!(sketch_id, ObjectId(1));
6549 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6550 let sketch_object = &scene_delta.new_graph.objects[1];
6551 assert_eq!(sketch_object.id, ObjectId(1));
6552 assert_eq!(
6553 sketch_object.kind,
6554 ObjectKind::Sketch(Sketch {
6555 args: SketchCtor {
6556 on: Plane::Default(PlaneName::Xy)
6557 },
6558 plane: ObjectId(0),
6559 segments: vec![],
6560 constraints: vec![],
6561 })
6562 );
6563 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6564
6565 let line_ctor = LineCtor {
6566 start: Point2d {
6567 x: Expr::Number(Number {
6568 value: 0.0,
6569 units: NumericSuffix::Mm,
6570 }),
6571 y: Expr::Number(Number {
6572 value: 0.0,
6573 units: NumericSuffix::Mm,
6574 }),
6575 },
6576 end: Point2d {
6577 x: Expr::Number(Number {
6578 value: 10.0,
6579 units: NumericSuffix::Mm,
6580 }),
6581 y: Expr::Number(Number {
6582 value: 10.0,
6583 units: NumericSuffix::Mm,
6584 }),
6585 },
6586 construction: None,
6587 };
6588 let segment = SegmentCtor::Line(line_ctor);
6589 let (src_delta, scene_delta) = frontend
6590 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6591 .await
6592 .unwrap();
6593 assert_eq!(
6594 src_delta.text.as_str(),
6595 "sketch001 = sketch(on = XY) {
6596 line(start = [0mm, 0mm], end = [10mm, 10mm])
6597}
6598"
6599 );
6600 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6601 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6602 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6603 assert_eq!(scene_object.id.0, i);
6604 }
6605
6606 let line = *scene_delta.new_objects.last().unwrap();
6608
6609 let line_ctor = LineCtor {
6610 start: Point2d {
6611 x: Expr::Number(Number {
6612 value: 1.0,
6613 units: NumericSuffix::Mm,
6614 }),
6615 y: Expr::Number(Number {
6616 value: 2.0,
6617 units: NumericSuffix::Mm,
6618 }),
6619 },
6620 end: Point2d {
6621 x: Expr::Number(Number {
6622 value: 13.0,
6623 units: NumericSuffix::Mm,
6624 }),
6625 y: Expr::Number(Number {
6626 value: 14.0,
6627 units: NumericSuffix::Mm,
6628 }),
6629 },
6630 construction: None,
6631 };
6632 let segments = vec![ExistingSegmentCtor {
6633 id: line,
6634 ctor: SegmentCtor::Line(line_ctor),
6635 }];
6636 let (src_delta, scene_delta) = frontend
6637 .edit_segments(&mock_ctx, version, sketch_id, segments)
6638 .await
6639 .unwrap();
6640 assert_eq!(
6641 src_delta.text.as_str(),
6642 "sketch001 = sketch(on = XY) {
6643 line(start = [1mm, 2mm], end = [13mm, 14mm])
6644}
6645"
6646 );
6647 assert_eq!(scene_delta.new_objects, vec![]);
6648 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6649
6650 ctx.close().await;
6651 mock_ctx.close().await;
6652 }
6653
6654 #[tokio::test(flavor = "multi_thread")]
6655 async fn test_new_sketch_add_arc_edit_arc() {
6656 let program = Program::empty();
6657
6658 let mut frontend = FrontendState::new();
6659 frontend.program = program;
6660
6661 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6662 let mock_ctx = ExecutorContext::new_mock(None).await;
6663 let version = Version(0);
6664
6665 let sketch_args = SketchCtor {
6666 on: Plane::Default(PlaneName::Xy),
6667 };
6668 let (_src_delta, scene_delta, sketch_id) = frontend
6669 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6670 .await
6671 .unwrap();
6672 assert_eq!(sketch_id, ObjectId(1));
6673 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6674 let sketch_object = &scene_delta.new_graph.objects[1];
6675 assert_eq!(sketch_object.id, ObjectId(1));
6676 assert_eq!(
6677 sketch_object.kind,
6678 ObjectKind::Sketch(Sketch {
6679 args: SketchCtor {
6680 on: Plane::Default(PlaneName::Xy),
6681 },
6682 plane: ObjectId(0),
6683 segments: vec![],
6684 constraints: vec![],
6685 })
6686 );
6687 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6688
6689 let arc_ctor = ArcCtor {
6690 start: Point2d {
6691 x: Expr::Var(Number {
6692 value: 0.0,
6693 units: NumericSuffix::Mm,
6694 }),
6695 y: Expr::Var(Number {
6696 value: 0.0,
6697 units: NumericSuffix::Mm,
6698 }),
6699 },
6700 end: Point2d {
6701 x: Expr::Var(Number {
6702 value: 10.0,
6703 units: NumericSuffix::Mm,
6704 }),
6705 y: Expr::Var(Number {
6706 value: 10.0,
6707 units: NumericSuffix::Mm,
6708 }),
6709 },
6710 center: Point2d {
6711 x: Expr::Var(Number {
6712 value: 10.0,
6713 units: NumericSuffix::Mm,
6714 }),
6715 y: Expr::Var(Number {
6716 value: 0.0,
6717 units: NumericSuffix::Mm,
6718 }),
6719 },
6720 construction: None,
6721 };
6722 let segment = SegmentCtor::Arc(arc_ctor);
6723 let (src_delta, scene_delta) = frontend
6724 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6725 .await
6726 .unwrap();
6727 assert_eq!(
6728 src_delta.text.as_str(),
6729 "sketch001 = sketch(on = XY) {
6730 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6731}
6732"
6733 );
6734 assert_eq!(
6735 scene_delta.new_objects,
6736 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6737 );
6738 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6739 assert_eq!(scene_object.id.0, i);
6740 }
6741 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6742
6743 let arc = *scene_delta.new_objects.last().unwrap();
6745
6746 let arc_ctor = ArcCtor {
6747 start: Point2d {
6748 x: Expr::Var(Number {
6749 value: 1.0,
6750 units: NumericSuffix::Mm,
6751 }),
6752 y: Expr::Var(Number {
6753 value: 2.0,
6754 units: NumericSuffix::Mm,
6755 }),
6756 },
6757 end: Point2d {
6758 x: Expr::Var(Number {
6759 value: 13.0,
6760 units: NumericSuffix::Mm,
6761 }),
6762 y: Expr::Var(Number {
6763 value: 14.0,
6764 units: NumericSuffix::Mm,
6765 }),
6766 },
6767 center: Point2d {
6768 x: Expr::Var(Number {
6769 value: 13.0,
6770 units: NumericSuffix::Mm,
6771 }),
6772 y: Expr::Var(Number {
6773 value: 2.0,
6774 units: NumericSuffix::Mm,
6775 }),
6776 },
6777 construction: None,
6778 };
6779 let segments = vec![ExistingSegmentCtor {
6780 id: arc,
6781 ctor: SegmentCtor::Arc(arc_ctor),
6782 }];
6783 let (src_delta, scene_delta) = frontend
6784 .edit_segments(&mock_ctx, version, sketch_id, segments)
6785 .await
6786 .unwrap();
6787 assert_eq!(
6788 src_delta.text.as_str(),
6789 "sketch001 = sketch(on = XY) {
6790 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6791}
6792"
6793 );
6794 assert_eq!(scene_delta.new_objects, vec![]);
6795 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6796
6797 ctx.close().await;
6798 mock_ctx.close().await;
6799 }
6800
6801 #[tokio::test(flavor = "multi_thread")]
6802 async fn test_new_sketch_add_circle_edit_circle() {
6803 let program = Program::empty();
6804
6805 let mut frontend = FrontendState::new();
6806 frontend.program = program;
6807
6808 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6809 let mock_ctx = ExecutorContext::new_mock(None).await;
6810 let version = Version(0);
6811
6812 let sketch_args = SketchCtor {
6813 on: Plane::Default(PlaneName::Xy),
6814 };
6815 let (_src_delta, _scene_delta, sketch_id) = frontend
6816 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6817 .await
6818 .unwrap();
6819
6820 let circle_ctor = CircleCtor {
6822 start: Point2d {
6823 x: Expr::Var(Number {
6824 value: 5.0,
6825 units: NumericSuffix::Mm,
6826 }),
6827 y: Expr::Var(Number {
6828 value: 0.0,
6829 units: NumericSuffix::Mm,
6830 }),
6831 },
6832 center: Point2d {
6833 x: Expr::Var(Number {
6834 value: 0.0,
6835 units: NumericSuffix::Mm,
6836 }),
6837 y: Expr::Var(Number {
6838 value: 0.0,
6839 units: NumericSuffix::Mm,
6840 }),
6841 },
6842 construction: None,
6843 };
6844 let segment = SegmentCtor::Circle(circle_ctor);
6845 let (src_delta, scene_delta) = frontend
6846 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6847 .await
6848 .unwrap();
6849 assert_eq!(
6850 src_delta.text.as_str(),
6851 "sketch001 = sketch(on = XY) {
6852 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6853}
6854"
6855 );
6856 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6858 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6859
6860 let circle = *scene_delta.new_objects.last().unwrap();
6861
6862 let circle_ctor = CircleCtor {
6864 start: Point2d {
6865 x: Expr::Var(Number {
6866 value: 10.0,
6867 units: NumericSuffix::Mm,
6868 }),
6869 y: Expr::Var(Number {
6870 value: 0.0,
6871 units: NumericSuffix::Mm,
6872 }),
6873 },
6874 center: Point2d {
6875 x: Expr::Var(Number {
6876 value: 3.0,
6877 units: NumericSuffix::Mm,
6878 }),
6879 y: Expr::Var(Number {
6880 value: 4.0,
6881 units: NumericSuffix::Mm,
6882 }),
6883 },
6884 construction: None,
6885 };
6886 let segments = vec![ExistingSegmentCtor {
6887 id: circle,
6888 ctor: SegmentCtor::Circle(circle_ctor),
6889 }];
6890 let (src_delta, scene_delta) = frontend
6891 .edit_segments(&mock_ctx, version, sketch_id, segments)
6892 .await
6893 .unwrap();
6894 assert_eq!(
6895 src_delta.text.as_str(),
6896 "sketch001 = sketch(on = XY) {
6897 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6898}
6899"
6900 );
6901 assert_eq!(scene_delta.new_objects, vec![]);
6902 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6903
6904 ctx.close().await;
6905 mock_ctx.close().await;
6906 }
6907
6908 #[tokio::test(flavor = "multi_thread")]
6909 async fn test_delete_circle() {
6910 let initial_source = "sketch001 = sketch(on = XY) {
6911 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6912}
6913";
6914
6915 let program = Program::parse(initial_source).unwrap().0.unwrap();
6916 let mut frontend = FrontendState::new();
6917
6918 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6919 let mock_ctx = ExecutorContext::new_mock(None).await;
6920 let version = Version(0);
6921
6922 frontend.hack_set_program(&ctx, program).await.unwrap();
6923 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6924 let sketch_id = sketch_object.id;
6925 let sketch = expect_sketch(sketch_object);
6926
6927 assert_eq!(sketch.segments.len(), 3);
6929 let circle_id = sketch.segments[2];
6930
6931 let (src_delta, scene_delta) = frontend
6933 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6934 .await
6935 .unwrap();
6936 assert_eq!(
6937 src_delta.text.as_str(),
6938 "sketch001 = sketch(on = XY) {
6939}
6940"
6941 );
6942 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6943 let new_sketch = expect_sketch(new_sketch_object);
6944 assert_eq!(new_sketch.segments.len(), 0);
6945
6946 ctx.close().await;
6947 mock_ctx.close().await;
6948 }
6949
6950 #[tokio::test(flavor = "multi_thread")]
6951 async fn test_edit_circle_via_point() {
6952 let initial_source = "sketch001 = sketch(on = XY) {
6953 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6954}
6955";
6956
6957 let program = Program::parse(initial_source).unwrap().0.unwrap();
6958 let mut frontend = FrontendState::new();
6959
6960 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6961 let mock_ctx = ExecutorContext::new_mock(None).await;
6962 let version = Version(0);
6963
6964 frontend.hack_set_program(&ctx, program).await.unwrap();
6965 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6966 let sketch_id = sketch_object.id;
6967 let sketch = expect_sketch(sketch_object);
6968
6969 let circle_id = sketch
6971 .segments
6972 .iter()
6973 .copied()
6974 .find(|seg_id| {
6975 matches!(
6976 &frontend.scene_graph.objects[seg_id.0].kind,
6977 ObjectKind::Segment {
6978 segment: Segment::Circle(_)
6979 }
6980 )
6981 })
6982 .expect("Expected a circle segment in sketch");
6983 let circle_object = &frontend.scene_graph.objects[circle_id.0];
6984 let ObjectKind::Segment {
6985 segment: Segment::Circle(circle),
6986 } = &circle_object.kind
6987 else {
6988 panic!("Expected circle segment, got: {:?}", circle_object.kind);
6989 };
6990 let start_point_id = circle.start;
6991
6992 let segments = vec![ExistingSegmentCtor {
6994 id: start_point_id,
6995 ctor: SegmentCtor::Point(PointCtor {
6996 position: Point2d {
6997 x: Expr::Var(Number {
6998 value: 7.0,
6999 units: NumericSuffix::Mm,
7000 }),
7001 y: Expr::Var(Number {
7002 value: 1.0,
7003 units: NumericSuffix::Mm,
7004 }),
7005 },
7006 }),
7007 }];
7008 let (src_delta, _scene_delta) = frontend
7009 .edit_segments(&mock_ctx, version, sketch_id, segments)
7010 .await
7011 .unwrap();
7012 assert_eq!(
7013 src_delta.text.as_str(),
7014 "sketch001 = sketch(on = XY) {
7015 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7016}
7017"
7018 );
7019
7020 ctx.close().await;
7021 mock_ctx.close().await;
7022 }
7023
7024 #[tokio::test(flavor = "multi_thread")]
7025 async fn test_add_line_when_sketch_block_uses_variable() {
7026 let initial_source = "s = sketch(on = XY) {}
7027";
7028
7029 let program = Program::parse(initial_source).unwrap().0.unwrap();
7030
7031 let mut frontend = FrontendState::new();
7032
7033 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7034 let mock_ctx = ExecutorContext::new_mock(None).await;
7035 let version = Version(0);
7036
7037 frontend.hack_set_program(&ctx, program).await.unwrap();
7038 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7039 let sketch_id = sketch_object.id;
7040
7041 let line_ctor = LineCtor {
7042 start: Point2d {
7043 x: Expr::Number(Number {
7044 value: 0.0,
7045 units: NumericSuffix::Mm,
7046 }),
7047 y: Expr::Number(Number {
7048 value: 0.0,
7049 units: NumericSuffix::Mm,
7050 }),
7051 },
7052 end: Point2d {
7053 x: Expr::Number(Number {
7054 value: 10.0,
7055 units: NumericSuffix::Mm,
7056 }),
7057 y: Expr::Number(Number {
7058 value: 10.0,
7059 units: NumericSuffix::Mm,
7060 }),
7061 },
7062 construction: None,
7063 };
7064 let segment = SegmentCtor::Line(line_ctor);
7065 let (src_delta, scene_delta) = frontend
7066 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7067 .await
7068 .unwrap();
7069 assert_eq!(
7070 src_delta.text.as_str(),
7071 "s = sketch(on = XY) {
7072 line(start = [0mm, 0mm], end = [10mm, 10mm])
7073}
7074"
7075 );
7076 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7077 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7078
7079 ctx.close().await;
7080 mock_ctx.close().await;
7081 }
7082
7083 #[tokio::test(flavor = "multi_thread")]
7084 async fn test_new_sketch_add_line_delete_sketch() {
7085 let program = Program::empty();
7086
7087 let mut frontend = FrontendState::new();
7088 frontend.program = program;
7089
7090 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7091 let mock_ctx = ExecutorContext::new_mock(None).await;
7092 let version = Version(0);
7093
7094 let sketch_args = SketchCtor {
7095 on: Plane::Default(PlaneName::Xy),
7096 };
7097 let (_src_delta, scene_delta, sketch_id) = frontend
7098 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7099 .await
7100 .unwrap();
7101 assert_eq!(sketch_id, ObjectId(1));
7102 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7103 let sketch_object = &scene_delta.new_graph.objects[1];
7104 assert_eq!(sketch_object.id, ObjectId(1));
7105 assert_eq!(
7106 sketch_object.kind,
7107 ObjectKind::Sketch(Sketch {
7108 args: SketchCtor {
7109 on: Plane::Default(PlaneName::Xy)
7110 },
7111 plane: ObjectId(0),
7112 segments: vec![],
7113 constraints: vec![],
7114 })
7115 );
7116 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7117
7118 let line_ctor = LineCtor {
7119 start: Point2d {
7120 x: Expr::Number(Number {
7121 value: 0.0,
7122 units: NumericSuffix::Mm,
7123 }),
7124 y: Expr::Number(Number {
7125 value: 0.0,
7126 units: NumericSuffix::Mm,
7127 }),
7128 },
7129 end: Point2d {
7130 x: Expr::Number(Number {
7131 value: 10.0,
7132 units: NumericSuffix::Mm,
7133 }),
7134 y: Expr::Number(Number {
7135 value: 10.0,
7136 units: NumericSuffix::Mm,
7137 }),
7138 },
7139 construction: None,
7140 };
7141 let segment = SegmentCtor::Line(line_ctor);
7142 let (src_delta, scene_delta) = frontend
7143 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7144 .await
7145 .unwrap();
7146 assert_eq!(
7147 src_delta.text.as_str(),
7148 "sketch001 = sketch(on = XY) {
7149 line(start = [0mm, 0mm], end = [10mm, 10mm])
7150}
7151"
7152 );
7153 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7154
7155 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7156 assert_eq!(src_delta.text.as_str(), "");
7157 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7158
7159 ctx.close().await;
7160 mock_ctx.close().await;
7161 }
7162
7163 #[tokio::test(flavor = "multi_thread")]
7164 async fn test_delete_sketch_when_sketch_block_uses_variable() {
7165 let initial_source = "s = sketch(on = XY) {}
7166";
7167
7168 let program = Program::parse(initial_source).unwrap().0.unwrap();
7169
7170 let mut frontend = FrontendState::new();
7171
7172 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7173 let mock_ctx = ExecutorContext::new_mock(None).await;
7174 let version = Version(0);
7175
7176 frontend.hack_set_program(&ctx, program).await.unwrap();
7177 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7178 let sketch_id = sketch_object.id;
7179
7180 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7181 assert_eq!(src_delta.text.as_str(), "");
7182 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7183
7184 ctx.close().await;
7185 mock_ctx.close().await;
7186 }
7187
7188 #[tokio::test(flavor = "multi_thread")]
7189 async fn test_delete_sketch_after_comment() {
7190 let initial_source = "sketch001 = sketch(on = XZ) {
7191}
7192";
7193
7194 let program = Program::parse(initial_source).unwrap().0.unwrap();
7195 let mut frontend = FrontendState::new();
7196
7197 let ctx = ExecutorContext::new_with_engine(
7198 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7199 Default::default(),
7200 );
7201 let version = Version(0);
7202
7203 frontend.hack_set_program(&ctx, program).await.unwrap();
7204 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7205 let sketch_id = sketch_object.id;
7206 let original_source = sketch_object.source.clone();
7207
7208 let commented_source = "// test 1
7209sketch001 = sketch(on = XZ) {
7210}
7211";
7212 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7213 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7214
7215 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7216 assert_eq!(cached_sketch_object.source, original_source);
7217
7218 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7219 assert!(
7220 !src_delta.text.contains("sketch001"),
7221 "sketch was not deleted: {}",
7222 src_delta.text
7223 );
7224 assert_eq!(src_delta.text.as_str(), "// test 1\n");
7226 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7227
7228 ctx.close().await;
7229 }
7230
7231 #[tokio::test(flavor = "multi_thread")]
7232 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7233 let initial_source = "sketch001 = sketch(on = XZ) {
7234}
7235foo = 1
7236";
7237
7238 let program = Program::parse(initial_source).unwrap().0.unwrap();
7239 let mut frontend = FrontendState::new();
7240
7241 let ctx = ExecutorContext::new_with_engine(
7242 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7243 Default::default(),
7244 );
7245 let version = Version(0);
7246
7247 frontend.hack_set_program(&ctx, program).await.unwrap();
7248 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7249 let sketch_id = sketch_object.id;
7250
7251 let commented_source = "// keep me
7252sketch001 = sketch(on = XZ) {
7253}
7254foo = 1
7255";
7256 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7257 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7258
7259 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7260 assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7262
7263 ctx.close().await;
7264 }
7265
7266 #[tokio::test(flavor = "multi_thread")]
7267 async fn test_delete_segment_preserves_pre_comment() {
7268 let initial_source = "\
7269sketch(on = XY) {
7270 point(at = [var 1, var 2])
7271 // describe the middle point
7272 point(at = [var 3, var 4])
7273 point(at = [var 5, var 6])
7274}
7275";
7276
7277 let program = Program::parse(initial_source).unwrap().0.unwrap();
7278 let mut frontend = FrontendState::new();
7279
7280 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7281 let mock_ctx = ExecutorContext::new_mock(None).await;
7282 let version = Version(0);
7283
7284 frontend.hack_set_program(&ctx, program).await.unwrap();
7285 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7286 let sketch_id = sketch_object.id;
7287 let sketch = expect_sketch(sketch_object);
7288
7289 let middle_point_id = *sketch.segments.get(1).unwrap();
7290
7291 let (src_delta, _scene_delta) = frontend
7292 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7293 .await
7294 .unwrap();
7295 assert_eq!(
7298 src_delta.text.as_str(),
7299 "\
7300sketch(on = XY) {
7301 point(at = [var 1mm, var 2mm])
7302 // describe the middle point
7303 point(at = [var 5mm, var 6mm])
7304}
7305"
7306 );
7307
7308 ctx.close().await;
7309 mock_ctx.close().await;
7310 }
7311
7312 #[tokio::test(flavor = "multi_thread")]
7313 async fn test_delete_last_segment_preserves_pre_comment() {
7314 let initial_source = "\
7315sketch(on = XY) {
7316 point(at = [var 1, var 2])
7317 // describe the trailing point
7318 point(at = [var 3, var 4])
7319}
7320";
7321
7322 let program = Program::parse(initial_source).unwrap().0.unwrap();
7323 let mut frontend = FrontendState::new();
7324
7325 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7326 let mock_ctx = ExecutorContext::new_mock(None).await;
7327 let version = Version(0);
7328
7329 frontend.hack_set_program(&ctx, program).await.unwrap();
7330 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7331 let sketch_id = sketch_object.id;
7332 let sketch = expect_sketch(sketch_object);
7333
7334 let last_point_id = *sketch.segments.last().unwrap();
7335
7336 let (src_delta, _scene_delta) = frontend
7337 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7338 .await
7339 .unwrap();
7340 assert_eq!(
7343 src_delta.text.as_str(),
7344 "\
7345sketch(on = XY) {
7346 point(at = [var 1mm, var 2mm])
7347 // describe the trailing point
7348}
7349"
7350 );
7351
7352 ctx.close().await;
7353 mock_ctx.close().await;
7354 }
7355
7356 #[tokio::test(flavor = "multi_thread")]
7357 async fn test_delete_segment_drops_inline_trailing_comment() {
7358 let initial_source = "\
7359sketch(on = XY) {
7360 point(at = [var 1, var 2])
7361 point(at = [var 3, var 4]) // same-line note that gets dropped
7362 point(at = [var 5, var 6])
7363}
7364";
7365
7366 let program = Program::parse(initial_source).unwrap().0.unwrap();
7367 let mut frontend = FrontendState::new();
7368
7369 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7370 let mock_ctx = ExecutorContext::new_mock(None).await;
7371 let version = Version(0);
7372
7373 frontend.hack_set_program(&ctx, program).await.unwrap();
7374 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7375 let sketch_id = sketch_object.id;
7376 let sketch = expect_sketch(sketch_object);
7377
7378 let middle_point_id = *sketch.segments.get(1).unwrap();
7379
7380 let (src_delta, _scene_delta) = frontend
7381 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7382 .await
7383 .unwrap();
7384 assert!(
7386 !src_delta.text.contains("same-line note"),
7387 "inline comment should have been removed: {}",
7388 src_delta.text
7389 );
7390
7391 ctx.close().await;
7392 mock_ctx.close().await;
7393 }
7394
7395 #[tokio::test(flavor = "multi_thread")]
7396 async fn test_delete_segments_preserves_block_comments_across_positions() {
7397 let initial_source = "\
7405sketch(on = XY) {
7406 /* above first - moves to middle */
7407 point(at = [var 1, var 2]) /* same-line on first - dropped */
7408 /* above middle - stays */
7409 point(at = [var 3, var 4])
7410 /* above last - moves to trailing meta */
7411 point(at = [var 5, var 6])
7412}
7413";
7414
7415 let program = Program::parse(initial_source).unwrap().0.unwrap();
7416 let mut frontend = FrontendState::new();
7417
7418 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7419 let mock_ctx = ExecutorContext::new_mock(None).await;
7420 let version = Version(0);
7421
7422 frontend.hack_set_program(&ctx, program).await.unwrap();
7423 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7424 let sketch_id = sketch_object.id;
7425 let sketch = expect_sketch(sketch_object);
7426
7427 let first_point_id = *sketch.segments.first().unwrap();
7428 let last_point_id = *sketch.segments.last().unwrap();
7429
7430 let (src_delta, _scene_delta) = frontend
7431 .delete_objects(
7432 &mock_ctx,
7433 version,
7434 sketch_id,
7435 Vec::new(),
7436 vec![first_point_id, last_point_id],
7437 )
7438 .await
7439 .unwrap();
7440 assert_eq!(
7441 src_delta.text.as_str(),
7442 "\
7443sketch(on = XY) {
7444 /* above first - moves to middle */
7445 /* above middle - stays */
7446 point(at = [var 3mm, var 4mm])
7447 /* above last - moves to trailing meta */
7448}
7449"
7450 );
7451
7452 ctx.close().await;
7453 mock_ctx.close().await;
7454 }
7455
7456 #[tokio::test(flavor = "multi_thread")]
7457 async fn test_edit_line_when_editing_its_start_point() {
7458 let initial_source = "\
7459sketch(on = XY) {
7460 line(start = [var 1, var 2], end = [var 3, var 4])
7461}
7462";
7463
7464 let program = Program::parse(initial_source).unwrap().0.unwrap();
7465
7466 let mut frontend = FrontendState::new();
7467
7468 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7469 let mock_ctx = ExecutorContext::new_mock(None).await;
7470 let version = Version(0);
7471
7472 frontend.hack_set_program(&ctx, program).await.unwrap();
7473 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7474 let sketch_id = sketch_object.id;
7475 let sketch = expect_sketch(sketch_object);
7476
7477 let point_id = *sketch.segments.first().unwrap();
7478
7479 let point_ctor = PointCtor {
7480 position: Point2d {
7481 x: Expr::Var(Number {
7482 value: 5.0,
7483 units: NumericSuffix::Inch,
7484 }),
7485 y: Expr::Var(Number {
7486 value: 6.0,
7487 units: NumericSuffix::Inch,
7488 }),
7489 },
7490 };
7491 let segments = vec![ExistingSegmentCtor {
7492 id: point_id,
7493 ctor: SegmentCtor::Point(point_ctor),
7494 }];
7495 let (src_delta, scene_delta) = frontend
7496 .edit_segments(&mock_ctx, version, sketch_id, segments)
7497 .await
7498 .unwrap();
7499 assert_eq!(
7500 src_delta.text.as_str(),
7501 "\
7502sketch(on = XY) {
7503 line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7504}
7505"
7506 );
7507 assert_eq!(scene_delta.new_objects, vec![]);
7508 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7509
7510 ctx.close().await;
7511 mock_ctx.close().await;
7512 }
7513
7514 #[tokio::test(flavor = "multi_thread")]
7515 async fn test_edit_line_when_editing_its_end_point() {
7516 let initial_source = "\
7517sketch(on = XY) {
7518 line(start = [var 1, var 2], end = [var 3, var 4])
7519}
7520";
7521
7522 let program = Program::parse(initial_source).unwrap().0.unwrap();
7523
7524 let mut frontend = FrontendState::new();
7525
7526 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7527 let mock_ctx = ExecutorContext::new_mock(None).await;
7528 let version = Version(0);
7529
7530 frontend.hack_set_program(&ctx, program).await.unwrap();
7531 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7532 let sketch_id = sketch_object.id;
7533 let sketch = expect_sketch(sketch_object);
7534 let point_id = *sketch.segments.get(1).unwrap();
7535
7536 let point_ctor = PointCtor {
7537 position: Point2d {
7538 x: Expr::Var(Number {
7539 value: 5.0,
7540 units: NumericSuffix::Inch,
7541 }),
7542 y: Expr::Var(Number {
7543 value: 6.0,
7544 units: NumericSuffix::Inch,
7545 }),
7546 },
7547 };
7548 let segments = vec![ExistingSegmentCtor {
7549 id: point_id,
7550 ctor: SegmentCtor::Point(point_ctor),
7551 }];
7552 let (src_delta, scene_delta) = frontend
7553 .edit_segments(&mock_ctx, version, sketch_id, segments)
7554 .await
7555 .unwrap();
7556 assert_eq!(
7557 src_delta.text.as_str(),
7558 "\
7559sketch(on = XY) {
7560 line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
7561}
7562"
7563 );
7564 assert_eq!(scene_delta.new_objects, vec![]);
7565 assert_eq!(
7566 scene_delta.new_graph.objects.len(),
7567 5,
7568 "{:#?}",
7569 scene_delta.new_graph.objects
7570 );
7571
7572 ctx.close().await;
7573 mock_ctx.close().await;
7574 }
7575
7576 #[tokio::test(flavor = "multi_thread")]
7577 async fn test_edit_line_with_coincident_feedback() {
7578 let initial_source = "\
7579sketch(on = XY) {
7580 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
7581 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7582 fixed([line1.start, [0, 0]])
7583 coincident([line1.end, line2.start])
7584 equalLength([line1, line2])
7585}
7586";
7587
7588 let program = Program::parse(initial_source).unwrap().0.unwrap();
7589
7590 let mut frontend = FrontendState::new();
7591
7592 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7593 let mock_ctx = ExecutorContext::new_mock(None).await;
7594 let version = Version(0);
7595
7596 frontend.hack_set_program(&ctx, program).await.unwrap();
7597 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7598 let sketch_id = sketch_object.id;
7599 let sketch = expect_sketch(sketch_object);
7600 let line2_end_id = *sketch.segments.get(4).unwrap();
7601
7602 let segments = vec![ExistingSegmentCtor {
7603 id: line2_end_id,
7604 ctor: SegmentCtor::Point(PointCtor {
7605 position: Point2d {
7606 x: Expr::Var(Number {
7607 value: 9.0,
7608 units: NumericSuffix::None,
7609 }),
7610 y: Expr::Var(Number {
7611 value: 10.0,
7612 units: NumericSuffix::None,
7613 }),
7614 },
7615 }),
7616 }];
7617 let (src_delta, scene_delta) = frontend
7618 .edit_segments(&mock_ctx, version, sketch_id, segments)
7619 .await
7620 .unwrap();
7621 assert_eq!(
7622 src_delta.text.as_str(),
7623 "\
7624sketch(on = XY) {
7625 line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7626 line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7627 fixed([line1.start, [0, 0]])
7628 coincident([line1.end, line2.start])
7629 equalLength([line1, line2])
7630}
7631"
7632 );
7633 assert_eq!(
7634 scene_delta.new_graph.objects.len(),
7635 11,
7636 "{:#?}",
7637 scene_delta.new_graph.objects
7638 );
7639
7640 ctx.close().await;
7641 mock_ctx.close().await;
7642 }
7643
7644 #[tokio::test(flavor = "multi_thread")]
7645 async fn test_delete_point_without_var() {
7646 let initial_source = "\
7647sketch(on = XY) {
7648 point(at = [var 1, var 2])
7649 point(at = [var 3, var 4])
7650 point(at = [var 5, var 6])
7651}
7652";
7653
7654 let program = Program::parse(initial_source).unwrap().0.unwrap();
7655
7656 let mut frontend = FrontendState::new();
7657
7658 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7659 let mock_ctx = ExecutorContext::new_mock(None).await;
7660 let version = Version(0);
7661
7662 frontend.hack_set_program(&ctx, program).await.unwrap();
7663 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7664 let sketch_id = sketch_object.id;
7665 let sketch = expect_sketch(sketch_object);
7666
7667 let point_id = *sketch.segments.get(1).unwrap();
7668
7669 let (src_delta, scene_delta) = frontend
7670 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7671 .await
7672 .unwrap();
7673 assert_eq!(
7674 src_delta.text.as_str(),
7675 "\
7676sketch(on = XY) {
7677 point(at = [var 1mm, var 2mm])
7678 point(at = [var 5mm, var 6mm])
7679}
7680"
7681 );
7682 assert_eq!(scene_delta.new_objects, vec![]);
7683 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7684
7685 ctx.close().await;
7686 mock_ctx.close().await;
7687 }
7688
7689 #[tokio::test(flavor = "multi_thread")]
7690 async fn test_delete_point_with_var() {
7691 let initial_source = "\
7692sketch(on = XY) {
7693 point(at = [var 1, var 2])
7694 point1 = point(at = [var 3, var 4])
7695 point(at = [var 5, var 6])
7696}
7697";
7698
7699 let program = Program::parse(initial_source).unwrap().0.unwrap();
7700
7701 let mut frontend = FrontendState::new();
7702
7703 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7704 let mock_ctx = ExecutorContext::new_mock(None).await;
7705 let version = Version(0);
7706
7707 frontend.hack_set_program(&ctx, program).await.unwrap();
7708 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7709 let sketch_id = sketch_object.id;
7710 let sketch = expect_sketch(sketch_object);
7711
7712 let point_id = *sketch.segments.get(1).unwrap();
7713
7714 let (src_delta, scene_delta) = frontend
7715 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7716 .await
7717 .unwrap();
7718 assert_eq!(
7719 src_delta.text.as_str(),
7720 "\
7721sketch(on = XY) {
7722 point(at = [var 1mm, var 2mm])
7723 point(at = [var 5mm, var 6mm])
7724}
7725"
7726 );
7727 assert_eq!(scene_delta.new_objects, vec![]);
7728 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7729
7730 ctx.close().await;
7731 mock_ctx.close().await;
7732 }
7733
7734 #[tokio::test(flavor = "multi_thread")]
7735 async fn test_delete_multiple_points() {
7736 let initial_source = "\
7737sketch(on = XY) {
7738 point(at = [var 1, var 2])
7739 point1 = point(at = [var 3, var 4])
7740 point(at = [var 5, var 6])
7741}
7742";
7743
7744 let program = Program::parse(initial_source).unwrap().0.unwrap();
7745
7746 let mut frontend = FrontendState::new();
7747
7748 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7749 let mock_ctx = ExecutorContext::new_mock(None).await;
7750 let version = Version(0);
7751
7752 frontend.hack_set_program(&ctx, program).await.unwrap();
7753 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7754 let sketch_id = sketch_object.id;
7755
7756 let sketch = expect_sketch(sketch_object);
7757
7758 let point1_id = *sketch.segments.first().unwrap();
7759 let point2_id = *sketch.segments.get(1).unwrap();
7760
7761 let (src_delta, scene_delta) = frontend
7762 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
7763 .await
7764 .unwrap();
7765 assert_eq!(
7766 src_delta.text.as_str(),
7767 "\
7768sketch(on = XY) {
7769 point(at = [var 5mm, var 6mm])
7770}
7771"
7772 );
7773 assert_eq!(scene_delta.new_objects, vec![]);
7774 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7775
7776 ctx.close().await;
7777 mock_ctx.close().await;
7778 }
7779
7780 #[tokio::test(flavor = "multi_thread")]
7781 async fn test_delete_coincident_constraint() {
7782 let initial_source = "\
7783sketch(on = XY) {
7784 point1 = point(at = [var 1, var 2])
7785 point2 = point(at = [var 3, var 4])
7786 coincident([point1, point2])
7787 point(at = [var 5, var 6])
7788}
7789";
7790
7791 let program = Program::parse(initial_source).unwrap().0.unwrap();
7792
7793 let mut frontend = FrontendState::new();
7794
7795 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7796 let mock_ctx = ExecutorContext::new_mock(None).await;
7797 let version = Version(0);
7798
7799 frontend.hack_set_program(&ctx, program).await.unwrap();
7800 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7801 let sketch_id = sketch_object.id;
7802 let sketch = expect_sketch(sketch_object);
7803
7804 let coincident_id = *sketch.constraints.first().unwrap();
7805
7806 let (src_delta, scene_delta) = frontend
7807 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7808 .await
7809 .unwrap();
7810 assert_eq!(
7811 src_delta.text.as_str(),
7812 "\
7813sketch(on = XY) {
7814 point1 = point(at = [var 1mm, var 2mm])
7815 point2 = point(at = [var 3mm, var 4mm])
7816 point(at = [var 5mm, var 6mm])
7817}
7818"
7819 );
7820 assert_eq!(scene_delta.new_objects, vec![]);
7821 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7822
7823 ctx.close().await;
7824 mock_ctx.close().await;
7825 }
7826
7827 #[tokio::test(flavor = "multi_thread")]
7828 async fn test_delete_line_cascades_to_coincident_constraint() {
7829 let initial_source = "\
7830sketch(on = XY) {
7831 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7832 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7833 coincident([line1.end, line2.start])
7834}
7835";
7836
7837 let program = Program::parse(initial_source).unwrap().0.unwrap();
7838
7839 let mut frontend = FrontendState::new();
7840
7841 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7842 let mock_ctx = ExecutorContext::new_mock(None).await;
7843 let version = Version(0);
7844
7845 frontend.hack_set_program(&ctx, program).await.unwrap();
7846 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7847 let sketch_id = sketch_object.id;
7848 let sketch = expect_sketch(sketch_object);
7849 let line_id = *sketch.segments.get(5).unwrap();
7850
7851 let (src_delta, scene_delta) = frontend
7852 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7853 .await
7854 .unwrap();
7855 assert_eq!(
7856 src_delta.text.as_str(),
7857 "\
7858sketch(on = XY) {
7859 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7860}
7861"
7862 );
7863 assert_eq!(
7864 scene_delta.new_graph.objects.len(),
7865 5,
7866 "{:#?}",
7867 scene_delta.new_graph.objects
7868 );
7869
7870 ctx.close().await;
7871 mock_ctx.close().await;
7872 }
7873
7874 #[tokio::test(flavor = "multi_thread")]
7875 async fn test_delete_line_cascades_to_distance_constraint() {
7876 let initial_source = "\
7877sketch(on = XY) {
7878 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7879 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7880 distance([line1.end, line2.start]) == 10mm
7881}
7882";
7883
7884 let program = Program::parse(initial_source).unwrap().0.unwrap();
7885
7886 let mut frontend = FrontendState::new();
7887
7888 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7889 let mock_ctx = ExecutorContext::new_mock(None).await;
7890 let version = Version(0);
7891
7892 frontend.hack_set_program(&ctx, program).await.unwrap();
7893 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7894 let sketch_id = sketch_object.id;
7895 let sketch = expect_sketch(sketch_object);
7896 let line_id = *sketch.segments.get(5).unwrap();
7897
7898 let (src_delta, scene_delta) = frontend
7899 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7900 .await
7901 .unwrap();
7902 assert_eq!(
7903 src_delta.text.as_str(),
7904 "\
7905sketch(on = XY) {
7906 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7907}
7908"
7909 );
7910 assert_eq!(
7911 scene_delta.new_graph.objects.len(),
7912 5,
7913 "{:#?}",
7914 scene_delta.new_graph.objects
7915 );
7916
7917 ctx.close().await;
7918 mock_ctx.close().await;
7919 }
7920
7921 #[tokio::test(flavor = "multi_thread")]
7922 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
7923 let initial_source = "\
7924sketch(on = XY) {
7925 point1 = point(at = [var 1, var 2])
7926 point2 = point(at = [var 3, var 4])
7927 horizontalDistance([point1, point2]) == 10mm
7928}
7929";
7930
7931 let program = Program::parse(initial_source).unwrap().0.unwrap();
7932
7933 let mut frontend = FrontendState::new();
7934
7935 let mock_ctx = ExecutorContext::new_mock(None).await;
7936 let version = Version(0);
7937
7938 frontend.program = program.clone();
7939 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7940 frontend.update_state_after_exec(outcome, true);
7941 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7942 let sketch_id = sketch_object.id;
7943 let sketch = expect_sketch(sketch_object);
7944 let point2_id = *sketch.segments.get(1).unwrap();
7945
7946 let (src_delta, scene_delta) = frontend
7947 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
7948 .await
7949 .unwrap();
7950 assert_eq!(
7951 src_delta.text.as_str(),
7952 "\
7953sketch(on = XY) {
7954 point1 = point(at = [var 1mm, var 2mm])
7955}
7956"
7957 );
7958 assert_eq!(
7959 scene_delta.new_graph.objects.len(),
7960 3,
7961 "{:#?}",
7962 scene_delta.new_graph.objects
7963 );
7964
7965 mock_ctx.close().await;
7966 }
7967
7968 #[tokio::test(flavor = "multi_thread")]
7969 async fn test_delete_line_cascades_to_fixed_constraint() {
7970 let initial_source = "\
7971sketch(on = XY) {
7972 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7973 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7974 fixed([line1.start, [0, 0]])
7975}
7976";
7977
7978 let program = Program::parse(initial_source).unwrap().0.unwrap();
7979
7980 let mut frontend = FrontendState::new();
7981
7982 let mock_ctx = ExecutorContext::new_mock(None).await;
7983 let version = Version(0);
7984
7985 frontend.program = program.clone();
7986 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7987 frontend.update_state_after_exec(outcome, true);
7988 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7989 let sketch_id = sketch_object.id;
7990 let sketch = expect_sketch(sketch_object);
7991 let line1_id = *sketch.segments.get(2).unwrap();
7992
7993 let (src_delta, scene_delta) = frontend
7994 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7995 .await
7996 .unwrap();
7997 assert_eq!(
7998 src_delta.text.as_str(),
7999 "\
8000sketch(on = XY) {
8001 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8002}
8003"
8004 );
8005 assert_eq!(
8006 scene_delta.new_graph.objects.len(),
8007 5,
8008 "{:#?}",
8009 scene_delta.new_graph.objects
8010 );
8011
8012 mock_ctx.close().await;
8013 }
8014
8015 #[tokio::test(flavor = "multi_thread")]
8016 async fn test_delete_line_cascades_to_midpoint_constraint() {
8017 let initial_source = "\
8018sketch(on = XY) {
8019 point1 = point(at = [var 1, var 2])
8020 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
8021 midpoint(line1, point = point1)
8022}
8023";
8024
8025 let program = Program::parse(initial_source).unwrap().0.unwrap();
8026
8027 let mut frontend = FrontendState::new();
8028
8029 let mock_ctx = ExecutorContext::new_mock(None).await;
8030 let version = Version(0);
8031
8032 frontend.program = program.clone();
8033 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8034 frontend.update_state_after_exec(outcome, true);
8035 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8036 let sketch_id = sketch_object.id;
8037 let sketch = expect_sketch(sketch_object);
8038 let line1_id = *sketch.segments.get(3).unwrap();
8039
8040 let (src_delta, scene_delta) = frontend
8041 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8042 .await
8043 .unwrap();
8044 assert_eq!(
8045 src_delta.text.as_str(),
8046 "\
8047sketch(on = XY) {
8048 point1 = point(at = [var 1mm, var 2mm])
8049}
8050"
8051 );
8052 assert_eq!(
8053 scene_delta.new_graph.objects.len(),
8054 3,
8055 "{:#?}",
8056 scene_delta.new_graph.objects
8057 );
8058
8059 mock_ctx.close().await;
8060 }
8061
8062 #[tokio::test(flavor = "multi_thread")]
8063 async fn test_delete_point_preserves_multiline_coincident_constraint() {
8064 let initial_source = "\
8065sketch(on = XY) {
8066 point1 = point(at = [var 1, var 2])
8067 point2 = point(at = [var 3, var 4])
8068 point3 = point(at = [var 5, var 6])
8069 coincident([point1, point2, point3])
8070}
8071";
8072
8073 let program = Program::parse(initial_source).unwrap().0.unwrap();
8074
8075 let mut frontend = FrontendState::new();
8076
8077 let mock_ctx = ExecutorContext::new_mock(None).await;
8078 let version = Version(0);
8079
8080 frontend.program = program.clone();
8081 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8082 frontend.update_state_after_exec(outcome, true);
8083 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8084 let sketch_id = sketch_object.id;
8085 let sketch = expect_sketch(sketch_object);
8086 let point3_id = *sketch.segments.get(2).unwrap();
8087
8088 let (src_delta, scene_delta) = frontend
8089 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8090 .await
8091 .unwrap();
8092 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8093 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8094 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8095 assert!(
8096 src_delta.text.contains("coincident([point1, point2])"),
8097 "{}",
8098 src_delta.text
8099 );
8100
8101 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8102 let sketch = expect_sketch(sketch_object);
8103 assert_eq!(sketch.segments.len(), 2);
8104 assert_eq!(sketch.constraints.len(), 1);
8105
8106 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8107 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8108 panic!("Expected constraint object");
8109 };
8110 let Constraint::Coincident(coincident) = constraint else {
8111 panic!("Expected coincident constraint");
8112 };
8113 assert_eq!(
8114 coincident.segments,
8115 sketch
8116 .segments
8117 .iter()
8118 .copied()
8119 .map(Into::into)
8120 .collect::<Vec<ConstraintSegment>>()
8121 );
8122
8123 mock_ctx.close().await;
8124 }
8125
8126 #[tokio::test(flavor = "multi_thread")]
8127 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8128 let initial_source = "\
8129sketch(on = XY) {
8130 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8131 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8132 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8133 equalLength([line1, line2, line3])
8134}
8135";
8136
8137 let program = Program::parse(initial_source).unwrap().0.unwrap();
8138
8139 let mut frontend = FrontendState::new();
8140
8141 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8142 let mock_ctx = ExecutorContext::new_mock(None).await;
8143 let version = Version(0);
8144
8145 frontend.hack_set_program(&ctx, program).await.unwrap();
8146 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8147 let sketch_id = sketch_object.id;
8148 let sketch = expect_sketch(sketch_object);
8149 let line3_id = *sketch.segments.get(8).unwrap();
8150
8151 let (src_delta, scene_delta) = frontend
8152 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8153 .await
8154 .unwrap();
8155 assert_eq!(
8156 src_delta.text.as_str(),
8157 "\
8158sketch(on = XY) {
8159 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8160 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8161 equalLength([line1, line2])
8162}
8163"
8164 );
8165
8166 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8167 let sketch = expect_sketch(sketch_object);
8168 assert_eq!(sketch.constraints.len(), 1);
8169
8170 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8171 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8172 panic!("Expected constraint object");
8173 };
8174 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8175 panic!("Expected lines equal length constraint");
8176 };
8177 assert_eq!(lines_equal_length.lines.len(), 2);
8178
8179 ctx.close().await;
8180 mock_ctx.close().await;
8181 }
8182
8183 #[tokio::test(flavor = "multi_thread")]
8184 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8185 let initial_source = "\
8186sketch(on = XY) {
8187 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8188 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8189 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8190 horizontal([line1.end, line2.start, line3.start])
8191}
8192";
8193
8194 let program = Program::parse(initial_source).unwrap().0.unwrap();
8195
8196 let mut frontend = FrontendState::new();
8197
8198 let mock_ctx = ExecutorContext::new_mock(None).await;
8199 let version = Version(0);
8200
8201 frontend.program = program.clone();
8202 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8203 frontend.update_state_after_exec(outcome, true);
8204 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8205 let sketch_id = sketch_object.id;
8206 let sketch = expect_sketch(sketch_object);
8207 let line1_id = *sketch.segments.get(2).unwrap();
8208
8209 let (src_delta, scene_delta) = frontend
8210 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8211 .await
8212 .unwrap();
8213 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8214 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8215 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8216 assert!(
8217 src_delta.text.contains("horizontal([line2.start, line3.start])"),
8218 "{}",
8219 src_delta.text
8220 );
8221
8222 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8223 let sketch = expect_sketch(sketch_object);
8224 assert_eq!(sketch.constraints.len(), 1);
8225
8226 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8227 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8228 panic!("Expected constraint object");
8229 };
8230 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8231 panic!("Expected horizontal points constraint");
8232 };
8233 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8234 assert_eq!(*points, remaining_points);
8235
8236 mock_ctx.close().await;
8237 }
8238
8239 #[tokio::test(flavor = "multi_thread")]
8240 async fn test_delete_line_preserves_multiline_vertical_constraint() {
8241 let initial_source = "\
8242sketch(on = XY) {
8243 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8244 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8245 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8246 vertical([line1.end, line2.start, line3.start])
8247}
8248";
8249
8250 let program = Program::parse(initial_source).unwrap().0.unwrap();
8251
8252 let mut frontend = FrontendState::new();
8253
8254 let mock_ctx = ExecutorContext::new_mock(None).await;
8255 let version = Version(0);
8256
8257 frontend.program = program.clone();
8258 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8259 frontend.update_state_after_exec(outcome, true);
8260 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8261 let sketch_id = sketch_object.id;
8262 let sketch = expect_sketch(sketch_object);
8263 let line1_id = *sketch.segments.get(2).unwrap();
8264
8265 let (src_delta, scene_delta) = frontend
8266 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8267 .await
8268 .unwrap();
8269 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8270 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8271 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8272 assert!(
8273 src_delta.text.contains("vertical([line2.start, line3.start])"),
8274 "{}",
8275 src_delta.text
8276 );
8277
8278 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8279 let sketch = expect_sketch(sketch_object);
8280 assert_eq!(sketch.constraints.len(), 1);
8281
8282 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8283 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8284 panic!("Expected constraint object");
8285 };
8286 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8287 panic!("Expected vertical points constraint");
8288 };
8289 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8290 assert_eq!(*points, remaining_points);
8291
8292 mock_ctx.close().await;
8293 }
8294
8295 #[tokio::test(flavor = "multi_thread")]
8296 async fn test_delete_line_preserves_multiline_coincident_constraint() {
8297 let initial_source = "\
8298sketch(on = XY) {
8299 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8300 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8301 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8302 coincident([line1.end, line2.start, line3.start])
8303}
8304";
8305
8306 let program = Program::parse(initial_source).unwrap().0.unwrap();
8307
8308 let mut frontend = FrontendState::new();
8309
8310 let mock_ctx = ExecutorContext::new_mock(None).await;
8311 let version = Version(0);
8312
8313 frontend.program = program.clone();
8314 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8315 frontend.update_state_after_exec(outcome, true);
8316 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8317 let sketch_id = sketch_object.id;
8318 let sketch = expect_sketch(sketch_object);
8319 let line1_id = *sketch.segments.get(2).unwrap();
8320
8321 let (src_delta, scene_delta) = frontend
8322 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8323 .await
8324 .unwrap();
8325 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8326 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8327 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8328 assert!(
8329 src_delta.text.contains("coincident([line2.start, line3.start])"),
8330 "{}",
8331 src_delta.text
8332 );
8333
8334 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8335 let sketch = expect_sketch(sketch_object);
8336 assert_eq!(sketch.constraints.len(), 1);
8337
8338 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8339 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8340 panic!("Expected constraint object");
8341 };
8342 let Constraint::Coincident(coincident) = constraint else {
8343 panic!("Expected coincident constraint");
8344 };
8345 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8346 assert_eq!(coincident.segments, remaining_segments);
8347
8348 mock_ctx.close().await;
8349 }
8350
8351 #[tokio::test(flavor = "multi_thread")]
8352 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8353 let initial_source = "\
8354sketch(on = XY) {
8355 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8356 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8357 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8358 equalLength([line1, line2, line3])
8359}
8360";
8361
8362 let program = Program::parse(initial_source).unwrap().0.unwrap();
8363
8364 let mut frontend = FrontendState::new();
8365
8366 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8367 let mock_ctx = ExecutorContext::new_mock(None).await;
8368 let version = Version(0);
8369
8370 frontend.hack_set_program(&ctx, program).await.unwrap();
8371 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8372 let sketch_id = sketch_object.id;
8373 let sketch = expect_sketch(sketch_object);
8374 let line2_id = *sketch.segments.get(5).unwrap();
8375 let line3_id = *sketch.segments.get(8).unwrap();
8376
8377 let (src_delta, scene_delta) = frontend
8378 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8379 .await
8380 .unwrap();
8381 assert_eq!(
8382 src_delta.text.as_str(),
8383 "\
8384sketch(on = XY) {
8385 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8386}
8387"
8388 );
8389
8390 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8391 let sketch = expect_sketch(sketch_object);
8392 assert!(sketch.constraints.is_empty());
8393
8394 ctx.close().await;
8395 mock_ctx.close().await;
8396 }
8397
8398 #[tokio::test(flavor = "multi_thread")]
8399 async fn test_delete_line_preserves_multiline_parallel_constraint() {
8400 let initial_source = "\
8401sketch(on = XY) {
8402 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8403 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8404 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8405 parallel([line1, line2, line3])
8406}
8407";
8408
8409 let program = Program::parse(initial_source).unwrap().0.unwrap();
8410
8411 let mut frontend = FrontendState::new();
8412
8413 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8414 let mock_ctx = ExecutorContext::new_mock(None).await;
8415 let version = Version(0);
8416
8417 frontend.hack_set_program(&ctx, program).await.unwrap();
8418 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8419 let sketch_id = sketch_object.id;
8420 let sketch = expect_sketch(sketch_object);
8421 let line3_id = *sketch.segments.get(8).unwrap();
8422
8423 let (src_delta, scene_delta) = frontend
8424 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8425 .await
8426 .unwrap();
8427 assert_eq!(
8428 src_delta.text.as_str(),
8429 "\
8430sketch(on = XY) {
8431 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8432 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8433 parallel([line1, line2])
8434}
8435"
8436 );
8437
8438 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8439 let sketch = expect_sketch(sketch_object);
8440 assert_eq!(sketch.constraints.len(), 1);
8441
8442 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8443 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8444 panic!("Expected constraint object");
8445 };
8446 let Constraint::Parallel(parallel) = constraint else {
8447 panic!("Expected parallel constraint");
8448 };
8449 assert_eq!(parallel.lines.len(), 2);
8450
8451 ctx.close().await;
8452 mock_ctx.close().await;
8453 }
8454
8455 #[tokio::test(flavor = "multi_thread")]
8456 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8457 let initial_source = "\
8458sketch(on = XY) {
8459 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8460 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8461 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8462 parallel([line1, line2, line3])
8463}
8464";
8465
8466 let program = Program::parse(initial_source).unwrap().0.unwrap();
8467
8468 let mut frontend = FrontendState::new();
8469
8470 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8471 let mock_ctx = ExecutorContext::new_mock(None).await;
8472 let version = Version(0);
8473
8474 frontend.hack_set_program(&ctx, program).await.unwrap();
8475 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8476 let sketch_id = sketch_object.id;
8477 let sketch = expect_sketch(sketch_object);
8478 let line2_id = *sketch.segments.get(5).unwrap();
8479 let line3_id = *sketch.segments.get(8).unwrap();
8480
8481 let (src_delta, scene_delta) = frontend
8482 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8483 .await
8484 .unwrap();
8485 assert_eq!(
8486 src_delta.text.as_str(),
8487 "\
8488sketch(on = XY) {
8489 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8490}
8491"
8492 );
8493
8494 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8495 let sketch = expect_sketch(sketch_object);
8496 assert!(sketch.constraints.is_empty());
8497
8498 ctx.close().await;
8499 mock_ctx.close().await;
8500 }
8501
8502 #[tokio::test(flavor = "multi_thread")]
8503 async fn test_delete_line_line_coincident_constraint() {
8504 let initial_source = "\
8505sketch(on = XY) {
8506 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8507 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8508 coincident([line1, line2])
8509}
8510";
8511
8512 let program = Program::parse(initial_source).unwrap().0.unwrap();
8513
8514 let mut frontend = FrontendState::new();
8515
8516 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8517 let mock_ctx = ExecutorContext::new_mock(None).await;
8518 let version = Version(0);
8519
8520 frontend.hack_set_program(&ctx, program).await.unwrap();
8521 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8522 let sketch_id = sketch_object.id;
8523 let sketch = expect_sketch(sketch_object);
8524
8525 let coincident_id = *sketch.constraints.first().unwrap();
8526
8527 let (src_delta, scene_delta) = frontend
8528 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8529 .await
8530 .unwrap();
8531 assert_eq!(
8532 src_delta.text.as_str(),
8533 "\
8534sketch(on = XY) {
8535 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8536 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8537}
8538"
8539 );
8540 assert_eq!(scene_delta.new_objects, vec![]);
8541 assert_eq!(scene_delta.new_graph.objects.len(), 8);
8542
8543 ctx.close().await;
8544 mock_ctx.close().await;
8545 }
8546
8547 #[tokio::test(flavor = "multi_thread")]
8548 async fn test_two_points_coincident() {
8549 let initial_source = "\
8550sketch(on = XY) {
8551 point1 = point(at = [var 1, var 2])
8552 point(at = [3, 4])
8553}
8554";
8555
8556 let program = Program::parse(initial_source).unwrap().0.unwrap();
8557
8558 let mut frontend = FrontendState::new();
8559
8560 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8561 let mock_ctx = ExecutorContext::new_mock(None).await;
8562 let version = Version(0);
8563
8564 frontend.hack_set_program(&ctx, program).await.unwrap();
8565 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8566 let sketch_id = sketch_object.id;
8567 let sketch = expect_sketch(sketch_object);
8568 let point0_id = *sketch.segments.first().unwrap();
8569 let point1_id = *sketch.segments.get(1).unwrap();
8570
8571 let constraint = Constraint::Coincident(Coincident {
8572 segments: vec![point0_id.into(), point1_id.into()],
8573 });
8574 let (src_delta, scene_delta) = frontend
8575 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8576 .await
8577 .unwrap();
8578 assert_eq!(
8579 src_delta.text.as_str(),
8580 "\
8581sketch(on = XY) {
8582 point1 = point(at = [var 1, var 2])
8583 point2 = point(at = [3, 4])
8584 coincident([point1, point2])
8585}
8586"
8587 );
8588 assert_eq!(
8589 scene_delta.new_graph.objects.len(),
8590 5,
8591 "{:#?}",
8592 scene_delta.new_graph.objects
8593 );
8594
8595 ctx.close().await;
8596 mock_ctx.close().await;
8597 }
8598
8599 #[tokio::test(flavor = "multi_thread")]
8600 async fn test_three_points_coincident() {
8601 let initial_source = "\
8602sketch(on = XY) {
8603 point1 = point(at = [var 1, var 2])
8604 point(at = [var 3, var 4])
8605 point(at = [var 5, var 6])
8606}
8607";
8608
8609 let program = Program::parse(initial_source).unwrap().0.unwrap();
8610
8611 let mut frontend = FrontendState::new();
8612
8613 let mock_ctx = ExecutorContext::new_mock(None).await;
8614 let version = Version(0);
8615
8616 frontend.program = program.clone();
8617 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8618 frontend.update_state_after_exec(outcome, true);
8619 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8620 let sketch_id = sketch_object.id;
8621 let sketch = expect_sketch(sketch_object);
8622 let segments = sketch
8623 .segments
8624 .iter()
8625 .take(3)
8626 .copied()
8627 .map(Into::into)
8628 .collect::<Vec<ConstraintSegment>>();
8629
8630 let constraint = Constraint::Coincident(Coincident {
8631 segments: segments.clone(),
8632 });
8633 let (src_delta, scene_delta) = frontend
8634 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8635 .await
8636 .unwrap();
8637 assert_eq!(
8638 src_delta.text.as_str(),
8639 "\
8640sketch(on = XY) {
8641 point1 = point(at = [var 1, var 2])
8642 point2 = point(at = [var 3, var 4])
8643 point3 = point(at = [var 5, var 6])
8644 coincident([point1, point2, point3])
8645}
8646"
8647 );
8648
8649 let constraint_object = scene_delta
8650 .new_graph
8651 .objects
8652 .iter()
8653 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8654 .unwrap();
8655
8656 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8657 panic!("expected a constraint object");
8658 };
8659
8660 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8661
8662 mock_ctx.close().await;
8663 }
8664
8665 #[tokio::test(flavor = "multi_thread")]
8666 async fn test_source_with_three_point_coincident_tracks_all_segments() {
8667 let initial_source = "\
8668sketch(on = XY) {
8669 point1 = point(at = [var 1, var 2])
8670 point2 = point(at = [var 3, var 4])
8671 point3 = point(at = [var 5, var 6])
8672 coincident([point1, point2, point3])
8673}
8674";
8675
8676 let program = Program::parse(initial_source).unwrap().0.unwrap();
8677
8678 let mut frontend = FrontendState::new();
8679
8680 let ctx = ExecutorContext::new_mock(None).await;
8681 frontend.program = program.clone();
8682 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8683 frontend.update_state_after_exec(outcome, true);
8684
8685 let constraint_object = frontend
8686 .scene_graph
8687 .objects
8688 .iter()
8689 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8690 .unwrap();
8691 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8692 panic!("expected a constraint object");
8693 };
8694
8695 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8696 let sketch = expect_sketch(sketch_object);
8697 let expected_segments = sketch
8698 .segments
8699 .iter()
8700 .take(3)
8701 .copied()
8702 .map(Into::into)
8703 .collect::<Vec<ConstraintSegment>>();
8704
8705 assert_eq!(
8706 constraint,
8707 &Constraint::Coincident(Coincident {
8708 segments: expected_segments,
8709 })
8710 );
8711
8712 ctx.close().await;
8713 }
8714
8715 #[tokio::test(flavor = "multi_thread")]
8716 async fn test_point_origin_coincident_preserves_order() {
8717 let initial_source = "\
8718sketch(on = XY) {
8719 point(at = [var 1, var 2])
8720}
8721";
8722
8723 for (origin_first, expected_source) in [
8724 (
8725 true,
8726 "\
8727sketch(on = XY) {
8728 point1 = point(at = [var 1, var 2])
8729 coincident([ORIGIN, point1])
8730}
8731",
8732 ),
8733 (
8734 false,
8735 "\
8736sketch(on = XY) {
8737 point1 = point(at = [var 1, var 2])
8738 coincident([point1, ORIGIN])
8739}
8740",
8741 ),
8742 ] {
8743 let program = Program::parse(initial_source).unwrap().0.unwrap();
8744
8745 let mut frontend = FrontendState::new();
8746
8747 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8748 let mock_ctx = ExecutorContext::new_mock(None).await;
8749 let version = Version(0);
8750
8751 frontend.hack_set_program(&ctx, program).await.unwrap();
8752 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8753 let sketch_id = sketch_object.id;
8754 let sketch = expect_sketch(sketch_object);
8755 let point_id = *sketch.segments.first().unwrap();
8756
8757 let segments = if origin_first {
8758 vec![ConstraintSegment::ORIGIN, point_id.into()]
8759 } else {
8760 vec![point_id.into(), ConstraintSegment::ORIGIN]
8761 };
8762 let constraint = Constraint::Coincident(Coincident {
8763 segments: segments.clone(),
8764 });
8765 let (src_delta, scene_delta) = frontend
8766 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8767 .await
8768 .unwrap();
8769 assert_eq!(src_delta.text.as_str(), expected_source);
8770
8771 let constraint_object = scene_delta
8772 .new_graph
8773 .objects
8774 .iter()
8775 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8776 .unwrap();
8777
8778 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8779 panic!("expected a constraint object");
8780 };
8781
8782 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8783
8784 ctx.close().await;
8785 mock_ctx.close().await;
8786 }
8787 }
8788
8789 #[tokio::test(flavor = "multi_thread")]
8790 async fn test_coincident_of_line_end_points() {
8791 let initial_source = "\
8792sketch(on = XY) {
8793 line(start = [var 1, var 2], end = [var 3, var 4])
8794 line(start = [var 5, var 6], end = [var 7, var 8])
8795}
8796";
8797
8798 let program = Program::parse(initial_source).unwrap().0.unwrap();
8799
8800 let mut frontend = FrontendState::new();
8801
8802 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8803 let mock_ctx = ExecutorContext::new_mock(None).await;
8804 let version = Version(0);
8805
8806 frontend.hack_set_program(&ctx, program).await.unwrap();
8807 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8808 let sketch_id = sketch_object.id;
8809 let sketch = expect_sketch(sketch_object);
8810 let point0_id = *sketch.segments.get(1).unwrap();
8811 let point1_id = *sketch.segments.get(3).unwrap();
8812
8813 let constraint = Constraint::Coincident(Coincident {
8814 segments: vec![point0_id.into(), point1_id.into()],
8815 });
8816 let (src_delta, scene_delta) = frontend
8817 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8818 .await
8819 .unwrap();
8820 assert_eq!(
8821 src_delta.text.as_str(),
8822 "\
8823sketch(on = XY) {
8824 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8825 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8826 coincident([line1.end, line2.start])
8827}
8828"
8829 );
8830 assert_eq!(
8831 scene_delta.new_graph.objects.len(),
8832 9,
8833 "{:#?}",
8834 scene_delta.new_graph.objects
8835 );
8836
8837 ctx.close().await;
8838 mock_ctx.close().await;
8839 }
8840
8841 #[tokio::test(flavor = "multi_thread")]
8842 async fn test_coincident_of_line_point_and_circle_segment() {
8843 let initial_source = "\
8844sketch(on = XY) {
8845 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8846 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8847}
8848";
8849 let program = Program::parse(initial_source).unwrap().0.unwrap();
8850 let mut frontend = FrontendState::new();
8851
8852 let mock_ctx = ExecutorContext::new_mock(None).await;
8853 let version = Version(0);
8854
8855 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8856 frontend.program = program;
8857 frontend.update_state_after_exec(outcome, true);
8858 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
8859 let sketch_id = sketch_object.id;
8860 let sketch = expect_sketch(sketch_object);
8861
8862 let circle_id = sketch
8863 .segments
8864 .iter()
8865 .copied()
8866 .find(|seg_id| {
8867 matches!(
8868 &frontend.scene_graph.objects[seg_id.0].kind,
8869 ObjectKind::Segment {
8870 segment: Segment::Circle(_)
8871 }
8872 )
8873 })
8874 .expect("Expected a circle segment in sketch");
8875 let line_id = sketch
8876 .segments
8877 .iter()
8878 .copied()
8879 .find(|seg_id| {
8880 matches!(
8881 &frontend.scene_graph.objects[seg_id.0].kind,
8882 ObjectKind::Segment {
8883 segment: Segment::Line(_)
8884 }
8885 )
8886 })
8887 .expect("Expected a line segment in sketch");
8888
8889 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
8890 ObjectKind::Segment {
8891 segment: Segment::Line(line),
8892 } => line.start,
8893 _ => panic!("Expected line segment object"),
8894 };
8895
8896 let constraint = Constraint::Coincident(Coincident {
8897 segments: vec![line_start_point_id.into(), circle_id.into()],
8898 });
8899 let (src_delta, _scene_delta) = frontend
8900 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8901 .await
8902 .unwrap();
8903 assert_eq!(
8904 src_delta.text.as_str(),
8905 "\
8906sketch(on = XY) {
8907 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8908 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8909 coincident([line1.start, circle1])
8910}
8911"
8912 );
8913
8914 mock_ctx.close().await;
8915 }
8916
8917 #[tokio::test(flavor = "multi_thread")]
8918 async fn test_invalid_coincident_arc_and_line_preserves_state() {
8919 let program = Program::empty();
8927
8928 let mut frontend = FrontendState::new();
8929 frontend.program = program;
8930
8931 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8932 let mock_ctx = ExecutorContext::new_mock(None).await;
8933 let version = Version(0);
8934
8935 let sketch_args = SketchCtor {
8936 on: Plane::Default(PlaneName::Xy),
8937 };
8938 let (_src_delta, _scene_delta, sketch_id) = frontend
8939 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8940 .await
8941 .unwrap();
8942
8943 let arc_ctor = ArcCtor {
8945 start: Point2d {
8946 x: Expr::Var(Number {
8947 value: 0.0,
8948 units: NumericSuffix::Mm,
8949 }),
8950 y: Expr::Var(Number {
8951 value: 0.0,
8952 units: NumericSuffix::Mm,
8953 }),
8954 },
8955 end: Point2d {
8956 x: Expr::Var(Number {
8957 value: 10.0,
8958 units: NumericSuffix::Mm,
8959 }),
8960 y: Expr::Var(Number {
8961 value: 10.0,
8962 units: NumericSuffix::Mm,
8963 }),
8964 },
8965 center: Point2d {
8966 x: Expr::Var(Number {
8967 value: 10.0,
8968 units: NumericSuffix::Mm,
8969 }),
8970 y: Expr::Var(Number {
8971 value: 0.0,
8972 units: NumericSuffix::Mm,
8973 }),
8974 },
8975 construction: None,
8976 };
8977 let (_src_delta, scene_delta) = frontend
8978 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
8979 .await
8980 .unwrap();
8981 let arc_id = *scene_delta.new_objects.last().unwrap();
8983
8984 let line_ctor = LineCtor {
8986 start: Point2d {
8987 x: Expr::Var(Number {
8988 value: 20.0,
8989 units: NumericSuffix::Mm,
8990 }),
8991 y: Expr::Var(Number {
8992 value: 0.0,
8993 units: NumericSuffix::Mm,
8994 }),
8995 },
8996 end: Point2d {
8997 x: Expr::Var(Number {
8998 value: 30.0,
8999 units: NumericSuffix::Mm,
9000 }),
9001 y: Expr::Var(Number {
9002 value: 10.0,
9003 units: NumericSuffix::Mm,
9004 }),
9005 },
9006 construction: None,
9007 };
9008 let (_src_delta, scene_delta) = frontend
9009 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
9010 .await
9011 .unwrap();
9012 let line_id = *scene_delta.new_objects.last().unwrap();
9014
9015 let constraint = Constraint::Coincident(Coincident {
9018 segments: vec![arc_id.into(), line_id.into()],
9019 });
9020 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
9021
9022 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
9024
9025 let sketch_object_after =
9028 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
9029 let sketch_after = expect_sketch(sketch_object_after);
9030
9031 assert!(
9033 sketch_after.segments.contains(&arc_id),
9034 "Arc segment should still exist after failed constraint"
9035 );
9036 assert!(
9037 sketch_after.segments.contains(&line_id),
9038 "Line segment should still exist after failed constraint"
9039 );
9040
9041 let arc_obj = frontend
9043 .scene_graph
9044 .objects
9045 .get(arc_id.0)
9046 .expect("Arc object should still be accessible");
9047 let line_obj = frontend
9048 .scene_graph
9049 .objects
9050 .get(line_id.0)
9051 .expect("Line object should still be accessible");
9052
9053 match &arc_obj.kind {
9056 ObjectKind::Segment {
9057 segment: Segment::Arc(_),
9058 } => {}
9059 _ => panic!("Arc object should still be an arc segment"),
9060 }
9061 match &line_obj.kind {
9062 ObjectKind::Segment {
9063 segment: Segment::Line(_),
9064 } => {}
9065 _ => panic!("Line object should still be a line segment"),
9066 }
9067
9068 ctx.close().await;
9069 mock_ctx.close().await;
9070 }
9071
9072 #[tokio::test(flavor = "multi_thread")]
9073 async fn test_distance_two_points() {
9074 let initial_source = "\
9075sketch(on = XY) {
9076 point(at = [var 1, var 2])
9077 point(at = [var 3, var 4])
9078}
9079";
9080
9081 let program = Program::parse(initial_source).unwrap().0.unwrap();
9082
9083 let mut frontend = FrontendState::new();
9084
9085 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9086 let mock_ctx = ExecutorContext::new_mock(None).await;
9087 let version = Version(0);
9088
9089 frontend.hack_set_program(&ctx, program).await.unwrap();
9090 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9091 let sketch_id = sketch_object.id;
9092 let sketch = expect_sketch(sketch_object);
9093 let point0_id = *sketch.segments.first().unwrap();
9094 let point1_id = *sketch.segments.get(1).unwrap();
9095
9096 let constraint = Constraint::Distance(Distance {
9097 points: vec![point0_id.into(), point1_id.into()],
9098 distance: Number {
9099 value: 2.0,
9100 units: NumericSuffix::Mm,
9101 },
9102 label_position: None,
9103 source: Default::default(),
9104 });
9105 let (src_delta, scene_delta) = frontend
9106 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9107 .await
9108 .unwrap();
9109 assert_eq!(
9110 src_delta.text.as_str(),
9111 "\
9113sketch(on = XY) {
9114 point1 = point(at = [var 1, var 2])
9115 point2 = point(at = [var 3, var 4])
9116 distance([point1, point2]) == 2mm
9117}
9118"
9119 );
9120 assert_eq!(
9121 scene_delta.new_graph.objects.len(),
9122 5,
9123 "{:#?}",
9124 scene_delta.new_graph.objects
9125 );
9126
9127 ctx.close().await;
9128 mock_ctx.close().await;
9129 }
9130
9131 #[tokio::test(flavor = "multi_thread")]
9132 async fn test_distance_two_points_with_label() {
9133 let initial_source = "\
9134sketch(on = XY) {
9135 point(at = [var 1, var 2])
9136 point(at = [var 3, var 4])
9137}
9138";
9139
9140 let program = Program::parse(initial_source).unwrap().0.unwrap();
9141
9142 let mut frontend = FrontendState::new();
9143
9144 let mock_ctx = ExecutorContext::new_mock(None).await;
9145 let version = Version(0);
9146
9147 frontend.program = program.clone();
9148 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9149 frontend.update_state_after_exec(outcome, true);
9150 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9151 let sketch_id = sketch_object.id;
9152 let sketch = expect_sketch(sketch_object);
9153 let point0_id = *sketch.segments.first().unwrap();
9154 let point1_id = *sketch.segments.get(1).unwrap();
9155
9156 let label_position = Point2d {
9157 x: Number {
9158 value: 10.0,
9159 units: NumericSuffix::Mm,
9160 },
9161 y: Number {
9162 value: 11.0,
9163 units: NumericSuffix::Mm,
9164 },
9165 };
9166 let constraint = Constraint::Distance(Distance {
9167 points: vec![point0_id.into(), point1_id.into()],
9168 distance: Number {
9169 value: 2.0,
9170 units: NumericSuffix::Mm,
9171 },
9172 label_position: Some(label_position.clone()),
9173 source: Default::default(),
9174 });
9175 let (src_delta, scene_delta) = frontend
9176 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9177 .await
9178 .unwrap();
9179 assert_eq!(
9180 src_delta.text.as_str(),
9181 "\
9182sketch(on = XY) {
9183 point1 = point(at = [var 1, var 2])
9184 point2 = point(at = [var 3, var 4])
9185 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9186}
9187"
9188 );
9189
9190 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9191 let sketch = expect_sketch(sketch_object);
9192 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9193 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9194 panic!("Expected constraint object");
9195 };
9196 let Constraint::Distance(distance) = constraint else {
9197 panic!("Expected distance constraint");
9198 };
9199 assert_eq!(distance.label_position, Some(label_position));
9200
9201 mock_ctx.close().await;
9202 }
9203
9204 #[tokio::test(flavor = "multi_thread")]
9205 async fn test_edit_distance_constraint_label_position() {
9206 let initial_source = "\
9207sketch(on = XY) {
9208 point(at = [var 1, var 2])
9209 point(at = [var 3, var 2])
9210}
9211";
9212
9213 let program = Program::parse(initial_source).unwrap().0.unwrap();
9214
9215 let mut frontend = FrontendState::new();
9216
9217 let mock_ctx = ExecutorContext::new_mock(None).await;
9218 let version = Version(0);
9219
9220 frontend.program = program.clone();
9221 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9222 frontend.update_state_after_exec(outcome, true);
9223 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9224 let sketch_id = sketch_object.id;
9225 let sketch = expect_sketch(sketch_object);
9226 let point0_id = *sketch.segments.first().unwrap();
9227 let point1_id = *sketch.segments.get(1).unwrap();
9228
9229 let constraint = Constraint::Distance(Distance {
9230 points: vec![point0_id.into(), point1_id.into()],
9231 distance: Number {
9232 value: 2.0,
9233 units: NumericSuffix::Mm,
9234 },
9235 label_position: None,
9236 source: Default::default(),
9237 });
9238 let (_, scene_delta) = frontend
9239 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9240 .await
9241 .unwrap();
9242 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9243 let sketch = expect_sketch(sketch_object);
9244 let constraint_id = sketch.constraints[0];
9245 let label_position = Point2d {
9246 x: Number {
9247 value: 10.0,
9248 units: NumericSuffix::Mm,
9249 },
9250 y: Number {
9251 value: 11.0,
9252 units: NumericSuffix::Mm,
9253 },
9254 };
9255
9256 let (src_delta, scene_delta) = frontend
9257 .edit_distance_constraint_label_position(
9258 &mock_ctx,
9259 version,
9260 sketch_id,
9261 constraint_id,
9262 label_position.clone(),
9263 vec![],
9264 )
9265 .await
9266 .unwrap();
9267 assert_eq!(
9268 src_delta.text.as_str(),
9269 "\
9270sketch(on = XY) {
9271 point1 = point(at = [var 1mm, var 2mm])
9272 point2 = point(at = [var 3mm, var 2mm])
9273 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9274}
9275"
9276 );
9277
9278 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9279 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9280 panic!("Expected constraint object");
9281 };
9282 let Constraint::Distance(distance) = constraint else {
9283 panic!("Expected distance constraint");
9284 };
9285 assert_eq!(distance.label_position, Some(label_position));
9286
9287 mock_ctx.close().await;
9288 }
9289
9290 #[tokio::test(flavor = "multi_thread")]
9291 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9292 let initial_source = "\
9293sketch(on = XY) {
9294 point1 = point(at = [var 0mm, var 0mm])
9295 point2 = point(at = [var 10mm, var 0mm])
9296 distance([point1, point2]) == 5mm
9297}
9298";
9299
9300 let program = Program::parse(initial_source).unwrap().0.unwrap();
9301 let mut frontend = FrontendState::new();
9302 let mock_ctx = ExecutorContext::new_mock(None).await;
9303 let version = Version(0);
9304
9305 frontend.program = program.clone();
9306 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9307 frontend.update_state_after_exec(outcome, true);
9308 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9309 let sketch_id = sketch_object.id;
9310 let sketch = expect_sketch(sketch_object);
9311 let point0_id = sketch.segments[0];
9312 let point1_id = sketch.segments[1];
9313 let constraint_id = sketch.constraints[0];
9314
9315 let edited_segments = vec![ExistingSegmentCtor {
9316 id: point0_id,
9317 ctor: SegmentCtor::Point(PointCtor {
9318 position: Point2d {
9319 x: Expr::Var(Number {
9320 value: 2.0,
9321 units: NumericSuffix::Mm,
9322 }),
9323 y: Expr::Var(Number {
9324 value: 1.0,
9325 units: NumericSuffix::Mm,
9326 }),
9327 },
9328 }),
9329 }];
9330 let (_, scene_delta) = frontend
9331 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9332 .await
9333 .unwrap();
9334 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9335 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9336
9337 let label_position = Point2d {
9338 x: Number {
9339 value: 3.0,
9340 units: NumericSuffix::Mm,
9341 },
9342 y: Number {
9343 value: 4.0,
9344 units: NumericSuffix::Mm,
9345 },
9346 };
9347 let (_, scene_delta) = frontend
9348 .edit_distance_constraint_label_position(
9349 &mock_ctx,
9350 version,
9351 sketch_id,
9352 constraint_id,
9353 label_position,
9354 vec![point0_id],
9355 )
9356 .await
9357 .unwrap();
9358
9359 assert_point_position_close(
9360 point_position(&scene_delta.new_graph, point0_id),
9361 point0_after_segment_edit,
9362 );
9363 assert_point_position_close(
9364 point_position(&scene_delta.new_graph, point1_id),
9365 point1_after_segment_edit,
9366 );
9367
9368 mock_ctx.close().await;
9369 }
9370
9371 #[tokio::test(flavor = "multi_thread")]
9372 async fn test_horizontal_distance_two_points() {
9373 let initial_source = "\
9374sketch(on = XY) {
9375 point(at = [var 1, var 2])
9376 point(at = [var 3, var 4])
9377}
9378";
9379
9380 let program = Program::parse(initial_source).unwrap().0.unwrap();
9381
9382 let mut frontend = FrontendState::new();
9383
9384 let mock_ctx = ExecutorContext::new_mock(None).await;
9385 let version = Version(0);
9386
9387 frontend.program = program.clone();
9388 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9389 frontend.update_state_after_exec(outcome, true);
9390 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9391 let sketch_id = sketch_object.id;
9392 let sketch = expect_sketch(sketch_object);
9393 let point0_id = *sketch.segments.first().unwrap();
9394 let point1_id = *sketch.segments.get(1).unwrap();
9395 let label_position = Point2d {
9396 x: Number {
9397 value: 10.0,
9398 units: NumericSuffix::Mm,
9399 },
9400 y: Number {
9401 value: 11.0,
9402 units: NumericSuffix::Mm,
9403 },
9404 };
9405
9406 let constraint = Constraint::HorizontalDistance(Distance {
9407 points: vec![point0_id.into(), point1_id.into()],
9408 distance: Number {
9409 value: 2.0,
9410 units: NumericSuffix::Mm,
9411 },
9412 label_position: Some(label_position.clone()),
9413 source: Default::default(),
9414 });
9415 let (src_delta, scene_delta) = frontend
9416 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9417 .await
9418 .unwrap();
9419 assert_eq!(
9420 src_delta.text.as_str(),
9421 "\
9423sketch(on = XY) {
9424 point1 = point(at = [var 1, var 2])
9425 point2 = point(at = [var 3, var 4])
9426 horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9427}
9428"
9429 );
9430 assert_eq!(
9431 scene_delta.new_graph.objects.len(),
9432 5,
9433 "{:#?}",
9434 scene_delta.new_graph.objects
9435 );
9436 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9437 let sketch = expect_sketch(sketch_object);
9438 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9439 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9440 panic!("Expected constraint object");
9441 };
9442 let Constraint::HorizontalDistance(distance) = constraint else {
9443 panic!("Expected horizontal distance constraint");
9444 };
9445 assert_eq!(distance.label_position, Some(label_position));
9446
9447 mock_ctx.close().await;
9448 }
9449
9450 #[tokio::test(flavor = "multi_thread")]
9451 async fn test_radius_single_arc_segment() {
9452 let initial_source = "\
9453sketch(on = XY) {
9454 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9455}
9456";
9457
9458 let program = Program::parse(initial_source).unwrap().0.unwrap();
9459
9460 let mut frontend = FrontendState::new();
9461
9462 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9463 let mock_ctx = ExecutorContext::new_mock(None).await;
9464 let version = Version(0);
9465
9466 frontend.hack_set_program(&ctx, program).await.unwrap();
9467 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9468 let sketch_id = sketch_object.id;
9469 let sketch = expect_sketch(sketch_object);
9470 let arc_id = sketch
9472 .segments
9473 .iter()
9474 .find(|&seg_id| {
9475 let obj = frontend.scene_graph.objects.get(seg_id.0);
9476 matches!(
9477 obj.map(|o| &o.kind),
9478 Some(ObjectKind::Segment {
9479 segment: Segment::Arc(_)
9480 })
9481 )
9482 })
9483 .unwrap();
9484
9485 let constraint = Constraint::Radius(Radius {
9486 arc: *arc_id,
9487 radius: Number {
9488 value: 5.0,
9489 units: NumericSuffix::Mm,
9490 },
9491 label_position: None,
9492 source: Default::default(),
9493 });
9494 let (src_delta, scene_delta) = frontend
9495 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9496 .await
9497 .unwrap();
9498 assert_eq!(
9499 src_delta.text.as_str(),
9500 "\
9502sketch(on = XY) {
9503 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9504 radius(arc1) == 5mm
9505}
9506"
9507 );
9508 assert_eq!(
9509 scene_delta.new_graph.objects.len(),
9510 7, "{:#?}",
9512 scene_delta.new_graph.objects
9513 );
9514
9515 ctx.close().await;
9516 mock_ctx.close().await;
9517 }
9518
9519 #[tokio::test(flavor = "multi_thread")]
9520 async fn test_radius_single_arc_segment_with_label_position() {
9521 let initial_source = "\
9522sketch(on = XY) {
9523 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9524}
9525";
9526
9527 let program = Program::parse(initial_source).unwrap().0.unwrap();
9528 let mut frontend = FrontendState::new();
9529 let mock_ctx = ExecutorContext::new_mock(None).await;
9530 let version = Version(0);
9531
9532 frontend.program = program.clone();
9533 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9534 frontend.update_state_after_exec(outcome, true);
9535 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9536 let sketch_id = sketch_object.id;
9537 let sketch = expect_sketch(sketch_object);
9538 let arc_id = sketch
9539 .segments
9540 .iter()
9541 .find(|&seg_id| {
9542 let obj = frontend.scene_graph.objects.get(seg_id.0);
9543 matches!(
9544 obj.map(|o| &o.kind),
9545 Some(ObjectKind::Segment {
9546 segment: Segment::Arc(_)
9547 })
9548 )
9549 })
9550 .unwrap();
9551
9552 let label_position = Point2d {
9553 x: Number {
9554 value: 10.0,
9555 units: NumericSuffix::Mm,
9556 },
9557 y: Number {
9558 value: 11.0,
9559 units: NumericSuffix::Mm,
9560 },
9561 };
9562 let constraint = Constraint::Radius(Radius {
9563 arc: *arc_id,
9564 radius: Number {
9565 value: 5.0,
9566 units: NumericSuffix::Mm,
9567 },
9568 label_position: Some(label_position.clone()),
9569 source: Default::default(),
9570 });
9571 let (src_delta, scene_delta) = frontend
9572 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9573 .await
9574 .unwrap();
9575 assert_eq!(
9576 src_delta.text.as_str(),
9577 "\
9578sketch(on = XY) {
9579 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9580 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
9581}
9582"
9583 );
9584
9585 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9586 let sketch = expect_sketch(sketch_object);
9587 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9588 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9589 panic!("Expected constraint object");
9590 };
9591 let Constraint::Radius(radius) = constraint else {
9592 panic!("Expected radius constraint");
9593 };
9594 assert_eq!(radius.label_position, Some(label_position));
9595
9596 mock_ctx.close().await;
9597 }
9598
9599 #[tokio::test(flavor = "multi_thread")]
9600 async fn test_edit_radius_constraint_label_position() {
9601 let initial_source = "\
9602sketch(on = XY) {
9603 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
9604 radius(arc1) == 5mm
9605}
9606";
9607
9608 let program = Program::parse(initial_source).unwrap().0.unwrap();
9609 let mut frontend = FrontendState::new();
9610 let mock_ctx = ExecutorContext::new_mock(None).await;
9611 let version = Version(0);
9612
9613 frontend.program = program.clone();
9614 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9615 frontend.update_state_after_exec(outcome, true);
9616 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9617 let sketch_id = sketch_object.id;
9618 let sketch = expect_sketch(sketch_object);
9619 let constraint_id = sketch.constraints[0];
9620 let label_position = Point2d {
9621 x: Number {
9622 value: 10.0,
9623 units: NumericSuffix::Mm,
9624 },
9625 y: Number {
9626 value: 11.0,
9627 units: NumericSuffix::Mm,
9628 },
9629 };
9630
9631 let (src_delta, scene_delta) = frontend
9632 .edit_distance_constraint_label_position(
9633 &mock_ctx,
9634 version,
9635 sketch_id,
9636 constraint_id,
9637 label_position.clone(),
9638 vec![],
9639 )
9640 .await
9641 .unwrap();
9642 assert_eq!(
9643 src_delta.text.as_str(),
9644 "\
9645sketch(on = XY) {
9646 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
9647 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
9648}
9649"
9650 );
9651
9652 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9653 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9654 panic!("Expected constraint object");
9655 };
9656 let Constraint::Radius(radius) = constraint else {
9657 panic!("Expected radius constraint");
9658 };
9659 assert_eq!(radius.label_position, Some(label_position));
9660
9661 mock_ctx.close().await;
9662 }
9663
9664 #[tokio::test(flavor = "multi_thread")]
9665 async fn test_vertical_distance_two_points() {
9666 let initial_source = "\
9667sketch(on = XY) {
9668 point(at = [var 1, var 2])
9669 point(at = [var 3, var 4])
9670}
9671";
9672
9673 let program = Program::parse(initial_source).unwrap().0.unwrap();
9674
9675 let mut frontend = FrontendState::new();
9676
9677 let mock_ctx = ExecutorContext::new_mock(None).await;
9678 let version = Version(0);
9679
9680 frontend.program = program.clone();
9681 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9682 frontend.update_state_after_exec(outcome, true);
9683 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9684 let sketch_id = sketch_object.id;
9685 let sketch = expect_sketch(sketch_object);
9686 let point0_id = *sketch.segments.first().unwrap();
9687 let point1_id = *sketch.segments.get(1).unwrap();
9688 let label_position = Point2d {
9689 x: Number {
9690 value: 10.0,
9691 units: NumericSuffix::Mm,
9692 },
9693 y: Number {
9694 value: 11.0,
9695 units: NumericSuffix::Mm,
9696 },
9697 };
9698
9699 let constraint = Constraint::VerticalDistance(Distance {
9700 points: vec![point0_id.into(), point1_id.into()],
9701 distance: Number {
9702 value: 2.0,
9703 units: NumericSuffix::Mm,
9704 },
9705 label_position: Some(label_position.clone()),
9706 source: Default::default(),
9707 });
9708 let (src_delta, scene_delta) = frontend
9709 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9710 .await
9711 .unwrap();
9712 assert_eq!(
9713 src_delta.text.as_str(),
9714 "\
9716sketch(on = XY) {
9717 point1 = point(at = [var 1, var 2])
9718 point2 = point(at = [var 3, var 4])
9719 verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9720}
9721"
9722 );
9723 assert_eq!(
9724 scene_delta.new_graph.objects.len(),
9725 5,
9726 "{:#?}",
9727 scene_delta.new_graph.objects
9728 );
9729 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9730 let sketch = expect_sketch(sketch_object);
9731 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9732 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9733 panic!("Expected constraint object");
9734 };
9735 let Constraint::VerticalDistance(distance) = constraint else {
9736 panic!("Expected vertical distance constraint");
9737 };
9738 assert_eq!(distance.label_position, Some(label_position));
9739
9740 mock_ctx.close().await;
9741 }
9742
9743 #[tokio::test(flavor = "multi_thread")]
9744 async fn test_add_fixed_standalone_point() {
9745 let initial_source = "\
9746sketch(on = XY) {
9747 point(at = [var 1, var 2])
9748}
9749";
9750
9751 let program = Program::parse(initial_source).unwrap().0.unwrap();
9752
9753 let mut frontend = FrontendState::new();
9754
9755 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9756 let mock_ctx = ExecutorContext::new_mock(None).await;
9757 let version = Version(0);
9758
9759 frontend.hack_set_program(&ctx, program).await.unwrap();
9760 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9761 let sketch_id = sketch_object.id;
9762 let sketch = expect_sketch(sketch_object);
9763 let point_id = *sketch.segments.first().unwrap();
9764
9765 let (src_delta, scene_delta) = frontend
9766 .add_constraint(
9767 &mock_ctx,
9768 version,
9769 sketch_id,
9770 Constraint::Fixed(Fixed {
9771 points: vec![FixedPoint {
9772 point: point_id,
9773 position: Point2d {
9774 x: Number {
9775 value: 2.0,
9776 units: NumericSuffix::Mm,
9777 },
9778 y: Number {
9779 value: 3.0,
9780 units: NumericSuffix::Mm,
9781 },
9782 },
9783 }],
9784 }),
9785 )
9786 .await
9787 .unwrap();
9788 assert_eq!(
9789 src_delta.text.as_str(),
9790 "\
9791sketch(on = XY) {
9792 point1 = point(at = [var 1, var 2])
9793 fixed([point1, [2mm, 3mm]])
9794}
9795"
9796 );
9797 assert_eq!(
9798 scene_delta.new_graph.objects.len(),
9799 4,
9800 "{:#?}",
9801 scene_delta.new_graph.objects
9802 );
9803
9804 ctx.close().await;
9805 mock_ctx.close().await;
9806 }
9807
9808 #[tokio::test(flavor = "multi_thread")]
9809 async fn test_add_fixed_multiple_points() {
9810 let initial_source = "\
9811sketch(on = XY) {
9812 point(at = [var 1, var 2])
9813 point(at = [var 3, var 4])
9814}
9815";
9816
9817 let program = Program::parse(initial_source).unwrap().0.unwrap();
9818
9819 let mut frontend = FrontendState::new();
9820
9821 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9822 let mock_ctx = ExecutorContext::new_mock(None).await;
9823 let version = Version(0);
9824
9825 frontend.hack_set_program(&ctx, program).await.unwrap();
9826 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9827 let sketch_id = sketch_object.id;
9828 let sketch = expect_sketch(sketch_object);
9829 let point0_id = *sketch.segments.first().unwrap();
9830 let point1_id = *sketch.segments.get(1).unwrap();
9831
9832 let (src_delta, scene_delta) = frontend
9833 .add_constraint(
9834 &mock_ctx,
9835 version,
9836 sketch_id,
9837 Constraint::Fixed(Fixed {
9838 points: vec![
9839 FixedPoint {
9840 point: point0_id,
9841 position: Point2d {
9842 x: Number {
9843 value: 2.0,
9844 units: NumericSuffix::Mm,
9845 },
9846 y: Number {
9847 value: 3.0,
9848 units: NumericSuffix::Mm,
9849 },
9850 },
9851 },
9852 FixedPoint {
9853 point: point1_id,
9854 position: Point2d {
9855 x: Number {
9856 value: 4.0,
9857 units: NumericSuffix::Mm,
9858 },
9859 y: Number {
9860 value: 5.0,
9861 units: NumericSuffix::Mm,
9862 },
9863 },
9864 },
9865 ],
9866 }),
9867 )
9868 .await
9869 .unwrap();
9870 assert_eq!(
9871 src_delta.text.as_str(),
9872 "\
9873sketch(on = XY) {
9874 point1 = point(at = [var 1, var 2])
9875 point2 = point(at = [var 3, var 4])
9876 fixed([point1, [2mm, 3mm]])
9877 fixed([point2, [4mm, 5mm]])
9878}
9879"
9880 );
9881 assert_eq!(
9882 scene_delta.new_graph.objects.len(),
9883 6,
9884 "{:#?}",
9885 scene_delta.new_graph.objects
9886 );
9887
9888 ctx.close().await;
9889 mock_ctx.close().await;
9890 }
9891
9892 #[tokio::test(flavor = "multi_thread")]
9893 async fn test_add_fixed_owned_point() {
9894 let initial_source = "\
9895sketch(on = XY) {
9896 line(start = [var 1, var 2], end = [var 3, var 4])
9897}
9898";
9899
9900 let program = Program::parse(initial_source).unwrap().0.unwrap();
9901
9902 let mut frontend = FrontendState::new();
9903
9904 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9905 let mock_ctx = ExecutorContext::new_mock(None).await;
9906 let version = Version(0);
9907
9908 frontend.hack_set_program(&ctx, program).await.unwrap();
9909 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9910 let sketch_id = sketch_object.id;
9911 let sketch = expect_sketch(sketch_object);
9912 let line_start_id = *sketch.segments.first().unwrap();
9913
9914 let (src_delta, scene_delta) = frontend
9915 .add_constraint(
9916 &mock_ctx,
9917 version,
9918 sketch_id,
9919 Constraint::Fixed(Fixed {
9920 points: vec![FixedPoint {
9921 point: line_start_id,
9922 position: Point2d {
9923 x: Number {
9924 value: 2.0,
9925 units: NumericSuffix::Mm,
9926 },
9927 y: Number {
9928 value: 3.0,
9929 units: NumericSuffix::Mm,
9930 },
9931 },
9932 }],
9933 }),
9934 )
9935 .await
9936 .unwrap();
9937 assert_eq!(
9938 src_delta.text.as_str(),
9939 "\
9940sketch(on = XY) {
9941 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9942 fixed([line1.start, [2mm, 3mm]])
9943}
9944"
9945 );
9946 assert_eq!(
9947 scene_delta.new_graph.objects.len(),
9948 6,
9949 "{:#?}",
9950 scene_delta.new_graph.objects
9951 );
9952
9953 ctx.close().await;
9954 mock_ctx.close().await;
9955 }
9956
9957 #[tokio::test(flavor = "multi_thread")]
9958 async fn test_radius_error_cases() {
9959 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9960 let mock_ctx = ExecutorContext::new_mock(None).await;
9961 let version = Version(0);
9962
9963 let initial_source_point = "\
9965sketch(on = XY) {
9966 point(at = [var 1, var 2])
9967}
9968";
9969 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
9970 let mut frontend_point = FrontendState::new();
9971 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
9972 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
9973 let sketch_id_point = sketch_object_point.id;
9974 let sketch_point = expect_sketch(sketch_object_point);
9975 let point_id = *sketch_point.segments.first().unwrap();
9976
9977 let constraint_point = Constraint::Radius(Radius {
9978 arc: point_id,
9979 radius: Number {
9980 value: 5.0,
9981 units: NumericSuffix::Mm,
9982 },
9983 label_position: None,
9984 source: Default::default(),
9985 });
9986 let result_point = frontend_point
9987 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
9988 .await;
9989 assert!(result_point.is_err(), "Single point should error for radius");
9990
9991 let initial_source_line = "\
9993sketch(on = XY) {
9994 line(start = [var 1, var 2], end = [var 3, var 4])
9995}
9996";
9997 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
9998 let mut frontend_line = FrontendState::new();
9999 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10000 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10001 let sketch_id_line = sketch_object_line.id;
10002 let sketch_line = expect_sketch(sketch_object_line);
10003 let line_id = *sketch_line.segments.first().unwrap();
10004
10005 let constraint_line = Constraint::Radius(Radius {
10006 arc: line_id,
10007 radius: Number {
10008 value: 5.0,
10009 units: NumericSuffix::Mm,
10010 },
10011 label_position: None,
10012 source: Default::default(),
10013 });
10014 let result_line = frontend_line
10015 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10016 .await;
10017 assert!(result_line.is_err(), "Single line segment should error for radius");
10018
10019 ctx.close().await;
10020 mock_ctx.close().await;
10021 }
10022
10023 #[tokio::test(flavor = "multi_thread")]
10024 async fn test_diameter_single_arc_segment() {
10025 let initial_source = "\
10026sketch(on = XY) {
10027 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10028}
10029";
10030
10031 let program = Program::parse(initial_source).unwrap().0.unwrap();
10032
10033 let mut frontend = FrontendState::new();
10034
10035 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10036 let mock_ctx = ExecutorContext::new_mock(None).await;
10037 let version = Version(0);
10038
10039 frontend.hack_set_program(&ctx, program).await.unwrap();
10040 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10041 let sketch_id = sketch_object.id;
10042 let sketch = expect_sketch(sketch_object);
10043 let arc_id = sketch
10045 .segments
10046 .iter()
10047 .find(|&seg_id| {
10048 let obj = frontend.scene_graph.objects.get(seg_id.0);
10049 matches!(
10050 obj.map(|o| &o.kind),
10051 Some(ObjectKind::Segment {
10052 segment: Segment::Arc(_)
10053 })
10054 )
10055 })
10056 .unwrap();
10057
10058 let constraint = Constraint::Diameter(Diameter {
10059 arc: *arc_id,
10060 diameter: Number {
10061 value: 10.0,
10062 units: NumericSuffix::Mm,
10063 },
10064 label_position: None,
10065 source: Default::default(),
10066 });
10067 let (src_delta, scene_delta) = frontend
10068 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10069 .await
10070 .unwrap();
10071 assert_eq!(
10072 src_delta.text.as_str(),
10073 "\
10075sketch(on = XY) {
10076 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10077 diameter(arc1) == 10mm
10078}
10079"
10080 );
10081 assert_eq!(
10082 scene_delta.new_graph.objects.len(),
10083 7, "{:#?}",
10085 scene_delta.new_graph.objects
10086 );
10087
10088 ctx.close().await;
10089 mock_ctx.close().await;
10090 }
10091
10092 #[tokio::test(flavor = "multi_thread")]
10093 async fn test_diameter_single_arc_segment_with_label_position() {
10094 let initial_source = "\
10095sketch(on = XY) {
10096 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10097}
10098";
10099
10100 let program = Program::parse(initial_source).unwrap().0.unwrap();
10101 let mut frontend = FrontendState::new();
10102 let mock_ctx = ExecutorContext::new_mock(None).await;
10103 let version = Version(0);
10104
10105 frontend.program = program.clone();
10106 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10107 frontend.update_state_after_exec(outcome, true);
10108 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10109 let sketch_id = sketch_object.id;
10110 let sketch = expect_sketch(sketch_object);
10111 let arc_id = sketch
10112 .segments
10113 .iter()
10114 .find(|&seg_id| {
10115 let obj = frontend.scene_graph.objects.get(seg_id.0);
10116 matches!(
10117 obj.map(|o| &o.kind),
10118 Some(ObjectKind::Segment {
10119 segment: Segment::Arc(_)
10120 })
10121 )
10122 })
10123 .unwrap();
10124
10125 let label_position = Point2d {
10126 x: Number {
10127 value: 10.0,
10128 units: NumericSuffix::Mm,
10129 },
10130 y: Number {
10131 value: 11.0,
10132 units: NumericSuffix::Mm,
10133 },
10134 };
10135 let constraint = Constraint::Diameter(Diameter {
10136 arc: *arc_id,
10137 diameter: Number {
10138 value: 10.0,
10139 units: NumericSuffix::Mm,
10140 },
10141 label_position: Some(label_position.clone()),
10142 source: Default::default(),
10143 });
10144 let (src_delta, scene_delta) = frontend
10145 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10146 .await
10147 .unwrap();
10148 assert_eq!(
10149 src_delta.text.as_str(),
10150 "\
10151sketch(on = XY) {
10152 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10153 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10154}
10155"
10156 );
10157
10158 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10159 let sketch = expect_sketch(sketch_object);
10160 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10161 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10162 panic!("Expected constraint object");
10163 };
10164 let Constraint::Diameter(diameter) = constraint else {
10165 panic!("Expected diameter constraint");
10166 };
10167 assert_eq!(diameter.label_position, Some(label_position));
10168
10169 mock_ctx.close().await;
10170 }
10171
10172 #[tokio::test(flavor = "multi_thread")]
10173 async fn test_edit_diameter_constraint_label_position() {
10174 let initial_source = "\
10175sketch(on = XY) {
10176 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10177 diameter(arc1) == 10mm
10178}
10179";
10180
10181 let program = Program::parse(initial_source).unwrap().0.unwrap();
10182 let mut frontend = FrontendState::new();
10183 let mock_ctx = ExecutorContext::new_mock(None).await;
10184 let version = Version(0);
10185
10186 frontend.program = program.clone();
10187 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10188 frontend.update_state_after_exec(outcome, true);
10189 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10190 let sketch_id = sketch_object.id;
10191 let sketch = expect_sketch(sketch_object);
10192 let constraint_id = sketch.constraints[0];
10193 let label_position = Point2d {
10194 x: Number {
10195 value: 10.0,
10196 units: NumericSuffix::Mm,
10197 },
10198 y: Number {
10199 value: 11.0,
10200 units: NumericSuffix::Mm,
10201 },
10202 };
10203
10204 let (src_delta, scene_delta) = frontend
10205 .edit_distance_constraint_label_position(
10206 &mock_ctx,
10207 version,
10208 sketch_id,
10209 constraint_id,
10210 label_position.clone(),
10211 vec![],
10212 )
10213 .await
10214 .unwrap();
10215 assert_eq!(
10216 src_delta.text.as_str(),
10217 "\
10218sketch(on = XY) {
10219 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10220 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10221}
10222"
10223 );
10224
10225 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10226 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10227 panic!("Expected constraint object");
10228 };
10229 let Constraint::Diameter(diameter) = constraint else {
10230 panic!("Expected diameter constraint");
10231 };
10232 assert_eq!(diameter.label_position, Some(label_position));
10233
10234 mock_ctx.close().await;
10235 }
10236
10237 #[tokio::test(flavor = "multi_thread")]
10238 async fn test_diameter_error_cases() {
10239 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10240 let mock_ctx = ExecutorContext::new_mock(None).await;
10241 let version = Version(0);
10242
10243 let initial_source_point = "\
10245sketch(on = XY) {
10246 point(at = [var 1, var 2])
10247}
10248";
10249 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10250 let mut frontend_point = FrontendState::new();
10251 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
10252 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
10253 let sketch_id_point = sketch_object_point.id;
10254 let sketch_point = expect_sketch(sketch_object_point);
10255 let point_id = *sketch_point.segments.first().unwrap();
10256
10257 let constraint_point = Constraint::Diameter(Diameter {
10258 arc: point_id,
10259 diameter: Number {
10260 value: 10.0,
10261 units: NumericSuffix::Mm,
10262 },
10263 label_position: None,
10264 source: Default::default(),
10265 });
10266 let result_point = frontend_point
10267 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
10268 .await;
10269 assert!(result_point.is_err(), "Single point should error for diameter");
10270
10271 let initial_source_line = "\
10273sketch(on = XY) {
10274 line(start = [var 1, var 2], end = [var 3, var 4])
10275}
10276";
10277 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
10278 let mut frontend_line = FrontendState::new();
10279 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10280 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10281 let sketch_id_line = sketch_object_line.id;
10282 let sketch_line = expect_sketch(sketch_object_line);
10283 let line_id = *sketch_line.segments.first().unwrap();
10284
10285 let constraint_line = Constraint::Diameter(Diameter {
10286 arc: line_id,
10287 diameter: Number {
10288 value: 10.0,
10289 units: NumericSuffix::Mm,
10290 },
10291 label_position: None,
10292 source: Default::default(),
10293 });
10294 let result_line = frontend_line
10295 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10296 .await;
10297 assert!(result_line.is_err(), "Single line segment should error for diameter");
10298
10299 ctx.close().await;
10300 mock_ctx.close().await;
10301 }
10302
10303 #[tokio::test(flavor = "multi_thread")]
10304 async fn test_line_horizontal() {
10305 let initial_source = "\
10306sketch(on = XY) {
10307 line(start = [var 1, var 2], end = [var 3, var 4])
10308}
10309";
10310
10311 let program = Program::parse(initial_source).unwrap().0.unwrap();
10312
10313 let mut frontend = FrontendState::new();
10314
10315 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10316 let mock_ctx = ExecutorContext::new_mock(None).await;
10317 let version = Version(0);
10318
10319 frontend.hack_set_program(&ctx, program).await.unwrap();
10320 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10321 let sketch_id = sketch_object.id;
10322 let sketch = expect_sketch(sketch_object);
10323 let line1_id = *sketch.segments.get(2).unwrap();
10324
10325 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
10326 let (src_delta, scene_delta) = frontend
10327 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10328 .await
10329 .unwrap();
10330 assert_eq!(
10331 src_delta.text.as_str(),
10332 "\
10333sketch(on = XY) {
10334 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10335 horizontal(line1)
10336}
10337"
10338 );
10339 assert_eq!(
10340 scene_delta.new_graph.objects.len(),
10341 6,
10342 "{:#?}",
10343 scene_delta.new_graph.objects
10344 );
10345
10346 ctx.close().await;
10347 mock_ctx.close().await;
10348 }
10349
10350 #[tokio::test(flavor = "multi_thread")]
10351 async fn test_line_vertical() {
10352 let initial_source = "\
10353sketch(on = XY) {
10354 line(start = [var 1, var 2], end = [var 3, var 4])
10355}
10356";
10357
10358 let program = Program::parse(initial_source).unwrap().0.unwrap();
10359
10360 let mut frontend = FrontendState::new();
10361
10362 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10363 let mock_ctx = ExecutorContext::new_mock(None).await;
10364 let version = Version(0);
10365
10366 frontend.hack_set_program(&ctx, program).await.unwrap();
10367 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10368 let sketch_id = sketch_object.id;
10369 let sketch = expect_sketch(sketch_object);
10370 let line1_id = *sketch.segments.get(2).unwrap();
10371
10372 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
10373 let (src_delta, scene_delta) = frontend
10374 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10375 .await
10376 .unwrap();
10377 assert_eq!(
10378 src_delta.text.as_str(),
10379 "\
10380sketch(on = XY) {
10381 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10382 vertical(line1)
10383}
10384"
10385 );
10386 assert_eq!(
10387 scene_delta.new_graph.objects.len(),
10388 6,
10389 "{:#?}",
10390 scene_delta.new_graph.objects
10391 );
10392
10393 ctx.close().await;
10394 mock_ctx.close().await;
10395 }
10396
10397 #[tokio::test(flavor = "multi_thread")]
10398 async fn test_points_vertical() {
10399 let initial_source = "\
10400sketch001 = sketch(on = XY) {
10401 p0 = point(at = [var -2.23mm, var 3.1mm])
10402 pf = point(at = [4, 4])
10403}
10404";
10405
10406 let program = Program::parse(initial_source).unwrap().0.unwrap();
10407
10408 let mut frontend = FrontendState::new();
10409
10410 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10411 let mock_ctx = ExecutorContext::new_mock(None).await;
10412 let version = Version(0);
10413
10414 frontend.hack_set_program(&ctx, program).await.unwrap();
10415 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10416 let sketch_id = sketch_object.id;
10417 let sketch = expect_sketch(sketch_object);
10418 let point_ids = vec![
10419 sketch.segments.first().unwrap().to_owned(),
10420 sketch.segments.get(1).unwrap().to_owned(),
10421 ];
10422
10423 let constraint = Constraint::Vertical(Vertical::Points {
10424 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10425 });
10426 let (src_delta, scene_delta) = frontend
10427 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10428 .await
10429 .unwrap();
10430 assert_eq!(
10431 src_delta.text.as_str(),
10432 "\
10433sketch001 = sketch(on = XY) {
10434 p0 = point(at = [var -2.23mm, var 3.1mm])
10435 pf = point(at = [4, 4])
10436 vertical([p0, pf])
10437}
10438"
10439 );
10440 assert_eq!(
10441 scene_delta.new_graph.objects.len(),
10442 5,
10443 "{:#?}",
10444 scene_delta.new_graph.objects
10445 );
10446
10447 ctx.close().await;
10448 mock_ctx.close().await;
10449 }
10450
10451 #[tokio::test(flavor = "multi_thread")]
10452 async fn test_points_horizontal() {
10453 let initial_source = "\
10454sketch001 = sketch(on = XY) {
10455 p0 = point(at = [var -2.23mm, var 3.1mm])
10456 pf = point(at = [4, 4])
10457}
10458";
10459
10460 let program = Program::parse(initial_source).unwrap().0.unwrap();
10461
10462 let mut frontend = FrontendState::new();
10463
10464 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10465 let mock_ctx = ExecutorContext::new_mock(None).await;
10466 let version = Version(0);
10467
10468 frontend.hack_set_program(&ctx, program).await.unwrap();
10469 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10470 let sketch_id = sketch_object.id;
10471 let sketch = expect_sketch(sketch_object);
10472 let point_ids = vec![
10473 sketch.segments.first().unwrap().to_owned(),
10474 sketch.segments.get(1).unwrap().to_owned(),
10475 ];
10476
10477 let constraint = Constraint::Horizontal(Horizontal::Points {
10478 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10479 });
10480 let (src_delta, scene_delta) = frontend
10481 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10482 .await
10483 .unwrap();
10484 assert_eq!(
10485 src_delta.text.as_str(),
10486 "\
10487sketch001 = sketch(on = XY) {
10488 p0 = point(at = [var -2.23mm, var 3.1mm])
10489 pf = point(at = [4, 4])
10490 horizontal([p0, pf])
10491}
10492"
10493 );
10494 assert_eq!(
10495 scene_delta.new_graph.objects.len(),
10496 5,
10497 "{:#?}",
10498 scene_delta.new_graph.objects
10499 );
10500
10501 ctx.close().await;
10502 mock_ctx.close().await;
10503 }
10504
10505 #[tokio::test(flavor = "multi_thread")]
10506 async fn test_point_horizontal_with_origin() {
10507 let initial_source = "\
10508sketch001 = sketch(on = XY) {
10509 p0 = point(at = [var -2.23mm, var 3.1mm])
10510}
10511";
10512
10513 let program = Program::parse(initial_source).unwrap().0.unwrap();
10514
10515 let mut frontend = FrontendState::new();
10516
10517 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10518 let mock_ctx = ExecutorContext::new_mock(None).await;
10519 let version = Version(0);
10520
10521 frontend.hack_set_program(&ctx, program).await.unwrap();
10522 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10523 let sketch_id = sketch_object.id;
10524 let sketch = expect_sketch(sketch_object);
10525 let point_id = *sketch.segments.first().unwrap();
10526
10527 let constraint = Constraint::Horizontal(Horizontal::Points {
10528 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
10529 });
10530 let (src_delta, scene_delta) = frontend
10531 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10532 .await
10533 .unwrap();
10534 assert_eq!(
10535 src_delta.text.as_str(),
10536 "\
10537sketch001 = sketch(on = XY) {
10538 p0 = point(at = [var -2.23mm, var 3.1mm])
10539 horizontal([p0, ORIGIN])
10540}
10541"
10542 );
10543 assert_eq!(
10544 scene_delta.new_graph.objects.len(),
10545 4,
10546 "{:#?}",
10547 scene_delta.new_graph.objects
10548 );
10549
10550 ctx.close().await;
10551 mock_ctx.close().await;
10552 }
10553
10554 #[tokio::test(flavor = "multi_thread")]
10555 async fn test_lines_equal_length() {
10556 let initial_source = "\
10557sketch(on = XY) {
10558 line(start = [var 1, var 2], end = [var 3, var 4])
10559 line(start = [var 5, var 6], end = [var 7, var 8])
10560}
10561";
10562
10563 let program = Program::parse(initial_source).unwrap().0.unwrap();
10564
10565 let mut frontend = FrontendState::new();
10566
10567 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10568 let mock_ctx = ExecutorContext::new_mock(None).await;
10569 let version = Version(0);
10570
10571 frontend.hack_set_program(&ctx, program).await.unwrap();
10572 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10573 let sketch_id = sketch_object.id;
10574 let sketch = expect_sketch(sketch_object);
10575 let line1_id = *sketch.segments.get(2).unwrap();
10576 let line2_id = *sketch.segments.get(5).unwrap();
10577
10578 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10579 lines: vec![line1_id, line2_id],
10580 });
10581 let (src_delta, scene_delta) = frontend
10582 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10583 .await
10584 .unwrap();
10585 assert_eq!(
10586 src_delta.text.as_str(),
10587 "\
10588sketch(on = XY) {
10589 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10590 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10591 equalLength([line1, line2])
10592}
10593"
10594 );
10595 assert_eq!(
10596 scene_delta.new_graph.objects.len(),
10597 9,
10598 "{:#?}",
10599 scene_delta.new_graph.objects
10600 );
10601
10602 ctx.close().await;
10603 mock_ctx.close().await;
10604 }
10605
10606 #[tokio::test(flavor = "multi_thread")]
10607 async fn test_add_constraint_multi_line_equal_length() {
10608 let initial_source = "\
10609sketch(on = XY) {
10610 line(start = [var 1, var 2], end = [var 3, var 4])
10611 line(start = [var 5, var 6], end = [var 7, var 8])
10612 line(start = [var 9, var 10], end = [var 11, var 12])
10613}
10614";
10615
10616 let program = Program::parse(initial_source).unwrap().0.unwrap();
10617
10618 let mut frontend = FrontendState::new();
10619 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10620 let mock_ctx = ExecutorContext::new_mock(None).await;
10621 let version = Version(0);
10622
10623 frontend.hack_set_program(&ctx, program).await.unwrap();
10624 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10625 let sketch_id = sketch_object.id;
10626 let sketch = expect_sketch(sketch_object);
10627 let line1_id = *sketch.segments.get(2).unwrap();
10628 let line2_id = *sketch.segments.get(5).unwrap();
10629 let line3_id = *sketch.segments.get(8).unwrap();
10630
10631 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10632 lines: vec![line1_id, line2_id, line3_id],
10633 });
10634 let (src_delta, scene_delta) = frontend
10635 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10636 .await
10637 .unwrap();
10638 assert_eq!(
10639 src_delta.text.as_str(),
10640 "\
10641sketch(on = XY) {
10642 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10643 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10644 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10645 equalLength([line1, line2, line3])
10646}
10647"
10648 );
10649 let constraints = scene_delta
10650 .new_graph
10651 .objects
10652 .iter()
10653 .filter_map(|obj| {
10654 let ObjectKind::Constraint { constraint } = &obj.kind else {
10655 return None;
10656 };
10657 Some(constraint)
10658 })
10659 .collect::<Vec<_>>();
10660
10661 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
10662 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
10663 panic!("expected equal length constraint, got {:?}", constraints[0]);
10664 };
10665 assert_eq!(lines_equal_length.lines.len(), 3);
10666
10667 ctx.close().await;
10668 mock_ctx.close().await;
10669 }
10670
10671 #[tokio::test(flavor = "multi_thread")]
10672 async fn test_lines_parallel() {
10673 let initial_source = "\
10674sketch(on = XY) {
10675 line(start = [var 1, var 2], end = [var 3, var 4])
10676 line(start = [var 5, var 6], end = [var 7, var 8])
10677}
10678";
10679
10680 let program = Program::parse(initial_source).unwrap().0.unwrap();
10681
10682 let mut frontend = FrontendState::new();
10683
10684 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10685 let mock_ctx = ExecutorContext::new_mock(None).await;
10686 let version = Version(0);
10687
10688 frontend.hack_set_program(&ctx, program).await.unwrap();
10689 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10690 let sketch_id = sketch_object.id;
10691 let sketch = expect_sketch(sketch_object);
10692 let line1_id = *sketch.segments.get(2).unwrap();
10693 let line2_id = *sketch.segments.get(5).unwrap();
10694
10695 let constraint = Constraint::Parallel(Parallel {
10696 lines: vec![line1_id, line2_id],
10697 });
10698 let (src_delta, scene_delta) = frontend
10699 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10700 .await
10701 .unwrap();
10702 assert_eq!(
10703 src_delta.text.as_str(),
10704 "\
10705sketch(on = XY) {
10706 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10707 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10708 parallel([line1, line2])
10709}
10710"
10711 );
10712 assert_eq!(
10713 scene_delta.new_graph.objects.len(),
10714 9,
10715 "{:#?}",
10716 scene_delta.new_graph.objects
10717 );
10718
10719 ctx.close().await;
10720 mock_ctx.close().await;
10721 }
10722
10723 #[tokio::test(flavor = "multi_thread")]
10724 async fn test_lines_parallel_multiline() {
10725 let initial_source = "\
10726sketch(on = XY) {
10727 line(start = [var 1, var 2], end = [var 3, var 4])
10728 line(start = [var 5, var 6], end = [var 7, var 8])
10729 line(start = [var 9, var 10], end = [var 11, var 12])
10730}
10731";
10732
10733 let program = Program::parse(initial_source).unwrap().0.unwrap();
10734
10735 let mut frontend = FrontendState::new();
10736
10737 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10738 let mock_ctx = ExecutorContext::new_mock(None).await;
10739 let version = Version(0);
10740
10741 frontend.hack_set_program(&ctx, program).await.unwrap();
10742 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10743 let sketch_id = sketch_object.id;
10744 let sketch = expect_sketch(sketch_object);
10745 let line1_id = *sketch.segments.get(2).unwrap();
10746 let line2_id = *sketch.segments.get(5).unwrap();
10747 let line3_id = *sketch.segments.get(8).unwrap();
10748
10749 let constraint = Constraint::Parallel(Parallel {
10750 lines: vec![line1_id, line2_id, line3_id],
10751 });
10752 let (src_delta, scene_delta) = frontend
10753 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10754 .await
10755 .unwrap();
10756 assert_eq!(
10757 src_delta.text.as_str(),
10758 "\
10759sketch(on = XY) {
10760 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10761 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10762 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10763 parallel([line1, line2, line3])
10764}
10765"
10766 );
10767
10768 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10769 let sketch = expect_sketch(sketch_object);
10770 assert_eq!(sketch.constraints.len(), 1);
10771
10772 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10773 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10774 panic!("Expected constraint object");
10775 };
10776 let Constraint::Parallel(parallel) = constraint else {
10777 panic!("Expected parallel constraint");
10778 };
10779 assert_eq!(parallel.lines.len(), 3);
10780
10781 ctx.close().await;
10782 mock_ctx.close().await;
10783 }
10784
10785 #[tokio::test(flavor = "multi_thread")]
10786 async fn test_lines_perpendicular() {
10787 let initial_source = "\
10788sketch(on = XY) {
10789 line(start = [var 1, var 2], end = [var 3, var 4])
10790 line(start = [var 5, var 6], end = [var 7, var 8])
10791}
10792";
10793
10794 let program = Program::parse(initial_source).unwrap().0.unwrap();
10795
10796 let mut frontend = FrontendState::new();
10797
10798 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10799 let mock_ctx = ExecutorContext::new_mock(None).await;
10800 let version = Version(0);
10801
10802 frontend.hack_set_program(&ctx, program).await.unwrap();
10803 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10804 let sketch_id = sketch_object.id;
10805 let sketch = expect_sketch(sketch_object);
10806 let line1_id = *sketch.segments.get(2).unwrap();
10807 let line2_id = *sketch.segments.get(5).unwrap();
10808
10809 let constraint = Constraint::Perpendicular(Perpendicular {
10810 lines: vec![line1_id, line2_id],
10811 });
10812 let (src_delta, scene_delta) = frontend
10813 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10814 .await
10815 .unwrap();
10816 assert_eq!(
10817 src_delta.text.as_str(),
10818 "\
10819sketch(on = XY) {
10820 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10821 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10822 perpendicular([line1, line2])
10823}
10824"
10825 );
10826 assert_eq!(
10827 scene_delta.new_graph.objects.len(),
10828 9,
10829 "{:#?}",
10830 scene_delta.new_graph.objects
10831 );
10832
10833 ctx.close().await;
10834 mock_ctx.close().await;
10835 }
10836
10837 #[tokio::test(flavor = "multi_thread")]
10838 async fn test_lines_angle() {
10839 let initial_source = "\
10840sketch(on = XY) {
10841 line(start = [var 1, var 2], end = [var 3, var 4])
10842 line(start = [var 5, var 6], end = [var 7, var 8])
10843}
10844";
10845
10846 let program = Program::parse(initial_source).unwrap().0.unwrap();
10847
10848 let mut frontend = FrontendState::new();
10849
10850 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10851 let mock_ctx = ExecutorContext::new_mock(None).await;
10852 let version = Version(0);
10853
10854 frontend.hack_set_program(&ctx, program).await.unwrap();
10855 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10856 let sketch_id = sketch_object.id;
10857 let sketch = expect_sketch(sketch_object);
10858 let line1_id = *sketch.segments.get(2).unwrap();
10859 let line2_id = *sketch.segments.get(5).unwrap();
10860
10861 let constraint = Constraint::Angle(Angle {
10862 lines: vec![line1_id, line2_id],
10863 angle: Number {
10864 value: 30.0,
10865 units: NumericSuffix::Deg,
10866 },
10867 source: Default::default(),
10868 });
10869 let (src_delta, scene_delta) = frontend
10870 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10871 .await
10872 .unwrap();
10873 assert_eq!(
10874 src_delta.text.as_str(),
10875 "\
10877sketch(on = XY) {
10878 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10879 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10880 angle([line1, line2]) == 30deg
10881}
10882"
10883 );
10884 assert_eq!(
10885 scene_delta.new_graph.objects.len(),
10886 9,
10887 "{:#?}",
10888 scene_delta.new_graph.objects
10889 );
10890
10891 ctx.close().await;
10892 mock_ctx.close().await;
10893 }
10894
10895 #[tokio::test(flavor = "multi_thread")]
10896 async fn test_segments_tangent() {
10897 let initial_source = "\
10898sketch(on = XY) {
10899 line(start = [var 1, var 2], end = [var 3, var 4])
10900 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10901}
10902";
10903
10904 let program = Program::parse(initial_source).unwrap().0.unwrap();
10905
10906 let mut frontend = FrontendState::new();
10907
10908 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10909 let mock_ctx = ExecutorContext::new_mock(None).await;
10910 let version = Version(0);
10911
10912 frontend.hack_set_program(&ctx, program).await.unwrap();
10913 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10914 let sketch_id = sketch_object.id;
10915 let sketch = expect_sketch(sketch_object);
10916 let line1_id = *sketch.segments.get(2).unwrap();
10917 let arc1_id = *sketch.segments.get(6).unwrap();
10918
10919 let constraint = Constraint::Tangent(Tangent {
10920 input: vec![line1_id, arc1_id],
10921 });
10922 let (src_delta, scene_delta) = frontend
10923 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10924 .await
10925 .unwrap();
10926 assert_eq!(
10927 src_delta.text.as_str(),
10928 "\
10929sketch(on = XY) {
10930 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10931 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10932 tangent([line1, arc1])
10933}
10934"
10935 );
10936 assert_eq!(
10937 scene_delta.new_graph.objects.len(),
10938 10,
10939 "{:#?}",
10940 scene_delta.new_graph.objects
10941 );
10942
10943 ctx.close().await;
10944 mock_ctx.close().await;
10945 }
10946
10947 #[tokio::test(flavor = "multi_thread")]
10948 async fn test_point_midpoint() {
10949 let initial_source = "\
10950sketch(on = XY) {
10951 point(at = [var 1, var 1])
10952 line(start = [var 0, var 0], end = [var 6, var 4])
10953}
10954";
10955
10956 let program = Program::parse(initial_source).unwrap().0.unwrap();
10957
10958 let mut frontend = FrontendState::new();
10959
10960 let ctx = ExecutorContext::new_mock(None).await;
10961 let version = Version(0);
10962
10963 frontend.program = program.clone();
10964 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10965 frontend.update_state_after_exec(outcome, true);
10966 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10967 let sketch_id = sketch_object.id;
10968 let sketch = expect_sketch(sketch_object);
10969 let point_id = *sketch.segments.first().unwrap();
10970 let line_id = *sketch.segments.get(3).unwrap();
10971
10972 let constraint = Constraint::Midpoint(Midpoint {
10973 point: point_id,
10974 segment: line_id,
10975 });
10976 let (src_delta, scene_delta) = frontend
10977 .add_constraint(&ctx, version, sketch_id, constraint)
10978 .await
10979 .unwrap();
10980 assert_eq!(
10981 src_delta.text.as_str(),
10982 "\
10983sketch(on = XY) {
10984 point1 = point(at = [var 1, var 1])
10985 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10986 midpoint(line1, point = point1)
10987}
10988"
10989 );
10990 assert_eq!(
10991 scene_delta.new_graph.objects.len(),
10992 7,
10993 "{:#?}",
10994 scene_delta.new_graph.objects
10995 );
10996
10997 ctx.close().await;
10998 }
10999
11000 #[tokio::test(flavor = "multi_thread")]
11001 async fn test_segments_symmetric() {
11002 let initial_source = "\
11003sketch(on = XY) {
11004 line(start = [var 0, var 0], end = [var 0, var 4])
11005 line(start = [var 4, var 0], end = [var 4, var 4])
11006 line(start = [var 2, var -1], end = [var 2, var 5])
11007}
11008";
11009
11010 let program = Program::parse(initial_source).unwrap().0.unwrap();
11011
11012 let mut frontend = FrontendState::new();
11013
11014 let ctx = ExecutorContext::new_mock(None).await;
11015 let version = Version(0);
11016
11017 frontend.program = program.clone();
11018 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11019 frontend.update_state_after_exec(outcome, true);
11020 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11021 let sketch_id = sketch_object.id;
11022 let sketch = expect_sketch(sketch_object);
11023 let line1_id = *sketch.segments.get(2).unwrap();
11024 let line2_id = *sketch.segments.get(5).unwrap();
11025 let axis_id = *sketch.segments.get(8).unwrap();
11026
11027 let constraint = Constraint::Symmetric(Symmetric {
11028 input: vec![line1_id, line2_id],
11029 axis: axis_id,
11030 });
11031 let (src_delta, scene_delta) = frontend
11032 .add_constraint(&ctx, version, sketch_id, constraint)
11033 .await
11034 .unwrap();
11035 assert_eq!(
11036 src_delta.text.as_str(),
11037 "\
11038sketch(on = XY) {
11039 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
11040 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
11041 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
11042 symmetric([line1, line2], axis = line3)
11043}
11044"
11045 );
11046 assert_eq!(
11047 scene_delta.new_graph.objects.len(),
11048 12,
11049 "{:#?}",
11050 scene_delta.new_graph.objects
11051 );
11052
11053 ctx.close().await;
11054 }
11055
11056 #[tokio::test(flavor = "multi_thread")]
11057 async fn test_point_arc_midpoint() {
11058 let initial_source = "\
11059sketch(on = XY) {
11060 point(at = [var 6, var 3])
11061 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11062}
11063";
11064
11065 let program = Program::parse(initial_source).unwrap().0.unwrap();
11066
11067 let mut frontend = FrontendState::new();
11068
11069 let ctx = ExecutorContext::new_mock(None).await;
11070 let version = Version(0);
11071
11072 frontend.program = program.clone();
11073 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11074 frontend.update_state_after_exec(outcome, true);
11075 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11076 let sketch_id = sketch_object.id;
11077 let sketch = expect_sketch(sketch_object);
11078 let point_id = *sketch.segments.first().unwrap();
11079 let arc_id = *sketch.segments.get(4).unwrap();
11080
11081 let constraint = Constraint::Midpoint(Midpoint {
11082 point: point_id,
11083 segment: arc_id,
11084 });
11085 let (src_delta, scene_delta) = frontend
11086 .add_constraint(&ctx, version, sketch_id, constraint)
11087 .await
11088 .unwrap();
11089 assert_eq!(
11090 src_delta.text.as_str(),
11091 "\
11092sketch(on = XY) {
11093 point1 = point(at = [var 6, var 3])
11094 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11095 midpoint(arc1, point = point1)
11096}
11097"
11098 );
11099 assert_eq!(
11100 scene_delta.new_graph.objects.len(),
11101 8,
11102 "{:#?}",
11103 scene_delta.new_graph.objects
11104 );
11105
11106 ctx.close().await;
11107 }
11108
11109 #[tokio::test(flavor = "multi_thread")]
11110 async fn test_segments_symmetric_arcs() {
11111 let initial_source = "\
11112sketch(on = XY) {
11113 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11114 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11115 line(start = [var 0, var -10], end = [var 0, var 10])
11116}
11117";
11118
11119 let program = Program::parse(initial_source).unwrap().0.unwrap();
11120
11121 let mut frontend = FrontendState::new();
11122
11123 let ctx = ExecutorContext::new_mock(None).await;
11124 let version = Version(0);
11125
11126 frontend.program = program.clone();
11127 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11128 frontend.update_state_after_exec(outcome, true);
11129 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11130 let sketch_id = sketch_object.id;
11131 let sketch = expect_sketch(sketch_object);
11132 let arc1_id = *sketch.segments.get(3).unwrap();
11133 let arc2_id = *sketch.segments.get(7).unwrap();
11134 let axis_id = *sketch.segments.get(10).unwrap();
11135
11136 let constraint = Constraint::Symmetric(Symmetric {
11137 input: vec![arc1_id, arc2_id],
11138 axis: axis_id,
11139 });
11140 let (src_delta, scene_delta) = frontend
11141 .add_constraint(&ctx, version, sketch_id, constraint)
11142 .await
11143 .unwrap();
11144 assert_eq!(
11145 src_delta.text.as_str(),
11146 "\
11147sketch(on = XY) {
11148 arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11149 arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11150 line1 = line(start = [var 0, var -10], end = [var 0, var 10])
11151 symmetric([arc1, arc2], axis = line1)
11152}
11153"
11154 );
11155 assert_eq!(
11156 scene_delta.new_graph.objects.len(),
11157 14,
11158 "{:#?}",
11159 scene_delta.new_graph.objects
11160 );
11161
11162 ctx.close().await;
11163 }
11164
11165 #[tokio::test(flavor = "multi_thread")]
11166 async fn test_sketch_on_face_simple() {
11167 let initial_source = "\
11168len = 2mm
11169cube = startSketchOn(XY)
11170 |> startProfile(at = [0, 0])
11171 |> line(end = [len, 0], tag = $side)
11172 |> line(end = [0, len])
11173 |> line(end = [-len, 0])
11174 |> line(end = [0, -len])
11175 |> close()
11176 |> extrude(length = len)
11177
11178face = faceOf(cube, face = side)
11179";
11180
11181 let program = Program::parse(initial_source).unwrap().0.unwrap();
11182
11183 let mut frontend = FrontendState::new();
11184
11185 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11186 let mock_ctx = ExecutorContext::new_mock(None).await;
11187 let version = Version(0);
11188
11189 frontend.hack_set_program(&ctx, program).await.unwrap();
11190 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
11191 let face_id = face_object.id;
11192
11193 let sketch_args = SketchCtor {
11194 on: Plane::Object(face_id),
11195 };
11196 let (_src_delta, scene_delta, sketch_id) = frontend
11197 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11198 .await
11199 .unwrap();
11200 assert_eq!(sketch_id, ObjectId(2));
11201 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11202 let sketch_object = &scene_delta.new_graph.objects[2];
11203 assert_eq!(sketch_object.id, ObjectId(2));
11204 assert_eq!(
11205 sketch_object.kind,
11206 ObjectKind::Sketch(Sketch {
11207 args: SketchCtor {
11208 on: Plane::Object(face_id),
11209 },
11210 plane: face_id,
11211 segments: vec![],
11212 constraints: vec![],
11213 })
11214 );
11215 assert_eq!(scene_delta.new_graph.objects.len(), 8);
11216
11217 ctx.close().await;
11218 mock_ctx.close().await;
11219 }
11220
11221 #[tokio::test(flavor = "multi_thread")]
11222 async fn test_sketch_on_wall_artifact_from_region_extrude() {
11223 let initial_source = "\
11224s = sketch(on = YZ) {
11225 line1 = line(start = [0, 0], end = [0, 1])
11226 line2 = line(start = [0, 1], end = [1, 1])
11227 line3 = line(start = [1, 1], end = [0, 0])
11228}
11229region001 = region(point = [0.1, 0.1], sketch = s)
11230extrude001 = extrude(region001, length = 5)
11231";
11232
11233 let program = Program::parse(initial_source).unwrap().0.unwrap();
11234
11235 let mut frontend = FrontendState::new();
11236 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11237 let version = Version(0);
11238
11239 frontend.hack_set_program(&ctx, program).await.unwrap();
11240 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11241
11242 let sketch_args = SketchCtor {
11243 on: Plane::Object(wall_object_id),
11244 };
11245 let (src_delta, _scene_delta, _sketch_id) = frontend
11246 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11247 .await
11248 .unwrap();
11249 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11250
11251 ctx.close().await;
11252 }
11253
11254 #[tokio::test(flavor = "multi_thread")]
11255 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
11256 let initial_source = "\
11257sketch001 = sketch(on = YZ) {
11258 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
11259 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
11260 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
11261 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
11262 coincident([line1.end, line2.start])
11263 coincident([line2.end, line3.start])
11264 coincident([line3.end, line4.start])
11265 coincident([line4.end, line1.start])
11266 parallel([line2, line4])
11267 parallel([line3, line1])
11268 perpendicular([line1, line2])
11269 horizontal(line3)
11270 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
11271}
11272region001 = region(point = [3.1, 3.74], sketch = sketch001)
11273extrude001 = extrude(region001, length = 5)
11274";
11275
11276 let program = Program::parse(initial_source).unwrap().0.unwrap();
11277
11278 let mut frontend = FrontendState::new();
11279 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11280 let version = Version(0);
11281
11282 frontend.hack_set_program(&ctx, program).await.unwrap();
11283 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11284
11285 let sketch_args = SketchCtor {
11286 on: Plane::Object(wall_object_id),
11287 };
11288 let (src_delta, _scene_delta, _sketch_id) = frontend
11289 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11290 .await
11291 .unwrap();
11292 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11293
11294 ctx.close().await;
11295 }
11296
11297 #[tokio::test(flavor = "multi_thread")]
11298 async fn test_sketch_on_plane_incremental() {
11299 let initial_source = "\
11300len = 2mm
11301cube = startSketchOn(XY)
11302 |> startProfile(at = [0, 0])
11303 |> line(end = [len, 0], tag = $side)
11304 |> line(end = [0, len])
11305 |> line(end = [-len, 0])
11306 |> line(end = [0, -len])
11307 |> close()
11308 |> extrude(length = len)
11309
11310plane = planeOf(cube, face = side)
11311";
11312
11313 let program = Program::parse(initial_source).unwrap().0.unwrap();
11314
11315 let mut frontend = FrontendState::new();
11316
11317 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11318 let mock_ctx = ExecutorContext::new_mock(None).await;
11319 let version = Version(0);
11320
11321 frontend.hack_set_program(&ctx, program).await.unwrap();
11322 let plane_object = frontend
11324 .scene_graph
11325 .objects
11326 .iter()
11327 .rev()
11328 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
11329 .unwrap();
11330 let plane_id = plane_object.id;
11331
11332 let sketch_args = SketchCtor {
11333 on: Plane::Object(plane_id),
11334 };
11335 let (src_delta, scene_delta, sketch_id) = frontend
11336 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11337 .await
11338 .unwrap();
11339 assert_eq!(
11340 src_delta.text.as_str(),
11341 "\
11342len = 2mm
11343cube = startSketchOn(XY)
11344 |> startProfile(at = [0, 0])
11345 |> line(end = [len, 0], tag = $side)
11346 |> line(end = [0, len])
11347 |> line(end = [-len, 0])
11348 |> line(end = [0, -len])
11349 |> close()
11350 |> extrude(length = len)
11351
11352plane = planeOf(cube, face = side)
11353sketch001 = sketch(on = plane) {
11354}
11355"
11356 );
11357 assert_eq!(sketch_id, ObjectId(2));
11358 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11359 let sketch_object = &scene_delta.new_graph.objects[2];
11360 assert_eq!(sketch_object.id, ObjectId(2));
11361 assert_eq!(
11362 sketch_object.kind,
11363 ObjectKind::Sketch(Sketch {
11364 args: SketchCtor {
11365 on: Plane::Object(plane_id),
11366 },
11367 plane: plane_id,
11368 segments: vec![],
11369 constraints: vec![],
11370 })
11371 );
11372 assert_eq!(scene_delta.new_graph.objects.len(), 9);
11373
11374 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
11375 assert_eq!(plane_object.id, plane_id);
11376 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
11377
11378 ctx.close().await;
11379 mock_ctx.close().await;
11380 }
11381
11382 #[tokio::test(flavor = "multi_thread")]
11383 async fn test_new_sketch_uses_unique_variable_name() {
11384 let initial_source = "\
11385sketch1 = sketch(on = XY) {
11386}
11387";
11388
11389 let program = Program::parse(initial_source).unwrap().0.unwrap();
11390
11391 let mut frontend = FrontendState::new();
11392 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11393 let version = Version(0);
11394
11395 frontend.hack_set_program(&ctx, program).await.unwrap();
11396
11397 let sketch_args = SketchCtor {
11398 on: Plane::Default(PlaneName::Yz),
11399 };
11400 let (src_delta, _, _) = frontend
11401 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11402 .await
11403 .unwrap();
11404
11405 assert_eq!(
11406 src_delta.text.as_str(),
11407 "\
11408sketch1 = sketch(on = XY) {
11409}
11410sketch001 = sketch(on = YZ) {
11411}
11412"
11413 );
11414
11415 ctx.close().await;
11416 }
11417
11418 #[tokio::test(flavor = "multi_thread")]
11419 async fn test_new_sketch_twice_using_same_plane() {
11420 let initial_source = "\
11421sketch1 = sketch(on = XY) {
11422}
11423";
11424
11425 let program = Program::parse(initial_source).unwrap().0.unwrap();
11426
11427 let mut frontend = FrontendState::new();
11428 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11429 let version = Version(0);
11430
11431 frontend.hack_set_program(&ctx, program).await.unwrap();
11432
11433 let sketch_args = SketchCtor {
11434 on: Plane::Default(PlaneName::Xy),
11435 };
11436 let (src_delta, _, _) = frontend
11437 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11438 .await
11439 .unwrap();
11440
11441 assert_eq!(
11442 src_delta.text.as_str(),
11443 "\
11444sketch1 = sketch(on = XY) {
11445}
11446sketch001 = sketch(on = XY) {
11447}
11448"
11449 );
11450
11451 ctx.close().await;
11452 }
11453
11454 #[tokio::test(flavor = "multi_thread")]
11455 async fn test_sketch_mode_reuses_cached_on_expression() {
11456 let initial_source = "\
11457width = 2mm
11458sketch(on = offsetPlane(XY, offset = width)) {
11459 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
11460 distance([line1.start, line1.end]) == width
11461}
11462";
11463 let program = Program::parse(initial_source).unwrap().0.unwrap();
11464
11465 let mut frontend = FrontendState::new();
11466 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11467 let mock_ctx = ExecutorContext::new_mock(None).await;
11468 let version = Version(0);
11469 let project_id = ProjectId(0);
11470 let file_id = FileId(0);
11471
11472 frontend.hack_set_program(&ctx, program).await.unwrap();
11473 let initial_object_count = frontend.scene_graph.objects.len();
11474 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
11475 .expect("Expected sketch object to exist")
11476 .id;
11477
11478 let scene_delta = frontend
11481 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11482 .await
11483 .unwrap();
11484 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11485
11486 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
11489 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11490
11491 ctx.close().await;
11492 mock_ctx.close().await;
11493 }
11494
11495 #[tokio::test(flavor = "multi_thread")]
11496 async fn test_multiple_sketch_blocks() {
11497 let initial_source = "\
11498// Cube that requires the engine.
11499width = 2
11500sketch001 = startSketchOn(XY)
11501profile001 = startProfile(sketch001, at = [0, 0])
11502 |> yLine(length = width, tag = $seg1)
11503 |> xLine(length = width)
11504 |> yLine(length = -width)
11505 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11506 |> close()
11507extrude001 = extrude(profile001, length = width)
11508
11509// Get a value that requires the engine.
11510x = segLen(seg1)
11511
11512// Triangle with side length 2*x.
11513sketch(on = XY) {
11514 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11515 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11516 coincident([line1.end, line2.start])
11517 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11518 coincident([line2.end, line3.start])
11519 coincident([line3.end, line1.start])
11520 equalLength([line3, line1])
11521 equalLength([line1, line2])
11522 distance([line1.start, line1.end]) == 2*x
11523}
11524
11525// Line segment with length x.
11526sketch2 = sketch(on = XY) {
11527 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11528 distance([line1.start, line1.end]) == x
11529}
11530";
11531
11532 let program = Program::parse(initial_source).unwrap().0.unwrap();
11533
11534 let mut frontend = FrontendState::new();
11535
11536 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11537 let mock_ctx = ExecutorContext::new_mock(None).await;
11538 let version = Version(0);
11539 let project_id = ProjectId(0);
11540 let file_id = FileId(0);
11541
11542 frontend.hack_set_program(&ctx, program).await.unwrap();
11543 let sketch_objects = frontend
11544 .scene_graph
11545 .objects
11546 .iter()
11547 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
11548 .collect::<Vec<_>>();
11549 let sketch1_id = sketch_objects.first().unwrap().id;
11550 let sketch2_id = sketch_objects.get(1).unwrap().id;
11551 let point1_id = ObjectId(sketch1_id.0 + 1);
11553 let point2_id = ObjectId(sketch2_id.0 + 1);
11555
11556 let scene_delta = frontend
11565 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11566 .await
11567 .unwrap();
11568 assert_eq!(
11569 scene_delta.new_graph.objects.len(),
11570 18,
11571 "{:#?}",
11572 scene_delta.new_graph.objects
11573 );
11574
11575 let point_ctor = PointCtor {
11577 position: Point2d {
11578 x: Expr::Var(Number {
11579 value: 1.0,
11580 units: NumericSuffix::Mm,
11581 }),
11582 y: Expr::Var(Number {
11583 value: 2.0,
11584 units: NumericSuffix::Mm,
11585 }),
11586 },
11587 };
11588 let segments = vec![ExistingSegmentCtor {
11589 id: point1_id,
11590 ctor: SegmentCtor::Point(point_ctor),
11591 }];
11592 let (src_delta, _) = frontend
11593 .edit_segments(&mock_ctx, version, sketch1_id, segments)
11594 .await
11595 .unwrap();
11596 assert_eq!(
11598 src_delta.text.as_str(),
11599 "\
11600// Cube that requires the engine.
11601width = 2
11602sketch001 = startSketchOn(XY)
11603profile001 = startProfile(sketch001, at = [0, 0])
11604 |> yLine(length = width, tag = $seg1)
11605 |> xLine(length = width)
11606 |> yLine(length = -width)
11607 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11608 |> close()
11609extrude001 = extrude(profile001, length = width)
11610
11611// Get a value that requires the engine.
11612x = segLen(seg1)
11613
11614// Triangle with side length 2*x.
11615sketch(on = XY) {
11616 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
11617 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
11618 coincident([line1.end, line2.start])
11619 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
11620 coincident([line2.end, line3.start])
11621 coincident([line3.end, line1.start])
11622 equalLength([line3, line1])
11623 equalLength([line1, line2])
11624 distance([line1.start, line1.end]) == 2 * x
11625}
11626
11627// Line segment with length x.
11628sketch2 = sketch(on = XY) {
11629 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11630 distance([line1.start, line1.end]) == x
11631}
11632"
11633 );
11634
11635 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
11637 assert_eq!(
11639 src_delta.text.as_str(),
11640 "\
11641// Cube that requires the engine.
11642width = 2
11643sketch001 = startSketchOn(XY)
11644profile001 = startProfile(sketch001, at = [0, 0])
11645 |> yLine(length = width, tag = $seg1)
11646 |> xLine(length = width)
11647 |> yLine(length = -width)
11648 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11649 |> close()
11650extrude001 = extrude(profile001, length = width)
11651
11652// Get a value that requires the engine.
11653x = segLen(seg1)
11654
11655// Triangle with side length 2*x.
11656sketch(on = XY) {
11657 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11658 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11659 coincident([line1.end, line2.start])
11660 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11661 coincident([line2.end, line3.start])
11662 coincident([line3.end, line1.start])
11663 equalLength([line3, line1])
11664 equalLength([line1, line2])
11665 distance([line1.start, line1.end]) == 2 * x
11666}
11667
11668// Line segment with length x.
11669sketch2 = sketch(on = XY) {
11670 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11671 distance([line1.start, line1.end]) == x
11672}
11673"
11674 );
11675 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11683 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
11684
11685 let scene_delta = frontend
11693 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11694 .await
11695 .unwrap();
11696 assert_eq!(
11697 scene_delta.new_graph.objects.len(),
11698 24,
11699 "{:#?}",
11700 scene_delta.new_graph.objects
11701 );
11702
11703 let point_ctor = PointCtor {
11705 position: Point2d {
11706 x: Expr::Var(Number {
11707 value: 3.0,
11708 units: NumericSuffix::Mm,
11709 }),
11710 y: Expr::Var(Number {
11711 value: 4.0,
11712 units: NumericSuffix::Mm,
11713 }),
11714 },
11715 };
11716 let segments = vec![ExistingSegmentCtor {
11717 id: point2_id,
11718 ctor: SegmentCtor::Point(point_ctor),
11719 }];
11720 let (src_delta, _) = frontend
11721 .edit_segments(&mock_ctx, version, sketch2_id, segments)
11722 .await
11723 .unwrap();
11724 assert_eq!(
11726 src_delta.text.as_str(),
11727 "\
11728// Cube that requires the engine.
11729width = 2
11730sketch001 = startSketchOn(XY)
11731profile001 = startProfile(sketch001, at = [0, 0])
11732 |> yLine(length = width, tag = $seg1)
11733 |> xLine(length = width)
11734 |> yLine(length = -width)
11735 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11736 |> close()
11737extrude001 = extrude(profile001, length = width)
11738
11739// Get a value that requires the engine.
11740x = segLen(seg1)
11741
11742// Triangle with side length 2*x.
11743sketch(on = XY) {
11744 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11745 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11746 coincident([line1.end, line2.start])
11747 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11748 coincident([line2.end, line3.start])
11749 coincident([line3.end, line1.start])
11750 equalLength([line3, line1])
11751 equalLength([line1, line2])
11752 distance([line1.start, line1.end]) == 2 * x
11753}
11754
11755// Line segment with length x.
11756sketch2 = sketch(on = XY) {
11757 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
11758 distance([line1.start, line1.end]) == x
11759}
11760"
11761 );
11762
11763 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
11765 assert_eq!(
11767 src_delta.text.as_str(),
11768 "\
11769// Cube that requires the engine.
11770width = 2
11771sketch001 = startSketchOn(XY)
11772profile001 = startProfile(sketch001, at = [0, 0])
11773 |> yLine(length = width, tag = $seg1)
11774 |> xLine(length = width)
11775 |> yLine(length = -width)
11776 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11777 |> close()
11778extrude001 = extrude(profile001, length = width)
11779
11780// Get a value that requires the engine.
11781x = segLen(seg1)
11782
11783// Triangle with side length 2*x.
11784sketch(on = XY) {
11785 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11786 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11787 coincident([line1.end, line2.start])
11788 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11789 coincident([line2.end, line3.start])
11790 coincident([line3.end, line1.start])
11791 equalLength([line3, line1])
11792 equalLength([line1, line2])
11793 distance([line1.start, line1.end]) == 2 * x
11794}
11795
11796// Line segment with length x.
11797sketch2 = sketch(on = XY) {
11798 line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
11799 distance([line1.start, line1.end]) == x
11800}
11801"
11802 );
11803
11804 ctx.close().await;
11805 mock_ctx.close().await;
11806 }
11807
11808 #[tokio::test(flavor = "multi_thread")]
11809 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
11810 clear_mem_cache().await;
11811
11812 let source = r#"sketch001 = sketch(on = XZ) {
11813 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
11814}
11815sketch002 = sketch(on = XY) {
11816 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
11817 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
11818 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
11819 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
11820 coincident([line1.end, line2.start])
11821 coincident([line2.end, line3.start])
11822 coincident([line3.end, line4.start])
11823 coincident([line4.end, line1.start])
11824 parallel([line2, line4])
11825 parallel([line3, line1])
11826 perpendicular([line1, line2])
11827 horizontal(line3)
11828 coincident([line1.start, ORIGIN])
11829}
11830"#;
11831
11832 let program = Program::parse(source).unwrap().0.unwrap();
11833 let mut frontend = FrontendState::new();
11834 let ctx = ExecutorContext::new_with_engine(
11835 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
11836 Default::default(),
11837 );
11838 let mock_ctx = ExecutorContext::new_mock(None).await;
11839 let version = Version(0);
11840 let project_id = ProjectId(0);
11841 let file_id = FileId(0);
11842
11843 frontend.hack_set_program(&ctx, program).await.unwrap();
11844 let sketch_objects = frontend
11845 .scene_graph
11846 .objects
11847 .iter()
11848 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
11849 .collect::<Vec<_>>();
11850 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
11851
11852 let sketch1_id = sketch_objects[0].id;
11853 let sketch2_id = sketch_objects[1].id;
11854
11855 frontend
11856 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11857 .await
11858 .unwrap();
11859 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11860
11861 let scene_delta = frontend
11862 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11863 .await
11864 .unwrap();
11865 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
11866
11867 clear_mem_cache().await;
11868 ctx.close().await;
11869 mock_ctx.close().await;
11870 }
11871
11872 #[tokio::test(flavor = "multi_thread")]
11877 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
11878 let initial_source = "@settings(defaultLengthUnit = mm)
11880
11881
11882
11883sketch001 = sketch(on = XY) {
11884 point(at = [1in, 2in])
11885}
11886";
11887
11888 let program = Program::parse(initial_source).unwrap().0.unwrap();
11889 let mut frontend = FrontendState::new();
11890
11891 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11892 let mock_ctx = ExecutorContext::new_mock(None).await;
11893 let version = Version(0);
11894 let project_id = ProjectId(0);
11895 let file_id = FileId(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
11901 frontend
11903 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11904 .await
11905 .unwrap();
11906
11907 let point_ctor = PointCtor {
11909 position: Point2d {
11910 x: Expr::Number(Number {
11911 value: 5.0,
11912 units: NumericSuffix::Mm,
11913 }),
11914 y: Expr::Number(Number {
11915 value: 6.0,
11916 units: NumericSuffix::Mm,
11917 }),
11918 },
11919 };
11920 let segment = SegmentCtor::Point(point_ctor);
11921 let (src_delta, scene_delta) = frontend
11922 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11923 .await
11924 .unwrap();
11925 assert!(
11927 src_delta.text.contains("point(at = [5mm, 6mm])"),
11928 "Expected new point in source, got: {}",
11929 src_delta.text
11930 );
11931 assert!(!scene_delta.new_objects.is_empty());
11932
11933 ctx.close().await;
11934 mock_ctx.close().await;
11935 }
11936
11937 #[tokio::test(flavor = "multi_thread")]
11938 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
11939 let initial_source = "@settings(defaultLengthUnit = mm)
11941
11942
11943
11944s = sketch(on = XY) {}
11945";
11946
11947 let program = Program::parse(initial_source).unwrap().0.unwrap();
11948 let mut frontend = FrontendState::new();
11949
11950 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11951 let mock_ctx = ExecutorContext::new_mock(None).await;
11952 let version = Version(0);
11953
11954 frontend.hack_set_program(&ctx, program).await.unwrap();
11955 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11956 let sketch_id = sketch_object.id;
11957
11958 let line_ctor = LineCtor {
11959 start: Point2d {
11960 x: Expr::Number(Number {
11961 value: 0.0,
11962 units: NumericSuffix::Mm,
11963 }),
11964 y: Expr::Number(Number {
11965 value: 0.0,
11966 units: NumericSuffix::Mm,
11967 }),
11968 },
11969 end: Point2d {
11970 x: Expr::Number(Number {
11971 value: 10.0,
11972 units: NumericSuffix::Mm,
11973 }),
11974 y: Expr::Number(Number {
11975 value: 10.0,
11976 units: NumericSuffix::Mm,
11977 }),
11978 },
11979 construction: None,
11980 };
11981 let segment = SegmentCtor::Line(line_ctor);
11982 let (src_delta, scene_delta) = frontend
11983 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11984 .await
11985 .unwrap();
11986 assert!(
11987 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
11988 "Expected line in source, got: {}",
11989 src_delta.text
11990 );
11991 assert_eq!(scene_delta.new_objects.len(), 3);
11993
11994 ctx.close().await;
11995 mock_ctx.close().await;
11996 }
11997
11998 #[tokio::test(flavor = "multi_thread")]
11999 async fn test_extra_newlines_between_operations_edit_line() {
12000 let initial_source = "@settings(defaultLengthUnit = mm)
12002
12003
12004sketch001 = sketch(on = XY) {
12005
12006 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12007
12008}
12009";
12010
12011 let program = Program::parse(initial_source).unwrap().0.unwrap();
12012 let mut frontend = FrontendState::new();
12013
12014 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12015 let mock_ctx = ExecutorContext::new_mock(None).await;
12016 let version = Version(0);
12017 let project_id = ProjectId(0);
12018 let file_id = FileId(0);
12019
12020 frontend.hack_set_program(&ctx, program).await.unwrap();
12021 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12022 let sketch_id = sketch_object.id;
12023 let sketch = expect_sketch(sketch_object);
12024
12025 let line_id = sketch
12027 .segments
12028 .iter()
12029 .copied()
12030 .find(|seg_id| {
12031 matches!(
12032 &frontend.scene_graph.objects[seg_id.0].kind,
12033 ObjectKind::Segment {
12034 segment: Segment::Line(_)
12035 }
12036 )
12037 })
12038 .expect("Expected a line segment in sketch");
12039
12040 frontend
12042 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12043 .await
12044 .unwrap();
12045
12046 let line_ctor = LineCtor {
12048 start: Point2d {
12049 x: Expr::Var(Number {
12050 value: 1.0,
12051 units: NumericSuffix::Mm,
12052 }),
12053 y: Expr::Var(Number {
12054 value: 2.0,
12055 units: NumericSuffix::Mm,
12056 }),
12057 },
12058 end: Point2d {
12059 x: Expr::Var(Number {
12060 value: 13.0,
12061 units: NumericSuffix::Mm,
12062 }),
12063 y: Expr::Var(Number {
12064 value: 14.0,
12065 units: NumericSuffix::Mm,
12066 }),
12067 },
12068 construction: None,
12069 };
12070 let segments = vec![ExistingSegmentCtor {
12071 id: line_id,
12072 ctor: SegmentCtor::Line(line_ctor),
12073 }];
12074 let (src_delta, _scene_delta) = frontend
12075 .edit_segments(&mock_ctx, version, sketch_id, segments)
12076 .await
12077 .unwrap();
12078 assert!(
12079 src_delta
12080 .text
12081 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
12082 "Expected edited line in source, got: {}",
12083 src_delta.text
12084 );
12085
12086 ctx.close().await;
12087 mock_ctx.close().await;
12088 }
12089
12090 #[tokio::test(flavor = "multi_thread")]
12091 async fn test_extra_newlines_delete_segment() {
12092 let initial_source = "@settings(defaultLengthUnit = mm)
12094
12095
12096
12097sketch001 = sketch(on = XY) {
12098 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
12099}
12100";
12101
12102 let program = Program::parse(initial_source).unwrap().0.unwrap();
12103 let mut frontend = FrontendState::new();
12104
12105 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12106 let mock_ctx = ExecutorContext::new_mock(None).await;
12107 let version = Version(0);
12108
12109 frontend.hack_set_program(&ctx, program).await.unwrap();
12110 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12111 let sketch_id = sketch_object.id;
12112 let sketch = expect_sketch(sketch_object);
12113
12114 assert_eq!(sketch.segments.len(), 3);
12116 let circle_id = sketch.segments[2];
12117
12118 let (src_delta, scene_delta) = frontend
12120 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
12121 .await
12122 .unwrap();
12123 assert!(
12124 src_delta.text.contains("sketch(on = XY) {"),
12125 "Expected sketch block in source, got: {}",
12126 src_delta.text
12127 );
12128 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12129 let new_sketch = expect_sketch(new_sketch_object);
12130 assert_eq!(new_sketch.segments.len(), 0);
12131
12132 ctx.close().await;
12133 mock_ctx.close().await;
12134 }
12135
12136 #[tokio::test(flavor = "multi_thread")]
12137 async fn test_unformatted_source_add_arc() {
12138 let initial_source = "@settings(defaultLengthUnit = mm)
12140
12141
12142
12143
12144sketch001 = sketch(on = XY) {
12145}
12146";
12147
12148 let program = Program::parse(initial_source).unwrap().0.unwrap();
12149 let mut frontend = FrontendState::new();
12150
12151 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12152 let mock_ctx = ExecutorContext::new_mock(None).await;
12153 let version = Version(0);
12154
12155 frontend.hack_set_program(&ctx, program).await.unwrap();
12156 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12157 let sketch_id = sketch_object.id;
12158
12159 let arc_ctor = ArcCtor {
12160 start: Point2d {
12161 x: Expr::Var(Number {
12162 value: 5.0,
12163 units: NumericSuffix::Mm,
12164 }),
12165 y: Expr::Var(Number {
12166 value: 0.0,
12167 units: NumericSuffix::Mm,
12168 }),
12169 },
12170 end: Point2d {
12171 x: Expr::Var(Number {
12172 value: 0.0,
12173 units: NumericSuffix::Mm,
12174 }),
12175 y: Expr::Var(Number {
12176 value: 5.0,
12177 units: NumericSuffix::Mm,
12178 }),
12179 },
12180 center: Point2d {
12181 x: Expr::Var(Number {
12182 value: 0.0,
12183 units: NumericSuffix::Mm,
12184 }),
12185 y: Expr::Var(Number {
12186 value: 0.0,
12187 units: NumericSuffix::Mm,
12188 }),
12189 },
12190 construction: None,
12191 };
12192 let segment = SegmentCtor::Arc(arc_ctor);
12193 let (src_delta, scene_delta) = frontend
12194 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12195 .await
12196 .unwrap();
12197 assert!(
12198 src_delta
12199 .text
12200 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
12201 "Expected arc in source, got: {}",
12202 src_delta.text
12203 );
12204 assert!(!scene_delta.new_objects.is_empty());
12205
12206 ctx.close().await;
12207 mock_ctx.close().await;
12208 }
12209
12210 #[tokio::test(flavor = "multi_thread")]
12211 async fn test_extra_newlines_add_circle() {
12212 let initial_source = "@settings(defaultLengthUnit = mm)
12214
12215
12216
12217sketch001 = sketch(on = XY) {
12218}
12219";
12220
12221 let program = Program::parse(initial_source).unwrap().0.unwrap();
12222 let mut frontend = FrontendState::new();
12223
12224 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12225 let mock_ctx = ExecutorContext::new_mock(None).await;
12226 let version = Version(0);
12227
12228 frontend.hack_set_program(&ctx, program).await.unwrap();
12229 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12230 let sketch_id = sketch_object.id;
12231
12232 let circle_ctor = CircleCtor {
12233 start: Point2d {
12234 x: Expr::Var(Number {
12235 value: 5.0,
12236 units: NumericSuffix::Mm,
12237 }),
12238 y: Expr::Var(Number {
12239 value: 0.0,
12240 units: NumericSuffix::Mm,
12241 }),
12242 },
12243 center: Point2d {
12244 x: Expr::Var(Number {
12245 value: 0.0,
12246 units: NumericSuffix::Mm,
12247 }),
12248 y: Expr::Var(Number {
12249 value: 0.0,
12250 units: NumericSuffix::Mm,
12251 }),
12252 },
12253 construction: None,
12254 };
12255 let segment = SegmentCtor::Circle(circle_ctor);
12256 let (src_delta, scene_delta) = frontend
12257 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12258 .await
12259 .unwrap();
12260 assert!(
12261 src_delta
12262 .text
12263 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
12264 "Expected circle in source, got: {}",
12265 src_delta.text
12266 );
12267 assert!(!scene_delta.new_objects.is_empty());
12268
12269 ctx.close().await;
12270 mock_ctx.close().await;
12271 }
12272
12273 #[tokio::test(flavor = "multi_thread")]
12274 async fn test_extra_newlines_add_constraint() {
12275 let initial_source = "@settings(defaultLengthUnit = mm)
12277
12278
12279
12280sketch001 = sketch(on = XY) {
12281 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12282 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
12283}
12284";
12285
12286 let program = Program::parse(initial_source).unwrap().0.unwrap();
12287 let mut frontend = FrontendState::new();
12288
12289 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12290 let mock_ctx = ExecutorContext::new_mock(None).await;
12291 let version = Version(0);
12292 let project_id = ProjectId(0);
12293 let file_id = FileId(0);
12294
12295 frontend.hack_set_program(&ctx, program).await.unwrap();
12296 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12297 let sketch_id = sketch_object.id;
12298 let sketch = expect_sketch(sketch_object);
12299
12300 let line_ids: Vec<ObjectId> = sketch
12302 .segments
12303 .iter()
12304 .copied()
12305 .filter(|seg_id| {
12306 matches!(
12307 &frontend.scene_graph.objects[seg_id.0].kind,
12308 ObjectKind::Segment {
12309 segment: Segment::Line(_)
12310 }
12311 )
12312 })
12313 .collect();
12314 assert_eq!(line_ids.len(), 2, "Expected two line segments");
12315
12316 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
12317 let ObjectKind::Segment {
12318 segment: Segment::Line(line1_data),
12319 } = &line1.kind
12320 else {
12321 panic!("Expected line");
12322 };
12323 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
12324 let ObjectKind::Segment {
12325 segment: Segment::Line(line2_data),
12326 } = &line2.kind
12327 else {
12328 panic!("Expected line");
12329 };
12330
12331 let constraint = Constraint::Coincident(Coincident {
12333 segments: vec![line1_data.end.into(), line2_data.start.into()],
12334 });
12335
12336 frontend
12338 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12339 .await
12340 .unwrap();
12341 let (src_delta, _scene_delta) = frontend
12342 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12343 .await
12344 .unwrap();
12345 assert!(
12346 src_delta.text.contains("coincident("),
12347 "Expected coincident constraint in source, got: {}",
12348 src_delta.text
12349 );
12350
12351 ctx.close().await;
12352 mock_ctx.close().await;
12353 }
12354
12355 #[tokio::test(flavor = "multi_thread")]
12356 async fn test_extra_newlines_add_line_then_edit_line() {
12357 let initial_source = "@settings(defaultLengthUnit = mm)
12359
12360
12361
12362sketch001 = sketch(on = XY) {
12363}
12364";
12365
12366 let program = Program::parse(initial_source).unwrap().0.unwrap();
12367 let mut frontend = FrontendState::new();
12368
12369 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12370 let mock_ctx = ExecutorContext::new_mock(None).await;
12371 let version = Version(0);
12372
12373 frontend.hack_set_program(&ctx, program).await.unwrap();
12374 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12375 let sketch_id = sketch_object.id;
12376
12377 let line_ctor = LineCtor {
12379 start: Point2d {
12380 x: Expr::Number(Number {
12381 value: 0.0,
12382 units: NumericSuffix::Mm,
12383 }),
12384 y: Expr::Number(Number {
12385 value: 0.0,
12386 units: NumericSuffix::Mm,
12387 }),
12388 },
12389 end: Point2d {
12390 x: Expr::Number(Number {
12391 value: 10.0,
12392 units: NumericSuffix::Mm,
12393 }),
12394 y: Expr::Number(Number {
12395 value: 10.0,
12396 units: NumericSuffix::Mm,
12397 }),
12398 },
12399 construction: None,
12400 };
12401 let segment = SegmentCtor::Line(line_ctor);
12402 let (src_delta, scene_delta) = frontend
12403 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12404 .await
12405 .unwrap();
12406 assert!(
12407 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12408 "Expected line in source after add, got: {}",
12409 src_delta.text
12410 );
12411 let line_id = *scene_delta.new_objects.last().unwrap();
12413
12414 let line_ctor = LineCtor {
12416 start: Point2d {
12417 x: Expr::Number(Number {
12418 value: 1.0,
12419 units: NumericSuffix::Mm,
12420 }),
12421 y: Expr::Number(Number {
12422 value: 2.0,
12423 units: NumericSuffix::Mm,
12424 }),
12425 },
12426 end: Point2d {
12427 x: Expr::Number(Number {
12428 value: 13.0,
12429 units: NumericSuffix::Mm,
12430 }),
12431 y: Expr::Number(Number {
12432 value: 14.0,
12433 units: NumericSuffix::Mm,
12434 }),
12435 },
12436 construction: None,
12437 };
12438 let segments = vec![ExistingSegmentCtor {
12439 id: line_id,
12440 ctor: SegmentCtor::Line(line_ctor),
12441 }];
12442 let (src_delta, scene_delta) = frontend
12443 .edit_segments(&mock_ctx, version, sketch_id, segments)
12444 .await
12445 .unwrap();
12446 assert!(
12447 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
12448 "Expected edited line in source, got: {}",
12449 src_delta.text
12450 );
12451 assert_eq!(scene_delta.new_objects, vec![]);
12452
12453 ctx.close().await;
12454 mock_ctx.close().await;
12455 }
12456}