glyphweave 0.3.0

Shape-constrained SVG word clouds, built for speed. Fast Rust CLI + library.
Documentation
use crate::core::error::GlyphWeaveError;
use fontdue::Font;
use std::ops::RangeInclusive;
use std::path::PathBuf;
use std::sync::Arc;

#[derive(Debug, Clone)]
pub struct CanvasConfig {
	pub width: usize,
	pub height: usize,
	pub margin: usize,
}

impl Default for CanvasConfig {
	fn default() -> Self {
		Self {
			width: 1920,
			height: 1080,
			margin: 10,
		}
	}
}

#[derive(Debug, Clone)]
pub enum FontSizeSpec {
	Fixed(usize),
	AutoFit,
}

#[derive(Debug, Clone)]
pub struct ShapeConfig {
	pub text: String,
	pub font_size: FontSizeSpec,
}

#[derive(Debug, Clone)]
pub struct WordEntry {
	pub text: String,
	pub weight: f32,
}

impl WordEntry {
	pub fn new(text: impl Into<String>, weight: f32) -> Self {
		Self {
			text: text.into(),
			weight,
		}
	}
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Rotation {
	Deg0,
	Deg90,
}

impl Rotation {
	pub fn degrees(self) -> u16 {
		match self {
			Rotation::Deg0 => 0,
			Rotation::Deg90 => 90,
		}
	}
}

#[derive(Debug, Clone)]
pub struct StyleConfig {
	pub font_size_range: RangeInclusive<usize>,
	pub padding: usize,
	pub colors: Vec<String>,
	pub rotations: Vec<Rotation>,
}

impl Default for StyleConfig {
	fn default() -> Self {
		Self {
			font_size_range: 10..=30,
			padding: 0,
			colors: vec!["#000000".to_string()],
			rotations: vec![Rotation::Deg0],
		}
	}
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
pub enum AlgorithmKind {
	RandomBaseline,
	#[default]
	FastGrid,
	SpiralGreedy,
	Mcts,
	SimulatedAnnealing,
}

#[derive(Debug, Clone)]
pub struct RenderOptions {
	pub show_progress: bool,
	pub debug_mask_out: Option<PathBuf>,
}

impl Default for RenderOptions {
	fn default() -> Self {
		Self {
			show_progress: true,
			debug_mask_out: None,
		}
	}
}

#[derive(Debug, Clone)]
pub struct CloudRequest {
	pub canvas: CanvasConfig,
	pub shape: ShapeConfig,
	pub words: Vec<WordEntry>,
	pub style: StyleConfig,
	pub algorithm: AlgorithmKind,
	pub ratio_threshold: f32,
	pub max_try_count: usize,
	pub seed: Option<u64>,
	pub font: Arc<Font>,
	pub render: RenderOptions,
}

impl CloudRequest {
	pub fn validate(&self) -> Result<(), GlyphWeaveError> {
		if self.canvas.width == 0 || self.canvas.height == 0 {
			return Err(GlyphWeaveError::InvalidConfig(
				"canvas width/height must be greater than 0".to_string(),
			));
		}

		if self.canvas.margin * 2 >= self.canvas.width
			|| self.canvas.margin * 2 >= self.canvas.height
		{
			return Err(GlyphWeaveError::InvalidConfig(
				"canvas margin is too large for the configured canvas size".to_string(),
			));
		}

		if self.words.is_empty() {
			return Err(GlyphWeaveError::InvalidConfig(
				"at least one word is required".to_string(),
			));
		}

		if self.shape.text.trim().is_empty() {
			return Err(GlyphWeaveError::InvalidConfig(
				"shape text must not be empty".to_string(),
			));
		}

		if !(0.0..=1.0).contains(&self.ratio_threshold) {
			return Err(GlyphWeaveError::InvalidConfig(
				"ratio threshold must be between 0.0 and 1.0".to_string(),
			));
		}

		if self.max_try_count == 0 {
			return Err(GlyphWeaveError::InvalidConfig(
				"max_try_count must be greater than 0".to_string(),
			));
		}

		let min_size = *self.style.font_size_range.start();
		let max_size = *self.style.font_size_range.end();
		if min_size == 0 || min_size > max_size {
			return Err(GlyphWeaveError::InvalidConfig(
				"font_size_range must be valid and greater than 0".to_string(),
			));
		}

		if self.style.colors.is_empty() {
			return Err(GlyphWeaveError::InvalidConfig(
				"at least one color is required".to_string(),
			));
		}

		if self.style.rotations.is_empty() {
			return Err(GlyphWeaveError::InvalidConfig(
				"at least one rotation is required".to_string(),
			));
		}

		if self.words.iter().any(|w| w.text.trim().is_empty()) {
			return Err(GlyphWeaveError::InvalidConfig(
				"word list contains empty entries".to_string(),
			));
		}

		Ok(())
	}
}

#[derive(Debug, Clone)]
pub struct CloudPlacement {
	pub word: String,
	pub x: usize,
	pub y: usize,
	pub font_size: usize,
	pub color: String,
	pub rotation: Rotation,
}

#[derive(Debug, Clone)]
pub struct CloudStats {
	pub seed: u64,
	pub shape_font_size: usize,
	pub total_usable_area: usize,
	pub used_area: usize,
	pub fill_ratio: f32,
	pub attempts: usize,
	pub placed_words: usize,
	pub elapsed_ms: u128,
}

#[derive(Debug, Clone)]
pub struct CloudResult {
	pub svg: String,
	pub placements: Vec<CloudPlacement>,
	pub stats: CloudStats,
}