glyphweave 0.3.0

Shape-constrained SVG word clouds, built for speed. Fast Rust CLI + library.
Documentation
use crate::core::error::GlyphWeaveError;
use crate::layout::common::{
	apply_candidate, available_positions, candidate_quality, create_progress_bar, finish_progress,
	pick_color, random_unit_f32, sample_candidate, total_area, update_progress,
};
use crate::layout::{LayoutRequest, LayoutResult, LayoutStrategy};
use rand::RngCore;

const CANDIDATE_TRIALS: usize = 48;
const INITIAL_TEMPERATURE: f32 = 1.0;
const MIN_TEMPERATURE: f32 = 0.02;
const COOLING_RATE: f32 = 0.996;
const POOL_REFILL_THRESHOLD: usize = 256;

pub struct SimulatedAnnealingStrategy;

impl LayoutStrategy for SimulatedAnnealingStrategy {
	fn place(
		&self,
		request: &LayoutRequest<'_>,
		rng: &mut dyn RngCore,
	) -> Result<LayoutResult, GlyphWeaveError> {
		let mut mask = request.mask.clone();
		let total_usable_area = total_area(&mask);
		if total_usable_area == 0 {
			return Err(GlyphWeaveError::Generation(
				"shape mask has no usable area".to_string(),
			));
		}

		let mut positions = available_positions(&mask);
		let mut placements = Vec::new();
		let mut attempts = 0usize;
		let mut used_area = 0usize;
		let mut current_score = 0.0f32;
		let mut temperature = INITIAL_TEMPERATURE;
		let progress = create_progress_bar(request.show_progress);

		while attempts < request.max_try_count {
			let fill_ratio = used_area as f32 / total_usable_area as f32;
			if fill_ratio >= request.ratio_threshold {
				break;
			}

			attempts += 1;

			if positions.len() < POOL_REFILL_THRESHOLD {
				positions = available_positions(&mask);
			}
			if positions.is_empty() {
				break;
			}

			let Some(candidate) =
				sample_candidate(&mask, &mut positions, request, rng, CANDIDATE_TRIALS)
			else {
				temperature = (temperature * COOLING_RATE).max(MIN_TEMPERATURE);
				continue;
			};

			let candidate_score = candidate_quality(&candidate, total_usable_area);
			let delta = candidate_score - current_score;
			let accepted = if delta >= 0.0 {
				true
			} else {
				let acceptance = (delta / temperature.max(1e-6)).exp();
				random_unit_f32(rng) < acceptance
			};

			if accepted {
				let color = pick_color(&request.style.colors, rng);
				let (placed, consumed) = apply_candidate(&mut mask, &candidate, color);
				used_area += consumed;
				current_score = candidate_score;
				placements.push(placed);
			}

			temperature = (temperature * COOLING_RATE).max(MIN_TEMPERATURE);

			let ratio_progress = (used_area * 100) / total_usable_area;
			let try_progress = (attempts * 100) / request.max_try_count;
			update_progress(&progress, ratio_progress.max(try_progress));
		}

		finish_progress(&progress);

		Ok(LayoutResult {
			placements,
			attempts,
			used_area,
		})
	}
}