mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Add String[] as a graphic type for typography (#4141)
* feat: Render List<String> as raw paths in SVG and Vell mode * chore: code review * chore: change the hardcoded layout bounds to parley's * chore: code review * feat: Split text node to text_layer and text_to_vector node * fix: CI fail because of difference in nature of Mac and github action * chore: fix * chore: replace FontStack as it got removed in parley 0.9 * chore: fmt * chore: migrate the rendering as of new resource architechture * chore: add text_layer node to text tool for testing * code review * Make boolean ops support the Text type * chore: Move fallback_font_resource authority from editor to text node * Fix 'Text Layer' node missing font dropdown * Change node doc comments from Vec<T> to T[] * Add migrations from the old Text node to Text -> Text to Vector * Consolidate * Rename the text attributes and reorder tilt to come before max_width/height * Detect legacy Text nodes in the split migration by their trailing separate_glyphs input * Code review * Frame Text layer thumbnails by laying out their text for bounds * Give Text layers click targets and selection outlines via collect_metadata * Route Text tool through Text to Vector with fill, fixing editing-preview placement --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -244,7 +244,7 @@ impl RenderExt for List<Graphic> {
|
||||
let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, transformed_bounds, render_params, target);
|
||||
format!(r##" {paint_attr}="url(#{gradient_id})""##)
|
||||
}
|
||||
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) => {
|
||||
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) | Some(Graphic::Text(_)) => {
|
||||
let bounds = if target == PaintTarget::Stroke {
|
||||
// To prevent a wraparound artefact occurring when the tile boundary and the stroke region are perfectly aligned, the local coordinate is expanded slightly.
|
||||
let inverse = |len: f64| if len > 0. { 1. / len } else { 0. };
|
||||
|
||||
@@ -6,18 +6,21 @@ use core_types::bounds::BoundingBox;
|
||||
use core_types::bounds::RenderBoundingBox;
|
||||
use core_types::color::Color;
|
||||
use core_types::color::SRGBA8;
|
||||
use core_types::consts::DEFAULT_FONT_SIZE;
|
||||
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List};
|
||||
use core_types::math::quad::Quad;
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::transform::Footprint;
|
||||
use core_types::uuid::{NodeId, generate_uuid};
|
||||
use core_types::{
|
||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME,
|
||||
ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM,
|
||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT,
|
||||
ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD,
|
||||
ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
|
||||
};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_hash::CacheHashWrapper;
|
||||
use graphene_resource::Resource;
|
||||
use graphic_types::graphic::{fill_graphic_list_at, graphic_list_at, is_stroke_fully_transparent_at, stroke_graphic_list_at};
|
||||
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
|
||||
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
|
||||
@@ -25,10 +28,15 @@ use graphic_types::vector_types::subpath::Subpath;
|
||||
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
|
||||
use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphic_types::{Artboard, Graphic, Vector};
|
||||
use kurbo::{Affine, Cap, Join, Shape, StrokeOpts};
|
||||
use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts};
|
||||
use num_traits::Zero;
|
||||
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
|
||||
use skrifa::outline::{DrawSettings, OutlinePen};
|
||||
use skrifa::raw::FontRef as SkrifaFontRef;
|
||||
use skrifa::{GlyphId, MetadataProvider};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Write;
|
||||
use std::hash::Hash;
|
||||
use std::ops::Deref;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use vector_types::gradient::GradientSpreadMethod;
|
||||
@@ -232,8 +240,10 @@ impl RenderParams {
|
||||
}
|
||||
|
||||
pub fn for_alignment(&self, transform: DAffine2) -> Self {
|
||||
let alignment_parent_transform = Some(transform);
|
||||
Self { alignment_parent_transform, ..*self }
|
||||
Self {
|
||||
alignment_parent_transform: Some(transform),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_pattern(&self) -> Self {
|
||||
@@ -547,6 +557,7 @@ impl Render for Graphic {
|
||||
Graphic::RasterGPU(_) => (),
|
||||
Graphic::Color(list) => list.render_svg(render, render_params),
|
||||
Graphic::Gradient(list) => list.render_svg(render, render_params),
|
||||
Graphic::Text(list) => list.render_svg(render, render_params),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,6 +569,7 @@ impl Render for Graphic {
|
||||
Graphic::RasterGPU(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||
Graphic::Color(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||
Graphic::Gradient(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||
Graphic::Text(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,6 +618,14 @@ impl Render for Graphic {
|
||||
Graphic::Gradient(list) => {
|
||||
metadata.upstream_footprints.insert(element_id, footprint);
|
||||
|
||||
// TODO: Find a way to handle more than the first item
|
||||
if !list.is_empty() {
|
||||
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0));
|
||||
}
|
||||
}
|
||||
Graphic::Text(list) => {
|
||||
metadata.upstream_footprints.insert(element_id, footprint);
|
||||
|
||||
// TODO: Find a way to handle more than the first item
|
||||
if !list.is_empty() {
|
||||
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0));
|
||||
@@ -621,6 +641,7 @@ impl Render for Graphic {
|
||||
Graphic::RasterGPU(list) => list.collect_metadata(metadata, footprint, element_id),
|
||||
Graphic::Color(list) => list.collect_metadata(metadata, footprint, element_id),
|
||||
Graphic::Gradient(list) => list.collect_metadata(metadata, footprint, element_id),
|
||||
Graphic::Text(list) => list.collect_metadata(metadata, footprint, element_id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,6 +653,7 @@ impl Render for Graphic {
|
||||
Graphic::RasterGPU(list) => list.add_upstream_click_targets(click_targets),
|
||||
Graphic::Color(list) => list.add_upstream_click_targets(click_targets),
|
||||
Graphic::Gradient(list) => list.add_upstream_click_targets(click_targets),
|
||||
Graphic::Text(list) => list.add_upstream_click_targets(click_targets),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,6 +665,7 @@ impl Render for Graphic {
|
||||
Graphic::RasterGPU(list) => list.add_upstream_outline_targets(outlines),
|
||||
Graphic::Color(list) => list.add_upstream_outline_targets(outlines),
|
||||
Graphic::Gradient(list) => list.add_upstream_outline_targets(outlines),
|
||||
Graphic::Text(list) => list.add_upstream_outline_targets(outlines),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,6 +677,7 @@ impl Render for Graphic {
|
||||
Graphic::RasterGPU(list) => list.contains_artboard(),
|
||||
Graphic::Color(list) => list.contains_artboard(),
|
||||
Graphic::Gradient(list) => list.contains_artboard(),
|
||||
Graphic::Text(list) => list.contains_artboard(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,6 +689,7 @@ impl Render for Graphic {
|
||||
Graphic::RasterGPU(_) => (),
|
||||
Graphic::Color(_) => (),
|
||||
Graphic::Gradient(_) => (),
|
||||
Graphic::Text(_) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1379,7 +1404,7 @@ impl Render for List<Vector> {
|
||||
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
|
||||
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &brush, Some(brush_transform), path);
|
||||
}
|
||||
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) => {
|
||||
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) => {
|
||||
scene.push_clip_layer(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), path);
|
||||
paint.render_to_vello(scene, multiplied_transform, context, render_params);
|
||||
scene.pop_layer();
|
||||
@@ -1461,7 +1486,7 @@ impl Render for List<Vector> {
|
||||
|
||||
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, Some(brush_transform), &path);
|
||||
}
|
||||
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) => {
|
||||
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) => {
|
||||
let stroked = peniko::kurbo::stroke(path.iter(), &stroke, &StrokeOpts::default(), 0.01);
|
||||
|
||||
scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &stroked);
|
||||
@@ -2244,6 +2269,376 @@ impl Render for List<GradientStops> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a `kurbo::BezPath` from a glyph outline, baking in the glyph origin (`ox`, `oy`) and faux-italic shear (`tilt_tan`).
|
||||
struct GlyphOutlinePen<'a> {
|
||||
path: &'a mut BezPath,
|
||||
ox: f64,
|
||||
oy: f64,
|
||||
tilt_tan: f64,
|
||||
}
|
||||
|
||||
impl GlyphOutlinePen<'_> {
|
||||
#[inline]
|
||||
fn px(&self, x: f32, y: f32) -> f64 {
|
||||
self.ox + x as f64 + (y as f64 * self.tilt_tan)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn py(&self, y: f32) -> f64 {
|
||||
self.oy - y as f64
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlinePen for GlyphOutlinePen<'_> {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
self.path.move_to((self.px(x, y), self.py(y)));
|
||||
}
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.path.line_to((self.px(x, y), self.py(y)));
|
||||
}
|
||||
fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
|
||||
self.path.quad_to((self.px(cx, cy), self.py(cy)), (self.px(x, y), self.py(y)));
|
||||
}
|
||||
fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) {
|
||||
self.path.curve_to((self.px(cx1, cy1), self.py(cy1)), (self.px(cx2, cy2), self.py(cy2)), (self.px(x, y), self.py(y)));
|
||||
}
|
||||
fn close(&mut self) {
|
||||
self.path.close_path();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws each glyph of `glyph_run` into a `BezPath` (with the run's position and faux-italic `tilt_tan` baked in)
|
||||
/// and calls `emit` for each non-empty glyph. Zero-geometry glyphs advance by `space_extra` for justified spacing.
|
||||
fn draw_glyph_run_to_bezpaths(glyph_run: &parley::GlyphRun<'_, ()>, x_offset: f32, space_extra: f32, tilt_tan: f64, mut emit: impl FnMut(&BezPath)) {
|
||||
let mut run_x = glyph_run.offset() + x_offset;
|
||||
let run_y = glyph_run.baseline();
|
||||
let run = glyph_run.run();
|
||||
let font = run.font();
|
||||
let font_size_pts = run.font_size();
|
||||
let normalized_coords: Vec<NormalizedCoord> = run.normalized_coords().iter().map(|c| NormalizedCoord::from_bits(*c)).collect();
|
||||
|
||||
let Ok(font_ref) = SkrifaFontRef::from_index(font.data.as_ref(), font.index) else { return };
|
||||
let outlines = font_ref.outline_glyphs();
|
||||
|
||||
let mut bez_path = BezPath::new();
|
||||
for glyph in glyph_run.glyphs() {
|
||||
let ox = (run_x + glyph.x) as f64;
|
||||
let oy = (run_y - glyph.y) as f64;
|
||||
run_x += glyph.advance;
|
||||
|
||||
let Some(outline) = outlines.get(GlyphId::from(glyph.id)) else { continue };
|
||||
let settings = DrawSettings::unhinted(Size::new(font_size_pts), LocationRef::new(&normalized_coords));
|
||||
|
||||
bez_path.truncate(0);
|
||||
let path = &mut bez_path;
|
||||
let mut pen = GlyphOutlinePen { path, ox, oy, tilt_tan };
|
||||
if outline.draw(settings, &mut pen).is_ok() && !bez_path.elements().is_empty() {
|
||||
emit(&bez_path);
|
||||
} else if space_extra != 0. && glyph.advance > 0. {
|
||||
run_x += space_extra;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lays out text item `index` of a styled `List<String>` and returns its local size and transform. The `BoundingBox` trait can't do
|
||||
/// this since a bare `String` carries no typography, so click-target and bounding-box computation share this. Falls back to an em
|
||||
/// square if the font isn't registered yet.
|
||||
fn text_item_size_and_transform(list: &List<String>, index: usize) -> Option<(DVec2, DAffine2)> {
|
||||
let text = list.element(index)?;
|
||||
let font: Resource = {
|
||||
let f: Resource = list.attribute_cloned_or_default(ATTR_FONT, index);
|
||||
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f }
|
||||
};
|
||||
let font_size: f64 = list.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE);
|
||||
let line_height: f64 = list.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2);
|
||||
let letter_spacing: f64 = list.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.);
|
||||
let max_width: Option<f64> = list.attribute_cloned_or(ATTR_MAX_WIDTH, index, None);
|
||||
let max_height: Option<f64> = list.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None);
|
||||
let align: text_nodes::TextAlign = list.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index);
|
||||
let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||
|
||||
let typesetting = text_nodes::TypesettingConfig {
|
||||
font_size,
|
||||
line_height_ratio: line_height,
|
||||
letter_spacing,
|
||||
letter_tilt: 0.,
|
||||
max_width,
|
||||
max_height,
|
||||
align,
|
||||
};
|
||||
|
||||
let (width, height) = text_nodes::TextContext::with_thread_local(|ctx| {
|
||||
ctx.layout_text(text, &font, typesetting).map(|layout| {
|
||||
let w = max_width.unwrap_or_else(|| layout.width() as f64);
|
||||
let h = max_height.unwrap_or_else(|| layout.height() as f64);
|
||||
(w, h)
|
||||
})
|
||||
})
|
||||
.unwrap_or((font_size, font_size));
|
||||
|
||||
Some((DVec2::new(width, height), transform))
|
||||
}
|
||||
|
||||
/// Union bounding box of a styled `List<String>`, laid out per item. The `BoundingBox` trait returns `None` for `List<String>`
|
||||
/// (a bare `String` has no extent), so text-layer thumbnails and bounds use this instead. Each item is laid out under `outer_transform`.
|
||||
pub fn text_list_bounding_box(list: &List<String>, outer_transform: DAffine2) -> RenderBoundingBox {
|
||||
let mut bounds: Option<[DVec2; 2]> = None;
|
||||
for index in 0..list.len() {
|
||||
let Some((size, transform)) = text_item_size_and_transform(list, index) else { continue };
|
||||
let full_transform = outer_transform * transform;
|
||||
for corner in [DVec2::ZERO, DVec2::new(size.x, 0.), DVec2::new(0., size.y), size] {
|
||||
let point = full_transform.transform_point2(corner);
|
||||
bounds = Some(match bounds {
|
||||
Some([min, max]) => [min.min(point), max.max(point)],
|
||||
None => [point, point],
|
||||
});
|
||||
}
|
||||
}
|
||||
match bounds {
|
||||
Some(bounds) => RenderBoundingBox::Rectangle(bounds),
|
||||
None => RenderBoundingBox::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Like `List<Graphic>::thumbnail_bounding_box`, but lays out `Graphic::Text` items, which the `BoundingBox` trait reports as `None`.
|
||||
/// Used for layer thumbnails so text layers (whose content is a `List<Graphic>` wrapping the text) frame their content.
|
||||
pub fn graphic_list_bounding_box(list: &List<Graphic>, transform: DAffine2) -> RenderBoundingBox {
|
||||
let mut combined: Option<[DVec2; 2]> = None;
|
||||
let mut any_infinite = false;
|
||||
|
||||
for index in 0..list.len() {
|
||||
let item_transform = transform * list.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, index);
|
||||
let Some(graphic) = list.element(index) else { continue };
|
||||
let bounds = match graphic {
|
||||
Graphic::Text(text_list) => text_list_bounding_box(text_list, item_transform),
|
||||
Graphic::Graphic(sub_list) => graphic_list_bounding_box(sub_list, item_transform),
|
||||
other => other.thumbnail_bounding_box(item_transform, true),
|
||||
};
|
||||
match bounds {
|
||||
RenderBoundingBox::None => {}
|
||||
RenderBoundingBox::Infinite => any_infinite = true,
|
||||
RenderBoundingBox::Rectangle([min, max]) => {
|
||||
combined = Some(match combined {
|
||||
Some([existing_min, existing_max]) => [existing_min.min(min), existing_max.max(max)],
|
||||
None => [min, max],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (combined, any_infinite) {
|
||||
(Some(bounds), _) => RenderBoundingBox::Rectangle(bounds),
|
||||
(None, true) => RenderBoundingBox::Infinite,
|
||||
(None, false) => RenderBoundingBox::None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for List<String> {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
for index in 0..self.len() {
|
||||
let Some(text) = self.element(index) else { continue };
|
||||
if text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
|
||||
let font: Resource = {
|
||||
let f: Resource = self.attribute_cloned_or_default(ATTR_FONT, index);
|
||||
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f }
|
||||
};
|
||||
let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE);
|
||||
let line_height: f64 = self.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2);
|
||||
let letter_spacing: f64 = self.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.);
|
||||
let max_width: Option<f64> = self.attribute_cloned_or(ATTR_MAX_WIDTH, index, None);
|
||||
let max_height: Option<f64> = self.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None);
|
||||
let letter_tilt: f64 = self.attribute_cloned_or(ATTR_LETTER_TILT, index, 0.);
|
||||
let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index);
|
||||
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
||||
|
||||
let typesetting = text_nodes::TypesettingConfig {
|
||||
font_size,
|
||||
line_height_ratio: line_height,
|
||||
letter_spacing,
|
||||
letter_tilt,
|
||||
max_width,
|
||||
max_height,
|
||||
align,
|
||||
};
|
||||
|
||||
let mut glyph_paths: Vec<String> = Vec::new();
|
||||
|
||||
text_nodes::TextContext::with_thread_local(|ctx| {
|
||||
let Some(layout) = ctx.layout_text(text, &font, typesetting) else { return };
|
||||
let tilt_tan = letter_tilt.to_radians().tan();
|
||||
|
||||
text_nodes::for_each_styled_glyph_run(&layout, text, typesetting, |glyph_run, x_offset, space_extra| {
|
||||
draw_glyph_run_to_bezpaths(glyph_run, x_offset, space_extra, tilt_tan, |bez_path| {
|
||||
glyph_paths.push(bez_path.to_svg());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if glyph_paths.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wrap all glyph <path> elements in a <g> with the item's transform/opacity/blend-mode.
|
||||
render.parent_tag(
|
||||
"g",
|
||||
|attributes| {
|
||||
let matrix = format_transform_matrix(transform);
|
||||
if !matrix.is_empty() {
|
||||
attributes.push("transform", matrix);
|
||||
}
|
||||
if opacity < 1. {
|
||||
attributes.push("opacity", opacity.to_string());
|
||||
}
|
||||
if blend_mode_attr != BlendMode::default() {
|
||||
attributes.push("style", blend_mode_attr.render());
|
||||
}
|
||||
},
|
||||
|render| {
|
||||
for path_d in glyph_paths {
|
||||
render.leaf_tag("path", |attributes| {
|
||||
attributes.push("d", path_d);
|
||||
if let RenderMode::Outline = render_params.render_mode {
|
||||
attributes.push("fill", "none");
|
||||
attributes.push("stroke", "black");
|
||||
attributes.push("stroke-width", "1");
|
||||
} else {
|
||||
attributes.push("fill", "black");
|
||||
attributes.push("fill-rule", "nonzero");
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
|
||||
for index in 0..self.len() {
|
||||
let Some(text) = self.element(index) else { continue };
|
||||
if text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||
let font: Resource = {
|
||||
let f: Resource = self.attribute_cloned_or_default(ATTR_FONT, index);
|
||||
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f }
|
||||
};
|
||||
let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE);
|
||||
let line_height: f64 = self.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2);
|
||||
let letter_spacing: f64 = self.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.);
|
||||
let max_width: Option<f64> = self.attribute_cloned_or(ATTR_MAX_WIDTH, index, None);
|
||||
let max_height: Option<f64> = self.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None);
|
||||
let letter_tilt: f64 = self.attribute_cloned_or(ATTR_LETTER_TILT, index, 0.);
|
||||
let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index);
|
||||
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
|
||||
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
||||
|
||||
let typesetting = text_nodes::TypesettingConfig {
|
||||
font_size,
|
||||
line_height_ratio: line_height,
|
||||
letter_spacing,
|
||||
letter_tilt,
|
||||
max_width,
|
||||
max_height,
|
||||
align,
|
||||
};
|
||||
|
||||
let affine = Affine::new((transform * item_transform).to_cols_array());
|
||||
|
||||
text_nodes::TextContext::with_thread_local(|ctx| {
|
||||
let Some(layout) = ctx.layout_text(text, &font, typesetting) else { return };
|
||||
|
||||
let needs_layer = opacity < 1. || blend_mode_attr != BlendMode::default();
|
||||
if needs_layer {
|
||||
let alignment_width = max_width.map(|w| w as f32).unwrap_or_else(|| layout.full_width());
|
||||
let blending = peniko::BlendMode::new(blend_mode_attr.to_peniko(), peniko::Compose::SrcOver);
|
||||
let padding = font_size;
|
||||
let bounds = kurbo::Rect::new(-padding, -padding, alignment_width as f64 + padding, layout.height() as f64 + padding);
|
||||
let transformed_bounds = affine.transform_rect_bbox(bounds);
|
||||
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &transformed_bounds);
|
||||
}
|
||||
|
||||
let tilt_tan = letter_tilt.to_radians().tan();
|
||||
|
||||
text_nodes::for_each_styled_glyph_run(&layout, text, typesetting, |glyph_run, x_offset, space_extra| {
|
||||
draw_glyph_run_to_bezpaths(glyph_run, x_offset, space_extra, tilt_tan, |bez_path| {
|
||||
if let RenderMode::Outline = render_params.render_mode {
|
||||
let (outline_stroke, outline_color) = get_outline_styles(render_params);
|
||||
scene.stroke(&outline_stroke, affine, outline_color, None, bez_path);
|
||||
} else {
|
||||
scene.fill(peniko::Fill::NonZero, affine, peniko::Color::BLACK, None, bez_path);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if needs_layer {
|
||||
scene.pop_layer();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
|
||||
// Click targets are baked relative to item 0's transform, which `Graphic::collect_metadata` records as `local_transforms[element_id]`.
|
||||
let item_zero_transform: DAffine2 = if !self.is_empty() {
|
||||
self.attribute_cloned_or_default(ATTR_TRANSFORM, 0)
|
||||
} else {
|
||||
DAffine2::IDENTITY
|
||||
};
|
||||
let item_zero_inverse = if item_zero_transform.matrix2.determinant() != 0. {
|
||||
item_zero_transform.inverse()
|
||||
} else {
|
||||
DAffine2::IDENTITY
|
||||
};
|
||||
|
||||
let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
|
||||
|
||||
for index in 0..self.len() {
|
||||
let layer_path: List<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
|
||||
let layer = layer_path.iter_element_values().next_back().copied();
|
||||
let Some(element_id) = caller_element_id.or(layer) else { continue };
|
||||
|
||||
// When recovering element_id from the item's tag (caller passed None), also store the transform metadata.
|
||||
if caller_element_id.is_none() {
|
||||
metadata.upstream_footprints.entry(element_id).or_insert(footprint);
|
||||
metadata.local_transforms.entry(element_id).or_insert(item_zero_transform);
|
||||
}
|
||||
|
||||
let Some((size, item_transform)) = text_item_size_and_transform(self, index) else { continue };
|
||||
let subpath = Subpath::new_rectangle(DVec2::ZERO, size);
|
||||
let mut target = ClickTarget::new_with_subpath(subpath, 0.);
|
||||
target.apply_transform(item_zero_inverse * item_transform);
|
||||
accumulated_click_targets.entry(element_id).or_default().push(Arc::new(target));
|
||||
}
|
||||
|
||||
// One rectangle per text item, reused for the selection outline (there's no letterform geometry to outline at this stage).
|
||||
for (element_id, targets) in accumulated_click_targets {
|
||||
metadata.outlines.insert(element_id, targets.clone());
|
||||
metadata.click_targets.insert(element_id, targets);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
for index in 0..self.len() {
|
||||
let Some((size, transform)) = text_item_size_and_transform(self, index) else { continue };
|
||||
let subpath = Subpath::new_rectangle(DVec2::ZERO, size);
|
||||
let mut target = ClickTarget::new_with_subpath(subpath, 0.);
|
||||
target.apply_transform(transform);
|
||||
click_targets.push(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SvgSegment {
|
||||
Slice(&'static str),
|
||||
|
||||
Reference in New Issue
Block a user