use super::{
Diagram, Graph, SUBLABEL_SIZE, count,
graph::Chain,
measure::{Model, Spacing},
place::Plan,
ports::Ports,
};
use crate::{
error::ChartWarning,
metrics::TextMetrics,
scene::Element,
spec::{ChartSpec, DiagramOrientation},
};
const SPREAD: f64 = 0.3;
const MOST_SPREAD: f64 = 24.0;
const STEP: f64 = 12.0;
pub(crate) struct Block {
pub node: usize,
pub size: (f64, f64),
pub elements: Vec<Element>,
pub members: Vec<Member>,
}
pub(crate) struct Member {
pub node: usize,
pub bounds: (f64, f64, f64, f64),
pub open: [bool; 4],
}
pub(crate) struct Whole {
pub nodes: Vec<usize>,
pub ends: Vec<(usize, usize)>,
pub all_ends: Vec<(usize, usize)>,
pub labels: Vec<String>,
}
impl Block {
pub(crate) fn new(
spec: &ChartSpec,
inner: &Diagram,
node: usize,
metrics: &impl TextMetrics,
warnings: &mut Vec<ChartWarning>,
) -> Self {
let spacing = Spacing::new(spec.width, inner.spacing);
let model = Model::new(inner, (spacing, spec.number_style()), metrics, warnings);
let landscape = inner.orientation == DiagramOrientation::Landscape;
let mut canvas = spec.clone();
canvas.width = 0;
canvas.height = 0;
let plan = Plan::new(&canvas, &model, landscape, 0.0, metrics);
let mut elements = Vec::new();
plan.draw(&model, metrics, warnings, &mut elements);
let (left, top, right, bottom) = reach(&elements);
let elements = placed(&elements, (-left, -top));
let size = (right - left, bottom - top);
let boxes: Vec<(f64, f64, f64, f64)> = (0..inner.nodes.len())
.map(|node| {
let (x, y, width, height) = plan.node_box(&model, node);
(x - left, y - top, width, height)
})
.collect();
let obstacles = obstacles(&elements, metrics);
let whole = inner
.whole
.as_ref()
.expect("a block names its nodes in the whole");
let members = boxes
.iter()
.enumerate()
.map(|(index, &bounds)| {
let others: Vec<(f64, f64, f64, f64)> = boxes
.iter()
.enumerate()
.filter(|(other, _)| *other != index)
.map(|(_, &(x, y, width, height))| (x, y, x + width, y + height))
.chain(obstacles.iter().copied())
.collect();
Member {
node: whole.nodes[index],
bounds,
open: open(bounds, size, &others),
}
})
.collect();
Self {
node,
size,
elements,
members,
}
}
fn member(&self, node: usize) -> usize {
self.members
.iter()
.position(|member| member.node == node)
.expect("an edge into a block joins one of its members")
}
fn meeting(&self, chains: &[Option<Chain>], whole: &Whole) -> Vec<(usize, bool, usize)> {
let mut meeting = Vec::new();
for (edge, chain) in chains.iter().enumerate() {
let Some(chain) = chain else { continue };
let (from, to) = whole.ends[edge];
let (first, last) = if chain.reversed {
(to, from)
} else {
(from, to)
};
if chain.items[0] == self.node {
meeting.push((self.member(first), true, edge));
}
if chain.items[chain.items.len() - 1] == self.node {
meeting.push((self.member(last), false, edge));
}
}
meeting
}
}
const fn side(leaving: bool, landscape: bool) -> usize {
match (landscape, leaving) {
(false, false) => 0,
(true, true) => 1,
(false, true) => 2,
(true, false) => 3,
}
}
pub(crate) fn blocks_reachable(diagram: &Diagram) -> [bool; 2] {
let Some(whole) = &diagram.whole else {
return [true, true];
};
let graph = Graph::new(diagram);
[false, true].map(|landscape| {
diagram.blocks.iter().all(|block| {
block
.meeting(&graph.chains, whole)
.into_iter()
.all(|(member, leaving, _)| block.members[member].open[side(leaving, landscape)])
})
})
}
impl Ports {
pub(super) fn blocks(&mut self, graph: &Graph, model: &Model, landscape: bool) {
let diagram = model.diagram;
let Some(whole) = &diagram.whole else { return };
for block in &diagram.blocks {
let (width, height) = block.size;
let meeting = block.meeting(&graph.chains, whole);
for &(member, leaving, edge) in &meeting {
let (x, y, member_width, member_height) = block.members[member].bounds;
let (middle, across, depth, breadth) = if landscape {
(
x + member_width / 2.0 - width / 2.0,
y + member_height / 2.0 - height / 2.0,
member_width,
member_height,
)
} else {
(
y + member_height / 2.0 - height / 2.0,
x + member_width / 2.0 - width / 2.0,
member_height,
member_width,
)
};
let siblings: Vec<usize> = meeting
.iter()
.filter(|(other, way, _)| *other == member && *way == leaving)
.map(|(_, _, edge)| *edge)
.collect();
let index = siblings
.iter()
.position(|sibling| *sibling == edge)
.expect("the edge is among them");
let spread = 2.0 * (breadth * SPREAD).min(MOST_SPREAD);
let step = STEP.min(spread / count(siblings.len().max(2) - 1));
let offset = across + (count(index) - count(siblings.len() - 1) / 2.0) * step;
if leaving {
self.leave[edge] = offset;
self.leave_into[edge] = Some(middle + depth / 2.0);
} else {
self.reach[edge] = offset;
self.reach_into[edge] = Some(middle - depth / 2.0);
}
}
}
}
}
fn open(
(x, y, width, height): (f64, f64, f64, f64),
(block_width, block_height): (f64, f64),
others: &[(f64, f64, f64, f64)],
) -> [bool; 4] {
let (half_width, half_height) = (
(width * SPREAD).min(MOST_SPREAD),
(height * SPREAD).min(MOST_SPREAD),
);
let (middle_x, middle_y) = (x + width / 2.0, y + height / 2.0);
let bands = [
(middle_x - half_width, 0.0, middle_x + half_width, y),
(
x + width,
middle_y - half_height,
block_width,
middle_y + half_height,
),
(
middle_x - half_width,
y + height,
middle_x + half_width,
block_height,
),
(0.0, middle_y - half_height, x, middle_y + half_height),
];
bands.map(|band| {
!others.iter().any(|other| {
band.0 < other.2 && other.0 < band.2 && band.1 < other.3 && other.1 < band.3
})
})
}
fn obstacles(elements: &[Element], metrics: &impl TextMetrics) -> Vec<(f64, f64, f64, f64)> {
let mut obstacles = Vec::new();
for element in elements {
match element {
Element::Polyline(line)
if line.class.starts_with("chartlet-flow-edge")
|| line.class.starts_with("chartlet-flow-head") =>
{
obstacles.extend(line.points.windows(2).map(|pair| {
(
pair[0].0.min(pair[1].0),
pair[0].1.min(pair[1].1),
pair[0].0.max(pair[1].0),
pair[0].1.max(pair[1].1),
)
}));
}
Element::Rect(rect) if rect.class == "chartlet-diagram-chip" => {
obstacles.push((rect.x, rect.y, rect.x + rect.width, rect.y + rect.height));
}
Element::Text(text) if text.class == "chartlet-flow-group-label" => {
let width = metrics.width(&text.content, SUBLABEL_SIZE);
obstacles.push((text.x, text.y - 11.0, text.x + width, text.y + 3.0));
}
_ => {}
}
}
obstacles
}
fn reach(elements: &[Element]) -> (f64, f64, f64, f64) {
let mut reach = (
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
);
let mut take = |(x, y): (f64, f64)| {
reach = (
reach.0.min(x),
reach.1.min(y),
reach.2.max(x),
reach.3.max(y),
);
};
for element in elements {
match element {
Element::Rect(rect) => {
take((rect.x, rect.y));
take((rect.x + rect.width, rect.y + rect.height));
}
Element::Polyline(line) => {
line.points.iter().copied().for_each(&mut take);
}
Element::Line(line) => {
take((line.x1, line.y1));
take((line.x2, line.y2));
}
Element::Circle(circle) => {
take((circle.cx - circle.radius, circle.cy - circle.radius));
take((circle.cx + circle.radius, circle.cy + circle.radius));
}
_ => {}
}
}
reach
}
pub(super) fn placed(elements: &[Element], (dx, dy): (f64, f64)) -> Vec<Element> {
let point = |(x, y): (f64, f64)| (x + dx, y + dy);
elements
.iter()
.cloned()
.map(|mut element| {
match &mut element {
Element::Circle(circle) => {
(circle.cx, circle.cy) = point((circle.cx, circle.cy));
}
Element::Line(line) => {
(line.x1, line.y1) = point((line.x1, line.y1));
(line.x2, line.y2) = point((line.x2, line.y2));
}
Element::Polyline(line) => {
line.points.iter_mut().for_each(|at| *at = point(*at));
}
Element::Rect(rect) => (rect.x, rect.y) = point((rect.x, rect.y)),
Element::SeriesText(text, _) | Element::Text(text) => {
(text.x, text.y) = point((text.x, text.y));
}
Element::Hotspot(hotspot) => {
(hotspot.x, hotspot.y) = point((hotspot.x, hotspot.y));
}
Element::Symbol(symbol) => (symbol.x, symbol.y) = point((symbol.x, symbol.y)),
Element::Hook(_)
| Element::Group(_)
| Element::NodeGroup(..)
| Element::EdgeGroup(..)
| Element::GroupEnd => {}
}
element
})
.collect()
}