๐Ÿ”ฎ :: Circle Packing 2D - Greedy Algorithm

BamgasiJMยท2025๋…„ 9์›” 12์ผ

Nannou <Generative Art>

๋ชฉ๋ก ๋ณด๊ธฐ
6/55
post-thumbnail

Cargo.toml ์˜์กด์„ฑ ์ถ”๊ฐ€:
[dependencies]
nannou = "0.19"
rand = "0.8"

๐Ÿ“ Rust Code

use nannou::prelude::*;
use rand::prelude::*;

// ์‚ฌ์šฉ์ž ์„ค์ • ์ƒ์ˆ˜
const NUM_CANDIDATES: usize = 2000;  // ํ›„๋ณด ์ค‘์‹ฌ์  ์ƒ˜ํ”Œ ์ˆ˜
const R_MIN: f32 = 2.0;              // ์ตœ์†Œ ์› ๋ฐ˜์ง€๋ฆ„
const R_MAX: f32 = 50.0;             // ์ตœ๋Œ€ ์› ๋ฐ˜์ง€๋ฆ„
const RELAX_ITERS: usize = 30;       // ์œ„์น˜ ์ตœ์ ํ™” ๋ฐ˜๋ณต ํšŸ์ˆ˜
const RELAX_STEP: f32 = 0.5;         // ์ตœ์ ํ™” ์‹œ ์ด๋™ ์Šคํ… ํฌ๊ธฐ
const SEED: u64 = 42;                // ๋žœ๋ค ์‹œ๋“œ
const EDGE_EPS: f32 = 1e-6;          // ๊ฒฝ๊ณ„์„  ํŒ์ • ํ—ˆ์šฉ ์˜ค์ฐจ

#[derive(Clone, Copy, Debug)]
struct Circle {
    position: Vec2,
    radius: f32,
}

struct Model {
    circles: Vec<Circle>,
    polygon: Vec<Vec2>,
    _window: window::Id,
}

fn main() {
    nannou::app(model).update(update).run();
}

fn model(app: &App) -> Model {
    let _window = app.new_window()
        .size(1080, 1080)
        .view(view)
        .build()
        .unwrap();
    
    // ๊ธฐ๋ณธ ๋‹ค๊ฐํ˜• ์ •์˜ (์‚ฌ๊ฐํ˜•)
    let polygon = vec![
        Vec2::new(-540.0, -540.0),
        Vec2::new(540.0, -540.0),
        Vec2::new(540.0, 540.0),
        Vec2::new(-540.0, 540.0),
    ];
    
    // Circle packing ์‹คํ–‰
    let circles = run_circle_packing(&polygon);
    
    Model {
        circles,
        polygon,
        _window,
    }
}

fn update(_app: &App, _model: &mut Model, _update: Update) {
    // ์—…๋ฐ์ดํŠธ ๋กœ์ง (ํ•„์š”์‹œ ์ถ”๊ฐ€)
}

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();
    
    // ๋ฐฐ๊ฒฝ์„ ๊ฒ€์€์ƒ‰์œผ๋กœ ์„ค์ •
    draw.background().color(BLACK);
    
    // ๋‹ค๊ฐํ˜• ๊ฒฝ๊ณ„์„  ๊ทธ๋ฆฌ๊ธฐ (์˜ต์…˜)
    for i in 0..model.polygon.len() {
        let start = model.polygon[i];
        let end = model.polygon[(i + 1) % model.polygon.len()];
        draw.line()
            .start(start)
            .end(end)
            .color(GRAY)
            .stroke_weight(1.0);
    }
    
    // ์›๋“ค ๊ทธ๋ฆฌ๊ธฐ (ํฐ์ƒ‰)
    for circle in &model.circles {
        draw.ellipse()
            .xy(circle.position)
            .radius(circle.radius)
            // .color(WHITE)
            .color(hsv(random_range(0.0, 1.0), 0.5, 0.9))
            .stroke_weight(0.0);
    }
    
    draw.to_frame(app, &frame).unwrap();
}

