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