use crate::types::{Length as Inches, Point, Scaler};
use facet_svg::PathData;
use glam::{DVec2, dvec2};
use super::defaults;
use super::types::*;
#[derive(Debug, Clone, Copy)]
pub enum CompassPoint {
North,
NorthEast,
East,
SouthEast,
South,
SouthWest,
West,
NorthWest,
}
impl CompassPoint {
pub fn from_direction(dir: DVec2) -> Self {
let (dx, dy) = (dir.x, dir.y);
if dx > 0.0 {
if dy >= 2.414 * dx {
CompassPoint::North } else if dy > 0.414 * dx {
CompassPoint::NorthEast } else if dy > -0.414 * dx {
CompassPoint::East } else if dy > -2.414 * dx {
CompassPoint::SouthEast } else {
CompassPoint::South }
} else if dx < 0.0 {
if dy >= -2.414 * dx {
CompassPoint::North } else if dy > -0.414 * dx {
CompassPoint::NorthWest } else if dy > 0.414 * dx {
CompassPoint::West } else if dy > 2.414 * dx {
CompassPoint::SouthWest } else {
CompassPoint::South }
} else {
if dy >= 0.0 {
CompassPoint::North
} else {
CompassPoint::South
}
}
}
pub fn from_svg_direction(center: DVec2, toward: DVec2, half_size: DVec2) -> Self {
let dx = (toward.x - center.x) * half_size.y / half_size.x;
let dy = -(toward.y - center.y);
Self::from_direction(dvec2(dx, dy))
}
}
pub fn chop_line(start: DVec2, end: DVec2, amount: f64) -> (DVec2, DVec2) {
let delta = end - start;
let len = delta.length();
if len < amount * 2.0 {
let mid = (start + end) * 0.5;
return (mid, mid);
}
let unit = delta / len;
let new_start = start + unit * amount;
let new_end = end - unit * amount;
(new_start, new_end)
}
pub fn apply_auto_chop_simple_line(
scaler: &Scaler,
obj: &RenderedObject,
start: DVec2,
end: DVec2,
offset_x: Inches,
max_y: Inches,
) -> (DVec2, DVec2) {
if obj.start_attachment.is_none() && obj.end_attachment.is_none() {
return (start, end);
}
let has_explicit_chop = obj.style().chop;
let has_both_attachments = obj.start_attachment.is_some() && obj.end_attachment.is_some();
let should_chop_start = has_explicit_chop || has_both_attachments;
let should_chop_end = obj.end_attachment.is_some();
let end_center_px = obj
.end_attachment
.as_ref()
.map(|info| info.center.to_svg(scaler, offset_x, max_y))
.unwrap_or(end);
let start_center_px = obj
.start_attachment
.as_ref()
.map(|info| info.center.to_svg(scaler, offset_x, max_y))
.unwrap_or(start);
let mut new_start = start;
if should_chop_start && let Some(ref start_info) = obj.start_attachment {
if let Some(chopped) =
chop_against_endpoint(scaler, start_info, end_center_px, offset_x, max_y)
{
new_start = chopped;
}
}
let mut new_end = end;
if should_chop_end && let Some(ref end_info) = obj.end_attachment {
if let Some(chopped) =
chop_against_endpoint(scaler, end_info, start_center_px, offset_x, max_y)
{
new_end = chopped;
}
}
(new_start, new_end)
}
fn chop_against_box_compass_point(
center: DVec2,
half_size: DVec2,
corner_radius: f64,
toward: DVec2,
) -> Option<DVec2> {
if half_size.x <= 0.0 || half_size.y <= 0.0 {
return None;
}
let compass_point = CompassPoint::from_svg_direction(center, toward, half_size);
let rad = corner_radius.min(half_size.x).min(half_size.y);
let rx = if rad > 0.0 {
0.292_893_218_813_452_54 * rad
} else {
0.0
};
let offset = match compass_point {
CompassPoint::North => dvec2(0.0, -half_size.y),
CompassPoint::NorthEast => dvec2(half_size.x - rx, -half_size.y + rx),
CompassPoint::East => dvec2(half_size.x, 0.0),
CompassPoint::SouthEast => dvec2(half_size.x - rx, half_size.y - rx),
CompassPoint::South => dvec2(0.0, half_size.y),
CompassPoint::SouthWest => dvec2(-half_size.x + rx, half_size.y - rx),
CompassPoint::West => dvec2(-half_size.x, 0.0),
CompassPoint::NorthWest => dvec2(-half_size.x + rx, -half_size.y + rx),
};
Some(center + offset)
}
fn chop_against_file_compass_point(
center: DVec2,
half_size: DVec2,
filerad: f64,
toward: DVec2,
) -> Option<DVec2> {
if half_size.x <= 0.0 || half_size.y <= 0.0 {
return None;
}
let compass_point = CompassPoint::from_svg_direction(center, toward, half_size);
let mn = half_size.x.min(half_size.y);
let mut rx = filerad;
if rx > mn {
rx = mn;
}
if rx < mn * 0.25 {
rx = mn * 0.25;
}
rx *= 0.5;
let offset = match compass_point {
CompassPoint::North => dvec2(0.0, -half_size.y),
CompassPoint::NorthEast => dvec2(half_size.x - rx, -half_size.y + rx), CompassPoint::East => dvec2(half_size.x, 0.0),
CompassPoint::SouthEast => dvec2(half_size.x, half_size.y), CompassPoint::South => dvec2(0.0, half_size.y),
CompassPoint::SouthWest => dvec2(-half_size.x, half_size.y),
CompassPoint::West => dvec2(-half_size.x, 0.0),
CompassPoint::NorthWest => dvec2(-half_size.x, -half_size.y),
};
Some(center + offset)
}
fn chop_against_diamond_compass_point(
center: DVec2,
half_size: DVec2,
toward: DVec2,
) -> Option<DVec2> {
if half_size.x <= 0.0 || half_size.y <= 0.0 {
return None;
}
let compass_point = CompassPoint::from_svg_direction(center, toward, half_size);
let quarter = half_size / 2.0;
let offset = match compass_point {
CompassPoint::North => dvec2(0.0, -half_size.y),
CompassPoint::NorthEast => dvec2(quarter.x, -quarter.y),
CompassPoint::East => dvec2(half_size.x, 0.0),
CompassPoint::SouthEast => dvec2(quarter.x, quarter.y),
CompassPoint::South => dvec2(0.0, half_size.y),
CompassPoint::SouthWest => dvec2(-quarter.x, quarter.y),
CompassPoint::West => dvec2(-half_size.x, 0.0),
CompassPoint::NorthWest => dvec2(-quarter.x, -quarter.y),
};
Some(center + offset)
}
fn chop_against_cylinder_compass_point(
center: DVec2,
half_size: DVec2,
ellipse_ry: f64,
toward: DVec2,
) -> Option<DVec2> {
if half_size.x <= 0.0 || half_size.y <= 0.0 {
return None;
}
let compass_point = CompassPoint::from_svg_direction(center, toward, half_size);
let h2 = half_size.y - ellipse_ry;
let offset = match compass_point {
CompassPoint::North => dvec2(0.0, -half_size.y),
CompassPoint::NorthEast => dvec2(half_size.x, -h2),
CompassPoint::East => dvec2(half_size.x, 0.0),
CompassPoint::SouthEast => dvec2(half_size.x, h2),
CompassPoint::South => dvec2(0.0, half_size.y),
CompassPoint::SouthWest => dvec2(-half_size.x, h2),
CompassPoint::West => dvec2(-half_size.x, 0.0),
CompassPoint::NorthWest => dvec2(-half_size.x, -h2),
};
Some(center + offset)
}
fn chop_against_endpoint(
scaler: &Scaler,
endpoint: &EndpointObject,
toward: DVec2,
offset_x: Inches,
max_y: Inches,
) -> Option<DVec2> {
let center = endpoint.center.to_svg(scaler, offset_x, max_y);
let half_size = dvec2(
scaler.px(endpoint.width / 2.0),
scaler.px(endpoint.height / 2.0),
);
let corner_radius = scaler.px(endpoint.corner_radius);
match endpoint.class {
ClassName::Circle => {
chop_against_ellipse(center, half_size, toward)
}
ClassName::Ellipse => {
chop_against_ellipse(center, half_size, toward)
}
ClassName::Box => {
chop_against_box_compass_point(center, half_size, corner_radius, toward)
}
ClassName::File => {
let filerad = scaler.px(defaults::FILE_RAD);
chop_against_file_compass_point(center, half_size, filerad, toward)
}
ClassName::Cylinder => {
let cylrad = scaler.px(Inches::inches(0.075)); chop_against_cylinder_compass_point(center, half_size, cylrad, toward)
}
ClassName::Oval => {
let oval_radius = half_size.x.min(half_size.y);
chop_against_box_compass_point(center, half_size, oval_radius, toward)
}
ClassName::Diamond => {
chop_against_diamond_compass_point(center, half_size, toward)
}
_ => None,
}
}
fn chop_against_ellipse(center: DVec2, half_size: DVec2, toward: DVec2) -> Option<DVec2> {
if half_size.x <= 0.0 || half_size.y <= 0.0 {
return None;
}
let delta = toward - center;
if delta.x.abs() < f64::EPSILON && delta.y.abs() < f64::EPSILON {
return None;
}
let denom = (delta.x * delta.x) / (half_size.x * half_size.x)
+ (delta.y * delta.y) / (half_size.y * half_size.y);
if denom <= 0.0 {
return None;
}
let scale = 1.0 / denom.sqrt();
Some(center + delta * scale)
}
pub fn create_rounded_box_path(x1: f64, y1: f64, x2: f64, y2: f64, r: f64) -> PathData {
PathData::new()
.m(x1 + r, y2) .l(x2 - r, y2) .a(r, r, 0.0, false, false, x2, y2 - r) .l(x2, y1 + r) .a(r, r, 0.0, false, false, x2 - r, y1) .l(x1 + r, y1) .a(r, r, 0.0, false, false, x1, y1 + r) .l(x1, y2 - r) .a(r, r, 0.0, false, false, x1 + r, y2) .z() }
pub fn create_oval_path(x1: f64, y1: f64, x2: f64, y2: f64, rad: f64) -> PathData {
let xi1 = x1 + rad; let xi2 = x2 - rad; let yi_bottom = y2 - rad; let yi_top = y1 + rad;
let mut path = PathData::new();
path = path.m(xi1, y2);
if xi2 > xi1 {
path = path.l(xi2, y2);
}
path = path.a(rad, rad, 0.0, false, false, x2, yi_bottom);
if yi_bottom > yi_top {
path = path.l(x2, yi_top);
}
path = path.a(rad, rad, 0.0, false, false, xi2, y1);
if xi2 > xi1 {
path = path.l(xi1, y1);
}
path = path.a(rad, rad, 0.0, false, false, x1, yi_top);
if yi_bottom > yi_top {
path = path.l(x1, yi_bottom);
}
path = path.a(rad, rad, 0.0, false, false, xi1, y2);
path = path.z();
path
}
pub fn create_cylinder_paths_with_rad(
cx: f64,
cy: f64,
width: f64,
height: f64,
ry: f64,
) -> (PathData, PathData) {
let rx = width / 2.0;
let h2 = height / 2.0;
let top_y = cy - h2 + ry;
let bottom_y = cy + h2 - ry;
let body_path = PathData::new()
.m(cx - rx, top_y) .l(cx - rx, bottom_y) .a(rx, ry, 0.0, false, false, cx + rx, bottom_y) .l(cx + rx, top_y) .a(rx, ry, 0.0, false, false, cx - rx, top_y) .a(rx, ry, 0.0, false, false, cx + rx, top_y);
let bottom_arc_path = PathData::new();
(body_path, bottom_arc_path)
}
pub fn create_file_paths(
cx: f64,
cy: f64,
width: f64,
height: f64,
fold_size: f64,
) -> (PathData, PathData) {
let left = cx - width / 2.0;
let right = cx + width / 2.0;
let top = cy - height / 2.0;
let bottom = cy + height / 2.0;
let main_path = PathData::new()
.m(left, bottom) .l(right, bottom) .l(right, top + fold_size) .l(right - fold_size, top) .l(left, top) .z();
let fold_path = PathData::new()
.m(right - fold_size, top) .l(right - fold_size, top + fold_size) .l(right, top + fold_size);
(main_path, fold_path)
}
pub fn create_line_path(
waypoints: &[Point<Inches>],
scaler: &Scaler,
offset_x: Inches,
max_y: Inches,
) -> PathData {
if waypoints.is_empty() {
return PathData::new();
}
let points: Vec<DVec2> = waypoints
.iter()
.map(|p| p.to_svg(scaler, offset_x, max_y))
.collect();
let mut path = PathData::new().m(points[0].x, points[0].y);
for p in points.iter().skip(1) {
path = path.l(p.x, p.y);
}
path
}
fn radius_midpoint(from: DVec2, to: DVec2, r: f64) -> (DVec2, bool) {
let delta = to - from;
let dist = delta.length();
if dist <= 0.0 {
return (to, false);
}
let dir = delta / dist;
if r > 0.5 * dist {
let mid = (from + to) * 0.5;
(mid, true)
} else {
let m = to - dir * r;
(m, false)
}
}
pub fn create_spline_path(
waypoints: &[Point<Inches>],
scaler: &Scaler,
offset_x: Inches,
max_y: Inches,
radius: Inches,
) -> PathData {
if waypoints.is_empty() {
return PathData::new();
}
let a: Vec<DVec2> = waypoints
.iter()
.map(|p| p.to_svg(scaler, offset_x, max_y))
.collect();
let n = a.len();
let r = scaler.px(radius);
let mut path = PathData::new().m(a[0].x, a[0].y);
let (m, _) = radius_midpoint(a[0], a[1], r);
path = path.l(m.x, m.y);
let i_last = n - 1;
for i in 1..i_last {
let an = a[i + 1];
let (m, is_mid) = radius_midpoint(an, a[i], r);
path = path.q(a[i].x, a[i].y, m.x, m.y);
if !is_mid {
let (m2, _) = radius_midpoint(a[i], an, r);
path = path.l(m2.x, m2.y);
}
}
path = path.l(a[n - 1].x, a[n - 1].y);
path
}
pub fn arc_control_point(clockwise: bool, from: DVec2, to: DVec2) -> DVec2 {
let midpoint = (from + to) * 0.5;
let delta = to - from;
let perp = DVec2::new(delta.y, -delta.x);
if clockwise {
midpoint + perp * 0.5
} else {
midpoint - perp * 0.5
}
}
pub fn create_arc_path(start: DVec2, end: DVec2, clockwise: bool) -> PathData {
let control = arc_control_point(clockwise, start, end);
PathData::new()
.m(start.x, start.y)
.q(control.x, control.y, end.x, end.y)
}
pub fn create_arc_path_with_control(start: DVec2, control: DVec2, end: DVec2) -> PathData {
PathData::new()
.m(start.x, start.y)
.q(control.x, control.y, end.x, end.y)
}
pub fn autochop_inches(from: PointIn, to: PointIn, endpoint: &EndpointObject) -> PointIn {
let from_vec = dvec2(from.x.raw(), from.y.raw());
let chopped = match endpoint.class {
ClassName::Box
| ClassName::Cylinder
| ClassName::Diamond
| ClassName::File
| ClassName::Oval
| ClassName::Text => box_chop_inches(endpoint, from_vec),
ClassName::Circle | ClassName::Dot => circle_chop_inches(endpoint, from_vec),
ClassName::Ellipse => ellipse_chop_inches(endpoint, from_vec),
_ => None,
};
match chopped {
Some(pt) => PointIn::new(Inches(pt.x), Inches(pt.y)),
None => to,
}
}
fn box_chop_inches(obj: &EndpointObject, toward: DVec2) -> Option<DVec2> {
let center = dvec2(obj.center.x.raw(), obj.center.y.raw());
let w = obj.width.raw();
let h = obj.height.raw();
if w <= 0.0 || h <= 0.0 {
return Some(center);
}
let dx = (toward.x - center.x) * h / w;
let dy = toward.y - center.y;
let cp = CompassPoint::from_direction(dvec2(dx, dy));
let offset = match obj.class {
ClassName::Box | ClassName::Text => box_offset_inches(obj, cp),
ClassName::Cylinder => cylinder_offset_inches(obj, cp),
ClassName::Diamond => diamond_offset_inches(obj, cp),
ClassName::File => file_offset_inches(obj, cp),
ClassName::Oval => oval_offset_inches(obj, cp),
_ => box_offset_inches(obj, cp), };
Some(center + offset)
}
fn circle_chop_inches(obj: &EndpointObject, toward: DVec2) -> Option<DVec2> {
let center = dvec2(obj.center.x.raw(), obj.center.y.raw());
let rad = obj.width.raw() / 2.0;
let dx = toward.x - center.x;
let dy = toward.y - center.y;
let dist = (dx * dx + dy * dy).sqrt();
if dist < rad || dist <= 0.0 {
return Some(center);
}
Some(dvec2(
center.x + dx * rad / dist,
center.y + dy * rad / dist,
))
}
fn ellipse_chop_inches(obj: &EndpointObject, toward: DVec2) -> Option<DVec2> {
let center = dvec2(obj.center.x.raw(), obj.center.y.raw());
let w = obj.width.raw();
let h = obj.height.raw();
if w <= 0.0 || h <= 0.0 {
return Some(center);
}
let dx = toward.x - center.x;
let dy = toward.y - center.y;
let s = h / w;
let dq = dx * s;
let dist = (dq * dq + dy * dy).sqrt();
if dist < h {
return Some(center);
}
Some(dvec2(
center.x + 0.5 * dq * h / (dist * s),
center.y + 0.5 * dy * h / dist,
))
}
fn box_offset_inches(obj: &EndpointObject, cp: CompassPoint) -> DVec2 {
let w2 = obj.width.raw() / 2.0;
let h2 = obj.height.raw() / 2.0;
let rad = obj.corner_radius.raw();
let mn = w2.min(h2);
let rad_clamped = rad.min(mn);
let rx = if rad_clamped > 0.0 {
0.292_893_218_813_452_54 * rad_clamped
} else {
0.0
};
match cp {
CompassPoint::North => dvec2(0.0, h2),
CompassPoint::NorthEast => dvec2(w2 - rx, h2 - rx),
CompassPoint::East => dvec2(w2, 0.0),
CompassPoint::SouthEast => dvec2(w2 - rx, -h2 + rx),
CompassPoint::South => dvec2(0.0, -h2),
CompassPoint::SouthWest => dvec2(-w2 + rx, -h2 + rx),
CompassPoint::West => dvec2(-w2, 0.0),
CompassPoint::NorthWest => dvec2(-w2 + rx, h2 - rx),
}
}
fn cylinder_offset_inches(obj: &EndpointObject, cp: CompassPoint) -> DVec2 {
let w2 = obj.width.raw() / 2.0;
let h2 = obj.height.raw() / 2.0;
let default_cylrad = 0.075;
let rad = obj.corner_radius.raw().max(default_cylrad);
let h2_inner = h2 - rad;
match cp {
CompassPoint::North => dvec2(0.0, h2),
CompassPoint::NorthEast => dvec2(w2, h2_inner),
CompassPoint::East => dvec2(w2, 0.0),
CompassPoint::SouthEast => dvec2(w2, -h2_inner),
CompassPoint::South => dvec2(0.0, -h2),
CompassPoint::SouthWest => dvec2(-w2, -h2_inner),
CompassPoint::West => dvec2(-w2, 0.0),
CompassPoint::NorthWest => dvec2(-w2, h2_inner),
}
}
fn diamond_offset_inches(obj: &EndpointObject, cp: CompassPoint) -> DVec2 {
let w2 = obj.width.raw() / 2.0;
let h2 = obj.height.raw() / 2.0;
let w4 = w2 / 2.0;
let h4 = h2 / 2.0;
match cp {
CompassPoint::North => dvec2(0.0, h2),
CompassPoint::NorthEast => dvec2(w4, h4),
CompassPoint::East => dvec2(w2, 0.0),
CompassPoint::SouthEast => dvec2(w4, -h4),
CompassPoint::South => dvec2(0.0, -h2),
CompassPoint::SouthWest => dvec2(-w4, -h4),
CompassPoint::West => dvec2(-w2, 0.0),
CompassPoint::NorthWest => dvec2(-w4, h4),
}
}
fn file_offset_inches(obj: &EndpointObject, cp: CompassPoint) -> DVec2 {
let w2 = obj.width.raw() / 2.0;
let h2 = obj.height.raw() / 2.0;
let mn = w2.min(h2);
let mut rx = defaults::FILE_RAD.raw();
if rx > mn {
rx = mn;
}
if rx < mn * 0.25 {
rx = mn * 0.25;
}
rx *= 0.5;
match cp {
CompassPoint::North => dvec2(0.0, h2),
CompassPoint::NorthEast => dvec2(w2 - rx, h2 - rx), CompassPoint::East => dvec2(w2, 0.0),
CompassPoint::SouthEast => dvec2(w2, -h2), CompassPoint::South => dvec2(0.0, -h2),
CompassPoint::SouthWest => dvec2(-w2, -h2),
CompassPoint::West => dvec2(-w2, 0.0),
CompassPoint::NorthWest => dvec2(-w2, h2),
}
}
fn oval_offset_inches(obj: &EndpointObject, cp: CompassPoint) -> DVec2 {
let w2 = obj.width.raw() / 2.0;
let h2 = obj.height.raw() / 2.0;
let rad = w2.min(h2);
let rx = 0.292_893_218_813_452_54 * rad;
match cp {
CompassPoint::North => dvec2(0.0, h2),
CompassPoint::NorthEast => dvec2(w2 - rx, h2 - rx),
CompassPoint::East => dvec2(w2, 0.0),
CompassPoint::SouthEast => dvec2(w2 - rx, -h2 + rx),
CompassPoint::South => dvec2(0.0, -h2),
CompassPoint::SouthWest => dvec2(-w2 + rx, -h2 + rx),
CompassPoint::West => dvec2(-w2, 0.0),
CompassPoint::NorthWest => dvec2(-w2 + rx, h2 - rx),
}
}