Skip to content

Commit c0cd93f

Browse files
committed
Enable high-fidelity SVG export for text-on-path
1 parent a4436a8 commit c0cd93f

5 files changed

Lines changed: 58 additions & 1 deletion

File tree

node-graph/libraries/graphic-types/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pub mod migrations {
7777
segment_domain: old.segment_domain,
7878
region_domain: old.region_domain,
7979
upstream_data: old.upstream_graphic_group,
80+
text_on_path_metadata: None,
8081
});
8182
*vector_table.iter_mut().next().unwrap().transform = old.transform;
8283
*vector_table.iter_mut().next().unwrap().alpha_blending = old.alpha_blending;

node-graph/libraries/rendering/src/renderer.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,27 @@ impl Render for Table<Graphic> {
787787
impl Render for Table<Vector> {
788788
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
789789
for row in self.iter() {
790+
if render_params.for_export {
791+
if let Some(ref meta) = row.element.text_on_path_metadata {
792+
let path_id = format!("textpath-{}", generate_uuid());
793+
write!(&mut render.svg_defs, r#"<path id="{path_id}" d="{}" fill="none"/>"#, meta.path_d).unwrap();
794+
795+
let font_style_css = format!("font-family: {}; font-size: {}px; font-style: {};", meta.font_family, meta.font_size, meta.font_style);
796+
let start_offset_attr = if meta.start_offset_percent { format!("{}%", meta.start_offset * 100.0) } else { format!("{}", meta.start_offset) };
797+
let matrix = format_transform_matrix(*row.transform);
798+
let transform_attr = if matrix.is_empty() { String::new() } else { format!(r#" transform="{matrix}""#) };
799+
let text_length_attr = meta.text_length.map(|tl| format!(r#" textLength="{tl}" lengthAdjust="{}""#, meta.length_adjust)).unwrap_or_default();
800+
let side_attr = if meta.side == "right" { r#" side="right""# } else { "" };
801+
let anchor_style = format!("text-anchor: {};", meta.text_anchor);
802+
let method = &meta.method;
803+
let spacing = &meta.spacing;
804+
let text = &meta.text;
805+
806+
render.leaf_node(format!(r##"<text style="{font_style_css} {anchor_style}"{transform_attr}><textPath href="#{path_id}" startOffset="{start_offset_attr}" method="{method}" spacing="{spacing}"{side_attr}{text_length_attr}>{text}</textPath></text>"##));
807+
continue;
808+
}
809+
}
810+
790811
let multiplied_transform = *row.transform;
791812
let vector = &row.element;
792813
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform

node-graph/libraries/vector-types/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub use math::{QuadExt, RectExt};
1313
pub use subpath::Subpath;
1414
pub use vector::Vector;
1515
pub use vector::reference_point::ReferencePoint;
16+
pub use vector::TextOnPathMetadata;
1617

1718
// Re-export dependencies that users of this crate will need
1819
pub use dyn_any;

node-graph/libraries/vector-types/src/vector/vector_types.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,33 @@ use dyn_any::StaticType;
1414
use glam::{DAffine2, DVec2};
1515
use kurbo::{Affine, BezPath, Rect, Shape};
1616
use std::collections::HashMap;
17+
use std::sync::Arc;
18+
19+
/// Metadata carried by a text-on-path `Vector` to enable lossless SVG `<textPath>` export.
20+
/// When present on the first row of a `Table<Vector>`, the SVG renderer emits
21+
/// `<text><textPath href="...">` instead of raw `<path>` outlines.
22+
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
23+
pub struct TextOnPathMetadata {
24+
pub text: String,
25+
pub font_family: String,
26+
pub font_style: String,
27+
pub font_size: f64,
28+
/// SVG path `d` attribute string for the reference path.
29+
pub path_d: String,
30+
pub start_offset: f64,
31+
pub start_offset_percent: bool,
32+
/// "start" | "middle" | "end"
33+
pub text_anchor: String,
34+
/// "left" | "right"
35+
pub side: String,
36+
/// "align" | "stretch"
37+
pub method: String,
38+
/// "exact" | "auto"
39+
pub spacing: String,
40+
pub text_length: Option<f64>,
41+
/// "spacing" | "spacingAndGlyphs"
42+
pub length_adjust: String,
43+
}
1744

1845
/// Represents vector graphics data, composed of Bézier curves in a path or mesh arrangement.
1946
///
@@ -36,6 +63,11 @@ pub struct Vector<Upstream> {
3663
/// Without this, the tools would be working with a collapsed version of the data which has no reference to the original child layers that were booleaned together, resulting in the inner layers not being editable.
3764
#[serde(alias = "upstream_group")]
3865
pub upstream_data: Upstream,
66+
67+
/// When set, this vector was produced by a text-on-path node. SVG export uses this metadata
68+
/// to emit a `<text><textPath>` element instead of raw path outlines.
69+
#[serde(default, skip_serializing_if = "Option::is_none")]
70+
pub text_on_path_metadata: Option<Arc<TextOnPathMetadata>>,
3971
}
4072
unsafe impl<Upstream: 'static> StaticType for Vector<Upstream> {
4173
type Static = Self;
@@ -50,6 +82,7 @@ impl<Upstream: Default + 'static> Default for Vector<Upstream> {
5082
segment_domain: SegmentDomain::new(),
5183
region_domain: RegionDomain::new(),
5284
upstream_data: Upstream::default(),
85+
text_on_path_metadata: None,
5386
}
5487
}
5588
}
@@ -61,7 +94,7 @@ impl<Upstream> std::hash::Hash for Vector<Upstream> {
6194
self.region_domain.hash(state);
6295
self.style.hash(state);
6396
self.colinear_manipulators.hash(state);
64-
// We don't hash the upstream_data intentionally
97+
// We don't hash upstream_data or text_on_path_metadata intentionally
6598
}
6699
}
67100

node-graph/nodes/vector/src/vector_nodes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,6 +1343,7 @@ async fn sample_polyline(
13431343
colinear_manipulators: Default::default(),
13441344
style: std::mem::take(&mut row.element.style),
13451345
upstream_data: std::mem::take(&mut row.element.upstream_data),
1346+
..Default::default()
13461347
};
13471348
// Transfer the stroke transform from the input vector content to the result.
13481349
result.style.set_stroke_transform(row.transform);

0 commit comments

Comments
 (0)