1use std::f64::consts::TAU;
2
3use indexmap::IndexMap;
4use indexmap::IndexSet;
5use kcl_api::UnitLength;
6
7use crate::execution::ArtifactId;
8use crate::execution::types::adjust_length;
9use crate::front::Horizontal;
10use crate::front::Vertical;
11use crate::frontend::api::Number;
12use crate::frontend::api::Object;
13use crate::frontend::api::ObjectId;
14use crate::frontend::api::ObjectKind;
15use crate::frontend::sketch::ArcDirection;
16use crate::frontend::sketch::Constraint;
17use crate::frontend::sketch::ConstraintSegment;
18use crate::frontend::sketch::Segment;
19use crate::frontend::sketch::SegmentCtor;
20use crate::pretty::NumericSuffix;
21use crate::util::MathExt;
22
23#[cfg(test)]
24mod tests;
25
26const EPSILON_PARALLEL: f64 = 1e-10;
28const EPSILON_POINT_ON_SEGMENT: f64 = 1e-6;
29const EPSILON_COINCIDENT_TERMINATION_SNAP: f64 = 5e-2;
30
31fn suffix_to_unit(suffix: NumericSuffix) -> UnitLength {
33 match suffix {
34 NumericSuffix::Mm => UnitLength::Millimeters,
35 NumericSuffix::Cm => UnitLength::Centimeters,
36 NumericSuffix::M => UnitLength::Meters,
37 NumericSuffix::Inch => UnitLength::Inches,
38 NumericSuffix::Ft => UnitLength::Feet,
39 NumericSuffix::Yd => UnitLength::Yards,
40 _ => UnitLength::Millimeters,
41 }
42}
43
44fn number_to_unit(n: &Number, target_unit: UnitLength) -> f64 {
46 adjust_length(suffix_to_unit(n.units), n.value, target_unit).0
47}
48
49fn unit_to_number(value: f64, source_unit: UnitLength, target_suffix: NumericSuffix) -> Number {
51 let (value, _) = adjust_length(source_unit, value, suffix_to_unit(target_suffix));
52 Number {
53 value,
54 units: target_suffix,
55 }
56}
57
58fn normalize_trim_points_to_unit(points: &[Coords2d], default_unit: UnitLength) -> Vec<Coords2d> {
60 points
61 .iter()
62 .map(|point| Coords2d {
63 x: adjust_length(UnitLength::Millimeters, point.x, default_unit).0,
64 y: adjust_length(UnitLength::Millimeters, point.y, default_unit).0,
65 })
66 .collect()
67}
68
69#[derive(Debug, Clone, Copy)]
71pub struct Coords2d {
72 pub x: f64,
73 pub y: f64,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum LineEndpoint {
79 Start,
80 End,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum ArcPoint {
86 Start,
87 End,
88 Center,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93enum CirclePoint {
94 Start,
95 Center,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum TrimDirection {
101 Left,
102 Right,
103}
104
105#[derive(Debug, Clone)]
113pub enum TrimItem {
114 Spawn {
115 trim_spawn_seg_id: ObjectId,
116 trim_spawn_coords: Coords2d,
117 next_index: usize,
118 },
119 None {
120 next_index: usize,
121 },
122}
123
124#[derive(Debug, Clone)]
131pub enum TrimTermination {
132 SegEndPoint {
133 trim_termination_coords: Coords2d,
134 },
135 Intersection {
136 trim_termination_coords: Coords2d,
137 intersecting_seg_id: ObjectId,
138 },
139 TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
140 trim_termination_coords: Coords2d,
141 intersecting_seg_id: ObjectId,
142 other_segment_point_id: ObjectId,
143 },
144}
145
146#[derive(Debug, Clone)]
148pub struct TrimTerminations {
149 pub left_side: TrimTermination,
150 pub right_side: TrimTermination,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum AttachToEndpoint {
156 Start,
157 End,
158 Segment,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum EndpointChanged {
164 Start,
165 End,
166}
167
168#[derive(Debug, Clone)]
170pub struct CoincidentData {
171 pub intersecting_seg_id: ObjectId,
172 pub intersecting_endpoint_point_id: Option<ObjectId>,
173 pub existing_point_segment_constraint_id: Option<ObjectId>,
174}
175
176#[derive(Debug, Clone)]
178pub struct ConstraintToMigrate {
179 pub constraint_id: ObjectId,
180 pub other_entity_id: ObjectId,
181 pub is_point_point: bool,
184 pub attach_to_endpoint: AttachToEndpoint,
185}
186
187#[derive(Debug, Clone)]
189#[allow(clippy::large_enum_variant)]
190enum TrimPlan {
191 DeleteSegment {
192 segment_id: ObjectId,
193 },
194 TailCut {
195 segment_id: ObjectId,
196 endpoint_changed: EndpointChanged,
197 ctor: SegmentCtor,
198 segment_or_point_to_make_coincident_to: ObjectId,
199 intersecting_endpoint_point_id: Option<ObjectId>,
200 constraint_ids_to_delete: Vec<ObjectId>,
201 additional_edited_segment_ids: Vec<ObjectId>,
202 },
203 TailCutControlPointSpline {
204 segment_id: ObjectId,
205 ctor: SegmentCtor,
206 constraint_ids_to_delete: Vec<ObjectId>,
207 },
208 ReplaceCircleWithArc {
209 circle_id: ObjectId,
210 arc_start_coords: Coords2d,
211 arc_end_coords: Coords2d,
212 arc_start_termination: Box<TrimTermination>,
213 arc_end_termination: Box<TrimTermination>,
214 },
215 SplitSegment {
216 segment_id: ObjectId,
217 left_trim_coords: Coords2d,
218 right_trim_coords: Coords2d,
219 original_end_coords: Coords2d,
220 left_side: Box<TrimTermination>,
221 right_side: Box<TrimTermination>,
222 left_side_coincident_data: CoincidentData,
223 right_side_coincident_data: CoincidentData,
224 constraints_to_migrate: Vec<ConstraintToMigrate>,
225 constraints_to_delete: Vec<ObjectId>,
226 },
227 SplitControlPointSpline {
228 segment_id: ObjectId,
229 left_ctor: SegmentCtor,
230 right_ctor: SegmentCtor,
231 left_side: Box<TrimTermination>,
232 right_side: Box<TrimTermination>,
233 constraint_ids_to_delete: Vec<ObjectId>,
234 },
235}
236
237fn lower_trim_plan(plan: &TrimPlan) -> Vec<TrimOperation> {
238 match plan {
239 TrimPlan::DeleteSegment { segment_id } => vec![TrimOperation::SimpleTrim {
240 segment_to_trim_id: *segment_id,
241 }],
242 TrimPlan::TailCut {
243 segment_id,
244 endpoint_changed,
245 ctor,
246 segment_or_point_to_make_coincident_to,
247 intersecting_endpoint_point_id,
248 constraint_ids_to_delete,
249 additional_edited_segment_ids,
250 } => {
251 let mut ops = vec![
252 TrimOperation::EditSegment {
253 segment_id: *segment_id,
254 ctor: ctor.clone(),
255 endpoint_changed: *endpoint_changed,
256 additional_edited_segment_ids: additional_edited_segment_ids.clone(),
257 },
258 TrimOperation::AddCoincidentConstraint {
259 segment_id: *segment_id,
260 endpoint_changed: *endpoint_changed,
261 segment_or_point_to_make_coincident_to: *segment_or_point_to_make_coincident_to,
262 intersecting_endpoint_point_id: *intersecting_endpoint_point_id,
263 },
264 ];
265 if !constraint_ids_to_delete.is_empty() {
266 ops.push(TrimOperation::DeleteConstraints {
267 constraint_ids: constraint_ids_to_delete.clone(),
268 });
269 }
270 ops
271 }
272 TrimPlan::TailCutControlPointSpline {
273 segment_id,
274 ctor,
275 constraint_ids_to_delete,
276 } => {
277 let mut ops = vec![TrimOperation::EditControlPointSpline {
278 segment_id: *segment_id,
279 ctor: ctor.clone(),
280 }];
281 if !constraint_ids_to_delete.is_empty() {
282 ops.push(TrimOperation::DeleteConstraints {
283 constraint_ids: constraint_ids_to_delete.clone(),
284 });
285 }
286 ops
287 }
288 TrimPlan::ReplaceCircleWithArc {
289 circle_id,
290 arc_start_coords,
291 arc_end_coords,
292 arc_start_termination,
293 arc_end_termination,
294 } => vec![TrimOperation::ReplaceCircleWithArc {
295 circle_id: *circle_id,
296 arc_start_coords: *arc_start_coords,
297 arc_end_coords: *arc_end_coords,
298 arc_start_termination: arc_start_termination.clone(),
299 arc_end_termination: arc_end_termination.clone(),
300 }],
301 TrimPlan::SplitSegment {
302 segment_id,
303 left_trim_coords,
304 right_trim_coords,
305 original_end_coords,
306 left_side,
307 right_side,
308 left_side_coincident_data,
309 right_side_coincident_data,
310 constraints_to_migrate,
311 constraints_to_delete,
312 } => vec![TrimOperation::SplitSegment {
313 segment_id: *segment_id,
314 left_trim_coords: *left_trim_coords,
315 right_trim_coords: *right_trim_coords,
316 original_end_coords: *original_end_coords,
317 left_side: left_side.clone(),
318 right_side: right_side.clone(),
319 left_side_coincident_data: left_side_coincident_data.clone(),
320 right_side_coincident_data: right_side_coincident_data.clone(),
321 constraints_to_migrate: constraints_to_migrate.clone(),
322 constraints_to_delete: constraints_to_delete.clone(),
323 }],
324 TrimPlan::SplitControlPointSpline {
325 segment_id,
326 left_ctor,
327 right_ctor,
328 left_side,
329 right_side,
330 constraint_ids_to_delete,
331 } => vec![TrimOperation::SplitControlPointSpline {
332 segment_id: *segment_id,
333 left_ctor: left_ctor.clone(),
334 right_ctor: right_ctor.clone(),
335 left_side: left_side.clone(),
336 right_side: right_side.clone(),
337 constraint_ids_to_delete: constraint_ids_to_delete.clone(),
338 }],
339 }
340}
341
342fn trim_plan_modifies_geometry(plan: &TrimPlan) -> bool {
343 matches!(
344 plan,
345 TrimPlan::DeleteSegment { .. }
346 | TrimPlan::TailCut { .. }
347 | TrimPlan::TailCutControlPointSpline { .. }
348 | TrimPlan::ReplaceCircleWithArc { .. }
349 | TrimPlan::SplitSegment { .. }
350 | TrimPlan::SplitControlPointSpline { .. }
351 )
352}
353
354fn rewrite_object_id(id: ObjectId, rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>) -> ObjectId {
355 rewrite_map.get(&id).copied().unwrap_or(id)
356}
357
358fn rewrite_constraint_segment(
359 segment: crate::frontend::sketch::ConstraintSegment,
360 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
361) -> crate::frontend::sketch::ConstraintSegment {
362 match segment {
363 crate::frontend::sketch::ConstraintSegment::Segment(id) => {
364 crate::frontend::sketch::ConstraintSegment::Segment(rewrite_object_id(id, rewrite_map))
365 }
366 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
367 crate::frontend::sketch::ConstraintSegment::Origin(origin)
368 }
369 }
370}
371
372fn rewrite_constraint_segments(
373 segments: &[crate::frontend::sketch::ConstraintSegment],
374 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
375) -> Vec<crate::frontend::sketch::ConstraintSegment> {
376 segments
377 .iter()
378 .copied()
379 .map(|segment| rewrite_constraint_segment(segment, rewrite_map))
380 .collect()
381}
382
383fn constraint_segments_reference_any(
384 segments: &[crate::frontend::sketch::ConstraintSegment],
385 ids: &std::collections::HashSet<ObjectId>,
386) -> bool {
387 segments.iter().any(|segment| match segment {
388 crate::frontend::sketch::ConstraintSegment::Segment(id) => ids.contains(id),
389 crate::frontend::sketch::ConstraintSegment::Origin(_) => false,
390 })
391}
392
393fn rewrite_constraint_with_map(
394 constraint: &Constraint,
395 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
396) -> Option<Constraint> {
397 match constraint {
401 Constraint::Coincident(coincident) => Some(Constraint::Coincident(crate::frontend::sketch::Coincident {
402 segments: rewrite_constraint_segments(&coincident.segments, rewrite_map),
403 })),
404 Constraint::Distance(distance) => Some(Constraint::Distance(crate::frontend::sketch::Distance {
405 points: rewrite_constraint_segments(&distance.points, rewrite_map),
406 distance: distance.distance,
407 label_position: distance.label_position.clone(),
408 source: distance.source.clone(),
409 })),
410 Constraint::HorizontalDistance(distance) => {
411 Some(Constraint::HorizontalDistance(crate::frontend::sketch::Distance {
412 points: rewrite_constraint_segments(&distance.points, rewrite_map),
413 distance: distance.distance,
414 label_position: distance.label_position.clone(),
415 source: distance.source.clone(),
416 }))
417 }
418 Constraint::VerticalDistance(distance) => {
419 Some(Constraint::VerticalDistance(crate::frontend::sketch::Distance {
420 points: rewrite_constraint_segments(&distance.points, rewrite_map),
421 distance: distance.distance,
422 label_position: distance.label_position.clone(),
423 source: distance.source.clone(),
424 }))
425 }
426 Constraint::Radius(radius) => Some(Constraint::Radius(crate::frontend::sketch::Radius {
427 arc: rewrite_object_id(radius.arc, rewrite_map),
428 radius: radius.radius,
429 label_position: radius.label_position.clone(),
430 source: radius.source.clone(),
431 })),
432 Constraint::Diameter(diameter) => Some(Constraint::Diameter(crate::frontend::sketch::Diameter {
433 arc: rewrite_object_id(diameter.arc, rewrite_map),
434 diameter: diameter.diameter,
435 label_position: diameter.label_position.clone(),
436 source: diameter.source.clone(),
437 })),
438 Constraint::EqualRadius(equal_radius) => Some(Constraint::EqualRadius(crate::frontend::sketch::EqualRadius {
439 input: equal_radius
440 .input
441 .iter()
442 .map(|id| rewrite_object_id(*id, rewrite_map))
443 .collect(),
444 })),
445 Constraint::Midpoint(midpoint) => Some(Constraint::Midpoint(crate::frontend::sketch::Midpoint {
446 point: rewrite_constraint_segment(midpoint.point, rewrite_map),
447 segment: rewrite_object_id(midpoint.segment, rewrite_map),
448 })),
449 Constraint::Tangent(tangent) => Some(Constraint::Tangent(crate::frontend::sketch::Tangent {
450 input: tangent
451 .input
452 .iter()
453 .map(|id| rewrite_object_id(*id, rewrite_map))
454 .collect(),
455 })),
456 Constraint::Symmetric(symmetric) => Some(Constraint::Symmetric(crate::frontend::sketch::Symmetric {
457 input: symmetric
458 .input
459 .iter()
460 .map(|id| rewrite_object_id(*id, rewrite_map))
461 .collect(),
462 axis: rewrite_object_id(symmetric.axis, rewrite_map),
463 })),
464 Constraint::Parallel(parallel) => Some(Constraint::Parallel(crate::frontend::sketch::Parallel {
465 lines: parallel
466 .lines
467 .iter()
468 .map(|id| rewrite_object_id(*id, rewrite_map))
469 .collect(),
470 })),
471 Constraint::Perpendicular(perpendicular) => {
472 Some(Constraint::Perpendicular(crate::frontend::sketch::Perpendicular {
473 lines: perpendicular
474 .lines
475 .iter()
476 .map(|id| rewrite_object_id(*id, rewrite_map))
477 .collect(),
478 }))
479 }
480 Constraint::Horizontal(horizontal) => match horizontal {
481 crate::front::Horizontal::Line { line } => {
482 Some(Constraint::Horizontal(crate::frontend::sketch::Horizontal::Line {
483 line: rewrite_object_id(*line, rewrite_map),
484 }))
485 }
486 crate::front::Horizontal::Points { points } => Some(Constraint::Horizontal(Horizontal::Points {
487 points: points
488 .iter()
489 .map(|point| match point {
490 crate::frontend::sketch::ConstraintSegment::Segment(point) => {
491 crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
492 }
493 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
494 crate::frontend::sketch::ConstraintSegment::Origin(*origin)
495 }
496 })
497 .collect(),
498 })),
499 },
500 Constraint::Vertical(vertical) => match vertical {
501 crate::front::Vertical::Line { line } => {
502 Some(Constraint::Vertical(crate::frontend::sketch::Vertical::Line {
503 line: rewrite_object_id(*line, rewrite_map),
504 }))
505 }
506 crate::front::Vertical::Points { points } => Some(Constraint::Vertical(Vertical::Points {
507 points: points
508 .iter()
509 .map(|point| match point {
510 crate::frontend::sketch::ConstraintSegment::Segment(point) => {
511 crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
512 }
513 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
514 crate::frontend::sketch::ConstraintSegment::Origin(*origin)
515 }
516 })
517 .collect(),
518 })),
519 },
520 Constraint::Angle(_) | Constraint::Fixed(_) | Constraint::LinesEqualLength(_) => None,
521 }
522}
523
524fn point_axis_constraint_references_point(constraint: &Constraint, point_id: ObjectId) -> bool {
525 match constraint {
528 Constraint::Horizontal(Horizontal::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
529 Constraint::Vertical(Vertical::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
530 Constraint::Angle(_)
531 | Constraint::Coincident(_)
532 | Constraint::Diameter(_)
533 | Constraint::Distance(_)
534 | Constraint::EqualRadius(_)
535 | Constraint::Fixed(_)
536 | Constraint::Horizontal(Horizontal::Line { .. })
537 | Constraint::HorizontalDistance(_)
538 | Constraint::LinesEqualLength(_)
539 | Constraint::Midpoint(_)
540 | Constraint::Parallel(_)
541 | Constraint::Perpendicular(_)
542 | Constraint::Radius(_)
543 | Constraint::Symmetric(_)
544 | Constraint::Tangent(_)
545 | Constraint::Vertical(Vertical::Line { .. })
546 | Constraint::VerticalDistance(_) => false,
547 }
548}
549
550fn owner_or_segment_id(objects: &[Object], segment_id: ObjectId) -> ObjectId {
551 if let Some(segment_object) = objects.iter().find(|obj| obj.id == segment_id)
552 && let ObjectKind::Segment {
553 segment: Segment::Point(point),
554 } = &segment_object.kind
555 && let Some(owner_id) = point.owner
556 {
557 owner_id
558 } else {
559 segment_id
560 }
561}
562
563fn segment_id_is_or_is_owned_by_curve(objects: &[Object], segment_id: ObjectId) -> bool {
564 objects.iter().find(|obj| obj.id == segment_id).is_some_and(|object| {
565 let ObjectKind::Segment { segment } = &object.kind else {
566 return false;
567 };
568
569 match segment {
570 Segment::Arc(_) | Segment::Circle(_) => true,
571 Segment::Point(point) => point.owner.is_some_and(|owner_id| {
572 objects.iter().find(|obj| obj.id == owner_id).is_some_and(|owner| {
573 matches!(
574 owner.kind,
575 ObjectKind::Segment {
576 segment: Segment::Arc(_) | Segment::Circle(_)
577 }
578 )
579 })
580 }),
581 _ => false,
582 }
583 })
584}
585
586fn sketch_segment_ids_for_segment(objects: &[Object], segment_id: ObjectId) -> Vec<ObjectId> {
587 objects
588 .iter()
589 .find_map(|obj| {
590 let ObjectKind::Sketch(sketch) = &obj.kind else {
591 return None;
592 };
593
594 sketch.segments.contains(&segment_id).then(|| sketch.segments.clone())
595 })
596 .unwrap_or_default()
597}
598
599#[derive(Debug, Clone)]
600#[allow(clippy::large_enum_variant)]
601pub enum TrimOperation {
602 SimpleTrim {
603 segment_to_trim_id: ObjectId,
604 },
605 EditSegment {
606 segment_id: ObjectId,
607 ctor: SegmentCtor,
608 endpoint_changed: EndpointChanged,
609 additional_edited_segment_ids: Vec<ObjectId>,
610 },
611 EditControlPointSpline {
612 segment_id: ObjectId,
613 ctor: SegmentCtor,
614 },
615 AddCoincidentConstraint {
616 segment_id: ObjectId,
617 endpoint_changed: EndpointChanged,
618 segment_or_point_to_make_coincident_to: ObjectId,
619 intersecting_endpoint_point_id: Option<ObjectId>,
620 },
621 SplitSegment {
622 segment_id: ObjectId,
623 left_trim_coords: Coords2d,
624 right_trim_coords: Coords2d,
625 original_end_coords: Coords2d,
626 left_side: Box<TrimTermination>,
627 right_side: Box<TrimTermination>,
628 left_side_coincident_data: CoincidentData,
629 right_side_coincident_data: CoincidentData,
630 constraints_to_migrate: Vec<ConstraintToMigrate>,
631 constraints_to_delete: Vec<ObjectId>,
632 },
633 SplitControlPointSpline {
634 segment_id: ObjectId,
635 left_ctor: SegmentCtor,
636 right_ctor: SegmentCtor,
637 left_side: Box<TrimTermination>,
638 right_side: Box<TrimTermination>,
639 constraint_ids_to_delete: Vec<ObjectId>,
640 },
641 ReplaceCircleWithArc {
642 circle_id: ObjectId,
643 arc_start_coords: Coords2d,
644 arc_end_coords: Coords2d,
645 arc_start_termination: Box<TrimTermination>,
646 arc_end_termination: Box<TrimTermination>,
647 },
648 DeleteConstraints {
649 constraint_ids: Vec<ObjectId>,
650 },
651}
652
653pub fn is_point_on_line_segment(
657 point: Coords2d,
658 segment_start: Coords2d,
659 segment_end: Coords2d,
660 epsilon: f64,
661) -> Option<Coords2d> {
662 let dx = segment_end.x - segment_start.x;
663 let dy = segment_end.y - segment_start.y;
664 let segment_length_sq = dx * dx + dy * dy;
665
666 if segment_length_sq < EPSILON_PARALLEL {
667 let dist_sq = (point.x - segment_start.x) * (point.x - segment_start.x)
669 + (point.y - segment_start.y) * (point.y - segment_start.y);
670 if dist_sq <= epsilon * epsilon {
671 return Some(point);
672 }
673 return None;
674 }
675
676 let point_dx = point.x - segment_start.x;
677 let point_dy = point.y - segment_start.y;
678 let projection_param = (point_dx * dx + point_dy * dy) / segment_length_sq;
679
680 if !(0.0..=1.0).contains(&projection_param) {
682 return None;
683 }
684
685 let projected_point = Coords2d {
687 x: segment_start.x + projection_param * dx,
688 y: segment_start.y + projection_param * dy,
689 };
690
691 let dist_dx = point.x - projected_point.x;
693 let dist_dy = point.y - projected_point.y;
694 let distance_sq = dist_dx * dist_dx + dist_dy * dist_dy;
695
696 if distance_sq <= epsilon * epsilon {
697 Some(point)
698 } else {
699 None
700 }
701}
702
703pub fn line_segment_intersection(
707 line1_start: Coords2d,
708 line1_end: Coords2d,
709 line2_start: Coords2d,
710 line2_end: Coords2d,
711 epsilon: f64,
712) -> Option<Coords2d> {
713 if let Some(point) = is_point_on_line_segment(line1_start, line2_start, line2_end, epsilon) {
715 return Some(point);
716 }
717
718 if let Some(point) = is_point_on_line_segment(line1_end, line2_start, line2_end, epsilon) {
719 return Some(point);
720 }
721
722 if let Some(point) = is_point_on_line_segment(line2_start, line1_start, line1_end, epsilon) {
723 return Some(point);
724 }
725
726 if let Some(point) = is_point_on_line_segment(line2_end, line1_start, line1_end, epsilon) {
727 return Some(point);
728 }
729
730 let x1 = line1_start.x;
732 let y1 = line1_start.y;
733 let x2 = line1_end.x;
734 let y2 = line1_end.y;
735 let x3 = line2_start.x;
736 let y3 = line2_start.y;
737 let x4 = line2_end.x;
738 let y4 = line2_end.y;
739
740 let denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
741 if denominator.abs() < EPSILON_PARALLEL {
742 return None;
744 }
745
746 let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator;
747 let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominator;
748
749 if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
751 let x = x1 + t * (x2 - x1);
752 let y = y1 + t * (y2 - y1);
753 return Some(Coords2d { x, y });
754 }
755
756 None
757}
758
759pub fn project_point_onto_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
764 let dx = segment_end.x - segment_start.x;
765 let dy = segment_end.y - segment_start.y;
766 let segment_length_sq = dx * dx + dy * dy;
767
768 if segment_length_sq < EPSILON_PARALLEL {
769 return 0.0;
771 }
772
773 let point_dx = point.x - segment_start.x;
774 let point_dy = point.y - segment_start.y;
775
776 (point_dx * dx + point_dy * dy) / segment_length_sq
777}
778
779pub fn perpendicular_distance_to_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
783 let dx = segment_end.x - segment_start.x;
784 let dy = segment_end.y - segment_start.y;
785 let segment_length_sq = dx * dx + dy * dy;
786
787 if segment_length_sq < EPSILON_PARALLEL {
788 let dist_dx = point.x - segment_start.x;
790 let dist_dy = point.y - segment_start.y;
791 return (dist_dx * dist_dx + dist_dy * dist_dy).sqrt();
792 }
793
794 let point_dx = point.x - segment_start.x;
796 let point_dy = point.y - segment_start.y;
797
798 let t = (point_dx * dx + point_dy * dy) / segment_length_sq;
800
801 let clamped_t = t.clamp(0.0, 1.0);
803 let closest_point = Coords2d {
804 x: segment_start.x + clamped_t * dx,
805 y: segment_start.y + clamped_t * dy,
806 };
807
808 let dist_dx = point.x - closest_point.x;
810 let dist_dy = point.y - closest_point.y;
811 (dist_dx * dist_dx + dist_dy * dist_dy).sqrt()
812}
813
814fn is_point_on_arc(point: Coords2d, center: Coords2d, start: Coords2d, end: Coords2d, epsilon: f64) -> bool {
818 let radius = ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
820
821 let dist_from_center =
823 ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
824 if (dist_from_center - radius).abs() > epsilon {
825 return false;
826 }
827
828 let start_angle = libm::atan2(start.y - center.y, start.x - center.x);
830 let end_angle = libm::atan2(end.y - center.y, end.x - center.x);
831 let point_angle = libm::atan2(point.y - center.y, point.x - center.x);
832
833 let normalize_angle = |angle: f64| -> f64 {
835 if !angle.is_finite() {
836 return angle;
837 }
838 let mut normalized = angle;
839 while normalized < 0.0 {
840 normalized += TAU;
841 }
842 while normalized >= TAU {
843 normalized -= TAU;
844 }
845 normalized
846 };
847
848 let normalized_start = normalize_angle(start_angle);
849 let normalized_end = normalize_angle(end_angle);
850 let normalized_point = normalize_angle(point_angle);
851
852 if normalized_start < normalized_end {
856 normalized_point >= normalized_start && normalized_point <= normalized_end
858 } else {
859 normalized_point >= normalized_start || normalized_point <= normalized_end
861 }
862}
863
864fn line_arc_intersections(
868 line_start: Coords2d,
869 line_end: Coords2d,
870 arc_center: Coords2d,
871 arc_start: Coords2d,
872 arc_end: Coords2d,
873 epsilon: f64,
874) -> Vec<(f64, Coords2d)> {
875 let radius = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
877 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
878 .sqrt();
879
880 let translated_line_start = Coords2d {
882 x: line_start.x - arc_center.x,
883 y: line_start.y - arc_center.y,
884 };
885 let translated_line_end = Coords2d {
886 x: line_end.x - arc_center.x,
887 y: line_end.y - arc_center.y,
888 };
889
890 let dx = translated_line_end.x - translated_line_start.x;
892 let dy = translated_line_end.y - translated_line_start.y;
893
894 let a = dx * dx + dy * dy;
901 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
902 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
903 - radius * radius;
904
905 let discriminant = b * b - 4.0 * a * c;
906
907 if discriminant < 0.0 {
908 return Vec::new();
910 }
911
912 if a.abs() < EPSILON_PARALLEL {
913 let dist_from_center = (translated_line_start.x * translated_line_start.x
915 + translated_line_start.y * translated_line_start.y)
916 .sqrt();
917 if (dist_from_center - radius).abs() <= epsilon {
918 let point = line_start;
920 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
921 return vec![(0.0, point)];
922 }
923 }
924 return Vec::new();
925 }
926
927 let sqrt_discriminant = discriminant.sqrt();
928 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
929 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
930
931 let mut candidates: Vec<(f64, Coords2d)> = Vec::new();
933 if (0.0..=1.0).contains(&t1) {
934 let point = Coords2d {
935 x: line_start.x + t1 * (line_end.x - line_start.x),
936 y: line_start.y + t1 * (line_end.y - line_start.y),
937 };
938 candidates.push((t1, point));
939 }
940 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
941 let point = Coords2d {
942 x: line_start.x + t2 * (line_end.x - line_start.x),
943 y: line_start.y + t2 * (line_end.y - line_start.y),
944 };
945 candidates.push((t2, point));
946 }
947
948 candidates.retain(|(_, point)| is_point_on_arc(*point, arc_center, arc_start, arc_end, epsilon));
949 candidates.sort_by(|(a_t, _), (b_t, _)| a_t.partial_cmp(b_t).unwrap_or(std::cmp::Ordering::Equal));
950 candidates
951}
952
953fn line_arc_intersection(
957 line_start: Coords2d,
958 line_end: Coords2d,
959 arc_center: Coords2d,
960 arc_start: Coords2d,
961 arc_end: Coords2d,
962 epsilon: f64,
963) -> Option<Coords2d> {
964 line_arc_intersections(line_start, line_end, arc_center, arc_start, arc_end, epsilon)
965 .into_iter()
966 .map(|(_, point)| point)
967 .next()
968}
969
970fn line_circle_intersections(
975 line_start: Coords2d,
976 line_end: Coords2d,
977 circle_center: Coords2d,
978 radius: f64,
979 epsilon: f64,
980) -> Vec<(f64, Coords2d)> {
981 let translated_line_start = Coords2d {
983 x: line_start.x - circle_center.x,
984 y: line_start.y - circle_center.y,
985 };
986 let translated_line_end = Coords2d {
987 x: line_end.x - circle_center.x,
988 y: line_end.y - circle_center.y,
989 };
990
991 let dx = translated_line_end.x - translated_line_start.x;
992 let dy = translated_line_end.y - translated_line_start.y;
993 let a = dx * dx + dy * dy;
994 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
995 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
996 - radius * radius;
997
998 if a.abs() < EPSILON_PARALLEL {
999 return Vec::new();
1000 }
1001
1002 let discriminant = b * b - 4.0 * a * c;
1003 if discriminant < 0.0 {
1004 return Vec::new();
1005 }
1006
1007 let sqrt_discriminant = discriminant.sqrt();
1008 let mut intersections = Vec::new();
1009
1010 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
1011 if (0.0..=1.0).contains(&t1) {
1012 intersections.push((
1013 t1,
1014 Coords2d {
1015 x: line_start.x + t1 * (line_end.x - line_start.x),
1016 y: line_start.y + t1 * (line_end.y - line_start.y),
1017 },
1018 ));
1019 }
1020
1021 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
1022 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
1023 intersections.push((
1024 t2,
1025 Coords2d {
1026 x: line_start.x + t2 * (line_end.x - line_start.x),
1027 y: line_start.y + t2 * (line_end.y - line_start.y),
1028 },
1029 ));
1030 }
1031
1032 intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1033 intersections
1034}
1035
1036fn project_point_onto_circle(point: Coords2d, center: Coords2d, start: Coords2d) -> f64 {
1042 let normalize_angle = |angle: f64| -> f64 {
1043 if !angle.is_finite() {
1044 return angle;
1045 }
1046 let mut normalized = angle;
1047 while normalized < 0.0 {
1048 normalized += TAU;
1049 }
1050 while normalized >= TAU {
1051 normalized -= TAU;
1052 }
1053 normalized
1054 };
1055
1056 let start_angle = normalize_angle(libm::atan2(start.y - center.y, start.x - center.x));
1057 let point_angle = normalize_angle(libm::atan2(point.y - center.y, point.x - center.x));
1058 let delta_ccw = (point_angle - start_angle).rem_euclid(TAU);
1059 delta_ccw / TAU
1060}
1061
1062fn is_point_on_circle(point: Coords2d, center: Coords2d, radius: f64, epsilon: f64) -> bool {
1063 let dist = ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
1064 (dist - radius).abs() <= epsilon
1065}
1066
1067pub fn project_point_onto_arc(
1073 point: Coords2d,
1074 arc_center: Coords2d,
1075 arc_start: Coords2d,
1076 arc_end: Coords2d,
1077 direction: ArcDirection,
1078) -> f64 {
1079 let (sweep_start, sweep_end) = direction.ccw_order(arc_start, arc_end);
1080 let t = project_point_onto_ccw_arc(point, arc_center, sweep_start, sweep_end);
1081 if direction.is_clockwise() { 1.0 - t } else { t }
1085}
1086
1087fn project_point_onto_ccw_arc(point: Coords2d, arc_center: Coords2d, arc_start: Coords2d, arc_end: Coords2d) -> f64 {
1090 let start_angle = libm::atan2(arc_start.y - arc_center.y, arc_start.x - arc_center.x);
1092 let end_angle = libm::atan2(arc_end.y - arc_center.y, arc_end.x - arc_center.x);
1093 let point_angle = libm::atan2(point.y - arc_center.y, point.x - arc_center.x);
1094
1095 let normalize_angle = |angle: f64| -> f64 {
1097 if !angle.is_finite() {
1098 return angle;
1099 }
1100 let mut normalized = angle;
1101 while normalized < 0.0 {
1102 normalized += TAU;
1103 }
1104 while normalized >= TAU {
1105 normalized -= TAU;
1106 }
1107 normalized
1108 };
1109
1110 let normalized_start = normalize_angle(start_angle);
1111 let normalized_end = normalize_angle(end_angle);
1112 let normalized_point = normalize_angle(point_angle);
1113
1114 let arc_length = if normalized_start < normalized_end {
1116 normalized_end - normalized_start
1117 } else {
1118 TAU - normalized_start + normalized_end
1120 };
1121
1122 if arc_length < EPSILON_PARALLEL {
1123 return 0.0;
1125 }
1126
1127 let point_arc_length = if normalized_start < normalized_end {
1129 if normalized_point >= normalized_start && normalized_point <= normalized_end {
1130 normalized_point - normalized_start
1131 } else {
1132 let dist_to_start = libm::fmin(
1134 (normalized_point - normalized_start).abs(),
1135 TAU - (normalized_point - normalized_start).abs(),
1136 );
1137 let dist_to_end = libm::fmin(
1138 (normalized_point - normalized_end).abs(),
1139 TAU - (normalized_point - normalized_end).abs(),
1140 );
1141 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
1142 }
1143 } else {
1144 if normalized_point >= normalized_start || normalized_point <= normalized_end {
1146 if normalized_point >= normalized_start {
1147 normalized_point - normalized_start
1148 } else {
1149 TAU - normalized_start + normalized_point
1150 }
1151 } else {
1152 let dist_to_start = libm::fmin(
1154 (normalized_point - normalized_start).abs(),
1155 TAU - (normalized_point - normalized_start).abs(),
1156 );
1157 let dist_to_end = libm::fmin(
1158 (normalized_point - normalized_end).abs(),
1159 TAU - (normalized_point - normalized_end).abs(),
1160 );
1161 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
1162 }
1163 };
1164
1165 point_arc_length / arc_length
1167}
1168
1169fn arc_arc_intersections(
1173 arc1_center: Coords2d,
1174 arc1_start: Coords2d,
1175 arc1_end: Coords2d,
1176 arc2_center: Coords2d,
1177 arc2_start: Coords2d,
1178 arc2_end: Coords2d,
1179 epsilon: f64,
1180) -> Vec<Coords2d> {
1181 let r1 = ((arc1_start.x - arc1_center.x) * (arc1_start.x - arc1_center.x)
1183 + (arc1_start.y - arc1_center.y) * (arc1_start.y - arc1_center.y))
1184 .sqrt();
1185 let r2 = ((arc2_start.x - arc2_center.x) * (arc2_start.x - arc2_center.x)
1186 + (arc2_start.y - arc2_center.y) * (arc2_start.y - arc2_center.y))
1187 .sqrt();
1188
1189 let dx = arc2_center.x - arc1_center.x;
1191 let dy = arc2_center.y - arc1_center.y;
1192 let d = (dx * dx + dy * dy).sqrt();
1193
1194 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon {
1196 return Vec::new();
1198 }
1199
1200 if d < EPSILON_PARALLEL {
1202 return Vec::new();
1204 }
1205
1206 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1209 let h_sq = r1 * r1 - a * a;
1210
1211 if h_sq < 0.0 {
1213 return Vec::new();
1214 }
1215
1216 let h = h_sq.sqrt();
1217
1218 if h.is_nan() {
1220 return Vec::new();
1221 }
1222
1223 let ux = dx / d;
1225 let uy = dy / d;
1226
1227 let px = -uy;
1229 let py = ux;
1230
1231 let mid_point = Coords2d {
1233 x: arc1_center.x + a * ux,
1234 y: arc1_center.y + a * uy,
1235 };
1236
1237 let intersection1 = Coords2d {
1239 x: mid_point.x + h * px,
1240 y: mid_point.y + h * py,
1241 };
1242 let intersection2 = Coords2d {
1243 x: mid_point.x - h * px,
1244 y: mid_point.y - h * py,
1245 };
1246
1247 let mut candidates: Vec<Coords2d> = Vec::new();
1249
1250 if is_point_on_arc(intersection1, arc1_center, arc1_start, arc1_end, epsilon)
1251 && is_point_on_arc(intersection1, arc2_center, arc2_start, arc2_end, epsilon)
1252 {
1253 candidates.push(intersection1);
1254 }
1255
1256 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1257 if is_point_on_arc(intersection2, arc1_center, arc1_start, arc1_end, epsilon)
1259 && is_point_on_arc(intersection2, arc2_center, arc2_start, arc2_end, epsilon)
1260 {
1261 candidates.push(intersection2);
1262 }
1263 }
1264
1265 candidates
1266}
1267
1268fn circle_arc_intersections(
1272 circle_center: Coords2d,
1273 circle_radius: f64,
1274 arc_center: Coords2d,
1275 arc_start: Coords2d,
1276 arc_end: Coords2d,
1277 epsilon: f64,
1278) -> Vec<Coords2d> {
1279 let r1 = circle_radius;
1280 let r2 = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
1281 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
1282 .sqrt();
1283
1284 let dx = arc_center.x - circle_center.x;
1285 let dy = arc_center.y - circle_center.y;
1286 let d = (dx * dx + dy * dy).sqrt();
1287
1288 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon || d < EPSILON_PARALLEL {
1289 return Vec::new();
1290 }
1291
1292 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1293 let h_sq = r1 * r1 - a * a;
1294 if h_sq < 0.0 {
1295 return Vec::new();
1296 }
1297 let h = h_sq.sqrt();
1298 if h.is_nan() {
1299 return Vec::new();
1300 }
1301
1302 let ux = dx / d;
1303 let uy = dy / d;
1304 let px = -uy;
1305 let py = ux;
1306 let mid_point = Coords2d {
1307 x: circle_center.x + a * ux,
1308 y: circle_center.y + a * uy,
1309 };
1310
1311 let intersection1 = Coords2d {
1312 x: mid_point.x + h * px,
1313 y: mid_point.y + h * py,
1314 };
1315 let intersection2 = Coords2d {
1316 x: mid_point.x - h * px,
1317 y: mid_point.y - h * py,
1318 };
1319
1320 let mut intersections = Vec::new();
1321 if is_point_on_arc(intersection1, arc_center, arc_start, arc_end, epsilon) {
1322 intersections.push(intersection1);
1323 }
1324 if ((intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon)
1325 && is_point_on_arc(intersection2, arc_center, arc_start, arc_end, epsilon)
1326 {
1327 intersections.push(intersection2);
1328 }
1329 intersections
1330}
1331
1332fn circle_circle_intersections(
1336 circle1_center: Coords2d,
1337 circle1_radius: f64,
1338 circle2_center: Coords2d,
1339 circle2_radius: f64,
1340 epsilon: f64,
1341) -> Vec<Coords2d> {
1342 let dx = circle2_center.x - circle1_center.x;
1343 let dy = circle2_center.y - circle1_center.y;
1344 let d = (dx * dx + dy * dy).sqrt();
1345
1346 if d > circle1_radius + circle2_radius + epsilon
1347 || d < (circle1_radius - circle2_radius).abs() - epsilon
1348 || d < EPSILON_PARALLEL
1349 {
1350 return Vec::new();
1351 }
1352
1353 let a = (circle1_radius * circle1_radius - circle2_radius * circle2_radius + d * d) / (2.0 * d);
1354 let h_sq = circle1_radius * circle1_radius - a * a;
1355 if h_sq < 0.0 {
1356 return Vec::new();
1357 }
1358
1359 let h = if h_sq <= epsilon { 0.0 } else { h_sq.sqrt() };
1360 if h.is_nan() {
1361 return Vec::new();
1362 }
1363
1364 let ux = dx / d;
1365 let uy = dy / d;
1366 let px = -uy;
1367 let py = ux;
1368
1369 let mid_point = Coords2d {
1370 x: circle1_center.x + a * ux,
1371 y: circle1_center.y + a * uy,
1372 };
1373
1374 let intersection1 = Coords2d {
1375 x: mid_point.x + h * px,
1376 y: mid_point.y + h * py,
1377 };
1378 let intersection2 = Coords2d {
1379 x: mid_point.x - h * px,
1380 y: mid_point.y - h * py,
1381 };
1382
1383 let mut intersections = vec![intersection1];
1384 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1385 intersections.push(intersection2);
1386 }
1387 intersections
1388}
1389
1390fn get_point_coords_from_native(objects: &[Object], point_id: ObjectId, default_unit: UnitLength) -> Option<Coords2d> {
1393 let point_obj = objects.get(point_id.0)?;
1394
1395 let ObjectKind::Segment { segment } = &point_obj.kind else {
1397 return None;
1398 };
1399
1400 let Segment::Point(point) = segment else {
1401 return None;
1402 };
1403
1404 Some(Coords2d {
1406 x: number_to_unit(&point.position.x, default_unit),
1407 y: number_to_unit(&point.position.y, default_unit),
1408 })
1409}
1410
1411pub fn get_position_coords_for_line(
1414 segment_obj: &Object,
1415 which: LineEndpoint,
1416 objects: &[Object],
1417 default_unit: UnitLength,
1418) -> Option<Coords2d> {
1419 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1420 return None;
1421 };
1422
1423 let Segment::Line(line) = segment else {
1424 return None;
1425 };
1426
1427 let point_id = match which {
1429 LineEndpoint::Start => line.start,
1430 LineEndpoint::End => line.end,
1431 };
1432
1433 get_point_coords_from_native(objects, point_id, default_unit)
1434}
1435
1436fn is_point_coincident_with_segment_native(point_id: ObjectId, segment_id: ObjectId, objects: &[Object]) -> bool {
1438 for obj in objects {
1440 let ObjectKind::Constraint { constraint } = &obj.kind else {
1441 continue;
1442 };
1443
1444 let Constraint::Coincident(coincident) = constraint else {
1445 continue;
1446 };
1447
1448 let has_point = coincident.contains_segment(point_id);
1450 let has_segment = coincident.contains_segment(segment_id);
1451
1452 if has_point && has_segment {
1453 return true;
1454 }
1455 }
1456 false
1457}
1458
1459pub fn get_position_coords_from_arc(
1461 segment_obj: &Object,
1462 which: ArcPoint,
1463 objects: &[Object],
1464 default_unit: UnitLength,
1465) -> Option<Coords2d> {
1466 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1467 return None;
1468 };
1469
1470 let Segment::Arc(arc) = segment else {
1471 return None;
1472 };
1473
1474 let point_id = match which {
1476 ArcPoint::Start => arc.start,
1477 ArcPoint::End => arc.end,
1478 ArcPoint::Center => arc.center,
1479 };
1480
1481 get_point_coords_from_native(objects, point_id, default_unit)
1482}
1483
1484fn get_position_coords_from_circle(
1486 segment_obj: &Object,
1487 which: CirclePoint,
1488 objects: &[Object],
1489 default_unit: UnitLength,
1490) -> Option<Coords2d> {
1491 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1492 return None;
1493 };
1494
1495 let Segment::Circle(circle) = segment else {
1496 return None;
1497 };
1498
1499 let point_id = match which {
1500 CirclePoint::Start => circle.start,
1501 CirclePoint::Center => circle.center,
1502 };
1503
1504 get_point_coords_from_native(objects, point_id, default_unit)
1505}
1506
1507#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1509enum CurveKind {
1510 Line,
1511 Circular,
1512 Spline,
1513}
1514
1515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1517enum CurveDomain {
1518 Open,
1519 Closed,
1520}
1521
1522#[derive(Debug, Clone)]
1524struct SampledCurvePoint {
1525 parameter: f64,
1526 point: Coords2d,
1527}
1528
1529#[derive(Debug, Clone)]
1530struct CurveHandle {
1531 segment_id: ObjectId,
1532 kind: CurveKind,
1533 domain: CurveDomain,
1534 start: Coords2d,
1537 end: Coords2d,
1539 center: Option<Coords2d>,
1540 radius: Option<f64>,
1541 direction: ArcDirection,
1544 sampled_points: Option<Vec<SampledCurvePoint>>,
1545}
1546
1547impl CurveHandle {
1548 fn sweep_start_end(&self) -> (Coords2d, Coords2d) {
1553 self.direction.ccw_order(self.start, self.end)
1554 }
1555
1556 fn project_for_trim(&self, point: Coords2d) -> Result<f64, String> {
1557 match (self.kind, self.domain) {
1558 (CurveKind::Line, CurveDomain::Open) => Ok(project_point_onto_segment(point, self.start, self.end)),
1559 (CurveKind::Circular, CurveDomain::Open) => {
1560 let center = self
1561 .center
1562 .ok_or_else(|| format!("Curve {} missing center for arc projection", self.segment_id.0))?;
1563 Ok(project_point_onto_arc(
1564 point,
1565 center,
1566 self.start,
1567 self.end,
1568 self.direction,
1569 ))
1570 }
1571 (CurveKind::Circular, CurveDomain::Closed) => {
1572 let center = self
1573 .center
1574 .ok_or_else(|| format!("Curve {} missing center for circle projection", self.segment_id.0))?;
1575 Ok(project_point_onto_circle(point, center, self.start))
1576 }
1577 (CurveKind::Line, CurveDomain::Closed) => Err(format!(
1578 "Invalid curve state: line {} cannot be closed",
1579 self.segment_id.0
1580 )),
1581 (CurveKind::Spline, CurveDomain::Open) => project_point_onto_sampled_curve(
1582 self.sampled_points.as_deref().ok_or_else(|| {
1583 format!(
1584 "Curve {} missing sampled points for spline projection",
1585 self.segment_id.0
1586 )
1587 })?,
1588 point,
1589 ),
1590 (CurveKind::Spline, CurveDomain::Closed) => Err(format!(
1591 "Invalid curve state: spline {} cannot be closed",
1592 self.segment_id.0
1593 )),
1594 }
1595 }
1596}
1597
1598const CONTROL_POINT_SPLINE_TRIM_SAMPLES_PER_SPAN: usize = 32;
1599
1600fn build_open_uniform_knot_vector(control_count: usize, degree: usize) -> Vec<f64> {
1601 let span_count = control_count.saturating_sub(degree);
1602 let mut knots = vec![0.0; degree + 1];
1603 if span_count > 1 {
1604 for value in 1..span_count {
1605 knots.push(value as f64);
1606 }
1607 }
1608 knots.extend(std::iter::repeat_n(span_count as f64, degree + 1));
1609 knots
1610}
1611
1612fn find_knot_span(parameter: f64, degree: usize, knots: &[f64], control_count: usize) -> usize {
1613 let n = control_count - 1;
1614 if parameter >= knots[n + 1] {
1615 return n;
1616 }
1617 if parameter <= knots[degree] {
1618 return degree;
1619 }
1620
1621 let mut low = degree;
1622 let mut high = n + 1;
1623 let mut mid = (low + high) / 2;
1624 while parameter < knots[mid] || parameter >= knots[mid + 1] {
1625 if parameter < knots[mid] {
1626 high = mid;
1627 } else {
1628 low = mid;
1629 }
1630 mid = (low + high) / 2;
1631 }
1632 mid
1633}
1634
1635fn de_boor_point(parameter: f64, degree: usize, knots: &[f64], controls: &[Coords2d]) -> Coords2d {
1636 let span = find_knot_span(parameter, degree, knots, controls.len());
1637 let mut points = (0..=degree).map(|j| controls[span - degree + j]).collect::<Vec<_>>();
1638
1639 for r in 1..=degree {
1640 for j in (r..=degree).rev() {
1641 let knot_index = span - degree + j;
1642 let denominator = knots[knot_index + degree + 1 - r] - knots[knot_index];
1643 let alpha = if denominator.abs() <= f64::EPSILON {
1644 0.0
1645 } else {
1646 (parameter - knots[knot_index]) / denominator
1647 };
1648 points[j] = Coords2d {
1649 x: (1.0 - alpha) * points[j - 1].x + alpha * points[j].x,
1650 y: (1.0 - alpha) * points[j - 1].y + alpha * points[j].y,
1651 };
1652 }
1653 }
1654
1655 points[degree]
1656}
1657
1658fn sample_control_point_spline_for_trim(controls: &[Coords2d], degree: usize) -> Vec<SampledCurvePoint> {
1659 let knots = build_open_uniform_knot_vector(controls.len(), degree);
1660 let span_count = controls.len().saturating_sub(degree);
1661 let mut samples = Vec::with_capacity(span_count * CONTROL_POINT_SPLINE_TRIM_SAMPLES_PER_SPAN + 1);
1662 samples.push(SampledCurvePoint {
1663 parameter: 0.0,
1664 point: controls[0],
1665 });
1666
1667 for span_index in 0..span_count {
1668 let start = span_index as f64;
1669 let end = (span_index + 1) as f64;
1670 let is_last_span = span_index + 1 == span_count;
1671 let max_step = if is_last_span {
1672 CONTROL_POINT_SPLINE_TRIM_SAMPLES_PER_SPAN
1673 } else {
1674 CONTROL_POINT_SPLINE_TRIM_SAMPLES_PER_SPAN - 1
1675 };
1676
1677 for step in 1..=max_step {
1678 let t = step as f64 / CONTROL_POINT_SPLINE_TRIM_SAMPLES_PER_SPAN as f64;
1679 let parameter = if is_last_span && step == CONTROL_POINT_SPLINE_TRIM_SAMPLES_PER_SPAN {
1680 end
1681 } else {
1682 start + t * (end - start)
1683 };
1684 samples.push(SampledCurvePoint {
1685 parameter,
1686 point: de_boor_point(parameter, degree, &knots, controls),
1687 });
1688 }
1689 }
1690
1691 samples
1692}
1693
1694fn project_point_onto_sampled_curve(samples: &[SampledCurvePoint], point: Coords2d) -> Result<f64, String> {
1695 if samples.len() < 2 {
1696 return Err("Need at least two sampled points to project onto spline".to_string());
1697 }
1698
1699 let mut best_parameter = samples[0].parameter;
1700 let mut best_distance_sq = f64::INFINITY;
1701 for window in samples.windows(2) {
1702 let start = window[0].point;
1703 let end = window[1].point;
1704 let dx = end.x - start.x;
1705 let dy = end.y - start.y;
1706 let segment_length_sq = dx * dx + dy * dy;
1707 let local_t = if segment_length_sq <= f64::EPSILON {
1708 0.0
1709 } else {
1710 (((point.x - start.x) * dx + (point.y - start.y) * dy) / segment_length_sq).clamp(0.0, 1.0)
1711 };
1712 let projected = Coords2d {
1713 x: start.x + local_t * dx,
1714 y: start.y + local_t * dy,
1715 };
1716 let distance_sq = (point.x - projected.x).powi(2) + (point.y - projected.y).powi(2);
1717 if distance_sq < best_distance_sq {
1718 best_distance_sq = distance_sq;
1719 best_parameter = window[0].parameter + local_t * (window[1].parameter - window[0].parameter);
1720 }
1721 }
1722
1723 Ok(best_parameter)
1724}
1725
1726fn is_control_point_spline_owned_helper_line(segment_obj: &Object, objects: &[Object]) -> bool {
1728 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1729 return false;
1730 };
1731 let Segment::Line(line) = segment else {
1732 return false;
1733 };
1734 let Some(owner_id) = line.owner else {
1735 return false;
1736 };
1737 objects.iter().find(|obj| obj.id == owner_id).is_some_and(|owner_obj| {
1738 matches!(
1739 &owner_obj.kind,
1740 ObjectKind::Segment {
1741 segment: Segment::ControlPointSpline(_)
1742 }
1743 )
1744 })
1745}
1746
1747fn get_control_point_spline_controls(
1748 segment_obj: &Object,
1749 objects: &[Object],
1750 default_unit: UnitLength,
1751) -> Result<Vec<(ObjectId, Coords2d)>, String> {
1752 let ObjectKind::Segment {
1753 segment: Segment::ControlPointSpline(spline),
1754 } = &segment_obj.kind
1755 else {
1756 return Err(format!("Segment {} is not a control point spline", segment_obj.id.0));
1757 };
1758
1759 spline
1760 .controls
1761 .iter()
1762 .map(|control_id| {
1763 let point_obj = objects.iter().find(|obj| obj.id == *control_id).ok_or_else(|| {
1764 format!(
1765 "Control point {} not found for spline {}",
1766 control_id.0, segment_obj.id.0
1767 )
1768 })?;
1769 let ObjectKind::Segment {
1770 segment: Segment::Point(point),
1771 } = &point_obj.kind
1772 else {
1773 return Err(format!(
1774 "Control point {} for spline {} is not a point",
1775 control_id.0, segment_obj.id.0
1776 ));
1777 };
1778 Ok((
1779 *control_id,
1780 Coords2d {
1781 x: number_to_unit(&point.position.x, default_unit),
1782 y: number_to_unit(&point.position.y, default_unit),
1783 },
1784 ))
1785 })
1786 .collect()
1787}
1788
1789fn load_curve_handle(
1790 segment_obj: &Object,
1791 objects: &[Object],
1792 default_unit: UnitLength,
1793) -> Result<CurveHandle, String> {
1794 if is_control_point_spline_owned_helper_line(segment_obj, objects) {
1795 return Err(format!(
1796 "Control point spline helper line {} cannot be used as a trim curve",
1797 segment_obj.id.0
1798 ));
1799 }
1800
1801 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1802 return Err("Object is not a segment".to_owned());
1803 };
1804
1805 match segment {
1806 Segment::Line(_) => {
1807 let start = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit)
1808 .ok_or_else(|| format!("Could not get line start for segment {}", segment_obj.id.0))?;
1809 let end = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit)
1810 .ok_or_else(|| format!("Could not get line end for segment {}", segment_obj.id.0))?;
1811 Ok(CurveHandle {
1812 segment_id: segment_obj.id,
1813 kind: CurveKind::Line,
1814 domain: CurveDomain::Open,
1815 start,
1816 end,
1817 center: None,
1818 radius: None,
1819 direction: ArcDirection::Ccw,
1820 sampled_points: None,
1821 })
1822 }
1823 Segment::Arc(arc) => {
1824 let start = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit)
1825 .ok_or_else(|| format!("Could not get arc start for segment {}", segment_obj.id.0))?;
1826 let end = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit)
1827 .ok_or_else(|| format!("Could not get arc end for segment {}", segment_obj.id.0))?;
1828 let center = get_position_coords_from_arc(segment_obj, ArcPoint::Center, objects, default_unit)
1829 .ok_or_else(|| format!("Could not get arc center for segment {}", segment_obj.id.0))?;
1830 let radius =
1831 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1832 Ok(CurveHandle {
1833 segment_id: segment_obj.id,
1834 kind: CurveKind::Circular,
1835 domain: CurveDomain::Open,
1836 start,
1837 end,
1838 center: Some(center),
1839 radius: Some(radius),
1840 direction: arc.direction,
1841 sampled_points: None,
1842 })
1843 }
1844 Segment::Circle(_) => {
1845 let start = get_position_coords_from_circle(segment_obj, CirclePoint::Start, objects, default_unit)
1846 .ok_or_else(|| format!("Could not get circle start for segment {}", segment_obj.id.0))?;
1847 let center = get_position_coords_from_circle(segment_obj, CirclePoint::Center, objects, default_unit)
1848 .ok_or_else(|| format!("Could not get circle center for segment {}", segment_obj.id.0))?;
1849 let radius =
1850 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1851 Ok(CurveHandle {
1852 segment_id: segment_obj.id,
1853 kind: CurveKind::Circular,
1854 domain: CurveDomain::Closed,
1855 start,
1856 end: start,
1858 center: Some(center),
1859 radius: Some(radius),
1860 direction: ArcDirection::Ccw,
1861 sampled_points: None,
1862 })
1863 }
1864 Segment::Point(_) => Err(format!(
1865 "Point segment {} cannot be used as trim curve",
1866 segment_obj.id.0
1867 )),
1868 Segment::ControlPointSpline(spline) => {
1869 let controls = get_control_point_spline_controls(segment_obj, objects, default_unit)?;
1870 let sampled_points = sample_control_point_spline_for_trim(
1871 &controls.iter().map(|(_, point)| *point).collect::<Vec<_>>(),
1872 spline.degree as usize,
1873 );
1874 let start = controls
1875 .first()
1876 .map(|(_, point)| *point)
1877 .ok_or_else(|| format!("Spline {} has no control points", segment_obj.id.0))?;
1878 let end = controls
1879 .last()
1880 .map(|(_, point)| *point)
1881 .ok_or_else(|| format!("Spline {} has no control points", segment_obj.id.0))?;
1882 Ok(CurveHandle {
1883 segment_id: segment_obj.id,
1884 kind: CurveKind::Spline,
1885 domain: CurveDomain::Open,
1886 start,
1887 end,
1888 center: None,
1889 radius: None,
1890 direction: ArcDirection::Ccw,
1891 sampled_points: Some(sampled_points),
1892 })
1893 }
1894 }
1895}
1896
1897fn project_point_onto_curve(curve: &CurveHandle, point: Coords2d) -> Result<f64, String> {
1898 curve.project_for_trim(point)
1899}
1900
1901fn curve_contains_point(curve: &CurveHandle, point: Coords2d, epsilon: f64) -> bool {
1902 match (curve.kind, curve.domain) {
1903 (CurveKind::Line, CurveDomain::Open) => {
1904 let t = project_point_onto_segment(point, curve.start, curve.end);
1905 (0.0..=1.0).contains(&t) && perpendicular_distance_to_segment(point, curve.start, curve.end) <= epsilon
1906 }
1907 (CurveKind::Circular, CurveDomain::Open) => curve.center.is_some_and(|center| {
1908 let (sweep_start, sweep_end) = curve.sweep_start_end();
1909 is_point_on_arc(point, center, sweep_start, sweep_end, epsilon)
1910 }),
1911 (CurveKind::Circular, CurveDomain::Closed) => curve.center.is_some_and(|center| {
1912 let radius = curve.radius.unwrap_or_else(|| {
1913 ((curve.start.x - center.x).squared() + (curve.start.y - center.y).squared()).sqrt()
1914 });
1915 is_point_on_circle(point, center, radius, epsilon)
1916 }),
1917 (CurveKind::Line, CurveDomain::Closed) => false,
1918 (CurveKind::Spline, CurveDomain::Open) => {
1919 project_point_onto_sampled_curve(curve.sampled_points.as_deref().unwrap_or(&[]), point)
1920 .ok()
1921 .and_then(|parameter| {
1922 curve.sampled_points.as_ref().map(|samples| {
1923 let nearest = samples
1924 .windows(2)
1925 .map(|window| {
1926 let start = window[0].point;
1927 let end = window[1].point;
1928 let dx = end.x - start.x;
1929 let dy = end.y - start.y;
1930 let segment_length_sq = dx * dx + dy * dy;
1931 let local_t = if segment_length_sq <= f64::EPSILON {
1932 0.0
1933 } else {
1934 ((parameter - window[0].parameter) / (window[1].parameter - window[0].parameter))
1935 .clamp(0.0, 1.0)
1936 };
1937 let projected = Coords2d {
1938 x: start.x + local_t * dx,
1939 y: start.y + local_t * dy,
1940 };
1941 ((point.x - projected.x).powi(2) + (point.y - projected.y).powi(2)).sqrt()
1942 })
1943 .fold(f64::INFINITY, libm::fmin);
1944 nearest <= epsilon
1945 })
1946 })
1947 .unwrap_or(false)
1948 }
1949 (CurveKind::Spline, CurveDomain::Closed) => false,
1950 }
1951}
1952
1953fn curve_line_segment_intersections(
1954 curve: &CurveHandle,
1955 line_start: Coords2d,
1956 line_end: Coords2d,
1957 epsilon: f64,
1958) -> Vec<(f64, Coords2d)> {
1959 match (curve.kind, curve.domain) {
1960 (CurveKind::Line, CurveDomain::Open) => {
1961 line_segment_intersection(line_start, line_end, curve.start, curve.end, epsilon)
1962 .map(|intersection| {
1963 (
1964 project_point_onto_segment(intersection, line_start, line_end),
1965 intersection,
1966 )
1967 })
1968 .into_iter()
1969 .collect()
1970 }
1971 (CurveKind::Circular, CurveDomain::Open) => curve
1972 .center
1973 .map(|center| {
1974 let (sweep_start, sweep_end) = curve.sweep_start_end();
1975 line_arc_intersections(line_start, line_end, center, sweep_start, sweep_end, epsilon)
1976 })
1977 .unwrap_or_default(),
1978 (CurveKind::Circular, CurveDomain::Closed) => {
1979 let Some(center) = curve.center else {
1980 return Vec::new();
1981 };
1982 let radius = curve.radius.unwrap_or_else(|| {
1983 ((curve.start.x - center.x).squared() + (curve.start.y - center.y).squared()).sqrt()
1984 });
1985 line_circle_intersections(line_start, line_end, center, radius, epsilon)
1986 }
1987 (CurveKind::Line, CurveDomain::Closed) => Vec::new(),
1988 (CurveKind::Spline, CurveDomain::Open) => {
1989 let Some(samples) = curve.sampled_points.as_ref() else {
1990 return Vec::new();
1991 };
1992 let mut intersections = Vec::new();
1993 for window in samples.windows(2) {
1994 if let Some(intersection) =
1995 line_segment_intersection(line_start, line_end, window[0].point, window[1].point, epsilon)
1996 {
1997 let t = project_point_onto_segment(intersection, line_start, line_end);
1998 intersections.push((t, intersection));
1999 }
2000 }
2001 intersections
2002 }
2003 (CurveKind::Spline, CurveDomain::Closed) => Vec::new(),
2004 }
2005}
2006
2007fn curve_polyline_intersections(curve: &CurveHandle, polyline: &[Coords2d], epsilon: f64) -> Vec<(Coords2d, usize)> {
2008 let mut intersections = Vec::new();
2009
2010 for i in 0..polyline.len().saturating_sub(1) {
2011 let p1 = polyline[i];
2012 let p2 = polyline[i + 1];
2013 for (_, intersection) in curve_line_segment_intersections(curve, p1, p2, epsilon) {
2014 intersections.push((intersection, i));
2015 }
2016 }
2017
2018 intersections
2019}
2020
2021fn curve_curve_intersections(curve: &CurveHandle, other: &CurveHandle, epsilon: f64) -> Vec<Coords2d> {
2022 match (curve.kind, curve.domain, other.kind, other.domain) {
2023 (CurveKind::Line, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => {
2024 line_segment_intersection(curve.start, curve.end, other.start, other.end, epsilon)
2025 .into_iter()
2026 .collect()
2027 }
2028 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => other
2029 .center
2030 .map(|other_center| {
2031 let (other_sweep_start, other_sweep_end) = other.sweep_start_end();
2032 line_arc_intersections(
2033 curve.start,
2034 curve.end,
2035 other_center,
2036 other_sweep_start,
2037 other_sweep_end,
2038 epsilon,
2039 )
2040 .into_iter()
2041 .map(|(_, point)| point)
2042 .collect()
2043 })
2044 .unwrap_or_default(),
2045 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
2046 let Some(other_center) = other.center else {
2047 return Vec::new();
2048 };
2049 let other_radius = other.radius.unwrap_or_else(|| {
2050 ((other.start.x - other_center.x).squared() + (other.start.y - other_center.y).squared()).sqrt()
2051 });
2052 line_circle_intersections(curve.start, curve.end, other_center, other_radius, epsilon)
2053 .into_iter()
2054 .map(|(_, point)| point)
2055 .collect()
2056 }
2057 (CurveKind::Circular, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => curve
2058 .center
2059 .map(|curve_center| {
2060 let (curve_sweep_start, curve_sweep_end) = curve.sweep_start_end();
2061 line_arc_intersections(
2062 other.start,
2063 other.end,
2064 curve_center,
2065 curve_sweep_start,
2066 curve_sweep_end,
2067 epsilon,
2068 )
2069 .into_iter()
2070 .map(|(_, point)| point)
2071 .collect()
2072 })
2073 .unwrap_or_default(),
2074 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => {
2075 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
2076 return Vec::new();
2077 };
2078 let (curve_sweep_start, curve_sweep_end) = curve.sweep_start_end();
2079 let (other_sweep_start, other_sweep_end) = other.sweep_start_end();
2080 arc_arc_intersections(
2081 curve_center,
2082 curve_sweep_start,
2083 curve_sweep_end,
2084 other_center,
2085 other_sweep_start,
2086 other_sweep_end,
2087 epsilon,
2088 )
2089 }
2090 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
2091 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
2092 return Vec::new();
2093 };
2094 let other_radius = other.radius.unwrap_or_else(|| {
2095 ((other.start.x - other_center.x).squared() + (other.start.y - other_center.y).squared()).sqrt()
2096 });
2097 let (curve_sweep_start, curve_sweep_end) = curve.sweep_start_end();
2098 circle_arc_intersections(
2099 other_center,
2100 other_radius,
2101 curve_center,
2102 curve_sweep_start,
2103 curve_sweep_end,
2104 epsilon,
2105 )
2106 }
2107 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Line, CurveDomain::Open) => {
2108 let Some(curve_center) = curve.center else {
2109 return Vec::new();
2110 };
2111 let curve_radius = curve.radius.unwrap_or_else(|| {
2112 ((curve.start.x - curve_center.x).squared() + (curve.start.y - curve_center.y).squared()).sqrt()
2113 });
2114 line_circle_intersections(other.start, other.end, curve_center, curve_radius, epsilon)
2115 .into_iter()
2116 .map(|(_, point)| point)
2117 .collect()
2118 }
2119 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Open) => {
2120 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
2121 return Vec::new();
2122 };
2123 let curve_radius = curve.radius.unwrap_or_else(|| {
2124 ((curve.start.x - curve_center.x).squared() + (curve.start.y - curve_center.y).squared()).sqrt()
2125 });
2126 let (other_sweep_start, other_sweep_end) = other.sweep_start_end();
2127 circle_arc_intersections(
2128 curve_center,
2129 curve_radius,
2130 other_center,
2131 other_sweep_start,
2132 other_sweep_end,
2133 epsilon,
2134 )
2135 }
2136 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Closed) => {
2137 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
2138 return Vec::new();
2139 };
2140 let curve_radius = curve.radius.unwrap_or_else(|| {
2141 ((curve.start.x - curve_center.x).squared() + (curve.start.y - curve_center.y).squared()).sqrt()
2142 });
2143 let other_radius = other.radius.unwrap_or_else(|| {
2144 ((other.start.x - other_center.x).squared() + (other.start.y - other_center.y).squared()).sqrt()
2145 });
2146 circle_circle_intersections(curve_center, curve_radius, other_center, other_radius, epsilon)
2147 }
2148 (CurveKind::Spline, CurveDomain::Open, _, _) => sampled_curve_curve_intersections(curve, other, epsilon),
2149 (_, _, CurveKind::Spline, CurveDomain::Open) => sampled_curve_curve_intersections(other, curve, epsilon),
2150 _ => Vec::new(),
2151 }
2152}
2153
2154fn sampled_curve_curve_intersections(sampled_curve: &CurveHandle, other: &CurveHandle, epsilon: f64) -> Vec<Coords2d> {
2155 let Some(samples) = sampled_curve.sampled_points.as_ref() else {
2156 return Vec::new();
2157 };
2158 let mut intersections = Vec::new();
2159
2160 for window in samples.windows(2) {
2161 let start = window[0].point;
2162 let end = window[1].point;
2163 match (other.kind, other.domain) {
2164 (CurveKind::Line, CurveDomain::Open) => {
2165 if let Some(intersection) = line_segment_intersection(start, end, other.start, other.end, epsilon) {
2166 intersections.push(intersection);
2167 }
2168 }
2169 (CurveKind::Circular, CurveDomain::Open) => {
2170 let (other_sweep_start, other_sweep_end) = other.sweep_start_end();
2171 if let Some(center) = other.center
2172 && let Some(intersection) =
2173 line_arc_intersection(start, end, center, other_sweep_start, other_sweep_end, epsilon)
2174 {
2175 intersections.push(intersection);
2176 }
2177 }
2178 (CurveKind::Circular, CurveDomain::Closed) => {
2179 if let Some(center) = other.center {
2180 let radius = other.radius.unwrap_or_else(|| {
2181 ((other.start.x - center.x).powi(2) + (other.start.y - center.y).powi(2)).sqrt()
2182 });
2183 intersections.extend(
2184 line_circle_intersections(start, end, center, radius, epsilon)
2185 .into_iter()
2186 .map(|(_, point)| point),
2187 );
2188 }
2189 }
2190 (CurveKind::Spline, CurveDomain::Open) => {
2191 let Some(other_samples) = other.sampled_points.as_ref() else {
2192 continue;
2193 };
2194 for other_window in other_samples.windows(2) {
2195 if let Some(intersection) =
2196 line_segment_intersection(start, end, other_window[0].point, other_window[1].point, epsilon)
2197 {
2198 intersections.push(intersection);
2199 }
2200 }
2201 }
2202 _ => {}
2203 }
2204 }
2205
2206 intersections
2207}
2208
2209fn segment_endpoint_points(
2210 segment_obj: &Object,
2211 objects: &[Object],
2212 default_unit: UnitLength,
2213) -> Vec<(ObjectId, Coords2d)> {
2214 let ObjectKind::Segment { segment } = &segment_obj.kind else {
2215 return Vec::new();
2216 };
2217
2218 match segment {
2219 Segment::Line(line) => {
2220 if is_control_point_spline_owned_helper_line(segment_obj, objects) {
2221 return Vec::new();
2222 }
2223 let mut points = Vec::new();
2224 if let Some(start) = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit) {
2225 points.push((line.start, start));
2226 }
2227 if let Some(end) = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit) {
2228 points.push((line.end, end));
2229 }
2230 points
2231 }
2232 Segment::Arc(arc) => {
2233 let mut points = Vec::new();
2234 if let Some(start) = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit) {
2235 points.push((arc.start, start));
2236 }
2237 if let Some(end) = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit) {
2238 points.push((arc.end, end));
2239 }
2240 points
2241 }
2242 Segment::ControlPointSpline(spline) => {
2243 let mut points = Vec::new();
2244 if let Ok(controls) = get_control_point_spline_controls(segment_obj, objects, default_unit) {
2245 if let Some((control_id, point)) = controls.first() {
2246 points.push((*control_id, *point));
2247 }
2248 if let Some((control_id, point)) = controls.last()
2249 && Some(*control_id) != points.first().map(|(id, _)| *id)
2250 {
2251 points.push((*control_id, *point));
2252 }
2253 } else if !spline.controls.is_empty() {
2254 return Vec::new();
2255 }
2256 points
2257 }
2258 _ => Vec::new(),
2259 }
2260}
2261
2262pub fn get_next_trim_spawn(
2290 points: &[Coords2d],
2291 start_index: usize,
2292 objects: &[Object],
2293 default_unit: UnitLength,
2294) -> TrimItem {
2295 get_next_trim_spawn_filtered(points, start_index, objects, default_unit, None)
2296}
2297
2298fn get_next_trim_spawn_filtered(
2299 points: &[Coords2d],
2300 start_index: usize,
2301 objects: &[Object],
2302 default_unit: UnitLength,
2303 eligible_segment_ids: Option<&IndexSet<ObjectId>>,
2304) -> TrimItem {
2305 let scene_curves: Vec<CurveHandle> = objects
2306 .iter()
2307 .filter_map(|obj| load_curve_handle(obj, objects, default_unit).ok())
2308 .filter(|curve| eligible_segment_ids.is_none_or(|ids| ids.contains(&curve.segment_id)))
2309 .collect();
2310
2311 for i in start_index..points.len().saturating_sub(1) {
2313 let p1 = points[i];
2314 let p2 = points[i + 1];
2315
2316 for curve in &scene_curves {
2318 let intersections = curve_line_segment_intersections(curve, p1, p2, EPSILON_POINT_ON_SEGMENT);
2319 if let Some((_, intersection)) = intersections.first() {
2320 return TrimItem::Spawn {
2321 trim_spawn_seg_id: curve.segment_id,
2322 trim_spawn_coords: *intersection,
2323 next_index: i,
2324 };
2325 }
2326 }
2327 }
2328
2329 TrimItem::None {
2331 next_index: points.len().saturating_sub(1),
2332 }
2333}
2334
2335fn trim_stroke_intersection_counts(
2340 points: &[Coords2d],
2341 objects: &[Object],
2342 default_unit: UnitLength,
2343) -> IndexMap<ArtifactId, usize> {
2344 objects
2345 .iter()
2346 .filter_map(|object| load_curve_handle(object, objects, default_unit).ok())
2347 .filter_map(|curve| {
2348 let intersection_count = curve_polyline_intersections(&curve, points, EPSILON_POINT_ON_SEGMENT).len();
2349 (intersection_count > 0).then(|| {
2350 let artifact_id = objects
2351 .iter()
2352 .find(|object| object.id == curve.segment_id)
2353 .map(|object| object.artifact_id)
2354 .unwrap_or_else(ArtifactId::placeholder);
2355 (artifact_id, intersection_count)
2356 })
2357 })
2358 .collect()
2359}
2360
2361pub fn get_trim_spawn_terminations(
2416 trim_spawn_seg_id: ObjectId,
2417 trim_spawn_coords: &[Coords2d],
2418 objects: &[Object],
2419 default_unit: UnitLength,
2420) -> Result<TrimTerminations, String> {
2421 let trim_spawn_seg = objects.iter().find(|obj| obj.id == trim_spawn_seg_id);
2423
2424 let trim_spawn_seg = match trim_spawn_seg {
2425 Some(seg) => seg,
2426 None => {
2427 return Err(format!("Trim spawn segment {} not found", trim_spawn_seg_id.0));
2428 }
2429 };
2430
2431 let trim_curve = load_curve_handle(trim_spawn_seg, objects, default_unit).map_err(|e| {
2432 format!(
2433 "Failed to load trim spawn segment {} as normalized curve: {}",
2434 trim_spawn_seg_id.0, e
2435 )
2436 })?;
2437
2438 let all_intersections = curve_polyline_intersections(&trim_curve, trim_spawn_coords, EPSILON_POINT_ON_SEGMENT);
2443
2444 let intersection_point = if all_intersections.is_empty() {
2447 return Err("Could not find intersection point between polyline and trim spawn segment".to_string());
2448 } else {
2449 let mid_index = (trim_spawn_coords.len() - 1) / 2;
2451 let mid_point = trim_spawn_coords[mid_index];
2452
2453 let mut min_dist = f64::INFINITY;
2455 let mut closest_intersection = all_intersections[0].0;
2456
2457 for (intersection, _) in &all_intersections {
2458 let dist = ((intersection.x - mid_point.x) * (intersection.x - mid_point.x)
2459 + (intersection.y - mid_point.y) * (intersection.y - mid_point.y))
2460 .sqrt();
2461 if dist < min_dist {
2462 min_dist = dist;
2463 closest_intersection = *intersection;
2464 }
2465 }
2466
2467 closest_intersection
2468 };
2469
2470 let intersection_t = project_point_onto_curve(&trim_curve, intersection_point)?;
2472
2473 let left_termination = find_termination_in_direction(
2475 trim_spawn_seg,
2476 &trim_curve,
2477 intersection_t,
2478 TrimDirection::Left,
2479 objects,
2480 default_unit,
2481 )?;
2482
2483 let right_termination = find_termination_in_direction(
2484 trim_spawn_seg,
2485 &trim_curve,
2486 intersection_t,
2487 TrimDirection::Right,
2488 objects,
2489 default_unit,
2490 )?;
2491
2492 Ok(TrimTerminations {
2493 left_side: left_termination,
2494 right_side: right_termination,
2495 })
2496}
2497
2498fn find_termination_in_direction(
2551 trim_spawn_seg: &Object,
2552 trim_curve: &CurveHandle,
2553 intersection_t: f64,
2554 direction: TrimDirection,
2555 objects: &[Object],
2556 default_unit: UnitLength,
2557) -> Result<TrimTermination, String> {
2558 let ObjectKind::Segment { segment } = &trim_spawn_seg.kind else {
2560 return Err("Trim spawn segment is not a segment".to_string());
2561 };
2562
2563 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2565 enum CandidateType {
2566 Intersection,
2567 Coincident,
2568 Endpoint,
2569 }
2570
2571 #[derive(Debug, Clone)]
2572 struct Candidate {
2573 t: f64,
2574 point: Coords2d,
2575 candidate_type: CandidateType,
2576 segment_id: Option<ObjectId>,
2577 point_id: Option<ObjectId>,
2578 }
2579
2580 let mut candidates: Vec<Candidate> = Vec::new();
2581
2582 match segment {
2584 Segment::Line(line) => {
2585 candidates.push(Candidate {
2586 t: 0.0,
2587 point: trim_curve.start,
2588 candidate_type: CandidateType::Endpoint,
2589 segment_id: None,
2590 point_id: Some(line.start),
2591 });
2592 candidates.push(Candidate {
2593 t: 1.0,
2594 point: trim_curve.end,
2595 candidate_type: CandidateType::Endpoint,
2596 segment_id: None,
2597 point_id: Some(line.end),
2598 });
2599 }
2600 Segment::Arc(arc) => {
2601 candidates.push(Candidate {
2603 t: 0.0,
2604 point: trim_curve.start,
2605 candidate_type: CandidateType::Endpoint,
2606 segment_id: None,
2607 point_id: Some(arc.start),
2608 });
2609 candidates.push(Candidate {
2610 t: 1.0,
2611 point: trim_curve.end,
2612 candidate_type: CandidateType::Endpoint,
2613 segment_id: None,
2614 point_id: Some(arc.end),
2615 });
2616 }
2617 Segment::Circle(_) => {
2618 }
2620 Segment::ControlPointSpline(spline) => {
2621 let end_t = trim_curve
2622 .sampled_points
2623 .as_ref()
2624 .and_then(|samples| samples.last())
2625 .map(|sample| sample.parameter)
2626 .unwrap_or_else(|| spline.controls.len().saturating_sub(1) as f64);
2627 candidates.push(Candidate {
2628 t: 0.0,
2629 point: trim_curve.start,
2630 candidate_type: CandidateType::Endpoint,
2631 segment_id: None,
2632 point_id: spline.controls.first().copied(),
2633 });
2634 candidates.push(Candidate {
2635 t: end_t,
2636 point: trim_curve.end,
2637 candidate_type: CandidateType::Endpoint,
2638 segment_id: None,
2639 point_id: spline.controls.last().copied(),
2640 });
2641 }
2642 _ => {}
2643 }
2644
2645 let trim_spawn_seg_id = trim_spawn_seg.id;
2647
2648 for other_seg in objects.iter() {
2650 let other_id = other_seg.id;
2651 if other_id == trim_spawn_seg_id {
2652 continue;
2653 }
2654
2655 if let Ok(other_curve) = load_curve_handle(other_seg, objects, default_unit) {
2656 for intersection in curve_curve_intersections(trim_curve, &other_curve, EPSILON_POINT_ON_SEGMENT) {
2657 let Ok(t) = project_point_onto_curve(trim_curve, intersection) else {
2658 continue;
2659 };
2660 candidates.push(Candidate {
2661 t,
2662 point: intersection,
2663 candidate_type: CandidateType::Intersection,
2664 segment_id: Some(other_id),
2665 point_id: None,
2666 });
2667 }
2668 }
2669
2670 for (other_point_id, other_point) in segment_endpoint_points(other_seg, objects, default_unit) {
2671 if !is_point_coincident_with_segment_native(other_point_id, trim_spawn_seg_id, objects) {
2672 continue;
2673 }
2674 if !curve_contains_point(trim_curve, other_point, EPSILON_POINT_ON_SEGMENT) {
2675 continue;
2676 }
2677 let Ok(t) = project_point_onto_curve(trim_curve, other_point) else {
2678 continue;
2679 };
2680 candidates.push(Candidate {
2681 t,
2682 point: other_point,
2683 candidate_type: CandidateType::Coincident,
2684 segment_id: Some(other_id),
2685 point_id: Some(other_point_id),
2686 });
2687 }
2688 }
2689
2690 let is_circle_segment = trim_curve.domain == CurveDomain::Closed;
2691
2692 let intersection_epsilon = EPSILON_POINT_ON_SEGMENT * 10.0; let direction_distance = |candidate_t: f64| -> f64 {
2696 if is_circle_segment {
2697 match direction {
2698 TrimDirection::Left => (intersection_t - candidate_t).rem_euclid(1.0),
2699 TrimDirection::Right => (candidate_t - intersection_t).rem_euclid(1.0),
2700 }
2701 } else {
2702 (candidate_t - intersection_t).abs()
2703 }
2704 };
2705 let filtered_candidates: Vec<Candidate> = candidates
2706 .into_iter()
2707 .filter(|candidate| {
2708 let dist_from_intersection = if is_circle_segment {
2709 let ccw = (candidate.t - intersection_t).rem_euclid(1.0);
2710 let cw = (intersection_t - candidate.t).rem_euclid(1.0);
2711 libm::fmin(ccw, cw)
2712 } else {
2713 (candidate.t - intersection_t).abs()
2714 };
2715 if dist_from_intersection < intersection_epsilon {
2716 return false; }
2718
2719 if is_circle_segment {
2720 direction_distance(candidate.t) > intersection_epsilon
2721 } else {
2722 match direction {
2723 TrimDirection::Left => candidate.t < intersection_t,
2724 TrimDirection::Right => candidate.t > intersection_t,
2725 }
2726 }
2727 })
2728 .collect();
2729
2730 let mut sorted_candidates = filtered_candidates;
2736 sorted_candidates.sort_by(|a, b| {
2737 let dist_a = direction_distance(a.t);
2738 let dist_b = direction_distance(b.t);
2739 let dist_diff = dist_a - dist_b;
2740 let coincident_snap_applies = dist_diff.abs() <= EPSILON_COINCIDENT_TERMINATION_SNAP
2741 && (a.candidate_type == CandidateType::Coincident || b.candidate_type == CandidateType::Coincident);
2742 if dist_diff.abs() > EPSILON_POINT_ON_SEGMENT && !coincident_snap_applies {
2743 dist_diff.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal)
2744 } else {
2745 let type_priority = |candidate_type: CandidateType| -> i32 {
2747 match candidate_type {
2748 CandidateType::Coincident => 0,
2749 CandidateType::Intersection => 1,
2750 CandidateType::Endpoint => 2,
2751 }
2752 };
2753 type_priority(a.candidate_type).cmp(&type_priority(b.candidate_type))
2754 }
2755 });
2756
2757 let closest_candidate = match sorted_candidates.first() {
2759 Some(c) => c,
2760 None => {
2761 if is_circle_segment {
2762 return Err("No trim termination candidate found for circle".to_string());
2763 }
2764 let endpoint = match direction {
2766 TrimDirection::Left => trim_curve.start,
2767 TrimDirection::Right => trim_curve.end,
2768 };
2769 return Ok(TrimTermination::SegEndPoint {
2770 trim_termination_coords: endpoint,
2771 });
2772 }
2773 };
2774
2775 if !is_circle_segment
2779 && closest_candidate.candidate_type == CandidateType::Intersection
2780 && let Some(seg_id) = closest_candidate.segment_id
2781 {
2782 let intersecting_seg = objects.iter().find(|obj| obj.id == seg_id);
2783
2784 if let Some(intersecting_seg) = intersecting_seg {
2785 let endpoint_epsilon = EPSILON_POINT_ON_SEGMENT * 1000.0; let is_other_seg_endpoint = segment_endpoint_points(intersecting_seg, objects, default_unit)
2788 .into_iter()
2789 .any(|(_, endpoint)| {
2790 let dist_to_endpoint = ((closest_candidate.point.x - endpoint.x).squared()
2791 + (closest_candidate.point.y - endpoint.y).squared())
2792 .sqrt();
2793 dist_to_endpoint < endpoint_epsilon
2794 });
2795
2796 if is_other_seg_endpoint {
2799 let endpoint = match direction {
2800 TrimDirection::Left => trim_curve.start,
2801 TrimDirection::Right => trim_curve.end,
2802 };
2803 return Ok(TrimTermination::SegEndPoint {
2804 trim_termination_coords: endpoint,
2805 });
2806 }
2807 }
2808
2809 let endpoint_t = match direction {
2811 TrimDirection::Left => 0.0,
2812 TrimDirection::Right => 1.0,
2813 };
2814 let endpoint = match direction {
2815 TrimDirection::Left => trim_curve.start,
2816 TrimDirection::Right => trim_curve.end,
2817 };
2818 let dist_to_endpoint_param = (closest_candidate.t - endpoint_t).abs();
2819 let dist_to_endpoint_coords = ((closest_candidate.point.x - endpoint.x)
2820 * (closest_candidate.point.x - endpoint.x)
2821 + (closest_candidate.point.y - endpoint.y) * (closest_candidate.point.y - endpoint.y))
2822 .sqrt();
2823
2824 let is_at_endpoint =
2825 dist_to_endpoint_param < EPSILON_POINT_ON_SEGMENT || dist_to_endpoint_coords < EPSILON_POINT_ON_SEGMENT;
2826
2827 if is_at_endpoint {
2828 return Ok(TrimTermination::SegEndPoint {
2830 trim_termination_coords: endpoint,
2831 });
2832 }
2833 }
2834
2835 let endpoint_t_for_return = match direction {
2837 TrimDirection::Left => 0.0,
2838 TrimDirection::Right => 1.0,
2839 };
2840 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Intersection {
2841 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2842 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2843 let endpoint = match direction {
2846 TrimDirection::Left => trim_curve.start,
2847 TrimDirection::Right => trim_curve.end,
2848 };
2849 return Ok(TrimTermination::SegEndPoint {
2850 trim_termination_coords: endpoint,
2851 });
2852 }
2853 }
2854
2855 let endpoint = match direction {
2861 TrimDirection::Left => trim_curve.start,
2862 TrimDirection::Right => trim_curve.end,
2863 };
2864 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Coincident {
2865 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2866 let coord_distance = ((closest_candidate.point.x - endpoint.x).squared()
2867 + (closest_candidate.point.y - endpoint.y).squared())
2868 .sqrt();
2869 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT || coord_distance < EPSILON_POINT_ON_SEGMENT {
2870 return Ok(TrimTermination::SegEndPoint {
2871 trim_termination_coords: endpoint,
2872 });
2873 }
2874 }
2875
2876 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Endpoint {
2878 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2879 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2880 return Ok(TrimTermination::SegEndPoint {
2882 trim_termination_coords: endpoint,
2883 });
2884 }
2885 }
2886
2887 if closest_candidate.candidate_type == CandidateType::Coincident {
2889 Ok(TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2891 trim_termination_coords: closest_candidate.point,
2892 intersecting_seg_id: closest_candidate
2893 .segment_id
2894 .ok_or_else(|| "Missing segment_id for coincident".to_string())?,
2895 other_segment_point_id: closest_candidate
2896 .point_id
2897 .ok_or_else(|| "Missing point_id for coincident".to_string())?,
2898 })
2899 } else if closest_candidate.candidate_type == CandidateType::Intersection {
2900 Ok(TrimTermination::Intersection {
2901 trim_termination_coords: closest_candidate.point,
2902 intersecting_seg_id: closest_candidate
2903 .segment_id
2904 .ok_or_else(|| "Missing segment_id for intersection".to_string())?,
2905 })
2906 } else {
2907 if is_circle_segment {
2908 return Err("Circle trim termination unexpectedly resolved to endpoint".to_string());
2909 }
2910 Ok(TrimTermination::SegEndPoint {
2912 trim_termination_coords: closest_candidate.point,
2913 })
2914 }
2915}
2916
2917#[cfg(test)]
2928#[allow(dead_code)]
2929pub(crate) async fn execute_trim_loop<F, Fut>(
2930 points: &[Coords2d],
2931 default_unit: UnitLength,
2932 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2933 mut execute_operations: F,
2934) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>
2935where
2936 F: FnMut(Vec<TrimOperation>, crate::frontend::api::SceneGraphDelta) -> Fut,
2937 Fut: std::future::Future<
2938 Output = Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>,
2939 >,
2940{
2941 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2943 let points = normalized_points.as_slice();
2944
2945 let mut start_index = 0;
2946 let max_iterations = 1000;
2947 let mut iteration_count = 0;
2948 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2949 crate::frontend::api::SourceDelta { text: String::new() },
2950 initial_scene_graph_delta.clone(),
2951 ));
2952 let mut invalidates_ids = false;
2953 let mut current_scene_graph_delta = initial_scene_graph_delta;
2954 let selected_intersection_counts =
2955 trim_stroke_intersection_counts(points, ¤t_scene_graph_delta.new_graph.objects, default_unit);
2956 let initial_intersection_count: usize = selected_intersection_counts.values().sum();
2957 let mut processed_intersection_counts: IndexMap<ArtifactId, usize> = IndexMap::new();
2958 let circle_delete_fallback_strategy =
2959 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2960 if !error.contains("No trim termination candidate found for circle") {
2961 return None;
2962 }
2963 let is_circle = scene_objects
2964 .iter()
2965 .find(|obj| obj.id == segment_id)
2966 .is_some_and(|obj| {
2967 matches!(
2968 obj.kind,
2969 ObjectKind::Segment {
2970 segment: Segment::Circle(_)
2971 }
2972 )
2973 });
2974 if is_circle {
2975 Some(vec![TrimOperation::SimpleTrim {
2976 segment_to_trim_id: segment_id,
2977 }])
2978 } else {
2979 None
2980 }
2981 };
2982
2983 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2984 iteration_count += 1;
2985
2986 let eligible_segment_ids: IndexSet<ObjectId> = current_scene_graph_delta
2988 .new_graph
2989 .objects
2990 .iter()
2991 .filter(|object| {
2992 initial_intersection_count > 1
2993 || selected_intersection_counts
2994 .get(&object.artifact_id)
2995 .copied()
2996 .unwrap_or(0)
2997 > processed_intersection_counts
2998 .get(&object.artifact_id)
2999 .copied()
3000 .unwrap_or(0)
3001 })
3002 .map(|object| object.id)
3003 .collect();
3004 let next_trim_spawn = get_next_trim_spawn_filtered(
3005 points,
3006 start_index,
3007 ¤t_scene_graph_delta.new_graph.objects,
3008 default_unit,
3009 Some(&eligible_segment_ids),
3010 );
3011
3012 match &next_trim_spawn {
3013 TrimItem::None { next_index } => {
3014 let old_start_index = start_index;
3015 start_index = *next_index;
3016
3017 if start_index <= old_start_index {
3019 start_index = old_start_index + 1;
3020 }
3021
3022 if start_index >= points.len().saturating_sub(1) {
3024 break;
3025 }
3026 continue;
3027 }
3028 TrimItem::Spawn {
3029 trim_spawn_seg_id,
3030 trim_spawn_coords,
3031 next_index,
3032 ..
3033 } => {
3034 let terminations = match get_trim_spawn_terminations(
3036 *trim_spawn_seg_id,
3037 points,
3038 ¤t_scene_graph_delta.new_graph.objects,
3039 default_unit,
3040 ) {
3041 Ok(terms) => terms,
3042 Err(e) => {
3043 crate::logln!("Error getting trim spawn terminations: {}", e);
3044 if let Some(strategy) = circle_delete_fallback_strategy(
3045 &e,
3046 *trim_spawn_seg_id,
3047 ¤t_scene_graph_delta.new_graph.objects,
3048 ) {
3049 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
3050 Ok((source_delta, scene_graph_delta)) => {
3051 last_result = Some((source_delta, scene_graph_delta.clone()));
3052 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
3053 current_scene_graph_delta = scene_graph_delta;
3054 }
3055 Err(exec_err) => {
3056 crate::logln!(
3057 "Error executing circle-delete fallback trim operation: {}",
3058 exec_err
3059 );
3060 }
3061 }
3062
3063 let old_start_index = start_index;
3064 start_index = *next_index;
3065 if start_index <= old_start_index {
3066 start_index = old_start_index + 1;
3067 }
3068 continue;
3069 }
3070
3071 let old_start_index = start_index;
3072 start_index = *next_index;
3073 if start_index <= old_start_index {
3074 start_index = old_start_index + 1;
3075 }
3076 continue;
3077 }
3078 };
3079
3080 let trim_spawn_segment = current_scene_graph_delta
3082 .new_graph
3083 .objects
3084 .iter()
3085 .find(|obj| obj.id == *trim_spawn_seg_id)
3086 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
3087 let trim_spawn_artifact_id = trim_spawn_segment.artifact_id;
3088
3089 let plan = match build_trim_plan(
3090 *trim_spawn_seg_id,
3091 *trim_spawn_coords,
3092 trim_spawn_segment,
3093 &terminations.left_side,
3094 &terminations.right_side,
3095 ¤t_scene_graph_delta.new_graph.objects,
3096 default_unit,
3097 ) {
3098 Ok(plan) => plan,
3099 Err(e) => {
3100 crate::logln!("Error determining trim strategy: {}", e);
3101 let old_start_index = start_index;
3102 start_index = *next_index;
3103 if start_index <= old_start_index {
3104 start_index = old_start_index + 1;
3105 }
3106 continue;
3107 }
3108 };
3109 let strategy = lower_trim_plan(&plan);
3110
3111 let mut geometry_was_modified = false;
3114
3115 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
3117 Ok((source_delta, scene_graph_delta)) => {
3118 last_result = Some((source_delta, scene_graph_delta.clone()));
3119 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
3120 current_scene_graph_delta = scene_graph_delta;
3121 geometry_was_modified = trim_plan_modifies_geometry(&plan);
3122 *processed_intersection_counts.entry(trim_spawn_artifact_id).or_default() += 1;
3123 }
3124 Err(e) => {
3125 crate::logln!("Error executing trim operations: {}", e);
3126 }
3128 }
3129
3130 let old_start_index = start_index;
3132 start_index = *next_index;
3133
3134 if start_index <= old_start_index && !geometry_was_modified {
3136 start_index = old_start_index + 1;
3137 }
3138 }
3139 }
3140 }
3141
3142 if iteration_count >= max_iterations {
3143 return Err(format!("Reached max iterations ({})", max_iterations));
3144 }
3145
3146 last_result.ok_or_else(|| "No trim operations were executed".to_string())
3148}
3149
3150#[cfg(test)]
3152#[derive(Debug, Clone)]
3153struct TrimFlowResult {
3154 pub kcl_code: String,
3155 pub invalidates_ids: bool,
3156}
3157
3158#[cfg(all(not(target_arch = "wasm32"), test))]
3174async fn execute_trim_flow(
3175 kcl_code: &str,
3176 trim_points: &[Coords2d],
3177 sketch_id: ObjectId,
3178) -> Result<TrimFlowResult, String> {
3179 use crate::ExecutorContext;
3180 use crate::Program;
3181 use crate::execution::MockConfig;
3182 use crate::frontend::FrontendState;
3183 use crate::frontend::api::Version;
3184
3185 let parse_result = Program::parse(kcl_code).map_err(|e| format!("Failed to parse KCL: {}", e))?;
3187 let (program_opt, errors) = parse_result;
3188 if !errors.is_empty() {
3189 return Err(format!("Failed to parse KCL: {:?}", errors));
3190 }
3191 let program = program_opt.ok_or_else(|| "No AST produced".to_string())?;
3192
3193 let mock_ctx = ExecutorContext::new_mock(None).await;
3194
3195 let result = async {
3197 let mut frontend = FrontendState::new();
3198
3199 frontend.program = program.clone();
3201
3202 let exec_outcome = mock_ctx
3203 .run_mock(&program, &MockConfig::default())
3204 .await
3205 .map_err(|e| format!("Failed to execute program: {}", e.error.message()))?;
3206
3207 let exec_outcome = frontend.update_state_after_exec(exec_outcome, false);
3208 let mut initial_scene_graph = frontend.scene_graph.clone();
3209
3210 if initial_scene_graph.objects.is_empty() && !exec_outcome.scene_objects.is_empty() {
3212 initial_scene_graph.objects = exec_outcome.scene_objects.clone();
3213 }
3214
3215 let actual_sketch_id = if let Some(sketch_mode) = initial_scene_graph.sketch_mode {
3218 sketch_mode
3219 } else {
3220 initial_scene_graph
3222 .objects
3223 .iter()
3224 .find(|obj| matches!(obj.kind, crate::frontend::api::ObjectKind::Sketch { .. }))
3225 .map(|obj| obj.id)
3226 .unwrap_or(sketch_id) };
3228
3229 let version = Version(0);
3230 let initial_scene_graph_delta = crate::frontend::api::SceneGraphDelta {
3231 new_graph: initial_scene_graph,
3232 new_objects: vec![],
3233 invalidates_ids: false,
3234 exec_outcome,
3235 };
3236
3237 let (source_delta, scene_graph_delta) = execute_trim_loop_with_context(
3242 trim_points,
3243 initial_scene_graph_delta,
3244 &mut frontend,
3245 &mock_ctx,
3246 version,
3247 actual_sketch_id,
3248 )
3249 .await?;
3250
3251 if source_delta.text.is_empty() {
3254 return Err("No trim operations were executed - source delta is empty".to_string());
3255 }
3256
3257 Ok(TrimFlowResult {
3258 kcl_code: source_delta.text,
3259 invalidates_ids: scene_graph_delta.invalidates_ids,
3260 })
3261 }
3262 .await;
3263
3264 mock_ctx.close().await;
3266
3267 result
3268}
3269
3270fn normalize_scene_graph_delta_for_internal_trim(
3271 frontend: &crate::frontend::FrontendState,
3272 scene_graph_delta: &mut crate::frontend::api::SceneGraphDelta,
3273) {
3274 scene_graph_delta.new_graph = frontend.scene_graph().clone();
3275}
3276
3277pub async fn execute_trim_loop_with_context(
3283 points: &[Coords2d],
3284 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
3285 frontend: &mut crate::frontend::FrontendState,
3286 ctx: &crate::ExecutorContext,
3287 version: crate::frontend::api::Version,
3288 sketch_id: ObjectId,
3289) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
3290 let default_unit = frontend.default_length_unit();
3292 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
3293
3294 let mut current_scene_graph_delta = initial_scene_graph_delta.clone();
3297 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
3298 crate::frontend::api::SourceDelta { text: String::new() },
3299 initial_scene_graph_delta.clone(),
3300 ));
3301 let mut invalidates_ids = false;
3302 let mut start_index = 0;
3303 let max_iterations = 1000;
3304 let mut iteration_count = 0;
3305 let circle_delete_fallback_strategy =
3306 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
3307 if !error.contains("No trim termination candidate found for circle") {
3308 return None;
3309 }
3310 let is_circle = scene_objects
3311 .iter()
3312 .find(|obj| obj.id == segment_id)
3313 .is_some_and(|obj| {
3314 matches!(
3315 obj.kind,
3316 ObjectKind::Segment {
3317 segment: Segment::Circle(_)
3318 }
3319 )
3320 });
3321 if is_circle {
3322 Some(vec![TrimOperation::SimpleTrim {
3323 segment_to_trim_id: segment_id,
3324 }])
3325 } else {
3326 None
3327 }
3328 };
3329
3330 let points = normalized_points.as_slice();
3331 let selected_intersection_counts =
3332 trim_stroke_intersection_counts(points, ¤t_scene_graph_delta.new_graph.objects, default_unit);
3333 let initial_intersection_count: usize = selected_intersection_counts.values().sum();
3334 let mut processed_intersection_counts: IndexMap<ArtifactId, usize> = IndexMap::new();
3335
3336 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
3337 iteration_count += 1;
3338
3339 let eligible_segment_ids: IndexSet<ObjectId> = current_scene_graph_delta
3341 .new_graph
3342 .objects
3343 .iter()
3344 .filter(|object| {
3345 initial_intersection_count > 1
3346 || selected_intersection_counts
3347 .get(&object.artifact_id)
3348 .copied()
3349 .unwrap_or(0)
3350 > processed_intersection_counts
3351 .get(&object.artifact_id)
3352 .copied()
3353 .unwrap_or(0)
3354 })
3355 .map(|object| object.id)
3356 .collect();
3357 let next_trim_spawn = get_next_trim_spawn_filtered(
3358 points,
3359 start_index,
3360 ¤t_scene_graph_delta.new_graph.objects,
3361 default_unit,
3362 Some(&eligible_segment_ids),
3363 );
3364
3365 match &next_trim_spawn {
3366 TrimItem::None { next_index } => {
3367 let old_start_index = start_index;
3368 start_index = *next_index;
3369 if start_index <= old_start_index {
3370 start_index = old_start_index + 1;
3371 }
3372 if start_index >= points.len().saturating_sub(1) {
3373 break;
3374 }
3375 continue;
3376 }
3377 TrimItem::Spawn {
3378 trim_spawn_seg_id,
3379 trim_spawn_coords,
3380 next_index,
3381 ..
3382 } => {
3383 let terminations = match get_trim_spawn_terminations(
3385 *trim_spawn_seg_id,
3386 points,
3387 ¤t_scene_graph_delta.new_graph.objects,
3388 default_unit,
3389 ) {
3390 Ok(terms) => terms,
3391 Err(e) => {
3392 crate::logln!("Error getting trim spawn terminations: {}", e);
3393 if let Some(strategy) = circle_delete_fallback_strategy(
3394 &e,
3395 *trim_spawn_seg_id,
3396 ¤t_scene_graph_delta.new_graph.objects,
3397 ) {
3398 match execute_trim_operations_simple(
3399 strategy.clone(),
3400 ¤t_scene_graph_delta,
3401 frontend,
3402 ctx,
3403 version,
3404 sketch_id,
3405 )
3406 .await
3407 {
3408 Ok((source_delta, mut scene_graph_delta)) => {
3409 normalize_scene_graph_delta_for_internal_trim(frontend, &mut scene_graph_delta);
3410 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
3411 last_result = Some((source_delta, scene_graph_delta.clone()));
3412 current_scene_graph_delta = scene_graph_delta;
3413 if let Some(object) = current_scene_graph_delta
3414 .new_graph
3415 .objects
3416 .iter()
3417 .find(|object| object.id == *trim_spawn_seg_id)
3418 {
3419 *processed_intersection_counts.entry(object.artifact_id).or_default() += 1;
3420 }
3421 }
3422 Err(exec_err) => {
3423 crate::logln!(
3424 "Error executing circle-delete fallback trim operation: {}",
3425 exec_err
3426 );
3427 }
3428 }
3429
3430 let old_start_index = start_index;
3431 start_index = *next_index;
3432 if start_index <= old_start_index {
3433 start_index = old_start_index + 1;
3434 }
3435 continue;
3436 }
3437
3438 let old_start_index = start_index;
3439 start_index = *next_index;
3440 if start_index <= old_start_index {
3441 start_index = old_start_index + 1;
3442 }
3443 continue;
3444 }
3445 };
3446
3447 let trim_spawn_segment = current_scene_graph_delta
3449 .new_graph
3450 .objects
3451 .iter()
3452 .find(|obj| obj.id == *trim_spawn_seg_id)
3453 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
3454 let trim_spawn_artifact_id = trim_spawn_segment.artifact_id;
3455
3456 let plan = match build_trim_plan(
3457 *trim_spawn_seg_id,
3458 *trim_spawn_coords,
3459 trim_spawn_segment,
3460 &terminations.left_side,
3461 &terminations.right_side,
3462 ¤t_scene_graph_delta.new_graph.objects,
3463 default_unit,
3464 ) {
3465 Ok(plan) => plan,
3466 Err(e) => {
3467 crate::logln!("Error determining trim strategy: {}", e);
3468 let old_start_index = start_index;
3469 start_index = *next_index;
3470 if start_index <= old_start_index {
3471 start_index = old_start_index + 1;
3472 }
3473 continue;
3474 }
3475 };
3476 let strategy = lower_trim_plan(&plan);
3477 let mut geometry_was_modified = false;
3480
3481 match execute_trim_operations_simple(
3483 strategy.clone(),
3484 ¤t_scene_graph_delta,
3485 frontend,
3486 ctx,
3487 version,
3488 sketch_id,
3489 )
3490 .await
3491 {
3492 Ok((source_delta, mut scene_graph_delta)) => {
3493 normalize_scene_graph_delta_for_internal_trim(frontend, &mut scene_graph_delta);
3494 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
3495 last_result = Some((source_delta, scene_graph_delta.clone()));
3496 current_scene_graph_delta = scene_graph_delta;
3497 geometry_was_modified = trim_plan_modifies_geometry(&plan);
3498 *processed_intersection_counts.entry(trim_spawn_artifact_id).or_default() += 1;
3499 }
3500 Err(e) => {
3501 crate::logln!("Error executing trim operations: {}", e);
3502 }
3503 }
3504
3505 let old_start_index = start_index;
3507 start_index = *next_index;
3508 if start_index <= old_start_index && !geometry_was_modified {
3509 start_index = old_start_index + 1;
3510 }
3511 }
3512 }
3513 }
3514
3515 if iteration_count >= max_iterations {
3516 return Err(format!("Reached max iterations ({})", max_iterations));
3517 }
3518
3519 let (source_delta, mut scene_graph_delta) =
3520 last_result.ok_or_else(|| "No trim operations were executed".to_string())?;
3521 scene_graph_delta.invalidates_ids = invalidates_ids;
3523 Ok((source_delta, scene_graph_delta))
3524}
3525
3526fn segment_ctor_units(ctor: &SegmentCtor) -> NumericSuffix {
3586 match ctor {
3587 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
3588 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3589 _ => NumericSuffix::Mm,
3590 },
3591 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
3592 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3593 _ => NumericSuffix::Mm,
3594 },
3595 SegmentCtor::Circle(circle_ctor) => match &circle_ctor.start.x {
3596 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3597 _ => NumericSuffix::Mm,
3598 },
3599 SegmentCtor::ControlPointSpline(spline_ctor) => spline_ctor
3600 .points
3601 .first()
3602 .and_then(|point| match &point.x {
3603 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => Some(v.units),
3604 _ => None,
3605 })
3606 .unwrap_or(NumericSuffix::Mm),
3607 SegmentCtor::Point(point_ctor) => match &point_ctor.position.x {
3608 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3609 _ => NumericSuffix::Mm,
3610 },
3611 }
3612}
3613
3614fn coords_to_expr_point(
3615 coords: Coords2d,
3616 default_unit: UnitLength,
3617 units: NumericSuffix,
3618) -> crate::frontend::sketch::Point2d<crate::frontend::api::Expr> {
3619 crate::frontend::sketch::Point2d {
3620 x: crate::frontend::api::Expr::Var(unit_to_number(coords.x, default_unit, units)),
3621 y: crate::frontend::api::Expr::Var(unit_to_number(coords.y, default_unit, units)),
3622 }
3623}
3624
3625fn resample_control_point_spline_interval(
3626 controls: &[Coords2d],
3627 degree: usize,
3628 start_parameter: f64,
3629 end_parameter: f64,
3630 control_count: usize,
3631) -> Vec<Coords2d> {
3632 let knots = build_open_uniform_knot_vector(controls.len(), degree);
3633 (0..control_count)
3634 .map(|index| {
3635 let ratio = if control_count <= 1 {
3636 0.0
3637 } else {
3638 index as f64 / (control_count - 1) as f64
3639 };
3640 let parameter = start_parameter + ratio * (end_parameter - start_parameter);
3641 de_boor_point(parameter, degree, &knots, controls)
3642 })
3643 .collect()
3644}
3645
3646fn build_trimmed_control_point_spline_ctor(
3647 trim_spawn_segment: &Object,
3648 objects: &[Object],
3649 default_unit: UnitLength,
3650 start_parameter: f64,
3651 end_parameter: f64,
3652) -> Result<SegmentCtor, String> {
3653 let ObjectKind::Segment {
3654 segment: Segment::ControlPointSpline(spline),
3655 } = &trim_spawn_segment.kind
3656 else {
3657 return Err("Trim spawn segment is not a control point spline".to_string());
3658 };
3659 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
3660 return Err("Control point spline segment is missing a control point spline ctor".to_string());
3661 };
3662 let controls = get_control_point_spline_controls(trim_spawn_segment, objects, default_unit)?
3663 .into_iter()
3664 .map(|(_, point)| point)
3665 .collect::<Vec<_>>();
3666 let units = segment_ctor_units(&spline.ctor);
3667 let resampled = resample_control_point_spline_interval(
3668 &controls,
3669 spline.degree as usize,
3670 start_parameter,
3671 end_parameter,
3672 spline.controls.len(),
3673 );
3674 Ok(SegmentCtor::ControlPointSpline(
3675 crate::frontend::sketch::ControlPointSplineCtor {
3676 points: resampled
3677 .into_iter()
3678 .map(|coords| coords_to_expr_point(coords, default_unit, units))
3679 .collect(),
3680 construction: spline_ctor.construction,
3681 },
3682 ))
3683}
3684
3685fn spline_constraint_ids_to_delete(
3686 spline: &crate::frontend::sketch::ControlPointSpline,
3687 trimmed_endpoint_id: Option<ObjectId>,
3688 objects: &[Object],
3689) -> Vec<ObjectId> {
3690 let internal_control_ids: std::collections::HashSet<ObjectId> = spline
3691 .controls
3692 .iter()
3693 .copied()
3694 .skip(1)
3695 .take(spline.controls.len().saturating_sub(2))
3696 .collect();
3697 let spline_control_ids: std::collections::HashSet<ObjectId> = spline.controls.iter().copied().collect();
3698 let mut deletions = IndexSet::new();
3699
3700 for obj in objects {
3701 let ObjectKind::Constraint { constraint } = &obj.kind else {
3702 continue;
3703 };
3704 match constraint {
3705 Constraint::Coincident(coincident) => {
3706 let ids: Vec<ObjectId> = coincident.segment_ids().collect();
3707 if ids.iter().any(|id| internal_control_ids.contains(id))
3708 || trimmed_endpoint_id.is_some_and(|endpoint_id| ids.contains(&endpoint_id))
3709 {
3710 deletions.insert(obj.id);
3711 }
3712 }
3713 Constraint::Distance(distance)
3714 | Constraint::HorizontalDistance(distance)
3715 | Constraint::VerticalDistance(distance)
3716 if distance.point_ids().any(|id| spline_control_ids.contains(&id)) =>
3717 {
3718 deletions.insert(obj.id);
3719 }
3720 Constraint::Horizontal(Horizontal::Points { points })
3721 | Constraint::Vertical(Vertical::Points { points })
3722 if points.iter().any(
3723 |point| matches!(point, ConstraintSegment::Segment(id) if spline_control_ids.contains(id)),
3724 ) =>
3725 {
3726 deletions.insert(obj.id);
3727 }
3728 Constraint::Fixed(fixed)
3729 if fixed
3730 .points
3731 .iter()
3732 .any(|fixed_point| spline_control_ids.contains(&fixed_point.point)) =>
3733 {
3734 deletions.insert(obj.id);
3735 }
3736 _ => {}
3737 }
3738 }
3739
3740 deletions.into_iter().collect()
3741}
3742
3743fn build_trim_plan(
3744 trim_spawn_id: ObjectId,
3745 trim_spawn_coords: Coords2d,
3746 trim_spawn_segment: &Object,
3747 left_side: &TrimTermination,
3748 right_side: &TrimTermination,
3749 objects: &[Object],
3750 default_unit: UnitLength,
3751) -> Result<TrimPlan, String> {
3752 if matches!(left_side, TrimTermination::SegEndPoint { .. })
3754 && matches!(right_side, TrimTermination::SegEndPoint { .. })
3755 {
3756 return Ok(TrimPlan::DeleteSegment {
3757 segment_id: trim_spawn_id,
3758 });
3759 }
3760
3761 let is_intersect_or_coincident = |side: &TrimTermination| -> bool {
3763 matches!(
3764 side,
3765 TrimTermination::Intersection { .. }
3766 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3767 )
3768 };
3769
3770 let left_side_needs_tail_cut = is_intersect_or_coincident(left_side) && !is_intersect_or_coincident(right_side);
3771 let right_side_needs_tail_cut = is_intersect_or_coincident(right_side) && !is_intersect_or_coincident(left_side);
3772
3773 let ObjectKind::Segment { segment } = &trim_spawn_segment.kind else {
3775 return Err("Trim spawn segment is not a segment".to_string());
3776 };
3777
3778 let (_segment_type, ctor) = match segment {
3779 Segment::Line(line) => ("Line", &line.ctor),
3780 Segment::Arc(arc) => ("Arc", &arc.ctor),
3781 Segment::Circle(circle) => ("Circle", &circle.ctor),
3782 Segment::ControlPointSpline(spline) => ("ControlPointSpline", &spline.ctor),
3783 _ => {
3784 return Err("Trim spawn segment is not a Line, Arc, Circle, or Control Point Spline".to_string());
3785 }
3786 };
3787
3788 let units = segment_ctor_units(ctor);
3790
3791 let find_distance_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
3793 let mut constraint_ids = Vec::new();
3794 for obj in objects {
3795 let ObjectKind::Constraint { constraint } = &obj.kind else {
3796 continue;
3797 };
3798
3799 let Constraint::Distance(distance) = constraint else {
3800 continue;
3801 };
3802
3803 let points_owned_by_segment: Vec<bool> = distance
3809 .point_ids()
3810 .map(|point_id| {
3811 if let Some(point_obj) = objects.iter().find(|o| o.id == point_id)
3812 && let ObjectKind::Segment { segment } = &point_obj.kind
3813 && let Segment::Point(point) = segment
3814 && let Some(owner_id) = point.owner
3815 {
3816 return owner_id == segment_id;
3817 }
3818 false
3819 })
3820 .collect();
3821
3822 if points_owned_by_segment.len() == 2 && points_owned_by_segment.iter().all(|&owned| owned) {
3824 constraint_ids.push(obj.id);
3825 }
3826 }
3827 constraint_ids
3828 };
3829
3830 let find_existing_point_segment_coincident =
3832 |trim_seg_id: ObjectId, intersecting_seg_id: ObjectId| -> CoincidentData {
3833 let lookup_by_point_id = |point_id: ObjectId| -> Option<CoincidentData> {
3835 for obj in objects {
3836 let ObjectKind::Constraint { constraint } = &obj.kind else {
3837 continue;
3838 };
3839
3840 let Constraint::Coincident(coincident) = constraint else {
3841 continue;
3842 };
3843
3844 let involves_trim_seg = coincident.segment_ids().any(|id| id == trim_seg_id || id == point_id);
3845 let involves_point = coincident.contains_segment(point_id);
3846
3847 if involves_trim_seg && involves_point {
3848 return Some(CoincidentData {
3849 intersecting_seg_id,
3850 intersecting_endpoint_point_id: Some(point_id),
3851 existing_point_segment_constraint_id: Some(obj.id),
3852 });
3853 }
3854 }
3855 None
3856 };
3857
3858 let trim_seg = objects.iter().find(|obj| obj.id == trim_seg_id);
3860
3861 let mut trim_endpoint_ids: Vec<ObjectId> = Vec::new();
3862 if let Some(seg) = trim_seg
3863 && let ObjectKind::Segment { segment } = &seg.kind
3864 {
3865 match segment {
3866 Segment::Line(line) => {
3867 trim_endpoint_ids.push(line.start);
3868 trim_endpoint_ids.push(line.end);
3869 }
3870 Segment::Arc(arc) => {
3871 trim_endpoint_ids.push(arc.start);
3872 trim_endpoint_ids.push(arc.end);
3873 }
3874 Segment::ControlPointSpline(spline) => {
3875 if let Some(start) = spline.controls.first() {
3876 trim_endpoint_ids.push(*start);
3877 }
3878 if let Some(end) = spline.controls.last() {
3879 trim_endpoint_ids.push(*end);
3880 }
3881 }
3882 _ => {}
3883 }
3884 }
3885
3886 let intersecting_obj = objects.iter().find(|obj| obj.id == intersecting_seg_id);
3887
3888 if let Some(obj) = intersecting_obj
3889 && let ObjectKind::Segment { segment } = &obj.kind
3890 && let Segment::Point(_) = segment
3891 && let Some(found) = lookup_by_point_id(intersecting_seg_id)
3892 {
3893 return found;
3894 }
3895
3896 let mut intersecting_endpoint_ids: Vec<ObjectId> = Vec::new();
3898 if let Some(obj) = intersecting_obj
3899 && let ObjectKind::Segment { segment } = &obj.kind
3900 {
3901 match segment {
3902 Segment::Line(line) => {
3903 intersecting_endpoint_ids.push(line.start);
3904 intersecting_endpoint_ids.push(line.end);
3905 }
3906 Segment::Arc(arc) => {
3907 intersecting_endpoint_ids.push(arc.start);
3908 intersecting_endpoint_ids.push(arc.end);
3909 }
3910 Segment::ControlPointSpline(spline) => {
3911 if let Some(start) = spline.controls.first() {
3912 intersecting_endpoint_ids.push(*start);
3913 }
3914 if let Some(end) = spline.controls.last() {
3915 intersecting_endpoint_ids.push(*end);
3916 }
3917 }
3918 _ => {}
3919 }
3920 }
3921
3922 intersecting_endpoint_ids.push(intersecting_seg_id);
3924
3925 for obj in objects {
3927 let ObjectKind::Constraint { constraint } = &obj.kind else {
3928 continue;
3929 };
3930
3931 let Constraint::Coincident(coincident) = constraint else {
3932 continue;
3933 };
3934
3935 let constraint_segment_ids: Vec<ObjectId> = coincident.get_segments();
3936
3937 let involves_trim_seg = constraint_segment_ids.contains(&trim_seg_id)
3939 || trim_endpoint_ids.iter().any(|&id| constraint_segment_ids.contains(&id));
3940
3941 if !involves_trim_seg {
3942 continue;
3943 }
3944
3945 if let Some(&intersecting_endpoint_id) = intersecting_endpoint_ids
3947 .iter()
3948 .find(|&&id| constraint_segment_ids.contains(&id))
3949 {
3950 return CoincidentData {
3951 intersecting_seg_id,
3952 intersecting_endpoint_point_id: Some(intersecting_endpoint_id),
3953 existing_point_segment_constraint_id: Some(obj.id),
3954 };
3955 }
3956 }
3957
3958 CoincidentData {
3960 intersecting_seg_id,
3961 intersecting_endpoint_point_id: None,
3962 existing_point_segment_constraint_id: None,
3963 }
3964 };
3965
3966 let find_point_segment_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<serde_json::Value> {
3968 let mut constraints: Vec<serde_json::Value> = Vec::new();
3969 for obj in objects {
3970 let ObjectKind::Constraint { constraint } = &obj.kind else {
3971 continue;
3972 };
3973
3974 let Constraint::Coincident(coincident) = constraint else {
3975 continue;
3976 };
3977
3978 if !coincident.contains_segment(endpoint_point_id) {
3980 continue;
3981 }
3982
3983 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3985
3986 if let Some(other_id) = other_segment_id
3987 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3988 {
3989 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3991 constraints.push(serde_json::json!({
3992 "constraintId": obj.id.0,
3993 "segmentOrPointId": other_id.0,
3994 }));
3995 }
3996 }
3997 }
3998 constraints
3999 };
4000
4001 let find_point_point_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
4004 let mut constraint_ids = Vec::new();
4005 for obj in objects {
4006 let ObjectKind::Constraint { constraint } = &obj.kind else {
4007 continue;
4008 };
4009
4010 let Constraint::Coincident(coincident) = constraint else {
4011 continue;
4012 };
4013
4014 if !coincident.contains_segment(endpoint_point_id) {
4016 continue;
4017 }
4018
4019 let is_point_point = coincident.segment_ids().all(|seg_id| {
4021 if let Some(seg_obj) = objects.iter().find(|o| o.id == seg_id) {
4022 matches!(&seg_obj.kind, ObjectKind::Segment { segment } if matches!(segment, Segment::Point(_)))
4023 } else {
4024 false
4025 }
4026 });
4027
4028 if is_point_point {
4029 constraint_ids.push(obj.id);
4030 }
4031 }
4032 constraint_ids
4033 };
4034
4035 let find_point_segment_coincident_constraint_ids = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
4038 let mut constraint_ids = Vec::new();
4039 for obj in objects {
4040 let ObjectKind::Constraint { constraint } = &obj.kind else {
4041 continue;
4042 };
4043
4044 let Constraint::Coincident(coincident) = constraint else {
4045 continue;
4046 };
4047
4048 if !coincident.contains_segment(endpoint_point_id) {
4050 continue;
4051 }
4052
4053 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
4055
4056 if let Some(other_id) = other_segment_id
4057 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
4058 {
4059 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
4061 constraint_ids.push(obj.id);
4062 }
4063 }
4064 }
4065 constraint_ids
4066 };
4067
4068 let find_body_coincident_constraints_at_endpoint =
4072 |segment_id: ObjectId, endpoint_coords: Coords2d| -> Vec<ObjectId> {
4073 objects
4074 .iter()
4075 .filter_map(|obj| {
4076 let ObjectKind::Constraint {
4077 constraint: Constraint::Coincident(coincident),
4078 } = &obj.kind
4079 else {
4080 return None;
4081 };
4082 if !coincident.contains_segment(segment_id) {
4083 return None;
4084 }
4085 coincident
4086 .segment_ids()
4087 .filter(|id| *id != segment_id)
4088 .filter_map(|point_id| get_point_coords_from_native(objects, point_id, default_unit))
4089 .any(|point| {
4090 ((point.x - endpoint_coords.x).squared() + (point.y - endpoint_coords.y).squared()).sqrt()
4091 < EPSILON_POINT_ON_SEGMENT
4092 })
4093 .then_some(obj.id)
4094 })
4095 .collect()
4096 };
4097
4098 let find_midpoint_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
4099 objects
4100 .iter()
4101 .filter_map(|obj| {
4102 let ObjectKind::Constraint { constraint } = &obj.kind else {
4103 return None;
4104 };
4105
4106 let Constraint::Midpoint(midpoint) = constraint else {
4107 return None;
4108 };
4109
4110 (midpoint.segment == segment_id).then_some(obj.id)
4111 })
4112 .collect()
4113 };
4114
4115 if left_side_needs_tail_cut || right_side_needs_tail_cut {
4117 let side = if left_side_needs_tail_cut {
4118 left_side
4119 } else {
4120 right_side
4121 };
4122
4123 let intersection_coords = match side {
4124 TrimTermination::Intersection {
4125 trim_termination_coords,
4126 ..
4127 }
4128 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4129 trim_termination_coords,
4130 ..
4131 } => *trim_termination_coords,
4132 TrimTermination::SegEndPoint { .. } => {
4133 return Err("Logic error: side should not be segEndPoint here".to_string());
4134 }
4135 };
4136
4137 let endpoint_to_change = if left_side_needs_tail_cut {
4138 EndpointChanged::End
4139 } else {
4140 EndpointChanged::Start
4141 };
4142
4143 let intersecting_seg_id = match side {
4144 TrimTermination::Intersection {
4145 intersecting_seg_id, ..
4146 }
4147 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4148 intersecting_seg_id, ..
4149 } => *intersecting_seg_id,
4150 TrimTermination::SegEndPoint { .. } => {
4151 return Err("Logic error".to_string());
4152 }
4153 };
4154
4155 let mut coincident_data = if matches!(
4156 side,
4157 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
4158 ) {
4159 let point_id = match side {
4160 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4161 other_segment_point_id, ..
4162 } => *other_segment_point_id,
4163 _ => return Err("Logic error".to_string()),
4164 };
4165 let mut data = find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id);
4166 data.intersecting_endpoint_point_id = Some(point_id);
4167 data
4168 } else {
4169 find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id)
4170 };
4171
4172 if matches!(side, TrimTermination::Intersection { .. })
4173 && let Some(point_id) = coincident_data.intersecting_endpoint_point_id
4174 {
4175 let endpoint_is_at_intersection = get_point_coords_from_native(objects, point_id, default_unit)
4176 .is_some_and(|point_coords| {
4177 ((point_coords.x - intersection_coords.x).squared()
4178 + (point_coords.y - intersection_coords.y).squared())
4179 .sqrt()
4180 <= EPSILON_POINT_ON_SEGMENT * 1000.0
4181 });
4182
4183 if !endpoint_is_at_intersection {
4184 coincident_data.existing_point_segment_constraint_id = None;
4185 coincident_data.intersecting_endpoint_point_id = None;
4186 }
4187 }
4188
4189 let trim_seg = objects.iter().find(|obj| obj.id == trim_spawn_id);
4191
4192 let endpoint_point_id = if let Some(seg) = trim_seg {
4193 let ObjectKind::Segment { segment } = &seg.kind else {
4194 return Err("Trim spawn segment is not a segment".to_string());
4195 };
4196 match segment {
4197 Segment::Line(line) => {
4198 if endpoint_to_change == EndpointChanged::Start {
4199 Some(line.start)
4200 } else {
4201 Some(line.end)
4202 }
4203 }
4204 Segment::Arc(arc) => {
4205 if endpoint_to_change == EndpointChanged::Start {
4206 Some(arc.start)
4207 } else {
4208 Some(arc.end)
4209 }
4210 }
4211 Segment::ControlPointSpline(spline) => {
4212 if endpoint_to_change == EndpointChanged::Start {
4213 spline.controls.first().copied()
4214 } else {
4215 spline.controls.last().copied()
4216 }
4217 }
4218 _ => None,
4219 }
4220 } else {
4221 None
4222 };
4223
4224 if let (Some(endpoint_id), Some(existing_constraint_id)) =
4225 (endpoint_point_id, coincident_data.existing_point_segment_constraint_id)
4226 {
4227 let constraint_involves_trimmed_endpoint = objects
4228 .iter()
4229 .find(|obj| obj.id == existing_constraint_id)
4230 .and_then(|obj| match &obj.kind {
4231 ObjectKind::Constraint {
4232 constraint: Constraint::Coincident(coincident),
4233 } => Some(coincident.contains_segment(endpoint_id) || coincident.contains_segment(trim_spawn_id)),
4234 _ => None,
4235 })
4236 .unwrap_or(false);
4237
4238 if !constraint_involves_trimmed_endpoint {
4239 coincident_data.existing_point_segment_constraint_id = None;
4240 coincident_data.intersecting_endpoint_point_id = None;
4241 }
4242 }
4243
4244 let coincident_end_constraint_to_delete_ids = if let Some(point_id) = endpoint_point_id {
4246 let mut constraint_ids = find_point_point_coincident_constraints(point_id);
4247 constraint_ids.extend(find_point_segment_coincident_constraint_ids(point_id));
4249 constraint_ids
4250 } else {
4251 Vec::new()
4252 };
4253 let trimmed_endpoint_coords = match endpoint_to_change {
4254 EndpointChanged::Start => load_curve_handle(trim_spawn_segment, objects, default_unit)?.start,
4255 EndpointChanged::End => load_curve_handle(trim_spawn_segment, objects, default_unit)?.end,
4256 };
4257
4258 let point_axis_constraint_ids_to_delete = if let Some(point_id) = endpoint_point_id {
4259 objects
4260 .iter()
4261 .filter_map(|obj| {
4262 let ObjectKind::Constraint { constraint } = &obj.kind else {
4263 return None;
4264 };
4265
4266 point_axis_constraint_references_point(constraint, point_id).then_some(obj.id)
4267 })
4268 .collect::<Vec<_>>()
4269 } else {
4270 Vec::new()
4271 };
4272
4273 if let Segment::ControlPointSpline(spline) = segment {
4274 let trim_curve = load_curve_handle(trim_spawn_segment, objects, default_unit)?;
4275 let intersection_parameter = project_point_onto_curve(&trim_curve, intersection_coords)?;
4276 let end_parameter = trim_curve
4277 .sampled_points
4278 .as_ref()
4279 .and_then(|samples| samples.last())
4280 .map(|sample| sample.parameter)
4281 .unwrap_or_else(|| spline.controls.len().saturating_sub(1) as f64);
4282 let (keep_start_parameter, keep_end_parameter) = if endpoint_to_change == EndpointChanged::End {
4283 (0.0, intersection_parameter)
4284 } else {
4285 (intersection_parameter, end_parameter)
4286 };
4287 let new_ctor = build_trimmed_control_point_spline_ctor(
4288 trim_spawn_segment,
4289 objects,
4290 default_unit,
4291 keep_start_parameter,
4292 keep_end_parameter,
4293 )?;
4294
4295 let mut all_constraint_ids_to_delete = spline_constraint_ids_to_delete(spline, endpoint_point_id, objects);
4296 all_constraint_ids_to_delete.extend(coincident_end_constraint_to_delete_ids);
4297 all_constraint_ids_to_delete.extend(point_axis_constraint_ids_to_delete);
4298 all_constraint_ids_to_delete.extend(find_distance_constraints_for_segment(trim_spawn_id));
4299 all_constraint_ids_to_delete.sort_unstable();
4300 all_constraint_ids_to_delete.dedup();
4301
4302 return Ok(TrimPlan::TailCutControlPointSpline {
4303 segment_id: trim_spawn_id,
4304 ctor: new_ctor,
4305 constraint_ids_to_delete: all_constraint_ids_to_delete,
4306 });
4307 }
4308
4309 let new_ctor = match ctor {
4311 SegmentCtor::Line(line_ctor) => {
4312 let new_point = crate::frontend::sketch::Point2d {
4314 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
4315 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
4316 };
4317 if endpoint_to_change == EndpointChanged::Start {
4318 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
4319 start: new_point,
4320 end: line_ctor.end.clone(),
4321 construction: line_ctor.construction,
4322 })
4323 } else {
4324 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
4325 start: line_ctor.start.clone(),
4326 end: new_point,
4327 construction: line_ctor.construction,
4328 })
4329 }
4330 }
4331 SegmentCtor::Arc(arc_ctor) => {
4332 let new_point = crate::frontend::sketch::Point2d {
4334 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
4335 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
4336 };
4337 if endpoint_to_change == EndpointChanged::Start {
4338 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4339 start: new_point,
4340 end: arc_ctor.end.clone(),
4341 center: arc_ctor.center.clone(),
4342 direction: arc_ctor.direction,
4343 construction: arc_ctor.construction,
4344 })
4345 } else {
4346 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4347 start: arc_ctor.start.clone(),
4348 end: new_point,
4349 center: arc_ctor.center.clone(),
4350 direction: arc_ctor.direction,
4351 construction: arc_ctor.construction,
4352 })
4353 }
4354 }
4355 _ => {
4356 return Err("Unsupported segment type for edit".to_string());
4357 }
4358 };
4359
4360 let mut all_constraint_ids_to_delete: Vec<ObjectId> = Vec::new();
4362 if let Some(constraint_id) = coincident_data.existing_point_segment_constraint_id {
4363 all_constraint_ids_to_delete.push(constraint_id);
4364 }
4365 all_constraint_ids_to_delete.extend(coincident_end_constraint_to_delete_ids);
4366 all_constraint_ids_to_delete.extend(find_body_coincident_constraints_at_endpoint(
4367 trim_spawn_id,
4368 trimmed_endpoint_coords,
4369 ));
4370 all_constraint_ids_to_delete.extend(point_axis_constraint_ids_to_delete);
4371 all_constraint_ids_to_delete.extend(find_midpoint_constraints_for_segment(trim_spawn_id));
4372
4373 let distance_constraint_ids = find_distance_constraints_for_segment(trim_spawn_id);
4376 all_constraint_ids_to_delete.extend(distance_constraint_ids);
4377 all_constraint_ids_to_delete.sort_unstable();
4378 all_constraint_ids_to_delete.dedup();
4379
4380 let coincident_target_id = coincident_data
4381 .intersecting_endpoint_point_id
4382 .unwrap_or(intersecting_seg_id);
4383 let adds_curved_segment_coincident = endpoint_point_id
4384 .is_some_and(|point_id| segment_id_is_or_is_owned_by_curve(objects, point_id))
4385 || segment_id_is_or_is_owned_by_curve(objects, coincident_target_id);
4386 let has_midpoint_deletions = all_constraint_ids_to_delete.iter().any(|constraint_id| {
4387 objects
4388 .iter()
4389 .find(|obj| obj.id == *constraint_id)
4390 .is_some_and(|object| {
4391 matches!(
4392 object.kind,
4393 ObjectKind::Constraint {
4394 constraint: Constraint::Midpoint(_)
4395 }
4396 )
4397 })
4398 });
4399
4400 let mut additional_edited_segment_ids = IndexSet::new();
4401 if has_midpoint_deletions || (adds_curved_segment_coincident && all_constraint_ids_to_delete.is_empty()) {
4402 additional_edited_segment_ids.extend(sketch_segment_ids_for_segment(objects, trim_spawn_id));
4403 }
4404
4405 if adds_curved_segment_coincident {
4406 for constraint_id in &all_constraint_ids_to_delete {
4407 let Some(constraint_object) = objects.iter().find(|obj| obj.id == *constraint_id) else {
4408 continue;
4409 };
4410 let ObjectKind::Constraint {
4411 constraint: Constraint::Coincident(coincident),
4412 } = &constraint_object.kind
4413 else {
4414 continue;
4415 };
4416
4417 additional_edited_segment_ids.extend(
4418 coincident
4419 .segment_ids()
4420 .map(|segment_id| owner_or_segment_id(objects, segment_id)),
4421 );
4422 }
4423 }
4424
4425 return Ok(TrimPlan::TailCut {
4426 segment_id: trim_spawn_id,
4427 endpoint_changed: endpoint_to_change,
4428 ctor: new_ctor,
4429 segment_or_point_to_make_coincident_to: intersecting_seg_id,
4430 intersecting_endpoint_point_id: coincident_data.intersecting_endpoint_point_id,
4431 constraint_ids_to_delete: all_constraint_ids_to_delete,
4432 additional_edited_segment_ids: additional_edited_segment_ids.into_iter().collect(),
4433 });
4434 }
4435
4436 if matches!(segment, Segment::Circle(_)) {
4439 let left_side_intersects = is_intersect_or_coincident(left_side);
4440 let right_side_intersects = is_intersect_or_coincident(right_side);
4441 if !(left_side_intersects && right_side_intersects) {
4442 return Err(format!(
4443 "Unsupported circle trim termination combination: left={:?} right={:?}",
4444 left_side, right_side
4445 ));
4446 }
4447
4448 let left_trim_coords = match left_side {
4449 TrimTermination::SegEndPoint {
4450 trim_termination_coords,
4451 }
4452 | TrimTermination::Intersection {
4453 trim_termination_coords,
4454 ..
4455 }
4456 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4457 trim_termination_coords,
4458 ..
4459 } => *trim_termination_coords,
4460 };
4461 let right_trim_coords = match right_side {
4462 TrimTermination::SegEndPoint {
4463 trim_termination_coords,
4464 }
4465 | TrimTermination::Intersection {
4466 trim_termination_coords,
4467 ..
4468 }
4469 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4470 trim_termination_coords,
4471 ..
4472 } => *trim_termination_coords,
4473 };
4474
4475 let trim_points_coincident = ((left_trim_coords.x - right_trim_coords.x)
4478 * (left_trim_coords.x - right_trim_coords.x)
4479 + (left_trim_coords.y - right_trim_coords.y) * (left_trim_coords.y - right_trim_coords.y))
4480 .sqrt()
4481 <= EPSILON_POINT_ON_SEGMENT * 10.0;
4482 if trim_points_coincident {
4483 return Ok(TrimPlan::DeleteSegment {
4484 segment_id: trim_spawn_id,
4485 });
4486 }
4487
4488 let circle_center_coords =
4489 get_position_coords_from_circle(trim_spawn_segment, CirclePoint::Center, objects, default_unit)
4490 .ok_or_else(|| {
4491 format!(
4492 "Could not get center coordinates for circle segment {}",
4493 trim_spawn_id.0
4494 )
4495 })?;
4496
4497 let spawn_on_left_to_right = is_point_on_arc(
4499 trim_spawn_coords,
4500 circle_center_coords,
4501 left_trim_coords,
4502 right_trim_coords,
4503 EPSILON_POINT_ON_SEGMENT,
4504 );
4505 let (arc_start_coords, arc_end_coords, arc_start_termination, arc_end_termination) = if spawn_on_left_to_right {
4506 (
4507 right_trim_coords,
4508 left_trim_coords,
4509 Box::new(right_side.clone()),
4510 Box::new(left_side.clone()),
4511 )
4512 } else {
4513 (
4514 left_trim_coords,
4515 right_trim_coords,
4516 Box::new(left_side.clone()),
4517 Box::new(right_side.clone()),
4518 )
4519 };
4520
4521 return Ok(TrimPlan::ReplaceCircleWithArc {
4522 circle_id: trim_spawn_id,
4523 arc_start_coords,
4524 arc_end_coords,
4525 arc_start_termination,
4526 arc_end_termination,
4527 });
4528 }
4529
4530 let left_side_intersects = is_intersect_or_coincident(left_side);
4532 let right_side_intersects = is_intersect_or_coincident(right_side);
4533
4534 if left_side_intersects && right_side_intersects {
4535 let left_intersecting_seg_id = match left_side {
4538 TrimTermination::Intersection {
4539 intersecting_seg_id, ..
4540 }
4541 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4542 intersecting_seg_id, ..
4543 } => *intersecting_seg_id,
4544 TrimTermination::SegEndPoint { .. } => {
4545 return Err("Logic error: left side should not be segEndPoint".to_string());
4546 }
4547 };
4548
4549 let right_intersecting_seg_id = match right_side {
4550 TrimTermination::Intersection {
4551 intersecting_seg_id, ..
4552 }
4553 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4554 intersecting_seg_id, ..
4555 } => *intersecting_seg_id,
4556 TrimTermination::SegEndPoint { .. } => {
4557 return Err("Logic error: right side should not be segEndPoint".to_string());
4558 }
4559 };
4560
4561 let left_coincident_data = if matches!(
4562 left_side,
4563 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
4564 ) {
4565 let point_id = match left_side {
4566 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4567 other_segment_point_id, ..
4568 } => *other_segment_point_id,
4569 _ => return Err("Logic error".to_string()),
4570 };
4571 let mut data = find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id);
4572 data.intersecting_endpoint_point_id = Some(point_id);
4573 data
4574 } else {
4575 find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id)
4576 };
4577
4578 let right_coincident_data = if matches!(
4579 right_side,
4580 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
4581 ) {
4582 let point_id = match right_side {
4583 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4584 other_segment_point_id, ..
4585 } => *other_segment_point_id,
4586 _ => return Err("Logic error".to_string()),
4587 };
4588 let mut data = find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id);
4589 data.intersecting_endpoint_point_id = Some(point_id);
4590 data
4591 } else {
4592 find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id)
4593 };
4594
4595 if let Segment::ControlPointSpline(spline) = segment {
4596 let trim_curve = load_curve_handle(trim_spawn_segment, objects, default_unit)?;
4597 let end_parameter = trim_curve
4598 .sampled_points
4599 .as_ref()
4600 .and_then(|samples| samples.last())
4601 .map(|sample| sample.parameter)
4602 .unwrap_or_else(|| spline.controls.len().saturating_sub(1) as f64);
4603 let left_trim_coords = match left_side {
4604 TrimTermination::SegEndPoint {
4605 trim_termination_coords,
4606 }
4607 | TrimTermination::Intersection {
4608 trim_termination_coords,
4609 ..
4610 }
4611 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4612 trim_termination_coords,
4613 ..
4614 } => *trim_termination_coords,
4615 };
4616 let right_trim_coords = match right_side {
4617 TrimTermination::SegEndPoint {
4618 trim_termination_coords,
4619 }
4620 | TrimTermination::Intersection {
4621 trim_termination_coords,
4622 ..
4623 }
4624 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4625 trim_termination_coords,
4626 ..
4627 } => *trim_termination_coords,
4628 };
4629 let left_trim_parameter = project_point_onto_curve(&trim_curve, left_trim_coords)?;
4630 let right_trim_parameter = project_point_onto_curve(&trim_curve, right_trim_coords)?;
4631
4632 if (right_trim_parameter - left_trim_parameter).abs() < EPSILON_POINT_ON_SEGMENT {
4633 return Err("Split trim on spline collapsed to the same parameter on both sides".to_string());
4634 }
4635
4636 let left_ctor = build_trimmed_control_point_spline_ctor(
4637 trim_spawn_segment,
4638 objects,
4639 default_unit,
4640 0.0,
4641 left_trim_parameter,
4642 )?;
4643 let right_ctor = build_trimmed_control_point_spline_ctor(
4644 trim_spawn_segment,
4645 objects,
4646 default_unit,
4647 right_trim_parameter,
4648 end_parameter,
4649 )?;
4650
4651 let mut constraint_ids_to_delete =
4652 spline_constraint_ids_to_delete(spline, spline.controls.last().copied(), objects);
4653 for obj in objects {
4654 let ObjectKind::Constraint { constraint } = &obj.kind else {
4655 continue;
4656 };
4657 match constraint {
4658 Constraint::Coincident(coincident)
4659 if spline
4660 .controls
4661 .last()
4662 .is_some_and(|end_id| coincident.contains_segment(*end_id)) =>
4663 {
4664 constraint_ids_to_delete.push(obj.id);
4665 }
4666 Constraint::Tangent(tangent) if tangent.input.contains(&trim_spawn_id) => {
4667 constraint_ids_to_delete.push(obj.id);
4668 }
4669 _ => {}
4670 }
4671 }
4672 constraint_ids_to_delete.sort_unstable();
4673 constraint_ids_to_delete.dedup();
4674
4675 return Ok(TrimPlan::SplitControlPointSpline {
4676 segment_id: trim_spawn_id,
4677 left_ctor,
4678 right_ctor,
4679 left_side: Box::new(left_side.clone()),
4680 right_side: Box::new(right_side.clone()),
4681 constraint_ids_to_delete,
4682 });
4683 }
4684
4685 let (original_start_point_id, original_end_point_id) = match segment {
4687 Segment::Line(line) => (Some(line.start), Some(line.end)),
4688 Segment::Arc(arc) => (Some(arc.start), Some(arc.end)),
4689 _ => (None, None),
4690 };
4691
4692 let original_end_point_coords = match segment {
4694 Segment::Line(_) => {
4695 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
4696 }
4697 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
4698 _ => None,
4699 };
4700
4701 let Some(original_end_coords) = original_end_point_coords else {
4702 return Err(
4703 "Could not get original end point coordinates before editing - this is required for split trim"
4704 .to_string(),
4705 );
4706 };
4707
4708 let left_trim_coords = match left_side {
4710 TrimTermination::SegEndPoint {
4711 trim_termination_coords,
4712 }
4713 | TrimTermination::Intersection {
4714 trim_termination_coords,
4715 ..
4716 }
4717 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4718 trim_termination_coords,
4719 ..
4720 } => *trim_termination_coords,
4721 };
4722
4723 let right_trim_coords = match right_side {
4724 TrimTermination::SegEndPoint {
4725 trim_termination_coords,
4726 }
4727 | TrimTermination::Intersection {
4728 trim_termination_coords,
4729 ..
4730 }
4731 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4732 trim_termination_coords,
4733 ..
4734 } => *trim_termination_coords,
4735 };
4736
4737 let dist_to_original_end = ((right_trim_coords.x - original_end_coords.x)
4739 * (right_trim_coords.x - original_end_coords.x)
4740 + (right_trim_coords.y - original_end_coords.y) * (right_trim_coords.y - original_end_coords.y))
4741 .sqrt();
4742 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
4743 return Err(
4744 "Split point is at original end point - this should be handled as cutTail, not split".to_string(),
4745 );
4746 }
4747
4748 let mut constraints_to_migrate: Vec<ConstraintToMigrate> = Vec::new();
4751 let mut constraints_to_delete_set: IndexSet<ObjectId> = IndexSet::new();
4752
4753 if let Some(constraint_id) = left_coincident_data.existing_point_segment_constraint_id {
4755 constraints_to_delete_set.insert(constraint_id);
4756 }
4757 if let Some(constraint_id) = right_coincident_data.existing_point_segment_constraint_id {
4758 constraints_to_delete_set.insert(constraint_id);
4759 }
4760
4761 if let Some(end_id) = original_end_point_id {
4762 for obj in objects {
4763 let ObjectKind::Constraint { constraint } = &obj.kind else {
4764 continue;
4765 };
4766
4767 if point_axis_constraint_references_point(constraint, end_id) {
4768 constraints_to_delete_set.insert(obj.id);
4769 }
4770 }
4771 }
4772
4773 if let Some(end_id) = original_end_point_id {
4775 let end_point_point_constraint_ids = find_point_point_coincident_constraints(end_id);
4776 for constraint_id in end_point_point_constraint_ids {
4777 let other_point_id_opt = objects.iter().find_map(|obj| {
4779 if obj.id != constraint_id {
4780 return None;
4781 }
4782 let ObjectKind::Constraint { constraint } = &obj.kind else {
4783 return None;
4784 };
4785 let Constraint::Coincident(coincident) = constraint else {
4786 return None;
4787 };
4788 coincident.segment_ids().find(|&seg_id| seg_id != end_id)
4789 });
4790
4791 if let Some(other_point_id) = other_point_id_opt {
4792 constraints_to_delete_set.insert(constraint_id);
4793 constraints_to_migrate.push(ConstraintToMigrate {
4795 constraint_id,
4796 other_entity_id: other_point_id,
4797 is_point_point: true,
4798 attach_to_endpoint: AttachToEndpoint::End,
4799 });
4800 }
4801 }
4802 }
4803
4804 if let Some(end_id) = original_end_point_id {
4806 let end_point_segment_constraints = find_point_segment_coincident_constraints(end_id);
4807 for constraint_json in end_point_segment_constraints {
4808 if let Some(constraint_id_usize) = constraint_json
4809 .get("constraintId")
4810 .and_then(|v| v.as_u64())
4811 .map(|id| id as usize)
4812 {
4813 let constraint_id = ObjectId(constraint_id_usize);
4814 constraints_to_delete_set.insert(constraint_id);
4815 if let Some(other_id_usize) = constraint_json
4817 .get("segmentOrPointId")
4818 .and_then(|v| v.as_u64())
4819 .map(|id| id as usize)
4820 {
4821 constraints_to_migrate.push(ConstraintToMigrate {
4822 constraint_id,
4823 other_entity_id: ObjectId(other_id_usize),
4824 is_point_point: false,
4825 attach_to_endpoint: AttachToEndpoint::End,
4826 });
4827 }
4828 }
4829 }
4830 }
4831
4832 if let Some(end_id) = original_end_point_id {
4837 for obj in objects {
4838 let ObjectKind::Constraint { constraint } = &obj.kind else {
4839 continue;
4840 };
4841
4842 let Constraint::Coincident(coincident) = constraint else {
4843 continue;
4844 };
4845
4846 if !coincident.contains_segment(trim_spawn_id) {
4851 continue;
4852 }
4853 if let (Some(start_id), Some(end_id_val)) = (original_start_point_id, Some(end_id))
4856 && coincident.segment_ids().any(|id| id == start_id || id == end_id_val)
4857 {
4858 continue; }
4860
4861 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
4863
4864 if let Some(other_id) = other_id {
4865 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
4867 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
4868 continue;
4869 };
4870
4871 let Segment::Point(point) = other_segment else {
4872 continue;
4873 };
4874
4875 let point_coords = Coords2d {
4877 x: number_to_unit(&point.position.x, default_unit),
4878 y: number_to_unit(&point.position.y, default_unit),
4879 };
4880
4881 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
4884 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
4885 if let ObjectKind::Segment {
4886 segment: Segment::Point(end_point),
4887 } = &end_point_obj.kind
4888 {
4889 Some(Coords2d {
4890 x: number_to_unit(&end_point.position.x, default_unit),
4891 y: number_to_unit(&end_point.position.y, default_unit),
4892 })
4893 } else {
4894 None
4895 }
4896 } else {
4897 None
4898 }
4899 } else {
4900 None
4901 };
4902
4903 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
4904 let dist_to_original_end = ((point_coords.x - reference_coords.x)
4905 * (point_coords.x - reference_coords.x)
4906 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
4907 .sqrt();
4908
4909 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
4910 let has_point_point_constraint = find_point_point_coincident_constraints(end_id)
4913 .iter()
4914 .any(|&constraint_id| {
4915 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
4916 if let ObjectKind::Constraint {
4917 constraint: Constraint::Coincident(coincident),
4918 } = &constraint_obj.kind
4919 {
4920 coincident.contains_segment(other_id)
4921 } else {
4922 false
4923 }
4924 } else {
4925 false
4926 }
4927 });
4928
4929 if !has_point_point_constraint {
4930 constraints_to_migrate.push(ConstraintToMigrate {
4932 constraint_id: obj.id,
4933 other_entity_id: other_id,
4934 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
4937 }
4938 constraints_to_delete_set.insert(obj.id);
4940 }
4941 }
4942 }
4943 }
4944 }
4945
4946 let split_point = right_trim_coords; let segment_start_coords = match segment {
4951 Segment::Line(_) => {
4952 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::Start, objects, default_unit)
4953 }
4954 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Start, objects, default_unit),
4955 _ => None,
4956 };
4957 let segment_end_coords = match segment {
4958 Segment::Line(_) => {
4959 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
4960 }
4961 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
4962 _ => None,
4963 };
4964 let segment_center_coords = match segment {
4965 Segment::Line(_) => None,
4966 Segment::Arc(_) => {
4967 get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Center, objects, default_unit)
4968 }
4969 _ => None,
4970 };
4971
4972 if let (Some(start_coords), Some(end_coords)) = (segment_start_coords, segment_end_coords) {
4973 let split_point_t_opt = match segment {
4975 Segment::Line(_) => Some(project_point_onto_segment(split_point, start_coords, end_coords)),
4976 Segment::Arc(arc) => segment_center_coords
4977 .map(|center| project_point_onto_arc(split_point, center, start_coords, end_coords, arc.direction)),
4978 _ => None,
4979 };
4980
4981 if let Some(split_point_t) = split_point_t_opt {
4982 for obj in objects {
4984 let ObjectKind::Constraint { constraint } = &obj.kind else {
4985 continue;
4986 };
4987
4988 let Constraint::Coincident(coincident) = constraint else {
4989 continue;
4990 };
4991
4992 if !coincident.contains_segment(trim_spawn_id) {
4994 continue;
4995 }
4996
4997 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id)
4999 && coincident.segment_ids().any(|id| id == start_id || id == end_id)
5000 {
5001 continue;
5002 }
5003
5004 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
5006
5007 if let Some(other_id) = other_id {
5008 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
5010 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
5011 continue;
5012 };
5013
5014 let Segment::Point(point) = other_segment else {
5015 continue;
5016 };
5017
5018 let point_coords = Coords2d {
5020 x: number_to_unit(&point.position.x, default_unit),
5021 y: number_to_unit(&point.position.y, default_unit),
5022 };
5023
5024 let point_t = match segment {
5026 Segment::Line(_) => project_point_onto_segment(point_coords, start_coords, end_coords),
5027 Segment::Arc(arc) => {
5028 if let Some(center) = segment_center_coords {
5029 project_point_onto_arc(
5030 point_coords,
5031 center,
5032 start_coords,
5033 end_coords,
5034 arc.direction,
5035 )
5036 } else {
5037 continue; }
5039 }
5040 _ => continue, };
5042
5043 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
5046 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
5047 if let ObjectKind::Segment {
5048 segment: Segment::Point(end_point),
5049 } = &end_point_obj.kind
5050 {
5051 Some(Coords2d {
5052 x: number_to_unit(&end_point.position.x, default_unit),
5053 y: number_to_unit(&end_point.position.y, default_unit),
5054 })
5055 } else {
5056 None
5057 }
5058 } else {
5059 None
5060 }
5061 } else {
5062 None
5063 };
5064
5065 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
5066 let dist_to_original_end = ((point_coords.x - reference_coords.x)
5067 * (point_coords.x - reference_coords.x)
5068 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
5069 .sqrt();
5070
5071 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
5072 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
5076 find_point_point_coincident_constraints(end_id)
5077 .iter()
5078 .any(|&constraint_id| {
5079 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
5080 {
5081 if let ObjectKind::Constraint {
5082 constraint: Constraint::Coincident(coincident),
5083 } = &constraint_obj.kind
5084 {
5085 coincident.contains_segment(other_id)
5086 } else {
5087 false
5088 }
5089 } else {
5090 false
5091 }
5092 })
5093 } else {
5094 false
5095 };
5096
5097 if !has_point_point_constraint {
5098 constraints_to_migrate.push(ConstraintToMigrate {
5100 constraint_id: obj.id,
5101 other_entity_id: other_id,
5102 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
5105 }
5106 constraints_to_delete_set.insert(obj.id);
5108 continue; }
5110
5111 let dist_to_start = ((point_coords.x - start_coords.x) * (point_coords.x - start_coords.x)
5113 + (point_coords.y - start_coords.y) * (point_coords.y - start_coords.y))
5114 .sqrt();
5115 let is_at_start = (point_t - 0.0).abs() < EPSILON_POINT_ON_SEGMENT
5116 || dist_to_start < EPSILON_POINT_ON_SEGMENT;
5117
5118 if is_at_start {
5119 continue; }
5121
5122 let dist_to_split = (point_t - split_point_t).abs();
5124 if dist_to_split < EPSILON_POINT_ON_SEGMENT * 100.0 {
5125 continue; }
5127
5128 if point_t > split_point_t {
5130 constraints_to_migrate.push(ConstraintToMigrate {
5131 constraint_id: obj.id,
5132 other_entity_id: other_id,
5133 is_point_point: false, attach_to_endpoint: AttachToEndpoint::Segment, });
5136 constraints_to_delete_set.insert(obj.id);
5137 }
5138 }
5139 }
5140 }
5141 } } let distance_constraint_ids_for_split = find_distance_constraints_for_segment(trim_spawn_id);
5149
5150 let arc_center_point_id: Option<ObjectId> = match segment {
5152 Segment::Arc(arc) => Some(arc.center),
5153 _ => None,
5154 };
5155
5156 for constraint_id in distance_constraint_ids_for_split {
5157 if let Some(center_id) = arc_center_point_id {
5159 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
5161 && let ObjectKind::Constraint { constraint } = &constraint_obj.kind
5162 && let Constraint::Distance(distance) = constraint
5163 && distance.contains_point(center_id)
5164 {
5165 continue;
5167 }
5168 }
5169
5170 constraints_to_delete_set.insert(constraint_id);
5171 }
5172
5173 for obj in objects {
5176 let ObjectKind::Constraint { constraint } = &obj.kind else {
5177 continue;
5178 };
5179
5180 let Constraint::Midpoint(midpoint) = constraint else {
5181 continue;
5182 };
5183
5184 let references_trimmed_segment = midpoint.segment == trim_spawn_id;
5185 let references_trimmed_endpoint = match midpoint.point {
5186 ConstraintSegment::Segment(point_id) => {
5187 original_start_point_id.is_some_and(|id| point_id == id)
5188 || original_end_point_id.is_some_and(|id| point_id == id)
5189 }
5190 ConstraintSegment::Origin(_) => false,
5191 };
5192
5193 if references_trimmed_segment || references_trimmed_endpoint {
5194 constraints_to_delete_set.insert(obj.id);
5195 }
5196 }
5197
5198 for obj in objects {
5206 let ObjectKind::Constraint { constraint } = &obj.kind else {
5207 continue;
5208 };
5209
5210 let Constraint::Coincident(coincident) = constraint else {
5211 continue;
5212 };
5213
5214 if !coincident.contains_segment(trim_spawn_id) {
5216 continue;
5217 }
5218
5219 if constraints_to_delete_set.contains(&obj.id) {
5221 continue;
5222 }
5223
5224 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
5231
5232 if let Some(other_id) = other_id {
5233 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
5235 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
5236 continue;
5237 };
5238
5239 let Segment::Point(point) = other_segment else {
5240 continue;
5241 };
5242
5243 let _is_endpoint_constraint =
5246 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id) {
5247 coincident.segment_ids().any(|id| id == start_id || id == end_id)
5248 } else {
5249 false
5250 };
5251
5252 let point_coords = Coords2d {
5254 x: number_to_unit(&point.position.x, default_unit),
5255 y: number_to_unit(&point.position.y, default_unit),
5256 };
5257
5258 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
5260 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
5261 if let ObjectKind::Segment {
5262 segment: Segment::Point(end_point),
5263 } = &end_point_obj.kind
5264 {
5265 Some(Coords2d {
5266 x: number_to_unit(&end_point.position.x, default_unit),
5267 y: number_to_unit(&end_point.position.y, default_unit),
5268 })
5269 } else {
5270 None
5271 }
5272 } else {
5273 None
5274 }
5275 } else {
5276 None
5277 };
5278
5279 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
5280 let dist_to_original_end = ((point_coords.x - reference_coords.x)
5281 * (point_coords.x - reference_coords.x)
5282 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
5283 .sqrt();
5284
5285 let is_at_original_end = dist_to_original_end < EPSILON_POINT_ON_SEGMENT * 2.0;
5288
5289 if is_at_original_end {
5290 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
5293 find_point_point_coincident_constraints(end_id)
5294 .iter()
5295 .any(|&constraint_id| {
5296 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
5297 if let ObjectKind::Constraint {
5298 constraint: Constraint::Coincident(coincident),
5299 } = &constraint_obj.kind
5300 {
5301 coincident.contains_segment(other_id)
5302 } else {
5303 false
5304 }
5305 } else {
5306 false
5307 }
5308 })
5309 } else {
5310 false
5311 };
5312
5313 if !has_point_point_constraint {
5314 constraints_to_migrate.push(ConstraintToMigrate {
5316 constraint_id: obj.id,
5317 other_entity_id: other_id,
5318 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
5321 }
5322 constraints_to_delete_set.insert(obj.id);
5324 }
5325 }
5326 }
5327 }
5328
5329 let constraints_to_delete: Vec<ObjectId> = constraints_to_delete_set.iter().copied().collect();
5331 let plan = TrimPlan::SplitSegment {
5332 segment_id: trim_spawn_id,
5333 left_trim_coords,
5334 right_trim_coords,
5335 original_end_coords,
5336 left_side: Box::new(left_side.clone()),
5337 right_side: Box::new(right_side.clone()),
5338 left_side_coincident_data: CoincidentData {
5339 intersecting_seg_id: left_intersecting_seg_id,
5340 intersecting_endpoint_point_id: left_coincident_data.intersecting_endpoint_point_id,
5341 existing_point_segment_constraint_id: left_coincident_data.existing_point_segment_constraint_id,
5342 },
5343 right_side_coincident_data: CoincidentData {
5344 intersecting_seg_id: right_intersecting_seg_id,
5345 intersecting_endpoint_point_id: right_coincident_data.intersecting_endpoint_point_id,
5346 existing_point_segment_constraint_id: right_coincident_data.existing_point_segment_constraint_id,
5347 },
5348 constraints_to_migrate,
5349 constraints_to_delete,
5350 };
5351
5352 return Ok(plan);
5353 }
5354
5355 Err(format!(
5360 "Unsupported trim termination combination: left={:?} right={:?}",
5361 left_side, right_side
5362 ))
5363}
5364
5365pub(crate) async fn execute_trim_operations_simple(
5377 strategy: Vec<TrimOperation>,
5378 current_scene_graph_delta: &crate::frontend::api::SceneGraphDelta,
5379 frontend: &mut crate::frontend::FrontendState,
5380 ctx: &crate::ExecutorContext,
5381 version: crate::frontend::api::Version,
5382 sketch_id: ObjectId,
5383) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
5384 use crate::frontend::SketchApi;
5385 use crate::frontend::sketch::Constraint;
5386 use crate::frontend::sketch::ExistingSegmentCtor;
5387 use crate::frontend::sketch::SegmentCtor;
5388
5389 let default_unit = frontend.default_length_unit();
5390
5391 let mut op_index = 0;
5392 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = None;
5393 let mut invalidates_ids = false;
5394
5395 while op_index < strategy.len() {
5396 let mut consumed_ops = 1;
5397 let operation_result = match &strategy[op_index] {
5398 TrimOperation::SimpleTrim { segment_to_trim_id } => {
5399 frontend
5401 .delete_objects(
5402 ctx,
5403 version,
5404 sketch_id,
5405 Vec::new(), vec![*segment_to_trim_id], )
5408 .await
5409 .map_err(|e| format!("Failed to delete segment: {}", e.error.message()))
5410 }
5411 TrimOperation::EditSegment {
5412 segment_id,
5413 ctor,
5414 endpoint_changed,
5415 additional_edited_segment_ids,
5416 } => {
5417 if op_index + 1 < strategy.len() {
5420 if let TrimOperation::AddCoincidentConstraint {
5421 segment_id: coincident_seg_id,
5422 endpoint_changed: coincident_endpoint_changed,
5423 segment_or_point_to_make_coincident_to,
5424 intersecting_endpoint_point_id,
5425 } = &strategy[op_index + 1]
5426 {
5427 if segment_id == coincident_seg_id && endpoint_changed == coincident_endpoint_changed {
5428 let mut delete_constraint_ids: Vec<ObjectId> = Vec::new();
5430 consumed_ops = 2;
5431
5432 if op_index + 2 < strategy.len()
5433 && let TrimOperation::DeleteConstraints { constraint_ids } = &strategy[op_index + 2]
5434 {
5435 delete_constraint_ids = constraint_ids.to_vec();
5436 consumed_ops = 3;
5437 }
5438
5439 let segment_ctor = ctor.clone();
5441
5442 let edited_segment = current_scene_graph_delta
5444 .new_graph
5445 .objects
5446 .iter()
5447 .find(|obj| obj.id == *segment_id)
5448 .ok_or_else(|| format!("Failed to find segment {} for tail-cut batch", segment_id.0))?;
5449
5450 let endpoint_point_id = match &edited_segment.kind {
5451 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
5452 crate::frontend::sketch::Segment::Line(line) => {
5453 if *endpoint_changed == EndpointChanged::Start {
5454 line.start
5455 } else {
5456 line.end
5457 }
5458 }
5459 crate::frontend::sketch::Segment::Arc(arc) => {
5460 if *endpoint_changed == EndpointChanged::Start {
5461 arc.start
5462 } else {
5463 arc.end
5464 }
5465 }
5466 _ => {
5467 return Err("Unsupported segment type for tail-cut batch".to_string());
5468 }
5469 },
5470 _ => {
5471 return Err("Edited object is not a segment (tail-cut batch)".to_string());
5472 }
5473 };
5474
5475 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
5476 vec![endpoint_point_id.into(), (*point_id).into()]
5477 } else {
5478 vec![
5479 endpoint_point_id.into(),
5480 (*segment_or_point_to_make_coincident_to).into(),
5481 ]
5482 };
5483
5484 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
5485 segments: coincident_segments,
5486 });
5487
5488 let segment_to_edit = ExistingSegmentCtor {
5489 id: *segment_id,
5490 ctor: segment_ctor,
5491 };
5492
5493 frontend
5496 .batch_tail_cut_operations(
5497 ctx,
5498 version,
5499 sketch_id,
5500 vec![segment_to_edit],
5501 vec![constraint],
5502 delete_constraint_ids,
5503 additional_edited_segment_ids.clone(),
5504 )
5505 .await
5506 .map_err(|e| format!("Failed to batch tail-cut operations: {}", e.error.message()))
5507 } else {
5508 let segment_to_edit = ExistingSegmentCtor {
5510 id: *segment_id,
5511 ctor: ctor.clone(),
5512 };
5513
5514 frontend
5515 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
5516 .await
5517 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
5518 }
5519 } else {
5520 let segment_to_edit = ExistingSegmentCtor {
5522 id: *segment_id,
5523 ctor: ctor.clone(),
5524 };
5525
5526 frontend
5527 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
5528 .await
5529 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
5530 }
5531 } else {
5532 let segment_to_edit = ExistingSegmentCtor {
5534 id: *segment_id,
5535 ctor: ctor.clone(),
5536 };
5537
5538 frontend
5539 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
5540 .await
5541 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
5542 }
5543 }
5544 TrimOperation::EditControlPointSpline { segment_id, ctor } => {
5545 let segment_to_edit = ExistingSegmentCtor {
5546 id: *segment_id,
5547 ctor: ctor.clone(),
5548 };
5549
5550 frontend
5551 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
5552 .await
5553 .map_err(|e| format!("Failed to edit control point spline: {}", e.error.message()))
5554 }
5555 TrimOperation::AddCoincidentConstraint {
5556 segment_id,
5557 endpoint_changed,
5558 segment_or_point_to_make_coincident_to,
5559 intersecting_endpoint_point_id,
5560 } => {
5561 let edited_segment = current_scene_graph_delta
5563 .new_graph
5564 .objects
5565 .iter()
5566 .find(|obj| obj.id == *segment_id)
5567 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
5568
5569 let new_segment_endpoint_point_id = match &edited_segment.kind {
5571 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
5572 crate::frontend::sketch::Segment::Line(line) => {
5573 if *endpoint_changed == EndpointChanged::Start {
5574 line.start
5575 } else {
5576 line.end
5577 }
5578 }
5579 crate::frontend::sketch::Segment::Arc(arc) => {
5580 if *endpoint_changed == EndpointChanged::Start {
5581 arc.start
5582 } else {
5583 arc.end
5584 }
5585 }
5586 crate::frontend::sketch::Segment::ControlPointSpline(spline) => {
5587 if *endpoint_changed == EndpointChanged::Start {
5588 spline
5589 .controls
5590 .first()
5591 .copied()
5592 .ok_or_else(|| "Edited spline has no start control point".to_string())?
5593 } else {
5594 spline
5595 .controls
5596 .last()
5597 .copied()
5598 .ok_or_else(|| "Edited spline has no end control point".to_string())?
5599 }
5600 }
5601 _ => {
5602 return Err("Unsupported segment type for addCoincidentConstraint".to_string());
5603 }
5604 },
5605 _ => {
5606 return Err("Edited object is not a segment".to_string());
5607 }
5608 };
5609
5610 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
5612 vec![new_segment_endpoint_point_id.into(), (*point_id).into()]
5613 } else {
5614 vec![
5615 new_segment_endpoint_point_id.into(),
5616 (*segment_or_point_to_make_coincident_to).into(),
5617 ]
5618 };
5619
5620 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
5621 segments: coincident_segments,
5622 });
5623
5624 frontend
5625 .add_constraint(ctx, version, sketch_id, constraint)
5626 .await
5627 .map_err(|e| format!("Failed to add constraint: {}", e.error.message()))
5628 }
5629 TrimOperation::DeleteConstraints { constraint_ids } => {
5630 let constraint_object_ids: Vec<ObjectId> = constraint_ids.to_vec();
5632
5633 frontend
5634 .delete_objects(
5635 ctx,
5636 version,
5637 sketch_id,
5638 constraint_object_ids,
5639 Vec::new(), )
5641 .await
5642 .map_err(|e| format!("Failed to delete constraints: {}", e.error.message()))
5643 }
5644 TrimOperation::ReplaceCircleWithArc {
5645 circle_id,
5646 arc_start_coords,
5647 arc_end_coords,
5648 arc_start_termination,
5649 arc_end_termination,
5650 } => {
5651 let original_circle = current_scene_graph_delta
5653 .new_graph
5654 .objects
5655 .iter()
5656 .find(|obj| obj.id == *circle_id)
5657 .ok_or_else(|| format!("Failed to find original circle {}", circle_id.0))?;
5658
5659 let (original_circle_start_id, original_circle_center_id, circle_ctor) = match &original_circle.kind {
5660 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
5661 crate::frontend::sketch::Segment::Circle(circle) => match &circle.ctor {
5662 SegmentCtor::Circle(circle_ctor) => (circle.start, circle.center, circle_ctor.clone()),
5663 _ => return Err("Circle does not have a Circle ctor".to_string()),
5664 },
5665 _ => return Err("Original segment is not a circle".to_string()),
5666 },
5667 _ => return Err("Original object is not a segment".to_string()),
5668 };
5669
5670 let units = match &circle_ctor.start.x {
5671 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
5672 _ => crate::pretty::NumericSuffix::Mm,
5673 };
5674
5675 let coords_to_point_expr = |coords: Coords2d| crate::frontend::sketch::Point2d {
5676 x: crate::frontend::api::Expr::Var(unit_to_number(coords.x, default_unit, units)),
5677 y: crate::frontend::api::Expr::Var(unit_to_number(coords.y, default_unit, units)),
5678 };
5679
5680 let arc_ctor = SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
5683 start: coords_to_point_expr(*arc_start_coords),
5684 end: coords_to_point_expr(*arc_end_coords),
5685 center: circle_ctor.center.clone(),
5686 direction: None,
5687 construction: circle_ctor.construction,
5688 });
5689
5690 let (_add_source_delta, add_scene_graph_delta) = frontend
5691 .add_segment(ctx, version, sketch_id, arc_ctor, None)
5692 .await
5693 .map_err(|e| format!("Failed to add arc while replacing circle: {}", e.error.message()))?;
5694 invalidates_ids = invalidates_ids || add_scene_graph_delta.invalidates_ids;
5695
5696 let new_arc_id = *add_scene_graph_delta
5697 .new_objects
5698 .iter()
5699 .find(|&id| {
5700 add_scene_graph_delta
5701 .new_graph
5702 .objects
5703 .iter()
5704 .find(|o| o.id == *id)
5705 .is_some_and(|obj| {
5706 matches!(
5707 &obj.kind,
5708 crate::frontend::api::ObjectKind::Segment { segment }
5709 if matches!(segment, crate::frontend::sketch::Segment::Arc(_))
5710 )
5711 })
5712 })
5713 .ok_or_else(|| "Failed to find newly created arc segment".to_string())?;
5714
5715 let new_arc_obj = add_scene_graph_delta
5716 .new_graph
5717 .objects
5718 .iter()
5719 .find(|obj| obj.id == new_arc_id)
5720 .ok_or_else(|| format!("New arc segment not found {}", new_arc_id.0))?;
5721 let (new_arc_start_id, new_arc_end_id, new_arc_center_id) = match &new_arc_obj.kind {
5722 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
5723 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, arc.center),
5724 _ => return Err("New segment is not an arc".to_string()),
5725 },
5726 _ => return Err("New arc object is not a segment".to_string()),
5727 };
5728
5729 let constraint_segments_for =
5730 |arc_endpoint_id: ObjectId,
5731 term: &TrimTermination|
5732 -> Result<Vec<crate::frontend::sketch::ConstraintSegment>, String> {
5733 match term {
5734 TrimTermination::Intersection {
5735 intersecting_seg_id, ..
5736 } => Ok(vec![arc_endpoint_id.into(), (*intersecting_seg_id).into()]),
5737 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5738 other_segment_point_id,
5739 ..
5740 } => Ok(vec![arc_endpoint_id.into(), (*other_segment_point_id).into()]),
5741 TrimTermination::SegEndPoint { .. } => {
5742 Err("Circle replacement endpoint cannot terminate at seg endpoint".to_string())
5743 }
5744 }
5745 };
5746
5747 let start_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
5748 segments: constraint_segments_for(new_arc_start_id, arc_start_termination)?,
5749 });
5750 let (_c1_source_delta, c1_scene_graph_delta) = frontend
5751 .add_constraint(ctx, version, sketch_id, start_constraint)
5752 .await
5753 .map_err(|e| format!("Failed to add start coincident on replaced arc: {}", e.error.message()))?;
5754 invalidates_ids = invalidates_ids || c1_scene_graph_delta.invalidates_ids;
5755
5756 let end_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
5757 segments: constraint_segments_for(new_arc_end_id, arc_end_termination)?,
5758 });
5759 let (_c2_source_delta, c2_scene_graph_delta) = frontend
5760 .add_constraint(ctx, version, sketch_id, end_constraint)
5761 .await
5762 .map_err(|e| format!("Failed to add end coincident on replaced arc: {}", e.error.message()))?;
5763 invalidates_ids = invalidates_ids || c2_scene_graph_delta.invalidates_ids;
5764
5765 let mut termination_point_ids: Vec<ObjectId> = Vec::new();
5766 for term in [arc_start_termination, arc_end_termination] {
5767 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5768 other_segment_point_id,
5769 ..
5770 } = term.as_ref()
5771 {
5772 termination_point_ids.push(*other_segment_point_id);
5773 }
5774 }
5775
5776 let rewrite_map = std::collections::HashMap::from([
5780 (*circle_id, new_arc_id),
5781 (original_circle_center_id, new_arc_center_id),
5782 (original_circle_start_id, new_arc_start_id),
5783 ]);
5784 let rewrite_ids: std::collections::HashSet<ObjectId> = rewrite_map.keys().copied().collect();
5785
5786 let mut migrated_constraints: Vec<Constraint> = Vec::new();
5787 for obj in ¤t_scene_graph_delta.new_graph.objects {
5788 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5789 continue;
5790 };
5791
5792 match constraint {
5795 Constraint::Coincident(coincident) => {
5796 if !constraint_segments_reference_any(&coincident.segments, &rewrite_ids) {
5797 continue;
5798 }
5799
5800 if coincident.contains_segment(*circle_id)
5804 && coincident
5805 .segment_ids()
5806 .filter(|id| *id != *circle_id)
5807 .any(|id| termination_point_ids.contains(&id))
5808 {
5809 continue;
5810 }
5811
5812 let Some(Constraint::Coincident(migrated_coincident)) =
5813 rewrite_constraint_with_map(constraint, &rewrite_map)
5814 else {
5815 continue;
5816 };
5817
5818 let migrated_ids: Vec<ObjectId> = migrated_coincident
5822 .segments
5823 .iter()
5824 .filter_map(|segment| match segment {
5825 crate::frontend::sketch::ConstraintSegment::Segment(id) => Some(*id),
5826 crate::frontend::sketch::ConstraintSegment::Origin(_) => None,
5827 })
5828 .collect();
5829 if migrated_ids.contains(&new_arc_id)
5830 && (migrated_ids.contains(&new_arc_start_id) || migrated_ids.contains(&new_arc_end_id))
5831 {
5832 continue;
5833 }
5834
5835 migrated_constraints.push(Constraint::Coincident(migrated_coincident));
5836 }
5837 Constraint::Distance(distance) => {
5838 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
5839 continue;
5840 }
5841 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
5842 migrated_constraints.push(migrated);
5843 }
5844 }
5845 Constraint::HorizontalDistance(distance) => {
5846 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
5847 continue;
5848 }
5849 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
5850 migrated_constraints.push(migrated);
5851 }
5852 }
5853 Constraint::VerticalDistance(distance) => {
5854 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
5855 continue;
5856 }
5857 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
5858 migrated_constraints.push(migrated);
5859 }
5860 }
5861 Constraint::Radius(radius) => {
5862 if radius.arc == *circle_id
5863 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
5864 {
5865 migrated_constraints.push(migrated);
5866 }
5867 }
5868 Constraint::Diameter(diameter) => {
5869 if diameter.arc == *circle_id
5870 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
5871 {
5872 migrated_constraints.push(migrated);
5873 }
5874 }
5875 Constraint::EqualRadius(equal_radius) => {
5876 if equal_radius.input.contains(circle_id)
5877 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
5878 {
5879 migrated_constraints.push(migrated);
5880 }
5881 }
5882 Constraint::Tangent(tangent) => {
5883 if tangent.input.contains(circle_id)
5884 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
5885 {
5886 migrated_constraints.push(migrated);
5887 }
5888 }
5889 Constraint::Angle(_)
5890 | Constraint::Fixed(_)
5891 | Constraint::Horizontal(_)
5892 | Constraint::LinesEqualLength(_)
5893 | Constraint::Midpoint(_)
5894 | Constraint::Parallel(_)
5895 | Constraint::Perpendicular(_)
5896 | Constraint::Symmetric(_)
5897 | Constraint::Vertical(_) => {}
5898 }
5899 }
5900
5901 for constraint in migrated_constraints {
5902 let (_source_delta, migrated_scene_graph_delta) = frontend
5903 .add_constraint(ctx, version, sketch_id, constraint)
5904 .await
5905 .map_err(|e| format!("Failed to migrate circle constraint to arc: {}", e.error.message()))?;
5906 invalidates_ids = invalidates_ids || migrated_scene_graph_delta.invalidates_ids;
5907 }
5908
5909 frontend
5910 .delete_objects(ctx, version, sketch_id, Vec::new(), vec![*circle_id])
5911 .await
5912 .map_err(|e| format!("Failed to delete circle after arc replacement: {}", e.error.message()))
5913 }
5914 TrimOperation::SplitSegment {
5915 segment_id,
5916 left_trim_coords,
5917 right_trim_coords,
5918 original_end_coords,
5919 left_side,
5920 right_side,
5921 constraints_to_migrate,
5922 constraints_to_delete,
5923 ..
5924 } => {
5925 let original_segment = current_scene_graph_delta
5930 .new_graph
5931 .objects
5932 .iter()
5933 .find(|obj| obj.id == *segment_id)
5934 .ok_or_else(|| format!("Failed to find original segment {}", segment_id.0))?;
5935
5936 let (original_segment_start_point_id, original_segment_end_point_id, original_segment_center_point_id) =
5938 match &original_segment.kind {
5939 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
5940 crate::frontend::sketch::Segment::Line(line) => (Some(line.start), Some(line.end), None),
5941 crate::frontend::sketch::Segment::Arc(arc) => {
5942 (Some(arc.start), Some(arc.end), Some(arc.center))
5943 }
5944 _ => (None, None, None),
5945 },
5946 _ => (None, None, None),
5947 };
5948
5949 let mut center_point_constraints_to_migrate: Vec<(Constraint, ObjectId)> = Vec::new();
5951 if let Some(original_center_id) = original_segment_center_point_id {
5952 for obj in ¤t_scene_graph_delta.new_graph.objects {
5953 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5954 continue;
5955 };
5956
5957 if let Constraint::Coincident(coincident) = constraint
5959 && coincident.contains_segment(original_center_id)
5960 {
5961 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
5962 }
5963
5964 if let Constraint::Distance(distance) = constraint
5966 && distance.contains_point(original_center_id)
5967 {
5968 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
5969 }
5970 }
5971 }
5972
5973 let (_segment_type, original_ctor) = match &original_segment.kind {
5975 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
5976 crate::frontend::sketch::Segment::Line(line) => ("Line", line.ctor.clone()),
5977 crate::frontend::sketch::Segment::Arc(arc) => ("Arc", arc.ctor.clone()),
5978 _ => {
5979 return Err("Original segment is not a Line or Arc".to_string());
5980 }
5981 },
5982 _ => {
5983 return Err("Original object is not a segment".to_string());
5984 }
5985 };
5986
5987 let units = match &original_ctor {
5989 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
5990 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
5991 _ => crate::pretty::NumericSuffix::Mm,
5992 },
5993 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
5994 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
5995 _ => crate::pretty::NumericSuffix::Mm,
5996 },
5997 _ => crate::pretty::NumericSuffix::Mm,
5998 };
5999
6000 let coords_to_point =
6003 |coords: Coords2d| -> crate::frontend::sketch::Point2d<crate::frontend::api::Number> {
6004 crate::frontend::sketch::Point2d {
6005 x: unit_to_number(coords.x, default_unit, units),
6006 y: unit_to_number(coords.y, default_unit, units),
6007 }
6008 };
6009
6010 let point_to_expr = |point: crate::frontend::sketch::Point2d<crate::frontend::api::Number>| -> crate::frontend::sketch::Point2d<crate::frontend::api::Expr> {
6012 crate::frontend::sketch::Point2d {
6013 x: crate::frontend::api::Expr::Var(point.x),
6014 y: crate::frontend::api::Expr::Var(point.y),
6015 }
6016 };
6017
6018 let new_segment_ctor = match &original_ctor {
6020 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
6021 start: point_to_expr(coords_to_point(*right_trim_coords)),
6022 end: point_to_expr(coords_to_point(*original_end_coords)),
6023 construction: line_ctor.construction,
6024 }),
6025 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
6026 start: point_to_expr(coords_to_point(*right_trim_coords)),
6027 end: point_to_expr(coords_to_point(*original_end_coords)),
6028 center: arc_ctor.center.clone(),
6029 direction: arc_ctor.direction,
6030 construction: arc_ctor.construction,
6031 }),
6032 _ => {
6033 return Err("Unsupported segment type for new segment".to_string());
6034 }
6035 };
6036
6037 let (_add_source_delta, add_scene_graph_delta) = frontend
6038 .add_segment(ctx, version, sketch_id, new_segment_ctor, None)
6039 .await
6040 .map_err(|e| format!("Failed to add new segment: {}", e.error.message()))?;
6041
6042 let new_segment_id = *add_scene_graph_delta
6044 .new_objects
6045 .iter()
6046 .find(|&id| {
6047 if let Some(obj) = add_scene_graph_delta.new_graph.objects.iter().find(|o| o.id == *id) {
6048 matches!(
6049 &obj.kind,
6050 crate::frontend::api::ObjectKind::Segment { segment }
6051 if matches!(segment, crate::frontend::sketch::Segment::Line(_) | crate::frontend::sketch::Segment::Arc(_))
6052 )
6053 } else {
6054 false
6055 }
6056 })
6057 .ok_or_else(|| "Failed to find newly created segment".to_string())?;
6058
6059 let new_segment = add_scene_graph_delta
6060 .new_graph
6061 .objects
6062 .iter()
6063 .find(|o| o.id == new_segment_id)
6064 .ok_or_else(|| format!("New segment not found with id {}", new_segment_id.0))?;
6065
6066 let (new_segment_start_point_id, new_segment_end_point_id, new_segment_center_point_id) =
6068 match &new_segment.kind {
6069 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
6070 crate::frontend::sketch::Segment::Line(line) => (line.start, line.end, None),
6071 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, Some(arc.center)),
6072 _ => {
6073 return Err("New segment is not a Line or Arc".to_string());
6074 }
6075 },
6076 _ => {
6077 return Err("New segment is not a segment".to_string());
6078 }
6079 };
6080
6081 let edited_ctor = match &original_ctor {
6083 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
6084 start: line_ctor.start.clone(),
6085 end: point_to_expr(coords_to_point(*left_trim_coords)),
6086 construction: line_ctor.construction,
6087 }),
6088 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
6089 start: arc_ctor.start.clone(),
6090 end: point_to_expr(coords_to_point(*left_trim_coords)),
6091 center: arc_ctor.center.clone(),
6092 direction: arc_ctor.direction,
6093 construction: arc_ctor.construction,
6094 }),
6095 _ => {
6096 return Err("Unsupported segment type for split".to_string());
6097 }
6098 };
6099
6100 let edit_scene_graph_delta = add_scene_graph_delta;
6107 let left_side_endpoint_point_id =
6108 original_segment_end_point_id.ok_or_else(|| "Original segment has no end point".to_string())?;
6109
6110 let mut batch_constraints = Vec::new();
6112
6113 let left_intersecting_seg_id = match &**left_side {
6115 TrimTermination::Intersection {
6116 intersecting_seg_id, ..
6117 }
6118 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
6119 intersecting_seg_id, ..
6120 } => *intersecting_seg_id,
6121 _ => {
6122 return Err("Left side is not an intersection or coincident".to_string());
6123 }
6124 };
6125 let left_coincident_segments = match &**left_side {
6126 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
6127 other_segment_point_id,
6128 ..
6129 } => {
6130 vec![left_side_endpoint_point_id.into(), (*other_segment_point_id).into()]
6131 }
6132 _ => {
6133 vec![left_side_endpoint_point_id.into(), left_intersecting_seg_id.into()]
6134 }
6135 };
6136 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
6137 segments: left_coincident_segments,
6138 }));
6139
6140 let right_intersecting_seg_id = match &**right_side {
6142 TrimTermination::Intersection {
6143 intersecting_seg_id, ..
6144 }
6145 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
6146 intersecting_seg_id, ..
6147 } => *intersecting_seg_id,
6148 _ => {
6149 return Err("Right side is not an intersection or coincident".to_string());
6150 }
6151 };
6152
6153 let mut intersection_point_id: Option<ObjectId> = None;
6154 if matches!(&**right_side, TrimTermination::Intersection { .. }) {
6155 let intersecting_seg = edit_scene_graph_delta
6156 .new_graph
6157 .objects
6158 .iter()
6159 .find(|obj| obj.id == right_intersecting_seg_id);
6160
6161 if let Some(seg) = intersecting_seg {
6162 let endpoint_epsilon = 1e-3; let right_trim_coords_value = *right_trim_coords;
6164
6165 if let crate::frontend::api::ObjectKind::Segment { segment } = &seg.kind {
6166 match segment {
6167 crate::frontend::sketch::Segment::Line(_) => {
6168 if let (Some(start_coords), Some(end_coords)) = (
6169 crate::frontend::trim::get_position_coords_for_line(
6170 seg,
6171 crate::frontend::trim::LineEndpoint::Start,
6172 &edit_scene_graph_delta.new_graph.objects,
6173 default_unit,
6174 ),
6175 crate::frontend::trim::get_position_coords_for_line(
6176 seg,
6177 crate::frontend::trim::LineEndpoint::End,
6178 &edit_scene_graph_delta.new_graph.objects,
6179 default_unit,
6180 ),
6181 ) {
6182 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
6183 * (right_trim_coords_value.x - start_coords.x)
6184 + (right_trim_coords_value.y - start_coords.y)
6185 * (right_trim_coords_value.y - start_coords.y))
6186 .sqrt();
6187 if dist_to_start < endpoint_epsilon {
6188 if let crate::frontend::sketch::Segment::Line(line) = segment {
6189 intersection_point_id = Some(line.start);
6190 }
6191 } else {
6192 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
6193 * (right_trim_coords_value.x - end_coords.x)
6194 + (right_trim_coords_value.y - end_coords.y)
6195 * (right_trim_coords_value.y - end_coords.y))
6196 .sqrt();
6197 if dist_to_end < endpoint_epsilon
6198 && let crate::frontend::sketch::Segment::Line(line) = segment
6199 {
6200 intersection_point_id = Some(line.end);
6201 }
6202 }
6203 }
6204 }
6205 crate::frontend::sketch::Segment::Arc(_) => {
6206 if let (Some(start_coords), Some(end_coords)) = (
6207 crate::frontend::trim::get_position_coords_from_arc(
6208 seg,
6209 crate::frontend::trim::ArcPoint::Start,
6210 &edit_scene_graph_delta.new_graph.objects,
6211 default_unit,
6212 ),
6213 crate::frontend::trim::get_position_coords_from_arc(
6214 seg,
6215 crate::frontend::trim::ArcPoint::End,
6216 &edit_scene_graph_delta.new_graph.objects,
6217 default_unit,
6218 ),
6219 ) {
6220 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
6221 * (right_trim_coords_value.x - start_coords.x)
6222 + (right_trim_coords_value.y - start_coords.y)
6223 * (right_trim_coords_value.y - start_coords.y))
6224 .sqrt();
6225 if dist_to_start < endpoint_epsilon {
6226 if let crate::frontend::sketch::Segment::Arc(arc) = segment {
6227 intersection_point_id = Some(arc.start);
6228 }
6229 } else {
6230 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
6231 * (right_trim_coords_value.x - end_coords.x)
6232 + (right_trim_coords_value.y - end_coords.y)
6233 * (right_trim_coords_value.y - end_coords.y))
6234 .sqrt();
6235 if dist_to_end < endpoint_epsilon
6236 && let crate::frontend::sketch::Segment::Arc(arc) = segment
6237 {
6238 intersection_point_id = Some(arc.end);
6239 }
6240 }
6241 }
6242 }
6243 _ => {}
6244 }
6245 }
6246 }
6247 }
6248
6249 let right_coincident_segments = if let Some(point_id) = intersection_point_id {
6250 vec![new_segment_start_point_id.into(), point_id.into()]
6251 } else if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
6252 other_segment_point_id,
6253 ..
6254 } = &**right_side
6255 {
6256 vec![new_segment_start_point_id.into(), (*other_segment_point_id).into()]
6257 } else {
6258 vec![new_segment_start_point_id.into(), right_intersecting_seg_id.into()]
6259 };
6260 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
6261 segments: right_coincident_segments,
6262 }));
6263
6264 let mut points_constrained_to_new_segment_start = std::collections::HashSet::new();
6266 let mut points_constrained_to_new_segment_end = std::collections::HashSet::new();
6267
6268 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
6269 other_segment_point_id,
6270 ..
6271 } = &**right_side
6272 {
6273 points_constrained_to_new_segment_start.insert(other_segment_point_id);
6274 }
6275
6276 for constraint_to_migrate in constraints_to_migrate.iter() {
6277 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::End
6278 && constraint_to_migrate.is_point_point
6279 {
6280 points_constrained_to_new_segment_end.insert(constraint_to_migrate.other_entity_id);
6281 }
6282 }
6283
6284 for constraint_to_migrate in constraints_to_migrate.iter() {
6285 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment
6287 && (points_constrained_to_new_segment_start.contains(&constraint_to_migrate.other_entity_id)
6288 || points_constrained_to_new_segment_end.contains(&constraint_to_migrate.other_entity_id))
6289 {
6290 continue; }
6292
6293 let constraint_segments = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment {
6294 vec![constraint_to_migrate.other_entity_id.into(), new_segment_id.into()]
6295 } else {
6296 let target_endpoint_id = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Start
6297 {
6298 new_segment_start_point_id
6299 } else {
6300 new_segment_end_point_id
6301 };
6302 vec![target_endpoint_id.into(), constraint_to_migrate.other_entity_id.into()]
6303 };
6304 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
6305 segments: constraint_segments,
6306 }));
6307 }
6308
6309 let mut distance_constraints_to_re_add: Vec<(
6311 crate::frontend::api::Number,
6312 Option<crate::frontend::sketch::Point2d<crate::frontend::api::Number>>,
6313 crate::frontend::sketch::ConstraintSource,
6314 )> = Vec::new();
6315 if let (Some(original_start_id), Some(original_end_id)) =
6316 (original_segment_start_point_id, original_segment_end_point_id)
6317 {
6318 for obj in &edit_scene_graph_delta.new_graph.objects {
6319 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
6320 continue;
6321 };
6322
6323 let Constraint::Distance(distance) = constraint else {
6324 continue;
6325 };
6326
6327 let references_start = distance.contains_point(original_start_id);
6328 let references_end = distance.contains_point(original_end_id);
6329
6330 if references_start && references_end {
6331 distance_constraints_to_re_add.push((
6332 distance.distance,
6333 distance.label_position.clone(),
6334 distance.source.clone(),
6335 ));
6336 }
6337 }
6338 }
6339
6340 if let Some(original_start_id) = original_segment_start_point_id {
6342 for (distance_value, label_position, source) in distance_constraints_to_re_add {
6343 batch_constraints.push(Constraint::Distance(crate::frontend::sketch::Distance {
6344 points: vec![original_start_id.into(), new_segment_end_point_id.into()],
6345 distance: distance_value,
6346 label_position,
6347 source,
6348 }));
6349 }
6350 }
6351
6352 if let Some(new_center_id) = new_segment_center_point_id {
6354 for (constraint, original_center_id) in center_point_constraints_to_migrate {
6355 let center_rewrite_map = std::collections::HashMap::from([(original_center_id, new_center_id)]);
6356 if let Some(rewritten) = rewrite_constraint_with_map(&constraint, ¢er_rewrite_map)
6357 && matches!(rewritten, Constraint::Coincident(_) | Constraint::Distance(_))
6358 {
6359 batch_constraints.push(rewritten);
6360 }
6361 }
6362 }
6363
6364 let mut angle_rewrite_map = std::collections::HashMap::from([(*segment_id, new_segment_id)]);
6366 if let Some(original_end_id) = original_segment_end_point_id {
6367 angle_rewrite_map.insert(original_end_id, new_segment_end_point_id);
6368 }
6369 for obj in &edit_scene_graph_delta.new_graph.objects {
6370 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
6371 continue;
6372 };
6373
6374 let should_migrate = match constraint {
6377 Constraint::Parallel(parallel) => parallel.lines.contains(segment_id),
6378 Constraint::Perpendicular(perpendicular) => perpendicular.lines.contains(segment_id),
6379 Constraint::Horizontal(Horizontal::Line { line }) => line == segment_id,
6380 Constraint::Horizontal(Horizontal::Points { points }) => original_segment_end_point_id
6381 .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
6382 Constraint::Vertical(Vertical::Line { line }) => line == segment_id,
6383 Constraint::Vertical(Vertical::Points { points }) => original_segment_end_point_id
6384 .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
6385 Constraint::Angle(_)
6386 | Constraint::Coincident(_)
6387 | Constraint::Diameter(_)
6388 | Constraint::Distance(_)
6389 | Constraint::EqualRadius(_)
6390 | Constraint::Fixed(_)
6391 | Constraint::HorizontalDistance(_)
6392 | Constraint::LinesEqualLength(_)
6393 | Constraint::Midpoint(_)
6394 | Constraint::Radius(_)
6395 | Constraint::Symmetric(_)
6396 | Constraint::Tangent(_)
6397 | Constraint::VerticalDistance(_) => false,
6398 };
6399
6400 if should_migrate
6401 && let Some(migrated_constraint) = rewrite_constraint_with_map(constraint, &angle_rewrite_map)
6402 && matches!(
6403 migrated_constraint,
6404 Constraint::Parallel(_)
6405 | Constraint::Perpendicular(_)
6406 | Constraint::Horizontal(_)
6407 | Constraint::Vertical(_)
6408 )
6409 {
6410 batch_constraints.push(migrated_constraint);
6411 }
6412 }
6413
6414 let constraint_object_ids: Vec<ObjectId> = constraints_to_delete.to_vec();
6416
6417 let batch_result = frontend
6418 .batch_split_segment_operations(
6419 ctx,
6420 version,
6421 sketch_id,
6422 vec![ExistingSegmentCtor {
6423 id: *segment_id,
6424 ctor: edited_ctor,
6425 }],
6426 batch_constraints,
6427 constraint_object_ids,
6428 crate::frontend::sketch::NewSegmentInfo {
6429 segment_id: new_segment_id,
6430 start_point_id: new_segment_start_point_id,
6431 end_point_id: new_segment_end_point_id,
6432 center_point_id: new_segment_center_point_id,
6433 },
6434 )
6435 .await
6436 .map_err(|e| format!("Failed to batch split segment operations: {}", e.error.message()));
6437 if let Ok((_, ref batch_delta)) = batch_result {
6439 invalidates_ids = invalidates_ids || batch_delta.invalidates_ids;
6440 }
6441 batch_result
6442 }
6443 TrimOperation::SplitControlPointSpline {
6444 segment_id,
6445 left_ctor,
6446 right_ctor,
6447 left_side,
6448 right_side,
6449 constraint_ids_to_delete,
6450 } => {
6451 let original_segment = current_scene_graph_delta
6452 .new_graph
6453 .objects
6454 .iter()
6455 .find(|obj| obj.id == *segment_id)
6456 .ok_or_else(|| format!("Failed to find original control point spline {}", segment_id.0))?;
6457
6458 let (_original_start_id, original_end_id) = match &original_segment.kind {
6459 crate::frontend::api::ObjectKind::Segment {
6460 segment: crate::frontend::sketch::Segment::ControlPointSpline(spline),
6461 } => (
6462 spline
6463 .controls
6464 .first()
6465 .copied()
6466 .ok_or_else(|| format!("Spline {} has no start control point", segment_id.0))?,
6467 spline
6468 .controls
6469 .last()
6470 .copied()
6471 .ok_or_else(|| format!("Spline {} has no end control point", segment_id.0))?,
6472 ),
6473 _ => return Err("Original segment is not a control point spline".to_string()),
6474 };
6475
6476 let (_add_source_delta, add_scene_graph_delta) = frontend
6477 .add_segment(ctx, version, sketch_id, right_ctor.clone(), None)
6478 .await
6479 .map_err(|e| format!("Failed to add split spline segment: {}", e.error.message()))?;
6480 invalidates_ids = invalidates_ids || add_scene_graph_delta.invalidates_ids;
6481
6482 let new_right_segment_id = *add_scene_graph_delta
6483 .new_objects
6484 .iter()
6485 .find(|&&id| {
6486 add_scene_graph_delta
6487 .new_graph
6488 .objects
6489 .iter()
6490 .find(|obj| obj.id == id)
6491 .is_some_and(|obj| {
6492 matches!(
6493 obj.kind,
6494 crate::frontend::api::ObjectKind::Segment {
6495 segment: crate::frontend::sketch::Segment::ControlPointSpline(_)
6496 }
6497 )
6498 })
6499 })
6500 .ok_or_else(|| "Failed to find newly created split spline segment".to_string())?;
6501
6502 let new_right_segment = add_scene_graph_delta
6503 .new_graph
6504 .objects
6505 .iter()
6506 .find(|obj| obj.id == new_right_segment_id)
6507 .ok_or_else(|| format!("New split spline {} not found", new_right_segment_id.0))?;
6508 let (new_right_start_id, new_right_end_id) = match &new_right_segment.kind {
6509 crate::frontend::api::ObjectKind::Segment {
6510 segment: crate::frontend::sketch::Segment::ControlPointSpline(spline),
6511 } => (
6512 spline.controls.first().copied().ok_or_else(|| {
6513 format!("New split spline {} has no start control point", new_right_segment_id.0)
6514 })?,
6515 spline.controls.last().copied().ok_or_else(|| {
6516 format!("New split spline {} has no end control point", new_right_segment_id.0)
6517 })?,
6518 ),
6519 _ => return Err("New split segment is not a control point spline".to_string()),
6520 };
6521
6522 let (_edit_source_delta, edit_scene_graph_delta) = frontend
6523 .edit_segments(
6524 ctx,
6525 version,
6526 sketch_id,
6527 vec![ExistingSegmentCtor {
6528 id: *segment_id,
6529 ctor: left_ctor.clone(),
6530 }],
6531 )
6532 .await
6533 .map_err(|e| format!("Failed to edit original split spline: {}", e.error.message()))?;
6534 invalidates_ids = invalidates_ids || edit_scene_graph_delta.invalidates_ids;
6535
6536 let edited_left_segment = edit_scene_graph_delta
6537 .new_graph
6538 .objects
6539 .iter()
6540 .find(|obj| obj.id == *segment_id)
6541 .ok_or_else(|| format!("Edited split spline {} not found", segment_id.0))?;
6542 let edited_left_end_id = match &edited_left_segment.kind {
6543 crate::frontend::api::ObjectKind::Segment {
6544 segment: crate::frontend::sketch::Segment::ControlPointSpline(spline),
6545 } => spline
6546 .controls
6547 .last()
6548 .copied()
6549 .ok_or_else(|| format!("Edited split spline {} has no end control point", segment_id.0))?,
6550 _ => return Err("Edited split segment is not a control point spline".to_string()),
6551 };
6552
6553 let constraint_segments_for =
6554 |endpoint_id: ObjectId,
6555 term: &TrimTermination|
6556 -> Result<Vec<crate::frontend::sketch::ConstraintSegment>, String> {
6557 match term {
6558 TrimTermination::Intersection {
6559 intersecting_seg_id, ..
6560 } => Ok(vec![endpoint_id.into(), (*intersecting_seg_id).into()]),
6561 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
6562 other_segment_point_id,
6563 ..
6564 } => Ok(vec![endpoint_id.into(), (*other_segment_point_id).into()]),
6565 TrimTermination::SegEndPoint { .. } => {
6566 Err("Split spline termination cannot be a segment endpoint".to_string())
6567 }
6568 }
6569 };
6570
6571 let (_left_source_delta, left_scene_graph_delta) = frontend
6572 .add_constraint(
6573 ctx,
6574 version,
6575 sketch_id,
6576 Constraint::Coincident(crate::frontend::sketch::Coincident {
6577 segments: constraint_segments_for(edited_left_end_id, left_side)?,
6578 }),
6579 )
6580 .await
6581 .map_err(|e| format!("Failed to add left split spline coincident: {}", e.error.message()))?;
6582 invalidates_ids = invalidates_ids || left_scene_graph_delta.invalidates_ids;
6583
6584 let (_right_source_delta, right_scene_graph_delta) = frontend
6585 .add_constraint(
6586 ctx,
6587 version,
6588 sketch_id,
6589 Constraint::Coincident(crate::frontend::sketch::Coincident {
6590 segments: constraint_segments_for(new_right_start_id, right_side)?,
6591 }),
6592 )
6593 .await
6594 .map_err(|e| format!("Failed to add right split spline coincident: {}", e.error.message()))?;
6595 invalidates_ids = invalidates_ids || right_scene_graph_delta.invalidates_ids;
6596
6597 let original_end_owner_ids: std::collections::HashSet<ObjectId> = current_scene_graph_delta
6598 .new_graph
6599 .objects
6600 .iter()
6601 .filter_map(|obj| match &obj.kind {
6602 crate::frontend::api::ObjectKind::Constraint {
6603 constraint: Constraint::Coincident(coincident),
6604 } if coincident.contains_segment(original_end_id) => coincident.segment_ids().find_map(|id| {
6605 if id == original_end_id {
6606 None
6607 } else {
6608 current_scene_graph_delta
6609 .new_graph
6610 .objects
6611 .iter()
6612 .find(|candidate| candidate.id == id)
6613 .and_then(|candidate| match &candidate.kind {
6614 crate::frontend::api::ObjectKind::Segment {
6615 segment: crate::frontend::sketch::Segment::Point(point),
6616 } => point.owner,
6617 _ => Some(id),
6618 })
6619 }
6620 }),
6621 _ => None,
6622 })
6623 .collect();
6624
6625 for obj in ¤t_scene_graph_delta.new_graph.objects {
6626 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
6627 continue;
6628 };
6629 if !constraint_ids_to_delete.contains(&obj.id) {
6630 continue;
6631 }
6632
6633 match constraint {
6634 Constraint::Coincident(coincident) if coincident.contains_segment(original_end_id) => {
6635 let migrated_segments = coincident
6636 .segments
6637 .iter()
6638 .map(|segment| match segment {
6639 crate::frontend::sketch::ConstraintSegment::Segment(id)
6640 if *id == original_end_id =>
6641 {
6642 crate::frontend::sketch::ConstraintSegment::Segment(new_right_end_id)
6643 }
6644 _ => *segment,
6645 })
6646 .collect::<Vec<_>>();
6647 let (_source_delta, migrated_scene_graph_delta) = frontend
6648 .add_constraint(
6649 ctx,
6650 version,
6651 sketch_id,
6652 Constraint::Coincident(crate::frontend::sketch::Coincident {
6653 segments: migrated_segments,
6654 }),
6655 )
6656 .await
6657 .map_err(|e| {
6658 format!("Failed to migrate split spline coincident: {}", e.error.message())
6659 })?;
6660 invalidates_ids = invalidates_ids || migrated_scene_graph_delta.invalidates_ids;
6661 }
6662 Constraint::Tangent(tangent) if tangent.input.contains(segment_id) => {
6663 let other_ids = tangent
6664 .input
6665 .iter()
6666 .copied()
6667 .filter(|id| *id != *segment_id)
6668 .collect::<Vec<_>>();
6669 if other_ids.iter().any(|id| original_end_owner_ids.contains(id)) {
6670 let (_source_delta, migrated_scene_graph_delta) = frontend
6671 .add_constraint(
6672 ctx,
6673 version,
6674 sketch_id,
6675 Constraint::Tangent(crate::frontend::sketch::Tangent {
6676 input: tangent
6677 .input
6678 .iter()
6679 .map(|id| if *id == *segment_id { new_right_segment_id } else { *id })
6680 .collect(),
6681 }),
6682 )
6683 .await
6684 .map_err(|e| {
6685 format!("Failed to migrate split spline tangent: {}", e.error.message())
6686 })?;
6687 invalidates_ids = invalidates_ids || migrated_scene_graph_delta.invalidates_ids;
6688 }
6689 }
6690 _ => {}
6691 }
6692 }
6693
6694 frontend
6695 .delete_objects(ctx, version, sketch_id, constraint_ids_to_delete.clone(), Vec::new())
6696 .await
6697 .map_err(|e| format!("Failed to delete split spline constraints: {}", e.error.message()))
6698 }
6699 };
6700
6701 match operation_result {
6702 Ok((source_delta, mut scene_graph_delta)) => {
6703 normalize_scene_graph_delta_for_internal_trim(frontend, &mut scene_graph_delta);
6704 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
6706 last_result = Some((source_delta, scene_graph_delta.clone()));
6707 }
6708 Err(e) => {
6709 crate::logln!("Error executing trim operation {}: {}", op_index, e);
6710 }
6712 }
6713
6714 op_index += consumed_ops;
6715 }
6716
6717 let (source_delta, mut scene_graph_delta) =
6718 last_result.ok_or_else(|| "No operations were executed successfully".to_string())?;
6719 scene_graph_delta.invalidates_ids = invalidates_ids;
6721 Ok((source_delta, scene_graph_delta))
6722}