#![allow(
clippy::disallowed_macros,
reason = "raw-helper: this module calls the platform f32/f64 max/min via crate::raw_minmax::raw_minmax_here!() at sites proven safe by nonzero-const:/both-abs:/int-cast: reasoning, see scripts/raw_minmax_manifest.toml"
)]
crate::raw_minmax::raw_minmax_here!(max_f64);
use std::f64::consts::PI;
use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum LaserElement {
Line(f64, f64, f64, f64),
Circle(f64, f64, f64),
Dot(f64, f64),
Polyline(Vec<(f64, f64)>),
}
#[derive(Debug, Clone, Copy)]
pub struct Bounds {
pub x: f64,
pub y: f64,
pub w: f64,
pub h: f64,
}
impl Bounds {
#[must_use]
pub const fn new(x: f64, y: f64, w: f64, h: f64) -> Self {
Self { x, y, w, h }
}
#[must_use]
pub fn contains(&self, px: f64, py: f64) -> bool {
px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h
}
#[must_use]
#[allow(clippy::suboptimal_flops)]
pub const fn center(&self) -> (f64, f64) {
(self.x + self.w * 0.5, self.y + self.h * 0.5)
}
}
#[must_use]
pub fn hatch(angle_deg: f64, spacing: f64, bounds: &Bounds) -> Vec<LaserElement> {
if spacing <= 0.0 {
return Vec::new();
}
let angle = angle_deg.to_radians();
let cos_a = alice_det_math::cos64(angle);
let sin_a = alice_det_math::sin64(angle);
let corners = [
(bounds.x, bounds.y),
(bounds.x + bounds.w, bounds.y),
(bounds.x + bounds.w, bounds.y + bounds.h),
(bounds.x, bounds.y + bounds.h),
];
let projections: Vec<f64> = corners
.iter()
.map(|&(cx, cy)| (-sin_a).mul_add(cx, cos_a * cy))
.collect();
let proj_min = projections
.iter()
.copied()
.fold(f64::INFINITY, crate::det_math::min64);
let proj_max = projections
.iter()
.copied()
.fold(f64::NEG_INFINITY, crate::det_math::max64);
let mut elements = Vec::new();
let d0 = (proj_min / spacing).ceil() * spacing;
let line_count = cell_count(proj_max - d0, spacing) + 1;
for i in 0..line_count {
let d = usize_f64(i).mul_add(spacing, d0);
if let Some((x1, y1, x2, y2)) = clip_line_to_rect(cos_a, sin_a, d, bounds) {
elements.push(LaserElement::Line(x1, y1, x2, y2));
}
}
elements
}
#[must_use]
pub fn crosshatch(spacing: f64, bounds: &Bounds) -> Vec<LaserElement> {
let mut elements = hatch(0.0, spacing, bounds);
elements.extend(hatch(90.0, spacing, bounds));
elements
}
#[must_use]
#[allow(clippy::while_float)] pub fn density_hatch(
min_spacing: f64,
max_spacing: f64,
angle_deg: f64,
bounds: &Bounds,
density_fn: &dyn Fn(f64, f64) -> f64,
) -> Vec<LaserElement> {
if min_spacing <= 0.0 || max_spacing <= 0.0 {
return Vec::new();
}
let angle = angle_deg.to_radians();
let cos_a = alice_det_math::cos64(angle);
let sin_a = alice_det_math::sin64(angle);
let corners = [
(bounds.x, bounds.y),
(bounds.x + bounds.w, bounds.y),
(bounds.x + bounds.w, bounds.y + bounds.h),
(bounds.x, bounds.y + bounds.h),
];
let projections: Vec<f64> = corners
.iter()
.map(|&(cx, cy)| (-sin_a).mul_add(cx, cos_a * cy))
.collect();
let proj_min = projections
.iter()
.copied()
.fold(f64::INFINITY, crate::det_math::min64);
let proj_max = projections
.iter()
.copied()
.fold(f64::NEG_INFINITY, crate::det_math::max64);
let mut elements = Vec::new();
let mut d = proj_min;
while d <= proj_max {
if let Some((x1, y1, x2, y2)) = clip_line_to_rect(cos_a, sin_a, d, bounds) {
elements.push(LaserElement::Line(x1, y1, x2, y2));
let mx = f64::midpoint(x1, x2);
let my = f64::midpoint(y1, y2);
let density = density_fn(mx, my).clamp(0.0, 1.0);
let spacing = (max_spacing - min_spacing).mul_add(-density, max_spacing);
d += spacing;
} else {
d += min_spacing;
}
}
elements
}
fn clip_line_to_rect(
cos_a: f64,
sin_a: f64,
d: f64,
bounds: &Bounds,
) -> Option<(f64, f64, f64, f64)> {
let x_min = bounds.x;
let x_max = bounds.x + bounds.w;
let y_min = bounds.y;
let y_max = bounds.y + bounds.h;
let mut intersections = Vec::with_capacity(4);
let eps = 1e-12;
if sin_a.abs() > eps {
let x = cos_a.mul_add(-y_min, d) / (-sin_a);
if x >= x_min - eps && x <= x_max + eps {
intersections.push((x.clamp(x_min, x_max), y_min));
}
}
if sin_a.abs() > eps {
let x = cos_a.mul_add(-y_max, d) / (-sin_a);
if x >= x_min - eps && x <= x_max + eps {
intersections.push((x.clamp(x_min, x_max), y_max));
}
}
if cos_a.abs() > eps {
let y = sin_a.mul_add(x_min, d) / cos_a;
if y >= y_min - eps && y <= y_max + eps {
intersections.push((x_min, y.clamp(y_min, y_max)));
}
}
if cos_a.abs() > eps {
let y = sin_a.mul_add(x_max, d) / cos_a;
if y >= y_min - eps && y <= y_max + eps {
intersections.push((x_max, y.clamp(y_min, y_max)));
}
}
intersections.dedup_by(|a, b| (a.0 - b.0).abs() < eps && (a.1 - b.1).abs() < eps);
if intersections.len() >= 2 {
Some((
intersections[0].0,
intersections[0].1,
intersections[1].0,
intersections[1].1,
))
} else {
None
}
}
#[must_use]
pub fn halftone(
lpi: f64,
max_radius: f64,
bounds: &Bounds,
brightness_fn: &dyn Fn(f64, f64) -> f64,
) -> Vec<LaserElement> {
if lpi <= 0.0 || max_radius <= 0.0 {
return Vec::new();
}
let cell_size = 25.4 / lpi; let mut elements = Vec::new();
let cols = cell_count(bounds.w, cell_size);
let rows = cell_count(bounds.h, cell_size);
for row in 0..rows {
for col in 0..cols {
let cx = cell_center(col, cell_size, bounds.x);
let cy = cell_center(row, cell_size, bounds.y);
if !bounds.contains(cx, cy) {
continue;
}
let brightness = brightness_fn(cx, cy).clamp(0.0, 1.0);
let radius = max_radius * (1.0 - brightness);
if radius > 0.01 {
elements.push(LaserElement::Circle(cx, cy, radius));
}
}
}
elements
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DitherAlgorithm {
FloydSteinberg,
Atkinson,
Stucki,
Jarvis,
Bayer4x4,
}
#[must_use]
pub fn dither(
algorithm: DitherAlgorithm,
dpi: f64,
bounds: &Bounds,
brightness_fn: &dyn Fn(f64, f64) -> f64,
) -> Vec<LaserElement> {
if dpi <= 0.0 {
return Vec::new();
}
let pixel_size = 25.4 / dpi;
let cols = cell_count(bounds.w, pixel_size).max(1);
let rows = cell_count(bounds.h, pixel_size).max(1);
let mut grid: Vec<Vec<f64>> = Vec::with_capacity(rows);
for row in 0..rows {
let mut scanline = Vec::with_capacity(cols);
for col in 0..cols {
let x = cell_center(col, pixel_size, bounds.x);
let y = cell_center(row, pixel_size, bounds.y);
let b = brightness_fn(x, y).clamp(0.0, 1.0);
scanline.push(b);
}
grid.push(scanline);
}
match algorithm {
DitherAlgorithm::Bayer4x4 => dither_ordered(&grid, pixel_size, bounds, cols, rows),
_ => dither_error_diffusion(algorithm, &mut grid, pixel_size, bounds, cols, rows),
}
}
fn dither_error_diffusion(
algorithm: DitherAlgorithm,
grid: &mut [Vec<f64>],
pixel_size: f64,
bounds: &Bounds,
cols: usize,
rows: usize,
) -> Vec<LaserElement> {
let (offsets, divisor): (&[(isize, isize, f64)], f64) = match algorithm {
DitherAlgorithm::FloydSteinberg => {
(&[(1, 0, 7.0), (-1, 1, 3.0), (0, 1, 5.0), (1, 1, 1.0)], 16.0)
}
DitherAlgorithm::Atkinson => (
&[
(1, 0, 1.0),
(2, 0, 1.0),
(-1, 1, 1.0),
(0, 1, 1.0),
(1, 1, 1.0),
(0, 2, 1.0),
],
8.0, ),
DitherAlgorithm::Stucki => (
&[
(1, 0, 8.0),
(2, 0, 4.0),
(-2, 1, 2.0),
(-1, 1, 4.0),
(0, 1, 8.0),
(1, 1, 4.0),
(2, 1, 2.0),
(-2, 2, 1.0),
(-1, 2, 2.0),
(0, 2, 4.0),
(1, 2, 2.0),
(2, 2, 1.0),
],
42.0,
),
DitherAlgorithm::Jarvis => (
&[
(1, 0, 7.0),
(2, 0, 5.0),
(-2, 1, 3.0),
(-1, 1, 5.0),
(0, 1, 7.0),
(1, 1, 5.0),
(2, 1, 3.0),
(-2, 2, 1.0),
(-1, 2, 3.0),
(0, 2, 5.0),
(1, 2, 3.0),
(2, 2, 1.0),
],
48.0,
),
DitherAlgorithm::Bayer4x4 => unreachable!(),
};
let mut dots = Vec::new();
for y in 0..rows {
for x in 0..cols {
let old = grid[y][x];
let new_val = if old < 0.5 { 0.0 } else { 1.0 };
let error = old - new_val;
grid[y][x] = new_val;
for &(dx, dy, weight) in offsets {
let (Some(nx), Some(ny)) = (x.checked_add_signed(dx), y.checked_add_signed(dy))
else {
continue;
};
if nx < cols && ny < rows {
grid[ny][nx] += error * weight / divisor;
}
}
if new_val < 0.5 {
let px = cell_center(x, pixel_size, bounds.x);
let py = cell_center(y, pixel_size, bounds.y);
dots.push(LaserElement::Dot(px, py));
}
}
}
dots
}
fn dither_ordered(
grid: &[Vec<f64>],
pixel_size: f64,
bounds: &Bounds,
cols: usize,
rows: usize,
) -> Vec<LaserElement> {
#[rustfmt::skip]
const BAYER4: [[f64; 4]; 4] = [
[ 0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0],
[12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0],
[ 3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0],
[15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0],
];
let mut dots = Vec::new();
for y in 0..rows {
for x in 0..cols {
let threshold = BAYER4[y & 3][x & 3];
if grid[y][x] < threshold {
let px = cell_center(x, pixel_size, bounds.x);
let py = cell_center(y, pixel_size, bounds.y);
dots.push(LaserElement::Dot(px, py));
}
}
}
dots
}
#[must_use]
pub fn guilloche(
big_r: f64,
small_r: f64,
pen_d: f64,
steps: u32,
bounds: &Bounds,
) -> Vec<LaserElement> {
if small_r.abs() < 1e-12 || steps == 0 {
return Vec::new();
}
let (cx, cy) = bounds.center();
let t_max = 2.0 * PI * small_r / gcd_f64(big_r, small_r);
let dt = t_max / f64::from(steps);
let mut points = Vec::with_capacity(steps as usize + 1);
for i in 0..=steps {
let t = f64::from(i) * dt;
let diff = big_r - small_r;
let ratio = diff / small_r;
let x = pen_d.mul_add(
alice_det_math::cos64(ratio * t),
diff.mul_add(alice_det_math::cos64(t), cx),
);
let y = pen_d.mul_add(
-alice_det_math::sin64(ratio * t),
diff.mul_add(alice_det_math::sin64(t), cy),
);
points.push((x, y));
}
vec![LaserElement::Polyline(points)]
}
#[must_use]
pub fn lissajous(
freq_a: f64,
freq_b: f64,
delta: f64,
amplitude: f64,
steps: u32,
bounds: &Bounds,
) -> Vec<LaserElement> {
if steps == 0 {
return Vec::new();
}
let (cx, cy) = bounds.center();
let dt = 2.0 * PI / f64::from(steps);
let mut points = Vec::with_capacity(steps as usize + 1);
for i in 0..=steps {
let t = f64::from(i) * dt;
let x = amplitude.mul_add(alice_det_math::sin64(freq_a.mul_add(t, delta)), cx);
let y = amplitude.mul_add(alice_det_math::sin64(freq_b * t), cy);
points.push((x, y));
}
vec![LaserElement::Polyline(points)]
}
#[must_use]
#[allow(clippy::many_single_char_names)] pub fn rose(k: f64, amplitude: f64, steps: u32, bounds: &Bounds) -> Vec<LaserElement> {
if steps == 0 {
return Vec::new();
}
let (cx, cy) = bounds.center();
let t_max = 2.0 * PI;
let dt = t_max / f64::from(steps);
let mut points = Vec::with_capacity(steps as usize + 1);
for i in 0..=steps {
let t = f64::from(i) * dt;
let r = amplitude * alice_det_math::cos64(k * t);
let x = cx + r * alice_det_math::cos64(t);
let y = cy + r * alice_det_math::sin64(t);
points.push((x, y));
}
vec![LaserElement::Polyline(points)]
}
#[must_use]
pub fn phyllotaxis(n: u32, scale: f64, bounds: &Bounds) -> Vec<LaserElement> {
let (cx, cy) = bounds.center();
let golden_angle = 137.508_f64.to_radians();
let mut dots = Vec::with_capacity(n as usize);
for i in 0..n {
let theta = f64::from(i) * golden_angle;
let r = scale * f64::from(i).sqrt();
let x = r.mul_add(alice_det_math::cos64(theta), cx);
let y = r.mul_add(alice_det_math::sin64(theta), cy);
if bounds.contains(x, y) {
dots.push(LaserElement::Dot(x, y));
}
}
dots
}
#[must_use]
pub fn turing(
feed: f64,
kill: f64,
resolution: u32,
iterations: u32,
bounds: &Bounds,
threshold: f64,
) -> Vec<LaserElement> {
let n = resolution as usize;
if n < 4 {
return Vec::new();
}
let du = 0.16_f64;
let dv = 0.08_f64;
let dt = 1.0_f64;
let mut u = vec![vec![1.0_f64; n]; n];
let mut v = vec![vec![0.0_f64; n]; n];
let center = n / 2;
let seed_r = (n / 5).max(2);
for row in center.saturating_sub(seed_r)..=(center + seed_r).min(n - 1) {
for col in center.saturating_sub(seed_r)..=(center + seed_r).min(n - 1) {
u[row][col] = 0.5;
v[row][col] = 0.25;
}
}
let small_r = (n / 8).max(1);
for &(sy, sx) in &[
(n / 4, n / 4),
(n / 4, 3 * n / 4),
(3 * n / 4, n / 4),
(3 * n / 4, 3 * n / 4),
] {
for row in sy.saturating_sub(small_r)..=(sy + small_r).min(n - 1) {
for col in sx.saturating_sub(small_r)..=(sx + small_r).min(n - 1) {
u[row][col] = 0.5;
v[row][col] = 0.25;
}
}
}
let mut u_next = vec![vec![0.0_f64; n]; n];
let mut v_next = vec![vec![0.0_f64; n]; n];
for _ in 0..iterations {
for y in 0..n {
for x in 0..n {
let yp = if y == 0 { n - 1 } else { y - 1 };
let yn = if y == n - 1 { 0 } else { y + 1 };
let xp = if x == 0 { n - 1 } else { x - 1 };
let xn = if x == n - 1 { 0 } else { x + 1 };
let lap_u = 4.0f64.mul_add(-u[y][x], u[yp][x] + u[yn][x] + u[y][xp] + u[y][xn]);
let lap_v = 4.0f64.mul_add(-v[y][x], v[yp][x] + v[yn][x] + v[y][xp] + v[y][xn]);
let uv2 = u[y][x] * v[y][x] * v[y][x];
u_next[y][x] = dt.mul_add(
feed.mul_add(1.0 - u[y][x], du.mul_add(lap_u, -uv2)),
u[y][x],
);
v_next[y][x] = dt.mul_add(
(feed + kill).mul_add(-v[y][x], dv.mul_add(lap_v, uv2)),
v[y][x],
);
}
}
std::mem::swap(&mut u, &mut u_next);
std::mem::swap(&mut v, &mut v_next);
}
let cell_w = bounds.w / usize_f64(n);
let cell_h = bounds.h / usize_f64(n);
let mut dots = Vec::new();
#[allow(clippy::needless_range_loop)]
for y in 0..n {
for x in 0..n {
if v[y][x] >= threshold {
let px = cell_center(x, cell_w, bounds.x);
let py = cell_center(y, cell_h, bounds.y);
dots.push(LaserElement::Dot(px, py));
}
}
}
dots
}
#[must_use]
pub fn elements_to_svg(
elements: &[LaserElement],
stroke_color: &str,
stroke_width: f64,
bounds: &Bounds,
) -> String {
let mut svg = String::with_capacity(elements.len() * 80);
let _ = write!(
svg,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<svg xmlns=\"http://www.w3.org/2000/svg\"\n\
\x20 viewBox=\"{} {} {} {}\"\n\
\x20 width=\"{}mm\" height=\"{}mm\">\n",
bounds.x, bounds.y, bounds.w, bounds.h, bounds.w, bounds.h
);
for elem in elements {
match elem {
LaserElement::Line(x1, y1, x2, y2) => {
let _ = writeln!(
svg,
"<line x1=\"{x1:.3}\" y1=\"{y1:.3}\" x2=\"{x2:.3}\" y2=\"{y2:.3}\" \
stroke=\"{stroke_color}\" stroke-width=\"{stroke_width}\"/>"
);
}
LaserElement::Circle(cx, cy, r) => {
let _ = writeln!(
svg,
"<circle cx=\"{cx:.3}\" cy=\"{cy:.3}\" r=\"{r:.3}\" \
fill=\"{stroke_color}\"/>"
);
}
LaserElement::Dot(x, y) => {
let half = stroke_width * 0.5;
let _ = writeln!(
svg,
"<rect x=\"{:.3}\" y=\"{:.3}\" width=\"{stroke_width:.3}\" \
height=\"{stroke_width:.3}\" fill=\"{stroke_color}\"/>",
x - half,
y - half
);
}
LaserElement::Polyline(points) => {
if points.is_empty() {
continue;
}
svg.push_str("<polyline points=\"");
for (i, (x, y)) in points.iter().enumerate() {
if i > 0 {
svg.push(' ');
}
let _ = write!(svg, "{x:.3},{y:.3}");
}
let _ = writeln!(
svg,
"\" fill=\"none\" stroke=\"{stroke_color}\" \
stroke-width=\"{stroke_width}\"/>"
);
}
}
}
svg.push_str("</svg>\n");
svg
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn cell_count(extent: f64, cell: f64) -> usize {
raw_max_f64(
(extent / cell).ceil(),
0.0,
crate::raw_minmax::Reason::IntCast,
) as usize
}
fn cell_center(i: usize, cell: f64, origin: f64) -> f64 {
(usize_f64(i) + 0.5).mul_add(cell, origin)
}
#[allow(clippy::cast_precision_loss)]
const fn usize_f64(i: usize) -> f64 {
i as f64
}
#[allow(clippy::while_float)] fn gcd_f64(lhs: f64, rhs: f64) -> f64 {
let mut big = lhs.abs();
let mut small = rhs.abs();
let eps = 1e-9;
while small > eps {
let remainder = big % small;
big = small;
small = remainder;
}
big
}
#[cfg(test)]
mod tests {
use super::*;
fn card_bounds() -> Bounds {
Bounds::new(0.0, 0.0, 86.0, 54.0)
}
#[test]
fn test_hatch_horizontal() {
let elems = hatch(0.0, 2.0, &card_bounds());
assert!(!elems.is_empty());
for elem in &elems {
if let LaserElement::Line(_, y1, _, y2) = elem {
assert!((y1 - y2).abs() < 1e-9, "水平線のy座標が不一致");
}
}
}
#[test]
fn test_hatch_vertical() {
let elems = hatch(90.0, 2.0, &card_bounds());
assert!(!elems.is_empty());
for elem in &elems {
if let LaserElement::Line(x1, _, x2, _) = elem {
assert!((x1 - x2).abs() < 1e-9, "垂直線のx座標が不一致");
}
}
}
#[test]
fn test_hatch_diagonal() {
let elems = hatch(45.0, 3.0, &card_bounds());
assert!(!elems.is_empty());
}
#[test]
fn test_hatch_zero_spacing() {
let elems = hatch(0.0, 0.0, &card_bounds());
assert!(elems.is_empty());
}
#[test]
fn test_hatch_line_count() {
let spacing = 2.0;
let elems = hatch(0.0, spacing, &card_bounds());
let expected = cell_count(54.0, spacing);
assert!(
elems.len().abs_diff(expected) <= 1,
"線数 {} が期待値 {} と大幅に異なる",
elems.len(),
expected
);
}
#[test]
fn test_crosshatch() {
let elems = crosshatch(3.0, &card_bounds());
assert!(!elems.is_empty());
let h_count = hatch(0.0, 3.0, &card_bounds()).len();
let v_count = hatch(90.0, 3.0, &card_bounds()).len();
assert_eq!(elems.len(), h_count + v_count);
}
#[test]
fn test_density_hatch_uniform() {
let elems = density_hatch(2.0, 2.0, 0.0, &card_bounds(), &|_, _| 0.5);
assert!(!elems.is_empty());
}
#[test]
fn test_density_hatch_center_dense() {
let bounds = card_bounds();
let (cx, cy) = bounds.center();
let max_dist = cx.hypot(cy);
let elems = density_hatch(0.5, 4.0, 0.0, &bounds, &|x, y| {
let dx = x - cx;
let dy = y - cy;
1.0 - dx.hypot(dy) / max_dist
});
assert!(!elems.is_empty());
}
#[test]
fn test_halftone_uniform_black() {
let elems = halftone(20.0, 0.5, &card_bounds(), &|_, _| 0.0);
assert!(!elems.is_empty());
for elem in &elems {
if let LaserElement::Circle(_, _, r) = elem {
assert!((*r - 0.5).abs() < 0.01);
}
}
}
#[test]
fn test_halftone_uniform_white() {
let elems = halftone(20.0, 0.5, &card_bounds(), &|_, _| 1.0);
assert!(elems.is_empty());
}
#[test]
fn test_halftone_gradient() {
let bounds = card_bounds();
let elems = halftone(30.0, 0.4, &bounds, &|x, _| x / bounds.w);
assert!(!elems.is_empty());
}
#[test]
fn test_dither_floyd_steinberg() {
let bounds = Bounds::new(0.0, 0.0, 10.0, 10.0);
let elems = dither(DitherAlgorithm::FloydSteinberg, 50.0, &bounds, &|x, _| {
x / 10.0
});
assert!(!elems.is_empty());
}
#[test]
fn test_dither_atkinson() {
let bounds = Bounds::new(0.0, 0.0, 10.0, 10.0);
let elems = dither(DitherAlgorithm::Atkinson, 50.0, &bounds, &|_, y| y / 10.0);
assert!(!elems.is_empty());
}
#[test]
fn test_dither_stucki() {
let bounds = Bounds::new(0.0, 0.0, 10.0, 10.0);
let elems = dither(DitherAlgorithm::Stucki, 50.0, &bounds, &|x, y| {
(x + y) / 20.0
});
assert!(!elems.is_empty());
}
#[test]
fn test_dither_jarvis() {
let bounds = Bounds::new(0.0, 0.0, 10.0, 10.0);
let elems = dither(DitherAlgorithm::Jarvis, 50.0, &bounds, &|_, _| 0.3);
assert!(!elems.is_empty());
}
#[test]
fn test_dither_bayer() {
let bounds = Bounds::new(0.0, 0.0, 10.0, 10.0);
let elems = dither(DitherAlgorithm::Bayer4x4, 50.0, &bounds, &|x, _| x / 10.0);
assert!(!elems.is_empty());
}
#[test]
fn test_dither_all_white() {
let bounds = Bounds::new(0.0, 0.0, 5.0, 5.0);
let elems = dither(DitherAlgorithm::FloydSteinberg, 50.0, &bounds, &|_, _| 1.0);
assert!(elems.is_empty());
}
#[test]
fn test_guilloche_basic() {
let elems = guilloche(10.0, 7.0, 5.0, 1000, &card_bounds());
assert_eq!(elems.len(), 1);
if let LaserElement::Polyline(pts) = &elems[0] {
assert!(pts.len() > 100);
}
}
#[test]
fn test_guilloche_equal_radii() {
let elems = guilloche(5.0, 5.0, 3.0, 500, &card_bounds());
assert_eq!(elems.len(), 1);
}
#[test]
fn test_lissajous_basic() {
let elems = lissajous(3.0, 2.0, PI * 0.5, 20.0, 1000, &card_bounds());
assert_eq!(elems.len(), 1);
if let LaserElement::Polyline(pts) = &elems[0] {
assert_eq!(pts.len(), 1001);
}
}
#[test]
fn test_rose_3_petals() {
let elems = rose(3.0, 20.0, 500, &card_bounds());
assert_eq!(elems.len(), 1);
}
#[test]
fn test_rose_4_petals() {
let elems = rose(2.0, 20.0, 500, &card_bounds());
assert_eq!(elems.len(), 1);
}
#[test]
fn test_phyllotaxis_basic() {
let elems = phyllotaxis(500, 0.5, &card_bounds());
assert!(!elems.is_empty());
assert!(elems.len() <= 500);
}
#[test]
fn test_phyllotaxis_clipping() {
let small = Bounds::new(40.0, 24.0, 6.0, 6.0);
let elems = phyllotaxis(1000, 0.5, &small);
assert!(elems.len() < 1000);
}
#[test]
fn test_turing_spots() {
let bounds = Bounds::new(0.0, 0.0, 20.0, 20.0);
let elems = turing(0.055, 0.062, 64, 5000, &bounds, 0.15);
assert!(!elems.is_empty());
}
#[test]
fn test_turing_low_resolution() {
let bounds = Bounds::new(0.0, 0.0, 10.0, 10.0);
let elems = turing(0.040, 0.060, 3, 100, &bounds, 0.2);
assert!(elems.is_empty());
}
#[test]
fn test_elements_to_svg() {
let bounds = card_bounds();
let elems = hatch(0.0, 5.0, &bounds);
let svg = elements_to_svg(&elems, "#FFFFFF", 0.1, &bounds);
assert!(svg.contains("<svg"));
assert!(svg.contains("</svg>"));
assert!(svg.contains("<line"));
}
#[test]
fn test_svg_contains_polyline() {
let bounds = card_bounds();
let elems = guilloche(10.0, 7.0, 5.0, 200, &bounds);
let svg = elements_to_svg(&elems, "#C0C0C0", 0.05, &bounds);
assert!(svg.contains("<polyline"));
}
#[test]
fn test_svg_contains_circles() {
let bounds = card_bounds();
let elems = halftone(20.0, 0.3, &bounds, &|_, _| 0.5);
let svg = elements_to_svg(&elems, "#FFFFFF", 0.1, &bounds);
assert!(svg.contains("<circle"));
}
#[test]
fn test_svg_contains_dots() {
let bounds = Bounds::new(0.0, 0.0, 10.0, 10.0);
let elems = dither(DitherAlgorithm::FloydSteinberg, 30.0, &bounds, &|_, _| 0.3);
let svg = elements_to_svg(&elems, "#FFFFFF", 0.08, &bounds);
assert!(svg.contains("<rect"));
}
}