fn run_circle_packing(polygon: &[Vec2]) -> Vec<Circle> {
    println!("์ƒ˜ํ”Œ ํ›„๋ณด ์ƒ์„ฑ ์ค‘...");
    let candidates = sample_candidates(polygon, NUM_CANDIDATES);
    println!("ํ›„๋ณด ์ˆ˜: {}", candidates.len());
    
    println!("๊ทธ๋ฆฌ๋”” ํŒจํ‚น ์ˆ˜ํ–‰ ์ค‘...");
    let mut circles = greedy_pack(polygon, &candidates, R_MIN, R_MAX);
    println!("์ดˆ๊ธฐ ๋ฐฐ์น˜๋œ ์› ์ˆ˜: {}", circles.len());
    
    if RELAX_ITERS > 0 && !circles.is_empty() {
        println!("๋กœ์ปฌ ๋ฆด๋ž™์„ธ์ด์…˜ ์ˆ˜ํ–‰ ์ค‘...");
        circles = relax_circles(polygon, circles, RELAX_ITERS, RELAX_STEP);
    }
    
    println!("์™„๋ฃŒ. ์ƒ์„ฑ๋œ ์› ์ˆ˜: {}", circles.len());
    circles
}

// ์ ์—์„œ ์„ ๋ถ„๊นŒ์ง€์˜ ์ตœ๋‹จ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ (2D)
fn dist_point_segment_2d(p: Vec2, a: Vec2, b: Vec2) -> f32 {
    let pa = p - a;
    let ba = b - a;
    let ba_len_squared = ba.length_squared();
    
    if ba_len_squared == 0.0 {
        return (p - a).length();
    }
    
    let t = (pa.dot(ba) / ba_len_squared).clamp(0.0, 1.0);
    let proj = a + ba * t;
    (p - proj).length()
}

// ์ ์ด ๋‹ค๊ฐํ˜• ๋‚ด๋ถ€์— ์žˆ๋Š”์ง€ ํŒ๋‹จ (ray casting + edge check)
fn point_in_poly_2d(pt: Vec2, poly_verts: &[Vec2]) -> bool {
    let n = poly_verts.len();
    if n < 3 {
        return false;
    }
    
    // 1) ๊ฒฝ๊ณ„์„  ์ฒดํฌ
    for i in 0..n {
        let a = poly_verts[i];
        let b = poly_verts[(i + 1) % n];
        if dist_point_segment_2d(pt, a, b) <= EDGE_EPS {
            return true;
        }
    }
    
    // 2) Ray casting ์•Œ๊ณ ๋ฆฌ์ฆ˜
    let mut inside = false;
    let x = pt.x;
    let y = pt.y;
    
    for i in 0..n {
        let xi = poly_verts[i].x;
        let yi = poly_verts[i].y;
        let xj = poly_verts[(i + 1) % n].x;
        let yj = poly_verts[(i + 1) % n].y;
        
        if (yi > y) != (yj > y) {
            let x_int = xi + (y - yi) * (xj - xi) / (yj - yi);
            if x < x_int {
                inside = !inside;
            }
        }
    }
    
    inside
}

// ์ ์—์„œ ๋‹ค๊ฐํ˜• ๊ฒฝ๊ณ„์„ ๊นŒ์ง€์˜ ์ตœ๋‹จ๊ฑฐ๋ฆฌ
fn distance_to_boundary(p: Vec2, poly_verts: &[Vec2]) -> f32 {
    let mut min_dist = f32::INFINITY;
    let n = poly_verts.len();
    
    for i in 0..n {
        let a = poly_verts[i];
        let b = poly_verts[(i + 1) % n];
        let d = dist_point_segment_2d(p, a, b);
        if d < min_dist {
            min_dist = d;
        }
    }
    
    min_dist
}

// ์ ์—์„œ ๊ธฐ์กด ์›๋“ค๊นŒ์ง€์˜ ์ตœ์†Œ ์—ฌ์œ ๊ฑฐ๋ฆฌ
fn min_clearance_to_circles(p: Vec2, circles: &[Circle]) -> f32 {
    if circles.is_empty() {
        return f32::INFINITY;
    }
    
    let mut min_clear = f32::INFINITY;
    for circle in circles {
        let d = (p - circle.position).length() - circle.radius;
        if d < min_clear {
            min_clear = d;
        }
    }
    
    min_clear
}

