use std::rc::Rc;
use crate::canvas::{transform_segments, Fill, Item, Paint, Stroke};
use crate::keys;
use crate::label;
use crate::scene::{Label, Point, Segment, Transform};
use crate::tex::{self, Token};
use crate::{Error, PT};
use super::shapes::{self, BoxSize, Geometry, Layer, Usage};
use super::{tikra_tokens, Engine};
pub fn is_shape(name: &str) -> bool {
shapes::is_shape(name)
}
#[derive(Debug, Clone, Default)]
pub struct NodeSetup {
pub at: Option<Point>,
pub pos: Option<f64>,
pub sloped: bool,
pub transform_shape: bool,
pub transform_shape_nonlinear: bool,
pub name: Option<String>,
pub labels: Vec<(Vec<Token>, bool)>,
pub fit: Option<(f64, f64)>,
pub fit_rotate: f64,
pub through: Option<Point>,
}
#[derive(Debug, Clone)]
pub struct NodeInfo {
pub shape: String,
pub geometry: Rc<Geometry>,
pub transform: Transform,
}
impl NodeInfo {
pub fn center(&self) -> Point {
self.transform.apply(self.geometry.center)
}
pub fn border_toward(&self, p: Point) -> Point {
let inv = self.transform.inverse();
let q = inv.apply(p);
let b = self.geometry.border_point(q.minus(self.geometry.center));
self.transform.apply(b)
}
pub fn anchor(&self, name: &str) -> Option<Point> {
self.anchor_local(name).map(|p| self.transform.apply(p))
}
pub fn anchor_local(&self, name: &str) -> Option<Point> {
self.geometry.anchor(name)
}
}
pub fn split_parts(tokens: &[Token]) -> Vec<(String, Vec<Token>)> {
let mut parts = vec![("text".to_string(), Vec::new())];
let mut depth = 0usize;
let mut i = 0;
let skip_spaces = |j: &mut usize| {
while tokens.get(*j).is_some_and(|t| t.is_space()) {
*j += 1;
}
};
while i < tokens.len() {
let t = &tokens[i];
match t {
Token::Char(_, tex::Cat::Begin) => depth += 1,
Token::Char(_, tex::Cat::End) => depth = depth.saturating_sub(1),
_ => {}
}
if depth == 0 && t.is_cs("nodepart") {
let mut j = i + 1;
skip_spaces(&mut j);
if tokens.get(j).is_some_and(|t| t.is_char('[')) {
while j < tokens.len() && !tokens[j].is_char(']') {
j += 1;
}
j += 1;
skip_spaces(&mut j);
}
let mut name = Vec::new();
if let Some(Token::Char(_, tex::Cat::Begin)) = tokens.get(j) {
let mut d = 0usize;
while j < tokens.len() {
match &tokens[j] {
Token::Char(_, tex::Cat::Begin) => d += 1,
Token::Char(_, tex::Cat::End) => d -= 1,
_ => {}
}
if d == 0 {
break;
}
if !(d == 1 && matches!(tokens[j], Token::Char(_, tex::Cat::Begin))) {
name.push(tokens[j].clone());
}
j += 1;
}
j += 1;
} else if let Some(t) = tokens.get(j) {
name.push(t.clone());
j += 1;
}
let name = tex::tokens_to_string(&name).split_whitespace().collect::<Vec<_>>().join(" ");
let name = match name.as_str() {
"one" => "text".to_string(),
"second" => "two".to_string(),
"third" => "three".to_string(),
"fourth" => "four".to_string(),
_ => name,
};
parts.push((name, Vec::new()));
i = j;
continue;
}
parts.last_mut().expect("parts start with text").1.push(t.clone());
i += 1;
}
parts
}
pub(super) struct EngineKeys<'a>(pub(super) &'a mut Engine);
impl shapes::Keys for EngineKeys<'_> {
fn length(&mut self, key: &str, default: f64) -> f64 {
self.0.key_length(key, default)
}
fn length_of(&mut self, text: &str, default: f64) -> f64 {
self.0.length(text).unwrap_or(default)
}
fn number(&mut self, key: &str, default: f64) -> f64 {
match keys::value(&self.0.tex, key) {
Some(v) if !keys::trim(&v).is_empty() => self.0.number_of(v).unwrap_or(default),
_ => default,
}
}
fn text(&mut self, key: &str) -> String {
match keys::value(&self.0.tex, key) {
Some(v) => self.0.tex.expand_to_string(v).unwrap_or_default(),
None => String::new(),
}
}
fn flag(&mut self, name: &str) -> bool {
let m = self.0.tex.meaning_of(&format!("if{name}"));
m == self.0.tex.meaning_of("iftrue")
}
fn point(&mut self, key: &str) -> Option<Point> {
let text = self.text(key);
let text = crate::keys::strip_braces(text.trim()).trim().to_string();
if text.is_empty() {
return None;
}
let mut total: Option<Point> = None;
for part in text.split(")+(").map(|s| s.trim_start_matches('+')) {
let inner = part.trim().trim_start_matches('(').trim_end_matches(')').to_string();
let p = self.0.coordinate(&inner).ok()?.p;
total = Some(match total {
None => p,
Some(t) => t.plus(p.minus(self.0.coordinate("0,0").ok()?.p)),
});
}
total
}
fn text_box(&mut self, tex: &str) -> BoxSize {
let (text, _) = self.0.shape_text(tex);
if text.trim().is_empty() {
return BoxSize::default();
}
let mut warnings = Vec::new();
let l = label::compose_label(text.trim(), &self.0.st.font, None, 'c', self.0.st.text_paint_color(), &mut warnings);
BoxSize { wd: l.width(), ht: l.height(), dp: l.depth() }
}
fn code_paths(&mut self, key: &str, t: Transform) -> Vec<Vec<Segment>> {
let e = &mut *self.0;
let Some(code) = keys::value(&e.tex, key).filter(|v| !keys::trim(v).is_empty()) else { return Vec::new() };
let n = e.canvas.items.len();
let frozen = e.canvas_frozen;
let path = std::mem::take(&mut e.pgf_path);
e.begin_group();
e.canvas_frozen = true;
e.st.transform = t;
e.st.mode = Default::default();
e.st.mode.draw = true;
let r = e.run_tokens(code);
e.end_group();
e.canvas_frozen = frozen;
e.pgf_path = path;
if let Err(err) = r {
e.warn(format!("{key}: {err}"));
}
e.canvas.items.split_off(n).into_iter().filter_map(|i| if let Item::Path { segments, stroke: Some(_), .. } = i { Some(segments) } else { None }).collect()
}
}
type NodeLabels = (NodeInfo, Vec<(Vec<Token>, bool)>, (Option<NodeInfo>, Option<String>), Vec<(&'static str, tex::Meaning)>);
const LABEL_KEYS: [&str; 3] = ["/tikz/label distance", "/tikz/label position", "/tikz/pin distance"];
#[derive(Debug, Clone, Copy)]
pub enum OnPath {
Point(Point),
Segment(Point, Option<(Point, Point)>, Point),
Corner(Point, Point, Point),
Arc {
center: Point,
x: Point,
y: Point,
from: f64,
to: f64,
},
}
impl OnPath {
pub fn at(&self, t: f64) -> (Point, Point) {
match *self {
OnPath::Point(p) => (p, Point::new(1.0, 0.0)),
OnPath::Segment(a, None, b) => (a.lerp(b, t), b.minus(a)),
OnPath::Segment(a, Some((c1, c2)), b) => {
let u = 1.0 - t;
let p = a.scale(u * u * u).plus(c1.scale(3.0 * u * u * t)).plus(c2.scale(3.0 * u * t * t)).plus(b.scale(t * t * t));
let d = c1.minus(a).scale(3.0 * u * u).plus(c2.minus(c1).scale(6.0 * u * t)).plus(b.minus(c2).scale(3.0 * t * t));
(p, d)
}
OnPath::Arc { center, x, y, from, to } => {
let angle = (from + t * (to - from)).to_radians();
let (s, c) = angle.sin_cos();
let p = center.plus(x.scale(c)).plus(y.scale(s));
let d = y.scale(c).minus(x.scale(s)).scale(if to < from { -1.0 } else { 1.0 });
(p, d)
}
OnPath::Corner(a, c, b) => {
if t <= 0.5 {
(a.lerp(c, 2.0 * t), c.minus(a))
} else {
(c.lerp(b, 2.0 * t - 1.0), b.minus(c))
}
}
}
}
}
pub struct NodeSpec {
pub options: Vec<Token>,
pub name: Option<String>,
pub at: Option<Point>,
pub text: Vec<Token>,
pub place: OnPath,
pub point: Point,
pub coordinate: bool,
pub default_pos: Option<f64>,
}
impl Engine {
pub fn st_node(&mut self) -> &mut NodeSetup {
&mut self.st.node
}
pub fn key_text(&mut self, path: &str) -> String {
keys::value(&self.tex, path).map(|v| tex::tokens_to_string(&v)).unwrap_or_default()
}
fn key_length(&mut self, path: &str, default: f64) -> f64 {
match keys::value(&self.tex, path) {
Some(v) if !keys::trim(&v).is_empty() => self.length_of(v).unwrap_or(default),
_ => default,
}
}
pub fn alias_last_node(&mut self, alias: &str) {
let last = self.tex.expand_to_string(vec![Token::cs("tikzlastnode")]).unwrap_or_default();
let last = last.trim();
if let Some(f) = self.facts.nodes.iter_mut().rev().find(|f| f.key == last) {
f.key = alias.to_string();
}
}
pub fn node(&mut self, spec: NodeSpec) -> Result<Vec<Item>, Error> {
let mut spec = spec;
if spec.text.iter().any(|t| t.is_cs("pgfmathprintnumber") || t.is_cs("pgfplotspointplotattime") || t.is_cs("pgfplotspointgetcoordinates")) {
spec.text = super::pgfplots::draw::print_numbers(self, &spec.text)?;
}
self.begin_group();
self.st.node = NodeSetup { transform_shape: self.st.node.transform_shape, ..NodeSetup::default() };
self.st.mode = Default::default();
self.st.pending = Transform::default();
self.st.on_chain = None;
self.st.pending_joins.clear();
self.st.after_node_path.clear();
self.st.aliases.clear();
self.st.start_arrow = None;
self.st.end_arrow = None;
let outer = self.st.clone();
self.st.decorate = false;
self.st.preactions = Default::default();
self.st.postactions = Default::default();
let shifts_before = self.anim_shifts.len();
let r = self.node_in_group(spec, &outer);
let behind = matches!(self.tex.meaning_of("tikra@behindpath"), tex::Meaning::Macro(m) if tex::tokens_to_string(&tex::substitute(&m.body, &[])).trim() == "1");
self.end_group();
let (mut items, labels) = r?;
let shifts: Vec<Point> = self.anim_shifts.drain(shifts_before..).collect();
if let (false, Some(info)) = (shifts.is_empty(), self.last_node.clone()) {
let ends: Vec<Point> = ["north", "south", "east", "west"].iter().filter_map(|a| info.anchor(a)).collect();
if self.counts_for_size() {
for d in shifts {
for p in &ends {
self.canvas.include(p.plus(d));
}
}
}
}
if behind {
self.behind_path.append(&mut items);
}
if let Some((info, labels, last, kept)) = labels {
self.begin_group();
for (key, value) in kept {
self.tex.set_meaning(&Token::cs(&keys::macro_name(key)), value, false);
}
let r = (|| {
for (spec_tokens, pin) in labels {
items.extend(self.label(&info, spec_tokens, pin)?);
}
Ok::<(), Error>(())
})();
self.end_group();
r?;
(self.last_node, self.last_node_name) = last;
}
Ok(items)
}
pub fn late_options(&mut self, name: &str, options: Vec<Token>) -> Result<Vec<Item>, Error> {
let Some(info) = self.lookup_node(name) else {
self.warn(format!("late options: no node named '{name}'"));
return Ok(Vec::new());
};
self.begin_group();
self.st.node = NodeSetup::default();
let r = (|| {
self.process_keys(options, Some("/tikz"))?;
let mut items = Vec::new();
for (spec, pin) in std::mem::take(&mut self.st.node.labels) {
items.extend(self.label(&info, spec, pin)?);
}
Ok(items)
})();
self.end_group();
r
}
fn inner_picture(&mut self, code: Vec<Token>) -> Result<Option<(Vec<Item>, Point, Point)>, Error> {
let saved_canvas = std::mem::take(&mut self.canvas);
let saved_frozen = std::mem::replace(&mut self.canvas_frozen, false);
self.begin_group();
self.begin_scope();
self.st.transform = Transform::default();
self.st.pending = Transform::default();
let r = self.run_tokens(code);
self.end_scope();
self.end_group();
let crate::canvas::Canvas { items, min, max, .. } = std::mem::replace(&mut self.canvas, saved_canvas);
self.canvas_frozen = saved_frozen;
r?;
Ok(min.zip(max).map(|(lo, hi)| (items, lo, hi)))
}
pub fn position_node_now(&mut self) -> Result<(), Error> {
let p = self.point_arg()?;
let d = self.st.transform.apply(p);
let Some((items, mut info, name)) = self.later_node.take() else {
self.warn("\\pgfpositionnodenow: no node kept by \\pgfpositionnodelater".to_string());
return Ok(());
};
info.transform = Transform::translation(d.x, d.y).compose(&info.transform);
for mut item in items {
item.translate(d);
if let (Item::Path { segments, .. }, true) = (&item, self.counts_for_size()) {
self.canvas.include_path(segments);
}
self.canvas.items.push(item);
}
if let Some(n) = name {
self.nodes.insert(n, info);
}
Ok(())
}
fn expand_text(&mut self, tokens: Vec<Token>) -> Result<Vec<Token>, Error> {
const RUN: &[&str] = &["pgfmathparse", "pgfmathsetmacro", "pgfmathtruncatemacro", "pgfmathsetlengthmacro"];
let depth = self.tex.expand_start(tokens);
let mut out = Vec::new();
loop {
let (more, stop) = self.tex.expand_more(depth, RUN)?;
out.extend(more);
let Some(t) = stop else { return Ok(out) };
if let tex::Meaning::Native(id) = self.tex.meaning(&t) {
let r = self.command(id, &t);
if let Err(e) = r {
self.tex.drain_to(depth);
return Err(e);
}
}
}
}
fn node_in_group(&mut self, spec: NodeSpec, scope: &super::GState) -> Result<(Vec<Item>, Option<NodeLabels>), Error> {
if spec.coordinate {
self.tex.define_string(&keys::macro_name("/tikz/shape"), "coordinate", false);
}
let outer_name_path = self.st.name_path.clone();
self.process_keys(tikra_tokens("every node/.try"), Some("/tikz"))?;
self.process_keys(spec.options.clone(), Some("/tikz"))?;
let (chain_names, joins) = self.chain_node_found()?;
let mut after = std::mem::take(&mut self.st.after_node_path);
after.extend(joins);
self.after_node_path.extend(after);
let shape_name = self.key_text("/tikz/shape").split_whitespace().collect::<Vec<_>>().join(" ");
let user_shape = self.user_shapes.get(shape_name.trim()).cloned();
let shape = if let Some(u) = &user_shape {
u.base.clone()
} else if shapes::is_shape(&shape_name) {
shape_name.clone()
} else {
self.warn(format!("shape '{shape_name}' not supported: drawn as a rectangle"));
"rectangle".to_string()
};
self.process_keys(tikra_tokens(&format!("every {shape_name} node/.try")), Some("/tikz"))?;
let name = spec.name.clone().or_else(|| self.st.node.name.clone()).or_else(|| self.set_member_name());
let mut texts = Vec::new();
let body = |e: &Engine, name: &str| match e.tex.meaning_of(name) {
tex::Meaning::Macro(m) => tex::substitute(&m.body, &[]),
_ => Vec::new(),
};
let mut text_tokens = body(self, "tikz@atbegin@node");
text_tokens.extend(spec.text.clone());
text_tokens.extend(body(self, "tikz@atend@node"));
let expanded = self.expand_text(text_tokens)?;
for (part, tokens) in split_parts(&expanded) {
texts.push((part, tex::tokens_to_text(&tokens).trim().to_string()));
}
let text = texts[0].1.clone();
let text_width = {
let t = self.key_text("/tikz/text width");
if t.trim().is_empty() {
None
} else {
Some(self.length(&t)?)
}
};
let align = match self.key_text("/tikz/align").trim() {
"right" => 'r',
"flush right" => 'R',
"center" => 'c',
"flush center" => 'C',
"left" => 'l',
"flush left" => 'L',
"justify" => 'j',
"none" if text_width.is_some() => 'j',
_ if text_width.is_some() => 'l',
_ => 'c',
};
let color = self.st.text_paint_color();
let font = self.st.font.clone();
let mut labels: Vec<(String, Option<Label>)> = Vec::new();
let mut boxes: Vec<(String, BoxSize)> = Vec::new();
self.text_shading = None;
if let Some(name) = texts.first().and_then(|(_, t)| t.strip_prefix("\\pgfuseshading")).map(|n| n.trim().trim_start_matches('{').trim_end_matches('}').trim().to_string()) {
if let Some((w, h)) = self.shading_half_size(&name) {
self.text_shading = Some((name, w, h));
texts[0].1.clear();
}
}
self.text_picture = None;
let whole = texts.first().map(|(_, t)| t.trim().to_string()).unwrap_or_default();
let tikz = whole.strip_prefix("\\tikz").is_some_and(|r| !r.starts_with(|c: char| c.is_ascii_alphabetic()));
if texts.len() == 1 && (tikz || (whole.starts_with("\\begin{tikzpicture}") && whole.ends_with("\\end{tikzpicture}"))) {
if let Some(p) = self.inner_picture(spec.text.clone())? {
self.text_picture = Some(p);
texts[0].1.clear();
}
}
for (k, (part, t)) in texts.iter().enumerate() {
let lbl = if shape == "coordinate" || t.is_empty() {
None
} else {
let mut warnings = Vec::new();
self.begin_group();
let r = self.process_keys(tikra_tokens(&format!("every {part} node part/.try")), Some("/tikz"));
let (font, color) =
if self.st.font != font || self.st.text_paint_color() != color { (self.st.font.clone(), self.st.text_paint_color()) } else { (font.clone(), color) };
self.end_group();
r?;
let (t, color) = match whole_textcolor(t).and_then(|(c, inner)| self.color(&c).map(|c| (inner, c))) {
Some((inner, c)) => (inner, c),
None => (t.to_string(), color),
};
let t = self.inline_colors(&t);
let l = label::compose_label(&t, &font, text_width, align, color, &mut warnings);
for w in warnings {
self.warn(w);
}
Some(l)
};
let (mut wd, mut ht, mut dp) = match &lbl {
Some(l) => (text_width.unwrap_or_else(|| l.width()), l.height(), l.depth()),
None if shape != "coordinate" => (text_width.unwrap_or(0.0), 0.0, 0.0),
None => (0.0, 0.0, 0.0),
};
for (key, target) in [("/tikz/text height", &mut ht), ("/tikz/text depth", &mut dp)] {
let v = self.key_text(key);
if !v.trim().is_empty() {
*target = self.length(&v)?;
}
}
if k == 0 {
if let Some((_, w, h)) = &self.text_shading {
(wd, ht, dp) = (2.0 * w, 2.0 * h, 0.0);
}
if let Some((_, lo, hi)) = &self.text_picture {
(wd, ht, dp) = (hi.x - lo.x, hi.y - lo.y, 0.0);
}
if let Some(b) = self.forced_box.take() {
(wd, ht, dp) = b;
}
if let Some((w, h)) = self.st.node.fit {
(wd, ht, dp) = (w, h / 2.0, h / 2.0);
}
}
boxes.push((part.clone(), BoxSize { wd, ht, dp }));
labels.push((part.clone(), lbl));
}
if let Some(p) = self.st.node.through {
let at = self.st.node.at.or(spec.at).unwrap_or(spec.point);
let size = format!("{}pt", 2.0 * p.minus(at).norm() / PT);
for key in ["/pgf/minimum width", "/pgf/minimum height"] {
self.tex.define_string(&keys::macro_name(key), &size, false);
}
}
let xsep = self.key_length("/pgf/inner xsep", 0.3333 * 0.3515);
let ysep = self.key_length("/pgf/inner ysep", 0.3333 * 0.3515);
let min_w = self.key_length("/pgf/minimum width", PT);
let min_h = self.key_length("/pgf/minimum height", PT);
let ox = self.key_length("/pgf/outer xsep", 0.5 * self.st.line_width);
let oy = self.key_length("/pgf/outer ysep", 0.5 * self.st.line_width);
let aspect = self.key_text("/pgf/aspect").trim().parse::<f64>().ok().filter(|a| *a > 0.0).unwrap_or(1.0);
let ex = 0.43 * self.st.font_size * PT;
let line_width = self.st.line_width;
let compute = |e: &mut Engine, pointer: Option<Point>| {
let mut keys = EngineKeys(e);
let mut input = shapes::Input {
parts: &boxes,
inner_xsep: xsep,
inner_ysep: ysep,
minimum_width: min_w,
minimum_height: min_h,
outer_xsep: ox,
outer_ysep: oy,
aspect,
ex,
line_width,
pointer,
keys: &mut keys,
};
shapes::compute(&shape, &mut input).expect("a known shape")
};
let mut geometry = compute(self, None);
if shape.contains(" gate ") {
let flag = Token::cs("iftikra@gatebox");
let saved = self.tex.meaning(&flag);
let on = saved == self.tex.meaning_of("iftrue");
let other = self.tex.meaning_of(if on { "iffalse" } else { "iftrue" });
self.tex.set_meaning(&flag, other, false);
let g = compute(self, None);
self.tex.set_meaning(&flag, saved, false);
geometry.anchors.extend(g.anchors.into_iter().map(|(n, p)| (format!("@gatebox={} {n}", !on), p)));
}
if let Some(u) = &user_shape {
geometry = self.user_shape_geometry(u, geometry)?;
}
let mut info = NodeInfo { shape: shape.clone(), geometry: Rc::new(geometry), transform: Transform::default() };
let pos = self.st.node.pos.or(spec.default_pos);
let (target, direction) = match (self.st.node.at.or(spec.at), pos) {
(Some(p), _) => (p, Point::new(1.0, 0.0)),
(None, Some(t)) => spec.place.at(t),
(None, None) => (spec.point, Point::new(1.0, 0.0)),
};
let mut anchor = self.key_text("/tikz/anchor");
if let (Some(side), true, Some(_)) = (self.st.auto, anchor.trim() == "center", pos) {
anchor = auto_anchor(direction, (side == 'r') != self.st.swap).to_string();
}
let anchor = anchor.trim();
let align = crate::keys::value(&self.tex, "/pgfplots/near ticklabel align").map(|v| crate::tex::tokens_to_string(&v)).unwrap_or_default();
let near = self.frame.as_ref().and_then(|f| f.near_point(anchor, &self.st.pending, &align, &|n| info.anchor_local(n)));
let a_local = near.or_else(|| info.anchor_local(anchor)).unwrap_or_else(|| {
self.warn(format!("unknown anchor '{anchor}': center used"));
info.geometry.center
});
let mut linear = Transform::default();
if self.st.node.transform_shape {
linear = Transform { e: 0.0, f: 0.0, ..self.st.transform };
}
let mut target = target;
if !self.st.nonlinear.is_empty() {
let moved = self.nonlinear_point(target)?;
if self.st.node.transform_shape_nonlinear {
let h = 0.01;
let dx = self.nonlinear_point(target.plus(Point::new(h, 0.0)))?.minus(moved).scale(1.0 / h);
let dy = self.nonlinear_point(target.plus(Point::new(0.0, h)))?.minus(moved).scale(1.0 / h);
linear = Transform { a: dx.x, b: dx.y, c: dy.x, d: dy.y, e: 0.0, f: 0.0 }.compose(&linear);
}
target = moved;
}
if self.st.node.sloped {
let angle = direction.y.atan2(direction.x).to_degrees();
let angle = if !self.st.allow_upside_down && (angle > 90.0 + 1e-9 || angle < -90.0 - 1e-9) { angle + 180.0 } else { angle };
linear = linear.compose(&Transform::rotation(angle));
}
let t = Transform::translation(target.x, target.y).compose(&linear).compose(&self.st.pending).compose(&Transform::translation(-a_local.x, -a_local.y));
info.transform = t;
if shape.ends_with("callout") {
if let Some(p) = shapes::Keys::point(&mut EngineKeys(self), "/pgf/callout pointer@absolute") {
let local = t.inverse().apply(p);
info.geometry = Rc::new(compute(self, Some(local)));
}
}
let limit = super::path::MAX_COORDINATE;
let fine = |p: Point| p.x.is_finite() && p.y.is_finite() && p.x.abs() <= limit && p.y.abs() <= limit;
if !fine(info.center()) || !info.geometry.anchors.iter().all(|(_, p)| fine(t.apply(*p))) {
self.warn(format!("node '{text}': position or size is not finite or beyond ±{limit} cm: ignored"));
return Ok((Vec::new(), None));
}
if let Some(n) = self.st.name_path.clone().filter(|_| self.st.name_path != outer_name_path) {
let g = &info.geometry;
let border =
g.paths.iter().filter(|p| p.layer == Layer::Background && matches!(p.usage, Usage::Node)).flat_map(|p| transform_segments(&p.segments, &info.transform)).collect();
self.named_paths.insert(n, border);
}
let size = (self.canvas.min, self.canvas.max);
let items = self.draw_shape(&info, labels, text_width, scope)?;
if self.st.position_later {
(self.canvas.min, self.canvas.max) = size;
self.later_node = Some((items, info, name.map(|n| format!("{}{n}", self.st.name_prefix))));
return Ok((Vec::new(), None));
}
let key = if let Some(n) = &name {
let n = format!("{}{n}", self.st.name_prefix);
self.tex.define_string("tikzlastnode", &n, true);
self.join_sets(&n);
self.nodes.insert(n.clone(), info.clone());
n
} else {
let n = format!("tikz@f@{}", self.nodes.len());
self.tex.define_string("tikzlastnode", &n, true);
self.nodes.insert(n.clone(), info.clone());
n
};
for n in chain_names.into_iter().chain(std::mem::take(&mut self.st.aliases)) {
self.nodes.insert(n, info.clone());
}
let registered = name.as_ref().map(|n| format!("{}{n}", self.st.name_prefix));
if self.facts.mute == 0 && shape != "coordinate" {
let all: Vec<&str> = texts.iter().map(|(_, t)| t.as_str()).filter(|t| !t.trim().is_empty()).collect();
self.facts.nodes.push(crate::describe::NodeFact { key, named: registered.is_some(), text: all.join(" "), at: info.center(), on_link: false });
}
if let Some(slot @ None) = self.tree_child_names.last_mut() {
*slot = Some(registered.clone().unwrap_or_default());
}
let last = (Some(info.clone()), registered);
(self.last_node, self.last_node_name) = last.clone();
let labels = std::mem::take(&mut self.st.node.labels);
let kept = LABEL_KEYS.iter().map(|k| (*k, self.tex.meaning_of(&keys::macro_name(k)))).collect();
Ok((items, Some((info, labels, last, kept))))
}
fn draw_shape(&mut self, info: &NodeInfo, labels: Vec<(String, Option<Label>)>, text_width: Option<f64>, scope: &super::GState) -> Result<Vec<Item>, Error> {
let t = info.transform;
let g = info.geometry.clone();
let mut items = Vec::new();
let background: Vec<Segment> =
g.paths.iter().filter(|p| p.layer == Layer::Background && matches!(p.usage, Usage::Node)).flat_map(|p| transform_segments(&p.segments, &t)).collect();
let node_fill = if self.st.mode.shade && !background.is_empty() {
Some(Fill { paint: Paint::Shading(self.shading_of(&background)), opacity: self.st.fill_opacity, even_odd: self.st.even_odd })
} else if self.st.mode.fill {
Some(Fill { paint: Paint::Solid(self.st.fill_paint_color()), opacity: self.st.fill_opacity, even_odd: self.st.even_odd })
} else if self.st.mode.pattern {
Some(Fill {
paint: {
let (name, color) = (self.st.pattern_name.clone().unwrap_or_default(), self.st.pattern_color.unwrap_or(crate::color::Color::BLACK));
self.pattern_paint(&name, color).unwrap_or(Paint::Pattern { name, color })
},
opacity: self.st.fill_opacity,
even_odd: false,
})
} else {
None
};
let node_stroke = self.st.mode.draw.then(|| self.stroke());
let layer_items = |e: &mut Engine, layers: &[Layer], items: &mut Vec<Item>| -> Result<(), Error> {
for p in g.paths.iter().filter(|p| layers.contains(&p.layer)) {
let mut segs = p.segments.clone();
if p.layer == Layer::Background && g.roundable && e.st.rounded > 0.0 {
segs = super::output::round_corners(&segs, e.st.rounded);
}
let segs = transform_segments(&segs, &t);
if matches!(p.usage, Usage::Node)
&& p.layer == Layer::Background
&& (e.st.decorate || e.st.mode.picture.is_some() || !e.st.preactions.is_empty() || !e.st.postactions.is_empty() || (e.st.double.is_some() && e.st.mode.draw))
{
let saved = std::mem::take(&mut e.canvas.items);
let r = e.finish_path(segs, scope, "node");
items.extend(std::mem::replace(&mut e.canvas.items, saved));
r?;
continue;
}
let (fill, stroke) = match &p.usage {
Usage::Node => (node_fill.clone(), node_stroke.clone()),
Usage::Fill(c) => {
let color = e.color(c).unwrap_or(crate::color::Color::WHITE);
(Some(Fill { paint: Paint::Solid(color), opacity: e.st.fill_opacity, even_odd: false }), None)
}
Usage::Stroke => (None, Some(e.stroke())),
Usage::TipStroke(cap, join) => (None, Some(crate::canvas::Stroke { cap: *cap, join: *join, ..e.stroke() })),
Usage::LineFill => (Some(Fill { paint: Paint::Solid(e.st.stroke_color()), opacity: e.st.fill_opacity, even_odd: false }), None),
};
if e.counts_for_size() {
e.canvas.include_path(&segs);
if let Some(s) = &stroke {
e.canvas.include_path_margin(&segs, s.width / 2.0);
}
}
if fill.is_some() || stroke.is_some() {
items.push(Item::Path { segments: segs, fill, stroke });
}
}
Ok(())
};
layer_items(self, &[Layer::BehindBackground, Layer::Background, Layer::BeforeBackground], &mut items)?;
if let Some((drawn, lo, _)) = self.text_picture.take() {
if let Some(origin) = g.parts.iter().find(|(n, _)| n == "text").map(|(_, p)| *p) {
let place = t.compose(&Transform::translation(origin.x - lo.x, origin.y - lo.y));
items.push(Item::Transform { transform: place });
items.extend(drawn);
items.push(Item::End);
}
}
if let Some((name, w, h)) = self.text_shading.take() {
if let Some(origin) = g.parts.iter().find(|(n, _)| n == "text").map(|(_, p)| *p) {
let saved = std::mem::take(&mut self.canvas.items);
let r = self.draw_shading(&name, &t.compose(&Transform::translation(origin.x + w, origin.y + h)));
items.extend(std::mem::replace(&mut self.canvas.items, saved));
r?;
}
}
for (part, lbl) in labels {
let (Some(l), Some(origin)) = (lbl, g.parts.iter().find(|(n, _)| *n == part).map(|(_, p)| *p)) else { continue };
let offset = match (text_width, l.align) {
(Some(tw), 'c') => (tw - l.width()) / 2.0,
(Some(tw), 'r') => tw - l.width(),
_ => 0.0,
};
let place = t.compose(&Transform::translation(origin.x + offset, origin.y));
items.push(Item::Text { label: l, place, opacity: self.st.text_opacity });
}
for st in &g.texts {
let (text, own) = self.shape_text(&st.tex);
let color = own.or(st.color.clone()).and_then(|c| self.color(&c)).unwrap_or_else(|| self.st.text_paint_color());
if text.trim().is_empty() {
continue;
}
let mut warnings = Vec::new();
let text = self.inline_colors(text.trim());
let l = label::compose_label(&text, &self.st.font, None, 'c', color, &mut warnings);
for w in warnings {
self.warn(w);
}
items.push(Item::Text { label: l, place: t.compose(&Transform::translation(st.at.x, st.at.y)), opacity: self.st.text_opacity });
}
layer_items(self, &[Layer::BehindForeground, Layer::Foreground, Layer::BeforeForeground], &mut items)?;
Ok(items)
}
fn shape_text(&mut self, tex: &str) -> (String, Option<String>) {
let mut text = self.tex.expand_to_string(tikra_tokens(tex)).unwrap_or_else(|_| tex.to_string());
while let Some(i) = text.find("\\char") {
let rest = text[i + 5..].trim_start();
let rest = rest.strip_prefix('`').unwrap_or(rest);
let rest = rest.strip_prefix('\\').unwrap_or(rest);
let Some(ch) = rest.chars().next() else { break };
let after = rest[ch.len_utf8()..].to_string();
let ch = if "&%$#_{}".contains(ch) { format!("\\{ch}") } else { ch.to_string() };
text = format!("{}{ch}{after}", &text[..i]);
}
let mut color = None;
if let Some(rest) = text.trim_start().strip_prefix("\\color") {
let rest = rest.trim_start();
if let Some(r) = rest.strip_prefix('{') {
if let Some(j) = r.find('}') {
color = Some(r[..j].to_string());
text = r[j + 1..].to_string();
}
}
}
(text, color)
}
pub(super) fn label(&mut self, info: &NodeInfo, spec: Vec<Token>, pin: bool) -> Result<Vec<Item>, Error> {
let mut v = keys::trim(&spec).to_vec();
let mut opts = Vec::new();
if v.first().is_some_and(|t| t.is_char('[')) {
let mut depth = 0usize;
let mut end = 0;
for (i, t) in v.iter().enumerate() {
if t.is_char('[') {
depth += 1;
} else if t.is_char(']') {
depth -= 1;
if depth == 0 {
end = i;
break;
}
}
}
opts = v[1..end].to_vec();
v = v[end + 1..].to_vec();
}
let s = tex::tokens_to_string(&v);
let (angle_text, text_tokens) = match split_colon(&v) {
Some((a, b)) => (tex::tokens_to_string(&a).trim().to_string(), b),
None => (String::new(), v.clone()),
};
let _ = s;
let shorthands = keys::split_list(&opts).iter().any(|i| i.key == "direction shorthands");
let directions: &[&str] =
if shorthands { &["above", "below", "left", "right", "above left", "above right", "below left", "below right", "centered", "direction shorthands"] } else { &[] };
let mut given = None;
for i in keys::split_list(&opts) {
if i.key == "label position" {
if let Some(v) = &i.value {
given = Some(tex::tokens_to_string(v).trim().to_string());
}
}
}
let mut edge_options = Vec::new();
let items = keys::split_list(&opts);
let taken = |i: &keys::Item| (i.value.is_none() && directions.contains(&i.key.as_str())) || i.key == "pin edge";
if items.iter().any(taken) {
let mut rest = Vec::new();
for i in items {
if i.key == "pin edge" {
edge_options.push(Token::other(','));
edge_options.extend(i.value.unwrap_or_default());
} else if i.value.is_none() && directions.contains(&i.key.as_str()) {
if i.key != "direction shorthands" {
given = Some(i.key.clone());
}
} else {
if !rest.is_empty() {
rest.push(Token::other(','));
}
rest.extend(tikra_tokens(&i.key));
if let Some(v) = i.value {
rest.push(Token::other('='));
rest.push(Token::Char('{', tex::Cat::Begin));
rest.extend(v);
rest.push(Token::Char('}', tex::Cat::End));
}
}
}
opts = rest;
}
let position = if !angle_text.is_empty() {
angle_text
} else if let Some(g) = given {
g
} else {
self.key_text("/tikz/label position").trim().to_string()
};
let angle = match position.as_str() {
"above" | "north" => 90.0,
"below" | "south" => 270.0,
"left" | "west" => 180.0,
"right" | "east" => 0.0,
"above left" | "north west" => 135.0,
"above right" | "north east" => 45.0,
"below left" | "south west" => 225.0,
"below right" | "south east" => 315.0,
"center" | "centered" => f64::NAN,
other => self.number(other).unwrap_or(90.0),
};
let distance = if pin { self.key_length("/tikz/pin distance", 3.0 * 0.1515) } else { self.key_length("/tikz/label distance", 0.0) };
let absolute = matches!(self.tex.meaning_of("tikra@absolute"), tex::Meaning::Macro(m) if tex::tokens_to_string(&tex::substitute(&m.body, &[])).trim() == "true");
let (at, anchor) = if angle.is_nan() {
(info.center(), "center".to_string())
} else if absolute {
let u = Point::new(angle.to_radians().cos(), angle.to_radians().sin());
let border = info.border_toward(info.center().plus(u.scale(PT)));
(border.plus(u.scale(distance)), direction_anchor(angle).to_string())
} else {
let border = info.anchor(&format!("{}", angle.trunc())).unwrap_or(info.center());
let d = border.minus(info.center());
if d.norm() < 1e-9 {
(border.plus(Point::new(angle.to_radians().cos(), angle.to_radians().sin()).scale(distance)), direction_anchor(angle).to_string())
} else {
let u = d.scale(1.0 / d.norm());
(border.plus(u.scale(distance)), auto_anchor(Point::new(u.y, -u.x).scale(PT), false).to_string())
}
};
let mut options = tikra_tokens(&format!("every {}/.try,anchor={anchor},", if pin { "pin" } else { "label" }));
options.extend(opts);
let items = self.node(NodeSpec { options, name: None, at: Some(at), text: text_tokens, place: OnPath::Point(at), point: at, coordinate: false, default_pos: None })?;
if pin {
let Some(label) = self.last_node.clone() else { return Ok(items) };
let saved = (self.last_node.clone(), self.last_node_name.clone());
self.nodes.insert("tikra@pin@node".into(), info.clone());
self.nodes.insert("tikra@pin@label".into(), label);
self.begin_group();
self.st.transform = Transform::default();
let mut path = tikra_tokens("\\path (tikra@pin@node) edge[every pin edge/.try");
path.extend(edge_options);
path.extend(tikra_tokens("] (tikra@pin@label);"));
let r = self.run_tokens(path);
self.end_group();
r?;
(self.last_node, self.last_node_name) = saved;
return Ok(items);
}
Ok(items)
}
pub fn stroke(&self) -> Stroke {
Stroke {
color: self.st.stroke_color(),
width: self.st.line_width,
dash: self.st.dash.clone().filter(|d| !d.0.is_empty()),
cap: self.st.cap,
join: self.st.join,
miter_limit: self.st.miter_limit,
opacity: self.st.draw_opacity,
}
}
}
fn auto_anchor(d: Point, right: bool) -> &'static str {
let e = 0.05 * PT;
let (x, y) = (d.x, d.y);
let left = if x > e {
if y > e {
"south east"
} else if y < -e {
"south west"
} else {
"south"
}
} else if x < -e {
if y > e {
"north east"
} else if y < -e {
"north west"
} else {
"north"
}
} else if y > 0.0 {
"east"
} else {
"west"
};
if !right {
return left;
}
match left {
"south east" => "north west",
"south west" => "north east",
"south" => "north",
"north east" => "south west",
"north west" => "south east",
"north" => "south",
"east" => "west",
_ => "east",
}
}
fn direction_anchor(angle: f64) -> &'static str {
let a = (angle.trunc() as i64).rem_euclid(360);
match a {
0..=3 => "west",
4..=86 => "south west",
87..=93 => "south",
94..=176 => "south east",
177..=183 => "east",
184..=266 => "north east",
267..=273 => "north",
274..=356 => "north west",
_ => "west",
}
}
fn whole_textcolor(t: &str) -> Option<(String, String)> {
let group = |s: &str| -> Option<(String, usize)> {
let s2 = s.strip_prefix('{')?;
let mut depth = 1;
for (i, ch) in s2.char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return Some((s2[..i].to_string(), i + 2));
}
}
_ => {}
}
}
None
};
if let Some(rest) = t.trim().strip_prefix("\\color").map(str::trim_start) {
let (c, n) = group(rest)?;
return Some((c, rest[n..].trim().to_string()));
}
let rest = t.trim().strip_prefix("\\textcolor")?.trim_start();
let (c, n) = group(rest)?;
let rest = rest[n..].trim_start();
let (inner, m) = group(rest)?;
rest[m..].trim().is_empty().then_some((c, inner))
}
fn split_colon(v: &[Token]) -> Option<(Vec<Token>, Vec<Token>)> {
let mut depth = 0usize;
for (i, t) in v.iter().enumerate() {
match t {
Token::Char(_, tex::Cat::Begin) => depth += 1,
Token::Char(_, tex::Cat::End) => depth = depth.saturating_sub(1),
Token::Char(':', _) if depth == 0 => return Some((v[..i].to_vec(), tex::strip_outer_braces(keys::trim(&v[i + 1..]).to_vec()))),
_ => {}
}
}
None
}
impl Engine {
fn inline_colors(&mut self, text: &str) -> String {
let mut out = String::new();
let mut rest = text;
while let Some(i) = rest.find("\\textcolor{") {
out.push_str(&rest[..i]);
let after = &rest[i + "\\textcolor{".len()..];
let Some(end) = after.find('}') else {
out.push_str(&rest[i..]);
return out;
};
let spec = &after[..end];
let body_start = &after[end + 1..];
let Some(body) = body_start.strip_prefix('{') else {
out.push_str(&rest[i..i + "\\textcolor{".len() + end + 1]);
rest = body_start;
continue;
};
let mut depth = 1;
let mut close = body.len();
for (k, c) in body.char_indices() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
close = k;
break;
}
}
_ => {}
}
}
let inner = &body[..close];
match self.color(spec) {
Some(c) => {
let hex = c.hex();
let hex = hex.trim_start_matches('#');
if inner.contains('$') {
out.push_str(&format!("$\\textcolor[HTML]{{{hex}}}{{{}}}$", inner.replace('$', "")));
} else {
out.push_str(&format!("$\\textcolor[HTML]{{{hex}}}{{\\text{{{inner}}}}}$"));
}
}
None => out.push_str(inner),
}
rest = body.get(close + 1..).unwrap_or("");
}
out.push_str(rest);
out
}
}