Files
deckcraft/crates/engine/src/cmd/smartart/layout.rs
T

830 lines
38 KiB
Rust

use super::diagrams::{hierarchy, org_chart};
use super::nodes::{Item, groups};
use super::paint::{accent, deco, line_shape, paint, picture_shape, text_shape};
use super::{Align, ColorRef, ColorTransform, Diagram, DiagramNode, Fill, Geom, Line, Xfrm};
#[derive(Clone, Copy)]
struct L<'a> {
d: &'a Diagram,
area: Xfrm,
gs: &'a [Vec<usize>],
n: usize,
nf: f64,
}
impl L<'_> {
fn nodes(&self, g: &[usize]) -> Vec<&DiagramNode> {
g.iter().filter_map(|i| self.d.nodes.get(*i)).collect()
}
/// A titled shape for top-level group `i`, painted in its accent.
fn titled(&self, out: &mut Vec<Item>, preset: &str, x: Xfrm, i: usize, g: &[usize]) {
let mut sh = text_shape(preset, x, &self.nodes(g), true);
paint(self.d, &mut sh, i, false);
out.push((sh, g.to_vec()));
}
fn arrow(&self, out: &mut Vec<Item>, x: Xfrm, rot: f64, i: usize) {
let mut sh = deco("rightArrow", x, rot);
paint(self.d, &mut sh, i, true);
out.push((sh, vec![]));
}
}
pub(super) fn layout_raw(d: &Diagram, area: Xfrm) -> Vec<Item> {
let gs = groups(d);
let n = gs.len().max(1);
let l = L { d, area, gs: &gs, n, nf: n as f64 };
let mut out: Vec<Item> = vec![];
match d.kind.as_str() {
"hierarchy" => return hierarchy(d, area, false),
"horizontalHierarchy" => return hierarchy(d, area, true),
"orgChart" | "halfCircleOrgChart" | "nameTitleOrgChart" => return org_chart(d, area),
"list" | "hBulletList" => arm_list(&l, &mut out),
"blockList" => arm_block_list(&l, &mut out),
"process" | "chevron" => arm_process(&l, &mut out),
"verticalProcess" => arm_vertical_process(&l, &mut out),
"stepUp" => arm_step_up(&l, &mut out),
"cycle" | "blockCycle" => arm_cycle(&l, &mut out),
"radial" => arm_radial(&l, &mut out),
"pyramid" | "invertedPyramid" => arm_pyramid(&l, &mut out),
"matrix" | "titledMatrix" => arm_matrix(&l, &mut out),
"pictureList" => arm_picture_list(&l, &mut out),
"pictureCaption" | "pictureProcess" => arm_picture_caption(&l, &mut out),
"textCycle" | "continuousCycle" => arm_text_cycle(&l, &mut out),
"segmentedCycle" | "pie" => arm_segmented_cycle(&l, &mut out),
"multidirectionalCycle" => arm_multidirectional_cycle(&l, &mut out),
"cycleMatrix" => arm_cycle_matrix(&l, &mut out),
"segmentedPyramid" => arm_segmented_pyramid(&l, &mut out),
"verticalBoxList" => arm_vertical_box_list(&l, &mut out),
"trapezoidList" | "tableList" => arm_trapezoid_list(&l, &mut out),
"accentProcess" => arm_accent_process(&l, &mut out),
"continuousBlockProcess" => arm_continuous_block_process(&l, &mut out),
"bendingProcess" => arm_bending_process(&l, &mut out),
"linearVenn" | "stackedVenn" => arm_linear_venn(&l, &mut out),
"nestedTarget" => arm_nested_target(&l, &mut out),
"gridMatrix" => arm_grid_matrix(&l, &mut out),
"gear" => arm_gear(&l, &mut out),
"funnel" => arm_funnel(&l, &mut out),
"pictureHList" => arm_picture_h_list(&l, &mut out),
"timeline" => arm_timeline(&l, &mut out),
"venn" => arm_venn(&l, &mut out),
"target" => arm_target(&l, &mut out),
_ => {}
}
out
}
fn arm_list(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, nf, .. } = *l;
// Vertical / Horizontal Bullet List: a coloured title bar, sub-points in a pale box below.
let vertical = d.kind == "list";
for (i, g) in gs.iter().enumerate() {
let (cell, bar_h) = if vertical {
(Xfrm::new(area.x, area.y + i as f64 * area.h / nf, area.w, area.h / nf * 0.92), (area.h / nf * 0.92).min(area.h * 0.12).max(18.0))
} else {
let w = area.w / nf;
(Xfrm::new(area.x + i as f64 * w + w * 0.03, area.y, w * 0.94, area.h), area.h * 0.22)
};
let has_sub = g.len() > 1;
let bar_h = bar_h.min(cell.h);
l.titled(out, "roundRect", Xfrm::new(cell.x, cell.y, cell.w, bar_h), i, &g[..1]);
if has_sub {
let pad = cell.w * 0.03;
let x = Xfrm::new(cell.x + pad, cell.y + bar_h, cell.w - 2.0 * pad, (cell.h - bar_h).max(1.0));
let mut sh = text_shape("rect", x, &l.nodes(&g[1..]), false);
paint(d, &mut sh, i, true);
out.push((sh, g[1..].to_vec()));
}
}
}
fn arm_block_list(l: &L, out: &mut Vec<Item>) {
let L { area, gs, n, nf, .. } = *l;
// Basic Block List: equal blocks in rows, as square as the area allows.
let cols = ((nf * area.w / area.h.max(1.0) / 1.6).sqrt().ceil() as usize).clamp(1, n);
let rows = n.div_ceil(cols);
let (w, h) = (area.w / cols as f64, area.h / rows as f64);
let (bw, bh) = (w * 0.9, (h * 0.85).min(w * 0.9 * 0.6));
for (i, g) in gs.iter().enumerate() {
let (c, r) = (i % cols, i / cols);
let in_row = if r + 1 == rows { n - r * cols } else { cols };
let x0 = area.x + (area.w - in_row as f64 * w) / 2.0;
l.titled(out, "rect", Xfrm::new(x0 + c as f64 * w + (w - bw) / 2.0, area.y + r as f64 * h + (h - bh) / 2.0, bw, bh), i, g);
}
}
fn arm_process(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, n, nf } = *l;
let chevron = d.kind == "chevron";
let gap = if chevron { -area.w * 0.02 } else { area.w * 0.06 };
let w = ((area.w - gap * (nf - 1.0)) / nf).max(1.0);
let h = (area.h * 0.5).min(w * if chevron { 0.45 } else { 0.65 });
let y = area.y + (area.h - h) / 2.0;
for (i, g) in gs.iter().enumerate() {
let x = area.x + i as f64 * (w + gap);
l.titled(out, if chevron { "chevron" } else { "roundRect" }, Xfrm::new(x, y, w, h), i, g);
if !chevron && i + 1 < n {
let aw = gap * 0.7;
l.arrow(out, Xfrm::new(x + w + gap * 0.15, y + h / 2.0 - aw * 0.45, aw, aw * 0.9), 0.0, i);
}
}
}
fn arm_vertical_process(l: &L, out: &mut Vec<Item>) {
let L { area, gs, n, nf, .. } = *l;
let gap = area.h * 0.06;
let h = ((area.h - gap * (nf - 1.0)) / nf).max(1.0);
let w = area.w * 0.6;
let x = area.x + (area.w - w) / 2.0;
for (i, g) in gs.iter().enumerate() {
let y = area.y + i as f64 * (h + gap);
l.titled(out, "roundRect", Xfrm::new(x, y, w, h), i, g);
if i + 1 < n {
let s = gap * 0.8;
// A right arrow turned to point down.
l.arrow(out, Xfrm::new(area.x + area.w / 2.0 - s / 2.0, y + h + gap * 0.1, s, s), 90.0, i);
}
}
}
fn arm_step_up(l: &L, out: &mut Vec<Item>) {
let L { area, gs, nf, .. } = *l;
// Step Up Process: each step higher than the one before.
let w = area.w / nf;
let h = area.h / (nf + 1.0) * 1.6;
for (i, g) in gs.iter().enumerate() {
let y = area.y + area.h - h - i as f64 * (area.h - h) / (nf - 1.0).max(1.0);
l.titled(out, "rect", Xfrm::new(area.x + i as f64 * w + w * 0.04, y, w * 0.92, h), i, g);
}
}
fn arm_cycle(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, n, nf } = *l;
let block = d.kind == "blockCycle";
let r = area.h.min(area.w) * 0.36;
let (cx, cy) = (area.x + area.w / 2.0, area.y + area.h / 2.0);
let dd = (r * if block { 0.75 } else { 0.8 }).min(2.0 * std::f64::consts::PI * r / nf * 0.62);
for (i, g) in gs.iter().enumerate() {
let a = (i as f64 / nf * 360.0 - 90.0).to_radians();
let (w, h) = if block { (dd * 1.3, dd * 0.75) } else { (dd, dd) };
l.titled(out, if block { "roundRect" } else { "ellipse" }, Xfrm::new(cx + r * a.cos() - w / 2.0, cy + r * a.sin() - h / 2.0, w, h), i, g);
if n > 1 {
// An arrow halfway to the next node, along the circle.
let m = ((i as f64 + 0.5) / nf * 360.0 - 90.0).to_radians();
let s = (dd * 0.35).max(6.0);
let rot = (i as f64 + 0.5) / nf * 360.0;
l.arrow(out, Xfrm::new(cx + r * m.cos() - s / 2.0, cy + r * m.sin() - s * 0.4, s, s * 0.8), rot, i);
}
}
}
fn arm_radial(l: &L, out: &mut Vec<Item>) {
let L { area, gs, .. } = *l;
// Basic Radial: the first node in the middle, the others around it with spokes.
let Some((centre, rest)) = gs.split_first() else { return };
let m = rest.len().max(1) as f64;
let r = area.h.min(area.w) * 0.36;
let (cx, cy) = (area.x + area.w / 2.0, area.y + area.h / 2.0);
let dc = r * 0.75;
let dd = (r * 0.75).min(2.0 * std::f64::consts::PI * r / m * 0.7);
for (k, _) in rest.iter().enumerate() {
let a = (k as f64 / m * 360.0 - 90.0).to_radians();
let (ex, ey) = (cx + (r - dd / 2.0) * a.cos(), cy + (r - dd / 2.0) * a.sin());
out.push((line_shape((cx + dc / 2.0 * a.cos(), cy + dc / 2.0 * a.sin()), (ex, ey)), vec![]));
}
l.titled(out, "ellipse", Xfrm::new(cx - dc / 2.0, cy - dc / 2.0, dc, dc), 0, centre);
for (k, g) in rest.iter().enumerate() {
let a = (k as f64 / m * 360.0 - 90.0).to_radians();
l.titled(out, "ellipse", Xfrm::new(cx + r * a.cos() - dd / 2.0, cy + r * a.sin() - dd / 2.0, dd, dd), k + 1, g);
}
}
fn arm_pyramid(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, nf, .. } = *l;
// One triangle cut into bands; inverted, the wide band is on top. Each band is drawn as
// its own polygon so the sides keep the triangle's slope and the text stays upright.
let inverted = d.kind == "invertedPyramid";
let h = area.h / nf;
let pw = area.h.min(area.w) * 1.15;
let px = area.x + (area.w - pw) / 2.0;
for (i, g) in gs.iter().enumerate() {
let (a, b) = (i as f64 / nf, (i as f64 + 1.0) / nf);
let (t, bt) = if inverted { (1.0 - a, 1.0 - b) } else { (a, b) };
let (wt, wb) = (pw * t, pw * bt);
let w = wt.max(wb).max(1.0);
let bh = h * 0.96;
let x = px + (pw - w) / 2.0;
let (it, ib) = ((w - wt) / 2.0, (w - wb) / 2.0);
let path = format!("M {it:.2} 0 L {:.2} 0 L {:.2} {bh:.2} L {ib:.2} {bh:.2} Z", w - it, w - ib);
let mut sh = text_shape("rect", Xfrm::new(x, area.y + i as f64 * h, w, bh), &l.nodes(g), true);
sh.geom = Geom::Custom {
paths: vec![deckcraft_model::CustomPath { w, h: bh, d: path, fill: deckcraft_geom::preset::FillMode::Norm, stroke: true }],
};
// Keep the text inside the sloped sides: inset it to a bit past the band's mid width.
// The tip band is a triangle: its text sits at the wide end, inset to the width there.
let tip = wt.min(wb) < 1e-6;
let inset = if tip { w * 0.22 } else { (it + ib) / 2.0 + (it - ib).abs() / 4.0 + 4.0 };
if let Some(b) = &mut sh.text {
b.body.inset_l = Some(inset);
b.body.inset_r = Some(inset);
if tip {
b.body.anchor = Some(if inverted { deckcraft_model::text::Anchor::Top } else { deckcraft_model::text::Anchor::Bottom });
}
}
paint(d, &mut sh, i, false);
out.push((sh, g.clone()));
}
}
fn arm_matrix(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, .. } = *l;
let titled_m = d.kind == "titledMatrix";
let s = area.h.min(area.w);
let (x0, y0) = (area.x + (area.w - s) / 2.0, area.y + (area.h - s) / 2.0);
let (quads, title): (&[Vec<usize>], Option<&Vec<usize>>) =
if titled_m { gs.split_first().map(|(t, r)| (r, Some(t))).unwrap_or((&[], None)) } else { (gs, None) };
for (i, g) in quads.iter().take(4).enumerate() {
let x = Xfrm::new(x0 + (i % 2) as f64 * s / 2.0 + 3.0, y0 + (i / 2) as f64 * s / 2.0 + 3.0, s / 2.0 - 6.0, s / 2.0 - 6.0);
l.titled(out, "roundRect", x, i + usize::from(titled_m), g);
}
if let Some(t) = title {
let c = s * 0.42;
let mut sh = text_shape("roundRect", Xfrm::new(x0 + (s - c) / 2.0, y0 + (s - c * 0.6) / 2.0, c, c * 0.6), &l.nodes(t), true);
paint(d, &mut sh, 0, true);
out.push((sh, t.clone()));
}
}
fn arm_picture_list(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, nf, .. } = *l;
// Vertical Picture List: a picture square at the left of each row, the text beside it.
let h = area.h / nf;
let bh = h * 0.88;
for (i, g) in gs.iter().enumerate() {
let y = area.y + i as f64 * h;
let s = bh.min(area.w * 0.3);
out.push((picture_shape(d, "rect", Xfrm::new(area.x, y, s, bh), i, g[0]), vec![]));
l.titled(out, "roundRect", Xfrm::new(area.x + s + bh * 0.08, y, (area.w - s - bh * 0.08).max(1.0), bh), i, g);
}
}
fn arm_picture_caption(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, n, nf } = *l;
// Picture Caption List: a picture over each caption, in rows. Picture Accent Process: one
// row, arrows between the steps.
let process = d.kind == "pictureProcess";
let cols = if process { n } else { ((nf * area.w / area.h.max(1.0) / 1.2).sqrt().ceil() as usize).clamp(1, n) };
let rows = n.div_ceil(cols);
let gap = if process { area.w * 0.05 } else { 0.0 };
let w = (area.w - gap * (cols as f64 - 1.0)) / cols as f64;
let h = area.h / rows as f64;
let bw = if process { w } else { w * 0.9 };
let ch = (h * 0.9).min(bw * 1.1);
for (i, g) in gs.iter().enumerate() {
let (c, r) = (i % cols, i / cols);
let in_row = if r + 1 == rows { n - r * cols } else { cols };
let x0 = area.x + (area.w - in_row as f64 * w - gap * (in_row as f64 - 1.0)) / 2.0;
let x = x0 + c as f64 * (w + gap) + (w - bw) / 2.0;
let y = area.y + r as f64 * h + (h - ch) / 2.0;
let ph = ch * 0.62;
out.push((
picture_shape(d, if process { "ellipse" } else { "rect" }, Xfrm::new(x + (bw - ph.min(bw)) / 2.0, y, ph.min(bw), ph), i, g[0]),
vec![],
));
l.titled(out, if process { "roundRect" } else { "rect" }, Xfrm::new(x, y + ph + ch * 0.03, bw, ch * 0.35), i, g);
if process && i + 1 < n {
let a = (gap * 0.7).min(ch * 0.2);
l.arrow(out, Xfrm::new(x + bw + (gap - a) / 2.0, y + ph / 2.0 - a / 2.0, a, a), 0.0, i);
}
}
}
fn arm_text_cycle(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, n, nf } = *l;
// Text Cycle: the text alone around a circle, arrows between. Continuous Cycle: boxes on
// a ring.
let text_only = d.kind == "textCycle";
let r = area.h.min(area.w) * 0.36;
let (cx, cy) = (area.x + area.w / 2.0, area.y + area.h / 2.0);
let dd = (r * 0.8).min(2.0 * std::f64::consts::PI * r / nf * 0.62);
if !text_only {
let ring = r * 2.0 + dd * 0.3;
let mut sh = deco("donut", Xfrm::new(cx - ring / 2.0, cy - ring / 2.0, ring, ring), 0.0);
sh.geom = Geom::Preset { name: "donut".into(), adj: vec![7000.0] };
paint(d, &mut sh, 0, true);
out.push((sh, vec![]));
}
for (i, g) in gs.iter().enumerate() {
let a = (i as f64 / nf * 360.0 - 90.0).to_radians();
let (w, h) = (dd * 1.35, dd * 0.7);
let mut sh = text_shape("roundRect", Xfrm::new(cx + r * a.cos() - w / 2.0, cy + r * a.sin() - h / 2.0, w, h), &l.nodes(g), true);
paint(d, &mut sh, i, false);
if text_only {
sh.fill = Some(Fill::None);
sh.line = Some(Line { fill: Some(Fill::None), ..Default::default() });
sh.effects = None;
let c = ColorRef::scheme(accent(d, i)).with(ColorTransform::LumMod(75_000));
if let Some(b) = &mut sh.text {
for p in &mut b.paragraphs {
for r in &mut p.runs {
r.props.fill = Some(Fill::solid(c.clone()));
}
}
}
if n > 1 {
let m = ((i as f64 + 0.5) / nf * 360.0 - 90.0).to_radians();
let s = (dd * 0.3).max(6.0);
l.arrow(out, Xfrm::new(cx + r * m.cos() - s / 2.0, cy + r * m.sin() - s * 0.4, s, s * 0.8), (i as f64 + 0.5) / nf * 360.0, i);
}
}
out.push((sh, g.clone()));
}
}
fn arm_segmented_cycle(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, nf, .. } = *l;
// Segmented Cycle: a ring cut into one arc per node. Basic Pie: a disc cut into wedges.
// The text sits in a clear box over each segment's middle.
let ring = d.kind == "segmentedCycle";
let s = area.h.min(area.w);
let (cx, cy) = (area.x + area.w / 2.0, area.y + area.h / 2.0);
let gap = if ring { 2.0 } else { 0.0 };
for (i, g) in gs.iter().enumerate() {
// Angles clockwise from 3 o'clock, starting at the top.
let a0 = i as f64 / nf * 360.0 - 90.0 + gap / 2.0;
let a1 = (i as f64 + 1.0) / nf * 360.0 - 90.0 - gap / 2.0;
let mut sh = deco(if ring { "blockArc" } else { "pie" }, Xfrm::new(cx - s / 2.0, cy - s / 2.0, s, s), 0.0);
let ang = |a: f64| (a.rem_euclid(360.0) * 60_000.0).round();
// blockArc runs from its first angle to its second clockwise; so does pie.
sh.geom = Geom::Preset {
name: if ring { "blockArc" } else { "pie" }.into(),
adj: if ring { vec![ang(a0), ang(a1), 25_000.0] } else { vec![ang(a0), ang(a1)] },
};
paint(d, &mut sh, i, false);
out.push((sh, vec![]));
let m = ((a0 + a1) / 2.0).to_radians();
let rr = if ring { s * 0.4375 } else { s * 0.3 };
let tw = (s * if ring { 0.22 } else { 0.3 }).min(std::f64::consts::PI * 2.0 * rr / nf * 0.9);
let th = s * 0.16;
let mut t = text_shape("rect", Xfrm::new(cx + rr * m.cos() - tw / 2.0, cy + rr * m.sin() - th / 2.0, tw, th), &l.nodes(g), true);
paint(d, &mut t, i, false);
t.fill = Some(Fill::None);
t.line = Some(Line { fill: Some(Fill::None), ..Default::default() });
t.effects = None;
out.push((t, g.clone()));
}
}
fn arm_multidirectional_cycle(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, n, nf } = *l;
// Multidirectional Cycle: nodes around a circle, double-headed arrows between them.
let r = area.h.min(area.w) * 0.36;
let (cx, cy) = (area.x + area.w / 2.0, area.y + area.h / 2.0);
let dd = (r * 0.75).min(2.0 * std::f64::consts::PI * r / nf * 0.55);
for (i, g) in gs.iter().enumerate() {
let a = (i as f64 / nf * 360.0 - 90.0).to_radians();
l.titled(out, "roundRect", Xfrm::new(cx + r * a.cos() - dd * 0.65, cy + r * a.sin() - dd * 0.4, dd * 1.3, dd * 0.8), i, g);
if n > 1 {
let m = ((i as f64 + 0.5) / nf * 360.0 - 90.0).to_radians();
let s = (dd * 0.5).max(8.0);
let mut sh =
deco("leftRightArrow", Xfrm::new(cx + r * m.cos() - s / 2.0, cy + r * m.sin() - s * 0.2, s, s * 0.4), (i as f64 + 0.5) / nf * 360.0);
paint(d, &mut sh, i, true);
out.push((sh, vec![]));
}
}
}
fn arm_cycle_matrix(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, .. } = *l;
// Cycle Matrix: four quarters of a circle in the middle with the top-level text, each
// quarter's sub-points in a box at its corner.
let s = area.h.min(area.w);
let (cx, cy) = (area.x + area.w / 2.0, area.y + area.h / 2.0);
let c = s * 0.56;
for (i, g) in gs.iter().take(4).enumerate() {
let (sx, sy) = ([-1.0, 1.0, 1.0, -1.0][i], [-1.0, -1.0, 1.0, 1.0][i]);
let b = s * 0.4;
let bx = Xfrm::new(cx + if sx < 0.0 { -s / 2.0 } else { s / 2.0 - b }, cy + if sy < 0.0 { -s / 2.0 } else { s / 2.0 - b }, b, b);
if g.len() > 1 {
let mut sh = text_shape("roundRect", bx, &l.nodes(&g[1..]), false);
paint(d, &mut sh, i, true);
if let Some(body) = &mut sh.text {
// Text in the box's outer part, clear of the circle.
body.body.inset_l = Some(if sx > 0.0 { b * 0.35 } else { 6.0 });
body.body.inset_r = Some(if sx < 0.0 { b * 0.35 } else { 6.0 });
body.body.anchor = Some(if sy < 0.0 { deckcraft_model::text::Anchor::Top } else { deckcraft_model::text::Anchor::Bottom });
}
out.push((sh, g[1..].to_vec()));
}
}
for (i, g) in gs.iter().take(4).enumerate() {
let (sx, sy) = ([-1.0, 1.0, 1.0, -1.0][i], [-1.0, -1.0, 1.0, 1.0][i]);
let a0 = [180.0, 270.0, 0.0, 90.0][i] + 1.5;
let mut sh = deco("pie", Xfrm::new(cx - c / 2.0, cy - c / 2.0, c, c), 0.0);
sh.geom = Geom::Preset { name: "pie".into(), adj: vec![a0 * 60_000.0, (a0 + 87.0) * 60_000.0] };
paint(d, &mut sh, i, false);
out.push((sh, vec![]));
let q = c * 0.3;
let mut t = text_shape("rect", Xfrm::new(cx + sx * c * 0.2 - q / 2.0, cy + sy * c * 0.2 - q / 2.0, q, q), &l.nodes(&g[..1]), true);
paint(d, &mut t, i, false);
t.fill = Some(Fill::None);
t.line = Some(Line { fill: Some(Fill::None), ..Default::default() });
t.effects = None;
out.push((t, g[..1].to_vec()));
}
}
fn arm_segmented_pyramid(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, nf, .. } = *l;
// Segmented Pyramid: a triangle cut into rows of triangles (1, 3, 5, …), one per node,
// every other one upside down.
let rows = (nf.sqrt().ceil() as usize).max(1);
let s = area.h.min(area.w / 1.15);
let pw = s * 1.15;
let (x0, y0) = (area.x + (area.w - pw) / 2.0, area.y + (area.h - s) / 2.0);
let (tw, th) = (pw / rows as f64, s / rows as f64);
let mut k = 0;
'rows: for r in 0..rows {
let left = x0 + pw / 2.0 - (r as f64 + 1.0) * tw / 2.0;
for c in 0..(2 * r + 1) {
let Some(g) = gs.get(k) else { break 'rows };
let x = Xfrm::new(left + c as f64 * tw / 2.0, y0 + r as f64 * th, tw, th);
let mut sh = text_shape("rect", x, &l.nodes(g), true);
// Drawn as a polygon (not a flipped preset) so upside-down text stays upright.
let path = if c % 2 == 1 {
format!("M 0 0 L {tw:.2} 0 L {:.2} {th:.2} Z", tw / 2.0)
} else {
format!("M {:.2} 0 L {tw:.2} {th:.2} L 0 {th:.2} Z", tw / 2.0)
};
sh.geom = Geom::Custom {
paths: vec![deckcraft_model::CustomPath { w: tw, h: th, d: path, fill: deckcraft_geom::preset::FillMode::Norm, stroke: true }],
};
if let Some(b) = &mut sh.text {
// The text in the triangle's wide half.
b.body.inset_l = Some(tw * 0.22);
b.body.inset_r = Some(tw * 0.22);
if c % 2 == 1 {
b.body.anchor = Some(deckcraft_model::text::Anchor::Top);
b.body.inset_b = Some(th * 0.35);
} else {
b.body.anchor = Some(deckcraft_model::text::Anchor::Bottom);
b.body.inset_t = Some(th * 0.35);
}
}
paint(d, &mut sh, k, false);
out.push((sh, g.clone()));
k += 1;
}
}
}
fn arm_vertical_box_list(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, nf, .. } = *l;
// Vertical Box List: a pale box per node with its sub-points, its title on a tab
// overlapping the box's top left.
let h = area.h / nf;
for (i, g) in gs.iter().enumerate() {
let y = area.y + i as f64 * h;
let tab_h = (h * 0.3).min(40.0);
let mut bx = text_shape("rect", Xfrm::new(area.x, y + tab_h / 2.0, area.w, h * 0.9 - tab_h / 2.0), &l.nodes(&g[1..]), false);
paint(d, &mut bx, i, true);
if let Some(b) = &mut bx.text {
b.body.inset_t = Some(tab_h * 0.6);
}
out.push((bx, g[1..].to_vec()));
l.titled(out, "roundRect", Xfrm::new(area.x + area.w * 0.05, y, area.w * 0.6, tab_h), i, &g[..1]);
}
}
fn arm_trapezoid_list(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, .. } = *l;
// Trapezoid List: a column per node, each a trapezoid with its sub-points. Table List: the
// first node as a header bar over a column per other node.
let table = d.kind == "tableList";
let (head, cols): (Option<&Vec<usize>>, &[Vec<usize>]) =
if table { gs.split_first().map(|(h, r)| (Some(h), r)).unwrap_or((None, &[])) } else { (None, gs) };
let top = if head.is_some() { area.h * 0.28 } else { 0.0 };
if let Some(h) = head {
l.titled(out, "rect", Xfrm::new(area.x, area.y, area.w, top * 0.9), 0, h);
}
let m = cols.len().max(1) as f64;
let w = area.w / m;
for (k, g) in cols.iter().enumerate() {
let i = k + usize::from(table);
let x = Xfrm::new(area.x + k as f64 * w + w * 0.02, area.y + top, w * 0.96, area.h - top);
let mut sh = text_shape(if table { "rect" } else { "trapezoid" }, x, &l.nodes(g), true);
if let Some(b) = &mut sh.text {
b.body.anchor = Some(deckcraft_model::text::Anchor::Top);
if !table {
b.body.inset_l = Some(x.w * 0.2);
b.body.inset_r = Some(x.w * 0.2);
b.body.inset_t = Some(x.h * 0.08);
}
}
paint(d, &mut sh, i, table);
out.push((sh, g.clone()));
}
}
fn arm_accent_process(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, n, nf } = *l;
// Accent Process: a title box per step with its sub-points in a pale box below and to the
// right, arrows between the titles.
let gap = area.w * 0.05;
let w = (area.w - gap * (nf - 1.0)) / nf;
for (i, g) in gs.iter().enumerate() {
let x = area.x + i as f64 * (w + gap);
let th = area.h * 0.22;
if g.len() > 1 {
let mut sh = text_shape("rect", Xfrm::new(x + w * 0.12, area.y + th * 0.7, w * 0.88, area.h - th * 0.7), &l.nodes(&g[1..]), false);
paint(d, &mut sh, i, true);
if let Some(b) = &mut sh.text {
b.body.inset_t = Some(th * 0.45);
}
out.push((sh, g[1..].to_vec()));
}
l.titled(out, "roundRect", Xfrm::new(x, area.y, w * 0.85, th), i, &g[..1]);
if i + 1 < n {
let a = gap * 0.8;
l.arrow(out, Xfrm::new(x + w + (gap - a) / 2.0 - w * 0.07, area.y + th / 2.0 - a / 2.0, a, a), 0.0, i);
}
}
}
fn arm_continuous_block_process(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, nf, .. } = *l;
// Continuous Block Process: one big pale arrow behind a row of boxes.
let mut big = deco("rightArrow", Xfrm::new(area.x, area.y + area.h * 0.15, area.w, area.h * 0.7), 0.0);
paint(d, &mut big, 0, true);
out.push((big, vec![]));
let w = area.w * 0.85 / nf;
let h = area.h * 0.32;
for (i, g) in gs.iter().enumerate() {
let x = area.x + area.w * 0.02 + i as f64 * w;
l.titled(out, "roundRect", Xfrm::new(x + w * 0.05, area.y + (area.h - h) / 2.0, w * 0.9, h), i, g);
}
}
fn arm_bending_process(l: &L, out: &mut Vec<Item>) {
let L { area, gs, n, nf, .. } = *l;
// Basic Bending Process: rows of boxes read left to right, wrapping, arrows between.
let cols = ((nf * area.w / area.h.max(1.0) / 1.5).sqrt().ceil() as usize).clamp(1, n);
let rows = n.div_ceil(cols);
let (cw, rh) = (area.w / cols as f64, area.h / rows as f64);
let (bw, bh) = (cw * 0.72, (rh * 0.75).min(cw * 0.72 * 0.6));
for (i, g) in gs.iter().enumerate() {
let (c, r) = (i % cols, i / cols);
let x = area.x + c as f64 * cw;
let y = area.y + r as f64 * rh + (rh - bh) / 2.0;
l.titled(out, "rect", Xfrm::new(x, y, bw, bh), i, g);
if i + 1 < n {
let a = (cw - bw) * 0.7;
if c + 1 < cols {
l.arrow(out, Xfrm::new(x + bw + (cw - bw - a) / 2.0, y + bh / 2.0 - a / 2.0, a, a), 0.0, i);
} else {
// The bend: down from the row's last box towards the next row's first.
let a = (rh - bh).min(a * 1.5).max(6.0);
l.arrow(out, Xfrm::new(x + bw / 2.0 - a / 2.0, y + bh + (rh - bh - a) / 2.0, a, a), 90.0, i);
}
}
}
}
fn arm_linear_venn(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, n, nf } = *l;
// Linear Venn: overlapping circles in a row. Stacked Venn: circles nested from the bottom,
// the text near each one's top.
let linear = d.kind == "linearVenn";
if linear {
let dc = (area.w / (nf * 0.75 + 0.25)).min(area.h);
let step = dc * 0.75;
let x0 = area.x + (area.w - (step * (nf - 1.0) + dc)) / 2.0;
for (i, g) in gs.iter().enumerate() {
let mut sh = text_shape("ellipse", Xfrm::new(x0 + i as f64 * step, area.y + (area.h - dc) / 2.0, dc, dc), &l.nodes(g), true);
paint(d, &mut sh, i, false);
if let Some(Fill::Solid { color: c }) = &mut sh.fill {
*c = c.clone().with(ColorTransform::Alpha(55_000));
}
out.push((sh, g.clone()));
}
} else {
let s = area.h.min(area.w);
let (cx, bottom) = (area.x + area.w / 2.0, area.y + (area.h + s) / 2.0);
for (i, _) in gs.iter().enumerate() {
let dc = s * (1.0 - i as f64 / nf);
let mut sh = deco("ellipse", Xfrm::new(cx - dc / 2.0, bottom - dc, dc, dc), 0.0);
paint(d, &mut sh, i, false);
out.push((sh, vec![]));
}
// Each circle's text in the band between its top and the next circle's.
let band = s / nf;
for (i, g) in gs.iter().enumerate() {
let dc = s * (1.0 - i as f64 / nf);
let top = bottom - dc;
let h = if i + 1 < n { band } else { dc };
let w = dc * if i + 1 < n { 0.6 } else { 0.7 };
let mut t = text_shape("rect", Xfrm::new(cx - w / 2.0, top + if i + 1 < n { band * 0.05 } else { 0.0 }, w, h * 0.9), &l.nodes(g), true);
paint(d, &mut t, i, false);
t.fill = Some(Fill::None);
t.line = Some(Line { fill: Some(Fill::None), ..Default::default() });
t.effects = None;
out.push((t, g.clone()));
}
}
}
fn arm_nested_target(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, n, nf } = *l;
// Nested Target: rounded boxes nested inside each other, the text on each one's top band.
let band_x = area.w / 2.0 / nf * 0.6;
let band_y = area.h / nf * 0.9 / 2.0;
for (i, _) in gs.iter().enumerate() {
let (dx, dy) = (band_x * i as f64, band_y * i as f64);
let mut sh = deco("roundRect", Xfrm::new(area.x + dx, area.y + dy, area.w - 2.0 * dx, area.h - 2.0 * dy), 0.0);
paint(d, &mut sh, i, false);
out.push((sh, vec![]));
}
for (i, g) in gs.iter().enumerate() {
let (dx, dy) = (band_x * i as f64, band_y * i as f64);
let h = if i + 1 < n { band_y } else { area.h - 2.0 * dy };
let mut t =
text_shape("rect", Xfrm::new(area.x + dx + band_x * 0.3, area.y + dy, (area.w - 2.0 * dx - band_x * 0.6).max(1.0), h), &l.nodes(g), true);
paint(d, &mut t, i, false);
t.fill = Some(Fill::None);
t.line = Some(Line { fill: Some(Fill::None), ..Default::default() });
t.effects = None;
out.push((t, g.clone()));
}
}
fn arm_grid_matrix(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, .. } = *l;
// Grid Matrix: four quadrants on crossed axes.
let s = area.h.min(area.w);
let (x0, y0) = (area.x + (area.w - s) / 2.0, area.y + (area.h - s) / 2.0);
let t = s * 0.06;
for (preset, x) in [("leftRightArrow", Xfrm::new(x0, y0 + (s - t) / 2.0, s, t)), ("upDownArrow", Xfrm::new(x0 + (s - t) / 2.0, y0, t, s))] {
let mut sh = deco(preset, x, 0.0);
paint(d, &mut sh, 0, true);
out.push((sh, vec![]));
}
let q = s / 2.0 - t * 1.2;
for (i, g) in gs.iter().take(4).enumerate() {
let (c, r) = ((i % 2) as f64, (i / 2) as f64);
let x = Xfrm::new(x0 + c * (s / 2.0 + t * 0.7) + t * 0.5 * (1.0 - c), y0 + r * (s / 2.0 + t * 0.7) + t * 0.5 * (1.0 - r), q, q);
l.titled(out, "rect", x, i, g);
}
}
fn arm_gear(l: &L, out: &mut Vec<Item>) {
let L { area, gs, .. } = *l;
// Gear: up to three interlocking gears, the first the biggest.
let s = area.h.min(area.w * 0.62);
let (x0, y0) = (area.x + (area.w - s / 0.62) / 2.0, area.y + (area.h - s) / 2.0);
let spots = [(0.0, 0.18, 0.66, "gear9"), (0.5, 0.0, 0.48, "gear6"), (0.56, 0.48, 0.42, "gear6")];
for (i, g) in gs.iter().take(3).enumerate() {
let (fx, fy, fs, preset) = spots[i];
let z = s * fs;
l.titled(out, preset, Xfrm::new(x0 + fx * s * 1.3, y0 + fy * s, z, z), i, g);
}
}
fn arm_funnel(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, .. } = *l;
// Funnel: the ideas poured in at the top (up to three), the last one coming out below.
let s = area.h.min(area.w);
let (cx, y0) = (area.x + area.w / 2.0, area.y);
let fw = s * 0.8;
let fh = s * 0.62;
let mut f = deco("funnel", Xfrm::new(cx - fw / 2.0, y0, fw, fh), 0.0);
paint(d, &mut f, 0, true);
out.push((f, vec![]));
let (inside, last) = match gs.split_last() {
Some((l, r)) if !r.is_empty() => (r, Some(l)),
_ => (gs, None),
};
let k = inside.len().min(3);
let dc = fw * 0.27;
let spots = [(0.0, 0.04), (-0.2, 0.3), (0.2, 0.3)];
for (i, g) in inside.iter().take(3).enumerate() {
let (dx, dy) = if k == 1 { (0.0, 0.15) } else { spots[i] };
l.titled(out, "ellipse", Xfrm::new(cx + dx * fw - dc / 2.0, y0 + dy * fh + fh * 0.05, dc, dc), i, g);
}
if let Some(g) = last {
let a = s * 0.12;
let mut ar = deco("downArrow", Xfrm::new(cx - a / 2.0, y0 + fh, a, a * 1.2), 0.0);
paint(d, &mut ar, k, true);
out.push((ar, vec![]));
l.titled(out, "roundRect", Xfrm::new(cx - fw * 0.35, y0 + fh + a * 1.3, fw * 0.7, (s - fh - a * 1.3).max(10.0)), k, g);
}
}
fn arm_picture_h_list(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, nf, .. } = *l;
// Horizontal Picture List: a column per node, a big picture over its text.
let w = area.w / nf;
for (i, g) in gs.iter().enumerate() {
let x = area.x + i as f64 * w + w * 0.04;
let bw = w * 0.92;
let ph = area.h * 0.55;
out.push((picture_shape(d, "rect", Xfrm::new(x, area.y, bw, ph), i, g[0]), vec![]));
let mut sh = text_shape("rect", Xfrm::new(x, area.y + ph, bw, area.h - ph), &l.nodes(g), true);
if let Some(b) = &mut sh.text {
b.body.anchor = Some(deckcraft_model::text::Anchor::Top);
}
paint(d, &mut sh, i, false);
out.push((sh, g.clone()));
}
}
fn arm_timeline(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, nf, .. } = *l;
// Basic Timeline: an arrow across the middle, a dot per node on it, the text above and
// below in turn.
let mid = area.y + area.h / 2.0;
let bar = (area.h * 0.08).max(6.0);
let mut sh = deco("rightArrow", Xfrm::new(area.x, mid - bar, area.w, bar * 2.0), 0.0);
paint(d, &mut sh, 0, true);
out.push((sh, vec![]));
let w = area.w * 0.92 / nf;
for (i, g) in gs.iter().enumerate() {
let cx = area.x + w * (i as f64 + 0.5);
let dot = bar * 1.4;
let mut dsh = deco("ellipse", Xfrm::new(cx - dot / 2.0, mid - dot / 2.0, dot, dot), 0.0);
paint(d, &mut dsh, i, false);
out.push((dsh, vec![]));
let th = area.h / 2.0 - bar * 1.6;
let y = if i % 2 == 0 { area.y } else { mid + bar * 1.6 };
let mut t = text_shape("rect", Xfrm::new(cx - w * 0.48, y, w * 0.96, th.max(1.0)), &l.nodes(g), true);
if let Some(b) = &mut t.text {
b.body.anchor = Some(if i % 2 == 0 { deckcraft_model::text::Anchor::Bottom } else { deckcraft_model::text::Anchor::Top });
}
paint(d, &mut t, i, true);
t.fill = Some(Fill::None);
t.line = Some(Line { fill: Some(Fill::None), ..Default::default() });
out.push((t, g.clone()));
}
}
fn arm_venn(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, n, nf } = *l;
// Basic Venn: see-through circles overlapping in a ring (two or three in a row/triangle).
let s = area.h.min(area.w);
let (cx, cy) = (area.x + area.w / 2.0, area.y + area.h / 2.0);
let dc = if n == 1 { s } else { s * 0.62 };
let r = if n == 1 { 0.0 } else { (s - dc) / 2.0 };
for (i, g) in gs.iter().enumerate() {
let a = (i as f64 / nf * 360.0 + if n == 2 { 180.0 } else { -90.0 }).to_radians();
let mut sh = text_shape("ellipse", Xfrm::new(cx + r * a.cos() - dc / 2.0, cy + r * a.sin() - dc / 2.0, dc, dc), &l.nodes(g), true);
paint(d, &mut sh, i, false);
if let Some(Fill::Solid { color: c }) = &mut sh.fill {
*c = c.clone().with(ColorTransform::Alpha(50_000));
}
if let Some(b) = &mut sh.text {
// Text towards the circle's outer side, clear of the overlap.
let p = dc * 0.36;
let (dx, dy) = (a.cos() * p, a.sin() * p);
b.body.inset_l = Some((7.2 + dx).max(2.0));
b.body.inset_r = Some((7.2 - dx).max(2.0));
b.body.inset_t = Some((3.6 + dy).max(2.0));
b.body.inset_b = Some((3.6 - dy).max(2.0));
}
out.push((sh, g.clone()));
}
}
fn arm_target(l: &L, out: &mut Vec<Item>) {
let L { d, area, gs, nf, .. } = *l;
// Basic Target: nested rings, the outermost first, each ring's text in a box beside it
// with a line from the ring.
let s = area.h.min(area.w * 0.5);
let (cx, cy) = (area.x + s / 2.0, area.y + area.h / 2.0);
let band = s / 2.0 / nf;
for (i, _) in gs.iter().enumerate() {
let dc = s - 2.0 * band * i as f64;
let mut sh = deco("ellipse", Xfrm::new(cx - dc / 2.0, cy - dc / 2.0, dc, dc), 0.0);
paint(d, &mut sh, i, false);
out.push((sh, vec![]));
}
let tx = area.x + s + area.w * 0.06;
let rh = area.h / nf;
for (i, g) in gs.iter().enumerate() {
let ty = area.y + rh * (i as f64 + 0.5);
// The point on ring i's band level with the text, else its top.
let ry = cy - s / 2.0 + band * (i as f64 + 0.5);
out.push((line_shape((cx, ry.min(ty)), (tx, ty)), vec![]));
let mut t = text_shape("rect", Xfrm::new(tx, ty - rh * 0.45, (area.x + area.w - tx).max(1.0), rh * 0.9), &l.nodes(g), true);
paint(d, &mut t, i, true);
t.fill = Some(Fill::None);
t.line = Some(Line { fill: Some(Fill::None), ..Default::default() });
if let Some(b) = &mut t.text {
for p in &mut b.paragraphs {
p.props.align = Some(Align::Left);
}
}
out.push((t, g.clone()));
}
}