use super::values::num;
use crate::resolve::{MarkerKind, Markers};
use std::fmt::Write;
#[derive(Clone, Copy)]
pub struct MarkerPaint<'a> {
pub color: &'a str,
pub inline: bool,
pub thickness: f64,
}
pub fn marker_anchor(
from: (f64, f64),
to: (f64, f64),
at_start: bool,
) -> Option<((f64, f64), (f64, f64))> {
let (anchor, neighbor) = if at_start { (from, to) } else { (to, from) };
let dx = anchor.0 - neighbor.0;
let dy = anchor.1 - neighbor.1;
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-9 {
return Some((anchor, (1.0, 0.0)));
}
Some((anchor, (dx / len, dy / len)))
}
pub fn marker_size(thickness: f64) -> f64 {
5.0_f64.max(thickness * 4.0) + 1.0
}
pub fn datum_size(thickness: f64) -> f64 {
11.0_f64.max(thickness * 5.5)
}
const DOT_RADIUS: f64 = 0.375;
const CIRCLE_RADIUS: f64 = 0.5;
const CROW_DEPTH: f64 = 1.0; const CROW_SPREAD: f64 = 0.55; const BAR_HALF: f64 = 0.5; const RING_R: f64 = 0.4;
pub const MARKER_OVERLAP: f64 = 0.5;
pub fn line_inset(kind: MarkerKind, thickness: f64) -> f64 {
match kind {
MarkerKind::None => 0.0,
MarkerKind::Datum => datum_size(thickness),
k if k.is_open() => marker_size(thickness) * open_back_extent(k),
_ => thickness * 2.0,
}
}
fn open_back_extent(kind: MarkerKind) -> f64 {
match kind {
MarkerKind::One | MarkerKind::ExactlyOne => 0.0,
MarkerKind::Crow | MarkerKind::OneOrMany => CROW_DEPTH,
MarkerKind::ZeroOrOne => 1.5 + RING_R,
MarkerKind::ZeroOrMany => 1.7 + RING_R,
_ => CROW_DEPTH,
}
}
pub fn shorten_for_markers(
path: &[(f64, f64)],
markers: &Markers,
thickness: f64,
overlap: f64,
) -> Vec<(f64, f64)> {
let inset = |kind| (line_inset(kind, thickness) - overlap).max(0.0);
let mut p = path.to_vec();
if p.len() < 2 {
return p;
}
if markers.end != MarkerKind::None {
let n = p.len();
if let Some(q) = pulled_back(p[n - 2], p[n - 1], inset(markers.end)) {
p[n - 1] = q;
}
}
if markers.start != MarkerKind::None
&& let Some(q) = pulled_back(p[1], p[0], inset(markers.start))
{
p[0] = q;
}
p
}
fn pulled_back(inner: (f64, f64), endpoint: (f64, f64), amount: f64) -> Option<(f64, f64)> {
let (dx, dy) = (endpoint.0 - inner.0, endpoint.1 - inner.1);
let len = (dx * dx + dy * dy).sqrt();
if len <= amount + 0.5 {
return None;
}
Some((
endpoint.0 - dx / len * amount,
endpoint.1 - dy / len * amount,
))
}
fn round_center(tip: (f64, f64), direction: (f64, f64), r: f64) -> (f64, f64) {
(tip.0 - direction.0 * r, tip.1 - direction.1 * r)
}
fn emit_round(
out: &mut String,
indent: &str,
suffix: &str,
tip: (f64, f64),
direction: (f64, f64),
r: f64,
fill: &str,
) {
let (cx, cy) = round_center(tip, direction, r);
writeln!(
out,
r#"{}<circle class="lini-marker lini-marker-{}" cx="{}" cy="{}" r="{}"{}/>"#,
indent,
suffix,
num(cx),
num(cy),
num(r),
fill,
)
.unwrap();
}
pub fn emit_marker(
out: &mut String,
indent: &str,
kind: MarkerKind,
tip: (f64, f64),
direction: (f64, f64),
paint: &MarkerPaint,
) {
let MarkerPaint {
color,
inline,
thickness,
} = *paint;
let size = if kind == MarkerKind::Datum {
datum_size(thickness)
} else {
marker_size(thickness)
};
let ux = direction.0;
let uy = direction.1;
let px = -uy;
let py = ux;
let fill = if inline {
format!(r#" style="fill: {color}""#)
} else {
String::new()
};
match kind {
MarkerKind::Arrow => {
let bx = tip.0 - ux * size;
let by = tip.1 - uy * size;
let lx = bx + px * size * 0.5;
let ly = by + py * size * 0.5;
let rx = bx - px * size * 0.5;
let ry = by - py * size * 0.5;
writeln!(
out,
r#"{}<polygon class="lini-marker lini-marker-arrow" points="{},{} {},{} {},{}"{}/>"#,
indent,
num(tip.0), num(tip.1),
num(lx), num(ly),
num(rx), num(ry),
fill,
).unwrap();
}
MarkerKind::Dot => {
emit_round(out, indent, "dot", tip, direction, size * DOT_RADIUS, &fill);
}
MarkerKind::Circle => {
emit_round(
out,
indent,
"circle",
tip,
direction,
size * CIRCLE_RADIUS,
&fill,
);
}
MarkerKind::Datum => {
let ax = tip.0 - ux * size;
let ay = tip.1 - uy * size;
let half = size * 0.5;
writeln!(
out,
r#"{}<polygon class="lini-marker lini-marker-datum" points="{},{} {},{} {},{}"{}/>"#,
indent,
num(tip.0 + px * half), num(tip.1 + py * half),
num(tip.0 - px * half), num(tip.1 - py * half),
num(ax), num(ay),
fill,
).unwrap();
}
MarkerKind::Diamond => {
let bx = tip.0 - ux * size;
let by = tip.1 - uy * size;
let mx = (tip.0 + bx) / 2.0;
let my = (tip.1 + by) / 2.0;
let lx = mx + px * size * 0.425;
let ly = my + py * size * 0.425;
let rx = mx - px * size * 0.425;
let ry = my - py * size * 0.425;
writeln!(
out,
r#"{}<polygon class="lini-marker lini-marker-diamond" points="{},{} {},{} {},{} {},{}"{}/>"#,
indent,
num(tip.0), num(tip.1),
num(lx), num(ly),
num(bx), num(by),
num(rx), num(ry),
fill,
).unwrap();
}
MarkerKind::Crow => {
open_path(out, indent, &crow_d(tip, (ux, uy), (px, py), size));
}
MarkerKind::One => {
open_path(out, indent, &bar_d(tip, (ux, uy), (px, py), size, 0.7));
}
MarkerKind::ExactlyOne => {
open_path(out, indent, &bar_d(tip, (ux, uy), (px, py), size, 0.7));
open_path(out, indent, &bar_d(tip, (ux, uy), (px, py), size, 1.2));
}
MarkerKind::ZeroOrOne => {
open_ring(out, indent, tip, (ux, uy), size, 1.5);
open_stub(out, indent, tip, (ux, uy), size, 1.5 - RING_R, 0.0);
open_path(out, indent, &bar_d(tip, (ux, uy), (px, py), size, 0.6));
}
MarkerKind::OneOrMany => {
open_path(out, indent, &crow_d(tip, (ux, uy), (px, py), size));
open_path(out, indent, &bar_d(tip, (ux, uy), (px, py), size, 1.5));
}
MarkerKind::ZeroOrMany => {
open_path(out, indent, &crow_d(tip, (ux, uy), (px, py), size));
open_ring(out, indent, tip, (ux, uy), size, 1.7);
open_stub(out, indent, tip, (ux, uy), size, 1.7 - RING_R, CROW_DEPTH);
}
MarkerKind::None => {}
}
}
fn open_stub(
out: &mut String,
indent: &str,
tip: (f64, f64),
dir: (f64, f64),
size: f64,
from: f64,
to: f64,
) {
let (ax, ay) = (tip.0 - dir.0 * size * from, tip.1 - dir.1 * size * from);
let (bx, by) = (tip.0 - dir.0 * size * to, tip.1 - dir.1 * size * to);
open_path(
out,
indent,
&format!("M {} {} L {} {}", num(ax), num(ay), num(bx), num(by)),
);
}
fn open_path(out: &mut String, indent: &str, d: &str) {
writeln!(
out,
r#"{}<path class="lini-marker lini-marker-open" d="{}"/>"#,
indent, d
)
.unwrap();
}
fn open_ring(
out: &mut String,
indent: &str,
tip: (f64, f64),
dir: (f64, f64),
size: f64,
offset: f64,
) {
let (cx, cy) = (tip.0 - dir.0 * size * offset, tip.1 - dir.1 * size * offset);
writeln!(
out,
r#"{}<circle class="lini-marker lini-marker-open" cx="{}" cy="{}" r="{}"/>"#,
indent,
num(cx),
num(cy),
num(size * RING_R),
)
.unwrap();
}
fn crow_d(tip: (f64, f64), dir: (f64, f64), perp: (f64, f64), size: f64) -> String {
let (depth, spread) = (size * CROW_DEPTH, size * CROW_SPREAD);
let (cx, cy) = (tip.0 - dir.0 * depth, tip.1 - dir.1 * depth);
let (lx, ly) = (tip.0 + perp.0 * spread, tip.1 + perp.1 * spread);
let (rx, ry) = (tip.0 - perp.0 * spread, tip.1 - perp.1 * spread);
format!(
"M {} {} L {} {} L {} {} M {} {} L {} {}",
num(lx),
num(ly),
num(cx),
num(cy),
num(rx),
num(ry),
num(tip.0),
num(tip.1),
num(cx),
num(cy),
)
}
fn bar_d(tip: (f64, f64), dir: (f64, f64), perp: (f64, f64), size: f64, offset: f64) -> String {
let half = size * BAR_HALF;
let (bx, by) = (tip.0 - dir.0 * size * offset, tip.1 - dir.1 * size * offset);
format!(
"M {} {} L {} {}",
num(bx + perp.0 * half),
num(by + perp.1 * half),
num(bx - perp.0 * half),
num(by - perp.1 * half),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dot_sits_tangent_to_the_edge_on_the_link_side() {
let r = marker_size(1.0) * DOT_RADIUS;
let (cx, cy) = round_center((100.0, 50.0), (1.0, 0.0), r);
assert!((cx - (100.0 - r)).abs() < 1e-9, "centre pulled back by r");
assert!((cy - 50.0).abs() < 1e-9);
assert!(
(cx + r - 100.0).abs() < 1e-9,
"leading edge tangent to the shape edge, not past it"
);
}
#[test]
fn line_stops_a_uniform_stub_short_of_every_marker() {
assert_eq!(line_inset(MarkerKind::Dot, 1.0), 2.0);
assert_eq!(line_inset(MarkerKind::Circle, 1.0), 2.0);
assert_eq!(line_inset(MarkerKind::Arrow, 1.0), 2.0);
assert_eq!(line_inset(MarkerKind::None, 1.0), 0.0);
}
#[test]
fn a_circle_marker_is_a_fuller_dot() {
let size = marker_size(1.0);
assert!(
size * CIRCLE_RADIUS > size * DOT_RADIUS,
"circle radius exceeds the dot's"
);
}
#[test]
fn open_er_markers_are_stroked_via_the_shared_class() {
let paint = MarkerPaint {
color: "red",
inline: true,
thickness: 2.0,
};
for kind in [
MarkerKind::Crow,
MarkerKind::One,
MarkerKind::ExactlyOne,
MarkerKind::ZeroOrOne,
MarkerKind::OneOrMany,
MarkerKind::ZeroOrMany,
] {
let mut s = String::new();
emit_marker(&mut s, "", kind, (100.0, 50.0), (1.0, 0.0), &paint);
assert!(
s.contains("lini-marker-open"),
"{kind:?} uses the open class"
);
assert!(
!s.contains("fill"),
"{kind:?} is stroked, never inline-filled"
);
}
let mut s = String::new();
emit_marker(
&mut s,
"",
MarkerKind::ZeroOrMany,
(100.0, 50.0),
(1.0, 0.0),
&paint,
);
assert_eq!(s.matches("lini-marker-open").count(), 3);
let mut s = String::new();
emit_marker(
&mut s,
"",
MarkerKind::ExactlyOne,
(100.0, 50.0),
(1.0, 0.0),
&paint,
);
assert_eq!(s.matches("lini-marker-open").count(), 2);
}
#[test]
fn the_datum_triangle_bases_on_the_feature_apex_at_the_leader() {
let paint = MarkerPaint {
color: "black",
inline: false,
thickness: 1.0,
};
let mut s = String::new();
emit_marker(
&mut s,
"",
MarkerKind::Datum,
(100.0, 50.0),
(1.0, 0.0),
&paint,
);
let size = datum_size(1.0);
assert!(s.contains("lini-marker-datum"), "{s}");
assert!(
s.contains(&format!("{},{}", num(100.0 - size), num(50.0))),
"apex a head back along the line: {s}"
);
assert_eq!(
line_inset(MarkerKind::Datum, 1.0),
size,
"the leader stops at the apex"
);
}
#[test]
fn open_marker_line_inset_clears_the_whole_marker() {
assert!(line_inset(MarkerKind::Crow, 2.0) > line_inset(MarkerKind::Arrow, 2.0));
assert!(line_inset(MarkerKind::ZeroOrMany, 2.0) >= line_inset(MarkerKind::Crow, 2.0));
}
}