mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Restructure node crates (#3384)
* Restructure node-graph folder * Fix wasm compilation * Move node definitions out of *-types crates * Cleanup * Fix warnings * Fix warnings * Start adding migrations * Add migrations and move memo nodes to gcore * Move nodes/gsvg-render -> rendering * Replace some hard coded identifiers and fix automatic conversion * Fix Vec2Value node migration * Fix formatting * Add more migrations * Cleanup features * Fix core_types::raster import * Update demo artwork (to make profile ci work) * Move *-types to node-graph/libraries folder * Add missing node migrations * Migrate more nodes * Remove impure memo node * More fixes and remove warning * Migrate context and add a few missing migrations --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
26
node-graph/nodes/text/Cargo.toml
Normal file
26
node-graph/nodes/text/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "text-nodes"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Text operation nodes for Graphene"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
core-types = { workspace = true }
|
||||
vector-types = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
dyn-any = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
skrifa = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
100
node-graph/nodes/text/src/font_cache.rs
Normal file
100
node-graph/nodes/text/src/font_cache.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use dyn_any::DynAny;
|
||||
use parley::fontique::Blob;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
// Import specta so derive macros can find it
|
||||
use core_types::specta;
|
||||
|
||||
/// 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, core_types::specta::Type)]
|
||||
pub struct Font {
|
||||
#[serde(rename = "fontFamily")]
|
||||
pub font_family: String,
|
||||
#[serde(rename = "fontStyle", deserialize_with = "migrate_font_style")]
|
||||
pub font_style: String,
|
||||
}
|
||||
impl Font {
|
||||
pub fn new(font_family: String, font_style: String) -> Self {
|
||||
Self { font_family, font_style }
|
||||
}
|
||||
}
|
||||
impl Default for Font {
|
||||
fn default() -> Self {
|
||||
Self::new(core_types::consts::DEFAULT_FONT_FAMILY.into(), core_types::consts::DEFAULT_FONT_STYLE.into())
|
||||
}
|
||||
}
|
||||
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)]
|
||||
pub struct FontCache {
|
||||
/// Actual font file data used for rendering a font
|
||||
font_file_data: HashMap<Font, Vec<u8>>,
|
||||
/// Web font preview URLs used for showing fonts when live editing
|
||||
preview_urls: HashMap<Font, String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FontCache {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("FontCache")
|
||||
.field("font_file_data", &self.font_file_data.keys().collect::<Vec<_>>())
|
||||
.field("preview_urls", &self.preview_urls)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl FontCache {
|
||||
/// Returns the font family name if the font is cached, otherwise returns the fallback font family name if that is cached
|
||||
pub fn resolve_font<'a>(&'a self, font: &'a Font) -> Option<&'a Font> {
|
||||
if self.font_file_data.contains_key(font) {
|
||||
Some(font)
|
||||
} else {
|
||||
self.font_file_data
|
||||
.keys()
|
||||
.find(|font| font.font_family == core_types::consts::DEFAULT_FONT_FAMILY && font.font_style == core_types::consts::DEFAULT_FONT_STYLE)
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to get the bytes for a font
|
||||
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).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
|
||||
pub fn loaded_font(&self, font: &Font) -> bool {
|
||||
self.font_file_data.contains_key(font)
|
||||
}
|
||||
|
||||
/// Insert a new font into the cache
|
||||
pub fn insert(&mut self, font: Font, perview_url: String, data: Vec<u8>) {
|
||||
self.font_file_data.insert(font.clone(), data);
|
||||
self.preview_urls.insert(font, perview_url);
|
||||
}
|
||||
|
||||
/// Gets the preview URL for showing in text field when live editing
|
||||
pub fn get_preview_url(&self, font: &Font) -> Option<&String> {
|
||||
self.preview_urls.get(font)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for FontCache {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.preview_urls.len().hash(state);
|
||||
self.preview_urls.iter().for_each(|(font, url)| {
|
||||
font.hash(state);
|
||||
url.hash(state)
|
||||
});
|
||||
self.font_file_data.len().hash(state);
|
||||
self.font_file_data.keys().for_each(|font| font.hash(state));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
fn migrate_font_style<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
|
||||
use serde::Deserialize;
|
||||
String::deserialize(deserializer).map(|name| if name == "Normal (400)" { "Regular (400)".to_string() } else { name })
|
||||
}
|
||||
66
node-graph/nodes/text/src/lib.rs
Normal file
66
node-graph/nodes/text/src/lib.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
mod font_cache;
|
||||
mod path_builder;
|
||||
mod text_context;
|
||||
mod to_path;
|
||||
|
||||
use dyn_any::DynAny;
|
||||
pub use font_cache::*;
|
||||
pub use text_context::TextContext;
|
||||
pub use to_path::*;
|
||||
|
||||
// Re-export for convenience
|
||||
pub use core_types as gcore;
|
||||
pub use vector_types;
|
||||
|
||||
// Import specta so derive macros can find it
|
||||
use core_types::specta;
|
||||
|
||||
/// Alignment of lines of type within a text block.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, core_types::specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum TextAlign {
|
||||
#[default]
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
#[label("Justify")]
|
||||
JustifyLeft,
|
||||
// TODO: JustifyCenter, JustifyRight, JustifyAll
|
||||
}
|
||||
|
||||
impl From<TextAlign> for parley::Alignment {
|
||||
fn from(val: TextAlign) -> Self {
|
||||
match val {
|
||||
TextAlign::Left => parley::Alignment::Left,
|
||||
TextAlign::Center => parley::Alignment::Middle,
|
||||
TextAlign::Right => parley::Alignment::Right,
|
||||
TextAlign::JustifyLeft => parley::Alignment::Justified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
158
node-graph/nodes/text/src/path_builder.rs
Normal file
158
node-graph/nodes/text/src/path_builder.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use core_types::table::{Table, TableRow};
|
||||
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};
|
||||
use vector_types::subpath::{ManipulatorGroup, Subpath};
|
||||
use vector_types::vector::{PointId, Vector};
|
||||
|
||||
pub struct PathBuilder<Upstream> {
|
||||
current_subpath: Subpath<PointId>,
|
||||
origin: DVec2,
|
||||
glyph_subpaths: Vec<Subpath<PointId>>,
|
||||
pub vector_table: Table<Vector<Upstream>>,
|
||||
scale: f64,
|
||||
id: PointId,
|
||||
}
|
||||
|
||||
impl<Upstream: Default + 'static> PathBuilder<Upstream> {
|
||||
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<Upstream>> {
|
||||
if self.vector_table.is_empty() {
|
||||
self.vector_table = Table::new_from_element(Vector::default());
|
||||
}
|
||||
self.vector_table
|
||||
}
|
||||
}
|
||||
|
||||
impl<Upstream: Default + 'static> OutlinePen for PathBuilder<Upstream> {
|
||||
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)));
|
||||
}
|
||||
}
|
||||
127
node-graph/nodes/text/src/text_context.rs
Normal file
127
node-graph/nodes/text/src/text_context.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use super::{Font, FontCache, TypesettingConfig};
|
||||
use core::cell::RefCell;
|
||||
use core_types::table::Table;
|
||||
use glam::DVec2;
|
||||
use parley::fontique::{Blob, FamilyId, FontInfo};
|
||||
use parley::{AlignmentOptions, FontContext, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
|
||||
use std::collections::HashMap;
|
||||
use vector_types::Vector;
|
||||
|
||||
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)
|
||||
&& 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<Upstream: Default + 'static>(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector<Upstream>> {
|
||||
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 && 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
|
||||
}
|
||||
}
|
||||
23
node-graph/nodes/text/src/to_path.rs
Normal file
23
node-graph/nodes/text/src/to_path.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use super::text_context::TextContext;
|
||||
use super::{Font, FontCache, TypesettingConfig};
|
||||
use core_types::table::Table;
|
||||
use glam::DVec2;
|
||||
use parley::fontique::Blob;
|
||||
use std::sync::Arc;
|
||||
use vector_types::Vector;
|
||||
|
||||
pub fn to_path<Upstream: Default + 'static>(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector<Upstream>> {
|
||||
TextContext::with_thread_local(|ctx| ctx.to_path(text, font, font_cache, typesetting, per_glyph_instances))
|
||||
}
|
||||
|
||||
pub fn bounding_box(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
||||
TextContext::with_thread_local(|ctx| ctx.bounding_box(text, font, font_cache, typesetting, for_clipping_test))
|
||||
}
|
||||
|
||||
pub fn load_font(data: &[u8]) -> Blob<u8> {
|
||||
Blob::new(Arc::new(data.to_vec()))
|
||||
}
|
||||
|
||||
pub fn lines_clipping(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> bool {
|
||||
TextContext::with_thread_local(|ctx| ctx.lines_clipping(text, font, font_cache, typesetting))
|
||||
}
|
||||
Reference in New Issue
Block a user