Skip to content
Snippets Groups Projects
loader.rs 11.85 KiB
use bevy::{
    asset::{AssetLoader, LoadContext, LoadedAsset},
    render::render_resource::{Extent3d, TextureFormat},
    utils::{BoxedFuture, HashMap},
};

#[cfg(not(target_family = "wasm"))]
use bevy::asset::FileAssetIo;

use image::{EncodableLayout, RgbaImage};
use nanoserde::DeJson;

use crate::{
    msdf::{self, bitmap::FloatRGBBmp, shape::Shape, ttf_parser::ShapeBuilder, vector::Vector2},
    Glyph, ImageType, KayakFont, Rect, Sdf,
};
pub struct TTFLoader;

#[derive(DeJson, Default, Debug, Clone)]
pub struct KTTF {
    file: String,
    char_range_start: String,
    char_range_end: String,
    offset_x: Option<f32>,
    offset_y: Option<f32>,
}

impl AssetLoader for TTFLoader {
    fn load<'a>(
        &'a self,
        bytes: &'a [u8],
        load_context: &'a mut LoadContext,
    ) -> BoxedFuture<'a, Result<(), anyhow::Error>> {
        Box::pin(async move {
            #[cfg(not(target_family = "wasm"))]
            let asset_io = load_context
                .asset_io()
                .downcast_ref::<FileAssetIo>()
                .unwrap();

            let kttf: KTTF =
                nanoserde::DeJson::deserialize_json(std::str::from_utf8(bytes).unwrap()).unwrap();

            let char_range_start =
                u32::from_str_radix(kttf.char_range_start.trim_start_matches("0x"), 16)?;
            let char_range_end =
                u32::from_str_radix(kttf.char_range_end.trim_start_matches("0x"), 16)?;
            let font_bytes = load_context.read_asset_bytes(kttf.file.clone()).await?;

            let mut cache_path = std::path::PathBuf::from(load_context.path());
            let file_name = load_context
                .path()
                .file_name()
                .unwrap()
                .to_str()
                .unwrap()
                .to_string();
            cache_path.set_file_name(format!("{}-cached.png", file_name));
            let cache_image = load_context.read_asset_bytes(&cache_path).await;

            let font_range = char_range_start..char_range_end;
            let char_count = font_range.len() as u32;

            let size_x = 64usize;
            let size_y = 128usize;
            let face = ttf_parser::Face::parse(&font_bytes, 0).unwrap();
            let image_height = size_y as u32 * char_count;
            let mut image_builder: RgbaImage = image::ImageBuffer::new(size_x as u32, image_height);
            let mut yy = 0u32;
            let mut glyphs = vec![];

            // Build char to glyph mapping..
            let mut glyph_to_char: HashMap<ttf_parser::GlyphId, char> =
                HashMap::with_capacity(face.number_of_glyphs() as usize);
            let mut char_to_glyph: HashMap<char, ttf_parser::GlyphId> =
                HashMap::with_capacity(face.number_of_glyphs() as usize);
            if let Some(subtable) = face.tables().cmap {
                for subtable in subtable.subtables {
                    subtable.codepoints(|codepoint| {
                        if let Some(mapping) = subtable.glyph_index(codepoint) {
                            glyph_to_char
                                .insert(mapping, unsafe { std::mem::transmute(codepoint) });
                            char_to_glyph
                                .insert(unsafe { std::mem::transmute(codepoint) }, mapping);
                        }
                    })
                }
            }

            let loaded_file = &kttf;

            for char_u in font_range {
                let c = char::from_u32(char_u).unwrap();
                let glyph_id = *char_to_glyph.get(&c).unwrap();
                let mut output = FloatRGBBmp::new(size_x, size_y);
                let mut builder = ShapeBuilder::default();
                let pixel_scale = size_x as f64 / face.units_per_em() as f64;
                builder.pixel_scale = pixel_scale;
                let _result = face.outline_glyph(glyph_id, &mut builder);

                let char_bounds = face
                    .glyph_bounding_box(glyph_id)
                    .unwrap_or(ttf_parser::Rect {
                        x_min: 0,
                        x_max: size_x as i16,
                        y_min: 0,
                        y_max: size_y as i16,
                    });

                let mut shape = builder.build();
                shape.inverse_y_axis = true;
                // let (left, bottom, right, top) = shape.get_bounds();

                let scale = Vector2::new(1.0, 1.0);
                let px_range = 2.5;
                let range = px_range / scale.x.min(scale.y);

                let (translation, plane) = calculate_plane(
                    loaded_file,
                    &mut shape,
                    pixel_scale as f32,
                    1.0,
                    px_range as f32,
                    1.0,
                );
                let advance = face.glyph_hor_advance(glyph_id).unwrap_or(0) as f32 / size_x as f32;
                let c = *glyph_to_char.get(&glyph_id).unwrap();
                glyphs.push(Glyph {
                    unicode: c,
                    advance: advance * pixel_scale as f32,
                    atlas_bounds: Some(Rect {
                        left: 0.0,
                        bottom: 0.0 as f32,
                        right: size_x as f32,
                        top: size_y as f32,
                    }),
                    plane_bounds: Some(plane),
                });

                // let frame = Vector2::new(size_x as f64, size_y as f64);

                // dbg!((left, right, top, bottom));

                // left = (left - (size_x as f64 / 8.0)).max(0.0);
                // right = (right + (size_x as f64 / 8.0)).min(size_x as f64);
                // top = (top + (size_y as f64 / 8.0)).min(size_y as f64);
                // bottom = (bottom - (size_y as f64 / 8.0)).max(0.0);

                // dbg!((left, right, top, bottom));

                // let dims = Vector2::new(right - left, top - bottom);

                // let translate = Vector2::new(-left + (frame.x - dims.x), (frame.y - (bottom + dims.y)) - 1.0);
                if cache_image.is_err() {
                    msdf::edge_coloring::simple(&mut shape, 3.0, 0);
                    msdf::gen::generate_msdf(
                        &mut output,
                        &shape,
                        range,
                        scale,
                        translation + Vector2::new(0.0, size_x as f64 * 1.25),
                        1.11111111111111111,
                    );

                    // let left = (translation.x - char_bounds.x_min as f64 * pixel_scale).max(0.0).floor() as u32;
                    let right =
                        (translation.x + char_bounds.x_max as f64 * pixel_scale).floor() as u32;
                    // let top = (translation.y - char_bounds.y_min as f64 * pixel_scale).max(0.0).floor() as u32;
                    let bottom =
                        (translation.y + char_bounds.y_max as f64 * pixel_scale).floor() as u32;

                    for x in 0..(right + 2).min(64) {
                        for y in 0..bottom + 48 {
                            // for x in 0..size_x as u32 {
                            //     for y  in 0..size_y as u32 {
                            let pixel = output.get_pixel(x as usize, y as usize);
                            image_builder.put_pixel(
                                x,
                                yy + y,
                                image::Rgba([
                                    (pixel.r * 255.0) as u8,
                                    (pixel.g * 255.0) as u8,
                                    (pixel.b * 255.0) as u8,
                                    255,
                                ]),
                            );
                        }
                    }
                }
                // if c == '\"' {
                //     image_builder.save("test.png").unwrap();
                //     panic!("");
                // }
                yy += size_y as u32;
            }

            let image_bytes = if cache_image.is_err() {
                #[cfg(not(target_family = "wasm"))]
                image_builder
                    .save(asset_io.root_path().join(cache_path))
                    .unwrap();
                image_builder.as_bytes().to_vec()
            } else {
                let cache_image = cache_image.unwrap();
                let image = image::load_from_memory(&cache_image).unwrap();
                image.as_bytes().to_vec()
            };

            let mut sdf = Sdf::default();
            sdf.glyphs = glyphs;
            sdf.atlas.font_size = size_x as f32;

            let mut image = bevy::prelude::Image::new(
                Extent3d {
                    width: size_x as u32,
                    height: image_height,
                    depth_or_array_layers: 1,
                },
                bevy::render::render_resource::TextureDimension::D2,
                image_bytes,
                TextureFormat::Rgba8Unorm,
            );
            image.reinterpret_stacked_2d_as_array(char_count);
            let image_handle =
                load_context.set_labeled_asset("font_image", LoadedAsset::new(image));

            let font = KayakFont::new(sdf, ImageType::Array(image_handle));
            load_context.set_default_asset(LoadedAsset::new(font));

            Ok(())
        })
    }

    fn extensions(&self) -> &[&str] {
        &["kttf"]
    }
}

