use super::super::*;
use super::{BoundaryNode, boundary_for_node, intersect_for_layout_shape};
pub(in crate::svg::parity::flowchart) fn apply_flowchart_elk_endpoint_cutter(
ctx: &FlowchartRenderCtx<'_>,
edge: &crate::flowchart::FlowEdge,
origin_x: f64,
origin_y: f64,
normalize_cyclic_special: bool,
base_points: &[crate::model::LayoutPoint],
out: &mut Vec<crate::model::LayoutPoint>,
) {
out.clear();
out.extend_from_slice(base_points);
if base_points.len() < 2 {
return;
}
let Some(start_bounds) = boundary_for_node(
ctx,
edge.from.as_str(),
origin_x,
origin_y,
normalize_cyclic_special,
) else {
return;
};
let Some(end_bounds) = boundary_for_node(
ctx,
edge.to.as_str(),
origin_x,
origin_y,
normalize_cyclic_special,
) else {
return;
};
let start_shape = ctx
.nodes_by_id
.get(edge.from.as_str())
.and_then(|node| node.layout_shape.as_deref());
let end_shape = ctx
.nodes_by_id
.get(edge.to.as_str())
.and_then(|node| node.layout_shape.as_deref());
let start_center = crate::model::LayoutPoint {
x: start_bounds.x,
y: start_bounds.y,
};
let end_center = crate::model::LayoutPoint {
x: end_bounds.x,
y: end_bounds.y,
};
out.clear();
if !point_close(
base_points.first().unwrap_or(&start_center),
&start_center,
1e-6,
) {
out.push(start_center.clone());
}
out.extend_from_slice(base_points);
if !point_close(base_points.last().unwrap_or(&end_center), &end_center, 1e-6) {
out.push(end_center.clone());
}
let prev_points = out.clone();
apply_start_intersection(ctx, edge.from.as_str(), start_shape, &start_bounds, out);
apply_end_intersection(ctx, edge.to.as_str(), end_shape, &end_bounds, out);
trim_too_close_tail(out);
dedup_consecutive_points_in_place(out);
if out.len() < 2 || out.iter().any(|p| !p.x.is_finite() || !p.y.is_finite()) {
out.clear();
out.extend(prev_points);
dedup_consecutive_points_in_place(out);
}
}
fn apply_start_intersection(
ctx: &FlowchartRenderCtx<'_>,
node_id: &str,
shape: Option<&str>,
bounds: &BoundaryNode,
points: &mut Vec<crate::model::LayoutPoint>,
) {
let Some(first_outside) = points.iter().position(|point| outside_node(bounds, point)) else {
return;
};
let outside = points[first_outside].clone();
let center = points[0].clone();
let value = node_intersection(ctx, node_id, shape, bounds, &outside, ¢er);
replace_endpoint(points, Endpoint::Start, value);
}
fn apply_end_intersection(
ctx: &FlowchartRenderCtx<'_>,
node_id: &str,
shape: Option<&str>,
bounds: &BoundaryNode,
points: &mut Vec<crate::model::LayoutPoint>,
) {
let outside = points
.iter()
.rposition(|point| outside_node(bounds, point))
.or_else(|| (points.len() > 1).then_some(points.len() - 2));
let Some(outside) = outside else {
return;
};
let outside = points[outside].clone();
let center = points[points.len() - 1].clone();
let value = node_intersection(ctx, node_id, shape, bounds, &outside, ¢er);
replace_endpoint(points, Endpoint::End, value);
}
fn node_intersection(
ctx: &FlowchartRenderCtx<'_>,
node_id: &str,
shape: Option<&str>,
bounds: &BoundaryNode,
outside: &crate::model::LayoutPoint,
center: &crate::model::LayoutPoint,
) -> crate::model::LayoutPoint {
let outside = ensure_truly_outside(bounds, outside);
let candidate = intersect_for_layout_shape(ctx, node_id, bounds, shape, &outside);
if node_intersection_is_usable(bounds, &outside, &candidate) {
candidate
} else {
fallback_intersection(bounds, &outside, center)
}
}
#[derive(Debug, Clone, Copy)]
enum Endpoint {
Start,
End,
}
fn replace_endpoint(
points: &mut Vec<crate::model::LayoutPoint>,
endpoint: Endpoint,
value: crate::model::LayoutPoint,
) {
if points.is_empty() {
return;
}
match endpoint {
Endpoint::Start => {
if point_close(&points[0], &value, 0.1) {
points.remove(0);
} else {
points[0] = value;
}
}
Endpoint::End => {
let last = points.len() - 1;
if point_close(&points[last], &value, 0.1) {
points.pop();
} else {
points[last] = value;
}
}
}
}
fn outside_node(bounds: &BoundaryNode, point: &crate::model::LayoutPoint) -> bool {
let dx = (point.x - bounds.x).abs();
let dy = (point.y - bounds.y).abs();
dx >= bounds.width / 2.0 || dy >= bounds.height / 2.0
}
fn ensure_truly_outside(
bounds: &BoundaryNode,
point: &crate::model::LayoutPoint,
) -> crate::model::LayoutPoint {
const EPS: f64 = 1.0;
const PUSH_OUT: f64 = 10.0;
let dx = (point.x - bounds.x).abs();
let dy = (point.y - bounds.y).abs();
let w = bounds.width / 2.0;
let h = bounds.height / 2.0;
if (dx - w).abs() < EPS || (dy - h).abs() < EPS {
let dir_x = point.x - bounds.x;
let dir_y = point.y - bounds.y;
let len = (dir_x * dir_x + dir_y * dir_y).sqrt();
if len > 0.0 {
return crate::model::LayoutPoint {
x: bounds.x + (dir_x / len) * (len + PUSH_OUT),
y: bounds.y + (dir_y / len) * (len + PUSH_OUT),
};
}
}
point.clone()
}
fn node_intersection_is_usable(
bounds: &BoundaryNode,
outside: &crate::model::LayoutPoint,
candidate: &crate::model::LayoutPoint,
) -> bool {
const EPS: f64 = 1.0;
let wrong_side = (outside.x < bounds.x && candidate.x > bounds.x)
|| (outside.x > bounds.x && candidate.x < bounds.x);
if wrong_side {
return false;
}
let dx = outside.x - candidate.x;
let dy = outside.y - candidate.y;
(dx * dx + dy * dy).sqrt() > EPS
}
fn fallback_intersection(
bounds: &BoundaryNode,
outside: &crate::model::LayoutPoint,
center: &crate::model::LayoutPoint,
) -> crate::model::LayoutPoint {
let inside = make_inside_point(bounds, outside, center);
rect_intersection(bounds, outside, &inside)
}
fn make_inside_point(
bounds: &BoundaryNode,
outside: &crate::model::LayoutPoint,
center: &crate::model::LayoutPoint,
) -> crate::model::LayoutPoint {
const EPS: f64 = 1.0;
let is_vertical = (outside.x - bounds.x).abs() < EPS;
let is_horizontal = (outside.y - bounds.y).abs() < EPS;
crate::model::LayoutPoint {
x: if is_vertical {
outside.x
} else if outside.x < bounds.x {
bounds.x - bounds.width / 4.0
} else {
bounds.x + bounds.width / 4.0
},
y: if is_horizontal { outside.y } else { center.y },
}
}
fn rect_intersection(
bounds: &BoundaryNode,
outside: &crate::model::LayoutPoint,
inside: &crate::model::LayoutPoint,
) -> crate::model::LayoutPoint {
let x = bounds.x;
let y = bounds.y;
let w = bounds.width / 2.0;
let h = bounds.height / 2.0;
let q_total = (outside.y - inside.y).abs();
let r_total = (outside.x - inside.x).abs();
if (y - outside.y).abs() * w > (x - outside.x).abs() * h {
let q = if inside.y < outside.y {
outside.y - h - y
} else {
y - h - outside.y
};
let r = (r_total * q) / q_total;
let mut res = crate::model::LayoutPoint {
x: if inside.x < outside.x {
inside.x + r
} else {
inside.x - r_total + r
},
y: if inside.y < outside.y {
inside.y + q_total - q
} else {
inside.y - q_total + q
},
};
if r_total == 0.0 {
res.x = outside.x;
}
if q_total == 0.0 {
res.y = outside.y;
}
res
} else {
let r = if inside.x < outside.x {
outside.x - w - x
} else {
x - w - outside.x
};
let q = (q_total * r) / r_total;
let mut res = crate::model::LayoutPoint {
x: if inside.x < outside.x {
inside.x + r_total - r
} else {
inside.x - r_total + r
},
y: if inside.y < outside.y {
inside.y + q
} else {
inside.y - q
},
};
if r_total == 0.0 {
res.x = outside.x;
}
if q_total == 0.0 {
res.y = outside.y;
}
res
}
}
fn trim_too_close_tail(points: &mut Vec<crate::model::LayoutPoint>) {
if points.len() <= 1 {
return;
}
let last = points[points.len() - 1].clone();
let prev = points[points.len() - 2].clone();
if (last.x - prev.x).hypot(last.y - prev.y) < 2.0 {
points.pop();
}
}
fn dedup_consecutive_points_in_place(points: &mut Vec<crate::model::LayoutPoint>) {
if points.len() < 2 {
return;
}
let mut write = 1usize;
for read in 1..points.len() {
if point_close(&points[read], &points[write - 1], 1e-6) {
continue;
}
if write != read {
points[write] = points[read].clone();
}
write += 1;
}
points.truncate(write);
}
fn point_close(a: &crate::model::LayoutPoint, b: &crate::model::LayoutPoint, eps: f64) -> bool {
(a.x - b.x).abs() <= eps && (a.y - b.y).abs() <= eps
}