use std::f64::consts::TAU;
use indexmap::IndexSet;
use kittycad_modeling_cmds::units::UnitLength;
use crate::execution::types::adjust_length;
use crate::front::Horizontal;
use crate::front::Vertical;
use crate::frontend::api::Number;
use crate::frontend::api::Object;
use crate::frontend::api::ObjectId;
use crate::frontend::api::ObjectKind;
use crate::frontend::sketch::Constraint;
use crate::frontend::sketch::ConstraintSegment;
use crate::frontend::sketch::Segment;
use crate::frontend::sketch::SegmentCtor;
use crate::pretty::NumericSuffix;
#[cfg(all(feature = "artifact-graph", test))]
mod tests;
const EPSILON_PARALLEL: f64 = 1e-10;
const EPSILON_POINT_ON_SEGMENT: f64 = 1e-6;
fn suffix_to_unit(suffix: NumericSuffix) -> UnitLength {
match suffix {
NumericSuffix::Mm => UnitLength::Millimeters,
NumericSuffix::Cm => UnitLength::Centimeters,
NumericSuffix::M => UnitLength::Meters,
NumericSuffix::Inch => UnitLength::Inches,
NumericSuffix::Ft => UnitLength::Feet,
NumericSuffix::Yd => UnitLength::Yards,
_ => UnitLength::Millimeters,
}
}
fn number_to_unit(n: &Number, target_unit: UnitLength) -> f64 {
adjust_length(suffix_to_unit(n.units), n.value, target_unit).0
}
fn unit_to_number(value: f64, source_unit: UnitLength, target_suffix: NumericSuffix) -> Number {
let (value, _) = adjust_length(source_unit, value, suffix_to_unit(target_suffix));
Number {
value,
units: target_suffix,
}
}
fn normalize_trim_points_to_unit(points: &[Coords2d], default_unit: UnitLength) -> Vec<Coords2d> {
points
.iter()
.map(|point| Coords2d {
x: adjust_length(UnitLength::Millimeters, point.x, default_unit).0,
y: adjust_length(UnitLength::Millimeters, point.y, default_unit).0,
})
.collect()
}
#[derive(Debug, Clone, Copy)]
pub struct Coords2d {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineEndpoint {
Start,
End,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArcPoint {
Start,
End,
Center,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CirclePoint {
Start,
Center,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrimDirection {
Left,
Right,
}
#[derive(Debug, Clone)]
pub enum TrimItem {
Spawn {
trim_spawn_seg_id: ObjectId,
trim_spawn_coords: Coords2d,
next_index: usize,
},
None {
next_index: usize,
},
}
#[derive(Debug, Clone)]
pub enum TrimTermination {
SegEndPoint {
trim_termination_coords: Coords2d,
},
Intersection {
trim_termination_coords: Coords2d,
intersecting_seg_id: ObjectId,
},
TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
trim_termination_coords: Coords2d,
intersecting_seg_id: ObjectId,
other_segment_point_id: ObjectId,
},
}
#[derive(Debug, Clone)]
pub struct TrimTerminations {
pub left_side: TrimTermination,
pub right_side: TrimTermination,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttachToEndpoint {
Start,
End,
Segment,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EndpointChanged {
Start,
End,
}
#[derive(Debug, Clone)]
pub struct CoincidentData {
pub intersecting_seg_id: ObjectId,
pub intersecting_endpoint_point_id: Option<ObjectId>,
pub existing_point_segment_constraint_id: Option<ObjectId>,
}
#[derive(Debug, Clone)]
pub struct ConstraintToMigrate {
pub constraint_id: ObjectId,
pub other_entity_id: ObjectId,
pub is_point_point: bool,
pub attach_to_endpoint: AttachToEndpoint,
}
#[derive(Debug, Clone)]
pub enum TrimPlan {
DeleteSegment {
segment_id: ObjectId,
},
TailCut {
segment_id: ObjectId,
endpoint_changed: EndpointChanged,
ctor: SegmentCtor,
segment_or_point_to_make_coincident_to: ObjectId,
intersecting_endpoint_point_id: Option<ObjectId>,
constraint_ids_to_delete: Vec<ObjectId>,
},
ReplaceCircleWithArc {
circle_id: ObjectId,
arc_start_coords: Coords2d,
arc_end_coords: Coords2d,
arc_start_termination: Box<TrimTermination>,
arc_end_termination: Box<TrimTermination>,
},
SplitSegment {
segment_id: ObjectId,
left_trim_coords: Coords2d,
right_trim_coords: Coords2d,
original_end_coords: Coords2d,
left_side: Box<TrimTermination>,
right_side: Box<TrimTermination>,
left_side_coincident_data: CoincidentData,
right_side_coincident_data: CoincidentData,
constraints_to_migrate: Vec<ConstraintToMigrate>,
constraints_to_delete: Vec<ObjectId>,
},
}
fn lower_trim_plan(plan: &TrimPlan) -> Vec<TrimOperation> {
match plan {
TrimPlan::DeleteSegment { segment_id } => vec![TrimOperation::SimpleTrim {
segment_to_trim_id: *segment_id,
}],
TrimPlan::TailCut {
segment_id,
endpoint_changed,
ctor,
segment_or_point_to_make_coincident_to,
intersecting_endpoint_point_id,
constraint_ids_to_delete,
} => {
let mut ops = vec![
TrimOperation::EditSegment {
segment_id: *segment_id,
ctor: ctor.clone(),
endpoint_changed: *endpoint_changed,
},
TrimOperation::AddCoincidentConstraint {
segment_id: *segment_id,
endpoint_changed: *endpoint_changed,
segment_or_point_to_make_coincident_to: *segment_or_point_to_make_coincident_to,
intersecting_endpoint_point_id: *intersecting_endpoint_point_id,
},
];
if !constraint_ids_to_delete.is_empty() {
ops.push(TrimOperation::DeleteConstraints {
constraint_ids: constraint_ids_to_delete.clone(),
});
}
ops
}
TrimPlan::ReplaceCircleWithArc {
circle_id,
arc_start_coords,
arc_end_coords,
arc_start_termination,
arc_end_termination,
} => vec![TrimOperation::ReplaceCircleWithArc {
circle_id: *circle_id,
arc_start_coords: *arc_start_coords,
arc_end_coords: *arc_end_coords,
arc_start_termination: arc_start_termination.clone(),
arc_end_termination: arc_end_termination.clone(),
}],
TrimPlan::SplitSegment {
segment_id,
left_trim_coords,
right_trim_coords,
original_end_coords,
left_side,
right_side,
left_side_coincident_data,
right_side_coincident_data,
constraints_to_migrate,
constraints_to_delete,
} => vec![TrimOperation::SplitSegment {
segment_id: *segment_id,
left_trim_coords: *left_trim_coords,
right_trim_coords: *right_trim_coords,
original_end_coords: *original_end_coords,
left_side: left_side.clone(),
right_side: right_side.clone(),
left_side_coincident_data: left_side_coincident_data.clone(),
right_side_coincident_data: right_side_coincident_data.clone(),
constraints_to_migrate: constraints_to_migrate.clone(),
constraints_to_delete: constraints_to_delete.clone(),
}],
}
}
fn trim_plan_modifies_geometry(plan: &TrimPlan) -> bool {
matches!(
plan,
TrimPlan::DeleteSegment { .. }
| TrimPlan::TailCut { .. }
| TrimPlan::ReplaceCircleWithArc { .. }
| TrimPlan::SplitSegment { .. }
)
}
fn rewrite_object_id(id: ObjectId, rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>) -> ObjectId {
rewrite_map.get(&id).copied().unwrap_or(id)
}
fn rewrite_constraint_segment(
segment: crate::frontend::sketch::ConstraintSegment,
rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
) -> crate::frontend::sketch::ConstraintSegment {
match segment {
crate::frontend::sketch::ConstraintSegment::Segment(id) => {
crate::frontend::sketch::ConstraintSegment::Segment(rewrite_object_id(id, rewrite_map))
}
crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
crate::frontend::sketch::ConstraintSegment::Origin(origin)
}
}
}
fn rewrite_constraint_segments(
segments: &[crate::frontend::sketch::ConstraintSegment],
rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
) -> Vec<crate::frontend::sketch::ConstraintSegment> {
segments
.iter()
.copied()
.map(|segment| rewrite_constraint_segment(segment, rewrite_map))
.collect()
}
fn constraint_segments_reference_any(
segments: &[crate::frontend::sketch::ConstraintSegment],
ids: &std::collections::HashSet<ObjectId>,
) -> bool {
segments.iter().any(|segment| match segment {
crate::frontend::sketch::ConstraintSegment::Segment(id) => ids.contains(id),
crate::frontend::sketch::ConstraintSegment::Origin(_) => false,
})
}
fn rewrite_constraint_with_map(
constraint: &Constraint,
rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
) -> Option<Constraint> {
match constraint {
Constraint::Coincident(coincident) => Some(Constraint::Coincident(crate::frontend::sketch::Coincident {
segments: rewrite_constraint_segments(&coincident.segments, rewrite_map),
})),
Constraint::Distance(distance) => Some(Constraint::Distance(crate::frontend::sketch::Distance {
points: rewrite_constraint_segments(&distance.points, rewrite_map),
distance: distance.distance,
source: distance.source.clone(),
})),
Constraint::HorizontalDistance(distance) => {
Some(Constraint::HorizontalDistance(crate::frontend::sketch::Distance {
points: rewrite_constraint_segments(&distance.points, rewrite_map),
distance: distance.distance,
source: distance.source.clone(),
}))
}
Constraint::VerticalDistance(distance) => {
Some(Constraint::VerticalDistance(crate::frontend::sketch::Distance {
points: rewrite_constraint_segments(&distance.points, rewrite_map),
distance: distance.distance,
source: distance.source.clone(),
}))
}
Constraint::Radius(radius) => Some(Constraint::Radius(crate::frontend::sketch::Radius {
arc: rewrite_object_id(radius.arc, rewrite_map),
radius: radius.radius,
source: radius.source.clone(),
})),
Constraint::Diameter(diameter) => Some(Constraint::Diameter(crate::frontend::sketch::Diameter {
arc: rewrite_object_id(diameter.arc, rewrite_map),
diameter: diameter.diameter,
source: diameter.source.clone(),
})),
Constraint::EqualRadius(equal_radius) => Some(Constraint::EqualRadius(crate::frontend::sketch::EqualRadius {
input: equal_radius
.input
.iter()
.map(|id| rewrite_object_id(*id, rewrite_map))
.collect(),
})),
Constraint::Tangent(tangent) => Some(Constraint::Tangent(crate::frontend::sketch::Tangent {
input: tangent
.input
.iter()
.map(|id| rewrite_object_id(*id, rewrite_map))
.collect(),
})),
Constraint::Parallel(parallel) => Some(Constraint::Parallel(crate::frontend::sketch::Parallel {
lines: parallel
.lines
.iter()
.map(|id| rewrite_object_id(*id, rewrite_map))
.collect(),
})),
Constraint::Perpendicular(perpendicular) => {
Some(Constraint::Perpendicular(crate::frontend::sketch::Perpendicular {
lines: perpendicular
.lines
.iter()
.map(|id| rewrite_object_id(*id, rewrite_map))
.collect(),
}))
}
Constraint::Horizontal(horizontal) => match horizontal {
crate::front::Horizontal::Line { line } => {
Some(Constraint::Horizontal(crate::frontend::sketch::Horizontal::Line {
line: rewrite_object_id(*line, rewrite_map),
}))
}
crate::front::Horizontal::Points { points } => Some(Constraint::Horizontal(Horizontal::Points {
points: points
.iter()
.map(|point| match point {
crate::frontend::sketch::ConstraintSegment::Segment(point) => {
crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
}
crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
crate::frontend::sketch::ConstraintSegment::Origin(*origin)
}
})
.collect(),
})),
},
Constraint::Vertical(vertical) => match vertical {
crate::front::Vertical::Line { line } => {
Some(Constraint::Vertical(crate::frontend::sketch::Vertical::Line {
line: rewrite_object_id(*line, rewrite_map),
}))
}
crate::front::Vertical::Points { points } => Some(Constraint::Vertical(Vertical::Points {
points: points
.iter()
.map(|point| match point {
crate::frontend::sketch::ConstraintSegment::Segment(point) => {
crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
}
crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
crate::frontend::sketch::ConstraintSegment::Origin(*origin)
}
})
.collect(),
})),
},
_ => None,
}
}
fn point_axis_constraint_references_point(constraint: &Constraint, point_id: ObjectId) -> bool {
match constraint {
Constraint::Horizontal(Horizontal::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
Constraint::Vertical(Vertical::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
_ => false,
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum TrimOperation {
SimpleTrim {
segment_to_trim_id: ObjectId,
},
EditSegment {
segment_id: ObjectId,
ctor: SegmentCtor,
endpoint_changed: EndpointChanged,
},
AddCoincidentConstraint {
segment_id: ObjectId,
endpoint_changed: EndpointChanged,
segment_or_point_to_make_coincident_to: ObjectId,
intersecting_endpoint_point_id: Option<ObjectId>,
},
SplitSegment {
segment_id: ObjectId,
left_trim_coords: Coords2d,
right_trim_coords: Coords2d,
original_end_coords: Coords2d,
left_side: Box<TrimTermination>,
right_side: Box<TrimTermination>,
left_side_coincident_data: CoincidentData,
right_side_coincident_data: CoincidentData,
constraints_to_migrate: Vec<ConstraintToMigrate>,
constraints_to_delete: Vec<ObjectId>,
},
ReplaceCircleWithArc {
circle_id: ObjectId,
arc_start_coords: Coords2d,
arc_end_coords: Coords2d,
arc_start_termination: Box<TrimTermination>,
arc_end_termination: Box<TrimTermination>,
},
DeleteConstraints {
constraint_ids: Vec<ObjectId>,
},
}
pub fn is_point_on_line_segment(
point: Coords2d,
segment_start: Coords2d,
segment_end: Coords2d,
epsilon: f64,
) -> Option<Coords2d> {
let dx = segment_end.x - segment_start.x;
let dy = segment_end.y - segment_start.y;
let segment_length_sq = dx * dx + dy * dy;
if segment_length_sq < EPSILON_PARALLEL {
let dist_sq = (point.x - segment_start.x) * (point.x - segment_start.x)
+ (point.y - segment_start.y) * (point.y - segment_start.y);
if dist_sq <= epsilon * epsilon {
return Some(point);
}
return None;
}
let point_dx = point.x - segment_start.x;
let point_dy = point.y - segment_start.y;
let projection_param = (point_dx * dx + point_dy * dy) / segment_length_sq;
if !(0.0..=1.0).contains(&projection_param) {
return None;
}
let projected_point = Coords2d {
x: segment_start.x + projection_param * dx,
y: segment_start.y + projection_param * dy,
};
let dist_dx = point.x - projected_point.x;
let dist_dy = point.y - projected_point.y;
let distance_sq = dist_dx * dist_dx + dist_dy * dist_dy;
if distance_sq <= epsilon * epsilon {
Some(point)
} else {
None
}
}
pub fn line_segment_intersection(
line1_start: Coords2d,
line1_end: Coords2d,
line2_start: Coords2d,
line2_end: Coords2d,
epsilon: f64,
) -> Option<Coords2d> {
if let Some(point) = is_point_on_line_segment(line1_start, line2_start, line2_end, epsilon) {
return Some(point);
}
if let Some(point) = is_point_on_line_segment(line1_end, line2_start, line2_end, epsilon) {
return Some(point);
}
if let Some(point) = is_point_on_line_segment(line2_start, line1_start, line1_end, epsilon) {
return Some(point);
}
if let Some(point) = is_point_on_line_segment(line2_end, line1_start, line1_end, epsilon) {
return Some(point);
}
let x1 = line1_start.x;
let y1 = line1_start.y;
let x2 = line1_end.x;
let y2 = line1_end.y;
let x3 = line2_start.x;
let y3 = line2_start.y;
let x4 = line2_end.x;
let y4 = line2_end.y;
let denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if denominator.abs() < EPSILON_PARALLEL {
return None;
}
let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator;
let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominator;
if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
let x = x1 + t * (x2 - x1);
let y = y1 + t * (y2 - y1);
return Some(Coords2d { x, y });
}
None
}
pub fn project_point_onto_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
let dx = segment_end.x - segment_start.x;
let dy = segment_end.y - segment_start.y;
let segment_length_sq = dx * dx + dy * dy;
if segment_length_sq < EPSILON_PARALLEL {
return 0.0;
}
let point_dx = point.x - segment_start.x;
let point_dy = point.y - segment_start.y;
(point_dx * dx + point_dy * dy) / segment_length_sq
}
pub fn perpendicular_distance_to_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
let dx = segment_end.x - segment_start.x;
let dy = segment_end.y - segment_start.y;
let segment_length_sq = dx * dx + dy * dy;
if segment_length_sq < EPSILON_PARALLEL {
let dist_dx = point.x - segment_start.x;
let dist_dy = point.y - segment_start.y;
return (dist_dx * dist_dx + dist_dy * dist_dy).sqrt();
}
let point_dx = point.x - segment_start.x;
let point_dy = point.y - segment_start.y;
let t = (point_dx * dx + point_dy * dy) / segment_length_sq;
let clamped_t = t.clamp(0.0, 1.0);
let closest_point = Coords2d {
x: segment_start.x + clamped_t * dx,
y: segment_start.y + clamped_t * dy,
};
let dist_dx = point.x - closest_point.x;
let dist_dy = point.y - closest_point.y;
(dist_dx * dist_dx + dist_dy * dist_dy).sqrt()
}
pub fn is_point_on_arc(point: Coords2d, center: Coords2d, start: Coords2d, end: Coords2d, epsilon: f64) -> bool {
let radius = ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
let dist_from_center =
((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
if (dist_from_center - radius).abs() > epsilon {
return false;
}
let start_angle = libm::atan2(start.y - center.y, start.x - center.x);
let end_angle = libm::atan2(end.y - center.y, end.x - center.x);
let point_angle = libm::atan2(point.y - center.y, point.x - center.x);
let normalize_angle = |angle: f64| -> f64 {
if !angle.is_finite() {
return angle;
}
let mut normalized = angle;
while normalized < 0.0 {
normalized += TAU;
}
while normalized >= TAU {
normalized -= TAU;
}
normalized
};
let normalized_start = normalize_angle(start_angle);
let normalized_end = normalize_angle(end_angle);
let normalized_point = normalize_angle(point_angle);
if normalized_start < normalized_end {
normalized_point >= normalized_start && normalized_point <= normalized_end
} else {
normalized_point >= normalized_start || normalized_point <= normalized_end
}
}
pub fn line_arc_intersection(
line_start: Coords2d,
line_end: Coords2d,
arc_center: Coords2d,
arc_start: Coords2d,
arc_end: Coords2d,
epsilon: f64,
) -> Option<Coords2d> {
let radius = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
+ (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
.sqrt();
let translated_line_start = Coords2d {
x: line_start.x - arc_center.x,
y: line_start.y - arc_center.y,
};
let translated_line_end = Coords2d {
x: line_end.x - arc_center.x,
y: line_end.y - arc_center.y,
};
let dx = translated_line_end.x - translated_line_start.x;
let dy = translated_line_end.y - translated_line_start.y;
let a = dx * dx + dy * dy;
let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
- radius * radius;
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return None;
}
if a.abs() < EPSILON_PARALLEL {
let dist_from_center = (translated_line_start.x * translated_line_start.x
+ translated_line_start.y * translated_line_start.y)
.sqrt();
if (dist_from_center - radius).abs() <= epsilon {
let point = line_start;
if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
return Some(point);
}
}
return None;
}
let sqrt_discriminant = discriminant.sqrt();
let t1 = (-b - sqrt_discriminant) / (2.0 * a);
let t2 = (-b + sqrt_discriminant) / (2.0 * a);
let mut candidates: Vec<(f64, Coords2d)> = Vec::new();
if (0.0..=1.0).contains(&t1) {
let point = Coords2d {
x: line_start.x + t1 * (line_end.x - line_start.x),
y: line_start.y + t1 * (line_end.y - line_start.y),
};
candidates.push((t1, point));
}
if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
let point = Coords2d {
x: line_start.x + t2 * (line_end.x - line_start.x),
y: line_start.y + t2 * (line_end.y - line_start.y),
};
candidates.push((t2, point));
}
for (_t, point) in candidates {
if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
return Some(point);
}
}
None
}
pub fn line_circle_intersections(
line_start: Coords2d,
line_end: Coords2d,
circle_center: Coords2d,
radius: f64,
epsilon: f64,
) -> Vec<(f64, Coords2d)> {
let translated_line_start = Coords2d {
x: line_start.x - circle_center.x,
y: line_start.y - circle_center.y,
};
let translated_line_end = Coords2d {
x: line_end.x - circle_center.x,
y: line_end.y - circle_center.y,
};
let dx = translated_line_end.x - translated_line_start.x;
let dy = translated_line_end.y - translated_line_start.y;
let a = dx * dx + dy * dy;
let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
- radius * radius;
if a.abs() < EPSILON_PARALLEL {
return Vec::new();
}
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return Vec::new();
}
let sqrt_discriminant = discriminant.sqrt();
let mut intersections = Vec::new();
let t1 = (-b - sqrt_discriminant) / (2.0 * a);
if (0.0..=1.0).contains(&t1) {
intersections.push((
t1,
Coords2d {
x: line_start.x + t1 * (line_end.x - line_start.x),
y: line_start.y + t1 * (line_end.y - line_start.y),
},
));
}
let t2 = (-b + sqrt_discriminant) / (2.0 * a);
if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
intersections.push((
t2,
Coords2d {
x: line_start.x + t2 * (line_end.x - line_start.x),
y: line_start.y + t2 * (line_end.y - line_start.y),
},
));
}
intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
intersections
}
pub fn project_point_onto_circle(point: Coords2d, center: Coords2d, start: Coords2d) -> f64 {
let normalize_angle = |angle: f64| -> f64 {
if !angle.is_finite() {
return angle;
}
let mut normalized = angle;
while normalized < 0.0 {
normalized += TAU;
}
while normalized >= TAU {
normalized -= TAU;
}
normalized
};
let start_angle = normalize_angle(libm::atan2(start.y - center.y, start.x - center.x));
let point_angle = normalize_angle(libm::atan2(point.y - center.y, point.x - center.x));
let delta_ccw = (point_angle - start_angle).rem_euclid(TAU);
delta_ccw / TAU
}
fn is_point_on_circle(point: Coords2d, center: Coords2d, radius: f64, epsilon: f64) -> bool {
let dist = ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
(dist - radius).abs() <= epsilon
}
pub fn project_point_onto_arc(point: Coords2d, arc_center: Coords2d, arc_start: Coords2d, arc_end: Coords2d) -> f64 {
let start_angle = libm::atan2(arc_start.y - arc_center.y, arc_start.x - arc_center.x);
let end_angle = libm::atan2(arc_end.y - arc_center.y, arc_end.x - arc_center.x);
let point_angle = libm::atan2(point.y - arc_center.y, point.x - arc_center.x);
let normalize_angle = |angle: f64| -> f64 {
if !angle.is_finite() {
return angle;
}
let mut normalized = angle;
while normalized < 0.0 {
normalized += TAU;
}
while normalized >= TAU {
normalized -= TAU;
}
normalized
};
let normalized_start = normalize_angle(start_angle);
let normalized_end = normalize_angle(end_angle);
let normalized_point = normalize_angle(point_angle);
let arc_length = if normalized_start < normalized_end {
normalized_end - normalized_start
} else {
TAU - normalized_start + normalized_end
};
if arc_length < EPSILON_PARALLEL {
return 0.0;
}
let point_arc_length = if normalized_start < normalized_end {
if normalized_point >= normalized_start && normalized_point <= normalized_end {
normalized_point - normalized_start
} else {
let dist_to_start = (normalized_point - normalized_start)
.abs()
.min(TAU - (normalized_point - normalized_start).abs());
let dist_to_end = (normalized_point - normalized_end)
.abs()
.min(TAU - (normalized_point - normalized_end).abs());
return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
}
} else {
if normalized_point >= normalized_start || normalized_point <= normalized_end {
if normalized_point >= normalized_start {
normalized_point - normalized_start
} else {
TAU - normalized_start + normalized_point
}
} else {
let dist_to_start = (normalized_point - normalized_start)
.abs()
.min(TAU - (normalized_point - normalized_start).abs());
let dist_to_end = (normalized_point - normalized_end)
.abs()
.min(TAU - (normalized_point - normalized_end).abs());
return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
}
};
point_arc_length / arc_length
}
pub fn arc_arc_intersections(
arc1_center: Coords2d,
arc1_start: Coords2d,
arc1_end: Coords2d,
arc2_center: Coords2d,
arc2_start: Coords2d,
arc2_end: Coords2d,
epsilon: f64,
) -> Vec<Coords2d> {
let r1 = ((arc1_start.x - arc1_center.x) * (arc1_start.x - arc1_center.x)
+ (arc1_start.y - arc1_center.y) * (arc1_start.y - arc1_center.y))
.sqrt();
let r2 = ((arc2_start.x - arc2_center.x) * (arc2_start.x - arc2_center.x)
+ (arc2_start.y - arc2_center.y) * (arc2_start.y - arc2_center.y))
.sqrt();
let dx = arc2_center.x - arc1_center.x;
let dy = arc2_center.y - arc1_center.y;
let d = (dx * dx + dy * dy).sqrt();
if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon {
return Vec::new();
}
if d < EPSILON_PARALLEL {
return Vec::new();
}
let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
let h_sq = r1 * r1 - a * a;
if h_sq < 0.0 {
return Vec::new();
}
let h = h_sq.sqrt();
if h.is_nan() {
return Vec::new();
}
let ux = dx / d;
let uy = dy / d;
let px = -uy;
let py = ux;
let mid_point = Coords2d {
x: arc1_center.x + a * ux,
y: arc1_center.y + a * uy,
};
let intersection1 = Coords2d {
x: mid_point.x + h * px,
y: mid_point.y + h * py,
};
let intersection2 = Coords2d {
x: mid_point.x - h * px,
y: mid_point.y - h * py,
};
let mut candidates: Vec<Coords2d> = Vec::new();
if is_point_on_arc(intersection1, arc1_center, arc1_start, arc1_end, epsilon)
&& is_point_on_arc(intersection1, arc2_center, arc2_start, arc2_end, epsilon)
{
candidates.push(intersection1);
}
if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
if is_point_on_arc(intersection2, arc1_center, arc1_start, arc1_end, epsilon)
&& is_point_on_arc(intersection2, arc2_center, arc2_start, arc2_end, epsilon)
{
candidates.push(intersection2);
}
}
candidates
}
pub fn arc_arc_intersection(
arc1_center: Coords2d,
arc1_start: Coords2d,
arc1_end: Coords2d,
arc2_center: Coords2d,
arc2_start: Coords2d,
arc2_end: Coords2d,
epsilon: f64,
) -> Option<Coords2d> {
arc_arc_intersections(
arc1_center,
arc1_start,
arc1_end,
arc2_center,
arc2_start,
arc2_end,
epsilon,
)
.first()
.copied()
}
pub fn circle_arc_intersections(
circle_center: Coords2d,
circle_radius: f64,
arc_center: Coords2d,
arc_start: Coords2d,
arc_end: Coords2d,
epsilon: f64,
) -> Vec<Coords2d> {
let r1 = circle_radius;
let r2 = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
+ (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
.sqrt();
let dx = arc_center.x - circle_center.x;
let dy = arc_center.y - circle_center.y;
let d = (dx * dx + dy * dy).sqrt();
if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon || d < EPSILON_PARALLEL {
return Vec::new();
}
let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
let h_sq = r1 * r1 - a * a;
if h_sq < 0.0 {
return Vec::new();
}
let h = h_sq.sqrt();
if h.is_nan() {
return Vec::new();
}
let ux = dx / d;
let uy = dy / d;
let px = -uy;
let py = ux;
let mid_point = Coords2d {
x: circle_center.x + a * ux,
y: circle_center.y + a * uy,
};
let intersection1 = Coords2d {
x: mid_point.x + h * px,
y: mid_point.y + h * py,
};
let intersection2 = Coords2d {
x: mid_point.x - h * px,
y: mid_point.y - h * py,
};
let mut intersections = Vec::new();
if is_point_on_arc(intersection1, arc_center, arc_start, arc_end, epsilon) {
intersections.push(intersection1);
}
if ((intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon)
&& is_point_on_arc(intersection2, arc_center, arc_start, arc_end, epsilon)
{
intersections.push(intersection2);
}
intersections
}
pub fn circle_circle_intersections(
circle1_center: Coords2d,
circle1_radius: f64,
circle2_center: Coords2d,
circle2_radius: f64,
epsilon: f64,
) -> Vec<Coords2d> {
let dx = circle2_center.x - circle1_center.x;
let dy = circle2_center.y - circle1_center.y;
let d = (dx * dx + dy * dy).sqrt();
if d > circle1_radius + circle2_radius + epsilon
|| d < (circle1_radius - circle2_radius).abs() - epsilon
|| d < EPSILON_PARALLEL
{
return Vec::new();
}
let a = (circle1_radius * circle1_radius - circle2_radius * circle2_radius + d * d) / (2.0 * d);
let h_sq = circle1_radius * circle1_radius - a * a;
if h_sq < 0.0 {
return Vec::new();
}
let h = if h_sq <= epsilon { 0.0 } else { h_sq.sqrt() };
if h.is_nan() {
return Vec::new();
}
let ux = dx / d;
let uy = dy / d;
let px = -uy;
let py = ux;
let mid_point = Coords2d {
x: circle1_center.x + a * ux,
y: circle1_center.y + a * uy,
};
let intersection1 = Coords2d {
x: mid_point.x + h * px,
y: mid_point.y + h * py,
};
let intersection2 = Coords2d {
x: mid_point.x - h * px,
y: mid_point.y - h * py,
};
let mut intersections = vec![intersection1];
if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
intersections.push(intersection2);
}
intersections
}
fn get_point_coords_from_native(objects: &[Object], point_id: ObjectId, default_unit: UnitLength) -> Option<Coords2d> {
let point_obj = objects.get(point_id.0)?;
let ObjectKind::Segment { segment } = &point_obj.kind else {
return None;
};
let Segment::Point(point) = segment else {
return None;
};
Some(Coords2d {
x: number_to_unit(&point.position.x, default_unit),
y: number_to_unit(&point.position.y, default_unit),
})
}
pub fn get_position_coords_for_line(
segment_obj: &Object,
which: LineEndpoint,
objects: &[Object],
default_unit: UnitLength,
) -> Option<Coords2d> {
let ObjectKind::Segment { segment } = &segment_obj.kind else {
return None;
};
let Segment::Line(line) = segment else {
return None;
};
let point_id = match which {
LineEndpoint::Start => line.start,
LineEndpoint::End => line.end,
};
get_point_coords_from_native(objects, point_id, default_unit)
}
fn is_point_coincident_with_segment_native(point_id: ObjectId, segment_id: ObjectId, objects: &[Object]) -> bool {
for obj in objects {
let ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let Constraint::Coincident(coincident) = constraint else {
continue;
};
let has_point = coincident.contains_segment(point_id);
let has_segment = coincident.contains_segment(segment_id);
if has_point && has_segment {
return true;
}
}
false
}
pub fn get_position_coords_from_arc(
segment_obj: &Object,
which: ArcPoint,
objects: &[Object],
default_unit: UnitLength,
) -> Option<Coords2d> {
let ObjectKind::Segment { segment } = &segment_obj.kind else {
return None;
};
let Segment::Arc(arc) = segment else {
return None;
};
let point_id = match which {
ArcPoint::Start => arc.start,
ArcPoint::End => arc.end,
ArcPoint::Center => arc.center,
};
get_point_coords_from_native(objects, point_id, default_unit)
}
pub fn get_position_coords_from_circle(
segment_obj: &Object,
which: CirclePoint,
objects: &[Object],
default_unit: UnitLength,
) -> Option<Coords2d> {
let ObjectKind::Segment { segment } = &segment_obj.kind else {
return None;
};
let Segment::Circle(circle) = segment else {
return None;
};
let point_id = match which {
CirclePoint::Start => circle.start,
CirclePoint::Center => circle.center,
};
get_point_coords_from_native(objects, point_id, default_unit)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CurveKind {
Line,
Circular,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CurveDomain {
Open,
Closed,
}
#[derive(Debug, Clone, Copy)]
struct CurveHandle {
segment_id: ObjectId,
kind: CurveKind,
domain: CurveDomain,
start: Coords2d,
end: Coords2d,
center: Option<Coords2d>,
radius: Option<f64>,
}
impl CurveHandle {
fn project_for_trim(self, point: Coords2d) -> Result<f64, String> {
match (self.kind, self.domain) {
(CurveKind::Line, CurveDomain::Open) => Ok(project_point_onto_segment(point, self.start, self.end)),
(CurveKind::Circular, CurveDomain::Open) => {
let center = self
.center
.ok_or_else(|| format!("Curve {} missing center for arc projection", self.segment_id.0))?;
Ok(project_point_onto_arc(point, center, self.start, self.end))
}
(CurveKind::Circular, CurveDomain::Closed) => {
let center = self
.center
.ok_or_else(|| format!("Curve {} missing center for circle projection", self.segment_id.0))?;
Ok(project_point_onto_circle(point, center, self.start))
}
(CurveKind::Line, CurveDomain::Closed) => Err(format!(
"Invalid curve state: line {} cannot be closed",
self.segment_id.0
)),
}
}
}
fn load_curve_handle(
segment_obj: &Object,
objects: &[Object],
default_unit: UnitLength,
) -> Result<CurveHandle, String> {
let ObjectKind::Segment { segment } = &segment_obj.kind else {
return Err("Object is not a segment".to_owned());
};
match segment {
Segment::Line(_) => {
let start = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit)
.ok_or_else(|| format!("Could not get line start for segment {}", segment_obj.id.0))?;
let end = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit)
.ok_or_else(|| format!("Could not get line end for segment {}", segment_obj.id.0))?;
Ok(CurveHandle {
segment_id: segment_obj.id,
kind: CurveKind::Line,
domain: CurveDomain::Open,
start,
end,
center: None,
radius: None,
})
}
Segment::Arc(_) => {
let start = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit)
.ok_or_else(|| format!("Could not get arc start for segment {}", segment_obj.id.0))?;
let end = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit)
.ok_or_else(|| format!("Could not get arc end for segment {}", segment_obj.id.0))?;
let center = get_position_coords_from_arc(segment_obj, ArcPoint::Center, objects, default_unit)
.ok_or_else(|| format!("Could not get arc center for segment {}", segment_obj.id.0))?;
let radius =
((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
Ok(CurveHandle {
segment_id: segment_obj.id,
kind: CurveKind::Circular,
domain: CurveDomain::Open,
start,
end,
center: Some(center),
radius: Some(radius),
})
}
Segment::Circle(_) => {
let start = get_position_coords_from_circle(segment_obj, CirclePoint::Start, objects, default_unit)
.ok_or_else(|| format!("Could not get circle start for segment {}", segment_obj.id.0))?;
let center = get_position_coords_from_circle(segment_obj, CirclePoint::Center, objects, default_unit)
.ok_or_else(|| format!("Could not get circle center for segment {}", segment_obj.id.0))?;
let radius =
((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
Ok(CurveHandle {
segment_id: segment_obj.id,
kind: CurveKind::Circular,
domain: CurveDomain::Closed,
start,
end: start,
center: Some(center),
radius: Some(radius),
})
}
Segment::Point(_) => Err(format!(
"Point segment {} cannot be used as trim curve",
segment_obj.id.0
)),
}
}
fn project_point_onto_curve(curve: CurveHandle, point: Coords2d) -> Result<f64, String> {
curve.project_for_trim(point)
}
fn curve_contains_point(curve: CurveHandle, point: Coords2d, epsilon: f64) -> bool {
match (curve.kind, curve.domain) {
(CurveKind::Line, CurveDomain::Open) => {
let t = project_point_onto_segment(point, curve.start, curve.end);
(0.0..=1.0).contains(&t) && perpendicular_distance_to_segment(point, curve.start, curve.end) <= epsilon
}
(CurveKind::Circular, CurveDomain::Open) => curve
.center
.is_some_and(|center| is_point_on_arc(point, center, curve.start, curve.end, epsilon)),
(CurveKind::Circular, CurveDomain::Closed) => curve.center.is_some_and(|center| {
let radius = curve
.radius
.unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
is_point_on_circle(point, center, radius, epsilon)
}),
(CurveKind::Line, CurveDomain::Closed) => false,
}
}
fn curve_line_segment_intersections(
curve: CurveHandle,
line_start: Coords2d,
line_end: Coords2d,
epsilon: f64,
) -> Vec<(f64, Coords2d)> {
match (curve.kind, curve.domain) {
(CurveKind::Line, CurveDomain::Open) => {
line_segment_intersection(line_start, line_end, curve.start, curve.end, epsilon)
.map(|intersection| {
(
project_point_onto_segment(intersection, line_start, line_end),
intersection,
)
})
.into_iter()
.collect()
}
(CurveKind::Circular, CurveDomain::Open) => curve
.center
.and_then(|center| line_arc_intersection(line_start, line_end, center, curve.start, curve.end, epsilon))
.map(|intersection| {
(
project_point_onto_segment(intersection, line_start, line_end),
intersection,
)
})
.into_iter()
.collect(),
(CurveKind::Circular, CurveDomain::Closed) => {
let Some(center) = curve.center else {
return Vec::new();
};
let radius = curve
.radius
.unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
line_circle_intersections(line_start, line_end, center, radius, epsilon)
}
(CurveKind::Line, CurveDomain::Closed) => Vec::new(),
}
}
fn curve_polyline_intersections(curve: CurveHandle, polyline: &[Coords2d], epsilon: f64) -> Vec<(Coords2d, usize)> {
let mut intersections = Vec::new();
for i in 0..polyline.len().saturating_sub(1) {
let p1 = polyline[i];
let p2 = polyline[i + 1];
for (_, intersection) in curve_line_segment_intersections(curve, p1, p2, epsilon) {
intersections.push((intersection, i));
}
}
intersections
}
fn curve_curve_intersections(curve: CurveHandle, other: CurveHandle, epsilon: f64) -> Vec<Coords2d> {
match (curve.kind, curve.domain, other.kind, other.domain) {
(CurveKind::Line, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => {
line_segment_intersection(curve.start, curve.end, other.start, other.end, epsilon)
.into_iter()
.collect()
}
(CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => other
.center
.and_then(|other_center| {
line_arc_intersection(curve.start, curve.end, other_center, other.start, other.end, epsilon)
})
.into_iter()
.collect(),
(CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
let Some(other_center) = other.center else {
return Vec::new();
};
let other_radius = other.radius.unwrap_or_else(|| {
((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
});
line_circle_intersections(curve.start, curve.end, other_center, other_radius, epsilon)
.into_iter()
.map(|(_, point)| point)
.collect()
}
(CurveKind::Circular, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => curve
.center
.and_then(|curve_center| {
line_arc_intersection(other.start, other.end, curve_center, curve.start, curve.end, epsilon)
})
.into_iter()
.collect(),
(CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => {
let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
return Vec::new();
};
arc_arc_intersections(
curve_center,
curve.start,
curve.end,
other_center,
other.start,
other.end,
epsilon,
)
}
(CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
return Vec::new();
};
let other_radius = other.radius.unwrap_or_else(|| {
((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
});
circle_arc_intersections(
other_center,
other_radius,
curve_center,
curve.start,
curve.end,
epsilon,
)
}
(CurveKind::Circular, CurveDomain::Closed, CurveKind::Line, CurveDomain::Open) => {
let Some(curve_center) = curve.center else {
return Vec::new();
};
let curve_radius = curve.radius.unwrap_or_else(|| {
((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
});
line_circle_intersections(other.start, other.end, curve_center, curve_radius, epsilon)
.into_iter()
.map(|(_, point)| point)
.collect()
}
(CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Open) => {
let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
return Vec::new();
};
let curve_radius = curve.radius.unwrap_or_else(|| {
((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
});
circle_arc_intersections(
curve_center,
curve_radius,
other_center,
other.start,
other.end,
epsilon,
)
}
(CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Closed) => {
let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
return Vec::new();
};
let curve_radius = curve.radius.unwrap_or_else(|| {
((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
});
let other_radius = other.radius.unwrap_or_else(|| {
((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
});
circle_circle_intersections(curve_center, curve_radius, other_center, other_radius, epsilon)
}
_ => Vec::new(),
}
}
fn segment_endpoint_points(
segment_obj: &Object,
objects: &[Object],
default_unit: UnitLength,
) -> Vec<(ObjectId, Coords2d)> {
let ObjectKind::Segment { segment } = &segment_obj.kind else {
return Vec::new();
};
match segment {
Segment::Line(line) => {
let mut points = Vec::new();
if let Some(start) = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit) {
points.push((line.start, start));
}
if let Some(end) = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit) {
points.push((line.end, end));
}
points
}
Segment::Arc(arc) => {
let mut points = Vec::new();
if let Some(start) = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit) {
points.push((arc.start, start));
}
if let Some(end) = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit) {
points.push((arc.end, end));
}
points
}
_ => Vec::new(),
}
}
pub fn get_next_trim_spawn(
points: &[Coords2d],
start_index: usize,
objects: &[Object],
default_unit: UnitLength,
) -> TrimItem {
let scene_curves: Vec<CurveHandle> = objects
.iter()
.filter_map(|obj| load_curve_handle(obj, objects, default_unit).ok())
.collect();
for i in start_index..points.len().saturating_sub(1) {
let p1 = points[i];
let p2 = points[i + 1];
for curve in &scene_curves {
let intersections = curve_line_segment_intersections(*curve, p1, p2, EPSILON_POINT_ON_SEGMENT);
if let Some((_, intersection)) = intersections.first() {
return TrimItem::Spawn {
trim_spawn_seg_id: curve.segment_id,
trim_spawn_coords: *intersection,
next_index: i,
};
}
}
}
TrimItem::None {
next_index: points.len().saturating_sub(1),
}
}
pub fn get_trim_spawn_terminations(
trim_spawn_seg_id: ObjectId,
trim_spawn_coords: &[Coords2d],
objects: &[Object],
default_unit: UnitLength,
) -> Result<TrimTerminations, String> {
let trim_spawn_seg = objects.iter().find(|obj| obj.id == trim_spawn_seg_id);
let trim_spawn_seg = match trim_spawn_seg {
Some(seg) => seg,
None => {
return Err(format!("Trim spawn segment {} not found", trim_spawn_seg_id.0));
}
};
let trim_curve = load_curve_handle(trim_spawn_seg, objects, default_unit).map_err(|e| {
format!(
"Failed to load trim spawn segment {} as normalized curve: {}",
trim_spawn_seg_id.0, e
)
})?;
let all_intersections = curve_polyline_intersections(trim_curve, trim_spawn_coords, EPSILON_POINT_ON_SEGMENT);
let intersection_point = if all_intersections.is_empty() {
return Err("Could not find intersection point between polyline and trim spawn segment".to_string());
} else {
let mid_index = (trim_spawn_coords.len() - 1) / 2;
let mid_point = trim_spawn_coords[mid_index];
let mut min_dist = f64::INFINITY;
let mut closest_intersection = all_intersections[0].0;
for (intersection, _) in &all_intersections {
let dist = ((intersection.x - mid_point.x) * (intersection.x - mid_point.x)
+ (intersection.y - mid_point.y) * (intersection.y - mid_point.y))
.sqrt();
if dist < min_dist {
min_dist = dist;
closest_intersection = *intersection;
}
}
closest_intersection
};
let intersection_t = project_point_onto_curve(trim_curve, intersection_point)?;
let left_termination = find_termination_in_direction(
trim_spawn_seg,
trim_curve,
intersection_t,
TrimDirection::Left,
objects,
default_unit,
)?;
let right_termination = find_termination_in_direction(
trim_spawn_seg,
trim_curve,
intersection_t,
TrimDirection::Right,
objects,
default_unit,
)?;
Ok(TrimTerminations {
left_side: left_termination,
right_side: right_termination,
})
}
fn find_termination_in_direction(
trim_spawn_seg: &Object,
trim_curve: CurveHandle,
intersection_t: f64,
direction: TrimDirection,
objects: &[Object],
default_unit: UnitLength,
) -> Result<TrimTermination, String> {
let ObjectKind::Segment { segment } = &trim_spawn_seg.kind else {
return Err("Trim spawn segment is not a segment".to_string());
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum CandidateType {
Intersection,
Coincident,
Endpoint,
}
#[derive(Debug, Clone)]
struct Candidate {
t: f64,
point: Coords2d,
candidate_type: CandidateType,
segment_id: Option<ObjectId>,
point_id: Option<ObjectId>,
}
let mut candidates: Vec<Candidate> = Vec::new();
match segment {
Segment::Line(line) => {
candidates.push(Candidate {
t: 0.0,
point: trim_curve.start,
candidate_type: CandidateType::Endpoint,
segment_id: None,
point_id: Some(line.start),
});
candidates.push(Candidate {
t: 1.0,
point: trim_curve.end,
candidate_type: CandidateType::Endpoint,
segment_id: None,
point_id: Some(line.end),
});
}
Segment::Arc(arc) => {
candidates.push(Candidate {
t: 0.0,
point: trim_curve.start,
candidate_type: CandidateType::Endpoint,
segment_id: None,
point_id: Some(arc.start),
});
candidates.push(Candidate {
t: 1.0,
point: trim_curve.end,
candidate_type: CandidateType::Endpoint,
segment_id: None,
point_id: Some(arc.end),
});
}
Segment::Circle(_) => {
}
_ => {}
}
let trim_spawn_seg_id = trim_spawn_seg.id;
for other_seg in objects.iter() {
let other_id = other_seg.id;
if other_id == trim_spawn_seg_id {
continue;
}
if let Ok(other_curve) = load_curve_handle(other_seg, objects, default_unit) {
for intersection in curve_curve_intersections(trim_curve, other_curve, EPSILON_POINT_ON_SEGMENT) {
let Ok(t) = project_point_onto_curve(trim_curve, intersection) else {
continue;
};
candidates.push(Candidate {
t,
point: intersection,
candidate_type: CandidateType::Intersection,
segment_id: Some(other_id),
point_id: None,
});
}
}
for (other_point_id, other_point) in segment_endpoint_points(other_seg, objects, default_unit) {
if !is_point_coincident_with_segment_native(other_point_id, trim_spawn_seg_id, objects) {
continue;
}
if !curve_contains_point(trim_curve, other_point, EPSILON_POINT_ON_SEGMENT) {
continue;
}
let Ok(t) = project_point_onto_curve(trim_curve, other_point) else {
continue;
};
candidates.push(Candidate {
t,
point: other_point,
candidate_type: CandidateType::Coincident,
segment_id: Some(other_id),
point_id: Some(other_point_id),
});
}
}
let is_circle_segment = trim_curve.domain == CurveDomain::Closed;
let intersection_epsilon = EPSILON_POINT_ON_SEGMENT * 10.0; let direction_distance = |candidate_t: f64| -> f64 {
if is_circle_segment {
match direction {
TrimDirection::Left => (intersection_t - candidate_t).rem_euclid(1.0),
TrimDirection::Right => (candidate_t - intersection_t).rem_euclid(1.0),
}
} else {
(candidate_t - intersection_t).abs()
}
};
let filtered_candidates: Vec<Candidate> = candidates
.into_iter()
.filter(|candidate| {
let dist_from_intersection = if is_circle_segment {
let ccw = (candidate.t - intersection_t).rem_euclid(1.0);
let cw = (intersection_t - candidate.t).rem_euclid(1.0);
ccw.min(cw)
} else {
(candidate.t - intersection_t).abs()
};
if dist_from_intersection < intersection_epsilon {
return false; }
if is_circle_segment {
direction_distance(candidate.t) > intersection_epsilon
} else {
match direction {
TrimDirection::Left => candidate.t < intersection_t,
TrimDirection::Right => candidate.t > intersection_t,
}
}
})
.collect();
let mut sorted_candidates = filtered_candidates;
sorted_candidates.sort_by(|a, b| {
let dist_a = direction_distance(a.t);
let dist_b = direction_distance(b.t);
let dist_diff = dist_a - dist_b;
if dist_diff.abs() > EPSILON_POINT_ON_SEGMENT {
dist_diff.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal)
} else {
let type_priority = |candidate_type: CandidateType| -> i32 {
match candidate_type {
CandidateType::Coincident => 0,
CandidateType::Intersection => 1,
CandidateType::Endpoint => 2,
}
};
type_priority(a.candidate_type).cmp(&type_priority(b.candidate_type))
}
});
let closest_candidate = match sorted_candidates.first() {
Some(c) => c,
None => {
if is_circle_segment {
return Err("No trim termination candidate found for circle".to_string());
}
let endpoint = match direction {
TrimDirection::Left => trim_curve.start,
TrimDirection::Right => trim_curve.end,
};
return Ok(TrimTermination::SegEndPoint {
trim_termination_coords: endpoint,
});
}
};
if !is_circle_segment
&& closest_candidate.candidate_type == CandidateType::Intersection
&& let Some(seg_id) = closest_candidate.segment_id
{
let intersecting_seg = objects.iter().find(|obj| obj.id == seg_id);
if let Some(intersecting_seg) = intersecting_seg {
let endpoint_epsilon = EPSILON_POINT_ON_SEGMENT * 1000.0; let is_other_seg_endpoint = segment_endpoint_points(intersecting_seg, objects, default_unit)
.into_iter()
.any(|(_, endpoint)| {
let dist_to_endpoint = ((closest_candidate.point.x - endpoint.x).powi(2)
+ (closest_candidate.point.y - endpoint.y).powi(2))
.sqrt();
dist_to_endpoint < endpoint_epsilon
});
if is_other_seg_endpoint {
let endpoint = match direction {
TrimDirection::Left => trim_curve.start,
TrimDirection::Right => trim_curve.end,
};
return Ok(TrimTermination::SegEndPoint {
trim_termination_coords: endpoint,
});
}
}
let endpoint_t = match direction {
TrimDirection::Left => 0.0,
TrimDirection::Right => 1.0,
};
let endpoint = match direction {
TrimDirection::Left => trim_curve.start,
TrimDirection::Right => trim_curve.end,
};
let dist_to_endpoint_param = (closest_candidate.t - endpoint_t).abs();
let dist_to_endpoint_coords = ((closest_candidate.point.x - endpoint.x)
* (closest_candidate.point.x - endpoint.x)
+ (closest_candidate.point.y - endpoint.y) * (closest_candidate.point.y - endpoint.y))
.sqrt();
let is_at_endpoint =
dist_to_endpoint_param < EPSILON_POINT_ON_SEGMENT || dist_to_endpoint_coords < EPSILON_POINT_ON_SEGMENT;
if is_at_endpoint {
return Ok(TrimTermination::SegEndPoint {
trim_termination_coords: endpoint,
});
}
}
let endpoint_t_for_return = match direction {
TrimDirection::Left => 0.0,
TrimDirection::Right => 1.0,
};
if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Intersection {
let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
let endpoint = match direction {
TrimDirection::Left => trim_curve.start,
TrimDirection::Right => trim_curve.end,
};
return Ok(TrimTermination::SegEndPoint {
trim_termination_coords: endpoint,
});
}
}
let endpoint = match direction {
TrimDirection::Left => trim_curve.start,
TrimDirection::Right => trim_curve.end,
};
if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Endpoint {
let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
return Ok(TrimTermination::SegEndPoint {
trim_termination_coords: endpoint,
});
}
}
if closest_candidate.candidate_type == CandidateType::Coincident {
Ok(TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
trim_termination_coords: closest_candidate.point,
intersecting_seg_id: closest_candidate
.segment_id
.ok_or_else(|| "Missing segment_id for coincident".to_string())?,
other_segment_point_id: closest_candidate
.point_id
.ok_or_else(|| "Missing point_id for coincident".to_string())?,
})
} else if closest_candidate.candidate_type == CandidateType::Intersection {
Ok(TrimTermination::Intersection {
trim_termination_coords: closest_candidate.point,
intersecting_seg_id: closest_candidate
.segment_id
.ok_or_else(|| "Missing segment_id for intersection".to_string())?,
})
} else {
if is_circle_segment {
return Err("Circle trim termination unexpectedly resolved to endpoint".to_string());
}
Ok(TrimTermination::SegEndPoint {
trim_termination_coords: closest_candidate.point,
})
}
}
#[cfg(test)]
#[allow(dead_code)]
pub(crate) async fn execute_trim_loop<F, Fut>(
points: &[Coords2d],
default_unit: UnitLength,
initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
mut execute_operations: F,
) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>
where
F: FnMut(Vec<TrimOperation>, crate::frontend::api::SceneGraphDelta) -> Fut,
Fut: std::future::Future<
Output = Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>,
>,
{
let normalized_points = normalize_trim_points_to_unit(points, default_unit);
let points = normalized_points.as_slice();
let mut start_index = 0;
let max_iterations = 1000;
let mut iteration_count = 0;
let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
crate::frontend::api::SourceDelta { text: String::new() },
initial_scene_graph_delta.clone(),
));
let mut invalidates_ids = false;
let mut current_scene_graph_delta = initial_scene_graph_delta;
let circle_delete_fallback_strategy =
|error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
if !error.contains("No trim termination candidate found for circle") {
return None;
}
let is_circle = scene_objects
.iter()
.find(|obj| obj.id == segment_id)
.is_some_and(|obj| {
matches!(
obj.kind,
ObjectKind::Segment {
segment: Segment::Circle(_)
}
)
});
if is_circle {
Some(vec![TrimOperation::SimpleTrim {
segment_to_trim_id: segment_id,
}])
} else {
None
}
};
while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
iteration_count += 1;
let next_trim_spawn = get_next_trim_spawn(
points,
start_index,
¤t_scene_graph_delta.new_graph.objects,
default_unit,
);
match &next_trim_spawn {
TrimItem::None { next_index } => {
let old_start_index = start_index;
start_index = *next_index;
if start_index <= old_start_index {
start_index = old_start_index + 1;
}
if start_index >= points.len().saturating_sub(1) {
break;
}
continue;
}
TrimItem::Spawn {
trim_spawn_seg_id,
trim_spawn_coords,
next_index,
..
} => {
let terminations = match get_trim_spawn_terminations(
*trim_spawn_seg_id,
points,
¤t_scene_graph_delta.new_graph.objects,
default_unit,
) {
Ok(terms) => terms,
Err(e) => {
crate::logln!("Error getting trim spawn terminations: {}", e);
if let Some(strategy) = circle_delete_fallback_strategy(
&e,
*trim_spawn_seg_id,
¤t_scene_graph_delta.new_graph.objects,
) {
match execute_operations(strategy, current_scene_graph_delta.clone()).await {
Ok((source_delta, scene_graph_delta)) => {
last_result = Some((source_delta, scene_graph_delta.clone()));
invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
current_scene_graph_delta = scene_graph_delta;
}
Err(exec_err) => {
crate::logln!(
"Error executing circle-delete fallback trim operation: {}",
exec_err
);
}
}
let old_start_index = start_index;
start_index = *next_index;
if start_index <= old_start_index {
start_index = old_start_index + 1;
}
continue;
}
let old_start_index = start_index;
start_index = *next_index;
if start_index <= old_start_index {
start_index = old_start_index + 1;
}
continue;
}
};
let trim_spawn_segment = current_scene_graph_delta
.new_graph
.objects
.iter()
.find(|obj| obj.id == *trim_spawn_seg_id)
.ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
let plan = match build_trim_plan(
*trim_spawn_seg_id,
*trim_spawn_coords,
trim_spawn_segment,
&terminations.left_side,
&terminations.right_side,
¤t_scene_graph_delta.new_graph.objects,
default_unit,
) {
Ok(plan) => plan,
Err(e) => {
crate::logln!("Error determining trim strategy: {}", e);
let old_start_index = start_index;
start_index = *next_index;
if start_index <= old_start_index {
start_index = old_start_index + 1;
}
continue;
}
};
let strategy = lower_trim_plan(&plan);
let geometry_was_modified = trim_plan_modifies_geometry(&plan);
match execute_operations(strategy, current_scene_graph_delta.clone()).await {
Ok((source_delta, scene_graph_delta)) => {
last_result = Some((source_delta, scene_graph_delta.clone()));
invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
current_scene_graph_delta = scene_graph_delta;
}
Err(e) => {
crate::logln!("Error executing trim operations: {}", e);
}
}
let old_start_index = start_index;
start_index = *next_index;
if start_index <= old_start_index && !geometry_was_modified {
start_index = old_start_index + 1;
}
}
}
}
if iteration_count >= max_iterations {
return Err(format!("Reached max iterations ({})", max_iterations));
}
last_result.ok_or_else(|| "No trim operations were executed".to_string())
}
#[cfg(all(feature = "artifact-graph", test))]
#[derive(Debug, Clone)]
pub struct TrimFlowResult {
pub kcl_code: String,
pub invalidates_ids: bool,
}
#[cfg(all(not(target_arch = "wasm32"), feature = "artifact-graph", test))]
pub(crate) async fn execute_trim_flow(
kcl_code: &str,
trim_points: &[Coords2d],
sketch_id: ObjectId,
) -> Result<TrimFlowResult, String> {
use crate::ExecutorContext;
use crate::Program;
use crate::execution::MockConfig;
use crate::frontend::FrontendState;
use crate::frontend::api::Version;
let parse_result = Program::parse(kcl_code).map_err(|e| format!("Failed to parse KCL: {}", e))?;
let (program_opt, errors) = parse_result;
if !errors.is_empty() {
return Err(format!("Failed to parse KCL: {:?}", errors));
}
let program = program_opt.ok_or_else(|| "No AST produced".to_string())?;
let mock_ctx = ExecutorContext::new_mock(None).await;
let result = async {
let mut frontend = FrontendState::new();
frontend.program = program.clone();
let exec_outcome = mock_ctx
.run_mock(&program, &MockConfig::default())
.await
.map_err(|e| format!("Failed to execute program: {}", e.error.message()))?;
let exec_outcome = frontend.update_state_after_exec(exec_outcome, false);
#[allow(unused_mut)] let mut initial_scene_graph = frontend.scene_graph.clone();
#[cfg(feature = "artifact-graph")]
if initial_scene_graph.objects.is_empty() && !exec_outcome.scene_objects.is_empty() {
initial_scene_graph.objects = exec_outcome.scene_objects.clone();
}
let actual_sketch_id = if let Some(sketch_mode) = initial_scene_graph.sketch_mode {
sketch_mode
} else {
initial_scene_graph
.objects
.iter()
.find(|obj| matches!(obj.kind, crate::frontend::api::ObjectKind::Sketch { .. }))
.map(|obj| obj.id)
.unwrap_or(sketch_id) };
let version = Version(0);
let initial_scene_graph_delta = crate::frontend::api::SceneGraphDelta {
new_graph: initial_scene_graph,
new_objects: vec![],
invalidates_ids: false,
exec_outcome,
};
let (source_delta, scene_graph_delta) = execute_trim_loop_with_context(
trim_points,
initial_scene_graph_delta,
&mut frontend,
&mock_ctx,
version,
actual_sketch_id,
)
.await?;
if source_delta.text.is_empty() {
return Err("No trim operations were executed - source delta is empty".to_string());
}
Ok(TrimFlowResult {
kcl_code: source_delta.text,
invalidates_ids: scene_graph_delta.invalidates_ids,
})
}
.await;
mock_ctx.close().await;
result
}
pub async fn execute_trim_loop_with_context(
points: &[Coords2d],
initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
frontend: &mut crate::frontend::FrontendState,
ctx: &crate::ExecutorContext,
version: crate::frontend::api::Version,
sketch_id: ObjectId,
) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
let default_unit = frontend.default_length_unit();
let normalized_points = normalize_trim_points_to_unit(points, default_unit);
let mut current_scene_graph_delta = initial_scene_graph_delta.clone();
let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
crate::frontend::api::SourceDelta { text: String::new() },
initial_scene_graph_delta.clone(),
));
let mut invalidates_ids = false;
let mut start_index = 0;
let max_iterations = 1000;
let mut iteration_count = 0;
let circle_delete_fallback_strategy =
|error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
if !error.contains("No trim termination candidate found for circle") {
return None;
}
let is_circle = scene_objects
.iter()
.find(|obj| obj.id == segment_id)
.is_some_and(|obj| {
matches!(
obj.kind,
ObjectKind::Segment {
segment: Segment::Circle(_)
}
)
});
if is_circle {
Some(vec![TrimOperation::SimpleTrim {
segment_to_trim_id: segment_id,
}])
} else {
None
}
};
let points = normalized_points.as_slice();
while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
iteration_count += 1;
let next_trim_spawn = get_next_trim_spawn(
points,
start_index,
¤t_scene_graph_delta.new_graph.objects,
default_unit,
);
match &next_trim_spawn {
TrimItem::None { next_index } => {
let old_start_index = start_index;
start_index = *next_index;
if start_index <= old_start_index {
start_index = old_start_index + 1;
}
if start_index >= points.len().saturating_sub(1) {
break;
}
continue;
}
TrimItem::Spawn {
trim_spawn_seg_id,
trim_spawn_coords,
next_index,
..
} => {
let terminations = match get_trim_spawn_terminations(
*trim_spawn_seg_id,
points,
¤t_scene_graph_delta.new_graph.objects,
default_unit,
) {
Ok(terms) => terms,
Err(e) => {
crate::logln!("Error getting trim spawn terminations: {}", e);
if let Some(strategy) = circle_delete_fallback_strategy(
&e,
*trim_spawn_seg_id,
¤t_scene_graph_delta.new_graph.objects,
) {
match execute_trim_operations_simple(
strategy.clone(),
¤t_scene_graph_delta,
frontend,
ctx,
version,
sketch_id,
)
.await
{
Ok((source_delta, scene_graph_delta)) => {
invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
last_result = Some((source_delta, scene_graph_delta.clone()));
current_scene_graph_delta = scene_graph_delta;
}
Err(exec_err) => {
crate::logln!(
"Error executing circle-delete fallback trim operation: {}",
exec_err
);
}
}
let old_start_index = start_index;
start_index = *next_index;
if start_index <= old_start_index {
start_index = old_start_index + 1;
}
continue;
}
let old_start_index = start_index;
start_index = *next_index;
if start_index <= old_start_index {
start_index = old_start_index + 1;
}
continue;
}
};
let trim_spawn_segment = current_scene_graph_delta
.new_graph
.objects
.iter()
.find(|obj| obj.id == *trim_spawn_seg_id)
.ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
let plan = match build_trim_plan(
*trim_spawn_seg_id,
*trim_spawn_coords,
trim_spawn_segment,
&terminations.left_side,
&terminations.right_side,
¤t_scene_graph_delta.new_graph.objects,
default_unit,
) {
Ok(plan) => plan,
Err(e) => {
crate::logln!("Error determining trim strategy: {}", e);
let old_start_index = start_index;
start_index = *next_index;
if start_index <= old_start_index {
start_index = old_start_index + 1;
}
continue;
}
};
let strategy = lower_trim_plan(&plan);
let geometry_was_modified = trim_plan_modifies_geometry(&plan);
match execute_trim_operations_simple(
strategy.clone(),
¤t_scene_graph_delta,
frontend,
ctx,
version,
sketch_id,
)
.await
{
Ok((source_delta, scene_graph_delta)) => {
invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
last_result = Some((source_delta, scene_graph_delta.clone()));
current_scene_graph_delta = scene_graph_delta;
}
Err(e) => {
crate::logln!("Error executing trim operations: {}", e);
}
}
let old_start_index = start_index;
start_index = *next_index;
if start_index <= old_start_index && !geometry_was_modified {
start_index = old_start_index + 1;
}
}
}
}
if iteration_count >= max_iterations {
return Err(format!("Reached max iterations ({})", max_iterations));
}
let (source_delta, mut scene_graph_delta) =
last_result.ok_or_else(|| "No trim operations were executed".to_string())?;
scene_graph_delta.invalidates_ids = invalidates_ids;
Ok((source_delta, scene_graph_delta))
}
pub(crate) fn build_trim_plan(
trim_spawn_id: ObjectId,
trim_spawn_coords: Coords2d,
trim_spawn_segment: &Object,
left_side: &TrimTermination,
right_side: &TrimTermination,
objects: &[Object],
default_unit: UnitLength,
) -> Result<TrimPlan, String> {
if matches!(left_side, TrimTermination::SegEndPoint { .. })
&& matches!(right_side, TrimTermination::SegEndPoint { .. })
{
return Ok(TrimPlan::DeleteSegment {
segment_id: trim_spawn_id,
});
}
let is_intersect_or_coincident = |side: &TrimTermination| -> bool {
matches!(
side,
TrimTermination::Intersection { .. }
| TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
)
};
let left_side_needs_tail_cut = is_intersect_or_coincident(left_side) && !is_intersect_or_coincident(right_side);
let right_side_needs_tail_cut = is_intersect_or_coincident(right_side) && !is_intersect_or_coincident(left_side);
let ObjectKind::Segment { segment } = &trim_spawn_segment.kind else {
return Err("Trim spawn segment is not a segment".to_string());
};
let (_segment_type, ctor) = match segment {
Segment::Line(line) => ("Line", &line.ctor),
Segment::Arc(arc) => ("Arc", &arc.ctor),
Segment::Circle(circle) => ("Circle", &circle.ctor),
_ => {
return Err("Trim spawn segment is not a Line, Arc, or Circle".to_string());
}
};
let units = match ctor {
SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
_ => NumericSuffix::Mm,
},
SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
_ => NumericSuffix::Mm,
},
SegmentCtor::Circle(circle_ctor) => match &circle_ctor.start.x {
crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
_ => NumericSuffix::Mm,
},
_ => NumericSuffix::Mm,
};
let find_distance_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
let mut constraint_ids = Vec::new();
for obj in objects {
let ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let Constraint::Distance(distance) = constraint else {
continue;
};
let points_owned_by_segment: Vec<bool> = distance
.point_ids()
.map(|point_id| {
if let Some(point_obj) = objects.iter().find(|o| o.id == point_id)
&& let ObjectKind::Segment { segment } = &point_obj.kind
&& let Segment::Point(point) = segment
&& let Some(owner_id) = point.owner
{
return owner_id == segment_id;
}
false
})
.collect();
if points_owned_by_segment.len() == 2 && points_owned_by_segment.iter().all(|&owned| owned) {
constraint_ids.push(obj.id);
}
}
constraint_ids
};
let find_existing_point_segment_coincident =
|trim_seg_id: ObjectId, intersecting_seg_id: ObjectId| -> CoincidentData {
let lookup_by_point_id = |point_id: ObjectId| -> Option<CoincidentData> {
for obj in objects {
let ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let Constraint::Coincident(coincident) = constraint else {
continue;
};
let involves_trim_seg = coincident.segment_ids().any(|id| id == trim_seg_id || id == point_id);
let involves_point = coincident.contains_segment(point_id);
if involves_trim_seg && involves_point {
return Some(CoincidentData {
intersecting_seg_id,
intersecting_endpoint_point_id: Some(point_id),
existing_point_segment_constraint_id: Some(obj.id),
});
}
}
None
};
let trim_seg = objects.iter().find(|obj| obj.id == trim_seg_id);
let mut trim_endpoint_ids: Vec<ObjectId> = Vec::new();
if let Some(seg) = trim_seg
&& let ObjectKind::Segment { segment } = &seg.kind
{
match segment {
Segment::Line(line) => {
trim_endpoint_ids.push(line.start);
trim_endpoint_ids.push(line.end);
}
Segment::Arc(arc) => {
trim_endpoint_ids.push(arc.start);
trim_endpoint_ids.push(arc.end);
}
_ => {}
}
}
let intersecting_obj = objects.iter().find(|obj| obj.id == intersecting_seg_id);
if let Some(obj) = intersecting_obj
&& let ObjectKind::Segment { segment } = &obj.kind
&& let Segment::Point(_) = segment
&& let Some(found) = lookup_by_point_id(intersecting_seg_id)
{
return found;
}
let mut intersecting_endpoint_ids: Vec<ObjectId> = Vec::new();
if let Some(obj) = intersecting_obj
&& let ObjectKind::Segment { segment } = &obj.kind
{
match segment {
Segment::Line(line) => {
intersecting_endpoint_ids.push(line.start);
intersecting_endpoint_ids.push(line.end);
}
Segment::Arc(arc) => {
intersecting_endpoint_ids.push(arc.start);
intersecting_endpoint_ids.push(arc.end);
}
_ => {}
}
}
intersecting_endpoint_ids.push(intersecting_seg_id);
for obj in objects {
let ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let Constraint::Coincident(coincident) = constraint else {
continue;
};
let constraint_segment_ids: Vec<ObjectId> = coincident.get_segments();
let involves_trim_seg = constraint_segment_ids.contains(&trim_seg_id)
|| trim_endpoint_ids.iter().any(|&id| constraint_segment_ids.contains(&id));
if !involves_trim_seg {
continue;
}
if let Some(&intersecting_endpoint_id) = intersecting_endpoint_ids
.iter()
.find(|&&id| constraint_segment_ids.contains(&id))
{
return CoincidentData {
intersecting_seg_id,
intersecting_endpoint_point_id: Some(intersecting_endpoint_id),
existing_point_segment_constraint_id: Some(obj.id),
};
}
}
CoincidentData {
intersecting_seg_id,
intersecting_endpoint_point_id: None,
existing_point_segment_constraint_id: None,
}
};
let find_point_segment_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<serde_json::Value> {
let mut constraints: Vec<serde_json::Value> = Vec::new();
for obj in objects {
let ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let Constraint::Coincident(coincident) = constraint else {
continue;
};
if !coincident.contains_segment(endpoint_point_id) {
continue;
}
let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
if let Some(other_id) = other_segment_id
&& let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
{
if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
constraints.push(serde_json::json!({
"constraintId": obj.id.0,
"segmentOrPointId": other_id.0,
}));
}
}
}
constraints
};
let find_point_point_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
let mut constraint_ids = Vec::new();
for obj in objects {
let ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let Constraint::Coincident(coincident) = constraint else {
continue;
};
if !coincident.contains_segment(endpoint_point_id) {
continue;
}
let is_point_point = coincident.segment_ids().all(|seg_id| {
if let Some(seg_obj) = objects.iter().find(|o| o.id == seg_id) {
matches!(&seg_obj.kind, ObjectKind::Segment { segment } if matches!(segment, Segment::Point(_)))
} else {
false
}
});
if is_point_point {
constraint_ids.push(obj.id);
}
}
constraint_ids
};
let find_point_segment_coincident_constraint_ids = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
let mut constraint_ids = Vec::new();
for obj in objects {
let ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let Constraint::Coincident(coincident) = constraint else {
continue;
};
if !coincident.contains_segment(endpoint_point_id) {
continue;
}
let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
if let Some(other_id) = other_segment_id
&& let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
{
if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
constraint_ids.push(obj.id);
}
}
}
constraint_ids
};
if left_side_needs_tail_cut || right_side_needs_tail_cut {
let side = if left_side_needs_tail_cut {
left_side
} else {
right_side
};
let intersection_coords = match side {
TrimTermination::Intersection {
trim_termination_coords,
..
}
| TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
trim_termination_coords,
..
} => *trim_termination_coords,
TrimTermination::SegEndPoint { .. } => {
return Err("Logic error: side should not be segEndPoint here".to_string());
}
};
let endpoint_to_change = if left_side_needs_tail_cut {
EndpointChanged::End
} else {
EndpointChanged::Start
};
let intersecting_seg_id = match side {
TrimTermination::Intersection {
intersecting_seg_id, ..
}
| TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
intersecting_seg_id, ..
} => *intersecting_seg_id,
TrimTermination::SegEndPoint { .. } => {
return Err("Logic error".to_string());
}
};
let mut coincident_data = if matches!(
side,
TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
) {
let point_id = match side {
TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
other_segment_point_id, ..
} => *other_segment_point_id,
_ => return Err("Logic error".to_string()),
};
let mut data = find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id);
data.intersecting_endpoint_point_id = Some(point_id);
data
} else {
find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id)
};
let trim_seg = objects.iter().find(|obj| obj.id == trim_spawn_id);
let endpoint_point_id = if let Some(seg) = trim_seg {
let ObjectKind::Segment { segment } = &seg.kind else {
return Err("Trim spawn segment is not a segment".to_string());
};
match segment {
Segment::Line(line) => {
if endpoint_to_change == EndpointChanged::Start {
Some(line.start)
} else {
Some(line.end)
}
}
Segment::Arc(arc) => {
if endpoint_to_change == EndpointChanged::Start {
Some(arc.start)
} else {
Some(arc.end)
}
}
_ => None,
}
} else {
None
};
if let (Some(endpoint_id), Some(existing_constraint_id)) =
(endpoint_point_id, coincident_data.existing_point_segment_constraint_id)
{
let constraint_involves_trimmed_endpoint = objects
.iter()
.find(|obj| obj.id == existing_constraint_id)
.and_then(|obj| match &obj.kind {
ObjectKind::Constraint {
constraint: Constraint::Coincident(coincident),
} => Some(coincident.contains_segment(endpoint_id) || coincident.contains_segment(trim_spawn_id)),
_ => None,
})
.unwrap_or(false);
if !constraint_involves_trimmed_endpoint {
coincident_data.existing_point_segment_constraint_id = None;
coincident_data.intersecting_endpoint_point_id = None;
}
}
let coincident_end_constraint_to_delete_ids = if let Some(point_id) = endpoint_point_id {
let mut constraint_ids = find_point_point_coincident_constraints(point_id);
constraint_ids.extend(find_point_segment_coincident_constraint_ids(point_id));
constraint_ids
} else {
Vec::new()
};
let point_axis_constraint_ids_to_delete = if let Some(point_id) = endpoint_point_id {
objects
.iter()
.filter_map(|obj| {
let ObjectKind::Constraint { constraint } = &obj.kind else {
return None;
};
point_axis_constraint_references_point(constraint, point_id).then_some(obj.id)
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
let new_ctor = match ctor {
SegmentCtor::Line(line_ctor) => {
let new_point = crate::frontend::sketch::Point2d {
x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
};
if endpoint_to_change == EndpointChanged::Start {
SegmentCtor::Line(crate::frontend::sketch::LineCtor {
start: new_point,
end: line_ctor.end.clone(),
construction: line_ctor.construction,
})
} else {
SegmentCtor::Line(crate::frontend::sketch::LineCtor {
start: line_ctor.start.clone(),
end: new_point,
construction: line_ctor.construction,
})
}
}
SegmentCtor::Arc(arc_ctor) => {
let new_point = crate::frontend::sketch::Point2d {
x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
};
if endpoint_to_change == EndpointChanged::Start {
SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
start: new_point,
end: arc_ctor.end.clone(),
center: arc_ctor.center.clone(),
construction: arc_ctor.construction,
})
} else {
SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
start: arc_ctor.start.clone(),
end: new_point,
center: arc_ctor.center.clone(),
construction: arc_ctor.construction,
})
}
}
_ => {
return Err("Unsupported segment type for edit".to_string());
}
};
let mut all_constraint_ids_to_delete: Vec<ObjectId> = Vec::new();
if let Some(constraint_id) = coincident_data.existing_point_segment_constraint_id {
all_constraint_ids_to_delete.push(constraint_id);
}
all_constraint_ids_to_delete.extend(coincident_end_constraint_to_delete_ids);
all_constraint_ids_to_delete.extend(point_axis_constraint_ids_to_delete);
let distance_constraint_ids = find_distance_constraints_for_segment(trim_spawn_id);
all_constraint_ids_to_delete.extend(distance_constraint_ids);
return Ok(TrimPlan::TailCut {
segment_id: trim_spawn_id,
endpoint_changed: endpoint_to_change,
ctor: new_ctor,
segment_or_point_to_make_coincident_to: intersecting_seg_id,
intersecting_endpoint_point_id: coincident_data.intersecting_endpoint_point_id,
constraint_ids_to_delete: all_constraint_ids_to_delete,
});
}
if matches!(segment, Segment::Circle(_)) {
let left_side_intersects = is_intersect_or_coincident(left_side);
let right_side_intersects = is_intersect_or_coincident(right_side);
if !(left_side_intersects && right_side_intersects) {
return Err(format!(
"Unsupported circle trim termination combination: left={:?} right={:?}",
left_side, right_side
));
}
let left_trim_coords = match left_side {
TrimTermination::SegEndPoint {
trim_termination_coords,
}
| TrimTermination::Intersection {
trim_termination_coords,
..
}
| TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
trim_termination_coords,
..
} => *trim_termination_coords,
};
let right_trim_coords = match right_side {
TrimTermination::SegEndPoint {
trim_termination_coords,
}
| TrimTermination::Intersection {
trim_termination_coords,
..
}
| TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
trim_termination_coords,
..
} => *trim_termination_coords,
};
let trim_points_coincident = ((left_trim_coords.x - right_trim_coords.x)
* (left_trim_coords.x - right_trim_coords.x)
+ (left_trim_coords.y - right_trim_coords.y) * (left_trim_coords.y - right_trim_coords.y))
.sqrt()
<= EPSILON_POINT_ON_SEGMENT * 10.0;
if trim_points_coincident {
return Ok(TrimPlan::DeleteSegment {
segment_id: trim_spawn_id,
});
}
let circle_center_coords =
get_position_coords_from_circle(trim_spawn_segment, CirclePoint::Center, objects, default_unit)
.ok_or_else(|| {
format!(
"Could not get center coordinates for circle segment {}",
trim_spawn_id.0
)
})?;
let spawn_on_left_to_right = is_point_on_arc(
trim_spawn_coords,
circle_center_coords,
left_trim_coords,
right_trim_coords,
EPSILON_POINT_ON_SEGMENT,
);
let (arc_start_coords, arc_end_coords, arc_start_termination, arc_end_termination) = if spawn_on_left_to_right {
(
right_trim_coords,
left_trim_coords,
Box::new(right_side.clone()),
Box::new(left_side.clone()),
)
} else {
(
left_trim_coords,
right_trim_coords,
Box::new(left_side.clone()),
Box::new(right_side.clone()),
)
};
return Ok(TrimPlan::ReplaceCircleWithArc {
circle_id: trim_spawn_id,
arc_start_coords,
arc_end_coords,
arc_start_termination,
arc_end_termination,
});
}
let left_side_intersects = is_intersect_or_coincident(left_side);
let right_side_intersects = is_intersect_or_coincident(right_side);
if left_side_intersects && right_side_intersects {
let left_intersecting_seg_id = match left_side {
TrimTermination::Intersection {
intersecting_seg_id, ..
}
| TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
intersecting_seg_id, ..
} => *intersecting_seg_id,
TrimTermination::SegEndPoint { .. } => {
return Err("Logic error: left side should not be segEndPoint".to_string());
}
};
let right_intersecting_seg_id = match right_side {
TrimTermination::Intersection {
intersecting_seg_id, ..
}
| TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
intersecting_seg_id, ..
} => *intersecting_seg_id,
TrimTermination::SegEndPoint { .. } => {
return Err("Logic error: right side should not be segEndPoint".to_string());
}
};
let left_coincident_data = if matches!(
left_side,
TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
) {
let point_id = match left_side {
TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
other_segment_point_id, ..
} => *other_segment_point_id,
_ => return Err("Logic error".to_string()),
};
let mut data = find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id);
data.intersecting_endpoint_point_id = Some(point_id);
data
} else {
find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id)
};
let right_coincident_data = if matches!(
right_side,
TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
) {
let point_id = match right_side {
TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
other_segment_point_id, ..
} => *other_segment_point_id,
_ => return Err("Logic error".to_string()),
};
let mut data = find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id);
data.intersecting_endpoint_point_id = Some(point_id);
data
} else {
find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id)
};
let (original_start_point_id, original_end_point_id) = match segment {
Segment::Line(line) => (Some(line.start), Some(line.end)),
Segment::Arc(arc) => (Some(arc.start), Some(arc.end)),
_ => (None, None),
};
let original_end_point_coords = match segment {
Segment::Line(_) => {
get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
}
Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
_ => None,
};
let Some(original_end_coords) = original_end_point_coords else {
return Err(
"Could not get original end point coordinates before editing - this is required for split trim"
.to_string(),
);
};
let left_trim_coords = match left_side {
TrimTermination::SegEndPoint {
trim_termination_coords,
}
| TrimTermination::Intersection {
trim_termination_coords,
..
}
| TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
trim_termination_coords,
..
} => *trim_termination_coords,
};
let right_trim_coords = match right_side {
TrimTermination::SegEndPoint {
trim_termination_coords,
}
| TrimTermination::Intersection {
trim_termination_coords,
..
}
| TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
trim_termination_coords,
..
} => *trim_termination_coords,
};
let dist_to_original_end = ((right_trim_coords.x - original_end_coords.x)
* (right_trim_coords.x - original_end_coords.x)
+ (right_trim_coords.y - original_end_coords.y) * (right_trim_coords.y - original_end_coords.y))
.sqrt();
if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
return Err(
"Split point is at original end point - this should be handled as cutTail, not split".to_string(),
);
}
let mut constraints_to_migrate: Vec<ConstraintToMigrate> = Vec::new();
let mut constraints_to_delete_set: IndexSet<ObjectId> = IndexSet::new();
if let Some(constraint_id) = left_coincident_data.existing_point_segment_constraint_id {
constraints_to_delete_set.insert(constraint_id);
}
if let Some(constraint_id) = right_coincident_data.existing_point_segment_constraint_id {
constraints_to_delete_set.insert(constraint_id);
}
if let Some(end_id) = original_end_point_id {
for obj in objects {
let ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
if point_axis_constraint_references_point(constraint, end_id) {
constraints_to_delete_set.insert(obj.id);
}
}
}
if let Some(end_id) = original_end_point_id {
let end_point_point_constraint_ids = find_point_point_coincident_constraints(end_id);
for constraint_id in end_point_point_constraint_ids {
let other_point_id_opt = objects.iter().find_map(|obj| {
if obj.id != constraint_id {
return None;
}
let ObjectKind::Constraint { constraint } = &obj.kind else {
return None;
};
let Constraint::Coincident(coincident) = constraint else {
return None;
};
coincident.segment_ids().find(|&seg_id| seg_id != end_id)
});
if let Some(other_point_id) = other_point_id_opt {
constraints_to_delete_set.insert(constraint_id);
constraints_to_migrate.push(ConstraintToMigrate {
constraint_id,
other_entity_id: other_point_id,
is_point_point: true,
attach_to_endpoint: AttachToEndpoint::End,
});
}
}
}
if let Some(end_id) = original_end_point_id {
let end_point_segment_constraints = find_point_segment_coincident_constraints(end_id);
for constraint_json in end_point_segment_constraints {
if let Some(constraint_id_usize) = constraint_json
.get("constraintId")
.and_then(|v| v.as_u64())
.map(|id| id as usize)
{
let constraint_id = ObjectId(constraint_id_usize);
constraints_to_delete_set.insert(constraint_id);
if let Some(other_id_usize) = constraint_json
.get("segmentOrPointId")
.and_then(|v| v.as_u64())
.map(|id| id as usize)
{
constraints_to_migrate.push(ConstraintToMigrate {
constraint_id,
other_entity_id: ObjectId(other_id_usize),
is_point_point: false,
attach_to_endpoint: AttachToEndpoint::End,
});
}
}
}
}
if let Some(end_id) = original_end_point_id {
for obj in objects {
let ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let Constraint::Coincident(coincident) = constraint else {
continue;
};
if !coincident.contains_segment(trim_spawn_id) {
continue;
}
if let (Some(start_id), Some(end_id_val)) = (original_start_point_id, Some(end_id))
&& coincident.segment_ids().any(|id| id == start_id || id == end_id_val)
{
continue; }
let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
if let Some(other_id) = other_id {
if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
continue;
};
let Segment::Point(point) = other_segment else {
continue;
};
let point_coords = Coords2d {
x: number_to_unit(&point.position.x, default_unit),
y: number_to_unit(&point.position.y, default_unit),
};
let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
if let ObjectKind::Segment {
segment: Segment::Point(end_point),
} = &end_point_obj.kind
{
Some(Coords2d {
x: number_to_unit(&end_point.position.x, default_unit),
y: number_to_unit(&end_point.position.y, default_unit),
})
} else {
None
}
} else {
None
}
} else {
None
};
let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
let dist_to_original_end = ((point_coords.x - reference_coords.x)
* (point_coords.x - reference_coords.x)
+ (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
.sqrt();
if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
let has_point_point_constraint = find_point_point_coincident_constraints(end_id)
.iter()
.any(|&constraint_id| {
if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
if let ObjectKind::Constraint {
constraint: Constraint::Coincident(coincident),
} = &constraint_obj.kind
{
coincident.contains_segment(other_id)
} else {
false
}
} else {
false
}
});
if !has_point_point_constraint {
constraints_to_migrate.push(ConstraintToMigrate {
constraint_id: obj.id,
other_entity_id: other_id,
is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
}
constraints_to_delete_set.insert(obj.id);
}
}
}
}
}
let split_point = right_trim_coords; let segment_start_coords = match segment {
Segment::Line(_) => {
get_position_coords_for_line(trim_spawn_segment, LineEndpoint::Start, objects, default_unit)
}
Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Start, objects, default_unit),
_ => None,
};
let segment_end_coords = match segment {
Segment::Line(_) => {
get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
}
Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
_ => None,
};
let segment_center_coords = match segment {
Segment::Line(_) => None,
Segment::Arc(_) => {
get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Center, objects, default_unit)
}
_ => None,
};
if let (Some(start_coords), Some(end_coords)) = (segment_start_coords, segment_end_coords) {
let split_point_t_opt = match segment {
Segment::Line(_) => Some(project_point_onto_segment(split_point, start_coords, end_coords)),
Segment::Arc(_) => segment_center_coords
.map(|center| project_point_onto_arc(split_point, center, start_coords, end_coords)),
_ => None,
};
if let Some(split_point_t) = split_point_t_opt {
for obj in objects {
let ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let Constraint::Coincident(coincident) = constraint else {
continue;
};
if !coincident.contains_segment(trim_spawn_id) {
continue;
}
if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id)
&& coincident.segment_ids().any(|id| id == start_id || id == end_id)
{
continue;
}
let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
if let Some(other_id) = other_id {
if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
continue;
};
let Segment::Point(point) = other_segment else {
continue;
};
let point_coords = Coords2d {
x: number_to_unit(&point.position.x, default_unit),
y: number_to_unit(&point.position.y, default_unit),
};
let point_t = match segment {
Segment::Line(_) => project_point_onto_segment(point_coords, start_coords, end_coords),
Segment::Arc(_) => {
if let Some(center) = segment_center_coords {
project_point_onto_arc(point_coords, center, start_coords, end_coords)
} else {
continue; }
}
_ => continue, };
let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
if let ObjectKind::Segment {
segment: Segment::Point(end_point),
} = &end_point_obj.kind
{
Some(Coords2d {
x: number_to_unit(&end_point.position.x, default_unit),
y: number_to_unit(&end_point.position.y, default_unit),
})
} else {
None
}
} else {
None
}
} else {
None
};
let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
let dist_to_original_end = ((point_coords.x - reference_coords.x)
* (point_coords.x - reference_coords.x)
+ (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
.sqrt();
if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
find_point_point_coincident_constraints(end_id)
.iter()
.any(|&constraint_id| {
if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
{
if let ObjectKind::Constraint {
constraint: Constraint::Coincident(coincident),
} = &constraint_obj.kind
{
coincident.contains_segment(other_id)
} else {
false
}
} else {
false
}
})
} else {
false
};
if !has_point_point_constraint {
constraints_to_migrate.push(ConstraintToMigrate {
constraint_id: obj.id,
other_entity_id: other_id,
is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
}
constraints_to_delete_set.insert(obj.id);
continue; }
let dist_to_start = ((point_coords.x - start_coords.x) * (point_coords.x - start_coords.x)
+ (point_coords.y - start_coords.y) * (point_coords.y - start_coords.y))
.sqrt();
let is_at_start = (point_t - 0.0).abs() < EPSILON_POINT_ON_SEGMENT
|| dist_to_start < EPSILON_POINT_ON_SEGMENT;
if is_at_start {
continue; }
let dist_to_split = (point_t - split_point_t).abs();
if dist_to_split < EPSILON_POINT_ON_SEGMENT * 100.0 {
continue; }
if point_t > split_point_t {
constraints_to_migrate.push(ConstraintToMigrate {
constraint_id: obj.id,
other_entity_id: other_id,
is_point_point: false, attach_to_endpoint: AttachToEndpoint::Segment, });
constraints_to_delete_set.insert(obj.id);
}
}
}
}
} }
let distance_constraint_ids_for_split = find_distance_constraints_for_segment(trim_spawn_id);
let arc_center_point_id: Option<ObjectId> = match segment {
Segment::Arc(arc) => Some(arc.center),
_ => None,
};
for constraint_id in distance_constraint_ids_for_split {
if let Some(center_id) = arc_center_point_id {
if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
&& let ObjectKind::Constraint { constraint } = &constraint_obj.kind
&& let Constraint::Distance(distance) = constraint
&& distance.contains_point(center_id)
{
continue;
}
}
constraints_to_delete_set.insert(constraint_id);
}
for obj in objects {
let ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let Constraint::Coincident(coincident) = constraint else {
continue;
};
if !coincident.contains_segment(trim_spawn_id) {
continue;
}
if constraints_to_delete_set.contains(&obj.id) {
continue;
}
let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
if let Some(other_id) = other_id {
if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
continue;
};
let Segment::Point(point) = other_segment else {
continue;
};
let _is_endpoint_constraint =
if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id) {
coincident.segment_ids().any(|id| id == start_id || id == end_id)
} else {
false
};
let point_coords = Coords2d {
x: number_to_unit(&point.position.x, default_unit),
y: number_to_unit(&point.position.y, default_unit),
};
let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
if let ObjectKind::Segment {
segment: Segment::Point(end_point),
} = &end_point_obj.kind
{
Some(Coords2d {
x: number_to_unit(&end_point.position.x, default_unit),
y: number_to_unit(&end_point.position.y, default_unit),
})
} else {
None
}
} else {
None
}
} else {
None
};
let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
let dist_to_original_end = ((point_coords.x - reference_coords.x)
* (point_coords.x - reference_coords.x)
+ (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
.sqrt();
let is_at_original_end = dist_to_original_end < EPSILON_POINT_ON_SEGMENT * 2.0;
if is_at_original_end {
let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
find_point_point_coincident_constraints(end_id)
.iter()
.any(|&constraint_id| {
if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
if let ObjectKind::Constraint {
constraint: Constraint::Coincident(coincident),
} = &constraint_obj.kind
{
coincident.contains_segment(other_id)
} else {
false
}
} else {
false
}
})
} else {
false
};
if !has_point_point_constraint {
constraints_to_migrate.push(ConstraintToMigrate {
constraint_id: obj.id,
other_entity_id: other_id,
is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
}
constraints_to_delete_set.insert(obj.id);
}
}
}
}
let constraints_to_delete: Vec<ObjectId> = constraints_to_delete_set.iter().copied().collect();
let plan = TrimPlan::SplitSegment {
segment_id: trim_spawn_id,
left_trim_coords,
right_trim_coords,
original_end_coords,
left_side: Box::new(left_side.clone()),
right_side: Box::new(right_side.clone()),
left_side_coincident_data: CoincidentData {
intersecting_seg_id: left_intersecting_seg_id,
intersecting_endpoint_point_id: left_coincident_data.intersecting_endpoint_point_id,
existing_point_segment_constraint_id: left_coincident_data.existing_point_segment_constraint_id,
},
right_side_coincident_data: CoincidentData {
intersecting_seg_id: right_intersecting_seg_id,
intersecting_endpoint_point_id: right_coincident_data.intersecting_endpoint_point_id,
existing_point_segment_constraint_id: right_coincident_data.existing_point_segment_constraint_id,
},
constraints_to_migrate,
constraints_to_delete,
};
return Ok(plan);
}
Err(format!(
"Unsupported trim termination combination: left={:?} right={:?}",
left_side, right_side
))
}
pub(crate) async fn execute_trim_operations_simple(
strategy: Vec<TrimOperation>,
current_scene_graph_delta: &crate::frontend::api::SceneGraphDelta,
frontend: &mut crate::frontend::FrontendState,
ctx: &crate::ExecutorContext,
version: crate::frontend::api::Version,
sketch_id: ObjectId,
) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
use crate::frontend::SketchApi;
use crate::frontend::sketch::Constraint;
use crate::frontend::sketch::ExistingSegmentCtor;
use crate::frontend::sketch::SegmentCtor;
let default_unit = frontend.default_length_unit();
let mut op_index = 0;
let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = None;
let mut invalidates_ids = false;
while op_index < strategy.len() {
let mut consumed_ops = 1;
let operation_result = match &strategy[op_index] {
TrimOperation::SimpleTrim { segment_to_trim_id } => {
frontend
.delete_objects(
ctx,
version,
sketch_id,
Vec::new(), vec![*segment_to_trim_id], )
.await
.map_err(|e| format!("Failed to delete segment: {}", e.error.message()))
}
TrimOperation::EditSegment {
segment_id,
ctor,
endpoint_changed,
} => {
if op_index + 1 < strategy.len() {
if let TrimOperation::AddCoincidentConstraint {
segment_id: coincident_seg_id,
endpoint_changed: coincident_endpoint_changed,
segment_or_point_to_make_coincident_to,
intersecting_endpoint_point_id,
} = &strategy[op_index + 1]
{
if segment_id == coincident_seg_id && endpoint_changed == coincident_endpoint_changed {
let mut delete_constraint_ids: Vec<ObjectId> = Vec::new();
consumed_ops = 2;
if op_index + 2 < strategy.len()
&& let TrimOperation::DeleteConstraints { constraint_ids } = &strategy[op_index + 2]
{
delete_constraint_ids = constraint_ids.to_vec();
consumed_ops = 3;
}
let segment_ctor = ctor.clone();
let edited_segment = current_scene_graph_delta
.new_graph
.objects
.iter()
.find(|obj| obj.id == *segment_id)
.ok_or_else(|| format!("Failed to find segment {} for tail-cut batch", segment_id.0))?;
let endpoint_point_id = match &edited_segment.kind {
crate::frontend::api::ObjectKind::Segment { segment } => match segment {
crate::frontend::sketch::Segment::Line(line) => {
if *endpoint_changed == EndpointChanged::Start {
line.start
} else {
line.end
}
}
crate::frontend::sketch::Segment::Arc(arc) => {
if *endpoint_changed == EndpointChanged::Start {
arc.start
} else {
arc.end
}
}
_ => {
return Err("Unsupported segment type for tail-cut batch".to_string());
}
},
_ => {
return Err("Edited object is not a segment (tail-cut batch)".to_string());
}
};
let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
vec![endpoint_point_id.into(), (*point_id).into()]
} else {
vec![
endpoint_point_id.into(),
(*segment_or_point_to_make_coincident_to).into(),
]
};
let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
segments: coincident_segments,
});
let segment_to_edit = ExistingSegmentCtor {
id: *segment_id,
ctor: segment_ctor,
};
frontend
.batch_tail_cut_operations(
ctx,
version,
sketch_id,
vec![segment_to_edit],
vec![constraint],
delete_constraint_ids,
)
.await
.map_err(|e| format!("Failed to batch tail-cut operations: {}", e.error.message()))
} else {
let segment_to_edit = ExistingSegmentCtor {
id: *segment_id,
ctor: ctor.clone(),
};
frontend
.edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
.await
.map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
}
} else {
let segment_to_edit = ExistingSegmentCtor {
id: *segment_id,
ctor: ctor.clone(),
};
frontend
.edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
.await
.map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
}
} else {
let segment_to_edit = ExistingSegmentCtor {
id: *segment_id,
ctor: ctor.clone(),
};
frontend
.edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
.await
.map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
}
}
TrimOperation::AddCoincidentConstraint {
segment_id,
endpoint_changed,
segment_or_point_to_make_coincident_to,
intersecting_endpoint_point_id,
} => {
let edited_segment = current_scene_graph_delta
.new_graph
.objects
.iter()
.find(|obj| obj.id == *segment_id)
.ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
let new_segment_endpoint_point_id = match &edited_segment.kind {
crate::frontend::api::ObjectKind::Segment { segment } => match segment {
crate::frontend::sketch::Segment::Line(line) => {
if *endpoint_changed == EndpointChanged::Start {
line.start
} else {
line.end
}
}
crate::frontend::sketch::Segment::Arc(arc) => {
if *endpoint_changed == EndpointChanged::Start {
arc.start
} else {
arc.end
}
}
_ => {
return Err("Unsupported segment type for addCoincidentConstraint".to_string());
}
},
_ => {
return Err("Edited object is not a segment".to_string());
}
};
let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
vec![new_segment_endpoint_point_id.into(), (*point_id).into()]
} else {
vec![
new_segment_endpoint_point_id.into(),
(*segment_or_point_to_make_coincident_to).into(),
]
};
let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
segments: coincident_segments,
});
frontend
.add_constraint(ctx, version, sketch_id, constraint)
.await
.map_err(|e| format!("Failed to add constraint: {}", e.error.message()))
}
TrimOperation::DeleteConstraints { constraint_ids } => {
let constraint_object_ids: Vec<ObjectId> = constraint_ids.to_vec();
frontend
.delete_objects(
ctx,
version,
sketch_id,
constraint_object_ids,
Vec::new(), )
.await
.map_err(|e| format!("Failed to delete constraints: {}", e.error.message()))
}
TrimOperation::ReplaceCircleWithArc {
circle_id,
arc_start_coords,
arc_end_coords,
arc_start_termination,
arc_end_termination,
} => {
let original_circle = current_scene_graph_delta
.new_graph
.objects
.iter()
.find(|obj| obj.id == *circle_id)
.ok_or_else(|| format!("Failed to find original circle {}", circle_id.0))?;
let (original_circle_start_id, original_circle_center_id, circle_ctor) = match &original_circle.kind {
crate::frontend::api::ObjectKind::Segment { segment } => match segment {
crate::frontend::sketch::Segment::Circle(circle) => match &circle.ctor {
SegmentCtor::Circle(circle_ctor) => (circle.start, circle.center, circle_ctor.clone()),
_ => return Err("Circle does not have a Circle ctor".to_string()),
},
_ => return Err("Original segment is not a circle".to_string()),
},
_ => return Err("Original object is not a segment".to_string()),
};
let units = match &circle_ctor.start.x {
crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
_ => crate::pretty::NumericSuffix::Mm,
};
let coords_to_point_expr = |coords: Coords2d| crate::frontend::sketch::Point2d {
x: crate::frontend::api::Expr::Var(unit_to_number(coords.x, default_unit, units)),
y: crate::frontend::api::Expr::Var(unit_to_number(coords.y, default_unit, units)),
};
let arc_ctor = SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
start: coords_to_point_expr(*arc_start_coords),
end: coords_to_point_expr(*arc_end_coords),
center: circle_ctor.center.clone(),
construction: circle_ctor.construction,
});
let (_add_source_delta, add_scene_graph_delta) = frontend
.add_segment(ctx, version, sketch_id, arc_ctor, None)
.await
.map_err(|e| format!("Failed to add arc while replacing circle: {}", e.error.message()))?;
invalidates_ids = invalidates_ids || add_scene_graph_delta.invalidates_ids;
let new_arc_id = *add_scene_graph_delta
.new_objects
.iter()
.find(|&id| {
add_scene_graph_delta
.new_graph
.objects
.iter()
.find(|o| o.id == *id)
.is_some_and(|obj| {
matches!(
&obj.kind,
crate::frontend::api::ObjectKind::Segment { segment }
if matches!(segment, crate::frontend::sketch::Segment::Arc(_))
)
})
})
.ok_or_else(|| "Failed to find newly created arc segment".to_string())?;
let new_arc_obj = add_scene_graph_delta
.new_graph
.objects
.iter()
.find(|obj| obj.id == new_arc_id)
.ok_or_else(|| format!("New arc segment not found {}", new_arc_id.0))?;
let (new_arc_start_id, new_arc_end_id, new_arc_center_id) = match &new_arc_obj.kind {
crate::frontend::api::ObjectKind::Segment { segment } => match segment {
crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, arc.center),
_ => return Err("New segment is not an arc".to_string()),
},
_ => return Err("New arc object is not a segment".to_string()),
};
let constraint_segments_for =
|arc_endpoint_id: ObjectId,
term: &TrimTermination|
-> Result<Vec<crate::frontend::sketch::ConstraintSegment>, String> {
match term {
TrimTermination::Intersection {
intersecting_seg_id, ..
} => Ok(vec![arc_endpoint_id.into(), (*intersecting_seg_id).into()]),
TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
other_segment_point_id,
..
} => Ok(vec![arc_endpoint_id.into(), (*other_segment_point_id).into()]),
TrimTermination::SegEndPoint { .. } => {
Err("Circle replacement endpoint cannot terminate at seg endpoint".to_string())
}
}
};
let start_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
segments: constraint_segments_for(new_arc_start_id, arc_start_termination)?,
});
let (_c1_source_delta, c1_scene_graph_delta) = frontend
.add_constraint(ctx, version, sketch_id, start_constraint)
.await
.map_err(|e| format!("Failed to add start coincident on replaced arc: {}", e.error.message()))?;
invalidates_ids = invalidates_ids || c1_scene_graph_delta.invalidates_ids;
let end_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
segments: constraint_segments_for(new_arc_end_id, arc_end_termination)?,
});
let (_c2_source_delta, c2_scene_graph_delta) = frontend
.add_constraint(ctx, version, sketch_id, end_constraint)
.await
.map_err(|e| format!("Failed to add end coincident on replaced arc: {}", e.error.message()))?;
invalidates_ids = invalidates_ids || c2_scene_graph_delta.invalidates_ids;
let mut termination_point_ids: Vec<ObjectId> = Vec::new();
for term in [arc_start_termination, arc_end_termination] {
if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
other_segment_point_id,
..
} = term.as_ref()
{
termination_point_ids.push(*other_segment_point_id);
}
}
let rewrite_map = std::collections::HashMap::from([
(*circle_id, new_arc_id),
(original_circle_center_id, new_arc_center_id),
(original_circle_start_id, new_arc_start_id),
]);
let rewrite_ids: std::collections::HashSet<ObjectId> = rewrite_map.keys().copied().collect();
let mut migrated_constraints: Vec<Constraint> = Vec::new();
for obj in ¤t_scene_graph_delta.new_graph.objects {
let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
match constraint {
Constraint::Coincident(coincident) => {
if !constraint_segments_reference_any(&coincident.segments, &rewrite_ids) {
continue;
}
if coincident.contains_segment(*circle_id)
&& coincident
.segment_ids()
.filter(|id| *id != *circle_id)
.any(|id| termination_point_ids.contains(&id))
{
continue;
}
let Some(Constraint::Coincident(migrated_coincident)) =
rewrite_constraint_with_map(constraint, &rewrite_map)
else {
continue;
};
let migrated_ids: Vec<ObjectId> = migrated_coincident
.segments
.iter()
.filter_map(|segment| match segment {
crate::frontend::sketch::ConstraintSegment::Segment(id) => Some(*id),
crate::frontend::sketch::ConstraintSegment::Origin(_) => None,
})
.collect();
if migrated_ids.contains(&new_arc_id)
&& (migrated_ids.contains(&new_arc_start_id) || migrated_ids.contains(&new_arc_end_id))
{
continue;
}
migrated_constraints.push(Constraint::Coincident(migrated_coincident));
}
Constraint::Distance(distance) => {
if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
continue;
}
if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
migrated_constraints.push(migrated);
}
}
Constraint::HorizontalDistance(distance) => {
if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
continue;
}
if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
migrated_constraints.push(migrated);
}
}
Constraint::VerticalDistance(distance) => {
if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
continue;
}
if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
migrated_constraints.push(migrated);
}
}
Constraint::Radius(radius) => {
if radius.arc == *circle_id
&& let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
{
migrated_constraints.push(migrated);
}
}
Constraint::Diameter(diameter) => {
if diameter.arc == *circle_id
&& let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
{
migrated_constraints.push(migrated);
}
}
Constraint::EqualRadius(equal_radius) => {
if equal_radius.input.contains(circle_id)
&& let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
{
migrated_constraints.push(migrated);
}
}
Constraint::Tangent(tangent) => {
if tangent.input.contains(circle_id)
&& let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
{
migrated_constraints.push(migrated);
}
}
_ => {}
}
}
for constraint in migrated_constraints {
let (_source_delta, migrated_scene_graph_delta) = frontend
.add_constraint(ctx, version, sketch_id, constraint)
.await
.map_err(|e| format!("Failed to migrate circle constraint to arc: {}", e.error.message()))?;
invalidates_ids = invalidates_ids || migrated_scene_graph_delta.invalidates_ids;
}
frontend
.delete_objects(ctx, version, sketch_id, Vec::new(), vec![*circle_id])
.await
.map_err(|e| format!("Failed to delete circle after arc replacement: {}", e.error.message()))
}
TrimOperation::SplitSegment {
segment_id,
left_trim_coords,
right_trim_coords,
original_end_coords,
left_side,
right_side,
constraints_to_migrate,
constraints_to_delete,
..
} => {
let original_segment = current_scene_graph_delta
.new_graph
.objects
.iter()
.find(|obj| obj.id == *segment_id)
.ok_or_else(|| format!("Failed to find original segment {}", segment_id.0))?;
let (original_segment_start_point_id, original_segment_end_point_id, original_segment_center_point_id) =
match &original_segment.kind {
crate::frontend::api::ObjectKind::Segment { segment } => match segment {
crate::frontend::sketch::Segment::Line(line) => (Some(line.start), Some(line.end), None),
crate::frontend::sketch::Segment::Arc(arc) => {
(Some(arc.start), Some(arc.end), Some(arc.center))
}
_ => (None, None, None),
},
_ => (None, None, None),
};
let mut center_point_constraints_to_migrate: Vec<(Constraint, ObjectId)> = Vec::new();
if let Some(original_center_id) = original_segment_center_point_id {
for obj in ¤t_scene_graph_delta.new_graph.objects {
let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
if let Constraint::Coincident(coincident) = constraint
&& coincident.contains_segment(original_center_id)
{
center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
}
if let Constraint::Distance(distance) = constraint
&& distance.contains_point(original_center_id)
{
center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
}
}
}
let (_segment_type, original_ctor) = match &original_segment.kind {
crate::frontend::api::ObjectKind::Segment { segment } => match segment {
crate::frontend::sketch::Segment::Line(line) => ("Line", line.ctor.clone()),
crate::frontend::sketch::Segment::Arc(arc) => ("Arc", arc.ctor.clone()),
_ => {
return Err("Original segment is not a Line or Arc".to_string());
}
},
_ => {
return Err("Original object is not a segment".to_string());
}
};
let units = match &original_ctor {
SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
_ => crate::pretty::NumericSuffix::Mm,
},
SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
_ => crate::pretty::NumericSuffix::Mm,
},
_ => crate::pretty::NumericSuffix::Mm,
};
let coords_to_point =
|coords: Coords2d| -> crate::frontend::sketch::Point2d<crate::frontend::api::Number> {
crate::frontend::sketch::Point2d {
x: unit_to_number(coords.x, default_unit, units),
y: unit_to_number(coords.y, default_unit, units),
}
};
let point_to_expr = |point: crate::frontend::sketch::Point2d<crate::frontend::api::Number>| -> crate::frontend::sketch::Point2d<crate::frontend::api::Expr> {
crate::frontend::sketch::Point2d {
x: crate::frontend::api::Expr::Var(point.x),
y: crate::frontend::api::Expr::Var(point.y),
}
};
let new_segment_ctor = match &original_ctor {
SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
start: point_to_expr(coords_to_point(*right_trim_coords)),
end: point_to_expr(coords_to_point(*original_end_coords)),
construction: line_ctor.construction,
}),
SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
start: point_to_expr(coords_to_point(*right_trim_coords)),
end: point_to_expr(coords_to_point(*original_end_coords)),
center: arc_ctor.center.clone(),
construction: arc_ctor.construction,
}),
_ => {
return Err("Unsupported segment type for new segment".to_string());
}
};
let (_add_source_delta, add_scene_graph_delta) = frontend
.add_segment(ctx, version, sketch_id, new_segment_ctor, None)
.await
.map_err(|e| format!("Failed to add new segment: {}", e.error.message()))?;
let new_segment_id = *add_scene_graph_delta
.new_objects
.iter()
.find(|&id| {
if let Some(obj) = add_scene_graph_delta.new_graph.objects.iter().find(|o| o.id == *id) {
matches!(
&obj.kind,
crate::frontend::api::ObjectKind::Segment { segment }
if matches!(segment, crate::frontend::sketch::Segment::Line(_) | crate::frontend::sketch::Segment::Arc(_))
)
} else {
false
}
})
.ok_or_else(|| "Failed to find newly created segment".to_string())?;
let new_segment = add_scene_graph_delta
.new_graph
.objects
.iter()
.find(|o| o.id == new_segment_id)
.ok_or_else(|| format!("New segment not found with id {}", new_segment_id.0))?;
let (new_segment_start_point_id, new_segment_end_point_id, new_segment_center_point_id) =
match &new_segment.kind {
crate::frontend::api::ObjectKind::Segment { segment } => match segment {
crate::frontend::sketch::Segment::Line(line) => (line.start, line.end, None),
crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, Some(arc.center)),
_ => {
return Err("New segment is not a Line or Arc".to_string());
}
},
_ => {
return Err("New segment is not a segment".to_string());
}
};
let edited_ctor = match &original_ctor {
SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
start: line_ctor.start.clone(),
end: point_to_expr(coords_to_point(*left_trim_coords)),
construction: line_ctor.construction,
}),
SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
start: arc_ctor.start.clone(),
end: point_to_expr(coords_to_point(*left_trim_coords)),
center: arc_ctor.center.clone(),
construction: arc_ctor.construction,
}),
_ => {
return Err("Unsupported segment type for split".to_string());
}
};
let (_edit_source_delta, edit_scene_graph_delta) = frontend
.edit_segments(
ctx,
version,
sketch_id,
vec![ExistingSegmentCtor {
id: *segment_id,
ctor: edited_ctor,
}],
)
.await
.map_err(|e| format!("Failed to edit segment: {}", e.error.message()))?;
invalidates_ids = invalidates_ids || edit_scene_graph_delta.invalidates_ids;
let edited_segment = edit_scene_graph_delta
.new_graph
.objects
.iter()
.find(|obj| obj.id == *segment_id)
.ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
let left_side_endpoint_point_id = match &edited_segment.kind {
crate::frontend::api::ObjectKind::Segment { segment } => match segment {
crate::frontend::sketch::Segment::Line(line) => line.end,
crate::frontend::sketch::Segment::Arc(arc) => arc.end,
_ => {
return Err("Edited segment is not a Line or Arc".to_string());
}
},
_ => {
return Err("Edited segment is not a segment".to_string());
}
};
let mut batch_constraints = Vec::new();
let left_intersecting_seg_id = match &**left_side {
TrimTermination::Intersection {
intersecting_seg_id, ..
}
| TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
intersecting_seg_id, ..
} => *intersecting_seg_id,
_ => {
return Err("Left side is not an intersection or coincident".to_string());
}
};
let left_coincident_segments = match &**left_side {
TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
other_segment_point_id,
..
} => {
vec![left_side_endpoint_point_id.into(), (*other_segment_point_id).into()]
}
_ => {
vec![left_side_endpoint_point_id.into(), left_intersecting_seg_id.into()]
}
};
batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
segments: left_coincident_segments,
}));
let right_intersecting_seg_id = match &**right_side {
TrimTermination::Intersection {
intersecting_seg_id, ..
}
| TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
intersecting_seg_id, ..
} => *intersecting_seg_id,
_ => {
return Err("Right side is not an intersection or coincident".to_string());
}
};
let mut intersection_point_id: Option<ObjectId> = None;
if matches!(&**right_side, TrimTermination::Intersection { .. }) {
let intersecting_seg = edit_scene_graph_delta
.new_graph
.objects
.iter()
.find(|obj| obj.id == right_intersecting_seg_id);
if let Some(seg) = intersecting_seg {
let endpoint_epsilon = 1e-3; let right_trim_coords_value = *right_trim_coords;
if let crate::frontend::api::ObjectKind::Segment { segment } = &seg.kind {
match segment {
crate::frontend::sketch::Segment::Line(_) => {
if let (Some(start_coords), Some(end_coords)) = (
crate::frontend::trim::get_position_coords_for_line(
seg,
crate::frontend::trim::LineEndpoint::Start,
&edit_scene_graph_delta.new_graph.objects,
default_unit,
),
crate::frontend::trim::get_position_coords_for_line(
seg,
crate::frontend::trim::LineEndpoint::End,
&edit_scene_graph_delta.new_graph.objects,
default_unit,
),
) {
let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
* (right_trim_coords_value.x - start_coords.x)
+ (right_trim_coords_value.y - start_coords.y)
* (right_trim_coords_value.y - start_coords.y))
.sqrt();
if dist_to_start < endpoint_epsilon {
if let crate::frontend::sketch::Segment::Line(line) = segment {
intersection_point_id = Some(line.start);
}
} else {
let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
* (right_trim_coords_value.x - end_coords.x)
+ (right_trim_coords_value.y - end_coords.y)
* (right_trim_coords_value.y - end_coords.y))
.sqrt();
if dist_to_end < endpoint_epsilon
&& let crate::frontend::sketch::Segment::Line(line) = segment
{
intersection_point_id = Some(line.end);
}
}
}
}
crate::frontend::sketch::Segment::Arc(_) => {
if let (Some(start_coords), Some(end_coords)) = (
crate::frontend::trim::get_position_coords_from_arc(
seg,
crate::frontend::trim::ArcPoint::Start,
&edit_scene_graph_delta.new_graph.objects,
default_unit,
),
crate::frontend::trim::get_position_coords_from_arc(
seg,
crate::frontend::trim::ArcPoint::End,
&edit_scene_graph_delta.new_graph.objects,
default_unit,
),
) {
let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
* (right_trim_coords_value.x - start_coords.x)
+ (right_trim_coords_value.y - start_coords.y)
* (right_trim_coords_value.y - start_coords.y))
.sqrt();
if dist_to_start < endpoint_epsilon {
if let crate::frontend::sketch::Segment::Arc(arc) = segment {
intersection_point_id = Some(arc.start);
}
} else {
let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
* (right_trim_coords_value.x - end_coords.x)
+ (right_trim_coords_value.y - end_coords.y)
* (right_trim_coords_value.y - end_coords.y))
.sqrt();
if dist_to_end < endpoint_epsilon
&& let crate::frontend::sketch::Segment::Arc(arc) = segment
{
intersection_point_id = Some(arc.end);
}
}
}
}
_ => {}
}
}
}
}
let right_coincident_segments = if let Some(point_id) = intersection_point_id {
vec![new_segment_start_point_id.into(), point_id.into()]
} else if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
other_segment_point_id,
..
} = &**right_side
{
vec![new_segment_start_point_id.into(), (*other_segment_point_id).into()]
} else {
vec![new_segment_start_point_id.into(), right_intersecting_seg_id.into()]
};
batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
segments: right_coincident_segments,
}));
let mut points_constrained_to_new_segment_start = std::collections::HashSet::new();
let mut points_constrained_to_new_segment_end = std::collections::HashSet::new();
if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
other_segment_point_id,
..
} = &**right_side
{
points_constrained_to_new_segment_start.insert(other_segment_point_id);
}
for constraint_to_migrate in constraints_to_migrate.iter() {
if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::End
&& constraint_to_migrate.is_point_point
{
points_constrained_to_new_segment_end.insert(constraint_to_migrate.other_entity_id);
}
}
for constraint_to_migrate in constraints_to_migrate.iter() {
if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment
&& (points_constrained_to_new_segment_start.contains(&constraint_to_migrate.other_entity_id)
|| points_constrained_to_new_segment_end.contains(&constraint_to_migrate.other_entity_id))
{
continue; }
let constraint_segments = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment {
vec![constraint_to_migrate.other_entity_id.into(), new_segment_id.into()]
} else {
let target_endpoint_id = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Start
{
new_segment_start_point_id
} else {
new_segment_end_point_id
};
vec![target_endpoint_id.into(), constraint_to_migrate.other_entity_id.into()]
};
batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
segments: constraint_segments,
}));
}
let mut distance_constraints_to_re_add: Vec<(
crate::frontend::api::Number,
crate::frontend::sketch::ConstraintSource,
)> = Vec::new();
if let (Some(original_start_id), Some(original_end_id)) =
(original_segment_start_point_id, original_segment_end_point_id)
{
for obj in &edit_scene_graph_delta.new_graph.objects {
let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let Constraint::Distance(distance) = constraint else {
continue;
};
let references_start = distance.contains_point(original_start_id);
let references_end = distance.contains_point(original_end_id);
if references_start && references_end {
distance_constraints_to_re_add.push((distance.distance, distance.source.clone()));
}
}
}
if let Some(original_start_id) = original_segment_start_point_id {
for (distance_value, source) in distance_constraints_to_re_add {
batch_constraints.push(Constraint::Distance(crate::frontend::sketch::Distance {
points: vec![original_start_id.into(), new_segment_end_point_id.into()],
distance: distance_value,
source,
}));
}
}
if let Some(new_center_id) = new_segment_center_point_id {
for (constraint, original_center_id) in center_point_constraints_to_migrate {
let center_rewrite_map = std::collections::HashMap::from([(original_center_id, new_center_id)]);
if let Some(rewritten) = rewrite_constraint_with_map(&constraint, ¢er_rewrite_map)
&& matches!(rewritten, Constraint::Coincident(_) | Constraint::Distance(_))
{
batch_constraints.push(rewritten);
}
}
}
let mut angle_rewrite_map = std::collections::HashMap::from([(*segment_id, new_segment_id)]);
if let Some(original_end_id) = original_segment_end_point_id {
angle_rewrite_map.insert(original_end_id, new_segment_end_point_id);
}
for obj in &edit_scene_graph_delta.new_graph.objects {
let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
continue;
};
let should_migrate = match constraint {
Constraint::Parallel(parallel) => parallel.lines.contains(segment_id),
Constraint::Perpendicular(perpendicular) => perpendicular.lines.contains(segment_id),
Constraint::Horizontal(Horizontal::Line { line }) => line == segment_id,
Constraint::Horizontal(Horizontal::Points { points }) => original_segment_end_point_id
.is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
Constraint::Vertical(Vertical::Line { line }) => line == segment_id,
Constraint::Vertical(Vertical::Points { points }) => original_segment_end_point_id
.is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
_ => false,
};
if should_migrate
&& let Some(migrated_constraint) = rewrite_constraint_with_map(constraint, &angle_rewrite_map)
&& matches!(
migrated_constraint,
Constraint::Parallel(_)
| Constraint::Perpendicular(_)
| Constraint::Horizontal(_)
| Constraint::Vertical(_)
)
{
batch_constraints.push(migrated_constraint);
}
}
let constraint_object_ids: Vec<ObjectId> = constraints_to_delete.to_vec();
let batch_result = frontend
.batch_split_segment_operations(
ctx,
version,
sketch_id,
Vec::new(), batch_constraints,
constraint_object_ids,
crate::frontend::sketch::NewSegmentInfo {
segment_id: new_segment_id,
start_point_id: new_segment_start_point_id,
end_point_id: new_segment_end_point_id,
center_point_id: new_segment_center_point_id,
},
)
.await
.map_err(|e| format!("Failed to batch split segment operations: {}", e.error.message()));
if let Ok((_, ref batch_delta)) = batch_result {
invalidates_ids = invalidates_ids || batch_delta.invalidates_ids;
}
batch_result
}
};
match operation_result {
Ok((source_delta, scene_graph_delta)) => {
invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
last_result = Some((source_delta, scene_graph_delta.clone()));
}
Err(e) => {
crate::logln!("Error executing trim operation {}: {}", op_index, e);
}
}
op_index += consumed_ops;
}
let (source_delta, mut scene_graph_delta) =
last_result.ok_or_else(|| "No operations were executed successfully".to_string())?;
scene_graph_delta.invalidates_ids = invalidates_ids;
Ok((source_delta, scene_graph_delta))
}