use alice_font::{glyph_dispatcher, GlyphSdf, MetaFontParams};
use crate::sdf2d::Sdf2dNode;
use crate::types::SdfNode;
use std::sync::Arc;
pub fn glyph_to_sdf2d(glyph: &GlyphSdf) -> Sdf2dNode {
let mut data = Box::new([0.0f32; 4096]);
data.copy_from_slice(&glyph.data);
Sdf2dNode::FontGlyph {
data,
advance: glyph.advance,
bbox_min: [glyph.bbox_min.x, glyph.bbox_min.y],
bbox_max: [glyph.bbox_max.x, glyph.bbox_max.y],
}
}
pub fn char_to_sdf2d(ch: char, params: &MetaFontParams) -> Sdf2dNode {
let glyph = glyph_dispatcher::generate(ch, params);
glyph_to_sdf2d(&glyph)
}
pub fn text_to_sdf2d(text: &str, params: &MetaFontParams) -> Option<Sdf2dNode> {
let mut result: Option<Sdf2dNode> = None;
let mut cursor_x: f32 = 0.0;
for ch in text.chars() {
if ch == ' ' {
cursor_x += 0.5; continue;
}
let glyph = glyph_dispatcher::generate(ch, params);
let node = glyph_to_sdf2d(&glyph);
let placed = node.translate(cursor_x, 0.0);
cursor_x += glyph.advance;
result = Some(match result {
None => placed,
Some(existing) => existing.union(placed),
});
}
result
}
pub fn glyph_to_3d(glyph: &GlyphSdf, depth: f32) -> SdfNode {
let half_depth = depth * 0.5;
let w = glyph.bbox_max.x - glyph.bbox_min.x;
let h = glyph.bbox_max.y - glyph.bbox_min.y;
if w < 1e-6 || h < 1e-6 {
return SdfNode::box3d(0.01, 0.01, half_depth);
}
let cell_w = w / 64.0;
let cell_h = h / 64.0;
let cell_r = cell_w.max(cell_h) * 0.6;
let mut spheres: Vec<SdfNode> = Vec::new();
for iy in 0..64u32 {
for ix in 0..64u32 {
let d = glyph.data[(iy * 64 + ix) as usize];
if d < 0.0 {
let cx = ((ix as f32 + 0.5) * cell_w + glyph.bbox_min.x);
let cy = ((iy as f32 + 0.5) * cell_h + glyph.bbox_min.y);
spheres.push(SdfNode::sphere(cell_r).translate(cx, cy, 0.0));
}
}
}
if spheres.is_empty() {
return SdfNode::box3d(0.01, 0.01, half_depth);
}
let k = cell_r * 0.5;
let tree = balanced_smooth_union(spheres, k);
SdfNode::Extrude {
child: Arc::new(tree),
half_height: half_depth,
}
}
fn balanced_smooth_union(mut nodes: Vec<SdfNode>, k: f32) -> SdfNode {
while nodes.len() > 1 {
let mut next = Vec::with_capacity(nodes.len().div_ceil(2));
let mut i = 0;
while i + 1 < nodes.len() {
next.push(SdfNode::SmoothUnion {
a: Arc::new(nodes[i].clone()),
b: Arc::new(nodes[i + 1].clone()),
k,
});
i += 2;
}
if i < nodes.len() {
next.push(nodes[i].clone());
}
nodes = next;
}
nodes.into_iter().next().unwrap()
}
pub fn text_to_3d(text: &str, params: &MetaFontParams, depth: f32) -> Option<SdfNode> {
let mut result: Option<SdfNode> = None;
let mut cursor_x: f32 = 0.0;
for ch in text.chars() {
if ch == ' ' {
cursor_x += 0.5;
continue;
}
let glyph = glyph_dispatcher::generate(ch, params);
let node = glyph_to_3d(&glyph, depth);
let placed = node.translate(cursor_x, 0.0, 0.0);
cursor_x += glyph.advance;
result = Some(match result {
None => placed,
Some(existing) => SdfNode::Union {
a: Arc::new(existing),
b: Arc::new(placed),
},
});
}
result
}
pub const fn font_metrics(params: &MetaFontParams) -> FontMetrics {
FontMetrics {
cap_height: params.cap_height,
x_height: params.x_height,
ascender: params.ascender,
descender: params.descender,
weight: params.weight,
}
}
#[derive(Debug, Clone, Copy)]
pub struct FontMetrics {
pub cap_height: f32,
pub x_height: f32,
pub ascender: f32,
pub descender: f32,
pub weight: f32,
}
#[cfg(test)]
mod tests {
use super::*;
use alice_font::GlyphGenerator;
fn test_params() -> MetaFontParams {
MetaFontParams::sans_regular()
}
#[test]
fn glyph_to_sdf2d_conversion() {
let gen = GlyphGenerator::new(&test_params());
let glyph = gen.generate(b'A');
let node = glyph_to_sdf2d(&glyph);
match &node {
Sdf2dNode::FontGlyph { advance, .. } => {
assert!(*advance > 0.0);
}
_ => panic!("Expected FontGlyph node"),
}
}
#[test]
fn char_to_sdf2d_evaluates() {
let node = char_to_sdf2d('O', &test_params());
let gen = GlyphGenerator::new(&test_params());
let glyph = gen.generate(b'O');
let cx = (glyph.bbox_min.x + glyph.bbox_max.x) * 0.5;
let cy = (glyph.bbox_min.y + glyph.bbox_max.y) * 0.5;
let _d = crate::sdf2d::eval_2d(&node, [cx, cy]);
}
#[test]
fn text_to_sdf2d_single_char() {
let result = text_to_sdf2d("A", &test_params());
assert!(result.is_some());
}
#[test]
fn text_to_sdf2d_multiple_chars() {
let result = text_to_sdf2d("HI", &test_params());
assert!(result.is_some());
}
#[test]
fn text_to_sdf2d_with_space() {
let result = text_to_sdf2d("A B", &test_params());
assert!(result.is_some());
}
#[test]
fn text_to_sdf2d_empty() {
let result = text_to_sdf2d("", &test_params());
assert!(result.is_none());
}
#[test]
fn char_to_sdf2d_routes_cjk_through_dispatcher() {
let node_ni = char_to_sdf2d('日', &test_params());
let node_hon = char_to_sdf2d('本', &test_params());
let d_ni = crate::sdf2d::eval_2d(&node_ni, [0.5, 0.5]);
let d_hon = crate::sdf2d::eval_2d(&node_hon, [0.5, 0.5]);
assert!(d_ni.is_finite());
assert!(d_hon.is_finite());
}
#[test]
fn text_to_3d_routes_cjk_and_ascii_mix() {
let result = text_to_3d("HELLO AI 日本", &test_params(), 0.2);
assert!(
result.is_some(),
"text_to_3d must produce Some for bilingual input"
);
let node = result.unwrap();
let d = crate::eval::eval(&node, glam::Vec3::ZERO);
assert!(d.is_finite());
}
#[test]
fn glyph_to_3d_evaluates() {
let gen = GlyphGenerator::new(&test_params());
let glyph = gen.generate(b'T');
let node = glyph_to_3d(&glyph, 0.5);
let d = crate::eval::eval(&node, glam::Vec3::ZERO);
assert!(d.is_finite());
}
#[test]
fn text_to_3d_creates_node() {
let result = text_to_3d("A", &test_params(), 0.5);
assert!(result.is_some());
}
#[test]
fn font_metrics_extraction() {
let params = test_params();
let metrics = font_metrics(¶ms);
assert!(metrics.cap_height > 0.0);
assert!(metrics.x_height > 0.0);
}
#[test]
fn text_to_3d_multiple_chars() {
let result = text_to_3d("AB", &test_params(), 0.3);
assert!(result.is_some());
if let Some(node) = &result {
let d = crate::eval::eval(node, glam::Vec3::new(0.0, 0.0, 0.0));
assert!(d.is_finite());
}
}
}