Skip to content

Commit 9ef8ca3

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

3 files changed

Lines changed: 57 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: 53 additions & 26 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,45 @@ function vtkTextActor(publicAPI, model) {
4350
const ctx = canvas.getContext('2d', { willReadFrequently: true });
4451

4552
// Set the text properties to measure
46-
const textSize = fontSizeScale(resolution) * dpr;
53+
const normalizedText = normalizeLineBreaks(text);
54+
const textSize = fontSizeScale(resolution);
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;
65-
66-
const textHeight = textSize / dpr - vAdjustment;
58+
ctx.textBaseline = 'alphabetic';
59+
ctx.textAlign = 'left';
60+
61+
const lineMetrics = lines.map((line) => {
62+
const metrics = ctx.measureText(line);
63+
return {
64+
left: metrics.actualBoundingBoxLeft || 0,
65+
right: metrics.actualBoundingBoxRight || metrics.width || 0,
66+
actualAscent: metrics.actualBoundingBoxAscent || 0,
67+
actualDescent: metrics.actualBoundingBoxDescent || 0,
68+
};
69+
});
70+
const maxActualAscent = lineMetrics.reduce(
71+
(value, metrics) => Math.max(value, metrics.actualAscent),
72+
0
73+
);
74+
const maxActualDescent = lineMetrics.reduce(
75+
(value, metrics) => Math.max(value, metrics.actualDescent),
76+
0
77+
);
78+
const maxLeft = lineMetrics.reduce(
79+
(value, metrics) => Math.max(value, metrics.left),
80+
0
81+
);
82+
const maxRight = lineMetrics.reduce(
83+
(value, metrics) => Math.max(value, metrics.right),
84+
0
85+
);
86+
const lineAscent = maxActualAscent;
87+
const lineDescent = maxActualDescent;
88+
const baselineOffset = lineAscent;
89+
const lineHeight = Math.max(lineAscent + lineDescent, 1);
90+
const textWidth = maxLeft + maxRight;
91+
const textHeight = Math.max(lines.length * lineHeight, lineHeight);
6792

6893
// Update canvas size to fit text and ensure it is at least 1x1 pixel
6994
const width = Math.max(Math.round(textWidth * dpr), 1);
@@ -72,9 +97,7 @@ function vtkTextActor(publicAPI, model) {
7297
canvas.width = width;
7398
canvas.height = height;
7499

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

79102
// Clear the canvas
80103
ctx.clearRect(0, 0, width, height);
@@ -89,8 +112,8 @@ function vtkTextActor(publicAPI, model) {
89112
ctx.imageSmoothingQuality = 'high';
90113
ctx.font = `${fontStyle} ${textSize}px "${fontFamily}"`;
91114
ctx.fillStyle = vtkMath.floatRGB2HexCode(fontColor);
92-
ctx.textBaseline = 'middle';
93-
ctx.textAlign = 'center';
115+
ctx.textBaseline = 'alphabetic';
116+
ctx.textAlign = 'left';
94117

95118
// Set shadow
96119
if (shadowColor) {
@@ -100,8 +123,12 @@ function vtkTextActor(publicAPI, model) {
100123
ctx.shadowBlur = shadowBlur;
101124
}
102125

103-
// Draw the text
104-
ctx.fillText(text, width / 2 + hAdjustment, height / 2 + vAdjustment);
126+
const x = maxLeft;
127+
const baseline = baselineOffset;
128+
const lineHeightPx = lineHeight;
129+
lines.forEach((line, index) => {
130+
ctx.fillText(line, x, baseline + index * lineHeightPx);
131+
});
105132

106133
// Update plane dimensions to match text size
107134
plane.set({

0 commit comments

Comments
 (0)