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::Object;
43use crate::front::Parallel;
44use crate::front::Perpendicular;
45use crate::front::PointCtor;
46use crate::front::Tangent;
47use crate::frontend::api::Expr;
48use crate::frontend::api::FileId;
49use crate::frontend::api::Number;
50use crate::frontend::api::ObjectId;
51use crate::frontend::api::ObjectKind;
52use crate::frontend::api::Plane;
53use crate::frontend::api::ProjectId;
54use crate::frontend::api::RestoreSketchCheckpointOutcome;
55use crate::frontend::api::SceneGraph;
56use crate::frontend::api::SceneGraphDelta;
57use crate::frontend::api::SketchCheckpointId;
58use crate::frontend::api::SourceDelta;
59use crate::frontend::api::SourceRef;
60use crate::frontend::api::Version;
61use crate::frontend::modify::find_defined_names;
62use crate::frontend::modify::next_free_name;
63use crate::frontend::modify::next_free_name_with_padding;
64use crate::frontend::sketch::Coincident;
65use crate::frontend::sketch::Constraint;
66use crate::frontend::sketch::ConstraintSegment;
67use crate::frontend::sketch::Diameter;
68use crate::frontend::sketch::ExistingSegmentCtor;
69use crate::frontend::sketch::Horizontal;
70use crate::frontend::sketch::LineCtor;
71use crate::frontend::sketch::Point2d;
72use crate::frontend::sketch::Radius;
73use crate::frontend::sketch::Segment;
74use crate::frontend::sketch::SegmentCtor;
75use crate::frontend::sketch::SketchApi;
76use crate::frontend::sketch::SketchCtor;
77use crate::frontend::sketch::Vertical;
78use crate::frontend::traverse::MutateBodyItem;
79use crate::frontend::traverse::TraversalReturn;
80use crate::frontend::traverse::Visitor;
81use crate::frontend::traverse::dfs_mut;
82use crate::id::IncIdGenerator;
83use crate::parsing::ast::types as ast;
84use crate::pretty::NumericSuffix;
85use crate::std::constraints::LinesAtAngleKind;
86use crate::walk::NodeMut;
87use crate::walk::Visitable;
88
89pub(crate) mod api;
90pub(crate) mod modify;
91pub(crate) mod sketch;
92
93pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
94
95#[derive(Debug, Clone)]
96struct SketchCheckpoint {
97 id: SketchCheckpointId,
98 source: SourceDelta,
99 program: Program,
100 scene_graph: SceneGraph,
101 exec_outcome: ExecOutcome,
102 point_freedom_cache: HashMap<ObjectId, Freedom>,
103 mock_memory: Option<SketchModeState>,
104}
105mod traverse;
106pub(crate) mod trim;
107
108struct ArcSizeConstraintParams {
109 points: Vec<ObjectId>,
110 function_name: &'static str,
111 value: f64,
112 units: NumericSuffix,
113 constraint_type_name: &'static str,
114}
115
116const POINT_FN: &str = "point";
117const POINT_AT_PARAM: &str = "at";
118const LINE_FN: &str = "line";
119const LINE_START_PARAM: &str = "start";
120const LINE_END_PARAM: &str = "end";
121const ARC_FN: &str = "arc";
122const ARC_START_PARAM: &str = "start";
123const ARC_END_PARAM: &str = "end";
124const ARC_CENTER_PARAM: &str = "center";
125const CIRCLE_FN: &str = "circle";
126const CIRCLE_VARIABLE: &str = "circle";
127const CIRCLE_START_PARAM: &str = "start";
128const CIRCLE_CENTER_PARAM: &str = "center";
129
130const COINCIDENT_FN: &str = "coincident";
131const DIAMETER_FN: &str = "diameter";
132const DISTANCE_FN: &str = "distance";
133const FIXED_FN: &str = "fixed";
134const ANGLE_FN: &str = "angle";
135const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
136const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
137const EQUAL_LENGTH_FN: &str = "equalLength";
138const EQUAL_RADIUS_FN: &str = "equalRadius";
139const HORIZONTAL_FN: &str = "horizontal";
140const RADIUS_FN: &str = "radius";
141const TANGENT_FN: &str = "tangent";
142const VERTICAL_FN: &str = "vertical";
143
144const LINE_PROPERTY_START: &str = "start";
145const LINE_PROPERTY_END: &str = "end";
146
147const ARC_PROPERTY_START: &str = "start";
148const ARC_PROPERTY_END: &str = "end";
149const ARC_PROPERTY_CENTER: &str = "center";
150const CIRCLE_PROPERTY_START: &str = "start";
151const CIRCLE_PROPERTY_CENTER: &str = "center";
152
153const CONSTRUCTION_PARAM: &str = "construction";
154
155#[derive(Debug, Clone, Copy)]
156enum EditDeleteKind {
157 Edit,
158 DeleteNonSketch,
159}
160
161impl EditDeleteKind {
162 fn is_delete(&self) -> bool {
164 match self {
165 EditDeleteKind::Edit => false,
166 EditDeleteKind::DeleteNonSketch => true,
167 }
168 }
169
170 fn to_change_kind(self) -> ChangeKind {
171 match self {
172 EditDeleteKind::Edit => ChangeKind::Edit,
173 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
174 }
175 }
176}
177
178#[derive(Debug, Clone, Copy)]
179enum ChangeKind {
180 Add,
181 Edit,
182 Delete,
183 None,
184}
185
186#[derive(Debug, Clone, Serialize, ts_rs::TS)]
187#[ts(export, export_to = "FrontendApi.ts")]
188#[serde(tag = "type")]
189pub enum SetProgramOutcome {
190 #[serde(rename_all = "camelCase")]
191 Success {
192 scene_graph: Box<SceneGraph>,
193 exec_outcome: Box<ExecOutcome>,
194 checkpoint_id: Option<SketchCheckpointId>,
195 },
196 #[serde(rename_all = "camelCase")]
197 ExecFailure { error: Box<KclErrorWithOutputs> },
198}
199
200#[derive(Debug, Clone)]
201pub struct FrontendState {
202 program: Program,
203 scene_graph: SceneGraph,
204 point_freedom_cache: HashMap<ObjectId, Freedom>,
207 sketch_checkpoints: VecDeque<SketchCheckpoint>,
208 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
209}
210
211impl Default for FrontendState {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217impl FrontendState {
218 pub fn new() -> Self {
219 Self {
220 program: Program::empty(),
221 scene_graph: SceneGraph {
222 project: ProjectId(0),
223 file: FileId(0),
224 version: Version(0),
225 objects: Default::default(),
226 settings: Default::default(),
227 sketch_mode: Default::default(),
228 },
229 point_freedom_cache: HashMap::new(),
230 sketch_checkpoints: VecDeque::new(),
231 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
232 }
233 }
234
235 pub fn scene_graph(&self) -> &SceneGraph {
237 &self.scene_graph
238 }
239
240 pub fn default_length_unit(&self) -> UnitLength {
241 self.program
242 .meta_settings()
243 .ok()
244 .flatten()
245 .map(|settings| settings.default_length_units)
246 .unwrap_or(UnitLength::Millimeters)
247 }
248
249 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
250 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
251
252 let checkpoint = SketchCheckpoint {
253 id: checkpoint_id,
254 source: SourceDelta {
255 text: source_from_ast(&self.program.ast),
256 },
257 program: self.program.clone(),
258 scene_graph: self.scene_graph.clone(),
259 exec_outcome,
260 point_freedom_cache: self.point_freedom_cache.clone(),
261 mock_memory: read_old_memory().await,
262 };
263
264 self.sketch_checkpoints.push_back(checkpoint);
265 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
266 self.sketch_checkpoints.pop_front();
267 }
268
269 Ok(checkpoint_id)
270 }
271
272 pub async fn restore_sketch_checkpoint(
273 &mut self,
274 checkpoint_id: SketchCheckpointId,
275 ) -> api::Result<RestoreSketchCheckpointOutcome> {
276 let checkpoint = self
277 .sketch_checkpoints
278 .iter()
279 .find(|checkpoint| checkpoint.id == checkpoint_id)
280 .cloned()
281 .ok_or_else(|| Error {
282 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
283 })?;
284
285 self.program = checkpoint.program;
286 self.scene_graph = checkpoint.scene_graph.clone();
287 self.point_freedom_cache = checkpoint.point_freedom_cache;
288
289 if let Some(mock_memory) = checkpoint.mock_memory {
290 write_old_memory(mock_memory).await;
291 } else {
292 clear_mem_cache().await;
293 }
294
295 Ok(RestoreSketchCheckpointOutcome {
296 source_delta: checkpoint.source,
297 scene_graph_delta: SceneGraphDelta {
298 new_graph: checkpoint.scene_graph,
299 new_objects: Vec::new(),
300 invalidates_ids: true,
301 exec_outcome: checkpoint.exec_outcome,
302 },
303 })
304 }
305
306 pub fn clear_sketch_checkpoints(&mut self) {
307 self.sketch_checkpoints.clear();
308 }
309}
310
311impl SketchApi for FrontendState {
312 async fn execute_mock(
313 &mut self,
314 ctx: &ExecutorContext,
315 _version: Version,
316 sketch: ObjectId,
317 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
318 let sketch_block_ref =
319 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
320
321 let mut truncated_program = self.program.clone();
322 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
323 .map_err(KclErrorWithOutputs::no_outputs)?;
324
325 let outcome = ctx
327 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
328 .await?;
329 let new_source = source_from_ast(&self.program.ast);
330 let src_delta = SourceDelta { text: new_source };
331 let outcome = self.update_state_after_exec(outcome, true);
333 let scene_graph_delta = SceneGraphDelta {
334 new_graph: self.scene_graph.clone(),
335 new_objects: Default::default(),
336 invalidates_ids: false,
337 exec_outcome: outcome,
338 };
339 Ok((src_delta, scene_graph_delta))
340 }
341
342 async fn new_sketch(
343 &mut self,
344 ctx: &ExecutorContext,
345 _project: ProjectId,
346 _file: FileId,
347 _version: Version,
348 args: SketchCtor,
349 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
350 let mut new_ast = self.program.ast.clone();
353 let mut plane_ast =
355 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
356 let mut defined_names = find_defined_names(&new_ast);
357 let is_face_of_expr = matches!(
358 &plane_ast,
359 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
360 );
361 if is_face_of_expr {
362 let face_name = next_free_name_with_padding("face", &defined_names)
363 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
364 let face_decl = ast::VariableDeclaration::new(
365 ast::VariableDeclarator::new(&face_name, plane_ast),
366 ast::ItemVisibility::Default,
367 ast::VariableKind::Const,
368 );
369 new_ast
370 .body
371 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
372 face_decl,
373 ))));
374 defined_names.insert(face_name.clone());
375 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
376 }
377 let sketch_ast = ast::SketchBlock {
378 arguments: vec![ast::LabeledArg {
379 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
380 arg: plane_ast,
381 }],
382 body: Default::default(),
383 is_being_edited: false,
384 non_code_meta: Default::default(),
385 digest: None,
386 };
387 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
390 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
391 let sketch_decl = ast::VariableDeclaration::new(
392 ast::VariableDeclarator::new(
393 &sketch_name,
394 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
395 ),
396 ast::ItemVisibility::Default,
397 ast::VariableKind::Const,
398 );
399 new_ast
400 .body
401 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
402 sketch_decl,
403 ))));
404 let new_source = source_from_ast(&new_ast);
406 let (new_program, errors) = Program::parse(&new_source)
408 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
409 if !errors.is_empty() {
410 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
411 "Error parsing KCL source after adding sketch: {errors:?}"
412 ))));
413 }
414 let Some(new_program) = new_program else {
415 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
416 "No AST produced after adding sketch".to_owned(),
417 )));
418 };
419
420 self.program = new_program.clone();
422
423 let outcome = ctx.run_with_caching(new_program.clone()).await?;
426 let freedom_analysis_ran = true;
427
428 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
429
430 let Some(sketch_id) = self
431 .scene_graph
432 .objects
433 .iter()
434 .filter_map(|object| match object.kind {
435 ObjectKind::Sketch(_) => Some(object.id),
436 _ => None,
437 })
438 .max_by_key(|id| id.0)
439 else {
440 return Err(KclErrorWithOutputs::from_error_outcome(
441 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
442 outcome,
443 ));
444 };
445 self.scene_graph.sketch_mode = Some(sketch_id);
447
448 let src_delta = SourceDelta { text: new_source };
449 let scene_graph_delta = SceneGraphDelta {
450 new_graph: self.scene_graph.clone(),
451 invalidates_ids: false,
452 new_objects: vec![sketch_id],
453 exec_outcome: outcome,
454 };
455 Ok((src_delta, scene_graph_delta, sketch_id))
456 }
457
458 async fn edit_sketch(
459 &mut self,
460 ctx: &ExecutorContext,
461 _project: ProjectId,
462 _file: FileId,
463 _version: Version,
464 sketch: ObjectId,
465 ) -> ExecResult<SceneGraphDelta> {
466 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
470 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
471 })?;
472 let ObjectKind::Sketch(_) = &sketch_object.kind else {
473 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
474 "Object is not a sketch, it is {}",
475 sketch_object.kind.human_friendly_kind_with_article()
476 ))));
477 };
478 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
479
480 self.scene_graph.sketch_mode = Some(sketch);
482
483 let mut truncated_program = self.program.clone();
485 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
486 .map_err(KclErrorWithOutputs::no_outputs)?;
487
488 let outcome = ctx
491 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
492 .await?;
493
494 let outcome = self.update_state_after_exec(outcome, true);
496 let scene_graph_delta = SceneGraphDelta {
497 new_graph: self.scene_graph.clone(),
498 invalidates_ids: false,
499 new_objects: Vec::new(),
500 exec_outcome: outcome,
501 };
502 Ok(scene_graph_delta)
503 }
504
505 async fn exit_sketch(
506 &mut self,
507 ctx: &ExecutorContext,
508 _version: Version,
509 sketch: ObjectId,
510 ) -> ExecResult<SceneGraph> {
511 #[cfg(not(target_arch = "wasm32"))]
513 let _ = sketch;
514 #[cfg(target_arch = "wasm32")]
515 if self.scene_graph.sketch_mode != Some(sketch) {
516 web_sys::console::warn_1(
517 &format!(
518 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
519 &self.scene_graph.sketch_mode
520 )
521 .into(),
522 );
523 }
524 self.scene_graph.sketch_mode = None;
525
526 let outcome = ctx.run_with_caching(self.program.clone()).await?;
528
529 self.update_state_after_exec(outcome, false);
531
532 Ok(self.scene_graph.clone())
533 }
534
535 async fn delete_sketch(
536 &mut self,
537 ctx: &ExecutorContext,
538 _version: Version,
539 sketch: ObjectId,
540 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
541 let mut new_ast = self.program.ast.clone();
544
545 let sketch_id = sketch;
547 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
548 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
549 })?;
550 let ObjectKind::Sketch(_) = &sketch_object.kind else {
551 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
552 "Object is not a sketch, it is {}",
553 sketch_object.kind.human_friendly_kind_with_article(),
554 ))));
555 };
556
557 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
559 .map_err(KclErrorWithOutputs::no_outputs)?;
560
561 self.execute_after_delete_sketch(ctx, &mut new_ast).await
562 }
563
564 async fn add_segment(
565 &mut self,
566 ctx: &ExecutorContext,
567 _version: Version,
568 sketch: ObjectId,
569 segment: SegmentCtor,
570 _label: Option<String>,
571 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
572 match segment {
574 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
575 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
576 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
577 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
578 }
579 }
580
581 async fn edit_segments(
582 &mut self,
583 ctx: &ExecutorContext,
584 _version: Version,
585 sketch: ObjectId,
586 segments: Vec<ExistingSegmentCtor>,
587 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
588 let sketch_block_ref =
590 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
591
592 let mut new_ast = self.program.ast.clone();
593 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
594
595 for segment in &segments {
598 segment_ids_edited.insert(segment.id);
599 }
600
601 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
616
617 for segment in segments {
618 let segment_id = segment.id;
619 match segment.ctor {
620 SegmentCtor::Point(ctor) => {
621 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
623 && let ObjectKind::Segment { segment } = &segment_object.kind
624 && let Segment::Point(point) = segment
625 && let Some(owner_id) = point.owner
626 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
627 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
628 {
629 match owner_segment {
630 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
631 if let Some(existing) = final_edits.get_mut(&owner_id) {
632 let SegmentCtor::Line(line_ctor) = existing else {
633 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
634 "Internal: Expected line ctor for owner, but found {}",
635 existing.human_friendly_kind_with_article()
636 ))));
637 };
638 if line.start == segment_id {
640 line_ctor.start = ctor.position;
641 } else {
642 line_ctor.end = ctor.position;
643 }
644 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
645 let mut line_ctor = line_ctor.clone();
647 if line.start == segment_id {
648 line_ctor.start = ctor.position;
649 } else {
650 line_ctor.end = ctor.position;
651 }
652 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
653 } else {
654 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
656 "Internal: Line does not have line ctor, but found {}",
657 line.ctor.human_friendly_kind_with_article()
658 ))));
659 }
660 continue;
661 }
662 Segment::Arc(arc)
663 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
664 {
665 if let Some(existing) = final_edits.get_mut(&owner_id) {
666 let SegmentCtor::Arc(arc_ctor) = existing else {
667 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
668 "Internal: Expected arc ctor for owner, but found {}",
669 existing.human_friendly_kind_with_article()
670 ))));
671 };
672 if arc.start == segment_id {
673 arc_ctor.start = ctor.position;
674 } else if arc.end == segment_id {
675 arc_ctor.end = ctor.position;
676 } else {
677 arc_ctor.center = ctor.position;
678 }
679 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
680 let mut arc_ctor = arc_ctor.clone();
681 if arc.start == segment_id {
682 arc_ctor.start = ctor.position;
683 } else if arc.end == segment_id {
684 arc_ctor.end = ctor.position;
685 } else {
686 arc_ctor.center = ctor.position;
687 }
688 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
689 } else {
690 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
691 "Internal: Arc does not have arc ctor, but found {}",
692 arc.ctor.human_friendly_kind_with_article()
693 ))));
694 }
695 continue;
696 }
697 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
698 if let Some(existing) = final_edits.get_mut(&owner_id) {
699 let SegmentCtor::Circle(circle_ctor) = existing else {
700 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
701 "Internal: Expected circle ctor for owner, but found {}",
702 existing.human_friendly_kind_with_article()
703 ))));
704 };
705 if circle.start == segment_id {
706 circle_ctor.start = ctor.position;
707 } else {
708 circle_ctor.center = ctor.position;
709 }
710 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
711 let mut circle_ctor = circle_ctor.clone();
712 if circle.start == segment_id {
713 circle_ctor.start = ctor.position;
714 } else {
715 circle_ctor.center = ctor.position;
716 }
717 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
718 } else {
719 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
720 "Internal: Circle does not have circle ctor, but found {}",
721 circle.ctor.human_friendly_kind_with_article()
722 ))));
723 }
724 continue;
725 }
726 _ => {}
727 }
728 }
729
730 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
732 }
733 SegmentCtor::Line(ctor) => {
734 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
735 }
736 SegmentCtor::Arc(ctor) => {
737 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
738 }
739 SegmentCtor::Circle(ctor) => {
740 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
741 }
742 }
743 }
744
745 for (segment_id, ctor) in final_edits {
746 match ctor {
747 SegmentCtor::Point(ctor) => self
748 .edit_point(&mut new_ast, sketch, segment_id, ctor)
749 .map_err(KclErrorWithOutputs::no_outputs)?,
750 SegmentCtor::Line(ctor) => self
751 .edit_line(&mut new_ast, sketch, segment_id, ctor)
752 .map_err(KclErrorWithOutputs::no_outputs)?,
753 SegmentCtor::Arc(ctor) => self
754 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
755 .map_err(KclErrorWithOutputs::no_outputs)?,
756 SegmentCtor::Circle(ctor) => self
757 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
758 .map_err(KclErrorWithOutputs::no_outputs)?,
759 }
760 }
761 self.execute_after_edit(
762 ctx,
763 sketch,
764 sketch_block_ref,
765 segment_ids_edited,
766 EditDeleteKind::Edit,
767 &mut new_ast,
768 )
769 .await
770 }
771
772 async fn delete_objects(
773 &mut self,
774 ctx: &ExecutorContext,
775 _version: Version,
776 sketch: ObjectId,
777 constraint_ids: Vec<ObjectId>,
778 segment_ids: Vec<ObjectId>,
779 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
780 let sketch_block_ref =
782 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
783
784 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
786 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
787
788 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
791
792 for segment_id in segment_ids_set.iter().copied() {
793 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
794 && let ObjectKind::Segment { segment } = &segment_object.kind
795 && let Segment::Point(point) = segment
796 && let Some(owner_id) = point.owner
797 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
798 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
799 && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
800 {
801 resolved_segment_ids_to_delete.insert(owner_id);
803 } else {
804 resolved_segment_ids_to_delete.insert(segment_id);
806 }
807 }
808 let referenced_constraint_ids = self
809 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
810 .map_err(KclErrorWithOutputs::no_outputs)?;
811
812 let mut new_ast = self.program.ast.clone();
813
814 for constraint_id in referenced_constraint_ids {
815 if constraint_ids_set.contains(&constraint_id) {
816 continue;
817 }
818
819 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
820 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
821 })?;
822 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
823 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
824 "Object is not a constraint, it is {}",
825 constraint_object.kind.human_friendly_kind_with_article()
826 ))));
827 };
828
829 match constraint {
830 Constraint::EqualRadius(equal_radius) => {
831 let remaining_input = equal_radius
832 .input
833 .iter()
834 .copied()
835 .filter(|segment_id| !resolved_segment_ids_to_delete.contains(segment_id))
836 .collect::<Vec<_>>();
837
838 if remaining_input.len() >= 2 {
839 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
840 .map_err(KclErrorWithOutputs::no_outputs)?;
841 } else {
842 constraint_ids_set.insert(constraint_id);
843 }
844 }
845 Constraint::LinesEqualLength(lines_equal_length) => {
846 let remaining_lines = lines_equal_length
847 .lines
848 .iter()
849 .copied()
850 .filter(|line_id| !resolved_segment_ids_to_delete.contains(line_id))
851 .collect::<Vec<_>>();
852
853 if remaining_lines.len() >= 2 {
855 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
856 .map_err(KclErrorWithOutputs::no_outputs)?;
857 } else {
858 constraint_ids_set.insert(constraint_id);
859 }
860 }
861 Constraint::Parallel(parallel) => {
862 let remaining_lines = parallel
863 .lines
864 .iter()
865 .copied()
866 .filter(|line_id| !resolved_segment_ids_to_delete.contains(line_id))
867 .collect::<Vec<_>>();
868
869 if remaining_lines.len() >= 2 {
870 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
871 .map_err(KclErrorWithOutputs::no_outputs)?;
872 } else {
873 constraint_ids_set.insert(constraint_id);
874 }
875 }
876 _ => {
877 constraint_ids_set.insert(constraint_id);
879 }
880 }
881 }
882
883 for constraint_id in constraint_ids_set {
884 self.delete_constraint(&mut new_ast, sketch, constraint_id)
885 .map_err(KclErrorWithOutputs::no_outputs)?;
886 }
887 for segment_id in resolved_segment_ids_to_delete {
888 self.delete_segment(&mut new_ast, sketch, segment_id)
889 .map_err(KclErrorWithOutputs::no_outputs)?;
890 }
891
892 self.execute_after_edit(
893 ctx,
894 sketch,
895 sketch_block_ref,
896 Default::default(),
897 EditDeleteKind::DeleteNonSketch,
898 &mut new_ast,
899 )
900 .await
901 }
902
903 async fn add_constraint(
904 &mut self,
905 ctx: &ExecutorContext,
906 _version: Version,
907 sketch: ObjectId,
908 constraint: Constraint,
909 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
910 let original_program = self.program.clone();
914 let original_scene_graph = self.scene_graph.clone();
915
916 let mut new_ast = self.program.ast.clone();
917 let sketch_block_ref = match constraint {
918 Constraint::Coincident(coincident) => self
919 .add_coincident(sketch, coincident, &mut new_ast)
920 .await
921 .map_err(KclErrorWithOutputs::no_outputs)?,
922 Constraint::Distance(distance) => self
923 .add_distance(sketch, distance, &mut new_ast)
924 .await
925 .map_err(KclErrorWithOutputs::no_outputs)?,
926 Constraint::EqualRadius(equal_radius) => self
927 .add_equal_radius(sketch, equal_radius, &mut new_ast)
928 .await
929 .map_err(KclErrorWithOutputs::no_outputs)?,
930 Constraint::Fixed(fixed) => self
931 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
932 .await
933 .map_err(KclErrorWithOutputs::no_outputs)?,
934 Constraint::HorizontalDistance(distance) => self
935 .add_horizontal_distance(sketch, distance, &mut new_ast)
936 .await
937 .map_err(KclErrorWithOutputs::no_outputs)?,
938 Constraint::VerticalDistance(distance) => self
939 .add_vertical_distance(sketch, distance, &mut new_ast)
940 .await
941 .map_err(KclErrorWithOutputs::no_outputs)?,
942 Constraint::Horizontal(horizontal) => self
943 .add_horizontal(sketch, horizontal, &mut new_ast)
944 .await
945 .map_err(KclErrorWithOutputs::no_outputs)?,
946 Constraint::LinesEqualLength(lines_equal_length) => self
947 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
948 .await
949 .map_err(KclErrorWithOutputs::no_outputs)?,
950 Constraint::Parallel(parallel) => self
951 .add_parallel(sketch, parallel, &mut new_ast)
952 .await
953 .map_err(KclErrorWithOutputs::no_outputs)?,
954 Constraint::Perpendicular(perpendicular) => self
955 .add_perpendicular(sketch, perpendicular, &mut new_ast)
956 .await
957 .map_err(KclErrorWithOutputs::no_outputs)?,
958 Constraint::Radius(radius) => self
959 .add_radius(sketch, radius, &mut new_ast)
960 .await
961 .map_err(KclErrorWithOutputs::no_outputs)?,
962 Constraint::Diameter(diameter) => self
963 .add_diameter(sketch, diameter, &mut new_ast)
964 .await
965 .map_err(KclErrorWithOutputs::no_outputs)?,
966 Constraint::Vertical(vertical) => self
967 .add_vertical(sketch, vertical, &mut new_ast)
968 .await
969 .map_err(KclErrorWithOutputs::no_outputs)?,
970 Constraint::Angle(lines_at_angle) => self
971 .add_angle(sketch, lines_at_angle, &mut new_ast)
972 .await
973 .map_err(KclErrorWithOutputs::no_outputs)?,
974 Constraint::Tangent(tangent) => self
975 .add_tangent(sketch, tangent, &mut new_ast)
976 .await
977 .map_err(KclErrorWithOutputs::no_outputs)?,
978 };
979
980 let result = self
981 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
982 .await;
983
984 if result.is_err() {
986 self.program = original_program;
987 self.scene_graph = original_scene_graph;
988 }
989
990 result
991 }
992
993 async fn chain_segment(
994 &mut self,
995 ctx: &ExecutorContext,
996 version: Version,
997 sketch: ObjectId,
998 previous_segment_end_point_id: ObjectId,
999 segment: SegmentCtor,
1000 _label: Option<String>,
1001 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1002 let SegmentCtor::Line(line_ctor) = segment else {
1006 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1007 "chain_segment currently only supports Line segments, got {}",
1008 segment.human_friendly_kind_with_article(),
1009 ))));
1010 };
1011
1012 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1014
1015 let new_line_id = first_scene_delta
1018 .new_objects
1019 .iter()
1020 .find(|&obj_id| {
1021 let obj = self.scene_graph.objects.get(obj_id.0);
1022 if let Some(obj) = obj {
1023 matches!(
1024 &obj.kind,
1025 ObjectKind::Segment {
1026 segment: Segment::Line(_)
1027 }
1028 )
1029 } else {
1030 false
1031 }
1032 })
1033 .ok_or_else(|| {
1034 KclErrorWithOutputs::no_outputs(KclError::refactor(
1035 "Failed to find new line segment in scene graph".to_string(),
1036 ))
1037 })?;
1038
1039 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1040 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1041 "New line object not found: {new_line_id:?}"
1042 )))
1043 })?;
1044
1045 let ObjectKind::Segment {
1046 segment: new_line_segment,
1047 } = &new_line_obj.kind
1048 else {
1049 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1050 "Object is not a segment: {new_line_obj:?}"
1051 ))));
1052 };
1053
1054 let Segment::Line(new_line) = new_line_segment else {
1055 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1056 "Segment is not a line: {new_line_segment:?}"
1057 ))));
1058 };
1059
1060 let new_line_start_point_id = new_line.start;
1061
1062 let coincident = Coincident {
1064 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1065 };
1066
1067 let (final_src_delta, final_scene_delta) = self
1068 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1069 .await?;
1070
1071 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1074 combined_new_objects.extend(final_scene_delta.new_objects);
1075
1076 let scene_graph_delta = SceneGraphDelta {
1077 new_graph: self.scene_graph.clone(),
1078 invalidates_ids: false,
1079 new_objects: combined_new_objects,
1080 exec_outcome: final_scene_delta.exec_outcome,
1081 };
1082
1083 Ok((final_src_delta, scene_graph_delta))
1084 }
1085
1086 async fn edit_constraint(
1087 &mut self,
1088 ctx: &ExecutorContext,
1089 _version: Version,
1090 sketch: ObjectId,
1091 constraint_id: ObjectId,
1092 value_expression: String,
1093 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1094 let sketch_block_ref =
1096 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1097
1098 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1099 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1100 })?;
1101 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1102 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1103 "Object is not a constraint: {constraint_id:?}"
1104 ))));
1105 }
1106
1107 let mut new_ast = self.program.ast.clone();
1108
1109 let (parsed, errors) = Program::parse(&value_expression)
1111 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1112 if !errors.is_empty() {
1113 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114 "Error parsing value expression: {errors:?}"
1115 ))));
1116 }
1117 let mut parsed = parsed.ok_or_else(|| {
1118 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1119 })?;
1120 if parsed.ast.body.is_empty() {
1121 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1122 "Empty value expression".to_string(),
1123 )));
1124 }
1125 let first = parsed.ast.body.remove(0);
1126 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1127 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1128 "Value expression must be a simple expression".to_string(),
1129 )));
1130 };
1131
1132 let new_value: ast::BinaryPart = expr_stmt
1133 .inner
1134 .expression
1135 .try_into()
1136 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1137
1138 self.mutate_ast(
1139 &mut new_ast,
1140 constraint_id,
1141 AstMutateCommand::EditConstraintValue { value: new_value },
1142 )
1143 .map_err(KclErrorWithOutputs::no_outputs)?;
1144
1145 self.execute_after_edit(
1146 ctx,
1147 sketch,
1148 sketch_block_ref,
1149 Default::default(),
1150 EditDeleteKind::Edit,
1151 &mut new_ast,
1152 )
1153 .await
1154 }
1155
1156 async fn batch_split_segment_operations(
1164 &mut self,
1165 ctx: &ExecutorContext,
1166 _version: Version,
1167 sketch: ObjectId,
1168 edit_segments: Vec<ExistingSegmentCtor>,
1169 add_constraints: Vec<Constraint>,
1170 delete_constraint_ids: Vec<ObjectId>,
1171 _new_segment_info: sketch::NewSegmentInfo,
1172 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1173 let sketch_block_ref =
1175 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1176
1177 let mut new_ast = self.program.ast.clone();
1178 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1179
1180 for segment in edit_segments {
1182 segment_ids_edited.insert(segment.id);
1183 match segment.ctor {
1184 SegmentCtor::Point(ctor) => self
1185 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1186 .map_err(KclErrorWithOutputs::no_outputs)?,
1187 SegmentCtor::Line(ctor) => self
1188 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1189 .map_err(KclErrorWithOutputs::no_outputs)?,
1190 SegmentCtor::Arc(ctor) => self
1191 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1192 .map_err(KclErrorWithOutputs::no_outputs)?,
1193 SegmentCtor::Circle(ctor) => self
1194 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1195 .map_err(KclErrorWithOutputs::no_outputs)?,
1196 }
1197 }
1198
1199 for constraint in add_constraints {
1201 match constraint {
1202 Constraint::Coincident(coincident) => {
1203 self.add_coincident(sketch, coincident, &mut new_ast)
1204 .await
1205 .map_err(KclErrorWithOutputs::no_outputs)?;
1206 }
1207 Constraint::Distance(distance) => {
1208 self.add_distance(sketch, distance, &mut new_ast)
1209 .await
1210 .map_err(KclErrorWithOutputs::no_outputs)?;
1211 }
1212 Constraint::EqualRadius(equal_radius) => {
1213 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1214 .await
1215 .map_err(KclErrorWithOutputs::no_outputs)?;
1216 }
1217 Constraint::Fixed(fixed) => {
1218 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1219 .await
1220 .map_err(KclErrorWithOutputs::no_outputs)?;
1221 }
1222 Constraint::HorizontalDistance(distance) => {
1223 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1224 .await
1225 .map_err(KclErrorWithOutputs::no_outputs)?;
1226 }
1227 Constraint::VerticalDistance(distance) => {
1228 self.add_vertical_distance(sketch, distance, &mut new_ast)
1229 .await
1230 .map_err(KclErrorWithOutputs::no_outputs)?;
1231 }
1232 Constraint::Horizontal(horizontal) => {
1233 self.add_horizontal(sketch, horizontal, &mut new_ast)
1234 .await
1235 .map_err(KclErrorWithOutputs::no_outputs)?;
1236 }
1237 Constraint::LinesEqualLength(lines_equal_length) => {
1238 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1239 .await
1240 .map_err(KclErrorWithOutputs::no_outputs)?;
1241 }
1242 Constraint::Parallel(parallel) => {
1243 self.add_parallel(sketch, parallel, &mut new_ast)
1244 .await
1245 .map_err(KclErrorWithOutputs::no_outputs)?;
1246 }
1247 Constraint::Perpendicular(perpendicular) => {
1248 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1249 .await
1250 .map_err(KclErrorWithOutputs::no_outputs)?;
1251 }
1252 Constraint::Vertical(vertical) => {
1253 self.add_vertical(sketch, vertical, &mut new_ast)
1254 .await
1255 .map_err(KclErrorWithOutputs::no_outputs)?;
1256 }
1257 Constraint::Diameter(diameter) => {
1258 self.add_diameter(sketch, diameter, &mut new_ast)
1259 .await
1260 .map_err(KclErrorWithOutputs::no_outputs)?;
1261 }
1262 Constraint::Radius(radius) => {
1263 self.add_radius(sketch, radius, &mut new_ast)
1264 .await
1265 .map_err(KclErrorWithOutputs::no_outputs)?;
1266 }
1267 Constraint::Angle(angle) => {
1268 self.add_angle(sketch, angle, &mut new_ast)
1269 .await
1270 .map_err(KclErrorWithOutputs::no_outputs)?;
1271 }
1272 Constraint::Tangent(tangent) => {
1273 self.add_tangent(sketch, tangent, &mut new_ast)
1274 .await
1275 .map_err(KclErrorWithOutputs::no_outputs)?;
1276 }
1277 }
1278 }
1279
1280 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1282
1283 let has_constraint_deletions = !constraint_ids_set.is_empty();
1284 for constraint_id in constraint_ids_set {
1285 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1286 .map_err(KclErrorWithOutputs::no_outputs)?;
1287 }
1288
1289 let (source_delta, mut scene_graph_delta) = self
1293 .execute_after_edit(
1294 ctx,
1295 sketch,
1296 sketch_block_ref,
1297 segment_ids_edited,
1298 EditDeleteKind::Edit,
1299 &mut new_ast,
1300 )
1301 .await?;
1302
1303 if has_constraint_deletions {
1306 scene_graph_delta.invalidates_ids = true;
1307 }
1308
1309 Ok((source_delta, scene_graph_delta))
1310 }
1311
1312 async fn batch_tail_cut_operations(
1313 &mut self,
1314 ctx: &ExecutorContext,
1315 _version: Version,
1316 sketch: ObjectId,
1317 edit_segments: Vec<ExistingSegmentCtor>,
1318 add_constraints: Vec<Constraint>,
1319 delete_constraint_ids: Vec<ObjectId>,
1320 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1321 let sketch_block_ref =
1322 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1323
1324 let mut new_ast = self.program.ast.clone();
1325 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1326
1327 for segment in edit_segments {
1329 segment_ids_edited.insert(segment.id);
1330 match segment.ctor {
1331 SegmentCtor::Point(ctor) => self
1332 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1333 .map_err(KclErrorWithOutputs::no_outputs)?,
1334 SegmentCtor::Line(ctor) => self
1335 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1336 .map_err(KclErrorWithOutputs::no_outputs)?,
1337 SegmentCtor::Arc(ctor) => self
1338 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1339 .map_err(KclErrorWithOutputs::no_outputs)?,
1340 SegmentCtor::Circle(ctor) => self
1341 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1342 .map_err(KclErrorWithOutputs::no_outputs)?,
1343 }
1344 }
1345
1346 for constraint in add_constraints {
1348 match constraint {
1349 Constraint::Coincident(coincident) => {
1350 self.add_coincident(sketch, coincident, &mut new_ast)
1351 .await
1352 .map_err(KclErrorWithOutputs::no_outputs)?;
1353 }
1354 other => {
1355 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1356 "unsupported constraint in tail cut batch: {other:?}"
1357 ))));
1358 }
1359 }
1360 }
1361
1362 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1364
1365 let has_constraint_deletions = !constraint_ids_set.is_empty();
1366 for constraint_id in constraint_ids_set {
1367 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1368 .map_err(KclErrorWithOutputs::no_outputs)?;
1369 }
1370
1371 let (source_delta, mut scene_graph_delta) = self
1375 .execute_after_edit(
1376 ctx,
1377 sketch,
1378 sketch_block_ref,
1379 segment_ids_edited,
1380 EditDeleteKind::Edit,
1381 &mut new_ast,
1382 )
1383 .await?;
1384
1385 if has_constraint_deletions {
1388 scene_graph_delta.invalidates_ids = true;
1389 }
1390
1391 Ok((source_delta, scene_graph_delta))
1392 }
1393}
1394
1395impl FrontendState {
1396 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1397 self.program = program.clone();
1398
1399 self.point_freedom_cache.clear();
1410 match ctx.run_with_caching(program).await {
1411 Ok(outcome) => {
1412 let outcome = self.update_state_after_exec(outcome, true);
1413 let checkpoint_id = self
1414 .create_sketch_checkpoint(outcome.clone())
1415 .await
1416 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1417 Ok(SetProgramOutcome::Success {
1418 scene_graph: Box::new(self.scene_graph.clone()),
1419 exec_outcome: Box::new(outcome),
1420 checkpoint_id: Some(checkpoint_id),
1421 })
1422 }
1423 Err(mut err) => {
1424 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1427 self.update_state_after_exec(outcome, true);
1428 err.scene_graph = Some(self.scene_graph.clone());
1429 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1430 }
1431 }
1432 }
1433
1434 pub async fn engine_execute(
1437 &mut self,
1438 ctx: &ExecutorContext,
1439 program: Program,
1440 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1441 self.program = program.clone();
1442
1443 self.point_freedom_cache.clear();
1447 match ctx.run_with_caching(program).await {
1448 Ok(outcome) => {
1449 let outcome = self.update_state_after_exec(outcome, true);
1450 Ok(SceneGraphDelta {
1451 new_graph: self.scene_graph.clone(),
1452 exec_outcome: outcome,
1453 new_objects: Default::default(),
1455 invalidates_ids: Default::default(),
1457 })
1458 }
1459 Err(mut err) => {
1460 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1462 self.update_state_after_exec(outcome, true);
1463 err.scene_graph = Some(self.scene_graph.clone());
1464 Err(err)
1465 }
1466 }
1467 }
1468
1469 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1470 if matches!(err.error, KclError::EngineHangup { .. }) {
1471 return Err(err);
1475 }
1476
1477 let KclErrorWithOutputs {
1478 error,
1479 mut non_fatal,
1480 variables,
1481 #[cfg(feature = "artifact-graph")]
1482 operations,
1483 #[cfg(feature = "artifact-graph")]
1484 artifact_graph,
1485 #[cfg(feature = "artifact-graph")]
1486 scene_objects,
1487 #[cfg(feature = "artifact-graph")]
1488 source_range_to_object,
1489 #[cfg(feature = "artifact-graph")]
1490 var_solutions,
1491 filenames,
1492 default_planes,
1493 ..
1494 } = err;
1495
1496 if let Some(source_range) = error.source_ranges().first() {
1497 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1498 } else {
1499 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1500 }
1501
1502 Ok(ExecOutcome {
1503 variables,
1504 filenames,
1505 #[cfg(feature = "artifact-graph")]
1506 operations,
1507 #[cfg(feature = "artifact-graph")]
1508 artifact_graph,
1509 #[cfg(feature = "artifact-graph")]
1510 scene_objects,
1511 #[cfg(feature = "artifact-graph")]
1512 source_range_to_object,
1513 #[cfg(feature = "artifact-graph")]
1514 var_solutions,
1515 issues: non_fatal,
1516 default_planes,
1517 })
1518 }
1519
1520 async fn add_point(
1521 &mut self,
1522 ctx: &ExecutorContext,
1523 sketch: ObjectId,
1524 ctor: PointCtor,
1525 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1526 let at_ast = to_ast_point2d(&ctor.position)
1528 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1529 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1530 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1531 unlabeled: None,
1532 arguments: vec![ast::LabeledArg {
1533 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1534 arg: at_ast,
1535 }],
1536 digest: None,
1537 non_code_meta: Default::default(),
1538 })));
1539
1540 let sketch_id = sketch;
1542 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1543 #[cfg(target_arch = "wasm32")]
1544 web_sys::console::error_1(
1545 &format!(
1546 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1547 &self.scene_graph.objects
1548 )
1549 .into(),
1550 );
1551 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1552 })?;
1553 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1554 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1555 "Object is not a sketch, it is {}",
1556 sketch_object.kind.human_friendly_kind_with_article(),
1557 ))));
1558 };
1559 let mut new_ast = self.program.ast.clone();
1561 let (sketch_block_ref, _) = self
1562 .mutate_ast(
1563 &mut new_ast,
1564 sketch_id,
1565 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1566 )
1567 .map_err(KclErrorWithOutputs::no_outputs)?;
1568 let new_source = source_from_ast(&new_ast);
1570 let (new_program, errors) = Program::parse(&new_source)
1572 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1573 if !errors.is_empty() {
1574 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1575 "Error parsing KCL source after adding point: {errors:?}"
1576 ))));
1577 }
1578 let Some(new_program) = new_program else {
1579 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1580 "No AST produced after adding point".to_string(),
1581 )));
1582 };
1583
1584 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1585 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1586 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1587 )))
1588 })?;
1589 #[cfg(not(feature = "artifact-graph"))]
1590 let _ = point_node_ref;
1591
1592 self.program = new_program.clone();
1594
1595 let mut truncated_program = new_program;
1597 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1598 .map_err(KclErrorWithOutputs::no_outputs)?;
1599
1600 let outcome = ctx
1602 .run_mock(
1603 &truncated_program,
1604 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1605 )
1606 .await?;
1607
1608 #[cfg(not(feature = "artifact-graph"))]
1609 let new_object_ids = Vec::new();
1610 #[cfg(feature = "artifact-graph")]
1611 let new_object_ids = {
1612 let make_err =
1613 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1614 let segment_id = outcome
1615 .source_range_to_object
1616 .get(&point_node_ref.range)
1617 .copied()
1618 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1619 let segment_object = outcome
1620 .scene_objects
1621 .get(segment_id.0)
1622 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1623 let ObjectKind::Segment { segment } = &segment_object.kind else {
1624 return Err(make_err(format!(
1625 "Object is not a segment, it is {}",
1626 segment_object.kind.human_friendly_kind_with_article()
1627 )));
1628 };
1629 let Segment::Point(_) = segment else {
1630 return Err(make_err(format!(
1631 "Segment is not a point, it is {}",
1632 segment.human_friendly_kind_with_article()
1633 )));
1634 };
1635 vec![segment_id]
1636 };
1637 let src_delta = SourceDelta { text: new_source };
1638 let outcome = self.update_state_after_exec(outcome, false);
1640 let scene_graph_delta = SceneGraphDelta {
1641 new_graph: self.scene_graph.clone(),
1642 invalidates_ids: false,
1643 new_objects: new_object_ids,
1644 exec_outcome: outcome,
1645 };
1646 Ok((src_delta, scene_graph_delta))
1647 }
1648
1649 async fn add_line(
1650 &mut self,
1651 ctx: &ExecutorContext,
1652 sketch: ObjectId,
1653 ctor: LineCtor,
1654 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1655 let start_ast = to_ast_point2d(&ctor.start)
1657 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1658 let end_ast = to_ast_point2d(&ctor.end)
1659 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1660 let mut arguments = vec![
1661 ast::LabeledArg {
1662 label: Some(ast::Identifier::new(LINE_START_PARAM)),
1663 arg: start_ast,
1664 },
1665 ast::LabeledArg {
1666 label: Some(ast::Identifier::new(LINE_END_PARAM)),
1667 arg: end_ast,
1668 },
1669 ];
1670 if ctor.construction == Some(true) {
1672 arguments.push(ast::LabeledArg {
1673 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1674 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1675 value: ast::LiteralValue::Bool(true),
1676 raw: "true".to_string(),
1677 digest: None,
1678 }))),
1679 });
1680 }
1681 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1682 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1683 unlabeled: None,
1684 arguments,
1685 digest: None,
1686 non_code_meta: Default::default(),
1687 })));
1688
1689 let sketch_id = sketch;
1691 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1692 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1693 })?;
1694 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1695 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1696 "Object is not a sketch, it is {}",
1697 sketch_object.kind.human_friendly_kind_with_article(),
1698 ))));
1699 };
1700 let mut new_ast = self.program.ast.clone();
1702 let (sketch_block_ref, _) = self
1703 .mutate_ast(
1704 &mut new_ast,
1705 sketch_id,
1706 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1707 )
1708 .map_err(KclErrorWithOutputs::no_outputs)?;
1709 let new_source = source_from_ast(&new_ast);
1711 let (new_program, errors) = Program::parse(&new_source)
1713 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1714 if !errors.is_empty() {
1715 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1716 "Error parsing KCL source after adding line: {errors:?}"
1717 ))));
1718 }
1719 let Some(new_program) = new_program else {
1720 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1721 "No AST produced after adding line".to_string(),
1722 )));
1723 };
1724
1725 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1726 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1727 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1728 )))
1729 })?;
1730 #[cfg(not(feature = "artifact-graph"))]
1731 let _ = line_node_ref;
1732
1733 self.program = new_program.clone();
1735
1736 let mut truncated_program = new_program;
1738 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1739 .map_err(KclErrorWithOutputs::no_outputs)?;
1740
1741 let outcome = ctx
1743 .run_mock(
1744 &truncated_program,
1745 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1746 )
1747 .await?;
1748
1749 #[cfg(not(feature = "artifact-graph"))]
1750 let new_object_ids = Vec::new();
1751 #[cfg(feature = "artifact-graph")]
1752 let new_object_ids = {
1753 let make_err =
1754 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1755 let segment_id = outcome
1756 .source_range_to_object
1757 .get(&line_node_ref.range)
1758 .copied()
1759 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1760 let segment_object = outcome
1761 .scene_object_by_id(segment_id)
1762 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1763 let ObjectKind::Segment { segment } = &segment_object.kind else {
1764 return Err(make_err(format!(
1765 "Object is not a segment, it is {}",
1766 segment_object.kind.human_friendly_kind_with_article()
1767 )));
1768 };
1769 let Segment::Line(line) = segment else {
1770 return Err(make_err(format!(
1771 "Segment is not a line, it is {}",
1772 segment.human_friendly_kind_with_article()
1773 )));
1774 };
1775 vec![line.start, line.end, segment_id]
1776 };
1777 let src_delta = SourceDelta { text: new_source };
1778 let outcome = self.update_state_after_exec(outcome, false);
1780 let scene_graph_delta = SceneGraphDelta {
1781 new_graph: self.scene_graph.clone(),
1782 invalidates_ids: false,
1783 new_objects: new_object_ids,
1784 exec_outcome: outcome,
1785 };
1786 Ok((src_delta, scene_graph_delta))
1787 }
1788
1789 async fn add_arc(
1790 &mut self,
1791 ctx: &ExecutorContext,
1792 sketch: ObjectId,
1793 ctor: ArcCtor,
1794 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1795 let start_ast = to_ast_point2d(&ctor.start)
1797 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1798 let end_ast = to_ast_point2d(&ctor.end)
1799 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1800 let center_ast = to_ast_point2d(&ctor.center)
1801 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1802 let mut arguments = vec![
1803 ast::LabeledArg {
1804 label: Some(ast::Identifier::new(ARC_START_PARAM)),
1805 arg: start_ast,
1806 },
1807 ast::LabeledArg {
1808 label: Some(ast::Identifier::new(ARC_END_PARAM)),
1809 arg: end_ast,
1810 },
1811 ast::LabeledArg {
1812 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1813 arg: center_ast,
1814 },
1815 ];
1816 if ctor.construction == Some(true) {
1818 arguments.push(ast::LabeledArg {
1819 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1820 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1821 value: ast::LiteralValue::Bool(true),
1822 raw: "true".to_string(),
1823 digest: None,
1824 }))),
1825 });
1826 }
1827 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1828 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1829 unlabeled: None,
1830 arguments,
1831 digest: None,
1832 non_code_meta: Default::default(),
1833 })));
1834
1835 let sketch_id = sketch;
1837 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1838 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1839 })?;
1840 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1841 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1842 "Object is not a sketch, it is {}",
1843 sketch_object.kind.human_friendly_kind_with_article(),
1844 ))));
1845 };
1846 let mut new_ast = self.program.ast.clone();
1848 let (sketch_block_ref, _) = self
1849 .mutate_ast(
1850 &mut new_ast,
1851 sketch_id,
1852 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1853 )
1854 .map_err(KclErrorWithOutputs::no_outputs)?;
1855 let new_source = source_from_ast(&new_ast);
1857 let (new_program, errors) = Program::parse(&new_source)
1859 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1860 if !errors.is_empty() {
1861 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1862 "Error parsing KCL source after adding arc: {errors:?}"
1863 ))));
1864 }
1865 let Some(new_program) = new_program else {
1866 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1867 "No AST produced after adding arc".to_string(),
1868 )));
1869 };
1870
1871 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1872 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1873 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
1874 )))
1875 })?;
1876 #[cfg(not(feature = "artifact-graph"))]
1877 let _ = arc_node_ref;
1878
1879 self.program = new_program.clone();
1881
1882 let mut truncated_program = new_program;
1884 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1885 .map_err(KclErrorWithOutputs::no_outputs)?;
1886
1887 let outcome = ctx
1889 .run_mock(
1890 &truncated_program,
1891 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1892 )
1893 .await?;
1894
1895 #[cfg(not(feature = "artifact-graph"))]
1896 let new_object_ids = Vec::new();
1897 #[cfg(feature = "artifact-graph")]
1898 let new_object_ids = {
1899 let make_err =
1900 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1901 let segment_id = outcome
1902 .source_range_to_object
1903 .get(&arc_node_ref.range)
1904 .copied()
1905 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
1906 let segment_object = outcome
1907 .scene_objects
1908 .get(segment_id.0)
1909 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1910 let ObjectKind::Segment { segment } = &segment_object.kind else {
1911 return Err(make_err(format!(
1912 "Object is not a segment, it is {}",
1913 segment_object.kind.human_friendly_kind_with_article()
1914 )));
1915 };
1916 let Segment::Arc(arc) = segment else {
1917 return Err(make_err(format!(
1918 "Segment is not an arc, it is {}",
1919 segment.human_friendly_kind_with_article()
1920 )));
1921 };
1922 vec![arc.start, arc.end, arc.center, segment_id]
1923 };
1924 let src_delta = SourceDelta { text: new_source };
1925 let outcome = self.update_state_after_exec(outcome, false);
1927 let scene_graph_delta = SceneGraphDelta {
1928 new_graph: self.scene_graph.clone(),
1929 invalidates_ids: false,
1930 new_objects: new_object_ids,
1931 exec_outcome: outcome,
1932 };
1933 Ok((src_delta, scene_graph_delta))
1934 }
1935
1936 async fn add_circle(
1937 &mut self,
1938 ctx: &ExecutorContext,
1939 sketch: ObjectId,
1940 ctor: CircleCtor,
1941 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1942 let start_ast = to_ast_point2d(&ctor.start)
1944 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1945 let center_ast = to_ast_point2d(&ctor.center)
1946 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1947 let mut arguments = vec![
1948 ast::LabeledArg {
1949 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
1950 arg: start_ast,
1951 },
1952 ast::LabeledArg {
1953 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
1954 arg: center_ast,
1955 },
1956 ];
1957 if ctor.construction == Some(true) {
1959 arguments.push(ast::LabeledArg {
1960 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1961 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1962 value: ast::LiteralValue::Bool(true),
1963 raw: "true".to_string(),
1964 digest: None,
1965 }))),
1966 });
1967 }
1968 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1969 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
1970 unlabeled: None,
1971 arguments,
1972 digest: None,
1973 non_code_meta: Default::default(),
1974 })));
1975
1976 let sketch_id = sketch;
1978 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1979 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1980 })?;
1981 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1982 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1983 "Object is not a sketch, it is {}",
1984 sketch_object.kind.human_friendly_kind_with_article(),
1985 ))));
1986 };
1987 let mut new_ast = self.program.ast.clone();
1989 let (sketch_block_ref, _) = self
1990 .mutate_ast(
1991 &mut new_ast,
1992 sketch_id,
1993 AstMutateCommand::AddSketchBlockVarDecl {
1994 prefix: CIRCLE_VARIABLE.to_owned(),
1995 expr: circle_ast,
1996 },
1997 )
1998 .map_err(KclErrorWithOutputs::no_outputs)?;
1999 let new_source = source_from_ast(&new_ast);
2001 let (new_program, errors) = Program::parse(&new_source)
2003 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2004 if !errors.is_empty() {
2005 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2006 "Error parsing KCL source after adding circle: {errors:?}"
2007 ))));
2008 }
2009 let Some(new_program) = new_program else {
2010 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2011 "No AST produced after adding circle".to_string(),
2012 )));
2013 };
2014
2015 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2016 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2017 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2018 )))
2019 })?;
2020 #[cfg(not(feature = "artifact-graph"))]
2021 let _ = circle_node_ref;
2022
2023 self.program = new_program.clone();
2025
2026 let mut truncated_program = new_program;
2028 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2029 .map_err(KclErrorWithOutputs::no_outputs)?;
2030
2031 let outcome = ctx
2033 .run_mock(
2034 &truncated_program,
2035 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2036 )
2037 .await?;
2038
2039 #[cfg(not(feature = "artifact-graph"))]
2040 let new_object_ids = Vec::new();
2041 #[cfg(feature = "artifact-graph")]
2042 let new_object_ids = {
2043 let make_err =
2044 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2045 let segment_id = outcome
2046 .source_range_to_object
2047 .get(&circle_node_ref.range)
2048 .copied()
2049 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2050 let segment_object = outcome
2051 .scene_objects
2052 .get(segment_id.0)
2053 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2054 let ObjectKind::Segment { segment } = &segment_object.kind else {
2055 return Err(make_err(format!(
2056 "Object is not a segment, it is {}",
2057 segment_object.kind.human_friendly_kind_with_article()
2058 )));
2059 };
2060 let Segment::Circle(circle) = segment else {
2061 return Err(make_err(format!(
2062 "Segment is not a circle, it is {}",
2063 segment.human_friendly_kind_with_article()
2064 )));
2065 };
2066 vec![circle.start, circle.center, segment_id]
2067 };
2068 let src_delta = SourceDelta { text: new_source };
2069 let outcome = self.update_state_after_exec(outcome, false);
2071 let scene_graph_delta = SceneGraphDelta {
2072 new_graph: self.scene_graph.clone(),
2073 invalidates_ids: false,
2074 new_objects: new_object_ids,
2075 exec_outcome: outcome,
2076 };
2077 Ok((src_delta, scene_graph_delta))
2078 }
2079
2080 fn edit_point(
2081 &mut self,
2082 new_ast: &mut ast::Node<ast::Program>,
2083 sketch: ObjectId,
2084 point: ObjectId,
2085 ctor: PointCtor,
2086 ) -> Result<(), KclError> {
2087 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2089
2090 let sketch_id = sketch;
2092 let sketch_object = self
2093 .scene_graph
2094 .objects
2095 .get(sketch_id.0)
2096 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2097 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2098 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2099 };
2100 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2101 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2102 })?;
2103 let point_id = point;
2105 let point_object = self
2106 .scene_graph
2107 .objects
2108 .get(point_id.0)
2109 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2110 let ObjectKind::Segment {
2111 segment: Segment::Point(point),
2112 } = &point_object.kind
2113 else {
2114 return Err(KclError::refactor(format!(
2115 "Object is not a point segment: {point_object:?}"
2116 )));
2117 };
2118
2119 if let Some(owner_id) = point.owner {
2121 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2122 KclError::refactor(format!(
2123 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2124 ))
2125 })?;
2126 let ObjectKind::Segment { segment } = &owner_object.kind else {
2127 return Err(KclError::refactor(format!(
2128 "Internal: Owner of point is not a segment, but found {}",
2129 owner_object.kind.human_friendly_kind_with_article()
2130 )));
2131 };
2132
2133 if let Segment::Line(line) = segment {
2135 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2136 return Err(KclError::refactor(format!(
2137 "Internal: Owner of point does not have line ctor, but found {}",
2138 line.ctor.human_friendly_kind_with_article()
2139 )));
2140 };
2141 let mut line_ctor = line_ctor.clone();
2142 if line.start == point_id {
2144 line_ctor.start = ctor.position;
2145 } else if line.end == point_id {
2146 line_ctor.end = ctor.position;
2147 } else {
2148 return Err(KclError::refactor(format!(
2149 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2150 )));
2151 }
2152 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2153 }
2154
2155 if let Segment::Arc(arc) = segment {
2157 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2158 return Err(KclError::refactor(format!(
2159 "Internal: Owner of point does not have arc ctor, but found {}",
2160 arc.ctor.human_friendly_kind_with_article()
2161 )));
2162 };
2163 let mut arc_ctor = arc_ctor.clone();
2164 if arc.center == point_id {
2166 arc_ctor.center = ctor.position;
2167 } else if arc.start == point_id {
2168 arc_ctor.start = ctor.position;
2169 } else if arc.end == point_id {
2170 arc_ctor.end = ctor.position;
2171 } else {
2172 return Err(KclError::refactor(format!(
2173 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2174 )));
2175 }
2176 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2177 }
2178
2179 if let Segment::Circle(circle) = segment {
2181 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2182 return Err(KclError::refactor(format!(
2183 "Internal: Owner of point does not have circle ctor, but found {}",
2184 circle.ctor.human_friendly_kind_with_article()
2185 )));
2186 };
2187 let mut circle_ctor = circle_ctor.clone();
2188 if circle.center == point_id {
2189 circle_ctor.center = ctor.position;
2190 } else if circle.start == point_id {
2191 circle_ctor.start = ctor.position;
2192 } else {
2193 return Err(KclError::refactor(format!(
2194 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2195 )));
2196 }
2197 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2198 }
2199
2200 }
2203
2204 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2206 Ok(())
2207 }
2208
2209 fn edit_line(
2210 &mut self,
2211 new_ast: &mut ast::Node<ast::Program>,
2212 sketch: ObjectId,
2213 line: ObjectId,
2214 ctor: LineCtor,
2215 ) -> Result<(), KclError> {
2216 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2218 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2219
2220 let sketch_id = sketch;
2222 let sketch_object = self
2223 .scene_graph
2224 .objects
2225 .get(sketch_id.0)
2226 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2227 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2228 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2229 };
2230 sketch
2231 .segments
2232 .iter()
2233 .find(|o| **o == line)
2234 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2235 let line_id = line;
2237 let line_object = self
2238 .scene_graph
2239 .objects
2240 .get(line_id.0)
2241 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2242 let ObjectKind::Segment { .. } = &line_object.kind else {
2243 let kind = line_object.kind.human_friendly_kind_with_article();
2244 return Err(KclError::refactor(format!(
2245 "This constraint only works on Segments, but you selected {kind}"
2246 )));
2247 };
2248
2249 self.mutate_ast(
2251 new_ast,
2252 line_id,
2253 AstMutateCommand::EditLine {
2254 start: new_start_ast,
2255 end: new_end_ast,
2256 construction: ctor.construction,
2257 },
2258 )?;
2259 Ok(())
2260 }
2261
2262 fn edit_arc(
2263 &mut self,
2264 new_ast: &mut ast::Node<ast::Program>,
2265 sketch: ObjectId,
2266 arc: ObjectId,
2267 ctor: ArcCtor,
2268 ) -> Result<(), KclError> {
2269 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2271 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2272 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2273
2274 let sketch_id = sketch;
2276 let sketch_object = self
2277 .scene_graph
2278 .objects
2279 .get(sketch_id.0)
2280 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2281 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2282 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2283 };
2284 sketch
2285 .segments
2286 .iter()
2287 .find(|o| **o == arc)
2288 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2289 let arc_id = arc;
2291 let arc_object = self
2292 .scene_graph
2293 .objects
2294 .get(arc_id.0)
2295 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2296 let ObjectKind::Segment { .. } = &arc_object.kind else {
2297 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2298 };
2299
2300 self.mutate_ast(
2302 new_ast,
2303 arc_id,
2304 AstMutateCommand::EditArc {
2305 start: new_start_ast,
2306 end: new_end_ast,
2307 center: new_center_ast,
2308 construction: ctor.construction,
2309 },
2310 )?;
2311 Ok(())
2312 }
2313
2314 fn edit_circle(
2315 &mut self,
2316 new_ast: &mut ast::Node<ast::Program>,
2317 sketch: ObjectId,
2318 circle: ObjectId,
2319 ctor: CircleCtor,
2320 ) -> Result<(), KclError> {
2321 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2323 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2324
2325 let sketch_id = sketch;
2327 let sketch_object = self
2328 .scene_graph
2329 .objects
2330 .get(sketch_id.0)
2331 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2332 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2333 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2334 };
2335 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2336 KclError::refactor(format!(
2337 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2338 ))
2339 })?;
2340 let circle_id = circle;
2342 let circle_object = self
2343 .scene_graph
2344 .objects
2345 .get(circle_id.0)
2346 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2347 let ObjectKind::Segment { .. } = &circle_object.kind else {
2348 return Err(KclError::refactor(format!(
2349 "Object is not a segment: {circle_object:?}"
2350 )));
2351 };
2352
2353 self.mutate_ast(
2355 new_ast,
2356 circle_id,
2357 AstMutateCommand::EditCircle {
2358 start: new_start_ast,
2359 center: new_center_ast,
2360 construction: ctor.construction,
2361 },
2362 )?;
2363 Ok(())
2364 }
2365
2366 fn delete_segment(
2367 &mut self,
2368 new_ast: &mut ast::Node<ast::Program>,
2369 sketch: ObjectId,
2370 segment_id: ObjectId,
2371 ) -> Result<(), KclError> {
2372 let sketch_id = sketch;
2374 let sketch_object = self
2375 .scene_graph
2376 .objects
2377 .get(sketch_id.0)
2378 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2379 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2380 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2381 };
2382 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2383 KclError::refactor(format!(
2384 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2385 ))
2386 })?;
2387 let segment_object =
2389 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2390 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2391 })?;
2392 let ObjectKind::Segment { .. } = &segment_object.kind else {
2393 return Err(KclError::refactor(format!(
2394 "Object is not a segment, it is {}",
2395 segment_object.kind.human_friendly_kind_with_article()
2396 )));
2397 };
2398
2399 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2401 Ok(())
2402 }
2403
2404 fn delete_constraint(
2405 &mut self,
2406 new_ast: &mut ast::Node<ast::Program>,
2407 sketch: ObjectId,
2408 constraint_id: ObjectId,
2409 ) -> Result<(), KclError> {
2410 let sketch_id = sketch;
2412 let sketch_object = self
2413 .scene_graph
2414 .objects
2415 .get(sketch_id.0)
2416 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2417 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2418 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2419 };
2420 sketch
2421 .constraints
2422 .iter()
2423 .find(|o| **o == constraint_id)
2424 .ok_or_else(|| {
2425 KclError::refactor(format!(
2426 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2427 ))
2428 })?;
2429 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2431 KclError::refactor(format!(
2432 "Constraint not found in scene graph: constraint={constraint_id:?}"
2433 ))
2434 })?;
2435 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2436 return Err(KclError::refactor(format!(
2437 "Object is not a constraint, it is {}",
2438 constraint_object.kind.human_friendly_kind_with_article()
2439 )));
2440 };
2441
2442 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2444 Ok(())
2445 }
2446
2447 fn edit_equal_length_constraint(
2449 &mut self,
2450 new_ast: &mut ast::Node<ast::Program>,
2451 constraint_id: ObjectId,
2452 lines: Vec<ObjectId>,
2453 ) -> Result<(), KclError> {
2454 if lines.len() < 2 {
2455 return Err(KclError::refactor(format!(
2456 "Lines equal length constraint must have at least 2 lines, got {}",
2457 lines.len()
2458 )));
2459 }
2460
2461 let line_asts = lines
2462 .iter()
2463 .map(|line_id| {
2464 let line_object = self
2465 .scene_graph
2466 .objects
2467 .get(line_id.0)
2468 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2469 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2470 let kind = line_object.kind.human_friendly_kind_with_article();
2471 return Err(KclError::refactor(format!(
2472 "This constraint only works on Segments, but you selected {kind}"
2473 )));
2474 };
2475 let Segment::Line(_) = line_segment else {
2476 let kind = line_segment.human_friendly_kind_with_article();
2477 return Err(KclError::refactor(format!(
2478 "Only lines can be made equal length, but you selected {kind}"
2479 )));
2480 };
2481
2482 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
2483 })
2484 .collect::<Result<Vec<_>, _>>()?;
2485
2486 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2487 elements: line_asts,
2488 digest: None,
2489 non_code_meta: Default::default(),
2490 })));
2491
2492 self.mutate_ast(
2493 new_ast,
2494 constraint_id,
2495 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2496 )?;
2497 Ok(())
2498 }
2499
2500 fn edit_parallel_constraint(
2502 &mut self,
2503 new_ast: &mut ast::Node<ast::Program>,
2504 constraint_id: ObjectId,
2505 lines: Vec<ObjectId>,
2506 ) -> Result<(), KclError> {
2507 if lines.len() < 2 {
2508 return Err(KclError::refactor(format!(
2509 "Parallel constraint must have at least 2 lines, got {}",
2510 lines.len()
2511 )));
2512 }
2513
2514 let line_asts = lines
2515 .iter()
2516 .map(|line_id| {
2517 let line_object = self
2518 .scene_graph
2519 .objects
2520 .get(line_id.0)
2521 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2522 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2523 let kind = line_object.kind.human_friendly_kind_with_article();
2524 return Err(KclError::refactor(format!(
2525 "This constraint only works on Segments, but you selected {kind}"
2526 )));
2527 };
2528 let Segment::Line(_) = line_segment else {
2529 let kind = line_segment.human_friendly_kind_with_article();
2530 return Err(KclError::refactor(format!(
2531 "Only lines can be made parallel, but you selected {kind}"
2532 )));
2533 };
2534
2535 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
2536 })
2537 .collect::<Result<Vec<_>, _>>()?;
2538
2539 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2540 elements: line_asts,
2541 digest: None,
2542 non_code_meta: Default::default(),
2543 })));
2544
2545 self.mutate_ast(
2546 new_ast,
2547 constraint_id,
2548 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2549 )?;
2550 Ok(())
2551 }
2552
2553 fn edit_equal_radius_constraint(
2555 &mut self,
2556 new_ast: &mut ast::Node<ast::Program>,
2557 constraint_id: ObjectId,
2558 input: Vec<ObjectId>,
2559 ) -> Result<(), KclError> {
2560 if input.len() < 2 {
2561 return Err(KclError::refactor(format!(
2562 "equalRadius constraint must have at least 2 segments, got {}",
2563 input.len()
2564 )));
2565 }
2566
2567 let input_asts = input
2568 .iter()
2569 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2570 .collect::<Result<Vec<_>, _>>()?;
2571
2572 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2573 elements: input_asts,
2574 digest: None,
2575 non_code_meta: Default::default(),
2576 })));
2577
2578 self.mutate_ast(
2579 new_ast,
2580 constraint_id,
2581 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2582 )?;
2583 Ok(())
2584 }
2585
2586 async fn execute_after_edit(
2587 &mut self,
2588 ctx: &ExecutorContext,
2589 sketch: ObjectId,
2590 sketch_block_ref: AstNodeRef,
2591 segment_ids_edited: AhashIndexSet<ObjectId>,
2592 edit_kind: EditDeleteKind,
2593 new_ast: &mut ast::Node<ast::Program>,
2594 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2595 let new_source = source_from_ast(new_ast);
2597 let (new_program, errors) = Program::parse(&new_source)
2599 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2600 if !errors.is_empty() {
2601 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2602 "Error parsing KCL source after editing: {errors:?}"
2603 ))));
2604 }
2605 let Some(new_program) = new_program else {
2606 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2607 "No AST produced after editing".to_string(),
2608 )));
2609 };
2610
2611 self.program = new_program.clone();
2613
2614 let is_delete = edit_kind.is_delete();
2616 let truncated_program = {
2617 let mut truncated_program = new_program;
2618 only_sketch_block(
2619 &mut truncated_program.ast,
2620 &sketch_block_ref,
2621 edit_kind.to_change_kind(),
2622 )
2623 .map_err(KclErrorWithOutputs::no_outputs)?;
2624 truncated_program
2625 };
2626
2627 #[cfg(not(feature = "artifact-graph"))]
2628 drop(segment_ids_edited);
2629
2630 let mock_config = MockConfig {
2632 sketch_block_id: Some(sketch),
2633 freedom_analysis: is_delete,
2634 #[cfg(feature = "artifact-graph")]
2635 segment_ids_edited: segment_ids_edited.clone(),
2636 ..Default::default()
2637 };
2638 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2639
2640 let outcome = self.update_state_after_exec(outcome, is_delete);
2642
2643 #[cfg(feature = "artifact-graph")]
2644 let new_source = {
2645 let mut new_ast = self.program.ast.clone();
2650 for (var_range, value) in &outcome.var_solutions {
2651 let rounded = value.round(3);
2652 mutate_ast_node_by_source_range(
2653 &mut new_ast,
2654 *var_range,
2655 AstMutateCommand::EditVarInitialValue { value: rounded },
2656 )
2657 .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2658 }
2659 source_from_ast(&new_ast)
2660 };
2661
2662 let src_delta = SourceDelta { text: new_source };
2663 let scene_graph_delta = SceneGraphDelta {
2664 new_graph: self.scene_graph.clone(),
2665 invalidates_ids: is_delete,
2666 new_objects: Vec::new(),
2667 exec_outcome: outcome,
2668 };
2669 Ok((src_delta, scene_graph_delta))
2670 }
2671
2672 async fn execute_after_delete_sketch(
2673 &mut self,
2674 ctx: &ExecutorContext,
2675 new_ast: &mut ast::Node<ast::Program>,
2676 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2677 let new_source = source_from_ast(new_ast);
2679 let (new_program, errors) = Program::parse(&new_source)
2681 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2682 if !errors.is_empty() {
2683 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2684 "Error parsing KCL source after editing: {errors:?}"
2685 ))));
2686 }
2687 let Some(new_program) = new_program else {
2688 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2689 "No AST produced after editing".to_string(),
2690 )));
2691 };
2692
2693 self.program = new_program.clone();
2695
2696 let outcome = ctx.run_with_caching(new_program).await?;
2702 let freedom_analysis_ran = true;
2703
2704 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2705
2706 let src_delta = SourceDelta { text: new_source };
2707 let scene_graph_delta = SceneGraphDelta {
2708 new_graph: self.scene_graph.clone(),
2709 invalidates_ids: true,
2710 new_objects: Vec::new(),
2711 exec_outcome: outcome,
2712 };
2713 Ok((src_delta, scene_graph_delta))
2714 }
2715
2716 fn point_id_to_ast_reference(
2721 &self,
2722 point_id: ObjectId,
2723 new_ast: &mut ast::Node<ast::Program>,
2724 ) -> Result<ast::Expr, KclError> {
2725 let point_object = self
2726 .scene_graph
2727 .objects
2728 .get(point_id.0)
2729 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2730 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2731 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2732 };
2733 let Segment::Point(point) = point_segment else {
2734 return Err(KclError::refactor(format!(
2735 "Only points are currently supported: {point_object:?}"
2736 )));
2737 };
2738
2739 if let Some(owner_id) = point.owner {
2740 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2741 KclError::refactor(format!(
2742 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2743 ))
2744 })?;
2745 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2746 return Err(KclError::refactor(format!(
2747 "Owner of point is not a segment, but found {}",
2748 owner_object.kind.human_friendly_kind_with_article()
2749 )));
2750 };
2751
2752 match owner_segment {
2753 Segment::Line(line) => {
2754 let property = if line.start == point_id {
2755 LINE_PROPERTY_START
2756 } else if line.end == point_id {
2757 LINE_PROPERTY_END
2758 } else {
2759 return Err(KclError::refactor(format!(
2760 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2761 )));
2762 };
2763 get_or_insert_ast_reference(new_ast, &owner_object.source, "line", Some(property))
2764 }
2765 Segment::Arc(arc) => {
2766 let property = if arc.start == point_id {
2767 ARC_PROPERTY_START
2768 } else if arc.end == point_id {
2769 ARC_PROPERTY_END
2770 } else if arc.center == point_id {
2771 ARC_PROPERTY_CENTER
2772 } else {
2773 return Err(KclError::refactor(format!(
2774 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2775 )));
2776 };
2777 get_or_insert_ast_reference(new_ast, &owner_object.source, "arc", Some(property))
2778 }
2779 Segment::Circle(circle) => {
2780 let property = if circle.start == point_id {
2781 CIRCLE_PROPERTY_START
2782 } else if circle.center == point_id {
2783 CIRCLE_PROPERTY_CENTER
2784 } else {
2785 return Err(KclError::refactor(format!(
2786 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2787 )));
2788 };
2789 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
2790 }
2791 _ => Err(KclError::refactor(format!(
2792 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
2793 ))),
2794 }
2795 } else {
2796 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
2798 }
2799 }
2800
2801 fn coincident_segment_to_ast(
2802 &self,
2803 segment: &ConstraintSegment,
2804 new_ast: &mut ast::Node<ast::Program>,
2805 ) -> Result<ast::Expr, KclError> {
2806 match segment {
2807 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
2808 ConstraintSegment::Segment(segment_id) => {
2809 let segment_object = self
2810 .scene_graph
2811 .objects
2812 .get(segment_id.0)
2813 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
2814 let ObjectKind::Segment { segment } = &segment_object.kind else {
2815 return Err(KclError::refactor(format!(
2816 "Object is not a segment, it is {}",
2817 segment_object.kind.human_friendly_kind_with_article()
2818 )));
2819 };
2820
2821 match segment {
2822 Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
2823 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None),
2824 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
2825 Segment::Circle(_) => {
2826 get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
2827 }
2828 }
2829 }
2830 }
2831 }
2832
2833 fn axis_constraint_segment_to_ast(
2834 &self,
2835 segment: &ConstraintSegment,
2836 new_ast: &mut ast::Node<ast::Program>,
2837 ) -> Result<ast::Expr, KclError> {
2838 match segment {
2839 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
2840 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
2841 }
2842 }
2843
2844 async fn add_coincident(
2845 &mut self,
2846 sketch: ObjectId,
2847 coincident: Coincident,
2848 new_ast: &mut ast::Node<ast::Program>,
2849 ) -> Result<AstNodeRef, KclError> {
2850 let sketch_id = sketch;
2851 let [seg0_ast, seg1_ast] = match coincident.segments.as_slice() {
2852 [seg0, seg1] => [
2853 self.coincident_segment_to_ast(seg0, new_ast)?,
2854 self.coincident_segment_to_ast(seg1, new_ast)?,
2855 ],
2856 _ => {
2857 return Err(KclError::refactor(format!(
2858 "Coincident constraint must have exactly 2 inputs, got {}",
2859 coincident.segments.len()
2860 )));
2861 }
2862 };
2863
2864 let coincident_ast = create_coincident_ast(seg0_ast, seg1_ast);
2866
2867 let (sketch_block_ref, _) = self.mutate_ast(
2869 new_ast,
2870 sketch_id,
2871 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
2872 )?;
2873 Ok(sketch_block_ref)
2874 }
2875
2876 async fn add_distance(
2877 &mut self,
2878 sketch: ObjectId,
2879 distance: Distance,
2880 new_ast: &mut ast::Node<ast::Program>,
2881 ) -> Result<AstNodeRef, KclError> {
2882 let sketch_id = sketch;
2883 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
2884 [pt0, pt1] => [
2885 self.coincident_segment_to_ast(pt0, new_ast)?,
2886 self.coincident_segment_to_ast(pt1, new_ast)?,
2887 ],
2888 _ => {
2889 return Err(KclError::refactor(format!(
2890 "Distance constraint must have exactly 2 points, got {}",
2891 distance.points.len()
2892 )));
2893 }
2894 };
2895
2896 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2898 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
2899 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2900 ast::ArrayExpression {
2901 elements: vec![pt0_ast, pt1_ast],
2902 digest: None,
2903 non_code_meta: Default::default(),
2904 },
2905 )))),
2906 arguments: Default::default(),
2907 digest: None,
2908 non_code_meta: Default::default(),
2909 })));
2910 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2911 left: distance_call_ast,
2912 operator: ast::BinaryOperator::Eq,
2913 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2914 value: ast::LiteralValue::Number {
2915 value: distance.distance.value,
2916 suffix: distance.distance.units,
2917 },
2918 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
2919 KclError::refactor(format!(
2920 "Could not format numeric suffix: {:?}",
2921 distance.distance.units
2922 ))
2923 })?,
2924 digest: None,
2925 }))),
2926 digest: None,
2927 })));
2928
2929 let (sketch_block_ref, _) = self.mutate_ast(
2931 new_ast,
2932 sketch_id,
2933 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
2934 )?;
2935 Ok(sketch_block_ref)
2936 }
2937
2938 async fn add_angle(
2939 &mut self,
2940 sketch: ObjectId,
2941 angle: Angle,
2942 new_ast: &mut ast::Node<ast::Program>,
2943 ) -> Result<AstNodeRef, KclError> {
2944 let &[l0_id, l1_id] = angle.lines.as_slice() else {
2945 return Err(KclError::refactor(format!(
2946 "Angle constraint must have exactly 2 lines, got {}",
2947 angle.lines.len()
2948 )));
2949 };
2950 let sketch_id = sketch;
2951
2952 let line0_object = self
2954 .scene_graph
2955 .objects
2956 .get(l0_id.0)
2957 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
2958 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
2959 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
2960 };
2961 let Segment::Line(_) = line0_segment else {
2962 return Err(KclError::refactor(format!(
2963 "Only lines can be constrained to meet at an angle: {line0_object:?}",
2964 )));
2965 };
2966 let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
2967
2968 let line1_object = self
2969 .scene_graph
2970 .objects
2971 .get(l1_id.0)
2972 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
2973 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
2974 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
2975 };
2976 let Segment::Line(_) = line1_segment else {
2977 return Err(KclError::refactor(format!(
2978 "Only lines can be constrained to meet at an angle: {line1_object:?}",
2979 )));
2980 };
2981 let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
2982
2983 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2985 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
2986 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2987 ast::ArrayExpression {
2988 elements: vec![l0_ast, l1_ast],
2989 digest: None,
2990 non_code_meta: Default::default(),
2991 },
2992 )))),
2993 arguments: Default::default(),
2994 digest: None,
2995 non_code_meta: Default::default(),
2996 })));
2997 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2998 left: angle_call_ast,
2999 operator: ast::BinaryOperator::Eq,
3000 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3001 value: ast::LiteralValue::Number {
3002 value: angle.angle.value,
3003 suffix: angle.angle.units,
3004 },
3005 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3006 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3007 })?,
3008 digest: None,
3009 }))),
3010 digest: None,
3011 })));
3012
3013 let (sketch_block_ref, _) = self.mutate_ast(
3015 new_ast,
3016 sketch_id,
3017 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3018 )?;
3019 Ok(sketch_block_ref)
3020 }
3021
3022 async fn add_tangent(
3023 &mut self,
3024 sketch: ObjectId,
3025 tangent: Tangent,
3026 new_ast: &mut ast::Node<ast::Program>,
3027 ) -> Result<AstNodeRef, KclError> {
3028 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3029 return Err(KclError::refactor(format!(
3030 "Tangent constraint must have exactly 2 segments, got {}",
3031 tangent.input.len()
3032 )));
3033 };
3034 let sketch_id = sketch;
3035
3036 let seg0_object = self
3037 .scene_graph
3038 .objects
3039 .get(seg0_id.0)
3040 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3041 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3042 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3043 };
3044 let seg0_ast = match seg0_segment {
3045 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?,
3046 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?,
3047 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3048 _ => {
3049 return Err(KclError::refactor(format!(
3050 "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3051 )));
3052 }
3053 };
3054
3055 let seg1_object = self
3056 .scene_graph
3057 .objects
3058 .get(seg1_id.0)
3059 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3060 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3061 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3062 };
3063 let seg1_ast = match seg1_segment {
3064 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?,
3065 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?,
3066 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3067 _ => {
3068 return Err(KclError::refactor(format!(
3069 "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3070 )));
3071 }
3072 };
3073
3074 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3075 let (sketch_block_ref, _) = self.mutate_ast(
3076 new_ast,
3077 sketch_id,
3078 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3079 )?;
3080 Ok(sketch_block_ref)
3081 }
3082
3083 async fn add_equal_radius(
3084 &mut self,
3085 sketch: ObjectId,
3086 equal_radius: EqualRadius,
3087 new_ast: &mut ast::Node<ast::Program>,
3088 ) -> Result<AstNodeRef, KclError> {
3089 if equal_radius.input.len() < 2 {
3090 return Err(KclError::refactor(format!(
3091 "equalRadius constraint must have at least 2 segments, got {}",
3092 equal_radius.input.len()
3093 )));
3094 }
3095
3096 let sketch_id = sketch;
3097 let input_asts = equal_radius
3098 .input
3099 .iter()
3100 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3101 .collect::<Result<Vec<_>, _>>()?;
3102
3103 let equal_radius_ast = create_equal_radius_ast(input_asts);
3104 let (sketch_block_ref, _) = self.mutate_ast(
3105 new_ast,
3106 sketch_id,
3107 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3108 )?;
3109 Ok(sketch_block_ref)
3110 }
3111
3112 async fn add_radius(
3113 &mut self,
3114 sketch: ObjectId,
3115 radius: Radius,
3116 new_ast: &mut ast::Node<ast::Program>,
3117 ) -> Result<AstNodeRef, KclError> {
3118 let params = ArcSizeConstraintParams {
3119 points: vec![radius.arc],
3120 function_name: RADIUS_FN,
3121 value: radius.radius.value,
3122 units: radius.radius.units,
3123 constraint_type_name: "Radius",
3124 };
3125 self.add_arc_size_constraint(sketch, params, new_ast).await
3126 }
3127
3128 async fn add_diameter(
3129 &mut self,
3130 sketch: ObjectId,
3131 diameter: Diameter,
3132 new_ast: &mut ast::Node<ast::Program>,
3133 ) -> Result<AstNodeRef, KclError> {
3134 let params = ArcSizeConstraintParams {
3135 points: vec![diameter.arc],
3136 function_name: DIAMETER_FN,
3137 value: diameter.diameter.value,
3138 units: diameter.diameter.units,
3139 constraint_type_name: "Diameter",
3140 };
3141 self.add_arc_size_constraint(sketch, params, new_ast).await
3142 }
3143
3144 async fn add_fixed_constraints(
3145 &mut self,
3146 sketch: ObjectId,
3147 points: Vec<FixedPoint>,
3148 new_ast: &mut ast::Node<ast::Program>,
3149 ) -> Result<AstNodeRef, KclError> {
3150 let mut sketch_block_ref = None;
3151
3152 for fixed_point in points {
3153 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3154 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3155 .map_err(|err| KclError::refactor(err.to_string()))?;
3156
3157 let (sketch_ref, _) = self.mutate_ast(
3158 new_ast,
3159 sketch,
3160 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3161 )?;
3162 sketch_block_ref = Some(sketch_ref);
3163 }
3164
3165 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3166 }
3167
3168 async fn add_arc_size_constraint(
3169 &mut self,
3170 sketch: ObjectId,
3171 params: ArcSizeConstraintParams,
3172 new_ast: &mut ast::Node<ast::Program>,
3173 ) -> Result<AstNodeRef, KclError> {
3174 let sketch_id = sketch;
3175
3176 if params.points.len() != 1 {
3178 return Err(KclError::refactor(format!(
3179 "{} constraint must have exactly 1 argument (an arc segment), got {}",
3180 params.constraint_type_name,
3181 params.points.len()
3182 )));
3183 }
3184
3185 let arc_id = params.points[0];
3186 let arc_object = self
3187 .scene_graph
3188 .objects
3189 .get(arc_id.0)
3190 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3191 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3192 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3193 };
3194 let ref_type = match arc_segment {
3195 Segment::Arc(_) => "arc",
3196 Segment::Circle(_) => CIRCLE_VARIABLE,
3197 _ => {
3198 return Err(KclError::refactor(format!(
3199 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3200 params.constraint_type_name
3201 )));
3202 }
3203 };
3204 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3206
3207 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3209 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3210 unlabeled: Some(arc_ast),
3211 arguments: Default::default(),
3212 digest: None,
3213 non_code_meta: Default::default(),
3214 })));
3215 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3216 left: call_ast,
3217 operator: ast::BinaryOperator::Eq,
3218 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3219 value: ast::LiteralValue::Number {
3220 value: params.value,
3221 suffix: params.units,
3222 },
3223 raw: format_number_literal(params.value, params.units, None)
3224 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3225 digest: None,
3226 }))),
3227 digest: None,
3228 })));
3229
3230 let (sketch_block_ref, _) = self.mutate_ast(
3232 new_ast,
3233 sketch_id,
3234 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3235 )?;
3236 Ok(sketch_block_ref)
3237 }
3238
3239 async fn add_horizontal_distance(
3240 &mut self,
3241 sketch: ObjectId,
3242 distance: Distance,
3243 new_ast: &mut ast::Node<ast::Program>,
3244 ) -> Result<AstNodeRef, KclError> {
3245 let sketch_id = sketch;
3246 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3247 [pt0, pt1] => [
3248 self.coincident_segment_to_ast(pt0, new_ast)?,
3249 self.coincident_segment_to_ast(pt1, new_ast)?,
3250 ],
3251 _ => {
3252 return Err(KclError::refactor(format!(
3253 "Horizontal distance constraint must have exactly 2 points, got {}",
3254 distance.points.len()
3255 )));
3256 }
3257 };
3258
3259 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3261 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3262 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3263 ast::ArrayExpression {
3264 elements: vec![pt0_ast, pt1_ast],
3265 digest: None,
3266 non_code_meta: Default::default(),
3267 },
3268 )))),
3269 arguments: Default::default(),
3270 digest: None,
3271 non_code_meta: Default::default(),
3272 })));
3273 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3274 left: distance_call_ast,
3275 operator: ast::BinaryOperator::Eq,
3276 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3277 value: ast::LiteralValue::Number {
3278 value: distance.distance.value,
3279 suffix: distance.distance.units,
3280 },
3281 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3282 KclError::refactor(format!(
3283 "Could not format numeric suffix: {:?}",
3284 distance.distance.units
3285 ))
3286 })?,
3287 digest: None,
3288 }))),
3289 digest: None,
3290 })));
3291
3292 let (sketch_block_ref, _) = self.mutate_ast(
3294 new_ast,
3295 sketch_id,
3296 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3297 )?;
3298 Ok(sketch_block_ref)
3299 }
3300
3301 async fn add_vertical_distance(
3302 &mut self,
3303 sketch: ObjectId,
3304 distance: Distance,
3305 new_ast: &mut ast::Node<ast::Program>,
3306 ) -> Result<AstNodeRef, KclError> {
3307 let sketch_id = sketch;
3308 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3309 [pt0, pt1] => [
3310 self.coincident_segment_to_ast(pt0, new_ast)?,
3311 self.coincident_segment_to_ast(pt1, new_ast)?,
3312 ],
3313 _ => {
3314 return Err(KclError::refactor(format!(
3315 "Vertical distance constraint must have exactly 2 points, got {}",
3316 distance.points.len()
3317 )));
3318 }
3319 };
3320
3321 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3323 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3324 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3325 ast::ArrayExpression {
3326 elements: vec![pt0_ast, pt1_ast],
3327 digest: None,
3328 non_code_meta: Default::default(),
3329 },
3330 )))),
3331 arguments: Default::default(),
3332 digest: None,
3333 non_code_meta: Default::default(),
3334 })));
3335 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3336 left: distance_call_ast,
3337 operator: ast::BinaryOperator::Eq,
3338 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3339 value: ast::LiteralValue::Number {
3340 value: distance.distance.value,
3341 suffix: distance.distance.units,
3342 },
3343 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3344 KclError::refactor(format!(
3345 "Could not format numeric suffix: {:?}",
3346 distance.distance.units
3347 ))
3348 })?,
3349 digest: None,
3350 }))),
3351 digest: None,
3352 })));
3353
3354 let (sketch_block_ref, _) = self.mutate_ast(
3356 new_ast,
3357 sketch_id,
3358 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3359 )?;
3360 Ok(sketch_block_ref)
3361 }
3362
3363 async fn add_horizontal(
3364 &mut self,
3365 sketch: ObjectId,
3366 horizontal: Horizontal,
3367 new_ast: &mut ast::Node<ast::Program>,
3368 ) -> Result<AstNodeRef, KclError> {
3369 let sketch_id = sketch;
3370
3371 let first_arg_ast = match horizontal {
3373 Horizontal::Line { line } => {
3374 let line_object = self
3375 .scene_graph
3376 .objects
3377 .get(line.0)
3378 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3379 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3380 let kind = line_object.kind.human_friendly_kind_with_article();
3381 return Err(KclError::refactor(format!(
3382 "This constraint only works on Segments, but you selected {kind}"
3383 )));
3384 };
3385 let Segment::Line(_) = line_segment else {
3386 return Err(KclError::refactor(format!(
3387 "Only lines can be made horizontal, but you selected {}",
3388 line_segment.human_friendly_kind_with_article(),
3389 )));
3390 };
3391 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?
3392 }
3393 Horizontal::Points { points } => {
3394 let point_asts = points
3395 .iter()
3396 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3397 .collect::<Result<Vec<_>, _>>()?;
3398 ast::ArrayExpression::new(point_asts).into()
3399 }
3400 };
3401
3402 let horizontal_ast = create_horizontal_ast(first_arg_ast);
3404
3405 let (sketch_block_ref, _) = self.mutate_ast(
3407 new_ast,
3408 sketch_id,
3409 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3410 )?;
3411 Ok(sketch_block_ref)
3412 }
3413
3414 async fn add_lines_equal_length(
3415 &mut self,
3416 sketch: ObjectId,
3417 lines_equal_length: LinesEqualLength,
3418 new_ast: &mut ast::Node<ast::Program>,
3419 ) -> Result<AstNodeRef, KclError> {
3420 if lines_equal_length.lines.len() < 2 {
3421 return Err(KclError::refactor(format!(
3422 "Lines equal length constraint must have at least 2 lines, got {}",
3423 lines_equal_length.lines.len()
3424 )));
3425 };
3426
3427 let sketch_id = sketch;
3428
3429 let line_asts = lines_equal_length
3431 .lines
3432 .iter()
3433 .map(|line_id| {
3434 let line_object = self
3435 .scene_graph
3436 .objects
3437 .get(line_id.0)
3438 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3439 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3440 let kind = line_object.kind.human_friendly_kind_with_article();
3441 return Err(KclError::refactor(format!(
3442 "This constraint only works on Segments, but you selected {kind}"
3443 )));
3444 };
3445 let Segment::Line(_) = line_segment else {
3446 let kind = line_segment.human_friendly_kind_with_article();
3447 return Err(KclError::refactor(format!(
3448 "Only lines can be made equal length, but you selected {kind}"
3449 )));
3450 };
3451
3452 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
3453 })
3454 .collect::<Result<Vec<_>, _>>()?;
3455
3456 let equal_length_ast = create_equal_length_ast(line_asts);
3458
3459 let (sketch_block_ref, _) = self.mutate_ast(
3461 new_ast,
3462 sketch_id,
3463 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3464 )?;
3465 Ok(sketch_block_ref)
3466 }
3467
3468 fn equal_radius_segment_id_to_ast_reference(
3469 &mut self,
3470 segment_id: ObjectId,
3471 new_ast: &mut ast::Node<ast::Program>,
3472 ) -> Result<ast::Expr, KclError> {
3473 let segment_object = self
3474 .scene_graph
3475 .objects
3476 .get(segment_id.0)
3477 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3478 let ObjectKind::Segment { segment } = &segment_object.kind else {
3479 return Err(KclError::refactor(format!(
3480 "Object is not a segment, it was {}",
3481 segment_object.kind.human_friendly_kind_with_article()
3482 )));
3483 };
3484
3485 let ref_type = match segment {
3486 Segment::Arc(_) => "arc",
3487 Segment::Circle(_) => CIRCLE_VARIABLE,
3488 _ => {
3489 return Err(KclError::refactor(format!(
3490 "equalRadius supports only arc/circle segments, got {}",
3491 segment.human_friendly_kind_with_article()
3492 )));
3493 }
3494 };
3495
3496 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3497 }
3498
3499 async fn add_parallel(
3500 &mut self,
3501 sketch: ObjectId,
3502 parallel: Parallel,
3503 new_ast: &mut ast::Node<ast::Program>,
3504 ) -> Result<AstNodeRef, KclError> {
3505 if parallel.lines.len() < 2 {
3506 return Err(KclError::refactor(format!(
3507 "Parallel constraint must have at least 2 lines, got {}",
3508 parallel.lines.len()
3509 )));
3510 };
3511
3512 let sketch_id = sketch;
3513
3514 let line_asts = parallel
3515 .lines
3516 .iter()
3517 .map(|line_id| {
3518 let line_object = self
3519 .scene_graph
3520 .objects
3521 .get(line_id.0)
3522 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3523 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3524 let kind = line_object.kind.human_friendly_kind_with_article();
3525 return Err(KclError::refactor(format!(
3526 "This constraint only works on Segments, but you selected {kind}"
3527 )));
3528 };
3529 let Segment::Line(_) = line_segment else {
3530 let kind = line_segment.human_friendly_kind_with_article();
3531 return Err(KclError::refactor(format!(
3532 "Only lines can be made parallel, but you selected {kind}"
3533 )));
3534 };
3535
3536 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)
3537 })
3538 .collect::<Result<Vec<_>, _>>()?;
3539
3540 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3541 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3542 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3543 ast::ArrayExpression {
3544 elements: line_asts,
3545 digest: None,
3546 non_code_meta: Default::default(),
3547 },
3548 )))),
3549 arguments: Default::default(),
3550 digest: None,
3551 non_code_meta: Default::default(),
3552 })));
3553
3554 let (sketch_block_ref, _) = self.mutate_ast(
3555 new_ast,
3556 sketch_id,
3557 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3558 )?;
3559 Ok(sketch_block_ref)
3560 }
3561
3562 async fn add_perpendicular(
3563 &mut self,
3564 sketch: ObjectId,
3565 perpendicular: Perpendicular,
3566 new_ast: &mut ast::Node<ast::Program>,
3567 ) -> Result<AstNodeRef, KclError> {
3568 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3569 .await
3570 }
3571
3572 async fn add_lines_at_angle_constraint(
3573 &mut self,
3574 sketch: ObjectId,
3575 angle_kind: LinesAtAngleKind,
3576 lines: Vec<ObjectId>,
3577 new_ast: &mut ast::Node<ast::Program>,
3578 ) -> Result<AstNodeRef, KclError> {
3579 let &[line0_id, line1_id] = lines.as_slice() else {
3580 return Err(KclError::refactor(format!(
3581 "{} constraint must have exactly 2 lines, got {}",
3582 angle_kind.to_function_name(),
3583 lines.len()
3584 )));
3585 };
3586
3587 let sketch_id = sketch;
3588
3589 let line0_object = self
3591 .scene_graph
3592 .objects
3593 .get(line0_id.0)
3594 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3595 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3596 let kind = line0_object.kind.human_friendly_kind_with_article();
3597 return Err(KclError::refactor(format!(
3598 "This constraint only works on Segments, but you selected {kind}"
3599 )));
3600 };
3601 let Segment::Line(_) = line0_segment else {
3602 return Err(KclError::refactor(format!(
3603 "Only lines can be made {}, but you selected {}",
3604 angle_kind.to_function_name(),
3605 line0_segment.human_friendly_kind_with_article(),
3606 )));
3607 };
3608 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
3609
3610 let line1_object = self
3611 .scene_graph
3612 .objects
3613 .get(line1_id.0)
3614 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3615 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3616 let kind = line1_object.kind.human_friendly_kind_with_article();
3617 return Err(KclError::refactor(format!(
3618 "This constraint only works on Segments, but you selected {kind}"
3619 )));
3620 };
3621 let Segment::Line(_) = line1_segment else {
3622 return Err(KclError::refactor(format!(
3623 "Only lines can be made {}, but you selected {}",
3624 angle_kind.to_function_name(),
3625 line1_segment.human_friendly_kind_with_article(),
3626 )));
3627 };
3628 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
3629
3630 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3632 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3633 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3634 ast::ArrayExpression {
3635 elements: vec![line0_ast, line1_ast],
3636 digest: None,
3637 non_code_meta: Default::default(),
3638 },
3639 )))),
3640 arguments: Default::default(),
3641 digest: None,
3642 non_code_meta: Default::default(),
3643 })));
3644
3645 let (sketch_block_ref, _) = self.mutate_ast(
3647 new_ast,
3648 sketch_id,
3649 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3650 )?;
3651 Ok(sketch_block_ref)
3652 }
3653
3654 async fn add_vertical(
3655 &mut self,
3656 sketch: ObjectId,
3657 vertical: Vertical,
3658 new_ast: &mut ast::Node<ast::Program>,
3659 ) -> Result<AstNodeRef, KclError> {
3660 let sketch_id = sketch;
3661
3662 let first_arg_ast = match vertical {
3663 Vertical::Line { line } => {
3664 let line_object = self
3666 .scene_graph
3667 .objects
3668 .get(line.0)
3669 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3670 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3671 let kind = line_object.kind.human_friendly_kind_with_article();
3672 return Err(KclError::refactor(format!(
3673 "This constraint only works on Segments, but you selected {kind}"
3674 )));
3675 };
3676 let Segment::Line(_) = line_segment else {
3677 return Err(KclError::refactor(format!(
3678 "Only lines can be made vertical, but you selected {}",
3679 line_segment.human_friendly_kind_with_article()
3680 )));
3681 };
3682 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?
3683 }
3684 Vertical::Points { points } => {
3685 let point_asts = points
3686 .iter()
3687 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3688 .collect::<Result<Vec<_>, _>>()?;
3689 ast::ArrayExpression::new(point_asts).into()
3690 }
3691 };
3692
3693 let vertical_ast = create_vertical_ast(first_arg_ast);
3695
3696 let (sketch_block_ref, _) = self.mutate_ast(
3698 new_ast,
3699 sketch_id,
3700 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
3701 )?;
3702 Ok(sketch_block_ref)
3703 }
3704
3705 async fn execute_after_add_constraint(
3706 &mut self,
3707 ctx: &ExecutorContext,
3708 sketch_id: ObjectId,
3709 #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_ref: AstNodeRef,
3710 new_ast: &mut ast::Node<ast::Program>,
3711 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3712 let new_source = source_from_ast(new_ast);
3714 let (new_program, errors) = Program::parse(&new_source)
3716 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3717 if !errors.is_empty() {
3718 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3719 "Error parsing KCL source after adding constraint: {errors:?}"
3720 ))));
3721 }
3722 let Some(new_program) = new_program else {
3723 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3724 "No AST produced after adding constraint".to_string(),
3725 )));
3726 };
3727 #[cfg(feature = "artifact-graph")]
3728 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
3729 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3730 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
3731 )))
3732 })?;
3733
3734 let mut truncated_program = new_program.clone();
3737 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
3738 .map_err(KclErrorWithOutputs::no_outputs)?;
3739
3740 let outcome = ctx
3742 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
3743 .await?;
3744
3745 #[cfg(not(feature = "artifact-graph"))]
3746 let new_object_ids = Vec::new();
3747 #[cfg(feature = "artifact-graph")]
3748 let new_object_ids = {
3749 let constraint_id = outcome
3751 .source_range_to_object
3752 .get(&constraint_node_ref.range)
3753 .copied()
3754 .ok_or_else(|| {
3755 KclErrorWithOutputs::from_error_outcome(
3756 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
3757 outcome.clone(),
3758 )
3759 })?;
3760 vec![constraint_id]
3761 };
3762
3763 self.program = new_program;
3766
3767 let outcome = self.update_state_after_exec(outcome, true);
3769
3770 let src_delta = SourceDelta { text: new_source };
3771 let scene_graph_delta = SceneGraphDelta {
3772 new_graph: self.scene_graph.clone(),
3773 invalidates_ids: false,
3774 new_objects: new_object_ids,
3775 exec_outcome: outcome,
3776 };
3777 Ok((src_delta, scene_graph_delta))
3778 }
3779
3780 fn find_referenced_constraints(
3782 &self,
3783 sketch_id: ObjectId,
3784 segment_ids_set: &AhashIndexSet<ObjectId>,
3785 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
3786 let sketch_object = self
3788 .scene_graph
3789 .objects
3790 .get(sketch_id.0)
3791 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
3792 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3793 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3794 };
3795 let mut constraint_ids_set = AhashIndexSet::default();
3796 for constraint_id in &sketch.constraints {
3797 let constraint_object = self
3798 .scene_graph
3799 .objects
3800 .get(constraint_id.0)
3801 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
3802 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
3803 return Err(KclError::refactor(format!(
3804 "Object is not a constraint, it is {}",
3805 constraint_object.kind.human_friendly_kind_with_article()
3806 )));
3807 };
3808 let depends_on_segment = match constraint {
3809 Constraint::Coincident(c) => c.segment_ids().any(|seg_id| {
3810 if segment_ids_set.contains(&seg_id) {
3812 return true;
3813 }
3814 let seg_object = self.scene_graph.objects.get(seg_id.0);
3816 if let Some(obj) = seg_object
3817 && let ObjectKind::Segment { segment } = &obj.kind
3818 && let Segment::Point(pt) = segment
3819 && let Some(owner_line_id) = pt.owner
3820 {
3821 return segment_ids_set.contains(&owner_line_id);
3822 }
3823 false
3824 }),
3825 Constraint::Distance(d) => d.point_ids().any(|pt_id| {
3826 if segment_ids_set.contains(&pt_id) {
3827 return true;
3828 }
3829 let pt_object = self.scene_graph.objects.get(pt_id.0);
3830 if let Some(obj) = pt_object
3831 && let ObjectKind::Segment { segment } = &obj.kind
3832 && let Segment::Point(pt) = segment
3833 && let Some(owner_line_id) = pt.owner
3834 {
3835 return segment_ids_set.contains(&owner_line_id);
3836 }
3837 false
3838 }),
3839 Constraint::Fixed(_) => false,
3840 Constraint::Radius(r) => segment_ids_set.contains(&r.arc),
3841 Constraint::Diameter(d) => segment_ids_set.contains(&d.arc),
3842 Constraint::EqualRadius(equal_radius) => {
3843 equal_radius.input.iter().any(|seg_id| segment_ids_set.contains(seg_id))
3844 }
3845 Constraint::HorizontalDistance(d) => d.point_ids().any(|pt_id| {
3846 let pt_object = self.scene_graph.objects.get(pt_id.0);
3847 if let Some(obj) = pt_object
3848 && let ObjectKind::Segment { segment } = &obj.kind
3849 && let Segment::Point(pt) = segment
3850 && let Some(owner_line_id) = pt.owner
3851 {
3852 return segment_ids_set.contains(&owner_line_id);
3853 }
3854 false
3855 }),
3856 Constraint::VerticalDistance(d) => d.point_ids().any(|pt_id| {
3857 let pt_object = self.scene_graph.objects.get(pt_id.0);
3858 if let Some(obj) = pt_object
3859 && let ObjectKind::Segment { segment } = &obj.kind
3860 && let Segment::Point(pt) = segment
3861 && let Some(owner_line_id) = pt.owner
3862 {
3863 return segment_ids_set.contains(&owner_line_id);
3864 }
3865 false
3866 }),
3867 Constraint::Horizontal(h) => match h {
3868 Horizontal::Line { line } => segment_ids_set.contains(line),
3869 Horizontal::Points { points } => points.iter().any(|point| match point {
3870 ConstraintSegment::Segment(point) => segment_ids_set.contains(point),
3871 ConstraintSegment::Origin(_) => false,
3872 }),
3873 },
3874 Constraint::Vertical(v) => match v {
3875 Vertical::Line { line } => segment_ids_set.contains(line),
3876 Vertical::Points { points } => points.iter().any(|point| match point {
3877 ConstraintSegment::Segment(point) => segment_ids_set.contains(point),
3878 ConstraintSegment::Origin(_) => false,
3879 }),
3880 },
3881 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
3882 .lines
3883 .iter()
3884 .any(|line_id| segment_ids_set.contains(line_id)),
3885 Constraint::Parallel(parallel) => {
3886 parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
3887 }
3888 Constraint::Perpendicular(perpendicular) => perpendicular
3889 .lines
3890 .iter()
3891 .any(|line_id| segment_ids_set.contains(line_id)),
3892 Constraint::Angle(angle) => angle.lines.iter().any(|line_id| segment_ids_set.contains(line_id)),
3893 Constraint::Tangent(tangent) => tangent.input.iter().any(|seg_id| segment_ids_set.contains(seg_id)),
3894 };
3895 if depends_on_segment {
3896 constraint_ids_set.insert(*constraint_id);
3897 }
3898 }
3899 Ok(constraint_ids_set)
3900 }
3901
3902 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
3903 #[cfg(not(feature = "artifact-graph"))]
3904 {
3905 let _ = freedom_analysis_ran; outcome
3907 }
3908 #[cfg(feature = "artifact-graph")]
3909 {
3910 let mut outcome = outcome;
3911 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
3912
3913 if freedom_analysis_ran {
3914 self.point_freedom_cache.clear();
3917 for new_obj in &new_objects {
3918 if let ObjectKind::Segment {
3919 segment: crate::front::Segment::Point(point),
3920 } = &new_obj.kind
3921 {
3922 self.point_freedom_cache.insert(new_obj.id, point.freedom);
3923 }
3924 }
3925 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
3926 self.scene_graph.objects = new_objects;
3928 } else {
3929 for old_obj in &self.scene_graph.objects {
3932 if let ObjectKind::Segment {
3933 segment: crate::front::Segment::Point(point),
3934 } = &old_obj.kind
3935 {
3936 self.point_freedom_cache.insert(old_obj.id, point.freedom);
3937 }
3938 }
3939
3940 let mut updated_objects = Vec::with_capacity(new_objects.len());
3942 for new_obj in new_objects {
3943 let mut obj = new_obj;
3944 if let ObjectKind::Segment {
3945 segment: crate::front::Segment::Point(point),
3946 } = &mut obj.kind
3947 {
3948 let new_freedom = point.freedom;
3949 match new_freedom {
3955 Freedom::Free => {
3956 match self.point_freedom_cache.get(&obj.id).copied() {
3957 Some(Freedom::Conflict) => {
3958 }
3961 Some(Freedom::Fixed) => {
3962 point.freedom = Freedom::Fixed;
3964 }
3965 Some(Freedom::Free) => {
3966 }
3968 None => {
3969 }
3971 }
3972 }
3973 Freedom::Fixed => {
3974 }
3976 Freedom::Conflict => {
3977 }
3979 }
3980 self.point_freedom_cache.insert(obj.id, point.freedom);
3982 }
3983 updated_objects.push(obj);
3984 }
3985
3986 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
3987 self.scene_graph.objects = updated_objects;
3988 }
3989 outcome
3990 }
3991 }
3992
3993 fn mutate_ast(
3994 &mut self,
3995 ast: &mut ast::Node<ast::Program>,
3996 object_id: ObjectId,
3997 command: AstMutateCommand,
3998 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
3999 let sketch_object = self
4000 .scene_graph
4001 .objects
4002 .get(object_id.0)
4003 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4004 match &sketch_object.source {
4005 SourceRef::Simple { range, node_path: _ } => mutate_ast_node_by_source_range(ast, *range, command),
4006 SourceRef::BackTrace { .. } => {
4007 Err(KclError::refactor("BackTrace source refs not supported yet".to_owned()))
4008 }
4009 }
4010 }
4011}
4012
4013fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4014 let sketch_object = scene_graph
4016 .objects
4017 .get(sketch_id.0)
4018 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4019 let ObjectKind::Sketch(_) = &sketch_object.kind else {
4020 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4021 };
4022 expect_single_node_ref(sketch_object)
4023}
4024
4025fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4026 match &object.source {
4027 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4028 range: *range,
4029 node_path: node_path.clone(),
4030 }),
4031 SourceRef::BackTrace { ranges } => {
4032 let [range] = ranges.as_slice() else {
4033 return Err(KclError::refactor(format!(
4034 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4035 ranges.len()
4036 )));
4037 };
4038 Ok(AstNodeRef {
4039 range: range.0,
4040 node_path: range.1.clone(),
4041 })
4042 }
4043 }
4044}
4045
4046fn expect_single_source_range(source_ref: &SourceRef) -> Result<SourceRange, KclError> {
4047 match source_ref {
4048 SourceRef::Simple { range, node_path: _ } => Ok(*range),
4049 SourceRef::BackTrace { ranges } => {
4050 if ranges.len() != 1 {
4051 return Err(KclError::refactor(format!(
4052 "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
4053 ranges.len(),
4054 )));
4055 }
4056 Ok(ranges[0].0)
4057 }
4058 }
4059}
4060
4061fn only_sketch_block_from_range(
4064 ast: &mut ast::Node<ast::Program>,
4065 sketch_block_range: SourceRange,
4066 edit_kind: ChangeKind,
4067) -> Result<(), KclError> {
4068 let r1 = sketch_block_range;
4069 let matches_range = |r2: SourceRange| -> bool {
4070 match edit_kind {
4073 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4074 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4076 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4077 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4079 }
4080 };
4081 let mut found = false;
4082 for item in ast.body.iter_mut() {
4083 match item {
4084 ast::BodyItem::ImportStatement(_) => {}
4085 ast::BodyItem::ExpressionStatement(node) => {
4086 if matches_range(SourceRange::from(&*node))
4087 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4088 {
4089 sketch_block.is_being_edited = true;
4090 found = true;
4091 break;
4092 }
4093 }
4094 ast::BodyItem::VariableDeclaration(node) => {
4095 if matches_range(SourceRange::from(&node.declaration.init))
4096 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4097 {
4098 sketch_block.is_being_edited = true;
4099 found = true;
4100 break;
4101 }
4102 }
4103 ast::BodyItem::TypeDeclaration(_) => {}
4104 ast::BodyItem::ReturnStatement(node) => {
4105 if matches_range(SourceRange::from(&node.argument))
4106 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4107 {
4108 sketch_block.is_being_edited = true;
4109 found = true;
4110 break;
4111 }
4112 }
4113 }
4114 }
4115 if !found {
4116 return Err(KclError::refactor(format!(
4117 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4118 )));
4119 }
4120
4121 Ok(())
4122}
4123
4124fn only_sketch_block(
4125 ast: &mut ast::Node<ast::Program>,
4126 sketch_block_ref: &AstNodeRef,
4127 edit_kind: ChangeKind,
4128) -> Result<(), KclError> {
4129 let Some(target_node_path) = &sketch_block_ref.node_path else {
4130 #[cfg(target_arch = "wasm32")]
4131 web_sys::console::warn_1(
4132 &format!(
4133 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4134 &sketch_block_ref
4135 )
4136 .into(),
4137 );
4138 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4139 };
4140 let mut found = false;
4141 for item in ast.body.iter_mut() {
4142 match item {
4143 ast::BodyItem::ImportStatement(_) => {}
4144 ast::BodyItem::ExpressionStatement(node) => {
4145 if let Some(node_path) = &node.node_path
4147 && node_path == target_node_path
4148 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4149 {
4150 sketch_block.is_being_edited = true;
4151 found = true;
4152 break;
4153 }
4154 if let Some(node_path) = node.expression.node_path()
4156 && node_path == target_node_path
4157 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4158 {
4159 sketch_block.is_being_edited = true;
4160 found = true;
4161 break;
4162 }
4163 }
4164 ast::BodyItem::VariableDeclaration(node) => {
4165 if let Some(node_path) = node.declaration.init.node_path()
4166 && node_path == target_node_path
4167 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4168 {
4169 sketch_block.is_being_edited = true;
4170 found = true;
4171 break;
4172 }
4173 }
4174 ast::BodyItem::TypeDeclaration(_) => {}
4175 ast::BodyItem::ReturnStatement(node) => {
4176 if let Some(node_path) = node.argument.node_path()
4177 && node_path == target_node_path
4178 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4179 {
4180 sketch_block.is_being_edited = true;
4181 found = true;
4182 break;
4183 }
4184 }
4185 }
4186 }
4187 if !found {
4188 return Err(KclError::refactor(format!(
4189 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4190 )));
4191 }
4192
4193 Ok(())
4194}
4195
4196fn sketch_on_ast_expr(
4197 ast: &mut ast::Node<ast::Program>,
4198 scene_graph: &SceneGraph,
4199 on: &Plane,
4200) -> Result<ast::Expr, KclError> {
4201 match on {
4202 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4203 Plane::Object(object_id) => {
4204 let on_object = scene_graph
4205 .objects
4206 .get(object_id.0)
4207 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4208 #[cfg(feature = "artifact-graph")]
4209 {
4210 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4211 return Ok(face_expr);
4212 }
4213 }
4214 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4215 }
4216 }
4217}
4218
4219#[cfg(feature = "artifact-graph")]
4220fn sketch_face_of_scene_object_ast_expr(
4221 ast: &mut ast::Node<ast::Program>,
4222 on_object: &crate::front::Object,
4223) -> Result<Option<ast::Expr>, KclError> {
4224 let SourceRef::BackTrace { ranges } = &on_object.source else {
4225 return Ok(None);
4226 };
4227
4228 match &on_object.kind {
4229 ObjectKind::Wall(_) => {
4230 let [sweep_range, segment_range] = ranges.as_slice() else {
4231 return Err(KclError::refactor(format!(
4232 "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4233 ranges.len(),
4234 on_object.artifact_id
4235 )));
4236 };
4237 let sweep_ref = get_or_insert_ast_reference(
4238 ast,
4239 &SourceRef::Simple {
4240 range: sweep_range.0,
4241 node_path: sweep_range.1.clone(),
4242 },
4243 "solid",
4244 None,
4245 )?;
4246 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4247 return Err(KclError::refactor(format!(
4248 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4249 on_object.artifact_id
4250 )));
4251 };
4252 let solid_name = solid_name_expr.name.name.clone();
4253 let solid_expr = ast_name_expr(solid_name.clone());
4254 let segment_ref = get_or_insert_ast_reference(
4255 ast,
4256 &SourceRef::Simple {
4257 range: segment_range.0,
4258 node_path: segment_range.1.clone(),
4259 },
4260 "line",
4261 None,
4262 )?;
4263
4264 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4265 let ast::Expr::Name(segment_name_expr) = segment_ref else {
4266 return Err(KclError::refactor(format!(
4267 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4268 on_object.artifact_id
4269 )));
4270 };
4271 create_member_expression(
4272 create_member_expression(ast_name_expr(region_name), "tags"),
4273 &segment_name_expr.name.name,
4274 )
4275 } else {
4276 segment_ref
4277 };
4278
4279 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4280 }
4281 ObjectKind::Cap(cap) => {
4282 let [range] = ranges.as_slice() else {
4283 return Err(KclError::refactor(format!(
4284 "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4285 ranges.len(),
4286 on_object.artifact_id
4287 )));
4288 };
4289 let sweep_ref = get_or_insert_ast_reference(
4290 ast,
4291 &SourceRef::Simple {
4292 range: range.0,
4293 node_path: range.1.clone(),
4294 },
4295 "solid",
4296 None,
4297 )?;
4298 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4299 return Err(KclError::refactor(format!(
4300 "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4301 on_object.artifact_id
4302 )));
4303 };
4304 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4305 let face_expr = match cap.kind {
4307 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4308 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4309 };
4310
4311 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4312 }
4313 _ => Ok(None),
4314 }
4315}
4316
4317#[cfg(feature = "artifact-graph")]
4318fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4319 let mut existing_artifact_ids = scene_objects
4320 .iter()
4321 .map(|object| object.artifact_id)
4322 .collect::<HashSet<_>>();
4323
4324 for artifact in artifact_graph.values() {
4325 match artifact {
4326 Artifact::Wall(wall) => {
4327 if existing_artifact_ids.contains(&wall.id) {
4328 continue;
4329 }
4330
4331 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4332 Artifact::Segment(segment) => Some(segment),
4333 _ => None,
4334 }) else {
4335 continue;
4336 };
4337 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4338 Artifact::Sweep(sweep) => Some(sweep),
4339 _ => None,
4340 }) else {
4341 continue;
4342 };
4343 let source_segment = segment
4344 .original_seg_id
4345 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4346 .and_then(|artifact| match artifact {
4347 Artifact::Segment(segment) => Some(segment),
4348 _ => None,
4349 })
4350 .unwrap_or(segment);
4351 let id = ObjectId(scene_objects.len());
4352 scene_objects.push(crate::front::Object {
4353 id,
4354 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4355 label: Default::default(),
4356 comments: Default::default(),
4357 artifact_id: wall.id,
4358 source: SourceRef::BackTrace {
4359 ranges: vec![
4360 (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4361 (
4362 source_segment.code_ref.range,
4363 Some(source_segment.code_ref.node_path.clone()),
4364 ),
4365 ],
4366 },
4367 });
4368 existing_artifact_ids.insert(wall.id);
4369 }
4370 Artifact::Cap(cap) => {
4371 if existing_artifact_ids.contains(&cap.id) {
4372 continue;
4373 }
4374
4375 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4376 Artifact::Sweep(sweep) => Some(sweep),
4377 _ => None,
4378 }) else {
4379 continue;
4380 };
4381 let id = ObjectId(scene_objects.len());
4382 let kind = match cap.sub_type {
4383 CapSubType::Start => crate::frontend::api::CapKind::Start,
4384 CapSubType::End => crate::frontend::api::CapKind::End,
4385 };
4386 scene_objects.push(crate::front::Object {
4387 id,
4388 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4389 label: Default::default(),
4390 comments: Default::default(),
4391 artifact_id: cap.id,
4392 source: SourceRef::BackTrace {
4393 ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4394 },
4395 });
4396 existing_artifact_ids.insert(cap.id);
4397 }
4398 _ => {}
4399 }
4400 }
4401}
4402
4403fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4404 use crate::engine::PlaneName;
4405
4406 match name {
4407 PlaneName::Xy => ast_name_expr("XY".to_owned()),
4408 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4409 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4410 PlaneName::NegXy => negated_plane_ast_expr("XY"),
4411 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4412 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4413 }
4414}
4415
4416fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4417 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4418 ast::UnaryOperator::Neg,
4419 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4420 )))
4421}
4422
4423#[cfg(feature = "artifact-graph")]
4424fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4425 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4426 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4427 unlabeled: Some(solid_expr),
4428 arguments: vec![ast::LabeledArg {
4429 label: Some(ast::Identifier::new("face")),
4430 arg: face_expr,
4431 }],
4432 digest: None,
4433 non_code_meta: Default::default(),
4434 })))
4435}
4436
4437#[cfg(feature = "artifact-graph")]
4438fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4439 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4440 return None;
4441 };
4442 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4443 return None;
4444 };
4445 if !matches!(
4446 sweep_call.callee.name.name.as_str(),
4447 "extrude" | "revolve" | "sweep" | "loft"
4448 ) {
4449 return None;
4450 }
4451 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4452 return None;
4453 };
4454 let candidate = region_name_expr.name.name.clone();
4455 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4456 return None;
4457 };
4458 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
4459 return None;
4460 };
4461 if region_call.callee.name.name != "region" {
4462 return None;
4463 }
4464 Some(candidate)
4465}
4466
4467fn get_or_insert_ast_reference(
4474 ast: &mut ast::Node<ast::Program>,
4475 source_ref: &SourceRef,
4476 prefix: &str,
4477 property: Option<&str>,
4478) -> Result<ast::Expr, KclError> {
4479 let range = expect_single_source_range(source_ref)?;
4480 let command = AstMutateCommand::AddVariableDeclaration {
4481 prefix: prefix.to_owned(),
4482 };
4483 let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
4484 let AstMutateCommandReturn::Name(var_name) = ret else {
4485 return Err(KclError::refactor(
4486 "Expected variable name returned from AddVariableDeclaration".to_owned(),
4487 ));
4488 };
4489 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4490 let Some(property) = property else {
4491 return Ok(var_expr);
4493 };
4494
4495 Ok(create_member_expression(var_expr, property))
4496}
4497
4498fn mutate_ast_node_by_source_range(
4499 ast: &mut ast::Node<ast::Program>,
4500 source_range: SourceRange,
4501 command: AstMutateCommand,
4502) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4503 let mut context = AstMutateContext {
4504 source_range,
4505 node_path: None,
4506 command,
4507 defined_names_stack: Default::default(),
4508 };
4509 let control = dfs_mut(ast, &mut context);
4510 match control {
4511 ControlFlow::Continue(_) => Err(KclError::refactor(format!("Source range not found: {source_range:?}"))),
4512 ControlFlow::Break(break_value) => break_value,
4513 }
4514}
4515
4516#[derive(Debug)]
4517struct AstMutateContext {
4518 source_range: SourceRange,
4519 node_path: Option<ast::NodePath>,
4520 command: AstMutateCommand,
4521 defined_names_stack: Vec<HashSet<String>>,
4522}
4523
4524#[derive(Debug)]
4525#[allow(clippy::large_enum_variant)]
4526enum AstMutateCommand {
4527 AddSketchBlockExprStmt {
4529 expr: ast::Expr,
4530 },
4531 AddSketchBlockVarDecl {
4533 prefix: String,
4534 expr: ast::Expr,
4535 },
4536 AddVariableDeclaration {
4537 prefix: String,
4538 },
4539 EditPoint {
4540 at: ast::Expr,
4541 },
4542 EditLine {
4543 start: ast::Expr,
4544 end: ast::Expr,
4545 construction: Option<bool>,
4546 },
4547 EditArc {
4548 start: ast::Expr,
4549 end: ast::Expr,
4550 center: ast::Expr,
4551 construction: Option<bool>,
4552 },
4553 EditCircle {
4554 start: ast::Expr,
4555 center: ast::Expr,
4556 construction: Option<bool>,
4557 },
4558 EditConstraintValue {
4559 value: ast::BinaryPart,
4560 },
4561 EditCallUnlabeled {
4562 arg: ast::Expr,
4563 },
4564 #[cfg(feature = "artifact-graph")]
4565 EditVarInitialValue {
4566 value: Number,
4567 },
4568 DeleteNode,
4569}
4570
4571impl AstMutateCommand {
4572 fn needs_defined_names_stack(&self) -> bool {
4573 matches!(
4574 self,
4575 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4576 )
4577 }
4578}
4579
4580#[derive(Debug)]
4581enum AstMutateCommandReturn {
4582 None,
4583 Name(String),
4584}
4585
4586#[derive(Debug, Clone)]
4587struct AstNodeRef {
4588 range: SourceRange,
4589 node_path: Option<ast::NodePath>,
4590}
4591
4592impl<T> From<&ast::Node<T>> for AstNodeRef {
4593 fn from(value: &ast::Node<T>) -> Self {
4594 AstNodeRef {
4595 range: value.into(),
4596 node_path: value.node_path.clone(),
4597 }
4598 }
4599}
4600
4601impl From<&ast::BodyItem> for AstNodeRef {
4602 fn from(value: &ast::BodyItem) -> Self {
4603 match value {
4604 ast::BodyItem::ImportStatement(node) => AstNodeRef {
4605 range: node.into(),
4606 node_path: node.node_path.clone(),
4607 },
4608 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4609 range: node.into(),
4610 node_path: node.node_path.clone(),
4611 },
4612 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4613 range: node.into(),
4614 node_path: node.node_path.clone(),
4615 },
4616 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4617 range: node.into(),
4618 node_path: node.node_path.clone(),
4619 },
4620 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
4621 range: node.into(),
4622 node_path: node.node_path.clone(),
4623 },
4624 }
4625 }
4626}
4627
4628impl From<&ast::Expr> for AstNodeRef {
4629 fn from(value: &ast::Expr) -> Self {
4630 AstNodeRef {
4631 range: SourceRange::from(value),
4632 node_path: value.node_path().cloned(),
4633 }
4634 }
4635}
4636
4637impl From<&AstMutateContext> for AstNodeRef {
4638 fn from(value: &AstMutateContext) -> Self {
4639 AstNodeRef {
4640 range: value.source_range,
4641 node_path: value.node_path.clone(),
4642 }
4643 }
4644}
4645
4646impl TryFrom<&NodeMut<'_>> for AstNodeRef {
4647 type Error = crate::walk::AstNodeError;
4648
4649 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
4650 Ok(AstNodeRef {
4651 range: SourceRange::try_from(value)?,
4652 node_path: value.try_into()?,
4653 })
4654 }
4655}
4656
4657impl From<AstNodeRef> for SourceRange {
4658 fn from(value: AstNodeRef) -> Self {
4659 value.range
4660 }
4661}
4662
4663impl Visitor for AstMutateContext {
4664 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
4665 type Continue = ();
4666
4667 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
4668 filter_and_process(self, node)
4669 }
4670
4671 fn finish(&mut self, node: NodeMut<'_>) {
4672 match &node {
4673 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
4674 self.defined_names_stack.pop();
4675 }
4676 _ => {}
4677 }
4678 }
4679}
4680
4681fn filter_and_process(
4682 ctx: &mut AstMutateContext,
4683 node: NodeMut,
4684) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
4685 let Ok(node_range) = SourceRange::try_from(&node) else {
4686 return TraversalReturn::new_continue(());
4688 };
4689 if let NodeMut::VariableDeclaration(var_decl) = &node {
4694 let expr_range = SourceRange::from(&var_decl.declaration.init);
4695 if expr_range == ctx.source_range {
4696 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
4697 return TraversalReturn::new_break(Ok((
4700 AstNodeRef::from(&**var_decl),
4701 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
4702 )));
4703 }
4704 if let AstMutateCommand::DeleteNode = &ctx.command {
4705 return TraversalReturn {
4708 mutate_body_item: MutateBodyItem::Delete,
4709 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
4710 };
4711 }
4712 }
4713 }
4714
4715 if ctx.command.needs_defined_names_stack() {
4716 if let NodeMut::Program(program) = &node {
4717 ctx.defined_names_stack.push(find_defined_names(*program));
4718 } else if let NodeMut::SketchBlock(block) = &node {
4719 ctx.defined_names_stack.push(find_defined_names(&block.body));
4720 }
4721 }
4722
4723 if node_range != ctx.source_range {
4726 return TraversalReturn::new_continue(());
4727 }
4728 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
4729 return TraversalReturn::new_continue(());
4730 };
4731 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
4732}
4733
4734fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
4735 match &ctx.command {
4736 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
4737 if let NodeMut::SketchBlock(sketch_block) = node {
4738 sketch_block
4739 .body
4740 .items
4741 .push(ast::BodyItem::ExpressionStatement(ast::Node {
4742 inner: ast::ExpressionStatement {
4743 expression: expr.clone(),
4744 digest: None,
4745 },
4746 start: Default::default(),
4747 end: Default::default(),
4748 module_id: Default::default(),
4749 node_path: None,
4750 outer_attrs: Default::default(),
4751 pre_comments: Default::default(),
4752 comment_start: Default::default(),
4753 }));
4754 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4755 }
4756 }
4757 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
4758 if let NodeMut::SketchBlock(sketch_block) = node {
4759 let empty_defined_names = HashSet::new();
4760 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
4761 let Ok(name) = next_free_name(prefix, defined_names) else {
4762 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4763 };
4764 sketch_block
4765 .body
4766 .items
4767 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
4768 ast::VariableDeclaration::new(
4769 ast::VariableDeclarator::new(&name, expr.clone()),
4770 ast::ItemVisibility::Default,
4771 ast::VariableKind::Const,
4772 ),
4773 ))));
4774 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
4775 }
4776 }
4777 AstMutateCommand::AddVariableDeclaration { prefix } => {
4778 if let NodeMut::VariableDeclaration(inner) = node {
4779 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
4780 }
4781 if let NodeMut::ExpressionStatement(expr_stmt) = node {
4782 let empty_defined_names = HashSet::new();
4783 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
4784 let Ok(name) = next_free_name(prefix, defined_names) else {
4785 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4787 };
4788 let mutate_node =
4789 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
4790 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
4791 ast::ItemVisibility::Default,
4792 ast::VariableKind::Const,
4793 ))));
4794 return TraversalReturn {
4795 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
4796 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
4797 };
4798 }
4799 }
4800 AstMutateCommand::EditPoint { at } => {
4801 if let NodeMut::CallExpressionKw(call) = node {
4802 if call.callee.name.name != POINT_FN {
4803 return TraversalReturn::new_continue(());
4804 }
4805 for labeled_arg in &mut call.arguments {
4807 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
4808 labeled_arg.arg = at.clone();
4809 }
4810 }
4811 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4812 }
4813 }
4814 AstMutateCommand::EditLine {
4815 start,
4816 end,
4817 construction,
4818 } => {
4819 if let NodeMut::CallExpressionKw(call) = node {
4820 if call.callee.name.name != LINE_FN {
4821 return TraversalReturn::new_continue(());
4822 }
4823 for labeled_arg in &mut call.arguments {
4825 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
4826 labeled_arg.arg = start.clone();
4827 }
4828 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
4829 labeled_arg.arg = end.clone();
4830 }
4831 }
4832 if let Some(construction_value) = construction {
4834 let construction_exists = call
4835 .arguments
4836 .iter()
4837 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
4838 if *construction_value {
4839 if construction_exists {
4841 for labeled_arg in &mut call.arguments {
4843 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
4844 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4845 value: ast::LiteralValue::Bool(true),
4846 raw: "true".to_string(),
4847 digest: None,
4848 })));
4849 }
4850 }
4851 } else {
4852 call.arguments.push(ast::LabeledArg {
4854 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
4855 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4856 value: ast::LiteralValue::Bool(true),
4857 raw: "true".to_string(),
4858 digest: None,
4859 }))),
4860 });
4861 }
4862 } else {
4863 call.arguments
4865 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
4866 }
4867 }
4868 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4869 }
4870 }
4871 AstMutateCommand::EditArc {
4872 start,
4873 end,
4874 center,
4875 construction,
4876 } => {
4877 if let NodeMut::CallExpressionKw(call) = node {
4878 if call.callee.name.name != ARC_FN {
4879 return TraversalReturn::new_continue(());
4880 }
4881 for labeled_arg in &mut call.arguments {
4883 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
4884 labeled_arg.arg = start.clone();
4885 }
4886 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
4887 labeled_arg.arg = end.clone();
4888 }
4889 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
4890 labeled_arg.arg = center.clone();
4891 }
4892 }
4893 if let Some(construction_value) = construction {
4895 let construction_exists = call
4896 .arguments
4897 .iter()
4898 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
4899 if *construction_value {
4900 if construction_exists {
4902 for labeled_arg in &mut call.arguments {
4904 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
4905 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4906 value: ast::LiteralValue::Bool(true),
4907 raw: "true".to_string(),
4908 digest: None,
4909 })));
4910 }
4911 }
4912 } else {
4913 call.arguments.push(ast::LabeledArg {
4915 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
4916 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4917 value: ast::LiteralValue::Bool(true),
4918 raw: "true".to_string(),
4919 digest: None,
4920 }))),
4921 });
4922 }
4923 } else {
4924 call.arguments
4926 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
4927 }
4928 }
4929 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4930 }
4931 }
4932 AstMutateCommand::EditCircle {
4933 start,
4934 center,
4935 construction,
4936 } => {
4937 if let NodeMut::CallExpressionKw(call) = node {
4938 if call.callee.name.name != CIRCLE_FN {
4939 return TraversalReturn::new_continue(());
4940 }
4941 for labeled_arg in &mut call.arguments {
4943 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
4944 labeled_arg.arg = start.clone();
4945 }
4946 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
4947 labeled_arg.arg = center.clone();
4948 }
4949 }
4950 if let Some(construction_value) = construction {
4952 let construction_exists = call
4953 .arguments
4954 .iter()
4955 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
4956 if *construction_value {
4957 if construction_exists {
4958 for labeled_arg in &mut call.arguments {
4959 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
4960 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4961 value: ast::LiteralValue::Bool(true),
4962 raw: "true".to_string(),
4963 digest: None,
4964 })));
4965 }
4966 }
4967 } else {
4968 call.arguments.push(ast::LabeledArg {
4969 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
4970 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
4971 value: ast::LiteralValue::Bool(true),
4972 raw: "true".to_string(),
4973 digest: None,
4974 }))),
4975 });
4976 }
4977 } else {
4978 call.arguments
4979 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
4980 }
4981 }
4982 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
4983 }
4984 }
4985 AstMutateCommand::EditConstraintValue { value } => {
4986 if let NodeMut::BinaryExpression(binary_expr) = node {
4987 let left_is_constraint = matches!(
4988 &binary_expr.left,
4989 ast::BinaryPart::CallExpressionKw(call)
4990 if matches!(
4991 call.callee.name.name.as_str(),
4992 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
4993 )
4994 );
4995 if left_is_constraint {
4996 binary_expr.right = value.clone();
4997 } else {
4998 binary_expr.left = value.clone();
4999 }
5000
5001 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5002 }
5003 }
5004 AstMutateCommand::EditCallUnlabeled { arg } => {
5005 if let NodeMut::CallExpressionKw(call) = node {
5006 call.unlabeled = Some(arg.clone());
5007 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5008 }
5009 }
5010 #[cfg(feature = "artifact-graph")]
5011 AstMutateCommand::EditVarInitialValue { value } => {
5012 if let NodeMut::NumericLiteral(numeric_literal) = node {
5013 let Ok(literal) = to_source_number(*value) else {
5015 return TraversalReturn::new_break(Err(KclError::refactor(format!(
5016 "Could not convert number to AST literal: {:?}",
5017 *value
5018 ))));
5019 };
5020 *numeric_literal = ast::Node::no_src(literal);
5021 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5022 }
5023 }
5024 AstMutateCommand::DeleteNode => {
5025 return TraversalReturn {
5026 mutate_body_item: MutateBodyItem::Delete,
5027 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5028 };
5029 }
5030 }
5031 TraversalReturn::new_continue(())
5032}
5033
5034struct FindSketchBlockSourceRange {
5035 target_before_mutation: SourceRange,
5037 found: Cell<Option<AstNodeRef>>,
5041}
5042
5043impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5044 type Error = crate::front::Error;
5045
5046 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5047 let Ok(node_range) = SourceRange::try_from(&node) else {
5048 return Ok(true);
5049 };
5050
5051 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5052 if node_range.module_id() == self.target_before_mutation.module_id()
5053 && node_range.start() == self.target_before_mutation.start()
5054 && node_range.end() >= self.target_before_mutation.end()
5056 {
5057 self.found.set(sketch_block.body.items.last().map(|item| match item {
5058 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5062 _ => AstNodeRef::from(item),
5063 }));
5064 return Ok(false);
5065 } else {
5066 return Ok(true);
5069 }
5070 }
5071
5072 for child in node.children().iter() {
5073 if !child.visit(*self)? {
5074 return Ok(false);
5075 }
5076 }
5077
5078 Ok(true)
5079 }
5080}
5081
5082struct FindSketchBlockByNodePath {
5083 target_node_path: ast::NodePath,
5085 found: Cell<Option<AstNodeRef>>,
5089}
5090
5091impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5092 type Error = crate::front::Error;
5093
5094 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5095 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5096 return Ok(true);
5097 };
5098
5099 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5100 if let Some(node_path) = node_path
5101 && node_path == self.target_node_path
5102 {
5103 self.found.set(sketch_block.body.items.last().map(|item| match item {
5104 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5108 _ => AstNodeRef::from(item),
5109 }));
5110
5111 return Ok(false);
5112 } else {
5113 return Ok(true);
5116 }
5117 }
5118
5119 for child in node.children().iter() {
5120 if !child.visit(*self)? {
5121 return Ok(false);
5122 }
5123 }
5124
5125 Ok(true)
5126 }
5127}
5128
5129fn find_sketch_block_added_item(
5137 ast: &ast::Node<ast::Program>,
5138 sketch_block_before_mutation: &AstNodeRef,
5139) -> Result<AstNodeRef, KclError> {
5140 if let Some(node_path) = &sketch_block_before_mutation.node_path {
5141 let find = FindSketchBlockByNodePath {
5142 target_node_path: node_path.clone(),
5143 found: Cell::new(None),
5144 };
5145 let node = crate::walk::Node::from(ast);
5146 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5147 find.found.into_inner().ok_or_else(|| {
5148 KclError::refactor(format!(
5149 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5150 ))
5151 })
5152 } else {
5153 let find = FindSketchBlockSourceRange {
5155 target_before_mutation: sketch_block_before_mutation.range,
5156 found: Cell::new(None),
5157 };
5158 let node = crate::walk::Node::from(ast);
5159 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5160 find.found.into_inner().ok_or_else(|| KclError::refactor(
5161 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?"),
5162 ))
5163 }
5164}
5165
5166fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5167 ast.recast_top(&Default::default(), 0)
5169}
5170
5171pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5172 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5173 inner: ast::ArrayExpression {
5174 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5175 non_code_meta: Default::default(),
5176 digest: None,
5177 },
5178 start: Default::default(),
5179 end: Default::default(),
5180 module_id: Default::default(),
5181 node_path: None,
5182 outer_attrs: Default::default(),
5183 pre_comments: Default::default(),
5184 comment_start: Default::default(),
5185 })))
5186}
5187
5188fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5189 match expr {
5190 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5191 inner: ast::Literal::from(to_source_number(*number)?),
5192 start: Default::default(),
5193 end: Default::default(),
5194 module_id: Default::default(),
5195 node_path: None,
5196 outer_attrs: Default::default(),
5197 pre_comments: Default::default(),
5198 comment_start: Default::default(),
5199 }))),
5200 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5201 inner: ast::SketchVar {
5202 initial: Some(Box::new(ast::Node {
5203 inner: to_source_number(*number)?,
5204 start: Default::default(),
5205 end: Default::default(),
5206 module_id: Default::default(),
5207 node_path: None,
5208 outer_attrs: Default::default(),
5209 pre_comments: Default::default(),
5210 comment_start: Default::default(),
5211 })),
5212 digest: None,
5213 },
5214 start: Default::default(),
5215 end: Default::default(),
5216 module_id: Default::default(),
5217 node_path: None,
5218 outer_attrs: Default::default(),
5219 pre_comments: Default::default(),
5220 comment_start: Default::default(),
5221 }))),
5222 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5223 }
5224}
5225
5226fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5227 Ok(ast::NumericLiteral {
5228 value: number.value,
5229 suffix: number.units,
5230 raw: format_number_literal(number.value, number.units, None)?,
5231 digest: None,
5232 })
5233}
5234
5235pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5236 ast::Expr::Name(Box::new(ast_name(name)))
5237}
5238
5239fn ast_name(name: String) -> ast::Node<ast::Name> {
5240 ast::Node {
5241 inner: ast::Name {
5242 name: ast::Node {
5243 inner: ast::Identifier { name, digest: None },
5244 start: Default::default(),
5245 end: Default::default(),
5246 module_id: Default::default(),
5247 node_path: None,
5248 outer_attrs: Default::default(),
5249 pre_comments: Default::default(),
5250 comment_start: Default::default(),
5251 },
5252 path: Vec::new(),
5253 abs_path: false,
5254 digest: None,
5255 },
5256 start: Default::default(),
5257 end: Default::default(),
5258 module_id: Default::default(),
5259 node_path: None,
5260 outer_attrs: Default::default(),
5261 pre_comments: Default::default(),
5262 comment_start: Default::default(),
5263 }
5264}
5265
5266pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5267 ast::Name {
5268 name: ast::Node {
5269 inner: ast::Identifier {
5270 name: name.to_owned(),
5271 digest: None,
5272 },
5273 start: Default::default(),
5274 end: Default::default(),
5275 module_id: Default::default(),
5276 node_path: None,
5277 outer_attrs: Default::default(),
5278 pre_comments: Default::default(),
5279 comment_start: Default::default(),
5280 },
5281 path: Default::default(),
5282 abs_path: false,
5283 digest: None,
5284 }
5285}
5286
5287pub(crate) fn create_coincident_ast(expr1: ast::Expr, expr2: ast::Expr) -> ast::Expr {
5291 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5293 elements: vec![expr1, expr2],
5294 digest: None,
5295 non_code_meta: Default::default(),
5296 })));
5297
5298 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5300 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5301 unlabeled: Some(array_expr),
5302 arguments: Default::default(),
5303 digest: None,
5304 non_code_meta: Default::default(),
5305 })))
5306}
5307
5308pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5310 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5311 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5312 unlabeled: None,
5313 arguments: vec![
5314 ast::LabeledArg {
5315 label: Some(ast::Identifier::new(LINE_START_PARAM)),
5316 arg: start_ast,
5317 },
5318 ast::LabeledArg {
5319 label: Some(ast::Identifier::new(LINE_END_PARAM)),
5320 arg: end_ast,
5321 },
5322 ],
5323 digest: None,
5324 non_code_meta: Default::default(),
5325 })))
5326}
5327
5328pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5330 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5331 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5332 unlabeled: None,
5333 arguments: vec![
5334 ast::LabeledArg {
5335 label: Some(ast::Identifier::new(ARC_START_PARAM)),
5336 arg: start_ast,
5337 },
5338 ast::LabeledArg {
5339 label: Some(ast::Identifier::new(ARC_END_PARAM)),
5340 arg: end_ast,
5341 },
5342 ast::LabeledArg {
5343 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5344 arg: center_ast,
5345 },
5346 ],
5347 digest: None,
5348 non_code_meta: Default::default(),
5349 })))
5350}
5351
5352pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5354 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5355 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
5356 unlabeled: None,
5357 arguments: vec![
5358 ast::LabeledArg {
5359 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5360 arg: start_ast,
5361 },
5362 ast::LabeledArg {
5363 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5364 arg: center_ast,
5365 },
5366 ],
5367 digest: None,
5368 non_code_meta: Default::default(),
5369 })))
5370}
5371
5372pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
5374 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5375 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
5376 unlabeled: Some(line_expr),
5377 arguments: Default::default(),
5378 digest: None,
5379 non_code_meta: Default::default(),
5380 })))
5381}
5382
5383pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5385 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5386 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5387 unlabeled: Some(line_expr),
5388 arguments: Default::default(),
5389 digest: None,
5390 non_code_meta: Default::default(),
5391 })))
5392}
5393
5394pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5396 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5397 object: object_expr,
5398 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5399 name: ast::Node::no_src(ast::Identifier {
5400 name: property.to_string(),
5401 digest: None,
5402 }),
5403 path: Vec::new(),
5404 abs_path: false,
5405 digest: None,
5406 }))),
5407 computed: false,
5408 digest: None,
5409 })))
5410}
5411
5412fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5414 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5416 position.x,
5417 )?))));
5418 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5419 position.y,
5420 )?))));
5421 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5422 elements: vec![x_literal, y_literal],
5423 digest: None,
5424 non_code_meta: Default::default(),
5425 })));
5426
5427 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5429 elements: vec![point_expr, point_array],
5430 digest: None,
5431 non_code_meta: Default::default(),
5432 })));
5433
5434 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5436 ast::CallExpressionKw {
5437 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5438 unlabeled: Some(array_expr),
5439 arguments: Default::default(),
5440 digest: None,
5441 non_code_meta: Default::default(),
5442 },
5443 ))))
5444}
5445
5446pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5448 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5449 elements: line_exprs,
5450 digest: None,
5451 non_code_meta: Default::default(),
5452 })));
5453
5454 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5456 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5457 unlabeled: Some(array_expr),
5458 arguments: Default::default(),
5459 digest: None,
5460 non_code_meta: Default::default(),
5461 })))
5462}
5463
5464pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5466 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5467 elements: segment_exprs,
5468 digest: None,
5469 non_code_meta: Default::default(),
5470 })));
5471
5472 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5473 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5474 unlabeled: Some(array_expr),
5475 arguments: Default::default(),
5476 digest: None,
5477 non_code_meta: Default::default(),
5478 })))
5479}
5480
5481pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5483 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5484 elements: vec![seg1_expr, seg2_expr],
5485 digest: None,
5486 non_code_meta: Default::default(),
5487 })));
5488
5489 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5490 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5491 unlabeled: Some(array_expr),
5492 arguments: Default::default(),
5493 digest: None,
5494 non_code_meta: Default::default(),
5495 })))
5496}
5497
5498#[cfg(all(feature = "artifact-graph", test))]
5499mod tests {
5500 use super::*;
5501 use crate::engine::PlaneName;
5502 use crate::execution::cache::SketchModeState;
5503 use crate::execution::cache::clear_mem_cache;
5504 use crate::execution::cache::read_old_memory;
5505 use crate::execution::cache::write_old_memory;
5506 use crate::front::Distance;
5507 use crate::front::Fixed;
5508 use crate::front::FixedPoint;
5509 use crate::front::Object;
5510 use crate::front::Plane;
5511 use crate::front::Sketch;
5512 use crate::front::Tangent;
5513 use crate::frontend::sketch::Vertical;
5514 use crate::pretty::NumericSuffix;
5515
5516 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
5517 for object in &scene_graph.objects {
5518 if let ObjectKind::Sketch(_) = &object.kind {
5519 return Some(object);
5520 }
5521 }
5522 None
5523 }
5524
5525 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
5526 for object in &scene_graph.objects {
5527 if let ObjectKind::Face(_) = &object.kind {
5528 return Some(object);
5529 }
5530 }
5531 None
5532 }
5533
5534 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
5535 for object in &scene_graph.objects {
5536 if matches!(&object.kind, ObjectKind::Wall(_)) {
5537 return Some(object.id);
5538 }
5539 }
5540 None
5541 }
5542
5543 #[test]
5544 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
5545 let source = "\
5546region001 = region(point = [0.1, 0.1], sketch = s)
5547extrude001 = extrude(region001, length = 5)
5548revolve001 = revolve(region001, axis = Y)
5549sweep001 = sweep(region001, path = path001)
5550loft001 = loft(region001)
5551not_sweep001 = shell(extrude001, faces = [], thickness = 1)
5552";
5553
5554 let program = Program::parse(source).unwrap().0.unwrap();
5555
5556 assert_eq!(
5557 region_name_from_sweep_variable(&program.ast, "extrude001"),
5558 Some("region001".to_owned())
5559 );
5560 assert_eq!(
5561 region_name_from_sweep_variable(&program.ast, "revolve001"),
5562 Some("region001".to_owned())
5563 );
5564 assert_eq!(
5565 region_name_from_sweep_variable(&program.ast, "sweep001"),
5566 Some("region001".to_owned())
5567 );
5568 assert_eq!(
5569 region_name_from_sweep_variable(&program.ast, "loft001"),
5570 Some("region001".to_owned())
5571 );
5572 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
5573 }
5574
5575 #[track_caller]
5576 fn expect_sketch(object: &Object) -> &Sketch {
5577 if let ObjectKind::Sketch(sketch) = &object.kind {
5578 sketch
5579 } else {
5580 panic!("Object is not a sketch: {:?}", object);
5581 }
5582 }
5583
5584 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
5585 LineCtor {
5586 start: Point2d {
5587 x: Expr::Number(Number { value: start_x, units }),
5588 y: Expr::Number(Number { value: start_y, units }),
5589 },
5590 end: Point2d {
5591 x: Expr::Number(Number { value: end_x, units }),
5592 y: Expr::Number(Number { value: end_y, units }),
5593 },
5594 construction: None,
5595 }
5596 }
5597
5598 async fn create_sketch_with_single_line(
5599 frontend: &mut FrontendState,
5600 ctx: &ExecutorContext,
5601 mock_ctx: &ExecutorContext,
5602 version: Version,
5603 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
5604 frontend.program = Program::empty();
5605
5606 let sketch_args = SketchCtor {
5607 on: Plane::Default(PlaneName::Xy),
5608 };
5609 let (_src_delta, _scene_delta, sketch_id) = frontend
5610 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
5611 .await
5612 .unwrap();
5613
5614 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
5615 let (source_delta, scene_graph_delta) = frontend
5616 .add_segment(mock_ctx, version, sketch_id, segment, None)
5617 .await
5618 .unwrap();
5619 let line_id = *scene_graph_delta
5620 .new_objects
5621 .last()
5622 .expect("Expected line object id to be created");
5623
5624 (sketch_id, line_id, source_delta, scene_graph_delta)
5625 }
5626
5627 #[tokio::test(flavor = "multi_thread")]
5628 async fn test_sketch_checkpoint_round_trip_restores_state() {
5629 let mut frontend = FrontendState::new();
5630 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5631 let mock_ctx = ExecutorContext::new_mock(None).await;
5632 let version = Version(0);
5633
5634 let (sketch_id, line_id, source_delta, scene_graph_delta) =
5635 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5636
5637 let expected_source = source_delta.text.clone();
5638 let expected_scene_graph = frontend.scene_graph.clone();
5639 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
5640 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
5641
5642 let checkpoint_id = frontend
5643 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5644 .await
5645 .unwrap();
5646
5647 let edited_segments = vec![ExistingSegmentCtor {
5648 id: line_id,
5649 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
5650 }];
5651 let (edited_source, _edited_scene) = frontend
5652 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
5653 .await
5654 .unwrap();
5655 assert_ne!(edited_source.text, expected_source);
5656
5657 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
5658
5659 assert_eq!(restored.source_delta.text, expected_source);
5660 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
5661 assert!(restored.scene_graph_delta.invalidates_ids);
5662 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
5663 assert_eq!(frontend.scene_graph, expected_scene_graph);
5664 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
5665
5666 ctx.close().await;
5667 mock_ctx.close().await;
5668 }
5669
5670 #[tokio::test(flavor = "multi_thread")]
5671 async fn test_sketch_checkpoints_prune_oldest_entries() {
5672 let mut frontend = FrontendState::new();
5673 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5674 let mock_ctx = ExecutorContext::new_mock(None).await;
5675 let version = Version(0);
5676
5677 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
5678 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5679
5680 let mut checkpoint_ids = Vec::new();
5681 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
5682 checkpoint_ids.push(
5683 frontend
5684 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5685 .await
5686 .unwrap(),
5687 );
5688 }
5689
5690 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
5691 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
5692
5693 let oldest_retained = checkpoint_ids[3];
5694 assert_eq!(
5695 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
5696 Some(oldest_retained)
5697 );
5698
5699 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
5700 assert!(evicted_restore.is_err());
5701 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
5702
5703 frontend
5704 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
5705 .await
5706 .unwrap();
5707
5708 ctx.close().await;
5709 mock_ctx.close().await;
5710 }
5711
5712 #[tokio::test(flavor = "multi_thread")]
5713 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
5714 let mut frontend = FrontendState::new();
5715 let missing_checkpoint = SketchCheckpointId::new(999);
5716
5717 let err = frontend
5718 .restore_sketch_checkpoint(missing_checkpoint)
5719 .await
5720 .expect_err("Expected restore to fail for missing checkpoint");
5721
5722 assert!(err.msg.contains("Sketch checkpoint not found"));
5723 }
5724
5725 #[tokio::test(flavor = "multi_thread")]
5726 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
5727 let mut frontend = FrontendState::new();
5728 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5729 let mock_ctx = ExecutorContext::new_mock(None).await;
5730 let version = Version(0);
5731
5732 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
5733 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5734
5735 let checkpoint_a = frontend
5736 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5737 .await
5738 .unwrap();
5739 let checkpoint_b = frontend
5740 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5741 .await
5742 .unwrap();
5743 assert_eq!(frontend.sketch_checkpoints.len(), 2);
5744
5745 frontend.clear_sketch_checkpoints();
5746 assert!(frontend.sketch_checkpoints.is_empty());
5747 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
5748 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
5749
5750 ctx.close().await;
5751 mock_ctx.close().await;
5752 }
5753
5754 #[tokio::test(flavor = "multi_thread")]
5755 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
5756 let mut frontend = FrontendState::new();
5757 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5758 let mock_ctx = ExecutorContext::new_mock(None).await;
5759 let version = Version(0);
5760
5761 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
5762 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5763 let old_source = source_delta.text.clone();
5764 let old_checkpoint = frontend
5765 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5766 .await
5767 .unwrap();
5768 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
5769
5770 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
5771 .unwrap()
5772 .0
5773 .unwrap();
5774
5775 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
5776 let SetProgramOutcome::Success {
5777 checkpoint_id: Some(new_checkpoint),
5778 ..
5779 } = result
5780 else {
5781 panic!("Expected Success with a fresh checkpoint baseline");
5782 };
5783
5784 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
5785
5786 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
5787 assert_eq!(old_restore.source_delta.text, old_source);
5788
5789 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
5790 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
5791
5792 ctx.close().await;
5793 mock_ctx.close().await;
5794 }
5795
5796 #[tokio::test(flavor = "multi_thread")]
5797 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
5798 let mut frontend = FrontendState::new();
5799 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5800 let mock_ctx = ExecutorContext::new_mock(None).await;
5801 let version = Version(0);
5802
5803 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
5804 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
5805 let old_checkpoint = frontend
5806 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
5807 .await
5808 .unwrap();
5809 let checkpoint_count_before = frontend.sketch_checkpoints.len();
5810
5811 let failing_program = Program::parse(
5812 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
5813 )
5814 .unwrap()
5815 .0
5816 .unwrap();
5817
5818 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
5819 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
5820 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
5821 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
5822
5823 ctx.close().await;
5824 mock_ctx.close().await;
5825 }
5826
5827 #[tokio::test(flavor = "multi_thread")]
5828 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
5829 let mut frontend = FrontendState::new();
5830 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5831
5832 let program = Program::parse(
5833 "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",
5834 )
5835 .unwrap()
5836 .0
5837 .unwrap();
5838 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
5839 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
5840 panic!("Expected successful baseline program execution");
5841 };
5842
5843 clear_mem_cache().await;
5844 assert!(read_old_memory().await.is_none());
5845
5846 let checkpoint_without_mock_memory = frontend
5847 .create_sketch_checkpoint((*exec_outcome).clone())
5848 .await
5849 .unwrap();
5850
5851 write_old_memory(SketchModeState::new_for_tests()).await;
5852 assert!(read_old_memory().await.is_some());
5853
5854 let checkpoint_with_mock_memory = frontend
5855 .create_sketch_checkpoint((*exec_outcome).clone())
5856 .await
5857 .unwrap();
5858
5859 clear_mem_cache().await;
5860 assert!(read_old_memory().await.is_none());
5861
5862 frontend
5863 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
5864 .await
5865 .unwrap();
5866 assert!(read_old_memory().await.is_some());
5867
5868 frontend
5869 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
5870 .await
5871 .unwrap();
5872 assert!(read_old_memory().await.is_none());
5873
5874 ctx.close().await;
5875 }
5876
5877 #[tokio::test(flavor = "multi_thread")]
5878 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
5879 let source = "\
5880sketch(on = XY) {
5881 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
5882}
5883
5884bad = missing_name
5885";
5886 let program = Program::parse(source).unwrap().0.unwrap();
5887
5888 let mut frontend = FrontendState::new();
5889
5890 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5891 let mock_ctx = ExecutorContext::new_mock(None).await;
5892 let version = Version(0);
5893 let project_id = ProjectId(0);
5894 let file_id = FileId(0);
5895
5896 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
5897 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
5898 };
5899
5900 let sketch_id = frontend
5901 .scene_graph
5902 .objects
5903 .iter()
5904 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
5905 .expect("Expected sketch object from errored hack_set_program");
5906
5907 frontend
5908 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
5909 .await
5910 .unwrap();
5911
5912 ctx.close().await;
5913 mock_ctx.close().await;
5914 }
5915
5916 #[tokio::test(flavor = "multi_thread")]
5917 async fn test_new_sketch_add_point_edit_point() {
5918 let program = Program::empty();
5919
5920 let mut frontend = FrontendState::new();
5921 frontend.program = program;
5922
5923 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5924 let mock_ctx = ExecutorContext::new_mock(None).await;
5925 let version = Version(0);
5926
5927 let sketch_args = SketchCtor {
5928 on: Plane::Default(PlaneName::Xy),
5929 };
5930 let (_src_delta, scene_delta, sketch_id) = frontend
5931 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5932 .await
5933 .unwrap();
5934 assert_eq!(sketch_id, ObjectId(1));
5935 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
5936 let sketch_object = &scene_delta.new_graph.objects[1];
5937 assert_eq!(sketch_object.id, ObjectId(1));
5938 assert_eq!(
5939 sketch_object.kind,
5940 ObjectKind::Sketch(Sketch {
5941 args: SketchCtor {
5942 on: Plane::Default(PlaneName::Xy)
5943 },
5944 plane: ObjectId(0),
5945 segments: vec![],
5946 constraints: vec![],
5947 })
5948 );
5949 assert_eq!(scene_delta.new_graph.objects.len(), 2);
5950
5951 let point_ctor = PointCtor {
5952 position: Point2d {
5953 x: Expr::Number(Number {
5954 value: 1.0,
5955 units: NumericSuffix::Inch,
5956 }),
5957 y: Expr::Number(Number {
5958 value: 2.0,
5959 units: NumericSuffix::Inch,
5960 }),
5961 },
5962 };
5963 let segment = SegmentCtor::Point(point_ctor);
5964 let (src_delta, scene_delta) = frontend
5965 .add_segment(&mock_ctx, version, sketch_id, segment, None)
5966 .await
5967 .unwrap();
5968 assert_eq!(
5969 src_delta.text.as_str(),
5970 "sketch001 = sketch(on = XY) {
5971 point(at = [1in, 2in])
5972}
5973"
5974 );
5975 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
5976 assert_eq!(scene_delta.new_graph.objects.len(), 3);
5977 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
5978 assert_eq!(scene_object.id.0, i);
5979 }
5980
5981 let point_id = *scene_delta.new_objects.last().unwrap();
5982
5983 let point_ctor = PointCtor {
5984 position: Point2d {
5985 x: Expr::Number(Number {
5986 value: 3.0,
5987 units: NumericSuffix::Inch,
5988 }),
5989 y: Expr::Number(Number {
5990 value: 4.0,
5991 units: NumericSuffix::Inch,
5992 }),
5993 },
5994 };
5995 let segments = vec![ExistingSegmentCtor {
5996 id: point_id,
5997 ctor: SegmentCtor::Point(point_ctor),
5998 }];
5999 let (src_delta, scene_delta) = frontend
6000 .edit_segments(&mock_ctx, version, sketch_id, segments)
6001 .await
6002 .unwrap();
6003 assert_eq!(
6004 src_delta.text.as_str(),
6005 "sketch001 = sketch(on = XY) {
6006 point(at = [3in, 4in])
6007}
6008"
6009 );
6010 assert_eq!(scene_delta.new_objects, vec![]);
6011 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6012
6013 ctx.close().await;
6014 mock_ctx.close().await;
6015 }
6016
6017 #[tokio::test(flavor = "multi_thread")]
6018 async fn test_new_sketch_add_line_edit_line() {
6019 let program = Program::empty();
6020
6021 let mut frontend = FrontendState::new();
6022 frontend.program = program;
6023
6024 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6025 let mock_ctx = ExecutorContext::new_mock(None).await;
6026 let version = Version(0);
6027
6028 let sketch_args = SketchCtor {
6029 on: Plane::Default(PlaneName::Xy),
6030 };
6031 let (_src_delta, scene_delta, sketch_id) = frontend
6032 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6033 .await
6034 .unwrap();
6035 assert_eq!(sketch_id, ObjectId(1));
6036 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6037 let sketch_object = &scene_delta.new_graph.objects[1];
6038 assert_eq!(sketch_object.id, ObjectId(1));
6039 assert_eq!(
6040 sketch_object.kind,
6041 ObjectKind::Sketch(Sketch {
6042 args: SketchCtor {
6043 on: Plane::Default(PlaneName::Xy)
6044 },
6045 plane: ObjectId(0),
6046 segments: vec![],
6047 constraints: vec![],
6048 })
6049 );
6050 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6051
6052 let line_ctor = LineCtor {
6053 start: Point2d {
6054 x: Expr::Number(Number {
6055 value: 0.0,
6056 units: NumericSuffix::Mm,
6057 }),
6058 y: Expr::Number(Number {
6059 value: 0.0,
6060 units: NumericSuffix::Mm,
6061 }),
6062 },
6063 end: Point2d {
6064 x: Expr::Number(Number {
6065 value: 10.0,
6066 units: NumericSuffix::Mm,
6067 }),
6068 y: Expr::Number(Number {
6069 value: 10.0,
6070 units: NumericSuffix::Mm,
6071 }),
6072 },
6073 construction: None,
6074 };
6075 let segment = SegmentCtor::Line(line_ctor);
6076 let (src_delta, scene_delta) = frontend
6077 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6078 .await
6079 .unwrap();
6080 assert_eq!(
6081 src_delta.text.as_str(),
6082 "sketch001 = sketch(on = XY) {
6083 line(start = [0mm, 0mm], end = [10mm, 10mm])
6084}
6085"
6086 );
6087 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6088 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6089 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6090 assert_eq!(scene_object.id.0, i);
6091 }
6092
6093 let line = *scene_delta.new_objects.last().unwrap();
6095
6096 let line_ctor = LineCtor {
6097 start: Point2d {
6098 x: Expr::Number(Number {
6099 value: 1.0,
6100 units: NumericSuffix::Mm,
6101 }),
6102 y: Expr::Number(Number {
6103 value: 2.0,
6104 units: NumericSuffix::Mm,
6105 }),
6106 },
6107 end: Point2d {
6108 x: Expr::Number(Number {
6109 value: 13.0,
6110 units: NumericSuffix::Mm,
6111 }),
6112 y: Expr::Number(Number {
6113 value: 14.0,
6114 units: NumericSuffix::Mm,
6115 }),
6116 },
6117 construction: None,
6118 };
6119 let segments = vec![ExistingSegmentCtor {
6120 id: line,
6121 ctor: SegmentCtor::Line(line_ctor),
6122 }];
6123 let (src_delta, scene_delta) = frontend
6124 .edit_segments(&mock_ctx, version, sketch_id, segments)
6125 .await
6126 .unwrap();
6127 assert_eq!(
6128 src_delta.text.as_str(),
6129 "sketch001 = sketch(on = XY) {
6130 line(start = [1mm, 2mm], end = [13mm, 14mm])
6131}
6132"
6133 );
6134 assert_eq!(scene_delta.new_objects, vec![]);
6135 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6136
6137 ctx.close().await;
6138 mock_ctx.close().await;
6139 }
6140
6141 #[tokio::test(flavor = "multi_thread")]
6142 async fn test_new_sketch_add_arc_edit_arc() {
6143 let program = Program::empty();
6144
6145 let mut frontend = FrontendState::new();
6146 frontend.program = program;
6147
6148 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6149 let mock_ctx = ExecutorContext::new_mock(None).await;
6150 let version = Version(0);
6151
6152 let sketch_args = SketchCtor {
6153 on: Plane::Default(PlaneName::Xy),
6154 };
6155 let (_src_delta, scene_delta, sketch_id) = frontend
6156 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6157 .await
6158 .unwrap();
6159 assert_eq!(sketch_id, ObjectId(1));
6160 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6161 let sketch_object = &scene_delta.new_graph.objects[1];
6162 assert_eq!(sketch_object.id, ObjectId(1));
6163 assert_eq!(
6164 sketch_object.kind,
6165 ObjectKind::Sketch(Sketch {
6166 args: SketchCtor {
6167 on: Plane::Default(PlaneName::Xy),
6168 },
6169 plane: ObjectId(0),
6170 segments: vec![],
6171 constraints: vec![],
6172 })
6173 );
6174 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6175
6176 let arc_ctor = ArcCtor {
6177 start: Point2d {
6178 x: Expr::Var(Number {
6179 value: 0.0,
6180 units: NumericSuffix::Mm,
6181 }),
6182 y: Expr::Var(Number {
6183 value: 0.0,
6184 units: NumericSuffix::Mm,
6185 }),
6186 },
6187 end: Point2d {
6188 x: Expr::Var(Number {
6189 value: 10.0,
6190 units: NumericSuffix::Mm,
6191 }),
6192 y: Expr::Var(Number {
6193 value: 10.0,
6194 units: NumericSuffix::Mm,
6195 }),
6196 },
6197 center: Point2d {
6198 x: Expr::Var(Number {
6199 value: 10.0,
6200 units: NumericSuffix::Mm,
6201 }),
6202 y: Expr::Var(Number {
6203 value: 0.0,
6204 units: NumericSuffix::Mm,
6205 }),
6206 },
6207 construction: None,
6208 };
6209 let segment = SegmentCtor::Arc(arc_ctor);
6210 let (src_delta, scene_delta) = frontend
6211 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6212 .await
6213 .unwrap();
6214 assert_eq!(
6215 src_delta.text.as_str(),
6216 "sketch001 = sketch(on = XY) {
6217 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6218}
6219"
6220 );
6221 assert_eq!(
6222 scene_delta.new_objects,
6223 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6224 );
6225 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6226 assert_eq!(scene_object.id.0, i);
6227 }
6228 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6229
6230 let arc = *scene_delta.new_objects.last().unwrap();
6232
6233 let arc_ctor = ArcCtor {
6234 start: Point2d {
6235 x: Expr::Var(Number {
6236 value: 1.0,
6237 units: NumericSuffix::Mm,
6238 }),
6239 y: Expr::Var(Number {
6240 value: 2.0,
6241 units: NumericSuffix::Mm,
6242 }),
6243 },
6244 end: Point2d {
6245 x: Expr::Var(Number {
6246 value: 13.0,
6247 units: NumericSuffix::Mm,
6248 }),
6249 y: Expr::Var(Number {
6250 value: 14.0,
6251 units: NumericSuffix::Mm,
6252 }),
6253 },
6254 center: Point2d {
6255 x: Expr::Var(Number {
6256 value: 13.0,
6257 units: NumericSuffix::Mm,
6258 }),
6259 y: Expr::Var(Number {
6260 value: 2.0,
6261 units: NumericSuffix::Mm,
6262 }),
6263 },
6264 construction: None,
6265 };
6266 let segments = vec![ExistingSegmentCtor {
6267 id: arc,
6268 ctor: SegmentCtor::Arc(arc_ctor),
6269 }];
6270 let (src_delta, scene_delta) = frontend
6271 .edit_segments(&mock_ctx, version, sketch_id, segments)
6272 .await
6273 .unwrap();
6274 assert_eq!(
6275 src_delta.text.as_str(),
6276 "sketch001 = sketch(on = XY) {
6277 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6278}
6279"
6280 );
6281 assert_eq!(scene_delta.new_objects, vec![]);
6282 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6283
6284 ctx.close().await;
6285 mock_ctx.close().await;
6286 }
6287
6288 #[tokio::test(flavor = "multi_thread")]
6289 async fn test_new_sketch_add_circle_edit_circle() {
6290 let program = Program::empty();
6291
6292 let mut frontend = FrontendState::new();
6293 frontend.program = program;
6294
6295 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6296 let mock_ctx = ExecutorContext::new_mock(None).await;
6297 let version = Version(0);
6298
6299 let sketch_args = SketchCtor {
6300 on: Plane::Default(PlaneName::Xy),
6301 };
6302 let (_src_delta, _scene_delta, sketch_id) = frontend
6303 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6304 .await
6305 .unwrap();
6306
6307 let circle_ctor = CircleCtor {
6309 start: Point2d {
6310 x: Expr::Var(Number {
6311 value: 5.0,
6312 units: NumericSuffix::Mm,
6313 }),
6314 y: Expr::Var(Number {
6315 value: 0.0,
6316 units: NumericSuffix::Mm,
6317 }),
6318 },
6319 center: Point2d {
6320 x: Expr::Var(Number {
6321 value: 0.0,
6322 units: NumericSuffix::Mm,
6323 }),
6324 y: Expr::Var(Number {
6325 value: 0.0,
6326 units: NumericSuffix::Mm,
6327 }),
6328 },
6329 construction: None,
6330 };
6331 let segment = SegmentCtor::Circle(circle_ctor);
6332 let (src_delta, scene_delta) = frontend
6333 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6334 .await
6335 .unwrap();
6336 assert_eq!(
6337 src_delta.text.as_str(),
6338 "sketch001 = sketch(on = XY) {
6339 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6340}
6341"
6342 );
6343 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6345 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6346
6347 let circle = *scene_delta.new_objects.last().unwrap();
6348
6349 let circle_ctor = CircleCtor {
6351 start: Point2d {
6352 x: Expr::Var(Number {
6353 value: 10.0,
6354 units: NumericSuffix::Mm,
6355 }),
6356 y: Expr::Var(Number {
6357 value: 0.0,
6358 units: NumericSuffix::Mm,
6359 }),
6360 },
6361 center: Point2d {
6362 x: Expr::Var(Number {
6363 value: 3.0,
6364 units: NumericSuffix::Mm,
6365 }),
6366 y: Expr::Var(Number {
6367 value: 4.0,
6368 units: NumericSuffix::Mm,
6369 }),
6370 },
6371 construction: None,
6372 };
6373 let segments = vec![ExistingSegmentCtor {
6374 id: circle,
6375 ctor: SegmentCtor::Circle(circle_ctor),
6376 }];
6377 let (src_delta, scene_delta) = frontend
6378 .edit_segments(&mock_ctx, version, sketch_id, segments)
6379 .await
6380 .unwrap();
6381 assert_eq!(
6382 src_delta.text.as_str(),
6383 "sketch001 = sketch(on = XY) {
6384 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6385}
6386"
6387 );
6388 assert_eq!(scene_delta.new_objects, vec![]);
6389 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6390
6391 ctx.close().await;
6392 mock_ctx.close().await;
6393 }
6394
6395 #[tokio::test(flavor = "multi_thread")]
6396 async fn test_delete_circle() {
6397 let initial_source = "sketch001 = sketch(on = XY) {
6398 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6399}
6400";
6401
6402 let program = Program::parse(initial_source).unwrap().0.unwrap();
6403 let mut frontend = FrontendState::new();
6404
6405 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6406 let mock_ctx = ExecutorContext::new_mock(None).await;
6407 let version = Version(0);
6408
6409 frontend.hack_set_program(&ctx, program).await.unwrap();
6410 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6411 let sketch_id = sketch_object.id;
6412 let sketch = expect_sketch(sketch_object);
6413
6414 assert_eq!(sketch.segments.len(), 3);
6416 let circle_id = sketch.segments[2];
6417
6418 let (src_delta, scene_delta) = frontend
6420 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6421 .await
6422 .unwrap();
6423 assert_eq!(
6424 src_delta.text.as_str(),
6425 "sketch001 = sketch(on = XY) {
6426}
6427"
6428 );
6429 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6430 let new_sketch = expect_sketch(new_sketch_object);
6431 assert_eq!(new_sketch.segments.len(), 0);
6432
6433 ctx.close().await;
6434 mock_ctx.close().await;
6435 }
6436
6437 #[tokio::test(flavor = "multi_thread")]
6438 async fn test_edit_circle_via_point() {
6439 let initial_source = "sketch001 = sketch(on = XY) {
6440 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6441}
6442";
6443
6444 let program = Program::parse(initial_source).unwrap().0.unwrap();
6445 let mut frontend = FrontendState::new();
6446
6447 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6448 let mock_ctx = ExecutorContext::new_mock(None).await;
6449 let version = Version(0);
6450
6451 frontend.hack_set_program(&ctx, program).await.unwrap();
6452 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6453 let sketch_id = sketch_object.id;
6454 let sketch = expect_sketch(sketch_object);
6455
6456 let circle_id = sketch
6458 .segments
6459 .iter()
6460 .copied()
6461 .find(|seg_id| {
6462 matches!(
6463 &frontend.scene_graph.objects[seg_id.0].kind,
6464 ObjectKind::Segment {
6465 segment: Segment::Circle(_)
6466 }
6467 )
6468 })
6469 .expect("Expected a circle segment in sketch");
6470 let circle_object = &frontend.scene_graph.objects[circle_id.0];
6471 let ObjectKind::Segment {
6472 segment: Segment::Circle(circle),
6473 } = &circle_object.kind
6474 else {
6475 panic!("Expected circle segment, got: {:?}", circle_object.kind);
6476 };
6477 let start_point_id = circle.start;
6478
6479 let segments = vec![ExistingSegmentCtor {
6481 id: start_point_id,
6482 ctor: SegmentCtor::Point(PointCtor {
6483 position: Point2d {
6484 x: Expr::Var(Number {
6485 value: 7.0,
6486 units: NumericSuffix::Mm,
6487 }),
6488 y: Expr::Var(Number {
6489 value: 1.0,
6490 units: NumericSuffix::Mm,
6491 }),
6492 },
6493 }),
6494 }];
6495 let (src_delta, _scene_delta) = frontend
6496 .edit_segments(&mock_ctx, version, sketch_id, segments)
6497 .await
6498 .unwrap();
6499 assert_eq!(
6500 src_delta.text.as_str(),
6501 "sketch001 = sketch(on = XY) {
6502 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
6503}
6504"
6505 );
6506
6507 ctx.close().await;
6508 mock_ctx.close().await;
6509 }
6510
6511 #[tokio::test(flavor = "multi_thread")]
6512 async fn test_add_line_when_sketch_block_uses_variable() {
6513 let initial_source = "s = sketch(on = XY) {}
6514";
6515
6516 let program = Program::parse(initial_source).unwrap().0.unwrap();
6517
6518 let mut frontend = FrontendState::new();
6519
6520 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6521 let mock_ctx = ExecutorContext::new_mock(None).await;
6522 let version = Version(0);
6523
6524 frontend.hack_set_program(&ctx, program).await.unwrap();
6525 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6526 let sketch_id = sketch_object.id;
6527
6528 let line_ctor = LineCtor {
6529 start: Point2d {
6530 x: Expr::Number(Number {
6531 value: 0.0,
6532 units: NumericSuffix::Mm,
6533 }),
6534 y: Expr::Number(Number {
6535 value: 0.0,
6536 units: NumericSuffix::Mm,
6537 }),
6538 },
6539 end: Point2d {
6540 x: Expr::Number(Number {
6541 value: 10.0,
6542 units: NumericSuffix::Mm,
6543 }),
6544 y: Expr::Number(Number {
6545 value: 10.0,
6546 units: NumericSuffix::Mm,
6547 }),
6548 },
6549 construction: None,
6550 };
6551 let segment = SegmentCtor::Line(line_ctor);
6552 let (src_delta, scene_delta) = frontend
6553 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6554 .await
6555 .unwrap();
6556 assert_eq!(
6557 src_delta.text.as_str(),
6558 "s = sketch(on = XY) {
6559 line(start = [0mm, 0mm], end = [10mm, 10mm])
6560}
6561"
6562 );
6563 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6564 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6565
6566 ctx.close().await;
6567 mock_ctx.close().await;
6568 }
6569
6570 #[tokio::test(flavor = "multi_thread")]
6571 async fn test_new_sketch_add_line_delete_sketch() {
6572 let program = Program::empty();
6573
6574 let mut frontend = FrontendState::new();
6575 frontend.program = program;
6576
6577 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6578 let mock_ctx = ExecutorContext::new_mock(None).await;
6579 let version = Version(0);
6580
6581 let sketch_args = SketchCtor {
6582 on: Plane::Default(PlaneName::Xy),
6583 };
6584 let (_src_delta, scene_delta, sketch_id) = frontend
6585 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6586 .await
6587 .unwrap();
6588 assert_eq!(sketch_id, ObjectId(1));
6589 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6590 let sketch_object = &scene_delta.new_graph.objects[1];
6591 assert_eq!(sketch_object.id, ObjectId(1));
6592 assert_eq!(
6593 sketch_object.kind,
6594 ObjectKind::Sketch(Sketch {
6595 args: SketchCtor {
6596 on: Plane::Default(PlaneName::Xy)
6597 },
6598 plane: ObjectId(0),
6599 segments: vec![],
6600 constraints: vec![],
6601 })
6602 );
6603 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6604
6605 let line_ctor = LineCtor {
6606 start: Point2d {
6607 x: Expr::Number(Number {
6608 value: 0.0,
6609 units: NumericSuffix::Mm,
6610 }),
6611 y: Expr::Number(Number {
6612 value: 0.0,
6613 units: NumericSuffix::Mm,
6614 }),
6615 },
6616 end: Point2d {
6617 x: Expr::Number(Number {
6618 value: 10.0,
6619 units: NumericSuffix::Mm,
6620 }),
6621 y: Expr::Number(Number {
6622 value: 10.0,
6623 units: NumericSuffix::Mm,
6624 }),
6625 },
6626 construction: None,
6627 };
6628 let segment = SegmentCtor::Line(line_ctor);
6629 let (src_delta, scene_delta) = frontend
6630 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6631 .await
6632 .unwrap();
6633 assert_eq!(
6634 src_delta.text.as_str(),
6635 "sketch001 = sketch(on = XY) {
6636 line(start = [0mm, 0mm], end = [10mm, 10mm])
6637}
6638"
6639 );
6640 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6641
6642 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
6643 assert_eq!(src_delta.text.as_str(), "");
6644 assert_eq!(scene_delta.new_graph.objects.len(), 0);
6645
6646 ctx.close().await;
6647 mock_ctx.close().await;
6648 }
6649
6650 #[tokio::test(flavor = "multi_thread")]
6651 async fn test_delete_sketch_when_sketch_block_uses_variable() {
6652 let initial_source = "s = sketch(on = XY) {}
6653";
6654
6655 let program = Program::parse(initial_source).unwrap().0.unwrap();
6656
6657 let mut frontend = FrontendState::new();
6658
6659 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6660 let mock_ctx = ExecutorContext::new_mock(None).await;
6661 let version = Version(0);
6662
6663 frontend.hack_set_program(&ctx, program).await.unwrap();
6664 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6665 let sketch_id = sketch_object.id;
6666
6667 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
6668 assert_eq!(src_delta.text.as_str(), "");
6669 assert_eq!(scene_delta.new_graph.objects.len(), 0);
6670
6671 ctx.close().await;
6672 mock_ctx.close().await;
6673 }
6674
6675 #[tokio::test(flavor = "multi_thread")]
6676 async fn test_edit_line_when_editing_its_start_point() {
6677 let initial_source = "\
6678sketch(on = XY) {
6679 line(start = [var 1, var 2], end = [var 3, var 4])
6680}
6681";
6682
6683 let program = Program::parse(initial_source).unwrap().0.unwrap();
6684
6685 let mut frontend = FrontendState::new();
6686
6687 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6688 let mock_ctx = ExecutorContext::new_mock(None).await;
6689 let version = Version(0);
6690
6691 frontend.hack_set_program(&ctx, program).await.unwrap();
6692 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6693 let sketch_id = sketch_object.id;
6694 let sketch = expect_sketch(sketch_object);
6695
6696 let point_id = *sketch.segments.first().unwrap();
6697
6698 let point_ctor = PointCtor {
6699 position: Point2d {
6700 x: Expr::Var(Number {
6701 value: 5.0,
6702 units: NumericSuffix::Inch,
6703 }),
6704 y: Expr::Var(Number {
6705 value: 6.0,
6706 units: NumericSuffix::Inch,
6707 }),
6708 },
6709 };
6710 let segments = vec![ExistingSegmentCtor {
6711 id: point_id,
6712 ctor: SegmentCtor::Point(point_ctor),
6713 }];
6714 let (src_delta, scene_delta) = frontend
6715 .edit_segments(&mock_ctx, version, sketch_id, segments)
6716 .await
6717 .unwrap();
6718 assert_eq!(
6719 src_delta.text.as_str(),
6720 "\
6721sketch(on = XY) {
6722 line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
6723}
6724"
6725 );
6726 assert_eq!(scene_delta.new_objects, vec![]);
6727 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6728
6729 ctx.close().await;
6730 mock_ctx.close().await;
6731 }
6732
6733 #[tokio::test(flavor = "multi_thread")]
6734 async fn test_edit_line_when_editing_its_end_point() {
6735 let initial_source = "\
6736sketch(on = XY) {
6737 line(start = [var 1, var 2], end = [var 3, var 4])
6738}
6739";
6740
6741 let program = Program::parse(initial_source).unwrap().0.unwrap();
6742
6743 let mut frontend = FrontendState::new();
6744
6745 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6746 let mock_ctx = ExecutorContext::new_mock(None).await;
6747 let version = Version(0);
6748
6749 frontend.hack_set_program(&ctx, program).await.unwrap();
6750 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6751 let sketch_id = sketch_object.id;
6752 let sketch = expect_sketch(sketch_object);
6753 let point_id = *sketch.segments.get(1).unwrap();
6754
6755 let point_ctor = PointCtor {
6756 position: Point2d {
6757 x: Expr::Var(Number {
6758 value: 5.0,
6759 units: NumericSuffix::Inch,
6760 }),
6761 y: Expr::Var(Number {
6762 value: 6.0,
6763 units: NumericSuffix::Inch,
6764 }),
6765 },
6766 };
6767 let segments = vec![ExistingSegmentCtor {
6768 id: point_id,
6769 ctor: SegmentCtor::Point(point_ctor),
6770 }];
6771 let (src_delta, scene_delta) = frontend
6772 .edit_segments(&mock_ctx, version, sketch_id, segments)
6773 .await
6774 .unwrap();
6775 assert_eq!(
6776 src_delta.text.as_str(),
6777 "\
6778sketch(on = XY) {
6779 line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
6780}
6781"
6782 );
6783 assert_eq!(scene_delta.new_objects, vec![]);
6784 assert_eq!(
6785 scene_delta.new_graph.objects.len(),
6786 5,
6787 "{:#?}",
6788 scene_delta.new_graph.objects
6789 );
6790
6791 ctx.close().await;
6792 mock_ctx.close().await;
6793 }
6794
6795 #[tokio::test(flavor = "multi_thread")]
6796 async fn test_edit_line_with_coincident_feedback() {
6797 let initial_source = "\
6798sketch(on = XY) {
6799 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
6800 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
6801 fixed([line1.start, [0, 0]])
6802 coincident([line1.end, line2.start])
6803 equalLength([line1, line2])
6804}
6805";
6806
6807 let program = Program::parse(initial_source).unwrap().0.unwrap();
6808
6809 let mut frontend = FrontendState::new();
6810
6811 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6812 let mock_ctx = ExecutorContext::new_mock(None).await;
6813 let version = Version(0);
6814
6815 frontend.hack_set_program(&ctx, program).await.unwrap();
6816 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6817 let sketch_id = sketch_object.id;
6818 let sketch = expect_sketch(sketch_object);
6819 let line2_end_id = *sketch.segments.get(4).unwrap();
6820
6821 let segments = vec![ExistingSegmentCtor {
6822 id: line2_end_id,
6823 ctor: SegmentCtor::Point(PointCtor {
6824 position: Point2d {
6825 x: Expr::Var(Number {
6826 value: 9.0,
6827 units: NumericSuffix::None,
6828 }),
6829 y: Expr::Var(Number {
6830 value: 10.0,
6831 units: NumericSuffix::None,
6832 }),
6833 },
6834 }),
6835 }];
6836 let (src_delta, scene_delta) = frontend
6837 .edit_segments(&mock_ctx, version, sketch_id, segments)
6838 .await
6839 .unwrap();
6840 assert_eq!(
6841 src_delta.text.as_str(),
6842 "\
6843sketch(on = XY) {
6844 line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
6845 line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
6846 fixed([line1.start, [0, 0]])
6847 coincident([line1.end, line2.start])
6848 equalLength([line1, line2])
6849}
6850"
6851 );
6852 assert_eq!(
6853 scene_delta.new_graph.objects.len(),
6854 11,
6855 "{:#?}",
6856 scene_delta.new_graph.objects
6857 );
6858
6859 ctx.close().await;
6860 mock_ctx.close().await;
6861 }
6862
6863 #[tokio::test(flavor = "multi_thread")]
6864 async fn test_delete_point_without_var() {
6865 let initial_source = "\
6866sketch(on = XY) {
6867 point(at = [var 1, var 2])
6868 point(at = [var 3, var 4])
6869 point(at = [var 5, var 6])
6870}
6871";
6872
6873 let program = Program::parse(initial_source).unwrap().0.unwrap();
6874
6875 let mut frontend = FrontendState::new();
6876
6877 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6878 let mock_ctx = ExecutorContext::new_mock(None).await;
6879 let version = Version(0);
6880
6881 frontend.hack_set_program(&ctx, program).await.unwrap();
6882 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6883 let sketch_id = sketch_object.id;
6884 let sketch = expect_sketch(sketch_object);
6885
6886 let point_id = *sketch.segments.get(1).unwrap();
6887
6888 let (src_delta, scene_delta) = frontend
6889 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
6890 .await
6891 .unwrap();
6892 assert_eq!(
6893 src_delta.text.as_str(),
6894 "\
6895sketch(on = XY) {
6896 point(at = [var 1mm, var 2mm])
6897 point(at = [var 5mm, var 6mm])
6898}
6899"
6900 );
6901 assert_eq!(scene_delta.new_objects, vec![]);
6902 assert_eq!(scene_delta.new_graph.objects.len(), 4);
6903
6904 ctx.close().await;
6905 mock_ctx.close().await;
6906 }
6907
6908 #[tokio::test(flavor = "multi_thread")]
6909 async fn test_delete_point_with_var() {
6910 let initial_source = "\
6911sketch(on = XY) {
6912 point(at = [var 1, var 2])
6913 point1 = point(at = [var 3, var 4])
6914 point(at = [var 5, var 6])
6915}
6916";
6917
6918 let program = Program::parse(initial_source).unwrap().0.unwrap();
6919
6920 let mut frontend = FrontendState::new();
6921
6922 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6923 let mock_ctx = ExecutorContext::new_mock(None).await;
6924 let version = Version(0);
6925
6926 frontend.hack_set_program(&ctx, program).await.unwrap();
6927 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6928 let sketch_id = sketch_object.id;
6929 let sketch = expect_sketch(sketch_object);
6930
6931 let point_id = *sketch.segments.get(1).unwrap();
6932
6933 let (src_delta, scene_delta) = frontend
6934 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
6935 .await
6936 .unwrap();
6937 assert_eq!(
6938 src_delta.text.as_str(),
6939 "\
6940sketch(on = XY) {
6941 point(at = [var 1mm, var 2mm])
6942 point(at = [var 5mm, var 6mm])
6943}
6944"
6945 );
6946 assert_eq!(scene_delta.new_objects, vec![]);
6947 assert_eq!(scene_delta.new_graph.objects.len(), 4);
6948
6949 ctx.close().await;
6950 mock_ctx.close().await;
6951 }
6952
6953 #[tokio::test(flavor = "multi_thread")]
6954 async fn test_delete_multiple_points() {
6955 let initial_source = "\
6956sketch(on = XY) {
6957 point(at = [var 1, var 2])
6958 point1 = point(at = [var 3, var 4])
6959 point(at = [var 5, var 6])
6960}
6961";
6962
6963 let program = Program::parse(initial_source).unwrap().0.unwrap();
6964
6965 let mut frontend = FrontendState::new();
6966
6967 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6968 let mock_ctx = ExecutorContext::new_mock(None).await;
6969 let version = Version(0);
6970
6971 frontend.hack_set_program(&ctx, program).await.unwrap();
6972 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6973 let sketch_id = sketch_object.id;
6974
6975 let sketch = expect_sketch(sketch_object);
6976
6977 let point1_id = *sketch.segments.first().unwrap();
6978 let point2_id = *sketch.segments.get(1).unwrap();
6979
6980 let (src_delta, scene_delta) = frontend
6981 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
6982 .await
6983 .unwrap();
6984 assert_eq!(
6985 src_delta.text.as_str(),
6986 "\
6987sketch(on = XY) {
6988 point(at = [var 5mm, var 6mm])
6989}
6990"
6991 );
6992 assert_eq!(scene_delta.new_objects, vec![]);
6993 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6994
6995 ctx.close().await;
6996 mock_ctx.close().await;
6997 }
6998
6999 #[tokio::test(flavor = "multi_thread")]
7000 async fn test_delete_coincident_constraint() {
7001 let initial_source = "\
7002sketch(on = XY) {
7003 point1 = point(at = [var 1, var 2])
7004 point2 = point(at = [var 3, var 4])
7005 coincident([point1, point2])
7006 point(at = [var 5, var 6])
7007}
7008";
7009
7010 let program = Program::parse(initial_source).unwrap().0.unwrap();
7011
7012 let mut frontend = FrontendState::new();
7013
7014 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7015 let mock_ctx = ExecutorContext::new_mock(None).await;
7016 let version = Version(0);
7017
7018 frontend.hack_set_program(&ctx, program).await.unwrap();
7019 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7020 let sketch_id = sketch_object.id;
7021 let sketch = expect_sketch(sketch_object);
7022
7023 let coincident_id = *sketch.constraints.first().unwrap();
7024
7025 let (src_delta, scene_delta) = frontend
7026 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7027 .await
7028 .unwrap();
7029 assert_eq!(
7030 src_delta.text.as_str(),
7031 "\
7032sketch(on = XY) {
7033 point1 = point(at = [var 1mm, var 2mm])
7034 point2 = point(at = [var 3mm, var 4mm])
7035 point(at = [var 5mm, var 6mm])
7036}
7037"
7038 );
7039 assert_eq!(scene_delta.new_objects, vec![]);
7040 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7041
7042 ctx.close().await;
7043 mock_ctx.close().await;
7044 }
7045
7046 #[tokio::test(flavor = "multi_thread")]
7047 async fn test_delete_line_cascades_to_coincident_constraint() {
7048 let initial_source = "\
7049sketch(on = XY) {
7050 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7051 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7052 coincident([line1.end, line2.start])
7053}
7054";
7055
7056 let program = Program::parse(initial_source).unwrap().0.unwrap();
7057
7058 let mut frontend = FrontendState::new();
7059
7060 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7061 let mock_ctx = ExecutorContext::new_mock(None).await;
7062 let version = Version(0);
7063
7064 frontend.hack_set_program(&ctx, program).await.unwrap();
7065 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7066 let sketch_id = sketch_object.id;
7067 let sketch = expect_sketch(sketch_object);
7068 let line_id = *sketch.segments.get(5).unwrap();
7069
7070 let (src_delta, scene_delta) = frontend
7071 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7072 .await
7073 .unwrap();
7074 assert_eq!(
7075 src_delta.text.as_str(),
7076 "\
7077sketch(on = XY) {
7078 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7079}
7080"
7081 );
7082 assert_eq!(
7083 scene_delta.new_graph.objects.len(),
7084 5,
7085 "{:#?}",
7086 scene_delta.new_graph.objects
7087 );
7088
7089 ctx.close().await;
7090 mock_ctx.close().await;
7091 }
7092
7093 #[tokio::test(flavor = "multi_thread")]
7094 async fn test_delete_line_cascades_to_distance_constraint() {
7095 let initial_source = "\
7096sketch(on = XY) {
7097 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7098 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7099 distance([line1.end, line2.start]) == 10mm
7100}
7101";
7102
7103 let program = Program::parse(initial_source).unwrap().0.unwrap();
7104
7105 let mut frontend = FrontendState::new();
7106
7107 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7108 let mock_ctx = ExecutorContext::new_mock(None).await;
7109 let version = Version(0);
7110
7111 frontend.hack_set_program(&ctx, program).await.unwrap();
7112 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7113 let sketch_id = sketch_object.id;
7114 let sketch = expect_sketch(sketch_object);
7115 let line_id = *sketch.segments.get(5).unwrap();
7116
7117 let (src_delta, scene_delta) = frontend
7118 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7119 .await
7120 .unwrap();
7121 assert_eq!(
7122 src_delta.text.as_str(),
7123 "\
7124sketch(on = XY) {
7125 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7126}
7127"
7128 );
7129 assert_eq!(
7130 scene_delta.new_graph.objects.len(),
7131 5,
7132 "{:#?}",
7133 scene_delta.new_graph.objects
7134 );
7135
7136 ctx.close().await;
7137 mock_ctx.close().await;
7138 }
7139
7140 #[tokio::test(flavor = "multi_thread")]
7141 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
7142 let initial_source = "\
7143sketch(on = XY) {
7144 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7145 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7146 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7147 equalLength([line1, line2, line3])
7148}
7149";
7150
7151 let program = Program::parse(initial_source).unwrap().0.unwrap();
7152
7153 let mut frontend = FrontendState::new();
7154
7155 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7156 let mock_ctx = ExecutorContext::new_mock(None).await;
7157 let version = Version(0);
7158
7159 frontend.hack_set_program(&ctx, program).await.unwrap();
7160 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7161 let sketch_id = sketch_object.id;
7162 let sketch = expect_sketch(sketch_object);
7163 let line3_id = *sketch.segments.get(8).unwrap();
7164
7165 let (src_delta, scene_delta) = frontend
7166 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
7167 .await
7168 .unwrap();
7169 assert_eq!(
7170 src_delta.text.as_str(),
7171 "\
7172sketch(on = XY) {
7173 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7174 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7175 equalLength([line1, line2])
7176}
7177"
7178 );
7179
7180 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7181 let sketch = expect_sketch(sketch_object);
7182 assert_eq!(sketch.constraints.len(), 1);
7183
7184 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7185 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7186 panic!("Expected constraint object");
7187 };
7188 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
7189 panic!("Expected lines equal length constraint");
7190 };
7191 assert_eq!(lines_equal_length.lines.len(), 2);
7192
7193 ctx.close().await;
7194 mock_ctx.close().await;
7195 }
7196
7197 #[tokio::test(flavor = "multi_thread")]
7198 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
7199 let initial_source = "\
7200sketch(on = XY) {
7201 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7202 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7203 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7204 equalLength([line1, line2, line3])
7205}
7206";
7207
7208 let program = Program::parse(initial_source).unwrap().0.unwrap();
7209
7210 let mut frontend = FrontendState::new();
7211
7212 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7213 let mock_ctx = ExecutorContext::new_mock(None).await;
7214 let version = Version(0);
7215
7216 frontend.hack_set_program(&ctx, program).await.unwrap();
7217 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7218 let sketch_id = sketch_object.id;
7219 let sketch = expect_sketch(sketch_object);
7220 let line2_id = *sketch.segments.get(5).unwrap();
7221 let line3_id = *sketch.segments.get(8).unwrap();
7222
7223 let (src_delta, scene_delta) = frontend
7224 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
7225 .await
7226 .unwrap();
7227 assert_eq!(
7228 src_delta.text.as_str(),
7229 "\
7230sketch(on = XY) {
7231 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7232}
7233"
7234 );
7235
7236 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7237 let sketch = expect_sketch(sketch_object);
7238 assert!(sketch.constraints.is_empty());
7239
7240 ctx.close().await;
7241 mock_ctx.close().await;
7242 }
7243
7244 #[tokio::test(flavor = "multi_thread")]
7245 async fn test_delete_line_preserves_multiline_parallel_constraint() {
7246 let initial_source = "\
7247sketch(on = XY) {
7248 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7249 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7250 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7251 parallel([line1, line2, line3])
7252}
7253";
7254
7255 let program = Program::parse(initial_source).unwrap().0.unwrap();
7256
7257 let mut frontend = FrontendState::new();
7258
7259 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7260 let mock_ctx = ExecutorContext::new_mock(None).await;
7261 let version = Version(0);
7262
7263 frontend.hack_set_program(&ctx, program).await.unwrap();
7264 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7265 let sketch_id = sketch_object.id;
7266 let sketch = expect_sketch(sketch_object);
7267 let line3_id = *sketch.segments.get(8).unwrap();
7268
7269 let (src_delta, scene_delta) = frontend
7270 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
7271 .await
7272 .unwrap();
7273 assert_eq!(
7274 src_delta.text.as_str(),
7275 "\
7276sketch(on = XY) {
7277 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7278 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7279 parallel([line1, line2])
7280}
7281"
7282 );
7283
7284 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7285 let sketch = expect_sketch(sketch_object);
7286 assert_eq!(sketch.constraints.len(), 1);
7287
7288 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
7289 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7290 panic!("Expected constraint object");
7291 };
7292 let Constraint::Parallel(parallel) = constraint else {
7293 panic!("Expected parallel constraint");
7294 };
7295 assert_eq!(parallel.lines.len(), 2);
7296
7297 ctx.close().await;
7298 mock_ctx.close().await;
7299 }
7300
7301 #[tokio::test(flavor = "multi_thread")]
7302 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
7303 let initial_source = "\
7304sketch(on = XY) {
7305 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7306 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7307 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
7308 parallel([line1, line2, line3])
7309}
7310";
7311
7312 let program = Program::parse(initial_source).unwrap().0.unwrap();
7313
7314 let mut frontend = FrontendState::new();
7315
7316 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7317 let mock_ctx = ExecutorContext::new_mock(None).await;
7318 let version = Version(0);
7319
7320 frontend.hack_set_program(&ctx, program).await.unwrap();
7321 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7322 let sketch_id = sketch_object.id;
7323 let sketch = expect_sketch(sketch_object);
7324 let line2_id = *sketch.segments.get(5).unwrap();
7325 let line3_id = *sketch.segments.get(8).unwrap();
7326
7327 let (src_delta, scene_delta) = frontend
7328 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
7329 .await
7330 .unwrap();
7331 assert_eq!(
7332 src_delta.text.as_str(),
7333 "\
7334sketch(on = XY) {
7335 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7336}
7337"
7338 );
7339
7340 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7341 let sketch = expect_sketch(sketch_object);
7342 assert!(sketch.constraints.is_empty());
7343
7344 ctx.close().await;
7345 mock_ctx.close().await;
7346 }
7347
7348 #[tokio::test(flavor = "multi_thread")]
7349 async fn test_delete_line_line_coincident_constraint() {
7350 let initial_source = "\
7351sketch(on = XY) {
7352 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7353 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7354 coincident([line1, line2])
7355}
7356";
7357
7358 let program = Program::parse(initial_source).unwrap().0.unwrap();
7359
7360 let mut frontend = FrontendState::new();
7361
7362 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7363 let mock_ctx = ExecutorContext::new_mock(None).await;
7364 let version = Version(0);
7365
7366 frontend.hack_set_program(&ctx, program).await.unwrap();
7367 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7368 let sketch_id = sketch_object.id;
7369 let sketch = expect_sketch(sketch_object);
7370
7371 let coincident_id = *sketch.constraints.first().unwrap();
7372
7373 let (src_delta, scene_delta) = frontend
7374 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7375 .await
7376 .unwrap();
7377 assert_eq!(
7378 src_delta.text.as_str(),
7379 "\
7380sketch(on = XY) {
7381 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7382 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7383}
7384"
7385 );
7386 assert_eq!(scene_delta.new_objects, vec![]);
7387 assert_eq!(scene_delta.new_graph.objects.len(), 8);
7388
7389 ctx.close().await;
7390 mock_ctx.close().await;
7391 }
7392
7393 #[tokio::test(flavor = "multi_thread")]
7394 async fn test_two_points_coincident() {
7395 let initial_source = "\
7396sketch(on = XY) {
7397 point1 = point(at = [var 1, var 2])
7398 point(at = [3, 4])
7399}
7400";
7401
7402 let program = Program::parse(initial_source).unwrap().0.unwrap();
7403
7404 let mut frontend = FrontendState::new();
7405
7406 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7407 let mock_ctx = ExecutorContext::new_mock(None).await;
7408 let version = Version(0);
7409
7410 frontend.hack_set_program(&ctx, program).await.unwrap();
7411 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7412 let sketch_id = sketch_object.id;
7413 let sketch = expect_sketch(sketch_object);
7414 let point0_id = *sketch.segments.first().unwrap();
7415 let point1_id = *sketch.segments.get(1).unwrap();
7416
7417 let constraint = Constraint::Coincident(Coincident {
7418 segments: vec![point0_id.into(), point1_id.into()],
7419 });
7420 let (src_delta, scene_delta) = frontend
7421 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7422 .await
7423 .unwrap();
7424 assert_eq!(
7425 src_delta.text.as_str(),
7426 "\
7427sketch(on = XY) {
7428 point1 = point(at = [var 1, var 2])
7429 point2 = point(at = [3, 4])
7430 coincident([point1, point2])
7431}
7432"
7433 );
7434 assert_eq!(
7435 scene_delta.new_graph.objects.len(),
7436 5,
7437 "{:#?}",
7438 scene_delta.new_graph.objects
7439 );
7440
7441 ctx.close().await;
7442 mock_ctx.close().await;
7443 }
7444
7445 #[tokio::test(flavor = "multi_thread")]
7446 async fn test_point_origin_coincident_preserves_order() {
7447 let initial_source = "\
7448sketch(on = XY) {
7449 point(at = [var 1, var 2])
7450}
7451";
7452
7453 for (origin_first, expected_source) in [
7454 (
7455 true,
7456 "\
7457sketch(on = XY) {
7458 point1 = point(at = [var 1, var 2])
7459 coincident([ORIGIN, point1])
7460}
7461",
7462 ),
7463 (
7464 false,
7465 "\
7466sketch(on = XY) {
7467 point1 = point(at = [var 1, var 2])
7468 coincident([point1, ORIGIN])
7469}
7470",
7471 ),
7472 ] {
7473 let program = Program::parse(initial_source).unwrap().0.unwrap();
7474
7475 let mut frontend = FrontendState::new();
7476
7477 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7478 let mock_ctx = ExecutorContext::new_mock(None).await;
7479 let version = Version(0);
7480
7481 frontend.hack_set_program(&ctx, program).await.unwrap();
7482 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7483 let sketch_id = sketch_object.id;
7484 let sketch = expect_sketch(sketch_object);
7485 let point_id = *sketch.segments.first().unwrap();
7486
7487 let segments = if origin_first {
7488 vec![ConstraintSegment::ORIGIN, point_id.into()]
7489 } else {
7490 vec![point_id.into(), ConstraintSegment::ORIGIN]
7491 };
7492 let constraint = Constraint::Coincident(Coincident {
7493 segments: segments.clone(),
7494 });
7495 let (src_delta, scene_delta) = frontend
7496 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7497 .await
7498 .unwrap();
7499 assert_eq!(src_delta.text.as_str(), expected_source);
7500
7501 let constraint_object = scene_delta
7502 .new_graph
7503 .objects
7504 .iter()
7505 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
7506 .unwrap();
7507
7508 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
7509 panic!("expected a constraint object");
7510 };
7511
7512 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
7513
7514 ctx.close().await;
7515 mock_ctx.close().await;
7516 }
7517 }
7518
7519 #[tokio::test(flavor = "multi_thread")]
7520 async fn test_coincident_of_line_end_points() {
7521 let initial_source = "\
7522sketch(on = XY) {
7523 line(start = [var 1, var 2], end = [var 3, var 4])
7524 line(start = [var 5, var 6], end = [var 7, var 8])
7525}
7526";
7527
7528 let program = Program::parse(initial_source).unwrap().0.unwrap();
7529
7530 let mut frontend = FrontendState::new();
7531
7532 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7533 let mock_ctx = ExecutorContext::new_mock(None).await;
7534 let version = Version(0);
7535
7536 frontend.hack_set_program(&ctx, program).await.unwrap();
7537 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7538 let sketch_id = sketch_object.id;
7539 let sketch = expect_sketch(sketch_object);
7540 let point0_id = *sketch.segments.get(1).unwrap();
7541 let point1_id = *sketch.segments.get(3).unwrap();
7542
7543 let constraint = Constraint::Coincident(Coincident {
7544 segments: vec![point0_id.into(), point1_id.into()],
7545 });
7546 let (src_delta, scene_delta) = frontend
7547 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7548 .await
7549 .unwrap();
7550 assert_eq!(
7551 src_delta.text.as_str(),
7552 "\
7553sketch(on = XY) {
7554 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7555 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7556 coincident([line1.end, line2.start])
7557}
7558"
7559 );
7560 assert_eq!(
7561 scene_delta.new_graph.objects.len(),
7562 9,
7563 "{:#?}",
7564 scene_delta.new_graph.objects
7565 );
7566
7567 ctx.close().await;
7568 mock_ctx.close().await;
7569 }
7570
7571 #[tokio::test(flavor = "multi_thread")]
7572 async fn test_coincident_of_line_point_and_circle_segment() {
7573 let initial_source = "\
7574sketch(on = XY) {
7575 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7576 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
7577}
7578";
7579 let program = Program::parse(initial_source).unwrap().0.unwrap();
7580 let mut frontend = FrontendState::new();
7581
7582 let mock_ctx = ExecutorContext::new_mock(None).await;
7583 let version = Version(0);
7584
7585 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7586 frontend.program = program;
7587 frontend.update_state_after_exec(outcome, true);
7588 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
7589 let sketch_id = sketch_object.id;
7590 let sketch = expect_sketch(sketch_object);
7591
7592 let circle_id = sketch
7593 .segments
7594 .iter()
7595 .copied()
7596 .find(|seg_id| {
7597 matches!(
7598 &frontend.scene_graph.objects[seg_id.0].kind,
7599 ObjectKind::Segment {
7600 segment: Segment::Circle(_)
7601 }
7602 )
7603 })
7604 .expect("Expected a circle segment in sketch");
7605 let line_id = sketch
7606 .segments
7607 .iter()
7608 .copied()
7609 .find(|seg_id| {
7610 matches!(
7611 &frontend.scene_graph.objects[seg_id.0].kind,
7612 ObjectKind::Segment {
7613 segment: Segment::Line(_)
7614 }
7615 )
7616 })
7617 .expect("Expected a line segment in sketch");
7618
7619 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
7620 ObjectKind::Segment {
7621 segment: Segment::Line(line),
7622 } => line.start,
7623 _ => panic!("Expected line segment object"),
7624 };
7625
7626 let constraint = Constraint::Coincident(Coincident {
7627 segments: vec![line_start_point_id.into(), circle_id.into()],
7628 });
7629 let (src_delta, _scene_delta) = frontend
7630 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7631 .await
7632 .unwrap();
7633 assert_eq!(
7634 src_delta.text.as_str(),
7635 "\
7636sketch(on = XY) {
7637 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7638 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
7639 coincident([line1.start, circle1])
7640}
7641"
7642 );
7643
7644 mock_ctx.close().await;
7645 }
7646
7647 #[tokio::test(flavor = "multi_thread")]
7648 async fn test_invalid_coincident_arc_and_line_preserves_state() {
7649 let program = Program::empty();
7657
7658 let mut frontend = FrontendState::new();
7659 frontend.program = program;
7660
7661 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7662 let mock_ctx = ExecutorContext::new_mock(None).await;
7663 let version = Version(0);
7664
7665 let sketch_args = SketchCtor {
7666 on: Plane::Default(PlaneName::Xy),
7667 };
7668 let (_src_delta, _scene_delta, sketch_id) = frontend
7669 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7670 .await
7671 .unwrap();
7672
7673 let arc_ctor = ArcCtor {
7675 start: Point2d {
7676 x: Expr::Var(Number {
7677 value: 0.0,
7678 units: NumericSuffix::Mm,
7679 }),
7680 y: Expr::Var(Number {
7681 value: 0.0,
7682 units: NumericSuffix::Mm,
7683 }),
7684 },
7685 end: Point2d {
7686 x: Expr::Var(Number {
7687 value: 10.0,
7688 units: NumericSuffix::Mm,
7689 }),
7690 y: Expr::Var(Number {
7691 value: 10.0,
7692 units: NumericSuffix::Mm,
7693 }),
7694 },
7695 center: Point2d {
7696 x: Expr::Var(Number {
7697 value: 10.0,
7698 units: NumericSuffix::Mm,
7699 }),
7700 y: Expr::Var(Number {
7701 value: 0.0,
7702 units: NumericSuffix::Mm,
7703 }),
7704 },
7705 construction: None,
7706 };
7707 let (_src_delta, scene_delta) = frontend
7708 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
7709 .await
7710 .unwrap();
7711 let arc_id = *scene_delta.new_objects.last().unwrap();
7713
7714 let line_ctor = LineCtor {
7716 start: Point2d {
7717 x: Expr::Var(Number {
7718 value: 20.0,
7719 units: NumericSuffix::Mm,
7720 }),
7721 y: Expr::Var(Number {
7722 value: 0.0,
7723 units: NumericSuffix::Mm,
7724 }),
7725 },
7726 end: Point2d {
7727 x: Expr::Var(Number {
7728 value: 30.0,
7729 units: NumericSuffix::Mm,
7730 }),
7731 y: Expr::Var(Number {
7732 value: 10.0,
7733 units: NumericSuffix::Mm,
7734 }),
7735 },
7736 construction: None,
7737 };
7738 let (_src_delta, scene_delta) = frontend
7739 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
7740 .await
7741 .unwrap();
7742 let line_id = *scene_delta.new_objects.last().unwrap();
7744
7745 let constraint = Constraint::Coincident(Coincident {
7748 segments: vec![arc_id.into(), line_id.into()],
7749 });
7750 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
7751
7752 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
7754
7755 let sketch_object_after =
7758 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
7759 let sketch_after = expect_sketch(sketch_object_after);
7760
7761 assert!(
7763 sketch_after.segments.contains(&arc_id),
7764 "Arc segment should still exist after failed constraint"
7765 );
7766 assert!(
7767 sketch_after.segments.contains(&line_id),
7768 "Line segment should still exist after failed constraint"
7769 );
7770
7771 let arc_obj = frontend
7773 .scene_graph
7774 .objects
7775 .get(arc_id.0)
7776 .expect("Arc object should still be accessible");
7777 let line_obj = frontend
7778 .scene_graph
7779 .objects
7780 .get(line_id.0)
7781 .expect("Line object should still be accessible");
7782
7783 match &arc_obj.kind {
7786 ObjectKind::Segment {
7787 segment: Segment::Arc(_),
7788 } => {}
7789 _ => panic!("Arc object should still be an arc segment"),
7790 }
7791 match &line_obj.kind {
7792 ObjectKind::Segment {
7793 segment: Segment::Line(_),
7794 } => {}
7795 _ => panic!("Line object should still be a line segment"),
7796 }
7797
7798 ctx.close().await;
7799 mock_ctx.close().await;
7800 }
7801
7802 #[tokio::test(flavor = "multi_thread")]
7803 async fn test_distance_two_points() {
7804 let initial_source = "\
7805sketch(on = XY) {
7806 point(at = [var 1, var 2])
7807 point(at = [var 3, var 4])
7808}
7809";
7810
7811 let program = Program::parse(initial_source).unwrap().0.unwrap();
7812
7813 let mut frontend = FrontendState::new();
7814
7815 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7816 let mock_ctx = ExecutorContext::new_mock(None).await;
7817 let version = Version(0);
7818
7819 frontend.hack_set_program(&ctx, program).await.unwrap();
7820 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7821 let sketch_id = sketch_object.id;
7822 let sketch = expect_sketch(sketch_object);
7823 let point0_id = *sketch.segments.first().unwrap();
7824 let point1_id = *sketch.segments.get(1).unwrap();
7825
7826 let constraint = Constraint::Distance(Distance {
7827 points: vec![point0_id.into(), point1_id.into()],
7828 distance: Number {
7829 value: 2.0,
7830 units: NumericSuffix::Mm,
7831 },
7832 source: Default::default(),
7833 });
7834 let (src_delta, scene_delta) = frontend
7835 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7836 .await
7837 .unwrap();
7838 assert_eq!(
7839 src_delta.text.as_str(),
7840 "\
7842sketch(on = XY) {
7843 point1 = point(at = [var 1, var 2])
7844 point2 = point(at = [var 3, var 4])
7845 distance([point1, point2]) == 2mm
7846}
7847"
7848 );
7849 assert_eq!(
7850 scene_delta.new_graph.objects.len(),
7851 5,
7852 "{:#?}",
7853 scene_delta.new_graph.objects
7854 );
7855
7856 ctx.close().await;
7857 mock_ctx.close().await;
7858 }
7859
7860 #[tokio::test(flavor = "multi_thread")]
7861 async fn test_horizontal_distance_two_points() {
7862 let initial_source = "\
7863sketch(on = XY) {
7864 point(at = [var 1, var 2])
7865 point(at = [var 3, var 4])
7866}
7867";
7868
7869 let program = Program::parse(initial_source).unwrap().0.unwrap();
7870
7871 let mut frontend = FrontendState::new();
7872
7873 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7874 let mock_ctx = ExecutorContext::new_mock(None).await;
7875 let version = Version(0);
7876
7877 frontend.hack_set_program(&ctx, program).await.unwrap();
7878 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7879 let sketch_id = sketch_object.id;
7880 let sketch = expect_sketch(sketch_object);
7881 let point0_id = *sketch.segments.first().unwrap();
7882 let point1_id = *sketch.segments.get(1).unwrap();
7883
7884 let constraint = Constraint::HorizontalDistance(Distance {
7885 points: vec![point0_id.into(), point1_id.into()],
7886 distance: Number {
7887 value: 2.0,
7888 units: NumericSuffix::Mm,
7889 },
7890 source: Default::default(),
7891 });
7892 let (src_delta, scene_delta) = frontend
7893 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7894 .await
7895 .unwrap();
7896 assert_eq!(
7897 src_delta.text.as_str(),
7898 "\
7900sketch(on = XY) {
7901 point1 = point(at = [var 1, var 2])
7902 point2 = point(at = [var 3, var 4])
7903 horizontalDistance([point1, point2]) == 2mm
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_radius_single_arc_segment() {
7920 let initial_source = "\
7921sketch(on = XY) {
7922 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
7923}
7924";
7925
7926 let program = Program::parse(initial_source).unwrap().0.unwrap();
7927
7928 let mut frontend = FrontendState::new();
7929
7930 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7931 let mock_ctx = ExecutorContext::new_mock(None).await;
7932 let version = Version(0);
7933
7934 frontend.hack_set_program(&ctx, program).await.unwrap();
7935 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7936 let sketch_id = sketch_object.id;
7937 let sketch = expect_sketch(sketch_object);
7938 let arc_id = sketch
7940 .segments
7941 .iter()
7942 .find(|&seg_id| {
7943 let obj = frontend.scene_graph.objects.get(seg_id.0);
7944 matches!(
7945 obj.map(|o| &o.kind),
7946 Some(ObjectKind::Segment {
7947 segment: Segment::Arc(_)
7948 })
7949 )
7950 })
7951 .unwrap();
7952
7953 let constraint = Constraint::Radius(Radius {
7954 arc: *arc_id,
7955 radius: Number {
7956 value: 5.0,
7957 units: NumericSuffix::Mm,
7958 },
7959 source: Default::default(),
7960 });
7961 let (src_delta, scene_delta) = frontend
7962 .add_constraint(&mock_ctx, version, sketch_id, constraint)
7963 .await
7964 .unwrap();
7965 assert_eq!(
7966 src_delta.text.as_str(),
7967 "\
7969sketch(on = XY) {
7970 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
7971 radius(arc1) == 5mm
7972}
7973"
7974 );
7975 assert_eq!(
7976 scene_delta.new_graph.objects.len(),
7977 7, "{:#?}",
7979 scene_delta.new_graph.objects
7980 );
7981
7982 ctx.close().await;
7983 mock_ctx.close().await;
7984 }
7985
7986 #[tokio::test(flavor = "multi_thread")]
7987 async fn test_vertical_distance_two_points() {
7988 let initial_source = "\
7989sketch(on = XY) {
7990 point(at = [var 1, var 2])
7991 point(at = [var 3, var 4])
7992}
7993";
7994
7995 let program = Program::parse(initial_source).unwrap().0.unwrap();
7996
7997 let mut frontend = FrontendState::new();
7998
7999 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8000 let mock_ctx = ExecutorContext::new_mock(None).await;
8001 let version = Version(0);
8002
8003 frontend.hack_set_program(&ctx, program).await.unwrap();
8004 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8005 let sketch_id = sketch_object.id;
8006 let sketch = expect_sketch(sketch_object);
8007 let point0_id = *sketch.segments.first().unwrap();
8008 let point1_id = *sketch.segments.get(1).unwrap();
8009
8010 let constraint = Constraint::VerticalDistance(Distance {
8011 points: vec![point0_id.into(), point1_id.into()],
8012 distance: Number {
8013 value: 2.0,
8014 units: NumericSuffix::Mm,
8015 },
8016 source: Default::default(),
8017 });
8018 let (src_delta, scene_delta) = frontend
8019 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8020 .await
8021 .unwrap();
8022 assert_eq!(
8023 src_delta.text.as_str(),
8024 "\
8026sketch(on = XY) {
8027 point1 = point(at = [var 1, var 2])
8028 point2 = point(at = [var 3, var 4])
8029 verticalDistance([point1, point2]) == 2mm
8030}
8031"
8032 );
8033 assert_eq!(
8034 scene_delta.new_graph.objects.len(),
8035 5,
8036 "{:#?}",
8037 scene_delta.new_graph.objects
8038 );
8039
8040 ctx.close().await;
8041 mock_ctx.close().await;
8042 }
8043
8044 #[tokio::test(flavor = "multi_thread")]
8045 async fn test_add_fixed_standalone_point() {
8046 let initial_source = "\
8047sketch(on = XY) {
8048 point(at = [var 1, var 2])
8049}
8050";
8051
8052 let program = Program::parse(initial_source).unwrap().0.unwrap();
8053
8054 let mut frontend = FrontendState::new();
8055
8056 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8057 let mock_ctx = ExecutorContext::new_mock(None).await;
8058 let version = Version(0);
8059
8060 frontend.hack_set_program(&ctx, program).await.unwrap();
8061 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8062 let sketch_id = sketch_object.id;
8063 let sketch = expect_sketch(sketch_object);
8064 let point_id = *sketch.segments.first().unwrap();
8065
8066 let (src_delta, scene_delta) = frontend
8067 .add_constraint(
8068 &mock_ctx,
8069 version,
8070 sketch_id,
8071 Constraint::Fixed(Fixed {
8072 points: vec![FixedPoint {
8073 point: point_id,
8074 position: Point2d {
8075 x: Number {
8076 value: 2.0,
8077 units: NumericSuffix::Mm,
8078 },
8079 y: Number {
8080 value: 3.0,
8081 units: NumericSuffix::Mm,
8082 },
8083 },
8084 }],
8085 }),
8086 )
8087 .await
8088 .unwrap();
8089 assert_eq!(
8090 src_delta.text.as_str(),
8091 "\
8092sketch(on = XY) {
8093 point1 = point(at = [var 1, var 2])
8094 fixed([point1, [2mm, 3mm]])
8095}
8096"
8097 );
8098 assert_eq!(
8099 scene_delta.new_graph.objects.len(),
8100 4,
8101 "{:#?}",
8102 scene_delta.new_graph.objects
8103 );
8104
8105 ctx.close().await;
8106 mock_ctx.close().await;
8107 }
8108
8109 #[tokio::test(flavor = "multi_thread")]
8110 async fn test_add_fixed_multiple_points() {
8111 let initial_source = "\
8112sketch(on = XY) {
8113 point(at = [var 1, var 2])
8114 point(at = [var 3, var 4])
8115}
8116";
8117
8118 let program = Program::parse(initial_source).unwrap().0.unwrap();
8119
8120 let mut frontend = FrontendState::new();
8121
8122 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8123 let mock_ctx = ExecutorContext::new_mock(None).await;
8124 let version = Version(0);
8125
8126 frontend.hack_set_program(&ctx, program).await.unwrap();
8127 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8128 let sketch_id = sketch_object.id;
8129 let sketch = expect_sketch(sketch_object);
8130 let point0_id = *sketch.segments.first().unwrap();
8131 let point1_id = *sketch.segments.get(1).unwrap();
8132
8133 let (src_delta, scene_delta) = frontend
8134 .add_constraint(
8135 &mock_ctx,
8136 version,
8137 sketch_id,
8138 Constraint::Fixed(Fixed {
8139 points: vec![
8140 FixedPoint {
8141 point: point0_id,
8142 position: Point2d {
8143 x: Number {
8144 value: 2.0,
8145 units: NumericSuffix::Mm,
8146 },
8147 y: Number {
8148 value: 3.0,
8149 units: NumericSuffix::Mm,
8150 },
8151 },
8152 },
8153 FixedPoint {
8154 point: point1_id,
8155 position: Point2d {
8156 x: Number {
8157 value: 4.0,
8158 units: NumericSuffix::Mm,
8159 },
8160 y: Number {
8161 value: 5.0,
8162 units: NumericSuffix::Mm,
8163 },
8164 },
8165 },
8166 ],
8167 }),
8168 )
8169 .await
8170 .unwrap();
8171 assert_eq!(
8172 src_delta.text.as_str(),
8173 "\
8174sketch(on = XY) {
8175 point1 = point(at = [var 1, var 2])
8176 point2 = point(at = [var 3, var 4])
8177 fixed([point1, [2mm, 3mm]])
8178 fixed([point2, [4mm, 5mm]])
8179}
8180"
8181 );
8182 assert_eq!(
8183 scene_delta.new_graph.objects.len(),
8184 6,
8185 "{:#?}",
8186 scene_delta.new_graph.objects
8187 );
8188
8189 ctx.close().await;
8190 mock_ctx.close().await;
8191 }
8192
8193 #[tokio::test(flavor = "multi_thread")]
8194 async fn test_add_fixed_owned_point() {
8195 let initial_source = "\
8196sketch(on = XY) {
8197 line(start = [var 1, var 2], end = [var 3, var 4])
8198}
8199";
8200
8201 let program = Program::parse(initial_source).unwrap().0.unwrap();
8202
8203 let mut frontend = FrontendState::new();
8204
8205 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8206 let mock_ctx = ExecutorContext::new_mock(None).await;
8207 let version = Version(0);
8208
8209 frontend.hack_set_program(&ctx, program).await.unwrap();
8210 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8211 let sketch_id = sketch_object.id;
8212 let sketch = expect_sketch(sketch_object);
8213 let line_start_id = *sketch.segments.first().unwrap();
8214
8215 let (src_delta, scene_delta) = frontend
8216 .add_constraint(
8217 &mock_ctx,
8218 version,
8219 sketch_id,
8220 Constraint::Fixed(Fixed {
8221 points: vec![FixedPoint {
8222 point: line_start_id,
8223 position: Point2d {
8224 x: Number {
8225 value: 2.0,
8226 units: NumericSuffix::Mm,
8227 },
8228 y: Number {
8229 value: 3.0,
8230 units: NumericSuffix::Mm,
8231 },
8232 },
8233 }],
8234 }),
8235 )
8236 .await
8237 .unwrap();
8238 assert_eq!(
8239 src_delta.text.as_str(),
8240 "\
8241sketch(on = XY) {
8242 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8243 fixed([line1.start, [2mm, 3mm]])
8244}
8245"
8246 );
8247 assert_eq!(
8248 scene_delta.new_graph.objects.len(),
8249 6,
8250 "{:#?}",
8251 scene_delta.new_graph.objects
8252 );
8253
8254 ctx.close().await;
8255 mock_ctx.close().await;
8256 }
8257
8258 #[tokio::test(flavor = "multi_thread")]
8259 async fn test_radius_error_cases() {
8260 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8261 let mock_ctx = ExecutorContext::new_mock(None).await;
8262 let version = Version(0);
8263
8264 let initial_source_point = "\
8266sketch(on = XY) {
8267 point(at = [var 1, var 2])
8268}
8269";
8270 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
8271 let mut frontend_point = FrontendState::new();
8272 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
8273 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
8274 let sketch_id_point = sketch_object_point.id;
8275 let sketch_point = expect_sketch(sketch_object_point);
8276 let point_id = *sketch_point.segments.first().unwrap();
8277
8278 let constraint_point = Constraint::Radius(Radius {
8279 arc: point_id,
8280 radius: Number {
8281 value: 5.0,
8282 units: NumericSuffix::Mm,
8283 },
8284 source: Default::default(),
8285 });
8286 let result_point = frontend_point
8287 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
8288 .await;
8289 assert!(result_point.is_err(), "Single point should error for radius");
8290
8291 let initial_source_line = "\
8293sketch(on = XY) {
8294 line(start = [var 1, var 2], end = [var 3, var 4])
8295}
8296";
8297 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
8298 let mut frontend_line = FrontendState::new();
8299 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
8300 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
8301 let sketch_id_line = sketch_object_line.id;
8302 let sketch_line = expect_sketch(sketch_object_line);
8303 let line_id = *sketch_line.segments.first().unwrap();
8304
8305 let constraint_line = Constraint::Radius(Radius {
8306 arc: line_id,
8307 radius: Number {
8308 value: 5.0,
8309 units: NumericSuffix::Mm,
8310 },
8311 source: Default::default(),
8312 });
8313 let result_line = frontend_line
8314 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
8315 .await;
8316 assert!(result_line.is_err(), "Single line segment should error for radius");
8317
8318 ctx.close().await;
8319 mock_ctx.close().await;
8320 }
8321
8322 #[tokio::test(flavor = "multi_thread")]
8323 async fn test_diameter_single_arc_segment() {
8324 let initial_source = "\
8325sketch(on = XY) {
8326 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8327}
8328";
8329
8330 let program = Program::parse(initial_source).unwrap().0.unwrap();
8331
8332 let mut frontend = FrontendState::new();
8333
8334 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8335 let mock_ctx = ExecutorContext::new_mock(None).await;
8336 let version = Version(0);
8337
8338 frontend.hack_set_program(&ctx, program).await.unwrap();
8339 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8340 let sketch_id = sketch_object.id;
8341 let sketch = expect_sketch(sketch_object);
8342 let arc_id = sketch
8344 .segments
8345 .iter()
8346 .find(|&seg_id| {
8347 let obj = frontend.scene_graph.objects.get(seg_id.0);
8348 matches!(
8349 obj.map(|o| &o.kind),
8350 Some(ObjectKind::Segment {
8351 segment: Segment::Arc(_)
8352 })
8353 )
8354 })
8355 .unwrap();
8356
8357 let constraint = Constraint::Diameter(Diameter {
8358 arc: *arc_id,
8359 diameter: Number {
8360 value: 10.0,
8361 units: NumericSuffix::Mm,
8362 },
8363 source: Default::default(),
8364 });
8365 let (src_delta, scene_delta) = frontend
8366 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8367 .await
8368 .unwrap();
8369 assert_eq!(
8370 src_delta.text.as_str(),
8371 "\
8373sketch(on = XY) {
8374 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
8375 diameter(arc1) == 10mm
8376}
8377"
8378 );
8379 assert_eq!(
8380 scene_delta.new_graph.objects.len(),
8381 7, "{:#?}",
8383 scene_delta.new_graph.objects
8384 );
8385
8386 ctx.close().await;
8387 mock_ctx.close().await;
8388 }
8389
8390 #[tokio::test(flavor = "multi_thread")]
8391 async fn test_diameter_error_cases() {
8392 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8393 let mock_ctx = ExecutorContext::new_mock(None).await;
8394 let version = Version(0);
8395
8396 let initial_source_point = "\
8398sketch(on = XY) {
8399 point(at = [var 1, var 2])
8400}
8401";
8402 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
8403 let mut frontend_point = FrontendState::new();
8404 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
8405 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
8406 let sketch_id_point = sketch_object_point.id;
8407 let sketch_point = expect_sketch(sketch_object_point);
8408 let point_id = *sketch_point.segments.first().unwrap();
8409
8410 let constraint_point = Constraint::Diameter(Diameter {
8411 arc: point_id,
8412 diameter: Number {
8413 value: 10.0,
8414 units: NumericSuffix::Mm,
8415 },
8416 source: Default::default(),
8417 });
8418 let result_point = frontend_point
8419 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
8420 .await;
8421 assert!(result_point.is_err(), "Single point should error for diameter");
8422
8423 let initial_source_line = "\
8425sketch(on = XY) {
8426 line(start = [var 1, var 2], end = [var 3, var 4])
8427}
8428";
8429 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
8430 let mut frontend_line = FrontendState::new();
8431 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
8432 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
8433 let sketch_id_line = sketch_object_line.id;
8434 let sketch_line = expect_sketch(sketch_object_line);
8435 let line_id = *sketch_line.segments.first().unwrap();
8436
8437 let constraint_line = Constraint::Diameter(Diameter {
8438 arc: line_id,
8439 diameter: Number {
8440 value: 10.0,
8441 units: NumericSuffix::Mm,
8442 },
8443 source: Default::default(),
8444 });
8445 let result_line = frontend_line
8446 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
8447 .await;
8448 assert!(result_line.is_err(), "Single line segment should error for diameter");
8449
8450 ctx.close().await;
8451 mock_ctx.close().await;
8452 }
8453
8454 #[tokio::test(flavor = "multi_thread")]
8455 async fn test_line_horizontal() {
8456 let initial_source = "\
8457sketch(on = XY) {
8458 line(start = [var 1, var 2], end = [var 3, var 4])
8459}
8460";
8461
8462 let program = Program::parse(initial_source).unwrap().0.unwrap();
8463
8464 let mut frontend = FrontendState::new();
8465
8466 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8467 let mock_ctx = ExecutorContext::new_mock(None).await;
8468 let version = Version(0);
8469
8470 frontend.hack_set_program(&ctx, program).await.unwrap();
8471 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8472 let sketch_id = sketch_object.id;
8473 let sketch = expect_sketch(sketch_object);
8474 let line1_id = *sketch.segments.get(2).unwrap();
8475
8476 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
8477 let (src_delta, scene_delta) = frontend
8478 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8479 .await
8480 .unwrap();
8481 assert_eq!(
8482 src_delta.text.as_str(),
8483 "\
8484sketch(on = XY) {
8485 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8486 horizontal(line1)
8487}
8488"
8489 );
8490 assert_eq!(
8491 scene_delta.new_graph.objects.len(),
8492 6,
8493 "{:#?}",
8494 scene_delta.new_graph.objects
8495 );
8496
8497 ctx.close().await;
8498 mock_ctx.close().await;
8499 }
8500
8501 #[tokio::test(flavor = "multi_thread")]
8502 async fn test_line_vertical() {
8503 let initial_source = "\
8504sketch(on = XY) {
8505 line(start = [var 1, var 2], end = [var 3, var 4])
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 let line1_id = *sketch.segments.get(2).unwrap();
8522
8523 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
8524 let (src_delta, scene_delta) = frontend
8525 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8526 .await
8527 .unwrap();
8528 assert_eq!(
8529 src_delta.text.as_str(),
8530 "\
8531sketch(on = XY) {
8532 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8533 vertical(line1)
8534}
8535"
8536 );
8537 assert_eq!(
8538 scene_delta.new_graph.objects.len(),
8539 6,
8540 "{:#?}",
8541 scene_delta.new_graph.objects
8542 );
8543
8544 ctx.close().await;
8545 mock_ctx.close().await;
8546 }
8547
8548 #[tokio::test(flavor = "multi_thread")]
8549 async fn test_points_vertical() {
8550 let initial_source = "\
8551sketch001 = sketch(on = XY) {
8552 p0 = point(at = [var -2.23mm, var 3.1mm])
8553 pf = point(at = [4, 4])
8554}
8555";
8556
8557 let program = Program::parse(initial_source).unwrap().0.unwrap();
8558
8559 let mut frontend = FrontendState::new();
8560
8561 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8562 let mock_ctx = ExecutorContext::new_mock(None).await;
8563 let version = Version(0);
8564
8565 frontend.hack_set_program(&ctx, program).await.unwrap();
8566 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8567 let sketch_id = sketch_object.id;
8568 let sketch = expect_sketch(sketch_object);
8569 let point_ids = vec![
8570 sketch.segments.first().unwrap().to_owned(),
8571 sketch.segments.get(1).unwrap().to_owned(),
8572 ];
8573
8574 let constraint = Constraint::Vertical(Vertical::Points {
8575 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
8576 });
8577 let (src_delta, scene_delta) = frontend
8578 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8579 .await
8580 .unwrap();
8581 assert_eq!(
8582 src_delta.text.as_str(),
8583 "\
8584sketch001 = sketch(on = XY) {
8585 p0 = point(at = [var -2.23mm, var 3.1mm])
8586 pf = point(at = [4, 4])
8587 vertical([p0, pf])
8588}
8589"
8590 );
8591 assert_eq!(
8592 scene_delta.new_graph.objects.len(),
8593 5,
8594 "{:#?}",
8595 scene_delta.new_graph.objects
8596 );
8597
8598 ctx.close().await;
8599 mock_ctx.close().await;
8600 }
8601
8602 #[tokio::test(flavor = "multi_thread")]
8603 async fn test_points_horizontal() {
8604 let initial_source = "\
8605sketch001 = sketch(on = XY) {
8606 p0 = point(at = [var -2.23mm, var 3.1mm])
8607 pf = point(at = [4, 4])
8608}
8609";
8610
8611 let program = Program::parse(initial_source).unwrap().0.unwrap();
8612
8613 let mut frontend = FrontendState::new();
8614
8615 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8616 let mock_ctx = ExecutorContext::new_mock(None).await;
8617 let version = Version(0);
8618
8619 frontend.hack_set_program(&ctx, program).await.unwrap();
8620 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8621 let sketch_id = sketch_object.id;
8622 let sketch = expect_sketch(sketch_object);
8623 let point_ids = vec![
8624 sketch.segments.first().unwrap().to_owned(),
8625 sketch.segments.get(1).unwrap().to_owned(),
8626 ];
8627
8628 let constraint = Constraint::Horizontal(Horizontal::Points {
8629 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
8630 });
8631 let (src_delta, scene_delta) = frontend
8632 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8633 .await
8634 .unwrap();
8635 assert_eq!(
8636 src_delta.text.as_str(),
8637 "\
8638sketch001 = sketch(on = XY) {
8639 p0 = point(at = [var -2.23mm, var 3.1mm])
8640 pf = point(at = [4, 4])
8641 horizontal([p0, pf])
8642}
8643"
8644 );
8645 assert_eq!(
8646 scene_delta.new_graph.objects.len(),
8647 5,
8648 "{:#?}",
8649 scene_delta.new_graph.objects
8650 );
8651
8652 ctx.close().await;
8653 mock_ctx.close().await;
8654 }
8655
8656 #[tokio::test(flavor = "multi_thread")]
8657 async fn test_point_horizontal_with_origin() {
8658 let initial_source = "\
8659sketch001 = sketch(on = XY) {
8660 p0 = point(at = [var -2.23mm, var 3.1mm])
8661}
8662";
8663
8664 let program = Program::parse(initial_source).unwrap().0.unwrap();
8665
8666 let mut frontend = FrontendState::new();
8667
8668 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8669 let mock_ctx = ExecutorContext::new_mock(None).await;
8670 let version = Version(0);
8671
8672 frontend.hack_set_program(&ctx, program).await.unwrap();
8673 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8674 let sketch_id = sketch_object.id;
8675 let sketch = expect_sketch(sketch_object);
8676 let point_id = *sketch.segments.first().unwrap();
8677
8678 let constraint = Constraint::Horizontal(Horizontal::Points {
8679 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
8680 });
8681 let (src_delta, scene_delta) = frontend
8682 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8683 .await
8684 .unwrap();
8685 assert_eq!(
8686 src_delta.text.as_str(),
8687 "\
8688sketch001 = sketch(on = XY) {
8689 p0 = point(at = [var -2.23mm, var 3.1mm])
8690 horizontal([p0, ORIGIN])
8691}
8692"
8693 );
8694 assert_eq!(
8695 scene_delta.new_graph.objects.len(),
8696 4,
8697 "{:#?}",
8698 scene_delta.new_graph.objects
8699 );
8700
8701 ctx.close().await;
8702 mock_ctx.close().await;
8703 }
8704
8705 #[tokio::test(flavor = "multi_thread")]
8706 async fn test_lines_equal_length() {
8707 let initial_source = "\
8708sketch(on = XY) {
8709 line(start = [var 1, var 2], end = [var 3, var 4])
8710 line(start = [var 5, var 6], end = [var 7, var 8])
8711}
8712";
8713
8714 let program = Program::parse(initial_source).unwrap().0.unwrap();
8715
8716 let mut frontend = FrontendState::new();
8717
8718 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8719 let mock_ctx = ExecutorContext::new_mock(None).await;
8720 let version = Version(0);
8721
8722 frontend.hack_set_program(&ctx, program).await.unwrap();
8723 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8724 let sketch_id = sketch_object.id;
8725 let sketch = expect_sketch(sketch_object);
8726 let line1_id = *sketch.segments.get(2).unwrap();
8727 let line2_id = *sketch.segments.get(5).unwrap();
8728
8729 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
8730 lines: vec![line1_id, line2_id],
8731 });
8732 let (src_delta, scene_delta) = frontend
8733 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8734 .await
8735 .unwrap();
8736 assert_eq!(
8737 src_delta.text.as_str(),
8738 "\
8739sketch(on = XY) {
8740 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8741 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8742 equalLength([line1, line2])
8743}
8744"
8745 );
8746 assert_eq!(
8747 scene_delta.new_graph.objects.len(),
8748 9,
8749 "{:#?}",
8750 scene_delta.new_graph.objects
8751 );
8752
8753 ctx.close().await;
8754 mock_ctx.close().await;
8755 }
8756
8757 #[tokio::test(flavor = "multi_thread")]
8758 async fn test_add_constraint_multi_line_equal_length() {
8759 let initial_source = "\
8760sketch(on = XY) {
8761 line(start = [var 1, var 2], end = [var 3, var 4])
8762 line(start = [var 5, var 6], end = [var 7, var 8])
8763 line(start = [var 9, var 10], end = [var 11, var 12])
8764}
8765";
8766
8767 let program = Program::parse(initial_source).unwrap().0.unwrap();
8768
8769 let mut frontend = FrontendState::new();
8770 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8771 let mock_ctx = ExecutorContext::new_mock(None).await;
8772 let version = Version(0);
8773
8774 frontend.hack_set_program(&ctx, program).await.unwrap();
8775 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8776 let sketch_id = sketch_object.id;
8777 let sketch = expect_sketch(sketch_object);
8778 let line1_id = *sketch.segments.get(2).unwrap();
8779 let line2_id = *sketch.segments.get(5).unwrap();
8780 let line3_id = *sketch.segments.get(8).unwrap();
8781
8782 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
8783 lines: vec![line1_id, line2_id, line3_id],
8784 });
8785 let (src_delta, scene_delta) = frontend
8786 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8787 .await
8788 .unwrap();
8789 assert_eq!(
8790 src_delta.text.as_str(),
8791 "\
8792sketch(on = XY) {
8793 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8794 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8795 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8796 equalLength([line1, line2, line3])
8797}
8798"
8799 );
8800 let constraints = scene_delta
8801 .new_graph
8802 .objects
8803 .iter()
8804 .filter_map(|obj| {
8805 let ObjectKind::Constraint { constraint } = &obj.kind else {
8806 return None;
8807 };
8808 Some(constraint)
8809 })
8810 .collect::<Vec<_>>();
8811
8812 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
8813 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
8814 panic!("expected equal length constraint, got {:?}", constraints[0]);
8815 };
8816 assert_eq!(lines_equal_length.lines.len(), 3);
8817
8818 ctx.close().await;
8819 mock_ctx.close().await;
8820 }
8821
8822 #[tokio::test(flavor = "multi_thread")]
8823 async fn test_lines_parallel() {
8824 let initial_source = "\
8825sketch(on = XY) {
8826 line(start = [var 1, var 2], end = [var 3, var 4])
8827 line(start = [var 5, var 6], end = [var 7, var 8])
8828}
8829";
8830
8831 let program = Program::parse(initial_source).unwrap().0.unwrap();
8832
8833 let mut frontend = FrontendState::new();
8834
8835 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8836 let mock_ctx = ExecutorContext::new_mock(None).await;
8837 let version = Version(0);
8838
8839 frontend.hack_set_program(&ctx, program).await.unwrap();
8840 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8841 let sketch_id = sketch_object.id;
8842 let sketch = expect_sketch(sketch_object);
8843 let line1_id = *sketch.segments.get(2).unwrap();
8844 let line2_id = *sketch.segments.get(5).unwrap();
8845
8846 let constraint = Constraint::Parallel(Parallel {
8847 lines: vec![line1_id, line2_id],
8848 });
8849 let (src_delta, scene_delta) = frontend
8850 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8851 .await
8852 .unwrap();
8853 assert_eq!(
8854 src_delta.text.as_str(),
8855 "\
8856sketch(on = XY) {
8857 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8858 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8859 parallel([line1, line2])
8860}
8861"
8862 );
8863 assert_eq!(
8864 scene_delta.new_graph.objects.len(),
8865 9,
8866 "{:#?}",
8867 scene_delta.new_graph.objects
8868 );
8869
8870 ctx.close().await;
8871 mock_ctx.close().await;
8872 }
8873
8874 #[tokio::test(flavor = "multi_thread")]
8875 async fn test_lines_parallel_multiline() {
8876 let initial_source = "\
8877sketch(on = XY) {
8878 line(start = [var 1, var 2], end = [var 3, var 4])
8879 line(start = [var 5, var 6], end = [var 7, var 8])
8880 line(start = [var 9, var 10], end = [var 11, var 12])
8881}
8882";
8883
8884 let program = Program::parse(initial_source).unwrap().0.unwrap();
8885
8886 let mut frontend = FrontendState::new();
8887
8888 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8889 let mock_ctx = ExecutorContext::new_mock(None).await;
8890 let version = Version(0);
8891
8892 frontend.hack_set_program(&ctx, program).await.unwrap();
8893 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8894 let sketch_id = sketch_object.id;
8895 let sketch = expect_sketch(sketch_object);
8896 let line1_id = *sketch.segments.get(2).unwrap();
8897 let line2_id = *sketch.segments.get(5).unwrap();
8898 let line3_id = *sketch.segments.get(8).unwrap();
8899
8900 let constraint = Constraint::Parallel(Parallel {
8901 lines: vec![line1_id, line2_id, line3_id],
8902 });
8903 let (src_delta, scene_delta) = frontend
8904 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8905 .await
8906 .unwrap();
8907 assert_eq!(
8908 src_delta.text.as_str(),
8909 "\
8910sketch(on = XY) {
8911 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8912 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8913 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8914 parallel([line1, line2, line3])
8915}
8916"
8917 );
8918
8919 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8920 let sketch = expect_sketch(sketch_object);
8921 assert_eq!(sketch.constraints.len(), 1);
8922
8923 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8924 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8925 panic!("Expected constraint object");
8926 };
8927 let Constraint::Parallel(parallel) = constraint else {
8928 panic!("Expected parallel constraint");
8929 };
8930 assert_eq!(parallel.lines.len(), 3);
8931
8932 ctx.close().await;
8933 mock_ctx.close().await;
8934 }
8935
8936 #[tokio::test(flavor = "multi_thread")]
8937 async fn test_lines_perpendicular() {
8938 let initial_source = "\
8939sketch(on = XY) {
8940 line(start = [var 1, var 2], end = [var 3, var 4])
8941 line(start = [var 5, var 6], end = [var 7, var 8])
8942}
8943";
8944
8945 let program = Program::parse(initial_source).unwrap().0.unwrap();
8946
8947 let mut frontend = FrontendState::new();
8948
8949 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8950 let mock_ctx = ExecutorContext::new_mock(None).await;
8951 let version = Version(0);
8952
8953 frontend.hack_set_program(&ctx, program).await.unwrap();
8954 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8955 let sketch_id = sketch_object.id;
8956 let sketch = expect_sketch(sketch_object);
8957 let line1_id = *sketch.segments.get(2).unwrap();
8958 let line2_id = *sketch.segments.get(5).unwrap();
8959
8960 let constraint = Constraint::Perpendicular(Perpendicular {
8961 lines: vec![line1_id, line2_id],
8962 });
8963 let (src_delta, scene_delta) = frontend
8964 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8965 .await
8966 .unwrap();
8967 assert_eq!(
8968 src_delta.text.as_str(),
8969 "\
8970sketch(on = XY) {
8971 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8972 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8973 perpendicular([line1, line2])
8974}
8975"
8976 );
8977 assert_eq!(
8978 scene_delta.new_graph.objects.len(),
8979 9,
8980 "{:#?}",
8981 scene_delta.new_graph.objects
8982 );
8983
8984 ctx.close().await;
8985 mock_ctx.close().await;
8986 }
8987
8988 #[tokio::test(flavor = "multi_thread")]
8989 async fn test_lines_angle() {
8990 let initial_source = "\
8991sketch(on = XY) {
8992 line(start = [var 1, var 2], end = [var 3, var 4])
8993 line(start = [var 5, var 6], end = [var 7, var 8])
8994}
8995";
8996
8997 let program = Program::parse(initial_source).unwrap().0.unwrap();
8998
8999 let mut frontend = FrontendState::new();
9000
9001 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9002 let mock_ctx = ExecutorContext::new_mock(None).await;
9003 let version = Version(0);
9004
9005 frontend.hack_set_program(&ctx, program).await.unwrap();
9006 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9007 let sketch_id = sketch_object.id;
9008 let sketch = expect_sketch(sketch_object);
9009 let line1_id = *sketch.segments.get(2).unwrap();
9010 let line2_id = *sketch.segments.get(5).unwrap();
9011
9012 let constraint = Constraint::Angle(Angle {
9013 lines: vec![line1_id, line2_id],
9014 angle: Number {
9015 value: 30.0,
9016 units: NumericSuffix::Deg,
9017 },
9018 source: Default::default(),
9019 });
9020 let (src_delta, scene_delta) = frontend
9021 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9022 .await
9023 .unwrap();
9024 assert_eq!(
9025 src_delta.text.as_str(),
9026 "\
9028sketch(on = XY) {
9029 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9030 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9031 angle([line1, line2]) == 30deg
9032}
9033"
9034 );
9035 assert_eq!(
9036 scene_delta.new_graph.objects.len(),
9037 9,
9038 "{:#?}",
9039 scene_delta.new_graph.objects
9040 );
9041
9042 ctx.close().await;
9043 mock_ctx.close().await;
9044 }
9045
9046 #[tokio::test(flavor = "multi_thread")]
9047 async fn test_segments_tangent() {
9048 let initial_source = "\
9049sketch(on = XY) {
9050 line(start = [var 1, var 2], end = [var 3, var 4])
9051 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
9052}
9053";
9054
9055 let program = Program::parse(initial_source).unwrap().0.unwrap();
9056
9057 let mut frontend = FrontendState::new();
9058
9059 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9060 let mock_ctx = ExecutorContext::new_mock(None).await;
9061 let version = Version(0);
9062
9063 frontend.hack_set_program(&ctx, program).await.unwrap();
9064 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9065 let sketch_id = sketch_object.id;
9066 let sketch = expect_sketch(sketch_object);
9067 let line1_id = *sketch.segments.get(2).unwrap();
9068 let arc1_id = *sketch.segments.get(6).unwrap();
9069
9070 let constraint = Constraint::Tangent(Tangent {
9071 input: vec![line1_id, arc1_id],
9072 });
9073 let (src_delta, scene_delta) = frontend
9074 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9075 .await
9076 .unwrap();
9077 assert_eq!(
9078 src_delta.text.as_str(),
9079 "\
9080sketch(on = XY) {
9081 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9082 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
9083 tangent([line1, arc1])
9084}
9085"
9086 );
9087 assert_eq!(
9088 scene_delta.new_graph.objects.len(),
9089 10,
9090 "{:#?}",
9091 scene_delta.new_graph.objects
9092 );
9093
9094 ctx.close().await;
9095 mock_ctx.close().await;
9096 }
9097
9098 #[tokio::test(flavor = "multi_thread")]
9099 async fn test_sketch_on_face_simple() {
9100 let initial_source = "\
9101len = 2mm
9102cube = startSketchOn(XY)
9103 |> startProfile(at = [0, 0])
9104 |> line(end = [len, 0], tag = $side)
9105 |> line(end = [0, len])
9106 |> line(end = [-len, 0])
9107 |> line(end = [0, -len])
9108 |> close()
9109 |> extrude(length = len)
9110
9111face = faceOf(cube, face = side)
9112";
9113
9114 let program = Program::parse(initial_source).unwrap().0.unwrap();
9115
9116 let mut frontend = FrontendState::new();
9117
9118 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9119 let mock_ctx = ExecutorContext::new_mock(None).await;
9120 let version = Version(0);
9121
9122 frontend.hack_set_program(&ctx, program).await.unwrap();
9123 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
9124 let face_id = face_object.id;
9125
9126 let sketch_args = SketchCtor {
9127 on: Plane::Object(face_id),
9128 };
9129 let (_src_delta, scene_delta, sketch_id) = frontend
9130 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9131 .await
9132 .unwrap();
9133 assert_eq!(sketch_id, ObjectId(2));
9134 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
9135 let sketch_object = &scene_delta.new_graph.objects[2];
9136 assert_eq!(sketch_object.id, ObjectId(2));
9137 assert_eq!(
9138 sketch_object.kind,
9139 ObjectKind::Sketch(Sketch {
9140 args: SketchCtor {
9141 on: Plane::Object(face_id),
9142 },
9143 plane: face_id,
9144 segments: vec![],
9145 constraints: vec![],
9146 })
9147 );
9148 assert_eq!(scene_delta.new_graph.objects.len(), 8);
9149
9150 ctx.close().await;
9151 mock_ctx.close().await;
9152 }
9153
9154 #[tokio::test(flavor = "multi_thread")]
9155 async fn test_sketch_on_wall_artifact_from_region_extrude() {
9156 let initial_source = "\
9157s = sketch(on = YZ) {
9158 line1 = line(start = [0, 0], end = [0, 1])
9159 line2 = line(start = [0, 1], end = [1, 1])
9160 line3 = line(start = [1, 1], end = [0, 0])
9161}
9162region001 = region(point = [0.1, 0.1], sketch = s)
9163extrude001 = extrude(region001, length = 5)
9164";
9165
9166 let program = Program::parse(initial_source).unwrap().0.unwrap();
9167
9168 let mut frontend = FrontendState::new();
9169 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9170 let version = Version(0);
9171
9172 frontend.hack_set_program(&ctx, program).await.unwrap();
9173 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
9174
9175 let sketch_args = SketchCtor {
9176 on: Plane::Object(wall_object_id),
9177 };
9178 let (src_delta, _scene_delta, _sketch_id) = frontend
9179 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9180 .await
9181 .unwrap();
9182 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
9183
9184 ctx.close().await;
9185 }
9186
9187 #[tokio::test(flavor = "multi_thread")]
9188 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
9189 let initial_source = "\
9190sketch001 = sketch(on = YZ) {
9191 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
9192 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
9193 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
9194 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
9195 coincident([line1.end, line2.start])
9196 coincident([line2.end, line3.start])
9197 coincident([line3.end, line4.start])
9198 coincident([line4.end, line1.start])
9199 parallel([line2, line4])
9200 parallel([line3, line1])
9201 perpendicular([line1, line2])
9202 horizontal(line3)
9203 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
9204}
9205region001 = region(point = [3.1, 3.74], sketch = sketch001)
9206extrude001 = extrude(region001, length = 5)
9207";
9208
9209 let program = Program::parse(initial_source).unwrap().0.unwrap();
9210
9211 let mut frontend = FrontendState::new();
9212 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9213 let version = Version(0);
9214
9215 frontend.hack_set_program(&ctx, program).await.unwrap();
9216 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
9217
9218 let sketch_args = SketchCtor {
9219 on: Plane::Object(wall_object_id),
9220 };
9221 let (src_delta, _scene_delta, _sketch_id) = frontend
9222 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9223 .await
9224 .unwrap();
9225 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
9226
9227 ctx.close().await;
9228 }
9229
9230 #[tokio::test(flavor = "multi_thread")]
9231 async fn test_sketch_on_plane_incremental() {
9232 let initial_source = "\
9233len = 2mm
9234cube = startSketchOn(XY)
9235 |> startProfile(at = [0, 0])
9236 |> line(end = [len, 0], tag = $side)
9237 |> line(end = [0, len])
9238 |> line(end = [-len, 0])
9239 |> line(end = [0, -len])
9240 |> close()
9241 |> extrude(length = len)
9242
9243plane = planeOf(cube, face = side)
9244";
9245
9246 let program = Program::parse(initial_source).unwrap().0.unwrap();
9247
9248 let mut frontend = FrontendState::new();
9249
9250 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9251 let mock_ctx = ExecutorContext::new_mock(None).await;
9252 let version = Version(0);
9253
9254 frontend.hack_set_program(&ctx, program).await.unwrap();
9255 let plane_object = frontend
9257 .scene_graph
9258 .objects
9259 .iter()
9260 .rev()
9261 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
9262 .unwrap();
9263 let plane_id = plane_object.id;
9264
9265 let sketch_args = SketchCtor {
9266 on: Plane::Object(plane_id),
9267 };
9268 let (src_delta, scene_delta, sketch_id) = frontend
9269 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9270 .await
9271 .unwrap();
9272 assert_eq!(
9273 src_delta.text.as_str(),
9274 "\
9275len = 2mm
9276cube = startSketchOn(XY)
9277 |> startProfile(at = [0, 0])
9278 |> line(end = [len, 0], tag = $side)
9279 |> line(end = [0, len])
9280 |> line(end = [-len, 0])
9281 |> line(end = [0, -len])
9282 |> close()
9283 |> extrude(length = len)
9284
9285plane = planeOf(cube, face = side)
9286sketch001 = sketch(on = plane) {
9287}
9288"
9289 );
9290 assert_eq!(sketch_id, ObjectId(2));
9291 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
9292 let sketch_object = &scene_delta.new_graph.objects[2];
9293 assert_eq!(sketch_object.id, ObjectId(2));
9294 assert_eq!(
9295 sketch_object.kind,
9296 ObjectKind::Sketch(Sketch {
9297 args: SketchCtor {
9298 on: Plane::Object(plane_id),
9299 },
9300 plane: plane_id,
9301 segments: vec![],
9302 constraints: vec![],
9303 })
9304 );
9305 assert_eq!(scene_delta.new_graph.objects.len(), 9);
9306
9307 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
9308 assert_eq!(plane_object.id, plane_id);
9309 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
9310
9311 ctx.close().await;
9312 mock_ctx.close().await;
9313 }
9314
9315 #[tokio::test(flavor = "multi_thread")]
9316 async fn test_new_sketch_uses_unique_variable_name() {
9317 let initial_source = "\
9318sketch1 = sketch(on = XY) {
9319}
9320";
9321
9322 let program = Program::parse(initial_source).unwrap().0.unwrap();
9323
9324 let mut frontend = FrontendState::new();
9325 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9326 let version = Version(0);
9327
9328 frontend.hack_set_program(&ctx, program).await.unwrap();
9329
9330 let sketch_args = SketchCtor {
9331 on: Plane::Default(PlaneName::Yz),
9332 };
9333 let (src_delta, _, _) = frontend
9334 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9335 .await
9336 .unwrap();
9337
9338 assert_eq!(
9339 src_delta.text.as_str(),
9340 "\
9341sketch1 = sketch(on = XY) {
9342}
9343sketch001 = sketch(on = YZ) {
9344}
9345"
9346 );
9347
9348 ctx.close().await;
9349 }
9350
9351 #[tokio::test(flavor = "multi_thread")]
9352 async fn test_new_sketch_twice_using_same_plane() {
9353 let initial_source = "\
9354sketch1 = sketch(on = XY) {
9355}
9356";
9357
9358 let program = Program::parse(initial_source).unwrap().0.unwrap();
9359
9360 let mut frontend = FrontendState::new();
9361 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9362 let version = Version(0);
9363
9364 frontend.hack_set_program(&ctx, program).await.unwrap();
9365
9366 let sketch_args = SketchCtor {
9367 on: Plane::Default(PlaneName::Xy),
9368 };
9369 let (src_delta, _, _) = frontend
9370 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9371 .await
9372 .unwrap();
9373
9374 assert_eq!(
9375 src_delta.text.as_str(),
9376 "\
9377sketch1 = sketch(on = XY) {
9378}
9379sketch001 = sketch(on = XY) {
9380}
9381"
9382 );
9383
9384 ctx.close().await;
9385 }
9386
9387 #[tokio::test(flavor = "multi_thread")]
9388 async fn test_sketch_mode_reuses_cached_on_expression() {
9389 let initial_source = "\
9390width = 2mm
9391sketch(on = offsetPlane(XY, offset = width)) {
9392 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
9393 distance([line1.start, line1.end]) == width
9394}
9395";
9396 let program = Program::parse(initial_source).unwrap().0.unwrap();
9397
9398 let mut frontend = FrontendState::new();
9399 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9400 let mock_ctx = ExecutorContext::new_mock(None).await;
9401 let version = Version(0);
9402 let project_id = ProjectId(0);
9403 let file_id = FileId(0);
9404
9405 frontend.hack_set_program(&ctx, program).await.unwrap();
9406 let initial_object_count = frontend.scene_graph.objects.len();
9407 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
9408 .expect("Expected sketch object to exist")
9409 .id;
9410
9411 let scene_delta = frontend
9414 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
9415 .await
9416 .unwrap();
9417 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
9418
9419 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9422 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
9423
9424 ctx.close().await;
9425 mock_ctx.close().await;
9426 }
9427
9428 #[tokio::test(flavor = "multi_thread")]
9429 async fn test_multiple_sketch_blocks() {
9430 let initial_source = "\
9431// Cube that requires the engine.
9432width = 2
9433sketch001 = startSketchOn(XY)
9434profile001 = startProfile(sketch001, at = [0, 0])
9435 |> yLine(length = width, tag = $seg1)
9436 |> xLine(length = width)
9437 |> yLine(length = -width)
9438 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
9439 |> close()
9440extrude001 = extrude(profile001, length = width)
9441
9442// Get a value that requires the engine.
9443x = segLen(seg1)
9444
9445// Triangle with side length 2*x.
9446sketch(on = XY) {
9447 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
9448 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
9449 coincident([line1.end, line2.start])
9450 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
9451 coincident([line2.end, line3.start])
9452 coincident([line3.end, line1.start])
9453 equalLength([line3, line1])
9454 equalLength([line1, line2])
9455 distance([line1.start, line1.end]) == 2*x
9456}
9457
9458// Line segment with length x.
9459sketch2 = sketch(on = XY) {
9460 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
9461 distance([line1.start, line1.end]) == x
9462}
9463";
9464
9465 let program = Program::parse(initial_source).unwrap().0.unwrap();
9466
9467 let mut frontend = FrontendState::new();
9468
9469 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9470 let mock_ctx = ExecutorContext::new_mock(None).await;
9471 let version = Version(0);
9472 let project_id = ProjectId(0);
9473 let file_id = FileId(0);
9474
9475 frontend.hack_set_program(&ctx, program).await.unwrap();
9476 let sketch_objects = frontend
9477 .scene_graph
9478 .objects
9479 .iter()
9480 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
9481 .collect::<Vec<_>>();
9482 let sketch1_id = sketch_objects.first().unwrap().id;
9483 let sketch2_id = sketch_objects.get(1).unwrap().id;
9484 let point1_id = ObjectId(sketch1_id.0 + 1);
9486 let point2_id = ObjectId(sketch2_id.0 + 1);
9488
9489 let scene_delta = frontend
9498 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
9499 .await
9500 .unwrap();
9501 assert_eq!(
9502 scene_delta.new_graph.objects.len(),
9503 18,
9504 "{:#?}",
9505 scene_delta.new_graph.objects
9506 );
9507
9508 let point_ctor = PointCtor {
9510 position: Point2d {
9511 x: Expr::Var(Number {
9512 value: 1.0,
9513 units: NumericSuffix::Mm,
9514 }),
9515 y: Expr::Var(Number {
9516 value: 2.0,
9517 units: NumericSuffix::Mm,
9518 }),
9519 },
9520 };
9521 let segments = vec![ExistingSegmentCtor {
9522 id: point1_id,
9523 ctor: SegmentCtor::Point(point_ctor),
9524 }];
9525 let (src_delta, _) = frontend
9526 .edit_segments(&mock_ctx, version, sketch1_id, segments)
9527 .await
9528 .unwrap();
9529 assert_eq!(
9531 src_delta.text.as_str(),
9532 "\
9533// Cube that requires the engine.
9534width = 2
9535sketch001 = startSketchOn(XY)
9536profile001 = startProfile(sketch001, at = [0, 0])
9537 |> yLine(length = width, tag = $seg1)
9538 |> xLine(length = width)
9539 |> yLine(length = -width)
9540 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
9541 |> close()
9542extrude001 = extrude(profile001, length = width)
9543
9544// Get a value that requires the engine.
9545x = segLen(seg1)
9546
9547// Triangle with side length 2*x.
9548sketch(on = XY) {
9549 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
9550 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
9551 coincident([line1.end, line2.start])
9552 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
9553 coincident([line2.end, line3.start])
9554 coincident([line3.end, line1.start])
9555 equalLength([line3, line1])
9556 equalLength([line1, line2])
9557 distance([line1.start, line1.end]) == 2 * x
9558}
9559
9560// Line segment with length x.
9561sketch2 = sketch(on = XY) {
9562 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
9563 distance([line1.start, line1.end]) == x
9564}
9565"
9566 );
9567
9568 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
9570 assert_eq!(
9572 src_delta.text.as_str(),
9573 "\
9574// Cube that requires the engine.
9575width = 2
9576sketch001 = startSketchOn(XY)
9577profile001 = startProfile(sketch001, at = [0, 0])
9578 |> yLine(length = width, tag = $seg1)
9579 |> xLine(length = width)
9580 |> yLine(length = -width)
9581 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
9582 |> close()
9583extrude001 = extrude(profile001, length = width)
9584
9585// Get a value that requires the engine.
9586x = segLen(seg1)
9587
9588// Triangle with side length 2*x.
9589sketch(on = XY) {
9590 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
9591 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
9592 coincident([line1.end, line2.start])
9593 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
9594 coincident([line2.end, line3.start])
9595 coincident([line3.end, line1.start])
9596 equalLength([line3, line1])
9597 equalLength([line1, line2])
9598 distance([line1.start, line1.end]) == 2 * x
9599}
9600
9601// Line segment with length x.
9602sketch2 = sketch(on = XY) {
9603 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
9604 distance([line1.start, line1.end]) == x
9605}
9606"
9607 );
9608 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
9616 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
9617
9618 let scene_delta = frontend
9626 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
9627 .await
9628 .unwrap();
9629 assert_eq!(
9630 scene_delta.new_graph.objects.len(),
9631 24,
9632 "{:#?}",
9633 scene_delta.new_graph.objects
9634 );
9635
9636 let point_ctor = PointCtor {
9638 position: Point2d {
9639 x: Expr::Var(Number {
9640 value: 3.0,
9641 units: NumericSuffix::Mm,
9642 }),
9643 y: Expr::Var(Number {
9644 value: 4.0,
9645 units: NumericSuffix::Mm,
9646 }),
9647 },
9648 };
9649 let segments = vec![ExistingSegmentCtor {
9650 id: point2_id,
9651 ctor: SegmentCtor::Point(point_ctor),
9652 }];
9653 let (src_delta, _) = frontend
9654 .edit_segments(&mock_ctx, version, sketch2_id, segments)
9655 .await
9656 .unwrap();
9657 assert_eq!(
9659 src_delta.text.as_str(),
9660 "\
9661// Cube that requires the engine.
9662width = 2
9663sketch001 = startSketchOn(XY)
9664profile001 = startProfile(sketch001, at = [0, 0])
9665 |> yLine(length = width, tag = $seg1)
9666 |> xLine(length = width)
9667 |> yLine(length = -width)
9668 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
9669 |> close()
9670extrude001 = extrude(profile001, length = width)
9671
9672// Get a value that requires the engine.
9673x = segLen(seg1)
9674
9675// Triangle with side length 2*x.
9676sketch(on = XY) {
9677 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
9678 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
9679 coincident([line1.end, line2.start])
9680 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
9681 coincident([line2.end, line3.start])
9682 coincident([line3.end, line1.start])
9683 equalLength([line3, line1])
9684 equalLength([line1, line2])
9685 distance([line1.start, line1.end]) == 2 * x
9686}
9687
9688// Line segment with length x.
9689sketch2 = sketch(on = XY) {
9690 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
9691 distance([line1.start, line1.end]) == x
9692}
9693"
9694 );
9695
9696 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
9698 assert_eq!(
9700 src_delta.text.as_str(),
9701 "\
9702// Cube that requires the engine.
9703width = 2
9704sketch001 = startSketchOn(XY)
9705profile001 = startProfile(sketch001, at = [0, 0])
9706 |> yLine(length = width, tag = $seg1)
9707 |> xLine(length = width)
9708 |> yLine(length = -width)
9709 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
9710 |> close()
9711extrude001 = extrude(profile001, length = width)
9712
9713// Get a value that requires the engine.
9714x = segLen(seg1)
9715
9716// Triangle with side length 2*x.
9717sketch(on = XY) {
9718 line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
9719 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
9720 coincident([line1.end, line2.start])
9721 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
9722 coincident([line2.end, line3.start])
9723 coincident([line3.end, line1.start])
9724 equalLength([line3, line1])
9725 equalLength([line1, line2])
9726 distance([line1.start, line1.end]) == 2 * x
9727}
9728
9729// Line segment with length x.
9730sketch2 = sketch(on = XY) {
9731 line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
9732 distance([line1.start, line1.end]) == x
9733}
9734"
9735 );
9736
9737 ctx.close().await;
9738 mock_ctx.close().await;
9739 }
9740
9741 #[tokio::test(flavor = "multi_thread")]
9746 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
9747 let initial_source = "@settings(defaultLengthUnit = mm)
9749
9750
9751
9752sketch001 = sketch(on = XY) {
9753 point(at = [1in, 2in])
9754}
9755";
9756
9757 let program = Program::parse(initial_source).unwrap().0.unwrap();
9758 let mut frontend = FrontendState::new();
9759
9760 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9761 let mock_ctx = ExecutorContext::new_mock(None).await;
9762 let version = Version(0);
9763 let project_id = ProjectId(0);
9764 let file_id = FileId(0);
9765
9766 frontend.hack_set_program(&ctx, program).await.unwrap();
9767 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9768 let sketch_id = sketch_object.id;
9769
9770 frontend
9772 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
9773 .await
9774 .unwrap();
9775
9776 let point_ctor = PointCtor {
9778 position: Point2d {
9779 x: Expr::Number(Number {
9780 value: 5.0,
9781 units: NumericSuffix::Mm,
9782 }),
9783 y: Expr::Number(Number {
9784 value: 6.0,
9785 units: NumericSuffix::Mm,
9786 }),
9787 },
9788 };
9789 let segment = SegmentCtor::Point(point_ctor);
9790 let (src_delta, scene_delta) = frontend
9791 .add_segment(&mock_ctx, version, sketch_id, segment, None)
9792 .await
9793 .unwrap();
9794 assert!(
9796 src_delta.text.contains("point(at = [5mm, 6mm])"),
9797 "Expected new point in source, got: {}",
9798 src_delta.text
9799 );
9800 assert!(!scene_delta.new_objects.is_empty());
9801
9802 ctx.close().await;
9803 mock_ctx.close().await;
9804 }
9805
9806 #[tokio::test(flavor = "multi_thread")]
9807 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
9808 let initial_source = "@settings(defaultLengthUnit = mm)
9810
9811
9812
9813s = sketch(on = XY) {}
9814";
9815
9816 let program = Program::parse(initial_source).unwrap().0.unwrap();
9817 let mut frontend = FrontendState::new();
9818
9819 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9820 let mock_ctx = ExecutorContext::new_mock(None).await;
9821 let version = Version(0);
9822
9823 frontend.hack_set_program(&ctx, program).await.unwrap();
9824 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9825 let sketch_id = sketch_object.id;
9826
9827 let line_ctor = LineCtor {
9828 start: Point2d {
9829 x: Expr::Number(Number {
9830 value: 0.0,
9831 units: NumericSuffix::Mm,
9832 }),
9833 y: Expr::Number(Number {
9834 value: 0.0,
9835 units: NumericSuffix::Mm,
9836 }),
9837 },
9838 end: Point2d {
9839 x: Expr::Number(Number {
9840 value: 10.0,
9841 units: NumericSuffix::Mm,
9842 }),
9843 y: Expr::Number(Number {
9844 value: 10.0,
9845 units: NumericSuffix::Mm,
9846 }),
9847 },
9848 construction: None,
9849 };
9850 let segment = SegmentCtor::Line(line_ctor);
9851 let (src_delta, scene_delta) = frontend
9852 .add_segment(&mock_ctx, version, sketch_id, segment, None)
9853 .await
9854 .unwrap();
9855 assert!(
9856 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
9857 "Expected line in source, got: {}",
9858 src_delta.text
9859 );
9860 assert_eq!(scene_delta.new_objects.len(), 3);
9862
9863 ctx.close().await;
9864 mock_ctx.close().await;
9865 }
9866
9867 #[tokio::test(flavor = "multi_thread")]
9868 async fn test_extra_newlines_between_operations_edit_line() {
9869 let initial_source = "@settings(defaultLengthUnit = mm)
9871
9872
9873sketch001 = sketch(on = XY) {
9874
9875 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
9876
9877}
9878";
9879
9880 let program = Program::parse(initial_source).unwrap().0.unwrap();
9881 let mut frontend = FrontendState::new();
9882
9883 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9884 let mock_ctx = ExecutorContext::new_mock(None).await;
9885 let version = Version(0);
9886 let project_id = ProjectId(0);
9887 let file_id = FileId(0);
9888
9889 frontend.hack_set_program(&ctx, program).await.unwrap();
9890 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9891 let sketch_id = sketch_object.id;
9892 let sketch = expect_sketch(sketch_object);
9893
9894 let line_id = sketch
9896 .segments
9897 .iter()
9898 .copied()
9899 .find(|seg_id| {
9900 matches!(
9901 &frontend.scene_graph.objects[seg_id.0].kind,
9902 ObjectKind::Segment {
9903 segment: Segment::Line(_)
9904 }
9905 )
9906 })
9907 .expect("Expected a line segment in sketch");
9908
9909 frontend
9911 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
9912 .await
9913 .unwrap();
9914
9915 let line_ctor = LineCtor {
9917 start: Point2d {
9918 x: Expr::Var(Number {
9919 value: 1.0,
9920 units: NumericSuffix::Mm,
9921 }),
9922 y: Expr::Var(Number {
9923 value: 2.0,
9924 units: NumericSuffix::Mm,
9925 }),
9926 },
9927 end: Point2d {
9928 x: Expr::Var(Number {
9929 value: 13.0,
9930 units: NumericSuffix::Mm,
9931 }),
9932 y: Expr::Var(Number {
9933 value: 14.0,
9934 units: NumericSuffix::Mm,
9935 }),
9936 },
9937 construction: None,
9938 };
9939 let segments = vec![ExistingSegmentCtor {
9940 id: line_id,
9941 ctor: SegmentCtor::Line(line_ctor),
9942 }];
9943 let (src_delta, _scene_delta) = frontend
9944 .edit_segments(&mock_ctx, version, sketch_id, segments)
9945 .await
9946 .unwrap();
9947 assert!(
9948 src_delta
9949 .text
9950 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
9951 "Expected edited line in source, got: {}",
9952 src_delta.text
9953 );
9954
9955 ctx.close().await;
9956 mock_ctx.close().await;
9957 }
9958
9959 #[tokio::test(flavor = "multi_thread")]
9960 async fn test_extra_newlines_delete_segment() {
9961 let initial_source = "@settings(defaultLengthUnit = mm)
9963
9964
9965
9966sketch001 = sketch(on = XY) {
9967 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
9968}
9969";
9970
9971 let program = Program::parse(initial_source).unwrap().0.unwrap();
9972 let mut frontend = FrontendState::new();
9973
9974 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9975 let mock_ctx = ExecutorContext::new_mock(None).await;
9976 let version = Version(0);
9977
9978 frontend.hack_set_program(&ctx, program).await.unwrap();
9979 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9980 let sketch_id = sketch_object.id;
9981 let sketch = expect_sketch(sketch_object);
9982
9983 assert_eq!(sketch.segments.len(), 3);
9985 let circle_id = sketch.segments[2];
9986
9987 let (src_delta, scene_delta) = frontend
9989 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
9990 .await
9991 .unwrap();
9992 assert!(
9993 src_delta.text.contains("sketch(on = XY) {"),
9994 "Expected sketch block in source, got: {}",
9995 src_delta.text
9996 );
9997 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9998 let new_sketch = expect_sketch(new_sketch_object);
9999 assert_eq!(new_sketch.segments.len(), 0);
10000
10001 ctx.close().await;
10002 mock_ctx.close().await;
10003 }
10004
10005 #[tokio::test(flavor = "multi_thread")]
10006 async fn test_unformatted_source_add_arc() {
10007 let initial_source = "@settings(defaultLengthUnit = mm)
10009
10010
10011
10012
10013sketch001 = sketch(on = XY) {
10014}
10015";
10016
10017 let program = Program::parse(initial_source).unwrap().0.unwrap();
10018 let mut frontend = FrontendState::new();
10019
10020 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10021 let mock_ctx = ExecutorContext::new_mock(None).await;
10022 let version = Version(0);
10023
10024 frontend.hack_set_program(&ctx, program).await.unwrap();
10025 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10026 let sketch_id = sketch_object.id;
10027
10028 let arc_ctor = ArcCtor {
10029 start: Point2d {
10030 x: Expr::Var(Number {
10031 value: 5.0,
10032 units: NumericSuffix::Mm,
10033 }),
10034 y: Expr::Var(Number {
10035 value: 0.0,
10036 units: NumericSuffix::Mm,
10037 }),
10038 },
10039 end: Point2d {
10040 x: Expr::Var(Number {
10041 value: 0.0,
10042 units: NumericSuffix::Mm,
10043 }),
10044 y: Expr::Var(Number {
10045 value: 5.0,
10046 units: NumericSuffix::Mm,
10047 }),
10048 },
10049 center: Point2d {
10050 x: Expr::Var(Number {
10051 value: 0.0,
10052 units: NumericSuffix::Mm,
10053 }),
10054 y: Expr::Var(Number {
10055 value: 0.0,
10056 units: NumericSuffix::Mm,
10057 }),
10058 },
10059 construction: None,
10060 };
10061 let segment = SegmentCtor::Arc(arc_ctor);
10062 let (src_delta, scene_delta) = frontend
10063 .add_segment(&mock_ctx, version, sketch_id, segment, None)
10064 .await
10065 .unwrap();
10066 assert!(
10067 src_delta
10068 .text
10069 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
10070 "Expected arc in source, got: {}",
10071 src_delta.text
10072 );
10073 assert!(!scene_delta.new_objects.is_empty());
10074
10075 ctx.close().await;
10076 mock_ctx.close().await;
10077 }
10078
10079 #[tokio::test(flavor = "multi_thread")]
10080 async fn test_extra_newlines_add_circle() {
10081 let initial_source = "@settings(defaultLengthUnit = mm)
10083
10084
10085
10086sketch001 = sketch(on = XY) {
10087}
10088";
10089
10090 let program = Program::parse(initial_source).unwrap().0.unwrap();
10091 let mut frontend = FrontendState::new();
10092
10093 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10094 let mock_ctx = ExecutorContext::new_mock(None).await;
10095 let version = Version(0);
10096
10097 frontend.hack_set_program(&ctx, program).await.unwrap();
10098 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10099 let sketch_id = sketch_object.id;
10100
10101 let circle_ctor = CircleCtor {
10102 start: Point2d {
10103 x: Expr::Var(Number {
10104 value: 5.0,
10105 units: NumericSuffix::Mm,
10106 }),
10107 y: Expr::Var(Number {
10108 value: 0.0,
10109 units: NumericSuffix::Mm,
10110 }),
10111 },
10112 center: Point2d {
10113 x: Expr::Var(Number {
10114 value: 0.0,
10115 units: NumericSuffix::Mm,
10116 }),
10117 y: Expr::Var(Number {
10118 value: 0.0,
10119 units: NumericSuffix::Mm,
10120 }),
10121 },
10122 construction: None,
10123 };
10124 let segment = SegmentCtor::Circle(circle_ctor);
10125 let (src_delta, scene_delta) = frontend
10126 .add_segment(&mock_ctx, version, sketch_id, segment, None)
10127 .await
10128 .unwrap();
10129 assert!(
10130 src_delta
10131 .text
10132 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
10133 "Expected circle in source, got: {}",
10134 src_delta.text
10135 );
10136 assert!(!scene_delta.new_objects.is_empty());
10137
10138 ctx.close().await;
10139 mock_ctx.close().await;
10140 }
10141
10142 #[tokio::test(flavor = "multi_thread")]
10143 async fn test_extra_newlines_add_constraint() {
10144 let initial_source = "@settings(defaultLengthUnit = mm)
10146
10147
10148
10149sketch001 = sketch(on = XY) {
10150 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
10151 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
10152}
10153";
10154
10155 let program = Program::parse(initial_source).unwrap().0.unwrap();
10156 let mut frontend = FrontendState::new();
10157
10158 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10159 let mock_ctx = ExecutorContext::new_mock(None).await;
10160 let version = Version(0);
10161 let project_id = ProjectId(0);
10162 let file_id = FileId(0);
10163
10164 frontend.hack_set_program(&ctx, program).await.unwrap();
10165 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10166 let sketch_id = sketch_object.id;
10167 let sketch = expect_sketch(sketch_object);
10168
10169 let line_ids: Vec<ObjectId> = sketch
10171 .segments
10172 .iter()
10173 .copied()
10174 .filter(|seg_id| {
10175 matches!(
10176 &frontend.scene_graph.objects[seg_id.0].kind,
10177 ObjectKind::Segment {
10178 segment: Segment::Line(_)
10179 }
10180 )
10181 })
10182 .collect();
10183 assert_eq!(line_ids.len(), 2, "Expected two line segments");
10184
10185 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
10186 let ObjectKind::Segment {
10187 segment: Segment::Line(line1_data),
10188 } = &line1.kind
10189 else {
10190 panic!("Expected line");
10191 };
10192 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
10193 let ObjectKind::Segment {
10194 segment: Segment::Line(line2_data),
10195 } = &line2.kind
10196 else {
10197 panic!("Expected line");
10198 };
10199
10200 let constraint = Constraint::Coincident(Coincident {
10202 segments: vec![line1_data.end.into(), line2_data.start.into()],
10203 });
10204
10205 frontend
10207 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
10208 .await
10209 .unwrap();
10210 let (src_delta, _scene_delta) = frontend
10211 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10212 .await
10213 .unwrap();
10214 assert!(
10215 src_delta.text.contains("coincident("),
10216 "Expected coincident constraint in source, got: {}",
10217 src_delta.text
10218 );
10219
10220 ctx.close().await;
10221 mock_ctx.close().await;
10222 }
10223
10224 #[tokio::test(flavor = "multi_thread")]
10225 async fn test_extra_newlines_add_line_then_edit_line() {
10226 let initial_source = "@settings(defaultLengthUnit = mm)
10228
10229
10230
10231sketch001 = sketch(on = XY) {
10232}
10233";
10234
10235 let program = Program::parse(initial_source).unwrap().0.unwrap();
10236 let mut frontend = FrontendState::new();
10237
10238 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10239 let mock_ctx = ExecutorContext::new_mock(None).await;
10240 let version = Version(0);
10241
10242 frontend.hack_set_program(&ctx, program).await.unwrap();
10243 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10244 let sketch_id = sketch_object.id;
10245
10246 let line_ctor = LineCtor {
10248 start: Point2d {
10249 x: Expr::Number(Number {
10250 value: 0.0,
10251 units: NumericSuffix::Mm,
10252 }),
10253 y: Expr::Number(Number {
10254 value: 0.0,
10255 units: NumericSuffix::Mm,
10256 }),
10257 },
10258 end: Point2d {
10259 x: Expr::Number(Number {
10260 value: 10.0,
10261 units: NumericSuffix::Mm,
10262 }),
10263 y: Expr::Number(Number {
10264 value: 10.0,
10265 units: NumericSuffix::Mm,
10266 }),
10267 },
10268 construction: None,
10269 };
10270 let segment = SegmentCtor::Line(line_ctor);
10271 let (src_delta, scene_delta) = frontend
10272 .add_segment(&mock_ctx, version, sketch_id, segment, None)
10273 .await
10274 .unwrap();
10275 assert!(
10276 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
10277 "Expected line in source after add, got: {}",
10278 src_delta.text
10279 );
10280 let line_id = *scene_delta.new_objects.last().unwrap();
10282
10283 let line_ctor = LineCtor {
10285 start: Point2d {
10286 x: Expr::Number(Number {
10287 value: 1.0,
10288 units: NumericSuffix::Mm,
10289 }),
10290 y: Expr::Number(Number {
10291 value: 2.0,
10292 units: NumericSuffix::Mm,
10293 }),
10294 },
10295 end: Point2d {
10296 x: Expr::Number(Number {
10297 value: 13.0,
10298 units: NumericSuffix::Mm,
10299 }),
10300 y: Expr::Number(Number {
10301 value: 14.0,
10302 units: NumericSuffix::Mm,
10303 }),
10304 },
10305 construction: None,
10306 };
10307 let segments = vec![ExistingSegmentCtor {
10308 id: line_id,
10309 ctor: SegmentCtor::Line(line_ctor),
10310 }];
10311 let (src_delta, scene_delta) = frontend
10312 .edit_segments(&mock_ctx, version, sketch_id, segments)
10313 .await
10314 .unwrap();
10315 assert!(
10316 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
10317 "Expected edited line in source, got: {}",
10318 src_delta.text
10319 );
10320 assert_eq!(scene_delta.new_objects, vec![]);
10321
10322 ctx.close().await;
10323 mock_ctx.close().await;
10324 }
10325}