@@ -28,6 +28,13 @@ function vtkTextActor(publicAPI, model) {
2828 yResolution : 1 ,
2929 } ) ;
3030
31+ function normalizeLineBreaks ( text ) {
32+ return text
33+ . replace ( / \\ r \\ n / g, '\n' )
34+ . replace ( / \\ n / g, '\n' )
35+ . replace ( / \\ r / g, '\r' ) ;
36+ }
37+
3138 function createImageData ( text ) {
3239 const fontSizeScale = publicAPI . getProperty ( ) . getFontSizeScale ( ) ;
3340 const fontStyle = publicAPI . getProperty ( ) . getFontStyle ( ) ;
@@ -43,27 +50,38 @@ function vtkTextActor(publicAPI, model) {
4350 const ctx = canvas . getContext ( '2d' , { willReadFrequently : true } ) ;
4451
4552 // Set the text properties to measure
53+ const normalizedText = normalizeLineBreaks ( text ) ;
4654 const textSize = fontSizeScale ( resolution ) * dpr ;
55+ const lines = normalizedText . split ( / \r \n | \r | \n / ) ;
4756
4857 ctx . font = `${ fontStyle } ${ textSize } px "${ fontFamily } "` ;
49- ctx . textBaseline = 'middle' ;
50- ctx . textAlign = 'center' ;
51-
52- // Measure the text
53- const metrics = ctx . measureText ( text ) ;
54- const textWidth = metrics . width / dpr ;
55-
56- const {
57- actualBoundingBoxLeft,
58- actualBoundingBoxRight,
59- actualBoundingBoxAscent,
60- actualBoundingBoxDescent,
61- } = metrics ;
62- const hAdjustment = ( actualBoundingBoxLeft - actualBoundingBoxRight ) / 2 ;
63- const vAdjustment =
64- ( actualBoundingBoxAscent - actualBoundingBoxDescent ) / 2 ;
58+ ctx . textBaseline = 'alphabetic' ;
59+ ctx . textAlign = 'left' ;
60+
61+ const fallbackMetrics = ctx . measureText ( 'Mg' ) ;
62+ const fallbackAscent =
63+ fallbackMetrics . actualBoundingBoxAscent || textSize / dpr ;
64+ const fallbackDescent = fallbackMetrics . actualBoundingBoxDescent || 0 ;
65+
66+ const lineMetrics = lines . map ( ( line ) => {
67+ const metrics = ctx . measureText ( line ) ;
68+ return {
69+ left : metrics . actualBoundingBoxLeft || 0 ,
70+ right : metrics . actualBoundingBoxRight || metrics . width || 0 ,
71+ } ;
72+ } ) ;
6573
66- const textHeight = textSize / dpr - vAdjustment ;
74+ const maxLeft = lineMetrics . reduce (
75+ ( value , metrics ) => Math . max ( value , metrics . left ) ,
76+ 0
77+ ) ;
78+ const maxRight = lineMetrics . reduce (
79+ ( value , metrics ) => Math . max ( value , metrics . right ) ,
80+ 0
81+ ) ;
82+ const lineHeight = fallbackAscent + fallbackDescent ;
83+ const textWidth = maxLeft + maxRight ;
84+ const textHeight = Math . max ( lines . length * lineHeight , lineHeight ) ;
6785
6886 // Update canvas size to fit text and ensure it is at least 1x1 pixel
6987 const width = Math . max ( Math . round ( textWidth * dpr ) , 1 ) ;
@@ -72,9 +90,7 @@ function vtkTextActor(publicAPI, model) {
7290 canvas . width = width ;
7391 canvas . height = height ;
7492
75- // Vertical flip
76- ctx . translate ( 0 , height ) ;
77- ctx . scale ( 1 , - 1 ) ;
93+ ctx . setTransform ( 1 , 0 , 0 , - 1 , 0 , height ) ;
7894
7995 // Clear the canvas
8096 ctx . clearRect ( 0 , 0 , width , height ) ;
@@ -89,8 +105,8 @@ function vtkTextActor(publicAPI, model) {
89105 ctx . imageSmoothingQuality = 'high' ;
90106 ctx . font = `${ fontStyle } ${ textSize } px "${ fontFamily } "` ;
91107 ctx . fillStyle = vtkMath . floatRGB2HexCode ( fontColor ) ;
92- ctx . textBaseline = 'middle ' ;
93- ctx . textAlign = 'center ' ;
108+ ctx . textBaseline = 'alphabetic ' ;
109+ ctx . textAlign = 'left ' ;
94110
95111 // Set shadow
96112 if ( shadowColor ) {
@@ -100,8 +116,12 @@ function vtkTextActor(publicAPI, model) {
100116 ctx . shadowBlur = shadowBlur ;
101117 }
102118
103- // Draw the text
104- ctx . fillText ( text , width / 2 + hAdjustment , height / 2 + vAdjustment ) ;
119+ const x = maxLeft * dpr ;
120+ const baseline = fallbackAscent * dpr ;
121+ const lineHeightPx = lineHeight * dpr ;
122+ lines . forEach ( ( line , index ) => {
123+ ctx . fillText ( line , x , baseline + index * lineHeightPx ) ;
124+ } ) ;
105125
106126 // Update plane dimensions to match text size
107127 plane . set ( {
0 commit comments