diff --git a/Cargo.toml b/Cargo.toml index 15ff80d..4673391 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,10 @@ version = "0.1.0" edition = "2021" [dependencies] +allsorts = "0.10" anyhow = "1" bytemuck = "1" canary_types = { path = "crates/types" } +lyon = "1" parking_lot = "0.12" wasmtime = "0.38" diff --git a/src/lib.rs b/src/lib.rs index b743a15..dbb344f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ pub use canary_types::*; use parking_lot::Mutex; +pub mod text; + /// Low-level script API callbacks. pub trait ScriptAbi { fn start_draw(&self); @@ -181,11 +183,14 @@ impl ScriptInstance for WasmtimeScript { #[derive(Default)] pub struct SimpleScriptAbi { draw_cmds: Mutex>, + text: text::DummyText, } impl ScriptAbi for SimpleScriptAbi { fn start_draw(&self) { - self.draw_cmds.lock().clear(); + let mut lock = self.draw_cmds.lock(); + lock.clear(); + self.text.draw(&mut lock); } fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) { diff --git a/src/text.rs b/src/text.rs new file mode 100644 index 0000000..1d88f83 --- /dev/null +++ b/src/text.rs @@ -0,0 +1,239 @@ +use super::{DrawCommand, MeshIndex, MeshVertex}; + +use allsorts::binary::read::ReadScope; +use allsorts::cff::CFF; +use allsorts::font::{GlyphTableFlags, MatchingPresentation}; +use allsorts::font_data::FontData; +use allsorts::glyph_position::{GlyphLayout, TextDirection}; +use allsorts::outline::OutlineBuilder; +use allsorts::pathfinder_geometry::{line_segment::LineSegment2F, vector::Vector2F}; +use allsorts::tables::{glyf::GlyfTable, loca::LocaTable}; +use allsorts::tables::{FontTableProvider, SfntVersion}; +use allsorts::{tag, Font}; +use std::collections::HashMap; + +const TEXT_SCALE: f32 = 0.00005; + +pub struct GlyphCache { + glyphs: HashMap, +} + +impl GlyphCache { + pub fn new() -> Self { + Self { + glyphs: Default::default(), + } + } + + pub fn write_glyph(&mut self, builder: &mut impl OutlineBuilder, index: u16) { + let mut sink = OutlineSink::new(); + builder.visit(index, &mut sink).unwrap(); + let path = sink.builder.build(); + + use lyon::tessellation::*; + let mut geometry: VertexBuffers = VertexBuffers::new(); + let mut tessellator = FillTessellator::new(); + + tessellator + .tessellate_path( + &path, + &FillOptions::default(), + &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { + let position = vertex.position() * TEXT_SCALE; + let position = canary_types::Vec2 { + x: position.x, + y: position.y, + }; + + MeshVertex { + position, + color: canary_types::Color::WHITE, + } + }), + ) + .unwrap(); + + self.glyphs.insert( + index, + CachedGlyph { + vertices: geometry.vertices, + indices: geometry.indices, + }, + ); + } +} + +pub struct CachedGlyph { + vertices: Vec, + indices: Vec, +} + +pub struct DummyText { + vertices: Vec, + indices: Vec, +} + +impl Default for DummyText { + fn default() -> Self { + // if you're looking at this for whatever reason, forgive the mess, + // Arabic rendering is misbehaving + let script = tag::ARAB; + let buffer = include_bytes!("/usr/share/fonts/TTF/Hack-Regular.ttf"); + let buffer = include_bytes!("/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"); + let buffer = include_bytes!("/usr/share/fonts/misc/ter-u16n.otb"); + let buffer = include_bytes!("/usr/share/fonts/noto/NotoNastaliqUrdu-Regular.ttf"); + // let buffer = include_bytes!("/usr/share/fonts/noto/NotoSansArabic-Regular.ttf"); + // let buffer = include_bytes!("/usr/share/fonts/noto/NotoKufiArabic-Regular.ttf"); + // let buffer = include_bytes!("/usr/share/fonts/noto/NotoNaskhArabic-Regular.ttf"); + let buffer = buffer.as_slice(); + let scope = ReadScope::new(&buffer); + let font_file = scope.read::>().unwrap(); + let provider = font_file.table_provider(0).unwrap(); + let mut font = Font::new(provider).unwrap().unwrap(); + + let text = "نص حكيم له سر قاطع وذو شأن عظيم مكتوب على ثوب أخضر ومغلف بجلد أزرق"; + let glyphs = font.map_glyphs(text, script, MatchingPresentation::NotRequired); + + let mut cache = GlyphCache::new(); + + if font.glyph_table_flags.contains(GlyphTableFlags::CFF) + && font.font_table_provider.sfnt_version() == tag::OTTO + { + let cff_data = font.font_table_provider.read_table_data(tag::CFF).unwrap(); + let mut cff = ReadScope::new(&cff_data).read::>().unwrap(); + + println!("warming cache with {} glyphs", font.num_glyphs()); + for index in 0..font.num_glyphs() { + cache.write_glyph(&mut cff, index); + } + } else if font.glyph_table_flags.contains(GlyphTableFlags::GLYF) { + let loca_data = font.font_table_provider.read_table_data(tag::LOCA).unwrap(); + let loca = ReadScope::new(&loca_data) + .read_dep::>(( + usize::from(font.maxp_table.num_glyphs), + font.head_table().unwrap().unwrap().index_to_loc_format, + )) + .unwrap(); + let glyf_data = font.font_table_provider.read_table_data(tag::GLYF).unwrap(); + let mut glyf = ReadScope::new(&glyf_data) + .read_dep::>(&loca) + .unwrap(); + + println!("warming cache with {} glyphs", font.num_glyphs()); + for index in 0..font.num_glyphs() { + cache.write_glyph(&mut glyf, index); + } + } else { + panic!("no glyf or CFF table"); + } + + let lang_tag = Some(tag::URD); + let features = allsorts::gsub::Features::Mask(allsorts::gsub::FeatureMask::all()); + let kerning = true; + let infos = font + .shape(glyphs, script, lang_tag, &features, kerning) + .unwrap(); + + let direction = TextDirection::RightToLeft; + let vertical = false; + let mut layout = GlyphLayout::new(&mut font, &infos, direction, vertical); + let positions = layout.glyph_positions().unwrap(); + + let mut mesh = Self { + vertices: Vec::new(), + indices: Vec::new(), + }; + + let mut xcur = 0; + let mut ycur = 0; + for (glyph, position) in infos.iter().zip(&positions) { + let xpos = xcur + position.x_offset; + let ypos = ycur + position.y_offset; + xcur += position.hori_advance; + ycur += position.vert_advance; + + println!("({}, {}): {:#?}", xpos, ypos, glyph); + + // TODO refactor + let glyph = cache.glyphs.get(&glyph.glyph.glyph_index).unwrap(); + let voff = mesh.vertices.len() as MeshIndex; + let xoff = (xpos as f32) * TEXT_SCALE; + let yoff = (ypos as f32) * TEXT_SCALE; + + for v in glyph.vertices.iter() { + let x = v.position.x + xoff + 0.5; + let y = v.position.y + yoff + 0.5; + mesh.vertices.push(MeshVertex { + position: canary_types::Vec2 { x, y }, + color: v.color, + }); + } + + for i in glyph.indices.iter() { + mesh.indices.push(i + voff); + } + } + + mesh + } +} + +impl DummyText { + pub fn draw(&self, cmds: &mut Vec) { + cmds.push(DrawCommand::Mesh { + vertices: self.vertices.clone(), + indices: self.indices.clone(), + }); + } +} + +pub struct OutlineSink { + builder: lyon::path::path::Builder, +} + +impl OutlineSink { + pub fn new() -> Self { + Self { + builder: lyon::path::Path::builder(), + } + } + + fn pf_line_to_lyon(line: &LineSegment2F) -> (lyon::geom::Point, lyon::geom::Point) { + ( + Self::pf_vector_to_lyon(&line.from()), + Self::pf_vector_to_lyon(&line.to()), + ) + } + + fn pf_vector_to_lyon(v: &Vector2F) -> lyon::geom::Point { + lyon::geom::Point::::new(v.x(), v.y()) + } +} + +impl allsorts::outline::OutlineSink for OutlineSink { + fn move_to(&mut self, to: Vector2F) { + let to = Self::pf_vector_to_lyon(&to); + self.builder.begin(to); + } + + fn line_to(&mut self, to: Vector2F) { + let to = Self::pf_vector_to_lyon(&to); + self.builder.line_to(to); + } + + fn quadratic_curve_to(&mut self, control: Vector2F, to: Vector2F) { + let control = Self::pf_vector_to_lyon(&control); + let to = Self::pf_vector_to_lyon(&to); + self.builder.quadratic_bezier_to(control, to); + } + + fn cubic_curve_to(&mut self, control: LineSegment2F, to: Vector2F) { + let (ctrl1, ctrl2) = Self::pf_line_to_lyon(&control); + let to = Self::pf_vector_to_lyon(&to); + self.builder.cubic_bezier_to(ctrl1, ctrl2, to); + } + + fn close(&mut self) { + self.builder.close(); + } +}