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