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