Skip to content

Commit 40c949f

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

3 files changed

Lines changed: 62 additions & 28 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const renderWindow = fullScreenRenderer.getRenderWindow();
2121
// ----------------------------------------------------------------------------
2222

2323
const actor = vtkTextActor.newInstance();
24-
actor.setInput('Hello World!');
24+
actor.setInput('Hello World!\nfrom vtk.js');
2525
actor.setDisplayPosition(window.innerWidth / 4, window.innerHeight / 4);
2626

2727
renderer.addActor2D(actor);
@@ -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.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export function newInstance(
5656

5757
/**
5858
* vtkTextActor can be used to place text annotation into a window.
59+
* Windows line endings (`\r\n`), Unix line endings (`\n`), and classic Mac
60+
* line endings (`\r`) are all treated as multiline breaks.
5961
*/
6062
export declare const vtkTextActor: {
6163
newInstance: typeof newInstance;

Sources/Rendering/Core/TextActor/index.js

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ function vtkTextActor(publicAPI, model) {
2828
yResolution: 1,
2929
});
3030

31+
/**
32+
* Normalize escaped and platform specific line endings to `\n`.
33+
*/
34+
function normalizeLineBreaks(text) {
35+
return text
36+
.replace(/\\r\\n/g, '\n')
37+
.replace(/\\n/g, '\n')
38+
.replace(/\\r/g, '\n')
39+
.replace(/\r\n/g, '\n')
40+
.replace(/\r/g, '\n');
41+
}
42+
3143
function createImageData(text) {
3244
const fontSizeScale = publicAPI.getProperty().getFontSizeScale();
3345
const fontStyle = publicAPI.getProperty().getFontStyle();
@@ -43,27 +55,45 @@ function vtkTextActor(publicAPI, model) {
4355
const ctx = canvas.getContext('2d', { willReadFrequently: true });
4456

4557
// Set the text properties to measure
46-
const textSize = fontSizeScale(resolution) * dpr;
58+
const normalizedText = normalizeLineBreaks(text);
59+
const textSize = fontSizeScale(resolution);
60+
const lines = normalizedText.split(/\r\n|\r|\n/);
4761

4862
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;
65-
66-
const textHeight = textSize / dpr - vAdjustment;
63+
ctx.textBaseline = 'alphabetic';
64+
ctx.textAlign = 'left';
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+
actualAscent: metrics.actualBoundingBoxAscent || 0,
72+
actualDescent: metrics.actualBoundingBoxDescent || 0,
73+
};
74+
});
75+
const maxActualAscent = lineMetrics.reduce(
76+
(value, metrics) => Math.max(value, metrics.actualAscent),
77+
0
78+
);
79+
const maxActualDescent = lineMetrics.reduce(
80+
(value, metrics) => Math.max(value, metrics.actualDescent),
81+
0
82+
);
83+
const maxLeft = lineMetrics.reduce(
84+
(value, metrics) => Math.max(value, metrics.left),
85+
0
86+
);
87+
const maxRight = lineMetrics.reduce(
88+
(value, metrics) => Math.max(value, metrics.right),
89+
0
90+
);
91+
const lineAscent = maxActualAscent;
92+
const lineDescent = maxActualDescent;
93+
const baselineOffset = lineAscent;
94+
const lineHeight = Math.max(lineAscent + lineDescent, 1);
95+
const textWidth = maxLeft + maxRight;
96+
const textHeight = Math.max(lines.length * lineHeight, lineHeight);
6797

6898
// Update canvas size to fit text and ensure it is at least 1x1 pixel
6999
const width = Math.max(Math.round(textWidth * dpr), 1);
@@ -72,9 +102,7 @@ function vtkTextActor(publicAPI, model) {
72102
canvas.width = width;
73103
canvas.height = height;
74104

75-
// Vertical flip
76-
ctx.translate(0, height);
77-
ctx.scale(1, -1);
105+
ctx.setTransform(dpr, 0, 0, -dpr, 0, height);
78106

79107
// Clear the canvas
80108
ctx.clearRect(0, 0, width, height);
@@ -89,8 +117,8 @@ function vtkTextActor(publicAPI, model) {
89117
ctx.imageSmoothingQuality = 'high';
90118
ctx.font = `${fontStyle} ${textSize}px "${fontFamily}"`;
91119
ctx.fillStyle = vtkMath.floatRGB2HexCode(fontColor);
92-
ctx.textBaseline = 'middle';
93-
ctx.textAlign = 'center';
120+
ctx.textBaseline = 'alphabetic';
121+
ctx.textAlign = 'left';
94122

95123
// Set shadow
96124
if (shadowColor) {
@@ -100,8 +128,12 @@ function vtkTextActor(publicAPI, model) {
100128
ctx.shadowBlur = shadowBlur;
101129
}
102130

103-
// Draw the text
104-
ctx.fillText(text, width / 2 + hAdjustment, height / 2 + vAdjustment);
131+
const x = maxLeft;
132+
const baseline = baselineOffset;
133+
const lineHeightPx = lineHeight;
134+
lines.forEach((line, index) => {
135+
ctx.fillText(line, x, baseline + index * lineHeightPx);
136+
});
105137

106138
// Update plane dimensions to match text size
107139
plane.set({

0 commit comments

Comments
 (0)