use glam::Vec2;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct LRule {
pub successor: String,
pub probability: f32,
}
impl LRule {
pub fn new(successor: impl Into<String>) -> Self {
Self {
successor: successor.into(),
probability: 1.0,
}
}
pub fn with_probability(successor: impl Into<String>, probability: f32) -> Self {
Self {
successor: successor.into(),
probability: probability.max(0.0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LineSegment {
pub start: Vec2,
pub end: Vec2,
}
#[derive(Debug, Clone, Copy)]
struct TurtleState {
position: Vec2,
angle: f32,
}
#[derive(Debug, Clone)]
pub struct LSystem {
pub axiom: String,
pub rules: HashMap<char, Vec<LRule>>,
pub angle: f32,
pub step: f32,
pub depth: usize,
pub draw_symbols: Vec<char>,
pub move_symbols: Vec<char>,
pub turn_left: char,
pub turn_right: char,
pub push_symbol: char,
pub pop_symbol: char,
pub seed: Option<u64>,
}
impl LSystem {
pub fn new(axiom: impl Into<String>, angle: f32, step: f32) -> Self {
Self {
axiom: axiom.into(),
rules: HashMap::new(),
angle,
step,
depth: 0,
draw_symbols: vec!['F'],
move_symbols: vec!['f'],
turn_left: '+',
turn_right: '-',
push_symbol: '[',
pop_symbol: ']',
seed: None,
}
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn add_rule(&mut self, predecessor: char, successor: impl Into<String>) -> &mut Self {
let entry = self.rules.entry(predecessor).or_default();
entry.push(LRule::new(successor));
self
}
pub fn add_stochastic_rule(
&mut self,
predecessor: char,
successor: impl Into<String>,
probability: f32,
) -> &mut Self {
let entry = self.rules.entry(predecessor).or_default();
entry.push(LRule::with_probability(successor, probability));
self
}
pub fn set_depth(&mut self, depth: usize) -> &mut Self {
self.depth = depth;
self
}
pub fn iterate(&self, iterations: usize) -> String {
let mut current = self.axiom.clone();
for _ in 0..iterations {
current = self.expand_once(¤t);
}
current
}
pub fn expanded(&self) -> String {
self.iterate(self.depth)
}
pub fn expanded_with_depth(&self, depth: u32) -> String {
self.iterate(depth as usize)
}
pub fn generate(&self) -> Vec<LineSegment> {
let lsystem_string = self.expanded();
self.interpret(&lsystem_string)
}
pub fn generate_with_depth(&self, depth: u32) -> Vec<LineSegment> {
let lsystem_string = self.expanded_with_depth(depth);
self.interpret(&lsystem_string)
}
fn expand_once(&self, input: &str) -> String {
let mut output = String::with_capacity(input.len() * 2);
for ch in input.chars() {
if let Some(rules) = self.rules.get(&ch) {
if rules.len() == 1 {
output.push_str(&rules[0].successor);
} else {
let chosen = self.pick_stochastic(rules);
output.push_str(&chosen.successor);
}
} else {
output.push(ch);
}
}
output
}
fn pick_stochastic<'a>(&'a self, rules: &'a [LRule]) -> &'a LRule {
let total_weight: f32 = rules.iter().map(|r| r.probability).sum();
if total_weight <= 0.0 {
return &rules[0];
}
let mut rng = self.make_rng();
let threshold = rng.next_f32() * total_weight;
let mut cumulative = 0.0;
for rule in rules {
cumulative += rule.probability;
if cumulative >= threshold {
return rule;
}
}
rules.last().unwrap()
}
fn make_rng(&self) -> SimpleRng {
match self.seed {
Some(s) => SimpleRng::new(s),
None => {
let hash = self.axiom.bytes().fold(0u64, |acc, b| {
acc.wrapping_mul(6364136223846793005).wrapping_add(b as u64)
});
SimpleRng::new(hash)
}
}
}
fn interpret(&self, program: &str) -> Vec<LineSegment> {
let mut segments = Vec::new();
let mut state = TurtleState {
position: Vec2::ZERO,
angle: 0.0_f32.to_radians(), };
let mut stack: Vec<TurtleState> = Vec::new();
let turn = self.angle.to_radians();
for ch in program.chars() {
if self.draw_symbols.contains(&ch) {
let delta = Vec2::new(state.angle.cos(), state.angle.sin()) * self.step;
let end = state.position + delta;
segments.push(LineSegment {
start: state.position,
end,
});
state.position = end;
} else if self.move_symbols.contains(&ch) {
let delta = Vec2::new(state.angle.cos(), state.angle.sin()) * self.step;
state.position += delta;
} else if ch == self.turn_left {
state.angle += turn;
} else if ch == self.turn_right {
state.angle -= turn;
} else if ch == self.push_symbol {
stack.push(state);
} else if ch == self.pop_symbol
&& let Some(restored) = stack.pop()
{
state = restored;
}
}
segments
}
}
#[derive(Debug, Clone)]
struct SimpleRng {
state: u64,
}
impl SimpleRng {
fn new(seed: u64) -> Self {
let state = if seed == 0 { 0xDEADBEEF } else { seed };
Self { state }
}
fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.state = x;
x.wrapping_mul(0x2545F4914F6CDD1D)
}
fn next_f32(&mut self) -> f32 {
let v = (self.next_u64() >> 40) as f32;
v / (1u64 << 24) as f32
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Bounds {
pub min: Vec2,
pub max: Vec2,
}
impl Bounds {
pub fn new(min: Vec2, max: Vec2) -> Self {
Self { min, max }
}
pub fn from_center_half(center: Vec2, half: Vec2) -> Self {
Self {
min: center - half,
max: center + half,
}
}
pub fn width(&self) -> f32 {
self.max.x - self.min.x
}
pub fn height(&self) -> f32 {
self.max.y - self.min.y
}
pub fn center(&self) -> Vec2 {
(self.min + self.max) * 0.5
}
pub fn contains(&self, point: Vec2) -> bool {
point.x >= self.min.x
&& point.x <= self.max.x
&& point.y >= self.min.y
&& point.y <= self.max.y
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VoronoiCell {
pub vertices: Vec<Vec2>,
pub site: Vec2,
pub centroid: Vec2,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Fragment {
pub vertices: Vec<Vec2>,
pub centroid: Vec2,
pub velocity: Vec2,
pub area: f32,
}
#[derive(Debug, Clone)]
pub struct VoronoiFracture {
pub cells: Vec<VoronoiCell>,
pub seed: u64,
rng: SimpleRng,
pub resolution: usize,
}
impl VoronoiFracture {
pub fn new() -> Self {
Self {
cells: Vec::new(),
seed: 42,
rng: SimpleRng::new(42),
resolution: 128,
}
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self.rng = SimpleRng::new(seed);
self
}
pub fn with_resolution(mut self, res: usize) -> Self {
self.resolution = res.max(8);
self
}
pub fn generate(&mut self, seed_count: usize, bounds: Bounds) {
self.cells.clear();
let sites = self.generate_sites(seed_count, bounds);
let grid = self.compute_voronoi_grid(&sites, bounds);
self.cells = self.extract_cells(&sites, &grid, bounds);
}
pub fn clip_polygon(&self, polygon: &[Vec2]) -> Vec<(Vec<Vec2>, Vec2)> {
if self.cells.is_empty() {
let centroid = polygon_centroid(polygon);
return vec![(polygon.to_vec(), centroid)];
}
let mut results = Vec::with_capacity(self.cells.len());
for cell in &self.cells {
if let Some(clipped) = Self::sutherland_hodgman(polygon, &cell.vertices)
&& clipped.len() >= 3
{
let centroid = polygon_centroid(&clipped);
results.push((clipped, centroid));
}
}
results
}
pub fn fracture(
&mut self,
bounds: Bounds,
polygon: &[Vec2],
seed_count: usize,
) -> Vec<Fragment> {
self.generate(seed_count, bounds);
let fracture_center = bounds.center();
self.clip_polygon(polygon)
.into_iter()
.filter_map(|(verts, centroid)| {
if verts.len() < 3 {
return None;
}
let area = polygon_area(&verts);
let direction = (centroid - fracture_center).normalize_or_zero();
let speed = direction * (area.sqrt() * 0.5); Some(Fragment {
vertices: verts,
centroid,
velocity: speed,
area,
})
})
.collect()
}
fn generate_sites(&mut self, count: usize, bounds: Bounds) -> Vec<Vec2> {
let mut sites = Vec::with_capacity(count);
for _ in 0..count {
let x = bounds.min.x + self.rng.next_f32() * bounds.width();
let y = bounds.min.y + self.rng.next_f32() * bounds.height();
sites.push(Vec2::new(x, y));
}
sites
}
fn compute_voronoi_grid(&self, sites: &[Vec2], bounds: Bounds) -> Vec<usize> {
let res = self.resolution;
let mut grid = vec![0usize; res * res];
for gy in 0..res {
for gx in 0..res {
let point = Vec2::new(
bounds.min.x + (gx as f32 + 0.5) / res as f32 * bounds.width(),
bounds.min.y + (gy as f32 + 0.5) / res as f32 * bounds.height(),
);
let mut best_idx = 0;
let mut best_dist = f32::MAX;
for (i, site) in sites.iter().enumerate() {
let d = point.distance_squared(*site);
if d < best_dist {
best_dist = d;
best_idx = i;
}
}
grid[gy * res + gx] = best_idx;
}
}
grid
}
fn extract_cells(&self, sites: &[Vec2], grid: &[usize], bounds: Bounds) -> Vec<VoronoiCell> {
let res = self.resolution;
let cell_width = bounds.width() / res as f32;
let cell_height = bounds.height() / res as f32;
sites
.iter()
.enumerate()
.filter_map(|(site_idx, &site)| {
let mut boundary_points: Vec<Vec2> = Vec::new();
for gy in 0..res {
for gx in 0..res {
if grid[gy * res + gx] != site_idx {
continue;
}
let is_boundary = self.is_grid_boundary(grid, gx, gy, site_idx, res);
let x0 = bounds.min.x + gx as f32 * cell_width;
let y0 = bounds.min.y + gy as f32 * cell_height;
let x1 = x0 + cell_width;
let y1 = y0 + cell_height;
if is_boundary {
boundary_points.push(Vec2::new(x0, y0));
boundary_points.push(Vec2::new(x1, y0));
boundary_points.push(Vec2::new(x1, y1));
boundary_points.push(Vec2::new(x0, y1));
} else {
boundary_points.push(Vec2::new((x0 + x1) * 0.5, (y0 + y1) * 0.5));
}
}
}
if boundary_points.is_empty() {
return None;
}
let hull = convex_hull_graham(&boundary_points);
if hull.len() < 3 {
return None;
}
let centroid = polygon_centroid(&hull);
Some(VoronoiCell {
vertices: hull,
site,
centroid,
})
})
.collect()
}
fn is_grid_boundary(
&self,
grid: &[usize],
gx: usize,
gy: usize,
site_idx: usize,
res: usize,
) -> bool {
let neighbours: [(isize, isize); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
for (dx, dy) in &neighbours {
let nx = gx as isize + dx;
let ny = gy as isize + dy;
if nx < 0 || nx >= res as isize || ny < 0 || ny >= res as isize {
return true; }
if grid[ny as usize * res + nx as usize] != site_idx {
return true;
}
}
false
}
fn sutherland_hodgman(subject: &[Vec2], clip: &[Vec2]) -> Option<Vec<Vec2>> {
if subject.len() < 3 || clip.len() < 3 {
return None;
}
let mut output = subject.to_vec();
let clip_len = clip.len();
for i in 0..clip_len {
if output.is_empty() {
return None;
}
let edge_start = clip[i];
let edge_end = clip[(i + 1) % clip_len];
let input = output;
output = Vec::with_capacity(input.len());
for j in 0..input.len() {
let current = input[j];
let next = input[(j + 1) % input.len()];
let current_inside = is_inside_edge(current, edge_start, edge_end);
let next_inside = is_inside_edge(next, edge_start, edge_end);
if current_inside {
output.push(current);
if !next_inside
&& let Some(inter) = line_intersection(current, next, edge_start, edge_end)
{
output.push(inter);
}
} else if next_inside
&& let Some(inter) = line_intersection(current, next, edge_start, edge_end)
{
output.push(inter);
}
}
}
if output.len() < 3 { None } else { Some(output) }
}
}
impl Default for VoronoiFracture {
fn default() -> Self {
Self::new()
}
}
fn polygon_area(polygon: &[Vec2]) -> f32 {
let n = polygon.len();
if n < 3 {
return 0.0;
}
let mut area = 0.0;
for i in 0..n {
let j = (i + 1) % n;
area += polygon[i].x * polygon[j].y;
area -= polygon[j].x * polygon[i].y;
}
area.abs() * 0.5
}
fn polygon_centroid(polygon: &[Vec2]) -> Vec2 {
let n = polygon.len();
if n == 0 {
return Vec2::ZERO;
}
if n == 1 {
return polygon[0];
}
if n == 2 {
return (polygon[0] + polygon[1]) * 0.5;
}
let mut cx = 0.0;
let mut cy = 0.0;
let mut signed_area = 0.0;
for i in 0..n {
let j = (i + 1) % n;
let cross = polygon[i].x * polygon[j].y - polygon[j].x * polygon[i].y;
signed_area += cross;
cx += (polygon[i].x + polygon[j].x) * cross;
cy += (polygon[i].y + polygon[j].y) * cross;
}
signed_area *= 0.5;
if signed_area.abs() < 1e-10 {
let sum: Vec2 = polygon.iter().copied().sum();
return sum / n as f32;
}
let factor = 1.0 / (6.0 * signed_area);
Vec2::new(cx * factor, cy * factor)
}
fn is_inside_edge(point: Vec2, edge_start: Vec2, edge_end: Vec2) -> bool {
let edge = edge_end - edge_start;
let to_point = point - edge_start;
edge.x * to_point.y - edge.y * to_point.x >= -1e-6
}
fn line_intersection(p1: Vec2, p2: Vec2, e1: Vec2, e2: Vec2) -> Option<Vec2> {
let d1 = p2 - p1;
let d2 = e2 - e1;
let cross = d1.x * d2.y - d1.y * d2.x;
if cross.abs() < 1e-10 {
return None; }
let t = ((e1.x - p1.x) * d2.y - (e1.y - p1.y) * d2.x) / cross;
Some(p1 + d1 * t)
}
fn convex_hull_graham(points: &[Vec2]) -> Vec<Vec2> {
let n = points.len();
if n <= 3 {
return points.to_vec();
}
let mut min_idx = 0;
for i in 1..n {
if points[i].y < points[min_idx].y
|| (points[i].y == points[min_idx].y && points[i].x < points[min_idx].x)
{
min_idx = i;
}
}
let pivot = points[min_idx];
let mut sorted: Vec<(f32, Vec2)> = points
.iter()
.enumerate()
.filter(|&(i, _)| i != min_idx)
.map(|(_, &p)| {
let angle = (p.y - pivot.y).atan2(p.x - pivot.x);
(angle, p)
})
.collect();
sorted.sort_by(|a, b| {
a.0.partial_cmp(&b.0).unwrap().then_with(|| {
let da = (a.1 - pivot).length_squared();
let db = (b.1 - pivot).length_squared();
da.partial_cmp(&db).unwrap()
})
});
let mut filtered: Vec<Vec2> = Vec::with_capacity(sorted.len());
for i in 0..sorted.len() {
if i + 1 < sorted.len() && (sorted[i + 1].0 - sorted[i].0).abs() < 1e-10 {
continue;
}
filtered.push(sorted[i].1);
}
if filtered.len() < 2 {
return vec![pivot];
}
let mut hull: Vec<Vec2> = vec![pivot, filtered[0]];
for i in 1..filtered.len() {
while hull.len() >= 2 {
let top = hull[hull.len() - 1];
let next = hull[hull.len() - 2];
let cross = (top - next).perp_dot(filtered[i] - top);
if cross <= 1e-6 {
hull.pop();
} else {
break;
}
}
hull.push(filtered[i]);
}
hull
}
#[allow(dead_code)]
trait Vec2Ext {
fn perp_dot(self, other: Vec2) -> f32;
}
impl Vec2Ext for Vec2 {
fn perp_dot(self, other: Vec2) -> f32 {
self.x * other.y - self.y * other.x
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lsystem_deterministic() {
let mut lsys = LSystem::new("F", 90.0, 1.0);
lsys.add_rule('F', "F+F-F-F+F");
let result = lsys.iterate(1);
assert_eq!(result, "F+F-F-F+F");
}
#[test]
fn test_lsystem_depth_2() {
let mut lsys = LSystem::new("F", 90.0, 1.0);
lsys.add_rule('F', "F+F-F-F+F");
let result = lsys.iterate(2);
assert!(result.contains("F+F-F-F+F"));
}
#[test]
fn test_lsystem_with_seed_deterministic() {
let mut lsys1 = LSystem::new("F", 90.0, 1.0).with_seed(123);
lsys1.add_stochastic_rule('F', "F+F", 0.5);
lsys1.add_stochastic_rule('F', "F-F", 0.5);
let mut lsys2 = LSystem::new("F", 90.0, 1.0).with_seed(123);
lsys2.add_stochastic_rule('F', "F+F", 0.5);
lsys2.add_stochastic_rule('F', "F-F", 0.5);
assert_eq!(lsys1.expanded(), lsys2.expanded());
}
#[test]
fn test_lsystem_branches_preserved() {
let mut lsys = LSystem::new("F", 90.0, 1.0);
lsys.add_rule('F', "F[+F]F[-F]F");
let result = lsys.iterate(1);
assert!(result.contains('['));
assert!(result.contains(']'));
assert!(result.contains('+'));
assert!(result.contains('-'));
}
#[test]
fn test_lsystem_generate_produces_segments() {
let mut lsys = LSystem::new("F", 60.0, 1.0);
lsys.add_rule('F', "FF+[+F-F-F]-[-F+F+F]");
lsys.set_depth(0);
let segments = lsys.generate();
assert_eq!(segments.len(), 1); }
#[test]
fn test_lsystem_no_rule_identity() {
let lsys = LSystem::new("F+F--F+F", 90.0, 1.0);
assert_eq!(lsys.expanded(), "F+F--F+F");
}
#[test]
fn test_lsystem_stochastic_probability_zero() {
let mut lsys = LSystem::new("F", 90.0, 1.0).with_seed(0);
lsys.add_stochastic_rule('F', "AB", 0.0);
lsys.add_stochastic_rule('F', "XY", 1.0);
let result = lsys.iterate(1);
assert_eq!(result, "XY");
}
#[test]
fn test_lsystem_segment_geometry() {
let lsys = LSystem::new("FF", 90.0, 2.0);
let segments = lsys.generate();
assert_eq!(segments.len(), 2);
assert_eq!(segments[0].start, Vec2::ZERO);
assert_eq!(segments[0].end, Vec2::new(2.0, 0.0));
assert_eq!(segments[1].start, Vec2::new(2.0, 0.0));
}
#[test]
fn test_lsystem_moves_no_draw() {
let lsys = LSystem::new("fF", 90.0, 1.0);
let segments = lsys.generate();
assert_eq!(segments.len(), 1); assert_eq!(segments[0].start, Vec2::new(1.0, 0.0));
}
#[test]
fn test_voronoi_generate_creates_cells() {
let mut vf = VoronoiFracture::new().with_seed(42).with_resolution(32);
let bounds = Bounds::new(Vec2::ZERO, Vec2::new(100.0, 100.0));
let fragments = vf.fracture(
bounds,
&[
Vec2::new(10.0, 10.0),
Vec2::new(90.0, 10.0),
Vec2::new(90.0, 90.0),
Vec2::new(10.0, 90.0),
],
5,
);
assert!(!fragments.is_empty());
for frag in &fragments {
assert!(frag.vertices.len() >= 3);
assert!(frag.area > 0.0);
}
}
#[test]
fn test_voronoi_deterministic() {
let bounds = Bounds::new(Vec2::ZERO, Vec2::new(100.0, 100.0));
let polygon = vec![
Vec2::new(10.0, 10.0),
Vec2::new(90.0, 10.0),
Vec2::new(90.0, 90.0),
Vec2::new(10.0, 90.0),
];
let mut vf1 = VoronoiFracture::new().with_seed(7).with_resolution(32);
let f1 = vf1.fracture(bounds, &polygon, 4);
let mut vf2 = VoronoiFracture::new().with_seed(7).with_resolution(32);
let f2 = vf2.fracture(bounds, &polygon, 4);
assert_eq!(f1.len(), f2.len());
for (a, b) in f1.iter().zip(f2.iter()) {
assert_eq!(a.vertices.len(), b.vertices.len());
assert!((a.velocity - b.velocity).length() < 1e-6);
}
}
#[test]
fn test_voronoi_single_seed() {
let bounds = Bounds::new(Vec2::ZERO, Vec2::new(100.0, 100.0));
let mut vf = VoronoiFracture::new().with_seed(0).with_resolution(32);
vf.generate(1, bounds);
assert_eq!(vf.cells.len(), 1);
}
#[test]
fn test_voronoi_zero_seeds() {
let bounds = Bounds::new(Vec2::ZERO, Vec2::new(100.0, 100.0));
let mut vf = VoronoiFracture::new();
vf.generate(0, bounds);
assert!(vf.cells.is_empty());
}
#[test]
fn test_voronoi_no_cells_returns_input() {
let vf = VoronoiFracture::new();
let polygon = vec![Vec2::ZERO, Vec2::new(10.0, 0.0), Vec2::new(10.0, 10.0)];
let result = vf.clip_polygon(&polygon);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, polygon);
}
#[test]
fn test_polygon_area_triangle() {
let tri = vec![Vec2::ZERO, Vec2::new(4.0, 0.0), Vec2::new(0.0, 3.0)];
let area = polygon_area(&tri);
assert!((area - 6.0).abs() < 1e-6);
}
#[test]
fn test_polygon_area_square() {
let sq = vec![
Vec2::ZERO,
Vec2::new(2.0, 0.0),
Vec2::new(2.0, 2.0),
Vec2::new(0.0, 2.0),
];
assert!((polygon_area(&sq) - 4.0).abs() < 1e-6);
}
#[test]
fn test_polygon_centroid_square() {
let sq = vec![
Vec2::ZERO,
Vec2::new(2.0, 0.0),
Vec2::new(2.0, 2.0),
Vec2::new(0.0, 2.0),
];
let c = polygon_centroid(&sq);
assert!((c - Vec2::new(1.0, 1.0)).length() < 1e-6);
}
#[test]
fn test_convex_hull_square() {
let points = vec![
Vec2::ZERO,
Vec2::new(1.0, 0.0),
Vec2::new(1.0, 1.0),
Vec2::new(0.0, 1.0),
Vec2::new(0.5, 0.5), ];
let hull = convex_hull_graham(&points);
eprintln!("hull: {:?}", hull);
assert_eq!(hull.len(), 4);
}
#[test]
fn test_bounds_contains() {
let b = Bounds::new(Vec2::ZERO, Vec2::new(10.0, 10.0));
assert!(b.contains(Vec2::new(5.0, 5.0)));
assert!(b.contains(Vec2::ZERO));
assert!(b.contains(Vec2::new(10.0, 10.0)));
assert!(!b.contains(Vec2::new(11.0, 5.0)));
}
#[test]
fn test_sutherland_hodgman_full_overlap() {
let subject = vec![
Vec2::ZERO,
Vec2::new(10.0, 0.0),
Vec2::new(10.0, 10.0),
Vec2::new(0.0, 10.0),
];
let clip = subject.clone();
let result = VoronoiFracture::sutherland_hodgman(&subject, &clip);
assert!(result.is_some());
}
#[test]
fn test_sutherland_hodgman_partial_overlap() {
let subject = vec![
Vec2::ZERO,
Vec2::new(10.0, 0.0),
Vec2::new(10.0, 10.0),
Vec2::new(0.0, 10.0),
];
let clip = vec![
Vec2::new(5.0, -5.0),
Vec2::new(15.0, -5.0),
Vec2::new(15.0, 5.0),
Vec2::new(5.0, 5.0),
];
let result = VoronoiFracture::sutherland_hodgman(&subject, &clip);
assert!(result.is_some());
let clipped = result.unwrap();
assert!(clipped.len() >= 3);
}
#[test]
fn test_sutherland_hodgman_no_overlap() {
let subject = vec![Vec2::ZERO, Vec2::new(1.0, 0.0), Vec2::new(1.0, 1.0)];
let clip = vec![
Vec2::new(10.0, 10.0),
Vec2::new(20.0, 10.0),
Vec2::new(20.0, 20.0),
];
assert!(VoronoiFracture::sutherland_hodgman(&subject, &clip).is_none());
}
#[test]
fn test_simple_rng_deterministic() {
let mut r1 = SimpleRng::new(99);
let mut r2 = SimpleRng::new(99);
for _ in 0..100 {
assert_eq!(r1.next_u64(), r2.next_u64());
}
}
#[test]
fn test_simple_rng_f32_range() {
let mut r = SimpleRng::new(1);
for _ in 0..1000 {
let v = r.next_f32();
assert!(v >= 0.0 && v < 1.0);
}
}
#[test]
fn test_fracture_velocities_point_outward() {
let bounds = Bounds::new(Vec2::ZERO, Vec2::new(200.0, 200.0));
let center = bounds.center();
let polygon = vec![
Vec2::new(50.0, 50.0),
Vec2::new(150.0, 50.0),
Vec2::new(150.0, 150.0),
Vec2::new(50.0, 150.0),
];
let mut vf = VoronoiFracture::new().with_seed(1).with_resolution(64);
let fragments = vf.fracture(bounds, &polygon, 6);
assert!(!fragments.is_empty());
for frag in &fragments {
let outward = (frag.centroid - center).normalize_or_zero();
if frag.velocity.length_squared() > 0.0 && outward.length_squared() > 0.0 {
let alignment = frag.velocity.normalize().dot(outward);
assert!(
alignment > -0.5,
"Fragment velocity not outward: {}",
alignment
);
}
}
}
}