use crate::scene::{Point, Transform};
use crate::tex::{Meaning, Token};
use crate::{Error, PT};
use super::{tikra_tokens, Engine};
pub const BASE: u16 = 880;
const COMMANDS: &[&str] = &["tikra@folding"];
pub fn define(e: &mut Engine) {
for (i, name) in COMMANDS.iter().enumerate() {
e.tex.define(name, Meaning::Native(BASE + i as u16));
}
}
pub fn is_folding(id: u16) -> bool {
(BASE..BASE + COMMANDS.len() as u16).contains(&id)
}
const NETS: &[(&str, Option<&str>, &str)] = &[
(
"dodecahedron",
Some("dodecahedron"),
"P(1 P(2 P(3 e e f e e) e f e e) P(4 P(5 e e f c e) e f c c) P(6 P(7 e e f c e) e f c c) P(8 P(9 e e f c e) e f c c) P(10 P(11 c c f c P(12 c \
c f c c)) c f c c))",
),
(
"alternatedodecahedron",
Some("dodecahedron'"),
"P(1 P(2 c e f c P(3 P(4 e e f c e) P(5 e e f c e) f P(6 e e f e c) P(7 e e f c e))) e P(8 P(9 c e f c c) P(10 c c f c c) f P(11 c e f c c) \
P(12 c e f c c)) e e)",
),
("tetrahedron", Some("tetrahedron"), "T(1 T(2 e f c) T(3 e f c) T(4 e f c))"),
("cube", Some("cube"), "S(1 S(2 c e f e) S(3 c c f c) S(4 S(5 e c f e) e f e) S(6 e c f c))"),
("octahedron", Some("octahedron"), "T(1 T(2 T(3 c f T(4 c f c)) f T(5 e f e)) T(6 e f e) T(7 c f T(8 e f c)))"),
(
"icosahedron",
Some("icosahedron"),
"T(1 T(2 T(3 T(4 c f T(5 c f T(6 e f T(7 e f c)))) f e) f e) T(8 T(9 T(10 c f T(11 c f T(12 T(13 c f c) f T(14 e f c)))) f e) f e) T(15 T(16 \
T(17 c f T(18 c f T(19 e f T(20 e f c)))) f e) f e))",
),
("truncatedtetrahedron", Some("tetrahedron truncated"), "H(1 H(2 e e e f e c) T(3 c f c) H(4 e e e f e c) T(5 c f c) H(6 T(7 c f c) e e f e c) T(8 c f c))"),
(
"cuboctahedron",
Some("cuboctahedron"),
"S(1 T(2 S(3 T(4 c f S(5 e e f e)) e f e) f c) T(6 S(7 e e f e) f c) T(8 S(9 e e f T(10 c f c)) f c) T(11 S(12 T(13 c f c) e f T(14 c f c)) f \
c))",
),
(
"truncatedcube",
Some("cube truncated"),
"O(1 O(2 e T(3 e f e) c T(4 e f e) f T(5 e f e) c T(6 e f e)) c O(7 c c e T(8 e f e) f c e c) c O(9 O(10 c c e T(11 e f e) f T(12 e f e) e c) \
c c c f c c c) c O(13 c c e c f T(14 e f e) e c) c)",
),
(
"truncatedoctahedron",
Some("octahedron truncated"),
"H(1 H(2 S(3 e c f c) H(4 e e e f e e) S(5 e e f c) f S(6 e c f e) H(7 e e e f e e)) c e S(8 H(9 H(10 c c e f e c) S(11 c e f c) H(12 c c c f \
c c) f H(13 c c c f c c) S(14 c c f e)) e f e) e c)",
),
(
"rhombicuboctahedron",
Some("rhombicuboctahedron"),
"S(1 S(2 S(3 S(4 S(5 c S(6 e e f c) f S(7 e c f e)) T(8 e f c) f T(9 c f e)) S(10 e e f c) f S(11 e c f e)) T(12 e f c) f T(13 c f e)) S(14 \
S(15 c c f c) e f c) S(16 S(17 S(18 e T(19 c f e) f T(20 e f c)) S(21 e c f e) f S(22 e e f c)) T(23 c f e) f T(24 e f c)) S(25 S(26 c c f \
c) c f e))",
),
(
"truncatedcuboctahedron",
Some("cuboctahedron truncated"),
"S(1 O(2 S(3 O(4 S(5 c H(6 e e >15 f e e) f H(7 e e e f <15 e)) c S(8 e c f c) c f c S(9 e c f c) c) H(10 e e >15 f <15 e) f H(11 e e >15 f \
<15 e)) c S(12 e c f c) c f c S(13 e c f c) c) H(14 e e >15 f <15 e) O(15 S(16 O(17 e c S(18 e c f c) c f c S(19 e c f c) c) H(20 e e >15 f \
<15 e) f H(21 e e >15 f <15 e)) c S(22 O(23 c c c c f c c c) c f c) c f c S(24 O(25 c c c c f c c c) c f c) c) H(26 e e >15 f <15 e))",
),
(
"snubcube",
Some("snub cube"),
"S(1 T(2 T(3 T(4 e f S(5 T(6 T(7 e f e) f T(8 e f c)) T(9 e f e) f T(10 c f c))) f c) f c) T(11 T(12 T(13 e f S(14 T(15 T(16 e f e) f T(17 e \
f c)) T(18 e f e) f T(19 c f c))) f c) f c) T(20 T(21 T(22 e f S(23 T(24 T(25 e f e) f T(26 e f c)) T(27 e f e) f T(28 c f c))) f c) f c) \
T(29 T(30 T(31 e f S(32 T(33 T(34 e f e) f T(35 S(36 c c f c) f c)) T(37 e f e) f T(38 c f c))) f c) f c))",
),
(
"icosidodecahedron",
Some("icosidodecahedron"),
"P(1 T(2 P(3 T(4 e f e) >24 f c T(5 e f c)) f c) T(6 P(7 e >24 f c T(8 e f P(9 e T(10 e f e) f c c))) f c) T(11 >24 f c) T(12 P(13 T(14 e f \
P(15 T(16 e f e) e f c c)) >24 f c T(17 >24 f c)) f P(18 T(19 P(20 e >24 f T(21 P(22 c >24 f c e) f c) T(23 e f c)) f c) T(24 >24 f c) f c \
T(25 P(26 e >24 f c c) f c))) T(27 P(28 T(29 e f P(30 T(31 c f c) e f c c)) >24 f c T(32 >24 f c)) f c))",
),
(
"rhombicdodecahedron",
Some("dodecahedron rhombic"),
"R(2 L(3@90 c e f e) R(5 L(6@90 c c f e) c f R(4@180 c c f c)) L(8@-90 R(7@180 L(12@-90 R(11@180 e c f e) L(10@90 e c f e) f e) c f e) L(8 e \
e f e) f e) R(6 c c f c))",
),
(
"goldenrhombicdodecahedron",
Some("dodecahedron golden"),
"G(1 g(2@180 c e f f) G(5@180 g(8 c c f e) c f g(7 c c f c)) g(4@180 G(9 g(12 G(11@180 e c f e) g(10@180 e c f f) f e) c f e) G(6 e e f e) f \
f) G(3@180 c c f c))",
),
(
"rhombictricontahedron",
None,
"G(1 g(2 G(6 g(9 G(10 g(11 f e f e) e f e) e f f) f f G(8 f c f e)) g(7 G(12 g(14 f c f e) c f G(13 f c f e)) e f f) f c) G(3 g(14 G(14 g(14 \
f c f e) c f e) c f e) e f G(14 f c f e)) g(4 G(4 g(4 G(4 f e f e) e f e) e f G(4 f e f e)) g(4 f e f e) f e) G(5 g(5 G(5 g(5 f e f f) e f \
G(5 f e f c)) e f c) f f G(5 g(5 G(5 f e f f) e f e) e f f)))",
),
];
pub fn definitions() -> String {
let mut s = String::from(
r"\def\tikra@fold@length{2cm}
\tikzset{folding line length/.code={\def\tikra@fold@length{#1}}, every ear/.style={}, every cut/.style={}, every fold/.style={help lines}}
",
);
let mut numbered = Vec::new();
for n in 1..=92 {
s.push_str(&format!(
"\\expandafter\\def\\csname tikra@fold@face@{n}\\endcsname{{}}\\tikzset{{face {n}/.code={{\\expandafter\\def\\csname tikra@fold@face@{n}\\endcsname{{#1}}}}}}\n"
));
numbered.push(format!("face {n}={{\\node{{{n}}};}}"));
}
s.push_str(&format!("\\tikzset{{numbered faces/.style={{{}}}}}\n", numbered.join(",")));
for (name, pic, _) in NETS {
s.push_str(&format!("\\def\\tikzfolding{name}#1[#2]#3;{{\\begingroup\\tikzset{{#2}}\\tikra@folding{{{name}}}\\endgroup}}\n"));
if let Some(pic) = pic {
s.push_str(&format!("\\tikzset{{pics/{pic} folding/.style={{code={{\\tikra@folding{{{name}}}}}}}}}\n"));
}
}
s
}
#[derive(Debug, Clone, PartialEq)]
enum Item {
Face(usize, f64),
Fold,
Cut,
Ear,
EarLeft(f64),
EarRight(f64),
Polygon(Polygon),
}
#[derive(Debug, Clone, PartialEq)]
struct Polygon {
kind: char,
face: Option<Box<Item>>,
edges: Vec<Item>,
}
fn parse(s: &str) -> Option<Polygon> {
let mut words = Vec::new();
let mut word = String::new();
for c in s.chars() {
match c {
'(' => {
word.push('(');
words.push(std::mem::take(&mut word));
}
')' | ' ' => {
if !word.is_empty() {
words.push(std::mem::take(&mut word));
}
if c == ')' {
words.push(")".into());
}
}
c => word.push(c),
}
}
let mut it = words.into_iter().peekable();
match polygon(&mut it)? {
Item::Polygon(p) => Some(p),
_ => None,
}
}
fn polygon(it: &mut std::iter::Peekable<std::vec::IntoIter<String>>) -> Option<Item> {
let w = it.next()?;
if let Some(kind) = w.strip_suffix('(') {
let kind = kind.chars().next()?;
let mut items = Vec::new();
while it.peek().map(String::as_str) != Some(")") {
items.push(polygon(it)?);
}
it.next();
let mut items = items.into_iter();
let face = items.next().filter(|f| matches!(f, Item::Face(..))).map(Box::new);
return Some(Item::Polygon(Polygon { kind, face, edges: items.collect() }));
}
Some(match w.as_str() {
"f" => Item::Fold,
"c" => Item::Cut,
"e" => Item::Ear,
_ if w.starts_with('<') => Item::EarLeft(w[1..].parse().ok()?),
_ if w.starts_with('>') => Item::EarRight(w[1..].parse().ok()?),
_ => {
let (n, turn) = w.split_once('@').unwrap_or((&w, "0"));
Item::Face(n.parse().ok()?, turn.parse().ok()?)
}
})
}
fn vertices(kind: char) -> (Vec<Point>, Point, f64) {
let rhombus = |angle: f64, center: (f64, f64), slant: f64| {
let (s, c) = angle.to_radians().sin_cos();
(vec![Point::new(0.0, 0.0), Point::new(1.0, 0.0), Point::new(1.0 + c, s), Point::new(c, s)], Point::new(center.0, center.1), slant)
};
match kind {
'R' => return rhombus(70.529, (0.63, 0.4755), 0.34),
'L' => return rhombus(109.471, (0.33, 0.4755), -0.34),
'G' => return rhombus(63.435, (0.724, 0.447), 0.447),
'g' => return rhombus(116.565, (0.276, 0.447), -0.447),
_ => {}
}
let n = match kind {
'T' => 3,
'S' => 4,
'P' => 5,
'H' => 6,
'O' => 8,
_ => 10,
};
let mut v = vec![Point::new(0.0, 0.0)];
for k in 0..n - 1 {
let (s, c) = (360.0 * k as f64 / n as f64).to_radians().sin_cos();
let last = v[v.len() - 1];
v.push(Point::new(last.x + c, last.y + s));
}
let center = Point::new(0.5, 0.5 / (std::f64::consts::PI / n as f64).tan());
(v, center, 0.0)
}
fn edge_frames(v: &[Point], length: f64) -> Vec<Transform> {
let n = v.len();
let start = n / 2 + 1;
(0..n)
.map(|i| {
let (a, b) = (v[(start + n - i) % n], v[(start + 2 * n - i - 1) % n]);
let angle = (b.y - a.y).atan2(b.x - a.x).to_degrees();
Transform::translation(a.x * length, a.y * length).compose(&Transform::rotation(angle))
})
.collect()
}
impl Engine {
pub fn folding_command(&mut self, _id: u16) -> Result<(), Error> {
let name = self.argument_string()?;
let Some((_, _, net)) = NETS.iter().find(|(n, _, _)| *n == name) else {
return Err(Error::Figure(format!("no folding named '{name}'")));
};
let net = parse(net).ok_or_else(|| Error::Figure(format!("internal: the net of {name} is malformed")))?;
let length = self.tex.expand_to_string(vec![Token::cs("tikra@fold@length")])?;
let length = self.length(&length)?;
self.fold_polygon(&net, length)
}
fn fold_polygon(&mut self, p: &Polygon, length: f64) -> Result<(), Error> {
let (v, center, slant) = vertices(p.kind);
if let Some(face) = &p.face {
let frame = Transform::translation(center.x * length, center.y * length).compose(&Transform { c: slant, ..Transform::default() });
self.fold_scope(frame, face, length)?;
}
for (frame, item) in edge_frames(&v, length).into_iter().zip(&p.edges) {
self.fold_scope(frame, item, length)?;
}
Ok(())
}
fn fold_scope(&mut self, frame: Transform, item: &Item, length: f64) -> Result<(), Error> {
self.begin_group();
self.begin_scope();
self.st.transform = self.st.transform.compose(&frame);
let r = self.fold_item(item, length);
self.end_scope();
self.end_group();
r
}
fn fold_item(&mut self, item: &Item, length: f64) -> Result<(), Error> {
let l = length / PT;
let pt = |x: f64, y: f64| format!("({}pt,{}pt)", x, y);
let (o, end) = (pt(0.0, 0.0), pt(l, 0.0));
let fold = format!(r"\draw[every fold]{o} -- {end};");
let code = match item {
Item::Face(n, turn) => {
self.st.transform = self.st.transform.compose(&Transform::rotation(*turn));
return self.run_tokens(vec![Token::cs(&format!("tikra@fold@face@{n}"))]);
}
Item::Fold => fold,
Item::Cut => format!(r"\draw[every cut]{o} -- {end};"),
Item::Ear | Item::EarLeft(_) | Item::EarRight(_) => {
let (tip, from_end) = match item {
Item::EarLeft(a) => {
let (s, c) = a.to_radians().sin_cos();
(pt(0.66 * l * c, 0.66 * l * s), false)
}
Item::EarRight(a) => {
let (s, c) = a.to_radians().sin_cos();
(pt(l - 0.66 * l * c, 0.66 * l * s), true)
}
_ => (pt(0.5 * l, 0.2 * l), false),
};
let cut = if from_end { format!("{end} -- {tip} -- {o}") } else { format!("{o} -- {tip} -- {end}") };
let area = if from_end { format!("{o} -- {end} -- {tip}") } else { format!("{o} -- {tip} -- {end}") };
format!(r"\path[every ear]{area} -- {o} -- cycle;{fold}\draw[every cut]{cut};")
}
Item::Polygon(p) => return self.fold_polygon(p, length),
};
self.run_tokens(tikra_tokens(&code))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_net_reads() {
for (name, _, net) in NETS {
let p = parse(net).unwrap_or_else(|| panic!("{name}"));
assert!(p.face.is_some(), "{name}");
}
let t = parse("T(1 T(2 e f c) <30 3@90)").unwrap();
assert_eq!(t.edges[1], Item::EarLeft(30.0));
assert_eq!(t.edges[2], Item::Face(3, 90.0));
}
#[test]
fn edges_go_clockwise_from_the_top() {
let (v, center, _) = vertices('S');
let f = edge_frames(&v, 1.0);
let at = |t: &Transform, x: f64| t.apply(Point::new(x, 0.0));
assert!((at(&f[0], 0.0).y - 1.0).abs() < 1e-9 && (at(&f[0], 1.0).x - 1.0).abs() < 1e-9);
assert!((at(&f[2], 0.0).x - 1.0).abs() < 1e-9 && at(&f[2], 1.0).x.abs() < 1e-9);
assert!((center.x - 0.5).abs() < 1e-9 && (center.y - 0.5).abs() < 1e-9);
}
}