@@ -12,11 +12,10 @@ use graph_craft::document::value::TaggedValue;
1212use graph_craft:: document:: { NodeId , NodeInput } ;
1313use graphene_std:: Color ;
1414use graphene_std:: renderer:: Quad ;
15- use graphene_std:: renderer:: convert_usvg_path:: convert_usvg_path;
15+ use graphene_std:: renderer:: convert_usvg_path:: { convert_tiny_skia_path , convert_usvg_path} ;
1616use graphene_std:: table:: Table ;
1717use graphene_std:: text:: { Font , TypesettingConfig } ;
1818use graphene_std:: vector:: style:: { Fill , Gradient , GradientStop , GradientStops , GradientType , PaintOrder , Stroke , StrokeAlign , StrokeCap , StrokeJoin } ;
19-
2019#[ derive( ExtractField ) ]
2120pub struct GraphOperationMessageContext < ' a > {
2221 pub network_interface : & ' a mut NodeNetworkInterface ,
@@ -394,7 +393,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
394393 insert_index,
395394 center,
396395 } => {
397- let tree = match usvg:: Tree :: from_str ( & svg, & usvg:: Options :: default ( ) ) {
396+ let mut options = usvg:: Options :: default ( ) ;
397+ options. fontdb_mut ( ) . load_font_data ( include_bytes ! ( "../overlays/source-sans-pro-regular.ttf" ) . to_vec ( ) ) ;
398+ options. font_family = "Source Sans Pro" . to_string ( ) ;
399+
400+ let svg = svg. replace ( "font-family=\" sans-serif\" " , "font-family=\" Source Sans Pro\" " ) ;
401+ let svg = svg. replace ( "font-family='sans-serif'" , "font-family='Source Sans Pro'" ) ;
402+
403+ let tree = match usvg:: Tree :: from_str ( & svg, & options) {
398404 Ok ( t) => t,
399405 Err ( e) => {
400406 responses. add ( DialogMessage :: DisplayDialogError {
@@ -424,6 +430,9 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
424430
425431 let graphite_gradient_stops = extract_graphite_gradient_stops ( & svg) ;
426432
433+ // Pre-parse the raw SVG XML for <textPath> attributes that usvg doesn't expose
434+ let mut textpath_attrs = pre_parse_textpath_attrs ( & svg) ;
435+
427436 // Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`.
428437 // The placement offset is then applied once to the root group layer below.
429438 import_usvg_node (
@@ -433,6 +442,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
433442 parent,
434443 insert_index,
435444 & graphite_gradient_stops,
445+ & mut textpath_attrs,
436446 ) ;
437447
438448 // After import, `layer_node` is set to the root group. Apply the placement transform to it
@@ -532,6 +542,36 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
532542 Some ( Color :: from_rgbaf32_unchecked ( r, g, b, opacity) )
533543}
534544
545+ #[ derive( Debug , Default , Clone ) ]
546+ struct TextPathAttrs {
547+ pub method : Option < String > ,
548+ pub spacing : Option < String > ,
549+ pub side : Option < String > ,
550+ pub text_length : Option < f64 > ,
551+ pub length_adjust : Option < String > ,
552+ }
553+
554+ fn pre_parse_textpath_attrs ( svg : & str ) -> std:: collections:: HashMap < String , Vec < TextPathAttrs > > {
555+ let mut map = std:: collections:: HashMap :: < String , Vec < TextPathAttrs > > :: new ( ) ;
556+ let doc = match usvg:: roxmltree:: Document :: parse ( svg) {
557+ Ok ( doc) => doc,
558+ Err ( _) => return map,
559+ } ;
560+ for node in doc. descendants ( ) {
561+ if node. tag_name ( ) . name ( ) == "textPath" {
562+ let id = node. attribute ( "id" ) . unwrap_or ( "" ) . to_string ( ) ;
563+ map. entry ( id) . or_default ( ) . push ( TextPathAttrs {
564+ method : node. attribute ( "method" ) . map ( str:: to_string) ,
565+ spacing : node. attribute ( "spacing" ) . map ( str:: to_string) ,
566+ side : node. attribute ( "side" ) . map ( str:: to_string) ,
567+ text_length : node. attribute ( "textLength" ) . and_then ( |v| v. parse ( ) . ok ( ) ) ,
568+ length_adjust : node. attribute ( "lengthAdjust" ) . map ( str:: to_string) ,
569+ } ) ;
570+ }
571+ }
572+ map
573+ }
574+
535575/// Import a usvg node as the root of an SVG import operation.
536576///
537577/// The root layer uses the full `move_layer_to_stack` (with push/collision logic) to correctly
@@ -545,6 +585,7 @@ fn import_usvg_node(
545585 parent : LayerNodeIdentifier ,
546586 insert_index : usize ,
547587 graphite_gradient_stops : & HashMap < String , GradientStops > ,
588+ textpath_attrs : & mut HashMap < String , Vec < TextPathAttrs > > ,
548589) {
549590 let layer = modify_inputs. create_layer ( id) ;
550591
@@ -565,7 +606,7 @@ fn import_usvg_node(
565606 modify_inputs. import = true ;
566607
567608 for child in group. children ( ) {
568- let extent = import_usvg_node_inner ( modify_inputs, child, NodeId :: new ( ) , layer, 0 , graphite_gradient_stops, & mut group_extents_map) ;
609+ let extent = import_usvg_node_inner ( modify_inputs, child, NodeId :: new ( ) , layer, 0 , graphite_gradient_stops, & mut group_extents_map, textpath_attrs ) ;
569610 child_extents_svg_order. push ( extent) ;
570611 }
571612
@@ -590,9 +631,7 @@ fn import_usvg_node(
590631 warn ! ( "Skip image" ) ;
591632 }
592633 usvg:: Node :: Text ( text) => {
593- let font = Font :: new ( graphene_std:: consts:: DEFAULT_FONT_FAMILY . to_string ( ) , graphene_std:: consts:: DEFAULT_FONT_STYLE . to_string ( ) ) ;
594- modify_inputs. insert_text ( text. chunks ( ) . iter ( ) . map ( |chunk| chunk. text ( ) ) . collect ( ) , font, TypesettingConfig :: default ( ) , layer) ;
595- modify_inputs. fill_set ( Fill :: Solid ( Color :: BLACK ) ) ;
634+ import_usvg_text ( modify_inputs, text, node. abs_transform ( ) , layer, parent, insert_index, textpath_attrs) ;
596635 }
597636 }
598637}
@@ -610,6 +649,7 @@ fn import_usvg_node_inner(
610649 insert_index : usize ,
611650 graphite_gradient_stops : & HashMap < String , GradientStops > ,
612651 group_extents_map : & mut HashMap < LayerNodeIdentifier , Vec < u32 > > ,
652+ textpath_attrs : & mut HashMap < String , Vec < TextPathAttrs > > ,
613653) -> u32 {
614654 let layer = modify_inputs. create_layer ( id) ;
615655 modify_inputs. network_interface . move_layer_to_stack_for_import ( layer, parent, insert_index, & [ ] ) ;
@@ -619,7 +659,7 @@ fn import_usvg_node_inner(
619659 usvg:: Node :: Group ( group) => {
620660 let mut child_extents: Vec < u32 > = Vec :: new ( ) ;
621661 for child in group. children ( ) {
622- let extent = import_usvg_node_inner ( modify_inputs, child, NodeId :: new ( ) , layer, 0 , graphite_gradient_stops, group_extents_map) ;
662+ let extent = import_usvg_node_inner ( modify_inputs, child, NodeId :: new ( ) , layer, 0 , graphite_gradient_stops, group_extents_map, textpath_attrs ) ;
623663 child_extents. push ( extent) ;
624664 }
625665 modify_inputs. layer_node = Some ( layer) ;
@@ -633,24 +673,21 @@ fn import_usvg_node_inner(
633673 group_extents_map. insert ( layer, child_extents) ;
634674 total_extent
635675 }
636- usvg:: Node :: Path ( path) => {
637- import_usvg_path ( modify_inputs, node, path, layer, graphite_gradient_stops) ;
638- 0
639- }
640676 usvg:: Node :: Image ( _image) => {
641677 warn ! ( "Skip image" ) ;
642678 0
643679 }
644680 usvg:: Node :: Text ( text) => {
645- let font = Font :: new ( graphene_std:: consts:: DEFAULT_FONT_FAMILY . to_string ( ) , graphene_std:: consts:: DEFAULT_FONT_STYLE . to_string ( ) ) ;
646- modify_inputs. insert_text ( text. chunks ( ) . iter ( ) . map ( |chunk| chunk. text ( ) ) . collect ( ) , font, TypesettingConfig :: default ( ) , layer) ;
647- modify_inputs. fill_set ( Fill :: Solid ( Color :: BLACK ) ) ;
681+ import_usvg_text ( modify_inputs, text, node. abs_transform ( ) , layer, parent, insert_index, textpath_attrs) ;
682+ 0
683+ }
684+ usvg:: Node :: Path ( path) => {
685+ import_usvg_path ( modify_inputs, node, path, layer, graphite_gradient_stops) ;
648686 0
649687 }
650688 }
651689}
652690
653- /// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
654691fn import_usvg_path ( modify_inputs : & mut ModifyInputsContext , node : & usvg:: Node , path : & usvg:: Path , layer : LayerNodeIdentifier , graphite_gradient_stops : & HashMap < String , GradientStops > ) {
655692 let subpaths = convert_usvg_path ( path) ;
656693 let bounds = subpaths. iter ( ) . filter_map ( |subpath| subpath. bounding_box ( ) ) . reduce ( Quad :: combine_bounds) . unwrap_or_default ( ) ;
@@ -674,6 +711,65 @@ fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
674711 }
675712}
676713
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 } ;
716+
717+ for ( i, chunk) in text. chunks ( ) . iter ( ) . enumerate ( ) {
718+ let current_layer = if i == 0 {
719+ layer
720+ } else {
721+ let new_id = NodeId :: new ( ) ;
722+ let new_layer = modify_inputs. create_layer ( new_id) ;
723+ modify_inputs. network_interface . move_layer_to_stack_for_import ( new_layer, parent, insert_index, & [ ] ) ;
724+ new_layer
725+ } ;
726+ modify_inputs. layer_node = Some ( current_layer) ;
727+
728+ let font_family = chunk
729+ . spans ( )
730+ . first ( )
731+ . and_then ( |span| span. font ( ) . families ( ) . first ( ) . map ( |f| f. to_string ( ) ) )
732+ . unwrap_or_else ( || graphene_std:: consts:: DEFAULT_FONT_FAMILY . to_string ( ) ) ;
733+ let font_style = graphene_std:: consts:: DEFAULT_FONT_STYLE . to_string ( ) ;
734+ let font = Font :: new ( font_family, font_style) ;
735+
736+ let font_size = chunk. spans ( ) . first ( ) . map ( |s| s. font_size ( ) . get ( ) ) . unwrap_or ( 24.0 ) as f64 ;
737+ let letter_spacing = chunk. spans ( ) . first ( ) . map ( |s| s. letter_spacing ( ) ) . unwrap_or ( 0.0 ) as f64 ;
738+
739+ if let usvg:: TextFlow :: Path ( text_path) = chunk. text_flow ( ) {
740+ 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 ( ) ;
742+ let path_subpaths = convert_tiny_skia_path ( text_path. path ( ) ) ;
743+ 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+ } ;
749+
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 ) ) ;
765+ } else {
766+ // Regular text fallback
767+ 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 ) ) ;
769+ }
770+ }
771+ }
772+
677773/// Set correct positions for all imported layers in a single top-down O(n) pass.
678774///
679775/// For each group's child stack:
0 commit comments