use std::collections::VecDeque;
use crate::classify::grow::replay_placements;
use crate::cyclotomic::IsRing;
use crate::geom::iso::Iso;
use crate::geom::matches::PatchMatch;
use crate::geom::patch::boundary_vertices;
use crate::geom::rat::Rat;
use crate::vis::draw::{TileStyle, rainbow};
use crate::vis::plotutils::R64;
use crate::vis::scene::{Color, Fill, Scene, Stroke, TextStyle, Viewport};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TileColoring {
Corona,
Rotation,
}
pub fn corona_ring_colors<T: IsRing>(base_verts: &[T], placements: &[Iso<T>]) -> Vec<Color> {
let polys: Vec<Vec<T>> = placements.iter().map(|iso| iso.tile(base_verts)).collect();
let rings = corona_rings::<T>(&polys);
let pal = ring_ramp(rings.iter().copied().max().map_or(1, |m| m + 1));
rings.iter().map(|&r| pal[r]).collect()
}
pub fn orientation_colors<T: IsRing>(placements: &[Iso<T>]) -> Vec<Color> {
let turn = T::turn() as usize;
let pal = rainbow(turn, 0.60, 0.55);
placements
.iter()
.map(|iso| pal[(iso.rot as usize).rem_euclid(turn)])
.collect()
}
pub fn scene_from_placements<T: IsRing>(
base_verts: &[T],
placements: &[Iso<T>],
fills: &[Color],
labels: Option<&[String]>,
border: f64,
) -> Scene {
let mut scene = Scene::new().with_background(Color::WHITE);
for (i, iso) in placements.iter().enumerate() {
let pts: Vec<(f64, f64)> = iso.tile(base_verts).iter().map(|q| q.xy()).collect();
let mut st = TileStyle::filled(Fill::solid(fills[i]), Stroke::solid(Color::BLACK, border));
if let Some(l) = labels {
st = st.with_center_label(l[i].clone(), TextStyle::new(0.45, Color::BLACK).bold());
}
scene.draw_tile(&pts, &st);
}
scene
}
fn build_scene<T: IsRing>(
base: &Rat<T>,
build: &[PatchMatch],
coloring: TileColoring,
) -> Option<(Scene, R64)> {
let placements = replay_placements(base, build)?;
let verts = boundary_vertices::<T>(base.seq());
let colors = match coloring {
TileColoring::Rotation => orientation_colors::<T>(&placements),
TileColoring::Corona => corona_ring_colors::<T>(&verts, &placements),
};
let scene = scene_from_placements::<T>(&verts, &placements, &colors, None, 0.05);
let bounds = scene.auto_bounds()?;
Some((scene, bounds))
}
pub fn witness_svg<T: IsRing>(
base: &Rat<T>,
build: &[PatchMatch],
coloring: TileColoring,
side_px: u32,
) -> Option<String> {
let (scene, bounds) = build_scene(base, build, coloring)?;
Some(scene.to_svg(&Viewport::square_for(side_px, bounds, side_px / 50)))
}
fn corona_rings<T: IsRing>(polys: &[Vec<T>]) -> Vec<usize> {
let n = polys.len();
let mut ring = vec![usize::MAX; n];
if n == 0 {
return ring;
}
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for a in 0..n {
for b in (a + 1)..n {
let touch = polys[a].iter().any(|p| polys[b].contains(p));
if touch {
adj[a].push(b);
adj[b].push(a);
}
}
}
ring[0] = 0;
let mut q = VecDeque::from([0usize]);
while let Some(u) = q.pop_front() {
for &v in &adj[u] {
if ring[v] == usize::MAX {
ring[v] = ring[u] + 1;
q.push_back(v);
}
}
}
for r in &mut ring {
if *r == usize::MAX {
*r = 0;
}
}
ring
}
fn ring_ramp(nrings: usize) -> Vec<Color> {
const RAMP: [Color; 5] = [
Color::rgb(230, 126, 34), Color::rgb(52, 120, 190), Color::rgb(120, 175, 225), Color::rgb(176, 206, 236), Color::rgb(208, 224, 242), ];
(0..nrings.max(1))
.map(|k| RAMP[k.min(RAMP.len() - 1)])
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::classify::heesch::{count_coronas, heesch_number_witnessed};
use crate::cyclotomic::ZZ12;
use crate::geom::tileset::TileSet;
#[test]
fn witness_renders_and_rings_match_heesch_number() {
let seq: &[i8] = &[-4, 0, 2, 2, 2, 2, -2, 0, 2, 0, 2, 4, -2, 0, 4];
let base = Rat::<ZZ12>::from_slice_trusted(seq);
let (_h, build) = heesch_number_witnessed(TileSet::single(base.clone()), 0, 3, 200_000);
let placements = replay_placements(&base, &build).expect("replay");
let verts = boundary_vertices::<ZZ12>(base.seq());
let polys: Vec<Vec<ZZ12>> = placements.iter().map(|iso| iso.tile(&verts)).collect();
let rings = corona_rings::<ZZ12>(&polys);
assert_eq!(rings[0], 0, "seed must be ring 0");
let maxr = rings.iter().copied().max().unwrap();
assert_eq!(
maxr,
count_coronas(&base, &build),
"max ring must equal the corona count"
);
for r in 0..=maxr {
assert!(rings.contains(&r), "ring {r} should be non-empty");
}
for mode in [TileColoring::Corona, TileColoring::Rotation] {
let svg = witness_svg(&base, &build, mode, 400).expect("svg");
assert!(svg.starts_with("<svg"), "svg well-formed ({mode:?})");
assert!(svg.contains("<polygon"), "svg has tile polygons ({mode:?})");
}
}
}