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