diagonal.rs
25.7 kB · rust · 709 lines
1use crate::bang::factory;2use crate::bang::universe::Code;3use crate::formulas::profile_of_tile;4use mrlycore::colors::{5 BLUE, BROWN, CYAN, GREEN, INDIGO, MINT, ORANGE, PINK, PURPLE, RED, TEAL, YELLOW,6};7use mrlycore::errors::{value_error, Result};8use mrlycore::Color;910const PALETTE: [Color; 12] = [11 RED, ORANGE, YELLOW, GREEN, MINT, TEAL, CYAN, BLUE, INDIGO, PURPLE, PINK, BROWN,12];1314use crate::formulas::diagonal::WIDEST;1516struct Solid {17 filled: Vec<bool>,18 ranks: Vec<usize>,19 count: usize,20 number: usize,21 level: usize,22 side: usize,23 top: usize,24}2526impl Solid {27 fn new(code: Code, number: usize, level: usize, base: usize) -> Result<Solid> {28 if level < 1 {29 return value_error("level must be at least 1.");30 }31 let side = match number.checked_pow(level as u32) {32 Some(side) if side <= WIDEST => side,33 _ => return value_error(format!("the side must stay at or below {WIDEST}.")),34 };35 let pattern = factory::create(code, number, 3, base, 1)?;36 let filled: Vec<bool> = pattern.bytes().iter().map(|&byte| byte != 0).collect();37 let mut ranks = vec![0usize; filled.len()];38 let mut count = 0;39 for (flat, &live) in filled.iter().enumerate() {40 if !live {41 continue;42 }43 ranks[flat] = count;44 count += 1;45 }46 Ok(Solid {47 filled,48 ranks,49 count,50 number,51 level,52 side,53 top: side / number,54 })55 }5657 fn holds(&self, x: usize, y: usize, z: usize) -> bool {58 let (mut a, mut b, mut c) = (x, y, z);59 for _ in 0..self.level {60 let flat =61 ((a % self.number) * self.number + b % self.number) * self.number + c % self.number;62 if !self.filled[flat] {63 return false;64 }65 a /= self.number;66 b /= self.number;67 c /= self.number;68 }69 true70 }7172 fn rank(&self, point: [u32; 3]) -> usize {73 let (x, y, z) = (74 point[0] as usize / self.top,75 point[1] as usize / self.top,76 point[2] as usize / self.top,77 );78 self.ranks[(x * self.number + y) * self.number + z]79 }80}8182// PROFILE8384/// Counts the filled cells on every diagonal plane `x + y + z = s`, for `s` in `0..=3*(side - 1)`.85///86/// The count is the coefficient of the digit polynomial, the product over the scales of the87/// pattern's weight sums, so no cell of the solid is ever built.88///89/// ```90/// let counts = mrlymath::three::profile(126, 2, 4, 2).unwrap();91/// assert_eq!(counts[15..=30].iter().copied().collect::<Vec<u128>>(), vec![81u128; 16]);92/// ```93pub fn profile(code: Code, number: usize, level: usize, base: usize) -> Result<Vec<u128>> {94 let tile = factory::create(code, number, 3, base, 1)?;95 profile_of_tile(&tile, level as u32)96}9798/// Returns the first and last height a profile fills, or none when the design is empty.99pub fn support(counts: &[u128]) -> Option<(usize, usize)> {100 let low = counts.iter().position(|&count| count > 0)?;101 let high = counts.iter().rposition(|&count| count > 0)?;102 Some((low, high))103}104105// SLICE106107/// Lists the filled cells on the diagonal plane `x + y + z = height`, as `x, y, z` triples.108///109/// ```110/// assert_eq!(mrlymath::three::diagonal_slice(126, 2, 3, 2, 10).unwrap().len(), 27);111/// ```112pub fn slice(113 code: Code,114 number: usize,115 level: usize,116 base: usize,117 height: usize,118) -> Result<Vec<[u32; 3]>> {119 let solid = Solid::new(code, number, level, base)?;120 let last = solid.side - 1;121 let mut out = Vec::new();122 if height > 3 * last {123 return Ok(out);124 }125 for x in height.saturating_sub(2 * last)..=last.min(height) {126 let rest = height - x;127 for y in rest.saturating_sub(last)..=last.min(rest) {128 let z = rest - y;129 if solid.holds(x, y, z) {130 out.push([x as u32, y as u32, z as u32]);131 }132 }133 }134 Ok(out)135}136137// PROJECTION138139/// Projects a cell down the `(1,1,1)` axis: `u = (x - y)/sqrt 2`, `v = (x + y - 2z)/sqrt 6`.140pub fn project(point: [u32; 3]) -> (f64, f64) {141 let (x, y, z) = (point[0] as f64, point[1] as f64, point[2] as f64);142 ((x - y) / 2f64.sqrt(), (x + y - 2.0 * z) / 6f64.sqrt())143}144145/// Returns the integer shadow `(x - y, x + y - 2z)`, the projection with its irrational scales dropped.146pub fn shadow(point: [u32; 3]) -> (i64, i64) {147 let (x, y, z) = (point[0] as i64, point[1] as i64, point[2] as i64);148 (x - y, x + y - 2 * z)149}150151// SVG152153/// Draws the given diagonal slices as one circle per cell, coloured by height slot and top-scale corner.154///155/// The frame is tight around the projected points and carries no background, so the drawing sits on156/// whatever the page is.157pub fn svg(158 code: Code,159 number: usize,160 level: usize,161 base: usize,162 heights: &[usize],163 scale: usize,164) -> Result<String> {165 let solid = Solid::new(code, number, level, base)?;166 let mut circles: Vec<(f64, f64, Color)> = Vec::new();167 for (slot, &height) in heights.iter().enumerate() {168 for point in slice(code, number, level, base, height)? {169 let (u, v) = project(point);170 let key = slot * solid.count.max(1) + solid.rank(point);171 circles.push((u, v, PALETTE[key % PALETTE.len()]));172 }173 }174 if circles.is_empty() {175 return value_error("nothing to render.");176 }177 let low_u = circles.iter().map(|c| c.0).fold(f64::MAX, f64::min);178 let high_u = circles.iter().map(|c| c.0).fold(f64::MIN, f64::max);179 let low_v = circles.iter().map(|c| c.1).fold(f64::MAX, f64::min);180 let high_v = circles.iter().map(|c| c.1).fold(f64::MIN, f64::max);181 let radius = 0.35 * scale as f64;182 let width = (high_u - low_u) * scale as f64 + 2.0 * radius;183 let height = (high_v - low_v) * scale as f64 + 2.0 * radius;184 let mut out = vec![format!(185 "<svg width=\"{width:.2}\" height=\"{height:.2}\" viewBox=\"0 0 {width:.2} {height:.2}\" xmlns=\"http://www.w3.org/2000/svg\">"186 )];187 for (u, v, fill) in circles {188 let cx = (u - low_u) * scale as f64 + radius;189 let cy = (high_v - v) * scale as f64 + radius;190 out.push(format!(191 "<circle cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{radius:.2}\" fill=\"{}\"/>",192 fill.to_hex()193 ));194 }195 out.push("</svg>".to_string());196 Ok(out.join("\n"))197}198199#[cfg(test)]200mod tests {201 use super::*;202 use std::collections::{BTreeMap, HashSet};203204 fn scan(code: Code, number: usize, level: usize, base: usize) -> Vec<Vec<[u32; 3]>> {205 let cell = crate::three::create(code, number, level, base).unwrap();206 let grid = cell.types();207 let side = grid.shape[0];208 let mut out = vec![Vec::new(); 3 * (side - 1) + 1];209 for (flat, &site) in grid.bytes().iter().enumerate() {210 if site == 0 {211 continue;212 }213 let (x, rest) = (flat / (side * side), flat % (side * side));214 let (y, z) = (rest / side, rest % side);215 out[x + y + z].push([x as u32, y as u32, z as u32]);216 }217 out218 }219220 fn digit_build(level: usize) -> Vec<[u32; 3]> {221 let corners = factory::code_to_corners(126, 3, 2).unwrap();222 let mut points = vec![[0u32, 0, 0]];223 for _ in 0..level {224 let mut next = Vec::with_capacity(points.len() * corners.len());225 for point in &points {226 for corner in &corners {227 next.push([228 2 * point[0] + corner[0] as u32,229 2 * point[1] + corner[1] as u32,230 2 * point[2] + corner[2] as u32,231 ]);232 }233 }234 points = next;235 }236 points237 }238239 fn constant_gasket(level: usize, weight_two: bool) -> HashSet<[u32; 3]> {240 let corners = if weight_two {241 [[0u32, 1, 1], [1, 0, 1], [1, 1, 0]]242 } else {243 [[1u32, 0, 0], [0, 1, 0], [0, 0, 1]]244 };245 let mut set: HashSet<[u32; 3]> = HashSet::new();246 set.insert([0, 0, 0]);247 for k in 0..level {248 let mut next = HashSet::new();249 for point in &set {250 for corner in &corners {251 next.insert([252 point[0] + (corner[0] << k),253 point[1] + (corner[1] << k),254 point[2] + (corner[2] << k),255 ]);256 }257 }258 set = next;259 }260 set261 }262263 fn odd_trinomials(layer: usize) -> usize {264 let width = layer + 1;265 let mut current = vec![0u8; width * width];266 current[0] = 1;267 for step in 1..=layer {268 let mut next = vec![0u8; width * width];269 for i in 0..=step {270 for j in 0..=step - i {271 let mut sum = current[i * width + j];272 if i > 0 {273 sum += current[(i - 1) * width + j];274 }275 if j > 0 {276 sum += current[i * width + j - 1];277 }278 next[i * width + j] = sum % 2;279 }280 }281 current = next;282 }283 current.iter().filter(|&&value| value == 1).count()284 }285286 fn det3(m: [[i64; 3]; 3]) -> i64 {287 m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])288 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])289 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])290 }291292 #[test]293 fn the_slices_agree_with_a_scan_of_the_solid() {294 for code in [23u128, 105, 126, 127] {295 for number in [2usize, 3] {296 for level in 1..3 {297 let scanned = scan(code, number, level, 2);298 let counts = profile(code, number, level, 2).unwrap();299 assert_eq!(counts.len(), scanned.len());300 for (height, wanted) in scanned.iter().enumerate() {301 assert_eq!(302 counts[height],303 wanted.len() as u128,304 "code {code} number {number} level {level} height {height}"305 );306 let mut got = slice(code, number, level, 2, height).unwrap();307 let mut want = wanted.clone();308 got.sort_unstable();309 want.sort_unstable();310 assert_eq!(got, want, "code {code} number {number} level {level}");311 }312 }313 }314 }315 }316317 #[test]318 fn the_digit_build_pairs_with_every_slice_to_level_eight() {319 for level in 1..=8usize {320 let last = (1usize << level) - 1;321 let mut buckets: Vec<Vec<[u32; 3]>> = vec![Vec::new(); 3 * last + 1];322 for point in digit_build(level) {323 buckets[(point[0] + point[1] + point[2]) as usize].push(point);324 }325 let counts = profile(126, 2, level, 2).unwrap();326 assert_eq!(counts.len(), buckets.len());327 for (height, bucket) in buckets.iter_mut().enumerate() {328 assert_eq!(329 counts[height],330 bucket.len() as u128,331 "level {level} height {height}"332 );333 let mut got = slice(126, 2, level, 2, height).unwrap();334 got.sort_unstable();335 bucket.sort_unstable();336 assert_eq!(got, *bucket, "level {level} height {height}");337 }338 }339 }340341 #[test]342 fn every_slice_of_one_two_six_holds_three_to_the_level() {343 for level in 1..=14usize {344 let counts = profile(126, 2, level, 2).unwrap();345 let (low, high) = support(&counts).unwrap();346 assert_eq!((low, high), ((1 << level) - 1, (1 << (level + 1)) - 2));347 let wanted = 3u128.pow(level as u32);348 assert!(counts[low..=high].iter().all(|&count| count == wanted));349 assert_eq!(counts.iter().sum::<u128>(), 6u128.pow(level as u32));350 }351 }352353 #[test]354 fn the_two_central_heights_carry_two_times_three_to_the_level() {355 for (index, &total) in [18u128, 54, 162, 486, 1458, 4374, 13122].iter().enumerate() {356 let level = index + 2;357 let counts = profile(126, 2, level, 2).unwrap();358 let low = ((1usize << level) - 1) + (1 << (level - 1)) - 1;359 assert_eq!(counts[low] + counts[low + 1], total);360 assert_eq!(counts[low], 3u128.pow(level as u32));361 }362 }363364 #[test]365 fn the_central_union_splits_evenly_by_coordinate_order() {366 let orders = [367 [0, 1, 2],368 [0, 2, 1],369 [1, 0, 2],370 [1, 2, 0],371 [2, 0, 1],372 [2, 1, 0],373 ];374 for level in 2..=8usize {375 let low = ((1usize << level) - 1) + (1 << (level - 1)) - 1;376 let mut union = slice(126, 2, level, 2, low).unwrap();377 union.extend(slice(126, 2, level, 2, low + 1).unwrap());378 let mut classes = [0usize; 6];379 let mut ties = 0;380 for point in &union {381 if point[0] == point[1] || point[1] == point[2] || point[0] == point[2] {382 ties += 1;383 continue;384 }385 for (index, order) in orders.iter().enumerate() {386 if point[order[0]] < point[order[1]] && point[order[1]] < point[order[2]] {387 classes[index] += 1;388 }389 }390 }391 assert_eq!(ties, 6);392 let each = 3usize.pow(level as u32 - 1) - 1;393 assert!(classes.iter().all(|&size| size == each), "level {level}");394 assert_eq!(union.len(), 2 * 3usize.pow(level as u32));395 }396 }397398 #[test]399 fn the_central_union_is_six_pieces_of_seven_two_nine_at_level_seven() {400 let low = 127 + 63;401 let mut pieces: BTreeMap<(usize, usize), usize> = BTreeMap::new();402 let mut shadows = HashSet::new();403 let mut total = 0;404 for (slot, height) in [low, low + 1].into_iter().enumerate() {405 for point in slice(126, 2, 7, 2, height).unwrap() {406 let corner = ((point[0] >> 6) * 4 + (point[1] >> 6) * 2 + (point[2] >> 6)) as usize;407 *pieces.entry((slot, corner)).or_default() += 1;408 shadows.insert(shadow(point));409 total += 1;410 }411 }412 assert_eq!(total, 4374);413 assert_eq!(pieces.len(), 6);414 assert!(pieces.values().all(|&size| size == 729));415 assert_eq!(shadows.len(), 4374);416 }417418 #[test]419 fn the_central_union_is_six_gaskets_from_the_level_below() {420 for level in 2..=8usize {421 let base = (1usize << level) - 1;422 let half = 1usize << (level - 1);423 let mut pieces: Vec<HashSet<[u32; 3]>> = Vec::new();424 let mut slices: HashSet<[u32; 3]> = HashSet::new();425 for offset in [half - 1, half] {426 let gasket = constant_gasket(level - 1, offset & 1 == 1);427 let corners = if (offset >> (level - 1)) & 1 == 1 {428 [[0u32, 1, 1], [1, 0, 1], [1, 1, 0]]429 } else {430 [[1u32, 0, 0], [0, 1, 0], [0, 0, 1]]431 };432 for corner in &corners {433 pieces.push(434 gasket435 .iter()436 .map(|point| {437 [438 corner[0] * half as u32 + point[0],439 corner[1] * half as u32 + point[1],440 corner[2] * half as u32 + point[2],441 ]442 })443 .collect(),444 );445 }446 slices.extend(slice(126, 2, level, 2, base + offset).unwrap());447 }448 assert_eq!(pieces.len(), 6);449 let each = 3usize.pow(level as u32 - 1);450 assert!(451 pieces.iter().all(|piece| piece.len() == each),452 "level {level}"453 );454 for (index, piece) in pieces.iter().enumerate() {455 for other in &pieces[index + 1..] {456 assert!(piece.is_disjoint(other), "level {level}");457 }458 }459 let union: HashSet<[u32; 3]> = pieces.iter().flatten().copied().collect();460 assert_eq!(union.len(), 2 * 3usize.pow(level as u32));461 assert_eq!(union, slices, "level {level}");462 }463 }464465 #[test]466 fn the_central_union_carries_the_order_twelve_symmetry() {467 let orders = [468 [0, 1, 2],469 [0, 2, 1],470 [1, 0, 2],471 [1, 2, 0],472 [2, 0, 1],473 [2, 1, 0],474 ];475 for level in 2..=8usize {476 let low = ((1usize << level) - 1) + (1 << (level - 1)) - 1;477 let below: HashSet<[u32; 3]> =478 slice(126, 2, level, 2, low).unwrap().into_iter().collect();479 let above: HashSet<[u32; 3]> = slice(126, 2, level, 2, low + 1)480 .unwrap()481 .into_iter()482 .collect();483 let union: HashSet<[u32; 3]> = below.union(&above).copied().collect();484 for order in &orders {485 let turned: HashSet<[u32; 3]> = union486 .iter()487 .map(|point| [point[order[0]], point[order[1]], point[order[2]]])488 .collect();489 assert_eq!(turned, union, "level {level}");490 }491 let last = (1u32 << level) - 1;492 let flip = |set: &HashSet<[u32; 3]>| -> HashSet<[u32; 3]> {493 set.iter()494 .map(|point| [last - point[0], last - point[1], last - point[2]])495 .collect()496 };497 assert_eq!(flip(&below), above, "level {level}");498 assert_eq!(flip(&above), below, "level {level}");499 let mut ties: Vec<[u32; 3]> = union500 .iter()501 .copied()502 .filter(|point| {503 point[0] == point[1] || point[1] == point[2] || point[0] == point[2]504 })505 .collect();506 ties.sort_unstable();507 let m = (1u32 << (level - 1)) - 1;508 let mut wanted: Vec<[u32; 3]> = Vec::new();509 for triple in [[m, m, m + 1], [m, m + 1, m + 1]] {510 for order in &orders {511 wanted.push([triple[order[0]], triple[order[1]], triple[order[2]]]);512 }513 }514 wanted.sort_unstable();515 wanted.dedup();516 assert_eq!(wanted.len(), 6);517 assert_eq!(ties, wanted, "level {level}");518 }519 }520521 #[test]522 fn every_scheduled_slice_is_the_digit_gasket() {523 let ones = [[1u32, 0, 0], [0, 1, 0], [0, 0, 1]];524 let twos = [[0u32, 1, 1], [1, 0, 1], [1, 1, 0]];525 for level in 1..=6usize {526 let low = (1usize << level) - 1;527 for offset in 0..(1usize << level) {528 let mut set: HashSet<[u32; 3]> = HashSet::new();529 set.insert([0, 0, 0]);530 for k in 0..level {531 let corners = if (offset >> k) & 1 == 1 { twos } else { ones };532 let mut next = HashSet::new();533 for point in &set {534 for corner in &corners {535 next.insert([536 point[0] + (corner[0] << k),537 point[1] + (corner[1] << k),538 point[2] + (corner[2] << k),539 ]);540 }541 }542 set = next;543 }544 let got: HashSet<[u32; 3]> = slice(126, 2, level, 2, low + offset)545 .unwrap()546 .into_iter()547 .collect();548 assert_eq!(got, set, "level {level} offset {offset}");549 }550 }551 }552553 #[test]554 fn the_neighbours_profile_at_level_four() {555 for (code, span, nonempty, low, high) in [556 (63u128, (0usize, 30usize), 31usize, 1u128, 81u128),557 (105, (0, 30), 16, 1, 81),558 (111, (0, 30), 31, 1, 111),559 (126, (15, 30), 16, 81, 81),560 (127, (0, 30), 31, 1, 162),561 ] {562 let counts = profile(code, 2, 4, 2).unwrap();563 assert_eq!(support(&counts).unwrap(), span);564 let live: Vec<u128> = counts[span.0..=span.1]565 .iter()566 .copied()567 .filter(|&count| count > 0)568 .collect();569 assert_eq!(live.len(), nonempty, "code {code}");570 assert_eq!(*live.iter().min().unwrap(), low, "code {code}");571 assert_eq!(*live.iter().max().unwrap(), high, "code {code}");572 let flat = live.len() == span.1 - span.0 + 1 && low == high;573 assert_eq!(flat, code == 126);574 }575 }576577 #[test]578 fn the_one_two_seven_cut_matches_no_closed_form() {579 for (index, &top) in [3u128, 12, 45, 162, 594, 2187].iter().enumerate() {580 let level = index + 1;581 let counts = profile(127, 2, level, 2).unwrap();582 assert_eq!(*counts.iter().max().unwrap(), top);583 assert_eq!(*counts.iter().filter(|&&count| count > 0).min().unwrap(), 1);584 assert_eq!(counts.iter().sum::<u128>(), 7u128.pow(level as u32));585 let closed = 4 * (level as u128 + 5) * 3u128.pow(level as u32 - 1);586 assert_ne!(top, closed);587 }588 }589590 #[test]591 fn the_flat_slice_is_the_odd_layer_of_pascals_pyramid() {592 for (index, &wanted) in [3usize, 9, 27, 81, 243].iter().enumerate() {593 let level = index + 1;594 let low = (1usize << level) - 1;595 let points = slice(126, 2, level, 2, low).unwrap();596 assert_eq!(points.len(), 3usize.pow(level as u32));597 assert_eq!(points.len(), wanted);598 assert_eq!(odd_trinomials(low), wanted);599 }600 }601602 #[test]603 fn the_flat_slice_has_pairwise_disjoint_binary_supports() {604 for level in 1..=6usize {605 let last = (1u32 << level) - 1;606 let got: HashSet<[u32; 3]> = slice(126, 2, level, 2, last as usize)607 .unwrap()608 .into_iter()609 .collect();610 let mut wanted: HashSet<[u32; 3]> = HashSet::new();611 for x in 0..=last {612 for y in 0..=last - x {613 let z = last - x - y;614 if x & y == 0 && y & z == 0 && x & z == 0 {615 wanted.insert([x, y, z]);616 }617 }618 }619 assert_eq!(got, wanted, "level {level}");620 assert_eq!(got.len(), 3usize.pow(level as u32));621 }622 }623624 #[test]625 fn the_twenty_three_slices_are_three_to_the_digit_sum() {626 assert_eq!(627 &profile(23, 2, 3, 2).unwrap()[..8],628 &[1u128, 3, 3, 9, 3, 9, 9, 27]629 );630 for level in 1..=6u32 {631 let counts = profile(23, 2, level as usize, 2).unwrap();632 for (height, &count) in counts.iter().enumerate() {633 let wanted = if height < (1 << level) {634 3u128.pow((height as u32).count_ones())635 } else {636 0637 };638 assert_eq!(count, wanted, "level {level} height {height}");639 }640 }641 }642643 #[test]644 fn the_six_cut_codes_are_canonical() {645 for code in [23u128, 63, 105, 111, 126, 127] {646 let orbit = crate::bang::universe::orbit(code, 3);647 assert_eq!(*orbit.iter().next().unwrap(), code);648 }649 }650651 #[test]652 fn the_centred_corners_are_the_octahedron_axes() {653 let corners = factory::code_to_corners(126, 3, 2).unwrap();654 let doubled: Vec<[i64; 3]> = corners655 .iter()656 .map(|corner| {657 [658 2 * corner[0] as i64 - 1,659 2 * corner[1] as i64 - 1,660 2 * corner[2] as i64 - 1,661 ]662 })663 .collect();664 assert_eq!(doubled.len(), 6);665 for axis in &doubled {666 assert!(doubled.contains(&[-axis[0], -axis[1], -axis[2]]));667 }668 let basis = [doubled[0], doubled[1], doubled[2]];669 let mat = [670 [basis[0][0], basis[1][0], basis[2][0]],671 [basis[0][1], basis[1][1], basis[2][1]],672 [basis[0][2], basis[1][2], basis[2][2]],673 ];674 let det = det3(mat);675 assert_eq!(det, -4);676 for axis in &doubled {677 let mut hits = 0;678 for k in 0..3 {679 let mut probe = mat;680 probe[0][k] = axis[0];681 probe[1][k] = axis[1];682 probe[2][k] = axis[2];683 let value = det3(probe);684 if value == 0 {685 continue;686 }687 assert_eq!(value.abs(), det.abs());688 hits += 1;689 }690 assert_eq!(hits, 1);691 }692 }693694 #[test]695 fn the_svg_draws_one_circle_for_every_cell() {696 let art = svg(126, 2, 3, 2, &[10, 11], 4).unwrap();697 assert_eq!(art.matches("<circle").count(), 54);698 assert!(!art.contains("<rect"));699 let fills: HashSet<&str> = art700 .split("fill=\"")701 .skip(1)702 .map(|piece| &piece[..7])703 .collect();704 assert_eq!(fills.len(), 6);705 assert!(fills.contains(RED.to_hex().as_str()));706 assert!(svg(0, 2, 2, 2, &[3], 4).is_err());707 assert!(profile(126, 2, 0, 2).is_err());708 }709}