1use std::f64::consts::TAU;
2
3use indexmap::IndexSet;
4use kittycad_modeling_cmds::units::UnitLength;
5
6use crate::execution::types::adjust_length;
7use crate::front::Horizontal;
8use crate::front::Vertical;
9use crate::frontend::api::Number;
10use crate::frontend::api::Object;
11use crate::frontend::api::ObjectId;
12use crate::frontend::api::ObjectKind;
13use crate::frontend::sketch::Constraint;
14use crate::frontend::sketch::ConstraintSegment;
15use crate::frontend::sketch::Segment;
16use crate::frontend::sketch::SegmentCtor;
17use crate::pretty::NumericSuffix;
18
19#[cfg(all(feature = "artifact-graph", test))]
20mod tests;
21
22const EPSILON_PARALLEL: f64 = 1e-10;
24const EPSILON_POINT_ON_SEGMENT: f64 = 1e-6;
25
26fn suffix_to_unit(suffix: NumericSuffix) -> UnitLength {
28 match suffix {
29 NumericSuffix::Mm => UnitLength::Millimeters,
30 NumericSuffix::Cm => UnitLength::Centimeters,
31 NumericSuffix::M => UnitLength::Meters,
32 NumericSuffix::Inch => UnitLength::Inches,
33 NumericSuffix::Ft => UnitLength::Feet,
34 NumericSuffix::Yd => UnitLength::Yards,
35 _ => UnitLength::Millimeters,
36 }
37}
38
39fn number_to_unit(n: &Number, target_unit: UnitLength) -> f64 {
41 adjust_length(suffix_to_unit(n.units), n.value, target_unit).0
42}
43
44fn unit_to_number(value: f64, source_unit: UnitLength, target_suffix: NumericSuffix) -> Number {
46 let (value, _) = adjust_length(source_unit, value, suffix_to_unit(target_suffix));
47 Number {
48 value,
49 units: target_suffix,
50 }
51}
52
53fn normalize_trim_points_to_unit(points: &[Coords2d], default_unit: UnitLength) -> Vec<Coords2d> {
55 points
56 .iter()
57 .map(|point| Coords2d {
58 x: adjust_length(UnitLength::Millimeters, point.x, default_unit).0,
59 y: adjust_length(UnitLength::Millimeters, point.y, default_unit).0,
60 })
61 .collect()
62}
63
64#[derive(Debug, Clone, Copy)]
66pub struct Coords2d {
67 pub x: f64,
68 pub y: f64,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum LineEndpoint {
74 Start,
75 End,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ArcPoint {
81 Start,
82 End,
83 Center,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum CirclePoint {
89 Start,
90 Center,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum TrimDirection {
96 Left,
97 Right,
98}
99
100#[derive(Debug, Clone)]
108pub enum TrimItem {
109 Spawn {
110 trim_spawn_seg_id: ObjectId,
111 trim_spawn_coords: Coords2d,
112 next_index: usize,
113 },
114 None {
115 next_index: usize,
116 },
117}
118
119#[derive(Debug, Clone)]
126pub enum TrimTermination {
127 SegEndPoint {
128 trim_termination_coords: Coords2d,
129 },
130 Intersection {
131 trim_termination_coords: Coords2d,
132 intersecting_seg_id: ObjectId,
133 },
134 TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
135 trim_termination_coords: Coords2d,
136 intersecting_seg_id: ObjectId,
137 other_segment_point_id: ObjectId,
138 },
139}
140
141#[derive(Debug, Clone)]
143pub struct TrimTerminations {
144 pub left_side: TrimTermination,
145 pub right_side: TrimTermination,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum AttachToEndpoint {
151 Start,
152 End,
153 Segment,
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum EndpointChanged {
159 Start,
160 End,
161}
162
163#[derive(Debug, Clone)]
165pub struct CoincidentData {
166 pub intersecting_seg_id: ObjectId,
167 pub intersecting_endpoint_point_id: Option<ObjectId>,
168 pub existing_point_segment_constraint_id: Option<ObjectId>,
169}
170
171#[derive(Debug, Clone)]
173pub struct ConstraintToMigrate {
174 pub constraint_id: ObjectId,
175 pub other_entity_id: ObjectId,
176 pub is_point_point: bool,
179 pub attach_to_endpoint: AttachToEndpoint,
180}
181
182#[derive(Debug, Clone)]
184pub enum TrimPlan {
185 DeleteSegment {
186 segment_id: ObjectId,
187 },
188 TailCut {
189 segment_id: ObjectId,
190 endpoint_changed: EndpointChanged,
191 ctor: SegmentCtor,
192 segment_or_point_to_make_coincident_to: ObjectId,
193 intersecting_endpoint_point_id: Option<ObjectId>,
194 constraint_ids_to_delete: Vec<ObjectId>,
195 },
196 ReplaceCircleWithArc {
197 circle_id: ObjectId,
198 arc_start_coords: Coords2d,
199 arc_end_coords: Coords2d,
200 arc_start_termination: Box<TrimTermination>,
201 arc_end_termination: Box<TrimTermination>,
202 },
203 SplitSegment {
204 segment_id: ObjectId,
205 left_trim_coords: Coords2d,
206 right_trim_coords: Coords2d,
207 original_end_coords: Coords2d,
208 left_side: Box<TrimTermination>,
209 right_side: Box<TrimTermination>,
210 left_side_coincident_data: CoincidentData,
211 right_side_coincident_data: CoincidentData,
212 constraints_to_migrate: Vec<ConstraintToMigrate>,
213 constraints_to_delete: Vec<ObjectId>,
214 },
215}
216
217fn lower_trim_plan(plan: &TrimPlan) -> Vec<TrimOperation> {
218 match plan {
219 TrimPlan::DeleteSegment { segment_id } => vec![TrimOperation::SimpleTrim {
220 segment_to_trim_id: *segment_id,
221 }],
222 TrimPlan::TailCut {
223 segment_id,
224 endpoint_changed,
225 ctor,
226 segment_or_point_to_make_coincident_to,
227 intersecting_endpoint_point_id,
228 constraint_ids_to_delete,
229 } => {
230 let mut ops = vec![
231 TrimOperation::EditSegment {
232 segment_id: *segment_id,
233 ctor: ctor.clone(),
234 endpoint_changed: *endpoint_changed,
235 },
236 TrimOperation::AddCoincidentConstraint {
237 segment_id: *segment_id,
238 endpoint_changed: *endpoint_changed,
239 segment_or_point_to_make_coincident_to: *segment_or_point_to_make_coincident_to,
240 intersecting_endpoint_point_id: *intersecting_endpoint_point_id,
241 },
242 ];
243 if !constraint_ids_to_delete.is_empty() {
244 ops.push(TrimOperation::DeleteConstraints {
245 constraint_ids: constraint_ids_to_delete.clone(),
246 });
247 }
248 ops
249 }
250 TrimPlan::ReplaceCircleWithArc {
251 circle_id,
252 arc_start_coords,
253 arc_end_coords,
254 arc_start_termination,
255 arc_end_termination,
256 } => vec![TrimOperation::ReplaceCircleWithArc {
257 circle_id: *circle_id,
258 arc_start_coords: *arc_start_coords,
259 arc_end_coords: *arc_end_coords,
260 arc_start_termination: arc_start_termination.clone(),
261 arc_end_termination: arc_end_termination.clone(),
262 }],
263 TrimPlan::SplitSegment {
264 segment_id,
265 left_trim_coords,
266 right_trim_coords,
267 original_end_coords,
268 left_side,
269 right_side,
270 left_side_coincident_data,
271 right_side_coincident_data,
272 constraints_to_migrate,
273 constraints_to_delete,
274 } => vec![TrimOperation::SplitSegment {
275 segment_id: *segment_id,
276 left_trim_coords: *left_trim_coords,
277 right_trim_coords: *right_trim_coords,
278 original_end_coords: *original_end_coords,
279 left_side: left_side.clone(),
280 right_side: right_side.clone(),
281 left_side_coincident_data: left_side_coincident_data.clone(),
282 right_side_coincident_data: right_side_coincident_data.clone(),
283 constraints_to_migrate: constraints_to_migrate.clone(),
284 constraints_to_delete: constraints_to_delete.clone(),
285 }],
286 }
287}
288
289fn trim_plan_modifies_geometry(plan: &TrimPlan) -> bool {
290 matches!(
291 plan,
292 TrimPlan::DeleteSegment { .. }
293 | TrimPlan::TailCut { .. }
294 | TrimPlan::ReplaceCircleWithArc { .. }
295 | TrimPlan::SplitSegment { .. }
296 )
297}
298
299fn rewrite_object_id(id: ObjectId, rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>) -> ObjectId {
300 rewrite_map.get(&id).copied().unwrap_or(id)
301}
302
303fn rewrite_constraint_segment(
304 segment: crate::frontend::sketch::ConstraintSegment,
305 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
306) -> crate::frontend::sketch::ConstraintSegment {
307 match segment {
308 crate::frontend::sketch::ConstraintSegment::Segment(id) => {
309 crate::frontend::sketch::ConstraintSegment::Segment(rewrite_object_id(id, rewrite_map))
310 }
311 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
312 crate::frontend::sketch::ConstraintSegment::Origin(origin)
313 }
314 }
315}
316
317fn rewrite_constraint_segments(
318 segments: &[crate::frontend::sketch::ConstraintSegment],
319 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
320) -> Vec<crate::frontend::sketch::ConstraintSegment> {
321 segments
322 .iter()
323 .copied()
324 .map(|segment| rewrite_constraint_segment(segment, rewrite_map))
325 .collect()
326}
327
328fn constraint_segments_reference_any(
329 segments: &[crate::frontend::sketch::ConstraintSegment],
330 ids: &std::collections::HashSet<ObjectId>,
331) -> bool {
332 segments.iter().any(|segment| match segment {
333 crate::frontend::sketch::ConstraintSegment::Segment(id) => ids.contains(id),
334 crate::frontend::sketch::ConstraintSegment::Origin(_) => false,
335 })
336}
337
338fn rewrite_constraint_with_map(
339 constraint: &Constraint,
340 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
341) -> Option<Constraint> {
342 match constraint {
343 Constraint::Coincident(coincident) => Some(Constraint::Coincident(crate::frontend::sketch::Coincident {
344 segments: rewrite_constraint_segments(&coincident.segments, rewrite_map),
345 })),
346 Constraint::Distance(distance) => Some(Constraint::Distance(crate::frontend::sketch::Distance {
347 points: rewrite_constraint_segments(&distance.points, rewrite_map),
348 distance: distance.distance,
349 source: distance.source.clone(),
350 })),
351 Constraint::HorizontalDistance(distance) => {
352 Some(Constraint::HorizontalDistance(crate::frontend::sketch::Distance {
353 points: rewrite_constraint_segments(&distance.points, rewrite_map),
354 distance: distance.distance,
355 source: distance.source.clone(),
356 }))
357 }
358 Constraint::VerticalDistance(distance) => {
359 Some(Constraint::VerticalDistance(crate::frontend::sketch::Distance {
360 points: rewrite_constraint_segments(&distance.points, rewrite_map),
361 distance: distance.distance,
362 source: distance.source.clone(),
363 }))
364 }
365 Constraint::Radius(radius) => Some(Constraint::Radius(crate::frontend::sketch::Radius {
366 arc: rewrite_object_id(radius.arc, rewrite_map),
367 radius: radius.radius,
368 source: radius.source.clone(),
369 })),
370 Constraint::Diameter(diameter) => Some(Constraint::Diameter(crate::frontend::sketch::Diameter {
371 arc: rewrite_object_id(diameter.arc, rewrite_map),
372 diameter: diameter.diameter,
373 source: diameter.source.clone(),
374 })),
375 Constraint::EqualRadius(equal_radius) => Some(Constraint::EqualRadius(crate::frontend::sketch::EqualRadius {
376 input: equal_radius
377 .input
378 .iter()
379 .map(|id| rewrite_object_id(*id, rewrite_map))
380 .collect(),
381 })),
382 Constraint::Midpoint(midpoint) => Some(Constraint::Midpoint(crate::frontend::sketch::Midpoint {
383 point: rewrite_object_id(midpoint.point, rewrite_map),
384 segment: rewrite_object_id(midpoint.segment, rewrite_map),
385 })),
386 Constraint::Tangent(tangent) => Some(Constraint::Tangent(crate::frontend::sketch::Tangent {
387 input: tangent
388 .input
389 .iter()
390 .map(|id| rewrite_object_id(*id, rewrite_map))
391 .collect(),
392 })),
393 Constraint::Parallel(parallel) => Some(Constraint::Parallel(crate::frontend::sketch::Parallel {
394 lines: parallel
395 .lines
396 .iter()
397 .map(|id| rewrite_object_id(*id, rewrite_map))
398 .collect(),
399 })),
400 Constraint::Perpendicular(perpendicular) => {
401 Some(Constraint::Perpendicular(crate::frontend::sketch::Perpendicular {
402 lines: perpendicular
403 .lines
404 .iter()
405 .map(|id| rewrite_object_id(*id, rewrite_map))
406 .collect(),
407 }))
408 }
409 Constraint::Horizontal(horizontal) => match horizontal {
410 crate::front::Horizontal::Line { line } => {
411 Some(Constraint::Horizontal(crate::frontend::sketch::Horizontal::Line {
412 line: rewrite_object_id(*line, rewrite_map),
413 }))
414 }
415 crate::front::Horizontal::Points { points } => Some(Constraint::Horizontal(Horizontal::Points {
416 points: points
417 .iter()
418 .map(|point| match point {
419 crate::frontend::sketch::ConstraintSegment::Segment(point) => {
420 crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
421 }
422 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
423 crate::frontend::sketch::ConstraintSegment::Origin(*origin)
424 }
425 })
426 .collect(),
427 })),
428 },
429 Constraint::Vertical(vertical) => match vertical {
430 crate::front::Vertical::Line { line } => {
431 Some(Constraint::Vertical(crate::frontend::sketch::Vertical::Line {
432 line: rewrite_object_id(*line, rewrite_map),
433 }))
434 }
435 crate::front::Vertical::Points { points } => Some(Constraint::Vertical(Vertical::Points {
436 points: points
437 .iter()
438 .map(|point| match point {
439 crate::frontend::sketch::ConstraintSegment::Segment(point) => {
440 crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
441 }
442 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
443 crate::frontend::sketch::ConstraintSegment::Origin(*origin)
444 }
445 })
446 .collect(),
447 })),
448 },
449 _ => None,
450 }
451}
452
453fn point_axis_constraint_references_point(constraint: &Constraint, point_id: ObjectId) -> bool {
454 match constraint {
455 Constraint::Horizontal(Horizontal::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
456 Constraint::Vertical(Vertical::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
457 _ => false,
458 }
459}
460
461#[derive(Debug, Clone)]
462#[allow(clippy::large_enum_variant)]
463pub enum TrimOperation {
464 SimpleTrim {
465 segment_to_trim_id: ObjectId,
466 },
467 EditSegment {
468 segment_id: ObjectId,
469 ctor: SegmentCtor,
470 endpoint_changed: EndpointChanged,
471 },
472 AddCoincidentConstraint {
473 segment_id: ObjectId,
474 endpoint_changed: EndpointChanged,
475 segment_or_point_to_make_coincident_to: ObjectId,
476 intersecting_endpoint_point_id: Option<ObjectId>,
477 },
478 SplitSegment {
479 segment_id: ObjectId,
480 left_trim_coords: Coords2d,
481 right_trim_coords: Coords2d,
482 original_end_coords: Coords2d,
483 left_side: Box<TrimTermination>,
484 right_side: Box<TrimTermination>,
485 left_side_coincident_data: CoincidentData,
486 right_side_coincident_data: CoincidentData,
487 constraints_to_migrate: Vec<ConstraintToMigrate>,
488 constraints_to_delete: Vec<ObjectId>,
489 },
490 ReplaceCircleWithArc {
491 circle_id: ObjectId,
492 arc_start_coords: Coords2d,
493 arc_end_coords: Coords2d,
494 arc_start_termination: Box<TrimTermination>,
495 arc_end_termination: Box<TrimTermination>,
496 },
497 DeleteConstraints {
498 constraint_ids: Vec<ObjectId>,
499 },
500}
501
502pub fn is_point_on_line_segment(
506 point: Coords2d,
507 segment_start: Coords2d,
508 segment_end: Coords2d,
509 epsilon: f64,
510) -> Option<Coords2d> {
511 let dx = segment_end.x - segment_start.x;
512 let dy = segment_end.y - segment_start.y;
513 let segment_length_sq = dx * dx + dy * dy;
514
515 if segment_length_sq < EPSILON_PARALLEL {
516 let dist_sq = (point.x - segment_start.x) * (point.x - segment_start.x)
518 + (point.y - segment_start.y) * (point.y - segment_start.y);
519 if dist_sq <= epsilon * epsilon {
520 return Some(point);
521 }
522 return None;
523 }
524
525 let point_dx = point.x - segment_start.x;
526 let point_dy = point.y - segment_start.y;
527 let projection_param = (point_dx * dx + point_dy * dy) / segment_length_sq;
528
529 if !(0.0..=1.0).contains(&projection_param) {
531 return None;
532 }
533
534 let projected_point = Coords2d {
536 x: segment_start.x + projection_param * dx,
537 y: segment_start.y + projection_param * dy,
538 };
539
540 let dist_dx = point.x - projected_point.x;
542 let dist_dy = point.y - projected_point.y;
543 let distance_sq = dist_dx * dist_dx + dist_dy * dist_dy;
544
545 if distance_sq <= epsilon * epsilon {
546 Some(point)
547 } else {
548 None
549 }
550}
551
552pub fn line_segment_intersection(
556 line1_start: Coords2d,
557 line1_end: Coords2d,
558 line2_start: Coords2d,
559 line2_end: Coords2d,
560 epsilon: f64,
561) -> Option<Coords2d> {
562 if let Some(point) = is_point_on_line_segment(line1_start, line2_start, line2_end, epsilon) {
564 return Some(point);
565 }
566
567 if let Some(point) = is_point_on_line_segment(line1_end, line2_start, line2_end, epsilon) {
568 return Some(point);
569 }
570
571 if let Some(point) = is_point_on_line_segment(line2_start, line1_start, line1_end, epsilon) {
572 return Some(point);
573 }
574
575 if let Some(point) = is_point_on_line_segment(line2_end, line1_start, line1_end, epsilon) {
576 return Some(point);
577 }
578
579 let x1 = line1_start.x;
581 let y1 = line1_start.y;
582 let x2 = line1_end.x;
583 let y2 = line1_end.y;
584 let x3 = line2_start.x;
585 let y3 = line2_start.y;
586 let x4 = line2_end.x;
587 let y4 = line2_end.y;
588
589 let denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
590 if denominator.abs() < EPSILON_PARALLEL {
591 return None;
593 }
594
595 let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator;
596 let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominator;
597
598 if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
600 let x = x1 + t * (x2 - x1);
601 let y = y1 + t * (y2 - y1);
602 return Some(Coords2d { x, y });
603 }
604
605 None
606}
607
608pub fn project_point_onto_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
613 let dx = segment_end.x - segment_start.x;
614 let dy = segment_end.y - segment_start.y;
615 let segment_length_sq = dx * dx + dy * dy;
616
617 if segment_length_sq < EPSILON_PARALLEL {
618 return 0.0;
620 }
621
622 let point_dx = point.x - segment_start.x;
623 let point_dy = point.y - segment_start.y;
624
625 (point_dx * dx + point_dy * dy) / segment_length_sq
626}
627
628pub fn perpendicular_distance_to_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
632 let dx = segment_end.x - segment_start.x;
633 let dy = segment_end.y - segment_start.y;
634 let segment_length_sq = dx * dx + dy * dy;
635
636 if segment_length_sq < EPSILON_PARALLEL {
637 let dist_dx = point.x - segment_start.x;
639 let dist_dy = point.y - segment_start.y;
640 return (dist_dx * dist_dx + dist_dy * dist_dy).sqrt();
641 }
642
643 let point_dx = point.x - segment_start.x;
645 let point_dy = point.y - segment_start.y;
646
647 let t = (point_dx * dx + point_dy * dy) / segment_length_sq;
649
650 let clamped_t = t.clamp(0.0, 1.0);
652 let closest_point = Coords2d {
653 x: segment_start.x + clamped_t * dx,
654 y: segment_start.y + clamped_t * dy,
655 };
656
657 let dist_dx = point.x - closest_point.x;
659 let dist_dy = point.y - closest_point.y;
660 (dist_dx * dist_dx + dist_dy * dist_dy).sqrt()
661}
662
663pub fn is_point_on_arc(point: Coords2d, center: Coords2d, start: Coords2d, end: Coords2d, epsilon: f64) -> bool {
667 let radius = ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
669
670 let dist_from_center =
672 ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
673 if (dist_from_center - radius).abs() > epsilon {
674 return false;
675 }
676
677 let start_angle = libm::atan2(start.y - center.y, start.x - center.x);
679 let end_angle = libm::atan2(end.y - center.y, end.x - center.x);
680 let point_angle = libm::atan2(point.y - center.y, point.x - center.x);
681
682 let normalize_angle = |angle: f64| -> f64 {
684 if !angle.is_finite() {
685 return angle;
686 }
687 let mut normalized = angle;
688 while normalized < 0.0 {
689 normalized += TAU;
690 }
691 while normalized >= TAU {
692 normalized -= TAU;
693 }
694 normalized
695 };
696
697 let normalized_start = normalize_angle(start_angle);
698 let normalized_end = normalize_angle(end_angle);
699 let normalized_point = normalize_angle(point_angle);
700
701 if normalized_start < normalized_end {
705 normalized_point >= normalized_start && normalized_point <= normalized_end
707 } else {
708 normalized_point >= normalized_start || normalized_point <= normalized_end
710 }
711}
712
713pub fn line_arc_intersection(
717 line_start: Coords2d,
718 line_end: Coords2d,
719 arc_center: Coords2d,
720 arc_start: Coords2d,
721 arc_end: Coords2d,
722 epsilon: f64,
723) -> Option<Coords2d> {
724 let radius = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
726 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
727 .sqrt();
728
729 let translated_line_start = Coords2d {
731 x: line_start.x - arc_center.x,
732 y: line_start.y - arc_center.y,
733 };
734 let translated_line_end = Coords2d {
735 x: line_end.x - arc_center.x,
736 y: line_end.y - arc_center.y,
737 };
738
739 let dx = translated_line_end.x - translated_line_start.x;
741 let dy = translated_line_end.y - translated_line_start.y;
742
743 let a = dx * dx + dy * dy;
750 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
751 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
752 - radius * radius;
753
754 let discriminant = b * b - 4.0 * a * c;
755
756 if discriminant < 0.0 {
757 return None;
759 }
760
761 if a.abs() < EPSILON_PARALLEL {
762 let dist_from_center = (translated_line_start.x * translated_line_start.x
764 + translated_line_start.y * translated_line_start.y)
765 .sqrt();
766 if (dist_from_center - radius).abs() <= epsilon {
767 let point = line_start;
769 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
770 return Some(point);
771 }
772 }
773 return None;
774 }
775
776 let sqrt_discriminant = discriminant.sqrt();
777 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
778 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
779
780 let mut candidates: Vec<(f64, Coords2d)> = Vec::new();
782 if (0.0..=1.0).contains(&t1) {
783 let point = Coords2d {
784 x: line_start.x + t1 * (line_end.x - line_start.x),
785 y: line_start.y + t1 * (line_end.y - line_start.y),
786 };
787 candidates.push((t1, point));
788 }
789 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
790 let point = Coords2d {
791 x: line_start.x + t2 * (line_end.x - line_start.x),
792 y: line_start.y + t2 * (line_end.y - line_start.y),
793 };
794 candidates.push((t2, point));
795 }
796
797 for (_t, point) in candidates {
799 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
800 return Some(point);
801 }
802 }
803
804 None
805}
806
807pub fn line_circle_intersections(
812 line_start: Coords2d,
813 line_end: Coords2d,
814 circle_center: Coords2d,
815 radius: f64,
816 epsilon: f64,
817) -> Vec<(f64, Coords2d)> {
818 let translated_line_start = Coords2d {
820 x: line_start.x - circle_center.x,
821 y: line_start.y - circle_center.y,
822 };
823 let translated_line_end = Coords2d {
824 x: line_end.x - circle_center.x,
825 y: line_end.y - circle_center.y,
826 };
827
828 let dx = translated_line_end.x - translated_line_start.x;
829 let dy = translated_line_end.y - translated_line_start.y;
830 let a = dx * dx + dy * dy;
831 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
832 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
833 - radius * radius;
834
835 if a.abs() < EPSILON_PARALLEL {
836 return Vec::new();
837 }
838
839 let discriminant = b * b - 4.0 * a * c;
840 if discriminant < 0.0 {
841 return Vec::new();
842 }
843
844 let sqrt_discriminant = discriminant.sqrt();
845 let mut intersections = Vec::new();
846
847 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
848 if (0.0..=1.0).contains(&t1) {
849 intersections.push((
850 t1,
851 Coords2d {
852 x: line_start.x + t1 * (line_end.x - line_start.x),
853 y: line_start.y + t1 * (line_end.y - line_start.y),
854 },
855 ));
856 }
857
858 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
859 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
860 intersections.push((
861 t2,
862 Coords2d {
863 x: line_start.x + t2 * (line_end.x - line_start.x),
864 y: line_start.y + t2 * (line_end.y - line_start.y),
865 },
866 ));
867 }
868
869 intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
870 intersections
871}
872
873pub fn project_point_onto_circle(point: Coords2d, center: Coords2d, start: Coords2d) -> f64 {
879 let normalize_angle = |angle: f64| -> f64 {
880 if !angle.is_finite() {
881 return angle;
882 }
883 let mut normalized = angle;
884 while normalized < 0.0 {
885 normalized += TAU;
886 }
887 while normalized >= TAU {
888 normalized -= TAU;
889 }
890 normalized
891 };
892
893 let start_angle = normalize_angle(libm::atan2(start.y - center.y, start.x - center.x));
894 let point_angle = normalize_angle(libm::atan2(point.y - center.y, point.x - center.x));
895 let delta_ccw = (point_angle - start_angle).rem_euclid(TAU);
896 delta_ccw / TAU
897}
898
899fn is_point_on_circle(point: Coords2d, center: Coords2d, radius: f64, epsilon: f64) -> bool {
900 let dist = ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
901 (dist - radius).abs() <= epsilon
902}
903
904pub fn project_point_onto_arc(point: Coords2d, arc_center: Coords2d, arc_start: Coords2d, arc_end: Coords2d) -> f64 {
907 let start_angle = libm::atan2(arc_start.y - arc_center.y, arc_start.x - arc_center.x);
909 let end_angle = libm::atan2(arc_end.y - arc_center.y, arc_end.x - arc_center.x);
910 let point_angle = libm::atan2(point.y - arc_center.y, point.x - arc_center.x);
911
912 let normalize_angle = |angle: f64| -> f64 {
914 if !angle.is_finite() {
915 return angle;
916 }
917 let mut normalized = angle;
918 while normalized < 0.0 {
919 normalized += TAU;
920 }
921 while normalized >= TAU {
922 normalized -= TAU;
923 }
924 normalized
925 };
926
927 let normalized_start = normalize_angle(start_angle);
928 let normalized_end = normalize_angle(end_angle);
929 let normalized_point = normalize_angle(point_angle);
930
931 let arc_length = if normalized_start < normalized_end {
933 normalized_end - normalized_start
934 } else {
935 TAU - normalized_start + normalized_end
937 };
938
939 if arc_length < EPSILON_PARALLEL {
940 return 0.0;
942 }
943
944 let point_arc_length = if normalized_start < normalized_end {
946 if normalized_point >= normalized_start && normalized_point <= normalized_end {
947 normalized_point - normalized_start
948 } else {
949 let dist_to_start = (normalized_point - normalized_start)
951 .abs()
952 .min(TAU - (normalized_point - normalized_start).abs());
953 let dist_to_end = (normalized_point - normalized_end)
954 .abs()
955 .min(TAU - (normalized_point - normalized_end).abs());
956 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
957 }
958 } else {
959 if normalized_point >= normalized_start || normalized_point <= normalized_end {
961 if normalized_point >= normalized_start {
962 normalized_point - normalized_start
963 } else {
964 TAU - normalized_start + normalized_point
965 }
966 } else {
967 let dist_to_start = (normalized_point - normalized_start)
969 .abs()
970 .min(TAU - (normalized_point - normalized_start).abs());
971 let dist_to_end = (normalized_point - normalized_end)
972 .abs()
973 .min(TAU - (normalized_point - normalized_end).abs());
974 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
975 }
976 };
977
978 point_arc_length / arc_length
980}
981
982pub fn arc_arc_intersections(
986 arc1_center: Coords2d,
987 arc1_start: Coords2d,
988 arc1_end: Coords2d,
989 arc2_center: Coords2d,
990 arc2_start: Coords2d,
991 arc2_end: Coords2d,
992 epsilon: f64,
993) -> Vec<Coords2d> {
994 let r1 = ((arc1_start.x - arc1_center.x) * (arc1_start.x - arc1_center.x)
996 + (arc1_start.y - arc1_center.y) * (arc1_start.y - arc1_center.y))
997 .sqrt();
998 let r2 = ((arc2_start.x - arc2_center.x) * (arc2_start.x - arc2_center.x)
999 + (arc2_start.y - arc2_center.y) * (arc2_start.y - arc2_center.y))
1000 .sqrt();
1001
1002 let dx = arc2_center.x - arc1_center.x;
1004 let dy = arc2_center.y - arc1_center.y;
1005 let d = (dx * dx + dy * dy).sqrt();
1006
1007 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon {
1009 return Vec::new();
1011 }
1012
1013 if d < EPSILON_PARALLEL {
1015 return Vec::new();
1017 }
1018
1019 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1022 let h_sq = r1 * r1 - a * a;
1023
1024 if h_sq < 0.0 {
1026 return Vec::new();
1027 }
1028
1029 let h = h_sq.sqrt();
1030
1031 if h.is_nan() {
1033 return Vec::new();
1034 }
1035
1036 let ux = dx / d;
1038 let uy = dy / d;
1039
1040 let px = -uy;
1042 let py = ux;
1043
1044 let mid_point = Coords2d {
1046 x: arc1_center.x + a * ux,
1047 y: arc1_center.y + a * uy,
1048 };
1049
1050 let intersection1 = Coords2d {
1052 x: mid_point.x + h * px,
1053 y: mid_point.y + h * py,
1054 };
1055 let intersection2 = Coords2d {
1056 x: mid_point.x - h * px,
1057 y: mid_point.y - h * py,
1058 };
1059
1060 let mut candidates: Vec<Coords2d> = Vec::new();
1062
1063 if is_point_on_arc(intersection1, arc1_center, arc1_start, arc1_end, epsilon)
1064 && is_point_on_arc(intersection1, arc2_center, arc2_start, arc2_end, epsilon)
1065 {
1066 candidates.push(intersection1);
1067 }
1068
1069 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1070 if is_point_on_arc(intersection2, arc1_center, arc1_start, arc1_end, epsilon)
1072 && is_point_on_arc(intersection2, arc2_center, arc2_start, arc2_end, epsilon)
1073 {
1074 candidates.push(intersection2);
1075 }
1076 }
1077
1078 candidates
1079}
1080
1081pub fn arc_arc_intersection(
1086 arc1_center: Coords2d,
1087 arc1_start: Coords2d,
1088 arc1_end: Coords2d,
1089 arc2_center: Coords2d,
1090 arc2_start: Coords2d,
1091 arc2_end: Coords2d,
1092 epsilon: f64,
1093) -> Option<Coords2d> {
1094 arc_arc_intersections(
1095 arc1_center,
1096 arc1_start,
1097 arc1_end,
1098 arc2_center,
1099 arc2_start,
1100 arc2_end,
1101 epsilon,
1102 )
1103 .first()
1104 .copied()
1105}
1106
1107pub fn circle_arc_intersections(
1111 circle_center: Coords2d,
1112 circle_radius: f64,
1113 arc_center: Coords2d,
1114 arc_start: Coords2d,
1115 arc_end: Coords2d,
1116 epsilon: f64,
1117) -> Vec<Coords2d> {
1118 let r1 = circle_radius;
1119 let r2 = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
1120 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
1121 .sqrt();
1122
1123 let dx = arc_center.x - circle_center.x;
1124 let dy = arc_center.y - circle_center.y;
1125 let d = (dx * dx + dy * dy).sqrt();
1126
1127 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon || d < EPSILON_PARALLEL {
1128 return Vec::new();
1129 }
1130
1131 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1132 let h_sq = r1 * r1 - a * a;
1133 if h_sq < 0.0 {
1134 return Vec::new();
1135 }
1136 let h = h_sq.sqrt();
1137 if h.is_nan() {
1138 return Vec::new();
1139 }
1140
1141 let ux = dx / d;
1142 let uy = dy / d;
1143 let px = -uy;
1144 let py = ux;
1145 let mid_point = Coords2d {
1146 x: circle_center.x + a * ux,
1147 y: circle_center.y + a * uy,
1148 };
1149
1150 let intersection1 = Coords2d {
1151 x: mid_point.x + h * px,
1152 y: mid_point.y + h * py,
1153 };
1154 let intersection2 = Coords2d {
1155 x: mid_point.x - h * px,
1156 y: mid_point.y - h * py,
1157 };
1158
1159 let mut intersections = Vec::new();
1160 if is_point_on_arc(intersection1, arc_center, arc_start, arc_end, epsilon) {
1161 intersections.push(intersection1);
1162 }
1163 if ((intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon)
1164 && is_point_on_arc(intersection2, arc_center, arc_start, arc_end, epsilon)
1165 {
1166 intersections.push(intersection2);
1167 }
1168 intersections
1169}
1170
1171pub fn circle_circle_intersections(
1175 circle1_center: Coords2d,
1176 circle1_radius: f64,
1177 circle2_center: Coords2d,
1178 circle2_radius: f64,
1179 epsilon: f64,
1180) -> Vec<Coords2d> {
1181 let dx = circle2_center.x - circle1_center.x;
1182 let dy = circle2_center.y - circle1_center.y;
1183 let d = (dx * dx + dy * dy).sqrt();
1184
1185 if d > circle1_radius + circle2_radius + epsilon
1186 || d < (circle1_radius - circle2_radius).abs() - epsilon
1187 || d < EPSILON_PARALLEL
1188 {
1189 return Vec::new();
1190 }
1191
1192 let a = (circle1_radius * circle1_radius - circle2_radius * circle2_radius + d * d) / (2.0 * d);
1193 let h_sq = circle1_radius * circle1_radius - a * a;
1194 if h_sq < 0.0 {
1195 return Vec::new();
1196 }
1197
1198 let h = if h_sq <= epsilon { 0.0 } else { h_sq.sqrt() };
1199 if h.is_nan() {
1200 return Vec::new();
1201 }
1202
1203 let ux = dx / d;
1204 let uy = dy / d;
1205 let px = -uy;
1206 let py = ux;
1207
1208 let mid_point = Coords2d {
1209 x: circle1_center.x + a * ux,
1210 y: circle1_center.y + a * uy,
1211 };
1212
1213 let intersection1 = Coords2d {
1214 x: mid_point.x + h * px,
1215 y: mid_point.y + h * py,
1216 };
1217 let intersection2 = Coords2d {
1218 x: mid_point.x - h * px,
1219 y: mid_point.y - h * py,
1220 };
1221
1222 let mut intersections = vec![intersection1];
1223 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1224 intersections.push(intersection2);
1225 }
1226 intersections
1227}
1228
1229fn get_point_coords_from_native(objects: &[Object], point_id: ObjectId, default_unit: UnitLength) -> Option<Coords2d> {
1232 let point_obj = objects.get(point_id.0)?;
1233
1234 let ObjectKind::Segment { segment } = &point_obj.kind else {
1236 return None;
1237 };
1238
1239 let Segment::Point(point) = segment else {
1240 return None;
1241 };
1242
1243 Some(Coords2d {
1245 x: number_to_unit(&point.position.x, default_unit),
1246 y: number_to_unit(&point.position.y, default_unit),
1247 })
1248}
1249
1250pub fn get_position_coords_for_line(
1253 segment_obj: &Object,
1254 which: LineEndpoint,
1255 objects: &[Object],
1256 default_unit: UnitLength,
1257) -> Option<Coords2d> {
1258 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1259 return None;
1260 };
1261
1262 let Segment::Line(line) = segment else {
1263 return None;
1264 };
1265
1266 let point_id = match which {
1268 LineEndpoint::Start => line.start,
1269 LineEndpoint::End => line.end,
1270 };
1271
1272 get_point_coords_from_native(objects, point_id, default_unit)
1273}
1274
1275fn is_point_coincident_with_segment_native(point_id: ObjectId, segment_id: ObjectId, objects: &[Object]) -> bool {
1277 for obj in objects {
1279 let ObjectKind::Constraint { constraint } = &obj.kind else {
1280 continue;
1281 };
1282
1283 let Constraint::Coincident(coincident) = constraint else {
1284 continue;
1285 };
1286
1287 let has_point = coincident.contains_segment(point_id);
1289 let has_segment = coincident.contains_segment(segment_id);
1290
1291 if has_point && has_segment {
1292 return true;
1293 }
1294 }
1295 false
1296}
1297
1298pub fn get_position_coords_from_arc(
1300 segment_obj: &Object,
1301 which: ArcPoint,
1302 objects: &[Object],
1303 default_unit: UnitLength,
1304) -> Option<Coords2d> {
1305 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1306 return None;
1307 };
1308
1309 let Segment::Arc(arc) = segment else {
1310 return None;
1311 };
1312
1313 let point_id = match which {
1315 ArcPoint::Start => arc.start,
1316 ArcPoint::End => arc.end,
1317 ArcPoint::Center => arc.center,
1318 };
1319
1320 get_point_coords_from_native(objects, point_id, default_unit)
1321}
1322
1323pub fn get_position_coords_from_circle(
1325 segment_obj: &Object,
1326 which: CirclePoint,
1327 objects: &[Object],
1328 default_unit: UnitLength,
1329) -> Option<Coords2d> {
1330 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1331 return None;
1332 };
1333
1334 let Segment::Circle(circle) = segment else {
1335 return None;
1336 };
1337
1338 let point_id = match which {
1339 CirclePoint::Start => circle.start,
1340 CirclePoint::Center => circle.center,
1341 };
1342
1343 get_point_coords_from_native(objects, point_id, default_unit)
1344}
1345
1346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1348enum CurveKind {
1349 Line,
1350 Circular,
1351}
1352
1353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1355enum CurveDomain {
1356 Open,
1357 Closed,
1358}
1359
1360#[derive(Debug, Clone, Copy)]
1362struct CurveHandle {
1363 segment_id: ObjectId,
1364 kind: CurveKind,
1365 domain: CurveDomain,
1366 start: Coords2d,
1367 end: Coords2d,
1368 center: Option<Coords2d>,
1369 radius: Option<f64>,
1370}
1371
1372impl CurveHandle {
1373 fn project_for_trim(self, point: Coords2d) -> Result<f64, String> {
1374 match (self.kind, self.domain) {
1375 (CurveKind::Line, CurveDomain::Open) => Ok(project_point_onto_segment(point, self.start, self.end)),
1376 (CurveKind::Circular, CurveDomain::Open) => {
1377 let center = self
1378 .center
1379 .ok_or_else(|| format!("Curve {} missing center for arc projection", self.segment_id.0))?;
1380 Ok(project_point_onto_arc(point, center, self.start, self.end))
1381 }
1382 (CurveKind::Circular, CurveDomain::Closed) => {
1383 let center = self
1384 .center
1385 .ok_or_else(|| format!("Curve {} missing center for circle projection", self.segment_id.0))?;
1386 Ok(project_point_onto_circle(point, center, self.start))
1387 }
1388 (CurveKind::Line, CurveDomain::Closed) => Err(format!(
1389 "Invalid curve state: line {} cannot be closed",
1390 self.segment_id.0
1391 )),
1392 }
1393 }
1394}
1395
1396fn load_curve_handle(
1398 segment_obj: &Object,
1399 objects: &[Object],
1400 default_unit: UnitLength,
1401) -> Result<CurveHandle, String> {
1402 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1403 return Err("Object is not a segment".to_owned());
1404 };
1405
1406 match segment {
1407 Segment::Line(_) => {
1408 let start = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit)
1409 .ok_or_else(|| format!("Could not get line start for segment {}", segment_obj.id.0))?;
1410 let end = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit)
1411 .ok_or_else(|| format!("Could not get line end for segment {}", segment_obj.id.0))?;
1412 Ok(CurveHandle {
1413 segment_id: segment_obj.id,
1414 kind: CurveKind::Line,
1415 domain: CurveDomain::Open,
1416 start,
1417 end,
1418 center: None,
1419 radius: None,
1420 })
1421 }
1422 Segment::Arc(_) => {
1423 let start = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit)
1424 .ok_or_else(|| format!("Could not get arc start for segment {}", segment_obj.id.0))?;
1425 let end = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit)
1426 .ok_or_else(|| format!("Could not get arc end for segment {}", segment_obj.id.0))?;
1427 let center = get_position_coords_from_arc(segment_obj, ArcPoint::Center, objects, default_unit)
1428 .ok_or_else(|| format!("Could not get arc center for segment {}", segment_obj.id.0))?;
1429 let radius =
1430 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1431 Ok(CurveHandle {
1432 segment_id: segment_obj.id,
1433 kind: CurveKind::Circular,
1434 domain: CurveDomain::Open,
1435 start,
1436 end,
1437 center: Some(center),
1438 radius: Some(radius),
1439 })
1440 }
1441 Segment::Circle(_) => {
1442 let start = get_position_coords_from_circle(segment_obj, CirclePoint::Start, objects, default_unit)
1443 .ok_or_else(|| format!("Could not get circle start for segment {}", segment_obj.id.0))?;
1444 let center = get_position_coords_from_circle(segment_obj, CirclePoint::Center, objects, default_unit)
1445 .ok_or_else(|| format!("Could not get circle center for segment {}", segment_obj.id.0))?;
1446 let radius =
1447 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1448 Ok(CurveHandle {
1449 segment_id: segment_obj.id,
1450 kind: CurveKind::Circular,
1451 domain: CurveDomain::Closed,
1452 start,
1453 end: start,
1455 center: Some(center),
1456 radius: Some(radius),
1457 })
1458 }
1459 Segment::Point(_) => Err(format!(
1460 "Point segment {} cannot be used as trim curve",
1461 segment_obj.id.0
1462 )),
1463 }
1464}
1465
1466fn project_point_onto_curve(curve: CurveHandle, point: Coords2d) -> Result<f64, String> {
1467 curve.project_for_trim(point)
1468}
1469
1470fn curve_contains_point(curve: CurveHandle, point: Coords2d, epsilon: f64) -> bool {
1471 match (curve.kind, curve.domain) {
1472 (CurveKind::Line, CurveDomain::Open) => {
1473 let t = project_point_onto_segment(point, curve.start, curve.end);
1474 (0.0..=1.0).contains(&t) && perpendicular_distance_to_segment(point, curve.start, curve.end) <= epsilon
1475 }
1476 (CurveKind::Circular, CurveDomain::Open) => curve
1477 .center
1478 .is_some_and(|center| is_point_on_arc(point, center, curve.start, curve.end, epsilon)),
1479 (CurveKind::Circular, CurveDomain::Closed) => curve.center.is_some_and(|center| {
1480 let radius = curve
1481 .radius
1482 .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1483 is_point_on_circle(point, center, radius, epsilon)
1484 }),
1485 (CurveKind::Line, CurveDomain::Closed) => false,
1486 }
1487}
1488
1489fn curve_line_segment_intersections(
1490 curve: CurveHandle,
1491 line_start: Coords2d,
1492 line_end: Coords2d,
1493 epsilon: f64,
1494) -> Vec<(f64, Coords2d)> {
1495 match (curve.kind, curve.domain) {
1496 (CurveKind::Line, CurveDomain::Open) => {
1497 line_segment_intersection(line_start, line_end, curve.start, curve.end, epsilon)
1498 .map(|intersection| {
1499 (
1500 project_point_onto_segment(intersection, line_start, line_end),
1501 intersection,
1502 )
1503 })
1504 .into_iter()
1505 .collect()
1506 }
1507 (CurveKind::Circular, CurveDomain::Open) => curve
1508 .center
1509 .and_then(|center| line_arc_intersection(line_start, line_end, center, curve.start, curve.end, epsilon))
1510 .map(|intersection| {
1511 (
1512 project_point_onto_segment(intersection, line_start, line_end),
1513 intersection,
1514 )
1515 })
1516 .into_iter()
1517 .collect(),
1518 (CurveKind::Circular, CurveDomain::Closed) => {
1519 let Some(center) = curve.center else {
1520 return Vec::new();
1521 };
1522 let radius = curve
1523 .radius
1524 .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1525 line_circle_intersections(line_start, line_end, center, radius, epsilon)
1526 }
1527 (CurveKind::Line, CurveDomain::Closed) => Vec::new(),
1528 }
1529}
1530
1531fn curve_polyline_intersections(curve: CurveHandle, polyline: &[Coords2d], epsilon: f64) -> Vec<(Coords2d, usize)> {
1532 let mut intersections = Vec::new();
1533
1534 for i in 0..polyline.len().saturating_sub(1) {
1535 let p1 = polyline[i];
1536 let p2 = polyline[i + 1];
1537 for (_, intersection) in curve_line_segment_intersections(curve, p1, p2, epsilon) {
1538 intersections.push((intersection, i));
1539 }
1540 }
1541
1542 intersections
1543}
1544
1545fn curve_curve_intersections(curve: CurveHandle, other: CurveHandle, epsilon: f64) -> Vec<Coords2d> {
1546 match (curve.kind, curve.domain, other.kind, other.domain) {
1547 (CurveKind::Line, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => {
1548 line_segment_intersection(curve.start, curve.end, other.start, other.end, epsilon)
1549 .into_iter()
1550 .collect()
1551 }
1552 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => other
1553 .center
1554 .and_then(|other_center| {
1555 line_arc_intersection(curve.start, curve.end, other_center, other.start, other.end, epsilon)
1556 })
1557 .into_iter()
1558 .collect(),
1559 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1560 let Some(other_center) = other.center else {
1561 return Vec::new();
1562 };
1563 let other_radius = other.radius.unwrap_or_else(|| {
1564 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1565 });
1566 line_circle_intersections(curve.start, curve.end, other_center, other_radius, epsilon)
1567 .into_iter()
1568 .map(|(_, point)| point)
1569 .collect()
1570 }
1571 (CurveKind::Circular, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => curve
1572 .center
1573 .and_then(|curve_center| {
1574 line_arc_intersection(other.start, other.end, curve_center, curve.start, curve.end, epsilon)
1575 })
1576 .into_iter()
1577 .collect(),
1578 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => {
1579 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1580 return Vec::new();
1581 };
1582 arc_arc_intersections(
1583 curve_center,
1584 curve.start,
1585 curve.end,
1586 other_center,
1587 other.start,
1588 other.end,
1589 epsilon,
1590 )
1591 }
1592 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1593 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1594 return Vec::new();
1595 };
1596 let other_radius = other.radius.unwrap_or_else(|| {
1597 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1598 });
1599 circle_arc_intersections(
1600 other_center,
1601 other_radius,
1602 curve_center,
1603 curve.start,
1604 curve.end,
1605 epsilon,
1606 )
1607 }
1608 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Line, CurveDomain::Open) => {
1609 let Some(curve_center) = curve.center else {
1610 return Vec::new();
1611 };
1612 let curve_radius = curve.radius.unwrap_or_else(|| {
1613 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1614 });
1615 line_circle_intersections(other.start, other.end, curve_center, curve_radius, epsilon)
1616 .into_iter()
1617 .map(|(_, point)| point)
1618 .collect()
1619 }
1620 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Open) => {
1621 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1622 return Vec::new();
1623 };
1624 let curve_radius = curve.radius.unwrap_or_else(|| {
1625 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1626 });
1627 circle_arc_intersections(
1628 curve_center,
1629 curve_radius,
1630 other_center,
1631 other.start,
1632 other.end,
1633 epsilon,
1634 )
1635 }
1636 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Closed) => {
1637 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1638 return Vec::new();
1639 };
1640 let curve_radius = curve.radius.unwrap_or_else(|| {
1641 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1642 });
1643 let other_radius = other.radius.unwrap_or_else(|| {
1644 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1645 });
1646 circle_circle_intersections(curve_center, curve_radius, other_center, other_radius, epsilon)
1647 }
1648 _ => Vec::new(),
1649 }
1650}
1651
1652fn segment_endpoint_points(
1653 segment_obj: &Object,
1654 objects: &[Object],
1655 default_unit: UnitLength,
1656) -> Vec<(ObjectId, Coords2d)> {
1657 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1658 return Vec::new();
1659 };
1660
1661 match segment {
1662 Segment::Line(line) => {
1663 let mut points = Vec::new();
1664 if let Some(start) = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit) {
1665 points.push((line.start, start));
1666 }
1667 if let Some(end) = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit) {
1668 points.push((line.end, end));
1669 }
1670 points
1671 }
1672 Segment::Arc(arc) => {
1673 let mut points = Vec::new();
1674 if let Some(start) = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit) {
1675 points.push((arc.start, start));
1676 }
1677 if let Some(end) = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit) {
1678 points.push((arc.end, end));
1679 }
1680 points
1681 }
1682 _ => Vec::new(),
1683 }
1684}
1685
1686pub fn get_next_trim_spawn(
1714 points: &[Coords2d],
1715 start_index: usize,
1716 objects: &[Object],
1717 default_unit: UnitLength,
1718) -> TrimItem {
1719 let scene_curves: Vec<CurveHandle> = objects
1720 .iter()
1721 .filter_map(|obj| load_curve_handle(obj, objects, default_unit).ok())
1722 .collect();
1723
1724 for i in start_index..points.len().saturating_sub(1) {
1726 let p1 = points[i];
1727 let p2 = points[i + 1];
1728
1729 for curve in &scene_curves {
1731 let intersections = curve_line_segment_intersections(*curve, p1, p2, EPSILON_POINT_ON_SEGMENT);
1732 if let Some((_, intersection)) = intersections.first() {
1733 return TrimItem::Spawn {
1734 trim_spawn_seg_id: curve.segment_id,
1735 trim_spawn_coords: *intersection,
1736 next_index: i,
1737 };
1738 }
1739 }
1740 }
1741
1742 TrimItem::None {
1744 next_index: points.len().saturating_sub(1),
1745 }
1746}
1747
1748pub fn get_trim_spawn_terminations(
1803 trim_spawn_seg_id: ObjectId,
1804 trim_spawn_coords: &[Coords2d],
1805 objects: &[Object],
1806 default_unit: UnitLength,
1807) -> Result<TrimTerminations, String> {
1808 let trim_spawn_seg = objects.iter().find(|obj| obj.id == trim_spawn_seg_id);
1810
1811 let trim_spawn_seg = match trim_spawn_seg {
1812 Some(seg) => seg,
1813 None => {
1814 return Err(format!("Trim spawn segment {} not found", trim_spawn_seg_id.0));
1815 }
1816 };
1817
1818 let trim_curve = load_curve_handle(trim_spawn_seg, objects, default_unit).map_err(|e| {
1819 format!(
1820 "Failed to load trim spawn segment {} as normalized curve: {}",
1821 trim_spawn_seg_id.0, e
1822 )
1823 })?;
1824
1825 let all_intersections = curve_polyline_intersections(trim_curve, trim_spawn_coords, EPSILON_POINT_ON_SEGMENT);
1830
1831 let intersection_point = if all_intersections.is_empty() {
1834 return Err("Could not find intersection point between polyline and trim spawn segment".to_string());
1835 } else {
1836 let mid_index = (trim_spawn_coords.len() - 1) / 2;
1838 let mid_point = trim_spawn_coords[mid_index];
1839
1840 let mut min_dist = f64::INFINITY;
1842 let mut closest_intersection = all_intersections[0].0;
1843
1844 for (intersection, _) in &all_intersections {
1845 let dist = ((intersection.x - mid_point.x) * (intersection.x - mid_point.x)
1846 + (intersection.y - mid_point.y) * (intersection.y - mid_point.y))
1847 .sqrt();
1848 if dist < min_dist {
1849 min_dist = dist;
1850 closest_intersection = *intersection;
1851 }
1852 }
1853
1854 closest_intersection
1855 };
1856
1857 let intersection_t = project_point_onto_curve(trim_curve, intersection_point)?;
1859
1860 let left_termination = find_termination_in_direction(
1862 trim_spawn_seg,
1863 trim_curve,
1864 intersection_t,
1865 TrimDirection::Left,
1866 objects,
1867 default_unit,
1868 )?;
1869
1870 let right_termination = find_termination_in_direction(
1871 trim_spawn_seg,
1872 trim_curve,
1873 intersection_t,
1874 TrimDirection::Right,
1875 objects,
1876 default_unit,
1877 )?;
1878
1879 Ok(TrimTerminations {
1880 left_side: left_termination,
1881 right_side: right_termination,
1882 })
1883}
1884
1885fn find_termination_in_direction(
1938 trim_spawn_seg: &Object,
1939 trim_curve: CurveHandle,
1940 intersection_t: f64,
1941 direction: TrimDirection,
1942 objects: &[Object],
1943 default_unit: UnitLength,
1944) -> Result<TrimTermination, String> {
1945 let ObjectKind::Segment { segment } = &trim_spawn_seg.kind else {
1947 return Err("Trim spawn segment is not a segment".to_string());
1948 };
1949
1950 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1952 enum CandidateType {
1953 Intersection,
1954 Coincident,
1955 Endpoint,
1956 }
1957
1958 #[derive(Debug, Clone)]
1959 struct Candidate {
1960 t: f64,
1961 point: Coords2d,
1962 candidate_type: CandidateType,
1963 segment_id: Option<ObjectId>,
1964 point_id: Option<ObjectId>,
1965 }
1966
1967 let mut candidates: Vec<Candidate> = Vec::new();
1968
1969 match segment {
1971 Segment::Line(line) => {
1972 candidates.push(Candidate {
1973 t: 0.0,
1974 point: trim_curve.start,
1975 candidate_type: CandidateType::Endpoint,
1976 segment_id: None,
1977 point_id: Some(line.start),
1978 });
1979 candidates.push(Candidate {
1980 t: 1.0,
1981 point: trim_curve.end,
1982 candidate_type: CandidateType::Endpoint,
1983 segment_id: None,
1984 point_id: Some(line.end),
1985 });
1986 }
1987 Segment::Arc(arc) => {
1988 candidates.push(Candidate {
1990 t: 0.0,
1991 point: trim_curve.start,
1992 candidate_type: CandidateType::Endpoint,
1993 segment_id: None,
1994 point_id: Some(arc.start),
1995 });
1996 candidates.push(Candidate {
1997 t: 1.0,
1998 point: trim_curve.end,
1999 candidate_type: CandidateType::Endpoint,
2000 segment_id: None,
2001 point_id: Some(arc.end),
2002 });
2003 }
2004 Segment::Circle(_) => {
2005 }
2007 _ => {}
2008 }
2009
2010 let trim_spawn_seg_id = trim_spawn_seg.id;
2012
2013 for other_seg in objects.iter() {
2015 let other_id = other_seg.id;
2016 if other_id == trim_spawn_seg_id {
2017 continue;
2018 }
2019
2020 if let Ok(other_curve) = load_curve_handle(other_seg, objects, default_unit) {
2021 for intersection in curve_curve_intersections(trim_curve, other_curve, EPSILON_POINT_ON_SEGMENT) {
2022 let Ok(t) = project_point_onto_curve(trim_curve, intersection) else {
2023 continue;
2024 };
2025 candidates.push(Candidate {
2026 t,
2027 point: intersection,
2028 candidate_type: CandidateType::Intersection,
2029 segment_id: Some(other_id),
2030 point_id: None,
2031 });
2032 }
2033 }
2034
2035 for (other_point_id, other_point) in segment_endpoint_points(other_seg, objects, default_unit) {
2036 if !is_point_coincident_with_segment_native(other_point_id, trim_spawn_seg_id, objects) {
2037 continue;
2038 }
2039 if !curve_contains_point(trim_curve, other_point, EPSILON_POINT_ON_SEGMENT) {
2040 continue;
2041 }
2042 let Ok(t) = project_point_onto_curve(trim_curve, other_point) else {
2043 continue;
2044 };
2045 candidates.push(Candidate {
2046 t,
2047 point: other_point,
2048 candidate_type: CandidateType::Coincident,
2049 segment_id: Some(other_id),
2050 point_id: Some(other_point_id),
2051 });
2052 }
2053 }
2054
2055 let is_circle_segment = trim_curve.domain == CurveDomain::Closed;
2056
2057 let intersection_epsilon = EPSILON_POINT_ON_SEGMENT * 10.0; let direction_distance = |candidate_t: f64| -> f64 {
2061 if is_circle_segment {
2062 match direction {
2063 TrimDirection::Left => (intersection_t - candidate_t).rem_euclid(1.0),
2064 TrimDirection::Right => (candidate_t - intersection_t).rem_euclid(1.0),
2065 }
2066 } else {
2067 (candidate_t - intersection_t).abs()
2068 }
2069 };
2070 let filtered_candidates: Vec<Candidate> = candidates
2071 .into_iter()
2072 .filter(|candidate| {
2073 let dist_from_intersection = if is_circle_segment {
2074 let ccw = (candidate.t - intersection_t).rem_euclid(1.0);
2075 let cw = (intersection_t - candidate.t).rem_euclid(1.0);
2076 ccw.min(cw)
2077 } else {
2078 (candidate.t - intersection_t).abs()
2079 };
2080 if dist_from_intersection < intersection_epsilon {
2081 return false; }
2083
2084 if is_circle_segment {
2085 direction_distance(candidate.t) > intersection_epsilon
2086 } else {
2087 match direction {
2088 TrimDirection::Left => candidate.t < intersection_t,
2089 TrimDirection::Right => candidate.t > intersection_t,
2090 }
2091 }
2092 })
2093 .collect();
2094
2095 let mut sorted_candidates = filtered_candidates;
2098 sorted_candidates.sort_by(|a, b| {
2099 let dist_a = direction_distance(a.t);
2100 let dist_b = direction_distance(b.t);
2101 let dist_diff = dist_a - dist_b;
2102 if dist_diff.abs() > EPSILON_POINT_ON_SEGMENT {
2103 dist_diff.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal)
2104 } else {
2105 let type_priority = |candidate_type: CandidateType| -> i32 {
2107 match candidate_type {
2108 CandidateType::Coincident => 0,
2109 CandidateType::Intersection => 1,
2110 CandidateType::Endpoint => 2,
2111 }
2112 };
2113 type_priority(a.candidate_type).cmp(&type_priority(b.candidate_type))
2114 }
2115 });
2116
2117 let closest_candidate = match sorted_candidates.first() {
2119 Some(c) => c,
2120 None => {
2121 if is_circle_segment {
2122 return Err("No trim termination candidate found for circle".to_string());
2123 }
2124 let endpoint = match direction {
2126 TrimDirection::Left => trim_curve.start,
2127 TrimDirection::Right => trim_curve.end,
2128 };
2129 return Ok(TrimTermination::SegEndPoint {
2130 trim_termination_coords: endpoint,
2131 });
2132 }
2133 };
2134
2135 if !is_circle_segment
2139 && closest_candidate.candidate_type == CandidateType::Intersection
2140 && let Some(seg_id) = closest_candidate.segment_id
2141 {
2142 let intersecting_seg = objects.iter().find(|obj| obj.id == seg_id);
2143
2144 if let Some(intersecting_seg) = intersecting_seg {
2145 let endpoint_epsilon = EPSILON_POINT_ON_SEGMENT * 1000.0; let is_other_seg_endpoint = segment_endpoint_points(intersecting_seg, objects, default_unit)
2148 .into_iter()
2149 .any(|(_, endpoint)| {
2150 let dist_to_endpoint = ((closest_candidate.point.x - endpoint.x).powi(2)
2151 + (closest_candidate.point.y - endpoint.y).powi(2))
2152 .sqrt();
2153 dist_to_endpoint < endpoint_epsilon
2154 });
2155
2156 if is_other_seg_endpoint {
2159 let endpoint = match direction {
2160 TrimDirection::Left => trim_curve.start,
2161 TrimDirection::Right => trim_curve.end,
2162 };
2163 return Ok(TrimTermination::SegEndPoint {
2164 trim_termination_coords: endpoint,
2165 });
2166 }
2167 }
2168
2169 let endpoint_t = match direction {
2171 TrimDirection::Left => 0.0,
2172 TrimDirection::Right => 1.0,
2173 };
2174 let endpoint = match direction {
2175 TrimDirection::Left => trim_curve.start,
2176 TrimDirection::Right => trim_curve.end,
2177 };
2178 let dist_to_endpoint_param = (closest_candidate.t - endpoint_t).abs();
2179 let dist_to_endpoint_coords = ((closest_candidate.point.x - endpoint.x)
2180 * (closest_candidate.point.x - endpoint.x)
2181 + (closest_candidate.point.y - endpoint.y) * (closest_candidate.point.y - endpoint.y))
2182 .sqrt();
2183
2184 let is_at_endpoint =
2185 dist_to_endpoint_param < EPSILON_POINT_ON_SEGMENT || dist_to_endpoint_coords < EPSILON_POINT_ON_SEGMENT;
2186
2187 if is_at_endpoint {
2188 return Ok(TrimTermination::SegEndPoint {
2190 trim_termination_coords: endpoint,
2191 });
2192 }
2193 }
2194
2195 let endpoint_t_for_return = match direction {
2197 TrimDirection::Left => 0.0,
2198 TrimDirection::Right => 1.0,
2199 };
2200 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Intersection {
2201 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2202 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2203 let endpoint = match direction {
2206 TrimDirection::Left => trim_curve.start,
2207 TrimDirection::Right => trim_curve.end,
2208 };
2209 return Ok(TrimTermination::SegEndPoint {
2210 trim_termination_coords: endpoint,
2211 });
2212 }
2213 }
2214
2215 let endpoint = match direction {
2217 TrimDirection::Left => trim_curve.start,
2218 TrimDirection::Right => trim_curve.end,
2219 };
2220 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Endpoint {
2221 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2222 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2223 return Ok(TrimTermination::SegEndPoint {
2225 trim_termination_coords: endpoint,
2226 });
2227 }
2228 }
2229
2230 if closest_candidate.candidate_type == CandidateType::Coincident {
2232 Ok(TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2234 trim_termination_coords: closest_candidate.point,
2235 intersecting_seg_id: closest_candidate
2236 .segment_id
2237 .ok_or_else(|| "Missing segment_id for coincident".to_string())?,
2238 other_segment_point_id: closest_candidate
2239 .point_id
2240 .ok_or_else(|| "Missing point_id for coincident".to_string())?,
2241 })
2242 } else if closest_candidate.candidate_type == CandidateType::Intersection {
2243 Ok(TrimTermination::Intersection {
2244 trim_termination_coords: closest_candidate.point,
2245 intersecting_seg_id: closest_candidate
2246 .segment_id
2247 .ok_or_else(|| "Missing segment_id for intersection".to_string())?,
2248 })
2249 } else {
2250 if is_circle_segment {
2251 return Err("Circle trim termination unexpectedly resolved to endpoint".to_string());
2252 }
2253 Ok(TrimTermination::SegEndPoint {
2255 trim_termination_coords: closest_candidate.point,
2256 })
2257 }
2258}
2259
2260#[cfg(test)]
2271#[allow(dead_code)]
2272pub(crate) async fn execute_trim_loop<F, Fut>(
2273 points: &[Coords2d],
2274 default_unit: UnitLength,
2275 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2276 mut execute_operations: F,
2277) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>
2278where
2279 F: FnMut(Vec<TrimOperation>, crate::frontend::api::SceneGraphDelta) -> Fut,
2280 Fut: std::future::Future<
2281 Output = Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>,
2282 >,
2283{
2284 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2286 let points = normalized_points.as_slice();
2287
2288 let mut start_index = 0;
2289 let max_iterations = 1000;
2290 let mut iteration_count = 0;
2291 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2292 crate::frontend::api::SourceDelta { text: String::new() },
2293 initial_scene_graph_delta.clone(),
2294 ));
2295 let mut invalidates_ids = false;
2296 let mut current_scene_graph_delta = initial_scene_graph_delta;
2297 let circle_delete_fallback_strategy =
2298 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2299 if !error.contains("No trim termination candidate found for circle") {
2300 return None;
2301 }
2302 let is_circle = scene_objects
2303 .iter()
2304 .find(|obj| obj.id == segment_id)
2305 .is_some_and(|obj| {
2306 matches!(
2307 obj.kind,
2308 ObjectKind::Segment {
2309 segment: Segment::Circle(_)
2310 }
2311 )
2312 });
2313 if is_circle {
2314 Some(vec![TrimOperation::SimpleTrim {
2315 segment_to_trim_id: segment_id,
2316 }])
2317 } else {
2318 None
2319 }
2320 };
2321
2322 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2323 iteration_count += 1;
2324
2325 let next_trim_spawn = get_next_trim_spawn(
2327 points,
2328 start_index,
2329 ¤t_scene_graph_delta.new_graph.objects,
2330 default_unit,
2331 );
2332
2333 match &next_trim_spawn {
2334 TrimItem::None { next_index } => {
2335 let old_start_index = start_index;
2336 start_index = *next_index;
2337
2338 if start_index <= old_start_index {
2340 start_index = old_start_index + 1;
2341 }
2342
2343 if start_index >= points.len().saturating_sub(1) {
2345 break;
2346 }
2347 continue;
2348 }
2349 TrimItem::Spawn {
2350 trim_spawn_seg_id,
2351 trim_spawn_coords,
2352 next_index,
2353 ..
2354 } => {
2355 let terminations = match get_trim_spawn_terminations(
2357 *trim_spawn_seg_id,
2358 points,
2359 ¤t_scene_graph_delta.new_graph.objects,
2360 default_unit,
2361 ) {
2362 Ok(terms) => terms,
2363 Err(e) => {
2364 crate::logln!("Error getting trim spawn terminations: {}", e);
2365 if let Some(strategy) = circle_delete_fallback_strategy(
2366 &e,
2367 *trim_spawn_seg_id,
2368 ¤t_scene_graph_delta.new_graph.objects,
2369 ) {
2370 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2371 Ok((source_delta, scene_graph_delta)) => {
2372 last_result = Some((source_delta, scene_graph_delta.clone()));
2373 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2374 current_scene_graph_delta = scene_graph_delta;
2375 }
2376 Err(exec_err) => {
2377 crate::logln!(
2378 "Error executing circle-delete fallback trim operation: {}",
2379 exec_err
2380 );
2381 }
2382 }
2383
2384 let old_start_index = start_index;
2385 start_index = *next_index;
2386 if start_index <= old_start_index {
2387 start_index = old_start_index + 1;
2388 }
2389 continue;
2390 }
2391
2392 let old_start_index = start_index;
2393 start_index = *next_index;
2394 if start_index <= old_start_index {
2395 start_index = old_start_index + 1;
2396 }
2397 continue;
2398 }
2399 };
2400
2401 let trim_spawn_segment = current_scene_graph_delta
2403 .new_graph
2404 .objects
2405 .iter()
2406 .find(|obj| obj.id == *trim_spawn_seg_id)
2407 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2408
2409 let plan = match build_trim_plan(
2410 *trim_spawn_seg_id,
2411 *trim_spawn_coords,
2412 trim_spawn_segment,
2413 &terminations.left_side,
2414 &terminations.right_side,
2415 ¤t_scene_graph_delta.new_graph.objects,
2416 default_unit,
2417 ) {
2418 Ok(plan) => plan,
2419 Err(e) => {
2420 crate::logln!("Error determining trim strategy: {}", e);
2421 let old_start_index = start_index;
2422 start_index = *next_index;
2423 if start_index <= old_start_index {
2424 start_index = old_start_index + 1;
2425 }
2426 continue;
2427 }
2428 };
2429 let strategy = lower_trim_plan(&plan);
2430
2431 let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2434
2435 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2437 Ok((source_delta, scene_graph_delta)) => {
2438 last_result = Some((source_delta, scene_graph_delta.clone()));
2439 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2440 current_scene_graph_delta = scene_graph_delta;
2441 }
2442 Err(e) => {
2443 crate::logln!("Error executing trim operations: {}", e);
2444 }
2446 }
2447
2448 let old_start_index = start_index;
2450 start_index = *next_index;
2451
2452 if start_index <= old_start_index && !geometry_was_modified {
2454 start_index = old_start_index + 1;
2455 }
2456 }
2457 }
2458 }
2459
2460 if iteration_count >= max_iterations {
2461 return Err(format!("Reached max iterations ({})", max_iterations));
2462 }
2463
2464 last_result.ok_or_else(|| "No trim operations were executed".to_string())
2466}
2467
2468#[cfg(all(feature = "artifact-graph", test))]
2470#[derive(Debug, Clone)]
2471pub struct TrimFlowResult {
2472 pub kcl_code: String,
2473 pub invalidates_ids: bool,
2474}
2475
2476#[cfg(all(not(target_arch = "wasm32"), feature = "artifact-graph", test))]
2492pub(crate) async fn execute_trim_flow(
2493 kcl_code: &str,
2494 trim_points: &[Coords2d],
2495 sketch_id: ObjectId,
2496) -> Result<TrimFlowResult, String> {
2497 use crate::ExecutorContext;
2498 use crate::Program;
2499 use crate::execution::MockConfig;
2500 use crate::frontend::FrontendState;
2501 use crate::frontend::api::Version;
2502
2503 let parse_result = Program::parse(kcl_code).map_err(|e| format!("Failed to parse KCL: {}", e))?;
2505 let (program_opt, errors) = parse_result;
2506 if !errors.is_empty() {
2507 return Err(format!("Failed to parse KCL: {:?}", errors));
2508 }
2509 let program = program_opt.ok_or_else(|| "No AST produced".to_string())?;
2510
2511 let mock_ctx = ExecutorContext::new_mock(None).await;
2512
2513 let result = async {
2515 let mut frontend = FrontendState::new();
2516
2517 frontend.program = program.clone();
2519
2520 let exec_outcome = mock_ctx
2521 .run_mock(&program, &MockConfig::default())
2522 .await
2523 .map_err(|e| format!("Failed to execute program: {}", e.error.message()))?;
2524
2525 let exec_outcome = frontend.update_state_after_exec(exec_outcome, false);
2526 #[allow(unused_mut)] let mut initial_scene_graph = frontend.scene_graph.clone();
2528
2529 #[cfg(feature = "artifact-graph")]
2532 if initial_scene_graph.objects.is_empty() && !exec_outcome.scene_objects.is_empty() {
2533 initial_scene_graph.objects = exec_outcome.scene_objects.clone();
2534 }
2535
2536 let actual_sketch_id = if let Some(sketch_mode) = initial_scene_graph.sketch_mode {
2539 sketch_mode
2540 } else {
2541 initial_scene_graph
2543 .objects
2544 .iter()
2545 .find(|obj| matches!(obj.kind, crate::frontend::api::ObjectKind::Sketch { .. }))
2546 .map(|obj| obj.id)
2547 .unwrap_or(sketch_id) };
2549
2550 let version = Version(0);
2551 let initial_scene_graph_delta = crate::frontend::api::SceneGraphDelta {
2552 new_graph: initial_scene_graph,
2553 new_objects: vec![],
2554 invalidates_ids: false,
2555 exec_outcome,
2556 };
2557
2558 let (source_delta, scene_graph_delta) = execute_trim_loop_with_context(
2563 trim_points,
2564 initial_scene_graph_delta,
2565 &mut frontend,
2566 &mock_ctx,
2567 version,
2568 actual_sketch_id,
2569 )
2570 .await?;
2571
2572 if source_delta.text.is_empty() {
2575 return Err("No trim operations were executed - source delta is empty".to_string());
2576 }
2577
2578 Ok(TrimFlowResult {
2579 kcl_code: source_delta.text,
2580 invalidates_ids: scene_graph_delta.invalidates_ids,
2581 })
2582 }
2583 .await;
2584
2585 mock_ctx.close().await;
2587
2588 result
2589}
2590
2591pub async fn execute_trim_loop_with_context(
2597 points: &[Coords2d],
2598 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2599 frontend: &mut crate::frontend::FrontendState,
2600 ctx: &crate::ExecutorContext,
2601 version: crate::frontend::api::Version,
2602 sketch_id: ObjectId,
2603) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
2604 let default_unit = frontend.default_length_unit();
2606 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2607
2608 let mut current_scene_graph_delta = initial_scene_graph_delta.clone();
2611 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2612 crate::frontend::api::SourceDelta { text: String::new() },
2613 initial_scene_graph_delta.clone(),
2614 ));
2615 let mut invalidates_ids = false;
2616 let mut start_index = 0;
2617 let max_iterations = 1000;
2618 let mut iteration_count = 0;
2619 let circle_delete_fallback_strategy =
2620 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2621 if !error.contains("No trim termination candidate found for circle") {
2622 return None;
2623 }
2624 let is_circle = scene_objects
2625 .iter()
2626 .find(|obj| obj.id == segment_id)
2627 .is_some_and(|obj| {
2628 matches!(
2629 obj.kind,
2630 ObjectKind::Segment {
2631 segment: Segment::Circle(_)
2632 }
2633 )
2634 });
2635 if is_circle {
2636 Some(vec![TrimOperation::SimpleTrim {
2637 segment_to_trim_id: segment_id,
2638 }])
2639 } else {
2640 None
2641 }
2642 };
2643
2644 let points = normalized_points.as_slice();
2645
2646 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2647 iteration_count += 1;
2648
2649 let next_trim_spawn = get_next_trim_spawn(
2651 points,
2652 start_index,
2653 ¤t_scene_graph_delta.new_graph.objects,
2654 default_unit,
2655 );
2656
2657 match &next_trim_spawn {
2658 TrimItem::None { next_index } => {
2659 let old_start_index = start_index;
2660 start_index = *next_index;
2661 if start_index <= old_start_index {
2662 start_index = old_start_index + 1;
2663 }
2664 if start_index >= points.len().saturating_sub(1) {
2665 break;
2666 }
2667 continue;
2668 }
2669 TrimItem::Spawn {
2670 trim_spawn_seg_id,
2671 trim_spawn_coords,
2672 next_index,
2673 ..
2674 } => {
2675 let terminations = match get_trim_spawn_terminations(
2677 *trim_spawn_seg_id,
2678 points,
2679 ¤t_scene_graph_delta.new_graph.objects,
2680 default_unit,
2681 ) {
2682 Ok(terms) => terms,
2683 Err(e) => {
2684 crate::logln!("Error getting trim spawn terminations: {}", e);
2685 if let Some(strategy) = circle_delete_fallback_strategy(
2686 &e,
2687 *trim_spawn_seg_id,
2688 ¤t_scene_graph_delta.new_graph.objects,
2689 ) {
2690 match execute_trim_operations_simple(
2691 strategy.clone(),
2692 ¤t_scene_graph_delta,
2693 frontend,
2694 ctx,
2695 version,
2696 sketch_id,
2697 )
2698 .await
2699 {
2700 Ok((source_delta, scene_graph_delta)) => {
2701 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2702 last_result = Some((source_delta, scene_graph_delta.clone()));
2703 current_scene_graph_delta = scene_graph_delta;
2704 }
2705 Err(exec_err) => {
2706 crate::logln!(
2707 "Error executing circle-delete fallback trim operation: {}",
2708 exec_err
2709 );
2710 }
2711 }
2712
2713 let old_start_index = start_index;
2714 start_index = *next_index;
2715 if start_index <= old_start_index {
2716 start_index = old_start_index + 1;
2717 }
2718 continue;
2719 }
2720
2721 let old_start_index = start_index;
2722 start_index = *next_index;
2723 if start_index <= old_start_index {
2724 start_index = old_start_index + 1;
2725 }
2726 continue;
2727 }
2728 };
2729
2730 let trim_spawn_segment = current_scene_graph_delta
2732 .new_graph
2733 .objects
2734 .iter()
2735 .find(|obj| obj.id == *trim_spawn_seg_id)
2736 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2737
2738 let plan = match build_trim_plan(
2739 *trim_spawn_seg_id,
2740 *trim_spawn_coords,
2741 trim_spawn_segment,
2742 &terminations.left_side,
2743 &terminations.right_side,
2744 ¤t_scene_graph_delta.new_graph.objects,
2745 default_unit,
2746 ) {
2747 Ok(plan) => plan,
2748 Err(e) => {
2749 crate::logln!("Error determining trim strategy: {}", e);
2750 let old_start_index = start_index;
2751 start_index = *next_index;
2752 if start_index <= old_start_index {
2753 start_index = old_start_index + 1;
2754 }
2755 continue;
2756 }
2757 };
2758 let strategy = lower_trim_plan(&plan);
2759
2760 let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2763
2764 match execute_trim_operations_simple(
2766 strategy.clone(),
2767 ¤t_scene_graph_delta,
2768 frontend,
2769 ctx,
2770 version,
2771 sketch_id,
2772 )
2773 .await
2774 {
2775 Ok((source_delta, scene_graph_delta)) => {
2776 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2777 last_result = Some((source_delta, scene_graph_delta.clone()));
2778 current_scene_graph_delta = scene_graph_delta;
2779 }
2780 Err(e) => {
2781 crate::logln!("Error executing trim operations: {}", e);
2782 }
2783 }
2784
2785 let old_start_index = start_index;
2787 start_index = *next_index;
2788 if start_index <= old_start_index && !geometry_was_modified {
2789 start_index = old_start_index + 1;
2790 }
2791 }
2792 }
2793 }
2794
2795 if iteration_count >= max_iterations {
2796 return Err(format!("Reached max iterations ({})", max_iterations));
2797 }
2798
2799 let (source_delta, mut scene_graph_delta) =
2800 last_result.ok_or_else(|| "No trim operations were executed".to_string())?;
2801 scene_graph_delta.invalidates_ids = invalidates_ids;
2803 Ok((source_delta, scene_graph_delta))
2804}
2805
2806pub(crate) fn build_trim_plan(
2866 trim_spawn_id: ObjectId,
2867 trim_spawn_coords: Coords2d,
2868 trim_spawn_segment: &Object,
2869 left_side: &TrimTermination,
2870 right_side: &TrimTermination,
2871 objects: &[Object],
2872 default_unit: UnitLength,
2873) -> Result<TrimPlan, String> {
2874 if matches!(left_side, TrimTermination::SegEndPoint { .. })
2876 && matches!(right_side, TrimTermination::SegEndPoint { .. })
2877 {
2878 return Ok(TrimPlan::DeleteSegment {
2879 segment_id: trim_spawn_id,
2880 });
2881 }
2882
2883 let is_intersect_or_coincident = |side: &TrimTermination| -> bool {
2885 matches!(
2886 side,
2887 TrimTermination::Intersection { .. }
2888 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
2889 )
2890 };
2891
2892 let left_side_needs_tail_cut = is_intersect_or_coincident(left_side) && !is_intersect_or_coincident(right_side);
2893 let right_side_needs_tail_cut = is_intersect_or_coincident(right_side) && !is_intersect_or_coincident(left_side);
2894
2895 let ObjectKind::Segment { segment } = &trim_spawn_segment.kind else {
2897 return Err("Trim spawn segment is not a segment".to_string());
2898 };
2899
2900 let (_segment_type, ctor) = match segment {
2901 Segment::Line(line) => ("Line", &line.ctor),
2902 Segment::Arc(arc) => ("Arc", &arc.ctor),
2903 Segment::Circle(circle) => ("Circle", &circle.ctor),
2904 _ => {
2905 return Err("Trim spawn segment is not a Line, Arc, or Circle".to_string());
2906 }
2907 };
2908
2909 let units = match ctor {
2911 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
2912 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2913 _ => NumericSuffix::Mm,
2914 },
2915 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
2916 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2917 _ => NumericSuffix::Mm,
2918 },
2919 SegmentCtor::Circle(circle_ctor) => match &circle_ctor.start.x {
2920 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2921 _ => NumericSuffix::Mm,
2922 },
2923 _ => NumericSuffix::Mm,
2924 };
2925
2926 let find_distance_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
2928 let mut constraint_ids = Vec::new();
2929 for obj in objects {
2930 let ObjectKind::Constraint { constraint } = &obj.kind else {
2931 continue;
2932 };
2933
2934 let Constraint::Distance(distance) = constraint else {
2935 continue;
2936 };
2937
2938 let points_owned_by_segment: Vec<bool> = distance
2944 .point_ids()
2945 .map(|point_id| {
2946 if let Some(point_obj) = objects.iter().find(|o| o.id == point_id)
2947 && let ObjectKind::Segment { segment } = &point_obj.kind
2948 && let Segment::Point(point) = segment
2949 && let Some(owner_id) = point.owner
2950 {
2951 return owner_id == segment_id;
2952 }
2953 false
2954 })
2955 .collect();
2956
2957 if points_owned_by_segment.len() == 2 && points_owned_by_segment.iter().all(|&owned| owned) {
2959 constraint_ids.push(obj.id);
2960 }
2961 }
2962 constraint_ids
2963 };
2964
2965 let find_existing_point_segment_coincident =
2967 |trim_seg_id: ObjectId, intersecting_seg_id: ObjectId| -> CoincidentData {
2968 let lookup_by_point_id = |point_id: ObjectId| -> Option<CoincidentData> {
2970 for obj in objects {
2971 let ObjectKind::Constraint { constraint } = &obj.kind else {
2972 continue;
2973 };
2974
2975 let Constraint::Coincident(coincident) = constraint else {
2976 continue;
2977 };
2978
2979 let involves_trim_seg = coincident.segment_ids().any(|id| id == trim_seg_id || id == point_id);
2980 let involves_point = coincident.contains_segment(point_id);
2981
2982 if involves_trim_seg && involves_point {
2983 return Some(CoincidentData {
2984 intersecting_seg_id,
2985 intersecting_endpoint_point_id: Some(point_id),
2986 existing_point_segment_constraint_id: Some(obj.id),
2987 });
2988 }
2989 }
2990 None
2991 };
2992
2993 let trim_seg = objects.iter().find(|obj| obj.id == trim_seg_id);
2995
2996 let mut trim_endpoint_ids: Vec<ObjectId> = Vec::new();
2997 if let Some(seg) = trim_seg
2998 && let ObjectKind::Segment { segment } = &seg.kind
2999 {
3000 match segment {
3001 Segment::Line(line) => {
3002 trim_endpoint_ids.push(line.start);
3003 trim_endpoint_ids.push(line.end);
3004 }
3005 Segment::Arc(arc) => {
3006 trim_endpoint_ids.push(arc.start);
3007 trim_endpoint_ids.push(arc.end);
3008 }
3009 _ => {}
3010 }
3011 }
3012
3013 let intersecting_obj = objects.iter().find(|obj| obj.id == intersecting_seg_id);
3014
3015 if let Some(obj) = intersecting_obj
3016 && let ObjectKind::Segment { segment } = &obj.kind
3017 && let Segment::Point(_) = segment
3018 && let Some(found) = lookup_by_point_id(intersecting_seg_id)
3019 {
3020 return found;
3021 }
3022
3023 let mut intersecting_endpoint_ids: Vec<ObjectId> = Vec::new();
3025 if let Some(obj) = intersecting_obj
3026 && let ObjectKind::Segment { segment } = &obj.kind
3027 {
3028 match segment {
3029 Segment::Line(line) => {
3030 intersecting_endpoint_ids.push(line.start);
3031 intersecting_endpoint_ids.push(line.end);
3032 }
3033 Segment::Arc(arc) => {
3034 intersecting_endpoint_ids.push(arc.start);
3035 intersecting_endpoint_ids.push(arc.end);
3036 }
3037 _ => {}
3038 }
3039 }
3040
3041 intersecting_endpoint_ids.push(intersecting_seg_id);
3043
3044 for obj in objects {
3046 let ObjectKind::Constraint { constraint } = &obj.kind else {
3047 continue;
3048 };
3049
3050 let Constraint::Coincident(coincident) = constraint else {
3051 continue;
3052 };
3053
3054 let constraint_segment_ids: Vec<ObjectId> = coincident.get_segments();
3055
3056 let involves_trim_seg = constraint_segment_ids.contains(&trim_seg_id)
3058 || trim_endpoint_ids.iter().any(|&id| constraint_segment_ids.contains(&id));
3059
3060 if !involves_trim_seg {
3061 continue;
3062 }
3063
3064 if let Some(&intersecting_endpoint_id) = intersecting_endpoint_ids
3066 .iter()
3067 .find(|&&id| constraint_segment_ids.contains(&id))
3068 {
3069 return CoincidentData {
3070 intersecting_seg_id,
3071 intersecting_endpoint_point_id: Some(intersecting_endpoint_id),
3072 existing_point_segment_constraint_id: Some(obj.id),
3073 };
3074 }
3075 }
3076
3077 CoincidentData {
3079 intersecting_seg_id,
3080 intersecting_endpoint_point_id: None,
3081 existing_point_segment_constraint_id: None,
3082 }
3083 };
3084
3085 let find_point_segment_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<serde_json::Value> {
3087 let mut constraints: Vec<serde_json::Value> = Vec::new();
3088 for obj in objects {
3089 let ObjectKind::Constraint { constraint } = &obj.kind else {
3090 continue;
3091 };
3092
3093 let Constraint::Coincident(coincident) = constraint else {
3094 continue;
3095 };
3096
3097 if !coincident.contains_segment(endpoint_point_id) {
3099 continue;
3100 }
3101
3102 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3104
3105 if let Some(other_id) = other_segment_id
3106 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3107 {
3108 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3110 constraints.push(serde_json::json!({
3111 "constraintId": obj.id.0,
3112 "segmentOrPointId": other_id.0,
3113 }));
3114 }
3115 }
3116 }
3117 constraints
3118 };
3119
3120 let find_point_point_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3123 let mut constraint_ids = Vec::new();
3124 for obj in objects {
3125 let ObjectKind::Constraint { constraint } = &obj.kind else {
3126 continue;
3127 };
3128
3129 let Constraint::Coincident(coincident) = constraint else {
3130 continue;
3131 };
3132
3133 if !coincident.contains_segment(endpoint_point_id) {
3135 continue;
3136 }
3137
3138 let is_point_point = coincident.segment_ids().all(|seg_id| {
3140 if let Some(seg_obj) = objects.iter().find(|o| o.id == seg_id) {
3141 matches!(&seg_obj.kind, ObjectKind::Segment { segment } if matches!(segment, Segment::Point(_)))
3142 } else {
3143 false
3144 }
3145 });
3146
3147 if is_point_point {
3148 constraint_ids.push(obj.id);
3149 }
3150 }
3151 constraint_ids
3152 };
3153
3154 let find_point_segment_coincident_constraint_ids = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3157 let mut constraint_ids = Vec::new();
3158 for obj in objects {
3159 let ObjectKind::Constraint { constraint } = &obj.kind else {
3160 continue;
3161 };
3162
3163 let Constraint::Coincident(coincident) = constraint else {
3164 continue;
3165 };
3166
3167 if !coincident.contains_segment(endpoint_point_id) {
3169 continue;
3170 }
3171
3172 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3174
3175 if let Some(other_id) = other_segment_id
3176 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3177 {
3178 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3180 constraint_ids.push(obj.id);
3181 }
3182 }
3183 }
3184 constraint_ids
3185 };
3186
3187 if left_side_needs_tail_cut || right_side_needs_tail_cut {
3189 let side = if left_side_needs_tail_cut {
3190 left_side
3191 } else {
3192 right_side
3193 };
3194
3195 let intersection_coords = match side {
3196 TrimTermination::Intersection {
3197 trim_termination_coords,
3198 ..
3199 }
3200 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3201 trim_termination_coords,
3202 ..
3203 } => *trim_termination_coords,
3204 TrimTermination::SegEndPoint { .. } => {
3205 return Err("Logic error: side should not be segEndPoint here".to_string());
3206 }
3207 };
3208
3209 let endpoint_to_change = if left_side_needs_tail_cut {
3210 EndpointChanged::End
3211 } else {
3212 EndpointChanged::Start
3213 };
3214
3215 let intersecting_seg_id = match side {
3216 TrimTermination::Intersection {
3217 intersecting_seg_id, ..
3218 }
3219 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3220 intersecting_seg_id, ..
3221 } => *intersecting_seg_id,
3222 TrimTermination::SegEndPoint { .. } => {
3223 return Err("Logic error".to_string());
3224 }
3225 };
3226
3227 let mut coincident_data = if matches!(
3228 side,
3229 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3230 ) {
3231 let point_id = match side {
3232 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3233 other_segment_point_id, ..
3234 } => *other_segment_point_id,
3235 _ => return Err("Logic error".to_string()),
3236 };
3237 let mut data = find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id);
3238 data.intersecting_endpoint_point_id = Some(point_id);
3239 data
3240 } else {
3241 find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id)
3242 };
3243
3244 let trim_seg = objects.iter().find(|obj| obj.id == trim_spawn_id);
3246
3247 let endpoint_point_id = if let Some(seg) = trim_seg {
3248 let ObjectKind::Segment { segment } = &seg.kind else {
3249 return Err("Trim spawn segment is not a segment".to_string());
3250 };
3251 match segment {
3252 Segment::Line(line) => {
3253 if endpoint_to_change == EndpointChanged::Start {
3254 Some(line.start)
3255 } else {
3256 Some(line.end)
3257 }
3258 }
3259 Segment::Arc(arc) => {
3260 if endpoint_to_change == EndpointChanged::Start {
3261 Some(arc.start)
3262 } else {
3263 Some(arc.end)
3264 }
3265 }
3266 _ => None,
3267 }
3268 } else {
3269 None
3270 };
3271
3272 if let (Some(endpoint_id), Some(existing_constraint_id)) =
3273 (endpoint_point_id, coincident_data.existing_point_segment_constraint_id)
3274 {
3275 let constraint_involves_trimmed_endpoint = objects
3276 .iter()
3277 .find(|obj| obj.id == existing_constraint_id)
3278 .and_then(|obj| match &obj.kind {
3279 ObjectKind::Constraint {
3280 constraint: Constraint::Coincident(coincident),
3281 } => Some(coincident.contains_segment(endpoint_id) || coincident.contains_segment(trim_spawn_id)),
3282 _ => None,
3283 })
3284 .unwrap_or(false);
3285
3286 if !constraint_involves_trimmed_endpoint {
3287 coincident_data.existing_point_segment_constraint_id = None;
3288 coincident_data.intersecting_endpoint_point_id = None;
3289 }
3290 }
3291
3292 let coincident_end_constraint_to_delete_ids = if let Some(point_id) = endpoint_point_id {
3294 let mut constraint_ids = find_point_point_coincident_constraints(point_id);
3295 constraint_ids.extend(find_point_segment_coincident_constraint_ids(point_id));
3297 constraint_ids
3298 } else {
3299 Vec::new()
3300 };
3301
3302 let point_axis_constraint_ids_to_delete = if let Some(point_id) = endpoint_point_id {
3303 objects
3304 .iter()
3305 .filter_map(|obj| {
3306 let ObjectKind::Constraint { constraint } = &obj.kind else {
3307 return None;
3308 };
3309
3310 point_axis_constraint_references_point(constraint, point_id).then_some(obj.id)
3311 })
3312 .collect::<Vec<_>>()
3313 } else {
3314 Vec::new()
3315 };
3316
3317 let new_ctor = match ctor {
3319 SegmentCtor::Line(line_ctor) => {
3320 let new_point = crate::frontend::sketch::Point2d {
3322 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3323 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3324 };
3325 if endpoint_to_change == EndpointChanged::Start {
3326 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3327 start: new_point,
3328 end: line_ctor.end.clone(),
3329 construction: line_ctor.construction,
3330 })
3331 } else {
3332 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3333 start: line_ctor.start.clone(),
3334 end: new_point,
3335 construction: line_ctor.construction,
3336 })
3337 }
3338 }
3339 SegmentCtor::Arc(arc_ctor) => {
3340 let new_point = crate::frontend::sketch::Point2d {
3342 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3343 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3344 };
3345 if endpoint_to_change == EndpointChanged::Start {
3346 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3347 start: new_point,
3348 end: arc_ctor.end.clone(),
3349 center: arc_ctor.center.clone(),
3350 construction: arc_ctor.construction,
3351 })
3352 } else {
3353 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3354 start: arc_ctor.start.clone(),
3355 end: new_point,
3356 center: arc_ctor.center.clone(),
3357 construction: arc_ctor.construction,
3358 })
3359 }
3360 }
3361 _ => {
3362 return Err("Unsupported segment type for edit".to_string());
3363 }
3364 };
3365
3366 let mut all_constraint_ids_to_delete: Vec<ObjectId> = Vec::new();
3368 if let Some(constraint_id) = coincident_data.existing_point_segment_constraint_id {
3369 all_constraint_ids_to_delete.push(constraint_id);
3370 }
3371 all_constraint_ids_to_delete.extend(coincident_end_constraint_to_delete_ids);
3372 all_constraint_ids_to_delete.extend(point_axis_constraint_ids_to_delete);
3373
3374 let distance_constraint_ids = find_distance_constraints_for_segment(trim_spawn_id);
3377 all_constraint_ids_to_delete.extend(distance_constraint_ids);
3378
3379 return Ok(TrimPlan::TailCut {
3380 segment_id: trim_spawn_id,
3381 endpoint_changed: endpoint_to_change,
3382 ctor: new_ctor,
3383 segment_or_point_to_make_coincident_to: intersecting_seg_id,
3384 intersecting_endpoint_point_id: coincident_data.intersecting_endpoint_point_id,
3385 constraint_ids_to_delete: all_constraint_ids_to_delete,
3386 });
3387 }
3388
3389 if matches!(segment, Segment::Circle(_)) {
3392 let left_side_intersects = is_intersect_or_coincident(left_side);
3393 let right_side_intersects = is_intersect_or_coincident(right_side);
3394 if !(left_side_intersects && right_side_intersects) {
3395 return Err(format!(
3396 "Unsupported circle trim termination combination: left={:?} right={:?}",
3397 left_side, right_side
3398 ));
3399 }
3400
3401 let left_trim_coords = match left_side {
3402 TrimTermination::SegEndPoint {
3403 trim_termination_coords,
3404 }
3405 | TrimTermination::Intersection {
3406 trim_termination_coords,
3407 ..
3408 }
3409 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3410 trim_termination_coords,
3411 ..
3412 } => *trim_termination_coords,
3413 };
3414 let right_trim_coords = match right_side {
3415 TrimTermination::SegEndPoint {
3416 trim_termination_coords,
3417 }
3418 | TrimTermination::Intersection {
3419 trim_termination_coords,
3420 ..
3421 }
3422 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3423 trim_termination_coords,
3424 ..
3425 } => *trim_termination_coords,
3426 };
3427
3428 let trim_points_coincident = ((left_trim_coords.x - right_trim_coords.x)
3431 * (left_trim_coords.x - right_trim_coords.x)
3432 + (left_trim_coords.y - right_trim_coords.y) * (left_trim_coords.y - right_trim_coords.y))
3433 .sqrt()
3434 <= EPSILON_POINT_ON_SEGMENT * 10.0;
3435 if trim_points_coincident {
3436 return Ok(TrimPlan::DeleteSegment {
3437 segment_id: trim_spawn_id,
3438 });
3439 }
3440
3441 let circle_center_coords =
3442 get_position_coords_from_circle(trim_spawn_segment, CirclePoint::Center, objects, default_unit)
3443 .ok_or_else(|| {
3444 format!(
3445 "Could not get center coordinates for circle segment {}",
3446 trim_spawn_id.0
3447 )
3448 })?;
3449
3450 let spawn_on_left_to_right = is_point_on_arc(
3452 trim_spawn_coords,
3453 circle_center_coords,
3454 left_trim_coords,
3455 right_trim_coords,
3456 EPSILON_POINT_ON_SEGMENT,
3457 );
3458 let (arc_start_coords, arc_end_coords, arc_start_termination, arc_end_termination) = if spawn_on_left_to_right {
3459 (
3460 right_trim_coords,
3461 left_trim_coords,
3462 Box::new(right_side.clone()),
3463 Box::new(left_side.clone()),
3464 )
3465 } else {
3466 (
3467 left_trim_coords,
3468 right_trim_coords,
3469 Box::new(left_side.clone()),
3470 Box::new(right_side.clone()),
3471 )
3472 };
3473
3474 return Ok(TrimPlan::ReplaceCircleWithArc {
3475 circle_id: trim_spawn_id,
3476 arc_start_coords,
3477 arc_end_coords,
3478 arc_start_termination,
3479 arc_end_termination,
3480 });
3481 }
3482
3483 let left_side_intersects = is_intersect_or_coincident(left_side);
3485 let right_side_intersects = is_intersect_or_coincident(right_side);
3486
3487 if left_side_intersects && right_side_intersects {
3488 let left_intersecting_seg_id = match left_side {
3491 TrimTermination::Intersection {
3492 intersecting_seg_id, ..
3493 }
3494 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3495 intersecting_seg_id, ..
3496 } => *intersecting_seg_id,
3497 TrimTermination::SegEndPoint { .. } => {
3498 return Err("Logic error: left side should not be segEndPoint".to_string());
3499 }
3500 };
3501
3502 let right_intersecting_seg_id = match right_side {
3503 TrimTermination::Intersection {
3504 intersecting_seg_id, ..
3505 }
3506 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3507 intersecting_seg_id, ..
3508 } => *intersecting_seg_id,
3509 TrimTermination::SegEndPoint { .. } => {
3510 return Err("Logic error: right side should not be segEndPoint".to_string());
3511 }
3512 };
3513
3514 let left_coincident_data = if matches!(
3515 left_side,
3516 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3517 ) {
3518 let point_id = match left_side {
3519 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3520 other_segment_point_id, ..
3521 } => *other_segment_point_id,
3522 _ => return Err("Logic error".to_string()),
3523 };
3524 let mut data = find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id);
3525 data.intersecting_endpoint_point_id = Some(point_id);
3526 data
3527 } else {
3528 find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id)
3529 };
3530
3531 let right_coincident_data = if matches!(
3532 right_side,
3533 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3534 ) {
3535 let point_id = match right_side {
3536 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3537 other_segment_point_id, ..
3538 } => *other_segment_point_id,
3539 _ => return Err("Logic error".to_string()),
3540 };
3541 let mut data = find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id);
3542 data.intersecting_endpoint_point_id = Some(point_id);
3543 data
3544 } else {
3545 find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id)
3546 };
3547
3548 let (original_start_point_id, original_end_point_id) = match segment {
3550 Segment::Line(line) => (Some(line.start), Some(line.end)),
3551 Segment::Arc(arc) => (Some(arc.start), Some(arc.end)),
3552 _ => (None, None),
3553 };
3554
3555 let original_end_point_coords = match segment {
3557 Segment::Line(_) => {
3558 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
3559 }
3560 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
3561 _ => None,
3562 };
3563
3564 let Some(original_end_coords) = original_end_point_coords else {
3565 return Err(
3566 "Could not get original end point coordinates before editing - this is required for split trim"
3567 .to_string(),
3568 );
3569 };
3570
3571 let left_trim_coords = match left_side {
3573 TrimTermination::SegEndPoint {
3574 trim_termination_coords,
3575 }
3576 | TrimTermination::Intersection {
3577 trim_termination_coords,
3578 ..
3579 }
3580 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3581 trim_termination_coords,
3582 ..
3583 } => *trim_termination_coords,
3584 };
3585
3586 let right_trim_coords = match right_side {
3587 TrimTermination::SegEndPoint {
3588 trim_termination_coords,
3589 }
3590 | TrimTermination::Intersection {
3591 trim_termination_coords,
3592 ..
3593 }
3594 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3595 trim_termination_coords,
3596 ..
3597 } => *trim_termination_coords,
3598 };
3599
3600 let dist_to_original_end = ((right_trim_coords.x - original_end_coords.x)
3602 * (right_trim_coords.x - original_end_coords.x)
3603 + (right_trim_coords.y - original_end_coords.y) * (right_trim_coords.y - original_end_coords.y))
3604 .sqrt();
3605 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3606 return Err(
3607 "Split point is at original end point - this should be handled as cutTail, not split".to_string(),
3608 );
3609 }
3610
3611 let mut constraints_to_migrate: Vec<ConstraintToMigrate> = Vec::new();
3614 let mut constraints_to_delete_set: IndexSet<ObjectId> = IndexSet::new();
3615
3616 if let Some(constraint_id) = left_coincident_data.existing_point_segment_constraint_id {
3618 constraints_to_delete_set.insert(constraint_id);
3619 }
3620 if let Some(constraint_id) = right_coincident_data.existing_point_segment_constraint_id {
3621 constraints_to_delete_set.insert(constraint_id);
3622 }
3623
3624 if let Some(end_id) = original_end_point_id {
3625 for obj in objects {
3626 let ObjectKind::Constraint { constraint } = &obj.kind else {
3627 continue;
3628 };
3629
3630 if point_axis_constraint_references_point(constraint, end_id) {
3631 constraints_to_delete_set.insert(obj.id);
3632 }
3633 }
3634 }
3635
3636 if let Some(end_id) = original_end_point_id {
3638 let end_point_point_constraint_ids = find_point_point_coincident_constraints(end_id);
3639 for constraint_id in end_point_point_constraint_ids {
3640 let other_point_id_opt = objects.iter().find_map(|obj| {
3642 if obj.id != constraint_id {
3643 return None;
3644 }
3645 let ObjectKind::Constraint { constraint } = &obj.kind else {
3646 return None;
3647 };
3648 let Constraint::Coincident(coincident) = constraint else {
3649 return None;
3650 };
3651 coincident.segment_ids().find(|&seg_id| seg_id != end_id)
3652 });
3653
3654 if let Some(other_point_id) = other_point_id_opt {
3655 constraints_to_delete_set.insert(constraint_id);
3656 constraints_to_migrate.push(ConstraintToMigrate {
3658 constraint_id,
3659 other_entity_id: other_point_id,
3660 is_point_point: true,
3661 attach_to_endpoint: AttachToEndpoint::End,
3662 });
3663 }
3664 }
3665 }
3666
3667 if let Some(end_id) = original_end_point_id {
3669 let end_point_segment_constraints = find_point_segment_coincident_constraints(end_id);
3670 for constraint_json in end_point_segment_constraints {
3671 if let Some(constraint_id_usize) = constraint_json
3672 .get("constraintId")
3673 .and_then(|v| v.as_u64())
3674 .map(|id| id as usize)
3675 {
3676 let constraint_id = ObjectId(constraint_id_usize);
3677 constraints_to_delete_set.insert(constraint_id);
3678 if let Some(other_id_usize) = constraint_json
3680 .get("segmentOrPointId")
3681 .and_then(|v| v.as_u64())
3682 .map(|id| id as usize)
3683 {
3684 constraints_to_migrate.push(ConstraintToMigrate {
3685 constraint_id,
3686 other_entity_id: ObjectId(other_id_usize),
3687 is_point_point: false,
3688 attach_to_endpoint: AttachToEndpoint::End,
3689 });
3690 }
3691 }
3692 }
3693 }
3694
3695 if let Some(end_id) = original_end_point_id {
3700 for obj in objects {
3701 let ObjectKind::Constraint { constraint } = &obj.kind else {
3702 continue;
3703 };
3704
3705 let Constraint::Coincident(coincident) = constraint else {
3706 continue;
3707 };
3708
3709 if !coincident.contains_segment(trim_spawn_id) {
3714 continue;
3715 }
3716 if let (Some(start_id), Some(end_id_val)) = (original_start_point_id, Some(end_id))
3719 && coincident.segment_ids().any(|id| id == start_id || id == end_id_val)
3720 {
3721 continue; }
3723
3724 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
3726
3727 if let Some(other_id) = other_id {
3728 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3730 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3731 continue;
3732 };
3733
3734 let Segment::Point(point) = other_segment else {
3735 continue;
3736 };
3737
3738 let point_coords = Coords2d {
3740 x: number_to_unit(&point.position.x, default_unit),
3741 y: number_to_unit(&point.position.y, default_unit),
3742 };
3743
3744 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3747 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3748 if let ObjectKind::Segment {
3749 segment: Segment::Point(end_point),
3750 } = &end_point_obj.kind
3751 {
3752 Some(Coords2d {
3753 x: number_to_unit(&end_point.position.x, default_unit),
3754 y: number_to_unit(&end_point.position.y, default_unit),
3755 })
3756 } else {
3757 None
3758 }
3759 } else {
3760 None
3761 }
3762 } else {
3763 None
3764 };
3765
3766 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3767 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3768 * (point_coords.x - reference_coords.x)
3769 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3770 .sqrt();
3771
3772 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3773 let has_point_point_constraint = find_point_point_coincident_constraints(end_id)
3776 .iter()
3777 .any(|&constraint_id| {
3778 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
3779 if let ObjectKind::Constraint {
3780 constraint: Constraint::Coincident(coincident),
3781 } = &constraint_obj.kind
3782 {
3783 coincident.contains_segment(other_id)
3784 } else {
3785 false
3786 }
3787 } else {
3788 false
3789 }
3790 });
3791
3792 if !has_point_point_constraint {
3793 constraints_to_migrate.push(ConstraintToMigrate {
3795 constraint_id: obj.id,
3796 other_entity_id: other_id,
3797 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3800 }
3801 constraints_to_delete_set.insert(obj.id);
3803 }
3804 }
3805 }
3806 }
3807 }
3808
3809 let split_point = right_trim_coords; let segment_start_coords = match segment {
3814 Segment::Line(_) => {
3815 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::Start, objects, default_unit)
3816 }
3817 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Start, objects, default_unit),
3818 _ => None,
3819 };
3820 let segment_end_coords = match segment {
3821 Segment::Line(_) => {
3822 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
3823 }
3824 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
3825 _ => None,
3826 };
3827 let segment_center_coords = match segment {
3828 Segment::Line(_) => None,
3829 Segment::Arc(_) => {
3830 get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Center, objects, default_unit)
3831 }
3832 _ => None,
3833 };
3834
3835 if let (Some(start_coords), Some(end_coords)) = (segment_start_coords, segment_end_coords) {
3836 let split_point_t_opt = match segment {
3838 Segment::Line(_) => Some(project_point_onto_segment(split_point, start_coords, end_coords)),
3839 Segment::Arc(_) => segment_center_coords
3840 .map(|center| project_point_onto_arc(split_point, center, start_coords, end_coords)),
3841 _ => None,
3842 };
3843
3844 if let Some(split_point_t) = split_point_t_opt {
3845 for obj in objects {
3847 let ObjectKind::Constraint { constraint } = &obj.kind else {
3848 continue;
3849 };
3850
3851 let Constraint::Coincident(coincident) = constraint else {
3852 continue;
3853 };
3854
3855 if !coincident.contains_segment(trim_spawn_id) {
3857 continue;
3858 }
3859
3860 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id)
3862 && coincident.segment_ids().any(|id| id == start_id || id == end_id)
3863 {
3864 continue;
3865 }
3866
3867 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
3869
3870 if let Some(other_id) = other_id {
3871 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3873 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3874 continue;
3875 };
3876
3877 let Segment::Point(point) = other_segment else {
3878 continue;
3879 };
3880
3881 let point_coords = Coords2d {
3883 x: number_to_unit(&point.position.x, default_unit),
3884 y: number_to_unit(&point.position.y, default_unit),
3885 };
3886
3887 let point_t = match segment {
3889 Segment::Line(_) => project_point_onto_segment(point_coords, start_coords, end_coords),
3890 Segment::Arc(_) => {
3891 if let Some(center) = segment_center_coords {
3892 project_point_onto_arc(point_coords, center, start_coords, end_coords)
3893 } else {
3894 continue; }
3896 }
3897 _ => continue, };
3899
3900 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3903 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3904 if let ObjectKind::Segment {
3905 segment: Segment::Point(end_point),
3906 } = &end_point_obj.kind
3907 {
3908 Some(Coords2d {
3909 x: number_to_unit(&end_point.position.x, default_unit),
3910 y: number_to_unit(&end_point.position.y, default_unit),
3911 })
3912 } else {
3913 None
3914 }
3915 } else {
3916 None
3917 }
3918 } else {
3919 None
3920 };
3921
3922 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3923 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3924 * (point_coords.x - reference_coords.x)
3925 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3926 .sqrt();
3927
3928 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3929 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
3933 find_point_point_coincident_constraints(end_id)
3934 .iter()
3935 .any(|&constraint_id| {
3936 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
3937 {
3938 if let ObjectKind::Constraint {
3939 constraint: Constraint::Coincident(coincident),
3940 } = &constraint_obj.kind
3941 {
3942 coincident.contains_segment(other_id)
3943 } else {
3944 false
3945 }
3946 } else {
3947 false
3948 }
3949 })
3950 } else {
3951 false
3952 };
3953
3954 if !has_point_point_constraint {
3955 constraints_to_migrate.push(ConstraintToMigrate {
3957 constraint_id: obj.id,
3958 other_entity_id: other_id,
3959 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3962 }
3963 constraints_to_delete_set.insert(obj.id);
3965 continue; }
3967
3968 let dist_to_start = ((point_coords.x - start_coords.x) * (point_coords.x - start_coords.x)
3970 + (point_coords.y - start_coords.y) * (point_coords.y - start_coords.y))
3971 .sqrt();
3972 let is_at_start = (point_t - 0.0).abs() < EPSILON_POINT_ON_SEGMENT
3973 || dist_to_start < EPSILON_POINT_ON_SEGMENT;
3974
3975 if is_at_start {
3976 continue; }
3978
3979 let dist_to_split = (point_t - split_point_t).abs();
3981 if dist_to_split < EPSILON_POINT_ON_SEGMENT * 100.0 {
3982 continue; }
3984
3985 if point_t > split_point_t {
3987 constraints_to_migrate.push(ConstraintToMigrate {
3988 constraint_id: obj.id,
3989 other_entity_id: other_id,
3990 is_point_point: false, attach_to_endpoint: AttachToEndpoint::Segment, });
3993 constraints_to_delete_set.insert(obj.id);
3994 }
3995 }
3996 }
3997 }
3998 } } let distance_constraint_ids_for_split = find_distance_constraints_for_segment(trim_spawn_id);
4006
4007 let arc_center_point_id: Option<ObjectId> = match segment {
4009 Segment::Arc(arc) => Some(arc.center),
4010 _ => None,
4011 };
4012
4013 for constraint_id in distance_constraint_ids_for_split {
4014 if let Some(center_id) = arc_center_point_id {
4016 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
4018 && let ObjectKind::Constraint { constraint } = &constraint_obj.kind
4019 && let Constraint::Distance(distance) = constraint
4020 && distance.contains_point(center_id)
4021 {
4022 continue;
4024 }
4025 }
4026
4027 constraints_to_delete_set.insert(constraint_id);
4028 }
4029
4030 for obj in objects {
4038 let ObjectKind::Constraint { constraint } = &obj.kind else {
4039 continue;
4040 };
4041
4042 let Constraint::Coincident(coincident) = constraint else {
4043 continue;
4044 };
4045
4046 if !coincident.contains_segment(trim_spawn_id) {
4048 continue;
4049 }
4050
4051 if constraints_to_delete_set.contains(&obj.id) {
4053 continue;
4054 }
4055
4056 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
4063
4064 if let Some(other_id) = other_id {
4065 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
4067 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
4068 continue;
4069 };
4070
4071 let Segment::Point(point) = other_segment else {
4072 continue;
4073 };
4074
4075 let _is_endpoint_constraint =
4078 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id) {
4079 coincident.segment_ids().any(|id| id == start_id || id == end_id)
4080 } else {
4081 false
4082 };
4083
4084 let point_coords = Coords2d {
4086 x: number_to_unit(&point.position.x, default_unit),
4087 y: number_to_unit(&point.position.y, default_unit),
4088 };
4089
4090 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
4092 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
4093 if let ObjectKind::Segment {
4094 segment: Segment::Point(end_point),
4095 } = &end_point_obj.kind
4096 {
4097 Some(Coords2d {
4098 x: number_to_unit(&end_point.position.x, default_unit),
4099 y: number_to_unit(&end_point.position.y, default_unit),
4100 })
4101 } else {
4102 None
4103 }
4104 } else {
4105 None
4106 }
4107 } else {
4108 None
4109 };
4110
4111 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
4112 let dist_to_original_end = ((point_coords.x - reference_coords.x)
4113 * (point_coords.x - reference_coords.x)
4114 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
4115 .sqrt();
4116
4117 let is_at_original_end = dist_to_original_end < EPSILON_POINT_ON_SEGMENT * 2.0;
4120
4121 if is_at_original_end {
4122 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
4125 find_point_point_coincident_constraints(end_id)
4126 .iter()
4127 .any(|&constraint_id| {
4128 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
4129 if let ObjectKind::Constraint {
4130 constraint: Constraint::Coincident(coincident),
4131 } = &constraint_obj.kind
4132 {
4133 coincident.contains_segment(other_id)
4134 } else {
4135 false
4136 }
4137 } else {
4138 false
4139 }
4140 })
4141 } else {
4142 false
4143 };
4144
4145 if !has_point_point_constraint {
4146 constraints_to_migrate.push(ConstraintToMigrate {
4148 constraint_id: obj.id,
4149 other_entity_id: other_id,
4150 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
4153 }
4154 constraints_to_delete_set.insert(obj.id);
4156 }
4157 }
4158 }
4159 }
4160
4161 let constraints_to_delete: Vec<ObjectId> = constraints_to_delete_set.iter().copied().collect();
4163 let plan = TrimPlan::SplitSegment {
4164 segment_id: trim_spawn_id,
4165 left_trim_coords,
4166 right_trim_coords,
4167 original_end_coords,
4168 left_side: Box::new(left_side.clone()),
4169 right_side: Box::new(right_side.clone()),
4170 left_side_coincident_data: CoincidentData {
4171 intersecting_seg_id: left_intersecting_seg_id,
4172 intersecting_endpoint_point_id: left_coincident_data.intersecting_endpoint_point_id,
4173 existing_point_segment_constraint_id: left_coincident_data.existing_point_segment_constraint_id,
4174 },
4175 right_side_coincident_data: CoincidentData {
4176 intersecting_seg_id: right_intersecting_seg_id,
4177 intersecting_endpoint_point_id: right_coincident_data.intersecting_endpoint_point_id,
4178 existing_point_segment_constraint_id: right_coincident_data.existing_point_segment_constraint_id,
4179 },
4180 constraints_to_migrate,
4181 constraints_to_delete,
4182 };
4183
4184 return Ok(plan);
4185 }
4186
4187 Err(format!(
4192 "Unsupported trim termination combination: left={:?} right={:?}",
4193 left_side, right_side
4194 ))
4195}
4196
4197pub(crate) async fn execute_trim_operations_simple(
4209 strategy: Vec<TrimOperation>,
4210 current_scene_graph_delta: &crate::frontend::api::SceneGraphDelta,
4211 frontend: &mut crate::frontend::FrontendState,
4212 ctx: &crate::ExecutorContext,
4213 version: crate::frontend::api::Version,
4214 sketch_id: ObjectId,
4215) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
4216 use crate::frontend::SketchApi;
4217 use crate::frontend::sketch::Constraint;
4218 use crate::frontend::sketch::ExistingSegmentCtor;
4219 use crate::frontend::sketch::SegmentCtor;
4220
4221 let default_unit = frontend.default_length_unit();
4222
4223 let mut op_index = 0;
4224 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = None;
4225 let mut invalidates_ids = false;
4226
4227 while op_index < strategy.len() {
4228 let mut consumed_ops = 1;
4229 let operation_result = match &strategy[op_index] {
4230 TrimOperation::SimpleTrim { segment_to_trim_id } => {
4231 frontend
4233 .delete_objects(
4234 ctx,
4235 version,
4236 sketch_id,
4237 Vec::new(), vec![*segment_to_trim_id], )
4240 .await
4241 .map_err(|e| format!("Failed to delete segment: {}", e.error.message()))
4242 }
4243 TrimOperation::EditSegment {
4244 segment_id,
4245 ctor,
4246 endpoint_changed,
4247 } => {
4248 if op_index + 1 < strategy.len() {
4251 if let TrimOperation::AddCoincidentConstraint {
4252 segment_id: coincident_seg_id,
4253 endpoint_changed: coincident_endpoint_changed,
4254 segment_or_point_to_make_coincident_to,
4255 intersecting_endpoint_point_id,
4256 } = &strategy[op_index + 1]
4257 {
4258 if segment_id == coincident_seg_id && endpoint_changed == coincident_endpoint_changed {
4259 let mut delete_constraint_ids: Vec<ObjectId> = Vec::new();
4261 consumed_ops = 2;
4262
4263 if op_index + 2 < strategy.len()
4264 && let TrimOperation::DeleteConstraints { constraint_ids } = &strategy[op_index + 2]
4265 {
4266 delete_constraint_ids = constraint_ids.to_vec();
4267 consumed_ops = 3;
4268 }
4269
4270 let segment_ctor = ctor.clone();
4272
4273 let edited_segment = current_scene_graph_delta
4275 .new_graph
4276 .objects
4277 .iter()
4278 .find(|obj| obj.id == *segment_id)
4279 .ok_or_else(|| format!("Failed to find segment {} for tail-cut batch", segment_id.0))?;
4280
4281 let endpoint_point_id = match &edited_segment.kind {
4282 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4283 crate::frontend::sketch::Segment::Line(line) => {
4284 if *endpoint_changed == EndpointChanged::Start {
4285 line.start
4286 } else {
4287 line.end
4288 }
4289 }
4290 crate::frontend::sketch::Segment::Arc(arc) => {
4291 if *endpoint_changed == EndpointChanged::Start {
4292 arc.start
4293 } else {
4294 arc.end
4295 }
4296 }
4297 _ => {
4298 return Err("Unsupported segment type for tail-cut batch".to_string());
4299 }
4300 },
4301 _ => {
4302 return Err("Edited object is not a segment (tail-cut batch)".to_string());
4303 }
4304 };
4305
4306 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4307 vec![endpoint_point_id.into(), (*point_id).into()]
4308 } else {
4309 vec![
4310 endpoint_point_id.into(),
4311 (*segment_or_point_to_make_coincident_to).into(),
4312 ]
4313 };
4314
4315 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4316 segments: coincident_segments,
4317 });
4318
4319 let segment_to_edit = ExistingSegmentCtor {
4320 id: *segment_id,
4321 ctor: segment_ctor,
4322 };
4323
4324 frontend
4327 .batch_tail_cut_operations(
4328 ctx,
4329 version,
4330 sketch_id,
4331 vec![segment_to_edit],
4332 vec![constraint],
4333 delete_constraint_ids,
4334 )
4335 .await
4336 .map_err(|e| format!("Failed to batch tail-cut operations: {}", e.error.message()))
4337 } else {
4338 let segment_to_edit = ExistingSegmentCtor {
4340 id: *segment_id,
4341 ctor: ctor.clone(),
4342 };
4343
4344 frontend
4345 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4346 .await
4347 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4348 }
4349 } else {
4350 let segment_to_edit = ExistingSegmentCtor {
4352 id: *segment_id,
4353 ctor: ctor.clone(),
4354 };
4355
4356 frontend
4357 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4358 .await
4359 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4360 }
4361 } else {
4362 let segment_to_edit = ExistingSegmentCtor {
4364 id: *segment_id,
4365 ctor: ctor.clone(),
4366 };
4367
4368 frontend
4369 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4370 .await
4371 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4372 }
4373 }
4374 TrimOperation::AddCoincidentConstraint {
4375 segment_id,
4376 endpoint_changed,
4377 segment_or_point_to_make_coincident_to,
4378 intersecting_endpoint_point_id,
4379 } => {
4380 let edited_segment = current_scene_graph_delta
4382 .new_graph
4383 .objects
4384 .iter()
4385 .find(|obj| obj.id == *segment_id)
4386 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
4387
4388 let new_segment_endpoint_point_id = match &edited_segment.kind {
4390 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4391 crate::frontend::sketch::Segment::Line(line) => {
4392 if *endpoint_changed == EndpointChanged::Start {
4393 line.start
4394 } else {
4395 line.end
4396 }
4397 }
4398 crate::frontend::sketch::Segment::Arc(arc) => {
4399 if *endpoint_changed == EndpointChanged::Start {
4400 arc.start
4401 } else {
4402 arc.end
4403 }
4404 }
4405 _ => {
4406 return Err("Unsupported segment type for addCoincidentConstraint".to_string());
4407 }
4408 },
4409 _ => {
4410 return Err("Edited object is not a segment".to_string());
4411 }
4412 };
4413
4414 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4416 vec![new_segment_endpoint_point_id.into(), (*point_id).into()]
4417 } else {
4418 vec![
4419 new_segment_endpoint_point_id.into(),
4420 (*segment_or_point_to_make_coincident_to).into(),
4421 ]
4422 };
4423
4424 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4425 segments: coincident_segments,
4426 });
4427
4428 frontend
4429 .add_constraint(ctx, version, sketch_id, constraint)
4430 .await
4431 .map_err(|e| format!("Failed to add constraint: {}", e.error.message()))
4432 }
4433 TrimOperation::DeleteConstraints { constraint_ids } => {
4434 let constraint_object_ids: Vec<ObjectId> = constraint_ids.to_vec();
4436
4437 frontend
4438 .delete_objects(
4439 ctx,
4440 version,
4441 sketch_id,
4442 constraint_object_ids,
4443 Vec::new(), )
4445 .await
4446 .map_err(|e| format!("Failed to delete constraints: {}", e.error.message()))
4447 }
4448 TrimOperation::ReplaceCircleWithArc {
4449 circle_id,
4450 arc_start_coords,
4451 arc_end_coords,
4452 arc_start_termination,
4453 arc_end_termination,
4454 } => {
4455 let original_circle = current_scene_graph_delta
4457 .new_graph
4458 .objects
4459 .iter()
4460 .find(|obj| obj.id == *circle_id)
4461 .ok_or_else(|| format!("Failed to find original circle {}", circle_id.0))?;
4462
4463 let (original_circle_start_id, original_circle_center_id, circle_ctor) = match &original_circle.kind {
4464 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4465 crate::frontend::sketch::Segment::Circle(circle) => match &circle.ctor {
4466 SegmentCtor::Circle(circle_ctor) => (circle.start, circle.center, circle_ctor.clone()),
4467 _ => return Err("Circle does not have a Circle ctor".to_string()),
4468 },
4469 _ => return Err("Original segment is not a circle".to_string()),
4470 },
4471 _ => return Err("Original object is not a segment".to_string()),
4472 };
4473
4474 let units = match &circle_ctor.start.x {
4475 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4476 _ => crate::pretty::NumericSuffix::Mm,
4477 };
4478
4479 let coords_to_point_expr = |coords: Coords2d| crate::frontend::sketch::Point2d {
4480 x: crate::frontend::api::Expr::Var(unit_to_number(coords.x, default_unit, units)),
4481 y: crate::frontend::api::Expr::Var(unit_to_number(coords.y, default_unit, units)),
4482 };
4483
4484 let arc_ctor = SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4485 start: coords_to_point_expr(*arc_start_coords),
4486 end: coords_to_point_expr(*arc_end_coords),
4487 center: circle_ctor.center.clone(),
4488 construction: circle_ctor.construction,
4489 });
4490
4491 let (_add_source_delta, add_scene_graph_delta) = frontend
4492 .add_segment(ctx, version, sketch_id, arc_ctor, None)
4493 .await
4494 .map_err(|e| format!("Failed to add arc while replacing circle: {}", e.error.message()))?;
4495 invalidates_ids = invalidates_ids || add_scene_graph_delta.invalidates_ids;
4496
4497 let new_arc_id = *add_scene_graph_delta
4498 .new_objects
4499 .iter()
4500 .find(|&id| {
4501 add_scene_graph_delta
4502 .new_graph
4503 .objects
4504 .iter()
4505 .find(|o| o.id == *id)
4506 .is_some_and(|obj| {
4507 matches!(
4508 &obj.kind,
4509 crate::frontend::api::ObjectKind::Segment { segment }
4510 if matches!(segment, crate::frontend::sketch::Segment::Arc(_))
4511 )
4512 })
4513 })
4514 .ok_or_else(|| "Failed to find newly created arc segment".to_string())?;
4515
4516 let new_arc_obj = add_scene_graph_delta
4517 .new_graph
4518 .objects
4519 .iter()
4520 .find(|obj| obj.id == new_arc_id)
4521 .ok_or_else(|| format!("New arc segment not found {}", new_arc_id.0))?;
4522 let (new_arc_start_id, new_arc_end_id, new_arc_center_id) = match &new_arc_obj.kind {
4523 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4524 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, arc.center),
4525 _ => return Err("New segment is not an arc".to_string()),
4526 },
4527 _ => return Err("New arc object is not a segment".to_string()),
4528 };
4529
4530 let constraint_segments_for =
4531 |arc_endpoint_id: ObjectId,
4532 term: &TrimTermination|
4533 -> Result<Vec<crate::frontend::sketch::ConstraintSegment>, String> {
4534 match term {
4535 TrimTermination::Intersection {
4536 intersecting_seg_id, ..
4537 } => Ok(vec![arc_endpoint_id.into(), (*intersecting_seg_id).into()]),
4538 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4539 other_segment_point_id,
4540 ..
4541 } => Ok(vec![arc_endpoint_id.into(), (*other_segment_point_id).into()]),
4542 TrimTermination::SegEndPoint { .. } => {
4543 Err("Circle replacement endpoint cannot terminate at seg endpoint".to_string())
4544 }
4545 }
4546 };
4547
4548 let start_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4549 segments: constraint_segments_for(new_arc_start_id, arc_start_termination)?,
4550 });
4551 let (_c1_source_delta, c1_scene_graph_delta) = frontend
4552 .add_constraint(ctx, version, sketch_id, start_constraint)
4553 .await
4554 .map_err(|e| format!("Failed to add start coincident on replaced arc: {}", e.error.message()))?;
4555 invalidates_ids = invalidates_ids || c1_scene_graph_delta.invalidates_ids;
4556
4557 let end_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4558 segments: constraint_segments_for(new_arc_end_id, arc_end_termination)?,
4559 });
4560 let (_c2_source_delta, c2_scene_graph_delta) = frontend
4561 .add_constraint(ctx, version, sketch_id, end_constraint)
4562 .await
4563 .map_err(|e| format!("Failed to add end coincident on replaced arc: {}", e.error.message()))?;
4564 invalidates_ids = invalidates_ids || c2_scene_graph_delta.invalidates_ids;
4565
4566 let mut termination_point_ids: Vec<ObjectId> = Vec::new();
4567 for term in [arc_start_termination, arc_end_termination] {
4568 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4569 other_segment_point_id,
4570 ..
4571 } = term.as_ref()
4572 {
4573 termination_point_ids.push(*other_segment_point_id);
4574 }
4575 }
4576
4577 let rewrite_map = std::collections::HashMap::from([
4581 (*circle_id, new_arc_id),
4582 (original_circle_center_id, new_arc_center_id),
4583 (original_circle_start_id, new_arc_start_id),
4584 ]);
4585 let rewrite_ids: std::collections::HashSet<ObjectId> = rewrite_map.keys().copied().collect();
4586
4587 let mut migrated_constraints: Vec<Constraint> = Vec::new();
4588 for obj in ¤t_scene_graph_delta.new_graph.objects {
4589 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4590 continue;
4591 };
4592
4593 match constraint {
4594 Constraint::Coincident(coincident) => {
4595 if !constraint_segments_reference_any(&coincident.segments, &rewrite_ids) {
4596 continue;
4597 }
4598
4599 if coincident.contains_segment(*circle_id)
4603 && coincident
4604 .segment_ids()
4605 .filter(|id| *id != *circle_id)
4606 .any(|id| termination_point_ids.contains(&id))
4607 {
4608 continue;
4609 }
4610
4611 let Some(Constraint::Coincident(migrated_coincident)) =
4612 rewrite_constraint_with_map(constraint, &rewrite_map)
4613 else {
4614 continue;
4615 };
4616
4617 let migrated_ids: Vec<ObjectId> = migrated_coincident
4621 .segments
4622 .iter()
4623 .filter_map(|segment| match segment {
4624 crate::frontend::sketch::ConstraintSegment::Segment(id) => Some(*id),
4625 crate::frontend::sketch::ConstraintSegment::Origin(_) => None,
4626 })
4627 .collect();
4628 if migrated_ids.contains(&new_arc_id)
4629 && (migrated_ids.contains(&new_arc_start_id) || migrated_ids.contains(&new_arc_end_id))
4630 {
4631 continue;
4632 }
4633
4634 migrated_constraints.push(Constraint::Coincident(migrated_coincident));
4635 }
4636 Constraint::Distance(distance) => {
4637 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4638 continue;
4639 }
4640 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4641 migrated_constraints.push(migrated);
4642 }
4643 }
4644 Constraint::HorizontalDistance(distance) => {
4645 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4646 continue;
4647 }
4648 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4649 migrated_constraints.push(migrated);
4650 }
4651 }
4652 Constraint::VerticalDistance(distance) => {
4653 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4654 continue;
4655 }
4656 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4657 migrated_constraints.push(migrated);
4658 }
4659 }
4660 Constraint::Radius(radius) => {
4661 if radius.arc == *circle_id
4662 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4663 {
4664 migrated_constraints.push(migrated);
4665 }
4666 }
4667 Constraint::Diameter(diameter) => {
4668 if diameter.arc == *circle_id
4669 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4670 {
4671 migrated_constraints.push(migrated);
4672 }
4673 }
4674 Constraint::EqualRadius(equal_radius) => {
4675 if equal_radius.input.contains(circle_id)
4676 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4677 {
4678 migrated_constraints.push(migrated);
4679 }
4680 }
4681 Constraint::Tangent(tangent) => {
4682 if tangent.input.contains(circle_id)
4683 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4684 {
4685 migrated_constraints.push(migrated);
4686 }
4687 }
4688 _ => {}
4689 }
4690 }
4691
4692 for constraint in migrated_constraints {
4693 let (_source_delta, migrated_scene_graph_delta) = frontend
4694 .add_constraint(ctx, version, sketch_id, constraint)
4695 .await
4696 .map_err(|e| format!("Failed to migrate circle constraint to arc: {}", e.error.message()))?;
4697 invalidates_ids = invalidates_ids || migrated_scene_graph_delta.invalidates_ids;
4698 }
4699
4700 frontend
4701 .delete_objects(ctx, version, sketch_id, Vec::new(), vec![*circle_id])
4702 .await
4703 .map_err(|e| format!("Failed to delete circle after arc replacement: {}", e.error.message()))
4704 }
4705 TrimOperation::SplitSegment {
4706 segment_id,
4707 left_trim_coords,
4708 right_trim_coords,
4709 original_end_coords,
4710 left_side,
4711 right_side,
4712 constraints_to_migrate,
4713 constraints_to_delete,
4714 ..
4715 } => {
4716 let original_segment = current_scene_graph_delta
4721 .new_graph
4722 .objects
4723 .iter()
4724 .find(|obj| obj.id == *segment_id)
4725 .ok_or_else(|| format!("Failed to find original segment {}", segment_id.0))?;
4726
4727 let (original_segment_start_point_id, original_segment_end_point_id, original_segment_center_point_id) =
4729 match &original_segment.kind {
4730 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4731 crate::frontend::sketch::Segment::Line(line) => (Some(line.start), Some(line.end), None),
4732 crate::frontend::sketch::Segment::Arc(arc) => {
4733 (Some(arc.start), Some(arc.end), Some(arc.center))
4734 }
4735 _ => (None, None, None),
4736 },
4737 _ => (None, None, None),
4738 };
4739
4740 let mut center_point_constraints_to_migrate: Vec<(Constraint, ObjectId)> = Vec::new();
4742 if let Some(original_center_id) = original_segment_center_point_id {
4743 for obj in ¤t_scene_graph_delta.new_graph.objects {
4744 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4745 continue;
4746 };
4747
4748 if let Constraint::Coincident(coincident) = constraint
4750 && coincident.contains_segment(original_center_id)
4751 {
4752 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4753 }
4754
4755 if let Constraint::Distance(distance) = constraint
4757 && distance.contains_point(original_center_id)
4758 {
4759 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4760 }
4761 }
4762 }
4763
4764 let (_segment_type, original_ctor) = match &original_segment.kind {
4766 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4767 crate::frontend::sketch::Segment::Line(line) => ("Line", line.ctor.clone()),
4768 crate::frontend::sketch::Segment::Arc(arc) => ("Arc", arc.ctor.clone()),
4769 _ => {
4770 return Err("Original segment is not a Line or Arc".to_string());
4771 }
4772 },
4773 _ => {
4774 return Err("Original object is not a segment".to_string());
4775 }
4776 };
4777
4778 let units = match &original_ctor {
4780 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
4781 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4782 _ => crate::pretty::NumericSuffix::Mm,
4783 },
4784 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
4785 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4786 _ => crate::pretty::NumericSuffix::Mm,
4787 },
4788 _ => crate::pretty::NumericSuffix::Mm,
4789 };
4790
4791 let coords_to_point =
4794 |coords: Coords2d| -> crate::frontend::sketch::Point2d<crate::frontend::api::Number> {
4795 crate::frontend::sketch::Point2d {
4796 x: unit_to_number(coords.x, default_unit, units),
4797 y: unit_to_number(coords.y, default_unit, units),
4798 }
4799 };
4800
4801 let point_to_expr = |point: crate::frontend::sketch::Point2d<crate::frontend::api::Number>| -> crate::frontend::sketch::Point2d<crate::frontend::api::Expr> {
4803 crate::frontend::sketch::Point2d {
4804 x: crate::frontend::api::Expr::Var(point.x),
4805 y: crate::frontend::api::Expr::Var(point.y),
4806 }
4807 };
4808
4809 let new_segment_ctor = match &original_ctor {
4811 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
4812 start: point_to_expr(coords_to_point(*right_trim_coords)),
4813 end: point_to_expr(coords_to_point(*original_end_coords)),
4814 construction: line_ctor.construction,
4815 }),
4816 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4817 start: point_to_expr(coords_to_point(*right_trim_coords)),
4818 end: point_to_expr(coords_to_point(*original_end_coords)),
4819 center: arc_ctor.center.clone(),
4820 construction: arc_ctor.construction,
4821 }),
4822 _ => {
4823 return Err("Unsupported segment type for new segment".to_string());
4824 }
4825 };
4826
4827 let (_add_source_delta, add_scene_graph_delta) = frontend
4828 .add_segment(ctx, version, sketch_id, new_segment_ctor, None)
4829 .await
4830 .map_err(|e| format!("Failed to add new segment: {}", e.error.message()))?;
4831
4832 let new_segment_id = *add_scene_graph_delta
4834 .new_objects
4835 .iter()
4836 .find(|&id| {
4837 if let Some(obj) = add_scene_graph_delta.new_graph.objects.iter().find(|o| o.id == *id) {
4838 matches!(
4839 &obj.kind,
4840 crate::frontend::api::ObjectKind::Segment { segment }
4841 if matches!(segment, crate::frontend::sketch::Segment::Line(_) | crate::frontend::sketch::Segment::Arc(_))
4842 )
4843 } else {
4844 false
4845 }
4846 })
4847 .ok_or_else(|| "Failed to find newly created segment".to_string())?;
4848
4849 let new_segment = add_scene_graph_delta
4850 .new_graph
4851 .objects
4852 .iter()
4853 .find(|o| o.id == new_segment_id)
4854 .ok_or_else(|| format!("New segment not found with id {}", new_segment_id.0))?;
4855
4856 let (new_segment_start_point_id, new_segment_end_point_id, new_segment_center_point_id) =
4858 match &new_segment.kind {
4859 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4860 crate::frontend::sketch::Segment::Line(line) => (line.start, line.end, None),
4861 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, Some(arc.center)),
4862 _ => {
4863 return Err("New segment is not a Line or Arc".to_string());
4864 }
4865 },
4866 _ => {
4867 return Err("New segment is not a segment".to_string());
4868 }
4869 };
4870
4871 let edited_ctor = match &original_ctor {
4873 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
4874 start: line_ctor.start.clone(),
4875 end: point_to_expr(coords_to_point(*left_trim_coords)),
4876 construction: line_ctor.construction,
4877 }),
4878 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4879 start: arc_ctor.start.clone(),
4880 end: point_to_expr(coords_to_point(*left_trim_coords)),
4881 center: arc_ctor.center.clone(),
4882 construction: arc_ctor.construction,
4883 }),
4884 _ => {
4885 return Err("Unsupported segment type for split".to_string());
4886 }
4887 };
4888
4889 let (_edit_source_delta, edit_scene_graph_delta) = frontend
4890 .edit_segments(
4891 ctx,
4892 version,
4893 sketch_id,
4894 vec![ExistingSegmentCtor {
4895 id: *segment_id,
4896 ctor: edited_ctor,
4897 }],
4898 )
4899 .await
4900 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))?;
4901 invalidates_ids = invalidates_ids || edit_scene_graph_delta.invalidates_ids;
4903
4904 let edited_segment = edit_scene_graph_delta
4906 .new_graph
4907 .objects
4908 .iter()
4909 .find(|obj| obj.id == *segment_id)
4910 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
4911
4912 let left_side_endpoint_point_id = match &edited_segment.kind {
4913 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4914 crate::frontend::sketch::Segment::Line(line) => line.end,
4915 crate::frontend::sketch::Segment::Arc(arc) => arc.end,
4916 _ => {
4917 return Err("Edited segment is not a Line or Arc".to_string());
4918 }
4919 },
4920 _ => {
4921 return Err("Edited segment is not a segment".to_string());
4922 }
4923 };
4924
4925 let mut batch_constraints = Vec::new();
4927
4928 let left_intersecting_seg_id = match &**left_side {
4930 TrimTermination::Intersection {
4931 intersecting_seg_id, ..
4932 }
4933 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4934 intersecting_seg_id, ..
4935 } => *intersecting_seg_id,
4936 _ => {
4937 return Err("Left side is not an intersection or coincident".to_string());
4938 }
4939 };
4940 let left_coincident_segments = match &**left_side {
4941 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4942 other_segment_point_id,
4943 ..
4944 } => {
4945 vec![left_side_endpoint_point_id.into(), (*other_segment_point_id).into()]
4946 }
4947 _ => {
4948 vec![left_side_endpoint_point_id.into(), left_intersecting_seg_id.into()]
4949 }
4950 };
4951 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
4952 segments: left_coincident_segments,
4953 }));
4954
4955 let right_intersecting_seg_id = match &**right_side {
4957 TrimTermination::Intersection {
4958 intersecting_seg_id, ..
4959 }
4960 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4961 intersecting_seg_id, ..
4962 } => *intersecting_seg_id,
4963 _ => {
4964 return Err("Right side is not an intersection or coincident".to_string());
4965 }
4966 };
4967
4968 let mut intersection_point_id: Option<ObjectId> = None;
4969 if matches!(&**right_side, TrimTermination::Intersection { .. }) {
4970 let intersecting_seg = edit_scene_graph_delta
4971 .new_graph
4972 .objects
4973 .iter()
4974 .find(|obj| obj.id == right_intersecting_seg_id);
4975
4976 if let Some(seg) = intersecting_seg {
4977 let endpoint_epsilon = 1e-3; let right_trim_coords_value = *right_trim_coords;
4979
4980 if let crate::frontend::api::ObjectKind::Segment { segment } = &seg.kind {
4981 match segment {
4982 crate::frontend::sketch::Segment::Line(_) => {
4983 if let (Some(start_coords), Some(end_coords)) = (
4984 crate::frontend::trim::get_position_coords_for_line(
4985 seg,
4986 crate::frontend::trim::LineEndpoint::Start,
4987 &edit_scene_graph_delta.new_graph.objects,
4988 default_unit,
4989 ),
4990 crate::frontend::trim::get_position_coords_for_line(
4991 seg,
4992 crate::frontend::trim::LineEndpoint::End,
4993 &edit_scene_graph_delta.new_graph.objects,
4994 default_unit,
4995 ),
4996 ) {
4997 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
4998 * (right_trim_coords_value.x - start_coords.x)
4999 + (right_trim_coords_value.y - start_coords.y)
5000 * (right_trim_coords_value.y - start_coords.y))
5001 .sqrt();
5002 if dist_to_start < endpoint_epsilon {
5003 if let crate::frontend::sketch::Segment::Line(line) = segment {
5004 intersection_point_id = Some(line.start);
5005 }
5006 } else {
5007 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
5008 * (right_trim_coords_value.x - end_coords.x)
5009 + (right_trim_coords_value.y - end_coords.y)
5010 * (right_trim_coords_value.y - end_coords.y))
5011 .sqrt();
5012 if dist_to_end < endpoint_epsilon
5013 && let crate::frontend::sketch::Segment::Line(line) = segment
5014 {
5015 intersection_point_id = Some(line.end);
5016 }
5017 }
5018 }
5019 }
5020 crate::frontend::sketch::Segment::Arc(_) => {
5021 if let (Some(start_coords), Some(end_coords)) = (
5022 crate::frontend::trim::get_position_coords_from_arc(
5023 seg,
5024 crate::frontend::trim::ArcPoint::Start,
5025 &edit_scene_graph_delta.new_graph.objects,
5026 default_unit,
5027 ),
5028 crate::frontend::trim::get_position_coords_from_arc(
5029 seg,
5030 crate::frontend::trim::ArcPoint::End,
5031 &edit_scene_graph_delta.new_graph.objects,
5032 default_unit,
5033 ),
5034 ) {
5035 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
5036 * (right_trim_coords_value.x - start_coords.x)
5037 + (right_trim_coords_value.y - start_coords.y)
5038 * (right_trim_coords_value.y - start_coords.y))
5039 .sqrt();
5040 if dist_to_start < endpoint_epsilon {
5041 if let crate::frontend::sketch::Segment::Arc(arc) = segment {
5042 intersection_point_id = Some(arc.start);
5043 }
5044 } else {
5045 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
5046 * (right_trim_coords_value.x - end_coords.x)
5047 + (right_trim_coords_value.y - end_coords.y)
5048 * (right_trim_coords_value.y - end_coords.y))
5049 .sqrt();
5050 if dist_to_end < endpoint_epsilon
5051 && let crate::frontend::sketch::Segment::Arc(arc) = segment
5052 {
5053 intersection_point_id = Some(arc.end);
5054 }
5055 }
5056 }
5057 }
5058 _ => {}
5059 }
5060 }
5061 }
5062 }
5063
5064 let right_coincident_segments = if let Some(point_id) = intersection_point_id {
5065 vec![new_segment_start_point_id.into(), point_id.into()]
5066 } else if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5067 other_segment_point_id,
5068 ..
5069 } = &**right_side
5070 {
5071 vec![new_segment_start_point_id.into(), (*other_segment_point_id).into()]
5072 } else {
5073 vec![new_segment_start_point_id.into(), right_intersecting_seg_id.into()]
5074 };
5075 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5076 segments: right_coincident_segments,
5077 }));
5078
5079 let mut points_constrained_to_new_segment_start = std::collections::HashSet::new();
5081 let mut points_constrained_to_new_segment_end = std::collections::HashSet::new();
5082
5083 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5084 other_segment_point_id,
5085 ..
5086 } = &**right_side
5087 {
5088 points_constrained_to_new_segment_start.insert(other_segment_point_id);
5089 }
5090
5091 for constraint_to_migrate in constraints_to_migrate.iter() {
5092 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::End
5093 && constraint_to_migrate.is_point_point
5094 {
5095 points_constrained_to_new_segment_end.insert(constraint_to_migrate.other_entity_id);
5096 }
5097 }
5098
5099 for constraint_to_migrate in constraints_to_migrate.iter() {
5100 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment
5102 && (points_constrained_to_new_segment_start.contains(&constraint_to_migrate.other_entity_id)
5103 || points_constrained_to_new_segment_end.contains(&constraint_to_migrate.other_entity_id))
5104 {
5105 continue; }
5107
5108 let constraint_segments = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment {
5109 vec![constraint_to_migrate.other_entity_id.into(), new_segment_id.into()]
5110 } else {
5111 let target_endpoint_id = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Start
5112 {
5113 new_segment_start_point_id
5114 } else {
5115 new_segment_end_point_id
5116 };
5117 vec![target_endpoint_id.into(), constraint_to_migrate.other_entity_id.into()]
5118 };
5119 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5120 segments: constraint_segments,
5121 }));
5122 }
5123
5124 let mut distance_constraints_to_re_add: Vec<(
5126 crate::frontend::api::Number,
5127 crate::frontend::sketch::ConstraintSource,
5128 )> = Vec::new();
5129 if let (Some(original_start_id), Some(original_end_id)) =
5130 (original_segment_start_point_id, original_segment_end_point_id)
5131 {
5132 for obj in &edit_scene_graph_delta.new_graph.objects {
5133 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5134 continue;
5135 };
5136
5137 let Constraint::Distance(distance) = constraint else {
5138 continue;
5139 };
5140
5141 let references_start = distance.contains_point(original_start_id);
5142 let references_end = distance.contains_point(original_end_id);
5143
5144 if references_start && references_end {
5145 distance_constraints_to_re_add.push((distance.distance, distance.source.clone()));
5146 }
5147 }
5148 }
5149
5150 if let Some(original_start_id) = original_segment_start_point_id {
5152 for (distance_value, source) in distance_constraints_to_re_add {
5153 batch_constraints.push(Constraint::Distance(crate::frontend::sketch::Distance {
5154 points: vec![original_start_id.into(), new_segment_end_point_id.into()],
5155 distance: distance_value,
5156 source,
5157 }));
5158 }
5159 }
5160
5161 if let Some(new_center_id) = new_segment_center_point_id {
5163 for (constraint, original_center_id) in center_point_constraints_to_migrate {
5164 let center_rewrite_map = std::collections::HashMap::from([(original_center_id, new_center_id)]);
5165 if let Some(rewritten) = rewrite_constraint_with_map(&constraint, ¢er_rewrite_map)
5166 && matches!(rewritten, Constraint::Coincident(_) | Constraint::Distance(_))
5167 {
5168 batch_constraints.push(rewritten);
5169 }
5170 }
5171 }
5172
5173 let mut angle_rewrite_map = std::collections::HashMap::from([(*segment_id, new_segment_id)]);
5175 if let Some(original_end_id) = original_segment_end_point_id {
5176 angle_rewrite_map.insert(original_end_id, new_segment_end_point_id);
5177 }
5178 for obj in &edit_scene_graph_delta.new_graph.objects {
5179 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5180 continue;
5181 };
5182
5183 let should_migrate = match constraint {
5184 Constraint::Parallel(parallel) => parallel.lines.contains(segment_id),
5185 Constraint::Perpendicular(perpendicular) => perpendicular.lines.contains(segment_id),
5186 Constraint::Midpoint(midpoint) => {
5187 midpoint.segment == *segment_id
5188 || original_segment_end_point_id.is_some_and(|end_id| midpoint.point == end_id)
5189 }
5190 Constraint::Horizontal(Horizontal::Line { line }) => line == segment_id,
5191 Constraint::Horizontal(Horizontal::Points { points }) => original_segment_end_point_id
5192 .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
5193 Constraint::Vertical(Vertical::Line { line }) => line == segment_id,
5194 Constraint::Vertical(Vertical::Points { points }) => original_segment_end_point_id
5195 .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
5196 _ => false,
5197 };
5198
5199 if should_migrate
5200 && let Some(migrated_constraint) = rewrite_constraint_with_map(constraint, &angle_rewrite_map)
5201 && matches!(
5202 migrated_constraint,
5203 Constraint::Midpoint(_)
5204 | Constraint::Parallel(_)
5205 | Constraint::Perpendicular(_)
5206 | Constraint::Horizontal(_)
5207 | Constraint::Vertical(_)
5208 )
5209 {
5210 batch_constraints.push(migrated_constraint);
5211 }
5212 }
5213
5214 let constraint_object_ids: Vec<ObjectId> = constraints_to_delete.to_vec();
5216
5217 let batch_result = frontend
5218 .batch_split_segment_operations(
5219 ctx,
5220 version,
5221 sketch_id,
5222 Vec::new(), batch_constraints,
5224 constraint_object_ids,
5225 crate::frontend::sketch::NewSegmentInfo {
5226 segment_id: new_segment_id,
5227 start_point_id: new_segment_start_point_id,
5228 end_point_id: new_segment_end_point_id,
5229 center_point_id: new_segment_center_point_id,
5230 },
5231 )
5232 .await
5233 .map_err(|e| format!("Failed to batch split segment operations: {}", e.error.message()));
5234 if let Ok((_, ref batch_delta)) = batch_result {
5236 invalidates_ids = invalidates_ids || batch_delta.invalidates_ids;
5237 }
5238 batch_result
5239 }
5240 };
5241
5242 match operation_result {
5243 Ok((source_delta, scene_graph_delta)) => {
5244 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
5246 last_result = Some((source_delta, scene_graph_delta.clone()));
5247 }
5248 Err(e) => {
5249 crate::logln!("Error executing trim operation {}: {}", op_index, e);
5250 }
5252 }
5253
5254 op_index += consumed_ops;
5255 }
5256
5257 let (source_delta, mut scene_graph_delta) =
5258 last_result.ok_or_else(|| "No operations were executed successfully".to_string())?;
5259 scene_graph_delta.invalidates_ids = invalidates_ids;
5261 Ok((source_delta, scene_graph_delta))
5262}