fn calculate_plane(
    loaded_file: &KTTF,
    shape: &mut Shape,
    geometry_scale: f32,
    scale: f32,
    range: f32,
    miter_limit: f32,
) -> (Vector2, Rect) {
    let bounds = shape.get_bounds();
    let bounds = Rect {
        left: bounds.0 as f32,
        bottom: bounds.1 as f32,
        right: bounds.2 as f32,
        top: bounds.3 as f32,
    };
    let scale = scale * geometry_scale;
    let range = range / geometry_scale;
    let (_w, _h, translation_x, translation_y) =
        if bounds.left < bounds.right && bounds.bottom < bounds.top {
            let mut l = bounds.left as f64;
            let mut b = bounds.bottom as f64;
            let mut r = bounds.right as f64;
            let mut t = bounds.top as f64;

            l -= 0.5 * range as f64;
            b -= 0.5 * range as f64;
            r += 0.5 * range as f64;
            t += 0.5 * range as f64;

            if miter_limit > 0.0 {
                shape.bound_miters(
                    &mut l,
                    &mut b,
                    &mut r,
                    &mut t,
                    0.5 * range as f64,
                    miter_limit as f64,
                    1,
                );
            }

            let w = scale as f64 * (r - l);
            let h = scale as f64 * (t - b);
            let box_w = w.ceil() as i32 + 1;
            let box_h = h.ceil() as i32 + 1;
            (
                box_w,
                box_h,
                -l + 0.5 * (box_w as f64 - w) / scale as f64,
                -b + 0.5 * (box_h as f64 - h) / scale as f64,
            )
        } else {
            (0, 0, 0.0, 0.0)
        };

    // let mut l = 0.0;
    // let mut r = 0.0;
    // let mut b = 0.0;
    // let mut t = 0.0;
    // if w > 0 && h > 0 {
    //     let inv_box_scale = 1.0 / scale as f64;
    //     l = geometry_scale as f64 * (-translation_x + 0.5 * inv_box_scale);
    //     b = geometry_scale as f64 * (-translation_y + 0.5 * inv_box_scale);
    //     r = geometry_scale as f64 * (-translation_x + (w as f64 - 0.5) * inv_box_scale);
    //     t = geometry_scale as f64 * (-translation_y + (h as f64 - 0.5) * inv_box_scale);
    // }

    let left = loaded_file.offset_x.unwrap_or_default();
    let top = loaded_file.offset_y.unwrap_or_default();

    (
        Vector2::new(translation_x, translation_y) * geometry_scale as f64,
        Rect {
            left: left * geometry_scale, // l as f32,
            bottom: 0.0,                 // b as f32,
            right: 0.0,                  // r as f32,
            top: top * geometry_scale,   //0.0, // t as f32,
        },
    )
}