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 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1445 let sketch_block_ref =
1446 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1447
1448 let mut new_ast = self.program.ast.clone();
1449 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1450
1451 for segment in edit_segments {
1453 segment_ids_edited.insert(segment.id);
1454 match segment.ctor {
1455 SegmentCtor::Point(ctor) => self
1456 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1457 .map_err(KclErrorWithOutputs::no_outputs)?,
1458 SegmentCtor::Line(ctor) => self
1459 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1460 .map_err(KclErrorWithOutputs::no_outputs)?,
1461 SegmentCtor::Arc(ctor) => self
1462 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1463 .map_err(KclErrorWithOutputs::no_outputs)?,
1464 SegmentCtor::Circle(ctor) => self
1465 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1466 .map_err(KclErrorWithOutputs::no_outputs)?,
1467 }
1468 }
1469
1470 for constraint in add_constraints {
1472 match constraint {
1473 Constraint::Coincident(coincident) => {
1474 self.add_coincident(sketch, coincident, &mut new_ast)
1475 .await
1476 .map_err(KclErrorWithOutputs::no_outputs)?;
1477 }
1478 other => {
1479 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1480 "unsupported constraint in tail cut batch: {other:?}"
1481 ))));
1482 }
1483 }
1484 }
1485
1486 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1488
1489 let has_constraint_deletions = !constraint_ids_set.is_empty();
1490 for constraint_id in constraint_ids_set {
1491 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1492 .map_err(KclErrorWithOutputs::no_outputs)?;
1493 }
1494
1495 let (source_delta, mut scene_graph_delta) = self
1499 .execute_after_edit(
1500 ctx,
1501 sketch,
1502 sketch_block_ref,
1503 segment_ids_edited,
1504 EditDeleteKind::Edit,
1505 &mut new_ast,
1506 )
1507 .await?;
1508
1509 if has_constraint_deletions {
1512 scene_graph_delta.invalidates_ids = true;
1513 }
1514
1515 Ok((source_delta, scene_graph_delta))
1516 }
1517}
1518
1519impl FrontendState {
1520 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1521 self.program = program.clone();
1522
1523 self.point_freedom_cache.clear();
1534 match ctx.run_with_caching(program).await {
1535 Ok(outcome) => {
1536 let outcome = self.update_state_after_exec(outcome, true);
1537 let checkpoint_id = self
1538 .create_sketch_checkpoint(outcome.clone())
1539 .await
1540 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1541 Ok(SetProgramOutcome::Success {
1542 scene_graph: Box::new(self.scene_graph.clone()),
1543 exec_outcome: Box::new(outcome),
1544 checkpoint_id: Some(checkpoint_id),
1545 })
1546 }
1547 Err(mut err) => {
1548 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1551 self.update_state_after_exec(outcome, true);
1552 err.scene_graph = Some(self.scene_graph.clone());
1553 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1554 }
1555 }
1556 }
1557
1558 pub async fn engine_execute(
1561 &mut self,
1562 ctx: &ExecutorContext,
1563 program: Program,
1564 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1565 self.program = program.clone();
1566
1567 self.point_freedom_cache.clear();
1571 match ctx.run_with_caching(program).await {
1572 Ok(outcome) => {
1573 let outcome = self.update_state_after_exec(outcome, true);
1574 Ok(SceneGraphDelta {
1575 new_graph: self.scene_graph.clone(),
1576 exec_outcome: outcome,
1577 new_objects: Default::default(),
1579 invalidates_ids: Default::default(),
1581 })
1582 }
1583 Err(mut err) => {
1584 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1586 self.update_state_after_exec(outcome, true);
1587 err.scene_graph = Some(self.scene_graph.clone());
1588 Err(err)
1589 }
1590 }
1591 }
1592
1593 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1594 if matches!(err.error, KclError::EngineHangup { .. }) {
1595 return Err(err);
1599 }
1600
1601 let KclErrorWithOutputs {
1602 error,
1603 mut non_fatal,
1604 variables,
1605 #[cfg(feature = "artifact-graph")]
1606 operations,
1607 #[cfg(feature = "artifact-graph")]
1608 artifact_graph,
1609 #[cfg(feature = "artifact-graph")]
1610 scene_objects,
1611 #[cfg(feature = "artifact-graph")]
1612 source_range_to_object,
1613 #[cfg(feature = "artifact-graph")]
1614 var_solutions,
1615 filenames,
1616 default_planes,
1617 ..
1618 } = err;
1619
1620 if let Some(source_range) = error.source_ranges().first() {
1621 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1622 } else {
1623 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1624 }
1625
1626 Ok(ExecOutcome {
1627 variables,
1628 filenames,
1629 #[cfg(feature = "artifact-graph")]
1630 operations,
1631 #[cfg(feature = "artifact-graph")]
1632 artifact_graph,
1633 #[cfg(feature = "artifact-graph")]
1634 scene_objects,
1635 #[cfg(feature = "artifact-graph")]
1636 source_range_to_object,
1637 #[cfg(feature = "artifact-graph")]
1638 var_solutions,
1639 issues: non_fatal,
1640 default_planes,
1641 })
1642 }
1643
1644 async fn add_point(
1645 &mut self,
1646 ctx: &ExecutorContext,
1647 sketch: ObjectId,
1648 ctor: PointCtor,
1649 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1650 let at_ast = to_ast_point2d(&ctor.position)
1652 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1653 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1654 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1655 unlabeled: None,
1656 arguments: vec![ast::LabeledArg {
1657 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1658 arg: at_ast,
1659 }],
1660 digest: None,
1661 non_code_meta: Default::default(),
1662 })));
1663
1664 let sketch_id = sketch;
1666 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1667 #[cfg(target_arch = "wasm32")]
1668 web_sys::console::error_1(
1669 &format!(
1670 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1671 &self.scene_graph.objects
1672 )
1673 .into(),
1674 );
1675 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1676 })?;
1677 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1678 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1679 "Object is not a sketch, it is {}",
1680 sketch_object.kind.human_friendly_kind_with_article(),
1681 ))));
1682 };
1683 let mut new_ast = self.program.ast.clone();
1685 let (sketch_block_ref, _) = self
1686 .mutate_ast(
1687 &mut new_ast,
1688 sketch_id,
1689 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1690 )
1691 .map_err(KclErrorWithOutputs::no_outputs)?;
1692 let new_source = source_from_ast(&new_ast);
1694 let (new_program, errors) = Program::parse(&new_source)
1696 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1697 if !errors.is_empty() {
1698 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1699 "Error parsing KCL source after adding point: {errors:?}"
1700 ))));
1701 }
1702 let Some(new_program) = new_program else {
1703 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1704 "No AST produced after adding point".to_string(),
1705 )));
1706 };
1707
1708 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1709 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1710 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1711 )))
1712 })?;
1713 #[cfg(not(feature = "artifact-graph"))]
1714 let _ = point_node_ref;
1715
1716 self.program = new_program.clone();
1718
1719 let mut truncated_program = new_program;
1721 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1722 .map_err(KclErrorWithOutputs::no_outputs)?;
1723
1724 let outcome = ctx
1726 .run_mock(
1727 &truncated_program,
1728 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1729 )
1730 .await?;
1731
1732 #[cfg(not(feature = "artifact-graph"))]
1733 let new_object_ids = Vec::new();
1734 #[cfg(feature = "artifact-graph")]
1735 let new_object_ids = {
1736 let make_err =
1737 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1738 let segment_id = outcome
1739 .source_range_to_object
1740 .get(&point_node_ref.range)
1741 .copied()
1742 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1743 let segment_object = outcome
1744 .scene_objects
1745 .get(segment_id.0)
1746 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1747 let ObjectKind::Segment { segment } = &segment_object.kind else {
1748 return Err(make_err(format!(
1749 "Object is not a segment, it is {}",
1750 segment_object.kind.human_friendly_kind_with_article()
1751 )));
1752 };
1753 let Segment::Point(_) = segment else {
1754 return Err(make_err(format!(
1755 "Segment is not a point, it is {}",
1756 segment.human_friendly_kind_with_article()
1757 )));
1758 };
1759 vec![segment_id]
1760 };
1761 let src_delta = SourceDelta { text: new_source };
1762 let outcome = self.update_state_after_exec(outcome, false);
1764 let scene_graph_delta = SceneGraphDelta {
1765 new_graph: self.scene_graph.clone(),
1766 invalidates_ids: false,
1767 new_objects: new_object_ids,
1768 exec_outcome: outcome,
1769 };
1770 Ok((src_delta, scene_graph_delta))
1771 }
1772
1773 async fn add_line(
1774 &mut self,
1775 ctx: &ExecutorContext,
1776 sketch: ObjectId,
1777 ctor: LineCtor,
1778 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1779 let start_ast = to_ast_point2d(&ctor.start)
1781 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1782 let end_ast = to_ast_point2d(&ctor.end)
1783 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1784 let mut arguments = vec![
1785 ast::LabeledArg {
1786 label: Some(ast::Identifier::new(LINE_START_PARAM)),
1787 arg: start_ast,
1788 },
1789 ast::LabeledArg {
1790 label: Some(ast::Identifier::new(LINE_END_PARAM)),
1791 arg: end_ast,
1792 },
1793 ];
1794 if ctor.construction == Some(true) {
1796 arguments.push(ast::LabeledArg {
1797 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1798 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1799 value: ast::LiteralValue::Bool(true),
1800 raw: "true".to_string(),
1801 digest: None,
1802 }))),
1803 });
1804 }
1805 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1806 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1807 unlabeled: None,
1808 arguments,
1809 digest: None,
1810 non_code_meta: Default::default(),
1811 })));
1812
1813 let sketch_id = sketch;
1815 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1816 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1817 })?;
1818 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1819 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1820 "Object is not a sketch, it is {}",
1821 sketch_object.kind.human_friendly_kind_with_article(),
1822 ))));
1823 };
1824 let mut new_ast = self.program.ast.clone();
1826 let (sketch_block_ref, _) = self
1827 .mutate_ast(
1828 &mut new_ast,
1829 sketch_id,
1830 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1831 )
1832 .map_err(KclErrorWithOutputs::no_outputs)?;
1833 let new_source = source_from_ast(&new_ast);
1835 let (new_program, errors) = Program::parse(&new_source)
1837 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1838 if !errors.is_empty() {
1839 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1840 "Error parsing KCL source after adding line: {errors:?}"
1841 ))));
1842 }
1843 let Some(new_program) = new_program else {
1844 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1845 "No AST produced after adding line".to_string(),
1846 )));
1847 };
1848
1849 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1850 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1851 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1852 )))
1853 })?;
1854 #[cfg(not(feature = "artifact-graph"))]
1855 let _ = line_node_ref;
1856
1857 self.program = new_program.clone();
1859
1860 let mut truncated_program = new_program;
1862 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1863 .map_err(KclErrorWithOutputs::no_outputs)?;
1864
1865 let outcome = ctx
1867 .run_mock(
1868 &truncated_program,
1869 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1870 )
1871 .await?;
1872
1873 #[cfg(not(feature = "artifact-graph"))]
1874 let new_object_ids = Vec::new();
1875 #[cfg(feature = "artifact-graph")]
1876 let new_object_ids = {
1877 let make_err =
1878 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1879 let segment_id = outcome
1880 .source_range_to_object
1881 .get(&line_node_ref.range)
1882 .copied()
1883 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1884 let segment_object = outcome
1885 .scene_object_by_id(segment_id)
1886 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1887 let ObjectKind::Segment { segment } = &segment_object.kind else {
1888 return Err(make_err(format!(
1889 "Object is not a segment, it is {}",
1890 segment_object.kind.human_friendly_kind_with_article()
1891 )));
1892 };
1893 let Segment::Line(line) = segment else {
1894 return Err(make_err(format!(
1895 "Segment is not a line, it is {}",
1896 segment.human_friendly_kind_with_article()
1897 )));
1898 };
1899 vec![line.start, line.end, segment_id]
1900 };
1901 let src_delta = SourceDelta { text: new_source };
1902 let outcome = self.update_state_after_exec(outcome, false);
1904 let scene_graph_delta = SceneGraphDelta {
1905 new_graph: self.scene_graph.clone(),
1906 invalidates_ids: false,
1907 new_objects: new_object_ids,
1908 exec_outcome: outcome,
1909 };
1910 Ok((src_delta, scene_graph_delta))
1911 }
1912
1913 async fn add_arc(
1914 &mut self,
1915 ctx: &ExecutorContext,
1916 sketch: ObjectId,
1917 ctor: ArcCtor,
1918 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1919 let start_ast = to_ast_point2d(&ctor.start)
1921 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1922 let end_ast = to_ast_point2d(&ctor.end)
1923 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1924 let center_ast = to_ast_point2d(&ctor.center)
1925 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1926 let mut arguments = vec![
1927 ast::LabeledArg {
1928 label: Some(ast::Identifier::new(ARC_START_PARAM)),
1929 arg: start_ast,
1930 },
1931 ast::LabeledArg {
1932 label: Some(ast::Identifier::new(ARC_END_PARAM)),
1933 arg: end_ast,
1934 },
1935 ast::LabeledArg {
1936 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1937 arg: center_ast,
1938 },
1939 ];
1940 if ctor.construction == Some(true) {
1942 arguments.push(ast::LabeledArg {
1943 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1944 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1945 value: ast::LiteralValue::Bool(true),
1946 raw: "true".to_string(),
1947 digest: None,
1948 }))),
1949 });
1950 }
1951 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1952 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1953 unlabeled: None,
1954 arguments,
1955 digest: None,
1956 non_code_meta: Default::default(),
1957 })));
1958
1959 let sketch_id = sketch;
1961 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1962 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1963 })?;
1964 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1965 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1966 "Object is not a sketch, it is {}",
1967 sketch_object.kind.human_friendly_kind_with_article(),
1968 ))));
1969 };
1970 let mut new_ast = self.program.ast.clone();
1972 let (sketch_block_ref, _) = self
1973 .mutate_ast(
1974 &mut new_ast,
1975 sketch_id,
1976 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1977 )
1978 .map_err(KclErrorWithOutputs::no_outputs)?;
1979 let new_source = source_from_ast(&new_ast);
1981 let (new_program, errors) = Program::parse(&new_source)
1983 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1984 if !errors.is_empty() {
1985 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1986 "Error parsing KCL source after adding arc: {errors:?}"
1987 ))));
1988 }
1989 let Some(new_program) = new_program else {
1990 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1991 "No AST produced after adding arc".to_string(),
1992 )));
1993 };
1994
1995 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1996 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1997 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
1998 )))
1999 })?;
2000 #[cfg(not(feature = "artifact-graph"))]
2001 let _ = arc_node_ref;
2002
2003 self.program = new_program.clone();
2005
2006 let mut truncated_program = new_program;
2008 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2009 .map_err(KclErrorWithOutputs::no_outputs)?;
2010
2011 let outcome = ctx
2013 .run_mock(
2014 &truncated_program,
2015 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2016 )
2017 .await?;
2018
2019 #[cfg(not(feature = "artifact-graph"))]
2020 let new_object_ids = Vec::new();
2021 #[cfg(feature = "artifact-graph")]
2022 let new_object_ids = {
2023 let make_err =
2024 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2025 let segment_id = outcome
2026 .source_range_to_object
2027 .get(&arc_node_ref.range)
2028 .copied()
2029 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2030 let segment_object = outcome
2031 .scene_objects
2032 .get(segment_id.0)
2033 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2034 let ObjectKind::Segment { segment } = &segment_object.kind else {
2035 return Err(make_err(format!(
2036 "Object is not a segment, it is {}",
2037 segment_object.kind.human_friendly_kind_with_article()
2038 )));
2039 };
2040 let Segment::Arc(arc) = segment else {
2041 return Err(make_err(format!(
2042 "Segment is not an arc, it is {}",
2043 segment.human_friendly_kind_with_article()
2044 )));
2045 };
2046 vec![arc.start, arc.end, arc.center, segment_id]
2047 };
2048 let src_delta = SourceDelta { text: new_source };
2049 let outcome = self.update_state_after_exec(outcome, false);
2051 let scene_graph_delta = SceneGraphDelta {
2052 new_graph: self.scene_graph.clone(),
2053 invalidates_ids: false,
2054 new_objects: new_object_ids,
2055 exec_outcome: outcome,
2056 };
2057 Ok((src_delta, scene_graph_delta))
2058 }
2059
2060 async fn add_circle(
2061 &mut self,
2062 ctx: &ExecutorContext,
2063 sketch: ObjectId,
2064 ctor: CircleCtor,
2065 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2066 let start_ast = to_ast_point2d(&ctor.start)
2068 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2069 let center_ast = to_ast_point2d(&ctor.center)
2070 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2071 let mut arguments = vec![
2072 ast::LabeledArg {
2073 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2074 arg: start_ast,
2075 },
2076 ast::LabeledArg {
2077 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2078 arg: center_ast,
2079 },
2080 ];
2081 if ctor.construction == Some(true) {
2083 arguments.push(ast::LabeledArg {
2084 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2085 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2086 value: ast::LiteralValue::Bool(true),
2087 raw: "true".to_string(),
2088 digest: None,
2089 }))),
2090 });
2091 }
2092 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2093 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2094 unlabeled: None,
2095 arguments,
2096 digest: None,
2097 non_code_meta: Default::default(),
2098 })));
2099
2100 let sketch_id = sketch;
2102 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2103 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2104 })?;
2105 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2106 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2107 "Object is not a sketch, it is {}",
2108 sketch_object.kind.human_friendly_kind_with_article(),
2109 ))));
2110 };
2111 let mut new_ast = self.program.ast.clone();
2113 let (sketch_block_ref, _) = self
2114 .mutate_ast(
2115 &mut new_ast,
2116 sketch_id,
2117 AstMutateCommand::AddSketchBlockVarDecl {
2118 prefix: CIRCLE_VARIABLE.to_owned(),
2119 expr: circle_ast,
2120 },
2121 )
2122 .map_err(KclErrorWithOutputs::no_outputs)?;
2123 let new_source = source_from_ast(&new_ast);
2125 let (new_program, errors) = Program::parse(&new_source)
2127 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2128 if !errors.is_empty() {
2129 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2130 "Error parsing KCL source after adding circle: {errors:?}"
2131 ))));
2132 }
2133 let Some(new_program) = new_program else {
2134 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2135 "No AST produced after adding circle".to_string(),
2136 )));
2137 };
2138
2139 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2140 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2141 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2142 )))
2143 })?;
2144 #[cfg(not(feature = "artifact-graph"))]
2145 let _ = circle_node_ref;
2146
2147 self.program = new_program.clone();
2149
2150 let mut truncated_program = new_program;
2152 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2153 .map_err(KclErrorWithOutputs::no_outputs)?;
2154
2155 let outcome = ctx
2157 .run_mock(
2158 &truncated_program,
2159 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2160 )
2161 .await?;
2162
2163 #[cfg(not(feature = "artifact-graph"))]
2164 let new_object_ids = Vec::new();
2165 #[cfg(feature = "artifact-graph")]
2166 let new_object_ids = {
2167 let make_err =
2168 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2169 let segment_id = outcome
2170 .source_range_to_object
2171 .get(&circle_node_ref.range)
2172 .copied()
2173 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2174 let segment_object = outcome
2175 .scene_objects
2176 .get(segment_id.0)
2177 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2178 let ObjectKind::Segment { segment } = &segment_object.kind else {
2179 return Err(make_err(format!(
2180 "Object is not a segment, it is {}",
2181 segment_object.kind.human_friendly_kind_with_article()
2182 )));
2183 };
2184 let Segment::Circle(circle) = segment else {
2185 return Err(make_err(format!(
2186 "Segment is not a circle, it is {}",
2187 segment.human_friendly_kind_with_article()
2188 )));
2189 };
2190 vec![circle.start, circle.center, segment_id]
2191 };
2192 let src_delta = SourceDelta { text: new_source };
2193 let outcome = self.update_state_after_exec(outcome, false);
2195 let scene_graph_delta = SceneGraphDelta {
2196 new_graph: self.scene_graph.clone(),
2197 invalidates_ids: false,
2198 new_objects: new_object_ids,
2199 exec_outcome: outcome,
2200 };
2201 Ok((src_delta, scene_graph_delta))
2202 }
2203
2204 fn edit_point(
2205 &mut self,
2206 new_ast: &mut ast::Node<ast::Program>,
2207 sketch: ObjectId,
2208 point: ObjectId,
2209 ctor: PointCtor,
2210 ) -> Result<(), KclError> {
2211 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2213
2214 let sketch_id = sketch;
2216 let sketch_object = self
2217 .scene_graph
2218 .objects
2219 .get(sketch_id.0)
2220 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2221 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2222 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2223 };
2224 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2225 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2226 })?;
2227 let point_id = point;
2229 let point_object = self
2230 .scene_graph
2231 .objects
2232 .get(point_id.0)
2233 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2234 let ObjectKind::Segment {
2235 segment: Segment::Point(point),
2236 } = &point_object.kind
2237 else {
2238 return Err(KclError::refactor(format!(
2239 "Object is not a point segment: {point_object:?}"
2240 )));
2241 };
2242
2243 if let Some(owner_id) = point.owner {
2245 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2246 KclError::refactor(format!(
2247 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2248 ))
2249 })?;
2250 let ObjectKind::Segment { segment } = &owner_object.kind else {
2251 return Err(KclError::refactor(format!(
2252 "Internal: Owner of point is not a segment, but found {}",
2253 owner_object.kind.human_friendly_kind_with_article()
2254 )));
2255 };
2256
2257 if let Segment::Line(line) = segment {
2259 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2260 return Err(KclError::refactor(format!(
2261 "Internal: Owner of point does not have line ctor, but found {}",
2262 line.ctor.human_friendly_kind_with_article()
2263 )));
2264 };
2265 let mut line_ctor = line_ctor.clone();
2266 if line.start == point_id {
2268 line_ctor.start = ctor.position;
2269 } else if line.end == point_id {
2270 line_ctor.end = ctor.position;
2271 } else {
2272 return Err(KclError::refactor(format!(
2273 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2274 )));
2275 }
2276 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2277 }
2278
2279 if let Segment::Arc(arc) = segment {
2281 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2282 return Err(KclError::refactor(format!(
2283 "Internal: Owner of point does not have arc ctor, but found {}",
2284 arc.ctor.human_friendly_kind_with_article()
2285 )));
2286 };
2287 let mut arc_ctor = arc_ctor.clone();
2288 if arc.center == point_id {
2290 arc_ctor.center = ctor.position;
2291 } else if arc.start == point_id {
2292 arc_ctor.start = ctor.position;
2293 } else if arc.end == point_id {
2294 arc_ctor.end = ctor.position;
2295 } else {
2296 return Err(KclError::refactor(format!(
2297 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2298 )));
2299 }
2300 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2301 }
2302
2303 if let Segment::Circle(circle) = segment {
2305 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2306 return Err(KclError::refactor(format!(
2307 "Internal: Owner of point does not have circle ctor, but found {}",
2308 circle.ctor.human_friendly_kind_with_article()
2309 )));
2310 };
2311 let mut circle_ctor = circle_ctor.clone();
2312 if circle.center == point_id {
2313 circle_ctor.center = ctor.position;
2314 } else if circle.start == point_id {
2315 circle_ctor.start = ctor.position;
2316 } else {
2317 return Err(KclError::refactor(format!(
2318 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2319 )));
2320 }
2321 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2322 }
2323
2324 }
2327
2328 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2330 Ok(())
2331 }
2332
2333 fn edit_line(
2334 &mut self,
2335 new_ast: &mut ast::Node<ast::Program>,
2336 sketch: ObjectId,
2337 line: ObjectId,
2338 ctor: LineCtor,
2339 ) -> Result<(), KclError> {
2340 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2342 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2343
2344 let sketch_id = sketch;
2346 let sketch_object = self
2347 .scene_graph
2348 .objects
2349 .get(sketch_id.0)
2350 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2351 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2352 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2353 };
2354 sketch
2355 .segments
2356 .iter()
2357 .find(|o| **o == line)
2358 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2359 let line_id = line;
2361 let line_object = self
2362 .scene_graph
2363 .objects
2364 .get(line_id.0)
2365 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2366 let ObjectKind::Segment { .. } = &line_object.kind else {
2367 let kind = line_object.kind.human_friendly_kind_with_article();
2368 return Err(KclError::refactor(format!(
2369 "This constraint only works on Segments, but you selected {kind}"
2370 )));
2371 };
2372
2373 self.mutate_ast(
2375 new_ast,
2376 line_id,
2377 AstMutateCommand::EditLine {
2378 start: new_start_ast,
2379 end: new_end_ast,
2380 construction: ctor.construction,
2381 },
2382 )?;
2383 Ok(())
2384 }
2385
2386 fn edit_arc(
2387 &mut self,
2388 new_ast: &mut ast::Node<ast::Program>,
2389 sketch: ObjectId,
2390 arc: ObjectId,
2391 ctor: ArcCtor,
2392 ) -> Result<(), KclError> {
2393 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2395 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2396 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2397
2398 let sketch_id = sketch;
2400 let sketch_object = self
2401 .scene_graph
2402 .objects
2403 .get(sketch_id.0)
2404 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2405 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2406 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2407 };
2408 sketch
2409 .segments
2410 .iter()
2411 .find(|o| **o == arc)
2412 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2413 let arc_id = arc;
2415 let arc_object = self
2416 .scene_graph
2417 .objects
2418 .get(arc_id.0)
2419 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2420 let ObjectKind::Segment { .. } = &arc_object.kind else {
2421 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2422 };
2423
2424 self.mutate_ast(
2426 new_ast,
2427 arc_id,
2428 AstMutateCommand::EditArc {
2429 start: new_start_ast,
2430 end: new_end_ast,
2431 center: new_center_ast,
2432 construction: ctor.construction,
2433 },
2434 )?;
2435 Ok(())
2436 }
2437
2438 fn edit_circle(
2439 &mut self,
2440 new_ast: &mut ast::Node<ast::Program>,
2441 sketch: ObjectId,
2442 circle: ObjectId,
2443 ctor: CircleCtor,
2444 ) -> Result<(), KclError> {
2445 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2447 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2448
2449 let sketch_id = sketch;
2451 let sketch_object = self
2452 .scene_graph
2453 .objects
2454 .get(sketch_id.0)
2455 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2456 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2457 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2458 };
2459 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2460 KclError::refactor(format!(
2461 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2462 ))
2463 })?;
2464 let circle_id = circle;
2466 let circle_object = self
2467 .scene_graph
2468 .objects
2469 .get(circle_id.0)
2470 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2471 let ObjectKind::Segment { .. } = &circle_object.kind else {
2472 return Err(KclError::refactor(format!(
2473 "Object is not a segment: {circle_object:?}"
2474 )));
2475 };
2476
2477 self.mutate_ast(
2479 new_ast,
2480 circle_id,
2481 AstMutateCommand::EditCircle {
2482 start: new_start_ast,
2483 center: new_center_ast,
2484 construction: ctor.construction,
2485 },
2486 )?;
2487 Ok(())
2488 }
2489
2490 fn delete_segment(
2491 &mut self,
2492 new_ast: &mut ast::Node<ast::Program>,
2493 sketch: ObjectId,
2494 segment_id: ObjectId,
2495 ) -> Result<(), KclError> {
2496 let sketch_id = sketch;
2498 let sketch_object = self
2499 .scene_graph
2500 .objects
2501 .get(sketch_id.0)
2502 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2503 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2504 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2505 };
2506 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2507 KclError::refactor(format!(
2508 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2509 ))
2510 })?;
2511 let segment_object =
2513 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2514 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2515 })?;
2516 let ObjectKind::Segment { .. } = &segment_object.kind else {
2517 return Err(KclError::refactor(format!(
2518 "Object is not a segment, it is {}",
2519 segment_object.kind.human_friendly_kind_with_article()
2520 )));
2521 };
2522
2523 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2525 Ok(())
2526 }
2527
2528 fn delete_constraint(
2529 &mut self,
2530 new_ast: &mut ast::Node<ast::Program>,
2531 sketch: ObjectId,
2532 constraint_id: ObjectId,
2533 ) -> Result<(), KclError> {
2534 let sketch_id = sketch;
2536 let sketch_object = self
2537 .scene_graph
2538 .objects
2539 .get(sketch_id.0)
2540 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2541 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2542 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2543 };
2544 sketch
2545 .constraints
2546 .iter()
2547 .find(|o| **o == constraint_id)
2548 .ok_or_else(|| {
2549 KclError::refactor(format!(
2550 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2551 ))
2552 })?;
2553 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2555 KclError::refactor(format!(
2556 "Constraint not found in scene graph: constraint={constraint_id:?}"
2557 ))
2558 })?;
2559 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2560 return Err(KclError::refactor(format!(
2561 "Object is not a constraint, it is {}",
2562 constraint_object.kind.human_friendly_kind_with_article()
2563 )));
2564 };
2565
2566 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2568 Ok(())
2569 }
2570
2571 fn edit_coincident_constraint(
2572 &mut self,
2573 new_ast: &mut ast::Node<ast::Program>,
2574 constraint_id: ObjectId,
2575 segments: Vec<ConstraintSegment>,
2576 ) -> Result<(), KclError> {
2577 if segments.len() < 2 {
2578 return Err(KclError::refactor(format!(
2579 "Coincident constraint must have at least 2 inputs, got {}",
2580 segments.len()
2581 )));
2582 }
2583
2584 let segment_asts = segments
2585 .iter()
2586 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2587 .collect::<Result<Vec<_>, _>>()?;
2588
2589 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2590 elements: segment_asts,
2591 digest: None,
2592 non_code_meta: Default::default(),
2593 })));
2594
2595 self.mutate_ast(
2596 new_ast,
2597 constraint_id,
2598 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2599 )?;
2600 Ok(())
2601 }
2602
2603 fn edit_horizontal_points_constraint(
2604 &mut self,
2605 new_ast: &mut ast::Node<ast::Program>,
2606 constraint_id: ObjectId,
2607 points: Vec<ConstraintSegment>,
2608 ) -> Result<(), KclError> {
2609 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2610 }
2611
2612 fn edit_vertical_points_constraint(
2613 &mut self,
2614 new_ast: &mut ast::Node<ast::Program>,
2615 constraint_id: ObjectId,
2616 points: Vec<ConstraintSegment>,
2617 ) -> Result<(), KclError> {
2618 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2619 }
2620
2621 fn edit_axis_points_constraint(
2622 &mut self,
2623 new_ast: &mut ast::Node<ast::Program>,
2624 constraint_id: ObjectId,
2625 points: Vec<ConstraintSegment>,
2626 constraint_name: &str,
2627 ) -> Result<(), KclError> {
2628 if points.len() < 2 {
2629 return Err(KclError::refactor(format!(
2630 "{constraint_name} points constraint must have at least 2 points, got {}",
2631 points.len()
2632 )));
2633 }
2634
2635 let point_asts = points
2636 .iter()
2637 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2638 .collect::<Result<Vec<_>, _>>()?;
2639
2640 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2641 elements: point_asts,
2642 digest: None,
2643 non_code_meta: Default::default(),
2644 })));
2645
2646 self.mutate_ast(
2647 new_ast,
2648 constraint_id,
2649 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2650 )?;
2651 Ok(())
2652 }
2653
2654 fn edit_equal_length_constraint(
2656 &mut self,
2657 new_ast: &mut ast::Node<ast::Program>,
2658 constraint_id: ObjectId,
2659 lines: Vec<ObjectId>,
2660 ) -> Result<(), KclError> {
2661 if lines.len() < 2 {
2662 return Err(KclError::refactor(format!(
2663 "Lines equal length constraint must have at least 2 lines, got {}",
2664 lines.len()
2665 )));
2666 }
2667
2668 let line_asts = lines
2669 .iter()
2670 .map(|line_id| {
2671 let line_object = self
2672 .scene_graph
2673 .objects
2674 .get(line_id.0)
2675 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2676 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2677 let kind = line_object.kind.human_friendly_kind_with_article();
2678 return Err(KclError::refactor(format!(
2679 "This constraint only works on Segments, but you selected {kind}"
2680 )));
2681 };
2682 let Segment::Line(_) = line_segment else {
2683 let kind = line_segment.human_friendly_kind_with_article();
2684 return Err(KclError::refactor(format!(
2685 "Only lines can be made equal length, but you selected {kind}"
2686 )));
2687 };
2688
2689 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2690 })
2691 .collect::<Result<Vec<_>, _>>()?;
2692
2693 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2694 elements: line_asts,
2695 digest: None,
2696 non_code_meta: Default::default(),
2697 })));
2698
2699 self.mutate_ast(
2700 new_ast,
2701 constraint_id,
2702 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2703 )?;
2704 Ok(())
2705 }
2706
2707 fn edit_parallel_constraint(
2709 &mut self,
2710 new_ast: &mut ast::Node<ast::Program>,
2711 constraint_id: ObjectId,
2712 lines: Vec<ObjectId>,
2713 ) -> Result<(), KclError> {
2714 if lines.len() < 2 {
2715 return Err(KclError::refactor(format!(
2716 "Parallel constraint must have at least 2 lines, got {}",
2717 lines.len()
2718 )));
2719 }
2720
2721 let line_asts = lines
2722 .iter()
2723 .map(|line_id| {
2724 let line_object = self
2725 .scene_graph
2726 .objects
2727 .get(line_id.0)
2728 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2729 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2730 let kind = line_object.kind.human_friendly_kind_with_article();
2731 return Err(KclError::refactor(format!(
2732 "This constraint only works on Segments, but you selected {kind}"
2733 )));
2734 };
2735 let Segment::Line(_) = line_segment else {
2736 let kind = line_segment.human_friendly_kind_with_article();
2737 return Err(KclError::refactor(format!(
2738 "Only lines can be made parallel, but you selected {kind}"
2739 )));
2740 };
2741
2742 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2743 })
2744 .collect::<Result<Vec<_>, _>>()?;
2745
2746 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2747 elements: line_asts,
2748 digest: None,
2749 non_code_meta: Default::default(),
2750 })));
2751
2752 self.mutate_ast(
2753 new_ast,
2754 constraint_id,
2755 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2756 )?;
2757 Ok(())
2758 }
2759
2760 fn edit_equal_radius_constraint(
2762 &mut self,
2763 new_ast: &mut ast::Node<ast::Program>,
2764 constraint_id: ObjectId,
2765 input: Vec<ObjectId>,
2766 ) -> Result<(), KclError> {
2767 if input.len() < 2 {
2768 return Err(KclError::refactor(format!(
2769 "equalRadius constraint must have at least 2 segments, got {}",
2770 input.len()
2771 )));
2772 }
2773
2774 let input_asts = input
2775 .iter()
2776 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2777 .collect::<Result<Vec<_>, _>>()?;
2778
2779 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2780 elements: input_asts,
2781 digest: None,
2782 non_code_meta: Default::default(),
2783 })));
2784
2785 self.mutate_ast(
2786 new_ast,
2787 constraint_id,
2788 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2789 )?;
2790 Ok(())
2791 }
2792
2793 async fn execute_after_edit(
2794 &mut self,
2795 ctx: &ExecutorContext,
2796 sketch: ObjectId,
2797 sketch_block_ref: AstNodeRef,
2798 segment_ids_edited: AhashIndexSet<ObjectId>,
2799 edit_kind: EditDeleteKind,
2800 new_ast: &mut ast::Node<ast::Program>,
2801 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2802 let new_source = source_from_ast(new_ast);
2804 let (new_program, errors) = Program::parse(&new_source)
2806 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2807 if !errors.is_empty() {
2808 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2809 "Error parsing KCL source after editing: {errors:?}"
2810 ))));
2811 }
2812 let Some(new_program) = new_program else {
2813 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2814 "No AST produced after editing".to_string(),
2815 )));
2816 };
2817
2818 self.program = new_program.clone();
2820
2821 let is_delete = edit_kind.is_delete();
2823 let truncated_program = {
2824 let mut truncated_program = new_program;
2825 only_sketch_block(
2826 &mut truncated_program.ast,
2827 &sketch_block_ref,
2828 edit_kind.to_change_kind(),
2829 )
2830 .map_err(KclErrorWithOutputs::no_outputs)?;
2831 truncated_program
2832 };
2833
2834 #[cfg(not(feature = "artifact-graph"))]
2835 drop(segment_ids_edited);
2836
2837 let mock_config = MockConfig {
2839 sketch_block_id: Some(sketch),
2840 freedom_analysis: is_delete,
2841 #[cfg(feature = "artifact-graph")]
2842 segment_ids_edited: segment_ids_edited.clone(),
2843 ..Default::default()
2844 };
2845 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2846
2847 let outcome = self.update_state_after_exec(outcome, is_delete);
2849
2850 #[cfg(feature = "artifact-graph")]
2851 let new_source = {
2852 let mut new_ast = self.program.ast.clone();
2857 for (var_range, value) in &outcome.var_solutions {
2858 let rounded = value.round(3);
2859 let source_ref = SourceRef::Simple {
2860 range: *var_range,
2861 node_path: None,
2862 };
2863 mutate_ast_node_by_source_ref(
2864 &mut new_ast,
2865 &source_ref,
2866 AstMutateCommand::EditVarInitialValue { value: rounded },
2867 )
2868 .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2869 }
2870 source_from_ast(&new_ast)
2871 };
2872
2873 let src_delta = SourceDelta { text: new_source };
2874 let scene_graph_delta = SceneGraphDelta {
2875 new_graph: self.scene_graph.clone(),
2876 invalidates_ids: is_delete,
2877 new_objects: Vec::new(),
2878 exec_outcome: outcome,
2879 };
2880 Ok((src_delta, scene_graph_delta))
2881 }
2882
2883 async fn execute_after_delete_sketch(
2884 &mut self,
2885 ctx: &ExecutorContext,
2886 new_ast: &mut ast::Node<ast::Program>,
2887 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2888 let new_source = source_from_ast(new_ast);
2890 let (new_program, errors) = Program::parse(&new_source)
2892 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2893 if !errors.is_empty() {
2894 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2895 "Error parsing KCL source after editing: {errors:?}"
2896 ))));
2897 }
2898 let Some(new_program) = new_program else {
2899 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2900 "No AST produced after editing".to_string(),
2901 )));
2902 };
2903
2904 self.program = new_program.clone();
2906
2907 let outcome = ctx.run_with_caching(new_program).await?;
2913 let freedom_analysis_ran = true;
2914
2915 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2916
2917 let src_delta = SourceDelta { text: new_source };
2918 let scene_graph_delta = SceneGraphDelta {
2919 new_graph: self.scene_graph.clone(),
2920 invalidates_ids: true,
2921 new_objects: Vec::new(),
2922 exec_outcome: outcome,
2923 };
2924 Ok((src_delta, scene_graph_delta))
2925 }
2926
2927 fn point_id_to_ast_reference(
2932 &self,
2933 point_id: ObjectId,
2934 new_ast: &mut ast::Node<ast::Program>,
2935 ) -> Result<ast::Expr, KclError> {
2936 let point_object = self
2937 .scene_graph
2938 .objects
2939 .get(point_id.0)
2940 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2941 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2942 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2943 };
2944 let Segment::Point(point) = point_segment else {
2945 return Err(KclError::refactor(format!(
2946 "Only points are currently supported: {point_object:?}"
2947 )));
2948 };
2949
2950 if let Some(owner_id) = point.owner {
2951 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2952 KclError::refactor(format!(
2953 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2954 ))
2955 })?;
2956 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2957 return Err(KclError::refactor(format!(
2958 "Owner of point is not a segment, but found {}",
2959 owner_object.kind.human_friendly_kind_with_article()
2960 )));
2961 };
2962
2963 match owner_segment {
2964 Segment::Line(line) => {
2965 let property = if line.start == point_id {
2966 LINE_PROPERTY_START
2967 } else if line.end == point_id {
2968 LINE_PROPERTY_END
2969 } else {
2970 return Err(KclError::refactor(format!(
2971 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2972 )));
2973 };
2974 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
2975 }
2976 Segment::Arc(arc) => {
2977 let property = if arc.start == point_id {
2978 ARC_PROPERTY_START
2979 } else if arc.end == point_id {
2980 ARC_PROPERTY_END
2981 } else if arc.center == point_id {
2982 ARC_PROPERTY_CENTER
2983 } else {
2984 return Err(KclError::refactor(format!(
2985 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2986 )));
2987 };
2988 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
2989 }
2990 Segment::Circle(circle) => {
2991 let property = if circle.start == point_id {
2992 CIRCLE_PROPERTY_START
2993 } else if circle.center == point_id {
2994 CIRCLE_PROPERTY_CENTER
2995 } else {
2996 return Err(KclError::refactor(format!(
2997 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2998 )));
2999 };
3000 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3001 }
3002 _ => Err(KclError::refactor(format!(
3003 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3004 ))),
3005 }
3006 } else {
3007 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3009 }
3010 }
3011
3012 fn coincident_segment_to_ast(
3013 &self,
3014 segment: &ConstraintSegment,
3015 new_ast: &mut ast::Node<ast::Program>,
3016 ) -> Result<ast::Expr, KclError> {
3017 match segment {
3018 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3019 ConstraintSegment::Segment(segment_id) => {
3020 let segment_object = self
3021 .scene_graph
3022 .objects
3023 .get(segment_id.0)
3024 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3025 let ObjectKind::Segment { segment } = &segment_object.kind else {
3026 return Err(KclError::refactor(format!(
3027 "Object is not a segment, it is {}",
3028 segment_object.kind.human_friendly_kind_with_article()
3029 )));
3030 };
3031
3032 match segment {
3033 Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
3034 Segment::Line(_) => {
3035 get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None)
3036 }
3037 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3038 Segment::Circle(_) => {
3039 get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
3040 }
3041 }
3042 }
3043 }
3044 }
3045
3046 fn axis_constraint_segment_to_ast(
3047 &self,
3048 segment: &ConstraintSegment,
3049 new_ast: &mut ast::Node<ast::Program>,
3050 ) -> Result<ast::Expr, KclError> {
3051 match segment {
3052 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3053 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3054 }
3055 }
3056
3057 async fn add_coincident(
3058 &mut self,
3059 sketch: ObjectId,
3060 coincident: Coincident,
3061 new_ast: &mut ast::Node<ast::Program>,
3062 ) -> Result<AstNodeRef, KclError> {
3063 let sketch_id = sketch;
3064 let segment_asts = coincident
3065 .segments
3066 .iter()
3067 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3068 .collect::<Result<Vec<_>, _>>()?;
3069 if segment_asts.len() < 2 {
3070 return Err(KclError::refactor(format!(
3071 "Coincident constraint must have at least 2 inputs, got {}",
3072 segment_asts.len()
3073 )));
3074 }
3075
3076 let coincident_ast = create_coincident_ast(segment_asts);
3078
3079 let (sketch_block_ref, _) = self.mutate_ast(
3081 new_ast,
3082 sketch_id,
3083 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3084 )?;
3085 Ok(sketch_block_ref)
3086 }
3087
3088 async fn add_distance(
3089 &mut self,
3090 sketch: ObjectId,
3091 distance: Distance,
3092 new_ast: &mut ast::Node<ast::Program>,
3093 ) -> Result<AstNodeRef, KclError> {
3094 let sketch_id = sketch;
3095 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3096 [pt0, pt1] => [
3097 self.coincident_segment_to_ast(pt0, new_ast)?,
3098 self.coincident_segment_to_ast(pt1, new_ast)?,
3099 ],
3100 _ => {
3101 return Err(KclError::refactor(format!(
3102 "Distance constraint must have exactly 2 points, got {}",
3103 distance.points.len()
3104 )));
3105 }
3106 };
3107
3108 let arguments = match &distance.label_position {
3109 Some(label_position) => vec![ast::LabeledArg {
3110 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3111 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3112 }],
3113 None => Default::default(),
3114 };
3115
3116 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3118 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3119 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3120 ast::ArrayExpression {
3121 elements: vec![pt0_ast, pt1_ast],
3122 digest: None,
3123 non_code_meta: Default::default(),
3124 },
3125 )))),
3126 arguments,
3127 digest: None,
3128 non_code_meta: Default::default(),
3129 })));
3130 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3131 left: distance_call_ast,
3132 operator: ast::BinaryOperator::Eq,
3133 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3134 value: ast::LiteralValue::Number {
3135 value: distance.distance.value,
3136 suffix: distance.distance.units,
3137 },
3138 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3139 KclError::refactor(format!(
3140 "Could not format numeric suffix: {:?}",
3141 distance.distance.units
3142 ))
3143 })?,
3144 digest: None,
3145 }))),
3146 digest: None,
3147 })));
3148
3149 let (sketch_block_ref, _) = self.mutate_ast(
3151 new_ast,
3152 sketch_id,
3153 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3154 )?;
3155 Ok(sketch_block_ref)
3156 }
3157
3158 async fn add_angle(
3159 &mut self,
3160 sketch: ObjectId,
3161 angle: Angle,
3162 new_ast: &mut ast::Node<ast::Program>,
3163 ) -> Result<AstNodeRef, KclError> {
3164 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3165 return Err(KclError::refactor(format!(
3166 "Angle constraint must have exactly 2 lines, got {}",
3167 angle.lines.len()
3168 )));
3169 };
3170 let sketch_id = sketch;
3171
3172 let line0_object = self
3174 .scene_graph
3175 .objects
3176 .get(l0_id.0)
3177 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3178 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3179 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3180 };
3181 let Segment::Line(_) = line0_segment else {
3182 return Err(KclError::refactor(format!(
3183 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3184 )));
3185 };
3186 let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3187
3188 let line1_object = self
3189 .scene_graph
3190 .objects
3191 .get(l1_id.0)
3192 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3193 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3194 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3195 };
3196 let Segment::Line(_) = line1_segment else {
3197 return Err(KclError::refactor(format!(
3198 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3199 )));
3200 };
3201 let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3202
3203 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3205 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3206 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3207 ast::ArrayExpression {
3208 elements: vec![l0_ast, l1_ast],
3209 digest: None,
3210 non_code_meta: Default::default(),
3211 },
3212 )))),
3213 arguments: Default::default(),
3214 digest: None,
3215 non_code_meta: Default::default(),
3216 })));
3217 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3218 left: angle_call_ast,
3219 operator: ast::BinaryOperator::Eq,
3220 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3221 value: ast::LiteralValue::Number {
3222 value: angle.angle.value,
3223 suffix: angle.angle.units,
3224 },
3225 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3226 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3227 })?,
3228 digest: None,
3229 }))),
3230 digest: None,
3231 })));
3232
3233 let (sketch_block_ref, _) = self.mutate_ast(
3235 new_ast,
3236 sketch_id,
3237 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3238 )?;
3239 Ok(sketch_block_ref)
3240 }
3241
3242 async fn add_tangent(
3243 &mut self,
3244 sketch: ObjectId,
3245 tangent: Tangent,
3246 new_ast: &mut ast::Node<ast::Program>,
3247 ) -> Result<AstNodeRef, KclError> {
3248 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3249 return Err(KclError::refactor(format!(
3250 "Tangent constraint must have exactly 2 segments, got {}",
3251 tangent.input.len()
3252 )));
3253 };
3254 let sketch_id = sketch;
3255
3256 let seg0_object = self
3257 .scene_graph
3258 .objects
3259 .get(seg0_id.0)
3260 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3261 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3262 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3263 };
3264 let seg0_ast = match seg0_segment {
3265 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, LINE_VARIABLE, None)?,
3266 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, ARC_VARIABLE, None)?,
3267 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3268 _ => {
3269 return Err(KclError::refactor(format!(
3270 "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3271 )));
3272 }
3273 };
3274
3275 let seg1_object = self
3276 .scene_graph
3277 .objects
3278 .get(seg1_id.0)
3279 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3280 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3281 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3282 };
3283 let seg1_ast = match seg1_segment {
3284 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, LINE_VARIABLE, None)?,
3285 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, ARC_VARIABLE, None)?,
3286 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3287 _ => {
3288 return Err(KclError::refactor(format!(
3289 "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3290 )));
3291 }
3292 };
3293
3294 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3295 let (sketch_block_ref, _) = self.mutate_ast(
3296 new_ast,
3297 sketch_id,
3298 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3299 )?;
3300 Ok(sketch_block_ref)
3301 }
3302
3303 async fn add_symmetric(
3304 &mut self,
3305 sketch: ObjectId,
3306 symmetric: Symmetric,
3307 new_ast: &mut ast::Node<ast::Program>,
3308 ) -> Result<AstNodeRef, KclError> {
3309 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3310 return Err(KclError::refactor(format!(
3311 "Symmetric constraint must have exactly 2 inputs, got {}",
3312 symmetric.input.len()
3313 )));
3314 };
3315 let sketch_id = sketch;
3316
3317 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3318 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3319 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3320
3321 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3322 let (sketch_block_ref, _) = self.mutate_ast(
3323 new_ast,
3324 sketch_id,
3325 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3326 )?;
3327 Ok(sketch_block_ref)
3328 }
3329
3330 async fn add_midpoint(
3331 &mut self,
3332 sketch: ObjectId,
3333 midpoint: Midpoint,
3334 new_ast: &mut ast::Node<ast::Program>,
3335 ) -> Result<AstNodeRef, KclError> {
3336 let sketch_id = sketch;
3337 let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3338
3339 let segment_object = self
3340 .scene_graph
3341 .objects
3342 .get(midpoint.segment.0)
3343 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3344 let ObjectKind::Segment {
3345 segment: midpoint_segment,
3346 } = &segment_object.kind
3347 else {
3348 return Err(KclError::refactor(format!(
3349 "Object must be a segment, but it was {}",
3350 segment_object.kind.human_friendly_kind_with_article()
3351 )));
3352 };
3353 let segment_ast = match midpoint_segment {
3354 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3355 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3356 _ => {
3357 return Err(KclError::refactor(format!(
3358 "Midpoint target must be a line or arc segment but it was {}",
3359 midpoint_segment.human_friendly_kind_with_article()
3360 )));
3361 }
3362 };
3363
3364 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3365 let (sketch_block_ref, _) = self.mutate_ast(
3366 new_ast,
3367 sketch_id,
3368 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3369 )?;
3370 Ok(sketch_block_ref)
3371 }
3372
3373 async fn add_equal_radius(
3374 &mut self,
3375 sketch: ObjectId,
3376 equal_radius: EqualRadius,
3377 new_ast: &mut ast::Node<ast::Program>,
3378 ) -> Result<AstNodeRef, KclError> {
3379 if equal_radius.input.len() < 2 {
3380 return Err(KclError::refactor(format!(
3381 "equalRadius constraint must have at least 2 segments, got {}",
3382 equal_radius.input.len()
3383 )));
3384 }
3385
3386 let sketch_id = sketch;
3387 let input_asts = equal_radius
3388 .input
3389 .iter()
3390 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3391 .collect::<Result<Vec<_>, _>>()?;
3392
3393 let equal_radius_ast = create_equal_radius_ast(input_asts);
3394 let (sketch_block_ref, _) = self.mutate_ast(
3395 new_ast,
3396 sketch_id,
3397 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3398 )?;
3399 Ok(sketch_block_ref)
3400 }
3401
3402 async fn add_radius(
3403 &mut self,
3404 sketch: ObjectId,
3405 radius: Radius,
3406 new_ast: &mut ast::Node<ast::Program>,
3407 ) -> Result<AstNodeRef, KclError> {
3408 let params = ArcSizeConstraintParams {
3409 points: vec![radius.arc],
3410 function_name: RADIUS_FN,
3411 value: radius.radius.value,
3412 units: radius.radius.units,
3413 label_position: radius.label_position,
3414 constraint_type_name: "Radius",
3415 };
3416 self.add_arc_size_constraint(sketch, params, new_ast).await
3417 }
3418
3419 async fn add_diameter(
3420 &mut self,
3421 sketch: ObjectId,
3422 diameter: Diameter,
3423 new_ast: &mut ast::Node<ast::Program>,
3424 ) -> Result<AstNodeRef, KclError> {
3425 let params = ArcSizeConstraintParams {
3426 points: vec![diameter.arc],
3427 function_name: DIAMETER_FN,
3428 value: diameter.diameter.value,
3429 units: diameter.diameter.units,
3430 label_position: diameter.label_position,
3431 constraint_type_name: "Diameter",
3432 };
3433 self.add_arc_size_constraint(sketch, params, new_ast).await
3434 }
3435
3436 async fn add_fixed_constraints(
3437 &mut self,
3438 sketch: ObjectId,
3439 points: Vec<FixedPoint>,
3440 new_ast: &mut ast::Node<ast::Program>,
3441 ) -> Result<AstNodeRef, KclError> {
3442 let mut sketch_block_ref = None;
3443
3444 for fixed_point in points {
3445 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3446 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3447 .map_err(|err| KclError::refactor(err.to_string()))?;
3448
3449 let (sketch_ref, _) = self.mutate_ast(
3450 new_ast,
3451 sketch,
3452 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3453 )?;
3454 sketch_block_ref = Some(sketch_ref);
3455 }
3456
3457 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3458 }
3459
3460 async fn add_arc_size_constraint(
3461 &mut self,
3462 sketch: ObjectId,
3463 params: ArcSizeConstraintParams,
3464 new_ast: &mut ast::Node<ast::Program>,
3465 ) -> Result<AstNodeRef, KclError> {
3466 let sketch_id = sketch;
3467
3468 if params.points.len() != 1 {
3470 return Err(KclError::refactor(format!(
3471 "{} constraint must have exactly 1 argument (an arc segment), got {}",
3472 params.constraint_type_name,
3473 params.points.len()
3474 )));
3475 }
3476
3477 let arc_id = params.points[0];
3478 let arc_object = self
3479 .scene_graph
3480 .objects
3481 .get(arc_id.0)
3482 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3483 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3484 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3485 };
3486 let ref_type = match arc_segment {
3487 Segment::Arc(_) => ARC_VARIABLE,
3488 Segment::Circle(_) => CIRCLE_VARIABLE,
3489 _ => {
3490 return Err(KclError::refactor(format!(
3491 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3492 params.constraint_type_name
3493 )));
3494 }
3495 };
3496 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3498 let arguments = match ¶ms.label_position {
3499 Some(label_position) => vec![ast::LabeledArg {
3500 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3501 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3502 }],
3503 None => Default::default(),
3504 };
3505
3506 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3508 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3509 unlabeled: Some(arc_ast),
3510 arguments,
3511 digest: None,
3512 non_code_meta: Default::default(),
3513 })));
3514 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3515 left: call_ast,
3516 operator: ast::BinaryOperator::Eq,
3517 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3518 value: ast::LiteralValue::Number {
3519 value: params.value,
3520 suffix: params.units,
3521 },
3522 raw: format_number_literal(params.value, params.units, None)
3523 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3524 digest: None,
3525 }))),
3526 digest: None,
3527 })));
3528
3529 let (sketch_block_ref, _) = self.mutate_ast(
3531 new_ast,
3532 sketch_id,
3533 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3534 )?;
3535 Ok(sketch_block_ref)
3536 }
3537
3538 async fn add_horizontal_distance(
3539 &mut self,
3540 sketch: ObjectId,
3541 distance: Distance,
3542 new_ast: &mut ast::Node<ast::Program>,
3543 ) -> Result<AstNodeRef, KclError> {
3544 let sketch_id = sketch;
3545 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3546 [pt0, pt1] => [
3547 self.coincident_segment_to_ast(pt0, new_ast)?,
3548 self.coincident_segment_to_ast(pt1, new_ast)?,
3549 ],
3550 _ => {
3551 return Err(KclError::refactor(format!(
3552 "Horizontal distance constraint must have exactly 2 points, got {}",
3553 distance.points.len()
3554 )));
3555 }
3556 };
3557
3558 let arguments = match &distance.label_position {
3559 Some(label_position) => vec![ast::LabeledArg {
3560 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3561 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3562 }],
3563 None => Default::default(),
3564 };
3565
3566 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3568 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3569 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3570 ast::ArrayExpression {
3571 elements: vec![pt0_ast, pt1_ast],
3572 digest: None,
3573 non_code_meta: Default::default(),
3574 },
3575 )))),
3576 arguments,
3577 digest: None,
3578 non_code_meta: Default::default(),
3579 })));
3580 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3581 left: distance_call_ast,
3582 operator: ast::BinaryOperator::Eq,
3583 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3584 value: ast::LiteralValue::Number {
3585 value: distance.distance.value,
3586 suffix: distance.distance.units,
3587 },
3588 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3589 KclError::refactor(format!(
3590 "Could not format numeric suffix: {:?}",
3591 distance.distance.units
3592 ))
3593 })?,
3594 digest: None,
3595 }))),
3596 digest: None,
3597 })));
3598
3599 let (sketch_block_ref, _) = self.mutate_ast(
3601 new_ast,
3602 sketch_id,
3603 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3604 )?;
3605 Ok(sketch_block_ref)
3606 }
3607
3608 async fn add_vertical_distance(
3609 &mut self,
3610 sketch: ObjectId,
3611 distance: Distance,
3612 new_ast: &mut ast::Node<ast::Program>,
3613 ) -> Result<AstNodeRef, KclError> {
3614 let sketch_id = sketch;
3615 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3616 [pt0, pt1] => [
3617 self.coincident_segment_to_ast(pt0, new_ast)?,
3618 self.coincident_segment_to_ast(pt1, new_ast)?,
3619 ],
3620 _ => {
3621 return Err(KclError::refactor(format!(
3622 "Vertical distance constraint must have exactly 2 points, got {}",
3623 distance.points.len()
3624 )));
3625 }
3626 };
3627
3628 let arguments = match &distance.label_position {
3629 Some(label_position) => vec![ast::LabeledArg {
3630 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3631 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3632 }],
3633 None => Default::default(),
3634 };
3635
3636 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3638 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3639 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3640 ast::ArrayExpression {
3641 elements: vec![pt0_ast, pt1_ast],
3642 digest: None,
3643 non_code_meta: Default::default(),
3644 },
3645 )))),
3646 arguments,
3647 digest: None,
3648 non_code_meta: Default::default(),
3649 })));
3650 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3651 left: distance_call_ast,
3652 operator: ast::BinaryOperator::Eq,
3653 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3654 value: ast::LiteralValue::Number {
3655 value: distance.distance.value,
3656 suffix: distance.distance.units,
3657 },
3658 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3659 KclError::refactor(format!(
3660 "Could not format numeric suffix: {:?}",
3661 distance.distance.units
3662 ))
3663 })?,
3664 digest: None,
3665 }))),
3666 digest: None,
3667 })));
3668
3669 let (sketch_block_ref, _) = self.mutate_ast(
3671 new_ast,
3672 sketch_id,
3673 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3674 )?;
3675 Ok(sketch_block_ref)
3676 }
3677
3678 async fn add_horizontal(
3679 &mut self,
3680 sketch: ObjectId,
3681 horizontal: Horizontal,
3682 new_ast: &mut ast::Node<ast::Program>,
3683 ) -> Result<AstNodeRef, KclError> {
3684 let sketch_id = sketch;
3685
3686 let first_arg_ast = match horizontal {
3688 Horizontal::Line { line } => {
3689 let line_object = self
3690 .scene_graph
3691 .objects
3692 .get(line.0)
3693 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3694 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3695 let kind = line_object.kind.human_friendly_kind_with_article();
3696 return Err(KclError::refactor(format!(
3697 "This constraint only works on Segments, but you selected {kind}"
3698 )));
3699 };
3700 let Segment::Line(_) = line_segment else {
3701 return Err(KclError::refactor(format!(
3702 "Only lines can be made horizontal, but you selected {}",
3703 line_segment.human_friendly_kind_with_article(),
3704 )));
3705 };
3706 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3707 }
3708 Horizontal::Points { points } => {
3709 let point_asts = points
3710 .iter()
3711 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3712 .collect::<Result<Vec<_>, _>>()?;
3713 ast::ArrayExpression::new(point_asts).into()
3714 }
3715 };
3716
3717 let horizontal_ast = create_horizontal_ast(first_arg_ast);
3719
3720 let (sketch_block_ref, _) = self.mutate_ast(
3722 new_ast,
3723 sketch_id,
3724 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3725 )?;
3726 Ok(sketch_block_ref)
3727 }
3728
3729 async fn add_lines_equal_length(
3730 &mut self,
3731 sketch: ObjectId,
3732 lines_equal_length: LinesEqualLength,
3733 new_ast: &mut ast::Node<ast::Program>,
3734 ) -> Result<AstNodeRef, KclError> {
3735 if lines_equal_length.lines.len() < 2 {
3736 return Err(KclError::refactor(format!(
3737 "Lines equal length constraint must have at least 2 lines, got {}",
3738 lines_equal_length.lines.len()
3739 )));
3740 };
3741
3742 let sketch_id = sketch;
3743
3744 let line_asts = lines_equal_length
3746 .lines
3747 .iter()
3748 .map(|line_id| {
3749 let line_object = self
3750 .scene_graph
3751 .objects
3752 .get(line_id.0)
3753 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3754 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3755 let kind = line_object.kind.human_friendly_kind_with_article();
3756 return Err(KclError::refactor(format!(
3757 "This constraint only works on Segments, but you selected {kind}"
3758 )));
3759 };
3760 let Segment::Line(_) = line_segment else {
3761 let kind = line_segment.human_friendly_kind_with_article();
3762 return Err(KclError::refactor(format!(
3763 "Only lines can be made equal length, but you selected {kind}"
3764 )));
3765 };
3766
3767 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3768 })
3769 .collect::<Result<Vec<_>, _>>()?;
3770
3771 let equal_length_ast = create_equal_length_ast(line_asts);
3773
3774 let (sketch_block_ref, _) = self.mutate_ast(
3776 new_ast,
3777 sketch_id,
3778 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3779 )?;
3780 Ok(sketch_block_ref)
3781 }
3782
3783 fn equal_radius_segment_id_to_ast_reference(
3784 &mut self,
3785 segment_id: ObjectId,
3786 new_ast: &mut ast::Node<ast::Program>,
3787 ) -> Result<ast::Expr, KclError> {
3788 let segment_object = self
3789 .scene_graph
3790 .objects
3791 .get(segment_id.0)
3792 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3793 let ObjectKind::Segment { segment } = &segment_object.kind else {
3794 return Err(KclError::refactor(format!(
3795 "Object is not a segment, it was {}",
3796 segment_object.kind.human_friendly_kind_with_article()
3797 )));
3798 };
3799
3800 let ref_type = match segment {
3801 Segment::Arc(_) => ARC_VARIABLE,
3802 Segment::Circle(_) => CIRCLE_VARIABLE,
3803 _ => {
3804 return Err(KclError::refactor(format!(
3805 "equalRadius supports only arc/circle segments, got {}",
3806 segment.human_friendly_kind_with_article()
3807 )));
3808 }
3809 };
3810
3811 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3812 }
3813
3814 fn symmetric_input_id_to_ast_reference(
3815 &mut self,
3816 segment_id: ObjectId,
3817 new_ast: &mut ast::Node<ast::Program>,
3818 ) -> Result<ast::Expr, KclError> {
3819 let segment_object = self
3820 .scene_graph
3821 .objects
3822 .get(segment_id.0)
3823 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3824 let ObjectKind::Segment { segment } = &segment_object.kind else {
3825 return Err(KclError::refactor(format!(
3826 "Object is not a segment, it was {}",
3827 segment_object.kind.human_friendly_kind_with_article()
3828 )));
3829 };
3830
3831 match segment {
3832 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3833 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3834 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3835 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3836 }
3837 }
3838
3839 fn symmetric_axis_id_to_ast_reference(
3840 &mut self,
3841 segment_id: ObjectId,
3842 new_ast: &mut ast::Node<ast::Program>,
3843 ) -> Result<ast::Expr, KclError> {
3844 let segment_object = self
3845 .scene_graph
3846 .objects
3847 .get(segment_id.0)
3848 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
3849 let ObjectKind::Segment { segment } = &segment_object.kind else {
3850 return Err(KclError::refactor(format!(
3851 "Object is not a segment, it was {}",
3852 segment_object.kind.human_friendly_kind_with_article()
3853 )));
3854 };
3855 match segment {
3856 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3857 _ => Err(KclError::refactor(format!(
3858 "Symmetric axis must be a line, got {}",
3859 segment.human_friendly_kind_with_article()
3860 ))),
3861 }
3862 }
3863
3864 async fn add_parallel(
3865 &mut self,
3866 sketch: ObjectId,
3867 parallel: Parallel,
3868 new_ast: &mut ast::Node<ast::Program>,
3869 ) -> Result<AstNodeRef, KclError> {
3870 if parallel.lines.len() < 2 {
3871 return Err(KclError::refactor(format!(
3872 "Parallel constraint must have at least 2 lines, got {}",
3873 parallel.lines.len()
3874 )));
3875 };
3876
3877 let sketch_id = sketch;
3878
3879 let line_asts = parallel
3880 .lines
3881 .iter()
3882 .map(|line_id| {
3883 let line_object = self
3884 .scene_graph
3885 .objects
3886 .get(line_id.0)
3887 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3888 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3889 let kind = line_object.kind.human_friendly_kind_with_article();
3890 return Err(KclError::refactor(format!(
3891 "This constraint only works on Segments, but you selected {kind}"
3892 )));
3893 };
3894 let Segment::Line(_) = line_segment else {
3895 let kind = line_segment.human_friendly_kind_with_article();
3896 return Err(KclError::refactor(format!(
3897 "Only lines can be made parallel, but you selected {kind}"
3898 )));
3899 };
3900
3901 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3902 })
3903 .collect::<Result<Vec<_>, _>>()?;
3904
3905 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3906 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3907 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3908 ast::ArrayExpression {
3909 elements: line_asts,
3910 digest: None,
3911 non_code_meta: Default::default(),
3912 },
3913 )))),
3914 arguments: Default::default(),
3915 digest: None,
3916 non_code_meta: Default::default(),
3917 })));
3918
3919 let (sketch_block_ref, _) = self.mutate_ast(
3920 new_ast,
3921 sketch_id,
3922 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3923 )?;
3924 Ok(sketch_block_ref)
3925 }
3926
3927 async fn add_perpendicular(
3928 &mut self,
3929 sketch: ObjectId,
3930 perpendicular: Perpendicular,
3931 new_ast: &mut ast::Node<ast::Program>,
3932 ) -> Result<AstNodeRef, KclError> {
3933 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3934 .await
3935 }
3936
3937 async fn add_lines_at_angle_constraint(
3938 &mut self,
3939 sketch: ObjectId,
3940 angle_kind: LinesAtAngleKind,
3941 lines: Vec<ObjectId>,
3942 new_ast: &mut ast::Node<ast::Program>,
3943 ) -> Result<AstNodeRef, KclError> {
3944 let &[line0_id, line1_id] = lines.as_slice() else {
3945 return Err(KclError::refactor(format!(
3946 "{} constraint must have exactly 2 lines, got {}",
3947 angle_kind.to_function_name(),
3948 lines.len()
3949 )));
3950 };
3951
3952 let sketch_id = sketch;
3953
3954 let line0_object = self
3956 .scene_graph
3957 .objects
3958 .get(line0_id.0)
3959 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3960 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3961 let kind = line0_object.kind.human_friendly_kind_with_article();
3962 return Err(KclError::refactor(format!(
3963 "This constraint only works on Segments, but you selected {kind}"
3964 )));
3965 };
3966 let Segment::Line(_) = line0_segment else {
3967 return Err(KclError::refactor(format!(
3968 "Only lines can be made {}, but you selected {}",
3969 angle_kind.to_function_name(),
3970 line0_segment.human_friendly_kind_with_article(),
3971 )));
3972 };
3973 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3974
3975 let line1_object = self
3976 .scene_graph
3977 .objects
3978 .get(line1_id.0)
3979 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3980 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3981 let kind = line1_object.kind.human_friendly_kind_with_article();
3982 return Err(KclError::refactor(format!(
3983 "This constraint only works on Segments, but you selected {kind}"
3984 )));
3985 };
3986 let Segment::Line(_) = line1_segment else {
3987 return Err(KclError::refactor(format!(
3988 "Only lines can be made {}, but you selected {}",
3989 angle_kind.to_function_name(),
3990 line1_segment.human_friendly_kind_with_article(),
3991 )));
3992 };
3993 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3994
3995 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3997 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3998 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3999 ast::ArrayExpression {
4000 elements: vec![line0_ast, line1_ast],
4001 digest: None,
4002 non_code_meta: Default::default(),
4003 },
4004 )))),
4005 arguments: Default::default(),
4006 digest: None,
4007 non_code_meta: Default::default(),
4008 })));
4009
4010 let (sketch_block_ref, _) = self.mutate_ast(
4012 new_ast,
4013 sketch_id,
4014 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4015 )?;
4016 Ok(sketch_block_ref)
4017 }
4018
4019 async fn add_vertical(
4020 &mut self,
4021 sketch: ObjectId,
4022 vertical: Vertical,
4023 new_ast: &mut ast::Node<ast::Program>,
4024 ) -> Result<AstNodeRef, KclError> {
4025 let sketch_id = sketch;
4026
4027 let first_arg_ast = match vertical {
4028 Vertical::Line { line } => {
4029 let line_object = self
4031 .scene_graph
4032 .objects
4033 .get(line.0)
4034 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4035 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4036 let kind = line_object.kind.human_friendly_kind_with_article();
4037 return Err(KclError::refactor(format!(
4038 "This constraint only works on Segments, but you selected {kind}"
4039 )));
4040 };
4041 let Segment::Line(_) = line_segment else {
4042 return Err(KclError::refactor(format!(
4043 "Only lines can be made vertical, but you selected {}",
4044 line_segment.human_friendly_kind_with_article()
4045 )));
4046 };
4047 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
4048 }
4049 Vertical::Points { points } => {
4050 let point_asts = points
4051 .iter()
4052 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4053 .collect::<Result<Vec<_>, _>>()?;
4054 ast::ArrayExpression::new(point_asts).into()
4055 }
4056 };
4057
4058 let vertical_ast = create_vertical_ast(first_arg_ast);
4060
4061 let (sketch_block_ref, _) = self.mutate_ast(
4063 new_ast,
4064 sketch_id,
4065 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4066 )?;
4067 Ok(sketch_block_ref)
4068 }
4069
4070 async fn execute_after_add_constraint(
4071 &mut self,
4072 ctx: &ExecutorContext,
4073 sketch_id: ObjectId,
4074 #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_ref: AstNodeRef,
4075 new_ast: &mut ast::Node<ast::Program>,
4076 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4077 let new_source = source_from_ast(new_ast);
4079 let (new_program, errors) = Program::parse(&new_source)
4081 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4082 if !errors.is_empty() {
4083 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4084 "Error parsing KCL source after adding constraint: {errors:?}"
4085 ))));
4086 }
4087 let Some(new_program) = new_program else {
4088 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4089 "No AST produced after adding constraint".to_string(),
4090 )));
4091 };
4092 #[cfg(feature = "artifact-graph")]
4093 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4094 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4095 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4096 )))
4097 })?;
4098
4099 let mut truncated_program = new_program.clone();
4102 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4103 .map_err(KclErrorWithOutputs::no_outputs)?;
4104
4105 let outcome = ctx
4107 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4108 .await?;
4109
4110 #[cfg(not(feature = "artifact-graph"))]
4111 let new_object_ids = Vec::new();
4112 #[cfg(feature = "artifact-graph")]
4113 let new_object_ids = {
4114 let constraint_id = outcome
4116 .source_range_to_object
4117 .get(&constraint_node_ref.range)
4118 .copied()
4119 .ok_or_else(|| {
4120 KclErrorWithOutputs::from_error_outcome(
4121 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4122 outcome.clone(),
4123 )
4124 })?;
4125 vec![constraint_id]
4126 };
4127
4128 self.program = new_program;
4131
4132 let outcome = self.update_state_after_exec(outcome, true);
4134
4135 let src_delta = SourceDelta { text: new_source };
4136 let scene_graph_delta = SceneGraphDelta {
4137 new_graph: self.scene_graph.clone(),
4138 invalidates_ids: false,
4139 new_objects: new_object_ids,
4140 exec_outcome: outcome,
4141 };
4142 Ok((src_delta, scene_graph_delta))
4143 }
4144
4145 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4147 if segment_ids_set.contains(&segment_id) {
4148 return true;
4149 }
4150
4151 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4152 return false;
4153 };
4154 let ObjectKind::Segment { segment } = &segment_object.kind else {
4155 return false;
4156 };
4157 let Segment::Point(point) = segment else {
4158 return false;
4159 };
4160
4161 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4162 }
4163
4164 fn remaining_constraint_segments(
4165 &self,
4166 segments: &[ConstraintSegment],
4167 segment_ids_set: &AhashIndexSet<ObjectId>,
4168 ) -> Vec<ConstraintSegment> {
4169 segments
4170 .iter()
4171 .copied()
4172 .filter(|segment| match segment {
4173 ConstraintSegment::Origin(_) => true,
4174 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4175 })
4176 .collect()
4177 }
4178
4179 fn find_referenced_constraints(
4180 &self,
4181 sketch_id: ObjectId,
4182 segment_ids_set: &AhashIndexSet<ObjectId>,
4183 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4184 let sketch_object = self
4186 .scene_graph
4187 .objects
4188 .get(sketch_id.0)
4189 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4190 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4191 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4192 };
4193 let mut constraint_ids_set = AhashIndexSet::default();
4194 for constraint_id in &sketch.constraints {
4195 let constraint_object = self
4196 .scene_graph
4197 .objects
4198 .get(constraint_id.0)
4199 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4200 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4201 return Err(KclError::refactor(format!(
4202 "Object is not a constraint, it is {}",
4203 constraint_object.kind.human_friendly_kind_with_article()
4204 )));
4205 };
4206 let depends_on_segment = match constraint {
4207 Constraint::Coincident(c) => c
4208 .segment_ids()
4209 .any(|seg_id| self.segment_will_be_deleted(seg_id, segment_ids_set)),
4210 Constraint::Distance(d) => d
4211 .point_ids()
4212 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4213 Constraint::Fixed(fixed) => fixed
4214 .points
4215 .iter()
4216 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4217 Constraint::Radius(r) => self.segment_will_be_deleted(r.arc, segment_ids_set),
4218 Constraint::Diameter(d) => self.segment_will_be_deleted(d.arc, segment_ids_set),
4219 Constraint::EqualRadius(equal_radius) => equal_radius
4220 .input
4221 .iter()
4222 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4223 Constraint::HorizontalDistance(d) => d
4224 .point_ids()
4225 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4226 Constraint::VerticalDistance(d) => d
4227 .point_ids()
4228 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4229 Constraint::Horizontal(h) => match h {
4230 Horizontal::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4231 Horizontal::Points { points } => points.iter().any(|point| match point {
4232 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4233 ConstraintSegment::Origin(_) => false,
4234 }),
4235 },
4236 Constraint::Vertical(v) => match v {
4237 Vertical::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4238 Vertical::Points { points } => points.iter().any(|point| match point {
4239 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4240 ConstraintSegment::Origin(_) => false,
4241 }),
4242 },
4243 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4244 .lines
4245 .iter()
4246 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4247 Constraint::Midpoint(midpoint) => {
4248 self.segment_will_be_deleted(midpoint.segment, segment_ids_set)
4249 || self.segment_will_be_deleted(midpoint.point, segment_ids_set)
4250 }
4251 Constraint::Parallel(parallel) => parallel
4252 .lines
4253 .iter()
4254 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4255 Constraint::Perpendicular(perpendicular) => perpendicular
4256 .lines
4257 .iter()
4258 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4259 Constraint::Angle(angle) => angle
4260 .lines
4261 .iter()
4262 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4263 Constraint::Symmetric(symmetric) => {
4264 self.segment_will_be_deleted(symmetric.axis, segment_ids_set)
4265 || symmetric
4266 .input
4267 .iter()
4268 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set))
4269 }
4270 Constraint::Tangent(tangent) => tangent
4271 .input
4272 .iter()
4273 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4274 };
4275 if depends_on_segment {
4276 constraint_ids_set.insert(*constraint_id);
4277 }
4278 }
4279 Ok(constraint_ids_set)
4280 }
4281
4282 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4283 #[cfg(not(feature = "artifact-graph"))]
4284 {
4285 let _ = freedom_analysis_ran; outcome
4287 }
4288 #[cfg(feature = "artifact-graph")]
4289 {
4290 let mut outcome = outcome;
4291 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4292
4293 if freedom_analysis_ran {
4294 self.point_freedom_cache.clear();
4297 for new_obj in &new_objects {
4298 if let ObjectKind::Segment {
4299 segment: crate::front::Segment::Point(point),
4300 } = &new_obj.kind
4301 {
4302 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4303 }
4304 }
4305 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4306 self.scene_graph.objects = new_objects;
4308 } else {
4309 for old_obj in &self.scene_graph.objects {
4312 if let ObjectKind::Segment {
4313 segment: crate::front::Segment::Point(point),
4314 } = &old_obj.kind
4315 {
4316 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4317 }
4318 }
4319
4320 let mut updated_objects = Vec::with_capacity(new_objects.len());
4322 for new_obj in new_objects {
4323 let mut obj = new_obj;
4324 if let ObjectKind::Segment {
4325 segment: crate::front::Segment::Point(point),
4326 } = &mut obj.kind
4327 {
4328 let new_freedom = point.freedom;
4329 match new_freedom {
4335 Freedom::Free => {
4336 match self.point_freedom_cache.get(&obj.id).copied() {
4337 Some(Freedom::Conflict) => {
4338 }
4341 Some(Freedom::Fixed) => {
4342 point.freedom = Freedom::Fixed;
4344 }
4345 Some(Freedom::Free) => {
4346 }
4348 None => {
4349 }
4351 }
4352 }
4353 Freedom::Fixed => {
4354 }
4356 Freedom::Conflict => {
4357 }
4359 }
4360 self.point_freedom_cache.insert(obj.id, point.freedom);
4362 }
4363 updated_objects.push(obj);
4364 }
4365
4366 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4367 self.scene_graph.objects = updated_objects;
4368 }
4369 outcome
4370 }
4371 }
4372
4373 fn mutate_ast(
4374 &mut self,
4375 ast: &mut ast::Node<ast::Program>,
4376 object_id: ObjectId,
4377 command: AstMutateCommand,
4378 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4379 let sketch_object = self
4380 .scene_graph
4381 .objects
4382 .get(object_id.0)
4383 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4384 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4385 }
4386}
4387
4388fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4389 let sketch_object = scene_graph
4391 .objects
4392 .get(sketch_id.0)
4393 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4394 let ObjectKind::Sketch(_) = &sketch_object.kind else {
4395 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4396 };
4397 expect_single_node_ref(sketch_object)
4398}
4399
4400fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4401 match &object.source {
4402 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4403 range: *range,
4404 node_path: node_path.clone(),
4405 }),
4406 SourceRef::BackTrace { ranges } => {
4407 let [range] = ranges.as_slice() else {
4408 return Err(KclError::refactor(format!(
4409 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4410 ranges.len()
4411 )));
4412 };
4413 Ok(AstNodeRef {
4414 range: range.0,
4415 node_path: range.1.clone(),
4416 })
4417 }
4418 }
4419}
4420
4421fn only_sketch_block_from_range(
4424 ast: &mut ast::Node<ast::Program>,
4425 sketch_block_range: SourceRange,
4426 edit_kind: ChangeKind,
4427) -> Result<(), KclError> {
4428 let r1 = sketch_block_range;
4429 let matches_range = |r2: SourceRange| -> bool {
4430 match edit_kind {
4433 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4434 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4436 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4437 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4439 }
4440 };
4441 let mut found = false;
4442 for item in ast.body.iter_mut() {
4443 match item {
4444 ast::BodyItem::ImportStatement(_) => {}
4445 ast::BodyItem::ExpressionStatement(node) => {
4446 if matches_range(SourceRange::from(&*node))
4447 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4448 {
4449 sketch_block.is_being_edited = true;
4450 found = true;
4451 break;
4452 }
4453 }
4454 ast::BodyItem::VariableDeclaration(node) => {
4455 if matches_range(SourceRange::from(&node.declaration.init))
4456 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4457 {
4458 sketch_block.is_being_edited = true;
4459 found = true;
4460 break;
4461 }
4462 }
4463 ast::BodyItem::TypeDeclaration(_) => {}
4464 ast::BodyItem::ReturnStatement(node) => {
4465 if matches_range(SourceRange::from(&node.argument))
4466 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4467 {
4468 sketch_block.is_being_edited = true;
4469 found = true;
4470 break;
4471 }
4472 }
4473 }
4474 }
4475 if !found {
4476 return Err(KclError::refactor(format!(
4477 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4478 )));
4479 }
4480
4481 Ok(())
4482}
4483
4484fn only_sketch_block(
4485 ast: &mut ast::Node<ast::Program>,
4486 sketch_block_ref: &AstNodeRef,
4487 edit_kind: ChangeKind,
4488) -> Result<(), KclError> {
4489 let Some(target_node_path) = &sketch_block_ref.node_path else {
4490 #[cfg(target_arch = "wasm32")]
4491 web_sys::console::warn_1(
4492 &format!(
4493 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4494 &sketch_block_ref
4495 )
4496 .into(),
4497 );
4498 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4499 };
4500 let mut found = false;
4501 for item in ast.body.iter_mut() {
4502 match item {
4503 ast::BodyItem::ImportStatement(_) => {}
4504 ast::BodyItem::ExpressionStatement(node) => {
4505 if let Some(node_path) = &node.node_path
4507 && node_path == target_node_path
4508 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4509 {
4510 sketch_block.is_being_edited = true;
4511 found = true;
4512 break;
4513 }
4514 if let Some(node_path) = node.expression.node_path()
4516 && node_path == target_node_path
4517 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4518 {
4519 sketch_block.is_being_edited = true;
4520 found = true;
4521 break;
4522 }
4523 }
4524 ast::BodyItem::VariableDeclaration(node) => {
4525 if let Some(node_path) = node.declaration.init.node_path()
4526 && node_path == target_node_path
4527 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4528 {
4529 sketch_block.is_being_edited = true;
4530 found = true;
4531 break;
4532 }
4533 }
4534 ast::BodyItem::TypeDeclaration(_) => {}
4535 ast::BodyItem::ReturnStatement(node) => {
4536 if let Some(node_path) = node.argument.node_path()
4537 && node_path == target_node_path
4538 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4539 {
4540 sketch_block.is_being_edited = true;
4541 found = true;
4542 break;
4543 }
4544 }
4545 }
4546 }
4547 if !found {
4548 return Err(KclError::refactor(format!(
4549 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4550 )));
4551 }
4552
4553 Ok(())
4554}
4555
4556fn sketch_on_ast_expr(
4557 ast: &mut ast::Node<ast::Program>,
4558 scene_graph: &SceneGraph,
4559 on: &Plane,
4560) -> Result<ast::Expr, KclError> {
4561 match on {
4562 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4563 Plane::Object(object_id) => {
4564 let on_object = scene_graph
4565 .objects
4566 .get(object_id.0)
4567 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4568 #[cfg(feature = "artifact-graph")]
4569 {
4570 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4571 return Ok(face_expr);
4572 }
4573 }
4574 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4575 }
4576 }
4577}
4578
4579#[cfg(feature = "artifact-graph")]
4580fn sketch_face_of_scene_object_ast_expr(
4581 ast: &mut ast::Node<ast::Program>,
4582 on_object: &crate::front::Object,
4583) -> Result<Option<ast::Expr>, KclError> {
4584 let SourceRef::BackTrace { ranges } = &on_object.source else {
4585 return Ok(None);
4586 };
4587
4588 match &on_object.kind {
4589 ObjectKind::Wall(_) => {
4590 let [sweep_range, segment_range] = ranges.as_slice() else {
4591 return Err(KclError::refactor(format!(
4592 "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4593 ranges.len(),
4594 on_object.artifact_id
4595 )));
4596 };
4597 let sweep_ref = get_or_insert_ast_reference(
4598 ast,
4599 &SourceRef::Simple {
4600 range: sweep_range.0,
4601 node_path: sweep_range.1.clone(),
4602 },
4603 "solid",
4604 None,
4605 )?;
4606 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4607 return Err(KclError::refactor(format!(
4608 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4609 on_object.artifact_id
4610 )));
4611 };
4612 let solid_name = solid_name_expr.name.name.clone();
4613 let solid_expr = ast_name_expr(solid_name.clone());
4614 let segment_ref = get_or_insert_ast_reference(
4615 ast,
4616 &SourceRef::Simple {
4617 range: segment_range.0,
4618 node_path: segment_range.1.clone(),
4619 },
4620 LINE_VARIABLE,
4621 None,
4622 )?;
4623
4624 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4625 let ast::Expr::Name(segment_name_expr) = segment_ref else {
4626 return Err(KclError::refactor(format!(
4627 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4628 on_object.artifact_id
4629 )));
4630 };
4631 create_member_expression(
4632 create_member_expression(ast_name_expr(region_name), "tags"),
4633 &segment_name_expr.name.name,
4634 )
4635 } else {
4636 segment_ref
4637 };
4638
4639 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4640 }
4641 ObjectKind::Cap(cap) => {
4642 let [range] = ranges.as_slice() else {
4643 return Err(KclError::refactor(format!(
4644 "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4645 ranges.len(),
4646 on_object.artifact_id
4647 )));
4648 };
4649 let sweep_ref = get_or_insert_ast_reference(
4650 ast,
4651 &SourceRef::Simple {
4652 range: range.0,
4653 node_path: range.1.clone(),
4654 },
4655 "solid",
4656 None,
4657 )?;
4658 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4659 return Err(KclError::refactor(format!(
4660 "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4661 on_object.artifact_id
4662 )));
4663 };
4664 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4665 let face_expr = match cap.kind {
4667 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4668 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4669 };
4670
4671 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4672 }
4673 _ => Ok(None),
4674 }
4675}
4676
4677#[cfg(feature = "artifact-graph")]
4678fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4679 let mut existing_artifact_ids = scene_objects
4680 .iter()
4681 .map(|object| object.artifact_id)
4682 .collect::<HashSet<_>>();
4683
4684 for artifact in artifact_graph.values() {
4685 match artifact {
4686 Artifact::Wall(wall) => {
4687 if existing_artifact_ids.contains(&wall.id) {
4688 continue;
4689 }
4690
4691 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4692 Artifact::Segment(segment) => Some(segment),
4693 _ => None,
4694 }) else {
4695 continue;
4696 };
4697 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4698 Artifact::Sweep(sweep) => Some(sweep),
4699 _ => None,
4700 }) else {
4701 continue;
4702 };
4703 let source_segment = segment
4704 .original_seg_id
4705 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4706 .and_then(|artifact| match artifact {
4707 Artifact::Segment(segment) => Some(segment),
4708 _ => None,
4709 })
4710 .unwrap_or(segment);
4711 let id = ObjectId(scene_objects.len());
4712 scene_objects.push(crate::front::Object {
4713 id,
4714 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4715 label: Default::default(),
4716 comments: Default::default(),
4717 artifact_id: wall.id,
4718 source: SourceRef::BackTrace {
4719 ranges: vec![
4720 (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4721 (
4722 source_segment.code_ref.range,
4723 Some(source_segment.code_ref.node_path.clone()),
4724 ),
4725 ],
4726 },
4727 });
4728 existing_artifact_ids.insert(wall.id);
4729 }
4730 Artifact::Cap(cap) => {
4731 if existing_artifact_ids.contains(&cap.id) {
4732 continue;
4733 }
4734
4735 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4736 Artifact::Sweep(sweep) => Some(sweep),
4737 _ => None,
4738 }) else {
4739 continue;
4740 };
4741 let id = ObjectId(scene_objects.len());
4742 let kind = match cap.sub_type {
4743 CapSubType::Start => crate::frontend::api::CapKind::Start,
4744 CapSubType::End => crate::frontend::api::CapKind::End,
4745 };
4746 scene_objects.push(crate::front::Object {
4747 id,
4748 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4749 label: Default::default(),
4750 comments: Default::default(),
4751 artifact_id: cap.id,
4752 source: SourceRef::BackTrace {
4753 ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4754 },
4755 });
4756 existing_artifact_ids.insert(cap.id);
4757 }
4758 _ => {}
4759 }
4760 }
4761}
4762
4763fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4764 use crate::engine::PlaneName;
4765
4766 match name {
4767 PlaneName::Xy => ast_name_expr("XY".to_owned()),
4768 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4769 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4770 PlaneName::NegXy => negated_plane_ast_expr("XY"),
4771 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4772 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4773 }
4774}
4775
4776fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4777 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4778 ast::UnaryOperator::Neg,
4779 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4780 )))
4781}
4782
4783#[cfg(feature = "artifact-graph")]
4784fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4785 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4786 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4787 unlabeled: Some(solid_expr),
4788 arguments: vec![ast::LabeledArg {
4789 label: Some(ast::Identifier::new("face")),
4790 arg: face_expr,
4791 }],
4792 digest: None,
4793 non_code_meta: Default::default(),
4794 })))
4795}
4796
4797#[cfg(feature = "artifact-graph")]
4798fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4799 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4800 return None;
4801 };
4802 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4803 return None;
4804 };
4805 if !matches!(
4806 sweep_call.callee.name.name.as_str(),
4807 "extrude" | "revolve" | "sweep" | "loft"
4808 ) {
4809 return None;
4810 }
4811 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4812 return None;
4813 };
4814 let candidate = region_name_expr.name.name.clone();
4815 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4816 return None;
4817 };
4818 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
4819 return None;
4820 };
4821 if region_call.callee.name.name != "region" {
4822 return None;
4823 }
4824 Some(candidate)
4825}
4826
4827fn get_or_insert_ast_reference(
4834 ast: &mut ast::Node<ast::Program>,
4835 source_ref: &SourceRef,
4836 prefix: &str,
4837 property: Option<&str>,
4838) -> Result<ast::Expr, KclError> {
4839 let command = AstMutateCommand::AddVariableDeclaration {
4840 prefix: prefix.to_owned(),
4841 };
4842 let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
4843 let AstMutateCommandReturn::Name(var_name) = ret else {
4844 return Err(KclError::refactor(
4845 "Expected variable name returned from AddVariableDeclaration".to_owned(),
4846 ));
4847 };
4848 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4849 let Some(property) = property else {
4850 return Ok(var_expr);
4852 };
4853
4854 Ok(create_member_expression(var_expr, property))
4855}
4856
4857fn mutate_ast_node_by_source_ref(
4858 ast: &mut ast::Node<ast::Program>,
4859 source_ref: &SourceRef,
4860 command: AstMutateCommand,
4861) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4862 let (source_range, node_path) = match source_ref {
4863 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
4864 SourceRef::BackTrace { ranges } => {
4865 let [range] = ranges.as_slice() else {
4866 return Err(KclError::refactor(format!(
4867 "Expected single source ref, got {}; ranges={ranges:#?}",
4868 ranges.len(),
4869 )));
4870 };
4871 (range.0, range.1.clone())
4872 }
4873 };
4874 let mut context = AstMutateContext {
4875 source_range,
4876 node_path,
4877 command,
4878 defined_names_stack: Default::default(),
4879 };
4880 let control = dfs_mut(ast, &mut context);
4881 match control {
4882 ControlFlow::Continue(_) => Err(KclError::refactor(
4883 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
4884 )),
4885 ControlFlow::Break(break_value) => break_value,
4886 }
4887}
4888
4889#[derive(Debug)]
4890struct AstMutateContext {
4891 source_range: SourceRange,
4892 node_path: Option<ast::NodePath>,
4893 command: AstMutateCommand,
4894 defined_names_stack: Vec<HashSet<String>>,
4895}
4896
4897#[derive(Debug)]
4898#[allow(clippy::large_enum_variant)]
4899enum AstMutateCommand {
4900 AddSketchBlockExprStmt {
4902 expr: ast::Expr,
4903 },
4904 AddSketchBlockVarDecl {
4906 prefix: String,
4907 expr: ast::Expr,
4908 },
4909 AddVariableDeclaration {
4910 prefix: String,
4911 },
4912 EditPoint {
4913 at: ast::Expr,
4914 },
4915 EditLine {
4916 start: ast::Expr,
4917 end: ast::Expr,
4918 construction: Option<bool>,
4919 },
4920 EditArc {
4921 start: ast::Expr,
4922 end: ast::Expr,
4923 center: ast::Expr,
4924 construction: Option<bool>,
4925 },
4926 EditCircle {
4927 start: ast::Expr,
4928 center: ast::Expr,
4929 construction: Option<bool>,
4930 },
4931 EditConstraintValue {
4932 value: ast::BinaryPart,
4933 },
4934 EditDistanceConstraintLabelPosition {
4935 label_position: ast::Expr,
4936 },
4937 EditCallUnlabeled {
4938 arg: ast::Expr,
4939 },
4940 #[cfg(feature = "artifact-graph")]
4941 EditVarInitialValue {
4942 value: Number,
4943 },
4944 DeleteNode,
4945}
4946
4947impl AstMutateCommand {
4948 fn needs_defined_names_stack(&self) -> bool {
4949 matches!(
4950 self,
4951 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4952 )
4953 }
4954}
4955
4956#[derive(Debug)]
4957enum AstMutateCommandReturn {
4958 None,
4959 Name(String),
4960}
4961
4962#[derive(Debug, Clone)]
4963struct AstNodeRef {
4964 range: SourceRange,
4965 node_path: Option<ast::NodePath>,
4966}
4967
4968impl<T> From<&ast::Node<T>> for AstNodeRef {
4969 fn from(value: &ast::Node<T>) -> Self {
4970 AstNodeRef {
4971 range: value.into(),
4972 node_path: value.node_path.clone(),
4973 }
4974 }
4975}
4976
4977impl From<&ast::BodyItem> for AstNodeRef {
4978 fn from(value: &ast::BodyItem) -> Self {
4979 match value {
4980 ast::BodyItem::ImportStatement(node) => AstNodeRef {
4981 range: node.into(),
4982 node_path: node.node_path.clone(),
4983 },
4984 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4985 range: node.into(),
4986 node_path: node.node_path.clone(),
4987 },
4988 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4989 range: node.into(),
4990 node_path: node.node_path.clone(),
4991 },
4992 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4993 range: node.into(),
4994 node_path: node.node_path.clone(),
4995 },
4996 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
4997 range: node.into(),
4998 node_path: node.node_path.clone(),
4999 },
5000 }
5001 }
5002}
5003
5004impl From<&ast::Expr> for AstNodeRef {
5005 fn from(value: &ast::Expr) -> Self {
5006 AstNodeRef {
5007 range: SourceRange::from(value),
5008 node_path: value.node_path().cloned(),
5009 }
5010 }
5011}
5012
5013impl From<&AstMutateContext> for AstNodeRef {
5014 fn from(value: &AstMutateContext) -> Self {
5015 AstNodeRef {
5016 range: value.source_range,
5017 node_path: value.node_path.clone(),
5018 }
5019 }
5020}
5021
5022impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5023 type Error = crate::walk::AstNodeError;
5024
5025 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5026 Ok(AstNodeRef {
5027 range: SourceRange::try_from(value)?,
5028 node_path: value.try_into()?,
5029 })
5030 }
5031}
5032
5033impl From<AstNodeRef> for SourceRange {
5034 fn from(value: AstNodeRef) -> Self {
5035 value.range
5036 }
5037}
5038
5039impl Visitor for AstMutateContext {
5040 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5041 type Continue = ();
5042
5043 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5044 filter_and_process(self, node)
5045 }
5046
5047 fn finish(&mut self, node: NodeMut<'_>) {
5048 match &node {
5049 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5050 self.defined_names_stack.pop();
5051 }
5052 _ => {}
5053 }
5054 }
5055}
5056
5057fn filter_and_process(
5058 ctx: &mut AstMutateContext,
5059 node: NodeMut,
5060) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5061 let Ok(node_range) = SourceRange::try_from(&node) else {
5062 return TraversalReturn::new_continue(());
5064 };
5065 if let NodeMut::VariableDeclaration(var_decl) = &node {
5070 let expr_range = SourceRange::from(&var_decl.declaration.init);
5071 let expr_node_path = var_decl.declaration.init.node_path();
5072 if source_ref_matches(ctx, expr_range, expr_node_path) {
5073 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5074 return TraversalReturn::new_break(Ok((
5077 AstNodeRef::from(&**var_decl),
5078 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5079 )));
5080 }
5081 if let AstMutateCommand::DeleteNode = &ctx.command {
5082 return TraversalReturn {
5085 mutate_body_item: MutateBodyItem::Delete,
5086 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5087 };
5088 }
5089 }
5090 }
5091 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5094 let expr_range = SourceRange::from(&expr_stmt.expression);
5095 let expr_node_path = expr_stmt.expression.node_path();
5096 if source_ref_matches(ctx, expr_range, expr_node_path) {
5097 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5098 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5101 return TraversalReturn::new_continue(());
5102 };
5103 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5104 }
5105 if let AstMutateCommand::DeleteNode = &ctx.command {
5106 return TraversalReturn {
5109 mutate_body_item: MutateBodyItem::Delete,
5110 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5111 };
5112 }
5113 }
5114 }
5115
5116 if ctx.command.needs_defined_names_stack() {
5117 if let NodeMut::Program(program) = &node {
5118 ctx.defined_names_stack.push(find_defined_names(*program));
5119 } else if let NodeMut::SketchBlock(block) = &node {
5120 ctx.defined_names_stack.push(find_defined_names(&block.body));
5121 }
5122 }
5123
5124 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5126 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5127 return TraversalReturn::new_continue(());
5128 }
5129 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5130 return TraversalReturn::new_continue(());
5131 };
5132 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5133}
5134
5135fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5136 match &ctx.node_path {
5137 Some(target) => Some(target) == node_path,
5138 None => node_range == ctx.source_range,
5139 }
5140}
5141
5142fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5143 match &ctx.command {
5144 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5145 if let NodeMut::SketchBlock(sketch_block) = node {
5146 sketch_block
5147 .body
5148 .items
5149 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5150 inner: ast::ExpressionStatement {
5151 expression: expr.clone(),
5152 digest: None,
5153 },
5154 start: Default::default(),
5155 end: Default::default(),
5156 module_id: Default::default(),
5157 node_path: None,
5158 outer_attrs: Default::default(),
5159 pre_comments: Default::default(),
5160 comment_start: Default::default(),
5161 }));
5162 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5163 }
5164 }
5165 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5166 if let NodeMut::SketchBlock(sketch_block) = node {
5167 let empty_defined_names = HashSet::new();
5168 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5169 let Ok(name) = next_free_name(prefix, defined_names) else {
5170 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5171 };
5172 sketch_block
5173 .body
5174 .items
5175 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5176 ast::VariableDeclaration::new(
5177 ast::VariableDeclarator::new(&name, expr.clone()),
5178 ast::ItemVisibility::Default,
5179 ast::VariableKind::Const,
5180 ),
5181 ))));
5182 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5183 }
5184 }
5185 AstMutateCommand::AddVariableDeclaration { prefix } => {
5186 if let NodeMut::VariableDeclaration(inner) = node {
5187 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5188 }
5189 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5190 let empty_defined_names = HashSet::new();
5191 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5192 let Ok(name) = next_free_name(prefix, defined_names) else {
5193 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5195 };
5196 let mutate_node =
5197 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5198 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5199 ast::ItemVisibility::Default,
5200 ast::VariableKind::Const,
5201 ))));
5202 return TraversalReturn {
5203 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5204 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5205 };
5206 }
5207 }
5208 AstMutateCommand::EditPoint { at } => {
5209 if let NodeMut::CallExpressionKw(call) = node {
5210 if call.callee.name.name != POINT_FN {
5211 return TraversalReturn::new_continue(());
5212 }
5213 for labeled_arg in &mut call.arguments {
5215 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5216 labeled_arg.arg = at.clone();
5217 }
5218 }
5219 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5220 }
5221 }
5222 AstMutateCommand::EditLine {
5223 start,
5224 end,
5225 construction,
5226 } => {
5227 if let NodeMut::CallExpressionKw(call) = node {
5228 if call.callee.name.name != LINE_FN {
5229 return TraversalReturn::new_continue(());
5230 }
5231 for labeled_arg in &mut call.arguments {
5233 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5234 labeled_arg.arg = start.clone();
5235 }
5236 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5237 labeled_arg.arg = end.clone();
5238 }
5239 }
5240 if let Some(construction_value) = construction {
5242 let construction_exists = call
5243 .arguments
5244 .iter()
5245 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5246 if *construction_value {
5247 if construction_exists {
5249 for labeled_arg in &mut call.arguments {
5251 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5252 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5253 value: ast::LiteralValue::Bool(true),
5254 raw: "true".to_string(),
5255 digest: None,
5256 })));
5257 }
5258 }
5259 } else {
5260 call.arguments.push(ast::LabeledArg {
5262 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5263 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5264 value: ast::LiteralValue::Bool(true),
5265 raw: "true".to_string(),
5266 digest: None,
5267 }))),
5268 });
5269 }
5270 } else {
5271 call.arguments
5273 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5274 }
5275 }
5276 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5277 }
5278 }
5279 AstMutateCommand::EditArc {
5280 start,
5281 end,
5282 center,
5283 construction,
5284 } => {
5285 if let NodeMut::CallExpressionKw(call) = node {
5286 if call.callee.name.name != ARC_FN {
5287 return TraversalReturn::new_continue(());
5288 }
5289 for labeled_arg in &mut call.arguments {
5291 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5292 labeled_arg.arg = start.clone();
5293 }
5294 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5295 labeled_arg.arg = end.clone();
5296 }
5297 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5298 labeled_arg.arg = center.clone();
5299 }
5300 }
5301 if let Some(construction_value) = construction {
5303 let construction_exists = call
5304 .arguments
5305 .iter()
5306 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5307 if *construction_value {
5308 if construction_exists {
5310 for labeled_arg in &mut call.arguments {
5312 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5313 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5314 value: ast::LiteralValue::Bool(true),
5315 raw: "true".to_string(),
5316 digest: None,
5317 })));
5318 }
5319 }
5320 } else {
5321 call.arguments.push(ast::LabeledArg {
5323 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5324 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5325 value: ast::LiteralValue::Bool(true),
5326 raw: "true".to_string(),
5327 digest: None,
5328 }))),
5329 });
5330 }
5331 } else {
5332 call.arguments
5334 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5335 }
5336 }
5337 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5338 }
5339 }
5340 AstMutateCommand::EditCircle {
5341 start,
5342 center,
5343 construction,
5344 } => {
5345 if let NodeMut::CallExpressionKw(call) = node {
5346 if call.callee.name.name != CIRCLE_FN {
5347 return TraversalReturn::new_continue(());
5348 }
5349 for labeled_arg in &mut call.arguments {
5351 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5352 labeled_arg.arg = start.clone();
5353 }
5354 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5355 labeled_arg.arg = center.clone();
5356 }
5357 }
5358 if let Some(construction_value) = construction {
5360 let construction_exists = call
5361 .arguments
5362 .iter()
5363 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5364 if *construction_value {
5365 if construction_exists {
5366 for labeled_arg in &mut call.arguments {
5367 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5368 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5369 value: ast::LiteralValue::Bool(true),
5370 raw: "true".to_string(),
5371 digest: None,
5372 })));
5373 }
5374 }
5375 } else {
5376 call.arguments.push(ast::LabeledArg {
5377 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5378 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5379 value: ast::LiteralValue::Bool(true),
5380 raw: "true".to_string(),
5381 digest: None,
5382 }))),
5383 });
5384 }
5385 } else {
5386 call.arguments
5387 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5388 }
5389 }
5390 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5391 }
5392 }
5393 AstMutateCommand::EditConstraintValue { value } => {
5394 if let NodeMut::BinaryExpression(binary_expr) = node {
5395 let left_is_constraint = matches!(
5396 &binary_expr.left,
5397 ast::BinaryPart::CallExpressionKw(call)
5398 if matches!(
5399 call.callee.name.name.as_str(),
5400 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5401 )
5402 );
5403 if left_is_constraint {
5404 binary_expr.right = value.clone();
5405 } else {
5406 binary_expr.left = value.clone();
5407 }
5408
5409 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5410 }
5411 }
5412 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5413 if let NodeMut::BinaryExpression(binary_expr) = node {
5414 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5415 return TraversalReturn::new_continue(());
5416 };
5417 if !matches!(
5418 call.callee.name.name.as_str(),
5419 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
5420 ) {
5421 return TraversalReturn::new_continue(());
5422 }
5423
5424 if let Some(label_arg) = call
5425 .arguments
5426 .iter_mut()
5427 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5428 {
5429 label_arg.arg = label_position.clone();
5430 } else {
5431 call.arguments.push(ast::LabeledArg {
5432 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5433 arg: label_position.clone(),
5434 });
5435 }
5436
5437 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5438 }
5439 }
5440 AstMutateCommand::EditCallUnlabeled { arg } => {
5441 if let NodeMut::CallExpressionKw(call) = node {
5442 call.unlabeled = Some(arg.clone());
5443 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5444 }
5445 }
5446 #[cfg(feature = "artifact-graph")]
5447 AstMutateCommand::EditVarInitialValue { value } => {
5448 if let NodeMut::NumericLiteral(numeric_literal) = node {
5449 let Ok(literal) = to_source_number(*value) else {
5451 return TraversalReturn::new_break(Err(KclError::refactor(format!(
5452 "Could not convert number to AST literal: {:?}",
5453 *value
5454 ))));
5455 };
5456 *numeric_literal = ast::Node::no_src(literal);
5457 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5458 }
5459 }
5460 AstMutateCommand::DeleteNode => {
5461 return TraversalReturn {
5462 mutate_body_item: MutateBodyItem::Delete,
5463 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5464 };
5465 }
5466 }
5467 TraversalReturn::new_continue(())
5468}
5469
5470struct FindSketchBlockSourceRange {
5471 target_before_mutation: SourceRange,
5473 found: Cell<Option<AstNodeRef>>,
5477}
5478
5479impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5480 type Error = crate::front::Error;
5481
5482 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5483 let Ok(node_range) = SourceRange::try_from(&node) else {
5484 return Ok(true);
5485 };
5486
5487 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5488 if node_range.module_id() == self.target_before_mutation.module_id()
5489 && node_range.start() == self.target_before_mutation.start()
5490 && node_range.end() >= self.target_before_mutation.end()
5492 {
5493 self.found.set(sketch_block.body.items.last().map(|item| match item {
5494 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5498 _ => AstNodeRef::from(item),
5499 }));
5500 return Ok(false);
5501 } else {
5502 return Ok(true);
5505 }
5506 }
5507
5508 for child in node.children().iter() {
5509 if !child.visit(*self)? {
5510 return Ok(false);
5511 }
5512 }
5513
5514 Ok(true)
5515 }
5516}
5517
5518struct FindSketchBlockByNodePath {
5519 target_node_path: ast::NodePath,
5521 found: Cell<Option<AstNodeRef>>,
5525}
5526
5527impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5528 type Error = crate::front::Error;
5529
5530 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5531 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5532 return Ok(true);
5533 };
5534
5535 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5536 if let Some(node_path) = node_path
5537 && node_path == self.target_node_path
5538 {
5539 self.found.set(sketch_block.body.items.last().map(|item| match item {
5540 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5544 _ => AstNodeRef::from(item),
5545 }));
5546
5547 return Ok(false);
5548 } else {
5549 return Ok(true);
5552 }
5553 }
5554
5555 for child in node.children().iter() {
5556 if !child.visit(*self)? {
5557 return Ok(false);
5558 }
5559 }
5560
5561 Ok(true)
5562 }
5563}
5564
5565fn find_sketch_block_added_item(
5573 ast: &ast::Node<ast::Program>,
5574 sketch_block_before_mutation: &AstNodeRef,
5575) -> Result<AstNodeRef, KclError> {
5576 if let Some(node_path) = &sketch_block_before_mutation.node_path {
5577 let find = FindSketchBlockByNodePath {
5578 target_node_path: node_path.clone(),
5579 found: Cell::new(None),
5580 };
5581 let node = crate::walk::Node::from(ast);
5582 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5583 find.found.into_inner().ok_or_else(|| {
5584 KclError::refactor(format!(
5585 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5586 ))
5587 })
5588 } else {
5589 let find = FindSketchBlockSourceRange {
5591 target_before_mutation: sketch_block_before_mutation.range,
5592 found: Cell::new(None),
5593 };
5594 let node = crate::walk::Node::from(ast);
5595 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5596 find.found.into_inner().ok_or_else(|| KclError::refactor(
5597 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?"),
5598 ))
5599 }
5600}
5601
5602fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5603 ast.recast_top(&Default::default(), 0)
5605}
5606
5607pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5608 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5609 inner: ast::ArrayExpression {
5610 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5611 non_code_meta: Default::default(),
5612 digest: None,
5613 },
5614 start: Default::default(),
5615 end: Default::default(),
5616 module_id: Default::default(),
5617 node_path: None,
5618 outer_attrs: Default::default(),
5619 pre_comments: Default::default(),
5620 comment_start: Default::default(),
5621 })))
5622}
5623
5624fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
5625 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
5626 ast::ArrayExpression {
5627 elements: vec![
5628 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5629 point.x,
5630 )?)))),
5631 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5632 point.y,
5633 )?)))),
5634 ],
5635 non_code_meta: Default::default(),
5636 digest: None,
5637 },
5638 ))))
5639}
5640
5641fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5642 match expr {
5643 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5644 inner: ast::Literal::from(to_source_number(*number)?),
5645 start: Default::default(),
5646 end: Default::default(),
5647 module_id: Default::default(),
5648 node_path: None,
5649 outer_attrs: Default::default(),
5650 pre_comments: Default::default(),
5651 comment_start: Default::default(),
5652 }))),
5653 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5654 inner: ast::SketchVar {
5655 initial: Some(Box::new(ast::Node {
5656 inner: to_source_number(*number)?,
5657 start: Default::default(),
5658 end: Default::default(),
5659 module_id: Default::default(),
5660 node_path: None,
5661 outer_attrs: Default::default(),
5662 pre_comments: Default::default(),
5663 comment_start: Default::default(),
5664 })),
5665 digest: None,
5666 },
5667 start: Default::default(),
5668 end: Default::default(),
5669 module_id: Default::default(),
5670 node_path: None,
5671 outer_attrs: Default::default(),
5672 pre_comments: Default::default(),
5673 comment_start: Default::default(),
5674 }))),
5675 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5676 }
5677}
5678
5679fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5680 Ok(ast::NumericLiteral {
5681 value: number.value,
5682 suffix: number.units,
5683 raw: format_number_literal(number.value, number.units, None)?,
5684 digest: None,
5685 })
5686}
5687
5688pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5689 ast::Expr::Name(Box::new(ast_name(name)))
5690}
5691
5692fn ast_name(name: String) -> ast::Node<ast::Name> {
5693 ast::Node {
5694 inner: ast::Name {
5695 name: ast::Node {
5696 inner: ast::Identifier { name, digest: None },
5697 start: Default::default(),
5698 end: Default::default(),
5699 module_id: Default::default(),
5700 node_path: None,
5701 outer_attrs: Default::default(),
5702 pre_comments: Default::default(),
5703 comment_start: Default::default(),
5704 },
5705 path: Vec::new(),
5706 abs_path: false,
5707 digest: None,
5708 },
5709 start: Default::default(),
5710 end: Default::default(),
5711 module_id: Default::default(),
5712 node_path: None,
5713 outer_attrs: Default::default(),
5714 pre_comments: Default::default(),
5715 comment_start: Default::default(),
5716 }
5717}
5718
5719pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5720 ast::Name {
5721 name: ast::Node {
5722 inner: ast::Identifier {
5723 name: name.to_owned(),
5724 digest: None,
5725 },
5726 start: Default::default(),
5727 end: Default::default(),
5728 module_id: Default::default(),
5729 node_path: None,
5730 outer_attrs: Default::default(),
5731 pre_comments: Default::default(),
5732 comment_start: Default::default(),
5733 },
5734 path: Default::default(),
5735 abs_path: false,
5736 digest: None,
5737 }
5738}
5739
5740pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
5744 let elements = exprs.into_iter().collect::<Vec<_>>();
5745 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
5746
5747 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5749 elements,
5750 digest: None,
5751 non_code_meta: Default::default(),
5752 })));
5753
5754 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5756 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5757 unlabeled: Some(array_expr),
5758 arguments: Default::default(),
5759 digest: None,
5760 non_code_meta: Default::default(),
5761 })))
5762}
5763
5764pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5766 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5767 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5768 unlabeled: None,
5769 arguments: vec![
5770 ast::LabeledArg {
5771 label: Some(ast::Identifier::new(LINE_START_PARAM)),
5772 arg: start_ast,
5773 },
5774 ast::LabeledArg {
5775 label: Some(ast::Identifier::new(LINE_END_PARAM)),
5776 arg: end_ast,
5777 },
5778 ],
5779 digest: None,
5780 non_code_meta: Default::default(),
5781 })))
5782}
5783
5784pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5786 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5787 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5788 unlabeled: None,
5789 arguments: vec![
5790 ast::LabeledArg {
5791 label: Some(ast::Identifier::new(ARC_START_PARAM)),
5792 arg: start_ast,
5793 },
5794 ast::LabeledArg {
5795 label: Some(ast::Identifier::new(ARC_END_PARAM)),
5796 arg: end_ast,
5797 },
5798 ast::LabeledArg {
5799 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5800 arg: center_ast,
5801 },
5802 ],
5803 digest: None,
5804 non_code_meta: Default::default(),
5805 })))
5806}
5807
5808pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5810 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5811 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
5812 unlabeled: None,
5813 arguments: vec![
5814 ast::LabeledArg {
5815 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5816 arg: start_ast,
5817 },
5818 ast::LabeledArg {
5819 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5820 arg: center_ast,
5821 },
5822 ],
5823 digest: None,
5824 non_code_meta: Default::default(),
5825 })))
5826}
5827
5828pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
5830 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5831 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
5832 unlabeled: Some(line_expr),
5833 arguments: Default::default(),
5834 digest: None,
5835 non_code_meta: Default::default(),
5836 })))
5837}
5838
5839pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5841 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5842 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5843 unlabeled: Some(line_expr),
5844 arguments: Default::default(),
5845 digest: None,
5846 non_code_meta: Default::default(),
5847 })))
5848}
5849
5850pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5852 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5853 object: object_expr,
5854 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5855 name: ast::Node::no_src(ast::Identifier {
5856 name: property.to_string(),
5857 digest: None,
5858 }),
5859 path: Vec::new(),
5860 abs_path: false,
5861 digest: None,
5862 }))),
5863 computed: false,
5864 digest: None,
5865 })))
5866}
5867
5868fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5870 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5872 position.x,
5873 )?))));
5874 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5875 position.y,
5876 )?))));
5877 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5878 elements: vec![x_literal, y_literal],
5879 digest: None,
5880 non_code_meta: Default::default(),
5881 })));
5882
5883 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5885 elements: vec![point_expr, point_array],
5886 digest: None,
5887 non_code_meta: Default::default(),
5888 })));
5889
5890 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5892 ast::CallExpressionKw {
5893 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5894 unlabeled: Some(array_expr),
5895 arguments: Default::default(),
5896 digest: None,
5897 non_code_meta: Default::default(),
5898 },
5899 ))))
5900}
5901
5902pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5904 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5905 elements: line_exprs,
5906 digest: None,
5907 non_code_meta: Default::default(),
5908 })));
5909
5910 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5912 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5913 unlabeled: Some(array_expr),
5914 arguments: Default::default(),
5915 digest: None,
5916 non_code_meta: Default::default(),
5917 })))
5918}
5919
5920pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5922 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5923 elements: segment_exprs,
5924 digest: None,
5925 non_code_meta: Default::default(),
5926 })));
5927
5928 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5929 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5930 unlabeled: Some(array_expr),
5931 arguments: Default::default(),
5932 digest: None,
5933 non_code_meta: Default::default(),
5934 })))
5935}
5936
5937pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5939 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5940 elements: vec![seg1_expr, seg2_expr],
5941 digest: None,
5942 non_code_meta: Default::default(),
5943 })));
5944
5945 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5946 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5947 unlabeled: Some(array_expr),
5948 arguments: Default::default(),
5949 digest: None,
5950 non_code_meta: Default::default(),
5951 })))
5952}
5953
5954pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
5956 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5957 elements: input_exprs,
5958 digest: None,
5959 non_code_meta: Default::default(),
5960 })));
5961 let arguments = vec![ast::LabeledArg {
5962 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
5963 arg: axis_expr,
5964 }];
5965
5966 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5967 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
5968 unlabeled: Some(array_expr),
5969 arguments,
5970 digest: None,
5971 non_code_meta: Default::default(),
5972 })))
5973}
5974
5975pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
5977 let arguments = vec![ast::LabeledArg {
5978 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
5979 arg: point_expr,
5980 }];
5981
5982 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5983 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
5984 unlabeled: Some(segment_expr),
5985 arguments,
5986 digest: None,
5987 non_code_meta: Default::default(),
5988 })))
5989}
5990
5991#[cfg(all(feature = "artifact-graph", test))]
5992mod tests {
5993 use super::*;
5994 use crate::engine::PlaneName;
5995 use crate::execution::cache::SketchModeState;
5996 use crate::execution::cache::clear_mem_cache;
5997 use crate::execution::cache::read_old_memory;
5998 use crate::execution::cache::write_old_memory;
5999 use crate::front::Distance;
6000 use crate::front::Fixed;
6001 use crate::front::FixedPoint;
6002 use crate::front::Midpoint;
6003 use crate::front::Object;
6004 use crate::front::Plane;
6005 use crate::front::Sketch;
6006 use crate::front::Tangent;
6007 use crate::frontend::sketch::Vertical;
6008 use crate::pretty::NumericSuffix;
6009
6010 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6011 for object in &scene_graph.objects {
6012 if let ObjectKind::Sketch(_) = &object.kind {
6013 return Some(object);
6014 }
6015 }
6016 None
6017 }
6018
6019 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6020 for object in &scene_graph.objects {
6021 if let ObjectKind::Face(_) = &object.kind {
6022 return Some(object);
6023 }
6024 }
6025 None
6026 }
6027
6028 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6029 for object in &scene_graph.objects {
6030 if matches!(&object.kind, ObjectKind::Wall(_)) {
6031 return Some(object.id);
6032 }
6033 }
6034 None
6035 }
6036
6037 #[test]
6038 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6039 let source = "\
6040region001 = region(point = [0.1, 0.1], sketch = s)
6041extrude001 = extrude(region001, length = 5)
6042revolve001 = revolve(region001, axis = Y)
6043sweep001 = sweep(region001, path = path001)
6044loft001 = loft(region001)
6045not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6046";
6047
6048 let program = Program::parse(source).unwrap().0.unwrap();
6049
6050 assert_eq!(
6051 region_name_from_sweep_variable(&program.ast, "extrude001"),
6052 Some("region001".to_owned())
6053 );
6054 assert_eq!(
6055 region_name_from_sweep_variable(&program.ast, "revolve001"),
6056 Some("region001".to_owned())
6057 );
6058 assert_eq!(
6059 region_name_from_sweep_variable(&program.ast, "sweep001"),
6060 Some("region001".to_owned())
6061 );
6062 assert_eq!(
6063 region_name_from_sweep_variable(&program.ast, "loft001"),
6064 Some("region001".to_owned())
6065 );
6066 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6067 }
6068
6069 #[track_caller]
6070 fn expect_sketch(object: &Object) -> &Sketch {
6071 if let ObjectKind::Sketch(sketch) = &object.kind {
6072 sketch
6073 } else {
6074 panic!("Object is not a sketch: {:?}", object);
6075 }
6076 }
6077
6078 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6079 let point_object = scene_graph.objects.get(point_id.0).unwrap();
6080 let ObjectKind::Segment {
6081 segment: Segment::Point(point),
6082 } = &point_object.kind
6083 else {
6084 panic!("Object is not a point segment: {point_object:?}");
6085 };
6086 point.position.clone()
6087 }
6088
6089 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6090 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6091 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6092 }
6093
6094 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6095 LineCtor {
6096 start: Point2d {
6097 x: Expr::Number(Number { value: start_x, units }),
6098 y: Expr::Number(Number { value: start_y, units }),
6099 },
6100 end: Point2d {
6101 x: Expr::Number(Number { value: end_x, units }),
6102 y: Expr::Number(Number { value: end_y, units }),
6103 },
6104 construction: None,
6105 }
6106 }
6107
6108 async fn create_sketch_with_single_line(
6109 frontend: &mut FrontendState,
6110 ctx: &ExecutorContext,
6111 mock_ctx: &ExecutorContext,
6112 version: Version,
6113 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6114 frontend.program = Program::empty();
6115
6116 let sketch_args = SketchCtor {
6117 on: Plane::Default(PlaneName::Xy),
6118 };
6119 let (_src_delta, _scene_delta, sketch_id) = frontend
6120 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6121 .await
6122 .unwrap();
6123
6124 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6125 let (source_delta, scene_graph_delta) = frontend
6126 .add_segment(mock_ctx, version, sketch_id, segment, None)
6127 .await
6128 .unwrap();
6129 let line_id = *scene_graph_delta
6130 .new_objects
6131 .last()
6132 .expect("Expected line object id to be created");
6133
6134 (sketch_id, line_id, source_delta, scene_graph_delta)
6135 }
6136
6137 #[tokio::test(flavor = "multi_thread")]
6138 async fn test_sketch_checkpoint_round_trip_restores_state() {
6139 let mut frontend = FrontendState::new();
6140 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6141 let mock_ctx = ExecutorContext::new_mock(None).await;
6142 let version = Version(0);
6143
6144 let (sketch_id, line_id, source_delta, scene_graph_delta) =
6145 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6146
6147 let expected_source = source_delta.text.clone();
6148 let expected_scene_graph = frontend.scene_graph.clone();
6149 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6150 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6151
6152 let checkpoint_id = frontend
6153 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6154 .await
6155 .unwrap();
6156
6157 let edited_segments = vec![ExistingSegmentCtor {
6158 id: line_id,
6159 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6160 }];
6161 let (edited_source, _edited_scene) = frontend
6162 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6163 .await
6164 .unwrap();
6165 assert_ne!(edited_source.text, expected_source);
6166
6167 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6168
6169 assert_eq!(restored.source_delta.text, expected_source);
6170 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6171 assert!(restored.scene_graph_delta.invalidates_ids);
6172 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6173 assert_eq!(frontend.scene_graph, expected_scene_graph);
6174 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6175
6176 ctx.close().await;
6177 mock_ctx.close().await;
6178 }
6179
6180 #[tokio::test(flavor = "multi_thread")]
6181 async fn test_sketch_checkpoints_prune_oldest_entries() {
6182 let mut frontend = FrontendState::new();
6183 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6184 let mock_ctx = ExecutorContext::new_mock(None).await;
6185 let version = Version(0);
6186
6187 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6188 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6189
6190 let mut checkpoint_ids = Vec::new();
6191 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6192 checkpoint_ids.push(
6193 frontend
6194 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6195 .await
6196 .unwrap(),
6197 );
6198 }
6199
6200 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6201 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6202
6203 let oldest_retained = checkpoint_ids[3];
6204 assert_eq!(
6205 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6206 Some(oldest_retained)
6207 );
6208
6209 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6210 assert!(evicted_restore.is_err());
6211 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6212
6213 frontend
6214 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6215 .await
6216 .unwrap();
6217
6218 ctx.close().await;
6219 mock_ctx.close().await;
6220 }
6221
6222 #[tokio::test(flavor = "multi_thread")]
6223 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6224 let mut frontend = FrontendState::new();
6225 let missing_checkpoint = SketchCheckpointId::new(999);
6226
6227 let err = frontend
6228 .restore_sketch_checkpoint(missing_checkpoint)
6229 .await
6230 .expect_err("Expected restore to fail for missing checkpoint");
6231
6232 assert!(err.msg.contains("Sketch checkpoint not found"));
6233 }
6234
6235 #[tokio::test(flavor = "multi_thread")]
6236 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6237 let mut frontend = FrontendState::new();
6238 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6239 let mock_ctx = ExecutorContext::new_mock(None).await;
6240 let version = Version(0);
6241
6242 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6243 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6244
6245 let checkpoint_a = frontend
6246 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6247 .await
6248 .unwrap();
6249 let checkpoint_b = frontend
6250 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6251 .await
6252 .unwrap();
6253 assert_eq!(frontend.sketch_checkpoints.len(), 2);
6254
6255 frontend.clear_sketch_checkpoints();
6256 assert!(frontend.sketch_checkpoints.is_empty());
6257 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6258 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6259
6260 ctx.close().await;
6261 mock_ctx.close().await;
6262 }
6263
6264 #[tokio::test(flavor = "multi_thread")]
6265 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6266 let mut frontend = FrontendState::new();
6267 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6268 let mock_ctx = ExecutorContext::new_mock(None).await;
6269 let version = Version(0);
6270
6271 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6272 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6273 let old_source = source_delta.text.clone();
6274 let old_checkpoint = frontend
6275 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6276 .await
6277 .unwrap();
6278 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6279
6280 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
6281 .unwrap()
6282 .0
6283 .unwrap();
6284
6285 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6286 let SetProgramOutcome::Success {
6287 checkpoint_id: Some(new_checkpoint),
6288 ..
6289 } = result
6290 else {
6291 panic!("Expected Success with a fresh checkpoint baseline");
6292 };
6293
6294 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6295
6296 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6297 assert_eq!(old_restore.source_delta.text, old_source);
6298
6299 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6300 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6301
6302 ctx.close().await;
6303 mock_ctx.close().await;
6304 }
6305
6306 #[tokio::test(flavor = "multi_thread")]
6307 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6308 let mut frontend = FrontendState::new();
6309 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6310 let mock_ctx = ExecutorContext::new_mock(None).await;
6311 let version = Version(0);
6312
6313 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6314 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6315 let old_checkpoint = frontend
6316 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6317 .await
6318 .unwrap();
6319 let checkpoint_count_before = frontend.sketch_checkpoints.len();
6320
6321 let failing_program = Program::parse(
6322 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6323 )
6324 .unwrap()
6325 .0
6326 .unwrap();
6327
6328 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6329 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6330 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6331 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6332
6333 ctx.close().await;
6334 mock_ctx.close().await;
6335 }
6336
6337 #[tokio::test(flavor = "multi_thread")]
6338 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6339 let mut frontend = FrontendState::new();
6340 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6341
6342 let program = Program::parse(
6343 "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",
6344 )
6345 .unwrap()
6346 .0
6347 .unwrap();
6348 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6349 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6350 panic!("Expected successful baseline program execution");
6351 };
6352
6353 clear_mem_cache().await;
6354 assert!(read_old_memory().await.is_none());
6355
6356 let checkpoint_without_mock_memory = frontend
6357 .create_sketch_checkpoint((*exec_outcome).clone())
6358 .await
6359 .unwrap();
6360
6361 write_old_memory(SketchModeState::new_for_tests()).await;
6362 assert!(read_old_memory().await.is_some());
6363
6364 let checkpoint_with_mock_memory = frontend
6365 .create_sketch_checkpoint((*exec_outcome).clone())
6366 .await
6367 .unwrap();
6368
6369 clear_mem_cache().await;
6370 assert!(read_old_memory().await.is_none());
6371
6372 frontend
6373 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6374 .await
6375 .unwrap();
6376 assert!(read_old_memory().await.is_some());
6377
6378 frontend
6379 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6380 .await
6381 .unwrap();
6382 assert!(read_old_memory().await.is_none());
6383
6384 ctx.close().await;
6385 }
6386
6387 #[tokio::test(flavor = "multi_thread")]
6388 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6389 let source = "\
6390sketch(on = XY) {
6391 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6392}
6393
6394bad = missing_name
6395";
6396 let program = Program::parse(source).unwrap().0.unwrap();
6397
6398 let mut frontend = FrontendState::new();
6399
6400 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6401 let mock_ctx = ExecutorContext::new_mock(None).await;
6402 let version = Version(0);
6403 let project_id = ProjectId(0);
6404 let file_id = FileId(0);
6405
6406 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6407 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6408 };
6409
6410 let sketch_id = frontend
6411 .scene_graph
6412 .objects
6413 .iter()
6414 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6415 .expect("Expected sketch object from errored hack_set_program");
6416
6417 frontend
6418 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6419 .await
6420 .unwrap();
6421
6422 ctx.close().await;
6423 mock_ctx.close().await;
6424 }
6425
6426 #[tokio::test(flavor = "multi_thread")]
6427 async fn test_new_sketch_add_point_edit_point() {
6428 let program = Program::empty();
6429
6430 let mut frontend = FrontendState::new();
6431 frontend.program = program;
6432
6433 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6434 let mock_ctx = ExecutorContext::new_mock(None).await;
6435 let version = Version(0);
6436
6437 let sketch_args = SketchCtor {
6438 on: Plane::Default(PlaneName::Xy),
6439 };
6440 let (_src_delta, scene_delta, sketch_id) = frontend
6441 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6442 .await
6443 .unwrap();
6444 assert_eq!(sketch_id, ObjectId(1));
6445 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6446 let sketch_object = &scene_delta.new_graph.objects[1];
6447 assert_eq!(sketch_object.id, ObjectId(1));
6448 assert_eq!(
6449 sketch_object.kind,
6450 ObjectKind::Sketch(Sketch {
6451 args: SketchCtor {
6452 on: Plane::Default(PlaneName::Xy)
6453 },
6454 plane: ObjectId(0),
6455 segments: vec![],
6456 constraints: vec![],
6457 })
6458 );
6459 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6460
6461 let point_ctor = PointCtor {
6462 position: Point2d {
6463 x: Expr::Number(Number {
6464 value: 1.0,
6465 units: NumericSuffix::Inch,
6466 }),
6467 y: Expr::Number(Number {
6468 value: 2.0,
6469 units: NumericSuffix::Inch,
6470 }),
6471 },
6472 };
6473 let segment = SegmentCtor::Point(point_ctor);
6474 let (src_delta, scene_delta) = frontend
6475 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6476 .await
6477 .unwrap();
6478 assert_eq!(
6479 src_delta.text.as_str(),
6480 "sketch001 = sketch(on = XY) {
6481 point(at = [1in, 2in])
6482}
6483"
6484 );
6485 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6486 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6487 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6488 assert_eq!(scene_object.id.0, i);
6489 }
6490
6491 let point_id = *scene_delta.new_objects.last().unwrap();
6492
6493 let point_ctor = PointCtor {
6494 position: Point2d {
6495 x: Expr::Number(Number {
6496 value: 3.0,
6497 units: NumericSuffix::Inch,
6498 }),
6499 y: Expr::Number(Number {
6500 value: 4.0,
6501 units: NumericSuffix::Inch,
6502 }),
6503 },
6504 };
6505 let segments = vec![ExistingSegmentCtor {
6506 id: point_id,
6507 ctor: SegmentCtor::Point(point_ctor),
6508 }];
6509 let (src_delta, scene_delta) = frontend
6510 .edit_segments(&mock_ctx, version, sketch_id, segments)
6511 .await
6512 .unwrap();
6513 assert_eq!(
6514 src_delta.text.as_str(),
6515 "sketch001 = sketch(on = XY) {
6516 point(at = [3in, 4in])
6517}
6518"
6519 );
6520 assert_eq!(scene_delta.new_objects, vec![]);
6521 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6522
6523 ctx.close().await;
6524 mock_ctx.close().await;
6525 }
6526
6527 #[tokio::test(flavor = "multi_thread")]
6528 async fn test_new_sketch_add_line_edit_line() {
6529 let program = Program::empty();
6530
6531 let mut frontend = FrontendState::new();
6532 frontend.program = program;
6533
6534 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6535 let mock_ctx = ExecutorContext::new_mock(None).await;
6536 let version = Version(0);
6537
6538 let sketch_args = SketchCtor {
6539 on: Plane::Default(PlaneName::Xy),
6540 };
6541 let (_src_delta, scene_delta, sketch_id) = frontend
6542 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6543 .await
6544 .unwrap();
6545 assert_eq!(sketch_id, ObjectId(1));
6546 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6547 let sketch_object = &scene_delta.new_graph.objects[1];
6548 assert_eq!(sketch_object.id, ObjectId(1));
6549 assert_eq!(
6550 sketch_object.kind,
6551 ObjectKind::Sketch(Sketch {
6552 args: SketchCtor {
6553 on: Plane::Default(PlaneName::Xy)
6554 },
6555 plane: ObjectId(0),
6556 segments: vec![],
6557 constraints: vec![],
6558 })
6559 );
6560 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6561
6562 let line_ctor = LineCtor {
6563 start: Point2d {
6564 x: Expr::Number(Number {
6565 value: 0.0,
6566 units: NumericSuffix::Mm,
6567 }),
6568 y: Expr::Number(Number {
6569 value: 0.0,
6570 units: NumericSuffix::Mm,
6571 }),
6572 },
6573 end: Point2d {
6574 x: Expr::Number(Number {
6575 value: 10.0,
6576 units: NumericSuffix::Mm,
6577 }),
6578 y: Expr::Number(Number {
6579 value: 10.0,
6580 units: NumericSuffix::Mm,
6581 }),
6582 },
6583 construction: None,
6584 };
6585 let segment = SegmentCtor::Line(line_ctor);
6586 let (src_delta, scene_delta) = frontend
6587 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6588 .await
6589 .unwrap();
6590 assert_eq!(
6591 src_delta.text.as_str(),
6592 "sketch001 = sketch(on = XY) {
6593 line(start = [0mm, 0mm], end = [10mm, 10mm])
6594}
6595"
6596 );
6597 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6598 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6599 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6600 assert_eq!(scene_object.id.0, i);
6601 }
6602
6603 let line = *scene_delta.new_objects.last().unwrap();
6605
6606 let line_ctor = LineCtor {
6607 start: Point2d {
6608 x: Expr::Number(Number {
6609 value: 1.0,
6610 units: NumericSuffix::Mm,
6611 }),
6612 y: Expr::Number(Number {
6613 value: 2.0,
6614 units: NumericSuffix::Mm,
6615 }),
6616 },
6617 end: Point2d {
6618 x: Expr::Number(Number {
6619 value: 13.0,
6620 units: NumericSuffix::Mm,
6621 }),
6622 y: Expr::Number(Number {
6623 value: 14.0,
6624 units: NumericSuffix::Mm,
6625 }),
6626 },
6627 construction: None,
6628 };
6629 let segments = vec![ExistingSegmentCtor {
6630 id: line,
6631 ctor: SegmentCtor::Line(line_ctor),
6632 }];
6633 let (src_delta, scene_delta) = frontend
6634 .edit_segments(&mock_ctx, version, sketch_id, segments)
6635 .await
6636 .unwrap();
6637 assert_eq!(
6638 src_delta.text.as_str(),
6639 "sketch001 = sketch(on = XY) {
6640 line(start = [1mm, 2mm], end = [13mm, 14mm])
6641}
6642"
6643 );
6644 assert_eq!(scene_delta.new_objects, vec![]);
6645 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6646
6647 ctx.close().await;
6648 mock_ctx.close().await;
6649 }
6650
6651 #[tokio::test(flavor = "multi_thread")]
6652 async fn test_new_sketch_add_arc_edit_arc() {
6653 let program = Program::empty();
6654
6655 let mut frontend = FrontendState::new();
6656 frontend.program = program;
6657
6658 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6659 let mock_ctx = ExecutorContext::new_mock(None).await;
6660 let version = Version(0);
6661
6662 let sketch_args = SketchCtor {
6663 on: Plane::Default(PlaneName::Xy),
6664 };
6665 let (_src_delta, scene_delta, sketch_id) = frontend
6666 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6667 .await
6668 .unwrap();
6669 assert_eq!(sketch_id, ObjectId(1));
6670 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6671 let sketch_object = &scene_delta.new_graph.objects[1];
6672 assert_eq!(sketch_object.id, ObjectId(1));
6673 assert_eq!(
6674 sketch_object.kind,
6675 ObjectKind::Sketch(Sketch {
6676 args: SketchCtor {
6677 on: Plane::Default(PlaneName::Xy),
6678 },
6679 plane: ObjectId(0),
6680 segments: vec![],
6681 constraints: vec![],
6682 })
6683 );
6684 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6685
6686 let arc_ctor = ArcCtor {
6687 start: Point2d {
6688 x: Expr::Var(Number {
6689 value: 0.0,
6690 units: NumericSuffix::Mm,
6691 }),
6692 y: Expr::Var(Number {
6693 value: 0.0,
6694 units: NumericSuffix::Mm,
6695 }),
6696 },
6697 end: Point2d {
6698 x: Expr::Var(Number {
6699 value: 10.0,
6700 units: NumericSuffix::Mm,
6701 }),
6702 y: Expr::Var(Number {
6703 value: 10.0,
6704 units: NumericSuffix::Mm,
6705 }),
6706 },
6707 center: Point2d {
6708 x: Expr::Var(Number {
6709 value: 10.0,
6710 units: NumericSuffix::Mm,
6711 }),
6712 y: Expr::Var(Number {
6713 value: 0.0,
6714 units: NumericSuffix::Mm,
6715 }),
6716 },
6717 construction: None,
6718 };
6719 let segment = SegmentCtor::Arc(arc_ctor);
6720 let (src_delta, scene_delta) = frontend
6721 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6722 .await
6723 .unwrap();
6724 assert_eq!(
6725 src_delta.text.as_str(),
6726 "sketch001 = sketch(on = XY) {
6727 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6728}
6729"
6730 );
6731 assert_eq!(
6732 scene_delta.new_objects,
6733 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6734 );
6735 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6736 assert_eq!(scene_object.id.0, i);
6737 }
6738 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6739
6740 let arc = *scene_delta.new_objects.last().unwrap();
6742
6743 let arc_ctor = ArcCtor {
6744 start: Point2d {
6745 x: Expr::Var(Number {
6746 value: 1.0,
6747 units: NumericSuffix::Mm,
6748 }),
6749 y: Expr::Var(Number {
6750 value: 2.0,
6751 units: NumericSuffix::Mm,
6752 }),
6753 },
6754 end: Point2d {
6755 x: Expr::Var(Number {
6756 value: 13.0,
6757 units: NumericSuffix::Mm,
6758 }),
6759 y: Expr::Var(Number {
6760 value: 14.0,
6761 units: NumericSuffix::Mm,
6762 }),
6763 },
6764 center: Point2d {
6765 x: Expr::Var(Number {
6766 value: 13.0,
6767 units: NumericSuffix::Mm,
6768 }),
6769 y: Expr::Var(Number {
6770 value: 2.0,
6771 units: NumericSuffix::Mm,
6772 }),
6773 },
6774 construction: None,
6775 };
6776 let segments = vec![ExistingSegmentCtor {
6777 id: arc,
6778 ctor: SegmentCtor::Arc(arc_ctor),
6779 }];
6780 let (src_delta, scene_delta) = frontend
6781 .edit_segments(&mock_ctx, version, sketch_id, segments)
6782 .await
6783 .unwrap();
6784 assert_eq!(
6785 src_delta.text.as_str(),
6786 "sketch001 = sketch(on = XY) {
6787 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6788}
6789"
6790 );
6791 assert_eq!(scene_delta.new_objects, vec![]);
6792 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6793
6794 ctx.close().await;
6795 mock_ctx.close().await;
6796 }
6797
6798 #[tokio::test(flavor = "multi_thread")]
6799 async fn test_new_sketch_add_circle_edit_circle() {
6800 let program = Program::empty();
6801
6802 let mut frontend = FrontendState::new();
6803 frontend.program = program;
6804
6805 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6806 let mock_ctx = ExecutorContext::new_mock(None).await;
6807 let version = Version(0);
6808
6809 let sketch_args = SketchCtor {
6810 on: Plane::Default(PlaneName::Xy),
6811 };
6812 let (_src_delta, _scene_delta, sketch_id) = frontend
6813 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6814 .await
6815 .unwrap();
6816
6817 let circle_ctor = CircleCtor {
6819 start: Point2d {
6820 x: Expr::Var(Number {
6821 value: 5.0,
6822 units: NumericSuffix::Mm,
6823 }),
6824 y: Expr::Var(Number {
6825 value: 0.0,
6826 units: NumericSuffix::Mm,
6827 }),
6828 },
6829 center: Point2d {
6830 x: Expr::Var(Number {
6831 value: 0.0,
6832 units: NumericSuffix::Mm,
6833 }),
6834 y: Expr::Var(Number {
6835 value: 0.0,
6836 units: NumericSuffix::Mm,
6837 }),
6838 },
6839 construction: None,
6840 };
6841 let segment = SegmentCtor::Circle(circle_ctor);
6842 let (src_delta, scene_delta) = frontend
6843 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6844 .await
6845 .unwrap();
6846 assert_eq!(
6847 src_delta.text.as_str(),
6848 "sketch001 = sketch(on = XY) {
6849 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6850}
6851"
6852 );
6853 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6855 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6856
6857 let circle = *scene_delta.new_objects.last().unwrap();
6858
6859 let circle_ctor = CircleCtor {
6861 start: Point2d {
6862 x: Expr::Var(Number {
6863 value: 10.0,
6864 units: NumericSuffix::Mm,
6865 }),
6866 y: Expr::Var(Number {
6867 value: 0.0,
6868 units: NumericSuffix::Mm,
6869 }),
6870 },
6871 center: Point2d {
6872 x: Expr::Var(Number {
6873 value: 3.0,
6874 units: NumericSuffix::Mm,
6875 }),
6876 y: Expr::Var(Number {
6877 value: 4.0,
6878 units: NumericSuffix::Mm,
6879 }),
6880 },
6881 construction: None,
6882 };
6883 let segments = vec![ExistingSegmentCtor {
6884 id: circle,
6885 ctor: SegmentCtor::Circle(circle_ctor),
6886 }];
6887 let (src_delta, scene_delta) = frontend
6888 .edit_segments(&mock_ctx, version, sketch_id, segments)
6889 .await
6890 .unwrap();
6891 assert_eq!(
6892 src_delta.text.as_str(),
6893 "sketch001 = sketch(on = XY) {
6894 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6895}
6896"
6897 );
6898 assert_eq!(scene_delta.new_objects, vec![]);
6899 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6900
6901 ctx.close().await;
6902 mock_ctx.close().await;
6903 }
6904
6905 #[tokio::test(flavor = "multi_thread")]
6906 async fn test_delete_circle() {
6907 let initial_source = "sketch001 = sketch(on = XY) {
6908 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6909}
6910";
6911
6912 let program = Program::parse(initial_source).unwrap().0.unwrap();
6913 let mut frontend = FrontendState::new();
6914
6915 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6916 let mock_ctx = ExecutorContext::new_mock(None).await;
6917 let version = Version(0);
6918
6919 frontend.hack_set_program(&ctx, program).await.unwrap();
6920 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6921 let sketch_id = sketch_object.id;
6922 let sketch = expect_sketch(sketch_object);
6923
6924 assert_eq!(sketch.segments.len(), 3);
6926 let circle_id = sketch.segments[2];
6927
6928 let (src_delta, scene_delta) = frontend
6930 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6931 .await
6932 .unwrap();
6933 assert_eq!(
6934 src_delta.text.as_str(),
6935 "sketch001 = sketch(on = XY) {
6936}
6937"
6938 );
6939 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6940 let new_sketch = expect_sketch(new_sketch_object);
6941 assert_eq!(new_sketch.segments.len(), 0);
6942
6943 ctx.close().await;
6944 mock_ctx.close().await;
6945 }
6946
6947 #[tokio::test(flavor = "multi_thread")]
6948 async fn test_edit_circle_via_point() {
6949 let initial_source = "sketch001 = sketch(on = XY) {
6950 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6951}
6952";
6953
6954 let program = Program::parse(initial_source).unwrap().0.unwrap();
6955 let mut frontend = FrontendState::new();
6956
6957 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6958 let mock_ctx = ExecutorContext::new_mock(None).await;
6959 let version = Version(0);
6960
6961 frontend.hack_set_program(&ctx, program).await.unwrap();
6962 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6963 let sketch_id = sketch_object.id;
6964 let sketch = expect_sketch(sketch_object);
6965
6966 let circle_id = sketch
6968 .segments
6969 .iter()
6970 .copied()
6971 .find(|seg_id| {
6972 matches!(
6973 &frontend.scene_graph.objects[seg_id.0].kind,
6974 ObjectKind::Segment {
6975 segment: Segment::Circle(_)
6976 }
6977 )
6978 })
6979 .expect("Expected a circle segment in sketch");
6980 let circle_object = &frontend.scene_graph.objects[circle_id.0];
6981 let ObjectKind::Segment {
6982 segment: Segment::Circle(circle),
6983 } = &circle_object.kind
6984 else {
6985 panic!("Expected circle segment, got: {:?}", circle_object.kind);
6986 };
6987 let start_point_id = circle.start;
6988
6989 let segments = vec![ExistingSegmentCtor {
6991 id: start_point_id,
6992 ctor: SegmentCtor::Point(PointCtor {
6993 position: Point2d {
6994 x: Expr::Var(Number {
6995 value: 7.0,
6996 units: NumericSuffix::Mm,
6997 }),
6998 y: Expr::Var(Number {
6999 value: 1.0,
7000 units: NumericSuffix::Mm,
7001 }),
7002 },
7003 }),
7004 }];
7005 let (src_delta, _scene_delta) = frontend
7006 .edit_segments(&mock_ctx, version, sketch_id, segments)
7007 .await
7008 .unwrap();
7009 assert_eq!(
7010 src_delta.text.as_str(),
7011 "sketch001 = sketch(on = XY) {
7012 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7013}
7014"
7015 );
7016
7017 ctx.close().await;
7018 mock_ctx.close().await;
7019 }
7020
7021 #[tokio::test(flavor = "multi_thread")]
7022 async fn test_add_line_when_sketch_block_uses_variable() {
7023 let initial_source = "s = sketch(on = XY) {}
7024";
7025
7026 let program = Program::parse(initial_source).unwrap().0.unwrap();
7027
7028 let mut frontend = FrontendState::new();
7029
7030 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7031 let mock_ctx = ExecutorContext::new_mock(None).await;
7032 let version = Version(0);
7033
7034 frontend.hack_set_program(&ctx, program).await.unwrap();
7035 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7036 let sketch_id = sketch_object.id;
7037
7038 let line_ctor = LineCtor {
7039 start: Point2d {
7040 x: Expr::Number(Number {
7041 value: 0.0,
7042 units: NumericSuffix::Mm,
7043 }),
7044 y: Expr::Number(Number {
7045 value: 0.0,
7046 units: NumericSuffix::Mm,
7047 }),
7048 },
7049 end: Point2d {
7050 x: Expr::Number(Number {
7051 value: 10.0,
7052 units: NumericSuffix::Mm,
7053 }),
7054 y: Expr::Number(Number {
7055 value: 10.0,
7056 units: NumericSuffix::Mm,
7057 }),
7058 },
7059 construction: None,
7060 };
7061 let segment = SegmentCtor::Line(line_ctor);
7062 let (src_delta, scene_delta) = frontend
7063 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7064 .await
7065 .unwrap();
7066 assert_eq!(
7067 src_delta.text.as_str(),
7068 "s = sketch(on = XY) {
7069 line(start = [0mm, 0mm], end = [10mm, 10mm])
7070}
7071"
7072 );
7073 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7074 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7075
7076 ctx.close().await;
7077 mock_ctx.close().await;
7078 }
7079
7080 #[tokio::test(flavor = "multi_thread")]
7081 async fn test_new_sketch_add_line_delete_sketch() {
7082 let program = Program::empty();
7083
7084 let mut frontend = FrontendState::new();
7085 frontend.program = program;
7086
7087 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7088 let mock_ctx = ExecutorContext::new_mock(None).await;
7089 let version = Version(0);
7090
7091 let sketch_args = SketchCtor {
7092 on: Plane::Default(PlaneName::Xy),
7093 };
7094 let (_src_delta, scene_delta, sketch_id) = frontend
7095 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7096 .await
7097 .unwrap();
7098 assert_eq!(sketch_id, ObjectId(1));
7099 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7100 let sketch_object = &scene_delta.new_graph.objects[1];
7101 assert_eq!(sketch_object.id, ObjectId(1));
7102 assert_eq!(
7103 sketch_object.kind,
7104 ObjectKind::Sketch(Sketch {
7105 args: SketchCtor {
7106 on: Plane::Default(PlaneName::Xy)
7107 },
7108 plane: ObjectId(0),
7109 segments: vec![],
7110 constraints: vec![],
7111 })
7112 );
7113 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7114
7115 let line_ctor = LineCtor {
7116 start: Point2d {
7117 x: Expr::Number(Number {
7118 value: 0.0,
7119 units: NumericSuffix::Mm,
7120 }),
7121 y: Expr::Number(Number {
7122 value: 0.0,
7123 units: NumericSuffix::Mm,
7124 }),
7125 },
7126 end: Point2d {
7127 x: Expr::Number(Number {
7128 value: 10.0,
7129 units: NumericSuffix::Mm,
7130 }),
7131 y: Expr::Number(Number {
7132 value: 10.0,
7133 units: NumericSuffix::Mm,
7134 }),
7135 },
7136 construction: None,
7137 };
7138 let segment = SegmentCtor::Line(line_ctor);
7139 let (src_delta, scene_delta) = frontend
7140 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7141 .await
7142 .unwrap();
7143 assert_eq!(
7144 src_delta.text.as_str(),
7145 "sketch001 = sketch(on = XY) {
7146 line(start = [0mm, 0mm], end = [10mm, 10mm])
7147}
7148"
7149 );
7150 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7151
7152 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7153 assert_eq!(src_delta.text.as_str(), "");
7154 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7155
7156 ctx.close().await;
7157 mock_ctx.close().await;
7158 }
7159
7160 #[tokio::test(flavor = "multi_thread")]
7161 async fn test_delete_sketch_when_sketch_block_uses_variable() {
7162 let initial_source = "s = sketch(on = XY) {}
7163";
7164
7165 let program = Program::parse(initial_source).unwrap().0.unwrap();
7166
7167 let mut frontend = FrontendState::new();
7168
7169 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7170 let mock_ctx = ExecutorContext::new_mock(None).await;
7171 let version = Version(0);
7172
7173 frontend.hack_set_program(&ctx, program).await.unwrap();
7174 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7175 let sketch_id = sketch_object.id;
7176
7177 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7178 assert_eq!(src_delta.text.as_str(), "");
7179 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7180
7181 ctx.close().await;
7182 mock_ctx.close().await;
7183 }
7184
7185 #[tokio::test(flavor = "multi_thread")]
7186 async fn test_delete_sketch_after_comment() {
7187 let initial_source = "sketch001 = sketch(on = XZ) {
7188}
7189";
7190
7191 let program = Program::parse(initial_source).unwrap().0.unwrap();
7192 let mut frontend = FrontendState::new();
7193
7194 let ctx = ExecutorContext::new_with_engine(
7195 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7196 Default::default(),
7197 );
7198 let version = Version(0);
7199
7200 frontend.hack_set_program(&ctx, program).await.unwrap();
7201 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7202 let sketch_id = sketch_object.id;
7203 let original_source = sketch_object.source.clone();
7204
7205 let commented_source = "// test 1
7206sketch001 = sketch(on = XZ) {
7207}
7208";
7209 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7210 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7211
7212 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7213 assert_eq!(cached_sketch_object.source, original_source);
7214
7215 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7216 assert!(
7217 !src_delta.text.contains("sketch001"),
7218 "sketch was not deleted: {}",
7219 src_delta.text
7220 );
7221 assert_eq!(src_delta.text.as_str(), "// test 1\n");
7223 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7224
7225 ctx.close().await;
7226 }
7227
7228 #[tokio::test(flavor = "multi_thread")]
7229 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7230 let initial_source = "sketch001 = sketch(on = XZ) {
7231}
7232foo = 1
7233";
7234
7235 let program = Program::parse(initial_source).unwrap().0.unwrap();
7236 let mut frontend = FrontendState::new();
7237
7238 let ctx = ExecutorContext::new_with_engine(
7239 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7240 Default::default(),
7241 );
7242 let version = Version(0);
7243
7244 frontend.hack_set_program(&ctx, program).await.unwrap();
7245 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7246 let sketch_id = sketch_object.id;
7247
7248 let commented_source = "// keep me
7249sketch001 = sketch(on = XZ) {
7250}
7251foo = 1
7252";
7253 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7254 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7255
7256 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7257 assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7259
7260 ctx.close().await;
7261 }
7262
7263 #[tokio::test(flavor = "multi_thread")]
7264 async fn test_delete_segment_preserves_pre_comment() {
7265 let initial_source = "\
7266sketch(on = XY) {
7267 point(at = [var 1, var 2])
7268 // describe the middle point
7269 point(at = [var 3, var 4])
7270 point(at = [var 5, var 6])
7271}
7272";
7273
7274 let program = Program::parse(initial_source).unwrap().0.unwrap();
7275 let mut frontend = FrontendState::new();
7276
7277 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7278 let mock_ctx = ExecutorContext::new_mock(None).await;
7279 let version = Version(0);
7280
7281 frontend.hack_set_program(&ctx, program).await.unwrap();
7282 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7283 let sketch_id = sketch_object.id;
7284 let sketch = expect_sketch(sketch_object);
7285
7286 let middle_point_id = *sketch.segments.get(1).unwrap();
7287
7288 let (src_delta, _scene_delta) = frontend
7289 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7290 .await
7291 .unwrap();
7292 assert_eq!(
7295 src_delta.text.as_str(),
7296 "\
7297sketch(on = XY) {
7298 point(at = [var 1mm, var 2mm])
7299 // describe the middle point
7300 point(at = [var 5mm, var 6mm])
7301}
7302"
7303 );
7304
7305 ctx.close().await;
7306 mock_ctx.close().await;
7307 }
7308
7309 #[tokio::test(flavor = "multi_thread")]
7310 async fn test_delete_last_segment_preserves_pre_comment() {
7311 let initial_source = "\
7312sketch(on = XY) {
7313 point(at = [var 1, var 2])
7314 // describe the trailing point
7315 point(at = [var 3, var 4])
7316}
7317";
7318
7319 let program = Program::parse(initial_source).unwrap().0.unwrap();
7320 let mut frontend = FrontendState::new();
7321
7322 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7323 let mock_ctx = ExecutorContext::new_mock(None).await;
7324 let version = Version(0);
7325
7326 frontend.hack_set_program(&ctx, program).await.unwrap();
7327 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7328 let sketch_id = sketch_object.id;
7329 let sketch = expect_sketch(sketch_object);
7330
7331 let last_point_id = *sketch.segments.last().unwrap();
7332
7333 let (src_delta, _scene_delta) = frontend
7334 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7335 .await
7336 .unwrap();
7337 assert_eq!(
7340 src_delta.text.as_str(),
7341 "\
7342sketch(on = XY) {
7343 point(at = [var 1mm, var 2mm])
7344 // describe the trailing point
7345}
7346"
7347 );
7348
7349 ctx.close().await;
7350 mock_ctx.close().await;
7351 }
7352
7353 #[tokio::test(flavor = "multi_thread")]
7354 async fn test_delete_segment_drops_inline_trailing_comment() {
7355 let initial_source = "\
7356sketch(on = XY) {
7357 point(at = [var 1, var 2])
7358 point(at = [var 3, var 4]) // same-line note that gets dropped
7359 point(at = [var 5, var 6])
7360}
7361";
7362
7363 let program = Program::parse(initial_source).unwrap().0.unwrap();
7364 let mut frontend = FrontendState::new();
7365
7366 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7367 let mock_ctx = ExecutorContext::new_mock(None).await;
7368 let version = Version(0);
7369
7370 frontend.hack_set_program(&ctx, program).await.unwrap();
7371 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7372 let sketch_id = sketch_object.id;
7373 let sketch = expect_sketch(sketch_object);
7374
7375 let middle_point_id = *sketch.segments.get(1).unwrap();
7376
7377 let (src_delta, _scene_delta) = frontend
7378 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7379 .await
7380 .unwrap();
7381 assert!(
7383 !src_delta.text.contains("same-line note"),
7384 "inline comment should have been removed: {}",
7385 src_delta.text
7386 );
7387
7388 ctx.close().await;
7389 mock_ctx.close().await;
7390 }
7391
7392 #[tokio::test(flavor = "multi_thread")]
7393 async fn test_delete_segments_preserves_block_comments_across_positions() {
7394 let initial_source = "\
7402sketch(on = XY) {
7403 /* above first - moves to middle */
7404 point(at = [var 1, var 2]) /* same-line on first - dropped */
7405 /* above middle - stays */
7406 point(at = [var 3, var 4])
7407 /* above last - moves to trailing meta */
7408 point(at = [var 5, var 6])
7409}
7410";
7411
7412 let program = Program::parse(initial_source).unwrap().0.unwrap();
7413 let mut frontend = FrontendState::new();
7414
7415 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7416 let mock_ctx = ExecutorContext::new_mock(None).await;
7417 let version = Version(0);
7418
7419 frontend.hack_set_program(&ctx, program).await.unwrap();
7420 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7421 let sketch_id = sketch_object.id;
7422 let sketch = expect_sketch(sketch_object);
7423
7424 let first_point_id = *sketch.segments.first().unwrap();
7425 let last_point_id = *sketch.segments.last().unwrap();
7426
7427 let (src_delta, _scene_delta) = frontend
7428 .delete_objects(
7429 &mock_ctx,
7430 version,
7431 sketch_id,
7432 Vec::new(),
7433 vec![first_point_id, last_point_id],
7434 )
7435 .await
7436 .unwrap();
7437 assert_eq!(
7438 src_delta.text.as_str(),
7439 "\
7440sketch(on = XY) {
7441 /* above first - moves to middle */
7442 /* above middle - stays */
7443 point(at = [var 3mm, var 4mm])
7444 /* above last - moves to trailing meta */
7445}
7446"
7447 );
7448
7449 ctx.close().await;
7450 mock_ctx.close().await;
7451 }
7452
7453 #[tokio::test(flavor = "multi_thread")]
7454 async fn test_edit_line_when_editing_its_start_point() {
7455 let initial_source = "\
7456sketch(on = XY) {
7457 line(start = [var 1, var 2], end = [var 3, var 4])
7458}
7459";
7460
7461 let program = Program::parse(initial_source).unwrap().0.unwrap();
7462
7463 let mut frontend = FrontendState::new();
7464
7465 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7466 let mock_ctx = ExecutorContext::new_mock(None).await;
7467 let version = Version(0);
7468
7469 frontend.hack_set_program(&ctx, program).await.unwrap();
7470 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7471 let sketch_id = sketch_object.id;
7472 let sketch = expect_sketch(sketch_object);
7473
7474 let point_id = *sketch.segments.first().unwrap();
7475
7476 let point_ctor = PointCtor {
7477 position: Point2d {
7478 x: Expr::Var(Number {
7479 value: 5.0,
7480 units: NumericSuffix::Inch,
7481 }),
7482 y: Expr::Var(Number {
7483 value: 6.0,
7484 units: NumericSuffix::Inch,
7485 }),
7486 },
7487 };
7488 let segments = vec![ExistingSegmentCtor {
7489 id: point_id,
7490 ctor: SegmentCtor::Point(point_ctor),
7491 }];
7492 let (src_delta, scene_delta) = frontend
7493 .edit_segments(&mock_ctx, version, sketch_id, segments)
7494 .await
7495 .unwrap();
7496 assert_eq!(
7497 src_delta.text.as_str(),
7498 "\
7499sketch(on = XY) {
7500 line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7501}
7502"
7503 );
7504 assert_eq!(scene_delta.new_objects, vec![]);
7505 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7506
7507 ctx.close().await;
7508 mock_ctx.close().await;
7509 }
7510
7511 #[tokio::test(flavor = "multi_thread")]
7512 async fn test_edit_line_when_editing_its_end_point() {
7513 let initial_source = "\
7514sketch(on = XY) {
7515 line(start = [var 1, var 2], end = [var 3, var 4])
7516}
7517";
7518
7519 let program = Program::parse(initial_source).unwrap().0.unwrap();
7520
7521 let mut frontend = FrontendState::new();
7522
7523 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7524 let mock_ctx = ExecutorContext::new_mock(None).await;
7525 let version = Version(0);
7526
7527 frontend.hack_set_program(&ctx, program).await.unwrap();
7528 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7529 let sketch_id = sketch_object.id;
7530 let sketch = expect_sketch(sketch_object);
7531 let point_id = *sketch.segments.get(1).unwrap();
7532
7533 let point_ctor = PointCtor {
7534 position: Point2d {
7535 x: Expr::Var(Number {
7536 value: 5.0,
7537 units: NumericSuffix::Inch,
7538 }),
7539 y: Expr::Var(Number {
7540 value: 6.0,
7541 units: NumericSuffix::Inch,
7542 }),
7543 },
7544 };
7545 let segments = vec![ExistingSegmentCtor {
7546 id: point_id,
7547 ctor: SegmentCtor::Point(point_ctor),
7548 }];
7549 let (src_delta, scene_delta) = frontend
7550 .edit_segments(&mock_ctx, version, sketch_id, segments)
7551 .await
7552 .unwrap();
7553 assert_eq!(
7554 src_delta.text.as_str(),
7555 "\
7556sketch(on = XY) {
7557 line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
7558}
7559"
7560 );
7561 assert_eq!(scene_delta.new_objects, vec![]);
7562 assert_eq!(
7563 scene_delta.new_graph.objects.len(),
7564 5,
7565 "{:#?}",
7566 scene_delta.new_graph.objects
7567 );
7568
7569 ctx.close().await;
7570 mock_ctx.close().await;
7571 }
7572
7573 #[tokio::test(flavor = "multi_thread")]
7574 async fn test_edit_line_with_coincident_feedback() {
7575 let initial_source = "\
7576sketch(on = XY) {
7577 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
7578 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7579 fixed([line1.start, [0, 0]])
7580 coincident([line1.end, line2.start])
7581 equalLength([line1, line2])
7582}
7583";
7584
7585 let program = Program::parse(initial_source).unwrap().0.unwrap();
7586
7587 let mut frontend = FrontendState::new();
7588
7589 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7590 let mock_ctx = ExecutorContext::new_mock(None).await;
7591 let version = Version(0);
7592
7593 frontend.hack_set_program(&ctx, program).await.unwrap();
7594 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7595 let sketch_id = sketch_object.id;
7596 let sketch = expect_sketch(sketch_object);
7597 let line2_end_id = *sketch.segments.get(4).unwrap();
7598
7599 let segments = vec![ExistingSegmentCtor {
7600 id: line2_end_id,
7601 ctor: SegmentCtor::Point(PointCtor {
7602 position: Point2d {
7603 x: Expr::Var(Number {
7604 value: 9.0,
7605 units: NumericSuffix::None,
7606 }),
7607 y: Expr::Var(Number {
7608 value: 10.0,
7609 units: NumericSuffix::None,
7610 }),
7611 },
7612 }),
7613 }];
7614 let (src_delta, scene_delta) = frontend
7615 .edit_segments(&mock_ctx, version, sketch_id, segments)
7616 .await
7617 .unwrap();
7618 assert_eq!(
7619 src_delta.text.as_str(),
7620 "\
7621sketch(on = XY) {
7622 line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7623 line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7624 fixed([line1.start, [0, 0]])
7625 coincident([line1.end, line2.start])
7626 equalLength([line1, line2])
7627}
7628"
7629 );
7630 assert_eq!(
7631 scene_delta.new_graph.objects.len(),
7632 11,
7633 "{:#?}",
7634 scene_delta.new_graph.objects
7635 );
7636
7637 ctx.close().await;
7638 mock_ctx.close().await;
7639 }
7640
7641 #[tokio::test(flavor = "multi_thread")]
7642 async fn test_delete_point_without_var() {
7643 let initial_source = "\
7644sketch(on = XY) {
7645 point(at = [var 1, var 2])
7646 point(at = [var 3, var 4])
7647 point(at = [var 5, var 6])
7648}
7649";
7650
7651 let program = Program::parse(initial_source).unwrap().0.unwrap();
7652
7653 let mut frontend = FrontendState::new();
7654
7655 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7656 let mock_ctx = ExecutorContext::new_mock(None).await;
7657 let version = Version(0);
7658
7659 frontend.hack_set_program(&ctx, program).await.unwrap();
7660 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7661 let sketch_id = sketch_object.id;
7662 let sketch = expect_sketch(sketch_object);
7663
7664 let point_id = *sketch.segments.get(1).unwrap();
7665
7666 let (src_delta, scene_delta) = frontend
7667 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7668 .await
7669 .unwrap();
7670 assert_eq!(
7671 src_delta.text.as_str(),
7672 "\
7673sketch(on = XY) {
7674 point(at = [var 1mm, var 2mm])
7675 point(at = [var 5mm, var 6mm])
7676}
7677"
7678 );
7679 assert_eq!(scene_delta.new_objects, vec![]);
7680 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7681
7682 ctx.close().await;
7683 mock_ctx.close().await;
7684 }
7685
7686 #[tokio::test(flavor = "multi_thread")]
7687 async fn test_delete_point_with_var() {
7688 let initial_source = "\
7689sketch(on = XY) {
7690 point(at = [var 1, var 2])
7691 point1 = point(at = [var 3, var 4])
7692 point(at = [var 5, var 6])
7693}
7694";
7695
7696 let program = Program::parse(initial_source).unwrap().0.unwrap();
7697
7698 let mut frontend = FrontendState::new();
7699
7700 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7701 let mock_ctx = ExecutorContext::new_mock(None).await;
7702 let version = Version(0);
7703
7704 frontend.hack_set_program(&ctx, program).await.unwrap();
7705 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7706 let sketch_id = sketch_object.id;
7707 let sketch = expect_sketch(sketch_object);
7708
7709 let point_id = *sketch.segments.get(1).unwrap();
7710
7711 let (src_delta, scene_delta) = frontend
7712 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7713 .await
7714 .unwrap();
7715 assert_eq!(
7716 src_delta.text.as_str(),
7717 "\
7718sketch(on = XY) {
7719 point(at = [var 1mm, var 2mm])
7720 point(at = [var 5mm, var 6mm])
7721}
7722"
7723 );
7724 assert_eq!(scene_delta.new_objects, vec![]);
7725 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7726
7727 ctx.close().await;
7728 mock_ctx.close().await;
7729 }
7730
7731 #[tokio::test(flavor = "multi_thread")]
7732 async fn test_delete_multiple_points() {
7733 let initial_source = "\
7734sketch(on = XY) {
7735 point(at = [var 1, var 2])
7736 point1 = point(at = [var 3, var 4])
7737 point(at = [var 5, var 6])
7738}
7739";
7740
7741 let program = Program::parse(initial_source).unwrap().0.unwrap();
7742
7743 let mut frontend = FrontendState::new();
7744
7745 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7746 let mock_ctx = ExecutorContext::new_mock(None).await;
7747 let version = Version(0);
7748
7749 frontend.hack_set_program(&ctx, program).await.unwrap();
7750 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7751 let sketch_id = sketch_object.id;
7752
7753 let sketch = expect_sketch(sketch_object);
7754
7755 let point1_id = *sketch.segments.first().unwrap();
7756 let point2_id = *sketch.segments.get(1).unwrap();
7757
7758 let (src_delta, scene_delta) = frontend
7759 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
7760 .await
7761 .unwrap();
7762 assert_eq!(
7763 src_delta.text.as_str(),
7764 "\
7765sketch(on = XY) {
7766 point(at = [var 5mm, var 6mm])
7767}
7768"
7769 );
7770 assert_eq!(scene_delta.new_objects, vec![]);
7771 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7772
7773 ctx.close().await;
7774 mock_ctx.close().await;
7775 }
7776
7777 #[tokio::test(flavor = "multi_thread")]
7778 async fn test_delete_coincident_constraint() {
7779 let initial_source = "\
7780sketch(on = XY) {
7781 point1 = point(at = [var 1, var 2])
7782 point2 = point(at = [var 3, var 4])
7783 coincident([point1, point2])
7784 point(at = [var 5, var 6])
7785}
7786";
7787
7788 let program = Program::parse(initial_source).unwrap().0.unwrap();
7789
7790 let mut frontend = FrontendState::new();
7791
7792 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7793 let mock_ctx = ExecutorContext::new_mock(None).await;
7794 let version = Version(0);
7795
7796 frontend.hack_set_program(&ctx, program).await.unwrap();
7797 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7798 let sketch_id = sketch_object.id;
7799 let sketch = expect_sketch(sketch_object);
7800
7801 let coincident_id = *sketch.constraints.first().unwrap();
7802
7803 let (src_delta, scene_delta) = frontend
7804 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7805 .await
7806 .unwrap();
7807 assert_eq!(
7808 src_delta.text.as_str(),
7809 "\
7810sketch(on = XY) {
7811 point1 = point(at = [var 1mm, var 2mm])
7812 point2 = point(at = [var 3mm, var 4mm])
7813 point(at = [var 5mm, var 6mm])
7814}
7815"
7816 );
7817 assert_eq!(scene_delta.new_objects, vec![]);
7818 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7819
7820 ctx.close().await;
7821 mock_ctx.close().await;
7822 }
7823
7824 #[tokio::test(flavor = "multi_thread")]
7825 async fn test_delete_line_cascades_to_coincident_constraint() {
7826 let initial_source = "\
7827sketch(on = XY) {
7828 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7829 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7830 coincident([line1.end, line2.start])
7831}
7832";
7833
7834 let program = Program::parse(initial_source).unwrap().0.unwrap();
7835
7836 let mut frontend = FrontendState::new();
7837
7838 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7839 let mock_ctx = ExecutorContext::new_mock(None).await;
7840 let version = Version(0);
7841
7842 frontend.hack_set_program(&ctx, program).await.unwrap();
7843 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7844 let sketch_id = sketch_object.id;
7845 let sketch = expect_sketch(sketch_object);
7846 let line_id = *sketch.segments.get(5).unwrap();
7847
7848 let (src_delta, scene_delta) = frontend
7849 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7850 .await
7851 .unwrap();
7852 assert_eq!(
7853 src_delta.text.as_str(),
7854 "\
7855sketch(on = XY) {
7856 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7857}
7858"
7859 );
7860 assert_eq!(
7861 scene_delta.new_graph.objects.len(),
7862 5,
7863 "{:#?}",
7864 scene_delta.new_graph.objects
7865 );
7866
7867 ctx.close().await;
7868 mock_ctx.close().await;
7869 }
7870
7871 #[tokio::test(flavor = "multi_thread")]
7872 async fn test_delete_line_cascades_to_distance_constraint() {
7873 let initial_source = "\
7874sketch(on = XY) {
7875 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7876 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7877 distance([line1.end, line2.start]) == 10mm
7878}
7879";
7880
7881 let program = Program::parse(initial_source).unwrap().0.unwrap();
7882
7883 let mut frontend = FrontendState::new();
7884
7885 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7886 let mock_ctx = ExecutorContext::new_mock(None).await;
7887 let version = Version(0);
7888
7889 frontend.hack_set_program(&ctx, program).await.unwrap();
7890 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7891 let sketch_id = sketch_object.id;
7892 let sketch = expect_sketch(sketch_object);
7893 let line_id = *sketch.segments.get(5).unwrap();
7894
7895 let (src_delta, scene_delta) = frontend
7896 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7897 .await
7898 .unwrap();
7899 assert_eq!(
7900 src_delta.text.as_str(),
7901 "\
7902sketch(on = XY) {
7903 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7904}
7905"
7906 );
7907 assert_eq!(
7908 scene_delta.new_graph.objects.len(),
7909 5,
7910 "{:#?}",
7911 scene_delta.new_graph.objects
7912 );
7913
7914 ctx.close().await;
7915 mock_ctx.close().await;
7916 }
7917
7918 #[tokio::test(flavor = "multi_thread")]
7919 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
7920 let initial_source = "\
7921sketch(on = XY) {
7922 point1 = point(at = [var 1, var 2])
7923 point2 = point(at = [var 3, var 4])
7924 horizontalDistance([point1, point2]) == 10mm
7925}
7926";
7927
7928 let program = Program::parse(initial_source).unwrap().0.unwrap();
7929
7930 let mut frontend = FrontendState::new();
7931
7932 let mock_ctx = ExecutorContext::new_mock(None).await;
7933 let version = Version(0);
7934
7935 frontend.program = program.clone();
7936 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7937 frontend.update_state_after_exec(outcome, true);
7938 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7939 let sketch_id = sketch_object.id;
7940 let sketch = expect_sketch(sketch_object);
7941 let point2_id = *sketch.segments.get(1).unwrap();
7942
7943 let (src_delta, scene_delta) = frontend
7944 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
7945 .await
7946 .unwrap();
7947 assert_eq!(
7948 src_delta.text.as_str(),
7949 "\
7950sketch(on = XY) {
7951 point1 = point(at = [var 1mm, var 2mm])
7952}
7953"
7954 );
7955 assert_eq!(
7956 scene_delta.new_graph.objects.len(),
7957 3,
7958 "{:#?}",
7959 scene_delta.new_graph.objects
7960 );
7961
7962 mock_ctx.close().await;
7963 }
7964
7965 #[tokio::test(flavor = "multi_thread")]
7966 async fn test_delete_line_cascades_to_fixed_constraint() {
7967 let initial_source = "\
7968sketch(on = XY) {
7969 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7970 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7971 fixed([line1.start, [0, 0]])
7972}
7973";
7974
7975 let program = Program::parse(initial_source).unwrap().0.unwrap();
7976
7977 let mut frontend = FrontendState::new();
7978
7979 let mock_ctx = ExecutorContext::new_mock(None).await;
7980 let version = Version(0);
7981
7982 frontend.program = program.clone();
7983 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7984 frontend.update_state_after_exec(outcome, true);
7985 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7986 let sketch_id = sketch_object.id;
7987 let sketch = expect_sketch(sketch_object);
7988 let line1_id = *sketch.segments.get(2).unwrap();
7989
7990 let (src_delta, scene_delta) = frontend
7991 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7992 .await
7993 .unwrap();
7994 assert_eq!(
7995 src_delta.text.as_str(),
7996 "\
7997sketch(on = XY) {
7998 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7999}
8000"
8001 );
8002 assert_eq!(
8003 scene_delta.new_graph.objects.len(),
8004 5,
8005 "{:#?}",
8006 scene_delta.new_graph.objects
8007 );
8008
8009 mock_ctx.close().await;
8010 }
8011
8012 #[tokio::test(flavor = "multi_thread")]
8013 async fn test_delete_line_cascades_to_midpoint_constraint() {
8014 let initial_source = "\
8015sketch(on = XY) {
8016 point1 = point(at = [var 1, var 2])
8017 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
8018 midpoint(line1, point = point1)
8019}
8020";
8021
8022 let program = Program::parse(initial_source).unwrap().0.unwrap();
8023
8024 let mut frontend = FrontendState::new();
8025
8026 let mock_ctx = ExecutorContext::new_mock(None).await;
8027 let version = Version(0);
8028
8029 frontend.program = program.clone();
8030 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8031 frontend.update_state_after_exec(outcome, true);
8032 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8033 let sketch_id = sketch_object.id;
8034 let sketch = expect_sketch(sketch_object);
8035 let line1_id = *sketch.segments.get(3).unwrap();
8036
8037 let (src_delta, scene_delta) = frontend
8038 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8039 .await
8040 .unwrap();
8041 assert_eq!(
8042 src_delta.text.as_str(),
8043 "\
8044sketch(on = XY) {
8045 point1 = point(at = [var 1mm, var 2mm])
8046}
8047"
8048 );
8049 assert_eq!(
8050 scene_delta.new_graph.objects.len(),
8051 3,
8052 "{:#?}",
8053 scene_delta.new_graph.objects
8054 );
8055
8056 mock_ctx.close().await;
8057 }
8058
8059 #[tokio::test(flavor = "multi_thread")]
8060 async fn test_delete_point_preserves_multiline_coincident_constraint() {
8061 let initial_source = "\
8062sketch(on = XY) {
8063 point1 = point(at = [var 1, var 2])
8064 point2 = point(at = [var 3, var 4])
8065 point3 = point(at = [var 5, var 6])
8066 coincident([point1, point2, point3])
8067}
8068";
8069
8070 let program = Program::parse(initial_source).unwrap().0.unwrap();
8071
8072 let mut frontend = FrontendState::new();
8073
8074 let mock_ctx = ExecutorContext::new_mock(None).await;
8075 let version = Version(0);
8076
8077 frontend.program = program.clone();
8078 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8079 frontend.update_state_after_exec(outcome, true);
8080 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8081 let sketch_id = sketch_object.id;
8082 let sketch = expect_sketch(sketch_object);
8083 let point3_id = *sketch.segments.get(2).unwrap();
8084
8085 let (src_delta, scene_delta) = frontend
8086 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8087 .await
8088 .unwrap();
8089 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8090 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8091 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8092 assert!(
8093 src_delta.text.contains("coincident([point1, point2])"),
8094 "{}",
8095 src_delta.text
8096 );
8097
8098 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8099 let sketch = expect_sketch(sketch_object);
8100 assert_eq!(sketch.segments.len(), 2);
8101 assert_eq!(sketch.constraints.len(), 1);
8102
8103 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8104 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8105 panic!("Expected constraint object");
8106 };
8107 let Constraint::Coincident(coincident) = constraint else {
8108 panic!("Expected coincident constraint");
8109 };
8110 assert_eq!(
8111 coincident.segments,
8112 sketch
8113 .segments
8114 .iter()
8115 .copied()
8116 .map(Into::into)
8117 .collect::<Vec<ConstraintSegment>>()
8118 );
8119
8120 mock_ctx.close().await;
8121 }
8122
8123 #[tokio::test(flavor = "multi_thread")]
8124 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8125 let initial_source = "\
8126sketch(on = XY) {
8127 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8128 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8129 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8130 equalLength([line1, line2, line3])
8131}
8132";
8133
8134 let program = Program::parse(initial_source).unwrap().0.unwrap();
8135
8136 let mut frontend = FrontendState::new();
8137
8138 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8139 let mock_ctx = ExecutorContext::new_mock(None).await;
8140 let version = Version(0);
8141
8142 frontend.hack_set_program(&ctx, program).await.unwrap();
8143 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8144 let sketch_id = sketch_object.id;
8145 let sketch = expect_sketch(sketch_object);
8146 let line3_id = *sketch.segments.get(8).unwrap();
8147
8148 let (src_delta, scene_delta) = frontend
8149 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8150 .await
8151 .unwrap();
8152 assert_eq!(
8153 src_delta.text.as_str(),
8154 "\
8155sketch(on = XY) {
8156 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8157 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8158 equalLength([line1, line2])
8159}
8160"
8161 );
8162
8163 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8164 let sketch = expect_sketch(sketch_object);
8165 assert_eq!(sketch.constraints.len(), 1);
8166
8167 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8168 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8169 panic!("Expected constraint object");
8170 };
8171 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8172 panic!("Expected lines equal length constraint");
8173 };
8174 assert_eq!(lines_equal_length.lines.len(), 2);
8175
8176 ctx.close().await;
8177 mock_ctx.close().await;
8178 }
8179
8180 #[tokio::test(flavor = "multi_thread")]
8181 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8182 let initial_source = "\
8183sketch(on = XY) {
8184 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8185 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8186 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8187 horizontal([line1.end, line2.start, line3.start])
8188}
8189";
8190
8191 let program = Program::parse(initial_source).unwrap().0.unwrap();
8192
8193 let mut frontend = FrontendState::new();
8194
8195 let mock_ctx = ExecutorContext::new_mock(None).await;
8196 let version = Version(0);
8197
8198 frontend.program = program.clone();
8199 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8200 frontend.update_state_after_exec(outcome, true);
8201 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8202 let sketch_id = sketch_object.id;
8203 let sketch = expect_sketch(sketch_object);
8204 let line1_id = *sketch.segments.get(2).unwrap();
8205
8206 let (src_delta, scene_delta) = frontend
8207 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8208 .await
8209 .unwrap();
8210 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8211 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8212 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8213 assert!(
8214 src_delta.text.contains("horizontal([line2.start, line3.start])"),
8215 "{}",
8216 src_delta.text
8217 );
8218
8219 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8220 let sketch = expect_sketch(sketch_object);
8221 assert_eq!(sketch.constraints.len(), 1);
8222
8223 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8224 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8225 panic!("Expected constraint object");
8226 };
8227 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8228 panic!("Expected horizontal points constraint");
8229 };
8230 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8231 assert_eq!(*points, remaining_points);
8232
8233 mock_ctx.close().await;
8234 }
8235
8236 #[tokio::test(flavor = "multi_thread")]
8237 async fn test_delete_line_preserves_multiline_vertical_constraint() {
8238 let initial_source = "\
8239sketch(on = XY) {
8240 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8241 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8242 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8243 vertical([line1.end, line2.start, line3.start])
8244}
8245";
8246
8247 let program = Program::parse(initial_source).unwrap().0.unwrap();
8248
8249 let mut frontend = FrontendState::new();
8250
8251 let mock_ctx = ExecutorContext::new_mock(None).await;
8252 let version = Version(0);
8253
8254 frontend.program = program.clone();
8255 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8256 frontend.update_state_after_exec(outcome, true);
8257 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8258 let sketch_id = sketch_object.id;
8259 let sketch = expect_sketch(sketch_object);
8260 let line1_id = *sketch.segments.get(2).unwrap();
8261
8262 let (src_delta, scene_delta) = frontend
8263 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8264 .await
8265 .unwrap();
8266 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8267 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8268 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8269 assert!(
8270 src_delta.text.contains("vertical([line2.start, line3.start])"),
8271 "{}",
8272 src_delta.text
8273 );
8274
8275 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8276 let sketch = expect_sketch(sketch_object);
8277 assert_eq!(sketch.constraints.len(), 1);
8278
8279 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8280 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8281 panic!("Expected constraint object");
8282 };
8283 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8284 panic!("Expected vertical points constraint");
8285 };
8286 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8287 assert_eq!(*points, remaining_points);
8288
8289 mock_ctx.close().await;
8290 }
8291
8292 #[tokio::test(flavor = "multi_thread")]
8293 async fn test_delete_line_preserves_multiline_coincident_constraint() {
8294 let initial_source = "\
8295sketch(on = XY) {
8296 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8297 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8298 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8299 coincident([line1.end, line2.start, line3.start])
8300}
8301";
8302
8303 let program = Program::parse(initial_source).unwrap().0.unwrap();
8304
8305 let mut frontend = FrontendState::new();
8306
8307 let mock_ctx = ExecutorContext::new_mock(None).await;
8308 let version = Version(0);
8309
8310 frontend.program = program.clone();
8311 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8312 frontend.update_state_after_exec(outcome, true);
8313 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8314 let sketch_id = sketch_object.id;
8315 let sketch = expect_sketch(sketch_object);
8316 let line1_id = *sketch.segments.get(2).unwrap();
8317
8318 let (src_delta, scene_delta) = frontend
8319 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8320 .await
8321 .unwrap();
8322 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8323 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8324 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8325 assert!(
8326 src_delta.text.contains("coincident([line2.start, line3.start])"),
8327 "{}",
8328 src_delta.text
8329 );
8330
8331 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8332 let sketch = expect_sketch(sketch_object);
8333 assert_eq!(sketch.constraints.len(), 1);
8334
8335 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8336 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8337 panic!("Expected constraint object");
8338 };
8339 let Constraint::Coincident(coincident) = constraint else {
8340 panic!("Expected coincident constraint");
8341 };
8342 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8343 assert_eq!(coincident.segments, remaining_segments);
8344
8345 mock_ctx.close().await;
8346 }
8347
8348 #[tokio::test(flavor = "multi_thread")]
8349 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8350 let initial_source = "\
8351sketch(on = XY) {
8352 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8353 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8354 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8355 equalLength([line1, line2, line3])
8356}
8357";
8358
8359 let program = Program::parse(initial_source).unwrap().0.unwrap();
8360
8361 let mut frontend = FrontendState::new();
8362
8363 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8364 let mock_ctx = ExecutorContext::new_mock(None).await;
8365 let version = Version(0);
8366
8367 frontend.hack_set_program(&ctx, program).await.unwrap();
8368 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8369 let sketch_id = sketch_object.id;
8370 let sketch = expect_sketch(sketch_object);
8371 let line2_id = *sketch.segments.get(5).unwrap();
8372 let line3_id = *sketch.segments.get(8).unwrap();
8373
8374 let (src_delta, scene_delta) = frontend
8375 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8376 .await
8377 .unwrap();
8378 assert_eq!(
8379 src_delta.text.as_str(),
8380 "\
8381sketch(on = XY) {
8382 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8383}
8384"
8385 );
8386
8387 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8388 let sketch = expect_sketch(sketch_object);
8389 assert!(sketch.constraints.is_empty());
8390
8391 ctx.close().await;
8392 mock_ctx.close().await;
8393 }
8394
8395 #[tokio::test(flavor = "multi_thread")]
8396 async fn test_delete_line_preserves_multiline_parallel_constraint() {
8397 let initial_source = "\
8398sketch(on = XY) {
8399 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8400 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8401 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8402 parallel([line1, line2, line3])
8403}
8404";
8405
8406 let program = Program::parse(initial_source).unwrap().0.unwrap();
8407
8408 let mut frontend = FrontendState::new();
8409
8410 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8411 let mock_ctx = ExecutorContext::new_mock(None).await;
8412 let version = Version(0);
8413
8414 frontend.hack_set_program(&ctx, program).await.unwrap();
8415 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8416 let sketch_id = sketch_object.id;
8417 let sketch = expect_sketch(sketch_object);
8418 let line3_id = *sketch.segments.get(8).unwrap();
8419
8420 let (src_delta, scene_delta) = frontend
8421 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8422 .await
8423 .unwrap();
8424 assert_eq!(
8425 src_delta.text.as_str(),
8426 "\
8427sketch(on = XY) {
8428 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8429 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8430 parallel([line1, line2])
8431}
8432"
8433 );
8434
8435 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8436 let sketch = expect_sketch(sketch_object);
8437 assert_eq!(sketch.constraints.len(), 1);
8438
8439 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8440 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8441 panic!("Expected constraint object");
8442 };
8443 let Constraint::Parallel(parallel) = constraint else {
8444 panic!("Expected parallel constraint");
8445 };
8446 assert_eq!(parallel.lines.len(), 2);
8447
8448 ctx.close().await;
8449 mock_ctx.close().await;
8450 }
8451
8452 #[tokio::test(flavor = "multi_thread")]
8453 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8454 let initial_source = "\
8455sketch(on = XY) {
8456 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8457 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8458 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8459 parallel([line1, line2, line3])
8460}
8461";
8462
8463 let program = Program::parse(initial_source).unwrap().0.unwrap();
8464
8465 let mut frontend = FrontendState::new();
8466
8467 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8468 let mock_ctx = ExecutorContext::new_mock(None).await;
8469 let version = Version(0);
8470
8471 frontend.hack_set_program(&ctx, program).await.unwrap();
8472 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8473 let sketch_id = sketch_object.id;
8474 let sketch = expect_sketch(sketch_object);
8475 let line2_id = *sketch.segments.get(5).unwrap();
8476 let line3_id = *sketch.segments.get(8).unwrap();
8477
8478 let (src_delta, scene_delta) = frontend
8479 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8480 .await
8481 .unwrap();
8482 assert_eq!(
8483 src_delta.text.as_str(),
8484 "\
8485sketch(on = XY) {
8486 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8487}
8488"
8489 );
8490
8491 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8492 let sketch = expect_sketch(sketch_object);
8493 assert!(sketch.constraints.is_empty());
8494
8495 ctx.close().await;
8496 mock_ctx.close().await;
8497 }
8498
8499 #[tokio::test(flavor = "multi_thread")]
8500 async fn test_delete_line_line_coincident_constraint() {
8501 let initial_source = "\
8502sketch(on = XY) {
8503 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8504 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8505 coincident([line1, line2])
8506}
8507";
8508
8509 let program = Program::parse(initial_source).unwrap().0.unwrap();
8510
8511 let mut frontend = FrontendState::new();
8512
8513 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8514 let mock_ctx = ExecutorContext::new_mock(None).await;
8515 let version = Version(0);
8516
8517 frontend.hack_set_program(&ctx, program).await.unwrap();
8518 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8519 let sketch_id = sketch_object.id;
8520 let sketch = expect_sketch(sketch_object);
8521
8522 let coincident_id = *sketch.constraints.first().unwrap();
8523
8524 let (src_delta, scene_delta) = frontend
8525 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8526 .await
8527 .unwrap();
8528 assert_eq!(
8529 src_delta.text.as_str(),
8530 "\
8531sketch(on = XY) {
8532 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8533 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8534}
8535"
8536 );
8537 assert_eq!(scene_delta.new_objects, vec![]);
8538 assert_eq!(scene_delta.new_graph.objects.len(), 8);
8539
8540 ctx.close().await;
8541 mock_ctx.close().await;
8542 }
8543
8544 #[tokio::test(flavor = "multi_thread")]
8545 async fn test_two_points_coincident() {
8546 let initial_source = "\
8547sketch(on = XY) {
8548 point1 = point(at = [var 1, var 2])
8549 point(at = [3, 4])
8550}
8551";
8552
8553 let program = Program::parse(initial_source).unwrap().0.unwrap();
8554
8555 let mut frontend = FrontendState::new();
8556
8557 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8558 let mock_ctx = ExecutorContext::new_mock(None).await;
8559 let version = Version(0);
8560
8561 frontend.hack_set_program(&ctx, program).await.unwrap();
8562 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8563 let sketch_id = sketch_object.id;
8564 let sketch = expect_sketch(sketch_object);
8565 let point0_id = *sketch.segments.first().unwrap();
8566 let point1_id = *sketch.segments.get(1).unwrap();
8567
8568 let constraint = Constraint::Coincident(Coincident {
8569 segments: vec![point0_id.into(), point1_id.into()],
8570 });
8571 let (src_delta, scene_delta) = frontend
8572 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8573 .await
8574 .unwrap();
8575 assert_eq!(
8576 src_delta.text.as_str(),
8577 "\
8578sketch(on = XY) {
8579 point1 = point(at = [var 1, var 2])
8580 point2 = point(at = [3, 4])
8581 coincident([point1, point2])
8582}
8583"
8584 );
8585 assert_eq!(
8586 scene_delta.new_graph.objects.len(),
8587 5,
8588 "{:#?}",
8589 scene_delta.new_graph.objects
8590 );
8591
8592 ctx.close().await;
8593 mock_ctx.close().await;
8594 }
8595
8596 #[tokio::test(flavor = "multi_thread")]
8597 async fn test_three_points_coincident() {
8598 let initial_source = "\
8599sketch(on = XY) {
8600 point1 = point(at = [var 1, var 2])
8601 point(at = [var 3, var 4])
8602 point(at = [var 5, var 6])
8603}
8604";
8605
8606 let program = Program::parse(initial_source).unwrap().0.unwrap();
8607
8608 let mut frontend = FrontendState::new();
8609
8610 let mock_ctx = ExecutorContext::new_mock(None).await;
8611 let version = Version(0);
8612
8613 frontend.program = program.clone();
8614 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8615 frontend.update_state_after_exec(outcome, true);
8616 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8617 let sketch_id = sketch_object.id;
8618 let sketch = expect_sketch(sketch_object);
8619 let segments = sketch
8620 .segments
8621 .iter()
8622 .take(3)
8623 .copied()
8624 .map(Into::into)
8625 .collect::<Vec<ConstraintSegment>>();
8626
8627 let constraint = Constraint::Coincident(Coincident {
8628 segments: segments.clone(),
8629 });
8630 let (src_delta, scene_delta) = frontend
8631 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8632 .await
8633 .unwrap();
8634 assert_eq!(
8635 src_delta.text.as_str(),
8636 "\
8637sketch(on = XY) {
8638 point1 = point(at = [var 1, var 2])
8639 point2 = point(at = [var 3, var 4])
8640 point3 = point(at = [var 5, var 6])
8641 coincident([point1, point2, point3])
8642}
8643"
8644 );
8645
8646 let constraint_object = scene_delta
8647 .new_graph
8648 .objects
8649 .iter()
8650 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8651 .unwrap();
8652
8653 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8654 panic!("expected a constraint object");
8655 };
8656
8657 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8658
8659 mock_ctx.close().await;
8660 }
8661
8662 #[tokio::test(flavor = "multi_thread")]
8663 async fn test_source_with_three_point_coincident_tracks_all_segments() {
8664 let initial_source = "\
8665sketch(on = XY) {
8666 point1 = point(at = [var 1, var 2])
8667 point2 = point(at = [var 3, var 4])
8668 point3 = point(at = [var 5, var 6])
8669 coincident([point1, point2, point3])
8670}
8671";
8672
8673 let program = Program::parse(initial_source).unwrap().0.unwrap();
8674
8675 let mut frontend = FrontendState::new();
8676
8677 let ctx = ExecutorContext::new_mock(None).await;
8678 frontend.program = program.clone();
8679 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8680 frontend.update_state_after_exec(outcome, true);
8681
8682 let constraint_object = frontend
8683 .scene_graph
8684 .objects
8685 .iter()
8686 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8687 .unwrap();
8688 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8689 panic!("expected a constraint object");
8690 };
8691
8692 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8693 let sketch = expect_sketch(sketch_object);
8694 let expected_segments = sketch
8695 .segments
8696 .iter()
8697 .take(3)
8698 .copied()
8699 .map(Into::into)
8700 .collect::<Vec<ConstraintSegment>>();
8701
8702 assert_eq!(
8703 constraint,
8704 &Constraint::Coincident(Coincident {
8705 segments: expected_segments,
8706 })
8707 );
8708
8709 ctx.close().await;
8710 }
8711
8712 #[tokio::test(flavor = "multi_thread")]
8713 async fn test_point_origin_coincident_preserves_order() {
8714 let initial_source = "\
8715sketch(on = XY) {
8716 point(at = [var 1, var 2])
8717}
8718";
8719
8720 for (origin_first, expected_source) in [
8721 (
8722 true,
8723 "\
8724sketch(on = XY) {
8725 point1 = point(at = [var 1, var 2])
8726 coincident([ORIGIN, point1])
8727}
8728",
8729 ),
8730 (
8731 false,
8732 "\
8733sketch(on = XY) {
8734 point1 = point(at = [var 1, var 2])
8735 coincident([point1, ORIGIN])
8736}
8737",
8738 ),
8739 ] {
8740 let program = Program::parse(initial_source).unwrap().0.unwrap();
8741
8742 let mut frontend = FrontendState::new();
8743
8744 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8745 let mock_ctx = ExecutorContext::new_mock(None).await;
8746 let version = Version(0);
8747
8748 frontend.hack_set_program(&ctx, program).await.unwrap();
8749 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8750 let sketch_id = sketch_object.id;
8751 let sketch = expect_sketch(sketch_object);
8752 let point_id = *sketch.segments.first().unwrap();
8753
8754 let segments = if origin_first {
8755 vec![ConstraintSegment::ORIGIN, point_id.into()]
8756 } else {
8757 vec![point_id.into(), ConstraintSegment::ORIGIN]
8758 };
8759 let constraint = Constraint::Coincident(Coincident {
8760 segments: segments.clone(),
8761 });
8762 let (src_delta, scene_delta) = frontend
8763 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8764 .await
8765 .unwrap();
8766 assert_eq!(src_delta.text.as_str(), expected_source);
8767
8768 let constraint_object = scene_delta
8769 .new_graph
8770 .objects
8771 .iter()
8772 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8773 .unwrap();
8774
8775 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8776 panic!("expected a constraint object");
8777 };
8778
8779 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8780
8781 ctx.close().await;
8782 mock_ctx.close().await;
8783 }
8784 }
8785
8786 #[tokio::test(flavor = "multi_thread")]
8787 async fn test_coincident_of_line_end_points() {
8788 let initial_source = "\
8789sketch(on = XY) {
8790 line(start = [var 1, var 2], end = [var 3, var 4])
8791 line(start = [var 5, var 6], end = [var 7, var 8])
8792}
8793";
8794
8795 let program = Program::parse(initial_source).unwrap().0.unwrap();
8796
8797 let mut frontend = FrontendState::new();
8798
8799 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8800 let mock_ctx = ExecutorContext::new_mock(None).await;
8801 let version = Version(0);
8802
8803 frontend.hack_set_program(&ctx, program).await.unwrap();
8804 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8805 let sketch_id = sketch_object.id;
8806 let sketch = expect_sketch(sketch_object);
8807 let point0_id = *sketch.segments.get(1).unwrap();
8808 let point1_id = *sketch.segments.get(3).unwrap();
8809
8810 let constraint = Constraint::Coincident(Coincident {
8811 segments: vec![point0_id.into(), point1_id.into()],
8812 });
8813 let (src_delta, scene_delta) = frontend
8814 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8815 .await
8816 .unwrap();
8817 assert_eq!(
8818 src_delta.text.as_str(),
8819 "\
8820sketch(on = XY) {
8821 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8822 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8823 coincident([line1.end, line2.start])
8824}
8825"
8826 );
8827 assert_eq!(
8828 scene_delta.new_graph.objects.len(),
8829 9,
8830 "{:#?}",
8831 scene_delta.new_graph.objects
8832 );
8833
8834 ctx.close().await;
8835 mock_ctx.close().await;
8836 }
8837
8838 #[tokio::test(flavor = "multi_thread")]
8839 async fn test_coincident_of_line_point_and_circle_segment() {
8840 let initial_source = "\
8841sketch(on = XY) {
8842 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8843 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8844}
8845";
8846 let program = Program::parse(initial_source).unwrap().0.unwrap();
8847 let mut frontend = FrontendState::new();
8848
8849 let mock_ctx = ExecutorContext::new_mock(None).await;
8850 let version = Version(0);
8851
8852 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8853 frontend.program = program;
8854 frontend.update_state_after_exec(outcome, true);
8855 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
8856 let sketch_id = sketch_object.id;
8857 let sketch = expect_sketch(sketch_object);
8858
8859 let circle_id = sketch
8860 .segments
8861 .iter()
8862 .copied()
8863 .find(|seg_id| {
8864 matches!(
8865 &frontend.scene_graph.objects[seg_id.0].kind,
8866 ObjectKind::Segment {
8867 segment: Segment::Circle(_)
8868 }
8869 )
8870 })
8871 .expect("Expected a circle segment in sketch");
8872 let line_id = sketch
8873 .segments
8874 .iter()
8875 .copied()
8876 .find(|seg_id| {
8877 matches!(
8878 &frontend.scene_graph.objects[seg_id.0].kind,
8879 ObjectKind::Segment {
8880 segment: Segment::Line(_)
8881 }
8882 )
8883 })
8884 .expect("Expected a line segment in sketch");
8885
8886 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
8887 ObjectKind::Segment {
8888 segment: Segment::Line(line),
8889 } => line.start,
8890 _ => panic!("Expected line segment object"),
8891 };
8892
8893 let constraint = Constraint::Coincident(Coincident {
8894 segments: vec![line_start_point_id.into(), circle_id.into()],
8895 });
8896 let (src_delta, _scene_delta) = frontend
8897 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8898 .await
8899 .unwrap();
8900 assert_eq!(
8901 src_delta.text.as_str(),
8902 "\
8903sketch(on = XY) {
8904 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8905 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8906 coincident([line1.start, circle1])
8907}
8908"
8909 );
8910
8911 mock_ctx.close().await;
8912 }
8913
8914 #[tokio::test(flavor = "multi_thread")]
8915 async fn test_invalid_coincident_arc_and_line_preserves_state() {
8916 let program = Program::empty();
8924
8925 let mut frontend = FrontendState::new();
8926 frontend.program = program;
8927
8928 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8929 let mock_ctx = ExecutorContext::new_mock(None).await;
8930 let version = Version(0);
8931
8932 let sketch_args = SketchCtor {
8933 on: Plane::Default(PlaneName::Xy),
8934 };
8935 let (_src_delta, _scene_delta, sketch_id) = frontend
8936 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8937 .await
8938 .unwrap();
8939
8940 let arc_ctor = ArcCtor {
8942 start: Point2d {
8943 x: Expr::Var(Number {
8944 value: 0.0,
8945 units: NumericSuffix::Mm,
8946 }),
8947 y: Expr::Var(Number {
8948 value: 0.0,
8949 units: NumericSuffix::Mm,
8950 }),
8951 },
8952 end: Point2d {
8953 x: Expr::Var(Number {
8954 value: 10.0,
8955 units: NumericSuffix::Mm,
8956 }),
8957 y: Expr::Var(Number {
8958 value: 10.0,
8959 units: NumericSuffix::Mm,
8960 }),
8961 },
8962 center: Point2d {
8963 x: Expr::Var(Number {
8964 value: 10.0,
8965 units: NumericSuffix::Mm,
8966 }),
8967 y: Expr::Var(Number {
8968 value: 0.0,
8969 units: NumericSuffix::Mm,
8970 }),
8971 },
8972 construction: None,
8973 };
8974 let (_src_delta, scene_delta) = frontend
8975 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
8976 .await
8977 .unwrap();
8978 let arc_id = *scene_delta.new_objects.last().unwrap();
8980
8981 let line_ctor = LineCtor {
8983 start: Point2d {
8984 x: Expr::Var(Number {
8985 value: 20.0,
8986 units: NumericSuffix::Mm,
8987 }),
8988 y: Expr::Var(Number {
8989 value: 0.0,
8990 units: NumericSuffix::Mm,
8991 }),
8992 },
8993 end: Point2d {
8994 x: Expr::Var(Number {
8995 value: 30.0,
8996 units: NumericSuffix::Mm,
8997 }),
8998 y: Expr::Var(Number {
8999 value: 10.0,
9000 units: NumericSuffix::Mm,
9001 }),
9002 },
9003 construction: None,
9004 };
9005 let (_src_delta, scene_delta) = frontend
9006 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
9007 .await
9008 .unwrap();
9009 let line_id = *scene_delta.new_objects.last().unwrap();
9011
9012 let constraint = Constraint::Coincident(Coincident {
9015 segments: vec![arc_id.into(), line_id.into()],
9016 });
9017 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
9018
9019 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
9021
9022 let sketch_object_after =
9025 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
9026 let sketch_after = expect_sketch(sketch_object_after);
9027
9028 assert!(
9030 sketch_after.segments.contains(&arc_id),
9031 "Arc segment should still exist after failed constraint"
9032 );
9033 assert!(
9034 sketch_after.segments.contains(&line_id),
9035 "Line segment should still exist after failed constraint"
9036 );
9037
9038 let arc_obj = frontend
9040 .scene_graph
9041 .objects
9042 .get(arc_id.0)
9043 .expect("Arc object should still be accessible");
9044 let line_obj = frontend
9045 .scene_graph
9046 .objects
9047 .get(line_id.0)
9048 .expect("Line object should still be accessible");
9049
9050 match &arc_obj.kind {
9053 ObjectKind::Segment {
9054 segment: Segment::Arc(_),
9055 } => {}
9056 _ => panic!("Arc object should still be an arc segment"),
9057 }
9058 match &line_obj.kind {
9059 ObjectKind::Segment {
9060 segment: Segment::Line(_),
9061 } => {}
9062 _ => panic!("Line object should still be a line segment"),
9063 }
9064
9065 ctx.close().await;
9066 mock_ctx.close().await;
9067 }
9068
9069 #[tokio::test(flavor = "multi_thread")]
9070 async fn test_distance_two_points() {
9071 let initial_source = "\
9072sketch(on = XY) {
9073 point(at = [var 1, var 2])
9074 point(at = [var 3, var 4])
9075}
9076";
9077
9078 let program = Program::parse(initial_source).unwrap().0.unwrap();
9079
9080 let mut frontend = FrontendState::new();
9081
9082 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9083 let mock_ctx = ExecutorContext::new_mock(None).await;
9084 let version = Version(0);
9085
9086 frontend.hack_set_program(&ctx, program).await.unwrap();
9087 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9088 let sketch_id = sketch_object.id;
9089 let sketch = expect_sketch(sketch_object);
9090 let point0_id = *sketch.segments.first().unwrap();
9091 let point1_id = *sketch.segments.get(1).unwrap();
9092
9093 let constraint = Constraint::Distance(Distance {
9094 points: vec![point0_id.into(), point1_id.into()],
9095 distance: Number {
9096 value: 2.0,
9097 units: NumericSuffix::Mm,
9098 },
9099 label_position: None,
9100 source: Default::default(),
9101 });
9102 let (src_delta, scene_delta) = frontend
9103 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9104 .await
9105 .unwrap();
9106 assert_eq!(
9107 src_delta.text.as_str(),
9108 "\
9110sketch(on = XY) {
9111 point1 = point(at = [var 1, var 2])
9112 point2 = point(at = [var 3, var 4])
9113 distance([point1, point2]) == 2mm
9114}
9115"
9116 );
9117 assert_eq!(
9118 scene_delta.new_graph.objects.len(),
9119 5,
9120 "{:#?}",
9121 scene_delta.new_graph.objects
9122 );
9123
9124 ctx.close().await;
9125 mock_ctx.close().await;
9126 }
9127
9128 #[tokio::test(flavor = "multi_thread")]
9129 async fn test_distance_two_points_with_label() {
9130 let initial_source = "\
9131sketch(on = XY) {
9132 point(at = [var 1, var 2])
9133 point(at = [var 3, var 4])
9134}
9135";
9136
9137 let program = Program::parse(initial_source).unwrap().0.unwrap();
9138
9139 let mut frontend = FrontendState::new();
9140
9141 let mock_ctx = ExecutorContext::new_mock(None).await;
9142 let version = Version(0);
9143
9144 frontend.program = program.clone();
9145 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9146 frontend.update_state_after_exec(outcome, true);
9147 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9148 let sketch_id = sketch_object.id;
9149 let sketch = expect_sketch(sketch_object);
9150 let point0_id = *sketch.segments.first().unwrap();
9151 let point1_id = *sketch.segments.get(1).unwrap();
9152
9153 let label_position = Point2d {
9154 x: Number {
9155 value: 10.0,
9156 units: NumericSuffix::Mm,
9157 },
9158 y: Number {
9159 value: 11.0,
9160 units: NumericSuffix::Mm,
9161 },
9162 };
9163 let constraint = Constraint::Distance(Distance {
9164 points: vec![point0_id.into(), point1_id.into()],
9165 distance: Number {
9166 value: 2.0,
9167 units: NumericSuffix::Mm,
9168 },
9169 label_position: Some(label_position.clone()),
9170 source: Default::default(),
9171 });
9172 let (src_delta, scene_delta) = frontend
9173 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9174 .await
9175 .unwrap();
9176 assert_eq!(
9177 src_delta.text.as_str(),
9178 "\
9179sketch(on = XY) {
9180 point1 = point(at = [var 1, var 2])
9181 point2 = point(at = [var 3, var 4])
9182 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9183}
9184"
9185 );
9186
9187 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9188 let sketch = expect_sketch(sketch_object);
9189 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9190 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9191 panic!("Expected constraint object");
9192 };
9193 let Constraint::Distance(distance) = constraint else {
9194 panic!("Expected distance constraint");
9195 };
9196 assert_eq!(distance.label_position, Some(label_position));
9197
9198 mock_ctx.close().await;
9199 }
9200
9201 #[tokio::test(flavor = "multi_thread")]
9202 async fn test_edit_distance_constraint_label_position() {
9203 let initial_source = "\
9204sketch(on = XY) {
9205 point(at = [var 1, var 2])
9206 point(at = [var 3, var 2])
9207}
9208";
9209
9210 let program = Program::parse(initial_source).unwrap().0.unwrap();
9211
9212 let mut frontend = FrontendState::new();
9213
9214 let mock_ctx = ExecutorContext::new_mock(None).await;
9215 let version = Version(0);
9216
9217 frontend.program = program.clone();
9218 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9219 frontend.update_state_after_exec(outcome, true);
9220 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9221 let sketch_id = sketch_object.id;
9222 let sketch = expect_sketch(sketch_object);
9223 let point0_id = *sketch.segments.first().unwrap();
9224 let point1_id = *sketch.segments.get(1).unwrap();
9225
9226 let constraint = Constraint::Distance(Distance {
9227 points: vec![point0_id.into(), point1_id.into()],
9228 distance: Number {
9229 value: 2.0,
9230 units: NumericSuffix::Mm,
9231 },
9232 label_position: None,
9233 source: Default::default(),
9234 });
9235 let (_, scene_delta) = frontend
9236 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9237 .await
9238 .unwrap();
9239 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9240 let sketch = expect_sketch(sketch_object);
9241 let constraint_id = sketch.constraints[0];
9242 let label_position = Point2d {
9243 x: Number {
9244 value: 10.0,
9245 units: NumericSuffix::Mm,
9246 },
9247 y: Number {
9248 value: 11.0,
9249 units: NumericSuffix::Mm,
9250 },
9251 };
9252
9253 let (src_delta, scene_delta) = frontend
9254 .edit_distance_constraint_label_position(
9255 &mock_ctx,
9256 version,
9257 sketch_id,
9258 constraint_id,
9259 label_position.clone(),
9260 vec![],
9261 )
9262 .await
9263 .unwrap();
9264 assert_eq!(
9265 src_delta.text.as_str(),
9266 "\
9267sketch(on = XY) {
9268 point1 = point(at = [var 1mm, var 2mm])
9269 point2 = point(at = [var 3mm, var 2mm])
9270 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9271}
9272"
9273 );
9274
9275 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9276 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9277 panic!("Expected constraint object");
9278 };
9279 let Constraint::Distance(distance) = constraint else {
9280 panic!("Expected distance constraint");
9281 };
9282 assert_eq!(distance.label_position, Some(label_position));
9283
9284 mock_ctx.close().await;
9285 }
9286
9287 #[tokio::test(flavor = "multi_thread")]
9288 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9289 let initial_source = "\
9290sketch(on = XY) {
9291 point1 = point(at = [var 0mm, var 0mm])
9292 point2 = point(at = [var 10mm, var 0mm])
9293 distance([point1, point2]) == 5mm
9294}
9295";
9296
9297 let program = Program::parse(initial_source).unwrap().0.unwrap();
9298 let mut frontend = FrontendState::new();
9299 let mock_ctx = ExecutorContext::new_mock(None).await;
9300 let version = Version(0);
9301
9302 frontend.program = program.clone();
9303 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9304 frontend.update_state_after_exec(outcome, true);
9305 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9306 let sketch_id = sketch_object.id;
9307 let sketch = expect_sketch(sketch_object);
9308 let point0_id = sketch.segments[0];
9309 let point1_id = sketch.segments[1];
9310 let constraint_id = sketch.constraints[0];
9311
9312 let edited_segments = vec![ExistingSegmentCtor {
9313 id: point0_id,
9314 ctor: SegmentCtor::Point(PointCtor {
9315 position: Point2d {
9316 x: Expr::Var(Number {
9317 value: 2.0,
9318 units: NumericSuffix::Mm,
9319 }),
9320 y: Expr::Var(Number {
9321 value: 1.0,
9322 units: NumericSuffix::Mm,
9323 }),
9324 },
9325 }),
9326 }];
9327 let (_, scene_delta) = frontend
9328 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9329 .await
9330 .unwrap();
9331 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9332 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9333
9334 let label_position = Point2d {
9335 x: Number {
9336 value: 3.0,
9337 units: NumericSuffix::Mm,
9338 },
9339 y: Number {
9340 value: 4.0,
9341 units: NumericSuffix::Mm,
9342 },
9343 };
9344 let (_, scene_delta) = frontend
9345 .edit_distance_constraint_label_position(
9346 &mock_ctx,
9347 version,
9348 sketch_id,
9349 constraint_id,
9350 label_position,
9351 vec![point0_id],
9352 )
9353 .await
9354 .unwrap();
9355
9356 assert_point_position_close(
9357 point_position(&scene_delta.new_graph, point0_id),
9358 point0_after_segment_edit,
9359 );
9360 assert_point_position_close(
9361 point_position(&scene_delta.new_graph, point1_id),
9362 point1_after_segment_edit,
9363 );
9364
9365 mock_ctx.close().await;
9366 }
9367
9368 #[tokio::test(flavor = "multi_thread")]
9369 async fn test_horizontal_distance_two_points() {
9370 let initial_source = "\
9371sketch(on = XY) {
9372 point(at = [var 1, var 2])
9373 point(at = [var 3, var 4])
9374}
9375";
9376
9377 let program = Program::parse(initial_source).unwrap().0.unwrap();
9378
9379 let mut frontend = FrontendState::new();
9380
9381 let mock_ctx = ExecutorContext::new_mock(None).await;
9382 let version = Version(0);
9383
9384 frontend.program = program.clone();
9385 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9386 frontend.update_state_after_exec(outcome, true);
9387 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9388 let sketch_id = sketch_object.id;
9389 let sketch = expect_sketch(sketch_object);
9390 let point0_id = *sketch.segments.first().unwrap();
9391 let point1_id = *sketch.segments.get(1).unwrap();
9392 let label_position = Point2d {
9393 x: Number {
9394 value: 10.0,
9395 units: NumericSuffix::Mm,
9396 },
9397 y: Number {
9398 value: 11.0,
9399 units: NumericSuffix::Mm,
9400 },
9401 };
9402
9403 let constraint = Constraint::HorizontalDistance(Distance {
9404 points: vec![point0_id.into(), point1_id.into()],
9405 distance: Number {
9406 value: 2.0,
9407 units: NumericSuffix::Mm,
9408 },
9409 label_position: Some(label_position.clone()),
9410 source: Default::default(),
9411 });
9412 let (src_delta, scene_delta) = frontend
9413 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9414 .await
9415 .unwrap();
9416 assert_eq!(
9417 src_delta.text.as_str(),
9418 "\
9420sketch(on = XY) {
9421 point1 = point(at = [var 1, var 2])
9422 point2 = point(at = [var 3, var 4])
9423 horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9424}
9425"
9426 );
9427 assert_eq!(
9428 scene_delta.new_graph.objects.len(),
9429 5,
9430 "{:#?}",
9431 scene_delta.new_graph.objects
9432 );
9433 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9434 let sketch = expect_sketch(sketch_object);
9435 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9436 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9437 panic!("Expected constraint object");
9438 };
9439 let Constraint::HorizontalDistance(distance) = constraint else {
9440 panic!("Expected horizontal distance constraint");
9441 };
9442 assert_eq!(distance.label_position, Some(label_position));
9443
9444 mock_ctx.close().await;
9445 }
9446
9447 #[tokio::test(flavor = "multi_thread")]
9448 async fn test_radius_single_arc_segment() {
9449 let initial_source = "\
9450sketch(on = XY) {
9451 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9452}
9453";
9454
9455 let program = Program::parse(initial_source).unwrap().0.unwrap();
9456
9457 let mut frontend = FrontendState::new();
9458
9459 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9460 let mock_ctx = ExecutorContext::new_mock(None).await;
9461 let version = Version(0);
9462
9463 frontend.hack_set_program(&ctx, program).await.unwrap();
9464 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9465 let sketch_id = sketch_object.id;
9466 let sketch = expect_sketch(sketch_object);
9467 let arc_id = sketch
9469 .segments
9470 .iter()
9471 .find(|&seg_id| {
9472 let obj = frontend.scene_graph.objects.get(seg_id.0);
9473 matches!(
9474 obj.map(|o| &o.kind),
9475 Some(ObjectKind::Segment {
9476 segment: Segment::Arc(_)
9477 })
9478 )
9479 })
9480 .unwrap();
9481
9482 let constraint = Constraint::Radius(Radius {
9483 arc: *arc_id,
9484 radius: Number {
9485 value: 5.0,
9486 units: NumericSuffix::Mm,
9487 },
9488 label_position: None,
9489 source: Default::default(),
9490 });
9491 let (src_delta, scene_delta) = frontend
9492 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9493 .await
9494 .unwrap();
9495 assert_eq!(
9496 src_delta.text.as_str(),
9497 "\
9499sketch(on = XY) {
9500 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9501 radius(arc1) == 5mm
9502}
9503"
9504 );
9505 assert_eq!(
9506 scene_delta.new_graph.objects.len(),
9507 7, "{:#?}",
9509 scene_delta.new_graph.objects
9510 );
9511
9512 ctx.close().await;
9513 mock_ctx.close().await;
9514 }
9515
9516 #[tokio::test(flavor = "multi_thread")]
9517 async fn test_radius_single_arc_segment_with_label_position() {
9518 let initial_source = "\
9519sketch(on = XY) {
9520 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9521}
9522";
9523
9524 let program = Program::parse(initial_source).unwrap().0.unwrap();
9525 let mut frontend = FrontendState::new();
9526 let mock_ctx = ExecutorContext::new_mock(None).await;
9527 let version = Version(0);
9528
9529 frontend.program = program.clone();
9530 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9531 frontend.update_state_after_exec(outcome, true);
9532 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9533 let sketch_id = sketch_object.id;
9534 let sketch = expect_sketch(sketch_object);
9535 let arc_id = sketch
9536 .segments
9537 .iter()
9538 .find(|&seg_id| {
9539 let obj = frontend.scene_graph.objects.get(seg_id.0);
9540 matches!(
9541 obj.map(|o| &o.kind),
9542 Some(ObjectKind::Segment {
9543 segment: Segment::Arc(_)
9544 })
9545 )
9546 })
9547 .unwrap();
9548
9549 let label_position = Point2d {
9550 x: Number {
9551 value: 10.0,
9552 units: NumericSuffix::Mm,
9553 },
9554 y: Number {
9555 value: 11.0,
9556 units: NumericSuffix::Mm,
9557 },
9558 };
9559 let constraint = Constraint::Radius(Radius {
9560 arc: *arc_id,
9561 radius: Number {
9562 value: 5.0,
9563 units: NumericSuffix::Mm,
9564 },
9565 label_position: Some(label_position.clone()),
9566 source: Default::default(),
9567 });
9568 let (src_delta, scene_delta) = frontend
9569 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9570 .await
9571 .unwrap();
9572 assert_eq!(
9573 src_delta.text.as_str(),
9574 "\
9575sketch(on = XY) {
9576 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9577 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
9578}
9579"
9580 );
9581
9582 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9583 let sketch = expect_sketch(sketch_object);
9584 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9585 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9586 panic!("Expected constraint object");
9587 };
9588 let Constraint::Radius(radius) = constraint else {
9589 panic!("Expected radius constraint");
9590 };
9591 assert_eq!(radius.label_position, Some(label_position));
9592
9593 mock_ctx.close().await;
9594 }
9595
9596 #[tokio::test(flavor = "multi_thread")]
9597 async fn test_edit_radius_constraint_label_position() {
9598 let initial_source = "\
9599sketch(on = XY) {
9600 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
9601 radius(arc1) == 5mm
9602}
9603";
9604
9605 let program = Program::parse(initial_source).unwrap().0.unwrap();
9606 let mut frontend = FrontendState::new();
9607 let mock_ctx = ExecutorContext::new_mock(None).await;
9608 let version = Version(0);
9609
9610 frontend.program = program.clone();
9611 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9612 frontend.update_state_after_exec(outcome, true);
9613 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9614 let sketch_id = sketch_object.id;
9615 let sketch = expect_sketch(sketch_object);
9616 let constraint_id = sketch.constraints[0];
9617 let label_position = Point2d {
9618 x: Number {
9619 value: 10.0,
9620 units: NumericSuffix::Mm,
9621 },
9622 y: Number {
9623 value: 11.0,
9624 units: NumericSuffix::Mm,
9625 },
9626 };
9627
9628 let (src_delta, scene_delta) = frontend
9629 .edit_distance_constraint_label_position(
9630 &mock_ctx,
9631 version,
9632 sketch_id,
9633 constraint_id,
9634 label_position.clone(),
9635 vec![],
9636 )
9637 .await
9638 .unwrap();
9639 assert_eq!(
9640 src_delta.text.as_str(),
9641 "\
9642sketch(on = XY) {
9643 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
9644 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
9645}
9646"
9647 );
9648
9649 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9650 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9651 panic!("Expected constraint object");
9652 };
9653 let Constraint::Radius(radius) = constraint else {
9654 panic!("Expected radius constraint");
9655 };
9656 assert_eq!(radius.label_position, Some(label_position));
9657
9658 mock_ctx.close().await;
9659 }
9660
9661 #[tokio::test(flavor = "multi_thread")]
9662 async fn test_vertical_distance_two_points() {
9663 let initial_source = "\
9664sketch(on = XY) {
9665 point(at = [var 1, var 2])
9666 point(at = [var 3, var 4])
9667}
9668";
9669
9670 let program = Program::parse(initial_source).unwrap().0.unwrap();
9671
9672 let mut frontend = FrontendState::new();
9673
9674 let mock_ctx = ExecutorContext::new_mock(None).await;
9675 let version = Version(0);
9676
9677 frontend.program = program.clone();
9678 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9679 frontend.update_state_after_exec(outcome, true);
9680 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9681 let sketch_id = sketch_object.id;
9682 let sketch = expect_sketch(sketch_object);
9683 let point0_id = *sketch.segments.first().unwrap();
9684 let point1_id = *sketch.segments.get(1).unwrap();
9685 let label_position = Point2d {
9686 x: Number {
9687 value: 10.0,
9688 units: NumericSuffix::Mm,
9689 },
9690 y: Number {
9691 value: 11.0,
9692 units: NumericSuffix::Mm,
9693 },
9694 };
9695
9696 let constraint = Constraint::VerticalDistance(Distance {
9697 points: vec![point0_id.into(), point1_id.into()],
9698 distance: Number {
9699 value: 2.0,
9700 units: NumericSuffix::Mm,
9701 },
9702 label_position: Some(label_position.clone()),
9703 source: Default::default(),
9704 });
9705 let (src_delta, scene_delta) = frontend
9706 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9707 .await
9708 .unwrap();
9709 assert_eq!(
9710 src_delta.text.as_str(),
9711 "\
9713sketch(on = XY) {
9714 point1 = point(at = [var 1, var 2])
9715 point2 = point(at = [var 3, var 4])
9716 verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9717}
9718"
9719 );
9720 assert_eq!(
9721 scene_delta.new_graph.objects.len(),
9722 5,
9723 "{:#?}",
9724 scene_delta.new_graph.objects
9725 );
9726 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9727 let sketch = expect_sketch(sketch_object);
9728 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9729 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9730 panic!("Expected constraint object");
9731 };
9732 let Constraint::VerticalDistance(distance) = constraint else {
9733 panic!("Expected vertical distance constraint");
9734 };
9735 assert_eq!(distance.label_position, Some(label_position));
9736
9737 mock_ctx.close().await;
9738 }
9739
9740 #[tokio::test(flavor = "multi_thread")]
9741 async fn test_add_fixed_standalone_point() {
9742 let initial_source = "\
9743sketch(on = XY) {
9744 point(at = [var 1, var 2])
9745}
9746";
9747
9748 let program = Program::parse(initial_source).unwrap().0.unwrap();
9749
9750 let mut frontend = FrontendState::new();
9751
9752 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9753 let mock_ctx = ExecutorContext::new_mock(None).await;
9754 let version = Version(0);
9755
9756 frontend.hack_set_program(&ctx, program).await.unwrap();
9757 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9758 let sketch_id = sketch_object.id;
9759 let sketch = expect_sketch(sketch_object);
9760 let point_id = *sketch.segments.first().unwrap();
9761
9762 let (src_delta, scene_delta) = frontend
9763 .add_constraint(
9764 &mock_ctx,
9765 version,
9766 sketch_id,
9767 Constraint::Fixed(Fixed {
9768 points: vec![FixedPoint {
9769 point: point_id,
9770 position: Point2d {
9771 x: Number {
9772 value: 2.0,
9773 units: NumericSuffix::Mm,
9774 },
9775 y: Number {
9776 value: 3.0,
9777 units: NumericSuffix::Mm,
9778 },
9779 },
9780 }],
9781 }),
9782 )
9783 .await
9784 .unwrap();
9785 assert_eq!(
9786 src_delta.text.as_str(),
9787 "\
9788sketch(on = XY) {
9789 point1 = point(at = [var 1, var 2])
9790 fixed([point1, [2mm, 3mm]])
9791}
9792"
9793 );
9794 assert_eq!(
9795 scene_delta.new_graph.objects.len(),
9796 4,
9797 "{:#?}",
9798 scene_delta.new_graph.objects
9799 );
9800
9801 ctx.close().await;
9802 mock_ctx.close().await;
9803 }
9804
9805 #[tokio::test(flavor = "multi_thread")]
9806 async fn test_add_fixed_multiple_points() {
9807 let initial_source = "\
9808sketch(on = XY) {
9809 point(at = [var 1, var 2])
9810 point(at = [var 3, var 4])
9811}
9812";
9813
9814 let program = Program::parse(initial_source).unwrap().0.unwrap();
9815
9816 let mut frontend = FrontendState::new();
9817
9818 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9819 let mock_ctx = ExecutorContext::new_mock(None).await;
9820 let version = Version(0);
9821
9822 frontend.hack_set_program(&ctx, program).await.unwrap();
9823 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9824 let sketch_id = sketch_object.id;
9825 let sketch = expect_sketch(sketch_object);
9826 let point0_id = *sketch.segments.first().unwrap();
9827 let point1_id = *sketch.segments.get(1).unwrap();
9828
9829 let (src_delta, scene_delta) = frontend
9830 .add_constraint(
9831 &mock_ctx,
9832 version,
9833 sketch_id,
9834 Constraint::Fixed(Fixed {
9835 points: vec![
9836 FixedPoint {
9837 point: point0_id,
9838 position: Point2d {
9839 x: Number {
9840 value: 2.0,
9841 units: NumericSuffix::Mm,
9842 },
9843 y: Number {
9844 value: 3.0,
9845 units: NumericSuffix::Mm,
9846 },
9847 },
9848 },
9849 FixedPoint {
9850 point: point1_id,
9851 position: Point2d {
9852 x: Number {
9853 value: 4.0,
9854 units: NumericSuffix::Mm,
9855 },
9856 y: Number {
9857 value: 5.0,
9858 units: NumericSuffix::Mm,
9859 },
9860 },
9861 },
9862 ],
9863 }),
9864 )
9865 .await
9866 .unwrap();
9867 assert_eq!(
9868 src_delta.text.as_str(),
9869 "\
9870sketch(on = XY) {
9871 point1 = point(at = [var 1, var 2])
9872 point2 = point(at = [var 3, var 4])
9873 fixed([point1, [2mm, 3mm]])
9874 fixed([point2, [4mm, 5mm]])
9875}
9876"
9877 );
9878 assert_eq!(
9879 scene_delta.new_graph.objects.len(),
9880 6,
9881 "{:#?}",
9882 scene_delta.new_graph.objects
9883 );
9884
9885 ctx.close().await;
9886 mock_ctx.close().await;
9887 }
9888
9889 #[tokio::test(flavor = "multi_thread")]
9890 async fn test_add_fixed_owned_point() {
9891 let initial_source = "\
9892sketch(on = XY) {
9893 line(start = [var 1, var 2], end = [var 3, var 4])
9894}
9895";
9896
9897 let program = Program::parse(initial_source).unwrap().0.unwrap();
9898
9899 let mut frontend = FrontendState::new();
9900
9901 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9902 let mock_ctx = ExecutorContext::new_mock(None).await;
9903 let version = Version(0);
9904
9905 frontend.hack_set_program(&ctx, program).await.unwrap();
9906 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9907 let sketch_id = sketch_object.id;
9908 let sketch = expect_sketch(sketch_object);
9909 let line_start_id = *sketch.segments.first().unwrap();
9910
9911 let (src_delta, scene_delta) = frontend
9912 .add_constraint(
9913 &mock_ctx,
9914 version,
9915 sketch_id,
9916 Constraint::Fixed(Fixed {
9917 points: vec![FixedPoint {
9918 point: line_start_id,
9919 position: Point2d {
9920 x: Number {
9921 value: 2.0,
9922 units: NumericSuffix::Mm,
9923 },
9924 y: Number {
9925 value: 3.0,
9926 units: NumericSuffix::Mm,
9927 },
9928 },
9929 }],
9930 }),
9931 )
9932 .await
9933 .unwrap();
9934 assert_eq!(
9935 src_delta.text.as_str(),
9936 "\
9937sketch(on = XY) {
9938 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9939 fixed([line1.start, [2mm, 3mm]])
9940}
9941"
9942 );
9943 assert_eq!(
9944 scene_delta.new_graph.objects.len(),
9945 6,
9946 "{:#?}",
9947 scene_delta.new_graph.objects
9948 );
9949
9950 ctx.close().await;
9951 mock_ctx.close().await;
9952 }
9953
9954 #[tokio::test(flavor = "multi_thread")]
9955 async fn test_radius_error_cases() {
9956 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9957 let mock_ctx = ExecutorContext::new_mock(None).await;
9958 let version = Version(0);
9959
9960 let initial_source_point = "\
9962sketch(on = XY) {
9963 point(at = [var 1, var 2])
9964}
9965";
9966 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
9967 let mut frontend_point = FrontendState::new();
9968 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
9969 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
9970 let sketch_id_point = sketch_object_point.id;
9971 let sketch_point = expect_sketch(sketch_object_point);
9972 let point_id = *sketch_point.segments.first().unwrap();
9973
9974 let constraint_point = Constraint::Radius(Radius {
9975 arc: point_id,
9976 radius: Number {
9977 value: 5.0,
9978 units: NumericSuffix::Mm,
9979 },
9980 label_position: None,
9981 source: Default::default(),
9982 });
9983 let result_point = frontend_point
9984 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
9985 .await;
9986 assert!(result_point.is_err(), "Single point should error for radius");
9987
9988 let initial_source_line = "\
9990sketch(on = XY) {
9991 line(start = [var 1, var 2], end = [var 3, var 4])
9992}
9993";
9994 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
9995 let mut frontend_line = FrontendState::new();
9996 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
9997 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
9998 let sketch_id_line = sketch_object_line.id;
9999 let sketch_line = expect_sketch(sketch_object_line);
10000 let line_id = *sketch_line.segments.first().unwrap();
10001
10002 let constraint_line = Constraint::Radius(Radius {
10003 arc: line_id,
10004 radius: Number {
10005 value: 5.0,
10006 units: NumericSuffix::Mm,
10007 },
10008 label_position: None,
10009 source: Default::default(),
10010 });
10011 let result_line = frontend_line
10012 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10013 .await;
10014 assert!(result_line.is_err(), "Single line segment should error for radius");
10015
10016 ctx.close().await;
10017 mock_ctx.close().await;
10018 }
10019
10020 #[tokio::test(flavor = "multi_thread")]
10021 async fn test_diameter_single_arc_segment() {
10022 let initial_source = "\
10023sketch(on = XY) {
10024 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10025}
10026";
10027
10028 let program = Program::parse(initial_source).unwrap().0.unwrap();
10029
10030 let mut frontend = FrontendState::new();
10031
10032 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10033 let mock_ctx = ExecutorContext::new_mock(None).await;
10034 let version = Version(0);
10035
10036 frontend.hack_set_program(&ctx, program).await.unwrap();
10037 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10038 let sketch_id = sketch_object.id;
10039 let sketch = expect_sketch(sketch_object);
10040 let arc_id = sketch
10042 .segments
10043 .iter()
10044 .find(|&seg_id| {
10045 let obj = frontend.scene_graph.objects.get(seg_id.0);
10046 matches!(
10047 obj.map(|o| &o.kind),
10048 Some(ObjectKind::Segment {
10049 segment: Segment::Arc(_)
10050 })
10051 )
10052 })
10053 .unwrap();
10054
10055 let constraint = Constraint::Diameter(Diameter {
10056 arc: *arc_id,
10057 diameter: Number {
10058 value: 10.0,
10059 units: NumericSuffix::Mm,
10060 },
10061 label_position: None,
10062 source: Default::default(),
10063 });
10064 let (src_delta, scene_delta) = frontend
10065 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10066 .await
10067 .unwrap();
10068 assert_eq!(
10069 src_delta.text.as_str(),
10070 "\
10072sketch(on = XY) {
10073 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10074 diameter(arc1) == 10mm
10075}
10076"
10077 );
10078 assert_eq!(
10079 scene_delta.new_graph.objects.len(),
10080 7, "{:#?}",
10082 scene_delta.new_graph.objects
10083 );
10084
10085 ctx.close().await;
10086 mock_ctx.close().await;
10087 }
10088
10089 #[tokio::test(flavor = "multi_thread")]
10090 async fn test_diameter_single_arc_segment_with_label_position() {
10091 let initial_source = "\
10092sketch(on = XY) {
10093 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10094}
10095";
10096
10097 let program = Program::parse(initial_source).unwrap().0.unwrap();
10098 let mut frontend = FrontendState::new();
10099 let mock_ctx = ExecutorContext::new_mock(None).await;
10100 let version = Version(0);
10101
10102 frontend.program = program.clone();
10103 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10104 frontend.update_state_after_exec(outcome, true);
10105 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10106 let sketch_id = sketch_object.id;
10107 let sketch = expect_sketch(sketch_object);
10108 let arc_id = sketch
10109 .segments
10110 .iter()
10111 .find(|&seg_id| {
10112 let obj = frontend.scene_graph.objects.get(seg_id.0);
10113 matches!(
10114 obj.map(|o| &o.kind),
10115 Some(ObjectKind::Segment {
10116 segment: Segment::Arc(_)
10117 })
10118 )
10119 })
10120 .unwrap();
10121
10122 let label_position = Point2d {
10123 x: Number {
10124 value: 10.0,
10125 units: NumericSuffix::Mm,
10126 },
10127 y: Number {
10128 value: 11.0,
10129 units: NumericSuffix::Mm,
10130 },
10131 };
10132 let constraint = Constraint::Diameter(Diameter {
10133 arc: *arc_id,
10134 diameter: Number {
10135 value: 10.0,
10136 units: NumericSuffix::Mm,
10137 },
10138 label_position: Some(label_position.clone()),
10139 source: Default::default(),
10140 });
10141 let (src_delta, scene_delta) = frontend
10142 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10143 .await
10144 .unwrap();
10145 assert_eq!(
10146 src_delta.text.as_str(),
10147 "\
10148sketch(on = XY) {
10149 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10150 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10151}
10152"
10153 );
10154
10155 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10156 let sketch = expect_sketch(sketch_object);
10157 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10158 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10159 panic!("Expected constraint object");
10160 };
10161 let Constraint::Diameter(diameter) = constraint else {
10162 panic!("Expected diameter constraint");
10163 };
10164 assert_eq!(diameter.label_position, Some(label_position));
10165
10166 mock_ctx.close().await;
10167 }
10168
10169 #[tokio::test(flavor = "multi_thread")]
10170 async fn test_edit_diameter_constraint_label_position() {
10171 let initial_source = "\
10172sketch(on = XY) {
10173 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10174 diameter(arc1) == 10mm
10175}
10176";
10177
10178 let program = Program::parse(initial_source).unwrap().0.unwrap();
10179 let mut frontend = FrontendState::new();
10180 let mock_ctx = ExecutorContext::new_mock(None).await;
10181 let version = Version(0);
10182
10183 frontend.program = program.clone();
10184 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10185 frontend.update_state_after_exec(outcome, true);
10186 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10187 let sketch_id = sketch_object.id;
10188 let sketch = expect_sketch(sketch_object);
10189 let constraint_id = sketch.constraints[0];
10190 let label_position = Point2d {
10191 x: Number {
10192 value: 10.0,
10193 units: NumericSuffix::Mm,
10194 },
10195 y: Number {
10196 value: 11.0,
10197 units: NumericSuffix::Mm,
10198 },
10199 };
10200
10201 let (src_delta, scene_delta) = frontend
10202 .edit_distance_constraint_label_position(
10203 &mock_ctx,
10204 version,
10205 sketch_id,
10206 constraint_id,
10207 label_position.clone(),
10208 vec![],
10209 )
10210 .await
10211 .unwrap();
10212 assert_eq!(
10213 src_delta.text.as_str(),
10214 "\
10215sketch(on = XY) {
10216 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10217 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10218}
10219"
10220 );
10221
10222 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10223 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10224 panic!("Expected constraint object");
10225 };
10226 let Constraint::Diameter(diameter) = constraint else {
10227 panic!("Expected diameter constraint");
10228 };
10229 assert_eq!(diameter.label_position, Some(label_position));
10230
10231 mock_ctx.close().await;
10232 }
10233
10234 #[tokio::test(flavor = "multi_thread")]
10235 async fn test_diameter_error_cases() {
10236 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10237 let mock_ctx = ExecutorContext::new_mock(None).await;
10238 let version = Version(0);
10239
10240 let initial_source_point = "\
10242sketch(on = XY) {
10243 point(at = [var 1, var 2])
10244}
10245";
10246 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10247 let mut frontend_point = FrontendState::new();
10248 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
10249 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
10250 let sketch_id_point = sketch_object_point.id;
10251 let sketch_point = expect_sketch(sketch_object_point);
10252 let point_id = *sketch_point.segments.first().unwrap();
10253
10254 let constraint_point = Constraint::Diameter(Diameter {
10255 arc: point_id,
10256 diameter: Number {
10257 value: 10.0,
10258 units: NumericSuffix::Mm,
10259 },
10260 label_position: None,
10261 source: Default::default(),
10262 });
10263 let result_point = frontend_point
10264 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
10265 .await;
10266 assert!(result_point.is_err(), "Single point should error for diameter");
10267
10268 let initial_source_line = "\
10270sketch(on = XY) {
10271 line(start = [var 1, var 2], end = [var 3, var 4])
10272}
10273";
10274 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
10275 let mut frontend_line = FrontendState::new();
10276 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10277 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10278 let sketch_id_line = sketch_object_line.id;
10279 let sketch_line = expect_sketch(sketch_object_line);
10280 let line_id = *sketch_line.segments.first().unwrap();
10281
10282 let constraint_line = Constraint::Diameter(Diameter {
10283 arc: line_id,
10284 diameter: Number {
10285 value: 10.0,
10286 units: NumericSuffix::Mm,
10287 },
10288 label_position: None,
10289 source: Default::default(),
10290 });
10291 let result_line = frontend_line
10292 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10293 .await;
10294 assert!(result_line.is_err(), "Single line segment should error for diameter");
10295
10296 ctx.close().await;
10297 mock_ctx.close().await;
10298 }
10299
10300 #[tokio::test(flavor = "multi_thread")]
10301 async fn test_line_horizontal() {
10302 let initial_source = "\
10303sketch(on = XY) {
10304 line(start = [var 1, var 2], end = [var 3, var 4])
10305}
10306";
10307
10308 let program = Program::parse(initial_source).unwrap().0.unwrap();
10309
10310 let mut frontend = FrontendState::new();
10311
10312 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10313 let mock_ctx = ExecutorContext::new_mock(None).await;
10314 let version = Version(0);
10315
10316 frontend.hack_set_program(&ctx, program).await.unwrap();
10317 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10318 let sketch_id = sketch_object.id;
10319 let sketch = expect_sketch(sketch_object);
10320 let line1_id = *sketch.segments.get(2).unwrap();
10321
10322 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
10323 let (src_delta, scene_delta) = frontend
10324 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10325 .await
10326 .unwrap();
10327 assert_eq!(
10328 src_delta.text.as_str(),
10329 "\
10330sketch(on = XY) {
10331 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10332 horizontal(line1)
10333}
10334"
10335 );
10336 assert_eq!(
10337 scene_delta.new_graph.objects.len(),
10338 6,
10339 "{:#?}",
10340 scene_delta.new_graph.objects
10341 );
10342
10343 ctx.close().await;
10344 mock_ctx.close().await;
10345 }
10346
10347 #[tokio::test(flavor = "multi_thread")]
10348 async fn test_line_vertical() {
10349 let initial_source = "\
10350sketch(on = XY) {
10351 line(start = [var 1, var 2], end = [var 3, var 4])
10352}
10353";
10354
10355 let program = Program::parse(initial_source).unwrap().0.unwrap();
10356
10357 let mut frontend = FrontendState::new();
10358
10359 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10360 let mock_ctx = ExecutorContext::new_mock(None).await;
10361 let version = Version(0);
10362
10363 frontend.hack_set_program(&ctx, program).await.unwrap();
10364 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10365 let sketch_id = sketch_object.id;
10366 let sketch = expect_sketch(sketch_object);
10367 let line1_id = *sketch.segments.get(2).unwrap();
10368
10369 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
10370 let (src_delta, scene_delta) = frontend
10371 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10372 .await
10373 .unwrap();
10374 assert_eq!(
10375 src_delta.text.as_str(),
10376 "\
10377sketch(on = XY) {
10378 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10379 vertical(line1)
10380}
10381"
10382 );
10383 assert_eq!(
10384 scene_delta.new_graph.objects.len(),
10385 6,
10386 "{:#?}",
10387 scene_delta.new_graph.objects
10388 );
10389
10390 ctx.close().await;
10391 mock_ctx.close().await;
10392 }
10393
10394 #[tokio::test(flavor = "multi_thread")]
10395 async fn test_points_vertical() {
10396 let initial_source = "\
10397sketch001 = sketch(on = XY) {
10398 p0 = point(at = [var -2.23mm, var 3.1mm])
10399 pf = point(at = [4, 4])
10400}
10401";
10402
10403 let program = Program::parse(initial_source).unwrap().0.unwrap();
10404
10405 let mut frontend = FrontendState::new();
10406
10407 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10408 let mock_ctx = ExecutorContext::new_mock(None).await;
10409 let version = Version(0);
10410
10411 frontend.hack_set_program(&ctx, program).await.unwrap();
10412 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10413 let sketch_id = sketch_object.id;
10414 let sketch = expect_sketch(sketch_object);
10415 let point_ids = vec![
10416 sketch.segments.first().unwrap().to_owned(),
10417 sketch.segments.get(1).unwrap().to_owned(),
10418 ];
10419
10420 let constraint = Constraint::Vertical(Vertical::Points {
10421 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10422 });
10423 let (src_delta, scene_delta) = frontend
10424 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10425 .await
10426 .unwrap();
10427 assert_eq!(
10428 src_delta.text.as_str(),
10429 "\
10430sketch001 = sketch(on = XY) {
10431 p0 = point(at = [var -2.23mm, var 3.1mm])
10432 pf = point(at = [4, 4])
10433 vertical([p0, pf])
10434}
10435"
10436 );
10437 assert_eq!(
10438 scene_delta.new_graph.objects.len(),
10439 5,
10440 "{:#?}",
10441 scene_delta.new_graph.objects
10442 );
10443
10444 ctx.close().await;
10445 mock_ctx.close().await;
10446 }
10447
10448 #[tokio::test(flavor = "multi_thread")]
10449 async fn test_points_horizontal() {
10450 let initial_source = "\
10451sketch001 = sketch(on = XY) {
10452 p0 = point(at = [var -2.23mm, var 3.1mm])
10453 pf = point(at = [4, 4])
10454}
10455";
10456
10457 let program = Program::parse(initial_source).unwrap().0.unwrap();
10458
10459 let mut frontend = FrontendState::new();
10460
10461 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10462 let mock_ctx = ExecutorContext::new_mock(None).await;
10463 let version = Version(0);
10464
10465 frontend.hack_set_program(&ctx, program).await.unwrap();
10466 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10467 let sketch_id = sketch_object.id;
10468 let sketch = expect_sketch(sketch_object);
10469 let point_ids = vec![
10470 sketch.segments.first().unwrap().to_owned(),
10471 sketch.segments.get(1).unwrap().to_owned(),
10472 ];
10473
10474 let constraint = Constraint::Horizontal(Horizontal::Points {
10475 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10476 });
10477 let (src_delta, scene_delta) = frontend
10478 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10479 .await
10480 .unwrap();
10481 assert_eq!(
10482 src_delta.text.as_str(),
10483 "\
10484sketch001 = sketch(on = XY) {
10485 p0 = point(at = [var -2.23mm, var 3.1mm])
10486 pf = point(at = [4, 4])
10487 horizontal([p0, pf])
10488}
10489"
10490 );
10491 assert_eq!(
10492 scene_delta.new_graph.objects.len(),
10493 5,
10494 "{:#?}",
10495 scene_delta.new_graph.objects
10496 );
10497
10498 ctx.close().await;
10499 mock_ctx.close().await;
10500 }
10501
10502 #[tokio::test(flavor = "multi_thread")]
10503 async fn test_point_horizontal_with_origin() {
10504 let initial_source = "\
10505sketch001 = sketch(on = XY) {
10506 p0 = point(at = [var -2.23mm, var 3.1mm])
10507}
10508";
10509
10510 let program = Program::parse(initial_source).unwrap().0.unwrap();
10511
10512 let mut frontend = FrontendState::new();
10513
10514 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10515 let mock_ctx = ExecutorContext::new_mock(None).await;
10516 let version = Version(0);
10517
10518 frontend.hack_set_program(&ctx, program).await.unwrap();
10519 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10520 let sketch_id = sketch_object.id;
10521 let sketch = expect_sketch(sketch_object);
10522 let point_id = *sketch.segments.first().unwrap();
10523
10524 let constraint = Constraint::Horizontal(Horizontal::Points {
10525 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
10526 });
10527 let (src_delta, scene_delta) = frontend
10528 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10529 .await
10530 .unwrap();
10531 assert_eq!(
10532 src_delta.text.as_str(),
10533 "\
10534sketch001 = sketch(on = XY) {
10535 p0 = point(at = [var -2.23mm, var 3.1mm])
10536 horizontal([p0, ORIGIN])
10537}
10538"
10539 );
10540 assert_eq!(
10541 scene_delta.new_graph.objects.len(),
10542 4,
10543 "{:#?}",
10544 scene_delta.new_graph.objects
10545 );
10546
10547 ctx.close().await;
10548 mock_ctx.close().await;
10549 }
10550
10551 #[tokio::test(flavor = "multi_thread")]
10552 async fn test_lines_equal_length() {
10553 let initial_source = "\
10554sketch(on = XY) {
10555 line(start = [var 1, var 2], end = [var 3, var 4])
10556 line(start = [var 5, var 6], end = [var 7, var 8])
10557}
10558";
10559
10560 let program = Program::parse(initial_source).unwrap().0.unwrap();
10561
10562 let mut frontend = FrontendState::new();
10563
10564 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10565 let mock_ctx = ExecutorContext::new_mock(None).await;
10566 let version = Version(0);
10567
10568 frontend.hack_set_program(&ctx, program).await.unwrap();
10569 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10570 let sketch_id = sketch_object.id;
10571 let sketch = expect_sketch(sketch_object);
10572 let line1_id = *sketch.segments.get(2).unwrap();
10573 let line2_id = *sketch.segments.get(5).unwrap();
10574
10575 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10576 lines: vec![line1_id, line2_id],
10577 });
10578 let (src_delta, scene_delta) = frontend
10579 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10580 .await
10581 .unwrap();
10582 assert_eq!(
10583 src_delta.text.as_str(),
10584 "\
10585sketch(on = XY) {
10586 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10587 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10588 equalLength([line1, line2])
10589}
10590"
10591 );
10592 assert_eq!(
10593 scene_delta.new_graph.objects.len(),
10594 9,
10595 "{:#?}",
10596 scene_delta.new_graph.objects
10597 );
10598
10599 ctx.close().await;
10600 mock_ctx.close().await;
10601 }
10602
10603 #[tokio::test(flavor = "multi_thread")]
10604 async fn test_add_constraint_multi_line_equal_length() {
10605 let initial_source = "\
10606sketch(on = XY) {
10607 line(start = [var 1, var 2], end = [var 3, var 4])
10608 line(start = [var 5, var 6], end = [var 7, var 8])
10609 line(start = [var 9, var 10], end = [var 11, var 12])
10610}
10611";
10612
10613 let program = Program::parse(initial_source).unwrap().0.unwrap();
10614
10615 let mut frontend = FrontendState::new();
10616 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10617 let mock_ctx = ExecutorContext::new_mock(None).await;
10618 let version = Version(0);
10619
10620 frontend.hack_set_program(&ctx, program).await.unwrap();
10621 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10622 let sketch_id = sketch_object.id;
10623 let sketch = expect_sketch(sketch_object);
10624 let line1_id = *sketch.segments.get(2).unwrap();
10625 let line2_id = *sketch.segments.get(5).unwrap();
10626 let line3_id = *sketch.segments.get(8).unwrap();
10627
10628 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10629 lines: vec![line1_id, line2_id, line3_id],
10630 });
10631 let (src_delta, scene_delta) = frontend
10632 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10633 .await
10634 .unwrap();
10635 assert_eq!(
10636 src_delta.text.as_str(),
10637 "\
10638sketch(on = XY) {
10639 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10640 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10641 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10642 equalLength([line1, line2, line3])
10643}
10644"
10645 );
10646 let constraints = scene_delta
10647 .new_graph
10648 .objects
10649 .iter()
10650 .filter_map(|obj| {
10651 let ObjectKind::Constraint { constraint } = &obj.kind else {
10652 return None;
10653 };
10654 Some(constraint)
10655 })
10656 .collect::<Vec<_>>();
10657
10658 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
10659 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
10660 panic!("expected equal length constraint, got {:?}", constraints[0]);
10661 };
10662 assert_eq!(lines_equal_length.lines.len(), 3);
10663
10664 ctx.close().await;
10665 mock_ctx.close().await;
10666 }
10667
10668 #[tokio::test(flavor = "multi_thread")]
10669 async fn test_lines_parallel() {
10670 let initial_source = "\
10671sketch(on = XY) {
10672 line(start = [var 1, var 2], end = [var 3, var 4])
10673 line(start = [var 5, var 6], end = [var 7, var 8])
10674}
10675";
10676
10677 let program = Program::parse(initial_source).unwrap().0.unwrap();
10678
10679 let mut frontend = FrontendState::new();
10680
10681 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10682 let mock_ctx = ExecutorContext::new_mock(None).await;
10683 let version = Version(0);
10684
10685 frontend.hack_set_program(&ctx, program).await.unwrap();
10686 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10687 let sketch_id = sketch_object.id;
10688 let sketch = expect_sketch(sketch_object);
10689 let line1_id = *sketch.segments.get(2).unwrap();
10690 let line2_id = *sketch.segments.get(5).unwrap();
10691
10692 let constraint = Constraint::Parallel(Parallel {
10693 lines: vec![line1_id, line2_id],
10694 });
10695 let (src_delta, scene_delta) = frontend
10696 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10697 .await
10698 .unwrap();
10699 assert_eq!(
10700 src_delta.text.as_str(),
10701 "\
10702sketch(on = XY) {
10703 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10704 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10705 parallel([line1, line2])
10706}
10707"
10708 );
10709 assert_eq!(
10710 scene_delta.new_graph.objects.len(),
10711 9,
10712 "{:#?}",
10713 scene_delta.new_graph.objects
10714 );
10715
10716 ctx.close().await;
10717 mock_ctx.close().await;
10718 }
10719
10720 #[tokio::test(flavor = "multi_thread")]
10721 async fn test_lines_parallel_multiline() {
10722 let initial_source = "\
10723sketch(on = XY) {
10724 line(start = [var 1, var 2], end = [var 3, var 4])
10725 line(start = [var 5, var 6], end = [var 7, var 8])
10726 line(start = [var 9, var 10], end = [var 11, var 12])
10727}
10728";
10729
10730 let program = Program::parse(initial_source).unwrap().0.unwrap();
10731
10732 let mut frontend = FrontendState::new();
10733
10734 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10735 let mock_ctx = ExecutorContext::new_mock(None).await;
10736 let version = Version(0);
10737
10738 frontend.hack_set_program(&ctx, program).await.unwrap();
10739 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10740 let sketch_id = sketch_object.id;
10741 let sketch = expect_sketch(sketch_object);
10742 let line1_id = *sketch.segments.get(2).unwrap();
10743 let line2_id = *sketch.segments.get(5).unwrap();
10744 let line3_id = *sketch.segments.get(8).unwrap();
10745
10746 let constraint = Constraint::Parallel(Parallel {
10747 lines: vec![line1_id, line2_id, line3_id],
10748 });
10749 let (src_delta, scene_delta) = frontend
10750 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10751 .await
10752 .unwrap();
10753 assert_eq!(
10754 src_delta.text.as_str(),
10755 "\
10756sketch(on = XY) {
10757 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10758 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10759 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10760 parallel([line1, line2, line3])
10761}
10762"
10763 );
10764
10765 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10766 let sketch = expect_sketch(sketch_object);
10767 assert_eq!(sketch.constraints.len(), 1);
10768
10769 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10770 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10771 panic!("Expected constraint object");
10772 };
10773 let Constraint::Parallel(parallel) = constraint else {
10774 panic!("Expected parallel constraint");
10775 };
10776 assert_eq!(parallel.lines.len(), 3);
10777
10778 ctx.close().await;
10779 mock_ctx.close().await;
10780 }
10781
10782 #[tokio::test(flavor = "multi_thread")]
10783 async fn test_lines_perpendicular() {
10784 let initial_source = "\
10785sketch(on = XY) {
10786 line(start = [var 1, var 2], end = [var 3, var 4])
10787 line(start = [var 5, var 6], end = [var 7, var 8])
10788}
10789";
10790
10791 let program = Program::parse(initial_source).unwrap().0.unwrap();
10792
10793 let mut frontend = FrontendState::new();
10794
10795 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10796 let mock_ctx = ExecutorContext::new_mock(None).await;
10797 let version = Version(0);
10798
10799 frontend.hack_set_program(&ctx, program).await.unwrap();
10800 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10801 let sketch_id = sketch_object.id;
10802 let sketch = expect_sketch(sketch_object);
10803 let line1_id = *sketch.segments.get(2).unwrap();
10804 let line2_id = *sketch.segments.get(5).unwrap();
10805
10806 let constraint = Constraint::Perpendicular(Perpendicular {
10807 lines: vec![line1_id, line2_id],
10808 });
10809 let (src_delta, scene_delta) = frontend
10810 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10811 .await
10812 .unwrap();
10813 assert_eq!(
10814 src_delta.text.as_str(),
10815 "\
10816sketch(on = XY) {
10817 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10818 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10819 perpendicular([line1, line2])
10820}
10821"
10822 );
10823 assert_eq!(
10824 scene_delta.new_graph.objects.len(),
10825 9,
10826 "{:#?}",
10827 scene_delta.new_graph.objects
10828 );
10829
10830 ctx.close().await;
10831 mock_ctx.close().await;
10832 }
10833
10834 #[tokio::test(flavor = "multi_thread")]
10835 async fn test_lines_angle() {
10836 let initial_source = "\
10837sketch(on = XY) {
10838 line(start = [var 1, var 2], end = [var 3, var 4])
10839 line(start = [var 5, var 6], end = [var 7, var 8])
10840}
10841";
10842
10843 let program = Program::parse(initial_source).unwrap().0.unwrap();
10844
10845 let mut frontend = FrontendState::new();
10846
10847 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10848 let mock_ctx = ExecutorContext::new_mock(None).await;
10849 let version = Version(0);
10850
10851 frontend.hack_set_program(&ctx, program).await.unwrap();
10852 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10853 let sketch_id = sketch_object.id;
10854 let sketch = expect_sketch(sketch_object);
10855 let line1_id = *sketch.segments.get(2).unwrap();
10856 let line2_id = *sketch.segments.get(5).unwrap();
10857
10858 let constraint = Constraint::Angle(Angle {
10859 lines: vec![line1_id, line2_id],
10860 angle: Number {
10861 value: 30.0,
10862 units: NumericSuffix::Deg,
10863 },
10864 source: Default::default(),
10865 });
10866 let (src_delta, scene_delta) = frontend
10867 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10868 .await
10869 .unwrap();
10870 assert_eq!(
10871 src_delta.text.as_str(),
10872 "\
10874sketch(on = XY) {
10875 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10876 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10877 angle([line1, line2]) == 30deg
10878}
10879"
10880 );
10881 assert_eq!(
10882 scene_delta.new_graph.objects.len(),
10883 9,
10884 "{:#?}",
10885 scene_delta.new_graph.objects
10886 );
10887
10888 ctx.close().await;
10889 mock_ctx.close().await;
10890 }
10891
10892 #[tokio::test(flavor = "multi_thread")]
10893 async fn test_segments_tangent() {
10894 let initial_source = "\
10895sketch(on = XY) {
10896 line(start = [var 1, var 2], end = [var 3, var 4])
10897 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10898}
10899";
10900
10901 let program = Program::parse(initial_source).unwrap().0.unwrap();
10902
10903 let mut frontend = FrontendState::new();
10904
10905 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10906 let mock_ctx = ExecutorContext::new_mock(None).await;
10907 let version = Version(0);
10908
10909 frontend.hack_set_program(&ctx, program).await.unwrap();
10910 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10911 let sketch_id = sketch_object.id;
10912 let sketch = expect_sketch(sketch_object);
10913 let line1_id = *sketch.segments.get(2).unwrap();
10914 let arc1_id = *sketch.segments.get(6).unwrap();
10915
10916 let constraint = Constraint::Tangent(Tangent {
10917 input: vec![line1_id, arc1_id],
10918 });
10919 let (src_delta, scene_delta) = frontend
10920 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10921 .await
10922 .unwrap();
10923 assert_eq!(
10924 src_delta.text.as_str(),
10925 "\
10926sketch(on = XY) {
10927 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10928 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10929 tangent([line1, arc1])
10930}
10931"
10932 );
10933 assert_eq!(
10934 scene_delta.new_graph.objects.len(),
10935 10,
10936 "{:#?}",
10937 scene_delta.new_graph.objects
10938 );
10939
10940 ctx.close().await;
10941 mock_ctx.close().await;
10942 }
10943
10944 #[tokio::test(flavor = "multi_thread")]
10945 async fn test_point_midpoint() {
10946 let initial_source = "\
10947sketch(on = XY) {
10948 point(at = [var 1, var 1])
10949 line(start = [var 0, var 0], end = [var 6, var 4])
10950}
10951";
10952
10953 let program = Program::parse(initial_source).unwrap().0.unwrap();
10954
10955 let mut frontend = FrontendState::new();
10956
10957 let ctx = ExecutorContext::new_mock(None).await;
10958 let version = Version(0);
10959
10960 frontend.program = program.clone();
10961 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10962 frontend.update_state_after_exec(outcome, true);
10963 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10964 let sketch_id = sketch_object.id;
10965 let sketch = expect_sketch(sketch_object);
10966 let point_id = *sketch.segments.first().unwrap();
10967 let line_id = *sketch.segments.get(3).unwrap();
10968
10969 let constraint = Constraint::Midpoint(Midpoint {
10970 point: point_id,
10971 segment: line_id,
10972 });
10973 let (src_delta, scene_delta) = frontend
10974 .add_constraint(&ctx, version, sketch_id, constraint)
10975 .await
10976 .unwrap();
10977 assert_eq!(
10978 src_delta.text.as_str(),
10979 "\
10980sketch(on = XY) {
10981 point1 = point(at = [var 1, var 1])
10982 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10983 midpoint(line1, point = point1)
10984}
10985"
10986 );
10987 assert_eq!(
10988 scene_delta.new_graph.objects.len(),
10989 7,
10990 "{:#?}",
10991 scene_delta.new_graph.objects
10992 );
10993
10994 ctx.close().await;
10995 }
10996
10997 #[tokio::test(flavor = "multi_thread")]
10998 async fn test_segments_symmetric() {
10999 let initial_source = "\
11000sketch(on = XY) {
11001 line(start = [var 0, var 0], end = [var 0, var 4])
11002 line(start = [var 4, var 0], end = [var 4, var 4])
11003 line(start = [var 2, var -1], end = [var 2, var 5])
11004}
11005";
11006
11007 let program = Program::parse(initial_source).unwrap().0.unwrap();
11008
11009 let mut frontend = FrontendState::new();
11010
11011 let ctx = ExecutorContext::new_mock(None).await;
11012 let version = Version(0);
11013
11014 frontend.program = program.clone();
11015 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11016 frontend.update_state_after_exec(outcome, true);
11017 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11018 let sketch_id = sketch_object.id;
11019 let sketch = expect_sketch(sketch_object);
11020 let line1_id = *sketch.segments.get(2).unwrap();
11021 let line2_id = *sketch.segments.get(5).unwrap();
11022 let axis_id = *sketch.segments.get(8).unwrap();
11023
11024 let constraint = Constraint::Symmetric(Symmetric {
11025 input: vec![line1_id, line2_id],
11026 axis: axis_id,
11027 });
11028 let (src_delta, scene_delta) = frontend
11029 .add_constraint(&ctx, version, sketch_id, constraint)
11030 .await
11031 .unwrap();
11032 assert_eq!(
11033 src_delta.text.as_str(),
11034 "\
11035sketch(on = XY) {
11036 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
11037 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
11038 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
11039 symmetric([line1, line2], axis = line3)
11040}
11041"
11042 );
11043 assert_eq!(
11044 scene_delta.new_graph.objects.len(),
11045 12,
11046 "{:#?}",
11047 scene_delta.new_graph.objects
11048 );
11049
11050 ctx.close().await;
11051 }
11052
11053 #[tokio::test(flavor = "multi_thread")]
11054 async fn test_point_arc_midpoint() {
11055 let initial_source = "\
11056sketch(on = XY) {
11057 point(at = [var 6, var 3])
11058 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11059}
11060";
11061
11062 let program = Program::parse(initial_source).unwrap().0.unwrap();
11063
11064 let mut frontend = FrontendState::new();
11065
11066 let ctx = ExecutorContext::new_mock(None).await;
11067 let version = Version(0);
11068
11069 frontend.program = program.clone();
11070 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11071 frontend.update_state_after_exec(outcome, true);
11072 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11073 let sketch_id = sketch_object.id;
11074 let sketch = expect_sketch(sketch_object);
11075 let point_id = *sketch.segments.first().unwrap();
11076 let arc_id = *sketch.segments.get(4).unwrap();
11077
11078 let constraint = Constraint::Midpoint(Midpoint {
11079 point: point_id,
11080 segment: arc_id,
11081 });
11082 let (src_delta, scene_delta) = frontend
11083 .add_constraint(&ctx, version, sketch_id, constraint)
11084 .await
11085 .unwrap();
11086 assert_eq!(
11087 src_delta.text.as_str(),
11088 "\
11089sketch(on = XY) {
11090 point1 = point(at = [var 6, var 3])
11091 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11092 midpoint(arc1, point = point1)
11093}
11094"
11095 );
11096 assert_eq!(
11097 scene_delta.new_graph.objects.len(),
11098 8,
11099 "{:#?}",
11100 scene_delta.new_graph.objects
11101 );
11102
11103 ctx.close().await;
11104 }
11105
11106 #[tokio::test(flavor = "multi_thread")]
11107 async fn test_segments_symmetric_arcs() {
11108 let initial_source = "\
11109sketch(on = XY) {
11110 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11111 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11112 line(start = [var 0, var -10], end = [var 0, var 10])
11113}
11114";
11115
11116 let program = Program::parse(initial_source).unwrap().0.unwrap();
11117
11118 let mut frontend = FrontendState::new();
11119
11120 let ctx = ExecutorContext::new_mock(None).await;
11121 let version = Version(0);
11122
11123 frontend.program = program.clone();
11124 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11125 frontend.update_state_after_exec(outcome, true);
11126 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11127 let sketch_id = sketch_object.id;
11128 let sketch = expect_sketch(sketch_object);
11129 let arc1_id = *sketch.segments.get(3).unwrap();
11130 let arc2_id = *sketch.segments.get(7).unwrap();
11131 let axis_id = *sketch.segments.get(10).unwrap();
11132
11133 let constraint = Constraint::Symmetric(Symmetric {
11134 input: vec![arc1_id, arc2_id],
11135 axis: axis_id,
11136 });
11137 let (src_delta, scene_delta) = frontend
11138 .add_constraint(&ctx, version, sketch_id, constraint)
11139 .await
11140 .unwrap();
11141 assert_eq!(
11142 src_delta.text.as_str(),
11143 "\
11144sketch(on = XY) {
11145 arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11146 arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11147 line1 = line(start = [var 0, var -10], end = [var 0, var 10])
11148 symmetric([arc1, arc2], axis = line1)
11149}
11150"
11151 );
11152 assert_eq!(
11153 scene_delta.new_graph.objects.len(),
11154 14,
11155 "{:#?}",
11156 scene_delta.new_graph.objects
11157 );
11158
11159 ctx.close().await;
11160 }
11161
11162 #[tokio::test(flavor = "multi_thread")]
11163 async fn test_sketch_on_face_simple() {
11164 let initial_source = "\
11165len = 2mm
11166cube = startSketchOn(XY)
11167 |> startProfile(at = [0, 0])
11168 |> line(end = [len, 0], tag = $side)
11169 |> line(end = [0, len])
11170 |> line(end = [-len, 0])
11171 |> line(end = [0, -len])
11172 |> close()
11173 |> extrude(length = len)
11174
11175face = faceOf(cube, face = side)
11176";
11177
11178 let program = Program::parse(initial_source).unwrap().0.unwrap();
11179
11180 let mut frontend = FrontendState::new();
11181
11182 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11183 let mock_ctx = ExecutorContext::new_mock(None).await;
11184 let version = Version(0);
11185
11186 frontend.hack_set_program(&ctx, program).await.unwrap();
11187 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
11188 let face_id = face_object.id;
11189
11190 let sketch_args = SketchCtor {
11191 on: Plane::Object(face_id),
11192 };
11193 let (_src_delta, scene_delta, sketch_id) = frontend
11194 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11195 .await
11196 .unwrap();
11197 assert_eq!(sketch_id, ObjectId(2));
11198 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11199 let sketch_object = &scene_delta.new_graph.objects[2];
11200 assert_eq!(sketch_object.id, ObjectId(2));
11201 assert_eq!(
11202 sketch_object.kind,
11203 ObjectKind::Sketch(Sketch {
11204 args: SketchCtor {
11205 on: Plane::Object(face_id),
11206 },
11207 plane: face_id,
11208 segments: vec![],
11209 constraints: vec![],
11210 })
11211 );
11212 assert_eq!(scene_delta.new_graph.objects.len(), 8);
11213
11214 ctx.close().await;
11215 mock_ctx.close().await;
11216 }
11217
11218 #[tokio::test(flavor = "multi_thread")]
11219 async fn test_sketch_on_wall_artifact_from_region_extrude() {
11220 let initial_source = "\
11221s = sketch(on = YZ) {
11222 line1 = line(start = [0, 0], end = [0, 1])
11223 line2 = line(start = [0, 1], end = [1, 1])
11224 line3 = line(start = [1, 1], end = [0, 0])
11225}
11226region001 = region(point = [0.1, 0.1], sketch = s)
11227extrude001 = extrude(region001, length = 5)
11228";
11229
11230 let program = Program::parse(initial_source).unwrap().0.unwrap();
11231
11232 let mut frontend = FrontendState::new();
11233 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11234 let version = Version(0);
11235
11236 frontend.hack_set_program(&ctx, program).await.unwrap();
11237 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11238
11239 let sketch_args = SketchCtor {
11240 on: Plane::Object(wall_object_id),
11241 };
11242 let (src_delta, _scene_delta, _sketch_id) = frontend
11243 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11244 .await
11245 .unwrap();
11246 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11247
11248 ctx.close().await;
11249 }
11250
11251 #[tokio::test(flavor = "multi_thread")]
11252 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
11253 let initial_source = "\
11254sketch001 = sketch(on = YZ) {
11255 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
11256 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
11257 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
11258 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
11259 coincident([line1.end, line2.start])
11260 coincident([line2.end, line3.start])
11261 coincident([line3.end, line4.start])
11262 coincident([line4.end, line1.start])
11263 parallel([line2, line4])
11264 parallel([line3, line1])
11265 perpendicular([line1, line2])
11266 horizontal(line3)
11267 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
11268}
11269region001 = region(point = [3.1, 3.74], sketch = sketch001)
11270extrude001 = extrude(region001, length = 5)
11271";
11272
11273 let program = Program::parse(initial_source).unwrap().0.unwrap();
11274
11275 let mut frontend = FrontendState::new();
11276 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11277 let version = Version(0);
11278
11279 frontend.hack_set_program(&ctx, program).await.unwrap();
11280 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11281
11282 let sketch_args = SketchCtor {
11283 on: Plane::Object(wall_object_id),
11284 };
11285 let (src_delta, _scene_delta, _sketch_id) = frontend
11286 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11287 .await
11288 .unwrap();
11289 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11290
11291 ctx.close().await;
11292 }
11293
11294 #[tokio::test(flavor = "multi_thread")]
11295 async fn test_sketch_on_plane_incremental() {
11296 let initial_source = "\
11297len = 2mm
11298cube = startSketchOn(XY)
11299 |> startProfile(at = [0, 0])
11300 |> line(end = [len, 0], tag = $side)
11301 |> line(end = [0, len])
11302 |> line(end = [-len, 0])
11303 |> line(end = [0, -len])
11304 |> close()
11305 |> extrude(length = len)
11306
11307plane = planeOf(cube, face = side)
11308";
11309
11310 let program = Program::parse(initial_source).unwrap().0.unwrap();
11311
11312 let mut frontend = FrontendState::new();
11313
11314 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11315 let mock_ctx = ExecutorContext::new_mock(None).await;
11316 let version = Version(0);
11317
11318 frontend.hack_set_program(&ctx, program).await.unwrap();
11319 let plane_object = frontend
11321 .scene_graph
11322 .objects
11323 .iter()
11324 .rev()
11325 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
11326 .unwrap();
11327 let plane_id = plane_object.id;
11328
11329 let sketch_args = SketchCtor {
11330 on: Plane::Object(plane_id),
11331 };
11332 let (src_delta, scene_delta, sketch_id) = frontend
11333 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11334 .await
11335 .unwrap();
11336 assert_eq!(
11337 src_delta.text.as_str(),
11338 "\
11339len = 2mm
11340cube = startSketchOn(XY)
11341 |> startProfile(at = [0, 0])
11342 |> line(end = [len, 0], tag = $side)
11343 |> line(end = [0, len])
11344 |> line(end = [-len, 0])
11345 |> line(end = [0, -len])
11346 |> close()
11347 |> extrude(length = len)
11348
11349plane = planeOf(cube, face = side)
11350sketch001 = sketch(on = plane) {
11351}
11352"
11353 );
11354 assert_eq!(sketch_id, ObjectId(2));
11355 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11356 let sketch_object = &scene_delta.new_graph.objects[2];
11357 assert_eq!(sketch_object.id, ObjectId(2));
11358 assert_eq!(
11359 sketch_object.kind,
11360 ObjectKind::Sketch(Sketch {
11361 args: SketchCtor {
11362 on: Plane::Object(plane_id),
11363 },
11364 plane: plane_id,
11365 segments: vec![],
11366 constraints: vec![],
11367 })
11368 );
11369 assert_eq!(scene_delta.new_graph.objects.len(), 9);
11370
11371 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
11372 assert_eq!(plane_object.id, plane_id);
11373 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
11374
11375 ctx.close().await;
11376 mock_ctx.close().await;
11377 }
11378
11379 #[tokio::test(flavor = "multi_thread")]
11380 async fn test_new_sketch_uses_unique_variable_name() {
11381 let initial_source = "\
11382sketch1 = sketch(on = XY) {
11383}
11384";
11385
11386 let program = Program::parse(initial_source).unwrap().0.unwrap();
11387
11388 let mut frontend = FrontendState::new();
11389 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11390 let version = Version(0);
11391
11392 frontend.hack_set_program(&ctx, program).await.unwrap();
11393
11394 let sketch_args = SketchCtor {
11395 on: Plane::Default(PlaneName::Yz),
11396 };
11397 let (src_delta, _, _) = frontend
11398 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11399 .await
11400 .unwrap();
11401
11402 assert_eq!(
11403 src_delta.text.as_str(),
11404 "\
11405sketch1 = sketch(on = XY) {
11406}
11407sketch001 = sketch(on = YZ) {
11408}
11409"
11410 );
11411
11412 ctx.close().await;
11413 }
11414
11415 #[tokio::test(flavor = "multi_thread")]
11416 async fn test_new_sketch_twice_using_same_plane() {
11417 let initial_source = "\
11418sketch1 = sketch(on = XY) {
11419}
11420";
11421
11422 let program = Program::parse(initial_source).unwrap().0.unwrap();
11423
11424 let mut frontend = FrontendState::new();
11425 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11426 let version = Version(0);
11427
11428 frontend.hack_set_program(&ctx, program).await.unwrap();
11429
11430 let sketch_args = SketchCtor {
11431 on: Plane::Default(PlaneName::Xy),
11432 };
11433 let (src_delta, _, _) = frontend
11434 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11435 .await
11436 .unwrap();
11437
11438 assert_eq!(
11439 src_delta.text.as_str(),
11440 "\
11441sketch1 = sketch(on = XY) {
11442}
11443sketch001 = sketch(on = XY) {
11444}
11445"
11446 );
11447
11448 ctx.close().await;
11449 }
11450
11451 #[tokio::test(flavor = "multi_thread")]
11452 async fn test_sketch_mode_reuses_cached_on_expression() {
11453 let initial_source = "\
11454width = 2mm
11455sketch(on = offsetPlane(XY, offset = width)) {
11456 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
11457 distance([line1.start, line1.end]) == width
11458}
11459";
11460 let program = Program::parse(initial_source).unwrap().0.unwrap();
11461
11462 let mut frontend = FrontendState::new();
11463 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11464 let mock_ctx = ExecutorContext::new_mock(None).await;
11465 let version = Version(0);
11466 let project_id = ProjectId(0);
11467 let file_id = FileId(0);
11468
11469 frontend.hack_set_program(&ctx, program).await.unwrap();
11470 let initial_object_count = frontend.scene_graph.objects.len();
11471 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
11472 .expect("Expected sketch object to exist")
11473 .id;
11474
11475 let scene_delta = frontend
11478 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11479 .await
11480 .unwrap();
11481 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11482
11483 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
11486 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11487
11488 ctx.close().await;
11489 mock_ctx.close().await;
11490 }
11491
11492 #[tokio::test(flavor = "multi_thread")]
11493 async fn test_multiple_sketch_blocks() {
11494 let initial_source = "\
11495// Cube that requires the engine.
11496width = 2
11497sketch001 = startSketchOn(XY)
11498profile001 = startProfile(sketch001, at = [0, 0])
11499 |> yLine(length = width, tag = $seg1)
11500 |> xLine(length = width)
11501 |> yLine(length = -width)
11502 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11503 |> close()
11504extrude001 = extrude(profile001, length = width)
11505
11506// Get a value that requires the engine.
11507x = segLen(seg1)
11508
11509// Triangle with side length 2*x.
11510sketch(on = XY) {
11511 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11512 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11513 coincident([line1.end, line2.start])
11514 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11515 coincident([line2.end, line3.start])
11516 coincident([line3.end, line1.start])
11517 equalLength([line3, line1])
11518 equalLength([line1, line2])
11519 distance([line1.start, line1.end]) == 2*x
11520}
11521
11522// Line segment with length x.
11523sketch2 = sketch(on = XY) {
11524 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11525 distance([line1.start, line1.end]) == x
11526}
11527";
11528
11529 let program = Program::parse(initial_source).unwrap().0.unwrap();
11530
11531 let mut frontend = FrontendState::new();
11532
11533 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11534 let mock_ctx = ExecutorContext::new_mock(None).await;
11535 let version = Version(0);
11536 let project_id = ProjectId(0);
11537 let file_id = FileId(0);
11538
11539 frontend.hack_set_program(&ctx, program).await.unwrap();
11540 let sketch_objects = frontend
11541 .scene_graph
11542 .objects
11543 .iter()
11544 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
11545 .collect::<Vec<_>>();
11546 let sketch1_id = sketch_objects.first().unwrap().id;
11547 let sketch2_id = sketch_objects.get(1).unwrap().id;
11548 let point1_id = ObjectId(sketch1_id.0 + 1);
11550 let point2_id = ObjectId(sketch2_id.0 + 1);
11552
11553 let scene_delta = frontend
11562 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11563 .await
11564 .unwrap();
11565 assert_eq!(
11566 scene_delta.new_graph.objects.len(),
11567 18,
11568 "{:#?}",
11569 scene_delta.new_graph.objects
11570 );
11571
11572 let point_ctor = PointCtor {
11574 position: Point2d {
11575 x: Expr::Var(Number {
11576 value: 1.0,
11577 units: NumericSuffix::Mm,
11578 }),
11579 y: Expr::Var(Number {
11580 value: 2.0,
11581 units: NumericSuffix::Mm,
11582 }),
11583 },
11584 };
11585 let segments = vec![ExistingSegmentCtor {
11586 id: point1_id,
11587 ctor: SegmentCtor::Point(point_ctor),
11588 }];
11589 let (src_delta, _) = frontend
11590 .edit_segments(&mock_ctx, version, sketch1_id, segments)
11591 .await
11592 .unwrap();
11593 assert_eq!(
11595 src_delta.text.as_str(),
11596 "\
11597// Cube that requires the engine.
11598width = 2
11599sketch001 = startSketchOn(XY)
11600profile001 = startProfile(sketch001, at = [0, 0])
11601 |> yLine(length = width, tag = $seg1)
11602 |> xLine(length = width)
11603 |> yLine(length = -width)
11604 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11605 |> close()
11606extrude001 = extrude(profile001, length = width)
11607
11608// Get a value that requires the engine.
11609x = segLen(seg1)
11610
11611// Triangle with side length 2*x.
11612sketch(on = XY) {
11613 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
11614 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
11615 coincident([line1.end, line2.start])
11616 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
11617 coincident([line2.end, line3.start])
11618 coincident([line3.end, line1.start])
11619 equalLength([line3, line1])
11620 equalLength([line1, line2])
11621 distance([line1.start, line1.end]) == 2 * x
11622}
11623
11624// Line segment with length x.
11625sketch2 = sketch(on = XY) {
11626 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11627 distance([line1.start, line1.end]) == x
11628}
11629"
11630 );
11631
11632 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
11634 assert_eq!(
11636 src_delta.text.as_str(),
11637 "\
11638// Cube that requires the engine.
11639width = 2
11640sketch001 = startSketchOn(XY)
11641profile001 = startProfile(sketch001, at = [0, 0])
11642 |> yLine(length = width, tag = $seg1)
11643 |> xLine(length = width)
11644 |> yLine(length = -width)
11645 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11646 |> close()
11647extrude001 = extrude(profile001, length = width)
11648
11649// Get a value that requires the engine.
11650x = segLen(seg1)
11651
11652// Triangle with side length 2*x.
11653sketch(on = XY) {
11654 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11655 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11656 coincident([line1.end, line2.start])
11657 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11658 coincident([line2.end, line3.start])
11659 coincident([line3.end, line1.start])
11660 equalLength([line3, line1])
11661 equalLength([line1, line2])
11662 distance([line1.start, line1.end]) == 2 * x
11663}
11664
11665// Line segment with length x.
11666sketch2 = sketch(on = XY) {
11667 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11668 distance([line1.start, line1.end]) == x
11669}
11670"
11671 );
11672 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11680 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
11681
11682 let scene_delta = frontend
11690 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11691 .await
11692 .unwrap();
11693 assert_eq!(
11694 scene_delta.new_graph.objects.len(),
11695 24,
11696 "{:#?}",
11697 scene_delta.new_graph.objects
11698 );
11699
11700 let point_ctor = PointCtor {
11702 position: Point2d {
11703 x: Expr::Var(Number {
11704 value: 3.0,
11705 units: NumericSuffix::Mm,
11706 }),
11707 y: Expr::Var(Number {
11708 value: 4.0,
11709 units: NumericSuffix::Mm,
11710 }),
11711 },
11712 };
11713 let segments = vec![ExistingSegmentCtor {
11714 id: point2_id,
11715 ctor: SegmentCtor::Point(point_ctor),
11716 }];
11717 let (src_delta, _) = frontend
11718 .edit_segments(&mock_ctx, version, sketch2_id, segments)
11719 .await
11720 .unwrap();
11721 assert_eq!(
11723 src_delta.text.as_str(),
11724 "\
11725// Cube that requires the engine.
11726width = 2
11727sketch001 = startSketchOn(XY)
11728profile001 = startProfile(sketch001, at = [0, 0])
11729 |> yLine(length = width, tag = $seg1)
11730 |> xLine(length = width)
11731 |> yLine(length = -width)
11732 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11733 |> close()
11734extrude001 = extrude(profile001, length = width)
11735
11736// Get a value that requires the engine.
11737x = segLen(seg1)
11738
11739// Triangle with side length 2*x.
11740sketch(on = XY) {
11741 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11742 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11743 coincident([line1.end, line2.start])
11744 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11745 coincident([line2.end, line3.start])
11746 coincident([line3.end, line1.start])
11747 equalLength([line3, line1])
11748 equalLength([line1, line2])
11749 distance([line1.start, line1.end]) == 2 * x
11750}
11751
11752// Line segment with length x.
11753sketch2 = sketch(on = XY) {
11754 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
11755 distance([line1.start, line1.end]) == x
11756}
11757"
11758 );
11759
11760 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
11762 assert_eq!(
11764 src_delta.text.as_str(),
11765 "\
11766// Cube that requires the engine.
11767width = 2
11768sketch001 = startSketchOn(XY)
11769profile001 = startProfile(sketch001, at = [0, 0])
11770 |> yLine(length = width, tag = $seg1)
11771 |> xLine(length = width)
11772 |> yLine(length = -width)
11773 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11774 |> close()
11775extrude001 = extrude(profile001, length = width)
11776
11777// Get a value that requires the engine.
11778x = segLen(seg1)
11779
11780// Triangle with side length 2*x.
11781sketch(on = XY) {
11782 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11783 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11784 coincident([line1.end, line2.start])
11785 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11786 coincident([line2.end, line3.start])
11787 coincident([line3.end, line1.start])
11788 equalLength([line3, line1])
11789 equalLength([line1, line2])
11790 distance([line1.start, line1.end]) == 2 * x
11791}
11792
11793// Line segment with length x.
11794sketch2 = sketch(on = XY) {
11795 line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
11796 distance([line1.start, line1.end]) == x
11797}
11798"
11799 );
11800
11801 ctx.close().await;
11802 mock_ctx.close().await;
11803 }
11804
11805 #[tokio::test(flavor = "multi_thread")]
11806 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
11807 clear_mem_cache().await;
11808
11809 let source = r#"sketch001 = sketch(on = XZ) {
11810 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
11811}
11812sketch002 = sketch(on = XY) {
11813 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
11814 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
11815 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
11816 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
11817 coincident([line1.end, line2.start])
11818 coincident([line2.end, line3.start])
11819 coincident([line3.end, line4.start])
11820 coincident([line4.end, line1.start])
11821 parallel([line2, line4])
11822 parallel([line3, line1])
11823 perpendicular([line1, line2])
11824 horizontal(line3)
11825 coincident([line1.start, ORIGIN])
11826}
11827"#;
11828
11829 let program = Program::parse(source).unwrap().0.unwrap();
11830 let mut frontend = FrontendState::new();
11831 let ctx = ExecutorContext::new_with_engine(
11832 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
11833 Default::default(),
11834 );
11835 let mock_ctx = ExecutorContext::new_mock(None).await;
11836 let version = Version(0);
11837 let project_id = ProjectId(0);
11838 let file_id = FileId(0);
11839
11840 frontend.hack_set_program(&ctx, program).await.unwrap();
11841 let sketch_objects = frontend
11842 .scene_graph
11843 .objects
11844 .iter()
11845 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
11846 .collect::<Vec<_>>();
11847 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
11848
11849 let sketch1_id = sketch_objects[0].id;
11850 let sketch2_id = sketch_objects[1].id;
11851
11852 frontend
11853 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11854 .await
11855 .unwrap();
11856 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11857
11858 let scene_delta = frontend
11859 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11860 .await
11861 .unwrap();
11862 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
11863
11864 clear_mem_cache().await;
11865 ctx.close().await;
11866 mock_ctx.close().await;
11867 }
11868
11869 #[tokio::test(flavor = "multi_thread")]
11874 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
11875 let initial_source = "@settings(defaultLengthUnit = mm)
11877
11878
11879
11880sketch001 = sketch(on = XY) {
11881 point(at = [1in, 2in])
11882}
11883";
11884
11885 let program = Program::parse(initial_source).unwrap().0.unwrap();
11886 let mut frontend = FrontendState::new();
11887
11888 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11889 let mock_ctx = ExecutorContext::new_mock(None).await;
11890 let version = Version(0);
11891 let project_id = ProjectId(0);
11892 let file_id = FileId(0);
11893
11894 frontend.hack_set_program(&ctx, program).await.unwrap();
11895 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11896 let sketch_id = sketch_object.id;
11897
11898 frontend
11900 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11901 .await
11902 .unwrap();
11903
11904 let point_ctor = PointCtor {
11906 position: Point2d {
11907 x: Expr::Number(Number {
11908 value: 5.0,
11909 units: NumericSuffix::Mm,
11910 }),
11911 y: Expr::Number(Number {
11912 value: 6.0,
11913 units: NumericSuffix::Mm,
11914 }),
11915 },
11916 };
11917 let segment = SegmentCtor::Point(point_ctor);
11918 let (src_delta, scene_delta) = frontend
11919 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11920 .await
11921 .unwrap();
11922 assert!(
11924 src_delta.text.contains("point(at = [5mm, 6mm])"),
11925 "Expected new point in source, got: {}",
11926 src_delta.text
11927 );
11928 assert!(!scene_delta.new_objects.is_empty());
11929
11930 ctx.close().await;
11931 mock_ctx.close().await;
11932 }
11933
11934 #[tokio::test(flavor = "multi_thread")]
11935 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
11936 let initial_source = "@settings(defaultLengthUnit = mm)
11938
11939
11940
11941s = sketch(on = XY) {}
11942";
11943
11944 let program = Program::parse(initial_source).unwrap().0.unwrap();
11945 let mut frontend = FrontendState::new();
11946
11947 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11948 let mock_ctx = ExecutorContext::new_mock(None).await;
11949 let version = Version(0);
11950
11951 frontend.hack_set_program(&ctx, program).await.unwrap();
11952 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11953 let sketch_id = sketch_object.id;
11954
11955 let line_ctor = LineCtor {
11956 start: Point2d {
11957 x: Expr::Number(Number {
11958 value: 0.0,
11959 units: NumericSuffix::Mm,
11960 }),
11961 y: Expr::Number(Number {
11962 value: 0.0,
11963 units: NumericSuffix::Mm,
11964 }),
11965 },
11966 end: Point2d {
11967 x: Expr::Number(Number {
11968 value: 10.0,
11969 units: NumericSuffix::Mm,
11970 }),
11971 y: Expr::Number(Number {
11972 value: 10.0,
11973 units: NumericSuffix::Mm,
11974 }),
11975 },
11976 construction: None,
11977 };
11978 let segment = SegmentCtor::Line(line_ctor);
11979 let (src_delta, scene_delta) = frontend
11980 .add_segment(&mock_ctx, version, sketch_id, segment, None)
11981 .await
11982 .unwrap();
11983 assert!(
11984 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
11985 "Expected line in source, got: {}",
11986 src_delta.text
11987 );
11988 assert_eq!(scene_delta.new_objects.len(), 3);
11990
11991 ctx.close().await;
11992 mock_ctx.close().await;
11993 }
11994
11995 #[tokio::test(flavor = "multi_thread")]
11996 async fn test_extra_newlines_between_operations_edit_line() {
11997 let initial_source = "@settings(defaultLengthUnit = mm)
11999
12000
12001sketch001 = sketch(on = XY) {
12002
12003 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12004
12005}
12006";
12007
12008 let program = Program::parse(initial_source).unwrap().0.unwrap();
12009 let mut frontend = FrontendState::new();
12010
12011 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12012 let mock_ctx = ExecutorContext::new_mock(None).await;
12013 let version = Version(0);
12014 let project_id = ProjectId(0);
12015 let file_id = FileId(0);
12016
12017 frontend.hack_set_program(&ctx, program).await.unwrap();
12018 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12019 let sketch_id = sketch_object.id;
12020 let sketch = expect_sketch(sketch_object);
12021
12022 let line_id = sketch
12024 .segments
12025 .iter()
12026 .copied()
12027 .find(|seg_id| {
12028 matches!(
12029 &frontend.scene_graph.objects[seg_id.0].kind,
12030 ObjectKind::Segment {
12031 segment: Segment::Line(_)
12032 }
12033 )
12034 })
12035 .expect("Expected a line segment in sketch");
12036
12037 frontend
12039 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12040 .await
12041 .unwrap();
12042
12043 let line_ctor = LineCtor {
12045 start: Point2d {
12046 x: Expr::Var(Number {
12047 value: 1.0,
12048 units: NumericSuffix::Mm,
12049 }),
12050 y: Expr::Var(Number {
12051 value: 2.0,
12052 units: NumericSuffix::Mm,
12053 }),
12054 },
12055 end: Point2d {
12056 x: Expr::Var(Number {
12057 value: 13.0,
12058 units: NumericSuffix::Mm,
12059 }),
12060 y: Expr::Var(Number {
12061 value: 14.0,
12062 units: NumericSuffix::Mm,
12063 }),
12064 },
12065 construction: None,
12066 };
12067 let segments = vec![ExistingSegmentCtor {
12068 id: line_id,
12069 ctor: SegmentCtor::Line(line_ctor),
12070 }];
12071 let (src_delta, _scene_delta) = frontend
12072 .edit_segments(&mock_ctx, version, sketch_id, segments)
12073 .await
12074 .unwrap();
12075 assert!(
12076 src_delta
12077 .text
12078 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
12079 "Expected edited line in source, got: {}",
12080 src_delta.text
12081 );
12082
12083 ctx.close().await;
12084 mock_ctx.close().await;
12085 }
12086
12087 #[tokio::test(flavor = "multi_thread")]
12088 async fn test_extra_newlines_delete_segment() {
12089 let initial_source = "@settings(defaultLengthUnit = mm)
12091
12092
12093
12094sketch001 = sketch(on = XY) {
12095 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
12096}
12097";
12098
12099 let program = Program::parse(initial_source).unwrap().0.unwrap();
12100 let mut frontend = FrontendState::new();
12101
12102 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12103 let mock_ctx = ExecutorContext::new_mock(None).await;
12104 let version = Version(0);
12105
12106 frontend.hack_set_program(&ctx, program).await.unwrap();
12107 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12108 let sketch_id = sketch_object.id;
12109 let sketch = expect_sketch(sketch_object);
12110
12111 assert_eq!(sketch.segments.len(), 3);
12113 let circle_id = sketch.segments[2];
12114
12115 let (src_delta, scene_delta) = frontend
12117 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
12118 .await
12119 .unwrap();
12120 assert!(
12121 src_delta.text.contains("sketch(on = XY) {"),
12122 "Expected sketch block in source, got: {}",
12123 src_delta.text
12124 );
12125 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12126 let new_sketch = expect_sketch(new_sketch_object);
12127 assert_eq!(new_sketch.segments.len(), 0);
12128
12129 ctx.close().await;
12130 mock_ctx.close().await;
12131 }
12132
12133 #[tokio::test(flavor = "multi_thread")]
12134 async fn test_unformatted_source_add_arc() {
12135 let initial_source = "@settings(defaultLengthUnit = mm)
12137
12138
12139
12140
12141sketch001 = sketch(on = XY) {
12142}
12143";
12144
12145 let program = Program::parse(initial_source).unwrap().0.unwrap();
12146 let mut frontend = FrontendState::new();
12147
12148 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12149 let mock_ctx = ExecutorContext::new_mock(None).await;
12150 let version = Version(0);
12151
12152 frontend.hack_set_program(&ctx, program).await.unwrap();
12153 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12154 let sketch_id = sketch_object.id;
12155
12156 let arc_ctor = ArcCtor {
12157 start: Point2d {
12158 x: Expr::Var(Number {
12159 value: 5.0,
12160 units: NumericSuffix::Mm,
12161 }),
12162 y: Expr::Var(Number {
12163 value: 0.0,
12164 units: NumericSuffix::Mm,
12165 }),
12166 },
12167 end: Point2d {
12168 x: Expr::Var(Number {
12169 value: 0.0,
12170 units: NumericSuffix::Mm,
12171 }),
12172 y: Expr::Var(Number {
12173 value: 5.0,
12174 units: NumericSuffix::Mm,
12175 }),
12176 },
12177 center: Point2d {
12178 x: Expr::Var(Number {
12179 value: 0.0,
12180 units: NumericSuffix::Mm,
12181 }),
12182 y: Expr::Var(Number {
12183 value: 0.0,
12184 units: NumericSuffix::Mm,
12185 }),
12186 },
12187 construction: None,
12188 };
12189 let segment = SegmentCtor::Arc(arc_ctor);
12190 let (src_delta, scene_delta) = frontend
12191 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12192 .await
12193 .unwrap();
12194 assert!(
12195 src_delta
12196 .text
12197 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
12198 "Expected arc in source, got: {}",
12199 src_delta.text
12200 );
12201 assert!(!scene_delta.new_objects.is_empty());
12202
12203 ctx.close().await;
12204 mock_ctx.close().await;
12205 }
12206
12207 #[tokio::test(flavor = "multi_thread")]
12208 async fn test_extra_newlines_add_circle() {
12209 let initial_source = "@settings(defaultLengthUnit = mm)
12211
12212
12213
12214sketch001 = sketch(on = XY) {
12215}
12216";
12217
12218 let program = Program::parse(initial_source).unwrap().0.unwrap();
12219 let mut frontend = FrontendState::new();
12220
12221 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12222 let mock_ctx = ExecutorContext::new_mock(None).await;
12223 let version = Version(0);
12224
12225 frontend.hack_set_program(&ctx, program).await.unwrap();
12226 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12227 let sketch_id = sketch_object.id;
12228
12229 let circle_ctor = CircleCtor {
12230 start: Point2d {
12231 x: Expr::Var(Number {
12232 value: 5.0,
12233 units: NumericSuffix::Mm,
12234 }),
12235 y: Expr::Var(Number {
12236 value: 0.0,
12237 units: NumericSuffix::Mm,
12238 }),
12239 },
12240 center: Point2d {
12241 x: Expr::Var(Number {
12242 value: 0.0,
12243 units: NumericSuffix::Mm,
12244 }),
12245 y: Expr::Var(Number {
12246 value: 0.0,
12247 units: NumericSuffix::Mm,
12248 }),
12249 },
12250 construction: None,
12251 };
12252 let segment = SegmentCtor::Circle(circle_ctor);
12253 let (src_delta, scene_delta) = frontend
12254 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12255 .await
12256 .unwrap();
12257 assert!(
12258 src_delta
12259 .text
12260 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
12261 "Expected circle in source, got: {}",
12262 src_delta.text
12263 );
12264 assert!(!scene_delta.new_objects.is_empty());
12265
12266 ctx.close().await;
12267 mock_ctx.close().await;
12268 }
12269
12270 #[tokio::test(flavor = "multi_thread")]
12271 async fn test_extra_newlines_add_constraint() {
12272 let initial_source = "@settings(defaultLengthUnit = mm)
12274
12275
12276
12277sketch001 = sketch(on = XY) {
12278 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12279 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
12280}
12281";
12282
12283 let program = Program::parse(initial_source).unwrap().0.unwrap();
12284 let mut frontend = FrontendState::new();
12285
12286 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12287 let mock_ctx = ExecutorContext::new_mock(None).await;
12288 let version = Version(0);
12289 let project_id = ProjectId(0);
12290 let file_id = FileId(0);
12291
12292 frontend.hack_set_program(&ctx, program).await.unwrap();
12293 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12294 let sketch_id = sketch_object.id;
12295 let sketch = expect_sketch(sketch_object);
12296
12297 let line_ids: Vec<ObjectId> = sketch
12299 .segments
12300 .iter()
12301 .copied()
12302 .filter(|seg_id| {
12303 matches!(
12304 &frontend.scene_graph.objects[seg_id.0].kind,
12305 ObjectKind::Segment {
12306 segment: Segment::Line(_)
12307 }
12308 )
12309 })
12310 .collect();
12311 assert_eq!(line_ids.len(), 2, "Expected two line segments");
12312
12313 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
12314 let ObjectKind::Segment {
12315 segment: Segment::Line(line1_data),
12316 } = &line1.kind
12317 else {
12318 panic!("Expected line");
12319 };
12320 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
12321 let ObjectKind::Segment {
12322 segment: Segment::Line(line2_data),
12323 } = &line2.kind
12324 else {
12325 panic!("Expected line");
12326 };
12327
12328 let constraint = Constraint::Coincident(Coincident {
12330 segments: vec![line1_data.end.into(), line2_data.start.into()],
12331 });
12332
12333 frontend
12335 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12336 .await
12337 .unwrap();
12338 let (src_delta, _scene_delta) = frontend
12339 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12340 .await
12341 .unwrap();
12342 assert!(
12343 src_delta.text.contains("coincident("),
12344 "Expected coincident constraint in source, got: {}",
12345 src_delta.text
12346 );
12347
12348 ctx.close().await;
12349 mock_ctx.close().await;
12350 }
12351
12352 #[tokio::test(flavor = "multi_thread")]
12353 async fn test_extra_newlines_add_line_then_edit_line() {
12354 let initial_source = "@settings(defaultLengthUnit = mm)
12356
12357
12358
12359sketch001 = sketch(on = XY) {
12360}
12361";
12362
12363 let program = Program::parse(initial_source).unwrap().0.unwrap();
12364 let mut frontend = FrontendState::new();
12365
12366 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12367 let mock_ctx = ExecutorContext::new_mock(None).await;
12368 let version = Version(0);
12369
12370 frontend.hack_set_program(&ctx, program).await.unwrap();
12371 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12372 let sketch_id = sketch_object.id;
12373
12374 let line_ctor = LineCtor {
12376 start: Point2d {
12377 x: Expr::Number(Number {
12378 value: 0.0,
12379 units: NumericSuffix::Mm,
12380 }),
12381 y: Expr::Number(Number {
12382 value: 0.0,
12383 units: NumericSuffix::Mm,
12384 }),
12385 },
12386 end: Point2d {
12387 x: Expr::Number(Number {
12388 value: 10.0,
12389 units: NumericSuffix::Mm,
12390 }),
12391 y: Expr::Number(Number {
12392 value: 10.0,
12393 units: NumericSuffix::Mm,
12394 }),
12395 },
12396 construction: None,
12397 };
12398 let segment = SegmentCtor::Line(line_ctor);
12399 let (src_delta, scene_delta) = frontend
12400 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12401 .await
12402 .unwrap();
12403 assert!(
12404 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12405 "Expected line in source after add, got: {}",
12406 src_delta.text
12407 );
12408 let line_id = *scene_delta.new_objects.last().unwrap();
12410
12411 let line_ctor = LineCtor {
12413 start: Point2d {
12414 x: Expr::Number(Number {
12415 value: 1.0,
12416 units: NumericSuffix::Mm,
12417 }),
12418 y: Expr::Number(Number {
12419 value: 2.0,
12420 units: NumericSuffix::Mm,
12421 }),
12422 },
12423 end: Point2d {
12424 x: Expr::Number(Number {
12425 value: 13.0,
12426 units: NumericSuffix::Mm,
12427 }),
12428 y: Expr::Number(Number {
12429 value: 14.0,
12430 units: NumericSuffix::Mm,
12431 }),
12432 },
12433 construction: None,
12434 };
12435 let segments = vec![ExistingSegmentCtor {
12436 id: line_id,
12437 ctor: SegmentCtor::Line(line_ctor),
12438 }];
12439 let (src_delta, scene_delta) = frontend
12440 .edit_segments(&mock_ctx, version, sketch_id, segments)
12441 .await
12442 .unwrap();
12443 assert!(
12444 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
12445 "Expected edited line in source, got: {}",
12446 src_delta.text
12447 );
12448 assert_eq!(scene_delta.new_objects, vec![]);
12449
12450 ctx.close().await;
12451 mock_ctx.close().await;
12452 }
12453}