Skip to content

Commit f05ebf6

Browse files
committed
fix(TextActor): add support for multi line text
fixes : #3484
1 parent fa0407f commit f05ebf6

2 files changed

Lines changed: 45 additions & 25 deletions

File tree

Sources/Rendering/Core/TextActor/example/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ renderWindow.render();
3434

3535
const gui = new GUI();
3636
const params = {
37-
text: 'Hello World!',
37+
text: 'Hello World\nfrom vtk.js',
3838
x: window.innerWidth / 4,
3939
y: window.innerHeight / 4,
4040
color: [0, 0, 0],

Sources/Rendering/Core/TextActor/index.js

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)