Skip to content

Commit c75cc64

Browse files
committed
Fix
1 parent 635605c commit c75cc64

6 files changed

Lines changed: 167 additions & 54 deletions

File tree

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
394394
center,
395395
} => {
396396
let mut options = usvg::Options::default();
397-
options.fontdb_mut().load_font_data(include_bytes!("../overlays/source-sans-pro-regular.ttf").to_vec());
398397
options.font_family = "Source Sans Pro".to_string();
398+
let fontdb = options.fontdb_mut();
399+
fontdb.load_font_data(include_bytes!("../overlays/source-sans-pro-regular.ttf").to_vec());
400+
fontdb.set_serif_family("Source Sans Pro");
401+
fontdb.set_sans_serif_family("Source Sans Pro");
399402

400403
let svg = svg.replace("font-family=\"sans-serif\"", "font-family=\"Source Sans Pro\"");
401404
let svg = svg.replace("font-family='sans-serif'", "font-family='Source Sans Pro'");
@@ -473,6 +476,7 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 {
473476
}
474477

475478
const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
479+
const XLINK_NAMESPACE: &str = "http://www.w3.org/1999/xlink";
476480

477481
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
478482
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
@@ -559,8 +563,10 @@ fn pre_parse_textpath_attrs(svg: &str) -> std::collections::HashMap<String, Vec<
559563
};
560564
for node in doc.descendants() {
561565
if node.tag_name().name() == "textPath" {
562-
let id = node.attribute("id").unwrap_or("").to_string();
563-
map.entry(id).or_default().push(TextPathAttrs {
566+
let Some(path_id) = textpath_href_id(node) else {
567+
continue;
568+
};
569+
map.entry(path_id).or_default().push(TextPathAttrs {
564570
method: node.attribute("method").map(str::to_string),
565571
spacing: node.attribute("spacing").map(str::to_string),
566572
side: node.attribute("side").map(str::to_string),
@@ -572,6 +578,13 @@ fn pre_parse_textpath_attrs(svg: &str) -> std::collections::HashMap<String, Vec<
572578
map
573579
}
574580

581+
fn textpath_href_id(node: usvg::roxmltree::Node) -> Option<String> {
582+
node.attribute((XLINK_NAMESPACE, "href"))
583+
.or_else(|| node.attribute("href"))
584+
.and_then(|href| href.strip_prefix('#'))
585+
.map(str::to_string)
586+
}
587+
575588
/// Import a usvg node as the root of an SVG import operation.
576589
///
577590
/// The root layer uses the full `move_layer_to_stack` (with push/collision logic) to correctly
@@ -625,12 +638,14 @@ fn import_usvg_node(
625638
modify_inputs.network_interface.unload_all_nodes_bounding_box(&[]);
626639
}
627640
usvg::Node::Path(path) => {
641+
log::info!("Importing node as Path: id={}", node.id());
628642
import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops);
629643
}
630644
usvg::Node::Image(_image) => {
631645
warn!("Skip image");
632646
}
633647
usvg::Node::Text(text) => {
648+
log::info!("Importing node as Text: id={}", node.id());
634649
import_usvg_text(modify_inputs, text, node.abs_transform(), layer, parent, insert_index, textpath_attrs);
635650
}
636651
}
@@ -711,8 +726,16 @@ fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
711726
}
712727
}
713728

714-
fn import_usvg_text(modify_inputs: &mut ModifyInputsContext, text: &usvg::Text, transform: usvg::Transform, layer: LayerNodeIdentifier, parent: LayerNodeIdentifier, insert_index: usize, textpath_attrs: &mut HashMap<String, Vec<TextPathAttrs>>) {
715-
use graphene_std::text::{LengthAdjust, TextAnchor, TextPathMethod, TextPathSide, TextPathSpacing};
729+
fn import_usvg_text(
730+
modify_inputs: &mut ModifyInputsContext,
731+
text: &usvg::Text,
732+
transform: usvg::Transform,
733+
layer: LayerNodeIdentifier,
734+
parent: LayerNodeIdentifier,
735+
insert_index: usize,
736+
textpath_attrs: &mut HashMap<String, Vec<TextPathAttrs>>,
737+
) {
738+
log::info!("Importing usvg text node with {} chunks", text.chunks().len());
716739

717740
for (i, chunk) in text.chunks().iter().enumerate() {
718741
let current_layer = if i == 0 {
@@ -738,38 +761,79 @@ fn import_usvg_text(modify_inputs: &mut ModifyInputsContext, text: &usvg::Text,
738761

739762
if let usvg::TextFlow::Path(text_path) = chunk.text_flow() {
740763
let tp_id = text_path.id();
741-
let tp_attrs = textpath_attrs.get_mut(tp_id).and_then(|vec| if !vec.is_empty() { Some(vec.remove(0)) } else { None }).unwrap_or_default();
764+
let tp_attrs = take_textpath_attrs(textpath_attrs, tp_id);
742765
let path_subpaths = convert_tiny_skia_path(text_path.path());
743766
let start_offset = text_path.start_offset() as f64;
744-
let anchor = match chunk.anchor() {
745-
usvg::TextAnchor::Start => TextAnchor::Start,
746-
usvg::TextAnchor::Middle => TextAnchor::Middle,
747-
usvg::TextAnchor::End => TextAnchor::End,
748-
};
749767

750-
let affine = DAffine2::from_cols_array(&[
751-
transform.sx as f64,
752-
transform.ky as f64,
753-
transform.kx as f64,
754-
transform.sy as f64,
755-
transform.tx as f64,
756-
transform.ty as f64,
757-
]);
758-
let method = if tp_attrs.method.as_deref() == Some("stretch") { TextPathMethod::Stretch } else { TextPathMethod::Align };
759-
let spacing = if tp_attrs.spacing.as_deref() == Some("auto") { TextPathSpacing::Auto } else { TextPathSpacing::Exact };
760-
let side = if tp_attrs.side.as_deref() == Some("right") { TextPathSide::Right } else { TextPathSide::Left };
761-
let length_adjust = if tp_attrs.length_adjust.as_deref() == Some("spacingAndGlyphs") { LengthAdjust::SpacingAndGlyphs } else { LengthAdjust::Spacing };
762-
763-
modify_inputs.insert_text_on_path(chunk.text().to_string(), font, font_size, letter_spacing, path_subpaths, start_offset, anchor, side, method, spacing, tp_attrs.text_length, length_adjust, affine, current_layer);
764-
modify_inputs.fill_set(Fill::Solid(Color::BLACK));
768+
modify_inputs.insert_text_on_path(
769+
chunk.text().to_string(),
770+
font,
771+
font_size,
772+
letter_spacing,
773+
path_subpaths,
774+
start_offset,
775+
text_anchor(chunk.anchor()),
776+
text_path_side(&tp_attrs),
777+
text_path_method(&tp_attrs),
778+
text_path_spacing(&tp_attrs),
779+
tp_attrs.text_length,
780+
text_length_adjust(&tp_attrs),
781+
usvg_transform(transform),
782+
current_layer,
783+
);
784+
if let Some(fill) = chunk.spans().first().and_then(|span| span.fill()) {
785+
apply_usvg_fill(fill, modify_inputs, DAffine2::IDENTITY, &HashMap::new());
786+
}
765787
} else {
766788
// Regular text fallback
767789
modify_inputs.insert_text(chunk.text().to_string(), font, TypesettingConfig { font_size, ..Default::default() }, current_layer);
768-
modify_inputs.fill_set(Fill::Solid(Color::BLACK));
790+
if let Some(fill) = chunk.spans().first().and_then(|span| span.fill()) {
791+
apply_usvg_fill(fill, modify_inputs, DAffine2::IDENTITY, &HashMap::new());
792+
}
769793
}
770794
}
771795
}
772796

797+
fn take_textpath_attrs(textpath_attrs: &mut HashMap<String, Vec<TextPathAttrs>>, path_id: &str) -> TextPathAttrs {
798+
textpath_attrs.get_mut(path_id).and_then(|attrs| (!attrs.is_empty()).then(|| attrs.remove(0))).unwrap_or_default()
799+
}
800+
801+
fn text_anchor(anchor: usvg::TextAnchor) -> graphene_std::text::TextAnchor {
802+
match anchor {
803+
usvg::TextAnchor::Start => graphene_std::text::TextAnchor::Start,
804+
usvg::TextAnchor::Middle => graphene_std::text::TextAnchor::Middle,
805+
usvg::TextAnchor::End => graphene_std::text::TextAnchor::End,
806+
}
807+
}
808+
809+
fn text_path_side(attrs: &TextPathAttrs) -> graphene_std::text::TextPathSide {
810+
match attrs.side.as_deref() {
811+
Some("right") => graphene_std::text::TextPathSide::Right,
812+
_ => graphene_std::text::TextPathSide::Left,
813+
}
814+
}
815+
816+
fn text_path_method(attrs: &TextPathAttrs) -> graphene_std::text::TextPathMethod {
817+
match attrs.method.as_deref() {
818+
Some("stretch") => graphene_std::text::TextPathMethod::Stretch,
819+
_ => graphene_std::text::TextPathMethod::Align,
820+
}
821+
}
822+
823+
fn text_path_spacing(attrs: &TextPathAttrs) -> graphene_std::text::TextPathSpacing {
824+
match attrs.spacing.as_deref() {
825+
Some("auto") => graphene_std::text::TextPathSpacing::Auto,
826+
_ => graphene_std::text::TextPathSpacing::Exact,
827+
}
828+
}
829+
830+
fn text_length_adjust(attrs: &TextPathAttrs) -> graphene_std::text::LengthAdjust {
831+
match attrs.length_adjust.as_deref() {
832+
Some("spacingAndGlyphs") => graphene_std::text::LengthAdjust::SpacingAndGlyphs,
833+
_ => graphene_std::text::LengthAdjust::Spacing,
834+
}
835+
}
836+
773837
/// Set correct positions for all imported layers in a single top-down O(n) pass.
774838
///
775839
/// For each group's child stack:

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,7 @@ impl<Upstream> Vector<Upstream> {
12071207
pub fn transform(&mut self, transform: DAffine2) {
12081208
self.point_domain.transform(transform);
12091209
self.segment_domain.transform(transform);
1210+
self.text_on_path_metadata = None;
12101211
}
12111212

12121213
pub fn vector_new_ids_from_hash(&mut self, node_id: u64) {

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,20 @@ impl<Upstream> std::hash::Hash for Vector<Upstream> {
9494
self.region_domain.hash(state);
9595
self.style.hash(state);
9696
self.colinear_manipulators.hash(state);
97-
// We don't hash upstream_data or text_on_path_metadata intentionally
97+
if let Some(metadata) = &self.text_on_path_metadata {
98+
metadata.text.hash(state);
99+
metadata.font_family.hash(state);
100+
metadata.font_style.hash(state);
101+
(metadata.font_size as u64).hash(state);
102+
metadata.path_d.hash(state);
103+
}
98104
}
99105
}
100106

101107
impl<Upstream> Vector<Upstream> {
102108
/// Add a subpath to this vector path.
103109
pub fn append_subpath(&mut self, subpath: impl Borrow<Subpath<PointId>>, preserve_id: bool) {
110+
self.text_on_path_metadata = None;
104111
let subpath: &Subpath<PointId> = subpath.borrow();
105112
let stroke_id = StrokeId::ZERO;
106113
let mut point_id = self.point_domain.next_id();

node-graph/nodes/text/src/font_cache.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,7 @@ impl FontCache {
122122
let fallback = self
123123
.font_file_data
124124
.keys()
125-
.find(|font| font.font_family == core_types::consts::DEFAULT_FONT_FAMILY && font.font_style == core_types::consts::DEFAULT_FONT_STYLE)
126-
.or_else(|| self.font_file_data.keys().next());
125+
.find(|font| font.font_family == core_types::consts::DEFAULT_FONT_FAMILY && font.font_style == core_types::consts::DEFAULT_FONT_STYLE);
127126
fallback
128127
}
129128
}

node-graph/nodes/text/src/path_builder.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
3838
self.current_point = Point::ZERO;
3939

4040
let settings = DrawSettings::unhinted(Size::new(size), normalized_coords);
41-
glyph.draw(settings, self).unwrap();
41+
if let Err(e) = glyph.draw(settings, self) {
42+
log::error!("Failed to draw glyph: {:?}", e);
43+
return;
44+
}
4245

4346
if !self.current_segments.is_empty() {
4447
self.glyph_subpaths.push(Subpath::from_beziers(&self.current_segments, false));

node-graph/nodes/text/src/text_on_path.rs

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -196,26 +196,34 @@ fn maybe_reverse_path(path: BezPath, side: TextPathSide) -> BezPath {
196196
}
197197
}
198198

199-
fn is_glyph_hidden(mid: f64, start_offset: f64, total_length: f64, is_closed: bool, text_anchor: TextAnchor, rtl: bool) -> bool {
200-
if !is_closed { return mid < 0.0 || mid > total_length; }
201-
let d = mid - start_offset;
202-
let effective_anchor = if rtl { match text_anchor { TextAnchor::Start => TextAnchor::End, TextAnchor::End => TextAnchor::Start, _ => text_anchor } } else { text_anchor };
203-
match effective_anchor {
204-
TextAnchor::Start => d < 0.0 || d > total_length,
205-
TextAnchor::Middle => d < -total_length / 2.0 || d > total_length / 2.0,
206-
TextAnchor::End => d < -total_length || d > 0.0,
199+
fn is_glyph_hidden(mid: f64, _start_offset: f64, total_length: f64, is_closed: bool, _text_anchor: TextAnchor, _rtl: bool) -> bool {
200+
if is_closed {
201+
return false;
207202
}
203+
mid < -1e-3 || mid > total_length + 1e-3
208204
}
209205

210206
fn resolve_startpoint(abs_offset: f64, total_advance: f64, text_anchor: TextAnchor) -> f64 {
211-
match text_anchor { TextAnchor::Start => abs_offset, TextAnchor::Middle => abs_offset - total_advance / 2.0, TextAnchor::End => abs_offset - total_advance }
207+
match text_anchor {
208+
TextAnchor::Start => abs_offset,
209+
TextAnchor::Middle => abs_offset - total_advance / 2.0,
210+
TextAnchor::End => abs_offset - total_advance,
211+
}
212212
}
213213

214214
fn curvature_spacing_adjustment(lut: &ArcLengthLut, mid: f64, advance: f64) -> f64 {
215215
let half = advance / 2.0;
216216
let (_, a0) = at_with_extension(lut, mid - half);
217217
let (_, a1) = at_with_extension(lut, mid + half);
218-
advance * (a1 - a0).abs() * 0.1
218+
let angle_delta = (a1 - a0 + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI;
219+
advance * angle_delta.abs() * 0.1
220+
}
221+
222+
fn text_path_spacing_adjustment(spacing: TextPathSpacing, lut: &ArcLengthLut, mid: f64, advance: f64) -> f64 {
223+
match spacing {
224+
TextPathSpacing::Exact => 0.0,
225+
TextPathSpacing::Auto => curvature_spacing_adjustment(lut, mid, advance),
226+
}
219227
}
220228

221229
#[allow(clippy::too_many_arguments)]
@@ -242,12 +250,16 @@ pub fn place_text_on_path<Upstream: Default + 'static>(
242250
log::warn!("textPath method='stretch' is not yet implemented; falling back to 'align'");
243251
}
244252

245-
let Some(original_bezpath) = path_table.iter().next().and_then(|row| row.element.stroke_bezpath_iter().find(|p| p.segments().next().is_some())) else { return Table::new() };
253+
let Some(original_bezpath) = path_table.iter().next().and_then(|row| row.element.stroke_bezpath_iter().find(|p| p.segments().next().is_some())) else {
254+
return Table::new();
255+
};
246256
let path_d_for_export = original_bezpath.to_svg();
247257

248258
let bezpath = maybe_reverse_path(original_bezpath, side);
249259
let lut = ArcLengthLut::build(&bezpath, 100);
250-
if lut.total_length < 1e-9 { return Table::new(); }
260+
if lut.total_length < 1e-9 {
261+
return Table::new();
262+
}
251263

252264
let typesetting = crate::TypesettingConfig {
253265
font_size,
@@ -256,10 +268,17 @@ pub fn place_text_on_path<Upstream: Default + 'static>(
256268
};
257269

258270
let layout = crate::TextContext::with_thread_local(|ctx| ctx.layout_text(text, font, font_cache, typesetting));
259-
let Some(layout) = layout else { return Table::new() };
271+
let Some(layout) = layout else {
272+
log::error!("Text layout failed for: {}", text);
273+
return Table::new();
274+
};
275+
276+
log::info!("Placing text on path: {} (length: {})", text, lut.total_length);
260277

261278
let mut abs_offset = if start_offset_percent { start_offset * lut.total_length } else { start_offset };
262-
if let Some(author_length) = path_length.filter(|&l| l > 1e-9) { abs_offset *= lut.total_length / author_length; }
279+
if let Some(author_length) = path_length.filter(|&l| l > 1e-9) {
280+
abs_offset *= lut.total_length / author_length;
281+
}
263282

264283
let mut path_builder = crate::path_builder::PathBuilder::new(true, layout.scale() as f64);
265284

@@ -298,7 +317,7 @@ pub fn place_text_on_path<Upstream: Default + 'static>(
298317
let raw_advance = glyph.advance as f64 * advance_scale;
299318
cumulative_offset += if glyph_index > 0 { spacing_delta } else { 0.0 };
300319
let mid = line_start + run_x as f64 * advance_scale + cumulative_offset - glyph_run.offset() as f64 * advance_scale + raw_advance / 2.0;
301-
let adjusted_mid = mid + if spacing == TextPathSpacing::Auto { curvature_spacing_adjustment(&lut, mid, raw_advance) } else { 0.0 };
320+
let adjusted_mid = mid + text_path_spacing_adjustment(spacing, &lut, mid, raw_advance);
302321

303322
run_x += glyph.advance;
304323
glyph_index += 1;
@@ -308,9 +327,8 @@ pub fn place_text_on_path<Upstream: Default + 'static>(
308327
let (point, angle) = if lut.is_closed { lut.at_or_zero(effective_mid) } else { at_with_extension(&lut, effective_mid) };
309328

310329
if let Some(glyph_outline) = outlines.get(skrifa::GlyphId::from(glyph.id)) {
311-
let final_transform = DAffine2::from_translation(DVec2::new(point.x, point.y))
312-
* DAffine2::from_angle(angle)
313-
* DAffine2::from_translation(DVec2::new(glyph.x as f64, -glyph.y as f64));
330+
let final_transform =
331+
DAffine2::from_translation(DVec2::new(point.x, point.y)) * DAffine2::from_angle(angle) * DAffine2::from_translation(DVec2::new(glyph.x as f64, -glyph.y as f64));
314332
path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, style_skew, final_transform, true);
315333
}
316334
}
@@ -332,12 +350,33 @@ pub fn place_text_on_path<Upstream: Default + 'static>(
332350
path_d: path_d_for_export,
333351
start_offset,
334352
start_offset_percent,
335-
text_anchor: match text_anchor { TextAnchor::Start => "start", TextAnchor::Middle => "middle", TextAnchor::End => "end" }.to_string(),
336-
side: match side { TextPathSide::Left => "left", TextPathSide::Right => "right" }.to_string(),
337-
method: match method { TextPathMethod::Align => "align", TextPathMethod::Stretch => "stretch" }.to_string(),
338-
spacing: match spacing { TextPathSpacing::Exact => "exact", TextPathSpacing::Auto => "auto" }.to_string(),
353+
text_anchor: match text_anchor {
354+
TextAnchor::Start => "start",
355+
TextAnchor::Middle => "middle",
356+
TextAnchor::End => "end",
357+
}
358+
.to_string(),
359+
side: match side {
360+
TextPathSide::Left => "left",
361+
TextPathSide::Right => "right",
362+
}
363+
.to_string(),
364+
method: match method {
365+
TextPathMethod::Align => "align",
366+
TextPathMethod::Stretch => "stretch",
367+
}
368+
.to_string(),
369+
spacing: match spacing {
370+
TextPathSpacing::Exact => "exact",
371+
TextPathSpacing::Auto => "auto",
372+
}
373+
.to_string(),
339374
text_length,
340-
length_adjust: match length_adjust { LengthAdjust::Spacing => "spacing", LengthAdjust::SpacingAndGlyphs => "spacingAndGlyphs" }.to_string(),
375+
length_adjust: match length_adjust {
376+
LengthAdjust::Spacing => "spacing",
377+
LengthAdjust::SpacingAndGlyphs => "spacingAndGlyphs",
378+
}
379+
.to_string(),
341380
});
342381
for row in result.iter_mut() {
343382
row.element.text_on_path_metadata = Some(Arc::clone(&metadata));

0 commit comments

Comments
 (0)