// ๋‹ค๊ฐํ˜• ๋‚ด๋ถ€์—์„œ ๋ฌด์ž‘์œ„ ํ›„๋ณด์  ์ƒ์„ฑ
fn sample_candidates(poly_verts: &[Vec2], num_candidates: usize) -> Vec<Vec2> {
    let mut rng = StdRng::seed_from_u64(SEED);
    
    // AABB ๊ณ„์‚ฐ
    let xs: Vec<f32> = poly_verts.iter().map(|v| v.x).collect();
    let ys: Vec<f32> = poly_verts.iter().map(|v| v.y).collect();
    let min_x = xs.iter().fold(f32::INFINITY, |a, &b| a.min(b));
    let max_x = xs.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
    let min_y = ys.iter().fold(f32::INFINITY, |a, &b| a.min(b));
    let max_y = ys.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
    
    let mut candidates = Vec::new();
    let mut tries = 0;
    let max_tries = num_candidates * 20;
    
    while candidates.len() < num_candidates && tries < max_tries {
        let x = rng.gen_range(min_x..max_x);
        let y = rng.gen_range(min_y..max_y);
        let pt = Vec2::new(x, y);
        
        if point_in_poly_2d(pt, poly_verts) {
            candidates.push(pt);
        }
        tries += 1;
    }
    
    candidates
}

// ๊ทธ๋ฆฌ๋”” ํŒจํ‚น ์•Œ๊ณ ๋ฆฌ์ฆ˜
fn greedy_pack(poly_verts: &[Vec2], candidates: &[Vec2], r_min: f32, r_max: f32) -> Vec<Circle> {
    // ํ›„๋ณด์ ๋“ค์„ ๊ฐ€๋Šฅํ•œ ๋ฐ˜์ง€๋ฆ„ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ
    let mut cand_info: Vec<(Vec2, f32)> = candidates.iter()
        .map(|&p| {
            let r_bound = distance_to_boundary(p, poly_verts).min(r_max);
            (p, r_bound)
        })
        .filter(|(_, r_bound)| *r_bound >= r_min)
        .collect();
    
    // ๋ฐ˜์ง€๋ฆ„ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ
    cand_info.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
    
    let mut circles = Vec::new();
    
    for (p, r_bound) in cand_info {
        if circles.is_empty() {
            circles.push(Circle {
                position: p,
                radius: r_bound,
            });
            continue;
        }
        
        let min_clear = min_clearance_to_circles(p, &circles);
        let allowed = r_bound.min(min_clear);
        
        if allowed >= r_min {
            circles.push(Circle {
                position: p,
                radius: allowed,
            });
        }
    }
    
    circles
}

// ๋กœ์ปฌ ๋ฆด๋ž™์„ธ์ด์…˜์œผ๋กœ ์œ„์น˜ ์ตœ์ ํ™”
fn relax_circles(poly_verts: &[Vec2], mut circles: Vec<Circle>, iters: usize, step: f32) -> Vec<Circle> {
    if iters == 0 || circles.is_empty() {
        return circles;
    }
    
    let mut rng = StdRng::seed_from_u64(SEED + 1);
    let n = circles.len();
    
    // 8๋ฐฉํ–ฅ ๋ฒกํ„ฐ
    let directions = vec![
        Vec2::new(1.0, 0.0),
        Vec2::new(-1.0, 0.0),
        Vec2::new(0.0, 1.0),
        Vec2::new(0.0, -1.0),
        Vec2::new(0.707, 0.707),
        Vec2::new(-0.707, 0.707),
        Vec2::new(0.707, -0.707),
        Vec2::new(-0.707, -0.707),
    ];
    
    for _ in 0..iters {
        let mut indices: Vec<usize> = (0..n).collect();
        indices.shuffle(&mut rng);
        
        for &i in &indices {
            let pos = circles[i].position;
            let r = circles[i].radius;
            let mut best_pos = pos;
            
            let cur_bound = distance_to_boundary(pos, poly_verts);
            let other_circles: Vec<Circle> = circles.iter().enumerate()
                .filter(|(j, _)| *j != i)
                .map(|(_, c)| *c)
                .collect();
            let cur_other = min_clearance_to_circles(pos, &other_circles);
            let mut best_clear = cur_bound.min(cur_other);
            
            for &dir in &directions {
                let random_factor = rng.gen_range(0.0..1.0f32);
                let move_dist = step * (0.5 + random_factor);
                let cand = pos + dir.normalize() * move_dist;
                
                if !point_in_poly_2d(cand, poly_verts) {
                    continue;
                }
                
                let bound_clear = distance_to_boundary(cand, poly_verts);
                if bound_clear < r {
                    continue;
                }
                
                let other_clear = min_clearance_to_circles(cand, &other_circles);
                if other_clear < 0.0 {
                    continue;
                }
                
                let total_clear = bound_clear.min(other_clear);
                if total_clear > best_clear + 1e-6 {
                    best_clear = total_clear;
                    best_pos = cand;
                }
            }
            
            circles[i].position = best_pos;
        }
    }
    
    circles
}
profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0๊ฐœ์˜ ๋Œ“๊ธ€