Restructure gcore/text module and fix memory leak (#3221)
* Restructure gcore/text module and fix memory leak * Remove unused import * Fix default font fallback causing wrong caching and rename to TextContext * Upgrade demo art
This commit is contained in:
parent
d595089bb0
commit
4e47b5db93
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -15,6 +15,8 @@ pub struct OverlaysMessageHandler {
|
||||||
canvas: Option<web_sys::HtmlCanvasElement>,
|
canvas: Option<web_sys::HtmlCanvasElement>,
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
context: Option<web_sys::CanvasRenderingContext2d>,
|
context: Option<web_sys::CanvasRenderingContext2d>,
|
||||||
|
#[cfg(all(not(target_family = "wasm"), not(test)))]
|
||||||
|
context: Option<super::utility_types::OverlayContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message_handler_data]
|
#[message_handler_data]
|
||||||
|
|
@ -80,7 +82,11 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
|
||||||
|
|
||||||
let size = ipp.viewport_bounds.size();
|
let size = ipp.viewport_bounds.size();
|
||||||
|
|
||||||
let overlay_context = OverlayContext::new(size, device_pixel_ratio, visibility_settings);
|
if self.context.is_none() {
|
||||||
|
self.context = Some(OverlayContext::new(size, device_pixel_ratio, visibility_settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
let overlay_context = self.context.as_mut().unwrap();
|
||||||
|
|
||||||
if visibility_settings.all() {
|
if visibility_settings.all() {
|
||||||
responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() });
|
responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() });
|
||||||
|
|
@ -89,7 +95,7 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
|
||||||
responses.add(provider(overlay_context.clone()));
|
responses.add(provider(overlay_context.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
responses.add(FrontendMessage::RenderOverlays { context: overlay_context });
|
responses.add(FrontendMessage::RenderOverlays { context: overlay_context.clone() });
|
||||||
}
|
}
|
||||||
#[cfg(all(not(target_family = "wasm"), test))]
|
#[cfg(all(not(target_family = "wasm"), test))]
|
||||||
OverlaysMessage::Draw => {
|
OverlaysMessage::Draw => {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ use graphene_std::Color;
|
||||||
use graphene_std::math::quad::Quad;
|
use graphene_std::math::quad::Quad;
|
||||||
use graphene_std::subpath::{self, Subpath};
|
use graphene_std::subpath::{self, Subpath};
|
||||||
use graphene_std::table::Table;
|
use graphene_std::table::Table;
|
||||||
use graphene_std::text::{TextAlign, TypesettingConfig, load_font, to_path};
|
use graphene_std::text::TextContext;
|
||||||
|
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig};
|
||||||
use graphene_std::vector::click_target::ClickTargetType;
|
use graphene_std::vector::click_target::ClickTargetType;
|
||||||
use graphene_std::vector::misc::point_to_dvec2;
|
use graphene_std::vector::misc::point_to_dvec2;
|
||||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||||
|
|
@ -215,7 +216,7 @@ impl OverlayContext {
|
||||||
|
|
||||||
pub fn take_scene(self) -> Scene {
|
pub fn take_scene(self) -> Scene {
|
||||||
let mut internal = self.internal.lock().expect("Failed to lock internal overlay context");
|
let mut internal = self.internal.lock().expect("Failed to lock internal overlay context");
|
||||||
std::mem::take(&mut *internal).scene
|
std::mem::take(&mut internal.scene)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn internal(&'_ self) -> MutexGuard<'_, OverlayContextInternal> {
|
fn internal(&'_ self) -> MutexGuard<'_, OverlayContextInternal> {
|
||||||
|
|
@ -411,26 +412,31 @@ pub(super) struct OverlayContextInternal {
|
||||||
size: DVec2,
|
size: DVec2,
|
||||||
device_pixel_ratio: f64,
|
device_pixel_ratio: f64,
|
||||||
visibility_settings: OverlaysVisibilitySettings,
|
visibility_settings: OverlaysVisibilitySettings,
|
||||||
|
font_cache: FontCache,
|
||||||
|
thread_text: TextContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for OverlayContextInternal {
|
impl Default for OverlayContextInternal {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self::new(DVec2::new(100., 100.), 1., OverlaysVisibilitySettings::default())
|
||||||
scene: Scene::new(),
|
|
||||||
size: DVec2::ZERO,
|
|
||||||
device_pixel_ratio: 1.0,
|
|
||||||
visibility_settings: OverlaysVisibilitySettings::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OverlayContextInternal {
|
impl OverlayContextInternal {
|
||||||
pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self {
|
pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self {
|
||||||
|
let mut font_cache = FontCache::default();
|
||||||
|
// Initialize with the hardcoded font used by overlay text
|
||||||
|
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
||||||
|
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
|
||||||
|
font_cache.insert(font, String::new(), FONT_DATA.to_vec());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
scene: Scene::new(),
|
scene: Scene::new(),
|
||||||
size,
|
size,
|
||||||
device_pixel_ratio,
|
device_pixel_ratio,
|
||||||
visibility_settings,
|
visibility_settings,
|
||||||
|
font_cache,
|
||||||
|
thread_text: TextContext::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1007,7 +1013,7 @@ impl OverlayContextInternal {
|
||||||
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);
|
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_width(&self, text: &str) -> f64 {
|
fn get_width(&mut self, text: &str) -> f64 {
|
||||||
// Use the actual text-to-path system to get precise text width
|
// Use the actual text-to-path system to get precise text width
|
||||||
const FONT_SIZE: f64 = 12.0;
|
const FONT_SIZE: f64 = 12.0;
|
||||||
|
|
||||||
|
|
@ -1024,13 +1030,9 @@ impl OverlayContextInternal {
|
||||||
// Load Source Sans Pro font data
|
// Load Source Sans Pro font data
|
||||||
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
|
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
|
||||||
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
|
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
|
||||||
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
|
||||||
let font_blob = Some(load_font(FONT_DATA));
|
let bounds = self.thread_text.bounding_box(text, &font, &self.font_cache, typesetting, false);
|
||||||
|
bounds.x
|
||||||
// Convert text to paths and calculate actual bounds
|
|
||||||
let text_table = to_path(text, font_blob, typesetting, false);
|
|
||||||
let text_bounds = self.calculate_text_bounds(&text_table);
|
|
||||||
text_bounds.width()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text(&mut self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
|
fn text(&mut self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
|
||||||
|
|
@ -1051,15 +1053,17 @@ impl OverlayContextInternal {
|
||||||
// Load Source Sans Pro font data
|
// Load Source Sans Pro font data
|
||||||
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
|
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
|
||||||
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
|
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
|
||||||
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
|
||||||
let font_blob = Some(load_font(FONT_DATA));
|
|
||||||
|
|
||||||
// Convert text to vector paths using the existing text system
|
// Get text dimensions directly from layout
|
||||||
let text_table = to_path(text, font_blob, typesetting, false);
|
let text_size = self.thread_text.bounding_box(text, &font, &self.font_cache, typesetting, false);
|
||||||
// Calculate text bounds from the generated paths
|
let text_width = text_size.x;
|
||||||
let text_bounds = self.calculate_text_bounds(&text_table);
|
let text_height = text_size.y;
|
||||||
let text_width = text_bounds.width();
|
// Create a rect from the size (assuming text starts at origin)
|
||||||
let text_height = text_bounds.height();
|
let text_bounds = kurbo::Rect::new(0.0, 0.0, text_width, text_height);
|
||||||
|
|
||||||
|
// Convert text to vector paths for rendering
|
||||||
|
let text_table = self.thread_text.to_path(text, &font, &self.font_cache, typesetting, false);
|
||||||
|
|
||||||
// Calculate position based on pivot
|
// Calculate position based on pivot
|
||||||
let mut position = DVec2::ZERO;
|
let mut position = DVec2::ZERO;
|
||||||
|
|
@ -1094,56 +1098,6 @@ impl OverlayContextInternal {
|
||||||
self.render_text_paths(&text_table, font_color, vello_transform);
|
self.render_text_paths(&text_table, font_color, vello_transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate bounds of text from vector table
|
|
||||||
fn calculate_text_bounds(&self, text_table: &Table<Vector>) -> kurbo::Rect {
|
|
||||||
let mut min_x = f64::INFINITY;
|
|
||||||
let mut min_y = f64::INFINITY;
|
|
||||||
let mut max_x = f64::NEG_INFINITY;
|
|
||||||
let mut max_y = f64::NEG_INFINITY;
|
|
||||||
|
|
||||||
for row in text_table.iter() {
|
|
||||||
// Use the existing segment_bezier_iter to get all bezier curves
|
|
||||||
for (_, bezier, _, _) in row.element.segment_bezier_iter() {
|
|
||||||
let transformed_bezier = bezier.apply_transformation(|point| row.transform.transform_point2(point));
|
|
||||||
|
|
||||||
// Add start and end points to bounds
|
|
||||||
let points = [transformed_bezier.start, transformed_bezier.end];
|
|
||||||
for point in points {
|
|
||||||
min_x = min_x.min(point.x);
|
|
||||||
min_y = min_y.min(point.y);
|
|
||||||
max_x = max_x.max(point.x);
|
|
||||||
max_y = max_y.max(point.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add handle points if they exist
|
|
||||||
match transformed_bezier.handles {
|
|
||||||
subpath::BezierHandles::Quadratic { handle } => {
|
|
||||||
min_x = min_x.min(handle.x);
|
|
||||||
min_y = min_y.min(handle.y);
|
|
||||||
max_x = max_x.max(handle.x);
|
|
||||||
max_y = max_y.max(handle.y);
|
|
||||||
}
|
|
||||||
subpath::BezierHandles::Cubic { handle_start, handle_end } => {
|
|
||||||
for handle in [handle_start, handle_end] {
|
|
||||||
min_x = min_x.min(handle.x);
|
|
||||||
min_y = min_y.min(handle.y);
|
|
||||||
max_x = max_x.max(handle.x);
|
|
||||||
max_y = max_y.max(handle.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() {
|
|
||||||
kurbo::Rect::new(min_x, min_y, max_x, max_y)
|
|
||||||
} else {
|
|
||||||
// Fallback for empty text
|
|
||||||
kurbo::Rect::new(0.0, 0.0, 0.0, 12.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render text paths to the vello scene using existing infrastructure
|
// Render text paths to the vello scene using existing infrastructure
|
||||||
fn render_text_paths(&mut self, text_table: &Table<Vector>, font_color: &str, base_transform: kurbo::Affine) {
|
fn render_text_paths(&mut self, text_table: &Table<Vector>, font_color: &str, base_transform: kurbo::Affine) {
|
||||||
let color = Self::parse_color(font_color);
|
let color = Self::parse_color(font_color);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use graph_craft::document::value::TaggedValue;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::subpath::{Bezier, BezierHandles};
|
use graphene_std::subpath::{Bezier, BezierHandles};
|
||||||
use graphene_std::table::Table;
|
use graphene_std::table::Table;
|
||||||
use graphene_std::text::{FontCache, load_font};
|
use graphene_std::text::FontCache;
|
||||||
use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table;
|
use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table;
|
||||||
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
|
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
|
||||||
use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModification, VectorModificationType};
|
use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModification, VectorModificationType};
|
||||||
|
|
@ -74,8 +74,7 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH
|
||||||
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
|
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
|
||||||
};
|
};
|
||||||
|
|
||||||
let font_data = font_cache.get(font).map(|data| load_font(data));
|
let far = graphene_std::text::bounding_box(text, font, font_cache, typesetting, false);
|
||||||
let far = graphene_std::text::bounding_box(text, font_data, typesetting, false);
|
|
||||||
|
|
||||||
// TODO: Once the instance tables refactor is complete and per_glyph_instances can be removed (since it'll be the default),
|
// TODO: Once the instance tables refactor is complete and per_glyph_instances can be removed (since it'll be the default),
|
||||||
// TODO: remove this because the top of the dashed bounding overlay should no longer be based on the first line's baseline.
|
// TODO: remove this because the top of the dashed bounding overlay should no longer be based on the first line's baseline.
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{NodeId, NodeInput};
|
use graph_craft::document::{NodeId, NodeInput};
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping, load_font};
|
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping};
|
||||||
use graphene_std::vector::style::Fill;
|
use graphene_std::vector::style::Fill;
|
||||||
|
|
||||||
#[derive(Default, ExtractField)]
|
#[derive(Default, ExtractField)]
|
||||||
|
|
@ -513,8 +513,7 @@ impl Fsm for TextToolFsmState {
|
||||||
transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(),
|
transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(),
|
||||||
});
|
});
|
||||||
if let Some(editing_text) = tool_data.editing_text.as_mut() {
|
if let Some(editing_text) = tool_data.editing_text.as_mut() {
|
||||||
let font_data = font_cache.get(&editing_text.font).map(|data| load_font(data));
|
let far = graphene_std::text::bounding_box(&tool_data.new_text, &editing_text.font, font_cache, editing_text.typesetting, false);
|
||||||
let far = graphene_std::text::bounding_box(&tool_data.new_text, font_data, editing_text.typesetting, false);
|
|
||||||
if far.x != 0. && far.y != 0. {
|
if far.x != 0. && far.y != 0. {
|
||||||
let quad = Quad::from_box([DVec2::ZERO, far]);
|
let quad = Quad::from_box([DVec2::ZERO, far]);
|
||||||
let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad;
|
let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad;
|
||||||
|
|
@ -562,8 +561,7 @@ impl Fsm for TextToolFsmState {
|
||||||
// Draw red overlay if text is clipped
|
// Draw red overlay if text is clipped
|
||||||
let transformed_quad = layer_transform * bounds;
|
let transformed_quad = layer_transform * bounds;
|
||||||
if let Some((text, font, typesetting, _)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface) {
|
if let Some((text, font, typesetting, _)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface) {
|
||||||
let font_data = font_cache.get(font).map(|data| load_font(data));
|
if lines_clipping(text.as_str(), font, font_cache, typesetting) {
|
||||||
if lines_clipping(text.as_str(), font_data, typesetting) {
|
|
||||||
overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.));
|
overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
mod font_cache;
|
mod font_cache;
|
||||||
|
mod path_builder;
|
||||||
|
mod text_context;
|
||||||
mod to_path;
|
mod to_path;
|
||||||
|
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
pub use font_cache::*;
|
pub use font_cache::*;
|
||||||
|
pub use text_context::TextContext;
|
||||||
pub use to_path::*;
|
pub use to_path::*;
|
||||||
|
|
||||||
/// Alignment of lines of type within a text block.
|
/// Alignment of lines of type within a text block.
|
||||||
|
|
@ -29,3 +32,28 @@ impl From<TextAlign> for parley::Alignment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct TypesettingConfig {
|
||||||
|
pub font_size: f64,
|
||||||
|
pub line_height_ratio: f64,
|
||||||
|
pub character_spacing: f64,
|
||||||
|
pub max_width: Option<f64>,
|
||||||
|
pub max_height: Option<f64>,
|
||||||
|
pub tilt: f64,
|
||||||
|
pub align: TextAlign,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TypesettingConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
font_size: 24.,
|
||||||
|
line_height_ratio: 1.2,
|
||||||
|
character_spacing: 0.,
|
||||||
|
max_width: None,
|
||||||
|
max_height: None,
|
||||||
|
tilt: 0.,
|
||||||
|
align: TextAlign::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
|
use parley::fontique::Blob;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// A font type (storing font family and font style and an optional preview URL)
|
/// A font type (storing font family and font style and an optional preview URL)
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq, DynAny, specta::Type)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq, DynAny, specta::Type)]
|
||||||
|
|
@ -50,8 +52,13 @@ impl FontCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to get the bytes for a font
|
/// Try to get the bytes for a font
|
||||||
pub fn get(&self, font: &Font) -> Option<&Vec<u8>> {
|
pub fn get<'a>(&'a self, font: &'a Font) -> Option<(&'a Vec<u8>, &'a Font)> {
|
||||||
self.resolve_font(font).and_then(|font| self.font_file_data.get(font))
|
self.resolve_font(font).and_then(|font| self.font_file_data.get(font).map(|data| (data, font)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get font data as a Blob for use with parley/skrifa
|
||||||
|
pub fn get_blob<'a>(&'a self, font: &'a Font) -> Option<(Blob<u8>, &'a Font)> {
|
||||||
|
self.get(font).map(|(data, font)| (Blob::new(Arc::new(data.clone())), font))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the font is already loaded
|
/// Check if the font is already loaded
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
use crate::subpath::{ManipulatorGroup, Subpath};
|
||||||
|
use crate::table::{Table, TableRow};
|
||||||
|
use crate::vector::{PointId, Vector};
|
||||||
|
use glam::{DAffine2, DVec2};
|
||||||
|
use parley::GlyphRun;
|
||||||
|
use skrifa::GlyphId;
|
||||||
|
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
|
||||||
|
use skrifa::outline::{DrawSettings, OutlinePen};
|
||||||
|
use skrifa::raw::FontRef as ReadFontsRef;
|
||||||
|
use skrifa::{MetadataProvider, OutlineGlyph};
|
||||||
|
|
||||||
|
pub struct PathBuilder {
|
||||||
|
current_subpath: Subpath<PointId>,
|
||||||
|
origin: DVec2,
|
||||||
|
glyph_subpaths: Vec<Subpath<PointId>>,
|
||||||
|
pub vector_table: Table<Vector>,
|
||||||
|
scale: f64,
|
||||||
|
id: PointId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathBuilder {
|
||||||
|
pub fn new(per_glyph_instances: bool, scale: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
current_subpath: Subpath::new(Vec::new(), false),
|
||||||
|
glyph_subpaths: Vec::new(),
|
||||||
|
vector_table: if per_glyph_instances { Table::new() } else { Table::new_from_element(Vector::default()) },
|
||||||
|
scale,
|
||||||
|
id: PointId::ZERO,
|
||||||
|
origin: DVec2::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point(&self, x: f32, y: f32) -> DVec2 {
|
||||||
|
DVec2::new(self.origin.x + x as f64, self.origin.y - y as f64) * self.scale
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], glyph_offset: DVec2, style_skew: Option<DAffine2>, skew: DAffine2, per_glyph_instances: bool) {
|
||||||
|
let location_ref = LocationRef::new(normalized_coords);
|
||||||
|
let settings = DrawSettings::unhinted(Size::new(size), location_ref);
|
||||||
|
glyph.draw(settings, self).unwrap();
|
||||||
|
|
||||||
|
// Apply transforms in correct order: style-based skew first, then user-requested skew
|
||||||
|
// This ensures font synthesis (italic) is applied before user transformations
|
||||||
|
for glyph_subpath in &mut self.glyph_subpaths {
|
||||||
|
if let Some(style_skew) = style_skew {
|
||||||
|
glyph_subpath.apply_transform(style_skew);
|
||||||
|
}
|
||||||
|
|
||||||
|
glyph_subpath.apply_transform(skew);
|
||||||
|
}
|
||||||
|
|
||||||
|
if per_glyph_instances {
|
||||||
|
self.vector_table.push(TableRow {
|
||||||
|
element: Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false),
|
||||||
|
transform: DAffine2::from_translation(glyph_offset),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
for subpath in self.glyph_subpaths.drain(..) {
|
||||||
|
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Vector` table element
|
||||||
|
self.vector_table.get_mut(0).unwrap().element.append_subpath(subpath, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_instances: bool) {
|
||||||
|
let mut run_x = glyph_run.offset();
|
||||||
|
let run_y = glyph_run.baseline();
|
||||||
|
|
||||||
|
let run = glyph_run.run();
|
||||||
|
|
||||||
|
// User-requested tilt applied around baseline to avoid vertical displacement
|
||||||
|
// Translation ensures rotation point is at the baseline, not origin
|
||||||
|
let skew = if per_glyph_instances {
|
||||||
|
DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
|
||||||
|
} else {
|
||||||
|
DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
||||||
|
* DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
|
||||||
|
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
|
||||||
|
};
|
||||||
|
|
||||||
|
let synthesis = run.synthesis();
|
||||||
|
|
||||||
|
// Font synthesis (e.g., synthetic italic) applied separately from user transforms
|
||||||
|
// This preserves the distinction between font styling and user transformations
|
||||||
|
let style_skew = synthesis.skew().map(|angle| {
|
||||||
|
if per_glyph_instances {
|
||||||
|
DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
|
||||||
|
} else {
|
||||||
|
DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
||||||
|
* DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
|
||||||
|
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let font = run.font();
|
||||||
|
let font_size = run.font_size();
|
||||||
|
|
||||||
|
let normalized_coords = run.normalized_coords().iter().map(|coord| NormalizedCoord::from_bits(*coord)).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// TODO: This can be cached for better performance
|
||||||
|
let font_collection_ref = font.data.as_ref();
|
||||||
|
let font_ref = ReadFontsRef::from_index(font_collection_ref, font.index).unwrap();
|
||||||
|
let outlines = font_ref.outline_glyphs();
|
||||||
|
|
||||||
|
for glyph in glyph_run.glyphs() {
|
||||||
|
let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64);
|
||||||
|
run_x += glyph.advance;
|
||||||
|
|
||||||
|
let glyph_id = GlyphId::from(glyph.id);
|
||||||
|
if let Some(glyph_outline) = outlines.get(glyph_id) {
|
||||||
|
if !per_glyph_instances {
|
||||||
|
self.origin = glyph_offset;
|
||||||
|
}
|
||||||
|
self.draw_glyph(&glyph_outline, font_size, &normalized_coords, glyph_offset, style_skew, skew, per_glyph_instances);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finalize(mut self) -> Table<Vector> {
|
||||||
|
if self.vector_table.is_empty() {
|
||||||
|
self.vector_table = Table::new_from_element(Vector::default());
|
||||||
|
}
|
||||||
|
self.vector_table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutlinePen for PathBuilder {
|
||||||
|
fn move_to(&mut self, x: f32, y: f32) {
|
||||||
|
if !self.current_subpath.is_empty() {
|
||||||
|
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||||
|
}
|
||||||
|
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_to(&mut self, x: f32, y: f32) {
|
||||||
|
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||||
|
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
|
||||||
|
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle);
|
||||||
|
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, None, None, self.id.next_id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
||||||
|
let [handle1, handle2, anchor] = [self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)];
|
||||||
|
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle1);
|
||||||
|
self.current_subpath
|
||||||
|
.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, Some(handle2), None, self.id.next_id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&mut self) {
|
||||||
|
self.current_subpath.set_closed(true);
|
||||||
|
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
use super::{Font, FontCache, TypesettingConfig};
|
||||||
|
use crate::table::Table;
|
||||||
|
use crate::vector::Vector;
|
||||||
|
use core::cell::RefCell;
|
||||||
|
use glam::DVec2;
|
||||||
|
use parley::fontique::{Blob, FamilyId, FontInfo};
|
||||||
|
use parley::{AlignmentOptions, FontContext, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::path_builder::PathBuilder;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static THREAD_TEXT: RefCell<TextContext> = RefCell::new(TextContext::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unified thread-local text processing context that combines font and layout management
|
||||||
|
/// for efficient text rendering operations.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TextContext {
|
||||||
|
font_context: FontContext,
|
||||||
|
layout_context: LayoutContext<()>,
|
||||||
|
/// Cached font metadata for performance optimization
|
||||||
|
font_info_cache: HashMap<Font, (FamilyId, FontInfo)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextContext {
|
||||||
|
/// Access the thread-local TextContext instance for text processing operations
|
||||||
|
pub fn with_thread_local<F, R>(f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut TextContext) -> R,
|
||||||
|
{
|
||||||
|
THREAD_TEXT.with_borrow_mut(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a font and return its data as a Blob if available
|
||||||
|
fn resolve_font_data<'a>(&self, font: &'a Font, font_cache: &'a FontCache) -> Option<(Blob<u8>, &'a Font)> {
|
||||||
|
font_cache.get_blob(font)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get or cache font information for a given font
|
||||||
|
fn get_font_info(&mut self, font: &Font, font_data: &Blob<u8>) -> Option<(String, FontInfo)> {
|
||||||
|
// Check if we already have the font info cached
|
||||||
|
if let Some((family_id, font_info)) = self.font_info_cache.get(font) {
|
||||||
|
if let Some(family_name) = self.font_context.collection.family_name(*family_id) {
|
||||||
|
return Some((family_name.to_string(), font_info.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the font and cache the info
|
||||||
|
let families = self.font_context.collection.register_fonts(font_data.clone(), None);
|
||||||
|
|
||||||
|
families.first().and_then(|(family_id, fonts_info)| {
|
||||||
|
fonts_info.first().and_then(|font_info| {
|
||||||
|
self.font_context.collection.family_name(*family_id).map(|family_name| {
|
||||||
|
// Cache the font info for future use
|
||||||
|
self.font_info_cache.insert(font.clone(), (*family_id, font_info.clone()));
|
||||||
|
(family_name.to_string(), font_info.clone())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a text layout using the specified font and typesetting configuration
|
||||||
|
fn layout_text(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> Option<Layout<()>> {
|
||||||
|
// Note that the actual_font may not be the desired font if that font is not yet loaded.
|
||||||
|
// It is important not to cache the default font under the name of another font.
|
||||||
|
let (font_data, actual_font) = self.resolve_font_data(font, font_cache)?;
|
||||||
|
let (font_family, font_info) = self.get_font_info(actual_font, &font_data)?;
|
||||||
|
|
||||||
|
const DISPLAY_SCALE: f32 = 1.;
|
||||||
|
let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, DISPLAY_SCALE, false);
|
||||||
|
|
||||||
|
builder.push_default(StyleProperty::FontSize(typesetting.font_size as f32));
|
||||||
|
builder.push_default(StyleProperty::LetterSpacing(typesetting.character_spacing as f32));
|
||||||
|
builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(std::borrow::Cow::Owned(font_family)))));
|
||||||
|
builder.push_default(StyleProperty::FontWeight(font_info.weight()));
|
||||||
|
builder.push_default(StyleProperty::FontStyle(font_info.style()));
|
||||||
|
builder.push_default(StyleProperty::FontWidth(font_info.width()));
|
||||||
|
builder.push_default(LineHeight::FontSizeRelative(typesetting.line_height_ratio as f32));
|
||||||
|
|
||||||
|
let mut layout: Layout<()> = builder.build(text);
|
||||||
|
|
||||||
|
layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32));
|
||||||
|
layout.align(typesetting.max_width.map(|max_w| max_w as f32), typesetting.align.into(), AlignmentOptions::default());
|
||||||
|
|
||||||
|
Some(layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert text to vector paths using the specified font and typesetting configuration
|
||||||
|
pub fn to_path(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector> {
|
||||||
|
let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else {
|
||||||
|
return Table::new_from_element(Vector::default());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut path_builder = PathBuilder::new(per_glyph_instances, layout.scale() as f64);
|
||||||
|
|
||||||
|
for line in layout.lines() {
|
||||||
|
for item in line.items() {
|
||||||
|
if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
|
||||||
|
path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_instances);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path_builder.finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the bounding box of text using the specified font and typesetting configuration
|
||||||
|
pub fn bounding_box(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
||||||
|
if !for_clipping_test {
|
||||||
|
if let (Some(max_height), Some(max_width)) = (typesetting.max_height, typesetting.max_width) {
|
||||||
|
return DVec2::new(max_width, max_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else {
|
||||||
|
return DVec2::ZERO;
|
||||||
|
};
|
||||||
|
|
||||||
|
DVec2::new(layout.full_width() as f64, layout.height() as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if text lines are being clipped due to height constraints
|
||||||
|
pub fn lines_clipping(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> bool {
|
||||||
|
let Some(max_height) = typesetting.max_height else { return false };
|
||||||
|
let bounds = self.bounding_box(text, font, font_cache, typesetting, true);
|
||||||
|
max_height < bounds.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,261 +1,23 @@
|
||||||
use super::TextAlign;
|
use super::text_context::TextContext;
|
||||||
use crate::subpath::{ManipulatorGroup, Subpath};
|
use super::{Font, FontCache, TypesettingConfig};
|
||||||
use crate::table::{Table, TableRow};
|
use crate::table::Table;
|
||||||
use crate::vector::{PointId, Vector};
|
use crate::vector::Vector;
|
||||||
use core::cell::RefCell;
|
use glam::DVec2;
|
||||||
use glam::{DAffine2, DVec2};
|
|
||||||
use parley::fontique::Blob;
|
use parley::fontique::Blob;
|
||||||
use parley::{AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
|
|
||||||
use skrifa::GlyphId;
|
|
||||||
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
|
|
||||||
use skrifa::outline::{DrawSettings, OutlinePen};
|
|
||||||
use skrifa::raw::FontRef as ReadFontsRef;
|
|
||||||
use skrifa::{MetadataProvider, OutlineGlyph};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
// Thread-local storage avoids expensive re-initialization of font and layout contexts
|
pub fn to_path(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector> {
|
||||||
// across multiple text rendering operations within the same thread
|
TextContext::with_thread_local(|ctx| ctx.to_path(text, font, font_cache, typesetting, per_glyph_instances))
|
||||||
thread_local! {
|
|
||||||
static FONT_CONTEXT: RefCell<FontContext> = RefCell::new(FontContext::new());
|
|
||||||
static LAYOUT_CONTEXT: RefCell<LayoutContext<()>> = RefCell::new(LayoutContext::new());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PathBuilder {
|
pub fn bounding_box(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
||||||
current_subpath: Subpath<PointId>,
|
TextContext::with_thread_local(|ctx| ctx.bounding_box(text, font, font_cache, typesetting, for_clipping_test))
|
||||||
origin: DVec2,
|
|
||||||
glyph_subpaths: Vec<Subpath<PointId>>,
|
|
||||||
vector_table: Table<Vector>,
|
|
||||||
scale: f64,
|
|
||||||
id: PointId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PathBuilder {
|
|
||||||
fn point(&self, x: f32, y: f32) -> DVec2 {
|
|
||||||
DVec2::new(self.origin.x + x as f64, self.origin.y - y as f64) * self.scale
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], glyph_offset: DVec2, style_skew: Option<DAffine2>, skew: DAffine2, per_glyph_instances: bool) {
|
|
||||||
let location_ref = LocationRef::new(normalized_coords);
|
|
||||||
let settings = DrawSettings::unhinted(Size::new(size), location_ref);
|
|
||||||
glyph.draw(settings, self).unwrap();
|
|
||||||
|
|
||||||
// Apply transforms in correct order: style-based skew first, then user-requested skew
|
|
||||||
// This ensures font synthesis (italic) is applied before user transformations
|
|
||||||
for glyph_subpath in &mut self.glyph_subpaths {
|
|
||||||
if let Some(style_skew) = style_skew {
|
|
||||||
glyph_subpath.apply_transform(style_skew);
|
|
||||||
}
|
|
||||||
|
|
||||||
glyph_subpath.apply_transform(skew);
|
|
||||||
}
|
|
||||||
|
|
||||||
if per_glyph_instances {
|
|
||||||
self.vector_table.push(TableRow {
|
|
||||||
element: Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false),
|
|
||||||
transform: DAffine2::from_translation(glyph_offset),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
for subpath in self.glyph_subpaths.drain(..) {
|
|
||||||
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Vector` table element
|
|
||||||
self.vector_table.get_mut(0).unwrap().element.append_subpath(subpath, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutlinePen for PathBuilder {
|
|
||||||
fn move_to(&mut self, x: f32, y: f32) {
|
|
||||||
if !self.current_subpath.is_empty() {
|
|
||||||
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
|
||||||
}
|
|
||||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn line_to(&mut self, x: f32, y: f32) {
|
|
||||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
|
||||||
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
|
|
||||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle);
|
|
||||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, None, None, self.id.next_id()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
|
||||||
let [handle1, handle2, anchor] = [self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)];
|
|
||||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle1);
|
|
||||||
self.current_subpath
|
|
||||||
.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, Some(handle2), None, self.id.next_id()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close(&mut self) {
|
|
||||||
self.current_subpath.set_closed(true);
|
|
||||||
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct TypesettingConfig {
|
|
||||||
pub font_size: f64,
|
|
||||||
pub line_height_ratio: f64,
|
|
||||||
pub character_spacing: f64,
|
|
||||||
pub max_width: Option<f64>,
|
|
||||||
pub max_height: Option<f64>,
|
|
||||||
pub tilt: f64,
|
|
||||||
pub align: TextAlign,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TypesettingConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
font_size: 24.,
|
|
||||||
line_height_ratio: 1.2,
|
|
||||||
character_spacing: 0.,
|
|
||||||
max_width: None,
|
|
||||||
max_height: None,
|
|
||||||
tilt: 0.,
|
|
||||||
align: TextAlign::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64, per_glyph_instances: bool) {
|
|
||||||
let mut run_x = glyph_run.offset();
|
|
||||||
let run_y = glyph_run.baseline();
|
|
||||||
|
|
||||||
let run = glyph_run.run();
|
|
||||||
|
|
||||||
// User-requested tilt applied around baseline to avoid vertical displacement
|
|
||||||
// Translation ensures rotation point is at the baseline, not origin
|
|
||||||
let skew = if per_glyph_instances {
|
|
||||||
DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
|
|
||||||
} else {
|
|
||||||
DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
|
||||||
* DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
|
|
||||||
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
|
|
||||||
};
|
|
||||||
|
|
||||||
let synthesis = run.synthesis();
|
|
||||||
|
|
||||||
// Font synthesis (e.g., synthetic italic) applied separately from user transforms
|
|
||||||
// This preserves the distinction between font styling and user transformations
|
|
||||||
let style_skew = synthesis.skew().map(|angle| {
|
|
||||||
if per_glyph_instances {
|
|
||||||
DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
|
|
||||||
} else {
|
|
||||||
DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
|
||||||
* DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
|
|
||||||
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let font = run.font();
|
|
||||||
let font_size = run.font_size();
|
|
||||||
|
|
||||||
let normalized_coords = run.normalized_coords().iter().map(|coord| NormalizedCoord::from_bits(*coord)).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// TODO: This can be cached for better performance
|
|
||||||
let font_collection_ref = font.data.as_ref();
|
|
||||||
let font_ref = ReadFontsRef::from_index(font_collection_ref, font.index).unwrap();
|
|
||||||
let outlines = font_ref.outline_glyphs();
|
|
||||||
|
|
||||||
for glyph in glyph_run.glyphs() {
|
|
||||||
let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64);
|
|
||||||
run_x += glyph.advance;
|
|
||||||
|
|
||||||
let glyph_id = GlyphId::from(glyph.id);
|
|
||||||
if let Some(glyph_outline) = outlines.get(glyph_id) {
|
|
||||||
if !per_glyph_instances {
|
|
||||||
path_builder.origin = glyph_offset;
|
|
||||||
}
|
|
||||||
path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, glyph_offset, style_skew, skew, per_glyph_instances);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout_text(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> Option<Layout<()>> {
|
|
||||||
FONT_CONTEXT.with_borrow_mut(|mut font_cx| {
|
|
||||||
LAYOUT_CONTEXT.with_borrow_mut(|layout_cx| {
|
|
||||||
let (font_family, font_info) = font_data.and_then(|font_data| {
|
|
||||||
let families = font_cx.collection.register_fonts(font_data, None);
|
|
||||||
|
|
||||||
families.first().and_then(|(family_id, fonts_info)| {
|
|
||||||
fonts_info
|
|
||||||
.first()
|
|
||||||
.and_then(|font_info| font_cx.collection.family_name(*family_id).map(|f| (f.to_string(), font_info.clone())))
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
const DISPLAY_SCALE: f32 = 1.;
|
|
||||||
let mut builder = layout_cx.ranged_builder(&mut font_cx, str, DISPLAY_SCALE, false);
|
|
||||||
|
|
||||||
builder.push_default(StyleProperty::FontSize(typesetting.font_size as f32));
|
|
||||||
builder.push_default(StyleProperty::LetterSpacing(typesetting.character_spacing as f32));
|
|
||||||
builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(std::borrow::Cow::Owned(font_family)))));
|
|
||||||
builder.push_default(StyleProperty::FontWeight(font_info.weight()));
|
|
||||||
builder.push_default(StyleProperty::FontStyle(font_info.style()));
|
|
||||||
builder.push_default(StyleProperty::FontWidth(font_info.width()));
|
|
||||||
builder.push_default(LineHeight::FontSizeRelative(typesetting.line_height_ratio as f32));
|
|
||||||
|
|
||||||
let mut layout: Layout<()> = builder.build(str);
|
|
||||||
|
|
||||||
layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32));
|
|
||||||
layout.align(typesetting.max_width.map(|max_w| max_w as f32), typesetting.align.into(), AlignmentOptions::default());
|
|
||||||
|
|
||||||
Some(layout)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector> {
|
|
||||||
let Some(layout) = layout_text(str, font_data, typesetting) else {
|
|
||||||
return Table::new_from_element(Vector::default());
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut path_builder = PathBuilder {
|
|
||||||
current_subpath: Subpath::new(Vec::new(), false),
|
|
||||||
glyph_subpaths: Vec::new(),
|
|
||||||
vector_table: if per_glyph_instances { Table::new() } else { Table::new_from_element(Vector::default()) },
|
|
||||||
scale: layout.scale() as f64,
|
|
||||||
id: PointId::ZERO,
|
|
||||||
origin: DVec2::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
for line in layout.lines() {
|
|
||||||
for item in line.items() {
|
|
||||||
if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
|
|
||||||
render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt, per_glyph_instances);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if path_builder.vector_table.is_empty() {
|
|
||||||
path_builder.vector_table = Table::new_from_element(Vector::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
path_builder.vector_table
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bounding_box(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
|
||||||
if !for_clipping_test {
|
|
||||||
if let (Some(max_height), Some(max_width)) = (typesetting.max_height, typesetting.max_width) {
|
|
||||||
return DVec2::new(max_width, max_height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(layout) = layout_text(str, font_data, typesetting) else { return DVec2::ZERO };
|
|
||||||
|
|
||||||
DVec2::new(layout.full_width() as f64, layout.height() as f64)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_font(data: &[u8]) -> Blob<u8> {
|
pub fn load_font(data: &[u8]) -> Blob<u8> {
|
||||||
Blob::new(Arc::new(data.to_vec()))
|
Blob::new(Arc::new(data.to_vec()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lines_clipping(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> bool {
|
pub fn lines_clipping(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> bool {
|
||||||
let Some(max_height) = typesetting.max_height else { return false };
|
TextContext::with_thread_local(|ctx| ctx.lines_clipping(text, font, font_cache, typesetting))
|
||||||
let bounds = bounding_box(str, font_data, typesetting, true);
|
|
||||||
max_height < bounds.y
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,5 @@ fn text<'i: 'n>(
|
||||||
align,
|
align,
|
||||||
};
|
};
|
||||||
|
|
||||||
let font_data = editor.font_cache.get(&font_name).map(|f| load_font(f));
|
to_path(&text, &font_name, &editor.font_cache, typesetting, per_glyph_instances)
|
||||||
|
|
||||||
to_path(&text, font_data, typesetting, per_glyph_instances)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue