1use std::cell::Cell;
2#[cfg(feature = "artifact-graph")]
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::collections::HashSet;
6use std::collections::VecDeque;
7use std::ops::ControlFlow;
8
9use indexmap::IndexMap;
10use kcl_error::CompilationIssue;
11use kcl_error::SourceRange;
12use kittycad_modeling_cmds::units::UnitLength;
13use serde::Serialize;
14
15use crate::ExecOutcome;
16use crate::ExecutorContext;
17use crate::KclError;
18use crate::KclErrorWithOutputs;
19use crate::Program;
20use crate::collections::AhashIndexSet;
21use crate::execution::Artifact;
22use crate::execution::ArtifactGraph;
23use crate::execution::CapSubType;
24use crate::execution::MockConfig;
25use crate::execution::SKETCH_BLOCK_PARAM_ON;
26use crate::execution::cache::SketchModeState;
27use crate::execution::cache::clear_mem_cache;
28use crate::execution::cache::read_old_memory;
29use crate::execution::cache::write_old_memory;
30use crate::fmt::format_number_literal;
31use crate::front::Angle;
32use crate::front::ArcCtor;
33use crate::front::CircleCtor;
34use crate::front::Distance;
35use crate::front::EqualRadius;
36use crate::front::Error;
37use crate::front::ExecResult;
38use crate::front::FixedPoint;
39use crate::front::Freedom;
40use crate::front::LinesEqualLength;
41use crate::front::Midpoint;
42use crate::front::Object;
43use crate::front::Parallel;
44use crate::front::Perpendicular;
45use crate::front::PointCtor;
46use crate::front::Symmetric;
47use crate::front::Tangent;
48use crate::frontend::api::Expr;
49use crate::frontend::api::FileId;
50use crate::frontend::api::Number;
51use crate::frontend::api::ObjectId;
52use crate::frontend::api::ObjectKind;
53use crate::frontend::api::Plane;
54use crate::frontend::api::ProjectId;
55use crate::frontend::api::RestoreSketchCheckpointOutcome;
56use crate::frontend::api::SceneGraph;
57use crate::frontend::api::SceneGraphDelta;
58use crate::frontend::api::SketchCheckpointId;
59use crate::frontend::api::SourceDelta;
60use crate::frontend::api::SourceRef;
61use crate::frontend::api::Version;
62use crate::frontend::modify::find_defined_names;
63use crate::frontend::modify::next_free_name;
64use crate::frontend::modify::next_free_name_with_padding;
65use crate::frontend::sketch::Coincident;
66use crate::frontend::sketch::Constraint;
67use crate::frontend::sketch::ConstraintSegment;
68use crate::frontend::sketch::Diameter;
69use crate::frontend::sketch::ExistingSegmentCtor;
70use crate::frontend::sketch::Horizontal;
71use crate::frontend::sketch::LineCtor;
72use crate::frontend::sketch::Point2d;
73use crate::frontend::sketch::Radius;
74use crate::frontend::sketch::Segment;
75use crate::frontend::sketch::SegmentCtor;
76use crate::frontend::sketch::SketchApi;
77use crate::frontend::sketch::SketchCtor;
78use crate::frontend::sketch::Vertical;
79use crate::frontend::traverse::MutateBodyItem;
80use crate::frontend::traverse::TraversalReturn;
81use crate::frontend::traverse::Visitor;
82use crate::frontend::traverse::dfs_mut;
83use crate::id::IncIdGenerator;
84use crate::parsing::ast::types as ast;
85use crate::pretty::NumericSuffix;
86use crate::std::constraints::LinesAtAngleKind;
87#[cfg(feature = "artifact-graph")]
88use crate::walk::Node;
89use crate::walk::NodeMut;
90use crate::walk::Visitable;
91
92pub(crate) mod api;
93pub(crate) mod modify;
94pub(crate) mod sketch;
95
96pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
97
98#[derive(Debug, Clone)]
99struct SketchCheckpoint {
100 id: SketchCheckpointId,
101 source: SourceDelta,
102 program: Program,
103 scene_graph: SceneGraph,
104 exec_outcome: ExecOutcome,
105 point_freedom_cache: HashMap<ObjectId, Freedom>,
106 mock_memory: Option<SketchModeState>,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110enum SketchVarUpdateMode {
111 WarmStartOnly,
114 CommitSolvedVars,
117 CommitSolvedVarsFromWarmStart,
120}
121mod traverse;
122pub(crate) mod trim;
123
124struct ArcSizeConstraintParams {
125 points: Vec<ObjectId>,
126 function_name: &'static str,
127 value: f64,
128 units: NumericSuffix,
129 label_position: Option<Point2d<Number>>,
130 constraint_type_name: &'static str,
131}
132
133const POINT_FN: &str = "point";
134const POINT_AT_PARAM: &str = "at";
135const LINE_FN: &str = "line";
136const LINE_VARIABLE: &str = "line";
137const LINE_START_PARAM: &str = "start";
138const LINE_END_PARAM: &str = "end";
139const ARC_FN: &str = "arc";
140const ARC_VARIABLE: &str = "arc";
141const ARC_START_PARAM: &str = "start";
142const ARC_END_PARAM: &str = "end";
143const ARC_CENTER_PARAM: &str = "center";
144const CIRCLE_FN: &str = "circle";
145const CIRCLE_VARIABLE: &str = "circle";
146const CIRCLE_START_PARAM: &str = "start";
147const CIRCLE_CENTER_PARAM: &str = "center";
148const LABEL_POSITION_PARAM: &str = "labelPosition";
149
150const COINCIDENT_FN: &str = "coincident";
151const DIAMETER_FN: &str = "diameter";
152const DISTANCE_FN: &str = "distance";
153const FIXED_FN: &str = "fixed";
154const ANGLE_FN: &str = "angle";
155const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
156const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
157const EQUAL_LENGTH_FN: &str = "equalLength";
158const EQUAL_RADIUS_FN: &str = "equalRadius";
159const HORIZONTAL_FN: &str = "horizontal";
160const MIDPOINT_FN: &str = "midpoint";
161const MIDPOINT_POINT_PARAM: &str = "point";
162const RADIUS_FN: &str = "radius";
163const SYMMETRIC_FN: &str = "symmetric";
164const SYMMETRIC_AXIS_PARAM: &str = "axis";
165const TANGENT_FN: &str = "tangent";
166const VERTICAL_FN: &str = "vertical";
167
168const LINE_PROPERTY_START: &str = "start";
169const LINE_PROPERTY_END: &str = "end";
170
171const ARC_PROPERTY_START: &str = "start";
172const ARC_PROPERTY_END: &str = "end";
173const ARC_PROPERTY_CENTER: &str = "center";
174const CIRCLE_PROPERTY_START: &str = "start";
175const CIRCLE_PROPERTY_CENTER: &str = "center";
176
177const CONSTRUCTION_PARAM: &str = "construction";
178
179#[derive(Debug, Clone, Copy)]
180enum EditDeleteKind {
181 Edit,
182 DeleteNonSketch,
183}
184
185impl EditDeleteKind {
186 fn is_delete(&self) -> bool {
188 match self {
189 EditDeleteKind::Edit => false,
190 EditDeleteKind::DeleteNonSketch => true,
191 }
192 }
193
194 fn to_change_kind(self) -> ChangeKind {
195 match self {
196 EditDeleteKind::Edit => ChangeKind::Edit,
197 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
198 }
199 }
200}
201
202struct ExecuteAfterEditOptions {
203 segment_ids_edited: AhashIndexSet<ObjectId>,
204 edit_kind: EditDeleteKind,
205 sketch_var_update_mode: SketchVarUpdateMode,
206}
207
208#[derive(Debug, Clone, Copy)]
209enum ChangeKind {
210 Add,
211 Edit,
212 Delete,
213 None,
214}
215
216#[derive(Debug, Clone, Serialize, ts_rs::TS)]
217#[ts(export, export_to = "FrontendApi.ts")]
218#[serde(tag = "type")]
219pub enum SetProgramOutcome {
220 #[serde(rename_all = "camelCase")]
221 Success {
222 scene_graph: Box<SceneGraph>,
223 exec_outcome: Box<ExecOutcome>,
224 checkpoint_id: Option<SketchCheckpointId>,
225 },
226 #[serde(rename_all = "camelCase")]
227 ExecFailure { error: Box<KclErrorWithOutputs> },
228}
229
230#[derive(Debug, Clone)]
231pub struct FrontendState {
232 program: Program,
233 scene_graph: SceneGraph,
234 point_freedom_cache: HashMap<ObjectId, Freedom>,
237 sketch_var_warm_start_overrides: HashMap<ObjectId, Vec<f64>>,
241 next_sketch_var_update_mode: Option<SketchVarUpdateMode>,
242 sketch_checkpoints: VecDeque<SketchCheckpoint>,
243 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
244}
245
246impl Default for FrontendState {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252impl FrontendState {
253 pub fn new() -> Self {
254 Self {
255 program: Program::empty(),
256 scene_graph: SceneGraph {
257 project: ProjectId(0),
258 file: FileId(0),
259 version: Version(0),
260 objects: Default::default(),
261 settings: Default::default(),
262 sketch_mode: Default::default(),
263 },
264 point_freedom_cache: HashMap::new(),
265 sketch_var_warm_start_overrides: HashMap::new(),
266 next_sketch_var_update_mode: None,
267 sketch_checkpoints: VecDeque::new(),
268 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
269 }
270 }
271
272 pub fn scene_graph(&self) -> &SceneGraph {
274 &self.scene_graph
275 }
276
277 pub fn default_length_unit(&self) -> UnitLength {
278 self.program
279 .meta_settings()
280 .ok()
281 .flatten()
282 .map(|settings| settings.default_length_units)
283 .unwrap_or(UnitLength::Millimeters)
284 }
285
286 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
287 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
288
289 let checkpoint = SketchCheckpoint {
290 id: checkpoint_id,
291 source: SourceDelta {
292 text: source_from_ast(&self.program.ast),
293 },
294 program: self.program.clone(),
295 scene_graph: self.scene_graph.clone(),
296 exec_outcome,
297 point_freedom_cache: self.point_freedom_cache.clone(),
298 mock_memory: read_old_memory().await,
299 };
300
301 self.sketch_checkpoints.push_back(checkpoint);
302 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
303 self.sketch_checkpoints.pop_front();
304 }
305
306 Ok(checkpoint_id)
307 }
308
309 pub async fn restore_sketch_checkpoint(
310 &mut self,
311 checkpoint_id: SketchCheckpointId,
312 ) -> api::Result<RestoreSketchCheckpointOutcome> {
313 let checkpoint = self
314 .sketch_checkpoints
315 .iter()
316 .find(|checkpoint| checkpoint.id == checkpoint_id)
317 .cloned()
318 .ok_or_else(|| Error {
319 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
320 })?;
321
322 self.program = checkpoint.program;
323 self.scene_graph = checkpoint.scene_graph.clone();
324 self.point_freedom_cache = checkpoint.point_freedom_cache;
325 self.clear_sketch_var_warm_starts();
326
327 if let Some(mock_memory) = checkpoint.mock_memory {
328 write_old_memory(mock_memory).await;
329 } else {
330 clear_mem_cache().await;
331 }
332
333 Ok(RestoreSketchCheckpointOutcome {
334 source_delta: checkpoint.source,
335 scene_graph_delta: SceneGraphDelta {
336 new_graph: checkpoint.scene_graph,
337 new_objects: Vec::new(),
338 invalidates_ids: true,
339 exec_outcome: checkpoint.exec_outcome,
340 },
341 })
342 }
343
344 pub fn clear_sketch_checkpoints(&mut self) {
345 self.sketch_checkpoints.clear();
346 }
347
348 pub(crate) fn clear_sketch_var_warm_starts(&mut self) {
349 self.sketch_var_warm_start_overrides.clear();
350 }
351
352 pub async fn edit_segments_for_preview(
353 &mut self,
354 ctx: &ExecutorContext,
355 version: Version,
356 sketch: ObjectId,
357 segments: Vec<ExistingSegmentCtor>,
358 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
359 self.next_sketch_var_update_mode = Some(SketchVarUpdateMode::WarmStartOnly);
360 SketchApi::edit_segments(self, ctx, version, sketch, segments).await
361 }
362
363 pub async fn edit_segments_commit_from_preview(
364 &mut self,
365 ctx: &ExecutorContext,
366 version: Version,
367 sketch: ObjectId,
368 segments: Vec<ExistingSegmentCtor>,
369 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
370 self.next_sketch_var_update_mode = Some(SketchVarUpdateMode::CommitSolvedVarsFromWarmStart);
371 SketchApi::edit_segments(self, ctx, version, sketch, segments).await
372 }
373
374 pub async fn execute_mock_from_preview(
375 &mut self,
376 ctx: &ExecutorContext,
377 version: Version,
378 sketch: ObjectId,
379 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
380 let _ = version;
381 self.execute_mock_with_warm_starts(ctx, sketch, true).await
382 }
383
384 fn sketch_mock_config(
385 &self,
386 sketch: ObjectId,
387 freedom_analysis: bool,
388 #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] segment_ids_edited: AhashIndexSet<
389 ObjectId,
390 >,
391 use_warm_starts: bool,
392 ) -> MockConfig {
393 let config = MockConfig {
394 sketch_block_id: Some(sketch),
395 freedom_analysis,
396 #[cfg(feature = "artifact-graph")]
397 segment_ids_edited,
398 ..Default::default()
399 };
400
401 match (use_warm_starts, self.sketch_var_warm_start_overrides.get(&sketch)) {
402 (true, Some(overrides)) => config.with_sketch_var_initial_guess_overrides(overrides.clone()),
403 _ => config,
404 }
405 }
406
407 async fn execute_mock_with_warm_starts(
408 &mut self,
409 ctx: &ExecutorContext,
410 sketch: ObjectId,
411 use_warm_starts: bool,
412 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
413 let sketch_block_ref =
414 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
415
416 let mut truncated_program = self.program.clone();
417 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
418 .map_err(KclErrorWithOutputs::no_outputs)?;
419
420 let mock_config = self.sketch_mock_config(sketch, true, Default::default(), use_warm_starts);
421 let run_outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
422 let outcome = self.update_state_after_exec(run_outcome, true);
423 self.replace_sketch_var_warm_starts(sketch, &outcome);
424 let new_source = self.commit_var_solutions_to_program(&outcome)?;
425
426 let src_delta = SourceDelta { text: new_source };
427 let scene_graph_delta = SceneGraphDelta {
428 new_graph: self.scene_graph.clone(),
429 new_objects: Default::default(),
430 invalidates_ids: false,
431 exec_outcome: outcome,
432 };
433 Ok((src_delta, scene_graph_delta))
434 }
435
436 fn replace_sketch_var_warm_starts(&mut self, sketch: ObjectId, outcome: &ExecOutcome) {
437 #[cfg(feature = "artifact-graph")]
438 {
439 let solution_by_range = outcome
440 .var_solutions
441 .iter()
442 .map(|(range, value)| (*range, value.value))
443 .collect::<HashMap<_, _>>();
444 let values = self
445 .sketch_var_initial_guesses_from_program(sketch, &solution_by_range)
446 .unwrap_or_else(|| outcome.var_solutions.iter().map(|(_, value)| value.value).collect());
447 self.sketch_var_warm_start_overrides.insert(sketch, values);
448 }
449 #[cfg(not(feature = "artifact-graph"))]
450 {
451 let _ = (sketch, outcome);
452 }
453 }
454
455 fn merge_committed_edit_sketch_var_warm_starts(
456 &mut self,
457 sketch: ObjectId,
458 outcome: &ExecOutcome,
459 segment_ids_edited: &AhashIndexSet<ObjectId>,
460 ) {
461 #[cfg(feature = "artifact-graph")]
462 {
463 let solution_by_range = outcome
464 .var_solutions
465 .iter()
466 .map(|(range, value)| (*range, value.value))
467 .collect::<HashMap<_, _>>();
468 let previous = self.sketch_var_warm_start_overrides.get(&sketch).cloned();
469 let Some(source_values) = self.sketch_var_source_values_from_program(sketch) else {
470 self.replace_sketch_var_warm_starts(sketch, outcome);
471 return;
472 };
473 let edited_var_indices = self.edited_sketch_var_indices(&source_values, segment_ids_edited);
474 let values = source_values
475 .into_iter()
476 .enumerate()
477 .map(|(index, (range, source_value))| {
478 if edited_var_indices.contains(&index) || !solution_by_range.contains_key(&range) {
479 source_value
480 } else {
481 previous
482 .as_ref()
483 .and_then(|values| values.get(index))
484 .copied()
485 .unwrap_or(source_value)
486 }
487 })
488 .collect();
489 self.sketch_var_warm_start_overrides.insert(sketch, values);
490 }
491 #[cfg(not(feature = "artifact-graph"))]
492 {
493 let _ = (sketch, outcome, segment_ids_edited);
494 }
495 }
496
497 #[cfg(feature = "artifact-graph")]
498 fn edited_sketch_var_indices(
499 &self,
500 source_values: &[(SourceRange, f64)],
501 segment_ids_edited: &AhashIndexSet<ObjectId>,
502 ) -> HashSet<usize> {
503 let edited_ranges = segment_ids_edited
504 .iter()
505 .filter_map(|segment_id| self.scene_graph.objects.get(segment_id.0))
506 .filter_map(|object| source_ref_primary_range(&object.source))
507 .collect::<Vec<_>>();
508
509 source_values
510 .iter()
511 .enumerate()
512 .filter_map(|(index, (range, _))| {
513 edited_ranges
514 .iter()
515 .any(|edited_range| edited_range.contains_range(range))
516 .then_some(index)
517 })
518 .collect()
519 }
520
521 #[cfg(feature = "artifact-graph")]
522 fn sketch_var_initial_guesses_from_program(
523 &self,
524 sketch: ObjectId,
525 solution_by_range: &HashMap<SourceRange, f64>,
526 ) -> Option<Vec<f64>> {
527 self.sketch_var_source_values_from_program(sketch).map(|source_values| {
528 source_values
529 .into_iter()
530 .map(|(range, source_value)| solution_by_range.get(&range).copied().unwrap_or(source_value))
531 .collect()
532 })
533 }
534
535 #[cfg(feature = "artifact-graph")]
536 fn sketch_var_source_values_from_program(&self, sketch: ObjectId) -> Option<Vec<(SourceRange, f64)>> {
537 let sketch_range = source_ref_primary_range(&self.scene_graph.objects.get(sketch.0)?.source)?;
538 let values = RefCell::new(Vec::new());
539 crate::walk::walk(&self.program.ast, |node| -> anyhow::Result<bool> {
540 let Node::SketchVar(sketch_var) = node else {
541 return Ok(true);
542 };
543 let Some(initial) = sketch_var.initial.as_ref() else {
544 return Ok(true);
545 };
546 let range = initial.as_source_range();
547 if sketch_range.contains_range(&range) {
548 values
549 .borrow_mut()
550 .push((range, sketch_var_initial_value_in_solver_units(initial)));
551 }
552 Ok(true)
553 })
554 .ok()?;
555 let values = values.into_inner();
556 (!values.is_empty()).then_some(values)
557 }
558
559 fn update_single_sketch_var_warm_starts(&mut self, outcome: &ExecOutcome) {
560 let mut sketch_ids = self.scene_graph.objects.iter().filter_map(|object| match object.kind {
561 ObjectKind::Sketch(_) => Some(object.id),
562 _ => None,
563 });
564 let Some(sketch_id) = sketch_ids.next() else {
565 return;
566 };
567 if sketch_ids.next().is_none() {
568 self.replace_sketch_var_warm_starts(sketch_id, outcome);
569 }
570 }
571
572 #[cfg(feature = "artifact-graph")]
573 fn source_with_committed_var_solutions(
574 &self,
575 outcome: &ExecOutcome,
576 ) -> ExecResult<(ast::Node<ast::Program>, String)> {
577 let mut new_ast = self.program.ast.clone();
578 for (var_range, value) in &outcome.var_solutions {
579 let rounded = value.round(3);
580 let source_ref = SourceRef::Simple {
581 range: *var_range,
582 node_path: None,
583 };
584 mutate_ast_node_by_source_ref(
585 &mut new_ast,
586 &source_ref,
587 AstMutateCommand::EditVarInitialValue { value: rounded },
588 )
589 .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
590 }
591 let source = source_from_ast(&new_ast);
592 Ok((new_ast, source))
593 }
594
595 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome) -> ExecResult<String> {
596 #[cfg(feature = "artifact-graph")]
597 {
598 let (new_ast, source) = self.source_with_committed_var_solutions(outcome)?;
599 self.program = Program {
600 ast: new_ast,
601 original_file_contents: source.clone(),
602 };
603 Ok(source)
604 }
605 #[cfg(not(feature = "artifact-graph"))]
606 {
607 let _ = outcome;
608 Ok(source_from_ast(&self.program.ast))
609 }
610 }
611}
612
613impl SketchApi for FrontendState {
614 async fn execute_mock(
615 &mut self,
616 ctx: &ExecutorContext,
617 _version: Version,
618 sketch: ObjectId,
619 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
620 self.execute_mock_with_warm_starts(ctx, sketch, false).await
621 }
622
623 async fn new_sketch(
624 &mut self,
625 ctx: &ExecutorContext,
626 _project: ProjectId,
627 _file: FileId,
628 _version: Version,
629 args: SketchCtor,
630 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
631 let mut new_ast = self.program.ast.clone();
634 let mut plane_ast =
636 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
637 let mut defined_names = find_defined_names(&new_ast);
638 let is_face_of_expr = matches!(
639 &plane_ast,
640 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
641 );
642 if is_face_of_expr {
643 let face_name = next_free_name_with_padding("face", &defined_names)
644 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
645 let face_decl = ast::VariableDeclaration::new(
646 ast::VariableDeclarator::new(&face_name, plane_ast),
647 ast::ItemVisibility::Default,
648 ast::VariableKind::Const,
649 );
650 new_ast
651 .body
652 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
653 face_decl,
654 ))));
655 defined_names.insert(face_name.clone());
656 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
657 }
658 let sketch_ast = ast::SketchBlock {
659 arguments: vec![ast::LabeledArg {
660 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
661 arg: plane_ast,
662 }],
663 body: Default::default(),
664 is_being_edited: false,
665 non_code_meta: Default::default(),
666 digest: None,
667 };
668 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
671 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
672 let sketch_decl = ast::VariableDeclaration::new(
673 ast::VariableDeclarator::new(
674 &sketch_name,
675 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
676 ),
677 ast::ItemVisibility::Default,
678 ast::VariableKind::Const,
679 );
680 new_ast
681 .body
682 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
683 sketch_decl,
684 ))));
685 let new_source = source_from_ast(&new_ast);
687 let (new_program, errors) = Program::parse(&new_source)
689 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
690 if !errors.is_empty() {
691 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
692 "Error parsing KCL source after adding sketch: {errors:?}"
693 ))));
694 }
695 let Some(new_program) = new_program else {
696 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
697 "No AST produced after adding sketch".to_owned(),
698 )));
699 };
700
701 self.program = new_program.clone();
703 self.clear_sketch_var_warm_starts();
704
705 let outcome = ctx.run_with_caching(new_program.clone()).await?;
708 let freedom_analysis_ran = true;
709
710 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
711
712 let Some(sketch_id) = self
713 .scene_graph
714 .objects
715 .iter()
716 .filter_map(|object| match object.kind {
717 ObjectKind::Sketch(_) => Some(object.id),
718 _ => None,
719 })
720 .max_by_key(|id| id.0)
721 else {
722 return Err(KclErrorWithOutputs::from_error_outcome(
723 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
724 outcome,
725 ));
726 };
727 self.scene_graph.sketch_mode = Some(sketch_id);
729
730 let src_delta = SourceDelta { text: new_source };
731 let scene_graph_delta = SceneGraphDelta {
732 new_graph: self.scene_graph.clone(),
733 invalidates_ids: false,
734 new_objects: vec![sketch_id],
735 exec_outcome: outcome,
736 };
737 Ok((src_delta, scene_graph_delta, sketch_id))
738 }
739
740 async fn edit_sketch(
741 &mut self,
742 ctx: &ExecutorContext,
743 _project: ProjectId,
744 _file: FileId,
745 _version: Version,
746 sketch: ObjectId,
747 ) -> ExecResult<SceneGraphDelta> {
748 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
752 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
753 })?;
754 let ObjectKind::Sketch(_) = &sketch_object.kind else {
755 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
756 "Object is not a sketch, it is {}",
757 sketch_object.kind.human_friendly_kind_with_article()
758 ))));
759 };
760 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
761
762 self.scene_graph.sketch_mode = Some(sketch);
764
765 let mut truncated_program = self.program.clone();
767 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
768 .map_err(KclErrorWithOutputs::no_outputs)?;
769
770 let outcome = ctx
773 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
774 .await?;
775
776 let outcome = self.update_state_after_exec(outcome, true);
778 self.replace_sketch_var_warm_starts(sketch, &outcome);
779 let scene_graph_delta = SceneGraphDelta {
780 new_graph: self.scene_graph.clone(),
781 invalidates_ids: false,
782 new_objects: Vec::new(),
783 exec_outcome: outcome,
784 };
785 Ok(scene_graph_delta)
786 }
787
788 async fn exit_sketch(
789 &mut self,
790 ctx: &ExecutorContext,
791 _version: Version,
792 sketch: ObjectId,
793 ) -> ExecResult<SceneGraph> {
794 #[cfg(not(target_arch = "wasm32"))]
796 let _ = sketch;
797 #[cfg(target_arch = "wasm32")]
798 if self.scene_graph.sketch_mode != Some(sketch) {
799 web_sys::console::warn_1(
800 &format!(
801 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
802 &self.scene_graph.sketch_mode
803 )
804 .into(),
805 );
806 }
807 self.scene_graph.sketch_mode = None;
808
809 let outcome = ctx.run_with_caching(self.program.clone()).await?;
811
812 self.update_state_after_exec(outcome, false);
814
815 Ok(self.scene_graph.clone())
816 }
817
818 async fn delete_sketch(
819 &mut self,
820 ctx: &ExecutorContext,
821 _version: Version,
822 sketch: ObjectId,
823 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
824 let mut new_ast = self.program.ast.clone();
827
828 let sketch_id = sketch;
830 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
831 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
832 })?;
833 let ObjectKind::Sketch(_) = &sketch_object.kind else {
834 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
835 "Object is not a sketch, it is {}",
836 sketch_object.kind.human_friendly_kind_with_article(),
837 ))));
838 };
839
840 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
842 .map_err(KclErrorWithOutputs::no_outputs)?;
843
844 self.execute_after_delete_sketch(ctx, &mut new_ast).await
845 }
846
847 async fn add_segment(
848 &mut self,
849 ctx: &ExecutorContext,
850 _version: Version,
851 sketch: ObjectId,
852 segment: SegmentCtor,
853 _label: Option<String>,
854 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
855 match segment {
857 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
858 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
859 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
860 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
861 }
862 }
863
864 async fn edit_segments(
865 &mut self,
866 ctx: &ExecutorContext,
867 _version: Version,
868 sketch: ObjectId,
869 segments: Vec<ExistingSegmentCtor>,
870 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
871 let sketch_var_update_mode = self
873 .next_sketch_var_update_mode
874 .take()
875 .unwrap_or(SketchVarUpdateMode::CommitSolvedVars);
876 let sketch_block_ref =
877 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
878
879 let mut new_ast = self.program.ast.clone();
880 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
881
882 for segment in &segments {
885 segment_ids_edited.insert(segment.id);
886 }
887
888 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
903
904 for segment in segments {
905 let segment_id = segment.id;
906 match segment.ctor {
907 SegmentCtor::Point(ctor) => {
908 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
910 && let ObjectKind::Segment { segment } = &segment_object.kind
911 && let Segment::Point(point) = segment
912 && let Some(owner_id) = point.owner
913 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
914 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
915 {
916 match owner_segment {
917 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
918 if let Some(existing) = final_edits.get_mut(&owner_id) {
919 let SegmentCtor::Line(line_ctor) = existing else {
920 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
921 "Internal: Expected line ctor for owner, but found {}",
922 existing.human_friendly_kind_with_article()
923 ))));
924 };
925 if line.start == segment_id {
927 line_ctor.start = ctor.position;
928 } else {
929 line_ctor.end = ctor.position;
930 }
931 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
932 let mut line_ctor = line_ctor.clone();
934 if line.start == segment_id {
935 line_ctor.start = ctor.position;
936 } else {
937 line_ctor.end = ctor.position;
938 }
939 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
940 } else {
941 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
943 "Internal: Line does not have line ctor, but found {}",
944 line.ctor.human_friendly_kind_with_article()
945 ))));
946 }
947 continue;
948 }
949 Segment::Arc(arc)
950 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
951 {
952 if let Some(existing) = final_edits.get_mut(&owner_id) {
953 let SegmentCtor::Arc(arc_ctor) = existing else {
954 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
955 "Internal: Expected arc ctor for owner, but found {}",
956 existing.human_friendly_kind_with_article()
957 ))));
958 };
959 if arc.start == segment_id {
960 arc_ctor.start = ctor.position;
961 } else if arc.end == segment_id {
962 arc_ctor.end = ctor.position;
963 } else {
964 arc_ctor.center = ctor.position;
965 }
966 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
967 let mut arc_ctor = arc_ctor.clone();
968 if arc.start == segment_id {
969 arc_ctor.start = ctor.position;
970 } else if arc.end == segment_id {
971 arc_ctor.end = ctor.position;
972 } else {
973 arc_ctor.center = ctor.position;
974 }
975 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
976 } else {
977 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
978 "Internal: Arc does not have arc ctor, but found {}",
979 arc.ctor.human_friendly_kind_with_article()
980 ))));
981 }
982 continue;
983 }
984 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
985 if let Some(existing) = final_edits.get_mut(&owner_id) {
986 let SegmentCtor::Circle(circle_ctor) = existing else {
987 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
988 "Internal: Expected circle ctor for owner, but found {}",
989 existing.human_friendly_kind_with_article()
990 ))));
991 };
992 if circle.start == segment_id {
993 circle_ctor.start = ctor.position;
994 } else {
995 circle_ctor.center = ctor.position;
996 }
997 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
998 let mut circle_ctor = circle_ctor.clone();
999 if circle.start == segment_id {
1000 circle_ctor.start = ctor.position;
1001 } else {
1002 circle_ctor.center = ctor.position;
1003 }
1004 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1005 } else {
1006 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1007 "Internal: Circle does not have circle ctor, but found {}",
1008 circle.ctor.human_friendly_kind_with_article()
1009 ))));
1010 }
1011 continue;
1012 }
1013 _ => {}
1014 }
1015 }
1016
1017 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1019 }
1020 SegmentCtor::Line(ctor) => {
1021 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1022 }
1023 SegmentCtor::Arc(ctor) => {
1024 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1025 }
1026 SegmentCtor::Circle(ctor) => {
1027 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1028 }
1029 }
1030 }
1031
1032 for (segment_id, ctor) in final_edits {
1033 match ctor {
1034 SegmentCtor::Point(ctor) => self
1035 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1036 .map_err(KclErrorWithOutputs::no_outputs)?,
1037 SegmentCtor::Line(ctor) => self
1038 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1039 .map_err(KclErrorWithOutputs::no_outputs)?,
1040 SegmentCtor::Arc(ctor) => self
1041 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1042 .map_err(KclErrorWithOutputs::no_outputs)?,
1043 SegmentCtor::Circle(ctor) => self
1044 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1045 .map_err(KclErrorWithOutputs::no_outputs)?,
1046 }
1047 }
1048 self.execute_after_edit(
1049 ctx,
1050 sketch,
1051 sketch_block_ref,
1052 ExecuteAfterEditOptions {
1053 segment_ids_edited,
1054 edit_kind: EditDeleteKind::Edit,
1055 sketch_var_update_mode,
1056 },
1057 &mut new_ast,
1058 )
1059 .await
1060 }
1061
1062 async fn delete_objects(
1063 &mut self,
1064 ctx: &ExecutorContext,
1065 _version: Version,
1066 sketch: ObjectId,
1067 constraint_ids: Vec<ObjectId>,
1068 segment_ids: Vec<ObjectId>,
1069 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1070 let sketch_block_ref =
1072 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1073
1074 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1076 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1077
1078 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1081
1082 for segment_id in segment_ids_set.iter().copied() {
1083 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1084 && let ObjectKind::Segment { segment } = &segment_object.kind
1085 && let Segment::Point(point) = segment
1086 && let Some(owner_id) = point.owner
1087 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1088 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1089 && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
1090 {
1091 resolved_segment_ids_to_delete.insert(owner_id);
1093 } else {
1094 resolved_segment_ids_to_delete.insert(segment_id);
1096 }
1097 }
1098 let referenced_constraint_ids = self
1099 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1100 .map_err(KclErrorWithOutputs::no_outputs)?;
1101
1102 let mut new_ast = self.program.ast.clone();
1103
1104 for constraint_id in referenced_constraint_ids {
1105 if constraint_ids_set.contains(&constraint_id) {
1106 continue;
1107 }
1108
1109 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1110 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1111 })?;
1112 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1113 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114 "Object is not a constraint, it is {}",
1115 constraint_object.kind.human_friendly_kind_with_article()
1116 ))));
1117 };
1118
1119 match constraint {
1120 Constraint::Coincident(coincident) => {
1121 let remaining_segments =
1122 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1123
1124 if remaining_segments.len() >= 2 {
1126 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1127 .map_err(KclErrorWithOutputs::no_outputs)?;
1128 } else {
1129 constraint_ids_set.insert(constraint_id);
1130 }
1131 }
1132 Constraint::EqualRadius(equal_radius) => {
1133 let remaining_input = equal_radius
1134 .input
1135 .iter()
1136 .copied()
1137 .filter(|segment_id| {
1138 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1139 })
1140 .collect::<Vec<_>>();
1141
1142 if remaining_input.len() >= 2 {
1143 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1144 .map_err(KclErrorWithOutputs::no_outputs)?;
1145 } else {
1146 constraint_ids_set.insert(constraint_id);
1147 }
1148 }
1149 Constraint::LinesEqualLength(lines_equal_length) => {
1150 let remaining_lines = lines_equal_length
1151 .lines
1152 .iter()
1153 .copied()
1154 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1155 .collect::<Vec<_>>();
1156
1157 if remaining_lines.len() >= 2 {
1159 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1160 .map_err(KclErrorWithOutputs::no_outputs)?;
1161 } else {
1162 constraint_ids_set.insert(constraint_id);
1163 }
1164 }
1165 Constraint::Parallel(parallel) => {
1166 let remaining_lines = parallel
1167 .lines
1168 .iter()
1169 .copied()
1170 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1171 .collect::<Vec<_>>();
1172
1173 if remaining_lines.len() >= 2 {
1174 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1175 .map_err(KclErrorWithOutputs::no_outputs)?;
1176 } else {
1177 constraint_ids_set.insert(constraint_id);
1178 }
1179 }
1180 Constraint::Horizontal(Horizontal::Points { points }) => {
1181 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1182
1183 if remaining_points.len() >= 2 {
1184 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1185 .map_err(KclErrorWithOutputs::no_outputs)?;
1186 } else {
1187 constraint_ids_set.insert(constraint_id);
1188 }
1189 }
1190 Constraint::Vertical(Vertical::Points { points }) => {
1191 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1192
1193 if remaining_points.len() >= 2 {
1194 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1195 .map_err(KclErrorWithOutputs::no_outputs)?;
1196 } else {
1197 constraint_ids_set.insert(constraint_id);
1198 }
1199 }
1200 Constraint::Fixed(fixed) => {
1201 if fixed.points.iter().any(|fixed_point| {
1202 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1203 }) {
1204 constraint_ids_set.insert(constraint_id);
1205 }
1206 }
1207 _ => {
1208 constraint_ids_set.insert(constraint_id);
1210 }
1211 }
1212 }
1213
1214 for constraint_id in constraint_ids_set {
1215 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1216 .map_err(KclErrorWithOutputs::no_outputs)?;
1217 }
1218 for segment_id in resolved_segment_ids_to_delete {
1219 self.delete_segment(&mut new_ast, sketch, segment_id)
1220 .map_err(KclErrorWithOutputs::no_outputs)?;
1221 }
1222
1223 self.execute_after_edit(
1224 ctx,
1225 sketch,
1226 sketch_block_ref,
1227 ExecuteAfterEditOptions {
1228 segment_ids_edited: Default::default(),
1229 edit_kind: EditDeleteKind::DeleteNonSketch,
1230 sketch_var_update_mode: SketchVarUpdateMode::CommitSolvedVars,
1231 },
1232 &mut new_ast,
1233 )
1234 .await
1235 }
1236
1237 async fn add_constraint(
1238 &mut self,
1239 ctx: &ExecutorContext,
1240 _version: Version,
1241 sketch: ObjectId,
1242 constraint: Constraint,
1243 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1244 let original_program = self.program.clone();
1248 let original_scene_graph = self.scene_graph.clone();
1249
1250 let mut new_ast = self.program.ast.clone();
1251 let sketch_block_ref = match constraint {
1252 Constraint::Coincident(coincident) => self
1253 .add_coincident(sketch, coincident, &mut new_ast)
1254 .await
1255 .map_err(KclErrorWithOutputs::no_outputs)?,
1256 Constraint::Distance(distance) => self
1257 .add_distance(sketch, distance, &mut new_ast)
1258 .await
1259 .map_err(KclErrorWithOutputs::no_outputs)?,
1260 Constraint::EqualRadius(equal_radius) => self
1261 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1262 .await
1263 .map_err(KclErrorWithOutputs::no_outputs)?,
1264 Constraint::Fixed(fixed) => self
1265 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1266 .await
1267 .map_err(KclErrorWithOutputs::no_outputs)?,
1268 Constraint::HorizontalDistance(distance) => self
1269 .add_horizontal_distance(sketch, distance, &mut new_ast)
1270 .await
1271 .map_err(KclErrorWithOutputs::no_outputs)?,
1272 Constraint::VerticalDistance(distance) => self
1273 .add_vertical_distance(sketch, distance, &mut new_ast)
1274 .await
1275 .map_err(KclErrorWithOutputs::no_outputs)?,
1276 Constraint::Horizontal(horizontal) => self
1277 .add_horizontal(sketch, horizontal, &mut new_ast)
1278 .await
1279 .map_err(KclErrorWithOutputs::no_outputs)?,
1280 Constraint::LinesEqualLength(lines_equal_length) => self
1281 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1282 .await
1283 .map_err(KclErrorWithOutputs::no_outputs)?,
1284 Constraint::Midpoint(midpoint) => self
1285 .add_midpoint(sketch, midpoint, &mut new_ast)
1286 .await
1287 .map_err(KclErrorWithOutputs::no_outputs)?,
1288 Constraint::Parallel(parallel) => self
1289 .add_parallel(sketch, parallel, &mut new_ast)
1290 .await
1291 .map_err(KclErrorWithOutputs::no_outputs)?,
1292 Constraint::Perpendicular(perpendicular) => self
1293 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1294 .await
1295 .map_err(KclErrorWithOutputs::no_outputs)?,
1296 Constraint::Radius(radius) => self
1297 .add_radius(sketch, radius, &mut new_ast)
1298 .await
1299 .map_err(KclErrorWithOutputs::no_outputs)?,
1300 Constraint::Diameter(diameter) => self
1301 .add_diameter(sketch, diameter, &mut new_ast)
1302 .await
1303 .map_err(KclErrorWithOutputs::no_outputs)?,
1304 Constraint::Symmetric(symmetric) => self
1305 .add_symmetric(sketch, symmetric, &mut new_ast)
1306 .await
1307 .map_err(KclErrorWithOutputs::no_outputs)?,
1308 Constraint::Vertical(vertical) => self
1309 .add_vertical(sketch, vertical, &mut new_ast)
1310 .await
1311 .map_err(KclErrorWithOutputs::no_outputs)?,
1312 Constraint::Angle(lines_at_angle) => self
1313 .add_angle(sketch, lines_at_angle, &mut new_ast)
1314 .await
1315 .map_err(KclErrorWithOutputs::no_outputs)?,
1316 Constraint::Tangent(tangent) => self
1317 .add_tangent(sketch, tangent, &mut new_ast)
1318 .await
1319 .map_err(KclErrorWithOutputs::no_outputs)?,
1320 };
1321
1322 let result = self
1323 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1324 .await;
1325
1326 if result.is_err() {
1328 self.program = original_program;
1329 self.scene_graph = original_scene_graph;
1330 }
1331
1332 result
1333 }
1334
1335 async fn chain_segment(
1336 &mut self,
1337 ctx: &ExecutorContext,
1338 version: Version,
1339 sketch: ObjectId,
1340 previous_segment_end_point_id: ObjectId,
1341 segment: SegmentCtor,
1342 _label: Option<String>,
1343 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1344 let SegmentCtor::Line(line_ctor) = segment else {
1348 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1349 "chain_segment currently only supports Line segments, got {}",
1350 segment.human_friendly_kind_with_article(),
1351 ))));
1352 };
1353
1354 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1356
1357 let new_line_id = first_scene_delta
1360 .new_objects
1361 .iter()
1362 .find(|&obj_id| {
1363 let obj = self.scene_graph.objects.get(obj_id.0);
1364 if let Some(obj) = obj {
1365 matches!(
1366 &obj.kind,
1367 ObjectKind::Segment {
1368 segment: Segment::Line(_)
1369 }
1370 )
1371 } else {
1372 false
1373 }
1374 })
1375 .ok_or_else(|| {
1376 KclErrorWithOutputs::no_outputs(KclError::refactor(
1377 "Failed to find new line segment in scene graph".to_string(),
1378 ))
1379 })?;
1380
1381 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1382 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1383 "New line object not found: {new_line_id:?}"
1384 )))
1385 })?;
1386
1387 let ObjectKind::Segment {
1388 segment: new_line_segment,
1389 } = &new_line_obj.kind
1390 else {
1391 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1392 "Object is not a segment: {new_line_obj:?}"
1393 ))));
1394 };
1395
1396 let Segment::Line(new_line) = new_line_segment else {
1397 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1398 "Segment is not a line: {new_line_segment:?}"
1399 ))));
1400 };
1401
1402 let new_line_start_point_id = new_line.start;
1403
1404 let coincident = Coincident {
1406 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1407 };
1408
1409 let (final_src_delta, final_scene_delta) = self
1410 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1411 .await?;
1412
1413 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1416 combined_new_objects.extend(final_scene_delta.new_objects);
1417
1418 let scene_graph_delta = SceneGraphDelta {
1419 new_graph: self.scene_graph.clone(),
1420 invalidates_ids: false,
1421 new_objects: combined_new_objects,
1422 exec_outcome: final_scene_delta.exec_outcome,
1423 };
1424
1425 Ok((final_src_delta, scene_graph_delta))
1426 }
1427
1428 async fn edit_constraint(
1429 &mut self,
1430 ctx: &ExecutorContext,
1431 _version: Version,
1432 sketch: ObjectId,
1433 constraint_id: ObjectId,
1434 value_expression: String,
1435 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1436 let sketch_block_ref =
1438 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1439
1440 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1441 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1442 })?;
1443 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1444 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1445 "Object is not a constraint: {constraint_id:?}"
1446 ))));
1447 }
1448
1449 let mut new_ast = self.program.ast.clone();
1450
1451 let (parsed, errors) = Program::parse(&value_expression)
1453 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1454 if !errors.is_empty() {
1455 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1456 "Error parsing value expression: {errors:?}"
1457 ))));
1458 }
1459 let mut parsed = parsed.ok_or_else(|| {
1460 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1461 })?;
1462 if parsed.ast.body.is_empty() {
1463 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1464 "Empty value expression".to_string(),
1465 )));
1466 }
1467 let first = parsed.ast.body.remove(0);
1468 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1469 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1470 "Value expression must be a simple expression".to_string(),
1471 )));
1472 };
1473
1474 let new_value: ast::BinaryPart = expr_stmt
1475 .inner
1476 .expression
1477 .try_into()
1478 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1479
1480 self.mutate_ast(
1481 &mut new_ast,
1482 constraint_id,
1483 AstMutateCommand::EditConstraintValue { value: new_value },
1484 )
1485 .map_err(KclErrorWithOutputs::no_outputs)?;
1486
1487 self.execute_after_edit(
1488 ctx,
1489 sketch,
1490 sketch_block_ref,
1491 ExecuteAfterEditOptions {
1492 segment_ids_edited: Default::default(),
1493 edit_kind: EditDeleteKind::Edit,
1494 sketch_var_update_mode: SketchVarUpdateMode::CommitSolvedVars,
1495 },
1496 &mut new_ast,
1497 )
1498 .await
1499 }
1500
1501 async fn edit_distance_constraint_label_position(
1502 &mut self,
1503 ctx: &ExecutorContext,
1504 _version: Version,
1505 sketch: ObjectId,
1506 constraint_id: ObjectId,
1507 label_position: Point2d<Number>,
1508 anchor_segment_ids: Vec<ObjectId>,
1509 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1510 let sketch_block_ref =
1512 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1513
1514 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1515 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1516 })?;
1517 if !matches!(
1518 &object.kind,
1519 ObjectKind::Constraint {
1520 constraint: Constraint::Distance(_)
1521 | Constraint::HorizontalDistance(_)
1522 | Constraint::VerticalDistance(_)
1523 | Constraint::Radius(_)
1524 | Constraint::Diameter(_),
1525 }
1526 ) {
1527 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1528 "Object does not support labelPosition: {constraint_id:?}"
1529 ))));
1530 }
1531
1532 let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1533 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1534 "Could not convert label position to AST: {err}"
1535 )))
1536 })?;
1537 let mut new_ast = self.program.ast.clone();
1538 self.mutate_ast(
1539 &mut new_ast,
1540 constraint_id,
1541 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1542 )
1543 .map_err(KclErrorWithOutputs::no_outputs)?;
1544
1545 self.execute_after_edit(
1546 ctx,
1547 sketch,
1548 sketch_block_ref,
1549 ExecuteAfterEditOptions {
1550 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1551 edit_kind: EditDeleteKind::Edit,
1552 sketch_var_update_mode: SketchVarUpdateMode::WarmStartOnly,
1553 },
1554 &mut new_ast,
1555 )
1556 .await
1557 }
1558
1559 async fn batch_split_segment_operations(
1567 &mut self,
1568 ctx: &ExecutorContext,
1569 _version: Version,
1570 sketch: ObjectId,
1571 edit_segments: Vec<ExistingSegmentCtor>,
1572 add_constraints: Vec<Constraint>,
1573 delete_constraint_ids: Vec<ObjectId>,
1574 _new_segment_info: sketch::NewSegmentInfo,
1575 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1576 let sketch_block_ref =
1578 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1579
1580 let mut new_ast = self.program.ast.clone();
1581 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1582
1583 for segment in edit_segments {
1585 segment_ids_edited.insert(segment.id);
1586 match segment.ctor {
1587 SegmentCtor::Point(ctor) => self
1588 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1589 .map_err(KclErrorWithOutputs::no_outputs)?,
1590 SegmentCtor::Line(ctor) => self
1591 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1592 .map_err(KclErrorWithOutputs::no_outputs)?,
1593 SegmentCtor::Arc(ctor) => self
1594 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1595 .map_err(KclErrorWithOutputs::no_outputs)?,
1596 SegmentCtor::Circle(ctor) => self
1597 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1598 .map_err(KclErrorWithOutputs::no_outputs)?,
1599 }
1600 }
1601
1602 for constraint in add_constraints {
1604 match constraint {
1605 Constraint::Coincident(coincident) => {
1606 self.add_coincident(sketch, coincident, &mut new_ast)
1607 .await
1608 .map_err(KclErrorWithOutputs::no_outputs)?;
1609 }
1610 Constraint::Distance(distance) => {
1611 self.add_distance(sketch, distance, &mut new_ast)
1612 .await
1613 .map_err(KclErrorWithOutputs::no_outputs)?;
1614 }
1615 Constraint::EqualRadius(equal_radius) => {
1616 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1617 .await
1618 .map_err(KclErrorWithOutputs::no_outputs)?;
1619 }
1620 Constraint::Fixed(fixed) => {
1621 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1622 .await
1623 .map_err(KclErrorWithOutputs::no_outputs)?;
1624 }
1625 Constraint::HorizontalDistance(distance) => {
1626 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1627 .await
1628 .map_err(KclErrorWithOutputs::no_outputs)?;
1629 }
1630 Constraint::VerticalDistance(distance) => {
1631 self.add_vertical_distance(sketch, distance, &mut new_ast)
1632 .await
1633 .map_err(KclErrorWithOutputs::no_outputs)?;
1634 }
1635 Constraint::Horizontal(horizontal) => {
1636 self.add_horizontal(sketch, horizontal, &mut new_ast)
1637 .await
1638 .map_err(KclErrorWithOutputs::no_outputs)?;
1639 }
1640 Constraint::LinesEqualLength(lines_equal_length) => {
1641 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1642 .await
1643 .map_err(KclErrorWithOutputs::no_outputs)?;
1644 }
1645 Constraint::Midpoint(midpoint) => {
1646 self.add_midpoint(sketch, midpoint, &mut new_ast)
1647 .await
1648 .map_err(KclErrorWithOutputs::no_outputs)?;
1649 }
1650 Constraint::Parallel(parallel) => {
1651 self.add_parallel(sketch, parallel, &mut new_ast)
1652 .await
1653 .map_err(KclErrorWithOutputs::no_outputs)?;
1654 }
1655 Constraint::Perpendicular(perpendicular) => {
1656 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1657 .await
1658 .map_err(KclErrorWithOutputs::no_outputs)?;
1659 }
1660 Constraint::Vertical(vertical) => {
1661 self.add_vertical(sketch, vertical, &mut new_ast)
1662 .await
1663 .map_err(KclErrorWithOutputs::no_outputs)?;
1664 }
1665 Constraint::Diameter(diameter) => {
1666 self.add_diameter(sketch, diameter, &mut new_ast)
1667 .await
1668 .map_err(KclErrorWithOutputs::no_outputs)?;
1669 }
1670 Constraint::Radius(radius) => {
1671 self.add_radius(sketch, radius, &mut new_ast)
1672 .await
1673 .map_err(KclErrorWithOutputs::no_outputs)?;
1674 }
1675 Constraint::Symmetric(symmetric) => {
1676 self.add_symmetric(sketch, symmetric, &mut new_ast)
1677 .await
1678 .map_err(KclErrorWithOutputs::no_outputs)?;
1679 }
1680 Constraint::Angle(angle) => {
1681 self.add_angle(sketch, angle, &mut new_ast)
1682 .await
1683 .map_err(KclErrorWithOutputs::no_outputs)?;
1684 }
1685 Constraint::Tangent(tangent) => {
1686 self.add_tangent(sketch, tangent, &mut new_ast)
1687 .await
1688 .map_err(KclErrorWithOutputs::no_outputs)?;
1689 }
1690 }
1691 }
1692
1693 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1695
1696 let has_constraint_deletions = !constraint_ids_set.is_empty();
1697 for constraint_id in constraint_ids_set {
1698 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1699 .map_err(KclErrorWithOutputs::no_outputs)?;
1700 }
1701
1702 let (source_delta, mut scene_graph_delta) = self
1706 .execute_after_edit(
1707 ctx,
1708 sketch,
1709 sketch_block_ref,
1710 ExecuteAfterEditOptions {
1711 segment_ids_edited,
1712 edit_kind: EditDeleteKind::Edit,
1713 sketch_var_update_mode: SketchVarUpdateMode::CommitSolvedVars,
1714 },
1715 &mut new_ast,
1716 )
1717 .await?;
1718
1719 if has_constraint_deletions {
1722 scene_graph_delta.invalidates_ids = true;
1723 }
1724
1725 Ok((source_delta, scene_graph_delta))
1726 }
1727
1728 async fn batch_tail_cut_operations(
1729 &mut self,
1730 ctx: &ExecutorContext,
1731 _version: Version,
1732 sketch: ObjectId,
1733 edit_segments: Vec<ExistingSegmentCtor>,
1734 add_constraints: Vec<Constraint>,
1735 delete_constraint_ids: Vec<ObjectId>,
1736 additional_edited_segment_ids: Vec<ObjectId>,
1737 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1738 let sketch_block_ref =
1739 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1740
1741 let mut new_ast = self.program.ast.clone();
1742 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1743
1744 for segment in edit_segments {
1746 segment_ids_edited.insert(segment.id);
1747 match segment.ctor {
1748 SegmentCtor::Point(ctor) => self
1749 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1750 .map_err(KclErrorWithOutputs::no_outputs)?,
1751 SegmentCtor::Line(ctor) => self
1752 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1753 .map_err(KclErrorWithOutputs::no_outputs)?,
1754 SegmentCtor::Arc(ctor) => self
1755 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1756 .map_err(KclErrorWithOutputs::no_outputs)?,
1757 SegmentCtor::Circle(ctor) => self
1758 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1759 .map_err(KclErrorWithOutputs::no_outputs)?,
1760 }
1761 }
1762
1763 segment_ids_edited.extend(additional_edited_segment_ids);
1764
1765 for constraint in add_constraints {
1767 match constraint {
1768 Constraint::Coincident(coincident) => {
1769 self.add_coincident(sketch, coincident, &mut new_ast)
1770 .await
1771 .map_err(KclErrorWithOutputs::no_outputs)?;
1772 }
1773 other => {
1774 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1775 "unsupported constraint in tail cut batch: {other:?}"
1776 ))));
1777 }
1778 }
1779 }
1780
1781 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1783
1784 let has_constraint_deletions = !constraint_ids_set.is_empty();
1785 for constraint_id in constraint_ids_set {
1786 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1787 .map_err(KclErrorWithOutputs::no_outputs)?;
1788 }
1789
1790 let (source_delta, mut scene_graph_delta) = self
1794 .execute_after_edit(
1795 ctx,
1796 sketch,
1797 sketch_block_ref,
1798 ExecuteAfterEditOptions {
1799 segment_ids_edited,
1800 edit_kind: EditDeleteKind::Edit,
1801 sketch_var_update_mode: SketchVarUpdateMode::CommitSolvedVars,
1802 },
1803 &mut new_ast,
1804 )
1805 .await?;
1806
1807 if has_constraint_deletions {
1810 scene_graph_delta.invalidates_ids = true;
1811 }
1812
1813 Ok((source_delta, scene_graph_delta))
1814 }
1815}
1816
1817impl FrontendState {
1818 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1819 self.program = program.clone();
1820 self.clear_sketch_var_warm_starts();
1821
1822 self.point_freedom_cache.clear();
1833 match ctx.run_with_caching(program).await {
1834 Ok(outcome) => {
1835 let outcome = self.update_state_after_exec(outcome, true);
1836 self.update_single_sketch_var_warm_starts(&outcome);
1837 let checkpoint_id = self
1838 .create_sketch_checkpoint(outcome.clone())
1839 .await
1840 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1841 Ok(SetProgramOutcome::Success {
1842 scene_graph: Box::new(self.scene_graph.clone()),
1843 exec_outcome: Box::new(outcome),
1844 checkpoint_id: Some(checkpoint_id),
1845 })
1846 }
1847 Err(mut err) => {
1848 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1851 self.update_state_after_exec(outcome, true);
1852 err.scene_graph = Some(self.scene_graph.clone());
1853 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1854 }
1855 }
1856 }
1857
1858 pub async fn engine_execute(
1861 &mut self,
1862 ctx: &ExecutorContext,
1863 program: Program,
1864 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1865 self.program = program.clone();
1866 self.clear_sketch_var_warm_starts();
1867
1868 self.point_freedom_cache.clear();
1872 match ctx.run_with_caching(program).await {
1873 Ok(outcome) => {
1874 let outcome = self.update_state_after_exec(outcome, true);
1875 self.update_single_sketch_var_warm_starts(&outcome);
1876 Ok(SceneGraphDelta {
1877 new_graph: self.scene_graph.clone(),
1878 exec_outcome: outcome,
1879 new_objects: Default::default(),
1881 invalidates_ids: Default::default(),
1883 })
1884 }
1885 Err(mut err) => {
1886 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1888 self.update_state_after_exec(outcome, true);
1889 err.scene_graph = Some(self.scene_graph.clone());
1890 Err(err)
1891 }
1892 }
1893 }
1894
1895 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1896 if matches!(err.error, KclError::EngineHangup { .. }) {
1897 return Err(err);
1901 }
1902
1903 let KclErrorWithOutputs {
1904 error,
1905 mut non_fatal,
1906 variables,
1907 operations,
1908 artifact_graph,
1909 scene_objects,
1910 source_range_to_object,
1911 var_solutions,
1912 filenames,
1913 default_planes,
1914 ..
1915 } = err;
1916
1917 if let Some(source_range) = error.source_ranges().first() {
1918 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1919 } else {
1920 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1921 }
1922
1923 Ok(ExecOutcome {
1924 variables,
1925 filenames,
1926 operations,
1927 artifact_graph,
1928 scene_objects,
1929 source_range_to_object,
1930 var_solutions,
1931 issues: non_fatal,
1932 default_planes,
1933 })
1934 }
1935
1936 async fn add_point(
1937 &mut self,
1938 ctx: &ExecutorContext,
1939 sketch: ObjectId,
1940 ctor: PointCtor,
1941 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1942 let at_ast = to_ast_point2d(&ctor.position)
1944 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1945 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1946 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1947 unlabeled: None,
1948 arguments: vec![ast::LabeledArg {
1949 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1950 arg: at_ast,
1951 }],
1952 digest: None,
1953 non_code_meta: Default::default(),
1954 })));
1955
1956 let sketch_id = sketch;
1958 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1959 #[cfg(target_arch = "wasm32")]
1960 web_sys::console::error_1(
1961 &format!(
1962 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1963 &self.scene_graph.objects
1964 )
1965 .into(),
1966 );
1967 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1968 })?;
1969 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1970 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1971 "Object is not a sketch, it is {}",
1972 sketch_object.kind.human_friendly_kind_with_article(),
1973 ))));
1974 };
1975 let mut new_ast = self.program.ast.clone();
1977 let (sketch_block_ref, _) = self
1978 .mutate_ast(
1979 &mut new_ast,
1980 sketch_id,
1981 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1982 )
1983 .map_err(KclErrorWithOutputs::no_outputs)?;
1984 let new_source = source_from_ast(&new_ast);
1986 let (new_program, errors) = Program::parse(&new_source)
1988 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1989 if !errors.is_empty() {
1990 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1991 "Error parsing KCL source after adding point: {errors:?}"
1992 ))));
1993 }
1994 let Some(new_program) = new_program else {
1995 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1996 "No AST produced after adding point".to_string(),
1997 )));
1998 };
1999
2000 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2001 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2002 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2003 )))
2004 })?;
2005
2006 self.program = new_program.clone();
2008
2009 let mut truncated_program = new_program;
2011 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2012 .map_err(KclErrorWithOutputs::no_outputs)?;
2013
2014 let outcome = ctx
2016 .run_mock(
2017 &truncated_program,
2018 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2019 )
2020 .await?;
2021
2022 let new_object_ids = {
2023 let make_err =
2024 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2025 let segment_id = outcome
2026 .source_range_to_object
2027 .get(&point_node_ref.range)
2028 .copied()
2029 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2030 let segment_object = outcome
2031 .scene_objects
2032 .get(segment_id.0)
2033 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2034 let ObjectKind::Segment { segment } = &segment_object.kind else {
2035 return Err(make_err(format!(
2036 "Object is not a segment, it is {}",
2037 segment_object.kind.human_friendly_kind_with_article()
2038 )));
2039 };
2040 let Segment::Point(_) = segment else {
2041 return Err(make_err(format!(
2042 "Segment is not a point, it is {}",
2043 segment.human_friendly_kind_with_article()
2044 )));
2045 };
2046 vec![segment_id]
2047 };
2048 let src_delta = SourceDelta { text: new_source };
2049 let outcome = self.update_state_after_exec(outcome, false);
2051 let scene_graph_delta = SceneGraphDelta {
2052 new_graph: self.scene_graph.clone(),
2053 invalidates_ids: false,
2054 new_objects: new_object_ids,
2055 exec_outcome: outcome,
2056 };
2057 Ok((src_delta, scene_graph_delta))
2058 }
2059
2060 async fn add_line(
2061 &mut self,
2062 ctx: &ExecutorContext,
2063 sketch: ObjectId,
2064 ctor: LineCtor,
2065 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2066 let start_ast = to_ast_point2d(&ctor.start)
2068 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2069 let end_ast = to_ast_point2d(&ctor.end)
2070 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2071 let mut arguments = vec![
2072 ast::LabeledArg {
2073 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2074 arg: start_ast,
2075 },
2076 ast::LabeledArg {
2077 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2078 arg: end_ast,
2079 },
2080 ];
2081 if ctor.construction == Some(true) {
2083 arguments.push(ast::LabeledArg {
2084 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2085 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2086 value: ast::LiteralValue::Bool(true),
2087 raw: "true".to_string(),
2088 digest: None,
2089 }))),
2090 });
2091 }
2092 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2093 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2094 unlabeled: None,
2095 arguments,
2096 digest: None,
2097 non_code_meta: Default::default(),
2098 })));
2099
2100 let sketch_id = sketch;
2102 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2103 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2104 })?;
2105 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2106 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2107 "Object is not a sketch, it is {}",
2108 sketch_object.kind.human_friendly_kind_with_article(),
2109 ))));
2110 };
2111 let mut new_ast = self.program.ast.clone();
2113 let (sketch_block_ref, _) = self
2114 .mutate_ast(
2115 &mut new_ast,
2116 sketch_id,
2117 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2118 )
2119 .map_err(KclErrorWithOutputs::no_outputs)?;
2120 let new_source = source_from_ast(&new_ast);
2122 let (new_program, errors) = Program::parse(&new_source)
2124 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2125 if !errors.is_empty() {
2126 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2127 "Error parsing KCL source after adding line: {errors:?}"
2128 ))));
2129 }
2130 let Some(new_program) = new_program else {
2131 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2132 "No AST produced after adding line".to_string(),
2133 )));
2134 };
2135
2136 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2137 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2138 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2139 )))
2140 })?;
2141
2142 self.program = new_program.clone();
2144
2145 let mut truncated_program = new_program;
2147 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2148 .map_err(KclErrorWithOutputs::no_outputs)?;
2149
2150 let outcome = ctx
2152 .run_mock(
2153 &truncated_program,
2154 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2155 )
2156 .await?;
2157
2158 let new_object_ids = {
2159 let make_err =
2160 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2161 let segment_id = outcome
2162 .source_range_to_object
2163 .get(&line_node_ref.range)
2164 .copied()
2165 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2166 let segment_object = outcome
2167 .scene_object_by_id(segment_id)
2168 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2169 let ObjectKind::Segment { segment } = &segment_object.kind else {
2170 return Err(make_err(format!(
2171 "Object is not a segment, it is {}",
2172 segment_object.kind.human_friendly_kind_with_article()
2173 )));
2174 };
2175 let Segment::Line(line) = segment else {
2176 return Err(make_err(format!(
2177 "Segment is not a line, it is {}",
2178 segment.human_friendly_kind_with_article()
2179 )));
2180 };
2181 vec![line.start, line.end, segment_id]
2182 };
2183 let src_delta = SourceDelta { text: new_source };
2184 let outcome = self.update_state_after_exec(outcome, false);
2186 let scene_graph_delta = SceneGraphDelta {
2187 new_graph: self.scene_graph.clone(),
2188 invalidates_ids: false,
2189 new_objects: new_object_ids,
2190 exec_outcome: outcome,
2191 };
2192 Ok((src_delta, scene_graph_delta))
2193 }
2194
2195 async fn add_arc(
2196 &mut self,
2197 ctx: &ExecutorContext,
2198 sketch: ObjectId,
2199 ctor: ArcCtor,
2200 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2201 let start_ast = to_ast_point2d(&ctor.start)
2203 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2204 let end_ast = to_ast_point2d(&ctor.end)
2205 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2206 let center_ast = to_ast_point2d(&ctor.center)
2207 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2208 let mut arguments = vec![
2209 ast::LabeledArg {
2210 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2211 arg: start_ast,
2212 },
2213 ast::LabeledArg {
2214 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2215 arg: end_ast,
2216 },
2217 ast::LabeledArg {
2218 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2219 arg: center_ast,
2220 },
2221 ];
2222 if ctor.construction == Some(true) {
2224 arguments.push(ast::LabeledArg {
2225 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2226 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2227 value: ast::LiteralValue::Bool(true),
2228 raw: "true".to_string(),
2229 digest: None,
2230 }))),
2231 });
2232 }
2233 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2234 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2235 unlabeled: None,
2236 arguments,
2237 digest: None,
2238 non_code_meta: Default::default(),
2239 })));
2240
2241 let sketch_id = sketch;
2243 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2244 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2245 })?;
2246 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2247 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2248 "Object is not a sketch, it is {}",
2249 sketch_object.kind.human_friendly_kind_with_article(),
2250 ))));
2251 };
2252 let mut new_ast = self.program.ast.clone();
2254 let (sketch_block_ref, _) = self
2255 .mutate_ast(
2256 &mut new_ast,
2257 sketch_id,
2258 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2259 )
2260 .map_err(KclErrorWithOutputs::no_outputs)?;
2261 let new_source = source_from_ast(&new_ast);
2263 let (new_program, errors) = Program::parse(&new_source)
2265 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2266 if !errors.is_empty() {
2267 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2268 "Error parsing KCL source after adding arc: {errors:?}"
2269 ))));
2270 }
2271 let Some(new_program) = new_program else {
2272 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2273 "No AST produced after adding arc".to_string(),
2274 )));
2275 };
2276
2277 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2278 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2279 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2280 )))
2281 })?;
2282
2283 self.program = new_program.clone();
2285
2286 let mut truncated_program = new_program;
2288 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2289 .map_err(KclErrorWithOutputs::no_outputs)?;
2290
2291 let outcome = ctx
2293 .run_mock(
2294 &truncated_program,
2295 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2296 )
2297 .await?;
2298
2299 let new_object_ids = {
2300 let make_err =
2301 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2302 let segment_id = outcome
2303 .source_range_to_object
2304 .get(&arc_node_ref.range)
2305 .copied()
2306 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2307 let segment_object = outcome
2308 .scene_objects
2309 .get(segment_id.0)
2310 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2311 let ObjectKind::Segment { segment } = &segment_object.kind else {
2312 return Err(make_err(format!(
2313 "Object is not a segment, it is {}",
2314 segment_object.kind.human_friendly_kind_with_article()
2315 )));
2316 };
2317 let Segment::Arc(arc) = segment else {
2318 return Err(make_err(format!(
2319 "Segment is not an arc, it is {}",
2320 segment.human_friendly_kind_with_article()
2321 )));
2322 };
2323 vec![arc.start, arc.end, arc.center, segment_id]
2324 };
2325 let src_delta = SourceDelta { text: new_source };
2326 let outcome = self.update_state_after_exec(outcome, false);
2328 let scene_graph_delta = SceneGraphDelta {
2329 new_graph: self.scene_graph.clone(),
2330 invalidates_ids: false,
2331 new_objects: new_object_ids,
2332 exec_outcome: outcome,
2333 };
2334 Ok((src_delta, scene_graph_delta))
2335 }
2336
2337 async fn add_circle(
2338 &mut self,
2339 ctx: &ExecutorContext,
2340 sketch: ObjectId,
2341 ctor: CircleCtor,
2342 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2343 let start_ast = to_ast_point2d(&ctor.start)
2345 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2346 let center_ast = to_ast_point2d(&ctor.center)
2347 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2348 let mut arguments = vec![
2349 ast::LabeledArg {
2350 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2351 arg: start_ast,
2352 },
2353 ast::LabeledArg {
2354 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2355 arg: center_ast,
2356 },
2357 ];
2358 if ctor.construction == Some(true) {
2360 arguments.push(ast::LabeledArg {
2361 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2362 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2363 value: ast::LiteralValue::Bool(true),
2364 raw: "true".to_string(),
2365 digest: None,
2366 }))),
2367 });
2368 }
2369 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2370 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2371 unlabeled: None,
2372 arguments,
2373 digest: None,
2374 non_code_meta: Default::default(),
2375 })));
2376
2377 let sketch_id = sketch;
2379 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2380 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2381 })?;
2382 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2383 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2384 "Object is not a sketch, it is {}",
2385 sketch_object.kind.human_friendly_kind_with_article(),
2386 ))));
2387 };
2388 let mut new_ast = self.program.ast.clone();
2390 let (sketch_block_ref, _) = self
2391 .mutate_ast(
2392 &mut new_ast,
2393 sketch_id,
2394 AstMutateCommand::AddSketchBlockVarDecl {
2395 prefix: CIRCLE_VARIABLE.to_owned(),
2396 expr: circle_ast,
2397 },
2398 )
2399 .map_err(KclErrorWithOutputs::no_outputs)?;
2400 let new_source = source_from_ast(&new_ast);
2402 let (new_program, errors) = Program::parse(&new_source)
2404 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2405 if !errors.is_empty() {
2406 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2407 "Error parsing KCL source after adding circle: {errors:?}"
2408 ))));
2409 }
2410 let Some(new_program) = new_program else {
2411 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2412 "No AST produced after adding circle".to_string(),
2413 )));
2414 };
2415
2416 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2417 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2418 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2419 )))
2420 })?;
2421
2422 self.program = new_program.clone();
2424
2425 let mut truncated_program = new_program;
2427 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2428 .map_err(KclErrorWithOutputs::no_outputs)?;
2429
2430 let outcome = ctx
2432 .run_mock(
2433 &truncated_program,
2434 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2435 )
2436 .await?;
2437
2438 let new_object_ids = {
2439 let make_err =
2440 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2441 let segment_id = outcome
2442 .source_range_to_object
2443 .get(&circle_node_ref.range)
2444 .copied()
2445 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2446 let segment_object = outcome
2447 .scene_objects
2448 .get(segment_id.0)
2449 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2450 let ObjectKind::Segment { segment } = &segment_object.kind else {
2451 return Err(make_err(format!(
2452 "Object is not a segment, it is {}",
2453 segment_object.kind.human_friendly_kind_with_article()
2454 )));
2455 };
2456 let Segment::Circle(circle) = segment else {
2457 return Err(make_err(format!(
2458 "Segment is not a circle, it is {}",
2459 segment.human_friendly_kind_with_article()
2460 )));
2461 };
2462 vec![circle.start, circle.center, segment_id]
2463 };
2464 let src_delta = SourceDelta { text: new_source };
2465 let outcome = self.update_state_after_exec(outcome, false);
2467 let scene_graph_delta = SceneGraphDelta {
2468 new_graph: self.scene_graph.clone(),
2469 invalidates_ids: false,
2470 new_objects: new_object_ids,
2471 exec_outcome: outcome,
2472 };
2473 Ok((src_delta, scene_graph_delta))
2474 }
2475
2476 fn edit_point(
2477 &mut self,
2478 new_ast: &mut ast::Node<ast::Program>,
2479 sketch: ObjectId,
2480 point: ObjectId,
2481 ctor: PointCtor,
2482 ) -> Result<(), KclError> {
2483 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2485
2486 let sketch_id = sketch;
2488 let sketch_object = self
2489 .scene_graph
2490 .objects
2491 .get(sketch_id.0)
2492 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2493 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2494 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2495 };
2496 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2497 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2498 })?;
2499 let point_id = point;
2501 let point_object = self
2502 .scene_graph
2503 .objects
2504 .get(point_id.0)
2505 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2506 let ObjectKind::Segment {
2507 segment: Segment::Point(point),
2508 } = &point_object.kind
2509 else {
2510 return Err(KclError::refactor(format!(
2511 "Object is not a point segment: {point_object:?}"
2512 )));
2513 };
2514
2515 if let Some(owner_id) = point.owner {
2517 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2518 KclError::refactor(format!(
2519 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2520 ))
2521 })?;
2522 let ObjectKind::Segment { segment } = &owner_object.kind else {
2523 return Err(KclError::refactor(format!(
2524 "Internal: Owner of point is not a segment, but found {}",
2525 owner_object.kind.human_friendly_kind_with_article()
2526 )));
2527 };
2528
2529 if let Segment::Line(line) = segment {
2531 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2532 return Err(KclError::refactor(format!(
2533 "Internal: Owner of point does not have line ctor, but found {}",
2534 line.ctor.human_friendly_kind_with_article()
2535 )));
2536 };
2537 let mut line_ctor = line_ctor.clone();
2538 if line.start == point_id {
2540 line_ctor.start = ctor.position;
2541 } else if line.end == point_id {
2542 line_ctor.end = ctor.position;
2543 } else {
2544 return Err(KclError::refactor(format!(
2545 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2546 )));
2547 }
2548 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2549 }
2550
2551 if let Segment::Arc(arc) = segment {
2553 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2554 return Err(KclError::refactor(format!(
2555 "Internal: Owner of point does not have arc ctor, but found {}",
2556 arc.ctor.human_friendly_kind_with_article()
2557 )));
2558 };
2559 let mut arc_ctor = arc_ctor.clone();
2560 if arc.center == point_id {
2562 arc_ctor.center = ctor.position;
2563 } else if arc.start == point_id {
2564 arc_ctor.start = ctor.position;
2565 } else if arc.end == point_id {
2566 arc_ctor.end = ctor.position;
2567 } else {
2568 return Err(KclError::refactor(format!(
2569 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2570 )));
2571 }
2572 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2573 }
2574
2575 if let Segment::Circle(circle) = segment {
2577 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2578 return Err(KclError::refactor(format!(
2579 "Internal: Owner of point does not have circle ctor, but found {}",
2580 circle.ctor.human_friendly_kind_with_article()
2581 )));
2582 };
2583 let mut circle_ctor = circle_ctor.clone();
2584 if circle.center == point_id {
2585 circle_ctor.center = ctor.position;
2586 } else if circle.start == point_id {
2587 circle_ctor.start = ctor.position;
2588 } else {
2589 return Err(KclError::refactor(format!(
2590 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2591 )));
2592 }
2593 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2594 }
2595
2596 }
2599
2600 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2602 Ok(())
2603 }
2604
2605 fn edit_line(
2606 &mut self,
2607 new_ast: &mut ast::Node<ast::Program>,
2608 sketch: ObjectId,
2609 line: ObjectId,
2610 ctor: LineCtor,
2611 ) -> Result<(), KclError> {
2612 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2614 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2615
2616 let sketch_id = sketch;
2618 let sketch_object = self
2619 .scene_graph
2620 .objects
2621 .get(sketch_id.0)
2622 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2623 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2624 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2625 };
2626 sketch
2627 .segments
2628 .iter()
2629 .find(|o| **o == line)
2630 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2631 let line_id = line;
2633 let line_object = self
2634 .scene_graph
2635 .objects
2636 .get(line_id.0)
2637 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2638 let ObjectKind::Segment { .. } = &line_object.kind else {
2639 let kind = line_object.kind.human_friendly_kind_with_article();
2640 return Err(KclError::refactor(format!(
2641 "This constraint only works on Segments, but you selected {kind}"
2642 )));
2643 };
2644
2645 self.mutate_ast(
2647 new_ast,
2648 line_id,
2649 AstMutateCommand::EditLine {
2650 start: new_start_ast,
2651 end: new_end_ast,
2652 construction: ctor.construction,
2653 },
2654 )?;
2655 Ok(())
2656 }
2657
2658 fn edit_arc(
2659 &mut self,
2660 new_ast: &mut ast::Node<ast::Program>,
2661 sketch: ObjectId,
2662 arc: ObjectId,
2663 ctor: ArcCtor,
2664 ) -> Result<(), KclError> {
2665 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2667 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2668 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2669
2670 let sketch_id = sketch;
2672 let sketch_object = self
2673 .scene_graph
2674 .objects
2675 .get(sketch_id.0)
2676 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2677 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2678 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2679 };
2680 sketch
2681 .segments
2682 .iter()
2683 .find(|o| **o == arc)
2684 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2685 let arc_id = arc;
2687 let arc_object = self
2688 .scene_graph
2689 .objects
2690 .get(arc_id.0)
2691 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2692 let ObjectKind::Segment { .. } = &arc_object.kind else {
2693 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2694 };
2695
2696 self.mutate_ast(
2698 new_ast,
2699 arc_id,
2700 AstMutateCommand::EditArc {
2701 start: new_start_ast,
2702 end: new_end_ast,
2703 center: new_center_ast,
2704 construction: ctor.construction,
2705 },
2706 )?;
2707 Ok(())
2708 }
2709
2710 fn edit_circle(
2711 &mut self,
2712 new_ast: &mut ast::Node<ast::Program>,
2713 sketch: ObjectId,
2714 circle: ObjectId,
2715 ctor: CircleCtor,
2716 ) -> Result<(), KclError> {
2717 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2719 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2720
2721 let sketch_id = sketch;
2723 let sketch_object = self
2724 .scene_graph
2725 .objects
2726 .get(sketch_id.0)
2727 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2728 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2729 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2730 };
2731 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2732 KclError::refactor(format!(
2733 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2734 ))
2735 })?;
2736 let circle_id = circle;
2738 let circle_object = self
2739 .scene_graph
2740 .objects
2741 .get(circle_id.0)
2742 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2743 let ObjectKind::Segment { .. } = &circle_object.kind else {
2744 return Err(KclError::refactor(format!(
2745 "Object is not a segment: {circle_object:?}"
2746 )));
2747 };
2748
2749 self.mutate_ast(
2751 new_ast,
2752 circle_id,
2753 AstMutateCommand::EditCircle {
2754 start: new_start_ast,
2755 center: new_center_ast,
2756 construction: ctor.construction,
2757 },
2758 )?;
2759 Ok(())
2760 }
2761
2762 fn delete_segment(
2763 &mut self,
2764 new_ast: &mut ast::Node<ast::Program>,
2765 sketch: ObjectId,
2766 segment_id: ObjectId,
2767 ) -> Result<(), KclError> {
2768 let sketch_id = sketch;
2770 let sketch_object = self
2771 .scene_graph
2772 .objects
2773 .get(sketch_id.0)
2774 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2775 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2776 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2777 };
2778 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2779 KclError::refactor(format!(
2780 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2781 ))
2782 })?;
2783 let segment_object =
2785 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2786 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2787 })?;
2788 let ObjectKind::Segment { .. } = &segment_object.kind else {
2789 return Err(KclError::refactor(format!(
2790 "Object is not a segment, it is {}",
2791 segment_object.kind.human_friendly_kind_with_article()
2792 )));
2793 };
2794
2795 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2797 Ok(())
2798 }
2799
2800 fn delete_constraint(
2801 &mut self,
2802 new_ast: &mut ast::Node<ast::Program>,
2803 sketch: ObjectId,
2804 constraint_id: ObjectId,
2805 ) -> Result<(), KclError> {
2806 let sketch_id = sketch;
2808 let sketch_object = self
2809 .scene_graph
2810 .objects
2811 .get(sketch_id.0)
2812 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2813 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2814 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2815 };
2816 sketch
2817 .constraints
2818 .iter()
2819 .find(|o| **o == constraint_id)
2820 .ok_or_else(|| {
2821 KclError::refactor(format!(
2822 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2823 ))
2824 })?;
2825 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2827 KclError::refactor(format!(
2828 "Constraint not found in scene graph: constraint={constraint_id:?}"
2829 ))
2830 })?;
2831 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2832 return Err(KclError::refactor(format!(
2833 "Object is not a constraint, it is {}",
2834 constraint_object.kind.human_friendly_kind_with_article()
2835 )));
2836 };
2837
2838 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2840 Ok(())
2841 }
2842
2843 fn edit_coincident_constraint(
2844 &mut self,
2845 new_ast: &mut ast::Node<ast::Program>,
2846 constraint_id: ObjectId,
2847 segments: Vec<ConstraintSegment>,
2848 ) -> Result<(), KclError> {
2849 if segments.len() < 2 {
2850 return Err(KclError::refactor(format!(
2851 "Coincident constraint must have at least 2 inputs, got {}",
2852 segments.len()
2853 )));
2854 }
2855
2856 let segment_asts = segments
2857 .iter()
2858 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2859 .collect::<Result<Vec<_>, _>>()?;
2860
2861 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2862 elements: segment_asts,
2863 digest: None,
2864 non_code_meta: Default::default(),
2865 })));
2866
2867 self.mutate_ast(
2868 new_ast,
2869 constraint_id,
2870 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2871 )?;
2872 Ok(())
2873 }
2874
2875 fn edit_horizontal_points_constraint(
2876 &mut self,
2877 new_ast: &mut ast::Node<ast::Program>,
2878 constraint_id: ObjectId,
2879 points: Vec<ConstraintSegment>,
2880 ) -> Result<(), KclError> {
2881 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2882 }
2883
2884 fn edit_vertical_points_constraint(
2885 &mut self,
2886 new_ast: &mut ast::Node<ast::Program>,
2887 constraint_id: ObjectId,
2888 points: Vec<ConstraintSegment>,
2889 ) -> Result<(), KclError> {
2890 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2891 }
2892
2893 fn edit_axis_points_constraint(
2894 &mut self,
2895 new_ast: &mut ast::Node<ast::Program>,
2896 constraint_id: ObjectId,
2897 points: Vec<ConstraintSegment>,
2898 constraint_name: &str,
2899 ) -> Result<(), KclError> {
2900 if points.len() < 2 {
2901 return Err(KclError::refactor(format!(
2902 "{constraint_name} points constraint must have at least 2 points, got {}",
2903 points.len()
2904 )));
2905 }
2906
2907 let point_asts = points
2908 .iter()
2909 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2910 .collect::<Result<Vec<_>, _>>()?;
2911
2912 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2913 elements: point_asts,
2914 digest: None,
2915 non_code_meta: Default::default(),
2916 })));
2917
2918 self.mutate_ast(
2919 new_ast,
2920 constraint_id,
2921 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2922 )?;
2923 Ok(())
2924 }
2925
2926 fn edit_equal_length_constraint(
2928 &mut self,
2929 new_ast: &mut ast::Node<ast::Program>,
2930 constraint_id: ObjectId,
2931 lines: Vec<ObjectId>,
2932 ) -> Result<(), KclError> {
2933 if lines.len() < 2 {
2934 return Err(KclError::refactor(format!(
2935 "Lines equal length constraint must have at least 2 lines, got {}",
2936 lines.len()
2937 )));
2938 }
2939
2940 let line_asts = lines
2941 .iter()
2942 .map(|line_id| {
2943 let line_object = self
2944 .scene_graph
2945 .objects
2946 .get(line_id.0)
2947 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2948 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2949 let kind = line_object.kind.human_friendly_kind_with_article();
2950 return Err(KclError::refactor(format!(
2951 "This constraint only works on Segments, but you selected {kind}"
2952 )));
2953 };
2954 let Segment::Line(_) = line_segment else {
2955 let kind = line_segment.human_friendly_kind_with_article();
2956 return Err(KclError::refactor(format!(
2957 "Only lines can be made equal length, but you selected {kind}"
2958 )));
2959 };
2960
2961 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2962 })
2963 .collect::<Result<Vec<_>, _>>()?;
2964
2965 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2966 elements: line_asts,
2967 digest: None,
2968 non_code_meta: Default::default(),
2969 })));
2970
2971 self.mutate_ast(
2972 new_ast,
2973 constraint_id,
2974 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2975 )?;
2976 Ok(())
2977 }
2978
2979 fn edit_parallel_constraint(
2981 &mut self,
2982 new_ast: &mut ast::Node<ast::Program>,
2983 constraint_id: ObjectId,
2984 lines: Vec<ObjectId>,
2985 ) -> Result<(), KclError> {
2986 if lines.len() < 2 {
2987 return Err(KclError::refactor(format!(
2988 "Parallel constraint must have at least 2 lines, got {}",
2989 lines.len()
2990 )));
2991 }
2992
2993 let line_asts = lines
2994 .iter()
2995 .map(|line_id| {
2996 let line_object = self
2997 .scene_graph
2998 .objects
2999 .get(line_id.0)
3000 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3001 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3002 let kind = line_object.kind.human_friendly_kind_with_article();
3003 return Err(KclError::refactor(format!(
3004 "This constraint only works on Segments, but you selected {kind}"
3005 )));
3006 };
3007 let Segment::Line(_) = line_segment else {
3008 let kind = line_segment.human_friendly_kind_with_article();
3009 return Err(KclError::refactor(format!(
3010 "Only lines can be made parallel, but you selected {kind}"
3011 )));
3012 };
3013
3014 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3015 })
3016 .collect::<Result<Vec<_>, _>>()?;
3017
3018 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3019 elements: line_asts,
3020 digest: None,
3021 non_code_meta: Default::default(),
3022 })));
3023
3024 self.mutate_ast(
3025 new_ast,
3026 constraint_id,
3027 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3028 )?;
3029 Ok(())
3030 }
3031
3032 fn edit_equal_radius_constraint(
3034 &mut self,
3035 new_ast: &mut ast::Node<ast::Program>,
3036 constraint_id: ObjectId,
3037 input: Vec<ObjectId>,
3038 ) -> Result<(), KclError> {
3039 if input.len() < 2 {
3040 return Err(KclError::refactor(format!(
3041 "equalRadius constraint must have at least 2 segments, got {}",
3042 input.len()
3043 )));
3044 }
3045
3046 let input_asts = input
3047 .iter()
3048 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3049 .collect::<Result<Vec<_>, _>>()?;
3050
3051 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3052 elements: input_asts,
3053 digest: None,
3054 non_code_meta: Default::default(),
3055 })));
3056
3057 self.mutate_ast(
3058 new_ast,
3059 constraint_id,
3060 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3061 )?;
3062 Ok(())
3063 }
3064
3065 async fn execute_after_edit(
3066 &mut self,
3067 ctx: &ExecutorContext,
3068 sketch: ObjectId,
3069 sketch_block_ref: AstNodeRef,
3070 options: ExecuteAfterEditOptions,
3071 new_ast: &mut ast::Node<ast::Program>,
3072 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3073 let new_source = source_from_ast(new_ast);
3075 let (new_program, errors) = Program::parse(&new_source)
3077 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3078 if !errors.is_empty() {
3079 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3080 "Error parsing KCL source after editing: {errors:?}"
3081 ))));
3082 }
3083 let Some(new_program) = new_program else {
3084 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3085 "No AST produced after editing".to_string(),
3086 )));
3087 };
3088
3089 self.program = new_program.clone();
3091
3092 let is_delete = options.edit_kind.is_delete();
3094 let truncated_program = {
3095 let mut truncated_program = new_program;
3096 only_sketch_block(
3097 &mut truncated_program.ast,
3098 &sketch_block_ref,
3099 options.edit_kind.to_change_kind(),
3100 )
3101 .map_err(KclErrorWithOutputs::no_outputs)?;
3102 truncated_program
3103 };
3104
3105 let use_warm_starts = !is_delete
3107 && matches!(
3108 options.sketch_var_update_mode,
3109 SketchVarUpdateMode::WarmStartOnly | SketchVarUpdateMode::CommitSolvedVarsFromWarmStart
3110 );
3111 let mock_config =
3112 self.sketch_mock_config(sketch, is_delete, options.segment_ids_edited.clone(), use_warm_starts);
3113 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3114
3115 let outcome = self.update_state_after_exec(outcome, is_delete);
3117 match options.sketch_var_update_mode {
3118 SketchVarUpdateMode::CommitSolvedVars => {
3119 self.merge_committed_edit_sketch_var_warm_starts(sketch, &outcome, &options.segment_ids_edited);
3120 }
3121 SketchVarUpdateMode::WarmStartOnly | SketchVarUpdateMode::CommitSolvedVarsFromWarmStart => {
3122 self.replace_sketch_var_warm_starts(sketch, &outcome);
3123 }
3124 }
3125
3126 let new_source = match options.sketch_var_update_mode {
3127 SketchVarUpdateMode::WarmStartOnly => new_source,
3128 SketchVarUpdateMode::CommitSolvedVars | SketchVarUpdateMode::CommitSolvedVarsFromWarmStart => {
3129 self.commit_var_solutions_to_program(&outcome)?
3130 }
3131 };
3132
3133 let src_delta = SourceDelta { text: new_source };
3134 let scene_graph_delta = SceneGraphDelta {
3135 new_graph: self.scene_graph.clone(),
3136 invalidates_ids: is_delete,
3137 new_objects: Vec::new(),
3138 exec_outcome: outcome,
3139 };
3140 Ok((src_delta, scene_graph_delta))
3141 }
3142
3143 async fn execute_after_delete_sketch(
3144 &mut self,
3145 ctx: &ExecutorContext,
3146 new_ast: &mut ast::Node<ast::Program>,
3147 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3148 let new_source = source_from_ast(new_ast);
3150 let (new_program, errors) = Program::parse(&new_source)
3152 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3153 if !errors.is_empty() {
3154 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3155 "Error parsing KCL source after editing: {errors:?}"
3156 ))));
3157 }
3158 let Some(new_program) = new_program else {
3159 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3160 "No AST produced after editing".to_string(),
3161 )));
3162 };
3163
3164 self.program = new_program.clone();
3166
3167 let outcome = ctx.run_with_caching(new_program).await?;
3173 let freedom_analysis_ran = true;
3174
3175 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3176
3177 let src_delta = SourceDelta { text: new_source };
3178 let scene_graph_delta = SceneGraphDelta {
3179 new_graph: self.scene_graph.clone(),
3180 invalidates_ids: true,
3181 new_objects: Vec::new(),
3182 exec_outcome: outcome,
3183 };
3184 Ok((src_delta, scene_graph_delta))
3185 }
3186
3187 fn point_id_to_ast_reference(
3192 &self,
3193 point_id: ObjectId,
3194 new_ast: &mut ast::Node<ast::Program>,
3195 ) -> Result<ast::Expr, KclError> {
3196 let point_object = self
3197 .scene_graph
3198 .objects
3199 .get(point_id.0)
3200 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3201 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3202 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3203 };
3204 let Segment::Point(point) = point_segment else {
3205 return Err(KclError::refactor(format!(
3206 "Only points are currently supported: {point_object:?}"
3207 )));
3208 };
3209
3210 if let Some(owner_id) = point.owner {
3211 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3212 KclError::refactor(format!(
3213 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3214 ))
3215 })?;
3216 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3217 return Err(KclError::refactor(format!(
3218 "Owner of point is not a segment, but found {}",
3219 owner_object.kind.human_friendly_kind_with_article()
3220 )));
3221 };
3222
3223 match owner_segment {
3224 Segment::Line(line) => {
3225 let property = if line.start == point_id {
3226 LINE_PROPERTY_START
3227 } else if line.end == point_id {
3228 LINE_PROPERTY_END
3229 } else {
3230 return Err(KclError::refactor(format!(
3231 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3232 )));
3233 };
3234 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3235 }
3236 Segment::Arc(arc) => {
3237 let property = if arc.start == point_id {
3238 ARC_PROPERTY_START
3239 } else if arc.end == point_id {
3240 ARC_PROPERTY_END
3241 } else if arc.center == point_id {
3242 ARC_PROPERTY_CENTER
3243 } else {
3244 return Err(KclError::refactor(format!(
3245 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3246 )));
3247 };
3248 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3249 }
3250 Segment::Circle(circle) => {
3251 let property = if circle.start == point_id {
3252 CIRCLE_PROPERTY_START
3253 } else if circle.center == point_id {
3254 CIRCLE_PROPERTY_CENTER
3255 } else {
3256 return Err(KclError::refactor(format!(
3257 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3258 )));
3259 };
3260 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3261 }
3262 _ => Err(KclError::refactor(format!(
3263 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3264 ))),
3265 }
3266 } else {
3267 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3269 }
3270 }
3271
3272 fn coincident_segment_to_ast(
3273 &self,
3274 segment: &ConstraintSegment,
3275 new_ast: &mut ast::Node<ast::Program>,
3276 ) -> Result<ast::Expr, KclError> {
3277 match segment {
3278 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3279 ConstraintSegment::Segment(segment_id) => {
3280 let segment_object = self
3281 .scene_graph
3282 .objects
3283 .get(segment_id.0)
3284 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3285 let ObjectKind::Segment { segment } = &segment_object.kind else {
3286 return Err(KclError::refactor(format!(
3287 "Object is not a segment, it is {}",
3288 segment_object.kind.human_friendly_kind_with_article()
3289 )));
3290 };
3291
3292 match segment {
3293 Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
3294 Segment::Line(_) => {
3295 get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None)
3296 }
3297 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3298 Segment::Circle(_) => {
3299 get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
3300 }
3301 }
3302 }
3303 }
3304 }
3305
3306 fn axis_constraint_segment_to_ast(
3307 &self,
3308 segment: &ConstraintSegment,
3309 new_ast: &mut ast::Node<ast::Program>,
3310 ) -> Result<ast::Expr, KclError> {
3311 match segment {
3312 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3313 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3314 }
3315 }
3316
3317 async fn add_coincident(
3318 &mut self,
3319 sketch: ObjectId,
3320 coincident: Coincident,
3321 new_ast: &mut ast::Node<ast::Program>,
3322 ) -> Result<AstNodeRef, KclError> {
3323 let sketch_id = sketch;
3324 let segment_asts = coincident
3325 .segments
3326 .iter()
3327 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3328 .collect::<Result<Vec<_>, _>>()?;
3329 if segment_asts.len() < 2 {
3330 return Err(KclError::refactor(format!(
3331 "Coincident constraint must have at least 2 inputs, got {}",
3332 segment_asts.len()
3333 )));
3334 }
3335
3336 let coincident_ast = create_coincident_ast(segment_asts);
3338
3339 let (sketch_block_ref, _) = self.mutate_ast(
3341 new_ast,
3342 sketch_id,
3343 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3344 )?;
3345 Ok(sketch_block_ref)
3346 }
3347
3348 async fn add_distance(
3349 &mut self,
3350 sketch: ObjectId,
3351 distance: Distance,
3352 new_ast: &mut ast::Node<ast::Program>,
3353 ) -> Result<AstNodeRef, KclError> {
3354 let sketch_id = sketch;
3355 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3356 [pt0, pt1] => [
3357 self.coincident_segment_to_ast(pt0, new_ast)?,
3358 self.coincident_segment_to_ast(pt1, new_ast)?,
3359 ],
3360 _ => {
3361 return Err(KclError::refactor(format!(
3362 "Distance constraint must have exactly 2 points, got {}",
3363 distance.points.len()
3364 )));
3365 }
3366 };
3367
3368 let arguments = match &distance.label_position {
3369 Some(label_position) => vec![ast::LabeledArg {
3370 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3371 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3372 }],
3373 None => Default::default(),
3374 };
3375
3376 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3378 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3379 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3380 ast::ArrayExpression {
3381 elements: vec![pt0_ast, pt1_ast],
3382 digest: None,
3383 non_code_meta: Default::default(),
3384 },
3385 )))),
3386 arguments,
3387 digest: None,
3388 non_code_meta: Default::default(),
3389 })));
3390 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3391 left: distance_call_ast,
3392 operator: ast::BinaryOperator::Eq,
3393 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3394 value: ast::LiteralValue::Number {
3395 value: distance.distance.value,
3396 suffix: distance.distance.units,
3397 },
3398 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3399 KclError::refactor(format!(
3400 "Could not format numeric suffix: {:?}",
3401 distance.distance.units
3402 ))
3403 })?,
3404 digest: None,
3405 }))),
3406 digest: None,
3407 })));
3408
3409 let (sketch_block_ref, _) = self.mutate_ast(
3411 new_ast,
3412 sketch_id,
3413 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3414 )?;
3415 Ok(sketch_block_ref)
3416 }
3417
3418 async fn add_angle(
3419 &mut self,
3420 sketch: ObjectId,
3421 angle: Angle,
3422 new_ast: &mut ast::Node<ast::Program>,
3423 ) -> Result<AstNodeRef, KclError> {
3424 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3425 return Err(KclError::refactor(format!(
3426 "Angle constraint must have exactly 2 lines, got {}",
3427 angle.lines.len()
3428 )));
3429 };
3430 let sketch_id = sketch;
3431
3432 let line0_object = self
3434 .scene_graph
3435 .objects
3436 .get(l0_id.0)
3437 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3438 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3439 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3440 };
3441 let Segment::Line(_) = line0_segment else {
3442 return Err(KclError::refactor(format!(
3443 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3444 )));
3445 };
3446 let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3447
3448 let line1_object = self
3449 .scene_graph
3450 .objects
3451 .get(l1_id.0)
3452 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3453 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3454 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3455 };
3456 let Segment::Line(_) = line1_segment else {
3457 return Err(KclError::refactor(format!(
3458 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3459 )));
3460 };
3461 let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3462
3463 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3465 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3466 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3467 ast::ArrayExpression {
3468 elements: vec![l0_ast, l1_ast],
3469 digest: None,
3470 non_code_meta: Default::default(),
3471 },
3472 )))),
3473 arguments: Default::default(),
3474 digest: None,
3475 non_code_meta: Default::default(),
3476 })));
3477 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3478 left: angle_call_ast,
3479 operator: ast::BinaryOperator::Eq,
3480 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3481 value: ast::LiteralValue::Number {
3482 value: angle.angle.value,
3483 suffix: angle.angle.units,
3484 },
3485 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3486 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3487 })?,
3488 digest: None,
3489 }))),
3490 digest: None,
3491 })));
3492
3493 let (sketch_block_ref, _) = self.mutate_ast(
3495 new_ast,
3496 sketch_id,
3497 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3498 )?;
3499 Ok(sketch_block_ref)
3500 }
3501
3502 async fn add_tangent(
3503 &mut self,
3504 sketch: ObjectId,
3505 tangent: Tangent,
3506 new_ast: &mut ast::Node<ast::Program>,
3507 ) -> Result<AstNodeRef, KclError> {
3508 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3509 return Err(KclError::refactor(format!(
3510 "Tangent constraint must have exactly 2 segments, got {}",
3511 tangent.input.len()
3512 )));
3513 };
3514 let sketch_id = sketch;
3515
3516 let seg0_object = self
3517 .scene_graph
3518 .objects
3519 .get(seg0_id.0)
3520 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3521 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3522 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3523 };
3524 let seg0_ast = match seg0_segment {
3525 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, LINE_VARIABLE, None)?,
3526 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, ARC_VARIABLE, None)?,
3527 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3528 _ => {
3529 return Err(KclError::refactor(format!(
3530 "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3531 )));
3532 }
3533 };
3534
3535 let seg1_object = self
3536 .scene_graph
3537 .objects
3538 .get(seg1_id.0)
3539 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3540 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3541 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3542 };
3543 let seg1_ast = match seg1_segment {
3544 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, LINE_VARIABLE, None)?,
3545 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, ARC_VARIABLE, None)?,
3546 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3547 _ => {
3548 return Err(KclError::refactor(format!(
3549 "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3550 )));
3551 }
3552 };
3553
3554 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3555 let (sketch_block_ref, _) = self.mutate_ast(
3556 new_ast,
3557 sketch_id,
3558 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3559 )?;
3560 Ok(sketch_block_ref)
3561 }
3562
3563 async fn add_symmetric(
3564 &mut self,
3565 sketch: ObjectId,
3566 symmetric: Symmetric,
3567 new_ast: &mut ast::Node<ast::Program>,
3568 ) -> Result<AstNodeRef, KclError> {
3569 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3570 return Err(KclError::refactor(format!(
3571 "Symmetric constraint must have exactly 2 inputs, got {}",
3572 symmetric.input.len()
3573 )));
3574 };
3575 let sketch_id = sketch;
3576
3577 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3578 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3579 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3580
3581 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3582 let (sketch_block_ref, _) = self.mutate_ast(
3583 new_ast,
3584 sketch_id,
3585 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3586 )?;
3587 Ok(sketch_block_ref)
3588 }
3589
3590 async fn add_midpoint(
3591 &mut self,
3592 sketch: ObjectId,
3593 midpoint: Midpoint,
3594 new_ast: &mut ast::Node<ast::Program>,
3595 ) -> Result<AstNodeRef, KclError> {
3596 let sketch_id = sketch;
3597 let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3598
3599 let segment_object = self
3600 .scene_graph
3601 .objects
3602 .get(midpoint.segment.0)
3603 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3604 let ObjectKind::Segment {
3605 segment: midpoint_segment,
3606 } = &segment_object.kind
3607 else {
3608 return Err(KclError::refactor(format!(
3609 "Object must be a segment, but it was {}",
3610 segment_object.kind.human_friendly_kind_with_article()
3611 )));
3612 };
3613 let segment_ast = match midpoint_segment {
3614 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3615 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3616 _ => {
3617 return Err(KclError::refactor(format!(
3618 "Midpoint target must be a line or arc segment but it was {}",
3619 midpoint_segment.human_friendly_kind_with_article()
3620 )));
3621 }
3622 };
3623
3624 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3625 let (sketch_block_ref, _) = self.mutate_ast(
3626 new_ast,
3627 sketch_id,
3628 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3629 )?;
3630 Ok(sketch_block_ref)
3631 }
3632
3633 async fn add_equal_radius(
3634 &mut self,
3635 sketch: ObjectId,
3636 equal_radius: EqualRadius,
3637 new_ast: &mut ast::Node<ast::Program>,
3638 ) -> Result<AstNodeRef, KclError> {
3639 if equal_radius.input.len() < 2 {
3640 return Err(KclError::refactor(format!(
3641 "equalRadius constraint must have at least 2 segments, got {}",
3642 equal_radius.input.len()
3643 )));
3644 }
3645
3646 let sketch_id = sketch;
3647 let input_asts = equal_radius
3648 .input
3649 .iter()
3650 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3651 .collect::<Result<Vec<_>, _>>()?;
3652
3653 let equal_radius_ast = create_equal_radius_ast(input_asts);
3654 let (sketch_block_ref, _) = self.mutate_ast(
3655 new_ast,
3656 sketch_id,
3657 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3658 )?;
3659 Ok(sketch_block_ref)
3660 }
3661
3662 async fn add_radius(
3663 &mut self,
3664 sketch: ObjectId,
3665 radius: Radius,
3666 new_ast: &mut ast::Node<ast::Program>,
3667 ) -> Result<AstNodeRef, KclError> {
3668 let params = ArcSizeConstraintParams {
3669 points: vec![radius.arc],
3670 function_name: RADIUS_FN,
3671 value: radius.radius.value,
3672 units: radius.radius.units,
3673 label_position: radius.label_position,
3674 constraint_type_name: "Radius",
3675 };
3676 self.add_arc_size_constraint(sketch, params, new_ast).await
3677 }
3678
3679 async fn add_diameter(
3680 &mut self,
3681 sketch: ObjectId,
3682 diameter: Diameter,
3683 new_ast: &mut ast::Node<ast::Program>,
3684 ) -> Result<AstNodeRef, KclError> {
3685 let params = ArcSizeConstraintParams {
3686 points: vec![diameter.arc],
3687 function_name: DIAMETER_FN,
3688 value: diameter.diameter.value,
3689 units: diameter.diameter.units,
3690 label_position: diameter.label_position,
3691 constraint_type_name: "Diameter",
3692 };
3693 self.add_arc_size_constraint(sketch, params, new_ast).await
3694 }
3695
3696 async fn add_fixed_constraints(
3697 &mut self,
3698 sketch: ObjectId,
3699 points: Vec<FixedPoint>,
3700 new_ast: &mut ast::Node<ast::Program>,
3701 ) -> Result<AstNodeRef, KclError> {
3702 let mut sketch_block_ref = None;
3703
3704 for fixed_point in points {
3705 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3706 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3707 .map_err(|err| KclError::refactor(err.to_string()))?;
3708
3709 let (sketch_ref, _) = self.mutate_ast(
3710 new_ast,
3711 sketch,
3712 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3713 )?;
3714 sketch_block_ref = Some(sketch_ref);
3715 }
3716
3717 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3718 }
3719
3720 async fn add_arc_size_constraint(
3721 &mut self,
3722 sketch: ObjectId,
3723 params: ArcSizeConstraintParams,
3724 new_ast: &mut ast::Node<ast::Program>,
3725 ) -> Result<AstNodeRef, KclError> {
3726 let sketch_id = sketch;
3727
3728 if params.points.len() != 1 {
3730 return Err(KclError::refactor(format!(
3731 "{} constraint must have exactly 1 argument (an arc segment), got {}",
3732 params.constraint_type_name,
3733 params.points.len()
3734 )));
3735 }
3736
3737 let arc_id = params.points[0];
3738 let arc_object = self
3739 .scene_graph
3740 .objects
3741 .get(arc_id.0)
3742 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3743 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3744 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3745 };
3746 let ref_type = match arc_segment {
3747 Segment::Arc(_) => ARC_VARIABLE,
3748 Segment::Circle(_) => CIRCLE_VARIABLE,
3749 _ => {
3750 return Err(KclError::refactor(format!(
3751 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3752 params.constraint_type_name
3753 )));
3754 }
3755 };
3756 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3758 let arguments = match ¶ms.label_position {
3759 Some(label_position) => vec![ast::LabeledArg {
3760 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3761 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3762 }],
3763 None => Default::default(),
3764 };
3765
3766 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3768 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3769 unlabeled: Some(arc_ast),
3770 arguments,
3771 digest: None,
3772 non_code_meta: Default::default(),
3773 })));
3774 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3775 left: call_ast,
3776 operator: ast::BinaryOperator::Eq,
3777 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3778 value: ast::LiteralValue::Number {
3779 value: params.value,
3780 suffix: params.units,
3781 },
3782 raw: format_number_literal(params.value, params.units, None)
3783 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3784 digest: None,
3785 }))),
3786 digest: None,
3787 })));
3788
3789 let (sketch_block_ref, _) = self.mutate_ast(
3791 new_ast,
3792 sketch_id,
3793 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3794 )?;
3795 Ok(sketch_block_ref)
3796 }
3797
3798 async fn add_horizontal_distance(
3799 &mut self,
3800 sketch: ObjectId,
3801 distance: Distance,
3802 new_ast: &mut ast::Node<ast::Program>,
3803 ) -> Result<AstNodeRef, KclError> {
3804 let sketch_id = sketch;
3805 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3806 [pt0, pt1] => [
3807 self.coincident_segment_to_ast(pt0, new_ast)?,
3808 self.coincident_segment_to_ast(pt1, new_ast)?,
3809 ],
3810 _ => {
3811 return Err(KclError::refactor(format!(
3812 "Horizontal distance constraint must have exactly 2 points, got {}",
3813 distance.points.len()
3814 )));
3815 }
3816 };
3817
3818 let arguments = match &distance.label_position {
3819 Some(label_position) => vec![ast::LabeledArg {
3820 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3821 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3822 }],
3823 None => Default::default(),
3824 };
3825
3826 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3828 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3829 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3830 ast::ArrayExpression {
3831 elements: vec![pt0_ast, pt1_ast],
3832 digest: None,
3833 non_code_meta: Default::default(),
3834 },
3835 )))),
3836 arguments,
3837 digest: None,
3838 non_code_meta: Default::default(),
3839 })));
3840 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3841 left: distance_call_ast,
3842 operator: ast::BinaryOperator::Eq,
3843 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3844 value: ast::LiteralValue::Number {
3845 value: distance.distance.value,
3846 suffix: distance.distance.units,
3847 },
3848 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3849 KclError::refactor(format!(
3850 "Could not format numeric suffix: {:?}",
3851 distance.distance.units
3852 ))
3853 })?,
3854 digest: None,
3855 }))),
3856 digest: None,
3857 })));
3858
3859 let (sketch_block_ref, _) = self.mutate_ast(
3861 new_ast,
3862 sketch_id,
3863 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3864 )?;
3865 Ok(sketch_block_ref)
3866 }
3867
3868 async fn add_vertical_distance(
3869 &mut self,
3870 sketch: ObjectId,
3871 distance: Distance,
3872 new_ast: &mut ast::Node<ast::Program>,
3873 ) -> Result<AstNodeRef, KclError> {
3874 let sketch_id = sketch;
3875 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3876 [pt0, pt1] => [
3877 self.coincident_segment_to_ast(pt0, new_ast)?,
3878 self.coincident_segment_to_ast(pt1, new_ast)?,
3879 ],
3880 _ => {
3881 return Err(KclError::refactor(format!(
3882 "Vertical distance constraint must have exactly 2 points, got {}",
3883 distance.points.len()
3884 )));
3885 }
3886 };
3887
3888 let arguments = match &distance.label_position {
3889 Some(label_position) => vec![ast::LabeledArg {
3890 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3891 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3892 }],
3893 None => Default::default(),
3894 };
3895
3896 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3898 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3899 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3900 ast::ArrayExpression {
3901 elements: vec![pt0_ast, pt1_ast],
3902 digest: None,
3903 non_code_meta: Default::default(),
3904 },
3905 )))),
3906 arguments,
3907 digest: None,
3908 non_code_meta: Default::default(),
3909 })));
3910 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3911 left: distance_call_ast,
3912 operator: ast::BinaryOperator::Eq,
3913 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3914 value: ast::LiteralValue::Number {
3915 value: distance.distance.value,
3916 suffix: distance.distance.units,
3917 },
3918 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3919 KclError::refactor(format!(
3920 "Could not format numeric suffix: {:?}",
3921 distance.distance.units
3922 ))
3923 })?,
3924 digest: None,
3925 }))),
3926 digest: None,
3927 })));
3928
3929 let (sketch_block_ref, _) = self.mutate_ast(
3931 new_ast,
3932 sketch_id,
3933 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3934 )?;
3935 Ok(sketch_block_ref)
3936 }
3937
3938 async fn add_horizontal(
3939 &mut self,
3940 sketch: ObjectId,
3941 horizontal: Horizontal,
3942 new_ast: &mut ast::Node<ast::Program>,
3943 ) -> Result<AstNodeRef, KclError> {
3944 let sketch_id = sketch;
3945
3946 let first_arg_ast = match horizontal {
3948 Horizontal::Line { line } => {
3949 let line_object = self
3950 .scene_graph
3951 .objects
3952 .get(line.0)
3953 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3954 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3955 let kind = line_object.kind.human_friendly_kind_with_article();
3956 return Err(KclError::refactor(format!(
3957 "This constraint only works on Segments, but you selected {kind}"
3958 )));
3959 };
3960 let Segment::Line(_) = line_segment else {
3961 return Err(KclError::refactor(format!(
3962 "Only lines can be made horizontal, but you selected {}",
3963 line_segment.human_friendly_kind_with_article(),
3964 )));
3965 };
3966 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3967 }
3968 Horizontal::Points { points } => {
3969 let point_asts = points
3970 .iter()
3971 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3972 .collect::<Result<Vec<_>, _>>()?;
3973 ast::ArrayExpression::new(point_asts).into()
3974 }
3975 };
3976
3977 let horizontal_ast = create_horizontal_ast(first_arg_ast);
3979
3980 let (sketch_block_ref, _) = self.mutate_ast(
3982 new_ast,
3983 sketch_id,
3984 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3985 )?;
3986 Ok(sketch_block_ref)
3987 }
3988
3989 async fn add_lines_equal_length(
3990 &mut self,
3991 sketch: ObjectId,
3992 lines_equal_length: LinesEqualLength,
3993 new_ast: &mut ast::Node<ast::Program>,
3994 ) -> Result<AstNodeRef, KclError> {
3995 if lines_equal_length.lines.len() < 2 {
3996 return Err(KclError::refactor(format!(
3997 "Lines equal length constraint must have at least 2 lines, got {}",
3998 lines_equal_length.lines.len()
3999 )));
4000 };
4001
4002 let sketch_id = sketch;
4003
4004 let line_asts = lines_equal_length
4006 .lines
4007 .iter()
4008 .map(|line_id| {
4009 let line_object = self
4010 .scene_graph
4011 .objects
4012 .get(line_id.0)
4013 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4014 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4015 let kind = line_object.kind.human_friendly_kind_with_article();
4016 return Err(KclError::refactor(format!(
4017 "This constraint only works on Segments, but you selected {kind}"
4018 )));
4019 };
4020 let Segment::Line(_) = line_segment else {
4021 let kind = line_segment.human_friendly_kind_with_article();
4022 return Err(KclError::refactor(format!(
4023 "Only lines can be made equal length, but you selected {kind}"
4024 )));
4025 };
4026
4027 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
4028 })
4029 .collect::<Result<Vec<_>, _>>()?;
4030
4031 let equal_length_ast = create_equal_length_ast(line_asts);
4033
4034 let (sketch_block_ref, _) = self.mutate_ast(
4036 new_ast,
4037 sketch_id,
4038 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4039 )?;
4040 Ok(sketch_block_ref)
4041 }
4042
4043 fn equal_radius_segment_id_to_ast_reference(
4044 &mut self,
4045 segment_id: ObjectId,
4046 new_ast: &mut ast::Node<ast::Program>,
4047 ) -> Result<ast::Expr, KclError> {
4048 let segment_object = self
4049 .scene_graph
4050 .objects
4051 .get(segment_id.0)
4052 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4053 let ObjectKind::Segment { segment } = &segment_object.kind else {
4054 return Err(KclError::refactor(format!(
4055 "Object is not a segment, it was {}",
4056 segment_object.kind.human_friendly_kind_with_article()
4057 )));
4058 };
4059
4060 let ref_type = match segment {
4061 Segment::Arc(_) => ARC_VARIABLE,
4062 Segment::Circle(_) => CIRCLE_VARIABLE,
4063 _ => {
4064 return Err(KclError::refactor(format!(
4065 "equalRadius supports only arc/circle segments, got {}",
4066 segment.human_friendly_kind_with_article()
4067 )));
4068 }
4069 };
4070
4071 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4072 }
4073
4074 fn symmetric_input_id_to_ast_reference(
4075 &mut self,
4076 segment_id: ObjectId,
4077 new_ast: &mut ast::Node<ast::Program>,
4078 ) -> Result<ast::Expr, KclError> {
4079 let segment_object = self
4080 .scene_graph
4081 .objects
4082 .get(segment_id.0)
4083 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4084 let ObjectKind::Segment { segment } = &segment_object.kind else {
4085 return Err(KclError::refactor(format!(
4086 "Object is not a segment, it was {}",
4087 segment_object.kind.human_friendly_kind_with_article()
4088 )));
4089 };
4090
4091 match segment {
4092 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4093 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4094 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4095 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4096 }
4097 }
4098
4099 fn symmetric_axis_id_to_ast_reference(
4100 &mut self,
4101 segment_id: ObjectId,
4102 new_ast: &mut ast::Node<ast::Program>,
4103 ) -> Result<ast::Expr, KclError> {
4104 let segment_object = self
4105 .scene_graph
4106 .objects
4107 .get(segment_id.0)
4108 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4109 let ObjectKind::Segment { segment } = &segment_object.kind else {
4110 return Err(KclError::refactor(format!(
4111 "Object is not a segment, it was {}",
4112 segment_object.kind.human_friendly_kind_with_article()
4113 )));
4114 };
4115 match segment {
4116 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4117 _ => Err(KclError::refactor(format!(
4118 "Symmetric axis must be a line, got {}",
4119 segment.human_friendly_kind_with_article()
4120 ))),
4121 }
4122 }
4123
4124 async fn add_parallel(
4125 &mut self,
4126 sketch: ObjectId,
4127 parallel: Parallel,
4128 new_ast: &mut ast::Node<ast::Program>,
4129 ) -> Result<AstNodeRef, KclError> {
4130 if parallel.lines.len() < 2 {
4131 return Err(KclError::refactor(format!(
4132 "Parallel constraint must have at least 2 lines, got {}",
4133 parallel.lines.len()
4134 )));
4135 };
4136
4137 let sketch_id = sketch;
4138
4139 let line_asts = parallel
4140 .lines
4141 .iter()
4142 .map(|line_id| {
4143 let line_object = self
4144 .scene_graph
4145 .objects
4146 .get(line_id.0)
4147 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4148 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4149 let kind = line_object.kind.human_friendly_kind_with_article();
4150 return Err(KclError::refactor(format!(
4151 "This constraint only works on Segments, but you selected {kind}"
4152 )));
4153 };
4154 let Segment::Line(_) = line_segment else {
4155 let kind = line_segment.human_friendly_kind_with_article();
4156 return Err(KclError::refactor(format!(
4157 "Only lines can be made parallel, but you selected {kind}"
4158 )));
4159 };
4160
4161 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
4162 })
4163 .collect::<Result<Vec<_>, _>>()?;
4164
4165 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4166 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4167 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4168 ast::ArrayExpression {
4169 elements: line_asts,
4170 digest: None,
4171 non_code_meta: Default::default(),
4172 },
4173 )))),
4174 arguments: Default::default(),
4175 digest: None,
4176 non_code_meta: Default::default(),
4177 })));
4178
4179 let (sketch_block_ref, _) = self.mutate_ast(
4180 new_ast,
4181 sketch_id,
4182 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4183 )?;
4184 Ok(sketch_block_ref)
4185 }
4186
4187 async fn add_perpendicular(
4188 &mut self,
4189 sketch: ObjectId,
4190 perpendicular: Perpendicular,
4191 new_ast: &mut ast::Node<ast::Program>,
4192 ) -> Result<AstNodeRef, KclError> {
4193 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4194 .await
4195 }
4196
4197 async fn add_lines_at_angle_constraint(
4198 &mut self,
4199 sketch: ObjectId,
4200 angle_kind: LinesAtAngleKind,
4201 lines: Vec<ObjectId>,
4202 new_ast: &mut ast::Node<ast::Program>,
4203 ) -> Result<AstNodeRef, KclError> {
4204 let &[line0_id, line1_id] = lines.as_slice() else {
4205 return Err(KclError::refactor(format!(
4206 "{} constraint must have exactly 2 lines, got {}",
4207 angle_kind.to_function_name(),
4208 lines.len()
4209 )));
4210 };
4211
4212 let sketch_id = sketch;
4213
4214 let line0_object = self
4216 .scene_graph
4217 .objects
4218 .get(line0_id.0)
4219 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4220 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4221 let kind = line0_object.kind.human_friendly_kind_with_article();
4222 return Err(KclError::refactor(format!(
4223 "This constraint only works on Segments, but you selected {kind}"
4224 )));
4225 };
4226 let Segment::Line(_) = line0_segment else {
4227 return Err(KclError::refactor(format!(
4228 "Only lines can be made {}, but you selected {}",
4229 angle_kind.to_function_name(),
4230 line0_segment.human_friendly_kind_with_article(),
4231 )));
4232 };
4233 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
4234
4235 let line1_object = self
4236 .scene_graph
4237 .objects
4238 .get(line1_id.0)
4239 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4240 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4241 let kind = line1_object.kind.human_friendly_kind_with_article();
4242 return Err(KclError::refactor(format!(
4243 "This constraint only works on Segments, but you selected {kind}"
4244 )));
4245 };
4246 let Segment::Line(_) = line1_segment else {
4247 return Err(KclError::refactor(format!(
4248 "Only lines can be made {}, but you selected {}",
4249 angle_kind.to_function_name(),
4250 line1_segment.human_friendly_kind_with_article(),
4251 )));
4252 };
4253 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
4254
4255 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4257 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4258 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4259 ast::ArrayExpression {
4260 elements: vec![line0_ast, line1_ast],
4261 digest: None,
4262 non_code_meta: Default::default(),
4263 },
4264 )))),
4265 arguments: Default::default(),
4266 digest: None,
4267 non_code_meta: Default::default(),
4268 })));
4269
4270 let (sketch_block_ref, _) = self.mutate_ast(
4272 new_ast,
4273 sketch_id,
4274 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4275 )?;
4276 Ok(sketch_block_ref)
4277 }
4278
4279 async fn add_vertical(
4280 &mut self,
4281 sketch: ObjectId,
4282 vertical: Vertical,
4283 new_ast: &mut ast::Node<ast::Program>,
4284 ) -> Result<AstNodeRef, KclError> {
4285 let sketch_id = sketch;
4286
4287 let first_arg_ast = match vertical {
4288 Vertical::Line { line } => {
4289 let line_object = self
4291 .scene_graph
4292 .objects
4293 .get(line.0)
4294 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4295 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4296 let kind = line_object.kind.human_friendly_kind_with_article();
4297 return Err(KclError::refactor(format!(
4298 "This constraint only works on Segments, but you selected {kind}"
4299 )));
4300 };
4301 let Segment::Line(_) = line_segment else {
4302 return Err(KclError::refactor(format!(
4303 "Only lines can be made vertical, but you selected {}",
4304 line_segment.human_friendly_kind_with_article()
4305 )));
4306 };
4307 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
4308 }
4309 Vertical::Points { points } => {
4310 let point_asts = points
4311 .iter()
4312 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4313 .collect::<Result<Vec<_>, _>>()?;
4314 ast::ArrayExpression::new(point_asts).into()
4315 }
4316 };
4317
4318 let vertical_ast = create_vertical_ast(first_arg_ast);
4320
4321 let (sketch_block_ref, _) = self.mutate_ast(
4323 new_ast,
4324 sketch_id,
4325 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4326 )?;
4327 Ok(sketch_block_ref)
4328 }
4329
4330 async fn execute_after_add_constraint(
4331 &mut self,
4332 ctx: &ExecutorContext,
4333 sketch_id: ObjectId,
4334 sketch_block_ref: AstNodeRef,
4335 new_ast: &mut ast::Node<ast::Program>,
4336 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4337 let new_source = source_from_ast(new_ast);
4339 let (new_program, errors) = Program::parse(&new_source)
4341 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4342 if !errors.is_empty() {
4343 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4344 "Error parsing KCL source after adding constraint: {errors:?}"
4345 ))));
4346 }
4347 let Some(new_program) = new_program else {
4348 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4349 "No AST produced after adding constraint".to_string(),
4350 )));
4351 };
4352 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4353 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4354 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4355 )))
4356 })?;
4357
4358 let mut truncated_program = new_program.clone();
4361 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4362 .map_err(KclErrorWithOutputs::no_outputs)?;
4363
4364 let mock_config = self.sketch_mock_config(sketch_id, true, Default::default(), false);
4366 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
4367
4368 let new_object_ids = {
4369 let constraint_id = outcome
4371 .source_range_to_object
4372 .get(&constraint_node_ref.range)
4373 .copied()
4374 .ok_or_else(|| {
4375 KclErrorWithOutputs::from_error_outcome(
4376 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4377 outcome.clone(),
4378 )
4379 })?;
4380 vec![constraint_id]
4381 };
4382
4383 self.program = new_program;
4386
4387 let outcome = self.update_state_after_exec(outcome, true);
4389 self.replace_sketch_var_warm_starts(sketch_id, &outcome);
4390
4391 let src_delta = SourceDelta { text: new_source };
4392 let scene_graph_delta = SceneGraphDelta {
4393 new_graph: self.scene_graph.clone(),
4394 invalidates_ids: false,
4395 new_objects: new_object_ids,
4396 exec_outcome: outcome,
4397 };
4398 Ok((src_delta, scene_graph_delta))
4399 }
4400
4401 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4403 if segment_ids_set.contains(&segment_id) {
4404 return true;
4405 }
4406
4407 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4408 return false;
4409 };
4410 let ObjectKind::Segment { segment } = &segment_object.kind else {
4411 return false;
4412 };
4413 let Segment::Point(point) = segment else {
4414 return false;
4415 };
4416
4417 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4418 }
4419
4420 fn remaining_constraint_segments(
4421 &self,
4422 segments: &[ConstraintSegment],
4423 segment_ids_set: &AhashIndexSet<ObjectId>,
4424 ) -> Vec<ConstraintSegment> {
4425 segments
4426 .iter()
4427 .copied()
4428 .filter(|segment| match segment {
4429 ConstraintSegment::Origin(_) => true,
4430 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4431 })
4432 .collect()
4433 }
4434
4435 fn find_referenced_constraints(
4436 &self,
4437 sketch_id: ObjectId,
4438 segment_ids_set: &AhashIndexSet<ObjectId>,
4439 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4440 let sketch_object = self
4442 .scene_graph
4443 .objects
4444 .get(sketch_id.0)
4445 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4446 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4447 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4448 };
4449 let mut constraint_ids_set = AhashIndexSet::default();
4450 for constraint_id in &sketch.constraints {
4451 let constraint_object = self
4452 .scene_graph
4453 .objects
4454 .get(constraint_id.0)
4455 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4456 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4457 return Err(KclError::refactor(format!(
4458 "Object is not a constraint, it is {}",
4459 constraint_object.kind.human_friendly_kind_with_article()
4460 )));
4461 };
4462 let depends_on_segment = match constraint {
4463 Constraint::Coincident(c) => c
4464 .segment_ids()
4465 .any(|seg_id| self.segment_will_be_deleted(seg_id, segment_ids_set)),
4466 Constraint::Distance(d) => d
4467 .point_ids()
4468 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4469 Constraint::Fixed(fixed) => fixed
4470 .points
4471 .iter()
4472 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4473 Constraint::Radius(r) => self.segment_will_be_deleted(r.arc, segment_ids_set),
4474 Constraint::Diameter(d) => self.segment_will_be_deleted(d.arc, segment_ids_set),
4475 Constraint::EqualRadius(equal_radius) => equal_radius
4476 .input
4477 .iter()
4478 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4479 Constraint::HorizontalDistance(d) => d
4480 .point_ids()
4481 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4482 Constraint::VerticalDistance(d) => d
4483 .point_ids()
4484 .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4485 Constraint::Horizontal(h) => match h {
4486 Horizontal::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4487 Horizontal::Points { points } => points.iter().any(|point| match point {
4488 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4489 ConstraintSegment::Origin(_) => false,
4490 }),
4491 },
4492 Constraint::Vertical(v) => match v {
4493 Vertical::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4494 Vertical::Points { points } => points.iter().any(|point| match point {
4495 ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4496 ConstraintSegment::Origin(_) => false,
4497 }),
4498 },
4499 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4500 .lines
4501 .iter()
4502 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4503 Constraint::Midpoint(midpoint) => {
4504 self.segment_will_be_deleted(midpoint.segment, segment_ids_set)
4505 || self.segment_will_be_deleted(midpoint.point, segment_ids_set)
4506 }
4507 Constraint::Parallel(parallel) => parallel
4508 .lines
4509 .iter()
4510 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4511 Constraint::Perpendicular(perpendicular) => perpendicular
4512 .lines
4513 .iter()
4514 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4515 Constraint::Angle(angle) => angle
4516 .lines
4517 .iter()
4518 .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4519 Constraint::Symmetric(symmetric) => {
4520 self.segment_will_be_deleted(symmetric.axis, segment_ids_set)
4521 || symmetric
4522 .input
4523 .iter()
4524 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set))
4525 }
4526 Constraint::Tangent(tangent) => tangent
4527 .input
4528 .iter()
4529 .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4530 };
4531 if depends_on_segment {
4532 constraint_ids_set.insert(*constraint_id);
4533 }
4534 }
4535 Ok(constraint_ids_set)
4536 }
4537
4538 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4539 let mut outcome = outcome;
4540 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4541
4542 if freedom_analysis_ran {
4543 self.point_freedom_cache.clear();
4546 for new_obj in &new_objects {
4547 if let ObjectKind::Segment {
4548 segment: crate::front::Segment::Point(point),
4549 } = &new_obj.kind
4550 {
4551 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4552 }
4553 }
4554 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4555 self.scene_graph.objects = new_objects;
4557 } else {
4558 for old_obj in &self.scene_graph.objects {
4561 if let ObjectKind::Segment {
4562 segment: crate::front::Segment::Point(point),
4563 } = &old_obj.kind
4564 {
4565 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4566 }
4567 }
4568
4569 let mut updated_objects = Vec::with_capacity(new_objects.len());
4571 for new_obj in new_objects {
4572 let mut obj = new_obj;
4573 if let ObjectKind::Segment {
4574 segment: crate::front::Segment::Point(point),
4575 } = &mut obj.kind
4576 {
4577 let new_freedom = point.freedom;
4578 match new_freedom {
4584 Freedom::Free => {
4585 match self.point_freedom_cache.get(&obj.id).copied() {
4586 Some(Freedom::Conflict) => {
4587 }
4590 Some(Freedom::Fixed) => {
4591 point.freedom = Freedom::Fixed;
4593 }
4594 Some(Freedom::Free) => {
4595 }
4597 None => {
4598 }
4600 }
4601 }
4602 Freedom::Fixed => {
4603 }
4605 Freedom::Conflict => {
4606 }
4608 }
4609 self.point_freedom_cache.insert(obj.id, point.freedom);
4611 }
4612 updated_objects.push(obj);
4613 }
4614
4615 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4616 self.scene_graph.objects = updated_objects;
4617 }
4618 outcome
4619 }
4620
4621 fn mutate_ast(
4622 &mut self,
4623 ast: &mut ast::Node<ast::Program>,
4624 object_id: ObjectId,
4625 command: AstMutateCommand,
4626 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4627 let sketch_object = self
4628 .scene_graph
4629 .objects
4630 .get(object_id.0)
4631 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4632 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4633 }
4634}
4635
4636fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4637 let sketch_object = scene_graph
4639 .objects
4640 .get(sketch_id.0)
4641 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4642 let ObjectKind::Sketch(_) = &sketch_object.kind else {
4643 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4644 };
4645 expect_single_node_ref(sketch_object)
4646}
4647
4648fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4649 match &object.source {
4650 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4651 range: *range,
4652 node_path: node_path.clone(),
4653 }),
4654 SourceRef::BackTrace { ranges } => {
4655 let [range] = ranges.as_slice() else {
4656 return Err(KclError::refactor(format!(
4657 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4658 ranges.len()
4659 )));
4660 };
4661 Ok(AstNodeRef {
4662 range: range.0,
4663 node_path: range.1.clone(),
4664 })
4665 }
4666 }
4667}
4668
4669fn only_sketch_block_from_range(
4672 ast: &mut ast::Node<ast::Program>,
4673 sketch_block_range: SourceRange,
4674 edit_kind: ChangeKind,
4675) -> Result<(), KclError> {
4676 let r1 = sketch_block_range;
4677 let matches_range = |r2: SourceRange| -> bool {
4678 match edit_kind {
4681 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4682 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4684 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4685 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4687 }
4688 };
4689 let mut found = false;
4690 for item in ast.body.iter_mut() {
4691 match item {
4692 ast::BodyItem::ImportStatement(_) => {}
4693 ast::BodyItem::ExpressionStatement(node) => {
4694 if matches_range(SourceRange::from(&*node))
4695 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4696 {
4697 sketch_block.is_being_edited = true;
4698 found = true;
4699 break;
4700 }
4701 }
4702 ast::BodyItem::VariableDeclaration(node) => {
4703 if matches_range(SourceRange::from(&node.declaration.init))
4704 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4705 {
4706 sketch_block.is_being_edited = true;
4707 found = true;
4708 break;
4709 }
4710 }
4711 ast::BodyItem::TypeDeclaration(_) => {}
4712 ast::BodyItem::ReturnStatement(node) => {
4713 if matches_range(SourceRange::from(&node.argument))
4714 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4715 {
4716 sketch_block.is_being_edited = true;
4717 found = true;
4718 break;
4719 }
4720 }
4721 }
4722 }
4723 if !found {
4724 return Err(KclError::refactor(format!(
4725 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4726 )));
4727 }
4728
4729 Ok(())
4730}
4731
4732fn only_sketch_block(
4733 ast: &mut ast::Node<ast::Program>,
4734 sketch_block_ref: &AstNodeRef,
4735 edit_kind: ChangeKind,
4736) -> Result<(), KclError> {
4737 let Some(target_node_path) = &sketch_block_ref.node_path else {
4738 #[cfg(target_arch = "wasm32")]
4739 web_sys::console::warn_1(
4740 &format!(
4741 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4742 &sketch_block_ref
4743 )
4744 .into(),
4745 );
4746 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4747 };
4748 let mut found = false;
4749 for item in ast.body.iter_mut() {
4750 match item {
4751 ast::BodyItem::ImportStatement(_) => {}
4752 ast::BodyItem::ExpressionStatement(node) => {
4753 if let Some(node_path) = &node.node_path
4755 && node_path == target_node_path
4756 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4757 {
4758 sketch_block.is_being_edited = true;
4759 found = true;
4760 break;
4761 }
4762 if let Some(node_path) = node.expression.node_path()
4764 && node_path == target_node_path
4765 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4766 {
4767 sketch_block.is_being_edited = true;
4768 found = true;
4769 break;
4770 }
4771 }
4772 ast::BodyItem::VariableDeclaration(node) => {
4773 if let Some(node_path) = node.declaration.init.node_path()
4774 && node_path == target_node_path
4775 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4776 {
4777 sketch_block.is_being_edited = true;
4778 found = true;
4779 break;
4780 }
4781 }
4782 ast::BodyItem::TypeDeclaration(_) => {}
4783 ast::BodyItem::ReturnStatement(node) => {
4784 if let Some(node_path) = node.argument.node_path()
4785 && node_path == target_node_path
4786 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4787 {
4788 sketch_block.is_being_edited = true;
4789 found = true;
4790 break;
4791 }
4792 }
4793 }
4794 }
4795 if !found {
4796 return Err(KclError::refactor(format!(
4797 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4798 )));
4799 }
4800
4801 Ok(())
4802}
4803
4804fn sketch_on_ast_expr(
4805 ast: &mut ast::Node<ast::Program>,
4806 scene_graph: &SceneGraph,
4807 on: &Plane,
4808) -> Result<ast::Expr, KclError> {
4809 match on {
4810 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4811 Plane::Object(object_id) => {
4812 let on_object = scene_graph
4813 .objects
4814 .get(object_id.0)
4815 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4816 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4817 return Ok(face_expr);
4818 }
4819 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4820 }
4821 }
4822}
4823
4824fn sketch_face_of_scene_object_ast_expr(
4825 ast: &mut ast::Node<ast::Program>,
4826 on_object: &crate::front::Object,
4827) -> Result<Option<ast::Expr>, KclError> {
4828 let SourceRef::BackTrace { ranges } = &on_object.source else {
4829 return Ok(None);
4830 };
4831
4832 match &on_object.kind {
4833 ObjectKind::Wall(_) => {
4834 let [sweep_range, segment_range] = ranges.as_slice() else {
4835 return Err(KclError::refactor(format!(
4836 "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4837 ranges.len(),
4838 on_object.artifact_id
4839 )));
4840 };
4841 let sweep_ref = get_or_insert_ast_reference(
4842 ast,
4843 &SourceRef::Simple {
4844 range: sweep_range.0,
4845 node_path: sweep_range.1.clone(),
4846 },
4847 "solid",
4848 None,
4849 )?;
4850 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4851 return Err(KclError::refactor(format!(
4852 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4853 on_object.artifact_id
4854 )));
4855 };
4856 let solid_name = solid_name_expr.name.name.clone();
4857 let solid_expr = ast_name_expr(solid_name.clone());
4858 let segment_ref = get_or_insert_ast_reference(
4859 ast,
4860 &SourceRef::Simple {
4861 range: segment_range.0,
4862 node_path: segment_range.1.clone(),
4863 },
4864 LINE_VARIABLE,
4865 None,
4866 )?;
4867
4868 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4869 let ast::Expr::Name(segment_name_expr) = segment_ref else {
4870 return Err(KclError::refactor(format!(
4871 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4872 on_object.artifact_id
4873 )));
4874 };
4875 create_member_expression(
4876 create_member_expression(ast_name_expr(region_name), "tags"),
4877 &segment_name_expr.name.name,
4878 )
4879 } else {
4880 segment_ref
4881 };
4882
4883 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4884 }
4885 ObjectKind::Cap(cap) => {
4886 let [range] = ranges.as_slice() else {
4887 return Err(KclError::refactor(format!(
4888 "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4889 ranges.len(),
4890 on_object.artifact_id
4891 )));
4892 };
4893 let sweep_ref = get_or_insert_ast_reference(
4894 ast,
4895 &SourceRef::Simple {
4896 range: range.0,
4897 node_path: range.1.clone(),
4898 },
4899 "solid",
4900 None,
4901 )?;
4902 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4903 return Err(KclError::refactor(format!(
4904 "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4905 on_object.artifact_id
4906 )));
4907 };
4908 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4909 let face_expr = match cap.kind {
4911 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4912 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4913 };
4914
4915 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4916 }
4917 _ => Ok(None),
4918 }
4919}
4920
4921fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4922 let mut existing_artifact_ids = scene_objects
4923 .iter()
4924 .map(|object| object.artifact_id)
4925 .collect::<HashSet<_>>();
4926
4927 for artifact in artifact_graph.values() {
4928 match artifact {
4929 Artifact::Wall(wall) => {
4930 if existing_artifact_ids.contains(&wall.id) {
4931 continue;
4932 }
4933
4934 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4935 Artifact::Segment(segment) => Some(segment),
4936 _ => None,
4937 }) else {
4938 continue;
4939 };
4940 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4941 Artifact::Sweep(sweep) => Some(sweep),
4942 _ => None,
4943 }) else {
4944 continue;
4945 };
4946 let source_segment = segment
4947 .original_seg_id
4948 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4949 .and_then(|artifact| match artifact {
4950 Artifact::Segment(segment) => Some(segment),
4951 _ => None,
4952 })
4953 .unwrap_or(segment);
4954 let id = ObjectId(scene_objects.len());
4955 scene_objects.push(crate::front::Object {
4956 id,
4957 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4958 label: Default::default(),
4959 comments: Default::default(),
4960 artifact_id: wall.id,
4961 source: SourceRef::BackTrace {
4962 ranges: vec![
4963 (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4964 (
4965 source_segment.code_ref.range,
4966 Some(source_segment.code_ref.node_path.clone()),
4967 ),
4968 ],
4969 },
4970 });
4971 existing_artifact_ids.insert(wall.id);
4972 }
4973 Artifact::Cap(cap) => {
4974 if existing_artifact_ids.contains(&cap.id) {
4975 continue;
4976 }
4977
4978 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4979 Artifact::Sweep(sweep) => Some(sweep),
4980 _ => None,
4981 }) else {
4982 continue;
4983 };
4984 let id = ObjectId(scene_objects.len());
4985 let kind = match cap.sub_type {
4986 CapSubType::Start => crate::frontend::api::CapKind::Start,
4987 CapSubType::End => crate::frontend::api::CapKind::End,
4988 };
4989 scene_objects.push(crate::front::Object {
4990 id,
4991 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4992 label: Default::default(),
4993 comments: Default::default(),
4994 artifact_id: cap.id,
4995 source: SourceRef::BackTrace {
4996 ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4997 },
4998 });
4999 existing_artifact_ids.insert(cap.id);
5000 }
5001 _ => {}
5002 }
5003 }
5004}
5005
5006fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5007 use crate::engine::PlaneName;
5008
5009 match name {
5010 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5011 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5012 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5013 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5014 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5015 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5016 }
5017}
5018
5019fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5020 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5021 ast::UnaryOperator::Neg,
5022 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5023 )))
5024}
5025
5026fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5027 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5028 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5029 unlabeled: Some(solid_expr),
5030 arguments: vec![ast::LabeledArg {
5031 label: Some(ast::Identifier::new("face")),
5032 arg: face_expr,
5033 }],
5034 digest: None,
5035 non_code_meta: Default::default(),
5036 })))
5037}
5038
5039fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5040 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5041 return None;
5042 };
5043 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5044 return None;
5045 };
5046 if !matches!(
5047 sweep_call.callee.name.name.as_str(),
5048 "extrude" | "revolve" | "sweep" | "loft"
5049 ) {
5050 return None;
5051 }
5052 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5053 return None;
5054 };
5055 let candidate = region_name_expr.name.name.clone();
5056 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5057 return None;
5058 };
5059 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5060 return None;
5061 };
5062 if region_call.callee.name.name != "region" {
5063 return None;
5064 }
5065 Some(candidate)
5066}
5067
5068fn get_or_insert_ast_reference(
5075 ast: &mut ast::Node<ast::Program>,
5076 source_ref: &SourceRef,
5077 prefix: &str,
5078 property: Option<&str>,
5079) -> Result<ast::Expr, KclError> {
5080 let command = AstMutateCommand::AddVariableDeclaration {
5081 prefix: prefix.to_owned(),
5082 };
5083 let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
5084 let AstMutateCommandReturn::Name(var_name) = ret else {
5085 return Err(KclError::refactor(
5086 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5087 ));
5088 };
5089 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5090 let Some(property) = property else {
5091 return Ok(var_expr);
5093 };
5094
5095 Ok(create_member_expression(var_expr, property))
5096}
5097
5098fn mutate_ast_node_by_source_ref(
5099 ast: &mut ast::Node<ast::Program>,
5100 source_ref: &SourceRef,
5101 command: AstMutateCommand,
5102) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5103 let (source_range, node_path) = match source_ref {
5104 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5105 SourceRef::BackTrace { ranges } => {
5106 let [range] = ranges.as_slice() else {
5107 return Err(KclError::refactor(format!(
5108 "Expected single source ref, got {}; ranges={ranges:#?}",
5109 ranges.len(),
5110 )));
5111 };
5112 (range.0, range.1.clone())
5113 }
5114 };
5115 let mut context = AstMutateContext {
5116 source_range,
5117 node_path,
5118 command,
5119 defined_names_stack: Default::default(),
5120 };
5121 let control = dfs_mut(ast, &mut context);
5122 match control {
5123 ControlFlow::Continue(_) => Err(KclError::refactor(
5124 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5125 )),
5126 ControlFlow::Break(break_value) => break_value,
5127 }
5128}
5129
5130#[derive(Debug)]
5131struct AstMutateContext {
5132 source_range: SourceRange,
5133 node_path: Option<ast::NodePath>,
5134 command: AstMutateCommand,
5135 defined_names_stack: Vec<HashSet<String>>,
5136}
5137
5138#[derive(Debug)]
5139#[allow(clippy::large_enum_variant)]
5140enum AstMutateCommand {
5141 AddSketchBlockExprStmt {
5143 expr: ast::Expr,
5144 },
5145 AddSketchBlockVarDecl {
5147 prefix: String,
5148 expr: ast::Expr,
5149 },
5150 AddVariableDeclaration {
5151 prefix: String,
5152 },
5153 EditPoint {
5154 at: ast::Expr,
5155 },
5156 EditLine {
5157 start: ast::Expr,
5158 end: ast::Expr,
5159 construction: Option<bool>,
5160 },
5161 EditArc {
5162 start: ast::Expr,
5163 end: ast::Expr,
5164 center: ast::Expr,
5165 construction: Option<bool>,
5166 },
5167 EditCircle {
5168 start: ast::Expr,
5169 center: ast::Expr,
5170 construction: Option<bool>,
5171 },
5172 EditConstraintValue {
5173 value: ast::BinaryPart,
5174 },
5175 EditDistanceConstraintLabelPosition {
5176 label_position: ast::Expr,
5177 },
5178 EditCallUnlabeled {
5179 arg: ast::Expr,
5180 },
5181 EditVarInitialValue {
5182 value: Number,
5183 },
5184 DeleteNode,
5185}
5186
5187impl AstMutateCommand {
5188 fn needs_defined_names_stack(&self) -> bool {
5189 matches!(
5190 self,
5191 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5192 )
5193 }
5194}
5195
5196#[derive(Debug)]
5197enum AstMutateCommandReturn {
5198 None,
5199 Name(String),
5200}
5201
5202#[derive(Debug, Clone)]
5203struct AstNodeRef {
5204 range: SourceRange,
5205 node_path: Option<ast::NodePath>,
5206}
5207
5208impl<T> From<&ast::Node<T>> for AstNodeRef {
5209 fn from(value: &ast::Node<T>) -> Self {
5210 AstNodeRef {
5211 range: value.into(),
5212 node_path: value.node_path.clone(),
5213 }
5214 }
5215}
5216
5217impl From<&ast::BodyItem> for AstNodeRef {
5218 fn from(value: &ast::BodyItem) -> Self {
5219 match value {
5220 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5221 range: node.into(),
5222 node_path: node.node_path.clone(),
5223 },
5224 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5225 range: node.into(),
5226 node_path: node.node_path.clone(),
5227 },
5228 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5229 range: node.into(),
5230 node_path: node.node_path.clone(),
5231 },
5232 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5233 range: node.into(),
5234 node_path: node.node_path.clone(),
5235 },
5236 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5237 range: node.into(),
5238 node_path: node.node_path.clone(),
5239 },
5240 }
5241 }
5242}
5243
5244impl From<&ast::Expr> for AstNodeRef {
5245 fn from(value: &ast::Expr) -> Self {
5246 AstNodeRef {
5247 range: SourceRange::from(value),
5248 node_path: value.node_path().cloned(),
5249 }
5250 }
5251}
5252
5253impl From<&AstMutateContext> for AstNodeRef {
5254 fn from(value: &AstMutateContext) -> Self {
5255 AstNodeRef {
5256 range: value.source_range,
5257 node_path: value.node_path.clone(),
5258 }
5259 }
5260}
5261
5262impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5263 type Error = crate::walk::AstNodeError;
5264
5265 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5266 Ok(AstNodeRef {
5267 range: SourceRange::try_from(value)?,
5268 node_path: value.try_into()?,
5269 })
5270 }
5271}
5272
5273impl From<AstNodeRef> for SourceRange {
5274 fn from(value: AstNodeRef) -> Self {
5275 value.range
5276 }
5277}
5278
5279impl Visitor for AstMutateContext {
5280 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5281 type Continue = ();
5282
5283 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5284 filter_and_process(self, node)
5285 }
5286
5287 fn finish(&mut self, node: NodeMut<'_>) {
5288 match &node {
5289 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5290 self.defined_names_stack.pop();
5291 }
5292 _ => {}
5293 }
5294 }
5295}
5296
5297fn filter_and_process(
5298 ctx: &mut AstMutateContext,
5299 node: NodeMut,
5300) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5301 let Ok(node_range) = SourceRange::try_from(&node) else {
5302 return TraversalReturn::new_continue(());
5304 };
5305 if let NodeMut::VariableDeclaration(var_decl) = &node {
5310 let expr_range = SourceRange::from(&var_decl.declaration.init);
5311 let expr_node_path = var_decl.declaration.init.node_path();
5312 if source_ref_matches(ctx, expr_range, expr_node_path) {
5313 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5314 return TraversalReturn::new_break(Ok((
5317 AstNodeRef::from(&**var_decl),
5318 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5319 )));
5320 }
5321 if let AstMutateCommand::DeleteNode = &ctx.command {
5322 return TraversalReturn {
5325 mutate_body_item: MutateBodyItem::Delete,
5326 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5327 };
5328 }
5329 }
5330 }
5331 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5334 let expr_range = SourceRange::from(&expr_stmt.expression);
5335 let expr_node_path = expr_stmt.expression.node_path();
5336 if source_ref_matches(ctx, expr_range, expr_node_path) {
5337 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5338 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5341 return TraversalReturn::new_continue(());
5342 };
5343 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5344 }
5345 if let AstMutateCommand::DeleteNode = &ctx.command {
5346 return TraversalReturn {
5349 mutate_body_item: MutateBodyItem::Delete,
5350 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5351 };
5352 }
5353 }
5354 }
5355
5356 if ctx.command.needs_defined_names_stack() {
5357 if let NodeMut::Program(program) = &node {
5358 ctx.defined_names_stack.push(find_defined_names(*program));
5359 } else if let NodeMut::SketchBlock(block) = &node {
5360 ctx.defined_names_stack.push(find_defined_names(&block.body));
5361 }
5362 }
5363
5364 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5366 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5367 return TraversalReturn::new_continue(());
5368 }
5369 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5370 return TraversalReturn::new_continue(());
5371 };
5372 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5373}
5374
5375fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5376 match &ctx.node_path {
5377 Some(target) => Some(target) == node_path,
5378 None => node_range == ctx.source_range,
5379 }
5380}
5381
5382fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5383 match &ctx.command {
5384 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5385 if let NodeMut::SketchBlock(sketch_block) = node {
5386 sketch_block
5387 .body
5388 .items
5389 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5390 inner: ast::ExpressionStatement {
5391 expression: expr.clone(),
5392 digest: None,
5393 },
5394 start: Default::default(),
5395 end: Default::default(),
5396 module_id: Default::default(),
5397 node_path: None,
5398 outer_attrs: Default::default(),
5399 pre_comments: Default::default(),
5400 comment_start: Default::default(),
5401 }));
5402 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5403 }
5404 }
5405 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5406 if let NodeMut::SketchBlock(sketch_block) = node {
5407 let empty_defined_names = HashSet::new();
5408 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5409 let Ok(name) = next_free_name(prefix, defined_names) else {
5410 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5411 };
5412 sketch_block
5413 .body
5414 .items
5415 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5416 ast::VariableDeclaration::new(
5417 ast::VariableDeclarator::new(&name, expr.clone()),
5418 ast::ItemVisibility::Default,
5419 ast::VariableKind::Const,
5420 ),
5421 ))));
5422 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5423 }
5424 }
5425 AstMutateCommand::AddVariableDeclaration { prefix } => {
5426 if let NodeMut::VariableDeclaration(inner) = node {
5427 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5428 }
5429 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5430 let empty_defined_names = HashSet::new();
5431 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5432 let Ok(name) = next_free_name(prefix, defined_names) else {
5433 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5435 };
5436 let mutate_node =
5437 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5438 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5439 ast::ItemVisibility::Default,
5440 ast::VariableKind::Const,
5441 ))));
5442 return TraversalReturn {
5443 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5444 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5445 };
5446 }
5447 }
5448 AstMutateCommand::EditPoint { at } => {
5449 if let NodeMut::CallExpressionKw(call) = node {
5450 if call.callee.name.name != POINT_FN {
5451 return TraversalReturn::new_continue(());
5452 }
5453 for labeled_arg in &mut call.arguments {
5455 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5456 labeled_arg.arg = at.clone();
5457 }
5458 }
5459 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5460 }
5461 }
5462 AstMutateCommand::EditLine {
5463 start,
5464 end,
5465 construction,
5466 } => {
5467 if let NodeMut::CallExpressionKw(call) = node {
5468 if call.callee.name.name != LINE_FN {
5469 return TraversalReturn::new_continue(());
5470 }
5471 for labeled_arg in &mut call.arguments {
5473 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5474 labeled_arg.arg = start.clone();
5475 }
5476 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5477 labeled_arg.arg = end.clone();
5478 }
5479 }
5480 if let Some(construction_value) = construction {
5482 let construction_exists = call
5483 .arguments
5484 .iter()
5485 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5486 if *construction_value {
5487 if construction_exists {
5489 for labeled_arg in &mut call.arguments {
5491 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5492 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5493 value: ast::LiteralValue::Bool(true),
5494 raw: "true".to_string(),
5495 digest: None,
5496 })));
5497 }
5498 }
5499 } else {
5500 call.arguments.push(ast::LabeledArg {
5502 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5503 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5504 value: ast::LiteralValue::Bool(true),
5505 raw: "true".to_string(),
5506 digest: None,
5507 }))),
5508 });
5509 }
5510 } else {
5511 call.arguments
5513 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5514 }
5515 }
5516 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5517 }
5518 }
5519 AstMutateCommand::EditArc {
5520 start,
5521 end,
5522 center,
5523 construction,
5524 } => {
5525 if let NodeMut::CallExpressionKw(call) = node {
5526 if call.callee.name.name != ARC_FN {
5527 return TraversalReturn::new_continue(());
5528 }
5529 for labeled_arg in &mut call.arguments {
5531 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5532 labeled_arg.arg = start.clone();
5533 }
5534 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5535 labeled_arg.arg = end.clone();
5536 }
5537 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5538 labeled_arg.arg = center.clone();
5539 }
5540 }
5541 if let Some(construction_value) = construction {
5543 let construction_exists = call
5544 .arguments
5545 .iter()
5546 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5547 if *construction_value {
5548 if construction_exists {
5550 for labeled_arg in &mut call.arguments {
5552 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5553 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5554 value: ast::LiteralValue::Bool(true),
5555 raw: "true".to_string(),
5556 digest: None,
5557 })));
5558 }
5559 }
5560 } else {
5561 call.arguments.push(ast::LabeledArg {
5563 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5564 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5565 value: ast::LiteralValue::Bool(true),
5566 raw: "true".to_string(),
5567 digest: None,
5568 }))),
5569 });
5570 }
5571 } else {
5572 call.arguments
5574 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5575 }
5576 }
5577 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5578 }
5579 }
5580 AstMutateCommand::EditCircle {
5581 start,
5582 center,
5583 construction,
5584 } => {
5585 if let NodeMut::CallExpressionKw(call) = node {
5586 if call.callee.name.name != CIRCLE_FN {
5587 return TraversalReturn::new_continue(());
5588 }
5589 for labeled_arg in &mut call.arguments {
5591 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5592 labeled_arg.arg = start.clone();
5593 }
5594 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5595 labeled_arg.arg = center.clone();
5596 }
5597 }
5598 if let Some(construction_value) = construction {
5600 let construction_exists = call
5601 .arguments
5602 .iter()
5603 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5604 if *construction_value {
5605 if construction_exists {
5606 for labeled_arg in &mut call.arguments {
5607 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5608 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5609 value: ast::LiteralValue::Bool(true),
5610 raw: "true".to_string(),
5611 digest: None,
5612 })));
5613 }
5614 }
5615 } else {
5616 call.arguments.push(ast::LabeledArg {
5617 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5618 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5619 value: ast::LiteralValue::Bool(true),
5620 raw: "true".to_string(),
5621 digest: None,
5622 }))),
5623 });
5624 }
5625 } else {
5626 call.arguments
5627 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5628 }
5629 }
5630 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5631 }
5632 }
5633 AstMutateCommand::EditConstraintValue { value } => {
5634 if let NodeMut::BinaryExpression(binary_expr) = node {
5635 let left_is_constraint = matches!(
5636 &binary_expr.left,
5637 ast::BinaryPart::CallExpressionKw(call)
5638 if matches!(
5639 call.callee.name.name.as_str(),
5640 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5641 )
5642 );
5643 if left_is_constraint {
5644 binary_expr.right = value.clone();
5645 } else {
5646 binary_expr.left = value.clone();
5647 }
5648
5649 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5650 }
5651 }
5652 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5653 if let NodeMut::BinaryExpression(binary_expr) = node {
5654 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5655 return TraversalReturn::new_continue(());
5656 };
5657 if !matches!(
5658 call.callee.name.name.as_str(),
5659 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
5660 ) {
5661 return TraversalReturn::new_continue(());
5662 }
5663
5664 if let Some(label_arg) = call
5665 .arguments
5666 .iter_mut()
5667 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5668 {
5669 label_arg.arg = label_position.clone();
5670 } else {
5671 call.arguments.push(ast::LabeledArg {
5672 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5673 arg: label_position.clone(),
5674 });
5675 }
5676
5677 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5678 }
5679 }
5680 AstMutateCommand::EditCallUnlabeled { arg } => {
5681 if let NodeMut::CallExpressionKw(call) = node {
5682 call.unlabeled = Some(arg.clone());
5683 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5684 }
5685 }
5686 AstMutateCommand::EditVarInitialValue { value } => {
5687 if let NodeMut::NumericLiteral(numeric_literal) = node {
5688 let Ok(literal) = to_source_number(*value) else {
5690 return TraversalReturn::new_break(Err(KclError::refactor(format!(
5691 "Could not convert number to AST literal: {:?}",
5692 *value
5693 ))));
5694 };
5695 *numeric_literal = ast::Node::no_src(literal);
5696 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5697 }
5698 }
5699 AstMutateCommand::DeleteNode => {
5700 return TraversalReturn {
5701 mutate_body_item: MutateBodyItem::Delete,
5702 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5703 };
5704 }
5705 }
5706 TraversalReturn::new_continue(())
5707}
5708
5709struct FindSketchBlockSourceRange {
5710 target_before_mutation: SourceRange,
5712 found: Cell<Option<AstNodeRef>>,
5716}
5717
5718impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5719 type Error = crate::front::Error;
5720
5721 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5722 let Ok(node_range) = SourceRange::try_from(&node) else {
5723 return Ok(true);
5724 };
5725
5726 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5727 if node_range.module_id() == self.target_before_mutation.module_id()
5728 && node_range.start() == self.target_before_mutation.start()
5729 && node_range.end() >= self.target_before_mutation.end()
5731 {
5732 self.found.set(sketch_block.body.items.last().map(|item| match item {
5733 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5737 _ => AstNodeRef::from(item),
5738 }));
5739 return Ok(false);
5740 } else {
5741 return Ok(true);
5744 }
5745 }
5746
5747 for child in node.children().iter() {
5748 if !child.visit(*self)? {
5749 return Ok(false);
5750 }
5751 }
5752
5753 Ok(true)
5754 }
5755}
5756
5757struct FindSketchBlockByNodePath {
5758 target_node_path: ast::NodePath,
5760 found: Cell<Option<AstNodeRef>>,
5764}
5765
5766impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5767 type Error = crate::front::Error;
5768
5769 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5770 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5771 return Ok(true);
5772 };
5773
5774 if let crate::walk::Node::SketchBlock(sketch_block) = node {
5775 if let Some(node_path) = node_path
5776 && node_path == self.target_node_path
5777 {
5778 self.found.set(sketch_block.body.items.last().map(|item| match item {
5779 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5783 _ => AstNodeRef::from(item),
5784 }));
5785
5786 return Ok(false);
5787 } else {
5788 return Ok(true);
5791 }
5792 }
5793
5794 for child in node.children().iter() {
5795 if !child.visit(*self)? {
5796 return Ok(false);
5797 }
5798 }
5799
5800 Ok(true)
5801 }
5802}
5803
5804fn find_sketch_block_added_item(
5812 ast: &ast::Node<ast::Program>,
5813 sketch_block_before_mutation: &AstNodeRef,
5814) -> Result<AstNodeRef, KclError> {
5815 if let Some(node_path) = &sketch_block_before_mutation.node_path {
5816 let find = FindSketchBlockByNodePath {
5817 target_node_path: node_path.clone(),
5818 found: Cell::new(None),
5819 };
5820 let node = crate::walk::Node::from(ast);
5821 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5822 find.found.into_inner().ok_or_else(|| {
5823 KclError::refactor(format!(
5824 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5825 ))
5826 })
5827 } else {
5828 let find = FindSketchBlockSourceRange {
5830 target_before_mutation: sketch_block_before_mutation.range,
5831 found: Cell::new(None),
5832 };
5833 let node = crate::walk::Node::from(ast);
5834 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5835 find.found.into_inner().ok_or_else(|| KclError::refactor(
5836 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?"),
5837 ))
5838 }
5839}
5840
5841fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5842 ast.recast_top(&Default::default(), 0)
5844}
5845
5846pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5847 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5848 inner: ast::ArrayExpression {
5849 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5850 non_code_meta: Default::default(),
5851 digest: None,
5852 },
5853 start: Default::default(),
5854 end: Default::default(),
5855 module_id: Default::default(),
5856 node_path: None,
5857 outer_attrs: Default::default(),
5858 pre_comments: Default::default(),
5859 comment_start: Default::default(),
5860 })))
5861}
5862
5863fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
5864 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
5865 ast::ArrayExpression {
5866 elements: vec![
5867 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5868 point.x,
5869 )?)))),
5870 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5871 point.y,
5872 )?)))),
5873 ],
5874 non_code_meta: Default::default(),
5875 digest: None,
5876 },
5877 ))))
5878}
5879
5880fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5881 match expr {
5882 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5883 inner: ast::Literal::from(to_source_number(*number)?),
5884 start: Default::default(),
5885 end: Default::default(),
5886 module_id: Default::default(),
5887 node_path: None,
5888 outer_attrs: Default::default(),
5889 pre_comments: Default::default(),
5890 comment_start: Default::default(),
5891 }))),
5892 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5893 inner: ast::SketchVar {
5894 initial: Some(Box::new(ast::Node {
5895 inner: to_source_number(*number)?,
5896 start: Default::default(),
5897 end: Default::default(),
5898 module_id: Default::default(),
5899 node_path: None,
5900 outer_attrs: Default::default(),
5901 pre_comments: Default::default(),
5902 comment_start: Default::default(),
5903 })),
5904 digest: None,
5905 },
5906 start: Default::default(),
5907 end: Default::default(),
5908 module_id: Default::default(),
5909 node_path: None,
5910 outer_attrs: Default::default(),
5911 pre_comments: Default::default(),
5912 comment_start: Default::default(),
5913 }))),
5914 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5915 }
5916}
5917
5918fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5919 Ok(ast::NumericLiteral {
5920 value: number.value,
5921 suffix: number.units,
5922 raw: format_number_literal(number.value, number.units, None)?,
5923 digest: None,
5924 })
5925}
5926
5927#[cfg(feature = "artifact-graph")]
5928fn sketch_var_initial_value_in_solver_units(number: &ast::Node<ast::NumericLiteral>) -> f64 {
5929 let unit = match number.suffix {
5930 NumericSuffix::Cm => UnitLength::Centimeters,
5931 NumericSuffix::M => UnitLength::Meters,
5932 NumericSuffix::Inch => UnitLength::Inches,
5933 NumericSuffix::Ft => UnitLength::Feet,
5934 NumericSuffix::Yd => UnitLength::Yards,
5935 _ => UnitLength::Millimeters,
5936 };
5937 crate::execution::types::adjust_length(unit, number.value, UnitLength::Millimeters).0
5938}
5939
5940#[cfg(feature = "artifact-graph")]
5941fn source_ref_primary_range(source: &SourceRef) -> Option<SourceRange> {
5942 match source {
5943 SourceRef::Simple { range, .. } => Some(*range),
5944 SourceRef::BackTrace { ranges } => ranges.first().map(|(range, _)| *range),
5945 }
5946}
5947
5948pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5949 ast::Expr::Name(Box::new(ast_name(name)))
5950}
5951
5952fn ast_name(name: String) -> ast::Node<ast::Name> {
5953 ast::Node {
5954 inner: ast::Name {
5955 name: ast::Node {
5956 inner: ast::Identifier { name, digest: None },
5957 start: Default::default(),
5958 end: Default::default(),
5959 module_id: Default::default(),
5960 node_path: None,
5961 outer_attrs: Default::default(),
5962 pre_comments: Default::default(),
5963 comment_start: Default::default(),
5964 },
5965 path: Vec::new(),
5966 abs_path: false,
5967 digest: None,
5968 },
5969 start: Default::default(),
5970 end: Default::default(),
5971 module_id: Default::default(),
5972 node_path: None,
5973 outer_attrs: Default::default(),
5974 pre_comments: Default::default(),
5975 comment_start: Default::default(),
5976 }
5977}
5978
5979pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5980 ast::Name {
5981 name: ast::Node {
5982 inner: ast::Identifier {
5983 name: name.to_owned(),
5984 digest: None,
5985 },
5986 start: Default::default(),
5987 end: Default::default(),
5988 module_id: Default::default(),
5989 node_path: None,
5990 outer_attrs: Default::default(),
5991 pre_comments: Default::default(),
5992 comment_start: Default::default(),
5993 },
5994 path: Default::default(),
5995 abs_path: false,
5996 digest: None,
5997 }
5998}
5999
6000pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6004 let elements = exprs.into_iter().collect::<Vec<_>>();
6005 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6006
6007 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6009 elements,
6010 digest: None,
6011 non_code_meta: Default::default(),
6012 })));
6013
6014 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6016 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6017 unlabeled: Some(array_expr),
6018 arguments: Default::default(),
6019 digest: None,
6020 non_code_meta: Default::default(),
6021 })))
6022}
6023
6024pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6026 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6027 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6028 unlabeled: None,
6029 arguments: vec![
6030 ast::LabeledArg {
6031 label: Some(ast::Identifier::new(LINE_START_PARAM)),
6032 arg: start_ast,
6033 },
6034 ast::LabeledArg {
6035 label: Some(ast::Identifier::new(LINE_END_PARAM)),
6036 arg: end_ast,
6037 },
6038 ],
6039 digest: None,
6040 non_code_meta: Default::default(),
6041 })))
6042}
6043
6044pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6046 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6047 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6048 unlabeled: None,
6049 arguments: vec![
6050 ast::LabeledArg {
6051 label: Some(ast::Identifier::new(ARC_START_PARAM)),
6052 arg: start_ast,
6053 },
6054 ast::LabeledArg {
6055 label: Some(ast::Identifier::new(ARC_END_PARAM)),
6056 arg: end_ast,
6057 },
6058 ast::LabeledArg {
6059 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6060 arg: center_ast,
6061 },
6062 ],
6063 digest: None,
6064 non_code_meta: Default::default(),
6065 })))
6066}
6067
6068pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6070 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6071 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6072 unlabeled: None,
6073 arguments: vec![
6074 ast::LabeledArg {
6075 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6076 arg: start_ast,
6077 },
6078 ast::LabeledArg {
6079 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6080 arg: center_ast,
6081 },
6082 ],
6083 digest: None,
6084 non_code_meta: Default::default(),
6085 })))
6086}
6087
6088pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6090 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6091 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6092 unlabeled: Some(line_expr),
6093 arguments: Default::default(),
6094 digest: None,
6095 non_code_meta: Default::default(),
6096 })))
6097}
6098
6099pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6101 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6102 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6103 unlabeled: Some(line_expr),
6104 arguments: Default::default(),
6105 digest: None,
6106 non_code_meta: Default::default(),
6107 })))
6108}
6109
6110pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6112 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6113 object: object_expr,
6114 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6115 name: ast::Node::no_src(ast::Identifier {
6116 name: property.to_string(),
6117 digest: None,
6118 }),
6119 path: Vec::new(),
6120 abs_path: false,
6121 digest: None,
6122 }))),
6123 computed: false,
6124 digest: None,
6125 })))
6126}
6127
6128fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6130 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6132 position.x,
6133 )?))));
6134 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6135 position.y,
6136 )?))));
6137 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6138 elements: vec![x_literal, y_literal],
6139 digest: None,
6140 non_code_meta: Default::default(),
6141 })));
6142
6143 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6145 elements: vec![point_expr, point_array],
6146 digest: None,
6147 non_code_meta: Default::default(),
6148 })));
6149
6150 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6152 ast::CallExpressionKw {
6153 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6154 unlabeled: Some(array_expr),
6155 arguments: Default::default(),
6156 digest: None,
6157 non_code_meta: Default::default(),
6158 },
6159 ))))
6160}
6161
6162pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6164 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6165 elements: line_exprs,
6166 digest: None,
6167 non_code_meta: Default::default(),
6168 })));
6169
6170 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6172 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6173 unlabeled: Some(array_expr),
6174 arguments: Default::default(),
6175 digest: None,
6176 non_code_meta: Default::default(),
6177 })))
6178}
6179
6180pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6182 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6183 elements: segment_exprs,
6184 digest: None,
6185 non_code_meta: Default::default(),
6186 })));
6187
6188 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6189 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6190 unlabeled: Some(array_expr),
6191 arguments: Default::default(),
6192 digest: None,
6193 non_code_meta: Default::default(),
6194 })))
6195}
6196
6197pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6199 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6200 elements: vec![seg1_expr, seg2_expr],
6201 digest: None,
6202 non_code_meta: Default::default(),
6203 })));
6204
6205 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6206 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6207 unlabeled: Some(array_expr),
6208 arguments: Default::default(),
6209 digest: None,
6210 non_code_meta: Default::default(),
6211 })))
6212}
6213
6214pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6216 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6217 elements: input_exprs,
6218 digest: None,
6219 non_code_meta: Default::default(),
6220 })));
6221 let arguments = vec![ast::LabeledArg {
6222 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6223 arg: axis_expr,
6224 }];
6225
6226 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6227 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6228 unlabeled: Some(array_expr),
6229 arguments,
6230 digest: None,
6231 non_code_meta: Default::default(),
6232 })))
6233}
6234
6235pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6237 let arguments = vec![ast::LabeledArg {
6238 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6239 arg: point_expr,
6240 }];
6241
6242 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6243 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6244 unlabeled: Some(segment_expr),
6245 arguments,
6246 digest: None,
6247 non_code_meta: Default::default(),
6248 })))
6249}
6250
6251#[cfg(test)]
6252mod tests {
6253 use super::*;
6254 use crate::engine::PlaneName;
6255 use crate::execution::cache::SketchModeState;
6256 use crate::execution::cache::clear_mem_cache;
6257 use crate::execution::cache::read_old_memory;
6258 use crate::execution::cache::write_old_memory;
6259 use crate::front::Distance;
6260 use crate::front::Fixed;
6261 use crate::front::FixedPoint;
6262 use crate::front::Midpoint;
6263 use crate::front::Object;
6264 use crate::front::Plane;
6265 use crate::front::Sketch;
6266 use crate::front::Tangent;
6267 use crate::frontend::sketch::Vertical;
6268 use crate::pretty::NumericSuffix;
6269
6270 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6271 for object in &scene_graph.objects {
6272 if let ObjectKind::Sketch(_) = &object.kind {
6273 return Some(object);
6274 }
6275 }
6276 None
6277 }
6278
6279 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6280 for object in &scene_graph.objects {
6281 if let ObjectKind::Face(_) = &object.kind {
6282 return Some(object);
6283 }
6284 }
6285 None
6286 }
6287
6288 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6289 for object in &scene_graph.objects {
6290 if matches!(&object.kind, ObjectKind::Wall(_)) {
6291 return Some(object.id);
6292 }
6293 }
6294 None
6295 }
6296
6297 #[test]
6298 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6299 let source = "\
6300region001 = region(point = [0.1, 0.1], sketch = s)
6301extrude001 = extrude(region001, length = 5)
6302revolve001 = revolve(region001, axis = Y)
6303sweep001 = sweep(region001, path = path001)
6304loft001 = loft(region001)
6305not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6306";
6307
6308 let program = Program::parse(source).unwrap().0.unwrap();
6309
6310 assert_eq!(
6311 region_name_from_sweep_variable(&program.ast, "extrude001"),
6312 Some("region001".to_owned())
6313 );
6314 assert_eq!(
6315 region_name_from_sweep_variable(&program.ast, "revolve001"),
6316 Some("region001".to_owned())
6317 );
6318 assert_eq!(
6319 region_name_from_sweep_variable(&program.ast, "sweep001"),
6320 Some("region001".to_owned())
6321 );
6322 assert_eq!(
6323 region_name_from_sweep_variable(&program.ast, "loft001"),
6324 Some("region001".to_owned())
6325 );
6326 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6327 }
6328
6329 #[track_caller]
6330 fn expect_sketch(object: &Object) -> &Sketch {
6331 if let ObjectKind::Sketch(sketch) = &object.kind {
6332 sketch
6333 } else {
6334 panic!("Object is not a sketch: {:?}", object);
6335 }
6336 }
6337
6338 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6339 let point_object = scene_graph.objects.get(point_id.0).unwrap();
6340 let ObjectKind::Segment {
6341 segment: Segment::Point(point),
6342 } = &point_object.kind
6343 else {
6344 panic!("Object is not a point segment: {point_object:?}");
6345 };
6346 point.position.clone()
6347 }
6348
6349 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6350 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6351 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6352 }
6353
6354 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6355 LineCtor {
6356 start: Point2d {
6357 x: Expr::Number(Number { value: start_x, units }),
6358 y: Expr::Number(Number { value: start_y, units }),
6359 },
6360 end: Point2d {
6361 x: Expr::Number(Number { value: end_x, units }),
6362 y: Expr::Number(Number { value: end_y, units }),
6363 },
6364 construction: None,
6365 }
6366 }
6367
6368 async fn create_sketch_with_single_line(
6369 frontend: &mut FrontendState,
6370 ctx: &ExecutorContext,
6371 mock_ctx: &ExecutorContext,
6372 version: Version,
6373 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6374 frontend.program = Program::empty();
6375
6376 let sketch_args = SketchCtor {
6377 on: Plane::Default(PlaneName::Xy),
6378 };
6379 let (_src_delta, _scene_delta, sketch_id) = frontend
6380 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6381 .await
6382 .unwrap();
6383
6384 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6385 let (source_delta, scene_graph_delta) = frontend
6386 .add_segment(mock_ctx, version, sketch_id, segment, None)
6387 .await
6388 .unwrap();
6389 let line_id = *scene_graph_delta
6390 .new_objects
6391 .last()
6392 .expect("Expected line object id to be created");
6393
6394 (sketch_id, line_id, source_delta, scene_graph_delta)
6395 }
6396
6397 #[tokio::test(flavor = "multi_thread")]
6398 async fn test_sketch_checkpoint_round_trip_restores_state() {
6399 let mut frontend = FrontendState::new();
6400 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6401 let mock_ctx = ExecutorContext::new_mock(None).await;
6402 let version = Version(0);
6403
6404 let (sketch_id, line_id, source_delta, scene_graph_delta) =
6405 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6406
6407 let expected_source = source_delta.text.clone();
6408 let expected_scene_graph = frontend.scene_graph.clone();
6409 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6410 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6411
6412 let checkpoint_id = frontend
6413 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6414 .await
6415 .unwrap();
6416
6417 let edited_segments = vec![ExistingSegmentCtor {
6418 id: line_id,
6419 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6420 }];
6421 let (edited_source, _edited_scene) = frontend
6422 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6423 .await
6424 .unwrap();
6425 assert_ne!(edited_source.text, expected_source);
6426
6427 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6428
6429 assert_eq!(restored.source_delta.text, expected_source);
6430 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6431 assert!(restored.scene_graph_delta.invalidates_ids);
6432 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6433 assert_eq!(frontend.scene_graph, expected_scene_graph);
6434 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6435
6436 ctx.close().await;
6437 mock_ctx.close().await;
6438 }
6439
6440 #[tokio::test(flavor = "multi_thread")]
6441 async fn test_sketch_checkpoints_prune_oldest_entries() {
6442 let mut frontend = FrontendState::new();
6443 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6444 let mock_ctx = ExecutorContext::new_mock(None).await;
6445 let version = Version(0);
6446
6447 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6448 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6449
6450 let mut checkpoint_ids = Vec::new();
6451 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6452 checkpoint_ids.push(
6453 frontend
6454 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6455 .await
6456 .unwrap(),
6457 );
6458 }
6459
6460 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6461 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6462
6463 let oldest_retained = checkpoint_ids[3];
6464 assert_eq!(
6465 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6466 Some(oldest_retained)
6467 );
6468
6469 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6470 assert!(evicted_restore.is_err());
6471 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6472
6473 frontend
6474 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6475 .await
6476 .unwrap();
6477
6478 ctx.close().await;
6479 mock_ctx.close().await;
6480 }
6481
6482 #[tokio::test(flavor = "multi_thread")]
6483 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6484 let mut frontend = FrontendState::new();
6485 let missing_checkpoint = SketchCheckpointId::new(999);
6486
6487 let err = frontend
6488 .restore_sketch_checkpoint(missing_checkpoint)
6489 .await
6490 .expect_err("Expected restore to fail for missing checkpoint");
6491
6492 assert!(err.msg.contains("Sketch checkpoint not found"));
6493 }
6494
6495 #[tokio::test(flavor = "multi_thread")]
6496 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6497 let mut frontend = FrontendState::new();
6498 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6499 let mock_ctx = ExecutorContext::new_mock(None).await;
6500 let version = Version(0);
6501
6502 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6503 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6504
6505 let checkpoint_a = frontend
6506 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6507 .await
6508 .unwrap();
6509 let checkpoint_b = frontend
6510 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6511 .await
6512 .unwrap();
6513 assert_eq!(frontend.sketch_checkpoints.len(), 2);
6514
6515 frontend.clear_sketch_checkpoints();
6516 assert!(frontend.sketch_checkpoints.is_empty());
6517 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6518 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6519
6520 ctx.close().await;
6521 mock_ctx.close().await;
6522 }
6523
6524 #[tokio::test(flavor = "multi_thread")]
6525 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6526 let mut frontend = FrontendState::new();
6527 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6528 let mock_ctx = ExecutorContext::new_mock(None).await;
6529 let version = Version(0);
6530
6531 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6532 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6533 let old_source = source_delta.text.clone();
6534 let old_checkpoint = frontend
6535 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6536 .await
6537 .unwrap();
6538 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6539
6540 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
6541 .unwrap()
6542 .0
6543 .unwrap();
6544
6545 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6546 let SetProgramOutcome::Success {
6547 checkpoint_id: Some(new_checkpoint),
6548 ..
6549 } = result
6550 else {
6551 panic!("Expected Success with a fresh checkpoint baseline");
6552 };
6553
6554 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6555
6556 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6557 assert_eq!(old_restore.source_delta.text, old_source);
6558
6559 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6560 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6561
6562 ctx.close().await;
6563 mock_ctx.close().await;
6564 }
6565
6566 #[tokio::test(flavor = "multi_thread")]
6567 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6568 let mut frontend = FrontendState::new();
6569 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6570 let mock_ctx = ExecutorContext::new_mock(None).await;
6571 let version = Version(0);
6572
6573 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6574 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6575 let old_checkpoint = frontend
6576 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6577 .await
6578 .unwrap();
6579 let checkpoint_count_before = frontend.sketch_checkpoints.len();
6580
6581 let failing_program = Program::parse(
6582 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6583 )
6584 .unwrap()
6585 .0
6586 .unwrap();
6587
6588 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6589 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6590 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6591 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6592
6593 ctx.close().await;
6594 mock_ctx.close().await;
6595 }
6596
6597 #[tokio::test(flavor = "multi_thread")]
6598 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6599 let mut frontend = FrontendState::new();
6600 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6601
6602 let program = Program::parse(
6603 "width = 2mm\nsketch001 = sketch(on = offsetPlane(XY, offset = width)) {\n line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])\n distance([line1.start, line1.end]) == width\n}\n",
6604 )
6605 .unwrap()
6606 .0
6607 .unwrap();
6608 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6609 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6610 panic!("Expected successful baseline program execution");
6611 };
6612
6613 clear_mem_cache().await;
6614 assert!(read_old_memory().await.is_none());
6615
6616 let checkpoint_without_mock_memory = frontend
6617 .create_sketch_checkpoint((*exec_outcome).clone())
6618 .await
6619 .unwrap();
6620
6621 write_old_memory(SketchModeState::new_for_tests()).await;
6622 assert!(read_old_memory().await.is_some());
6623
6624 let checkpoint_with_mock_memory = frontend
6625 .create_sketch_checkpoint((*exec_outcome).clone())
6626 .await
6627 .unwrap();
6628
6629 clear_mem_cache().await;
6630 assert!(read_old_memory().await.is_none());
6631
6632 frontend
6633 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6634 .await
6635 .unwrap();
6636 assert!(read_old_memory().await.is_some());
6637
6638 frontend
6639 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6640 .await
6641 .unwrap();
6642 assert!(read_old_memory().await.is_none());
6643
6644 ctx.close().await;
6645 }
6646
6647 #[tokio::test(flavor = "multi_thread")]
6648 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6649 let source = "\
6650sketch(on = XY) {
6651 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6652}
6653
6654bad = missing_name
6655";
6656 let program = Program::parse(source).unwrap().0.unwrap();
6657
6658 let mut frontend = FrontendState::new();
6659
6660 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6661 let mock_ctx = ExecutorContext::new_mock(None).await;
6662 let version = Version(0);
6663 let project_id = ProjectId(0);
6664 let file_id = FileId(0);
6665
6666 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6667 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6668 };
6669
6670 let sketch_id = frontend
6671 .scene_graph
6672 .objects
6673 .iter()
6674 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6675 .expect("Expected sketch object from errored hack_set_program");
6676
6677 frontend
6678 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6679 .await
6680 .unwrap();
6681
6682 ctx.close().await;
6683 mock_ctx.close().await;
6684 }
6685
6686 #[tokio::test(flavor = "multi_thread")]
6687 async fn test_new_sketch_add_point_edit_point() {
6688 let program = Program::empty();
6689
6690 let mut frontend = FrontendState::new();
6691 frontend.program = program;
6692
6693 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6694 let mock_ctx = ExecutorContext::new_mock(None).await;
6695 let version = Version(0);
6696
6697 let sketch_args = SketchCtor {
6698 on: Plane::Default(PlaneName::Xy),
6699 };
6700 let (_src_delta, scene_delta, sketch_id) = frontend
6701 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6702 .await
6703 .unwrap();
6704 assert_eq!(sketch_id, ObjectId(1));
6705 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6706 let sketch_object = &scene_delta.new_graph.objects[1];
6707 assert_eq!(sketch_object.id, ObjectId(1));
6708 assert_eq!(
6709 sketch_object.kind,
6710 ObjectKind::Sketch(Sketch {
6711 args: SketchCtor {
6712 on: Plane::Default(PlaneName::Xy)
6713 },
6714 plane: ObjectId(0),
6715 segments: vec![],
6716 constraints: vec![],
6717 })
6718 );
6719 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6720
6721 let point_ctor = PointCtor {
6722 position: Point2d {
6723 x: Expr::Number(Number {
6724 value: 1.0,
6725 units: NumericSuffix::Inch,
6726 }),
6727 y: Expr::Number(Number {
6728 value: 2.0,
6729 units: NumericSuffix::Inch,
6730 }),
6731 },
6732 };
6733 let segment = SegmentCtor::Point(point_ctor);
6734 let (src_delta, scene_delta) = frontend
6735 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6736 .await
6737 .unwrap();
6738 assert_eq!(
6739 src_delta.text.as_str(),
6740 "sketch001 = sketch(on = XY) {
6741 point(at = [1in, 2in])
6742}
6743"
6744 );
6745 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6746 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6747 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6748 assert_eq!(scene_object.id.0, i);
6749 }
6750
6751 let point_id = *scene_delta.new_objects.last().unwrap();
6752
6753 let point_ctor = PointCtor {
6754 position: Point2d {
6755 x: Expr::Number(Number {
6756 value: 3.0,
6757 units: NumericSuffix::Inch,
6758 }),
6759 y: Expr::Number(Number {
6760 value: 4.0,
6761 units: NumericSuffix::Inch,
6762 }),
6763 },
6764 };
6765 let segments = vec![ExistingSegmentCtor {
6766 id: point_id,
6767 ctor: SegmentCtor::Point(point_ctor),
6768 }];
6769 let (src_delta, scene_delta) = frontend
6770 .edit_segments(&mock_ctx, version, sketch_id, segments)
6771 .await
6772 .unwrap();
6773 assert_eq!(
6774 src_delta.text.as_str(),
6775 "sketch001 = sketch(on = XY) {
6776 point(at = [3in, 4in])
6777}
6778"
6779 );
6780 assert_eq!(scene_delta.new_objects, vec![]);
6781 assert_eq!(scene_delta.new_graph.objects.len(), 3);
6782
6783 ctx.close().await;
6784 mock_ctx.close().await;
6785 }
6786
6787 #[tokio::test(flavor = "multi_thread")]
6788 async fn test_new_sketch_add_line_edit_line() {
6789 let program = Program::empty();
6790
6791 let mut frontend = FrontendState::new();
6792 frontend.program = program;
6793
6794 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6795 let mock_ctx = ExecutorContext::new_mock(None).await;
6796 let version = Version(0);
6797
6798 let sketch_args = SketchCtor {
6799 on: Plane::Default(PlaneName::Xy),
6800 };
6801 let (_src_delta, scene_delta, sketch_id) = frontend
6802 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6803 .await
6804 .unwrap();
6805 assert_eq!(sketch_id, ObjectId(1));
6806 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6807 let sketch_object = &scene_delta.new_graph.objects[1];
6808 assert_eq!(sketch_object.id, ObjectId(1));
6809 assert_eq!(
6810 sketch_object.kind,
6811 ObjectKind::Sketch(Sketch {
6812 args: SketchCtor {
6813 on: Plane::Default(PlaneName::Xy)
6814 },
6815 plane: ObjectId(0),
6816 segments: vec![],
6817 constraints: vec![],
6818 })
6819 );
6820 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6821
6822 let line_ctor = LineCtor {
6823 start: Point2d {
6824 x: Expr::Number(Number {
6825 value: 0.0,
6826 units: NumericSuffix::Mm,
6827 }),
6828 y: Expr::Number(Number {
6829 value: 0.0,
6830 units: NumericSuffix::Mm,
6831 }),
6832 },
6833 end: Point2d {
6834 x: Expr::Number(Number {
6835 value: 10.0,
6836 units: NumericSuffix::Mm,
6837 }),
6838 y: Expr::Number(Number {
6839 value: 10.0,
6840 units: NumericSuffix::Mm,
6841 }),
6842 },
6843 construction: None,
6844 };
6845 let segment = SegmentCtor::Line(line_ctor);
6846 let (src_delta, scene_delta) = frontend
6847 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6848 .await
6849 .unwrap();
6850 assert_eq!(
6851 src_delta.text.as_str(),
6852 "sketch001 = sketch(on = XY) {
6853 line(start = [0mm, 0mm], end = [10mm, 10mm])
6854}
6855"
6856 );
6857 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6858 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6859 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6860 assert_eq!(scene_object.id.0, i);
6861 }
6862
6863 let line = *scene_delta.new_objects.last().unwrap();
6865
6866 let line_ctor = LineCtor {
6867 start: Point2d {
6868 x: Expr::Number(Number {
6869 value: 1.0,
6870 units: NumericSuffix::Mm,
6871 }),
6872 y: Expr::Number(Number {
6873 value: 2.0,
6874 units: NumericSuffix::Mm,
6875 }),
6876 },
6877 end: Point2d {
6878 x: Expr::Number(Number {
6879 value: 13.0,
6880 units: NumericSuffix::Mm,
6881 }),
6882 y: Expr::Number(Number {
6883 value: 14.0,
6884 units: NumericSuffix::Mm,
6885 }),
6886 },
6887 construction: None,
6888 };
6889 let segments = vec![ExistingSegmentCtor {
6890 id: line,
6891 ctor: SegmentCtor::Line(line_ctor),
6892 }];
6893 let (src_delta, scene_delta) = frontend
6894 .edit_segments(&mock_ctx, version, sketch_id, segments)
6895 .await
6896 .unwrap();
6897 assert_eq!(
6898 src_delta.text.as_str(),
6899 "sketch001 = sketch(on = XY) {
6900 line(start = [1mm, 2mm], end = [13mm, 14mm])
6901}
6902"
6903 );
6904 assert_eq!(scene_delta.new_objects, vec![]);
6905 assert_eq!(scene_delta.new_graph.objects.len(), 5);
6906
6907 ctx.close().await;
6908 mock_ctx.close().await;
6909 }
6910
6911 #[tokio::test(flavor = "multi_thread")]
6912 async fn test_new_sketch_add_arc_edit_arc() {
6913 let program = Program::empty();
6914
6915 let mut frontend = FrontendState::new();
6916 frontend.program = program;
6917
6918 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6919 let mock_ctx = ExecutorContext::new_mock(None).await;
6920 let version = Version(0);
6921
6922 let sketch_args = SketchCtor {
6923 on: Plane::Default(PlaneName::Xy),
6924 };
6925 let (_src_delta, scene_delta, sketch_id) = frontend
6926 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6927 .await
6928 .unwrap();
6929 assert_eq!(sketch_id, ObjectId(1));
6930 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6931 let sketch_object = &scene_delta.new_graph.objects[1];
6932 assert_eq!(sketch_object.id, ObjectId(1));
6933 assert_eq!(
6934 sketch_object.kind,
6935 ObjectKind::Sketch(Sketch {
6936 args: SketchCtor {
6937 on: Plane::Default(PlaneName::Xy),
6938 },
6939 plane: ObjectId(0),
6940 segments: vec![],
6941 constraints: vec![],
6942 })
6943 );
6944 assert_eq!(scene_delta.new_graph.objects.len(), 2);
6945
6946 let arc_ctor = ArcCtor {
6947 start: Point2d {
6948 x: Expr::Var(Number {
6949 value: 0.0,
6950 units: NumericSuffix::Mm,
6951 }),
6952 y: Expr::Var(Number {
6953 value: 0.0,
6954 units: NumericSuffix::Mm,
6955 }),
6956 },
6957 end: Point2d {
6958 x: Expr::Var(Number {
6959 value: 10.0,
6960 units: NumericSuffix::Mm,
6961 }),
6962 y: Expr::Var(Number {
6963 value: 10.0,
6964 units: NumericSuffix::Mm,
6965 }),
6966 },
6967 center: Point2d {
6968 x: Expr::Var(Number {
6969 value: 10.0,
6970 units: NumericSuffix::Mm,
6971 }),
6972 y: Expr::Var(Number {
6973 value: 0.0,
6974 units: NumericSuffix::Mm,
6975 }),
6976 },
6977 construction: None,
6978 };
6979 let segment = SegmentCtor::Arc(arc_ctor);
6980 let (src_delta, scene_delta) = frontend
6981 .add_segment(&mock_ctx, version, sketch_id, segment, None)
6982 .await
6983 .unwrap();
6984 assert_eq!(
6985 src_delta.text.as_str(),
6986 "sketch001 = sketch(on = XY) {
6987 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6988}
6989"
6990 );
6991 assert_eq!(
6992 scene_delta.new_objects,
6993 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6994 );
6995 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6996 assert_eq!(scene_object.id.0, i);
6997 }
6998 assert_eq!(scene_delta.new_graph.objects.len(), 6);
6999
7000 let arc = *scene_delta.new_objects.last().unwrap();
7002
7003 let arc_ctor = ArcCtor {
7004 start: Point2d {
7005 x: Expr::Var(Number {
7006 value: 1.0,
7007 units: NumericSuffix::Mm,
7008 }),
7009 y: Expr::Var(Number {
7010 value: 2.0,
7011 units: NumericSuffix::Mm,
7012 }),
7013 },
7014 end: Point2d {
7015 x: Expr::Var(Number {
7016 value: 13.0,
7017 units: NumericSuffix::Mm,
7018 }),
7019 y: Expr::Var(Number {
7020 value: 14.0,
7021 units: NumericSuffix::Mm,
7022 }),
7023 },
7024 center: Point2d {
7025 x: Expr::Var(Number {
7026 value: 13.0,
7027 units: NumericSuffix::Mm,
7028 }),
7029 y: Expr::Var(Number {
7030 value: 2.0,
7031 units: NumericSuffix::Mm,
7032 }),
7033 },
7034 construction: None,
7035 };
7036 let segments = vec![ExistingSegmentCtor {
7037 id: arc,
7038 ctor: SegmentCtor::Arc(arc_ctor),
7039 }];
7040 let (src_delta, scene_delta) = frontend
7041 .edit_segments(&mock_ctx, version, sketch_id, segments)
7042 .await
7043 .unwrap();
7044 assert_eq!(
7045 src_delta.text.as_str(),
7046 "sketch001 = sketch(on = XY) {
7047 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7048}
7049"
7050 );
7051 assert_eq!(scene_delta.new_objects, vec![]);
7052 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7053
7054 ctx.close().await;
7055 mock_ctx.close().await;
7056 }
7057
7058 #[tokio::test(flavor = "multi_thread")]
7059 async fn test_new_sketch_add_circle_edit_circle() {
7060 let program = Program::empty();
7061
7062 let mut frontend = FrontendState::new();
7063 frontend.program = program;
7064
7065 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7066 let mock_ctx = ExecutorContext::new_mock(None).await;
7067 let version = Version(0);
7068
7069 let sketch_args = SketchCtor {
7070 on: Plane::Default(PlaneName::Xy),
7071 };
7072 let (_src_delta, _scene_delta, sketch_id) = frontend
7073 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7074 .await
7075 .unwrap();
7076
7077 let circle_ctor = CircleCtor {
7079 start: Point2d {
7080 x: Expr::Var(Number {
7081 value: 5.0,
7082 units: NumericSuffix::Mm,
7083 }),
7084 y: Expr::Var(Number {
7085 value: 0.0,
7086 units: NumericSuffix::Mm,
7087 }),
7088 },
7089 center: Point2d {
7090 x: Expr::Var(Number {
7091 value: 0.0,
7092 units: NumericSuffix::Mm,
7093 }),
7094 y: Expr::Var(Number {
7095 value: 0.0,
7096 units: NumericSuffix::Mm,
7097 }),
7098 },
7099 construction: None,
7100 };
7101 let segment = SegmentCtor::Circle(circle_ctor);
7102 let (src_delta, scene_delta) = frontend
7103 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7104 .await
7105 .unwrap();
7106 assert_eq!(
7107 src_delta.text.as_str(),
7108 "sketch001 = sketch(on = XY) {
7109 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7110}
7111"
7112 );
7113 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7115 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7116
7117 let circle = *scene_delta.new_objects.last().unwrap();
7118
7119 let circle_ctor = CircleCtor {
7121 start: Point2d {
7122 x: Expr::Var(Number {
7123 value: 10.0,
7124 units: NumericSuffix::Mm,
7125 }),
7126 y: Expr::Var(Number {
7127 value: 0.0,
7128 units: NumericSuffix::Mm,
7129 }),
7130 },
7131 center: Point2d {
7132 x: Expr::Var(Number {
7133 value: 3.0,
7134 units: NumericSuffix::Mm,
7135 }),
7136 y: Expr::Var(Number {
7137 value: 4.0,
7138 units: NumericSuffix::Mm,
7139 }),
7140 },
7141 construction: None,
7142 };
7143 let segments = vec![ExistingSegmentCtor {
7144 id: circle,
7145 ctor: SegmentCtor::Circle(circle_ctor),
7146 }];
7147 let (src_delta, scene_delta) = frontend
7148 .edit_segments(&mock_ctx, version, sketch_id, segments)
7149 .await
7150 .unwrap();
7151 assert_eq!(
7152 src_delta.text.as_str(),
7153 "sketch001 = sketch(on = XY) {
7154 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7155}
7156"
7157 );
7158 assert_eq!(scene_delta.new_objects, vec![]);
7159 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7160
7161 ctx.close().await;
7162 mock_ctx.close().await;
7163 }
7164
7165 #[tokio::test(flavor = "multi_thread")]
7166 async fn test_delete_circle() {
7167 let initial_source = "sketch001 = sketch(on = XY) {
7168 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7169}
7170";
7171
7172 let program = Program::parse(initial_source).unwrap().0.unwrap();
7173 let mut frontend = FrontendState::new();
7174
7175 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7176 let mock_ctx = ExecutorContext::new_mock(None).await;
7177 let version = Version(0);
7178
7179 frontend.hack_set_program(&ctx, program).await.unwrap();
7180 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7181 let sketch_id = sketch_object.id;
7182 let sketch = expect_sketch(sketch_object);
7183
7184 assert_eq!(sketch.segments.len(), 3);
7186 let circle_id = sketch.segments[2];
7187
7188 let (src_delta, scene_delta) = frontend
7190 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7191 .await
7192 .unwrap();
7193 assert_eq!(
7194 src_delta.text.as_str(),
7195 "sketch001 = sketch(on = XY) {
7196}
7197"
7198 );
7199 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7200 let new_sketch = expect_sketch(new_sketch_object);
7201 assert_eq!(new_sketch.segments.len(), 0);
7202
7203 ctx.close().await;
7204 mock_ctx.close().await;
7205 }
7206
7207 #[tokio::test(flavor = "multi_thread")]
7208 async fn test_edit_circle_via_point() {
7209 let initial_source = "sketch001 = sketch(on = XY) {
7210 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7211}
7212";
7213
7214 let program = Program::parse(initial_source).unwrap().0.unwrap();
7215 let mut frontend = FrontendState::new();
7216
7217 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7218 let mock_ctx = ExecutorContext::new_mock(None).await;
7219 let version = Version(0);
7220
7221 frontend.hack_set_program(&ctx, program).await.unwrap();
7222 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7223 let sketch_id = sketch_object.id;
7224 let sketch = expect_sketch(sketch_object);
7225
7226 let circle_id = sketch
7228 .segments
7229 .iter()
7230 .copied()
7231 .find(|seg_id| {
7232 matches!(
7233 &frontend.scene_graph.objects[seg_id.0].kind,
7234 ObjectKind::Segment {
7235 segment: Segment::Circle(_)
7236 }
7237 )
7238 })
7239 .expect("Expected a circle segment in sketch");
7240 let circle_object = &frontend.scene_graph.objects[circle_id.0];
7241 let ObjectKind::Segment {
7242 segment: Segment::Circle(circle),
7243 } = &circle_object.kind
7244 else {
7245 panic!("Expected circle segment, got: {:?}", circle_object.kind);
7246 };
7247 let start_point_id = circle.start;
7248
7249 let segments = vec![ExistingSegmentCtor {
7251 id: start_point_id,
7252 ctor: SegmentCtor::Point(PointCtor {
7253 position: Point2d {
7254 x: Expr::Var(Number {
7255 value: 7.0,
7256 units: NumericSuffix::Mm,
7257 }),
7258 y: Expr::Var(Number {
7259 value: 1.0,
7260 units: NumericSuffix::Mm,
7261 }),
7262 },
7263 }),
7264 }];
7265 let (src_delta, _scene_delta) = frontend
7266 .edit_segments(&mock_ctx, version, sketch_id, segments)
7267 .await
7268 .unwrap();
7269 assert_eq!(
7270 src_delta.text.as_str(),
7271 "sketch001 = sketch(on = XY) {
7272 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7273}
7274"
7275 );
7276
7277 ctx.close().await;
7278 mock_ctx.close().await;
7279 }
7280
7281 #[tokio::test(flavor = "multi_thread")]
7282 async fn test_add_line_when_sketch_block_uses_variable() {
7283 let initial_source = "s = sketch(on = XY) {}
7284";
7285
7286 let program = Program::parse(initial_source).unwrap().0.unwrap();
7287
7288 let mut frontend = FrontendState::new();
7289
7290 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7291 let mock_ctx = ExecutorContext::new_mock(None).await;
7292 let version = Version(0);
7293
7294 frontend.hack_set_program(&ctx, program).await.unwrap();
7295 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7296 let sketch_id = sketch_object.id;
7297
7298 let line_ctor = LineCtor {
7299 start: Point2d {
7300 x: Expr::Number(Number {
7301 value: 0.0,
7302 units: NumericSuffix::Mm,
7303 }),
7304 y: Expr::Number(Number {
7305 value: 0.0,
7306 units: NumericSuffix::Mm,
7307 }),
7308 },
7309 end: Point2d {
7310 x: Expr::Number(Number {
7311 value: 10.0,
7312 units: NumericSuffix::Mm,
7313 }),
7314 y: Expr::Number(Number {
7315 value: 10.0,
7316 units: NumericSuffix::Mm,
7317 }),
7318 },
7319 construction: None,
7320 };
7321 let segment = SegmentCtor::Line(line_ctor);
7322 let (src_delta, scene_delta) = frontend
7323 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7324 .await
7325 .unwrap();
7326 assert_eq!(
7327 src_delta.text.as_str(),
7328 "s = sketch(on = XY) {
7329 line(start = [0mm, 0mm], end = [10mm, 10mm])
7330}
7331"
7332 );
7333 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7334 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7335
7336 ctx.close().await;
7337 mock_ctx.close().await;
7338 }
7339
7340 #[tokio::test(flavor = "multi_thread")]
7341 async fn test_new_sketch_add_line_delete_sketch() {
7342 let program = Program::empty();
7343
7344 let mut frontend = FrontendState::new();
7345 frontend.program = program;
7346
7347 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7348 let mock_ctx = ExecutorContext::new_mock(None).await;
7349 let version = Version(0);
7350
7351 let sketch_args = SketchCtor {
7352 on: Plane::Default(PlaneName::Xy),
7353 };
7354 let (_src_delta, scene_delta, sketch_id) = frontend
7355 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7356 .await
7357 .unwrap();
7358 assert_eq!(sketch_id, ObjectId(1));
7359 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7360 let sketch_object = &scene_delta.new_graph.objects[1];
7361 assert_eq!(sketch_object.id, ObjectId(1));
7362 assert_eq!(
7363 sketch_object.kind,
7364 ObjectKind::Sketch(Sketch {
7365 args: SketchCtor {
7366 on: Plane::Default(PlaneName::Xy)
7367 },
7368 plane: ObjectId(0),
7369 segments: vec![],
7370 constraints: vec![],
7371 })
7372 );
7373 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7374
7375 let line_ctor = LineCtor {
7376 start: Point2d {
7377 x: Expr::Number(Number {
7378 value: 0.0,
7379 units: NumericSuffix::Mm,
7380 }),
7381 y: Expr::Number(Number {
7382 value: 0.0,
7383 units: NumericSuffix::Mm,
7384 }),
7385 },
7386 end: Point2d {
7387 x: Expr::Number(Number {
7388 value: 10.0,
7389 units: NumericSuffix::Mm,
7390 }),
7391 y: Expr::Number(Number {
7392 value: 10.0,
7393 units: NumericSuffix::Mm,
7394 }),
7395 },
7396 construction: None,
7397 };
7398 let segment = SegmentCtor::Line(line_ctor);
7399 let (src_delta, scene_delta) = frontend
7400 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7401 .await
7402 .unwrap();
7403 assert_eq!(
7404 src_delta.text.as_str(),
7405 "sketch001 = sketch(on = XY) {
7406 line(start = [0mm, 0mm], end = [10mm, 10mm])
7407}
7408"
7409 );
7410 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7411
7412 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7413 assert_eq!(src_delta.text.as_str(), "");
7414 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7415
7416 ctx.close().await;
7417 mock_ctx.close().await;
7418 }
7419
7420 #[tokio::test(flavor = "multi_thread")]
7421 async fn test_delete_sketch_when_sketch_block_uses_variable() {
7422 let initial_source = "s = sketch(on = XY) {}
7423";
7424
7425 let program = Program::parse(initial_source).unwrap().0.unwrap();
7426
7427 let mut frontend = FrontendState::new();
7428
7429 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7430 let mock_ctx = ExecutorContext::new_mock(None).await;
7431 let version = Version(0);
7432
7433 frontend.hack_set_program(&ctx, program).await.unwrap();
7434 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7435 let sketch_id = sketch_object.id;
7436
7437 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7438 assert_eq!(src_delta.text.as_str(), "");
7439 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7440
7441 ctx.close().await;
7442 mock_ctx.close().await;
7443 }
7444
7445 #[tokio::test(flavor = "multi_thread")]
7446 async fn test_delete_sketch_after_comment() {
7447 let initial_source = "sketch001 = sketch(on = XZ) {
7448}
7449";
7450
7451 let program = Program::parse(initial_source).unwrap().0.unwrap();
7452 let mut frontend = FrontendState::new();
7453
7454 let ctx = ExecutorContext::new_with_engine(
7455 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7456 Default::default(),
7457 );
7458 let version = Version(0);
7459
7460 frontend.hack_set_program(&ctx, program).await.unwrap();
7461 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7462 let sketch_id = sketch_object.id;
7463 let original_source = sketch_object.source.clone();
7464
7465 let commented_source = "// test 1
7466sketch001 = sketch(on = XZ) {
7467}
7468";
7469 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7470 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7471
7472 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7473 assert_eq!(cached_sketch_object.source, original_source);
7474
7475 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7476 assert!(
7477 !src_delta.text.contains("sketch001"),
7478 "sketch was not deleted: {}",
7479 src_delta.text
7480 );
7481 assert_eq!(src_delta.text.as_str(), "// test 1\n");
7483 assert_eq!(scene_delta.new_graph.objects.len(), 0);
7484
7485 ctx.close().await;
7486 }
7487
7488 #[tokio::test(flavor = "multi_thread")]
7489 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7490 let initial_source = "sketch001 = sketch(on = XZ) {
7491}
7492foo = 1
7493";
7494
7495 let program = Program::parse(initial_source).unwrap().0.unwrap();
7496 let mut frontend = FrontendState::new();
7497
7498 let ctx = ExecutorContext::new_with_engine(
7499 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7500 Default::default(),
7501 );
7502 let version = Version(0);
7503
7504 frontend.hack_set_program(&ctx, program).await.unwrap();
7505 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7506 let sketch_id = sketch_object.id;
7507
7508 let commented_source = "// keep me
7509sketch001 = sketch(on = XZ) {
7510}
7511foo = 1
7512";
7513 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7514 frontend.engine_execute(&ctx, commented_program).await.unwrap();
7515
7516 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7517 assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7519
7520 ctx.close().await;
7521 }
7522
7523 #[tokio::test(flavor = "multi_thread")]
7524 async fn test_delete_segment_preserves_pre_comment() {
7525 let initial_source = "\
7526sketch(on = XY) {
7527 point(at = [var 1, var 2])
7528 // describe the middle point
7529 point(at = [var 3, var 4])
7530 point(at = [var 5, var 6])
7531}
7532";
7533
7534 let program = Program::parse(initial_source).unwrap().0.unwrap();
7535 let mut frontend = FrontendState::new();
7536
7537 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7538 let mock_ctx = ExecutorContext::new_mock(None).await;
7539 let version = Version(0);
7540
7541 frontend.hack_set_program(&ctx, program).await.unwrap();
7542 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7543 let sketch_id = sketch_object.id;
7544 let sketch = expect_sketch(sketch_object);
7545
7546 let middle_point_id = *sketch.segments.get(1).unwrap();
7547
7548 let (src_delta, _scene_delta) = frontend
7549 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7550 .await
7551 .unwrap();
7552 assert_eq!(
7555 src_delta.text.as_str(),
7556 "\
7557sketch(on = XY) {
7558 point(at = [var 1mm, var 2mm])
7559 // describe the middle point
7560 point(at = [var 5mm, var 6mm])
7561}
7562"
7563 );
7564
7565 ctx.close().await;
7566 mock_ctx.close().await;
7567 }
7568
7569 #[tokio::test(flavor = "multi_thread")]
7570 async fn test_delete_last_segment_preserves_pre_comment() {
7571 let initial_source = "\
7572sketch(on = XY) {
7573 point(at = [var 1, var 2])
7574 // describe the trailing point
7575 point(at = [var 3, var 4])
7576}
7577";
7578
7579 let program = Program::parse(initial_source).unwrap().0.unwrap();
7580 let mut frontend = FrontendState::new();
7581
7582 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7583 let mock_ctx = ExecutorContext::new_mock(None).await;
7584 let version = Version(0);
7585
7586 frontend.hack_set_program(&ctx, program).await.unwrap();
7587 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7588 let sketch_id = sketch_object.id;
7589 let sketch = expect_sketch(sketch_object);
7590
7591 let last_point_id = *sketch.segments.last().unwrap();
7592
7593 let (src_delta, _scene_delta) = frontend
7594 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7595 .await
7596 .unwrap();
7597 assert_eq!(
7600 src_delta.text.as_str(),
7601 "\
7602sketch(on = XY) {
7603 point(at = [var 1mm, var 2mm])
7604 // describe the trailing point
7605}
7606"
7607 );
7608
7609 ctx.close().await;
7610 mock_ctx.close().await;
7611 }
7612
7613 #[tokio::test(flavor = "multi_thread")]
7614 async fn test_delete_segment_drops_inline_trailing_comment() {
7615 let initial_source = "\
7616sketch(on = XY) {
7617 point(at = [var 1, var 2])
7618 point(at = [var 3, var 4]) // same-line note that gets dropped
7619 point(at = [var 5, var 6])
7620}
7621";
7622
7623 let program = Program::parse(initial_source).unwrap().0.unwrap();
7624 let mut frontend = FrontendState::new();
7625
7626 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7627 let mock_ctx = ExecutorContext::new_mock(None).await;
7628 let version = Version(0);
7629
7630 frontend.hack_set_program(&ctx, program).await.unwrap();
7631 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7632 let sketch_id = sketch_object.id;
7633 let sketch = expect_sketch(sketch_object);
7634
7635 let middle_point_id = *sketch.segments.get(1).unwrap();
7636
7637 let (src_delta, _scene_delta) = frontend
7638 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7639 .await
7640 .unwrap();
7641 assert!(
7643 !src_delta.text.contains("same-line note"),
7644 "inline comment should have been removed: {}",
7645 src_delta.text
7646 );
7647
7648 ctx.close().await;
7649 mock_ctx.close().await;
7650 }
7651
7652 #[tokio::test(flavor = "multi_thread")]
7653 async fn test_delete_segments_preserves_block_comments_across_positions() {
7654 let initial_source = "\
7662sketch(on = XY) {
7663 /* above first - moves to middle */
7664 point(at = [var 1, var 2]) /* same-line on first - dropped */
7665 /* above middle - stays */
7666 point(at = [var 3, var 4])
7667 /* above last - moves to trailing meta */
7668 point(at = [var 5, var 6])
7669}
7670";
7671
7672 let program = Program::parse(initial_source).unwrap().0.unwrap();
7673 let mut frontend = FrontendState::new();
7674
7675 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7676 let mock_ctx = ExecutorContext::new_mock(None).await;
7677 let version = Version(0);
7678
7679 frontend.hack_set_program(&ctx, program).await.unwrap();
7680 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7681 let sketch_id = sketch_object.id;
7682 let sketch = expect_sketch(sketch_object);
7683
7684 let first_point_id = *sketch.segments.first().unwrap();
7685 let last_point_id = *sketch.segments.last().unwrap();
7686
7687 let (src_delta, _scene_delta) = frontend
7688 .delete_objects(
7689 &mock_ctx,
7690 version,
7691 sketch_id,
7692 Vec::new(),
7693 vec![first_point_id, last_point_id],
7694 )
7695 .await
7696 .unwrap();
7697 assert_eq!(
7698 src_delta.text.as_str(),
7699 "\
7700sketch(on = XY) {
7701 /* above first - moves to middle */
7702 /* above middle - stays */
7703 point(at = [var 3mm, var 4mm])
7704 /* above last - moves to trailing meta */
7705}
7706"
7707 );
7708
7709 ctx.close().await;
7710 mock_ctx.close().await;
7711 }
7712
7713 #[tokio::test(flavor = "multi_thread")]
7714 async fn test_edit_line_when_editing_its_start_point() {
7715 let initial_source = "\
7716sketch(on = XY) {
7717 line(start = [var 1, var 2], end = [var 3, var 4])
7718}
7719";
7720
7721 let program = Program::parse(initial_source).unwrap().0.unwrap();
7722
7723 let mut frontend = FrontendState::new();
7724
7725 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7726 let mock_ctx = ExecutorContext::new_mock(None).await;
7727 let version = Version(0);
7728
7729 frontend.hack_set_program(&ctx, program).await.unwrap();
7730 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7731 let sketch_id = sketch_object.id;
7732 let sketch = expect_sketch(sketch_object);
7733
7734 let point_id = *sketch.segments.first().unwrap();
7735
7736 let point_ctor = PointCtor {
7737 position: Point2d {
7738 x: Expr::Var(Number {
7739 value: 5.0,
7740 units: NumericSuffix::Inch,
7741 }),
7742 y: Expr::Var(Number {
7743 value: 6.0,
7744 units: NumericSuffix::Inch,
7745 }),
7746 },
7747 };
7748 let segments = vec![ExistingSegmentCtor {
7749 id: point_id,
7750 ctor: SegmentCtor::Point(point_ctor),
7751 }];
7752 let (src_delta, scene_delta) = frontend
7753 .edit_segments(&mock_ctx, version, sketch_id, segments)
7754 .await
7755 .unwrap();
7756 assert_eq!(
7757 src_delta.text.as_str(),
7758 "\
7759sketch(on = XY) {
7760 line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7761}
7762"
7763 );
7764 assert_eq!(scene_delta.new_objects, vec![]);
7765 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7766
7767 ctx.close().await;
7768 mock_ctx.close().await;
7769 }
7770
7771 #[tokio::test(flavor = "multi_thread")]
7772 async fn test_edit_line_when_editing_its_end_point() {
7773 let initial_source = "\
7774sketch(on = XY) {
7775 line(start = [var 1, var 2], end = [var 3, var 4])
7776}
7777";
7778
7779 let program = Program::parse(initial_source).unwrap().0.unwrap();
7780
7781 let mut frontend = FrontendState::new();
7782
7783 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7784 let mock_ctx = ExecutorContext::new_mock(None).await;
7785 let version = Version(0);
7786
7787 frontend.hack_set_program(&ctx, program).await.unwrap();
7788 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7789 let sketch_id = sketch_object.id;
7790 let sketch = expect_sketch(sketch_object);
7791 let point_id = *sketch.segments.get(1).unwrap();
7792
7793 let point_ctor = PointCtor {
7794 position: Point2d {
7795 x: Expr::Var(Number {
7796 value: 5.0,
7797 units: NumericSuffix::Inch,
7798 }),
7799 y: Expr::Var(Number {
7800 value: 6.0,
7801 units: NumericSuffix::Inch,
7802 }),
7803 },
7804 };
7805 let segments = vec![ExistingSegmentCtor {
7806 id: point_id,
7807 ctor: SegmentCtor::Point(point_ctor),
7808 }];
7809 let (src_delta, scene_delta) = frontend
7810 .edit_segments(&mock_ctx, version, sketch_id, segments)
7811 .await
7812 .unwrap();
7813 assert_eq!(
7814 src_delta.text.as_str(),
7815 "\
7816sketch(on = XY) {
7817 line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
7818}
7819"
7820 );
7821 assert_eq!(scene_delta.new_objects, vec![]);
7822 assert_eq!(
7823 scene_delta.new_graph.objects.len(),
7824 5,
7825 "{:#?}",
7826 scene_delta.new_graph.objects
7827 );
7828
7829 ctx.close().await;
7830 mock_ctx.close().await;
7831 }
7832
7833 #[tokio::test(flavor = "multi_thread")]
7834 async fn test_edit_line_with_coincident_feedback() {
7835 let initial_source = "\
7836sketch(on = XY) {
7837 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
7838 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7839 fixed([line1.start, [0, 0]])
7840 coincident([line1.end, line2.start])
7841 equalLength([line1, line2])
7842}
7843";
7844
7845 let program = Program::parse(initial_source).unwrap().0.unwrap();
7846
7847 let mut frontend = FrontendState::new();
7848
7849 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7850 let mock_ctx = ExecutorContext::new_mock(None).await;
7851 let version = Version(0);
7852
7853 frontend.hack_set_program(&ctx, program).await.unwrap();
7854 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7855 let sketch_id = sketch_object.id;
7856 let sketch = expect_sketch(sketch_object);
7857 let line2_end_id = *sketch.segments.get(4).unwrap();
7858
7859 let segments = vec![ExistingSegmentCtor {
7860 id: line2_end_id,
7861 ctor: SegmentCtor::Point(PointCtor {
7862 position: Point2d {
7863 x: Expr::Var(Number {
7864 value: 9.0,
7865 units: NumericSuffix::None,
7866 }),
7867 y: Expr::Var(Number {
7868 value: 10.0,
7869 units: NumericSuffix::None,
7870 }),
7871 },
7872 }),
7873 }];
7874 let (src_delta, scene_delta) = frontend
7875 .edit_segments(&mock_ctx, version, sketch_id, segments)
7876 .await
7877 .unwrap();
7878 assert_eq!(
7879 src_delta.text.as_str(),
7880 "\
7881sketch(on = XY) {
7882 line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7883 line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7884 fixed([line1.start, [0, 0]])
7885 coincident([line1.end, line2.start])
7886 equalLength([line1, line2])
7887}
7888"
7889 );
7890 assert_eq!(
7891 scene_delta.new_graph.objects.len(),
7892 11,
7893 "{:#?}",
7894 scene_delta.new_graph.objects
7895 );
7896
7897 ctx.close().await;
7898 mock_ctx.close().await;
7899 }
7900
7901 #[tokio::test(flavor = "multi_thread")]
7902 async fn test_delete_point_without_var() {
7903 let initial_source = "\
7904sketch(on = XY) {
7905 point(at = [var 1, var 2])
7906 point(at = [var 3, var 4])
7907 point(at = [var 5, var 6])
7908}
7909";
7910
7911 let program = Program::parse(initial_source).unwrap().0.unwrap();
7912
7913 let mut frontend = FrontendState::new();
7914
7915 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7916 let mock_ctx = ExecutorContext::new_mock(None).await;
7917 let version = Version(0);
7918
7919 frontend.hack_set_program(&ctx, program).await.unwrap();
7920 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7921 let sketch_id = sketch_object.id;
7922 let sketch = expect_sketch(sketch_object);
7923
7924 let point_id = *sketch.segments.get(1).unwrap();
7925
7926 let (src_delta, scene_delta) = frontend
7927 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7928 .await
7929 .unwrap();
7930 assert_eq!(
7931 src_delta.text.as_str(),
7932 "\
7933sketch(on = XY) {
7934 point(at = [var 1mm, var 2mm])
7935 point(at = [var 5mm, var 6mm])
7936}
7937"
7938 );
7939 assert_eq!(scene_delta.new_objects, vec![]);
7940 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7941
7942 ctx.close().await;
7943 mock_ctx.close().await;
7944 }
7945
7946 #[tokio::test(flavor = "multi_thread")]
7947 async fn test_delete_point_with_var() {
7948 let initial_source = "\
7949sketch(on = XY) {
7950 point(at = [var 1, var 2])
7951 point1 = point(at = [var 3, var 4])
7952 point(at = [var 5, var 6])
7953}
7954";
7955
7956 let program = Program::parse(initial_source).unwrap().0.unwrap();
7957
7958 let mut frontend = FrontendState::new();
7959
7960 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7961 let mock_ctx = ExecutorContext::new_mock(None).await;
7962 let version = Version(0);
7963
7964 frontend.hack_set_program(&ctx, program).await.unwrap();
7965 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7966 let sketch_id = sketch_object.id;
7967 let sketch = expect_sketch(sketch_object);
7968
7969 let point_id = *sketch.segments.get(1).unwrap();
7970
7971 let (src_delta, scene_delta) = frontend
7972 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7973 .await
7974 .unwrap();
7975 assert_eq!(
7976 src_delta.text.as_str(),
7977 "\
7978sketch(on = XY) {
7979 point(at = [var 1mm, var 2mm])
7980 point(at = [var 5mm, var 6mm])
7981}
7982"
7983 );
7984 assert_eq!(scene_delta.new_objects, vec![]);
7985 assert_eq!(scene_delta.new_graph.objects.len(), 4);
7986
7987 ctx.close().await;
7988 mock_ctx.close().await;
7989 }
7990
7991 #[tokio::test(flavor = "multi_thread")]
7992 async fn test_delete_point_with_var_ignores_stale_warm_starts() {
7993 let initial_source = "\
7994sketch(on = XY) {
7995 point(at = [var 1, var 2])
7996 point1 = point(at = [var 3, var 4])
7997 point(at = [var 5, var 6])
7998}
7999";
8000
8001 let program = Program::parse(initial_source).unwrap().0.unwrap();
8002
8003 let mut frontend = FrontendState::new();
8004 let mock_ctx = ExecutorContext::new_mock(None).await;
8005 let version = Version(0);
8006
8007 frontend.program = program.clone();
8008 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8009 let outcome = frontend.update_state_after_exec(outcome, true);
8010 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8011 let sketch_id = sketch_object.id;
8012 let sketch = expect_sketch(sketch_object);
8013 let point_id = *sketch.segments.get(1).unwrap();
8014 frontend.replace_sketch_var_warm_starts(sketch_id, &outcome);
8015
8016 let (src_delta, _scene_delta) = frontend
8017 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
8018 .await
8019 .unwrap();
8020 assert_eq!(
8021 src_delta.text.as_str(),
8022 "\
8023sketch(on = XY) {
8024 point(at = [var 1mm, var 2mm])
8025 point(at = [var 5mm, var 6mm])
8026}
8027"
8028 );
8029
8030 mock_ctx.close().await;
8031 }
8032
8033 #[tokio::test(flavor = "multi_thread")]
8034 async fn test_delete_multiple_points() {
8035 let initial_source = "\
8036sketch(on = XY) {
8037 point(at = [var 1, var 2])
8038 point1 = point(at = [var 3, var 4])
8039 point(at = [var 5, var 6])
8040}
8041";
8042
8043 let program = Program::parse(initial_source).unwrap().0.unwrap();
8044
8045 let mut frontend = FrontendState::new();
8046
8047 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8048 let mock_ctx = ExecutorContext::new_mock(None).await;
8049 let version = Version(0);
8050
8051 frontend.hack_set_program(&ctx, program).await.unwrap();
8052 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8053 let sketch_id = sketch_object.id;
8054
8055 let sketch = expect_sketch(sketch_object);
8056
8057 let point1_id = *sketch.segments.first().unwrap();
8058 let point2_id = *sketch.segments.get(1).unwrap();
8059
8060 let (src_delta, scene_delta) = frontend
8061 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
8062 .await
8063 .unwrap();
8064 assert_eq!(
8065 src_delta.text.as_str(),
8066 "\
8067sketch(on = XY) {
8068 point(at = [var 5mm, var 6mm])
8069}
8070"
8071 );
8072 assert_eq!(scene_delta.new_objects, vec![]);
8073 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8074
8075 ctx.close().await;
8076 mock_ctx.close().await;
8077 }
8078
8079 #[tokio::test(flavor = "multi_thread")]
8080 async fn test_delete_coincident_constraint() {
8081 let initial_source = "\
8082sketch(on = XY) {
8083 point1 = point(at = [var 1, var 2])
8084 point2 = point(at = [var 3, var 4])
8085 coincident([point1, point2])
8086 point(at = [var 5, var 6])
8087}
8088";
8089
8090 let program = Program::parse(initial_source).unwrap().0.unwrap();
8091
8092 let mut frontend = FrontendState::new();
8093
8094 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8095 let mock_ctx = ExecutorContext::new_mock(None).await;
8096 let version = Version(0);
8097
8098 frontend.hack_set_program(&ctx, program).await.unwrap();
8099 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8100 let sketch_id = sketch_object.id;
8101 let sketch = expect_sketch(sketch_object);
8102
8103 let coincident_id = *sketch.constraints.first().unwrap();
8104
8105 let (src_delta, scene_delta) = frontend
8106 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8107 .await
8108 .unwrap();
8109 assert_eq!(
8110 src_delta.text.as_str(),
8111 "\
8112sketch(on = XY) {
8113 point1 = point(at = [var 1mm, var 2mm])
8114 point2 = point(at = [var 3mm, var 4mm])
8115 point(at = [var 5mm, var 6mm])
8116}
8117"
8118 );
8119 assert_eq!(scene_delta.new_objects, vec![]);
8120 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8121
8122 ctx.close().await;
8123 mock_ctx.close().await;
8124 }
8125
8126 #[tokio::test(flavor = "multi_thread")]
8127 async fn test_delete_line_cascades_to_coincident_constraint() {
8128 let initial_source = "\
8129sketch(on = XY) {
8130 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8131 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8132 coincident([line1.end, line2.start])
8133}
8134";
8135
8136 let program = Program::parse(initial_source).unwrap().0.unwrap();
8137
8138 let mut frontend = FrontendState::new();
8139
8140 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8141 let mock_ctx = ExecutorContext::new_mock(None).await;
8142 let version = Version(0);
8143
8144 frontend.hack_set_program(&ctx, program).await.unwrap();
8145 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8146 let sketch_id = sketch_object.id;
8147 let sketch = expect_sketch(sketch_object);
8148 let line_id = *sketch.segments.get(5).unwrap();
8149
8150 let (src_delta, scene_delta) = frontend
8151 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
8152 .await
8153 .unwrap();
8154 assert_eq!(
8155 src_delta.text.as_str(),
8156 "\
8157sketch(on = XY) {
8158 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8159}
8160"
8161 );
8162 assert_eq!(
8163 scene_delta.new_graph.objects.len(),
8164 5,
8165 "{:#?}",
8166 scene_delta.new_graph.objects
8167 );
8168
8169 ctx.close().await;
8170 mock_ctx.close().await;
8171 }
8172
8173 #[tokio::test(flavor = "multi_thread")]
8174 async fn test_delete_line_cascades_to_distance_constraint() {
8175 let initial_source = "\
8176sketch(on = XY) {
8177 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8178 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8179 distance([line1.end, line2.start]) == 10mm
8180}
8181";
8182
8183 let program = Program::parse(initial_source).unwrap().0.unwrap();
8184
8185 let mut frontend = FrontendState::new();
8186
8187 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8188 let mock_ctx = ExecutorContext::new_mock(None).await;
8189 let version = Version(0);
8190
8191 frontend.hack_set_program(&ctx, program).await.unwrap();
8192 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8193 let sketch_id = sketch_object.id;
8194 let sketch = expect_sketch(sketch_object);
8195 let line_id = *sketch.segments.get(5).unwrap();
8196
8197 let (src_delta, scene_delta) = frontend
8198 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
8199 .await
8200 .unwrap();
8201 assert_eq!(
8202 src_delta.text.as_str(),
8203 "\
8204sketch(on = XY) {
8205 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8206}
8207"
8208 );
8209 assert_eq!(
8210 scene_delta.new_graph.objects.len(),
8211 5,
8212 "{:#?}",
8213 scene_delta.new_graph.objects
8214 );
8215
8216 ctx.close().await;
8217 mock_ctx.close().await;
8218 }
8219
8220 #[tokio::test(flavor = "multi_thread")]
8221 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
8222 let initial_source = "\
8223sketch(on = XY) {
8224 point1 = point(at = [var 1, var 2])
8225 point2 = point(at = [var 3, var 4])
8226 horizontalDistance([point1, point2]) == 10mm
8227}
8228";
8229
8230 let program = Program::parse(initial_source).unwrap().0.unwrap();
8231
8232 let mut frontend = FrontendState::new();
8233
8234 let mock_ctx = ExecutorContext::new_mock(None).await;
8235 let version = Version(0);
8236
8237 frontend.program = program.clone();
8238 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8239 frontend.update_state_after_exec(outcome, true);
8240 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8241 let sketch_id = sketch_object.id;
8242 let sketch = expect_sketch(sketch_object);
8243 let point2_id = *sketch.segments.get(1).unwrap();
8244
8245 let (src_delta, scene_delta) = frontend
8246 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
8247 .await
8248 .unwrap();
8249 assert_eq!(
8250 src_delta.text.as_str(),
8251 "\
8252sketch(on = XY) {
8253 point1 = point(at = [var 1mm, var 2mm])
8254}
8255"
8256 );
8257 assert_eq!(
8258 scene_delta.new_graph.objects.len(),
8259 3,
8260 "{:#?}",
8261 scene_delta.new_graph.objects
8262 );
8263
8264 mock_ctx.close().await;
8265 }
8266
8267 #[tokio::test(flavor = "multi_thread")]
8268 async fn test_delete_line_cascades_to_fixed_constraint() {
8269 let initial_source = "\
8270sketch(on = XY) {
8271 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8272 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8273 fixed([line1.start, [0, 0]])
8274}
8275";
8276
8277 let program = Program::parse(initial_source).unwrap().0.unwrap();
8278
8279 let mut frontend = FrontendState::new();
8280
8281 let mock_ctx = ExecutorContext::new_mock(None).await;
8282 let version = Version(0);
8283
8284 frontend.program = program.clone();
8285 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8286 frontend.update_state_after_exec(outcome, true);
8287 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8288 let sketch_id = sketch_object.id;
8289 let sketch = expect_sketch(sketch_object);
8290 let line1_id = *sketch.segments.get(2).unwrap();
8291
8292 let (src_delta, scene_delta) = frontend
8293 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8294 .await
8295 .unwrap();
8296 assert_eq!(
8297 src_delta.text.as_str(),
8298 "\
8299sketch(on = XY) {
8300 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8301}
8302"
8303 );
8304 assert_eq!(
8305 scene_delta.new_graph.objects.len(),
8306 5,
8307 "{:#?}",
8308 scene_delta.new_graph.objects
8309 );
8310
8311 mock_ctx.close().await;
8312 }
8313
8314 #[tokio::test(flavor = "multi_thread")]
8315 async fn test_delete_line_cascades_to_midpoint_constraint() {
8316 let initial_source = "\
8317sketch(on = XY) {
8318 point1 = point(at = [var 1, var 2])
8319 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
8320 midpoint(line1, point = point1)
8321}
8322";
8323
8324 let program = Program::parse(initial_source).unwrap().0.unwrap();
8325
8326 let mut frontend = FrontendState::new();
8327
8328 let mock_ctx = ExecutorContext::new_mock(None).await;
8329 let version = Version(0);
8330
8331 frontend.program = program.clone();
8332 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8333 frontend.update_state_after_exec(outcome, true);
8334 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8335 let sketch_id = sketch_object.id;
8336 let sketch = expect_sketch(sketch_object);
8337 let line1_id = *sketch.segments.get(3).unwrap();
8338
8339 let (src_delta, scene_delta) = frontend
8340 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8341 .await
8342 .unwrap();
8343 assert_eq!(
8344 src_delta.text.as_str(),
8345 "\
8346sketch(on = XY) {
8347 point1 = point(at = [var 1mm, var 2mm])
8348}
8349"
8350 );
8351 assert_eq!(
8352 scene_delta.new_graph.objects.len(),
8353 3,
8354 "{:#?}",
8355 scene_delta.new_graph.objects
8356 );
8357
8358 mock_ctx.close().await;
8359 }
8360
8361 #[tokio::test(flavor = "multi_thread")]
8362 async fn test_delete_point_preserves_multiline_coincident_constraint() {
8363 let initial_source = "\
8364sketch(on = XY) {
8365 point1 = point(at = [var 1, var 2])
8366 point2 = point(at = [var 3, var 4])
8367 point3 = point(at = [var 5, var 6])
8368 coincident([point1, point2, point3])
8369}
8370";
8371
8372 let program = Program::parse(initial_source).unwrap().0.unwrap();
8373
8374 let mut frontend = FrontendState::new();
8375
8376 let mock_ctx = ExecutorContext::new_mock(None).await;
8377 let version = Version(0);
8378
8379 frontend.program = program.clone();
8380 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8381 frontend.update_state_after_exec(outcome, true);
8382 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8383 let sketch_id = sketch_object.id;
8384 let sketch = expect_sketch(sketch_object);
8385 let point3_id = *sketch.segments.get(2).unwrap();
8386
8387 let (src_delta, scene_delta) = frontend
8388 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8389 .await
8390 .unwrap();
8391 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8392 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8393 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8394 assert!(
8395 src_delta.text.contains("coincident([point1, point2])"),
8396 "{}",
8397 src_delta.text
8398 );
8399
8400 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8401 let sketch = expect_sketch(sketch_object);
8402 assert_eq!(sketch.segments.len(), 2);
8403 assert_eq!(sketch.constraints.len(), 1);
8404
8405 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8406 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8407 panic!("Expected constraint object");
8408 };
8409 let Constraint::Coincident(coincident) = constraint else {
8410 panic!("Expected coincident constraint");
8411 };
8412 assert_eq!(
8413 coincident.segments,
8414 sketch
8415 .segments
8416 .iter()
8417 .copied()
8418 .map(Into::into)
8419 .collect::<Vec<ConstraintSegment>>()
8420 );
8421
8422 mock_ctx.close().await;
8423 }
8424
8425 #[tokio::test(flavor = "multi_thread")]
8426 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8427 let initial_source = "\
8428sketch(on = XY) {
8429 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8430 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8431 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8432 equalLength([line1, line2, line3])
8433}
8434";
8435
8436 let program = Program::parse(initial_source).unwrap().0.unwrap();
8437
8438 let mut frontend = FrontendState::new();
8439
8440 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8441 let mock_ctx = ExecutorContext::new_mock(None).await;
8442 let version = Version(0);
8443
8444 frontend.hack_set_program(&ctx, program).await.unwrap();
8445 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8446 let sketch_id = sketch_object.id;
8447 let sketch = expect_sketch(sketch_object);
8448 let line3_id = *sketch.segments.get(8).unwrap();
8449
8450 let (src_delta, scene_delta) = frontend
8451 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8452 .await
8453 .unwrap();
8454 assert_eq!(
8455 src_delta.text.as_str(),
8456 "\
8457sketch(on = XY) {
8458 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8459 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8460 equalLength([line1, line2])
8461}
8462"
8463 );
8464
8465 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8466 let sketch = expect_sketch(sketch_object);
8467 assert_eq!(sketch.constraints.len(), 1);
8468
8469 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8470 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8471 panic!("Expected constraint object");
8472 };
8473 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8474 panic!("Expected lines equal length constraint");
8475 };
8476 assert_eq!(lines_equal_length.lines.len(), 2);
8477
8478 ctx.close().await;
8479 mock_ctx.close().await;
8480 }
8481
8482 #[tokio::test(flavor = "multi_thread")]
8483 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8484 let initial_source = "\
8485sketch(on = XY) {
8486 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8487 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8488 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8489 horizontal([line1.end, line2.start, line3.start])
8490}
8491";
8492
8493 let program = Program::parse(initial_source).unwrap().0.unwrap();
8494
8495 let mut frontend = FrontendState::new();
8496
8497 let mock_ctx = ExecutorContext::new_mock(None).await;
8498 let version = Version(0);
8499
8500 frontend.program = program.clone();
8501 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8502 frontend.update_state_after_exec(outcome, true);
8503 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8504 let sketch_id = sketch_object.id;
8505 let sketch = expect_sketch(sketch_object);
8506 let line1_id = *sketch.segments.get(2).unwrap();
8507
8508 let (src_delta, scene_delta) = frontend
8509 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8510 .await
8511 .unwrap();
8512 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8513 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8514 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8515 assert!(
8516 src_delta.text.contains("horizontal([line2.start, line3.start])"),
8517 "{}",
8518 src_delta.text
8519 );
8520
8521 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8522 let sketch = expect_sketch(sketch_object);
8523 assert_eq!(sketch.constraints.len(), 1);
8524
8525 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8526 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8527 panic!("Expected constraint object");
8528 };
8529 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8530 panic!("Expected horizontal points constraint");
8531 };
8532 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8533 assert_eq!(*points, remaining_points);
8534
8535 mock_ctx.close().await;
8536 }
8537
8538 #[tokio::test(flavor = "multi_thread")]
8539 async fn test_delete_line_preserves_multiline_vertical_constraint() {
8540 let initial_source = "\
8541sketch(on = XY) {
8542 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8543 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8544 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8545 vertical([line1.end, line2.start, line3.start])
8546}
8547";
8548
8549 let program = Program::parse(initial_source).unwrap().0.unwrap();
8550
8551 let mut frontend = FrontendState::new();
8552
8553 let mock_ctx = ExecutorContext::new_mock(None).await;
8554 let version = Version(0);
8555
8556 frontend.program = program.clone();
8557 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8558 frontend.update_state_after_exec(outcome, true);
8559 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8560 let sketch_id = sketch_object.id;
8561 let sketch = expect_sketch(sketch_object);
8562 let line1_id = *sketch.segments.get(2).unwrap();
8563
8564 let (src_delta, scene_delta) = frontend
8565 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8566 .await
8567 .unwrap();
8568 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8569 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8570 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8571 assert!(
8572 src_delta.text.contains("vertical([line2.start, line3.start])"),
8573 "{}",
8574 src_delta.text
8575 );
8576
8577 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8578 let sketch = expect_sketch(sketch_object);
8579 assert_eq!(sketch.constraints.len(), 1);
8580
8581 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8582 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8583 panic!("Expected constraint object");
8584 };
8585 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8586 panic!("Expected vertical points constraint");
8587 };
8588 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8589 assert_eq!(*points, remaining_points);
8590
8591 mock_ctx.close().await;
8592 }
8593
8594 #[tokio::test(flavor = "multi_thread")]
8595 async fn test_delete_line_preserves_multiline_coincident_constraint() {
8596 let initial_source = "\
8597sketch(on = XY) {
8598 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8599 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8600 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8601 coincident([line1.end, line2.start, line3.start])
8602}
8603";
8604
8605 let program = Program::parse(initial_source).unwrap().0.unwrap();
8606
8607 let mut frontend = FrontendState::new();
8608
8609 let mock_ctx = ExecutorContext::new_mock(None).await;
8610 let version = Version(0);
8611
8612 frontend.program = program.clone();
8613 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8614 frontend.update_state_after_exec(outcome, true);
8615 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8616 let sketch_id = sketch_object.id;
8617 let sketch = expect_sketch(sketch_object);
8618 let line1_id = *sketch.segments.get(2).unwrap();
8619
8620 let (src_delta, scene_delta) = frontend
8621 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8622 .await
8623 .unwrap();
8624 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8625 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8626 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8627 assert!(
8628 src_delta.text.contains("coincident([line2.start, line3.start])"),
8629 "{}",
8630 src_delta.text
8631 );
8632
8633 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8634 let sketch = expect_sketch(sketch_object);
8635 assert_eq!(sketch.constraints.len(), 1);
8636
8637 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8638 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8639 panic!("Expected constraint object");
8640 };
8641 let Constraint::Coincident(coincident) = constraint else {
8642 panic!("Expected coincident constraint");
8643 };
8644 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8645 assert_eq!(coincident.segments, remaining_segments);
8646
8647 mock_ctx.close().await;
8648 }
8649
8650 #[tokio::test(flavor = "multi_thread")]
8651 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8652 let initial_source = "\
8653sketch(on = XY) {
8654 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8655 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8656 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8657 equalLength([line1, line2, line3])
8658}
8659";
8660
8661 let program = Program::parse(initial_source).unwrap().0.unwrap();
8662
8663 let mut frontend = FrontendState::new();
8664
8665 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8666 let mock_ctx = ExecutorContext::new_mock(None).await;
8667 let version = Version(0);
8668
8669 frontend.hack_set_program(&ctx, program).await.unwrap();
8670 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8671 let sketch_id = sketch_object.id;
8672 let sketch = expect_sketch(sketch_object);
8673 let line2_id = *sketch.segments.get(5).unwrap();
8674 let line3_id = *sketch.segments.get(8).unwrap();
8675
8676 let (src_delta, scene_delta) = frontend
8677 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8678 .await
8679 .unwrap();
8680 assert_eq!(
8681 src_delta.text.as_str(),
8682 "\
8683sketch(on = XY) {
8684 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8685}
8686"
8687 );
8688
8689 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8690 let sketch = expect_sketch(sketch_object);
8691 assert!(sketch.constraints.is_empty());
8692
8693 ctx.close().await;
8694 mock_ctx.close().await;
8695 }
8696
8697 #[tokio::test(flavor = "multi_thread")]
8698 async fn test_delete_line_preserves_multiline_parallel_constraint() {
8699 let initial_source = "\
8700sketch(on = XY) {
8701 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8702 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8703 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8704 parallel([line1, line2, line3])
8705}
8706";
8707
8708 let program = Program::parse(initial_source).unwrap().0.unwrap();
8709
8710 let mut frontend = FrontendState::new();
8711
8712 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8713 let mock_ctx = ExecutorContext::new_mock(None).await;
8714 let version = Version(0);
8715
8716 frontend.hack_set_program(&ctx, program).await.unwrap();
8717 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8718 let sketch_id = sketch_object.id;
8719 let sketch = expect_sketch(sketch_object);
8720 let line3_id = *sketch.segments.get(8).unwrap();
8721
8722 let (src_delta, scene_delta) = frontend
8723 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8724 .await
8725 .unwrap();
8726 assert_eq!(
8727 src_delta.text.as_str(),
8728 "\
8729sketch(on = XY) {
8730 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8731 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8732 parallel([line1, line2])
8733}
8734"
8735 );
8736
8737 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8738 let sketch = expect_sketch(sketch_object);
8739 assert_eq!(sketch.constraints.len(), 1);
8740
8741 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8742 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8743 panic!("Expected constraint object");
8744 };
8745 let Constraint::Parallel(parallel) = constraint else {
8746 panic!("Expected parallel constraint");
8747 };
8748 assert_eq!(parallel.lines.len(), 2);
8749
8750 ctx.close().await;
8751 mock_ctx.close().await;
8752 }
8753
8754 #[tokio::test(flavor = "multi_thread")]
8755 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8756 let initial_source = "\
8757sketch(on = XY) {
8758 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8759 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8760 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8761 parallel([line1, line2, line3])
8762}
8763";
8764
8765 let program = Program::parse(initial_source).unwrap().0.unwrap();
8766
8767 let mut frontend = FrontendState::new();
8768
8769 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8770 let mock_ctx = ExecutorContext::new_mock(None).await;
8771 let version = Version(0);
8772
8773 frontend.hack_set_program(&ctx, program).await.unwrap();
8774 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8775 let sketch_id = sketch_object.id;
8776 let sketch = expect_sketch(sketch_object);
8777 let line2_id = *sketch.segments.get(5).unwrap();
8778 let line3_id = *sketch.segments.get(8).unwrap();
8779
8780 let (src_delta, scene_delta) = frontend
8781 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8782 .await
8783 .unwrap();
8784 assert_eq!(
8785 src_delta.text.as_str(),
8786 "\
8787sketch(on = XY) {
8788 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8789}
8790"
8791 );
8792
8793 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8794 let sketch = expect_sketch(sketch_object);
8795 assert!(sketch.constraints.is_empty());
8796
8797 ctx.close().await;
8798 mock_ctx.close().await;
8799 }
8800
8801 #[tokio::test(flavor = "multi_thread")]
8802 async fn test_delete_line_line_coincident_constraint() {
8803 let initial_source = "\
8804sketch(on = XY) {
8805 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8806 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8807 coincident([line1, line2])
8808}
8809";
8810
8811 let program = Program::parse(initial_source).unwrap().0.unwrap();
8812
8813 let mut frontend = FrontendState::new();
8814
8815 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8816 let mock_ctx = ExecutorContext::new_mock(None).await;
8817 let version = Version(0);
8818
8819 frontend.hack_set_program(&ctx, program).await.unwrap();
8820 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8821 let sketch_id = sketch_object.id;
8822 let sketch = expect_sketch(sketch_object);
8823
8824 let coincident_id = *sketch.constraints.first().unwrap();
8825
8826 let (src_delta, scene_delta) = frontend
8827 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8828 .await
8829 .unwrap();
8830 assert_eq!(
8831 src_delta.text.as_str(),
8832 "\
8833sketch(on = XY) {
8834 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8835 line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8836}
8837"
8838 );
8839 assert_eq!(scene_delta.new_objects, vec![]);
8840 assert_eq!(scene_delta.new_graph.objects.len(), 8);
8841
8842 ctx.close().await;
8843 mock_ctx.close().await;
8844 }
8845
8846 #[tokio::test(flavor = "multi_thread")]
8847 async fn test_two_points_coincident() {
8848 let initial_source = "\
8849sketch(on = XY) {
8850 point1 = point(at = [var 1, var 2])
8851 point(at = [3, 4])
8852}
8853";
8854
8855 let program = Program::parse(initial_source).unwrap().0.unwrap();
8856
8857 let mut frontend = FrontendState::new();
8858
8859 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8860 let mock_ctx = ExecutorContext::new_mock(None).await;
8861 let version = Version(0);
8862
8863 frontend.hack_set_program(&ctx, program).await.unwrap();
8864 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8865 let sketch_id = sketch_object.id;
8866 let sketch = expect_sketch(sketch_object);
8867 let point0_id = *sketch.segments.first().unwrap();
8868 let point1_id = *sketch.segments.get(1).unwrap();
8869
8870 let constraint = Constraint::Coincident(Coincident {
8871 segments: vec![point0_id.into(), point1_id.into()],
8872 });
8873 let (src_delta, scene_delta) = frontend
8874 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8875 .await
8876 .unwrap();
8877 assert_eq!(
8878 src_delta.text.as_str(),
8879 "\
8880sketch(on = XY) {
8881 point1 = point(at = [var 1, var 2])
8882 point2 = point(at = [3, 4])
8883 coincident([point1, point2])
8884}
8885"
8886 );
8887 assert_eq!(
8888 scene_delta.new_graph.objects.len(),
8889 5,
8890 "{:#?}",
8891 scene_delta.new_graph.objects
8892 );
8893
8894 ctx.close().await;
8895 mock_ctx.close().await;
8896 }
8897
8898 #[tokio::test(flavor = "multi_thread")]
8899 async fn test_three_points_coincident() {
8900 let initial_source = "\
8901sketch(on = XY) {
8902 point1 = point(at = [var 1, var 2])
8903 point(at = [var 3, var 4])
8904 point(at = [var 5, var 6])
8905}
8906";
8907
8908 let program = Program::parse(initial_source).unwrap().0.unwrap();
8909
8910 let mut frontend = FrontendState::new();
8911
8912 let mock_ctx = ExecutorContext::new_mock(None).await;
8913 let version = Version(0);
8914
8915 frontend.program = program.clone();
8916 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8917 frontend.update_state_after_exec(outcome, true);
8918 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8919 let sketch_id = sketch_object.id;
8920 let sketch = expect_sketch(sketch_object);
8921 let segments = sketch
8922 .segments
8923 .iter()
8924 .take(3)
8925 .copied()
8926 .map(Into::into)
8927 .collect::<Vec<ConstraintSegment>>();
8928
8929 let constraint = Constraint::Coincident(Coincident {
8930 segments: segments.clone(),
8931 });
8932 let (src_delta, scene_delta) = frontend
8933 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8934 .await
8935 .unwrap();
8936 assert_eq!(
8937 src_delta.text.as_str(),
8938 "\
8939sketch(on = XY) {
8940 point1 = point(at = [var 1, var 2])
8941 point2 = point(at = [var 3, var 4])
8942 point3 = point(at = [var 5, var 6])
8943 coincident([point1, point2, point3])
8944}
8945"
8946 );
8947
8948 let constraint_object = scene_delta
8949 .new_graph
8950 .objects
8951 .iter()
8952 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8953 .unwrap();
8954
8955 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8956 panic!("expected a constraint object");
8957 };
8958
8959 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8960
8961 mock_ctx.close().await;
8962 }
8963
8964 #[tokio::test(flavor = "multi_thread")]
8965 async fn test_source_with_three_point_coincident_tracks_all_segments() {
8966 let initial_source = "\
8967sketch(on = XY) {
8968 point1 = point(at = [var 1, var 2])
8969 point2 = point(at = [var 3, var 4])
8970 point3 = point(at = [var 5, var 6])
8971 coincident([point1, point2, point3])
8972}
8973";
8974
8975 let program = Program::parse(initial_source).unwrap().0.unwrap();
8976
8977 let mut frontend = FrontendState::new();
8978
8979 let ctx = ExecutorContext::new_mock(None).await;
8980 frontend.program = program.clone();
8981 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8982 frontend.update_state_after_exec(outcome, true);
8983
8984 let constraint_object = frontend
8985 .scene_graph
8986 .objects
8987 .iter()
8988 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8989 .unwrap();
8990 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8991 panic!("expected a constraint object");
8992 };
8993
8994 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8995 let sketch = expect_sketch(sketch_object);
8996 let expected_segments = sketch
8997 .segments
8998 .iter()
8999 .take(3)
9000 .copied()
9001 .map(Into::into)
9002 .collect::<Vec<ConstraintSegment>>();
9003
9004 assert_eq!(
9005 constraint,
9006 &Constraint::Coincident(Coincident {
9007 segments: expected_segments,
9008 })
9009 );
9010
9011 ctx.close().await;
9012 }
9013
9014 #[tokio::test(flavor = "multi_thread")]
9015 async fn test_point_origin_coincident_preserves_order() {
9016 let initial_source = "\
9017sketch(on = XY) {
9018 point(at = [var 1, var 2])
9019}
9020";
9021
9022 for (origin_first, expected_source) in [
9023 (
9024 true,
9025 "\
9026sketch(on = XY) {
9027 point1 = point(at = [var 1, var 2])
9028 coincident([ORIGIN, point1])
9029}
9030",
9031 ),
9032 (
9033 false,
9034 "\
9035sketch(on = XY) {
9036 point1 = point(at = [var 1, var 2])
9037 coincident([point1, ORIGIN])
9038}
9039",
9040 ),
9041 ] {
9042 let program = Program::parse(initial_source).unwrap().0.unwrap();
9043
9044 let mut frontend = FrontendState::new();
9045
9046 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9047 let mock_ctx = ExecutorContext::new_mock(None).await;
9048 let version = Version(0);
9049
9050 frontend.hack_set_program(&ctx, program).await.unwrap();
9051 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9052 let sketch_id = sketch_object.id;
9053 let sketch = expect_sketch(sketch_object);
9054 let point_id = *sketch.segments.first().unwrap();
9055
9056 let segments = if origin_first {
9057 vec![ConstraintSegment::ORIGIN, point_id.into()]
9058 } else {
9059 vec![point_id.into(), ConstraintSegment::ORIGIN]
9060 };
9061 let constraint = Constraint::Coincident(Coincident {
9062 segments: segments.clone(),
9063 });
9064 let (src_delta, scene_delta) = frontend
9065 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9066 .await
9067 .unwrap();
9068 assert_eq!(src_delta.text.as_str(), expected_source);
9069
9070 let constraint_object = scene_delta
9071 .new_graph
9072 .objects
9073 .iter()
9074 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
9075 .unwrap();
9076
9077 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9078 panic!("expected a constraint object");
9079 };
9080
9081 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
9082
9083 ctx.close().await;
9084 mock_ctx.close().await;
9085 }
9086 }
9087
9088 #[tokio::test(flavor = "multi_thread")]
9089 async fn test_coincident_of_line_end_points() {
9090 let initial_source = "\
9091sketch(on = XY) {
9092 line(start = [var 1, var 2], end = [var 3, var 4])
9093 line(start = [var 5, var 6], end = [var 7, var 8])
9094}
9095";
9096
9097 let program = Program::parse(initial_source).unwrap().0.unwrap();
9098
9099 let mut frontend = FrontendState::new();
9100
9101 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9102 let mock_ctx = ExecutorContext::new_mock(None).await;
9103 let version = Version(0);
9104
9105 frontend.hack_set_program(&ctx, program).await.unwrap();
9106 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9107 let sketch_id = sketch_object.id;
9108 let sketch = expect_sketch(sketch_object);
9109 let point0_id = *sketch.segments.get(1).unwrap();
9110 let point1_id = *sketch.segments.get(3).unwrap();
9111
9112 let constraint = Constraint::Coincident(Coincident {
9113 segments: vec![point0_id.into(), point1_id.into()],
9114 });
9115 let (src_delta, scene_delta) = frontend
9116 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9117 .await
9118 .unwrap();
9119 assert_eq!(
9120 src_delta.text.as_str(),
9121 "\
9122sketch(on = XY) {
9123 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9124 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9125 coincident([line1.end, line2.start])
9126}
9127"
9128 );
9129 assert_eq!(
9130 scene_delta.new_graph.objects.len(),
9131 9,
9132 "{:#?}",
9133 scene_delta.new_graph.objects
9134 );
9135
9136 ctx.close().await;
9137 mock_ctx.close().await;
9138 }
9139
9140 #[tokio::test(flavor = "multi_thread")]
9141 async fn test_coincident_of_line_point_and_circle_segment() {
9142 let initial_source = "\
9143sketch(on = XY) {
9144 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
9145 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
9146}
9147";
9148 let program = Program::parse(initial_source).unwrap().0.unwrap();
9149 let mut frontend = FrontendState::new();
9150
9151 let mock_ctx = ExecutorContext::new_mock(None).await;
9152 let version = Version(0);
9153
9154 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9155 frontend.program = program;
9156 frontend.update_state_after_exec(outcome, true);
9157 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
9158 let sketch_id = sketch_object.id;
9159 let sketch = expect_sketch(sketch_object);
9160
9161 let circle_id = sketch
9162 .segments
9163 .iter()
9164 .copied()
9165 .find(|seg_id| {
9166 matches!(
9167 &frontend.scene_graph.objects[seg_id.0].kind,
9168 ObjectKind::Segment {
9169 segment: Segment::Circle(_)
9170 }
9171 )
9172 })
9173 .expect("Expected a circle segment in sketch");
9174 let line_id = sketch
9175 .segments
9176 .iter()
9177 .copied()
9178 .find(|seg_id| {
9179 matches!(
9180 &frontend.scene_graph.objects[seg_id.0].kind,
9181 ObjectKind::Segment {
9182 segment: Segment::Line(_)
9183 }
9184 )
9185 })
9186 .expect("Expected a line segment in sketch");
9187
9188 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
9189 ObjectKind::Segment {
9190 segment: Segment::Line(line),
9191 } => line.start,
9192 _ => panic!("Expected line segment object"),
9193 };
9194
9195 let constraint = Constraint::Coincident(Coincident {
9196 segments: vec![line_start_point_id.into(), circle_id.into()],
9197 });
9198 let (src_delta, _scene_delta) = frontend
9199 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9200 .await
9201 .unwrap();
9202 assert_eq!(
9203 src_delta.text.as_str(),
9204 "\
9205sketch(on = XY) {
9206 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
9207 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
9208 coincident([line1.start, circle1])
9209}
9210"
9211 );
9212
9213 mock_ctx.close().await;
9214 }
9215
9216 #[tokio::test(flavor = "multi_thread")]
9217 async fn test_invalid_coincident_arc_and_line_preserves_state() {
9218 let program = Program::empty();
9226
9227 let mut frontend = FrontendState::new();
9228 frontend.program = program;
9229
9230 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9231 let mock_ctx = ExecutorContext::new_mock(None).await;
9232 let version = Version(0);
9233
9234 let sketch_args = SketchCtor {
9235 on: Plane::Default(PlaneName::Xy),
9236 };
9237 let (_src_delta, _scene_delta, sketch_id) = frontend
9238 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
9239 .await
9240 .unwrap();
9241
9242 let arc_ctor = ArcCtor {
9244 start: Point2d {
9245 x: Expr::Var(Number {
9246 value: 0.0,
9247 units: NumericSuffix::Mm,
9248 }),
9249 y: Expr::Var(Number {
9250 value: 0.0,
9251 units: NumericSuffix::Mm,
9252 }),
9253 },
9254 end: Point2d {
9255 x: Expr::Var(Number {
9256 value: 10.0,
9257 units: NumericSuffix::Mm,
9258 }),
9259 y: Expr::Var(Number {
9260 value: 10.0,
9261 units: NumericSuffix::Mm,
9262 }),
9263 },
9264 center: Point2d {
9265 x: Expr::Var(Number {
9266 value: 10.0,
9267 units: NumericSuffix::Mm,
9268 }),
9269 y: Expr::Var(Number {
9270 value: 0.0,
9271 units: NumericSuffix::Mm,
9272 }),
9273 },
9274 construction: None,
9275 };
9276 let (_src_delta, scene_delta) = frontend
9277 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
9278 .await
9279 .unwrap();
9280 let arc_id = *scene_delta.new_objects.last().unwrap();
9282
9283 let line_ctor = LineCtor {
9285 start: Point2d {
9286 x: Expr::Var(Number {
9287 value: 20.0,
9288 units: NumericSuffix::Mm,
9289 }),
9290 y: Expr::Var(Number {
9291 value: 0.0,
9292 units: NumericSuffix::Mm,
9293 }),
9294 },
9295 end: Point2d {
9296 x: Expr::Var(Number {
9297 value: 30.0,
9298 units: NumericSuffix::Mm,
9299 }),
9300 y: Expr::Var(Number {
9301 value: 10.0,
9302 units: NumericSuffix::Mm,
9303 }),
9304 },
9305 construction: None,
9306 };
9307 let (_src_delta, scene_delta) = frontend
9308 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
9309 .await
9310 .unwrap();
9311 let line_id = *scene_delta.new_objects.last().unwrap();
9313
9314 let constraint = Constraint::Coincident(Coincident {
9317 segments: vec![arc_id.into(), line_id.into()],
9318 });
9319 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
9320
9321 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
9323
9324 let sketch_object_after =
9327 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
9328 let sketch_after = expect_sketch(sketch_object_after);
9329
9330 assert!(
9332 sketch_after.segments.contains(&arc_id),
9333 "Arc segment should still exist after failed constraint"
9334 );
9335 assert!(
9336 sketch_after.segments.contains(&line_id),
9337 "Line segment should still exist after failed constraint"
9338 );
9339
9340 let arc_obj = frontend
9342 .scene_graph
9343 .objects
9344 .get(arc_id.0)
9345 .expect("Arc object should still be accessible");
9346 let line_obj = frontend
9347 .scene_graph
9348 .objects
9349 .get(line_id.0)
9350 .expect("Line object should still be accessible");
9351
9352 match &arc_obj.kind {
9355 ObjectKind::Segment {
9356 segment: Segment::Arc(_),
9357 } => {}
9358 _ => panic!("Arc object should still be an arc segment"),
9359 }
9360 match &line_obj.kind {
9361 ObjectKind::Segment {
9362 segment: Segment::Line(_),
9363 } => {}
9364 _ => panic!("Line object should still be a line segment"),
9365 }
9366
9367 ctx.close().await;
9368 mock_ctx.close().await;
9369 }
9370
9371 #[tokio::test(flavor = "multi_thread")]
9372 async fn test_distance_two_points() {
9373 let initial_source = "\
9374sketch(on = XY) {
9375 point(at = [var 1, var 2])
9376 point(at = [var 3, var 4])
9377}
9378";
9379
9380 let program = Program::parse(initial_source).unwrap().0.unwrap();
9381
9382 let mut frontend = FrontendState::new();
9383
9384 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9385 let mock_ctx = ExecutorContext::new_mock(None).await;
9386 let version = Version(0);
9387
9388 frontend.hack_set_program(&ctx, program).await.unwrap();
9389 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9390 let sketch_id = sketch_object.id;
9391 let sketch = expect_sketch(sketch_object);
9392 let point0_id = *sketch.segments.first().unwrap();
9393 let point1_id = *sketch.segments.get(1).unwrap();
9394
9395 let constraint = Constraint::Distance(Distance {
9396 points: vec![point0_id.into(), point1_id.into()],
9397 distance: Number {
9398 value: 2.0,
9399 units: NumericSuffix::Mm,
9400 },
9401 label_position: None,
9402 source: Default::default(),
9403 });
9404 let (src_delta, scene_delta) = frontend
9405 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9406 .await
9407 .unwrap();
9408 assert_eq!(
9409 src_delta.text.as_str(),
9410 "\
9412sketch(on = XY) {
9413 point1 = point(at = [var 1, var 2])
9414 point2 = point(at = [var 3, var 4])
9415 distance([point1, point2]) == 2mm
9416}
9417"
9418 );
9419 assert_eq!(
9420 scene_delta.new_graph.objects.len(),
9421 5,
9422 "{:#?}",
9423 scene_delta.new_graph.objects
9424 );
9425
9426 ctx.close().await;
9427 mock_ctx.close().await;
9428 }
9429
9430 #[tokio::test(flavor = "multi_thread")]
9431 async fn test_distance_two_points_with_label() {
9432 let initial_source = "\
9433sketch(on = XY) {
9434 point(at = [var 1, var 2])
9435 point(at = [var 3, var 4])
9436}
9437";
9438
9439 let program = Program::parse(initial_source).unwrap().0.unwrap();
9440
9441 let mut frontend = FrontendState::new();
9442
9443 let mock_ctx = ExecutorContext::new_mock(None).await;
9444 let version = Version(0);
9445
9446 frontend.program = program.clone();
9447 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9448 frontend.update_state_after_exec(outcome, true);
9449 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9450 let sketch_id = sketch_object.id;
9451 let sketch = expect_sketch(sketch_object);
9452 let point0_id = *sketch.segments.first().unwrap();
9453 let point1_id = *sketch.segments.get(1).unwrap();
9454
9455 let label_position = Point2d {
9456 x: Number {
9457 value: 10.0,
9458 units: NumericSuffix::Mm,
9459 },
9460 y: Number {
9461 value: 11.0,
9462 units: NumericSuffix::Mm,
9463 },
9464 };
9465 let constraint = Constraint::Distance(Distance {
9466 points: vec![point0_id.into(), point1_id.into()],
9467 distance: Number {
9468 value: 2.0,
9469 units: NumericSuffix::Mm,
9470 },
9471 label_position: Some(label_position.clone()),
9472 source: Default::default(),
9473 });
9474 let (src_delta, scene_delta) = frontend
9475 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9476 .await
9477 .unwrap();
9478 assert_eq!(
9479 src_delta.text.as_str(),
9480 "\
9481sketch(on = XY) {
9482 point1 = point(at = [var 1, var 2])
9483 point2 = point(at = [var 3, var 4])
9484 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9485}
9486"
9487 );
9488
9489 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9490 let sketch = expect_sketch(sketch_object);
9491 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9492 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9493 panic!("Expected constraint object");
9494 };
9495 let Constraint::Distance(distance) = constraint else {
9496 panic!("Expected distance constraint");
9497 };
9498 assert_eq!(distance.label_position, Some(label_position));
9499
9500 mock_ctx.close().await;
9501 }
9502
9503 #[tokio::test(flavor = "multi_thread")]
9504 async fn test_edit_distance_constraint_label_position() {
9505 let initial_source = "\
9506sketch(on = XY) {
9507 point(at = [var 1, var 2])
9508 point(at = [var 3, var 2])
9509}
9510";
9511
9512 let program = Program::parse(initial_source).unwrap().0.unwrap();
9513
9514 let mut frontend = FrontendState::new();
9515
9516 let mock_ctx = ExecutorContext::new_mock(None).await;
9517 let version = Version(0);
9518
9519 frontend.program = program.clone();
9520 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9521 frontend.update_state_after_exec(outcome, true);
9522 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9523 let sketch_id = sketch_object.id;
9524 let sketch = expect_sketch(sketch_object);
9525 let point0_id = *sketch.segments.first().unwrap();
9526 let point1_id = *sketch.segments.get(1).unwrap();
9527
9528 let constraint = Constraint::Distance(Distance {
9529 points: vec![point0_id.into(), point1_id.into()],
9530 distance: Number {
9531 value: 2.0,
9532 units: NumericSuffix::Mm,
9533 },
9534 label_position: None,
9535 source: Default::default(),
9536 });
9537 let (_, scene_delta) = frontend
9538 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9539 .await
9540 .unwrap();
9541 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9542 let sketch = expect_sketch(sketch_object);
9543 let constraint_id = sketch.constraints[0];
9544 let label_position = Point2d {
9545 x: Number {
9546 value: 10.0,
9547 units: NumericSuffix::Mm,
9548 },
9549 y: Number {
9550 value: 11.0,
9551 units: NumericSuffix::Mm,
9552 },
9553 };
9554
9555 let (src_delta, scene_delta) = frontend
9556 .edit_distance_constraint_label_position(
9557 &mock_ctx,
9558 version,
9559 sketch_id,
9560 constraint_id,
9561 label_position.clone(),
9562 vec![],
9563 )
9564 .await
9565 .unwrap();
9566 assert_eq!(
9567 src_delta.text.as_str(),
9568 "\
9569sketch(on = XY) {
9570 point1 = point(at = [var 1, var 2])
9571 point2 = point(at = [var 3, var 2])
9572 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9573}
9574"
9575 );
9576
9577 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9578 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9579 panic!("Expected constraint object");
9580 };
9581 let Constraint::Distance(distance) = constraint else {
9582 panic!("Expected distance constraint");
9583 };
9584 assert_eq!(distance.label_position, Some(label_position));
9585
9586 mock_ctx.close().await;
9587 }
9588
9589 #[tokio::test(flavor = "multi_thread")]
9590 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9591 let initial_source = "\
9592sketch(on = XY) {
9593 point1 = point(at = [var 0mm, var 0mm])
9594 point2 = point(at = [var 10mm, var 0mm])
9595 distance([point1, point2]) == 5mm
9596}
9597";
9598
9599 let program = Program::parse(initial_source).unwrap().0.unwrap();
9600 let mut frontend = FrontendState::new();
9601 let mock_ctx = ExecutorContext::new_mock(None).await;
9602 let version = Version(0);
9603
9604 frontend.program = program.clone();
9605 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9606 frontend.update_state_after_exec(outcome, true);
9607 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9608 let sketch_id = sketch_object.id;
9609 let sketch = expect_sketch(sketch_object);
9610 let point0_id = sketch.segments[0];
9611 let point1_id = sketch.segments[1];
9612 let constraint_id = sketch.constraints[0];
9613
9614 let edited_segments = vec![ExistingSegmentCtor {
9615 id: point0_id,
9616 ctor: SegmentCtor::Point(PointCtor {
9617 position: Point2d {
9618 x: Expr::Var(Number {
9619 value: 2.0,
9620 units: NumericSuffix::Mm,
9621 }),
9622 y: Expr::Var(Number {
9623 value: 1.0,
9624 units: NumericSuffix::Mm,
9625 }),
9626 },
9627 }),
9628 }];
9629 let (_, scene_delta) = frontend
9630 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9631 .await
9632 .unwrap();
9633 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9634 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9635
9636 let label_position = Point2d {
9637 x: Number {
9638 value: 3.0,
9639 units: NumericSuffix::Mm,
9640 },
9641 y: Number {
9642 value: 4.0,
9643 units: NumericSuffix::Mm,
9644 },
9645 };
9646 let (_, scene_delta) = frontend
9647 .edit_distance_constraint_label_position(
9648 &mock_ctx,
9649 version,
9650 sketch_id,
9651 constraint_id,
9652 label_position,
9653 vec![point0_id],
9654 )
9655 .await
9656 .unwrap();
9657
9658 assert_point_position_close(
9659 point_position(&scene_delta.new_graph, point0_id),
9660 point0_after_segment_edit,
9661 );
9662 assert_point_position_close(
9663 point_position(&scene_delta.new_graph, point1_id),
9664 point1_after_segment_edit,
9665 );
9666
9667 mock_ctx.close().await;
9668 }
9669
9670 #[tokio::test(flavor = "multi_thread")]
9671 async fn test_edit_distance_constraint_value_commits_solved_point_guesses() {
9672 let initial_source = "\
9673sketch(on = XY) {
9674 point1 = point(at = [var 1mm, var 2mm])
9675 point2 = point(at = [var 3mm, var 2mm])
9676 distance([point1, point2]) == 2mm
9677}
9678";
9679
9680 let program = Program::parse(initial_source).unwrap().0.unwrap();
9681 let mut frontend = FrontendState::new();
9682 let mock_ctx = ExecutorContext::new_mock(None).await;
9683 let version = Version(0);
9684
9685 frontend.program = program.clone();
9686 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9687 frontend.update_state_after_exec(outcome, true);
9688 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9689 let sketch_id = sketch_object.id;
9690 let sketch = expect_sketch(sketch_object);
9691 let constraint_id = sketch.constraints[0];
9692 let point0_id = sketch.segments[0];
9693 let point1_id = sketch.segments[1];
9694
9695 let (src_delta, scene_delta) = frontend
9696 .edit_constraint(&mock_ctx, version, sketch_id, constraint_id, "4mm".to_owned())
9697 .await
9698 .unwrap();
9699
9700 assert_eq!(
9701 src_delta.text.as_str(),
9702 "\
9703sketch(on = XY) {
9704 point1 = point(at = [var 0mm, var 2mm])
9705 point2 = point(at = [var 4mm, var 2mm])
9706 distance([point1, point2]) == 4mm
9707}
9708"
9709 );
9710
9711 assert_point_position_close(
9712 point_position(&scene_delta.new_graph, point0_id),
9713 Point2d {
9714 x: Number {
9715 value: 0.0,
9716 units: NumericSuffix::Mm,
9717 },
9718 y: Number {
9719 value: 2.0,
9720 units: NumericSuffix::Mm,
9721 },
9722 },
9723 );
9724 assert_point_position_close(
9725 point_position(&scene_delta.new_graph, point1_id),
9726 Point2d {
9727 x: Number {
9728 value: 4.0,
9729 units: NumericSuffix::Mm,
9730 },
9731 y: Number {
9732 value: 2.0,
9733 units: NumericSuffix::Mm,
9734 },
9735 },
9736 );
9737
9738 mock_ctx.close().await;
9739 }
9740
9741 #[tokio::test(flavor = "multi_thread")]
9742 async fn test_distance_point_line() {
9743 let initial_source = "\
9744sketch(on = XY) {
9745 point(at = [var 0, var 5])
9746 line(start = [var 0, var 0], end = [var 10, var 0])
9747}
9748";
9749
9750 let program = Program::parse(initial_source).unwrap().0.unwrap();
9751
9752 let mut frontend = FrontendState::new();
9753
9754 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9755 let mock_ctx = ExecutorContext::new_mock(None).await;
9756 let version = Version(0);
9757
9758 frontend.hack_set_program(&ctx, program).await.unwrap();
9759 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9760 let sketch_id = sketch_object.id;
9761 let sketch = expect_sketch(sketch_object);
9762 let point_id = *sketch.segments.first().unwrap();
9763 let line_id = *sketch
9764 .segments
9765 .iter()
9766 .find(|segment_id| {
9767 matches!(
9768 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9769 Some(ObjectKind::Segment {
9770 segment: Segment::Line(_)
9771 })
9772 )
9773 })
9774 .unwrap();
9775
9776 let label_position = Point2d {
9777 x: Number {
9778 value: 10.0,
9779 units: NumericSuffix::Mm,
9780 },
9781 y: Number {
9782 value: 11.0,
9783 units: NumericSuffix::Mm,
9784 },
9785 };
9786 let constraint = Constraint::Distance(Distance {
9787 points: vec![point_id.into(), line_id.into()],
9788 distance: Number {
9789 value: 5.0,
9790 units: NumericSuffix::Mm,
9791 },
9792 label_position: Some(label_position.clone()),
9793 source: Default::default(),
9794 });
9795 let (src_delta, scene_delta) = frontend
9796 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9797 .await
9798 .unwrap();
9799 assert_eq!(
9800 src_delta.text.as_str(),
9801 "\
9802sketch(on = XY) {
9803 point1 = point(at = [var 0, var 5])
9804 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9805 distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
9806}
9807"
9808 );
9809 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9810 let sketch = expect_sketch(sketch_object);
9811 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9812 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9813 panic!("Expected constraint object");
9814 };
9815 let Constraint::Distance(distance) = constraint else {
9816 panic!("Expected distance constraint");
9817 };
9818 assert_eq!(distance.label_position, Some(label_position));
9819
9820 ctx.close().await;
9821 mock_ctx.close().await;
9822 }
9823
9824 #[tokio::test(flavor = "multi_thread")]
9825 async fn test_distance_point_arc() {
9826 let initial_source = "\
9827sketch(on = XY) {
9828 point(at = [var 0, var 8])
9829 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9830}
9831";
9832
9833 let program = Program::parse(initial_source).unwrap().0.unwrap();
9834
9835 let mut frontend = FrontendState::new();
9836
9837 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9838 let mock_ctx = ExecutorContext::new_mock(None).await;
9839 let version = Version(0);
9840
9841 frontend.hack_set_program(&ctx, program).await.unwrap();
9842 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9843 let sketch_id = sketch_object.id;
9844 let sketch = expect_sketch(sketch_object);
9845 let point_id = *sketch.segments.first().unwrap();
9846 let arc_id = *sketch
9847 .segments
9848 .iter()
9849 .find(|segment_id| {
9850 matches!(
9851 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9852 Some(ObjectKind::Segment {
9853 segment: Segment::Arc(_)
9854 })
9855 )
9856 })
9857 .unwrap();
9858
9859 let constraint = Constraint::Distance(Distance {
9860 points: vec![point_id.into(), arc_id.into()],
9861 distance: Number {
9862 value: 3.0,
9863 units: NumericSuffix::Mm,
9864 },
9865 label_position: None,
9866 source: Default::default(),
9867 });
9868 let (src_delta, _scene_delta) = frontend
9869 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9870 .await
9871 .unwrap();
9872 assert_eq!(
9873 src_delta.text.as_str(),
9874 "\
9875sketch(on = XY) {
9876 point1 = point(at = [var 0, var 8])
9877 arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
9878 distance([point1, arc1]) == 3mm
9879}
9880"
9881 );
9882
9883 ctx.close().await;
9884 mock_ctx.close().await;
9885 }
9886
9887 #[tokio::test(flavor = "multi_thread")]
9888 async fn test_distance_arc_origin() {
9889 let initial_source = "\
9890sketch001 = sketch(on = XY) {
9891 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
9892}
9893";
9894
9895 let program = Program::parse(initial_source).unwrap().0.unwrap();
9896
9897 let mut frontend = FrontendState::new();
9898
9899 let mock_ctx = ExecutorContext::new_mock(None).await;
9900 let version = Version(0);
9901
9902 frontend.program = program.clone();
9903 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9904 frontend.update_state_after_exec(outcome, true);
9905 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9906 let sketch_id = sketch_object.id;
9907 let sketch = expect_sketch(sketch_object);
9908 let arc_id = *sketch
9909 .segments
9910 .iter()
9911 .find(|segment_id| {
9912 matches!(
9913 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9914 Some(ObjectKind::Segment {
9915 segment: Segment::Arc(_)
9916 })
9917 )
9918 })
9919 .unwrap();
9920
9921 let constraint = Constraint::Distance(Distance {
9922 points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
9923 distance: Number {
9924 value: 3.0,
9925 units: NumericSuffix::Mm,
9926 },
9927 label_position: None,
9928 source: Default::default(),
9929 });
9930 let (src_delta, _scene_delta) = frontend
9931 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9932 .await
9933 .unwrap();
9934 assert_eq!(
9935 src_delta.text.as_str(),
9936 "\
9937sketch001 = sketch(on = XY) {
9938 arc1 = arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
9939 distance([arc1, ORIGIN]) == 3mm
9940}
9941"
9942 );
9943
9944 mock_ctx.close().await;
9945 }
9946
9947 #[tokio::test(flavor = "multi_thread")]
9948 async fn test_distance_line_origin() {
9949 let initial_source = "\
9950sketch(on = XY) {
9951 line(start = [var 5, var 0], end = [var 5, var 10])
9952}
9953";
9954
9955 let program = Program::parse(initial_source).unwrap().0.unwrap();
9956
9957 let mut frontend = FrontendState::new();
9958
9959 let mock_ctx = ExecutorContext::new_mock(None).await;
9960 let version = Version(0);
9961
9962 frontend.program = program.clone();
9963 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9964 frontend.update_state_after_exec(outcome, true);
9965 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9966 let sketch_id = sketch_object.id;
9967 let sketch = expect_sketch(sketch_object);
9968 let line_id = *sketch
9969 .segments
9970 .iter()
9971 .find(|segment_id| {
9972 matches!(
9973 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
9974 Some(ObjectKind::Segment {
9975 segment: Segment::Line(_)
9976 })
9977 )
9978 })
9979 .unwrap();
9980
9981 let constraint = Constraint::Distance(Distance {
9982 points: vec![ConstraintSegment::ORIGIN, line_id.into()],
9983 distance: Number {
9984 value: 5.0,
9985 units: NumericSuffix::Mm,
9986 },
9987 label_position: None,
9988 source: Default::default(),
9989 });
9990 let (src_delta, _scene_delta) = frontend
9991 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9992 .await
9993 .unwrap();
9994 assert_eq!(
9995 src_delta.text.as_str(),
9996 "\
9997sketch(on = XY) {
9998 line1 = line(start = [var 5, var 0], end = [var 5, var 10])
9999 distance([ORIGIN, line1]) == 5mm
10000}
10001"
10002 );
10003
10004 mock_ctx.close().await;
10005 }
10006
10007 #[tokio::test(flavor = "multi_thread")]
10008 async fn test_distance_line_circle() {
10009 let initial_source = "\
10010sketch(on = XY) {
10011 line(start = [var -10, var 8], end = [var 10, var 8])
10012 circle(start = [var 5, var 0], center = [var 0, var 0])
10013}
10014";
10015
10016 let program = Program::parse(initial_source).unwrap().0.unwrap();
10017
10018 let mut frontend = FrontendState::new();
10019
10020 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10021 let mock_ctx = ExecutorContext::new_mock(None).await;
10022 let version = Version(0);
10023
10024 frontend.hack_set_program(&ctx, program).await.unwrap();
10025 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10026 let sketch_id = sketch_object.id;
10027 let sketch = expect_sketch(sketch_object);
10028 let line_id = *sketch
10029 .segments
10030 .iter()
10031 .find(|segment_id| {
10032 matches!(
10033 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10034 Some(ObjectKind::Segment {
10035 segment: Segment::Line(_)
10036 })
10037 )
10038 })
10039 .unwrap();
10040 let circle_id = *sketch
10041 .segments
10042 .iter()
10043 .find(|segment_id| {
10044 matches!(
10045 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10046 Some(ObjectKind::Segment {
10047 segment: Segment::Circle(_)
10048 })
10049 )
10050 })
10051 .unwrap();
10052
10053 let constraint = Constraint::Distance(Distance {
10054 points: vec![line_id.into(), circle_id.into()],
10055 distance: Number {
10056 value: 3.0,
10057 units: NumericSuffix::Mm,
10058 },
10059 label_position: None,
10060 source: Default::default(),
10061 });
10062 let (src_delta, _scene_delta) = frontend
10063 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10064 .await
10065 .unwrap();
10066 assert_eq!(
10067 src_delta.text.as_str(),
10068 "\
10069sketch(on = XY) {
10070 line1 = line(start = [var -10, var 8], end = [var 10, var 8])
10071 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
10072 distance([line1, circle1]) == 3mm
10073}
10074"
10075 );
10076
10077 ctx.close().await;
10078 mock_ctx.close().await;
10079 }
10080
10081 #[tokio::test(flavor = "multi_thread")]
10082 async fn test_distance_circle_arc() {
10083 let initial_source = "\
10084sketch(on = XY) {
10085 circle(start = [var 5, var 0], center = [var 0, var 0])
10086 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
10087}
10088";
10089
10090 let program = Program::parse(initial_source).unwrap().0.unwrap();
10091
10092 let mut frontend = FrontendState::new();
10093
10094 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10095 let mock_ctx = ExecutorContext::new_mock(None).await;
10096 let version = Version(0);
10097
10098 frontend.hack_set_program(&ctx, program).await.unwrap();
10099 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10100 let sketch_id = sketch_object.id;
10101 let sketch = expect_sketch(sketch_object);
10102 let circle_id = *sketch
10103 .segments
10104 .iter()
10105 .find(|segment_id| {
10106 matches!(
10107 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10108 Some(ObjectKind::Segment {
10109 segment: Segment::Circle(_)
10110 })
10111 )
10112 })
10113 .unwrap();
10114 let arc_id = *sketch
10115 .segments
10116 .iter()
10117 .find(|segment_id| {
10118 matches!(
10119 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10120 Some(ObjectKind::Segment {
10121 segment: Segment::Arc(_)
10122 })
10123 )
10124 })
10125 .unwrap();
10126
10127 let constraint = Constraint::Distance(Distance {
10128 points: vec![circle_id.into(), arc_id.into()],
10129 distance: Number {
10130 value: 3.0,
10131 units: NumericSuffix::Mm,
10132 },
10133 label_position: None,
10134 source: Default::default(),
10135 });
10136 let (src_delta, _scene_delta) = frontend
10137 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10138 .await
10139 .unwrap();
10140 assert_eq!(
10141 src_delta.text.as_str(),
10142 "\
10143sketch(on = XY) {
10144 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
10145 arc1 = arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
10146 distance([circle1, arc1]) == 3mm
10147}
10148"
10149 );
10150
10151 ctx.close().await;
10152 mock_ctx.close().await;
10153 }
10154
10155 #[tokio::test(flavor = "multi_thread")]
10156 async fn test_distance_parallel_lines() {
10157 let initial_source = "\
10158sketch(on = XY) {
10159 line(start = [var 0, var 0], end = [var 10, var 0])
10160 line(start = [var 0, var 5], end = [var 10, var 5])
10161}
10162";
10163
10164 let program = Program::parse(initial_source).unwrap().0.unwrap();
10165
10166 let mut frontend = FrontendState::new();
10167
10168 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10169 let mock_ctx = ExecutorContext::new_mock(None).await;
10170 let version = Version(0);
10171
10172 frontend.hack_set_program(&ctx, program).await.unwrap();
10173 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10174 let sketch_id = sketch_object.id;
10175 let sketch = expect_sketch(sketch_object);
10176 let line_ids = sketch
10177 .segments
10178 .iter()
10179 .copied()
10180 .filter(|segment_id| {
10181 matches!(
10182 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10183 Some(ObjectKind::Segment {
10184 segment: Segment::Line(_)
10185 })
10186 )
10187 })
10188 .collect::<Vec<_>>();
10189
10190 let constraint = Constraint::Distance(Distance {
10191 points: vec![line_ids[0].into(), line_ids[1].into()],
10192 distance: Number {
10193 value: 5.0,
10194 units: NumericSuffix::Mm,
10195 },
10196 label_position: None,
10197 source: Default::default(),
10198 });
10199 let (src_delta, _scene_delta) = frontend
10200 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10201 .await
10202 .unwrap();
10203 assert_eq!(
10204 src_delta.text.as_str(),
10205 "\
10206sketch(on = XY) {
10207 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10208 line2 = line(start = [var 0, var 5], end = [var 10, var 5])
10209 distance([line1, line2]) == 5mm
10210}
10211"
10212 );
10213
10214 ctx.close().await;
10215 mock_ctx.close().await;
10216 }
10217
10218 #[tokio::test(flavor = "multi_thread")]
10219 async fn test_distance_non_parallel_lines_lowers_to_distance() {
10220 let initial_source = "\
10221sketch(on = XY) {
10222 line(start = [var 0, var 0], end = [var 10, var 0])
10223 line(start = [var 0, var 0], end = [var 0, var 10])
10224}
10225";
10226
10227 let program = Program::parse(initial_source).unwrap().0.unwrap();
10228
10229 let mut frontend = FrontendState::new();
10230
10231 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10232 let mock_ctx = ExecutorContext::new_mock(None).await;
10233 let version = Version(0);
10234
10235 frontend.hack_set_program(&ctx, program).await.unwrap();
10236 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10237 let sketch_id = sketch_object.id;
10238 let sketch = expect_sketch(sketch_object);
10239 let line_ids = sketch
10240 .segments
10241 .iter()
10242 .copied()
10243 .filter(|segment_id| {
10244 matches!(
10245 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10246 Some(ObjectKind::Segment {
10247 segment: Segment::Line(_)
10248 })
10249 )
10250 })
10251 .collect::<Vec<_>>();
10252
10253 let constraint = Constraint::Distance(Distance {
10254 points: vec![line_ids[0].into(), line_ids[1].into()],
10255 distance: Number {
10256 value: 5.0,
10257 units: NumericSuffix::Mm,
10258 },
10259 label_position: None,
10260 source: Default::default(),
10261 });
10262 let (src_delta, _scene_delta) = frontend
10263 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10264 .await
10265 .unwrap();
10266 assert_eq!(
10267 src_delta.text.as_str(),
10268 "\
10269sketch(on = XY) {
10270 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10271 line2 = line(start = [var 0, var 0], end = [var 0, var 10])
10272 distance([line1, line2]) == 5mm
10273}
10274"
10275 );
10276
10277 ctx.close().await;
10278 mock_ctx.close().await;
10279 }
10280
10281 #[tokio::test(flavor = "multi_thread")]
10282 async fn test_horizontal_distance_two_points() {
10283 let initial_source = "\
10284sketch(on = XY) {
10285 point(at = [var 1, var 2])
10286 point(at = [var 3, var 4])
10287}
10288";
10289
10290 let program = Program::parse(initial_source).unwrap().0.unwrap();
10291
10292 let mut frontend = FrontendState::new();
10293
10294 let mock_ctx = ExecutorContext::new_mock(None).await;
10295 let version = Version(0);
10296
10297 frontend.program = program.clone();
10298 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10299 frontend.update_state_after_exec(outcome, true);
10300 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10301 let sketch_id = sketch_object.id;
10302 let sketch = expect_sketch(sketch_object);
10303 let point0_id = *sketch.segments.first().unwrap();
10304 let point1_id = *sketch.segments.get(1).unwrap();
10305 let label_position = Point2d {
10306 x: Number {
10307 value: 10.0,
10308 units: NumericSuffix::Mm,
10309 },
10310 y: Number {
10311 value: 11.0,
10312 units: NumericSuffix::Mm,
10313 },
10314 };
10315
10316 let constraint = Constraint::HorizontalDistance(Distance {
10317 points: vec![point0_id.into(), point1_id.into()],
10318 distance: Number {
10319 value: 2.0,
10320 units: NumericSuffix::Mm,
10321 },
10322 label_position: Some(label_position.clone()),
10323 source: Default::default(),
10324 });
10325 let (src_delta, scene_delta) = frontend
10326 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10327 .await
10328 .unwrap();
10329 assert_eq!(
10330 src_delta.text.as_str(),
10331 "\
10333sketch(on = XY) {
10334 point1 = point(at = [var 1, var 2])
10335 point2 = point(at = [var 3, var 4])
10336 horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10337}
10338"
10339 );
10340 assert_eq!(
10341 scene_delta.new_graph.objects.len(),
10342 5,
10343 "{:#?}",
10344 scene_delta.new_graph.objects
10345 );
10346 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10347 let sketch = expect_sketch(sketch_object);
10348 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10349 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10350 panic!("Expected constraint object");
10351 };
10352 let Constraint::HorizontalDistance(distance) = constraint else {
10353 panic!("Expected horizontal distance constraint");
10354 };
10355 assert_eq!(distance.label_position, Some(label_position));
10356
10357 mock_ctx.close().await;
10358 }
10359
10360 #[tokio::test(flavor = "multi_thread")]
10361 async fn test_radius_single_arc_segment() {
10362 let initial_source = "\
10363sketch(on = XY) {
10364 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10365}
10366";
10367
10368 let program = Program::parse(initial_source).unwrap().0.unwrap();
10369
10370 let mut frontend = FrontendState::new();
10371
10372 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10373 let mock_ctx = ExecutorContext::new_mock(None).await;
10374 let version = Version(0);
10375
10376 frontend.hack_set_program(&ctx, program).await.unwrap();
10377 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10378 let sketch_id = sketch_object.id;
10379 let sketch = expect_sketch(sketch_object);
10380 let arc_id = sketch
10382 .segments
10383 .iter()
10384 .find(|&seg_id| {
10385 let obj = frontend.scene_graph.objects.get(seg_id.0);
10386 matches!(
10387 obj.map(|o| &o.kind),
10388 Some(ObjectKind::Segment {
10389 segment: Segment::Arc(_)
10390 })
10391 )
10392 })
10393 .unwrap();
10394
10395 let constraint = Constraint::Radius(Radius {
10396 arc: *arc_id,
10397 radius: Number {
10398 value: 5.0,
10399 units: NumericSuffix::Mm,
10400 },
10401 label_position: None,
10402 source: Default::default(),
10403 });
10404 let (src_delta, scene_delta) = frontend
10405 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10406 .await
10407 .unwrap();
10408 assert_eq!(
10409 src_delta.text.as_str(),
10410 "\
10412sketch(on = XY) {
10413 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10414 radius(arc1) == 5mm
10415}
10416"
10417 );
10418 assert_eq!(
10419 scene_delta.new_graph.objects.len(),
10420 7, "{:#?}",
10422 scene_delta.new_graph.objects
10423 );
10424
10425 ctx.close().await;
10426 mock_ctx.close().await;
10427 }
10428
10429 #[tokio::test(flavor = "multi_thread")]
10430 async fn test_radius_single_arc_segment_with_label_position() {
10431 let initial_source = "\
10432sketch(on = XY) {
10433 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10434}
10435";
10436
10437 let program = Program::parse(initial_source).unwrap().0.unwrap();
10438 let mut frontend = FrontendState::new();
10439 let mock_ctx = ExecutorContext::new_mock(None).await;
10440 let version = Version(0);
10441
10442 frontend.program = program.clone();
10443 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10444 frontend.update_state_after_exec(outcome, true);
10445 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10446 let sketch_id = sketch_object.id;
10447 let sketch = expect_sketch(sketch_object);
10448 let arc_id = sketch
10449 .segments
10450 .iter()
10451 .find(|&seg_id| {
10452 let obj = frontend.scene_graph.objects.get(seg_id.0);
10453 matches!(
10454 obj.map(|o| &o.kind),
10455 Some(ObjectKind::Segment {
10456 segment: Segment::Arc(_)
10457 })
10458 )
10459 })
10460 .unwrap();
10461
10462 let label_position = Point2d {
10463 x: Number {
10464 value: 10.0,
10465 units: NumericSuffix::Mm,
10466 },
10467 y: Number {
10468 value: 11.0,
10469 units: NumericSuffix::Mm,
10470 },
10471 };
10472 let constraint = Constraint::Radius(Radius {
10473 arc: *arc_id,
10474 radius: Number {
10475 value: 5.0,
10476 units: NumericSuffix::Mm,
10477 },
10478 label_position: Some(label_position.clone()),
10479 source: Default::default(),
10480 });
10481 let (src_delta, scene_delta) = frontend
10482 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10483 .await
10484 .unwrap();
10485 assert_eq!(
10486 src_delta.text.as_str(),
10487 "\
10488sketch(on = XY) {
10489 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10490 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10491}
10492"
10493 );
10494
10495 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10496 let sketch = expect_sketch(sketch_object);
10497 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10498 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10499 panic!("Expected constraint object");
10500 };
10501 let Constraint::Radius(radius) = constraint else {
10502 panic!("Expected radius constraint");
10503 };
10504 assert_eq!(radius.label_position, Some(label_position));
10505
10506 mock_ctx.close().await;
10507 }
10508
10509 #[tokio::test(flavor = "multi_thread")]
10510 async fn test_edit_radius_constraint_label_position() {
10511 let initial_source = "\
10512sketch(on = XY) {
10513 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10514 radius(arc1) == 5mm
10515}
10516";
10517
10518 let program = Program::parse(initial_source).unwrap().0.unwrap();
10519 let mut frontend = FrontendState::new();
10520 let mock_ctx = ExecutorContext::new_mock(None).await;
10521 let version = Version(0);
10522
10523 frontend.program = program.clone();
10524 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10525 frontend.update_state_after_exec(outcome, true);
10526 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10527 let sketch_id = sketch_object.id;
10528 let sketch = expect_sketch(sketch_object);
10529 let constraint_id = sketch.constraints[0];
10530 let label_position = Point2d {
10531 x: Number {
10532 value: 10.0,
10533 units: NumericSuffix::Mm,
10534 },
10535 y: Number {
10536 value: 11.0,
10537 units: NumericSuffix::Mm,
10538 },
10539 };
10540
10541 let (src_delta, scene_delta) = frontend
10542 .edit_distance_constraint_label_position(
10543 &mock_ctx,
10544 version,
10545 sketch_id,
10546 constraint_id,
10547 label_position.clone(),
10548 vec![],
10549 )
10550 .await
10551 .unwrap();
10552 assert_eq!(
10553 src_delta.text.as_str(),
10554 "\
10555sketch(on = XY) {
10556 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10557 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
10558}
10559"
10560 );
10561
10562 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10563 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10564 panic!("Expected constraint object");
10565 };
10566 let Constraint::Radius(radius) = constraint else {
10567 panic!("Expected radius constraint");
10568 };
10569 assert_eq!(radius.label_position, Some(label_position));
10570
10571 mock_ctx.close().await;
10572 }
10573
10574 #[tokio::test(flavor = "multi_thread")]
10575 async fn test_vertical_distance_two_points() {
10576 let initial_source = "\
10577sketch(on = XY) {
10578 point(at = [var 1, var 2])
10579 point(at = [var 3, var 4])
10580}
10581";
10582
10583 let program = Program::parse(initial_source).unwrap().0.unwrap();
10584
10585 let mut frontend = FrontendState::new();
10586
10587 let mock_ctx = ExecutorContext::new_mock(None).await;
10588 let version = Version(0);
10589
10590 frontend.program = program.clone();
10591 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10592 frontend.update_state_after_exec(outcome, true);
10593 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10594 let sketch_id = sketch_object.id;
10595 let sketch = expect_sketch(sketch_object);
10596 let point0_id = *sketch.segments.first().unwrap();
10597 let point1_id = *sketch.segments.get(1).unwrap();
10598 let label_position = Point2d {
10599 x: Number {
10600 value: 10.0,
10601 units: NumericSuffix::Mm,
10602 },
10603 y: Number {
10604 value: 11.0,
10605 units: NumericSuffix::Mm,
10606 },
10607 };
10608
10609 let constraint = Constraint::VerticalDistance(Distance {
10610 points: vec![point0_id.into(), point1_id.into()],
10611 distance: Number {
10612 value: 2.0,
10613 units: NumericSuffix::Mm,
10614 },
10615 label_position: Some(label_position.clone()),
10616 source: Default::default(),
10617 });
10618 let (src_delta, scene_delta) = frontend
10619 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10620 .await
10621 .unwrap();
10622 assert_eq!(
10623 src_delta.text.as_str(),
10624 "\
10626sketch(on = XY) {
10627 point1 = point(at = [var 1, var 2])
10628 point2 = point(at = [var 3, var 4])
10629 verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10630}
10631"
10632 );
10633 assert_eq!(
10634 scene_delta.new_graph.objects.len(),
10635 5,
10636 "{:#?}",
10637 scene_delta.new_graph.objects
10638 );
10639 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10640 let sketch = expect_sketch(sketch_object);
10641 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10642 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10643 panic!("Expected constraint object");
10644 };
10645 let Constraint::VerticalDistance(distance) = constraint else {
10646 panic!("Expected vertical distance constraint");
10647 };
10648 assert_eq!(distance.label_position, Some(label_position));
10649
10650 mock_ctx.close().await;
10651 }
10652
10653 #[tokio::test(flavor = "multi_thread")]
10654 async fn test_add_fixed_standalone_point() {
10655 let initial_source = "\
10656sketch(on = XY) {
10657 point(at = [var 1, var 2])
10658}
10659";
10660
10661 let program = Program::parse(initial_source).unwrap().0.unwrap();
10662
10663 let mut frontend = FrontendState::new();
10664
10665 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10666 let mock_ctx = ExecutorContext::new_mock(None).await;
10667 let version = Version(0);
10668
10669 frontend.hack_set_program(&ctx, program).await.unwrap();
10670 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10671 let sketch_id = sketch_object.id;
10672 let sketch = expect_sketch(sketch_object);
10673 let point_id = *sketch.segments.first().unwrap();
10674
10675 let (src_delta, scene_delta) = frontend
10676 .add_constraint(
10677 &mock_ctx,
10678 version,
10679 sketch_id,
10680 Constraint::Fixed(Fixed {
10681 points: vec![FixedPoint {
10682 point: point_id,
10683 position: Point2d {
10684 x: Number {
10685 value: 2.0,
10686 units: NumericSuffix::Mm,
10687 },
10688 y: Number {
10689 value: 3.0,
10690 units: NumericSuffix::Mm,
10691 },
10692 },
10693 }],
10694 }),
10695 )
10696 .await
10697 .unwrap();
10698 assert_eq!(
10699 src_delta.text.as_str(),
10700 "\
10701sketch(on = XY) {
10702 point1 = point(at = [var 1, var 2])
10703 fixed([point1, [2mm, 3mm]])
10704}
10705"
10706 );
10707 assert_eq!(
10708 scene_delta.new_graph.objects.len(),
10709 4,
10710 "{:#?}",
10711 scene_delta.new_graph.objects
10712 );
10713
10714 ctx.close().await;
10715 mock_ctx.close().await;
10716 }
10717
10718 #[tokio::test(flavor = "multi_thread")]
10719 async fn test_add_fixed_multiple_points() {
10720 let initial_source = "\
10721sketch(on = XY) {
10722 point(at = [var 1, var 2])
10723 point(at = [var 3, var 4])
10724}
10725";
10726
10727 let program = Program::parse(initial_source).unwrap().0.unwrap();
10728
10729 let mut frontend = FrontendState::new();
10730
10731 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10732 let mock_ctx = ExecutorContext::new_mock(None).await;
10733 let version = Version(0);
10734
10735 frontend.hack_set_program(&ctx, program).await.unwrap();
10736 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10737 let sketch_id = sketch_object.id;
10738 let sketch = expect_sketch(sketch_object);
10739 let point0_id = *sketch.segments.first().unwrap();
10740 let point1_id = *sketch.segments.get(1).unwrap();
10741
10742 let (src_delta, scene_delta) = frontend
10743 .add_constraint(
10744 &mock_ctx,
10745 version,
10746 sketch_id,
10747 Constraint::Fixed(Fixed {
10748 points: vec![
10749 FixedPoint {
10750 point: point0_id,
10751 position: Point2d {
10752 x: Number {
10753 value: 2.0,
10754 units: NumericSuffix::Mm,
10755 },
10756 y: Number {
10757 value: 3.0,
10758 units: NumericSuffix::Mm,
10759 },
10760 },
10761 },
10762 FixedPoint {
10763 point: point1_id,
10764 position: Point2d {
10765 x: Number {
10766 value: 4.0,
10767 units: NumericSuffix::Mm,
10768 },
10769 y: Number {
10770 value: 5.0,
10771 units: NumericSuffix::Mm,
10772 },
10773 },
10774 },
10775 ],
10776 }),
10777 )
10778 .await
10779 .unwrap();
10780 assert_eq!(
10781 src_delta.text.as_str(),
10782 "\
10783sketch(on = XY) {
10784 point1 = point(at = [var 1, var 2])
10785 point2 = point(at = [var 3, var 4])
10786 fixed([point1, [2mm, 3mm]])
10787 fixed([point2, [4mm, 5mm]])
10788}
10789"
10790 );
10791 assert_eq!(
10792 scene_delta.new_graph.objects.len(),
10793 6,
10794 "{:#?}",
10795 scene_delta.new_graph.objects
10796 );
10797
10798 ctx.close().await;
10799 mock_ctx.close().await;
10800 }
10801
10802 #[tokio::test(flavor = "multi_thread")]
10803 async fn test_add_fixed_owned_point() {
10804 let initial_source = "\
10805sketch(on = XY) {
10806 line(start = [var 1, var 2], end = [var 3, var 4])
10807}
10808";
10809
10810 let program = Program::parse(initial_source).unwrap().0.unwrap();
10811
10812 let mut frontend = FrontendState::new();
10813
10814 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10815 let mock_ctx = ExecutorContext::new_mock(None).await;
10816 let version = Version(0);
10817
10818 frontend.hack_set_program(&ctx, program).await.unwrap();
10819 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10820 let sketch_id = sketch_object.id;
10821 let sketch = expect_sketch(sketch_object);
10822 let line_start_id = *sketch.segments.first().unwrap();
10823
10824 let (src_delta, scene_delta) = frontend
10825 .add_constraint(
10826 &mock_ctx,
10827 version,
10828 sketch_id,
10829 Constraint::Fixed(Fixed {
10830 points: vec![FixedPoint {
10831 point: line_start_id,
10832 position: Point2d {
10833 x: Number {
10834 value: 2.0,
10835 units: NumericSuffix::Mm,
10836 },
10837 y: Number {
10838 value: 3.0,
10839 units: NumericSuffix::Mm,
10840 },
10841 },
10842 }],
10843 }),
10844 )
10845 .await
10846 .unwrap();
10847 assert_eq!(
10848 src_delta.text.as_str(),
10849 "\
10850sketch(on = XY) {
10851 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10852 fixed([line1.start, [2mm, 3mm]])
10853}
10854"
10855 );
10856 assert_eq!(
10857 scene_delta.new_graph.objects.len(),
10858 6,
10859 "{:#?}",
10860 scene_delta.new_graph.objects
10861 );
10862
10863 ctx.close().await;
10864 mock_ctx.close().await;
10865 }
10866
10867 #[tokio::test(flavor = "multi_thread")]
10868 async fn test_radius_error_cases() {
10869 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10870 let mock_ctx = ExecutorContext::new_mock(None).await;
10871 let version = Version(0);
10872
10873 let initial_source_point = "\
10875sketch(on = XY) {
10876 point(at = [var 1, var 2])
10877}
10878";
10879 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10880 let mut frontend_point = FrontendState::new();
10881 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
10882 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
10883 let sketch_id_point = sketch_object_point.id;
10884 let sketch_point = expect_sketch(sketch_object_point);
10885 let point_id = *sketch_point.segments.first().unwrap();
10886
10887 let constraint_point = Constraint::Radius(Radius {
10888 arc: point_id,
10889 radius: Number {
10890 value: 5.0,
10891 units: NumericSuffix::Mm,
10892 },
10893 label_position: None,
10894 source: Default::default(),
10895 });
10896 let result_point = frontend_point
10897 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
10898 .await;
10899 assert!(result_point.is_err(), "Single point should error for radius");
10900
10901 let initial_source_line = "\
10903sketch(on = XY) {
10904 line(start = [var 1, var 2], end = [var 3, var 4])
10905}
10906";
10907 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
10908 let mut frontend_line = FrontendState::new();
10909 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10910 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10911 let sketch_id_line = sketch_object_line.id;
10912 let sketch_line = expect_sketch(sketch_object_line);
10913 let line_id = *sketch_line.segments.first().unwrap();
10914
10915 let constraint_line = Constraint::Radius(Radius {
10916 arc: line_id,
10917 radius: Number {
10918 value: 5.0,
10919 units: NumericSuffix::Mm,
10920 },
10921 label_position: None,
10922 source: Default::default(),
10923 });
10924 let result_line = frontend_line
10925 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10926 .await;
10927 assert!(result_line.is_err(), "Single line segment should error for radius");
10928
10929 ctx.close().await;
10930 mock_ctx.close().await;
10931 }
10932
10933 #[tokio::test(flavor = "multi_thread")]
10934 async fn test_diameter_single_arc_segment() {
10935 let initial_source = "\
10936sketch(on = XY) {
10937 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10938}
10939";
10940
10941 let program = Program::parse(initial_source).unwrap().0.unwrap();
10942
10943 let mut frontend = FrontendState::new();
10944
10945 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10946 let mock_ctx = ExecutorContext::new_mock(None).await;
10947 let version = Version(0);
10948
10949 frontend.hack_set_program(&ctx, program).await.unwrap();
10950 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10951 let sketch_id = sketch_object.id;
10952 let sketch = expect_sketch(sketch_object);
10953 let arc_id = sketch
10955 .segments
10956 .iter()
10957 .find(|&seg_id| {
10958 let obj = frontend.scene_graph.objects.get(seg_id.0);
10959 matches!(
10960 obj.map(|o| &o.kind),
10961 Some(ObjectKind::Segment {
10962 segment: Segment::Arc(_)
10963 })
10964 )
10965 })
10966 .unwrap();
10967
10968 let constraint = Constraint::Diameter(Diameter {
10969 arc: *arc_id,
10970 diameter: Number {
10971 value: 10.0,
10972 units: NumericSuffix::Mm,
10973 },
10974 label_position: None,
10975 source: Default::default(),
10976 });
10977 let (src_delta, scene_delta) = frontend
10978 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10979 .await
10980 .unwrap();
10981 assert_eq!(
10982 src_delta.text.as_str(),
10983 "\
10985sketch(on = XY) {
10986 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10987 diameter(arc1) == 10mm
10988}
10989"
10990 );
10991 assert_eq!(
10992 scene_delta.new_graph.objects.len(),
10993 7, "{:#?}",
10995 scene_delta.new_graph.objects
10996 );
10997
10998 ctx.close().await;
10999 mock_ctx.close().await;
11000 }
11001
11002 #[tokio::test(flavor = "multi_thread")]
11003 async fn test_diameter_single_arc_segment_with_label_position() {
11004 let initial_source = "\
11005sketch(on = XY) {
11006 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11007}
11008";
11009
11010 let program = Program::parse(initial_source).unwrap().0.unwrap();
11011 let mut frontend = FrontendState::new();
11012 let mock_ctx = ExecutorContext::new_mock(None).await;
11013 let version = Version(0);
11014
11015 frontend.program = program.clone();
11016 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11017 frontend.update_state_after_exec(outcome, true);
11018 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11019 let sketch_id = sketch_object.id;
11020 let sketch = expect_sketch(sketch_object);
11021 let arc_id = sketch
11022 .segments
11023 .iter()
11024 .find(|&seg_id| {
11025 let obj = frontend.scene_graph.objects.get(seg_id.0);
11026 matches!(
11027 obj.map(|o| &o.kind),
11028 Some(ObjectKind::Segment {
11029 segment: Segment::Arc(_)
11030 })
11031 )
11032 })
11033 .unwrap();
11034
11035 let label_position = Point2d {
11036 x: Number {
11037 value: 10.0,
11038 units: NumericSuffix::Mm,
11039 },
11040 y: Number {
11041 value: 11.0,
11042 units: NumericSuffix::Mm,
11043 },
11044 };
11045 let constraint = Constraint::Diameter(Diameter {
11046 arc: *arc_id,
11047 diameter: Number {
11048 value: 10.0,
11049 units: NumericSuffix::Mm,
11050 },
11051 label_position: Some(label_position.clone()),
11052 source: Default::default(),
11053 });
11054 let (src_delta, scene_delta) = frontend
11055 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11056 .await
11057 .unwrap();
11058 assert_eq!(
11059 src_delta.text.as_str(),
11060 "\
11061sketch(on = XY) {
11062 arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11063 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
11064}
11065"
11066 );
11067
11068 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11069 let sketch = expect_sketch(sketch_object);
11070 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11071 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11072 panic!("Expected constraint object");
11073 };
11074 let Constraint::Diameter(diameter) = constraint else {
11075 panic!("Expected diameter constraint");
11076 };
11077 assert_eq!(diameter.label_position, Some(label_position));
11078
11079 mock_ctx.close().await;
11080 }
11081
11082 #[tokio::test(flavor = "multi_thread")]
11083 async fn test_edit_diameter_constraint_label_position() {
11084 let initial_source = "\
11085sketch(on = XY) {
11086 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11087 diameter(arc1) == 10mm
11088}
11089";
11090
11091 let program = Program::parse(initial_source).unwrap().0.unwrap();
11092 let mut frontend = FrontendState::new();
11093 let mock_ctx = ExecutorContext::new_mock(None).await;
11094 let version = Version(0);
11095
11096 frontend.program = program.clone();
11097 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11098 frontend.update_state_after_exec(outcome, true);
11099 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11100 let sketch_id = sketch_object.id;
11101 let sketch = expect_sketch(sketch_object);
11102 let constraint_id = sketch.constraints[0];
11103 let label_position = Point2d {
11104 x: Number {
11105 value: 10.0,
11106 units: NumericSuffix::Mm,
11107 },
11108 y: Number {
11109 value: 11.0,
11110 units: NumericSuffix::Mm,
11111 },
11112 };
11113
11114 let (src_delta, scene_delta) = frontend
11115 .edit_distance_constraint_label_position(
11116 &mock_ctx,
11117 version,
11118 sketch_id,
11119 constraint_id,
11120 label_position.clone(),
11121 vec![],
11122 )
11123 .await
11124 .unwrap();
11125 assert_eq!(
11126 src_delta.text.as_str(),
11127 "\
11128sketch(on = XY) {
11129 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11130 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
11131}
11132"
11133 );
11134
11135 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11136 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11137 panic!("Expected constraint object");
11138 };
11139 let Constraint::Diameter(diameter) = constraint else {
11140 panic!("Expected diameter constraint");
11141 };
11142 assert_eq!(diameter.label_position, Some(label_position));
11143
11144 mock_ctx.close().await;
11145 }
11146
11147 #[tokio::test(flavor = "multi_thread")]
11148 async fn test_diameter_error_cases() {
11149 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11150 let mock_ctx = ExecutorContext::new_mock(None).await;
11151 let version = Version(0);
11152
11153 let initial_source_point = "\
11155sketch(on = XY) {
11156 point(at = [var 1, var 2])
11157}
11158";
11159 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
11160 let mut frontend_point = FrontendState::new();
11161 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11162 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11163 let sketch_id_point = sketch_object_point.id;
11164 let sketch_point = expect_sketch(sketch_object_point);
11165 let point_id = *sketch_point.segments.first().unwrap();
11166
11167 let constraint_point = Constraint::Diameter(Diameter {
11168 arc: point_id,
11169 diameter: Number {
11170 value: 10.0,
11171 units: NumericSuffix::Mm,
11172 },
11173 label_position: None,
11174 source: Default::default(),
11175 });
11176 let result_point = frontend_point
11177 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11178 .await;
11179 assert!(result_point.is_err(), "Single point should error for diameter");
11180
11181 let initial_source_line = "\
11183sketch(on = XY) {
11184 line(start = [var 1, var 2], end = [var 3, var 4])
11185}
11186";
11187 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11188 let mut frontend_line = FrontendState::new();
11189 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11190 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11191 let sketch_id_line = sketch_object_line.id;
11192 let sketch_line = expect_sketch(sketch_object_line);
11193 let line_id = *sketch_line.segments.first().unwrap();
11194
11195 let constraint_line = Constraint::Diameter(Diameter {
11196 arc: line_id,
11197 diameter: Number {
11198 value: 10.0,
11199 units: NumericSuffix::Mm,
11200 },
11201 label_position: None,
11202 source: Default::default(),
11203 });
11204 let result_line = frontend_line
11205 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11206 .await;
11207 assert!(result_line.is_err(), "Single line segment should error for diameter");
11208
11209 ctx.close().await;
11210 mock_ctx.close().await;
11211 }
11212
11213 #[tokio::test(flavor = "multi_thread")]
11214 async fn test_line_horizontal() {
11215 let initial_source = "\
11216sketch(on = XY) {
11217 line(start = [var 1, var 2], end = [var 3, var 4])
11218}
11219";
11220
11221 let program = Program::parse(initial_source).unwrap().0.unwrap();
11222
11223 let mut frontend = FrontendState::new();
11224
11225 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11226 let mock_ctx = ExecutorContext::new_mock(None).await;
11227 let version = Version(0);
11228
11229 frontend.hack_set_program(&ctx, program).await.unwrap();
11230 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11231 let sketch_id = sketch_object.id;
11232 let sketch = expect_sketch(sketch_object);
11233 let line1_id = *sketch.segments.get(2).unwrap();
11234
11235 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
11236 let (src_delta, scene_delta) = frontend
11237 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11238 .await
11239 .unwrap();
11240 assert_eq!(
11241 src_delta.text.as_str(),
11242 "\
11243sketch(on = XY) {
11244 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11245 horizontal(line1)
11246}
11247"
11248 );
11249 assert_eq!(
11250 scene_delta.new_graph.objects.len(),
11251 6,
11252 "{:#?}",
11253 scene_delta.new_graph.objects
11254 );
11255
11256 ctx.close().await;
11257 mock_ctx.close().await;
11258 }
11259
11260 #[tokio::test(flavor = "multi_thread")]
11261 async fn test_line_vertical() {
11262 let initial_source = "\
11263sketch(on = XY) {
11264 line(start = [var 1, var 2], end = [var 3, var 4])
11265}
11266";
11267
11268 let program = Program::parse(initial_source).unwrap().0.unwrap();
11269
11270 let mut frontend = FrontendState::new();
11271
11272 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11273 let mock_ctx = ExecutorContext::new_mock(None).await;
11274 let version = Version(0);
11275
11276 frontend.hack_set_program(&ctx, program).await.unwrap();
11277 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11278 let sketch_id = sketch_object.id;
11279 let sketch = expect_sketch(sketch_object);
11280 let line1_id = *sketch.segments.get(2).unwrap();
11281
11282 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
11283 let (src_delta, scene_delta) = frontend
11284 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11285 .await
11286 .unwrap();
11287 assert_eq!(
11288 src_delta.text.as_str(),
11289 "\
11290sketch(on = XY) {
11291 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11292 vertical(line1)
11293}
11294"
11295 );
11296 assert_eq!(
11297 scene_delta.new_graph.objects.len(),
11298 6,
11299 "{:#?}",
11300 scene_delta.new_graph.objects
11301 );
11302
11303 ctx.close().await;
11304 mock_ctx.close().await;
11305 }
11306
11307 #[tokio::test(flavor = "multi_thread")]
11308 async fn test_points_vertical() {
11309 let initial_source = "\
11310sketch001 = sketch(on = XY) {
11311 p0 = point(at = [var -2.23mm, var 3.1mm])
11312 pf = point(at = [4, 4])
11313}
11314";
11315
11316 let program = Program::parse(initial_source).unwrap().0.unwrap();
11317
11318 let mut frontend = FrontendState::new();
11319
11320 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11321 let mock_ctx = ExecutorContext::new_mock(None).await;
11322 let version = Version(0);
11323
11324 frontend.hack_set_program(&ctx, program).await.unwrap();
11325 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11326 let sketch_id = sketch_object.id;
11327 let sketch = expect_sketch(sketch_object);
11328 let point_ids = vec![
11329 sketch.segments.first().unwrap().to_owned(),
11330 sketch.segments.get(1).unwrap().to_owned(),
11331 ];
11332
11333 let constraint = Constraint::Vertical(Vertical::Points {
11334 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
11335 });
11336 let (src_delta, scene_delta) = frontend
11337 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11338 .await
11339 .unwrap();
11340 assert_eq!(
11341 src_delta.text.as_str(),
11342 "\
11343sketch001 = sketch(on = XY) {
11344 p0 = point(at = [var -2.23mm, var 3.1mm])
11345 pf = point(at = [4, 4])
11346 vertical([p0, pf])
11347}
11348"
11349 );
11350 assert_eq!(
11351 scene_delta.new_graph.objects.len(),
11352 5,
11353 "{:#?}",
11354 scene_delta.new_graph.objects
11355 );
11356
11357 ctx.close().await;
11358 mock_ctx.close().await;
11359 }
11360
11361 #[tokio::test(flavor = "multi_thread")]
11362 async fn test_points_horizontal() {
11363 let initial_source = "\
11364sketch001 = sketch(on = XY) {
11365 p0 = point(at = [var -2.23mm, var 3.1mm])
11366 pf = point(at = [4, 4])
11367}
11368";
11369
11370 let program = Program::parse(initial_source).unwrap().0.unwrap();
11371
11372 let mut frontend = FrontendState::new();
11373
11374 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11375 let mock_ctx = ExecutorContext::new_mock(None).await;
11376 let version = Version(0);
11377
11378 frontend.hack_set_program(&ctx, program).await.unwrap();
11379 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11380 let sketch_id = sketch_object.id;
11381 let sketch = expect_sketch(sketch_object);
11382 let point_ids = vec![
11383 sketch.segments.first().unwrap().to_owned(),
11384 sketch.segments.get(1).unwrap().to_owned(),
11385 ];
11386
11387 let constraint = Constraint::Horizontal(Horizontal::Points {
11388 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
11389 });
11390 let (src_delta, scene_delta) = frontend
11391 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11392 .await
11393 .unwrap();
11394 assert_eq!(
11395 src_delta.text.as_str(),
11396 "\
11397sketch001 = sketch(on = XY) {
11398 p0 = point(at = [var -2.23mm, var 3.1mm])
11399 pf = point(at = [4, 4])
11400 horizontal([p0, pf])
11401}
11402"
11403 );
11404 assert_eq!(
11405 scene_delta.new_graph.objects.len(),
11406 5,
11407 "{:#?}",
11408 scene_delta.new_graph.objects
11409 );
11410
11411 ctx.close().await;
11412 mock_ctx.close().await;
11413 }
11414
11415 #[tokio::test(flavor = "multi_thread")]
11416 async fn test_point_horizontal_with_origin() {
11417 let initial_source = "\
11418sketch001 = sketch(on = XY) {
11419 p0 = point(at = [var -2.23mm, var 3.1mm])
11420}
11421";
11422
11423 let program = Program::parse(initial_source).unwrap().0.unwrap();
11424
11425 let mut frontend = FrontendState::new();
11426
11427 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11428 let mock_ctx = ExecutorContext::new_mock(None).await;
11429 let version = Version(0);
11430
11431 frontend.hack_set_program(&ctx, program).await.unwrap();
11432 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11433 let sketch_id = sketch_object.id;
11434 let sketch = expect_sketch(sketch_object);
11435 let point_id = *sketch.segments.first().unwrap();
11436
11437 let constraint = Constraint::Horizontal(Horizontal::Points {
11438 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
11439 });
11440 let (src_delta, scene_delta) = frontend
11441 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11442 .await
11443 .unwrap();
11444 assert_eq!(
11445 src_delta.text.as_str(),
11446 "\
11447sketch001 = sketch(on = XY) {
11448 p0 = point(at = [var -2.23mm, var 3.1mm])
11449 horizontal([p0, ORIGIN])
11450}
11451"
11452 );
11453 assert_eq!(
11454 scene_delta.new_graph.objects.len(),
11455 4,
11456 "{:#?}",
11457 scene_delta.new_graph.objects
11458 );
11459
11460 ctx.close().await;
11461 mock_ctx.close().await;
11462 }
11463
11464 #[tokio::test(flavor = "multi_thread")]
11465 async fn test_lines_equal_length() {
11466 let initial_source = "\
11467sketch(on = XY) {
11468 line(start = [var 1, var 2], end = [var 3, var 4])
11469 line(start = [var 5, var 6], end = [var 7, var 8])
11470}
11471";
11472
11473 let program = Program::parse(initial_source).unwrap().0.unwrap();
11474
11475 let mut frontend = FrontendState::new();
11476
11477 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11478 let mock_ctx = ExecutorContext::new_mock(None).await;
11479 let version = Version(0);
11480
11481 frontend.hack_set_program(&ctx, program).await.unwrap();
11482 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11483 let sketch_id = sketch_object.id;
11484 let sketch = expect_sketch(sketch_object);
11485 let line1_id = *sketch.segments.get(2).unwrap();
11486 let line2_id = *sketch.segments.get(5).unwrap();
11487
11488 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
11489 lines: vec![line1_id, line2_id],
11490 });
11491 let (src_delta, scene_delta) = frontend
11492 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11493 .await
11494 .unwrap();
11495 assert_eq!(
11496 src_delta.text.as_str(),
11497 "\
11498sketch(on = XY) {
11499 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11500 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11501 equalLength([line1, line2])
11502}
11503"
11504 );
11505 assert_eq!(
11506 scene_delta.new_graph.objects.len(),
11507 9,
11508 "{:#?}",
11509 scene_delta.new_graph.objects
11510 );
11511
11512 ctx.close().await;
11513 mock_ctx.close().await;
11514 }
11515
11516 #[tokio::test(flavor = "multi_thread")]
11517 async fn test_add_constraint_multi_line_equal_length() {
11518 let initial_source = "\
11519sketch(on = XY) {
11520 line(start = [var 1, var 2], end = [var 3, var 4])
11521 line(start = [var 5, var 6], end = [var 7, var 8])
11522 line(start = [var 9, var 10], end = [var 11, var 12])
11523}
11524";
11525
11526 let program = Program::parse(initial_source).unwrap().0.unwrap();
11527
11528 let mut frontend = FrontendState::new();
11529 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11530 let mock_ctx = ExecutorContext::new_mock(None).await;
11531 let version = Version(0);
11532
11533 frontend.hack_set_program(&ctx, program).await.unwrap();
11534 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11535 let sketch_id = sketch_object.id;
11536 let sketch = expect_sketch(sketch_object);
11537 let line1_id = *sketch.segments.get(2).unwrap();
11538 let line2_id = *sketch.segments.get(5).unwrap();
11539 let line3_id = *sketch.segments.get(8).unwrap();
11540
11541 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
11542 lines: vec![line1_id, line2_id, line3_id],
11543 });
11544 let (src_delta, scene_delta) = frontend
11545 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11546 .await
11547 .unwrap();
11548 assert_eq!(
11549 src_delta.text.as_str(),
11550 "\
11551sketch(on = XY) {
11552 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11553 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11554 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
11555 equalLength([line1, line2, line3])
11556}
11557"
11558 );
11559 let constraints = scene_delta
11560 .new_graph
11561 .objects
11562 .iter()
11563 .filter_map(|obj| {
11564 let ObjectKind::Constraint { constraint } = &obj.kind else {
11565 return None;
11566 };
11567 Some(constraint)
11568 })
11569 .collect::<Vec<_>>();
11570
11571 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
11572 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
11573 panic!("expected equal length constraint, got {:?}", constraints[0]);
11574 };
11575 assert_eq!(lines_equal_length.lines.len(), 3);
11576
11577 ctx.close().await;
11578 mock_ctx.close().await;
11579 }
11580
11581 #[tokio::test(flavor = "multi_thread")]
11582 async fn test_lines_parallel() {
11583 let initial_source = "\
11584sketch(on = XY) {
11585 line(start = [var 1, var 2], end = [var 3, var 4])
11586 line(start = [var 5, var 6], end = [var 7, var 8])
11587}
11588";
11589
11590 let program = Program::parse(initial_source).unwrap().0.unwrap();
11591
11592 let mut frontend = FrontendState::new();
11593
11594 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11595 let mock_ctx = ExecutorContext::new_mock(None).await;
11596 let version = Version(0);
11597
11598 frontend.hack_set_program(&ctx, program).await.unwrap();
11599 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11600 let sketch_id = sketch_object.id;
11601 let sketch = expect_sketch(sketch_object);
11602 let line1_id = *sketch.segments.get(2).unwrap();
11603 let line2_id = *sketch.segments.get(5).unwrap();
11604
11605 let constraint = Constraint::Parallel(Parallel {
11606 lines: vec![line1_id, line2_id],
11607 });
11608 let (src_delta, scene_delta) = frontend
11609 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11610 .await
11611 .unwrap();
11612 assert_eq!(
11613 src_delta.text.as_str(),
11614 "\
11615sketch(on = XY) {
11616 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11617 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11618 parallel([line1, line2])
11619}
11620"
11621 );
11622 assert_eq!(
11623 scene_delta.new_graph.objects.len(),
11624 9,
11625 "{:#?}",
11626 scene_delta.new_graph.objects
11627 );
11628
11629 ctx.close().await;
11630 mock_ctx.close().await;
11631 }
11632
11633 #[tokio::test(flavor = "multi_thread")]
11634 async fn test_lines_parallel_multiline() {
11635 let initial_source = "\
11636sketch(on = XY) {
11637 line(start = [var 1, var 2], end = [var 3, var 4])
11638 line(start = [var 5, var 6], end = [var 7, var 8])
11639 line(start = [var 9, var 10], end = [var 11, var 12])
11640}
11641";
11642
11643 let program = Program::parse(initial_source).unwrap().0.unwrap();
11644
11645 let mut frontend = FrontendState::new();
11646
11647 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11648 let mock_ctx = ExecutorContext::new_mock(None).await;
11649 let version = Version(0);
11650
11651 frontend.hack_set_program(&ctx, program).await.unwrap();
11652 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11653 let sketch_id = sketch_object.id;
11654 let sketch = expect_sketch(sketch_object);
11655 let line1_id = *sketch.segments.get(2).unwrap();
11656 let line2_id = *sketch.segments.get(5).unwrap();
11657 let line3_id = *sketch.segments.get(8).unwrap();
11658
11659 let constraint = Constraint::Parallel(Parallel {
11660 lines: vec![line1_id, line2_id, line3_id],
11661 });
11662 let (src_delta, scene_delta) = frontend
11663 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11664 .await
11665 .unwrap();
11666 assert_eq!(
11667 src_delta.text.as_str(),
11668 "\
11669sketch(on = XY) {
11670 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11671 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11672 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
11673 parallel([line1, line2, line3])
11674}
11675"
11676 );
11677
11678 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11679 let sketch = expect_sketch(sketch_object);
11680 assert_eq!(sketch.constraints.len(), 1);
11681
11682 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11683 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11684 panic!("Expected constraint object");
11685 };
11686 let Constraint::Parallel(parallel) = constraint else {
11687 panic!("Expected parallel constraint");
11688 };
11689 assert_eq!(parallel.lines.len(), 3);
11690
11691 ctx.close().await;
11692 mock_ctx.close().await;
11693 }
11694
11695 #[tokio::test(flavor = "multi_thread")]
11696 async fn test_lines_perpendicular() {
11697 let initial_source = "\
11698sketch(on = XY) {
11699 line(start = [var 1, var 2], end = [var 3, var 4])
11700 line(start = [var 5, var 6], end = [var 7, var 8])
11701}
11702";
11703
11704 let program = Program::parse(initial_source).unwrap().0.unwrap();
11705
11706 let mut frontend = FrontendState::new();
11707
11708 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11709 let mock_ctx = ExecutorContext::new_mock(None).await;
11710 let version = Version(0);
11711
11712 frontend.hack_set_program(&ctx, program).await.unwrap();
11713 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11714 let sketch_id = sketch_object.id;
11715 let sketch = expect_sketch(sketch_object);
11716 let line1_id = *sketch.segments.get(2).unwrap();
11717 let line2_id = *sketch.segments.get(5).unwrap();
11718
11719 let constraint = Constraint::Perpendicular(Perpendicular {
11720 lines: vec![line1_id, line2_id],
11721 });
11722 let (src_delta, scene_delta) = frontend
11723 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11724 .await
11725 .unwrap();
11726 assert_eq!(
11727 src_delta.text.as_str(),
11728 "\
11729sketch(on = XY) {
11730 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11731 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11732 perpendicular([line1, line2])
11733}
11734"
11735 );
11736 assert_eq!(
11737 scene_delta.new_graph.objects.len(),
11738 9,
11739 "{:#?}",
11740 scene_delta.new_graph.objects
11741 );
11742
11743 ctx.close().await;
11744 mock_ctx.close().await;
11745 }
11746
11747 #[tokio::test(flavor = "multi_thread")]
11748 async fn test_lines_angle() {
11749 let initial_source = "\
11750sketch(on = XY) {
11751 line(start = [var 1, var 2], end = [var 3, var 4])
11752 line(start = [var 5, var 6], end = [var 7, var 8])
11753}
11754";
11755
11756 let program = Program::parse(initial_source).unwrap().0.unwrap();
11757
11758 let mut frontend = FrontendState::new();
11759
11760 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11761 let mock_ctx = ExecutorContext::new_mock(None).await;
11762 let version = Version(0);
11763
11764 frontend.hack_set_program(&ctx, program).await.unwrap();
11765 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11766 let sketch_id = sketch_object.id;
11767 let sketch = expect_sketch(sketch_object);
11768 let line1_id = *sketch.segments.get(2).unwrap();
11769 let line2_id = *sketch.segments.get(5).unwrap();
11770
11771 let constraint = Constraint::Angle(Angle {
11772 lines: vec![line1_id, line2_id],
11773 angle: Number {
11774 value: 30.0,
11775 units: NumericSuffix::Deg,
11776 },
11777 source: Default::default(),
11778 });
11779 let (src_delta, scene_delta) = frontend
11780 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11781 .await
11782 .unwrap();
11783 assert_eq!(
11784 src_delta.text.as_str(),
11785 "\
11787sketch(on = XY) {
11788 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11789 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
11790 angle([line1, line2]) == 30deg
11791}
11792"
11793 );
11794 assert_eq!(
11795 scene_delta.new_graph.objects.len(),
11796 9,
11797 "{:#?}",
11798 scene_delta.new_graph.objects
11799 );
11800
11801 ctx.close().await;
11802 mock_ctx.close().await;
11803 }
11804
11805 #[tokio::test(flavor = "multi_thread")]
11806 async fn test_segments_tangent() {
11807 let initial_source = "\
11808sketch(on = XY) {
11809 line(start = [var 1, var 2], end = [var 3, var 4])
11810 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11811}
11812";
11813
11814 let program = Program::parse(initial_source).unwrap().0.unwrap();
11815
11816 let mut frontend = FrontendState::new();
11817
11818 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11819 let mock_ctx = ExecutorContext::new_mock(None).await;
11820 let version = Version(0);
11821
11822 frontend.hack_set_program(&ctx, program).await.unwrap();
11823 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11824 let sketch_id = sketch_object.id;
11825 let sketch = expect_sketch(sketch_object);
11826 let line1_id = *sketch.segments.get(2).unwrap();
11827 let arc1_id = *sketch.segments.get(6).unwrap();
11828
11829 let constraint = Constraint::Tangent(Tangent {
11830 input: vec![line1_id, arc1_id],
11831 });
11832 let (src_delta, scene_delta) = frontend
11833 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11834 .await
11835 .unwrap();
11836 assert_eq!(
11837 src_delta.text.as_str(),
11838 "\
11839sketch(on = XY) {
11840 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
11841 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11842 tangent([line1, arc1])
11843}
11844"
11845 );
11846 assert_eq!(
11847 scene_delta.new_graph.objects.len(),
11848 10,
11849 "{:#?}",
11850 scene_delta.new_graph.objects
11851 );
11852
11853 ctx.close().await;
11854 mock_ctx.close().await;
11855 }
11856
11857 #[tokio::test(flavor = "multi_thread")]
11858 async fn test_point_midpoint() {
11859 let initial_source = "\
11860sketch(on = XY) {
11861 point(at = [var 1, var 1])
11862 line(start = [var 0, var 0], end = [var 6, var 4])
11863}
11864";
11865
11866 let program = Program::parse(initial_source).unwrap().0.unwrap();
11867
11868 let mut frontend = FrontendState::new();
11869
11870 let ctx = ExecutorContext::new_mock(None).await;
11871 let version = Version(0);
11872
11873 frontend.program = program.clone();
11874 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11875 frontend.update_state_after_exec(outcome, true);
11876 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11877 let sketch_id = sketch_object.id;
11878 let sketch = expect_sketch(sketch_object);
11879 let point_id = *sketch.segments.first().unwrap();
11880 let line_id = *sketch.segments.get(3).unwrap();
11881
11882 let constraint = Constraint::Midpoint(Midpoint {
11883 point: point_id,
11884 segment: line_id,
11885 });
11886 let (src_delta, scene_delta) = frontend
11887 .add_constraint(&ctx, version, sketch_id, constraint)
11888 .await
11889 .unwrap();
11890 assert_eq!(
11891 src_delta.text.as_str(),
11892 "\
11893sketch(on = XY) {
11894 point1 = point(at = [var 1, var 1])
11895 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
11896 midpoint(line1, point = point1)
11897}
11898"
11899 );
11900 assert_eq!(
11901 scene_delta.new_graph.objects.len(),
11902 7,
11903 "{:#?}",
11904 scene_delta.new_graph.objects
11905 );
11906
11907 ctx.close().await;
11908 }
11909
11910 #[tokio::test(flavor = "multi_thread")]
11911 async fn test_segments_symmetric() {
11912 let initial_source = "\
11913sketch(on = XY) {
11914 line(start = [var 0, var 0], end = [var 0, var 4])
11915 line(start = [var 4, var 0], end = [var 4, var 4])
11916 line(start = [var 2, var -1], end = [var 2, var 5])
11917}
11918";
11919
11920 let program = Program::parse(initial_source).unwrap().0.unwrap();
11921
11922 let mut frontend = FrontendState::new();
11923
11924 let ctx = ExecutorContext::new_mock(None).await;
11925 let version = Version(0);
11926
11927 frontend.program = program.clone();
11928 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11929 frontend.update_state_after_exec(outcome, true);
11930 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11931 let sketch_id = sketch_object.id;
11932 let sketch = expect_sketch(sketch_object);
11933 let line1_id = *sketch.segments.get(2).unwrap();
11934 let line2_id = *sketch.segments.get(5).unwrap();
11935 let axis_id = *sketch.segments.get(8).unwrap();
11936
11937 let constraint = Constraint::Symmetric(Symmetric {
11938 input: vec![line1_id, line2_id],
11939 axis: axis_id,
11940 });
11941 let (src_delta, scene_delta) = frontend
11942 .add_constraint(&ctx, version, sketch_id, constraint)
11943 .await
11944 .unwrap();
11945 assert_eq!(
11946 src_delta.text.as_str(),
11947 "\
11948sketch(on = XY) {
11949 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
11950 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
11951 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
11952 symmetric([line1, line2], axis = line3)
11953}
11954"
11955 );
11956 assert_eq!(
11957 scene_delta.new_graph.objects.len(),
11958 12,
11959 "{:#?}",
11960 scene_delta.new_graph.objects
11961 );
11962
11963 ctx.close().await;
11964 }
11965
11966 #[tokio::test(flavor = "multi_thread")]
11967 async fn test_point_arc_midpoint() {
11968 let initial_source = "\
11969sketch(on = XY) {
11970 point(at = [var 6, var 3])
11971 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11972}
11973";
11974
11975 let program = Program::parse(initial_source).unwrap().0.unwrap();
11976
11977 let mut frontend = FrontendState::new();
11978
11979 let ctx = ExecutorContext::new_mock(None).await;
11980 let version = Version(0);
11981
11982 frontend.program = program.clone();
11983 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11984 frontend.update_state_after_exec(outcome, true);
11985 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11986 let sketch_id = sketch_object.id;
11987 let sketch = expect_sketch(sketch_object);
11988 let point_id = *sketch.segments.first().unwrap();
11989 let arc_id = *sketch.segments.get(4).unwrap();
11990
11991 let constraint = Constraint::Midpoint(Midpoint {
11992 point: point_id,
11993 segment: arc_id,
11994 });
11995 let (src_delta, scene_delta) = frontend
11996 .add_constraint(&ctx, version, sketch_id, constraint)
11997 .await
11998 .unwrap();
11999 assert_eq!(
12000 src_delta.text.as_str(),
12001 "\
12002sketch(on = XY) {
12003 point1 = point(at = [var 6, var 3])
12004 arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
12005 midpoint(arc1, point = point1)
12006}
12007"
12008 );
12009 assert_eq!(
12010 scene_delta.new_graph.objects.len(),
12011 8,
12012 "{:#?}",
12013 scene_delta.new_graph.objects
12014 );
12015
12016 ctx.close().await;
12017 }
12018
12019 #[tokio::test(flavor = "multi_thread")]
12020 async fn test_segments_symmetric_arcs() {
12021 let initial_source = "\
12022sketch(on = XY) {
12023 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
12024 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
12025 line(start = [var 0, var -10], end = [var 0, var 10])
12026}
12027";
12028
12029 let program = Program::parse(initial_source).unwrap().0.unwrap();
12030
12031 let mut frontend = FrontendState::new();
12032
12033 let ctx = ExecutorContext::new_mock(None).await;
12034 let version = Version(0);
12035
12036 frontend.program = program.clone();
12037 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12038 frontend.update_state_after_exec(outcome, true);
12039 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12040 let sketch_id = sketch_object.id;
12041 let sketch = expect_sketch(sketch_object);
12042 let arc1_id = *sketch.segments.get(3).unwrap();
12043 let arc2_id = *sketch.segments.get(7).unwrap();
12044 let axis_id = *sketch.segments.get(10).unwrap();
12045
12046 let constraint = Constraint::Symmetric(Symmetric {
12047 input: vec![arc1_id, arc2_id],
12048 axis: axis_id,
12049 });
12050 let (src_delta, scene_delta) = frontend
12051 .add_constraint(&ctx, version, sketch_id, constraint)
12052 .await
12053 .unwrap();
12054 assert_eq!(
12055 src_delta.text.as_str(),
12056 "\
12057sketch(on = XY) {
12058 arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
12059 arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
12060 line1 = line(start = [var 0, var -10], end = [var 0, var 10])
12061 symmetric([arc1, arc2], axis = line1)
12062}
12063"
12064 );
12065 assert_eq!(
12066 scene_delta.new_graph.objects.len(),
12067 14,
12068 "{:#?}",
12069 scene_delta.new_graph.objects
12070 );
12071
12072 ctx.close().await;
12073 }
12074
12075 #[tokio::test(flavor = "multi_thread")]
12076 async fn test_sketch_on_face_simple() {
12077 let initial_source = "\
12078len = 2mm
12079cube = startSketchOn(XY)
12080 |> startProfile(at = [0, 0])
12081 |> line(end = [len, 0], tag = $side)
12082 |> line(end = [0, len])
12083 |> line(end = [-len, 0])
12084 |> line(end = [0, -len])
12085 |> close()
12086 |> extrude(length = len)
12087
12088face = faceOf(cube, face = side)
12089";
12090
12091 let program = Program::parse(initial_source).unwrap().0.unwrap();
12092
12093 let mut frontend = FrontendState::new();
12094
12095 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12096 let mock_ctx = ExecutorContext::new_mock(None).await;
12097 let version = Version(0);
12098
12099 frontend.hack_set_program(&ctx, program).await.unwrap();
12100 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
12101 let face_id = face_object.id;
12102
12103 let sketch_args = SketchCtor {
12104 on: Plane::Object(face_id),
12105 };
12106 let (_src_delta, scene_delta, sketch_id) = frontend
12107 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12108 .await
12109 .unwrap();
12110 assert_eq!(sketch_id, ObjectId(2));
12111 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
12112 let sketch_object = &scene_delta.new_graph.objects[2];
12113 assert_eq!(sketch_object.id, ObjectId(2));
12114 assert_eq!(
12115 sketch_object.kind,
12116 ObjectKind::Sketch(Sketch {
12117 args: SketchCtor {
12118 on: Plane::Object(face_id),
12119 },
12120 plane: face_id,
12121 segments: vec![],
12122 constraints: vec![],
12123 })
12124 );
12125 assert_eq!(scene_delta.new_graph.objects.len(), 8);
12126
12127 ctx.close().await;
12128 mock_ctx.close().await;
12129 }
12130
12131 #[tokio::test(flavor = "multi_thread")]
12132 async fn test_sketch_on_wall_artifact_from_region_extrude() {
12133 let initial_source = "\
12134s = sketch(on = YZ) {
12135 line1 = line(start = [0, 0], end = [0, 1])
12136 line2 = line(start = [0, 1], end = [1, 1])
12137 line3 = line(start = [1, 1], end = [0, 0])
12138}
12139region001 = region(point = [0.1, 0.1], sketch = s)
12140extrude001 = extrude(region001, length = 5)
12141";
12142
12143 let program = Program::parse(initial_source).unwrap().0.unwrap();
12144
12145 let mut frontend = FrontendState::new();
12146 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12147 let version = Version(0);
12148
12149 frontend.hack_set_program(&ctx, program).await.unwrap();
12150 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
12151
12152 let sketch_args = SketchCtor {
12153 on: Plane::Object(wall_object_id),
12154 };
12155 let (src_delta, _scene_delta, _sketch_id) = frontend
12156 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12157 .await
12158 .unwrap();
12159 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
12160
12161 ctx.close().await;
12162 }
12163
12164 #[tokio::test(flavor = "multi_thread")]
12165 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
12166 let initial_source = "\
12167sketch001 = sketch(on = YZ) {
12168 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
12169 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
12170 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
12171 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
12172 coincident([line1.end, line2.start])
12173 coincident([line2.end, line3.start])
12174 coincident([line3.end, line4.start])
12175 coincident([line4.end, line1.start])
12176 parallel([line2, line4])
12177 parallel([line3, line1])
12178 perpendicular([line1, line2])
12179 horizontal(line3)
12180 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
12181}
12182region001 = region(point = [3.1, 3.74], sketch = sketch001)
12183extrude001 = extrude(region001, length = 5)
12184";
12185
12186 let program = Program::parse(initial_source).unwrap().0.unwrap();
12187
12188 let mut frontend = FrontendState::new();
12189 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12190 let version = Version(0);
12191
12192 frontend.hack_set_program(&ctx, program).await.unwrap();
12193 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
12194
12195 let sketch_args = SketchCtor {
12196 on: Plane::Object(wall_object_id),
12197 };
12198 let (src_delta, _scene_delta, _sketch_id) = frontend
12199 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12200 .await
12201 .unwrap();
12202 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
12203
12204 ctx.close().await;
12205 }
12206
12207 #[tokio::test(flavor = "multi_thread")]
12208 async fn test_sketch_on_plane_incremental() {
12209 let initial_source = "\
12210len = 2mm
12211cube = startSketchOn(XY)
12212 |> startProfile(at = [0, 0])
12213 |> line(end = [len, 0], tag = $side)
12214 |> line(end = [0, len])
12215 |> line(end = [-len, 0])
12216 |> line(end = [0, -len])
12217 |> close()
12218 |> extrude(length = len)
12219
12220plane = planeOf(cube, face = side)
12221";
12222
12223 let program = Program::parse(initial_source).unwrap().0.unwrap();
12224
12225 let mut frontend = FrontendState::new();
12226
12227 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12228 let mock_ctx = ExecutorContext::new_mock(None).await;
12229 let version = Version(0);
12230
12231 frontend.hack_set_program(&ctx, program).await.unwrap();
12232 let plane_object = frontend
12234 .scene_graph
12235 .objects
12236 .iter()
12237 .rev()
12238 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
12239 .unwrap();
12240 let plane_id = plane_object.id;
12241
12242 let sketch_args = SketchCtor {
12243 on: Plane::Object(plane_id),
12244 };
12245 let (src_delta, scene_delta, sketch_id) = frontend
12246 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12247 .await
12248 .unwrap();
12249 assert_eq!(
12250 src_delta.text.as_str(),
12251 "\
12252len = 2mm
12253cube = startSketchOn(XY)
12254 |> startProfile(at = [0, 0])
12255 |> line(end = [len, 0], tag = $side)
12256 |> line(end = [0, len])
12257 |> line(end = [-len, 0])
12258 |> line(end = [0, -len])
12259 |> close()
12260 |> extrude(length = len)
12261
12262plane = planeOf(cube, face = side)
12263sketch001 = sketch(on = plane) {
12264}
12265"
12266 );
12267 assert_eq!(sketch_id, ObjectId(2));
12268 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
12269 let sketch_object = &scene_delta.new_graph.objects[2];
12270 assert_eq!(sketch_object.id, ObjectId(2));
12271 assert_eq!(
12272 sketch_object.kind,
12273 ObjectKind::Sketch(Sketch {
12274 args: SketchCtor {
12275 on: Plane::Object(plane_id),
12276 },
12277 plane: plane_id,
12278 segments: vec![],
12279 constraints: vec![],
12280 })
12281 );
12282 assert_eq!(scene_delta.new_graph.objects.len(), 9);
12283
12284 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
12285 assert_eq!(plane_object.id, plane_id);
12286 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
12287
12288 ctx.close().await;
12289 mock_ctx.close().await;
12290 }
12291
12292 #[tokio::test(flavor = "multi_thread")]
12293 async fn test_new_sketch_uses_unique_variable_name() {
12294 let initial_source = "\
12295sketch1 = sketch(on = XY) {
12296}
12297";
12298
12299 let program = Program::parse(initial_source).unwrap().0.unwrap();
12300
12301 let mut frontend = FrontendState::new();
12302 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12303 let version = Version(0);
12304
12305 frontend.hack_set_program(&ctx, program).await.unwrap();
12306
12307 let sketch_args = SketchCtor {
12308 on: Plane::Default(PlaneName::Yz),
12309 };
12310 let (src_delta, _, _) = frontend
12311 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12312 .await
12313 .unwrap();
12314
12315 assert_eq!(
12316 src_delta.text.as_str(),
12317 "\
12318sketch1 = sketch(on = XY) {
12319}
12320sketch001 = sketch(on = YZ) {
12321}
12322"
12323 );
12324
12325 ctx.close().await;
12326 }
12327
12328 #[tokio::test(flavor = "multi_thread")]
12329 async fn test_new_sketch_twice_using_same_plane() {
12330 let initial_source = "\
12331sketch1 = sketch(on = XY) {
12332}
12333";
12334
12335 let program = Program::parse(initial_source).unwrap().0.unwrap();
12336
12337 let mut frontend = FrontendState::new();
12338 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12339 let version = Version(0);
12340
12341 frontend.hack_set_program(&ctx, program).await.unwrap();
12342
12343 let sketch_args = SketchCtor {
12344 on: Plane::Default(PlaneName::Xy),
12345 };
12346 let (src_delta, _, _) = frontend
12347 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
12348 .await
12349 .unwrap();
12350
12351 assert_eq!(
12352 src_delta.text.as_str(),
12353 "\
12354sketch1 = sketch(on = XY) {
12355}
12356sketch001 = sketch(on = XY) {
12357}
12358"
12359 );
12360
12361 ctx.close().await;
12362 }
12363
12364 #[tokio::test(flavor = "multi_thread")]
12365 async fn test_sketch_mode_reuses_cached_on_expression() {
12366 let initial_source = "\
12367width = 2mm
12368sketch(on = offsetPlane(XY, offset = width)) {
12369 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
12370 distance([line1.start, line1.end]) == width
12371}
12372";
12373 let program = Program::parse(initial_source).unwrap().0.unwrap();
12374
12375 let mut frontend = FrontendState::new();
12376 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12377 let mock_ctx = ExecutorContext::new_mock(None).await;
12378 let version = Version(0);
12379 let project_id = ProjectId(0);
12380 let file_id = FileId(0);
12381
12382 frontend.hack_set_program(&ctx, program).await.unwrap();
12383 let initial_object_count = frontend.scene_graph.objects.len();
12384 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
12385 .expect("Expected sketch object to exist")
12386 .id;
12387
12388 let scene_delta = frontend
12391 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12392 .await
12393 .unwrap();
12394 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
12395
12396 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
12399 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
12400
12401 ctx.close().await;
12402 mock_ctx.close().await;
12403 }
12404
12405 #[tokio::test(flavor = "multi_thread")]
12406 async fn test_execute_mock_from_preview_consumes_sketch_var_warm_starts() {
12407 let initial_source = "\
12408sketch(on = XY) {
12409 point(at = [var 1mm, var 2mm])
12410}
12411";
12412
12413 let program = Program::parse(initial_source).unwrap().0.unwrap();
12414
12415 let mut frontend = FrontendState::new();
12416 let mock_ctx = ExecutorContext::new_mock(None).await;
12417 let version = Version(0);
12418
12419 frontend.program = program.clone();
12420 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12421 frontend.update_state_after_exec(outcome, true);
12422 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12423 let sketch_id = sketch_object.id;
12424 frontend
12425 .sketch_var_warm_start_overrides
12426 .insert(sketch_id, vec![5.0, 6.0]);
12427
12428 let mut cold_frontend = frontend.clone();
12429 let (warm_src_delta, _) = frontend
12430 .execute_mock_from_preview(&mock_ctx, version, sketch_id)
12431 .await
12432 .unwrap();
12433 let (cold_src_delta, _) = cold_frontend
12434 .execute_mock_with_warm_starts(&mock_ctx, sketch_id, false)
12435 .await
12436 .unwrap();
12437
12438 assert_eq!(
12439 warm_src_delta.text.as_str(),
12440 "\
12441sketch(on = XY) {
12442 point(at = [var 5mm, var 6mm])
12443}
12444"
12445 );
12446 assert_eq!(
12447 cold_src_delta.text.as_str(),
12448 "\
12449sketch(on = XY) {
12450 point(at = [var 1mm, var 2mm])
12451}
12452"
12453 );
12454
12455 mock_ctx.close().await;
12456 }
12457
12458 #[tokio::test(flavor = "multi_thread")]
12459 async fn test_committed_edit_merges_edited_vars_into_existing_warm_starts() {
12460 let initial_source = "\
12461sketch(on = XY) {
12462 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12463 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12464 coincident([line1.end, line2.start])
12465 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12466 coincident([line2.end, line3.start])
12467 coincident([line3.end, line1.start])
12468 equalLength([line3, line1])
12469 equalLength([line1, line2])
12470 distance([line1.start, line1.end]) == 4mm
12471}
12472";
12473
12474 let program = Program::parse(initial_source).unwrap().0.unwrap();
12475
12476 let mut frontend = FrontendState::new();
12477 let mock_ctx = ExecutorContext::new_mock(None).await;
12478 let version = Version(0);
12479
12480 frontend.program = program.clone();
12481 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12482 let outcome = frontend.update_state_after_exec(outcome, true);
12483 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12484 let sketch_id = sketch_object.id;
12485 let sketch = expect_sketch(sketch_object);
12486 let point_id = *sketch.segments.first().unwrap();
12487 frontend.replace_sketch_var_warm_starts(sketch_id, &outcome);
12488 let previous_warm_starts = frontend
12489 .sketch_var_warm_start_overrides
12490 .get(&sketch_id)
12491 .expect("Expected initial warm starts")
12492 .clone();
12493
12494 let segments = vec![ExistingSegmentCtor {
12495 id: point_id,
12496 ctor: SegmentCtor::Point(PointCtor {
12497 position: Point2d {
12498 x: Expr::Var(Number {
12499 value: 1.0,
12500 units: NumericSuffix::Mm,
12501 }),
12502 y: Expr::Var(Number {
12503 value: 2.0,
12504 units: NumericSuffix::Mm,
12505 }),
12506 },
12507 }),
12508 }];
12509 frontend
12510 .edit_segments(&mock_ctx, version, sketch_id, segments)
12511 .await
12512 .unwrap();
12513 let warm_starts = frontend
12514 .sketch_var_warm_start_overrides
12515 .get(&sketch_id)
12516 .expect("Expected warm starts for edited sketch");
12517
12518 assert_eq!(&warm_starts[0..4], &[1.0, 2.0, 1.28, -0.78]);
12519 assert_eq!(&warm_starts[4..], &previous_warm_starts[4..]);
12520
12521 mock_ctx.close().await;
12522 }
12523
12524 #[tokio::test(flavor = "multi_thread")]
12525 async fn test_multiple_sketch_blocks() {
12526 let initial_source = "\
12527// Cube that requires the engine.
12528width = 2
12529sketch001 = startSketchOn(XY)
12530profile001 = startProfile(sketch001, at = [0, 0])
12531 |> yLine(length = width, tag = $seg1)
12532 |> xLine(length = width)
12533 |> yLine(length = -width)
12534 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12535 |> close()
12536extrude001 = extrude(profile001, length = width)
12537
12538// Get a value that requires the engine.
12539x = segLen(seg1)
12540
12541// Triangle with side length 2*x.
12542sketch(on = XY) {
12543 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12544 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
12545 coincident([line1.end, line2.start])
12546 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
12547 coincident([line2.end, line3.start])
12548 coincident([line3.end, line1.start])
12549 equalLength([line3, line1])
12550 equalLength([line1, line2])
12551 distance([line1.start, line1.end]) == 2*x
12552}
12553
12554// Line segment with length x.
12555sketch2 = sketch(on = XY) {
12556 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12557 distance([line1.start, line1.end]) == x
12558}
12559";
12560
12561 let program = Program::parse(initial_source).unwrap().0.unwrap();
12562
12563 let mut frontend = FrontendState::new();
12564
12565 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12566 let mock_ctx = ExecutorContext::new_mock(None).await;
12567 let version = Version(0);
12568 let project_id = ProjectId(0);
12569 let file_id = FileId(0);
12570
12571 frontend.hack_set_program(&ctx, program).await.unwrap();
12572 let sketch_objects = frontend
12573 .scene_graph
12574 .objects
12575 .iter()
12576 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
12577 .collect::<Vec<_>>();
12578 let sketch1_id = sketch_objects.first().unwrap().id;
12579 let sketch2_id = sketch_objects.get(1).unwrap().id;
12580 let point1_id = ObjectId(sketch1_id.0 + 1);
12582 let point2_id = ObjectId(sketch2_id.0 + 1);
12584
12585 let scene_delta = frontend
12594 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
12595 .await
12596 .unwrap();
12597 assert_eq!(
12598 scene_delta.new_graph.objects.len(),
12599 18,
12600 "{:#?}",
12601 scene_delta.new_graph.objects
12602 );
12603
12604 let point_ctor = PointCtor {
12606 position: Point2d {
12607 x: Expr::Var(Number {
12608 value: 1.0,
12609 units: NumericSuffix::Mm,
12610 }),
12611 y: Expr::Var(Number {
12612 value: 2.0,
12613 units: NumericSuffix::Mm,
12614 }),
12615 },
12616 };
12617 let segments = vec![ExistingSegmentCtor {
12618 id: point1_id,
12619 ctor: SegmentCtor::Point(point_ctor),
12620 }];
12621 let (src_delta, _) = frontend
12622 .edit_segments(&mock_ctx, version, sketch1_id, segments)
12623 .await
12624 .unwrap();
12625 pretty_assertions::assert_eq!(
12627 "// Cube that requires the engine.\nwidth = 2\nsketch001 = startSketchOn(XY)\nprofile001 = startProfile(sketch001, at = [0, 0])\n |> yLine(length = width, tag = $seg1)\n |> xLine(length = width)\n |> yLine(length = -width)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude001 = extrude(profile001, length = width)\n\n// Get a value that requires the engine.\nx = segLen(seg1)\n\n// Triangle with side length 2*x.\nsketch(on = XY) {\n line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])\n line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])\n coincident([line1.end, line2.start])\n line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])\n coincident([line2.end, line3.start])\n coincident([line3.end, line1.start])\n equalLength([line3, line1])\n equalLength([line1, line2])\n distance([line1.start, line1.end]) == 2 * x\n}\n\n// Line segment with length x.\nsketch2 = sketch(on = XY) {\n line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])\n distance([line1.start, line1.end]) == x\n}\n",
12628 src_delta.text.as_str(),
12629 );
12630
12631 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
12633 pretty_assertions::assert_eq!(
12635 src_delta.text.as_str(),
12636 "\
12637// Cube that requires the engine.
12638width = 2
12639sketch001 = startSketchOn(XY)
12640profile001 = startProfile(sketch001, at = [0, 0])
12641 |> yLine(length = width, tag = $seg1)
12642 |> xLine(length = width)
12643 |> yLine(length = -width)
12644 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12645 |> close()
12646extrude001 = extrude(profile001, length = width)
12647
12648// Get a value that requires the engine.
12649x = segLen(seg1)
12650
12651// Triangle with side length 2*x.
12652sketch(on = XY) {
12653 line1 = line(start = [var 2mm, var 2mm], end = [var 2.32mm, var -1.78mm])
12654 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
12655 coincident([line1.end, line2.start])
12656 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
12657 coincident([line2.end, line3.start])
12658 coincident([line3.end, line1.start])
12659 equalLength([line3, line1])
12660 equalLength([line1, line2])
12661 distance([line1.start, line1.end]) == 2 * x
12662}
12663
12664// Line segment with length x.
12665sketch2 = sketch(on = XY) {
12666 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
12667 distance([line1.start, line1.end]) == x
12668}
12669"
12670 );
12671 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
12679 pretty_assertions::assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
12680
12681 let scene_delta = frontend
12689 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
12690 .await
12691 .unwrap();
12692 pretty_assertions::assert_eq!(
12693 scene_delta.new_graph.objects.len(),
12694 24,
12695 "{:#?}",
12696 scene_delta.new_graph.objects
12697 );
12698
12699 let point_ctor = PointCtor {
12701 position: Point2d {
12702 x: Expr::Var(Number {
12703 value: 3.0,
12704 units: NumericSuffix::Mm,
12705 }),
12706 y: Expr::Var(Number {
12707 value: 4.0,
12708 units: NumericSuffix::Mm,
12709 }),
12710 },
12711 };
12712 let segments = vec![ExistingSegmentCtor {
12713 id: point2_id,
12714 ctor: SegmentCtor::Point(point_ctor),
12715 }];
12716 let (src_delta, _) = frontend
12717 .edit_segments(&mock_ctx, version, sketch2_id, segments)
12718 .await
12719 .unwrap();
12720 pretty_assertions::assert_eq!(
12722 "\
12723// Cube that requires the engine.
12724width = 2
12725sketch001 = startSketchOn(XY)
12726profile001 = startProfile(sketch001, at = [0, 0])
12727 |> yLine(length = width, tag = $seg1)
12728 |> xLine(length = width)
12729 |> yLine(length = -width)
12730 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12731 |> close()
12732extrude001 = extrude(profile001, length = width)
12733
12734// Get a value that requires the engine.
12735x = segLen(seg1)
12736
12737// Triangle with side length 2*x.
12738sketch(on = XY) {
12739 line1 = line(start = [var 2mm, var 2mm], end = [var 2.32mm, var -1.78mm])
12740 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
12741 coincident([line1.end, line2.start])
12742 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
12743 coincident([line2.end, line3.start])
12744 coincident([line3.end, line1.start])
12745 equalLength([line3, line1])
12746 equalLength([line1, line2])
12747 distance([line1.start, line1.end]) == 2 * x
12748}
12749
12750// Line segment with length x.
12751sketch2 = sketch(on = XY) {
12752 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
12753 distance([line1.start, line1.end]) == x
12754}
12755",
12756 src_delta.text.as_str(),
12757 );
12758
12759 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
12761 pretty_assertions::assert_eq!(
12763 "\
12764// Cube that requires the engine.
12765width = 2
12766sketch001 = startSketchOn(XY)
12767profile001 = startProfile(sketch001, at = [0, 0])
12768 |> yLine(length = width, tag = $seg1)
12769 |> xLine(length = width)
12770 |> yLine(length = -width)
12771 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
12772 |> close()
12773extrude001 = extrude(profile001, length = width)
12774
12775// Get a value that requires the engine.
12776x = segLen(seg1)
12777
12778// Triangle with side length 2*x.
12779sketch(on = XY) {
12780 line1 = line(start = [var 2mm, var 2mm], end = [var 2.32mm, var -1.78mm])
12781 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
12782 coincident([line1.end, line2.start])
12783 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
12784 coincident([line2.end, line3.start])
12785 coincident([line3.end, line1.start])
12786 equalLength([line3, line1])
12787 equalLength([line1, line2])
12788 distance([line1.start, line1.end]) == 2 * x
12789}
12790
12791// Line segment with length x.
12792sketch2 = sketch(on = XY) {
12793 line1 = line(start = [var 2.12mm, var 4mm], end = [var 2.32mm, var 2.12mm])
12794 distance([line1.start, line1.end]) == x
12795}
12796",
12797 src_delta.text.as_str(),
12798 );
12799
12800 ctx.close().await;
12801 mock_ctx.close().await;
12802 }
12803
12804 #[tokio::test(flavor = "multi_thread")]
12805 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
12806 clear_mem_cache().await;
12807
12808 let source = r#"sketch001 = sketch(on = XZ) {
12809 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
12810}
12811sketch002 = sketch(on = XY) {
12812 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
12813 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
12814 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
12815 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
12816 coincident([line1.end, line2.start])
12817 coincident([line2.end, line3.start])
12818 coincident([line3.end, line4.start])
12819 coincident([line4.end, line1.start])
12820 parallel([line2, line4])
12821 parallel([line3, line1])
12822 perpendicular([line1, line2])
12823 horizontal(line3)
12824 coincident([line1.start, ORIGIN])
12825}
12826"#;
12827
12828 let program = Program::parse(source).unwrap().0.unwrap();
12829 let mut frontend = FrontendState::new();
12830 let ctx = ExecutorContext::new_with_engine(
12831 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
12832 Default::default(),
12833 );
12834 let mock_ctx = ExecutorContext::new_mock(None).await;
12835 let version = Version(0);
12836 let project_id = ProjectId(0);
12837 let file_id = FileId(0);
12838
12839 frontend.hack_set_program(&ctx, program).await.unwrap();
12840 let sketch_objects = frontend
12841 .scene_graph
12842 .objects
12843 .iter()
12844 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
12845 .collect::<Vec<_>>();
12846 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
12847
12848 let sketch1_id = sketch_objects[0].id;
12849 let sketch2_id = sketch_objects[1].id;
12850
12851 frontend
12852 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
12853 .await
12854 .unwrap();
12855 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
12856
12857 let scene_delta = frontend
12858 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
12859 .await
12860 .unwrap();
12861 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
12862
12863 clear_mem_cache().await;
12864 ctx.close().await;
12865 mock_ctx.close().await;
12866 }
12867
12868 #[tokio::test(flavor = "multi_thread")]
12873 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
12874 let initial_source = "@settings(defaultLengthUnit = mm)
12876
12877
12878
12879sketch001 = sketch(on = XY) {
12880 point(at = [1in, 2in])
12881}
12882";
12883
12884 let program = Program::parse(initial_source).unwrap().0.unwrap();
12885 let mut frontend = FrontendState::new();
12886
12887 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12888 let mock_ctx = ExecutorContext::new_mock(None).await;
12889 let version = Version(0);
12890 let project_id = ProjectId(0);
12891 let file_id = FileId(0);
12892
12893 frontend.hack_set_program(&ctx, program).await.unwrap();
12894 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12895 let sketch_id = sketch_object.id;
12896
12897 frontend
12899 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12900 .await
12901 .unwrap();
12902
12903 let point_ctor = PointCtor {
12905 position: Point2d {
12906 x: Expr::Number(Number {
12907 value: 5.0,
12908 units: NumericSuffix::Mm,
12909 }),
12910 y: Expr::Number(Number {
12911 value: 6.0,
12912 units: NumericSuffix::Mm,
12913 }),
12914 },
12915 };
12916 let segment = SegmentCtor::Point(point_ctor);
12917 let (src_delta, scene_delta) = frontend
12918 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12919 .await
12920 .unwrap();
12921 assert!(
12923 src_delta.text.contains("point(at = [5mm, 6mm])"),
12924 "Expected new point in source, got: {}",
12925 src_delta.text
12926 );
12927 assert!(!scene_delta.new_objects.is_empty());
12928
12929 ctx.close().await;
12930 mock_ctx.close().await;
12931 }
12932
12933 #[tokio::test(flavor = "multi_thread")]
12934 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
12935 let initial_source = "@settings(defaultLengthUnit = mm)
12937
12938
12939
12940s = sketch(on = XY) {}
12941";
12942
12943 let program = Program::parse(initial_source).unwrap().0.unwrap();
12944 let mut frontend = FrontendState::new();
12945
12946 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12947 let mock_ctx = ExecutorContext::new_mock(None).await;
12948 let version = Version(0);
12949
12950 frontend.hack_set_program(&ctx, program).await.unwrap();
12951 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12952 let sketch_id = sketch_object.id;
12953
12954 let line_ctor = LineCtor {
12955 start: Point2d {
12956 x: Expr::Number(Number {
12957 value: 0.0,
12958 units: NumericSuffix::Mm,
12959 }),
12960 y: Expr::Number(Number {
12961 value: 0.0,
12962 units: NumericSuffix::Mm,
12963 }),
12964 },
12965 end: Point2d {
12966 x: Expr::Number(Number {
12967 value: 10.0,
12968 units: NumericSuffix::Mm,
12969 }),
12970 y: Expr::Number(Number {
12971 value: 10.0,
12972 units: NumericSuffix::Mm,
12973 }),
12974 },
12975 construction: None,
12976 };
12977 let segment = SegmentCtor::Line(line_ctor);
12978 let (src_delta, scene_delta) = frontend
12979 .add_segment(&mock_ctx, version, sketch_id, segment, None)
12980 .await
12981 .unwrap();
12982 assert!(
12983 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12984 "Expected line in source, got: {}",
12985 src_delta.text
12986 );
12987 assert_eq!(scene_delta.new_objects.len(), 3);
12989
12990 ctx.close().await;
12991 mock_ctx.close().await;
12992 }
12993
12994 #[tokio::test(flavor = "multi_thread")]
12995 async fn test_extra_newlines_between_operations_edit_line() {
12996 let initial_source = "@settings(defaultLengthUnit = mm)
12998
12999
13000sketch001 = sketch(on = XY) {
13001
13002 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
13003
13004}
13005";
13006
13007 let program = Program::parse(initial_source).unwrap().0.unwrap();
13008 let mut frontend = FrontendState::new();
13009
13010 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13011 let mock_ctx = ExecutorContext::new_mock(None).await;
13012 let version = Version(0);
13013 let project_id = ProjectId(0);
13014 let file_id = FileId(0);
13015
13016 frontend.hack_set_program(&ctx, program).await.unwrap();
13017 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13018 let sketch_id = sketch_object.id;
13019 let sketch = expect_sketch(sketch_object);
13020
13021 let line_id = sketch
13023 .segments
13024 .iter()
13025 .copied()
13026 .find(|seg_id| {
13027 matches!(
13028 &frontend.scene_graph.objects[seg_id.0].kind,
13029 ObjectKind::Segment {
13030 segment: Segment::Line(_)
13031 }
13032 )
13033 })
13034 .expect("Expected a line segment in sketch");
13035
13036 frontend
13038 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13039 .await
13040 .unwrap();
13041
13042 let line_ctor = LineCtor {
13044 start: Point2d {
13045 x: Expr::Var(Number {
13046 value: 1.0,
13047 units: NumericSuffix::Mm,
13048 }),
13049 y: Expr::Var(Number {
13050 value: 2.0,
13051 units: NumericSuffix::Mm,
13052 }),
13053 },
13054 end: Point2d {
13055 x: Expr::Var(Number {
13056 value: 13.0,
13057 units: NumericSuffix::Mm,
13058 }),
13059 y: Expr::Var(Number {
13060 value: 14.0,
13061 units: NumericSuffix::Mm,
13062 }),
13063 },
13064 construction: None,
13065 };
13066 let segments = vec![ExistingSegmentCtor {
13067 id: line_id,
13068 ctor: SegmentCtor::Line(line_ctor),
13069 }];
13070 let (src_delta, _scene_delta) = frontend
13071 .edit_segments(&mock_ctx, version, sketch_id, segments)
13072 .await
13073 .unwrap();
13074 assert!(
13075 src_delta
13076 .text
13077 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
13078 "Expected edited line in source, got: {}",
13079 src_delta.text
13080 );
13081
13082 ctx.close().await;
13083 mock_ctx.close().await;
13084 }
13085
13086 #[tokio::test(flavor = "multi_thread")]
13087 async fn test_extra_newlines_delete_segment() {
13088 let initial_source = "@settings(defaultLengthUnit = mm)
13090
13091
13092
13093sketch001 = sketch(on = XY) {
13094 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
13095}
13096";
13097
13098 let program = Program::parse(initial_source).unwrap().0.unwrap();
13099 let mut frontend = FrontendState::new();
13100
13101 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13102 let mock_ctx = ExecutorContext::new_mock(None).await;
13103 let version = Version(0);
13104
13105 frontend.hack_set_program(&ctx, program).await.unwrap();
13106 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13107 let sketch_id = sketch_object.id;
13108 let sketch = expect_sketch(sketch_object);
13109
13110 assert_eq!(sketch.segments.len(), 3);
13112 let circle_id = sketch.segments[2];
13113
13114 let (src_delta, scene_delta) = frontend
13116 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
13117 .await
13118 .unwrap();
13119 assert!(
13120 src_delta.text.contains("sketch(on = XY) {"),
13121 "Expected sketch block in source, got: {}",
13122 src_delta.text
13123 );
13124 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13125 let new_sketch = expect_sketch(new_sketch_object);
13126 assert_eq!(new_sketch.segments.len(), 0);
13127
13128 ctx.close().await;
13129 mock_ctx.close().await;
13130 }
13131
13132 #[tokio::test(flavor = "multi_thread")]
13133 async fn test_unformatted_source_add_arc() {
13134 let initial_source = "@settings(defaultLengthUnit = mm)
13136
13137
13138
13139
13140sketch001 = sketch(on = XY) {
13141}
13142";
13143
13144 let program = Program::parse(initial_source).unwrap().0.unwrap();
13145 let mut frontend = FrontendState::new();
13146
13147 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13148 let mock_ctx = ExecutorContext::new_mock(None).await;
13149 let version = Version(0);
13150
13151 frontend.hack_set_program(&ctx, program).await.unwrap();
13152 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13153 let sketch_id = sketch_object.id;
13154
13155 let arc_ctor = ArcCtor {
13156 start: Point2d {
13157 x: Expr::Var(Number {
13158 value: 5.0,
13159 units: NumericSuffix::Mm,
13160 }),
13161 y: Expr::Var(Number {
13162 value: 0.0,
13163 units: NumericSuffix::Mm,
13164 }),
13165 },
13166 end: Point2d {
13167 x: Expr::Var(Number {
13168 value: 0.0,
13169 units: NumericSuffix::Mm,
13170 }),
13171 y: Expr::Var(Number {
13172 value: 5.0,
13173 units: NumericSuffix::Mm,
13174 }),
13175 },
13176 center: Point2d {
13177 x: Expr::Var(Number {
13178 value: 0.0,
13179 units: NumericSuffix::Mm,
13180 }),
13181 y: Expr::Var(Number {
13182 value: 0.0,
13183 units: NumericSuffix::Mm,
13184 }),
13185 },
13186 construction: None,
13187 };
13188 let segment = SegmentCtor::Arc(arc_ctor);
13189 let (src_delta, scene_delta) = frontend
13190 .add_segment(&mock_ctx, version, sketch_id, segment, None)
13191 .await
13192 .unwrap();
13193 assert!(
13194 src_delta
13195 .text
13196 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
13197 "Expected arc in source, got: {}",
13198 src_delta.text
13199 );
13200 assert!(!scene_delta.new_objects.is_empty());
13201
13202 ctx.close().await;
13203 mock_ctx.close().await;
13204 }
13205
13206 #[tokio::test(flavor = "multi_thread")]
13207 async fn test_extra_newlines_add_circle() {
13208 let initial_source = "@settings(defaultLengthUnit = mm)
13210
13211
13212
13213sketch001 = sketch(on = XY) {
13214}
13215";
13216
13217 let program = Program::parse(initial_source).unwrap().0.unwrap();
13218 let mut frontend = FrontendState::new();
13219
13220 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13221 let mock_ctx = ExecutorContext::new_mock(None).await;
13222 let version = Version(0);
13223
13224 frontend.hack_set_program(&ctx, program).await.unwrap();
13225 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13226 let sketch_id = sketch_object.id;
13227
13228 let circle_ctor = CircleCtor {
13229 start: Point2d {
13230 x: Expr::Var(Number {
13231 value: 5.0,
13232 units: NumericSuffix::Mm,
13233 }),
13234 y: Expr::Var(Number {
13235 value: 0.0,
13236 units: NumericSuffix::Mm,
13237 }),
13238 },
13239 center: Point2d {
13240 x: Expr::Var(Number {
13241 value: 0.0,
13242 units: NumericSuffix::Mm,
13243 }),
13244 y: Expr::Var(Number {
13245 value: 0.0,
13246 units: NumericSuffix::Mm,
13247 }),
13248 },
13249 construction: None,
13250 };
13251 let segment = SegmentCtor::Circle(circle_ctor);
13252 let (src_delta, scene_delta) = frontend
13253 .add_segment(&mock_ctx, version, sketch_id, segment, None)
13254 .await
13255 .unwrap();
13256 assert!(
13257 src_delta
13258 .text
13259 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
13260 "Expected circle in source, got: {}",
13261 src_delta.text
13262 );
13263 assert!(!scene_delta.new_objects.is_empty());
13264
13265 ctx.close().await;
13266 mock_ctx.close().await;
13267 }
13268
13269 #[tokio::test(flavor = "multi_thread")]
13270 async fn test_extra_newlines_add_constraint() {
13271 let initial_source = "@settings(defaultLengthUnit = mm)
13273
13274
13275
13276sketch001 = sketch(on = XY) {
13277 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
13278 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
13279}
13280";
13281
13282 let program = Program::parse(initial_source).unwrap().0.unwrap();
13283 let mut frontend = FrontendState::new();
13284
13285 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13286 let mock_ctx = ExecutorContext::new_mock(None).await;
13287 let version = Version(0);
13288 let project_id = ProjectId(0);
13289 let file_id = FileId(0);
13290
13291 frontend.hack_set_program(&ctx, program).await.unwrap();
13292 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13293 let sketch_id = sketch_object.id;
13294 let sketch = expect_sketch(sketch_object);
13295
13296 let line_ids: Vec<ObjectId> = sketch
13298 .segments
13299 .iter()
13300 .copied()
13301 .filter(|seg_id| {
13302 matches!(
13303 &frontend.scene_graph.objects[seg_id.0].kind,
13304 ObjectKind::Segment {
13305 segment: Segment::Line(_)
13306 }
13307 )
13308 })
13309 .collect();
13310 assert_eq!(line_ids.len(), 2, "Expected two line segments");
13311
13312 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
13313 let ObjectKind::Segment {
13314 segment: Segment::Line(line1_data),
13315 } = &line1.kind
13316 else {
13317 panic!("Expected line");
13318 };
13319 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
13320 let ObjectKind::Segment {
13321 segment: Segment::Line(line2_data),
13322 } = &line2.kind
13323 else {
13324 panic!("Expected line");
13325 };
13326
13327 let constraint = Constraint::Coincident(Coincident {
13329 segments: vec![line1_data.end.into(), line2_data.start.into()],
13330 });
13331
13332 frontend
13334 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13335 .await
13336 .unwrap();
13337 let (src_delta, _scene_delta) = frontend
13338 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13339 .await
13340 .unwrap();
13341 assert!(
13342 src_delta.text.contains("coincident("),
13343 "Expected coincident constraint in source, got: {}",
13344 src_delta.text
13345 );
13346
13347 ctx.close().await;
13348 mock_ctx.close().await;
13349 }
13350
13351 #[tokio::test(flavor = "multi_thread")]
13352 async fn test_extra_newlines_add_line_then_edit_line() {
13353 let initial_source = "@settings(defaultLengthUnit = mm)
13355
13356
13357
13358sketch001 = sketch(on = XY) {
13359}
13360";
13361
13362 let program = Program::parse(initial_source).unwrap().0.unwrap();
13363 let mut frontend = FrontendState::new();
13364
13365 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13366 let mock_ctx = ExecutorContext::new_mock(None).await;
13367 let version = Version(0);
13368
13369 frontend.hack_set_program(&ctx, program).await.unwrap();
13370 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13371 let sketch_id = sketch_object.id;
13372
13373 let line_ctor = LineCtor {
13375 start: Point2d {
13376 x: Expr::Number(Number {
13377 value: 0.0,
13378 units: NumericSuffix::Mm,
13379 }),
13380 y: Expr::Number(Number {
13381 value: 0.0,
13382 units: NumericSuffix::Mm,
13383 }),
13384 },
13385 end: Point2d {
13386 x: Expr::Number(Number {
13387 value: 10.0,
13388 units: NumericSuffix::Mm,
13389 }),
13390 y: Expr::Number(Number {
13391 value: 10.0,
13392 units: NumericSuffix::Mm,
13393 }),
13394 },
13395 construction: None,
13396 };
13397 let segment = SegmentCtor::Line(line_ctor);
13398 let (src_delta, scene_delta) = frontend
13399 .add_segment(&mock_ctx, version, sketch_id, segment, None)
13400 .await
13401 .unwrap();
13402 assert!(
13403 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
13404 "Expected line in source after add, got: {}",
13405 src_delta.text
13406 );
13407 let line_id = *scene_delta.new_objects.last().unwrap();
13409
13410 let line_ctor = LineCtor {
13412 start: Point2d {
13413 x: Expr::Number(Number {
13414 value: 1.0,
13415 units: NumericSuffix::Mm,
13416 }),
13417 y: Expr::Number(Number {
13418 value: 2.0,
13419 units: NumericSuffix::Mm,
13420 }),
13421 },
13422 end: Point2d {
13423 x: Expr::Number(Number {
13424 value: 13.0,
13425 units: NumericSuffix::Mm,
13426 }),
13427 y: Expr::Number(Number {
13428 value: 14.0,
13429 units: NumericSuffix::Mm,
13430 }),
13431 },
13432 construction: None,
13433 };
13434 let segments = vec![ExistingSegmentCtor {
13435 id: line_id,
13436 ctor: SegmentCtor::Line(line_ctor),
13437 }];
13438 let (src_delta, scene_delta) = frontend
13439 .edit_segments(&mock_ctx, version, sketch_id, segments)
13440 .await
13441 .unwrap();
13442 assert!(
13443 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
13444 "Expected edited line in source, got: {}",
13445 src_delta.text
13446 );
13447 assert_eq!(scene_delta.new_objects, vec![]);
13448
13449 ctx.close().await;
13450 mock_ctx.close().await;
13451 }
13452}